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