diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 6a8c8a1d..a8483d33 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -2,8 +2,8 @@ "version": 1, "isRoot": true, "tools": { - "fallout.cli": { - "version": "11.0.18", + "fallout.globaltool": { + "version": "10.4.0", "commands": [ "fallout" ] diff --git a/CHANGELOG.md b/CHANGELOG.md index ed966e3e..465f5936 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.0.2 + +Released on Friday, August 21 2026. + +- Fixed `oklab`/`oklch` usage with plain number as first argument (#225) @scasteran-jw + # 1.0.1 Released on Friday, July 31 2026. diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f62e6cee..6c9b8b2d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -25,6 +25,7 @@ AngleSharp.Css contains code written by (in order of first pull request / commit * [Sebastian Stehle](https://github.com/SebastianStehle) * [MaceWindu](https://github.com/MaceWindu) * [Serhan Apaydın](https://github.com/monoblaine) +* [scasteran](https://github.com/scasteran-jw) Without these awesome people AngleSharp.Css could not exist. Thanks to everyone for your contributions! :beers: diff --git a/README.md b/README.md index 4ecccc76..dcc5e464 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ [![GitHub Tag](https://img.shields.io/github/tag/AngleSharp/AngleSharp.Css.svg?style=flat-square)](https://github.com/AngleSharp/AngleSharp.Css/releases) [![NuGet Count](https://img.shields.io/nuget/dt/AngleSharp.Css.svg?style=flat-square)](https://www.nuget.org/packages/AngleSharp.Css/) [![Issues Open](https://img.shields.io/github/issues/AngleSharp/AngleSharp.Css.svg?style=flat-square)](https://github.com/AngleSharp/AngleSharp.Css/issues) -[![Gitter Chat](http://img.shields.io/badge/gitter-AngleSharp/AngleSharp-blue.svg?style=flat-square)](https://gitter.im/AngleSharp/AngleSharp) -[![StackOverflow Questions](https://img.shields.io/stackexchange/stackoverflow/t/anglesharp.svg?style=flat-square)](https://stackoverflow.com/tags/anglesharp) [![CLA Assistant](https://cla-assistant.io/readme/badge/AngleSharp/AngleSharp.Css?style=flat-square)](https://cla-assistant.io/AngleSharp/AngleSharp.Css) AngleSharp.Css extends the core AngleSharp library with some more powerful CSS capabilities. This repository is the home of the source for the AngleSharp.Css NuGet package. diff --git a/build/_build.csproj b/build/_build.csproj index 647ac264..11c89b02 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -11,9 +11,7 @@ - - - + diff --git a/src/AngleSharp.Css.Docs/package.json b/src/AngleSharp.Css.Docs/package.json index 2ba7b5bf..df838e5a 100644 --- a/src/AngleSharp.Css.Docs/package.json +++ b/src/AngleSharp.Css.Docs/package.json @@ -1,6 +1,6 @@ { "name": "@anglesharp/css", - "version": "1.0.0", + "version": "1.0.2", "preview": true, "description": "The doclet for the AngleSharp.Css documentation.", "keywords": [ diff --git a/src/AngleSharp.Css.Tests/Declarations/CssFontDescriptorProperty.cs b/src/AngleSharp.Css.Tests/Declarations/CssFontDescriptorProperty.cs new file mode 100644 index 00000000..7f945917 --- /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/Declarations/SetPropertyOverwriteTests.cs b/src/AngleSharp.Css.Tests/Declarations/SetPropertyOverwriteTests.cs new file mode 100644 index 00000000..9846940e --- /dev/null +++ b/src/AngleSharp.Css.Tests/Declarations/SetPropertyOverwriteTests.cs @@ -0,0 +1,81 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Declarations +{ + using AngleSharp.Css.Dom; + using NUnit.Framework; + using System; + using System.Collections.Generic; + using System.Linq; + using static CssConstructionFunctions; + + /// + /// Setting a declaration builds a fresh property and assigns its value, which always + /// replaces the raw value it starts with. These cases pin down that re-declaring a + /// property - longhand over shorthand, shorthand over longhand, important over + /// normal, or an invalid value over a valid one - does not depend on the new property + /// being seeded from the old one first. + /// + /// The expectations are a captured baseline of the behaviour, not a statement about + /// what the spec requires. + /// + [TestFixture] + public class SetPropertyOverwriteTests + { + private static readonly (String Input, String Expected)[] Cases = + { + ("color:red;color:blue", "color: rgba(0, 0, 255, 1)"), + ("color:blue;color:red", "color: rgba(255, 0, 0, 1)"), + ("color:red;color:notacolor", "color: rgba(255, 0, 0, 1)"), + ("color:red !important;color:blue", "color: rgba(255, 0, 0, 1) !important"), + ("color:red;color:blue !important", "color: rgba(0, 0, 255, 1) !important"), + ("color:red !important;color:blue !important", "color: rgba(0, 0, 255, 1) !important"), + ("--x:1;--x:2", "--x: 2"), + ("--x:1;color:red", "--x: 1; color: rgba(255, 0, 0, 1)"), + ("margin:1px;margin-top:2px", "margin-bottom: 1px; margin-left: 1px; margin-right: 1px; margin-top: 2px"), + ("margin-top:2px;margin:1px", "margin-bottom: 1px; margin-left: 1px; margin-right: 1px; margin-top: 1px"), + ("margin:1px;margin:notalength", "margin-bottom: 1px; margin-left: 1px; margin-right: 1px; margin-top: 1px"), + ("margin:1px !important;margin-top:2px", "margin-bottom: 1px !important; margin-left: 1px !important; margin-right: 1px !important; margin-top: 1px !important"), + ("padding:1px 2px;padding-left:9px;padding:3px", "padding-bottom: 3px; padding-left: 3px; padding-right: 3px; padding-top: 3px"), + ("border-radius:4px;border-top-left-radius:9px", "border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; border-top-left-radius: 9px; border-top-right-radius: 4px"), + ("flex:1 1 auto;flex-grow:5", "flex-basis: auto; flex-grow: 5; flex-shrink: 1"), + ("overflow:hidden;overflow-x:scroll", "overflow-x: scroll; overflow: hidden"), + ("grid-area:a;grid-row:2", "grid-column-end: a; grid-column-start: a; grid-row-end: auto; grid-row-start: 2"), + ("grid-row:2;grid-area:a", "grid-column-end: a; grid-column-start: a; grid-row-end: a; grid-row-start: a"), + ("transition:all .3s;transition-duration:1s", "transition-delay: initial; transition-duration: 1s; transition-property: all; transition-timing-function: initial"), + ("font:bold 14px/1.2 Arial;font-size:20px", "font-family: Arial; font-size: 20px; font-stretch: ; font-style: ; font-variant: ; font-weight: bold; line-height: 1.2"), + ("font-size:20px;font:bold 14px/1.2 Arial", "font-family: Arial; font-size: 14px; font-stretch: ; font-style: ; font-variant: ; font-weight: bold; line-height: 1.2"), + ("border:1px solid red;border-width:3px", "border-bottom-color: rgba(255, 0, 0, 1); border-bottom-style: solid; border-bottom-width: 3px; border-left-color: rgba(255, 0, 0, 1); border-left-style: solid; border-left-width: 3px; border-right-color: rgba(255, 0, 0, 1); border-right-style: solid; border-right-width: 3px; border-top-color: rgba(255, 0, 0, 1); border-top-style: solid; border-top-width: 3px"), + ("border-width:3px;border:1px solid red", "border-bottom-color: rgba(255, 0, 0, 1); border-bottom-style: solid; border-bottom-width: 1px; border-left-color: rgba(255, 0, 0, 1); border-left-style: solid; border-left-width: 1px; border-right-color: rgba(255, 0, 0, 1); border-right-style: solid; border-right-width: 1px; border-top-color: rgba(255, 0, 0, 1); border-top-style: solid; border-top-width: 1px"), + ("background:red;background:blue", "background-attachment: initial; background-clip: initial; background-color: rgba(0, 0, 255, 1); background-image: initial; background-origin: initial; background-position-x: initial; background-position-y: initial; background-repeat-x: initial; background-repeat-y: initial; background-size: initial"), + ("background:red;background-color:blue", "background-attachment: initial; background-clip: initial; background-color: rgba(0, 0, 255, 1); background-image: initial; background-origin: initial; background-position-x: initial; background-position-y: initial; background-repeat-x: initial; background-repeat-y: initial; background-size: initial"), + ("background-color:blue;background:red", "background-attachment: initial; background-clip: initial; background-color: rgba(255, 0, 0, 1); background-image: initial; background-origin: initial; background-position-x: initial; background-position-y: initial; background-repeat-x: initial; background-repeat-y: initial; background-size: initial"), + ("background:red;background:", "background-attachment: initial; background-clip: initial; background-color: rgba(255, 0, 0, 1); background-image: initial; background-origin: initial; background-position-x: initial; background-position-y: initial; background-repeat-x: initial; background-repeat-y: initial; background-size: initial"), + ("background:linear-gradient(red,blue);background:none", "background-attachment: initial; background-clip: initial; background-color: initial; background-image: none; background-origin: initial; background-position-x: initial; background-position-y: initial; background-repeat-x: initial; background-repeat-y: initial; background-size: initial"), + }; + + [Test] + public void RedeclaringAPropertyProducesTheExpectedBlock() + { + var failures = new List(); + + foreach (var (input, expected) in Cases) + { + var actual = Describe(ParseDeclarations(input)); + + if (!String.Equals(actual, expected, StringComparison.Ordinal)) + { + failures.Add($"{input}{Environment.NewLine} expected: {expected}{Environment.NewLine} actual: {actual}"); + } + } + + Assert.That(failures, Is.Empty, String.Join(Environment.NewLine, failures)); + } + + private static String Describe(ICssStyleDeclaration style) + { + var parts = style.Select(p => $"{p.Name}: {p.Value}{(p.IsImportant ? " !important" : String.Empty)}").ToList(); + parts.Sort(StringComparer.Ordinal); + return String.Join("; ", parts); + } + } +} diff --git a/src/AngleSharp.Css.Tests/Functions/CssColorFunction.cs b/src/AngleSharp.Css.Tests/Functions/CssColorFunction.cs index cf36e11b..e74d060d 100644 --- a/src/AngleSharp.Css.Tests/Functions/CssColorFunction.cs +++ b/src/AngleSharp.Css.Tests/Functions/CssColorFunction.cs @@ -75,6 +75,28 @@ public void ParseOklabToRgb_Second() Assert.AreEqual("rgba(198, 93, 7, 1)", color); } + [Test] + public void ParseOklabNumberToRgb_First() + { + var html = @"

Text

"; + var dom = ParseDocument(html); + var p = dom.QuerySelector("p"); + var s = p.GetStyle(); + var color = s.GetColor(); + Assert.AreEqual("rgba(125, 35, 40, 1)", color); + } + + [Test] + public void ParseOklabNumberToRgb_Second() + { + var html = @"

Text

"; + var dom = ParseDocument(html); + var p = dom.QuerySelector("p"); + var s = p.GetStyle(); + var color = s.GetColor(); + Assert.AreEqual("rgba(198, 93, 7, 1)", color); + } + [Test] public void ParseOklabToRgb_Alpha() { @@ -108,6 +130,28 @@ public void ParseOklchToRgb_Second() Assert.AreEqual("rgba(198, 93, 7, 1)", color); } + [Test] + public void ParseOklchNumberToRgb_First() + { + var html = @"

Text

"; + var dom = ParseDocument(html); + var p = dom.QuerySelector("p"); + var s = p.GetStyle(); + var color = s.GetColor(); + Assert.AreEqual("rgba(125, 35, 40, 1)", color); + } + + [Test] + public void ParseOklchNumberToRgb_Second() + { + var html = @"

Text

"; + var dom = ParseDocument(html); + var p = dom.QuerySelector("p"); + var s = p.GetStyle(); + var color = s.GetColor(); + Assert.AreEqual("rgba(198, 93, 7, 1)", color); + } + [Test] public void ParseOklchToRgb_Alpha() { diff --git a/src/AngleSharp.Css.Tests/Rules/FontFaceDescriptors.cs b/src/AngleSharp.Css.Tests/Rules/FontFaceDescriptors.cs new file mode 100644 index 00000000..f324c1e9 --- /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 9b6249c3..5cecbb1c 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 bacdb6aa..4d36e61b 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 166fb563..eb808595 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 822509b4..340bcf27 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 00000000..b9612271 --- /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 00000000..b5830286 --- /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 00000000..45cb76ed --- /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 00000000..2e956e8a --- /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 00000000..2e652eff --- /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/CssStyleDeclaration.cs b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs index b483b2d3..1163a282 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs @@ -284,7 +284,12 @@ public void SetProperty(String propertyName, String propertyValue, String priori { if (priority is null || priority.Isi(CssKeywords.Important)) { - var property = CreateProperty(propertyName); + // Deliberately not seeded from any existing declaration of the same + // name: assigning Value below always replaces the raw value, so + // reading the old one only costs a lookup. For a shorthand that + // lookup is not even a lookup - it rebuilds the shorthand from its + // longhands just to throw the result away. + var property = _context.CreateProperty(propertyName); if (property is not null) { @@ -372,19 +377,6 @@ private ICssProperty FindUnserializedLonghand(String name, IEnumerable s return null; } - private ICssProperty CreateProperty(String propertyName) - { - var newProperty = _context.CreateProperty(propertyName); - var existing = GetProperty(propertyName); - - if (existing is not null) - { - newProperty.RawValue = existing.RawValue; - } - - return newProperty; - } - private void SetProperty(ICssProperty property) { if (property.IsShorthand) diff --git a/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs b/src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs index 53d1a40f..9d252016 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 809644ff..d5eef949 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 abde6601..7a168ad6 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/Parser/Micro/ColorParser.cs b/src/AngleSharp.Css/Parser/Micro/ColorParser.cs index 033accc3..bc5bd43a 100644 --- a/src/AngleSharp.Css/Parser/Micro/ColorParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/ColorParser.cs @@ -292,7 +292,7 @@ static class ColorParser private static CssColorValue? ParseOklab(StringSource source) { - var l = ParseLabComponent(source); + var l = ParseLabComponent(source, numberScale: 100.0); source.SkipSpacesAndComments(); var a = ParseLabComponent(source); source.SkipSpacesAndComments(); @@ -324,7 +324,7 @@ static class ColorParser private static CssColorValue? ParseOklch(StringSource source) { - var l = ParseLabComponent(source); + var l = ParseLabComponent(source, numberScale: 100.0); source.SkipSpacesAndComments(); var c = ParseLabComponent(source); source.SkipSpacesAndComments(); @@ -385,7 +385,7 @@ static class ColorParser return null; } - private static Double? ParseLabComponent(StringSource source) + private static Double? ParseLabComponent(StringSource source, Double numberScale = 1.0) { var pos = source.Index; var unit = source.ParseUnit(); @@ -404,7 +404,7 @@ static class ColorParser if ((unit.Dimension == String.Empty || unit.Dimension == "%") && Double.TryParse(unit.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)) { - return value; + return unit.Dimension == String.Empty ? value * numberScale : value; } return null; diff --git a/src/AngleSharp.Css/ValueConverters.cs b/src/AngleSharp.Css/ValueConverters.cs index f7fe0121..8fb2a465 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); diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 929f1545..37fcde26 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ Extends the CSSOM from the core AngleSharp library. AngleSharp.Css - 1.0.0 + 1.0.2 enable latest true