diff --git a/src/AngleSharp.Css.Tests/Declarations/CssFontDescriptorProperty.cs b/src/AngleSharp.Css.Tests/Declarations/CssFontDescriptorProperty.cs new file mode 100644 index 0000000..7f94591 --- /dev/null +++ b/src/AngleSharp.Css.Tests/Declarations/CssFontDescriptorProperty.cs @@ -0,0 +1,91 @@ +namespace AngleSharp.Css.Tests.Declarations +{ + using NUnit.Framework; + using static CssConstructionFunctions; + + [TestFixture] + public class CssFontDescriptorPropertyTests + { + [TestCase("size-adjust: 100%", "100%")] + [TestCase("size-adjust: 90.5%", "90.5%")] + [TestCase("size-adjust: 0%", "0%")] + public void SizeAdjustLegalValues(string snippet, string expected) + { + var property = ParseDeclaration(snippet); + Assert.AreEqual("size-adjust", property.Name); + Assert.IsTrue(property.HasValue); + Assert.AreEqual(expected, property.Value); + } + + [TestCase("size-adjust: 10px")] + [TestCase("size-adjust: 100")] + [TestCase("size-adjust: auto")] + public void SizeAdjustIllegalValues(string snippet) + { + var property = ParseDeclaration(snippet); + Assert.IsFalse(property.HasValue); + } + + [TestCase("ascent-override: 90%", "90%")] + [TestCase("ascent-override: normal", "normal")] + [TestCase("descent-override: 20%", "20%")] + [TestCase("line-gap-override: 0%", "0%")] + public void MetricOverrideLegalValues(string snippet, string expected) + { + var property = ParseDeclaration(snippet); + Assert.IsTrue(property.HasValue); + Assert.AreEqual(expected, property.Value); + } + + [TestCase("ascent-override: 10px")] + [TestCase("descent-override: auto")] + [TestCase("line-gap-override: none")] + public void MetricOverrideIllegalValues(string snippet) + { + var property = ParseDeclaration(snippet); + Assert.IsFalse(property.HasValue); + } + + [TestCase("font-feature-settings: normal", "normal")] + [TestCase("font-feature-settings: \"liga\"", "\"liga\"")] + [TestCase("font-feature-settings: \"liga\" 1", "\"liga\" 1")] + [TestCase("font-feature-settings: \"kern\" on", "\"kern\" on")] + [TestCase("font-feature-settings: \"kern\" off", "\"kern\" off")] + [TestCase("font-feature-settings: \"liga\" 1, \"kern\" off", "\"liga\" 1, \"kern\" off")] + public void FontFeatureSettingsLegalValues(string snippet, string expected) + { + var property = ParseDeclaration(snippet); + Assert.AreEqual("font-feature-settings", property.Name); + Assert.IsTrue(property.HasValue); + Assert.AreEqual(expected, property.Value); + } + + [TestCase("font-feature-settings: 12")] + [TestCase("font-feature-settings: liga")] + [TestCase("font-feature-settings: \"liga\" bogus")] + public void FontFeatureSettingsIllegalValues(string snippet) + { + var property = ParseDeclaration(snippet); + Assert.IsFalse(property.HasValue); + } + + [TestCase("font-variation-settings: normal", "normal")] + [TestCase("font-variation-settings: \"wght\" 400", "\"wght\" 400")] + [TestCase("font-variation-settings: \"wght\" 400, \"slnt\" -10", "\"wght\" 400, \"slnt\" -10")] + public void FontVariationSettingsLegalValues(string snippet, string expected) + { + var property = ParseDeclaration(snippet); + Assert.AreEqual("font-variation-settings", property.Name); + Assert.IsTrue(property.HasValue); + Assert.AreEqual(expected, property.Value); + } + + [TestCase("font-variation-settings: wght 400")] + [TestCase("font-variation-settings: 400")] + public void FontVariationSettingsIllegalValues(string snippet) + { + var property = ParseDeclaration(snippet); + Assert.IsFalse(property.HasValue); + } + } +} diff --git a/src/AngleSharp.Css.Tests/Rules/FontFaceDescriptors.cs b/src/AngleSharp.Css.Tests/Rules/FontFaceDescriptors.cs new file mode 100644 index 0000000..f324c1e --- /dev/null +++ b/src/AngleSharp.Css.Tests/Rules/FontFaceDescriptors.cs @@ -0,0 +1,162 @@ +namespace AngleSharp.Css.Tests.Rules +{ + using AngleSharp.Css.Dom; + using AngleSharp.Css.Parser; + using NUnit.Framework; + using System.Linq; + using static CssConstructionFunctions; + + /// + /// Descriptors inside @font-face used to be limited to a hardcoded set of seven, + /// with everything else dropped silently and without regard to the parser options. + /// These cases pin down that the standard CSS Fonts Level 4 descriptors survive by + /// default and that anything else survives when unknown declarations are included. + /// + [TestFixture] + public class FontFaceDescriptorTests + { + private static readonly CssParserOptions IncludingUnknown = new() { IsIncludingUnknownDeclarations = true }; + + [Test] + public void FontFaceKeepsFontDisplay() + { + var sheet = ParseStyleSheet("@font-face { font-display: swap }"); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; + Assert.AreEqual("swap", fontface.GetPropertyValue(PropertyNames.FontDisplay)); + } + + [Test] + public void FontFaceKeepsSizeAdjust() + { + var sheet = ParseStyleSheet("@font-face { size-adjust: 90% }"); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; + Assert.AreEqual("90%", fontface.GetPropertyValue(PropertyNames.SizeAdjust)); + } + + [TestCase("ascent-override", "90%")] + [TestCase("descent-override", "20%")] + [TestCase("line-gap-override", "0%")] + [TestCase("ascent-override", "normal")] + public void FontFaceKeepsMetricOverrides(string name, string value) + { + var sheet = ParseStyleSheet($"@font-face {{ {name}: {value} }}"); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; + Assert.AreEqual(value, fontface.GetPropertyValue(name)); + } + + [Test] + public void FontFaceKeepsFontFeatureSettings() + { + var sheet = ParseStyleSheet("@font-face { font-feature-settings: \"liga\" 1, \"kern\" on, \"smcp\" }"); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; + Assert.AreEqual("\"liga\" 1, \"kern\" on, \"smcp\"", fontface.GetPropertyValue(PropertyNames.FontFeatureSettings)); + } + + [Test] + public void FontFaceKeepsFontVariationSettings() + { + var sheet = ParseStyleSheet("@font-face { font-variation-settings: \"wght\" 400, \"slnt\" -10 }"); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; + Assert.AreEqual("\"wght\" 400, \"slnt\" -10", fontface.GetPropertyValue(PropertyNames.FontVariationSettings)); + } + + [Test] + public void FontFaceFeaturesMapToFontFeatureSettings() + { + var sheet = ParseStyleSheet("@font-face { font-feature-settings: \"liga\" 1 }"); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; + Assert.AreEqual("\"liga\" 1", fontface.Features); + + fontface.Features = "\"kern\" off"; + Assert.AreEqual("\"kern\" off", fontface.GetPropertyValue(PropertyNames.FontFeatureSettings)); + } + + [Test] + public void FontFaceStandardDescriptorsRoundtripViaToCss() + { + var src = "@font-face { font-family: \"FontName\"; src: url(\"https://example.com/font.woff\") format(\"woff\"); font-display: swap; size-adjust: 100% }"; + var sheet = ParseStyleSheet(src); + var css = sheet.ToCss(); + Assert.That(css, Does.Contain("font-display: swap")); + Assert.That(css, Does.Contain("size-adjust: 100%")); + } + + [Test] + public void FontFaceDropsVendorDescriptorByDefault() + { + var sheet = ParseStyleSheet("@font-face { mso-generic-font-family: auto }"); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; + Assert.AreEqual(0, fontface.Length); + } + + [Test] + public void FontFaceKeepsVendorDescriptorWhenIncludingUnknownDeclarations() + { + var sheet = ParseStyleSheet("@font-face { mso-generic-font-family: auto }", IncludingUnknown); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; + Assert.AreEqual("auto", fontface.GetPropertyValue("mso-generic-font-family")); + Assert.That(sheet.ToCss(), Does.Contain("mso-generic-font-family: auto")); + } + + [Test] + public void FontFaceKeepsCustomPropertyWhenIncludingUnknownDeclarations() + { + var sheet = ParseStyleSheet("@font-face { --custom-thing: 12px }", IncludingUnknown); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; + Assert.AreEqual("12px", fontface.GetPropertyValue("--custom-thing")); + } + + [Test] + public void FontFaceIgnoresInvalidDescriptorValue() + { + var sheet = ParseStyleSheet("@font-face { size-adjust: 10px; ascent-override: bogus }"); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; + Assert.AreEqual(0, fontface.Length); + Assert.That(sheet.ToCss(), Does.Not.Contain("size-adjust")); + } + + [Test] + public void FontFaceInvalidValueDoesNotOverwriteValidOne() + { + var sheet = ParseStyleSheet("@font-face { font-weight: 400; font-weight: bogus }"); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; + Assert.AreEqual("400", fontface.Weight); + } + + [Test] + public void FontFaceValidValueOverwritesEarlierOne() + { + var sheet = ParseStyleSheet("@font-face { font-weight: 400; font-weight: 700 }"); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; + Assert.AreEqual("700", fontface.Weight); + Assert.AreEqual(1, fontface.Length); + } + + [Test] + public void CounterStyleKeepsDescriptorsWhenIncludingUnknownDeclarations() + { + var sheet = ParseStyleSheet("@counter-style thumbs { system: cyclic; symbols: \"X\" }", IncludingUnknown); + var rule = (ICssProperties)sheet.Rules[0]; + Assert.AreEqual("cyclic", rule.GetPropertyValue("system")); + Assert.AreEqual("\"X\"", rule.GetPropertyValue("symbols")); + } + + [Test] + public void ViewportKeepsUnknownDescriptorWhenIncludingUnknownDeclarations() + { + var sheet = ParseStyleSheet("@viewport { width: 100px; foo: bar }", IncludingUnknown); + var rule = (ICssProperties)sheet.Rules[0]; + Assert.AreEqual("bar", rule.GetPropertyValue("foo")); + Assert.AreEqual(2, rule.Length); + } + + [Test] + public void FontFaceDescriptorsAreEnumerable() + { + var sheet = ParseStyleSheet("@font-face { font-family: \"X\"; font-display: swap; size-adjust: 50% }"); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; + var names = fontface.Select(m => m.Name).ToArray(); + CollectionAssert.AreEquivalent(new[] { "font-family", "font-display", "size-adjust" }, names); + } + } +} diff --git a/src/AngleSharp.Css/BrowsingContextExtensions.cs b/src/AngleSharp.Css/BrowsingContextExtensions.cs index 9b6249c..5cecbb1 100644 --- a/src/AngleSharp.Css/BrowsingContextExtensions.cs +++ b/src/AngleSharp.Css/BrowsingContextExtensions.cs @@ -95,7 +95,7 @@ internal static CssProperty CreateProperty(this IBrowsingContext context, String private static Boolean AllowsDeclaration(this IBrowsingContext context, DeclarationInfo info) => info.Flags != PropertyFlags.Unknown || context.IsAllowingUnknownDeclarations(); - private static Boolean IsAllowingUnknownDeclarations(this IBrowsingContext context) + internal static Boolean IsAllowingUnknownDeclarations(this IBrowsingContext context) { var parser = context.GetProvider(); return parser?.Options.IsIncludingUnknownDeclarations ?? true; diff --git a/src/AngleSharp.Css/Constants/CssKeywords.cs b/src/AngleSharp.Css/Constants/CssKeywords.cs index bacdb6a..4d36e61 100644 --- a/src/AngleSharp.Css/Constants/CssKeywords.cs +++ b/src/AngleSharp.Css/Constants/CssKeywords.cs @@ -307,6 +307,16 @@ public static class CssKeywords /// public static readonly String Optional = "optional"; + /// + /// The on keyword (font-feature-settings). + /// + public static readonly String On = "on"; + + /// + /// The off keyword (font-feature-settings). + /// + public static readonly String Off = "off"; + /// /// The avoid keyword. /// diff --git a/src/AngleSharp.Css/Constants/InitialValues.cs b/src/AngleSharp.Css/Constants/InitialValues.cs index 166fb56..eb80859 100644 --- a/src/AngleSharp.Css/Constants/InitialValues.cs +++ b/src/AngleSharp.Css/Constants/InitialValues.cs @@ -47,6 +47,11 @@ static class InitialValues public static readonly ICssValue FontSynthesisStyleDecl = new CssConstantValue(CssKeywords.Auto, null); public static readonly ICssValue FontSynthesisSmallCapsDecl = new CssConstantValue(CssKeywords.Auto, null); public static readonly ICssValue FontVariationSettingsDecl = new CssConstantValue(CssKeywords.Normal, null); + public static readonly ICssValue FontFeatureSettingsDecl = new CssConstantValue(CssKeywords.Normal, null); + public static readonly ICssValue SizeAdjustDecl = new CssPercentageValue(100.0); + public static readonly ICssValue AscentOverrideDecl = new CssConstantValue(CssKeywords.Normal, null); + public static readonly ICssValue DescentOverrideDecl = new CssConstantValue(CssKeywords.Normal, null); + public static readonly ICssValue LineGapOverrideDecl = new CssConstantValue(CssKeywords.Normal, null); public static readonly ICssValue AccentColorDecl = new CssConstantValue(CssKeywords.Auto, null); public static readonly ICssValue AppearanceDecl = new CssIdentifierValue(CssKeywords.Auto); public static readonly ICssValue CaretColorDecl = new CssConstantValue(CssKeywords.Auto, null); diff --git a/src/AngleSharp.Css/Constants/PropertyNames.cs b/src/AngleSharp.Css/Constants/PropertyNames.cs index 822509b..340bcf2 100644 --- a/src/AngleSharp.Css/Constants/PropertyNames.cs +++ b/src/AngleSharp.Css/Constants/PropertyNames.cs @@ -907,6 +907,26 @@ public static class PropertyNames /// public static readonly String FontKerning = "font-kerning"; + /// + /// The size-adjust declaration (@font-face descriptor). + /// + public static readonly String SizeAdjust = "size-adjust"; + + /// + /// The ascent-override declaration (@font-face descriptor). + /// + public static readonly String AscentOverride = "ascent-override"; + + /// + /// The descent-override declaration (@font-face descriptor). + /// + public static readonly String DescentOverride = "descent-override"; + + /// + /// The line-gap-override declaration (@font-face descriptor). + /// + public static readonly String LineGapOverride = "line-gap-override"; + /// /// The font-language-override declaration. /// diff --git a/src/AngleSharp.Css/Declarations/AscentOverrideDeclaration.cs b/src/AngleSharp.Css/Declarations/AscentOverrideDeclaration.cs new file mode 100644 index 0000000..b961227 --- /dev/null +++ b/src/AngleSharp.Css/Declarations/AscentOverrideDeclaration.cs @@ -0,0 +1,17 @@ +namespace AngleSharp.Css.Declarations +{ + using AngleSharp.Css.Dom; + using System; + using static ValueConverters; + + static class AscentOverrideDeclaration + { + public static String Name = PropertyNames.AscentOverride; + + public static IValueConverter Converter = FontMetricOverrideConverter; + + public static ICssValue InitialValue = InitialValues.AscentOverrideDecl; + + public static PropertyFlags Flags = PropertyFlags.None; + } +} diff --git a/src/AngleSharp.Css/Declarations/DescentOverrideDeclaration.cs b/src/AngleSharp.Css/Declarations/DescentOverrideDeclaration.cs new file mode 100644 index 0000000..b583028 --- /dev/null +++ b/src/AngleSharp.Css/Declarations/DescentOverrideDeclaration.cs @@ -0,0 +1,17 @@ +namespace AngleSharp.Css.Declarations +{ + using AngleSharp.Css.Dom; + using System; + using static ValueConverters; + + static class DescentOverrideDeclaration + { + public static String Name = PropertyNames.DescentOverride; + + public static IValueConverter Converter = FontMetricOverrideConverter; + + public static ICssValue InitialValue = InitialValues.DescentOverrideDecl; + + public static PropertyFlags Flags = PropertyFlags.None; + } +} diff --git a/src/AngleSharp.Css/Declarations/FontFeatureSettingsDeclaration.cs b/src/AngleSharp.Css/Declarations/FontFeatureSettingsDeclaration.cs new file mode 100644 index 0000000..45cb76e --- /dev/null +++ b/src/AngleSharp.Css/Declarations/FontFeatureSettingsDeclaration.cs @@ -0,0 +1,17 @@ +namespace AngleSharp.Css.Declarations +{ + using AngleSharp.Css.Dom; + using System; + using static ValueConverters; + + static class FontFeatureSettingsDeclaration + { + public static String Name = PropertyNames.FontFeatureSettings; + + public static IValueConverter Converter = FontFeatureSettingsConverter; + + public static ICssValue InitialValue = InitialValues.FontFeatureSettingsDecl; + + public static PropertyFlags Flags = PropertyFlags.Inherited; + } +} diff --git a/src/AngleSharp.Css/Declarations/LineGapOverrideDeclaration.cs b/src/AngleSharp.Css/Declarations/LineGapOverrideDeclaration.cs new file mode 100644 index 0000000..2e956e8 --- /dev/null +++ b/src/AngleSharp.Css/Declarations/LineGapOverrideDeclaration.cs @@ -0,0 +1,17 @@ +namespace AngleSharp.Css.Declarations +{ + using AngleSharp.Css.Dom; + using System; + using static ValueConverters; + + static class LineGapOverrideDeclaration + { + public static String Name = PropertyNames.LineGapOverride; + + public static IValueConverter Converter = FontMetricOverrideConverter; + + public static ICssValue InitialValue = InitialValues.LineGapOverrideDecl; + + public static PropertyFlags Flags = PropertyFlags.None; + } +} diff --git a/src/AngleSharp.Css/Declarations/SizeAdjustDeclaration.cs b/src/AngleSharp.Css/Declarations/SizeAdjustDeclaration.cs new file mode 100644 index 0000000..2e652ef --- /dev/null +++ b/src/AngleSharp.Css/Declarations/SizeAdjustDeclaration.cs @@ -0,0 +1,17 @@ +namespace AngleSharp.Css.Declarations +{ + using AngleSharp.Css.Dom; + using System; + using static ValueConverters; + + static class SizeAdjustDeclaration + { + public static String Name = PropertyNames.SizeAdjust; + + public static IValueConverter Converter = SizeAdjustConverter; + + public static ICssValue InitialValue = InitialValues.SizeAdjustDecl; + + public static PropertyFlags Flags = PropertyFlags.None; + } +} diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs index 53d1a40..9d25201 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs @@ -85,7 +85,11 @@ public override void ToCss(TextWriter writer, IStyleFormatter formatter) private ICssProperty CreateNewProperty(String propertyName) { - if (_contained.Contains(propertyName)) + // Descriptors of the rule itself are always created. Anything else is + // kept only when unknown declarations are included - the same switch + // that preserves them in ordinary style rules. Without it the + // declaration would be dropped silently. + if (_contained.Contains(propertyName) || Owner.Context.IsAllowingUnknownDeclarations()) { return Owner.Context.CreateProperty(propertyName); } @@ -110,22 +114,32 @@ protected void SetValue(String propertyName, String valueText) { if (!String.IsNullOrEmpty(valueText)) { - foreach (var declaration in _declarations) + var property = CreateNewProperty(propertyName); + + if (property is null) { - if (declaration.Name.Is(propertyName)) - { - declaration.Value = valueText; - return; - } + return; } - var property = CreateNewProperty(propertyName); + property.Value = valueText; - if (property != null) + if (property.RawValue is null) { - property.Value = valueText; - _declarations.Add(property); + // The value is not valid for this declaration; ignore it instead + // of storing an empty declaration that would serialize as "name: ". + return; } + + for (var i = 0; i < _declarations.Count; i++) + { + if (_declarations[i].Name.Is(propertyName)) + { + _declarations[i] = property; + return; + } + } + + _declarations.Add(property); } else { diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssFontFaceRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssFontFaceRule.cs index 809644f..d5eef94 100644 --- a/src/AngleSharp.Css/Dom/Internal/Rules/CssFontFaceRule.cs +++ b/src/AngleSharp.Css/Dom/Internal/Rules/CssFontFaceRule.cs @@ -22,6 +22,13 @@ sealed class CssFontFaceRule : CssDeclarationRule, ICssFontFaceRule PropertyNames.FontStretch, PropertyNames.UnicodeRange, PropertyNames.FontVariant, + PropertyNames.FontDisplay, + PropertyNames.FontFeatureSettings, + PropertyNames.FontVariationSettings, + PropertyNames.SizeAdjust, + PropertyNames.AscentOverride, + PropertyNames.DescentOverride, + PropertyNames.LineGapOverride, }; #endregion @@ -83,8 +90,8 @@ String ICssFontFaceRule.Variant String ICssFontFaceRule.Features { - get => String.Empty; - set { } + get => GetValue(PropertyNames.FontFeatureSettings); + set => SetValue(PropertyNames.FontFeatureSettings, value); } #endregion @@ -101,6 +108,7 @@ protected override void ReplaceWith(ICssRule rule) SetValue(PropertyNames.FontStretch, newRule.Stretch); SetValue(PropertyNames.UnicodeRange, newRule.Range); SetValue(PropertyNames.FontVariant, newRule.Variant); + SetValue(PropertyNames.FontFeatureSettings, newRule.Features); } #endregion diff --git a/src/AngleSharp.Css/Factories/DefaultDeclarationFactory.cs b/src/AngleSharp.Css/Factories/DefaultDeclarationFactory.cs index abde660..7a168ad 100644 --- a/src/AngleSharp.Css/Factories/DefaultDeclarationFactory.cs +++ b/src/AngleSharp.Css/Factories/DefaultDeclarationFactory.cs @@ -974,6 +974,41 @@ public class DefaultDeclarationFactory : IDeclarationFactory initialValue: FontDisplayDeclaration.InitialValue, flags: FontDisplayDeclaration.Flags) }, + { + FontFeatureSettingsDeclaration.Name, new DeclarationInfo( + name: FontFeatureSettingsDeclaration.Name, + converter: FontFeatureSettingsDeclaration.Converter, + initialValue: FontFeatureSettingsDeclaration.InitialValue, + flags: FontFeatureSettingsDeclaration.Flags) + }, + { + SizeAdjustDeclaration.Name, new DeclarationInfo( + name: SizeAdjustDeclaration.Name, + converter: SizeAdjustDeclaration.Converter, + initialValue: SizeAdjustDeclaration.InitialValue, + flags: SizeAdjustDeclaration.Flags) + }, + { + AscentOverrideDeclaration.Name, new DeclarationInfo( + name: AscentOverrideDeclaration.Name, + converter: AscentOverrideDeclaration.Converter, + initialValue: AscentOverrideDeclaration.InitialValue, + flags: AscentOverrideDeclaration.Flags) + }, + { + DescentOverrideDeclaration.Name, new DeclarationInfo( + name: DescentOverrideDeclaration.Name, + converter: DescentOverrideDeclaration.Converter, + initialValue: DescentOverrideDeclaration.InitialValue, + flags: DescentOverrideDeclaration.Flags) + }, + { + LineGapOverrideDeclaration.Name, new DeclarationInfo( + name: LineGapOverrideDeclaration.Name, + converter: LineGapOverrideDeclaration.Converter, + initialValue: LineGapOverrideDeclaration.InitialValue, + flags: LineGapOverrideDeclaration.Flags) + }, { FontKerningDeclaration.Name, new DeclarationInfo( name: FontKerningDeclaration.Name, diff --git a/src/AngleSharp.Css/ValueConverters.cs b/src/AngleSharp.Css/ValueConverters.cs index f7fe012..8fb2a46 100644 --- a/src/AngleSharp.Css/ValueConverters.cs +++ b/src/AngleSharp.Css/ValueConverters.cs @@ -120,6 +120,11 @@ static class ValueConverters /// public static readonly IValueConverter OnlyLengthOrPercentConverter = new StructValueConverter(UnitParser.ParseDistance); + /// + /// Represents a percentage object, i.e. a number followed by a percent sign. + /// + public static readonly IValueConverter OnlyPercentConverter = new StructValueConverter(ParseOnlyPercent); + /// /// Represents a string object. /// @@ -216,6 +221,11 @@ static class ValueConverters /// public static readonly IValueConverter LengthOrPercentConverter = Or(OnlyLengthOrPercentConverter, CalcConverter); + /// + /// Represents a (calculated) percentage object. + /// + public static readonly IValueConverter PercentConverter = Or(OnlyPercentConverter, CalcConverter); + /// /// Represents an number object that is zero or greater. /// @@ -1088,6 +1098,32 @@ static class ValueConverters Assign(CssKeywords.Fallback, CssKeywords.Fallback), Assign(CssKeywords.Optional, CssKeywords.Optional)); + /// + /// Represents a converter for the size-adjust descriptor. + /// + public static readonly IValueConverter SizeAdjustConverter = PercentConverter; + + /// + /// Represents a converter for the ascent-override, descent-override and + /// line-gap-override descriptors. + /// + public static readonly IValueConverter FontMetricOverrideConverter = Or( + Assign(CssKeywords.Normal, CssKeywords.Normal), + PercentConverter); + + /// + /// Represents a converter for the font-feature-settings property. + /// + public static readonly IValueConverter FontFeatureSettingsConverter = Or( + Assign(CssKeywords.Normal, CssKeywords.Normal), + WithOrder( + StringConverter, + Or( + NaturalIntegerConverter, + Assign(CssKeywords.On, CssKeywords.On), + Assign(CssKeywords.Off, CssKeywords.Off))) + .FromList()); + /// /// Represents a converter for the font-kerning property. /// @@ -1152,7 +1188,12 @@ static class ValueConverters /// /// Represents a converter for the font-variation-settings property. /// - public static readonly IValueConverter FontVariationSettingsConverter = Assign(CssKeywords.Normal, CssKeywords.Normal); + public static readonly IValueConverter FontVariationSettingsConverter = Or( + Assign(CssKeywords.Normal, CssKeywords.Normal), + WithOrder( + StringConverter, + NumberConverter) + .FromList()); /// /// Represents a converter for the ResizeMode enumeration. @@ -1703,6 +1744,20 @@ private static IValueConverter FromParser(Func converter) private static IValueConverter FromParser(Func converter) where T : class, ICssValue => new ClassValueConverter(converter); + private static CssPercentageValue? ParseOnlyPercent(this StringSource source) + { + var pos = source.Index; + var result = source.ParsePercentOrNumber(); + + if (result?.Type == CssPercentageValue.Unit.Percent) + { + return result; + } + + source.BackTo(pos); + return null; + } + private static Func FromString(Func converter) => source => { var result = converter.Invoke(source);