From 5149605ebbbb95a34658862e3ce6af6f4434824e Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Tue, 20 Oct 2026 10:55:41 +0200 Subject: [PATCH 1/4] Prepare for 1.1.5 --- CHANGELOG.md | 7 +++++++ src/AngleSharp.Css.Docs/package.json | 2 +- src/Directory.Build.props | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 886d05bb..2dcaab9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 1.1.5 + +Released on Wednesday, October 21 2026. + +- tbd. + + # 1.1.4 Released on Tuesday, October 20 2026. diff --git a/src/AngleSharp.Css.Docs/package.json b/src/AngleSharp.Css.Docs/package.json index d12d0289..c32a0a73 100644 --- a/src/AngleSharp.Css.Docs/package.json +++ b/src/AngleSharp.Css.Docs/package.json @@ -1,6 +1,6 @@ { "name": "@anglesharp/css", - "version": "1.1.4", + "version": "1.1.5", "preview": true, "description": "The doclet for the AngleSharp.Css documentation.", "keywords": [ diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 2394772d..947e64f6 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.1.4 + 1.1.5 enable latest true From 4130fffa1187530d51616416493b4965c68308e1 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Tue, 20 Oct 2026 17:16:54 +0200 Subject: [PATCH 2/4] Prepare for 1.2.0 --- CHANGELOG.md | 39 ++- src/AngleSharp.Css.Docs/package.json | 2 +- .../Declarations/CssBackgroundProperty.cs | 12 +- .../Declarations/CssBoxModelProperty.cs | 5 +- .../Declarations/CssFontProperty.cs | 6 +- .../Declarations/CssModernValueSyntaxTests.cs | 103 ++++++++ .../Declarations/CssTextProperty.cs | 6 +- src/AngleSharp.Css.Tests/Rules/FontFace.cs | 28 ++- .../Styling/ComputedSerializationTests.cs | 50 ++++ .../Styling/Configuration.cs | 5 +- .../DefaultStyleSheetRenderingSectionTests.cs | 104 ++++++++ .../DroppedValidValuesComputedStyleTests.cs | 164 ++++++++++++ .../FlexSpaceEvenlyComputedStyleTests.cs | 37 +++ .../Styling/LineBreakingInheritanceTests.cs | 27 ++ .../RelativeValueComputedStyleTests.cs | 120 +++++++++ .../Styling/RelativeValueResolutionTests.cs | 93 +++++++ .../ShadowWithoutBlurComputedStyleTests.cs | 49 ++++ .../Values/Point2DComputation.cs | 16 +- src/AngleSharp.Css/Constants/CssKeywords.cs | 187 ++++++++++++++ src/AngleSharp.Css/Constants/FunctionNames.cs | 35 +++ src/AngleSharp.Css/Constants/InitialValues.cs | 4 + src/AngleSharp.Css/Constants/Map.cs | 47 ++++ src/AngleSharp.Css/Constants/PropertyNames.cs | 20 ++ src/AngleSharp.Css/Constants/UnitNames.cs | 65 +++++ .../Converters/AlphaValueConverter.cs | 53 ++++ .../Converters/BoxAlignmentValueConverter.cs | 133 ++++++++++ .../Converters/DisplayValueConverter.cs | 141 +++++++++++ .../Converters/FontStyleValueConverter.cs | 57 +++++ .../CssDefaultStyleSheetProvider.cs | 51 +++- .../Declarations/BackgroundClipDeclaration.cs | 2 +- .../Declarations/ColorDeclaration.cs | 25 +- .../ContentVisibilityDeclaration.cs | 2 +- .../Declarations/FillOpacityDeclaration.cs | 2 +- .../Declarations/FlexBasisDeclaration.cs | 2 +- .../Declarations/FlexDeclaration.cs | 10 + .../FontVariantLigaturesDeclaration.cs | 17 ++ .../FontVariantNumericDeclaration.cs | 17 ++ .../Declarations/FontWeightDeclaration.cs | 2 +- .../Declarations/GridDeclaration.cs | 15 +- .../Declarations/HeightDeclaration.cs | 2 +- .../Declarations/MaxBlockSizeDeclaration.cs | 2 +- .../Declarations/MaxHeightDeclaration.cs | 2 +- .../Declarations/MaxInlineSizeDeclaration.cs | 2 +- .../Declarations/MaxWidthDeclaration.cs | 2 +- .../Declarations/MinBlockSizeDeclaration.cs | 2 +- .../Declarations/MinHeightDeclaration.cs | 2 +- .../Declarations/MinInlineSizeDeclaration.cs | 2 +- .../Declarations/MinWidthDeclaration.cs | 2 +- .../Declarations/OpacityDeclaration.cs | 2 +- .../Declarations/OutlineDeclaration.cs | 2 +- .../Declarations/OutlineStyleDeclaration.cs | 2 +- .../Declarations/OverflowWrapDeclaration.cs | 2 +- .../Declarations/StrokeOpacityDeclaration.cs | 2 +- .../Declarations/TextIndentDeclaration.cs | 2 +- .../Declarations/TransformBoxDeclaration.cs | 17 ++ .../TransitionBehaviorDeclaration.cs | 18 ++ .../Declarations/WordBreakDeclaration.cs | 2 +- .../Declarations/WordWrapDeclaration.cs | 2 +- src/AngleSharp.Css/Dom/BorderRepeat.cs | 6 +- src/AngleSharp.Css/Dom/ClearMode.cs | 10 +- src/AngleSharp.Css/Dom/DisplayMode.cs | 8 + src/AngleSharp.Css/Dom/FlexContentMode.cs | 4 + src/AngleSharp.Css/Dom/Floating.cs | 8 + .../Dom/Internal/CssProperty.cs | 23 +- .../Dom/Internal/CssStyleDeclaration.cs | 6 + src/AngleSharp.Css/Dom/OverflowWrap.cs | 6 +- src/AngleSharp.Css/Dom/Sizing.cs | 5 + src/AngleSharp.Css/Dom/Whitespace.cs | 6 +- .../Extensions/CssOmExtensions.cs | 5 + .../Factories/DefaultDeclarationFactory.cs | 28 +++ src/AngleSharp.Css/Parser/Micro/CalcParser.cs | 50 +++- .../Parser/Micro/ColorParser.cs | 181 ++++++++++++++ .../Parser/Micro/CompoundParser.cs | 70 +++++- src/AngleSharp.Css/Parser/Micro/GridParser.cs | 30 +++ .../Parser/Micro/PointParser.cs | 77 +++++- .../Parser/Micro/ShadowParser.cs | 2 +- .../Parser/Micro/TimingFunctionParser.cs | 72 +++++- src/AngleSharp.Css/Parser/Micro/UnitParser.cs | 19 +- src/AngleSharp.Css/ValueConverters.cs | 233 ++++++++++++++++-- .../Values/Composites/CssEdgeOffsetValue.cs | 62 +++++ .../Values/Composites/CssShadowValue.cs | 2 +- .../Values/CssComputeContext.cs | 28 ++- .../Expressions/CssCalcAddExpression.cs | 3 +- .../Expressions/CssCalcBracketExpression.cs | 8 +- .../Expressions/CssCalcDivExpression.cs | 3 +- .../Expressions/CssCalcMulExpression.cs | 3 +- .../Expressions/CssCalcSubExpression.cs | 3 +- .../Values/Functions/CssCalcValue.cs | 11 +- .../Values/Functions/CssImageSetValue.cs | 80 ++++++ .../Values/Functions/CssLinearEasingValue.cs | 56 +++++ .../Values/Functions/CssMathValue.cs | 80 ++++++ .../Functions/CssRadialGradientValue.cs | 2 +- .../Values/Functions/CssStepsValue.cs | 27 +- .../Values/IFontComputeContext.cs | 24 ++ .../Values/IRelativeComputeContext.cs | 30 +++ .../Values/Multiples/CssFlowRelativeValue.cs | 6 + .../Values/Multiples/CssPeriodicValue.cs | 6 + .../Values/Multiples/CssRadiusValue.cs | 6 + .../Values/Multiples/CssTupleValue.cs | 6 + .../Primitives/CssCounterDefinitionValue.cs | 2 +- .../Primitives/CssFontSizeKeywordValue.cs | 71 ++++++ .../Values/Primitives/CssLengthValue.cs | 126 +++++++++- .../Values/Primitives/CssResolutionValue.cs | 8 +- src/Directory.Build.props | 2 +- 104 files changed, 3201 insertions(+), 125 deletions(-) create mode 100644 src/AngleSharp.Css.Tests/Declarations/CssModernValueSyntaxTests.cs create mode 100644 src/AngleSharp.Css.Tests/Styling/ComputedSerializationTests.cs create mode 100644 src/AngleSharp.Css.Tests/Styling/DefaultStyleSheetRenderingSectionTests.cs create mode 100644 src/AngleSharp.Css.Tests/Styling/DroppedValidValuesComputedStyleTests.cs create mode 100644 src/AngleSharp.Css.Tests/Styling/FlexSpaceEvenlyComputedStyleTests.cs create mode 100644 src/AngleSharp.Css.Tests/Styling/LineBreakingInheritanceTests.cs create mode 100644 src/AngleSharp.Css.Tests/Styling/RelativeValueComputedStyleTests.cs create mode 100644 src/AngleSharp.Css.Tests/Styling/RelativeValueResolutionTests.cs create mode 100644 src/AngleSharp.Css.Tests/Styling/ShadowWithoutBlurComputedStyleTests.cs create mode 100644 src/AngleSharp.Css/Converters/AlphaValueConverter.cs create mode 100644 src/AngleSharp.Css/Converters/BoxAlignmentValueConverter.cs create mode 100644 src/AngleSharp.Css/Converters/DisplayValueConverter.cs create mode 100644 src/AngleSharp.Css/Converters/FontStyleValueConverter.cs create mode 100644 src/AngleSharp.Css/Declarations/FontVariantLigaturesDeclaration.cs create mode 100644 src/AngleSharp.Css/Declarations/FontVariantNumericDeclaration.cs create mode 100644 src/AngleSharp.Css/Declarations/TransformBoxDeclaration.cs create mode 100644 src/AngleSharp.Css/Declarations/TransitionBehaviorDeclaration.cs create mode 100644 src/AngleSharp.Css/Values/Composites/CssEdgeOffsetValue.cs create mode 100644 src/AngleSharp.Css/Values/Functions/CssImageSetValue.cs create mode 100644 src/AngleSharp.Css/Values/Functions/CssLinearEasingValue.cs create mode 100644 src/AngleSharp.Css/Values/Functions/CssMathValue.cs create mode 100644 src/AngleSharp.Css/Values/IFontComputeContext.cs create mode 100644 src/AngleSharp.Css/Values/IRelativeComputeContext.cs create mode 100644 src/AngleSharp.Css/Values/Primitives/CssFontSizeKeywordValue.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dcaab9f..04fc3c34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,42 @@ -# 1.1.5 +# 1.2.0 Released on Wednesday, October 21 2026. -- tbd. - +- Improved default stylesheet to follow the HTML Standard's rendering section for links, `mark`, `pre`, `hr`, `fieldset`, `abbr[title]`, `summary`, table cell padding, and list padding +- Improved default stylesheet to isolate `bdi`, `[dir]`, and block elements for the bidi algorithm (`unicode-bidi: isolate`) +- Updated `p`, `pre`, `blockquote`, and list margins in the default stylesheet to `1em` (from `1.12em`) +- Updated computed percentages to remain percentages instead of resolving them against the viewport, except for `font-size` (parent font size) and `line-height` (element font size) +- Updated `calc()`, `min()`, and `max()` to keep their expression in the computed value when it cannot be resolved (e.g., `calc(50% + 10px)`) +- Updated `font` shorthand to serialize font size keywords as written (e.g., `large` instead of `1.2em`) +- Fixed `em` resolving against the device font size instead of the element's font size (the parent's font size for `font-size`) +- Fixed `rem` ignoring the root element's font size +- Fixed computed `font-size` keywords (`medium`, `x-large`, `larger`, `smaller`, ...) not resolving to lengths +- Fixed computing `ex` and `ch` lengths throwing an exception +- Fixed `ComputeCurrentStyle` throwing for a `box-shadow` or `text-shadow` layer without a blur radius +- Fixed `word-break`, `overflow-wrap`, and `word-wrap` not being inherited +- Fixed computed `border-radius` shorthand (and other shorthands merging computed lists, tuples, or periodic values) being empty +- Fixed `grid: auto-flow / ...` losing `auto-flow` and `dense` expanding to the wrong `grid-auto-flow` axis +- Fixed `counters()` losing the comma and the quotes of its separator string +- Fixed `color: currentcolor` not computing to the inherited color +- Fixed `flex: none` not expanding to `0 0 auto` +- Fixed `content-visibility` accepting `collapse` instead of `auto` +- Fixed `min-width`, `min-height`, and their logical counterparts rejecting `auto` +- Fixed radial gradients without a size serializing a double space +- Added `space-evenly` and the remaining CSS Box Alignment 3 keywords (`start`, `end`, `left`, `right`, `normal`, `stretch`, `self-start`, `self-end`, `legacy`, `first` / `last baseline`, `safe` / `unsafe`) +- Added support for the multi-keyword `display` syntax (e.g., `inline flex`) and `display: contents` / `ruby` +- Added support for `min()`, `max()`, and `clamp()` +- Added support for `stretch` and `fit-content()` in sizing properties +- Added support for the small, large, and dynamic viewport units (e.g., `svh`, `lvw`, `dvmin`) +- Added support for the space-separated syntax of `hsl()` and `hwb()`, `color-mix()` (interpolated in sRGB), and `light-dark()` +- Added support for percentages in `opacity`, `fill-opacity`, and `stroke-opacity` +- Added support for `currentcolor` in shadows +- Added support for three- and four-value positions (e.g., `right 10px bottom 20px`) and edge offsets in `background-position-x` / `-y` +- Added support for `image-set()` and the `x` resolution unit +- Added support for `aspect-ratio: auto `, `grid-template-*: subgrid`, and `text-indent` with `hanging` / `each-line` +- Added support for font weights in the range 1 to 1000 (also in `@font-face`) and `font-style: oblique ` +- Added support for the `jump-*` positions of `steps()` and the `linear()` easing function +- Added support for `plus-lighter`, `image-rendering: pixelated`, `background-clip: text`, `border-image-repeat: space`, `outline-style: auto`, `overflow-wrap: anywhere`, `white-space: break-spaces`, two-value `text-overflow`, and `float` / `clear` with `inline-start` / `inline-end` +- Added `font-variant-numeric`, `font-variant-ligatures`, `transform-box`, and `transition-behavior` properties # 1.1.4 diff --git a/src/AngleSharp.Css.Docs/package.json b/src/AngleSharp.Css.Docs/package.json index c32a0a73..cbb453cb 100644 --- a/src/AngleSharp.Css.Docs/package.json +++ b/src/AngleSharp.Css.Docs/package.json @@ -1,6 +1,6 @@ { "name": "@anglesharp/css", - "version": "1.1.5", + "version": "1.2.0", "preview": true, "description": "The doclet for the AngleSharp.Css documentation.", "keywords": [ diff --git a/src/AngleSharp.Css.Tests/Declarations/CssBackgroundProperty.cs b/src/AngleSharp.Css.Tests/Declarations/CssBackgroundProperty.cs index 5ba99278..f16f692b 100644 --- a/src/AngleSharp.Css.Tests/Declarations/CssBackgroundProperty.cs +++ b/src/AngleSharp.Css.Tests/Declarations/CssBackgroundProperty.cs @@ -353,13 +353,23 @@ public void CssBackgroundPositionCenterPercentLegal() } [Test] - public void CssBackgroundPositionRightLengthBottomLengthIllegal() + public void CssBackgroundPositionRightLengthBottomLengthLegal() { var snippet = "background-position: right 20px bottom 20px"; var property = ParseDeclaration(snippet); Assert.AreEqual("background-position", property.Name); Assert.IsFalse(property.IsImportant); Assert.IsFalse(property.IsInherited); + Assert.IsTrue(property.HasValue); + Assert.AreEqual("right 20px bottom 20px", property.Value); + } + + [Test] + public void CssBackgroundPositionCenterWithOffsetIllegal() + { + var snippet = "background-position: center 20px bottom 20px"; + var property = ParseDeclaration(snippet); + Assert.AreEqual("background-position", property.Name); Assert.IsFalse(property.HasValue); } diff --git a/src/AngleSharp.Css.Tests/Declarations/CssBoxModelProperty.cs b/src/AngleSharp.Css.Tests/Declarations/CssBoxModelProperty.cs index 6e87e6f1..acf69eac 100644 --- a/src/AngleSharp.Css.Tests/Declarations/CssBoxModelProperty.cs +++ b/src/AngleSharp.Css.Tests/Declarations/CssBoxModelProperty.cs @@ -174,12 +174,13 @@ public void MinBlockSizePixelLegal() } [Test] - public void MinBlockSizeAutoIllegal() + public void MinBlockSizeAutoLegal() { var snippet = "min-block-size: auto"; var property = ParseDeclaration(snippet); Assert.AreEqual("min-block-size", property.Name); - Assert.IsFalse(property.HasValue); + Assert.IsTrue(property.HasValue); + Assert.AreEqual("auto", property.Value); } #endregion diff --git a/src/AngleSharp.Css.Tests/Declarations/CssFontProperty.cs b/src/AngleSharp.Css.Tests/Declarations/CssFontProperty.cs index 87c7a0d1..68061cf7 100644 --- a/src/AngleSharp.Css.Tests/Declarations/CssFontProperty.cs +++ b/src/AngleSharp.Css.Tests/Declarations/CssFontProperty.cs @@ -439,7 +439,7 @@ public void CssFontShorthandBoldItalicLargeLegal() Assert.IsFalse(property.IsImportant); Assert.IsFalse(property.IsInherited); Assert.IsTrue(property.HasValue); - Assert.AreEqual("italic bold 1.2em serif", property.Value); + Assert.AreEqual("italic bold large serif", property.Value); } [Test] @@ -594,7 +594,7 @@ public void CssFontSizeHeightMultipleFamiliesLegal() Assert.IsFalse(property.IsImportant); Assert.IsFalse(property.IsInherited); Assert.IsTrue(property.HasValue); - Assert.AreEqual("1.5em / 110% \"New Century Schoolbook\", serif", property.Value); + Assert.AreEqual("x-large / 110% \"New Century Schoolbook\", serif", property.Value); } [Test] @@ -606,7 +606,7 @@ public void CssFontWeightVariantSizeFamiliesLegal() Assert.IsFalse(property.IsImportant); Assert.IsFalse(property.IsInherited); Assert.IsTrue(property.HasValue); - Assert.AreEqual("italic bold 1.2em Palatino, serif", property.Value); + Assert.AreEqual("italic bold large Palatino, serif", property.Value); } [Test] diff --git a/src/AngleSharp.Css.Tests/Declarations/CssModernValueSyntaxTests.cs b/src/AngleSharp.Css.Tests/Declarations/CssModernValueSyntaxTests.cs new file mode 100644 index 00000000..bbb50f59 --- /dev/null +++ b/src/AngleSharp.Css.Tests/Declarations/CssModernValueSyntaxTests.cs @@ -0,0 +1,103 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Declarations +{ + using NUnit.Framework; + using static CssConstructionFunctions; + + /// + /// Covers the parsing and serialization of value syntax from current CSS specifications, + /// next to the cases in . + /// + [TestFixture] + public class CssModernValueSyntaxTests + { + private static void AssertValue(string declaration, string expected) + { + var property = ParseDeclaration(declaration); + + if (expected is null) + { + Assert.IsFalse(property.HasValue, declaration); + } + else + { + Assert.IsTrue(property.HasValue, declaration); + Assert.AreEqual(expected, property.Value, declaration); + } + } + + [TestCase("display: inline flow-root", "inline-block")] + [TestCase("display: flex inline", "inline-flex")] + [TestCase("display: block table", "table")] + [TestCase("display: inline grid", "inline-grid")] + [TestCase("display: block flow", "block")] + [TestCase("display: flow", "block")] + [TestCase("display: block ruby", "block ruby")] + [TestCase("display: list-item inline", "inline list-item")] + [TestCase("display: block flow-root list-item", "flow-root list-item")] + [TestCase("display: table list-item", null)] + [TestCase("display: block inline", null)] + public void DisplayMultiKeywordSyntax(string declaration, string expected) => AssertValue(declaration, expected); + + [TestCase("justify-items: right legacy", "legacy right")] + [TestCase("justify-items: legacy center", "legacy center")] + [TestCase("align-self: unsafe self-end", "unsafe self-end")] + [TestCase("align-items: safe baseline", null)] + [TestCase("justify-content: space-evenly", "space-evenly")] + [TestCase("place-content: space-evenly start", "space-evenly start")] + public void BoxAlignmentSyntax(string declaration, string expected) => AssertValue(declaration, expected); + + [TestCase("transition-timing-function: steps(3, jump-none)", "steps(3, jump-none)")] + [TestCase("transition-timing-function: steps(2, jump-both)", "steps(2, jump-both)")] + [TestCase("transition-timing-function: steps(2, jump-start)", "steps(2, jump-start)")] + [TestCase("transition-timing-function: steps(1, jump-none)", null)] + [TestCase("transition-timing-function: steps(2, jump-sideways)", null)] + [TestCase("transition-timing-function: linear(0, 0.25 25% 75%, 1)", "linear(0, 0.25 25% 75%, 1)")] + [TestCase("transition-timing-function: linear(0)", null)] + [TestCase("transition-behavior: normal, allow-discrete", "normal, allow-discrete")] + public void EasingSyntax(string declaration, string expected) => AssertValue(declaration, expected); + + [TestCase("color: hsl(none 100% 50%)", "rgba(255, 0, 0, 1)")] + [TestCase("color: hsl(120 100 50)", "rgba(0, 255, 0, 1)")] + [TestCase("color: hwb(0 0% 0% / 0.5)", "rgba(255, 0, 0, 0.5)")] + [TestCase("color: color-mix(in srgb, red, blue)", "rgba(128, 0, 128, 1)")] + [TestCase("color: color-mix(in srgb, red 25%, blue 25%)", "rgba(128, 0, 128, 0.5)")] + [TestCase("color: light-dark(red, blue)", "rgba(255, 0, 0, 1)")] + [TestCase("color: currentcolor", "currentColor")] + [TestCase("color: color-mix(srgb, red, blue)", null)] + public void ColorSyntax(string declaration, string expected) => AssertValue(declaration, expected); + + [TestCase("background-image: image-set(\"a.png\" 1x type(\"image/png\"), url(b.png) 2x)", "image-set(\"a.png\" 1x type(\"image/png\"), url(\"b.png\") 2x)")] + [TestCase("background-position-x: right 10px, 20%", "right 10px, 20%")] + [TestCase("object-position: left 10px top 5%", "left 10px top 5%")] + [TestCase("transform-origin: right bottom 2cm", "right bottom 2cm")] + [TestCase("background-clip: text, border-box", "text, border-box")] + [TestCase("border-style: auto", null)] + [TestCase("opacity: 50%", "50%")] + public void PaintSyntax(string declaration, string expected) => AssertValue(declaration, expected); + + [TestCase("font-weight: 550", "550")] + [TestCase("font-weight: 1001", null)] + [TestCase("font-style: oblique 10deg", "oblique 10deg")] + [TestCase("font-style: oblique", "oblique")] + [TestCase("font-variant-numeric: slashed-zero oldstyle-nums", "oldstyle-nums slashed-zero")] + [TestCase("font-variant-numeric: lining-nums oldstyle-nums", null)] + [TestCase("font-variant-ligatures: no-common-ligatures contextual", "no-common-ligatures contextual")] + [TestCase("text-indent: each-line 5% hanging", "5% hanging each-line")] + [TestCase("text-indent: hanging", null)] + [TestCase("text-overflow: clip ellipsis", "clip ellipsis")] + public void TextSyntax(string declaration, string expected) => AssertValue(declaration, expected); + + [TestCase("grid-template-columns: subgrid [a] [b c]", "subgrid [a] [b c]")] + [TestCase("aspect-ratio: 16/9 auto", "auto 16/9")] + [TestCase("width: fit-content", "fit-content")] + [TestCase("width: fit-content(50%)", "fit-content(50%)")] + [TestCase("max-width: stretch", "stretch")] + [TestCase("max-width: auto", null)] + [TestCase("width: calc(min(10px, 20%) + 5px)", "calc(min(10px, 20%) + 5px)")] + [TestCase("width: clamp(10px, 20px)", null)] + [TestCase("height: 50dvmin", "50dvmin")] + [TestCase("transform-box: fill-box", "fill-box")] + public void LayoutSyntax(string declaration, string expected) => AssertValue(declaration, expected); + } +} diff --git a/src/AngleSharp.Css.Tests/Declarations/CssTextProperty.cs b/src/AngleSharp.Css.Tests/Declarations/CssTextProperty.cs index fc26edf2..0623a6ba 100644 --- a/src/AngleSharp.Css.Tests/Declarations/CssTextProperty.cs +++ b/src/AngleSharp.Css.Tests/Declarations/CssTextProperty.cs @@ -358,7 +358,7 @@ public void CssWordBreakNoneIllegal() var property = ParseDeclaration(snippet); Assert.AreEqual("word-break", property.Name); Assert.IsFalse(property.HasValue); - Assert.IsFalse(property.IsInherited); + Assert.IsTrue(property.IsInherited); Assert.IsFalse(property.IsImportant); } @@ -688,7 +688,7 @@ public void CssOverflowWrapNoneIllegal() var snippet = "overflow-wrap: none"; var property = ParseDeclaration(snippet); Assert.AreEqual("overflow-wrap", property.Name); - Assert.IsFalse(property.IsInherited); + Assert.IsTrue(property.IsInherited); Assert.IsFalse(property.IsImportant); Assert.IsFalse(property.HasValue); } @@ -699,7 +699,7 @@ public void CssOverflowWrapAlternateNameNoneIllegal() var snippet = "word-wrap: none"; var property = ParseDeclaration(snippet); Assert.AreEqual("word-wrap", property.Name); - Assert.IsFalse(property.IsInherited); + Assert.IsTrue(property.IsInherited); Assert.IsFalse(property.IsImportant); Assert.IsFalse(property.HasValue); } diff --git a/src/AngleSharp.Css.Tests/Rules/FontFace.cs b/src/AngleSharp.Css.Tests/Rules/FontFace.cs index 5470fd9b..0d4d4bb6 100644 --- a/src/AngleSharp.Css.Tests/Rules/FontFace.cs +++ b/src/AngleSharp.Css.Tests/Rules/FontFace.cs @@ -119,22 +119,44 @@ public void FontFaceWeightMaxBoundary900ShouldBePreserved() } [Test] - public void FontFaceWeightOutOfRange1ShouldBeInvalid() + public void FontFaceWeightBoundary1ShouldBeValid() { + // CSS Fonts 4 allows any number in the range [1, 1000]. var src = "@font-face { font-weight: 1; }"; var sheet = ParseStyleSheet(src); Assert.AreEqual(1, sheet.Rules.Length); var fontface = (ICssFontFaceRule)sheet.Rules[0]; - Assert.AreEqual("", fontface.Weight); + Assert.AreEqual("1", fontface.Weight); } [Test] - public void FontFaceWeightOutOfRange1000ShouldBeInvalid() + public void FontFaceWeightBoundary1000ShouldBeValid() { + // CSS Fonts 4 allows any number in the range [1, 1000]. var src = "@font-face { font-weight: 1000; }"; var sheet = ParseStyleSheet(src); Assert.AreEqual(1, sheet.Rules.Length); var fontface = (ICssFontFaceRule)sheet.Rules[0]; + Assert.AreEqual("1000", fontface.Weight); + } + + [Test] + public void FontFaceWeightOutOfRange0ShouldBeInvalid() + { + var src = "@font-face { font-weight: 0; }"; + var sheet = ParseStyleSheet(src); + Assert.AreEqual(1, sheet.Rules.Length); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; + Assert.AreEqual("", fontface.Weight); + } + + [Test] + public void FontFaceWeightOutOfRange1001ShouldBeInvalid() + { + var src = "@font-face { font-weight: 1001; }"; + var sheet = ParseStyleSheet(src); + Assert.AreEqual(1, sheet.Rules.Length); + var fontface = (ICssFontFaceRule)sheet.Rules[0]; Assert.AreEqual("", fontface.Weight); } diff --git a/src/AngleSharp.Css.Tests/Styling/ComputedSerializationTests.cs b/src/AngleSharp.Css.Tests/Styling/ComputedSerializationTests.cs new file mode 100644 index 00000000..ee096904 --- /dev/null +++ b/src/AngleSharp.Css.Tests/Styling/ComputedSerializationTests.cs @@ -0,0 +1,50 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Styling +{ + using AngleSharp.Css.Dom; + using AngleSharp.Dom; + using AngleSharp.Html.Parser; + using NUnit.Framework; + + /// + /// Computed values that are parsed correctly but come back wrong when serialized. Found in a + /// downstream renderer (AngleSharp.Renderer): + /// - the `border-radius` shorthand computes as empty (even for a single value), although the + /// per-corner longhands are right; + /// - `grid: auto-flow / 1fr 1fr` loses `auto-flow`; + /// - `counters(item, ".")` loses the comma and the quotes of its separator string. + /// + [TestFixture] + public class ComputedSerializationTests + { + private static string Computed(string declarations, string property) + { + var config = Configuration.Default + .WithCss() + .WithRenderDevice(new DefaultRenderDevice { ViewPortWidth = 400, ViewPortHeight = 300, FontSize = 16 }); + var document = BrowsingContext.New(config).GetService().ParseDocument($"
x
"); + return document.GetElementById("target").ComputeCurrentStyle().GetPropertyValue(property); + } + + [TestCase("10px", "10px")] + [TestCase("10px 20px", "10px 20px")] + [TestCase("10px / 20px", "10px / 20px")] + [TestCase("10px 20px / 5px", "10px 20px / 5px")] + public void BorderRadiusShorthandIsComputed(string value, string expected) + { + Assert.AreEqual(expected, Computed($"border-radius: {value}", "border-radius")); + } + + [Test] + public void GridShorthandKeepsAutoFlow() + { + Assert.AreEqual("auto-flow / 1fr 1fr", Computed("grid: auto-flow / 1fr 1fr", "grid")); + } + + [Test] + public void CountersKeepsItsSeparatorString() + { + Assert.AreEqual("counters(item, \".\")", Computed("content: counters(item, \".\")", "content")); + } + } +} diff --git a/src/AngleSharp.Css.Tests/Styling/Configuration.cs b/src/AngleSharp.Css.Tests/Styling/Configuration.cs index 2ffdc73b..4ce512d0 100644 --- a/src/AngleSharp.Css.Tests/Styling/Configuration.cs +++ b/src/AngleSharp.Css.Tests/Styling/Configuration.cs @@ -41,7 +41,10 @@ public void ObtainDefaultSheet() // 60, not 50: the UA stylesheet now also carries the rules for elements that are // never rendered (`[hidden]`, `input[type=hidden]`) and the default look of form // controls (text fields, buttons, checkboxes/radios, color and range inputs). - Assert.AreEqual(60, sheet.Rules.Length); + // 70, not 60: the UA stylesheet now also follows the HTML Standard's rendering + // section for bidi isolation, `summary`, `fieldset`, `mark`, `abbr[title]`, links, + // and the block margins / list padding. + Assert.AreEqual(70, sheet.Rules.Length); } } } diff --git a/src/AngleSharp.Css.Tests/Styling/DefaultStyleSheetRenderingSectionTests.cs b/src/AngleSharp.Css.Tests/Styling/DefaultStyleSheetRenderingSectionTests.cs new file mode 100644 index 00000000..78ba4c83 --- /dev/null +++ b/src/AngleSharp.Css.Tests/Styling/DefaultStyleSheetRenderingSectionTests.cs @@ -0,0 +1,104 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Styling +{ + using AngleSharp.Dom; + using NUnit.Framework; + using static CssConstructionFunctions; + + /// + /// Rules of the HTML Standard's rendering section + /// (https://html.spec.whatwg.org/multipage/rendering.html) that the default stylesheet still + /// lacks or gets differently. Found in a downstream renderer (AngleSharp.Renderer), which + /// takes every style from the computed style: links render black, `mark` has no highlight, + /// list backgrounds stop short of the markers, and `bdi`/`[dir]` do not isolate their text + /// for the bidi algorithm. + /// + [TestFixture] + public class DefaultStyleSheetRenderingSectionTests + { + private static string Computed(string html, string property) + { + var document = ParseDocument(html); + return document.GetElementById("target").ComputeCurrentStyle().GetPropertyValue(property); + } + + [Test] + public void LinksAreBlue() + { + Assert.AreEqual("rgba(0, 0, 238, 1)", Computed("

x

", "color")); + } + + [TestCase("background-color", "rgba(255, 255, 0, 1)")] + [TestCase("color", "rgba(0, 0, 0, 1)")] + public void MarkIsHighlighted(string property, string expected) + { + Assert.AreEqual(expected, Computed("

x

", property)); + } + + [TestCase("margin-top")] + [TestCase("margin-bottom")] + public void PreHasBlockMargins(string property) + { + Assert.AreEqual("16px", Computed("
x
", property)); + } + + [TestCase("margin-top", "8px")] + [TestCase("margin-bottom", "8px")] + [TestCase("margin-left", "auto")] + [TestCase("margin-right", "auto")] + public void HrHasMargins(string property, string expected) + { + Assert.AreEqual(expected, Computed("
", property)); + } + + [TestCase("td")] + [TestCase("th")] + public void TableCellsHavePadding(string tagName) + { + Assert.AreEqual("1px", Computed($"<{tagName} id=target>x
", "padding-top")); + } + + [TestCase("border-top-style", "groove")] + [TestCase("border-top-width", "2px")] + [TestCase("padding-top", "5.6px")] + [TestCase("padding-left", "12px")] + [TestCase("margin-left", "2px")] + public void FieldsetHasBorderAndPadding(string property, string expected) + { + Assert.AreEqual(expected, Computed("
lx
", property)); + } + + [TestCase("text-decoration-line", "underline")] + [TestCase("text-decoration-style", "dotted")] + public void AbbreviationsWithTitleAreUnderlinedDotted(string property, string expected) + { + Assert.AreEqual(expected, Computed("

x

", property)); + } + + [TestCase("

x

")] + [TestCase("

x

")] + [TestCase("

x

")] + public void BdiAndDirAttributeIsolate(string html) + { + Assert.AreEqual("isolate", Computed(html, "unicode-bidi")); + } + + [Test] + public void SummaryIsAListItemWithADisclosureMarker() + { + Assert.AreEqual("list-item", Computed("
sx
", "display")); + Assert.AreEqual("disclosure-closed", Computed("
sx
", "list-style-type")); + Assert.AreEqual("disclosure-open", Computed("
sx
", "list-style-type")); + } + + [TestCase("ul")] + [TestCase("ol")] + public void ListsIndentWithPadding(string tagName) + { + var html = $"<{tagName} id=target>
  • x
  • "; + + Assert.AreEqual("40px", Computed(html, "padding-left")); + Assert.AreEqual("0px", Computed(html, "margin-left")); + } + } +} diff --git a/src/AngleSharp.Css.Tests/Styling/DroppedValidValuesComputedStyleTests.cs b/src/AngleSharp.Css.Tests/Styling/DroppedValidValuesComputedStyleTests.cs new file mode 100644 index 00000000..bc8ac5d2 --- /dev/null +++ b/src/AngleSharp.Css.Tests/Styling/DroppedValidValuesComputedStyleTests.cs @@ -0,0 +1,164 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Styling +{ + using AngleSharp.Css.Dom; + using AngleSharp.Dom; + using AngleSharp.Html.Parser; + using NUnit.Framework; + + /// + /// Valid values (current CSS specifications, shipped in browsers) that the parser rejects, so + /// the whole declaration is silently dropped and the property computes as empty. Found by + /// probing every property a downstream renderer (AngleSharp.Renderer) reads. Where the + /// serialization of the computed value is not obvious the test only requires the + /// declaration to survive (`*`). `justify-content`/`align-content: space-evenly` are in + /// . + /// + [TestFixture] + public class DroppedValidValuesComputedStyleTests + { + private static string Computed(string html, string property) + { + var config = Configuration.Default + .WithCss() + .WithRenderDevice(new DefaultRenderDevice { ViewPortWidth = 400, ViewPortHeight = 300, FontSize = 16 }); + var document = BrowsingContext.New(config).GetService().ParseDocument(html); + return document.GetElementById("target").ComputeCurrentStyle().GetPropertyValue(property); + } + + private static void AssertComputed(string property, string value, string expected) + { + var computed = Computed($"
    x
    ", property); + + if (expected == "*") + { + Assert.That(computed, Is.Not.Empty, $"{property}: {value}"); + } + else + { + Assert.AreEqual(expected, computed, $"{property}: {value}"); + } + } + + // CSS Box Alignment 3 + [TestCase("justify-content", "start", "start")] + [TestCase("justify-content", "end", "end")] + [TestCase("justify-content", "left", "left")] + [TestCase("justify-content", "right", "right")] + [TestCase("justify-content", "normal", "normal")] + [TestCase("justify-content", "stretch", "stretch")] + [TestCase("justify-content", "safe center", "safe center")] + [TestCase("justify-content", "unsafe end", "unsafe end")] + [TestCase("align-content", "start", "start")] + [TestCase("align-content", "end", "end")] + [TestCase("align-content", "normal", "normal")] + [TestCase("align-content", "baseline", "baseline")] + [TestCase("align-items", "start", "start")] + [TestCase("align-items", "end", "end")] + [TestCase("align-items", "self-start", "self-start")] + [TestCase("align-items", "self-end", "self-end")] + [TestCase("align-items", "normal", "normal")] + [TestCase("align-items", "first baseline", "baseline")] + [TestCase("align-items", "last baseline", "last baseline")] + [TestCase("align-self", "start", "start")] + [TestCase("align-self", "end", "end")] + [TestCase("align-self", "self-end", "self-end")] + [TestCase("align-self", "normal", "normal")] + [TestCase("justify-self", "start", "start")] + [TestCase("justify-self", "end", "end")] + [TestCase("justify-items", "start", "start")] + [TestCase("justify-items", "legacy", "legacy")] + [TestCase("place-self", "end", "end")] + public void BoxAlignmentKeywordsAreKept(string property, string value, string expected) => + AssertComputed(property, value, expected); + + // CSS Flexbox, Sizing, Values and Units, Display + [TestCase("flex-basis", "max-content", "max-content")] + [TestCase("min-width", "auto", "auto")] + [TestCase("min-height", "auto", "auto")] + [TestCase("display", "contents", "contents")] + [TestCase("display", "inline flex", "inline-flex")] + [TestCase("display", "block flow-root", "flow-root")] + [TestCase("display", "inline list-item", "inline list-item")] + [TestCase("display", "ruby", "ruby")] + [TestCase("width", "min(100px, 50px)", "50px")] + [TestCase("width", "max(100px, 50px)", "100px")] + [TestCase("width", "clamp(100px, 50px, 300px)", "100px")] + [TestCase("font-size", "clamp(12px, 20px, 18px)", "18px")] + [TestCase("height", "10svh", "30px")] + [TestCase("height", "10lvh", "30px")] + [TestCase("height", "10dvh", "30px")] + [TestCase("aspect-ratio", "auto 4 / 3", "*")] + [TestCase("float", "inline-start", "inline-start")] + [TestCase("clear", "inline-end", "inline-end")] + [TestCase("width", "stretch", "stretch")] + [TestCase("width", "fit-content(100px)", "fit-content(100px)")] + [TestCase("grid-template-columns", "subgrid", "subgrid")] + [TestCase("content-visibility", "auto", "auto")] + public void LayoutValuesAreKept(string property, string value, string expected) => + AssertComputed(property, value, expected); + + [Test] + public void FlexNoneIsKept() + { + const string Html = "
    x
    "; + + Assert.AreEqual("0", Computed(Html, "flex-grow")); + Assert.AreEqual("0", Computed(Html, "flex-shrink")); + Assert.AreEqual("auto", Computed(Html, "flex-basis")); + } + + // CSS Color 4/5 + [TestCase("color", "hsl(120 100% 50%)", "rgba(0, 255, 0, 1)")] + [TestCase("color", "hsl(120deg 100% 50% / 50%)", "rgba(0, 255, 0, 0.5)")] + [TestCase("color", "hwb(120 0% 0%)", "rgba(0, 255, 0, 1)")] + [TestCase("color", "color-mix(in srgb, red 50%, blue)", "*")] + [TestCase("color", "light-dark(red, blue)", "*")] + [TestCase("opacity", "50%", "0.5")] + [TestCase("box-shadow", "0 0 0 1px currentcolor", "*")] + [TestCase("mix-blend-mode", "plus-lighter", "plus-lighter")] + public void ColorValuesAreKept(string property, string value, string expected) => + AssertComputed(property, value, expected); + + [TestCase("currentcolor")] + [TestCase("currentColor")] + public void ColorCurrentColorIsTheInheritedColor(string value) + { + Assert.AreEqual("rgba(255, 0, 0, 1)", Computed($"

    x

    ", "color")); + } + + // CSS Backgrounds 3/4, Images 4, Masking + [TestCase("background-position", "right 10px bottom 20px", "right 10px bottom 20px")] + [TestCase("background-position", "right 10px top", "*")] + [TestCase("background-position-x", "right 10px", "right 10px")] + [TestCase("background-position-y", "bottom 5px", "bottom 5px")] + [TestCase("background-image", "image-set(\"a.png\" 1x)", "*")] + [TestCase("background-clip", "text", "text")] + [TestCase("border-image-repeat", "space", "space")] + [TestCase("outline-style", "auto", "auto")] + [TestCase("image-rendering", "pixelated", "pixelated")] + [TestCase("transform-box", "fill-box", "fill-box")] + public void PaintValuesAreKept(string property, string value, string expected) => + AssertComputed(property, value, expected); + + // CSS Fonts 4, Text 3/4, Overflow + [TestCase("font-weight", "550", "550")] + [TestCase("font-style", "oblique 10deg", "oblique 10deg")] + [TestCase("font-variant-numeric", "tabular-nums", "tabular-nums")] + [TestCase("font-variant-ligatures", "none", "none")] + [TestCase("overflow-wrap", "anywhere", "anywhere")] + [TestCase("word-wrap", "anywhere", "anywhere")] + [TestCase("white-space", "break-spaces", "break-spaces")] + [TestCase("text-indent", "2em hanging", "*")] + [TestCase("text-overflow", "clip ellipsis", "clip ellipsis")] + public void TextValuesAreKept(string property, string value, string expected) => + AssertComputed(property, value, expected); + + // CSS Easing 2, Transitions 2 + [TestCase("transition-timing-function", "steps(4, jump-end)", "*")] + [TestCase("transition-timing-function", "linear(0, 0.5 50%, 1)", "*")] + [TestCase("transition-behavior", "allow-discrete", "allow-discrete")] + public void AnimationValuesAreKept(string property, string value, string expected) => + AssertComputed(property, value, expected); + } +} diff --git a/src/AngleSharp.Css.Tests/Styling/FlexSpaceEvenlyComputedStyleTests.cs b/src/AngleSharp.Css.Tests/Styling/FlexSpaceEvenlyComputedStyleTests.cs new file mode 100644 index 00000000..0f3eeb96 --- /dev/null +++ b/src/AngleSharp.Css.Tests/Styling/FlexSpaceEvenlyComputedStyleTests.cs @@ -0,0 +1,37 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Styling +{ + using AngleSharp.Dom; + using NUnit.Framework; + using static CssConstructionFunctions; + + /// + /// `space-evenly` (CSS Box Alignment 3, supported by every browser for flexbox and grid) is + /// missing from the `justify-content`/`align-content` keyword maps (and from + /// `FlexContentMode`), so the whole declaration is dropped and computes as empty, while + /// `space-between`/`space-around` work. Found in a downstream renderer (AngleSharp.Renderer), + /// where a flex container with `justify-content: space-evenly` packs its items at the start. + /// + [TestFixture] + public class FlexSpaceEvenlyComputedStyleTests + { + [TestCase("justify-content")] + [TestCase("align-content")] + public void SpaceEvenlyIsComputed(string property) + { + var document = ParseDocument($"
    "); + var target = document.GetElementById("target"); + + Assert.AreEqual("space-evenly", target.ComputeCurrentStyle().GetPropertyValue(property)); + } + + [Test] + public void SpaceEvenlyFromAStyleSheetIsComputed() + { + var document = ParseDocument("
    "); + var target = document.GetElementById("target"); + + Assert.AreEqual("space-evenly", target.ComputeCurrentStyle().GetPropertyValue("justify-content")); + } + } +} diff --git a/src/AngleSharp.Css.Tests/Styling/LineBreakingInheritanceTests.cs b/src/AngleSharp.Css.Tests/Styling/LineBreakingInheritanceTests.cs new file mode 100644 index 00000000..cdba7421 --- /dev/null +++ b/src/AngleSharp.Css.Tests/Styling/LineBreakingInheritanceTests.cs @@ -0,0 +1,27 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Styling +{ + using AngleSharp.Dom; + using NUnit.Framework; + using static CssConstructionFunctions; + + /// + /// `word-break` and `overflow-wrap` (with its legacy alias `word-wrap`) are inherited + /// properties (CSS Text 3), but a child reports them as empty. Found in a downstream renderer + /// (AngleSharp.Renderer), where `word-break: break-all` on a paragraph does not reach the text + /// inside its inline elements. + /// + [TestFixture] + public class LineBreakingInheritanceTests + { + [TestCase("word-break", "break-all")] + [TestCase("overflow-wrap", "break-word")] + [TestCase("word-wrap", "break-word")] + public void LineBreakingPropertiesAreInherited(string property, string value) + { + var document = ParseDocument($"

    x

    "); + + Assert.AreEqual(value, document.GetElementById("target").ComputeCurrentStyle().GetPropertyValue(property)); + } + } +} diff --git a/src/AngleSharp.Css.Tests/Styling/RelativeValueComputedStyleTests.cs b/src/AngleSharp.Css.Tests/Styling/RelativeValueComputedStyleTests.cs new file mode 100644 index 00000000..c251a3c0 --- /dev/null +++ b/src/AngleSharp.Css.Tests/Styling/RelativeValueComputedStyleTests.cs @@ -0,0 +1,120 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Styling +{ + using AngleSharp.Css.Dom; + using AngleSharp.Dom; + using AngleSharp.Html.Parser; + using NUnit.Framework; + + /// + /// Relative values are resolved against the wrong reference while computing styles. Found in + /// a downstream renderer (AngleSharp.Renderer), which paints `font-size: 2em` inside a 20px + /// parent at 32px. + /// - `em` always resolves against the device's default font size (16px) instead of the + /// element's own font size (the parent's for `font-size` itself), and `rem` ignores the + /// root element's font size. + /// - Percentages are resolved against the viewport width: lengths whose percentages refer to + /// the containing block (or another box) must either stay percentages or resolve against + /// that box, and `font-size`/`line-height`/`vertical-align` percentages refer to the parent + /// font size, the element's font size and the element's line height. + /// - Relative `font-size` keywords (`larger`, `smaller`) and absolute ones (`medium`, + /// `x-large`) are not turned into lengths. + /// The viewport is 400px wide here and the parent 200px, so the two cases are distinguishable. + /// + [TestFixture] + public class RelativeValueComputedStyleTests + { + private static string Computed(string html, string property) + { + var config = Configuration.Default + .WithCss() + .WithRenderDevice(new DefaultRenderDevice { ViewPortWidth = 400, ViewPortHeight = 300, FontSize = 16 }); + var document = BrowsingContext.New(config).GetService().ParseDocument(html); + return document.GetElementById("target").ComputeCurrentStyle().GetPropertyValue(property); + } + + [TestCase("font-size: 2em", "font-size", "40px")] + [TestCase("width: 2em", "width", "40px")] + [TestCase("margin-top: 1em", "margin-top", "20px")] + [TestCase("padding-left: 1em", "padding-left", "20px")] + [TestCase("line-height: 2em", "line-height", "40px")] + [TestCase("font-size: 10px; width: 2em", "width", "20px")] + [TestCase("font-size: 10px; letter-spacing: 1em", "letter-spacing", "10px")] + [TestCase("font-size: 10px; line-height: 2em", "line-height", "20px")] + public void EmResolvesAgainstTheElementFontSize(string declarations, string property, string expected) + { + Assert.AreEqual(expected, Computed($"

    x

    ", property)); + } + + [Test] + public void RemResolvesAgainstTheRootFontSize() + { + Assert.AreEqual("20px", Computed("

    x

    ", "width")); + } + + [TestCase("width: 50%", "width", "50%", "100px")] + [TestCase("margin-left: 10%", "margin-left", "10%", "20px")] + [TestCase("padding-top: 5%", "padding-top", "5%", "10px")] + [TestCase("text-indent: 10%", "text-indent", "10%", "20px")] + [TestCase("width: calc(50% + 10px)", "width", "calc(50% + 10px)", "110px")] + [TestCase("flex: 2 1 50%", "flex-basis", "50%", "100px")] + public void PercentagesDoNotResolveAgainstTheViewport(string declarations, string property, string percentage, string resolved) + { + var computed = Computed($"

    x

    ", property); + + Assert.That(computed, Is.AnyOf(percentage, resolved)); + } + + [TestCase("background-size: 50% auto", "background-size", "50% auto", "50%")] + [TestCase("object-position: right top", "object-position", "right top", "100% 0%")] + [TestCase("row-gap: 5%", "row-gap", "5%", "5%")] + public void PercentagesOfOtherBoxesStayPercentages(string declarations, string property, string expected, string alternative) + { + var computed = Computed($"

    x

    ", property); + + Assert.That(computed, Is.AnyOf(expected, alternative)); + } + + [Test] + public void FontSizePercentageResolvesAgainstTheParentFontSize() + { + Assert.AreEqual("24px", Computed("

    x

    ", "font-size")); + } + + [Test] + public void LineHeightPercentageResolvesAgainstTheElementFontSize() + { + Assert.AreEqual("30px", Computed("

    x

    ", "line-height")); + } + + [Test] + public void InheritedLineHeightPercentageIsTheParentsAbsoluteLength() + { + Assert.AreEqual("30px", Computed("

    x

    ", "line-height")); + } + + [Test] + public void VerticalAlignPercentageResolvesAgainstTheLineHeight() + { + var computed = Computed("
    x
    ", "vertical-align"); + + Assert.That(computed, Is.AnyOf("-20%", "-4px")); + } + + [TestCase("medium", "16px")] + [TestCase("x-large", "24px")] + public void AbsoluteFontSizeKeywordsComputeToLengths(string keyword, string expected) + { + Assert.AreEqual(expected, Computed($"

    x

    ", "font-size")); + } + + [TestCase("larger")] + [TestCase("smaller")] + public void RelativeFontSizeKeywordsComputeToLengths(string keyword) + { + var computed = Computed($"

    x

    ", "font-size"); + + Assert.That(computed, Does.EndWith("px")); + } + } +} diff --git a/src/AngleSharp.Css.Tests/Styling/RelativeValueResolutionTests.cs b/src/AngleSharp.Css.Tests/Styling/RelativeValueResolutionTests.cs new file mode 100644 index 00000000..fd459b7a --- /dev/null +++ b/src/AngleSharp.Css.Tests/Styling/RelativeValueResolutionTests.cs @@ -0,0 +1,93 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Styling +{ + using AngleSharp.Css.Dom; + using AngleSharp.Dom; + using AngleSharp.Html.Parser; + using NUnit.Framework; + + /// + /// Complements with the edge cases of resolving + /// relative values during the style computation. + /// + [TestFixture] + public class RelativeValueResolutionTests + { + private static string Computed(string html, string property) + { + var config = Configuration.Default + .WithCss() + .WithRenderDevice(new DefaultRenderDevice { ViewPortWidth = 400, ViewPortHeight = 300, FontSize = 16 }); + var document = BrowsingContext.New(config).GetService().ParseDocument(html); + return document.GetElementById("target").ComputeCurrentStyle().GetPropertyValue(property); + } + + private static string ComputedInParent(string declarations, string property) => + Computed($"

    x

    ", property); + + [TestCase("width: calc(50% + 10px)", "width", "calc(50% + 10px)")] + [TestCase("width: calc((10% + 5px) * 2)", "width", "calc((10% + 5px) * 2)")] + [TestCase("width: calc(1em + 10px)", "width", "30px")] + [TestCase("width: min(50%, 100px)", "width", "min(50%, 100px)")] + public void UnresolvableCalcKeepsItsExpression(string declarations, string property, string expected) + { + Assert.AreEqual(expected, ComputedInParent(declarations, property)); + } + + [TestCase("width: 2ch", "width", "20px")] + [TestCase("width: 2ex", "width", "20px")] + [TestCase("width: 10vw", "width", "40px")] + [TestCase("height: 10svh", "height", "30px")] + public void FontAndViewportRelativeUnitsCompute(string declarations, string property, string expected) + { + Assert.AreEqual(expected, ComputedInParent(declarations, property)); + } + + [TestCase("font-size: 2em; margin-top: 1em", "margin-top", "40px")] + [TestCase("font-size: 2rem", "font-size", "32px")] + [TestCase("font-size: inherit; width: 1em", "width", "20px")] + [TestCase("font-size: var(--size); --size: 15px; width: 2em", "width", "30px")] + public void EmUsesTheComputedFontSizeOfTheElement(string declarations, string property, string expected) + { + Assert.AreEqual(expected, ComputedInParent(declarations, property)); + } + + [TestCase("font: large serif", "font-size", "19.2px")] + [TestCase("font: larger serif", "font-size", "24px")] + [TestCase("font-size: xx-large", "font-size", "32px")] + public void FontSizeKeywordsComputeToLengths(string declarations, string property, string expected) + { + Assert.AreEqual(expected, ComputedInParent(declarations, property)); + } + + [TestCase("line-height: 1.5", "1.5")] + [TestCase("line-height: normal", "normal")] + public void UnitlessLineHeightIsKept(string declarations, string expected) + { + Assert.AreEqual(expected, ComputedInParent(declarations, "line-height")); + } + + [Test] + public void RemInsideNestedElementsUsesTheRootFontSize() + { + var html = "

    x

    "; + + Assert.AreEqual("30px", Computed(html, "width")); + } + + [Test] + public void CurrentColorOnTheRootIsTheInitialColor() + { + Assert.AreEqual("rgba(0, 0, 0, 1)", Computed("", "color")); + } + + [TestCase("border-radius: 50%", "border-radius", "50%")] + [TestCase("grid: 1fr / auto-flow dense", "grid", "1fr / auto-flow dense")] + [TestCase("opacity: 25%", "opacity", "0.25")] + [TestCase("font-style: oblique 0.5turn", "font-style", "oblique 180deg")] + public void ComputedValuesSerialize(string declarations, string property, string expected) + { + Assert.AreEqual(expected, ComputedInParent(declarations, property)); + } + } +} diff --git a/src/AngleSharp.Css.Tests/Styling/ShadowWithoutBlurComputedStyleTests.cs b/src/AngleSharp.Css.Tests/Styling/ShadowWithoutBlurComputedStyleTests.cs new file mode 100644 index 00000000..c5a36158 --- /dev/null +++ b/src/AngleSharp.Css.Tests/Styling/ShadowWithoutBlurComputedStyleTests.cs @@ -0,0 +1,49 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Styling +{ + using AngleSharp.Css.Dom; + using AngleSharp.Dom; + using AngleSharp.Html.Parser; + using NUnit.Framework; + + /// + /// A `box-shadow` or `text-shadow` layer without a blur radius (`2px 2px red`, the most common + /// short form) makes `ComputeCurrentStyle` throw a `NullReferenceException`, so a downstream + /// renderer (AngleSharp.Renderer) cannot render the document at all. Layers with a blur radius + /// compute fine. + /// + [TestFixture] + public class ShadowWithoutBlurComputedStyleTests + { + private static ICssStyleDeclaration ComputedStyle(string html) + { + var config = Configuration.Default + .WithCss() + .WithRenderDevice(new DefaultRenderDevice { ViewPortWidth = 400, ViewPortHeight = 300, FontSize = 16 }); + var document = BrowsingContext.New(config).GetService().ParseDocument(html); + return document.GetElementById("target").ComputeCurrentStyle(); + } + + [TestCase("box-shadow", "2px 2px red")] + [TestCase("box-shadow", "2px 2px red, 3px 3px blue")] + [TestCase("box-shadow", "inset 0 0 5px red, 2px 2px red")] + [TestCase("text-shadow", "1px 1px red")] + [TestCase("text-shadow", "1px 1px red, 0 0 2px blue")] + public void ShadowWithoutBlurRadiusComputes(string property, string value) + { + string computed = null; + + Assert.DoesNotThrow(() => computed = ComputedStyle($"
    x
    ").GetPropertyValue(property)); + Assert.That(computed, Does.Contain("rgba(255, 0, 0, 1)")); + } + + [Test] + public void ShadowWithoutBlurRadiusDoesNotBreakOtherProperties() + { + string computed = null; + + Assert.DoesNotThrow(() => computed = ComputedStyle("
    x
    ").GetPropertyValue("width")); + Assert.AreEqual("10px", computed); + } + } +} diff --git a/src/AngleSharp.Css.Tests/Values/Point2DComputation.cs b/src/AngleSharp.Css.Tests/Values/Point2DComputation.cs index 20aff064..e7ae7cdb 100644 --- a/src/AngleSharp.Css.Tests/Values/Point2DComputation.cs +++ b/src/AngleSharp.Css.Tests/Values/Point2DComputation.cs @@ -35,24 +35,16 @@ public void ComputedYCoordinateTracksItsOwnValueNotX() // A radial-gradient's `at ` is a CssPoint2D. Before the fix, Y always came // out equal to X's own computed value regardless of what Y itself was authored as - // 20% 80% computed to "200px 200px" (both from X's 20%), not "200px 800px". + // Percentages of a gradient position refer to the gradient box, which is only known + // during layout, so they remain percentages in the computed value (they are no longer + // resolved against the viewport's width, which also made Y ignore the height). var document = ParseWithRenderDevice( "
    ", 1000, 1000); var target = document.GetElementById("target"); var computed = target.ComputeCurrentStyle().GetPropertyValue("background-image"); - StringAssert.Contains("200px 800px", computed); + StringAssert.Contains("at 20% 80%", computed); } - - // A separate, still-open, narrower issue surfaced alongside this fix: Y always resolves - // its own percentage/length against the viewport's *width*, never its height (confirmed: - // "20% 80%" resolves to "200px 800px" regardless of viewport height, i.e. 80% of the 1000px - // *width* - matching a coincidence in this test's own numbers, not genuine height-tracking). - // That is a materially different, deeper problem (percentage resolution mode not threaded - // per-axis through a point's own Compute() at all) than the X-value-copied-into-Y typo this - // test pins down, and is not fixed here - a downstream renderer relying on correct Y-axis - // percentage resolution for a computed point still needs to read the pre-Compute() specified - // value instead (as AngleSharp.Renderer's own ResolveExplicitBackgroundImage now does for - // exactly this reason), not the computed one. } } diff --git a/src/AngleSharp.Css/Constants/CssKeywords.cs b/src/AngleSharp.Css/Constants/CssKeywords.cs index aa31d40b..666dc22d 100644 --- a/src/AngleSharp.Css/Constants/CssKeywords.cs +++ b/src/AngleSharp.Css/Constants/CssKeywords.cs @@ -2191,5 +2191,192 @@ public static class CssKeywords /// The active keyword for the forced-colors media feature. /// public static readonly String Active = "active"; + + + /// + /// The space-evenly keyword. + /// + public static readonly String SpaceEvenly = "space-evenly"; + + /// + /// The safe keyword. + /// + public static readonly String Safe = "safe"; + + /// + /// The unsafe keyword. + /// + public static readonly String Unsafe = "unsafe"; + + /// + /// The first keyword. + /// + public static readonly String First = "first"; + + /// + /// The last keyword. + /// + public static readonly String Last = "last"; + + + /// + /// The break-spaces keyword. + /// + public static readonly String BreakSpaces = "break-spaces"; + + /// + /// The inline-start keyword. + /// + public static readonly String InlineStart = "inline-start"; + + /// + /// The inline-end keyword. + /// + public static readonly String InlineEnd = "inline-end"; + + /// + /// The ruby keyword. + /// + public static readonly String Ruby = "ruby"; + + /// + /// The flow keyword. + /// + public static readonly String Flow = "flow"; + + /// + /// The pixelated keyword. + /// + public static readonly String Pixelated = "pixelated"; + + /// + /// The plus-lighter keyword. + /// + public static readonly String PlusLighter = "plus-lighter"; + + + /// + /// The in keyword. + /// + public static readonly String In = "in"; + + + /// + /// The lining-nums keyword. + /// + public static readonly String LiningNums = "lining-nums"; + + /// + /// The oldstyle-nums keyword. + /// + public static readonly String OldstyleNums = "oldstyle-nums"; + + /// + /// The proportional-nums keyword. + /// + public static readonly String ProportionalNums = "proportional-nums"; + + /// + /// The tabular-nums keyword. + /// + public static readonly String TabularNums = "tabular-nums"; + + /// + /// The diagonal-fractions keyword. + /// + public static readonly String DiagonalFractions = "diagonal-fractions"; + + /// + /// The stacked-fractions keyword. + /// + public static readonly String StackedFractions = "stacked-fractions"; + + /// + /// The ordinal keyword. + /// + public static readonly String Ordinal = "ordinal"; + + /// + /// The slashed-zero keyword. + /// + public static readonly String SlashedZero = "slashed-zero"; + + /// + /// The common-ligatures keyword. + /// + public static readonly String CommonLigatures = "common-ligatures"; + + /// + /// The no-common-ligatures keyword. + /// + public static readonly String NoCommonLigatures = "no-common-ligatures"; + + /// + /// The discretionary-ligatures keyword. + /// + public static readonly String DiscretionaryLigatures = "discretionary-ligatures"; + + /// + /// The no-discretionary-ligatures keyword. + /// + public static readonly String NoDiscretionaryLigatures = "no-discretionary-ligatures"; + + /// + /// The historical-ligatures keyword. + /// + public static readonly String HistoricalLigatures = "historical-ligatures"; + + /// + /// The no-historical-ligatures keyword. + /// + public static readonly String NoHistoricalLigatures = "no-historical-ligatures"; + + /// + /// The contextual keyword. + /// + public static readonly String Contextual = "contextual"; + + /// + /// The no-contextual keyword. + /// + public static readonly String NoContextual = "no-contextual"; + + /// + /// The allow-discrete keyword. + /// + public static readonly String AllowDiscrete = "allow-discrete"; + + + /// + /// The jump-start keyword. + /// + public static readonly String JumpStart = "jump-start"; + + /// + /// The jump-end keyword. + /// + public static readonly String JumpEnd = "jump-end"; + + /// + /// The jump-none keyword. + /// + public static readonly String JumpNone = "jump-none"; + + /// + /// The jump-both keyword. + /// + public static readonly String JumpBoth = "jump-both"; + + + /// + /// The subgrid keyword. + /// + public static readonly String Subgrid = "subgrid"; + + + /// + /// The each-line keyword. + /// + public static readonly String EachLine = "each-line"; } } diff --git a/src/AngleSharp.Css/Constants/FunctionNames.cs b/src/AngleSharp.Css/Constants/FunctionNames.cs index 207fd2df..07324702 100644 --- a/src/AngleSharp.Css/Constants/FunctionNames.cs +++ b/src/AngleSharp.Css/Constants/FunctionNames.cs @@ -77,6 +77,21 @@ public static class FunctionNames /// public static readonly String Minmax = "minmax"; + /// + /// The min function. + /// + public static readonly String Min = "min"; + + /// + /// The max function. + /// + public static readonly String Max = "max"; + + /// + /// The clamp function. + /// + public static readonly String Clamp = "clamp"; + /// /// The var function. /// @@ -287,6 +302,26 @@ public static class FunctionNames /// public static readonly String Oklch = "oklch"; + /// + /// The light-dark function (CSS Color 5). + /// + public static readonly String LightDark = "light-dark"; + + /// + /// The color-mix function (CSS Color 5). + /// + public static readonly String ColorMix = "color-mix"; + + /// + /// The image-set function (CSS Images 4). + /// + public static readonly String ImageSet = "image-set"; + + /// + /// The type function of an image-set option. + /// + public static readonly String Type = "type"; + /// /// The content function. /// diff --git a/src/AngleSharp.Css/Constants/InitialValues.cs b/src/AngleSharp.Css/Constants/InitialValues.cs index 5f6247c6..d7349c8d 100644 --- a/src/AngleSharp.Css/Constants/InitialValues.cs +++ b/src/AngleSharp.Css/Constants/InitialValues.cs @@ -220,6 +220,10 @@ static class InitialValues public static readonly ICssValue PositionDecl = new CssConstantValue(CssKeywords.Inline, PositionMode.Static); public static readonly ICssValue TransformDecl = new CssConstantValue(CssKeywords.None, null); public static readonly ICssValue TransformStyleDecl = new CssConstantValue(CssKeywords.Flat, true); + public static readonly ICssValue TransformBoxDecl = new CssConstantValue(CssKeywords.ViewBox, CssKeywords.ViewBox); + public static readonly ICssValue TransitionBehaviorDecl = new CssConstantValue(CssKeywords.Normal, CssKeywords.Normal); + public static readonly ICssValue FontVariantNumericDecl = new CssConstantValue(CssKeywords.Normal, CssKeywords.Normal); + public static readonly ICssValue FontVariantLigaturesDecl = new CssConstantValue(CssKeywords.Normal, CssKeywords.Normal); public static readonly ICssValue TransformOriginDecl = CssPoint2D.Center; public static readonly ICssValue RotateDecl = new CssConstantValue(CssKeywords.None, null); public static readonly ICssValue ScaleDecl = new CssConstantValue(CssKeywords.None, null); diff --git a/src/AngleSharp.Css/Constants/Map.cs b/src/AngleSharp.Css/Constants/Map.cs index 06af8866..23042066 100644 --- a/src/AngleSharp.Css/Constants/Map.cs +++ b/src/AngleSharp.Css/Constants/Map.cs @@ -21,6 +21,7 @@ static class Map { CssKeywords.Nowrap, Whitespace.NoWrap }, { CssKeywords.PreWrap, Whitespace.PreWrap }, { CssKeywords.PreLine, Whitespace.PreLine }, + { CssKeywords.BreakSpaces, Whitespace.BreakSpaces }, }; /// @@ -351,6 +352,7 @@ static class Map { CssKeywords.Stretch, BorderRepeat.Stretch }, { CssKeywords.Repeat, BorderRepeat.Repeat }, { CssKeywords.Round, BorderRepeat.Round }, + { CssKeywords.Space, BorderRepeat.Space }, }; /// @@ -580,6 +582,8 @@ static class Map { CssKeywords.Left, Floating.Left }, { CssKeywords.Right, Floating.Right }, { CssKeywords.Footnote, Floating.Footnote }, + { CssKeywords.InlineStart, Floating.InlineStart }, + { CssKeywords.InlineEnd, Floating.InlineEnd }, }; /// @@ -607,6 +611,8 @@ static class Map { CssKeywords.Grid, DisplayMode.Grid }, { CssKeywords.InlineGrid, DisplayMode.InlineGrid }, { CssKeywords.FlowRoot, DisplayMode.FlowRoot }, + { CssKeywords.Contents, DisplayMode.Contents }, + { CssKeywords.Ruby, DisplayMode.Ruby }, }; /// @@ -618,6 +624,8 @@ static class Map { CssKeywords.Left, ClearMode.Left }, { CssKeywords.Right, ClearMode.Right }, { CssKeywords.Both, ClearMode.Both }, + { CssKeywords.InlineStart, ClearMode.InlineStart }, + { CssKeywords.InlineEnd, ClearMode.InlineEnd }, }; /// @@ -778,6 +786,7 @@ static class Map { { CssKeywords.Normal, OverflowWrap.Normal }, { CssKeywords.BreakWord, OverflowWrap.BreakWord }, + { CssKeywords.Anywhere, OverflowWrap.Anywhere }, }; /// @@ -882,6 +891,13 @@ static class Map { CssKeywords.Center, FlexContentMode.Center }, { CssKeywords.SpaceBetween, FlexContentMode.SpaceBetween }, { CssKeywords.SpaceAround, FlexContentMode.SpaceAround }, + { CssKeywords.SpaceEvenly, FlexContentMode.SpaceEvenly }, + { CssKeywords.Stretch, FlexContentMode.Stretch }, + { CssKeywords.Normal, FlexContentMode.Stretch }, + { CssKeywords.Start, FlexContentMode.Start }, + { CssKeywords.End, FlexContentMode.End }, + { CssKeywords.Left, FlexContentMode.Start }, + { CssKeywords.Right, FlexContentMode.End }, }; /// @@ -895,6 +911,11 @@ static class Map { CssKeywords.SpaceBetween, FlexContentMode.SpaceBetween }, { CssKeywords.SpaceAround, FlexContentMode.SpaceAround }, { CssKeywords.Stretch, FlexContentMode.Stretch }, + { CssKeywords.SpaceEvenly, FlexContentMode.SpaceEvenly }, + { CssKeywords.Normal, FlexContentMode.Stretch }, + { CssKeywords.Baseline, FlexContentMode.Baseline }, + { CssKeywords.Start, FlexContentMode.Start }, + { CssKeywords.End, FlexContentMode.End }, }; /// @@ -907,6 +928,11 @@ static class Map { CssKeywords.Center, FlexContentMode.Center }, { CssKeywords.Baseline, FlexContentMode.Baseline }, { CssKeywords.Stretch, FlexContentMode.Stretch }, + { CssKeywords.Normal, FlexContentMode.Stretch }, + { CssKeywords.Start, FlexContentMode.Start }, + { CssKeywords.End, FlexContentMode.End }, + { CssKeywords.SelfStart, FlexContentMode.Start }, + { CssKeywords.SelfEnd, FlexContentMode.End }, }; /// @@ -920,6 +946,11 @@ static class Map { CssKeywords.Center, FlexContentMode.Center }, { CssKeywords.Baseline, FlexContentMode.Baseline }, { CssKeywords.Stretch, FlexContentMode.Stretch }, + { CssKeywords.Normal, FlexContentMode.Stretch }, + { CssKeywords.Start, FlexContentMode.Start }, + { CssKeywords.End, FlexContentMode.End }, + { CssKeywords.SelfStart, FlexContentMode.Start }, + { CssKeywords.SelfEnd, FlexContentMode.End }, }; /// @@ -932,6 +963,14 @@ static class Map { CssKeywords.Center, FlexContentMode.Center }, { CssKeywords.Baseline, FlexContentMode.Baseline }, { CssKeywords.Stretch, FlexContentMode.Stretch }, + { CssKeywords.Normal, FlexContentMode.Stretch }, + { CssKeywords.Legacy, FlexContentMode.Auto }, + { CssKeywords.Start, FlexContentMode.Start }, + { CssKeywords.End, FlexContentMode.End }, + { CssKeywords.Left, FlexContentMode.Start }, + { CssKeywords.Right, FlexContentMode.End }, + { CssKeywords.SelfStart, FlexContentMode.Start }, + { CssKeywords.SelfEnd, FlexContentMode.End }, }; /// @@ -945,6 +984,13 @@ static class Map { CssKeywords.Center, FlexContentMode.Center }, { CssKeywords.Baseline, FlexContentMode.Baseline }, { CssKeywords.Stretch, FlexContentMode.Stretch }, + { CssKeywords.Normal, FlexContentMode.Stretch }, + { CssKeywords.Start, FlexContentMode.Start }, + { CssKeywords.End, FlexContentMode.End }, + { CssKeywords.Left, FlexContentMode.Start }, + { CssKeywords.Right, FlexContentMode.End }, + { CssKeywords.SelfStart, FlexContentMode.Start }, + { CssKeywords.SelfEnd, FlexContentMode.End }, }; /// @@ -984,6 +1030,7 @@ static class Map { CssKeywords.FitContent, Sizing.FitContent }, { CssKeywords.MinContent, Sizing.MinContent}, { CssKeywords.MaxContent, Sizing.MaxContent }, + { CssKeywords.Stretch, Sizing.Stretch }, }; /// diff --git a/src/AngleSharp.Css/Constants/PropertyNames.cs b/src/AngleSharp.Css/Constants/PropertyNames.cs index 241ceabb..beccccce 100644 --- a/src/AngleSharp.Css/Constants/PropertyNames.cs +++ b/src/AngleSharp.Css/Constants/PropertyNames.cs @@ -892,6 +892,16 @@ public static class PropertyNames /// public static readonly String FontVariantCaps = "font-variant-caps"; + /// + /// The font-variant-numeric declaration. + /// + public static readonly String FontVariantNumeric = "font-variant-numeric"; + + /// + /// The font-variant-ligatures declaration. + /// + public static readonly String FontVariantLigatures = "font-variant-ligatures"; + /// /// The font-weight declaration. /// @@ -2092,6 +2102,11 @@ public static class PropertyNames /// public static readonly String TransformStyle = "transform-style"; + /// + /// The transform-box declaration. + /// + public static readonly String TransformBox = "transform-box"; + /// /// The translate declaration. /// @@ -2117,6 +2132,11 @@ public static class PropertyNames /// public static readonly String TransitionTimingFunction = "transition-timing-function"; + /// + /// The transition-behavior declaration. + /// + public static readonly String TransitionBehavior = "transition-behavior"; + /// /// The transition-property declaration. /// diff --git a/src/AngleSharp.Css/Constants/UnitNames.cs b/src/AngleSharp.Css/Constants/UnitNames.cs index abe35d33..2d28f31e 100644 --- a/src/AngleSharp.Css/Constants/UnitNames.cs +++ b/src/AngleSharp.Css/Constants/UnitNames.cs @@ -102,6 +102,66 @@ public static class UnitNames /// public static readonly String Vmax = "vmax"; + /// + /// The svw unit (relative to the small viewport width). + /// + public static readonly String Svw = "svw"; + + /// + /// The svh unit (relative to the small viewport height). + /// + public static readonly String Svh = "svh"; + + /// + /// The svmin unit (relative to the small minimum viewport dimension). + /// + public static readonly String Svmin = "svmin"; + + /// + /// The svmax unit (relative to the small maximum viewport dimension). + /// + public static readonly String Svmax = "svmax"; + + /// + /// The lvw unit (relative to the large viewport width). + /// + public static readonly String Lvw = "lvw"; + + /// + /// The lvh unit (relative to the large viewport height). + /// + public static readonly String Lvh = "lvh"; + + /// + /// The lvmin unit (relative to the large minimum viewport dimension). + /// + public static readonly String Lvmin = "lvmin"; + + /// + /// The lvmax unit (relative to the large maximum viewport dimension). + /// + public static readonly String Lvmax = "lvmax"; + + /// + /// The dvw unit (relative to the dynamic viewport width). + /// + public static readonly String Dvw = "dvw"; + + /// + /// The dvh unit (relative to the dynamic viewport height). + /// + public static readonly String Dvh = "dvh"; + + /// + /// The dvmin unit (relative to the dynamic minimum viewport dimension). + /// + public static readonly String Dvmin = "dvmin"; + + /// + /// The dvmax unit (relative to the dynamic maximum viewport dimension). + /// + public static readonly String Dvmax = "dvmax"; + /// /// Dots per inch. /// @@ -117,6 +177,11 @@ public static class UnitNames /// public static readonly String Dppx = "dppx"; + /// + /// Dots per pixel, given by its alias x. + /// + public static readonly String X = "x"; + /// /// Points. /// diff --git a/src/AngleSharp.Css/Converters/AlphaValueConverter.cs b/src/AngleSharp.Css/Converters/AlphaValueConverter.cs new file mode 100644 index 00000000..4ab59402 --- /dev/null +++ b/src/AngleSharp.Css/Converters/AlphaValueConverter.cs @@ -0,0 +1,53 @@ +#nullable enable +namespace AngleSharp.Css.Converters +{ + using AngleSharp.Css.Dom; + using AngleSharp.Css.Parser; + using AngleSharp.Css.Values; + using AngleSharp.Text; + using System; + + /// + /// Converts an <alpha-value>, i.e., a number or a percentage. A percentage keeps its + /// specified form, but computes to the equivalent number (e.g., 50% to 0.5). + /// + sealed class AlphaValueConverter : IValueConverter + { + private readonly IValueConverter _number; + + public AlphaValueConverter(IValueConverter number) + { + _number = number; + } + + public ICssValue? Convert(StringSource source) + { + var pos = source.Index; + var percent = source.ParsePercent(); + + if (percent.HasValue) + { + return new AlphaPercentValue(percent.Value); + } + + source.BackTo(pos); + return _number.Convert(source); + } + + sealed class AlphaPercentValue : ICssValue + { + private readonly Double _value; + + public AlphaPercentValue(Double value) + { + _value = value; + } + + public String CssText => new CssPercentageValue(_value * 100.0).CssText; + + public ICssValue Compute(ICssComputeContext context) => new CssNumberValue(_value); + + public Boolean Equals(ICssValue? other) => other is AlphaPercentValue value && value._value == _value; + } + } +} diff --git a/src/AngleSharp.Css/Converters/BoxAlignmentValueConverter.cs b/src/AngleSharp.Css/Converters/BoxAlignmentValueConverter.cs new file mode 100644 index 00000000..c366d60b --- /dev/null +++ b/src/AngleSharp.Css/Converters/BoxAlignmentValueConverter.cs @@ -0,0 +1,133 @@ +#nullable enable +namespace AngleSharp.Css.Converters +{ + using AngleSharp.Css.Dom; + using AngleSharp.Css.Parser; + using AngleSharp.Css.Values; + using AngleSharp.Text; + using System; + using System.Collections.Generic; + + /// + /// Converts the keywords of the CSS Box Alignment properties, including the + /// multi-keyword forms `[ safe | unsafe ] <position>`, + /// `[ first | last ] baseline`, and `legacy [ left | right | center ]`. + /// + sealed class BoxAlignmentValueConverter : IValueConverter + { + private static readonly HashSet PositionKeywords = new(StringComparer.OrdinalIgnoreCase) + { + CssKeywords.Center, + CssKeywords.Start, + CssKeywords.End, + CssKeywords.SelfStart, + CssKeywords.SelfEnd, + CssKeywords.FlexStart, + CssKeywords.FlexEnd, + CssKeywords.Left, + CssKeywords.Right, + }; + + private static readonly HashSet LegacyKeywords = new(StringComparer.OrdinalIgnoreCase) + { + CssKeywords.Left, + CssKeywords.Right, + CssKeywords.Center, + }; + + private readonly IDictionary _values; + + public BoxAlignmentValueConverter(IDictionary values) + { + _values = values; + } + + public ICssValue? Convert(StringSource source) + { + var start = source.Index; + var first = source.ParseIdent(); + + if (first is not null) + { + var keyword = first.ToLowerFast(); + + if (keyword.Is(CssKeywords.First) || keyword.Is(CssKeywords.Last)) + { + if (_values.TryGetValue(CssKeywords.Baseline, out var baseline) && TryParseKeyword(source, CssKeywords.Baseline)) + { + // first baseline serializes as baseline + var text = keyword.Is(CssKeywords.First) ? CssKeywords.Baseline : String.Concat(keyword, " ", CssKeywords.Baseline); + return new CssConstantValue(text, baseline); + } + } + else if (keyword.Is(CssKeywords.Safe) || keyword.Is(CssKeywords.Unsafe)) + { + var pos = source.Index; + source.SkipSpacesAndComments(); + var position = source.ParseIdent(); + + if (position is not null && PositionKeywords.Contains(position) && _values.TryGetValue(position, out var mode)) + { + return new CssConstantValue(String.Concat(keyword, " ", position.ToLowerFast()), mode); + } + + source.BackTo(pos); + } + else if (_values.TryGetValue(keyword, out var mode)) + { + if (_values.ContainsKey(CssKeywords.Legacy)) + { + var legacy = TryParseLegacy(source, keyword); + + if (legacy is not null) + { + return new CssConstantValue(legacy, mode); + } + } + + return new CssConstantValue(keyword, mode); + } + } + + source.BackTo(start); + return null; + } + + private static String? TryParseLegacy(StringSource source, String keyword) + { + var pos = source.Index; + source.SkipSpacesAndComments(); + var other = source.ParseIdent(); + + if (other is not null) + { + if (keyword.Is(CssKeywords.Legacy) && LegacyKeywords.Contains(other)) + { + return String.Concat(CssKeywords.Legacy, " ", other.ToLowerFast()); + } + else if (other.Isi(CssKeywords.Legacy) && LegacyKeywords.Contains(keyword)) + { + return String.Concat(CssKeywords.Legacy, " ", keyword); + } + } + + source.BackTo(pos); + return null; + } + + private static Boolean TryParseKeyword(StringSource source, String keyword) + { + var pos = source.Index; + source.SkipSpacesAndComments(); + var ident = source.ParseIdent(); + + if (ident is not null && ident.Isi(keyword)) + { + return true; + } + + source.BackTo(pos); + return false; + } + } +} diff --git a/src/AngleSharp.Css/Converters/DisplayValueConverter.cs b/src/AngleSharp.Css/Converters/DisplayValueConverter.cs new file mode 100644 index 00000000..089ee2f7 --- /dev/null +++ b/src/AngleSharp.Css/Converters/DisplayValueConverter.cs @@ -0,0 +1,141 @@ +#nullable enable +namespace AngleSharp.Css.Converters +{ + using AngleSharp.Css.Dom; + using AngleSharp.Css.Parser; + using AngleSharp.Css.Values; + using AngleSharp.Text; + using System; + + /// + /// Converts the display property, including the multi-keyword syntax + /// `[ <display-outside> || <display-inside> ] | <display-listitem>` + /// of CSS Display 3. Multi-keyword values serialize in their shortest form, + /// e.g., `inline flex` as `inline-flex` and `block flow-root` as `flow-root`. + /// + sealed class DisplayValueConverter : IValueConverter + { + private readonly IValueConverter _keywords = Map.DisplayModes.ToConverter(); + + public ICssValue? Convert(StringSource source) + { + var start = source.Index; + var outer = default(String); + var inner = default(String); + var listItem = false; + var count = 0; + + while (count < 3) + { + var pos = source.Index; + + if (count > 0) + { + source.SkipSpacesAndComments(); + } + + var ident = source.ParseIdent()?.ToLowerFast(); + + if (ident is null) + { + source.BackTo(pos); + break; + } + else if (outer is null && (ident.Is(CssKeywords.Block) || ident.Is(CssKeywords.Inline))) + { + outer = ident; + } + else if (inner is null && IsInner(ident)) + { + inner = ident; + } + else if (!listItem && ident.Is(CssKeywords.ListItem)) + { + listItem = true; + } + else + { + source.BackTo(pos); + break; + } + + count++; + } + + if (count > 1 || (count == 1 && inner is not null)) + { + var value = Normalize(outer, inner, listItem); + + if (value is not null) + { + return value; + } + } + + source.BackTo(start); + return _keywords.Convert(source); + } + + private static Boolean IsInner(String ident) => + ident.Is(CssKeywords.Flow) || ident.Is(CssKeywords.FlowRoot) || ident.Is(CssKeywords.Table) || + ident.Is(CssKeywords.Flex) || ident.Is(CssKeywords.Grid) || ident.Is(CssKeywords.Ruby); + + private static ICssValue? Normalize(String? outer, String? inner, Boolean listItem) + { + if (listItem) + { + if (inner is not null && !inner.Is(CssKeywords.Flow) && !inner.Is(CssKeywords.FlowRoot)) + { + return null; + } + + var text = CssKeywords.ListItem; + + if (inner is not null && inner.Is(CssKeywords.FlowRoot)) + { + text = String.Concat(inner, " ", text); + } + + if (outer is not null && outer.Is(CssKeywords.Inline)) + { + text = String.Concat(outer, " ", text); + } + + return new CssConstantValue(text, DisplayMode.ListItem); + } + + // A ruby container is inline by default, every other one is block-level. + var isInline = outer is not null ? outer.Is(CssKeywords.Inline) : inner is not null && inner.Is(CssKeywords.Ruby); + inner ??= CssKeywords.Flow; + + if (inner.Is(CssKeywords.Flow)) + { + return isInline ? Create(CssKeywords.Inline, DisplayMode.Inline) : Create(CssKeywords.Block, DisplayMode.Block); + } + else if (inner.Is(CssKeywords.FlowRoot)) + { + return isInline ? Create(CssKeywords.InlineBlock, DisplayMode.InlineBlock) : Create(CssKeywords.FlowRoot, DisplayMode.FlowRoot); + } + else if (inner.Is(CssKeywords.Table)) + { + return isInline ? Create(CssKeywords.InlineTable, DisplayMode.InlineTable) : Create(CssKeywords.Table, DisplayMode.Table); + } + else if (inner.Is(CssKeywords.Flex)) + { + return isInline ? Create(CssKeywords.InlineFlex, DisplayMode.InlineFlex) : Create(CssKeywords.Flex, DisplayMode.Flex); + } + else if (inner.Is(CssKeywords.Grid)) + { + return isInline ? Create(CssKeywords.InlineGrid, DisplayMode.InlineGrid) : Create(CssKeywords.Grid, DisplayMode.Grid); + } + else if (inner.Is(CssKeywords.Ruby)) + { + return isInline ? Create(CssKeywords.Ruby, DisplayMode.Ruby) : Create(String.Concat(CssKeywords.Block, " ", CssKeywords.Ruby), DisplayMode.Ruby); + } + + return null; + } + + private static ICssValue Create(String keyword, DisplayMode mode) => new CssConstantValue(keyword, mode); + } +} diff --git a/src/AngleSharp.Css/Converters/FontStyleValueConverter.cs b/src/AngleSharp.Css/Converters/FontStyleValueConverter.cs new file mode 100644 index 00000000..6ec05fc6 --- /dev/null +++ b/src/AngleSharp.Css/Converters/FontStyleValueConverter.cs @@ -0,0 +1,57 @@ +#nullable enable +namespace AngleSharp.Css.Converters +{ + using AngleSharp.Css.Dom; + using AngleSharp.Css.Parser; + using AngleSharp.Css.Values; + using AngleSharp.Text; + using System; + + /// + /// Converts the font-style property, including `oblique <angle>` (CSS Fonts 4). + /// + sealed class FontStyleValueConverter : IValueConverter + { + private readonly IValueConverter _keywords = Map.FontStyles.ToConverter(); + + public ICssValue? Convert(StringSource source) + { + var pos = source.Index; + + if (source.IsIdentifier(CssKeywords.Oblique)) + { + var keyword = source.Index; + source.SkipSpacesAndComments(); + var angle = source.ParseAngle(); + + if (angle.HasValue) + { + return new ObliqueValue(angle.Value); + } + + source.BackTo(keyword); + } + + source.BackTo(pos); + return _keywords.Convert(source); + } + + sealed class ObliqueValue : ICssValue + { + private readonly CssAngleValue _angle; + + public ObliqueValue(CssAngleValue angle) + { + _angle = angle; + } + + public String CssText => String.Concat(CssKeywords.Oblique, " ", _angle.CssText); + + // The computed angle is given in degrees. + public ICssValue Compute(ICssComputeContext context) => + new ObliqueValue(new CssAngleValue(_angle.ToDegree(), CssAngleValue.Unit.Deg)); + + public Boolean Equals(ICssValue? other) => other is ObliqueValue value && value._angle.Equals(_angle); + } + } +} diff --git a/src/AngleSharp.Css/CssDefaultStyleSheetProvider.cs b/src/AngleSharp.Css/CssDefaultStyleSheetProvider.cs index 84a69717..cf7e3179 100644 --- a/src/AngleSharp.Css/CssDefaultStyleSheetProvider.cs +++ b/src/AngleSharp.Css/CssDefaultStyleSheetProvider.cs @@ -58,9 +58,24 @@ private static ICssStyleSheet Parse(String source) figure, footer, header, hgroup, legend, main, nav, -search, section, -summary { display: block; unicode-bidi: embed } +search, section { display: block } +address, +blockquote, +center, div, +figure, figcaption, +footer, form, +header, hr, +legend, listing, +main, p, +plaintext, pre, +search, xmp, +bdi, output, +[dir=ltr i], +[dir=rtl i], +[dir=auto i] { unicode-bidi: isolate } li { display: list-item } +summary { display: list-item; list-style-type: disclosure-closed; list-style-position: inside } +details[open] > summary { list-style-type: disclosure-open } head, area, base, basefont, datalist, link, meta, noembed, @@ -75,7 +90,7 @@ private static ICssStyleSheet Parse(String source) tfoot { display: table-footer-group } col { display: table-column } colgroup { display: table-column-group } -td, th { display: table-cell } +td, th { display: table-cell; padding: 1px } caption { display: table-caption } th { font-weight: bolder; text-align: center } caption { text-align: center } @@ -83,17 +98,21 @@ private static ICssStyleSheet Parse(String source) h1 { font-size: 2em; margin: .67em 0 } h2 { font-size: 1.5em; margin: .75em 0 } h3 { font-size: 1.17em; margin: .83em 0 } -h4, p, -blockquote, ul, -fieldset, form, -ol, dl, dir, -menu { margin: 1.12em 0 } +h4, form { margin: 1.12em 0 } +blockquote, +figure, listing, +p, plaintext, +pre, xmp, +dir, dl, menu, +ol, ul { margin: 1em 0 } h5 { font-size: .83em; margin: 1.5em 0 } h6 { font-size: .75em; margin: 1.67em 0 } h1, h2, h3, h4, h5, h6, b, strong { font-weight: bolder } -blockquote { margin-left: 40px; margin-right: 40px } +blockquote, +figure { margin-left: 40px; margin-right: 40px } +fieldset { margin-left: 2px; margin-right: 2px; border: 2px groove rgb(192, 192, 192); padding: 0.35em 0.75em 0.625em } i, cite, em, var, address { font-style: italic } pre, tt, code, @@ -129,16 +148,22 @@ private static ICssStyleSheet Parse(String source) tfoot { vertical-align: middle } td, th, tr { vertical-align: inherit } s, strike, del { text-decoration: line-through } -hr { border: 1px inset } +hr { border: 1px inset; margin: 0.5em auto } ol, ul, dir, -menu, dd { margin-left: 40px; list-style-type: disc; list-style-position: outside } +menu { padding-left: 40px; list-style-type: disc; list-style-position: outside } +dd { margin-left: 40px } ol { list-style-type: decimal } ol ul, ul ol, ul ul, ol ol { margin-top: 0; margin-bottom: 0 } u, ins { text-decoration: underline } +abbr[title], +acronym[title] { text-decoration: dotted underline } +mark { background-color: yellow; color: black } br::before { content: '\A'; white-space: pre-line } *::before, *::after { display: inline } center { text-align: center } +:link { color: #0000ee } +:visited { color: #551a8b } :link, :visited { text-decoration: underline } :focus { outline: thin dotted invert } @@ -146,8 +171,8 @@ private static ICssStyleSheet Parse(String source) BDO[DIR='ltr'] { direction: ltr; unicode-bidi: bidi-override } BDO[DIR='rtl'] { direction: rtl; unicode-bidi: bidi-override } -*[DIR='ltr'] { direction: ltr; unicode-bidi: embed } -*[DIR='rtl'] { direction: rtl; unicode-bidi: embed } +[dir=ltr i] { direction: ltr } +[dir=rtl i] { direction: rtl } @media print { h1 { page-break-before: always } diff --git a/src/AngleSharp.Css/Declarations/BackgroundClipDeclaration.cs b/src/AngleSharp.Css/Declarations/BackgroundClipDeclaration.cs index 6a818dc6..4bd90e0e 100644 --- a/src/AngleSharp.Css/Declarations/BackgroundClipDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/BackgroundClipDeclaration.cs @@ -14,7 +14,7 @@ static class BackgroundClipDeclaration PropertyNames.Background, }; - public static IValueConverter Converter = BoxModelConverter.FromList(); + public static IValueConverter Converter = BackgroundClipConverter.FromList(); public static ICssValue InitialValue = InitialValues.BackgroundClipDecl; diff --git a/src/AngleSharp.Css/Declarations/ColorDeclaration.cs b/src/AngleSharp.Css/Declarations/ColorDeclaration.cs index 6c39446d..d06a0ab9 100644 --- a/src/AngleSharp.Css/Declarations/ColorDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/ColorDeclaration.cs @@ -1,6 +1,9 @@ namespace AngleSharp.Css.Declarations { using AngleSharp.Css.Dom; + using AngleSharp.Css.Parser; + using AngleSharp.Css.Values; + using AngleSharp.Text; using System; using static ValueConverters; @@ -8,10 +11,30 @@ static class ColorDeclaration { public static String Name = PropertyNames.Color; - public static IValueConverter Converter = ColorConverter; + public static IValueConverter Converter = new ColorValueConverter(); public static ICssValue InitialValue = InitialValues.ColorDecl; public static PropertyFlags Flags = PropertyFlags.Inherited | PropertyFlags.Hashless | PropertyFlags.Animatable; + + sealed class ColorValueConverter : IValueConverter + { + public ICssValue? Convert(StringSource source) => + source.IsIdentifier(CssKeywords.CurrentColor) ? CurrentColorValue.Instance : ColorConverter.Convert(source); + } + + /// + /// The currentcolor keyword on the color property itself, which computes like inherit. + /// + sealed class CurrentColorValue : ICssValue + { + public static readonly CurrentColorValue Instance = new(); + + public String CssText => CssKeywords.CurrentColor; + + public ICssValue Compute(ICssComputeContext context) => CssInheritValue.Instance; + + public Boolean Equals(ICssValue? other) => other is CurrentColorValue; + } } } diff --git a/src/AngleSharp.Css/Declarations/ContentVisibilityDeclaration.cs b/src/AngleSharp.Css/Declarations/ContentVisibilityDeclaration.cs index 7a0cc42a..86d43c10 100644 --- a/src/AngleSharp.Css/Declarations/ContentVisibilityDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/ContentVisibilityDeclaration.cs @@ -8,7 +8,7 @@ static class ContentVisibilityDeclaration { public static String Name = PropertyNames.ContentVisibility; - public static IValueConverter Converter = VisibilityConverter; + public static IValueConverter Converter = ContentVisibilityConverter; public static ICssValue InitialValue = InitialValues.ContentVisibilityDecl; diff --git a/src/AngleSharp.Css/Declarations/FillOpacityDeclaration.cs b/src/AngleSharp.Css/Declarations/FillOpacityDeclaration.cs index 9090a325..06768e53 100644 --- a/src/AngleSharp.Css/Declarations/FillOpacityDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/FillOpacityDeclaration.cs @@ -8,7 +8,7 @@ static class FillOpacityDeclaration { public static String Name = PropertyNames.FillOpacity; - public static IValueConverter Converter = NumberConverter; + public static IValueConverter Converter = AlphaValueConverter; public static ICssValue InitialValue = InitialValues.FillOpacityDecl; diff --git a/src/AngleSharp.Css/Declarations/FlexBasisDeclaration.cs b/src/AngleSharp.Css/Declarations/FlexBasisDeclaration.cs index 91ebb57c..5fa59450 100644 --- a/src/AngleSharp.Css/Declarations/FlexBasisDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/FlexBasisDeclaration.cs @@ -13,7 +13,7 @@ static class FlexBasisDeclaration PropertyNames.Flex, }; - public static IValueConverter Converter = Or(Content, AutoLengthOrPercentConverter); + public static IValueConverter Converter = Or(Content, WidthConverter); public static ICssValue InitialValue = InitialValues.FlexBasisDecl; diff --git a/src/AngleSharp.Css/Declarations/FlexDeclaration.cs b/src/AngleSharp.Css/Declarations/FlexDeclaration.cs index 81599942..ce5760ad 100644 --- a/src/AngleSharp.Css/Declarations/FlexDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/FlexDeclaration.cs @@ -63,6 +63,16 @@ public ICssValue[] Split(ICssValue value) options.Items[2], }; } + else if (value != null && value.CssText.Isi(CssKeywords.None)) + { + // flex: none is equivalent to flex: 0 0 auto + return new[] + { + new CssLengthValue(0, CssLengthValue.Unit.None), + new CssLengthValue(0, CssLengthValue.Unit.None), + InitialValues.FlexBasisDecl, + }; + } return null; } diff --git a/src/AngleSharp.Css/Declarations/FontVariantLigaturesDeclaration.cs b/src/AngleSharp.Css/Declarations/FontVariantLigaturesDeclaration.cs new file mode 100644 index 00000000..4b3b3470 --- /dev/null +++ b/src/AngleSharp.Css/Declarations/FontVariantLigaturesDeclaration.cs @@ -0,0 +1,17 @@ +namespace AngleSharp.Css.Declarations +{ + using AngleSharp.Css.Dom; + using System; + using static ValueConverters; + + static class FontVariantLigaturesDeclaration + { + public static String Name = PropertyNames.FontVariantLigatures; + + public static IValueConverter Converter = FontVariantLigaturesConverter; + + public static ICssValue InitialValue = InitialValues.FontVariantLigaturesDecl; + + public static PropertyFlags Flags = PropertyFlags.Inherited; + } +} diff --git a/src/AngleSharp.Css/Declarations/FontVariantNumericDeclaration.cs b/src/AngleSharp.Css/Declarations/FontVariantNumericDeclaration.cs new file mode 100644 index 00000000..14ebc70d --- /dev/null +++ b/src/AngleSharp.Css/Declarations/FontVariantNumericDeclaration.cs @@ -0,0 +1,17 @@ +namespace AngleSharp.Css.Declarations +{ + using AngleSharp.Css.Dom; + using System; + using static ValueConverters; + + static class FontVariantNumericDeclaration + { + public static String Name = PropertyNames.FontVariantNumeric; + + public static IValueConverter Converter = FontVariantNumericConverter; + + public static ICssValue InitialValue = InitialValues.FontVariantNumericDecl; + + public static PropertyFlags Flags = PropertyFlags.Inherited; + } +} diff --git a/src/AngleSharp.Css/Declarations/FontWeightDeclaration.cs b/src/AngleSharp.Css/Declarations/FontWeightDeclaration.cs index d047027b..f112f264 100644 --- a/src/AngleSharp.Css/Declarations/FontWeightDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/FontWeightDeclaration.cs @@ -14,7 +14,7 @@ static class FontWeightDeclaration PropertyNames.Font, }; - public static IValueConverter Converter = Or(FontWeightConverter, WeightIntegerConverter.Many(1, 2)); + public static IValueConverter Converter = Or(FontWeightConverter, WeightIntegerConverter.Many(1, 2), WeightNumberConverter); public static ICssValue InitialValue = InitialValues.FontWeightDecl; diff --git a/src/AngleSharp.Css/Declarations/GridDeclaration.cs b/src/AngleSharp.Css/Declarations/GridDeclaration.cs index 2e4a2ac6..1b7ac65c 100644 --- a/src/AngleSharp.Css/Declarations/GridDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/GridDeclaration.cs @@ -135,9 +135,17 @@ public ICssValue Merge(ICssValue[] values) } else if (templateRows != null || templateColumns != null) { + var flow = autoFlow?.CssText ?? String.Empty; + var isDense = flow.Contains(CssKeywords.Dense); + var isAutoFlow = templateRows is null ? flow.Contains(CssKeywords.Row) : templateColumns is null && flow.Contains(CssKeywords.Column); + if ((autoRows ?? autoColumns) is CssTupleValue tuple) { - return new CssGridValue(templateRows, templateColumns, tuple.Items, autoFlow != null); + return new CssGridValue(templateRows, templateColumns, tuple.Items, isDense); + } + else if (isAutoFlow) + { + return new CssGridValue(templateRows, templateColumns, Array.Empty(), isDense); } return new CssGridTemplateValue(templateRows, templateColumns, templateAreas); @@ -166,7 +174,8 @@ public ICssValue[] Split(ICssValue value) } else if (value is CssGridValue grid) { - var dense = grid.Rows is not null ? CssKeywords.Row : CssKeywords.Column; + // The auto-flow keyword is placed on the rows (row flow) or the columns (column flow). + var flow = grid.Rows is not null ? CssKeywords.Column : CssKeywords.Row; return new[] { grid.Rows, @@ -174,7 +183,7 @@ public ICssValue[] Split(ICssValue value) null, //new Identifier(CssKeywords.None), grid.Columns is not null ? new CssTupleValue(grid.Sizes) : null, //new Identifier(CssKeywords.Auto), grid.Rows is not null ? new CssTupleValue(grid.Sizes) : null, //new Identifier(CssKeywords.Auto), - grid.IsDense ? new CssIdentifierValue(dense) : null, + new CssIdentifierValue(grid.IsDense ? String.Concat(flow, " ", CssKeywords.Dense) : flow), null, //Length.Zero, null, //Length.Zero, null, //new Identifier(CssKeywords.Normal), diff --git a/src/AngleSharp.Css/Declarations/HeightDeclaration.cs b/src/AngleSharp.Css/Declarations/HeightDeclaration.cs index 7aa57cf4..e54eee48 100644 --- a/src/AngleSharp.Css/Declarations/HeightDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/HeightDeclaration.cs @@ -8,7 +8,7 @@ static class HeightDeclaration { public static String Name = PropertyNames.Height; - public static IValueConverter Converter = AutoLengthOrPercentConverter; + public static IValueConverter Converter = WidthConverter; public static ICssValue InitialValue = InitialValues.HeightDecl; diff --git a/src/AngleSharp.Css/Declarations/MaxBlockSizeDeclaration.cs b/src/AngleSharp.Css/Declarations/MaxBlockSizeDeclaration.cs index 2b3884e4..39a510fa 100644 --- a/src/AngleSharp.Css/Declarations/MaxBlockSizeDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/MaxBlockSizeDeclaration.cs @@ -8,7 +8,7 @@ static class MaxBlockSizeDeclaration { public static String Name = PropertyNames.MaxBlockSize; - public static IValueConverter Converter = OptionalLengthOrPercentConverter; + public static IValueConverter Converter = MaxWidthConverter; public static ICssValue InitialValue = InitialValues.MaxBlockSizeDecl; diff --git a/src/AngleSharp.Css/Declarations/MaxHeightDeclaration.cs b/src/AngleSharp.Css/Declarations/MaxHeightDeclaration.cs index 975eb9ea..130100bd 100644 --- a/src/AngleSharp.Css/Declarations/MaxHeightDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/MaxHeightDeclaration.cs @@ -8,7 +8,7 @@ static class MaxHeightDeclaration { public static String Name = PropertyNames.MaxHeight; - public static IValueConverter Converter = OptionalLengthOrPercentConverter; + public static IValueConverter Converter = MaxWidthConverter; public static ICssValue InitialValue = InitialValues.MaxHeightDecl; diff --git a/src/AngleSharp.Css/Declarations/MaxInlineSizeDeclaration.cs b/src/AngleSharp.Css/Declarations/MaxInlineSizeDeclaration.cs index a8385ca0..bf0e926f 100644 --- a/src/AngleSharp.Css/Declarations/MaxInlineSizeDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/MaxInlineSizeDeclaration.cs @@ -8,7 +8,7 @@ static class MaxInlineSizeDeclaration { public static String Name = PropertyNames.MaxInlineSize; - public static IValueConverter Converter = OptionalLengthOrPercentConverter; + public static IValueConverter Converter = MaxWidthConverter; public static ICssValue InitialValue = InitialValues.MaxInlineSizeDecl; diff --git a/src/AngleSharp.Css/Declarations/MaxWidthDeclaration.cs b/src/AngleSharp.Css/Declarations/MaxWidthDeclaration.cs index ced82b68..a4a13f4b 100644 --- a/src/AngleSharp.Css/Declarations/MaxWidthDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/MaxWidthDeclaration.cs @@ -8,7 +8,7 @@ static class MaxWidthDeclaration { public static String Name = PropertyNames.MaxWidth; - public static IValueConverter Converter = OptionalLengthOrPercentConverter; + public static IValueConverter Converter = MaxWidthConverter; public static ICssValue InitialValue = InitialValues.MaxWidthDecl; diff --git a/src/AngleSharp.Css/Declarations/MinBlockSizeDeclaration.cs b/src/AngleSharp.Css/Declarations/MinBlockSizeDeclaration.cs index 7e745a78..61f81f86 100644 --- a/src/AngleSharp.Css/Declarations/MinBlockSizeDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/MinBlockSizeDeclaration.cs @@ -8,7 +8,7 @@ static class MinBlockSizeDeclaration { public static String Name = PropertyNames.MinBlockSize; - public static IValueConverter Converter = LengthOrPercentConverter; + public static IValueConverter Converter = WidthConverter; public static ICssValue InitialValue = InitialValues.MinBlockSizeDecl; diff --git a/src/AngleSharp.Css/Declarations/MinHeightDeclaration.cs b/src/AngleSharp.Css/Declarations/MinHeightDeclaration.cs index 86e4f737..f30a887d 100644 --- a/src/AngleSharp.Css/Declarations/MinHeightDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/MinHeightDeclaration.cs @@ -8,7 +8,7 @@ static class MinHeightDeclaration { public static String Name = PropertyNames.MinHeight; - public static IValueConverter Converter = LengthOrPercentConverter; + public static IValueConverter Converter = WidthConverter; public static ICssValue InitialValue = InitialValues.MinHeightDecl; diff --git a/src/AngleSharp.Css/Declarations/MinInlineSizeDeclaration.cs b/src/AngleSharp.Css/Declarations/MinInlineSizeDeclaration.cs index dee3c399..e83bb2ad 100644 --- a/src/AngleSharp.Css/Declarations/MinInlineSizeDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/MinInlineSizeDeclaration.cs @@ -8,7 +8,7 @@ static class MinInlineSizeDeclaration { public static String Name = PropertyNames.MinInlineSize; - public static IValueConverter Converter = LengthOrPercentConverter; + public static IValueConverter Converter = WidthConverter; public static ICssValue InitialValue = InitialValues.MinInlineSizeDecl; diff --git a/src/AngleSharp.Css/Declarations/MinWidthDeclaration.cs b/src/AngleSharp.Css/Declarations/MinWidthDeclaration.cs index f261b889..5e8d7b40 100644 --- a/src/AngleSharp.Css/Declarations/MinWidthDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/MinWidthDeclaration.cs @@ -8,7 +8,7 @@ static class MinWidthDeclaration { public static String Name = PropertyNames.MinWidth; - public static IValueConverter Converter = LengthOrPercentConverter; + public static IValueConverter Converter = WidthConverter; public static ICssValue InitialValue = InitialValues.MinWidthDecl; diff --git a/src/AngleSharp.Css/Declarations/OpacityDeclaration.cs b/src/AngleSharp.Css/Declarations/OpacityDeclaration.cs index a9ffccc1..348d24ac 100644 --- a/src/AngleSharp.Css/Declarations/OpacityDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/OpacityDeclaration.cs @@ -8,7 +8,7 @@ static class OpacityDeclaration { public static String Name = PropertyNames.Opacity; - public static IValueConverter Converter = NumberConverter; + public static IValueConverter Converter = AlphaValueConverter; public static ICssValue InitialValue = InitialValues.OpacityDecl; diff --git a/src/AngleSharp.Css/Declarations/OutlineDeclaration.cs b/src/AngleSharp.Css/Declarations/OutlineDeclaration.cs index 2edb68c4..e21f822d 100644 --- a/src/AngleSharp.Css/Declarations/OutlineDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/OutlineDeclaration.cs @@ -13,7 +13,7 @@ static class OutlineDeclaration public static IValueConverter Converter = AggregateTuple( WithAny( LineWidthConverter.Option(InitialValues.OutlineWidthDecl), - LineStyleConverter.Option(InitialValues.OutlineStyleDecl), + OutlineStyleConverter.Option(InitialValues.OutlineStyleDecl), InvertedColorConverter.Option(InitialValues.OutlineColorDecl))); public static ICssValue InitialValue = null; diff --git a/src/AngleSharp.Css/Declarations/OutlineStyleDeclaration.cs b/src/AngleSharp.Css/Declarations/OutlineStyleDeclaration.cs index 668aa48a..67b50780 100644 --- a/src/AngleSharp.Css/Declarations/OutlineStyleDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/OutlineStyleDeclaration.cs @@ -13,7 +13,7 @@ static class OutlineStyleDeclaration PropertyNames.Outline, }; - public static IValueConverter Converter = LineStyleConverter; + public static IValueConverter Converter = OutlineStyleConverter; public static ICssValue InitialValue = InitialValues.OutlineStyleDecl; diff --git a/src/AngleSharp.Css/Declarations/OverflowWrapDeclaration.cs b/src/AngleSharp.Css/Declarations/OverflowWrapDeclaration.cs index 6001f5c9..010a2b6a 100644 --- a/src/AngleSharp.Css/Declarations/OverflowWrapDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/OverflowWrapDeclaration.cs @@ -12,6 +12,6 @@ static class OverflowWrapDeclaration public static ICssValue InitialValue = InitialValues.OverflowWrapDecl; - public static PropertyFlags Flags = PropertyFlags.None; + public static PropertyFlags Flags = PropertyFlags.Inherited; } } diff --git a/src/AngleSharp.Css/Declarations/StrokeOpacityDeclaration.cs b/src/AngleSharp.Css/Declarations/StrokeOpacityDeclaration.cs index 60bc5c88..3cac7410 100644 --- a/src/AngleSharp.Css/Declarations/StrokeOpacityDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/StrokeOpacityDeclaration.cs @@ -8,7 +8,7 @@ static class StrokeOpacityDeclaration { public static String Name = PropertyNames.StrokeOpacity; - public static IValueConverter Converter = NumberConverter; + public static IValueConverter Converter = AlphaValueConverter; public static ICssValue InitialValue = InitialValues.StrokeOpacityDecl; diff --git a/src/AngleSharp.Css/Declarations/TextIndentDeclaration.cs b/src/AngleSharp.Css/Declarations/TextIndentDeclaration.cs index 4dc67038..49e171c0 100644 --- a/src/AngleSharp.Css/Declarations/TextIndentDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/TextIndentDeclaration.cs @@ -8,7 +8,7 @@ static class TextIndentDeclaration { public static String Name = PropertyNames.TextIndent; - public static IValueConverter Converter = LengthOrPercentConverter; + public static IValueConverter Converter = TextIndentConverter; public static ICssValue InitialValue = InitialValues.TextIndentDecl; diff --git a/src/AngleSharp.Css/Declarations/TransformBoxDeclaration.cs b/src/AngleSharp.Css/Declarations/TransformBoxDeclaration.cs new file mode 100644 index 00000000..c166e92d --- /dev/null +++ b/src/AngleSharp.Css/Declarations/TransformBoxDeclaration.cs @@ -0,0 +1,17 @@ +namespace AngleSharp.Css.Declarations +{ + using AngleSharp.Css.Dom; + using System; + using static ValueConverters; + + static class TransformBoxDeclaration + { + public static String Name = PropertyNames.TransformBox; + + public static IValueConverter Converter = TransformBoxConverter; + + public static ICssValue InitialValue = InitialValues.TransformBoxDecl; + + public static PropertyFlags Flags = PropertyFlags.None; + } +} diff --git a/src/AngleSharp.Css/Declarations/TransitionBehaviorDeclaration.cs b/src/AngleSharp.Css/Declarations/TransitionBehaviorDeclaration.cs new file mode 100644 index 00000000..8308d8a0 --- /dev/null +++ b/src/AngleSharp.Css/Declarations/TransitionBehaviorDeclaration.cs @@ -0,0 +1,18 @@ +namespace AngleSharp.Css.Declarations +{ + using AngleSharp.Css.Converters; + using AngleSharp.Css.Dom; + using System; + using static ValueConverters; + + static class TransitionBehaviorDeclaration + { + public static String Name = PropertyNames.TransitionBehavior; + + public static IValueConverter Converter = TransitionBehaviorConverter.FromList(); + + public static ICssValue InitialValue = InitialValues.TransitionBehaviorDecl; + + public static PropertyFlags Flags = PropertyFlags.None; + } +} diff --git a/src/AngleSharp.Css/Declarations/WordBreakDeclaration.cs b/src/AngleSharp.Css/Declarations/WordBreakDeclaration.cs index 0f111e69..09faf740 100644 --- a/src/AngleSharp.Css/Declarations/WordBreakDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/WordBreakDeclaration.cs @@ -12,6 +12,6 @@ static class WordBreakDeclaration public static ICssValue InitialValue = InitialValues.WordBreakDecl; - public static PropertyFlags Flags = PropertyFlags.None; + public static PropertyFlags Flags = PropertyFlags.Inherited; } } diff --git a/src/AngleSharp.Css/Declarations/WordWrapDeclaration.cs b/src/AngleSharp.Css/Declarations/WordWrapDeclaration.cs index c880acde..04d73e2a 100644 --- a/src/AngleSharp.Css/Declarations/WordWrapDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/WordWrapDeclaration.cs @@ -12,6 +12,6 @@ static class WordWrapDeclaration public static ICssValue InitialValue = InitialValues.WordWrapDecl; - public static PropertyFlags Flags = PropertyFlags.None; + public static PropertyFlags Flags = PropertyFlags.Inherited; } } diff --git a/src/AngleSharp.Css/Dom/BorderRepeat.cs b/src/AngleSharp.Css/Dom/BorderRepeat.cs index 1c7397c7..4129a038 100644 --- a/src/AngleSharp.Css/Dom/BorderRepeat.cs +++ b/src/AngleSharp.Css/Dom/BorderRepeat.cs @@ -21,6 +21,10 @@ public enum BorderRepeat : byte /// after being repeated an integral number of times, the image is /// rescaled to fit. /// - Round + Round, + /// + /// The image is tiled to fill the area, with extra space distributed between the tiles. + /// + Space } } diff --git a/src/AngleSharp.Css/Dom/ClearMode.cs b/src/AngleSharp.Css/Dom/ClearMode.cs index 2542e644..7c7657c1 100644 --- a/src/AngleSharp.Css/Dom/ClearMode.cs +++ b/src/AngleSharp.Css/Dom/ClearMode.cs @@ -20,6 +20,14 @@ public enum ClearMode : byte /// /// The element is moved down to clear past both left and right floats. /// - Both + Both, + /// + /// The element is moved below the floats on the start side. + /// + InlineStart, + /// + /// The element is moved below the floats on the end side. + /// + InlineEnd } } diff --git a/src/AngleSharp.Css/Dom/DisplayMode.cs b/src/AngleSharp.Css/Dom/DisplayMode.cs index f9de9ea1..60585170 100644 --- a/src/AngleSharp.Css/Dom/DisplayMode.cs +++ b/src/AngleSharp.Css/Dom/DisplayMode.cs @@ -97,5 +97,13 @@ public enum DisplayMode : byte /// formatting context, i.e., it contains its floats. /// FlowRoot, + /// + /// The element does not generate a box, but its children do. + /// + Contents, + /// + /// The element generates a ruby container box. + /// + Ruby, } } diff --git a/src/AngleSharp.Css/Dom/FlexContentMode.cs b/src/AngleSharp.Css/Dom/FlexContentMode.cs index a6fafcb5..db73d763 100644 --- a/src/AngleSharp.Css/Dom/FlexContentMode.cs +++ b/src/AngleSharp.Css/Dom/FlexContentMode.cs @@ -38,5 +38,9 @@ public enum FlexContentMode /// Follow the baseline. /// Baseline, + /// + /// Distribute space evenly between and around items. + /// + SpaceEvenly, } } diff --git a/src/AngleSharp.Css/Dom/Floating.cs b/src/AngleSharp.Css/Dom/Floating.cs index 2ed00f70..72ef9f4a 100644 --- a/src/AngleSharp.Css/Dom/Floating.cs +++ b/src/AngleSharp.Css/Dom/Floating.cs @@ -21,5 +21,13 @@ public enum Floating : byte /// Each footnote element is placed in the footnote area of the page. /// Footnote, + /// + /// The element floats on the start side of its containing block. + /// + InlineStart, + /// + /// The element floats on the end side of its containing block. + /// + InlineEnd, } } diff --git a/src/AngleSharp.Css/Dom/Internal/CssProperty.cs b/src/AngleSharp.Css/Dom/Internal/CssProperty.cs index 4404315a..1e229a84 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssProperty.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssProperty.cs @@ -110,7 +110,7 @@ public Boolean IsImportant public ICssProperty Compute(ICssComputeContext context) { - var propertyContext = new PropertyComputeContext(context, _converter); + var propertyContext = new PropertyComputeContext(context, _converter, _name); var computedValue = _name.StartsWith("--", StringComparison.Ordinal) ? context.Resolve(_name) ?? CssInvalidValue.Instance : _value is CssChildValue child ? child.Compute(propertyContext, _name) : @@ -128,15 +128,17 @@ public ICssProperty Compute(ICssComputeContext context) #region Compute Context - sealed class PropertyComputeContext : ICssComputeContext, ILocalComputeContext, IInheritingComputeContext + sealed class PropertyComputeContext : ICssComputeContext, ILocalComputeContext, IInheritingComputeContext, IRelativeComputeContext { private readonly ICssComputeContext _parent; private readonly IValueConverter _converter; + private readonly String _name; - public PropertyComputeContext(ICssComputeContext parent, IValueConverter converter) + public PropertyComputeContext(ICssComputeContext parent, IValueConverter converter, String name) { _parent = parent; _converter = converter; + _name = name; } public IRenderDevice Device => _parent.Device; @@ -150,6 +152,21 @@ public PropertyComputeContext(ICssComputeContext parent, IValueConverter convert public ICssValue Resolve(String name) =>_parent.Resolve(name); public ICssValue InheritedValue(String name) => (_parent as IInheritingComputeContext)?.InheritedValue(name); + + private IFontComputeContext Fonts => _parent as IFontComputeContext; + + // em in font-size refers to the font size of the parent, everywhere else to the one of the element. + public Double? EmBase => _name.Is(PropertyNames.FontSize) ? Fonts?.ParentFontSize : Fonts?.FontSize; + + public Double? RootFontSize => Fonts?.RootFontSize; + + // Except for font-size and line-height, percentages refer to a box (or a length) that is only known during layout. + public Boolean KeepsPercentages => Fonts is not null && PercentageBasis is null; + + public Double? PercentageBasis => + _name.Is(PropertyNames.FontSize) ? Fonts?.ParentFontSize : + _name.Is(PropertyNames.LineHeight) ? Fonts?.FontSize : + null; } #endregion diff --git a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs index b264d3b9..07029b34 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs @@ -73,6 +73,12 @@ public String CssText public ICssRule Parent => _parent; + /// + /// Gets or sets the computed font size (in px) of the root element, which is carried + /// along from computed style to computed style to resolve rem units. + /// + internal Double? RootFontSize { get; set; } + #endregion #region Methods diff --git a/src/AngleSharp.Css/Dom/OverflowWrap.cs b/src/AngleSharp.Css/Dom/OverflowWrap.cs index 03b38c97..64539ef5 100644 --- a/src/AngleSharp.Css/Dom/OverflowWrap.cs +++ b/src/AngleSharp.Css/Dom/OverflowWrap.cs @@ -13,6 +13,10 @@ public enum OverflowWrap : byte /// Indicates that normally unbreakable words may be broken at arbitrary points /// if there are no otherwise acceptable break points in the line. /// - BreakWord + BreakWord, + /// + /// Like break-word, but soft wrap opportunities are considered for min-content sizes. + /// + Anywhere } } diff --git a/src/AngleSharp.Css/Dom/Sizing.cs b/src/AngleSharp.Css/Dom/Sizing.cs index 0f1c8d1b..28094c11 100644 --- a/src/AngleSharp.Css/Dom/Sizing.cs +++ b/src/AngleSharp.Css/Dom/Sizing.cs @@ -18,5 +18,10 @@ public enum Sizing /// The intrinsic minimum width. /// MinContent, + /// + /// The box fills the available space of its containing block. + /// + /// + Stretch, } } diff --git a/src/AngleSharp.Css/Dom/Whitespace.cs b/src/AngleSharp.Css/Dom/Whitespace.cs index 42849b12..54d4a9ed 100644 --- a/src/AngleSharp.Css/Dom/Whitespace.cs +++ b/src/AngleSharp.Css/Dom/Whitespace.cs @@ -30,6 +30,10 @@ public enum Whitespace : byte /// Sequences of whitespace are collapsed. Lines are broken at newline characters, /// at br, and as necessary to fill line boxes. /// - PreLine + PreLine, + /// + /// Like pre-wrap, but preserved white space always takes up space and can be broken after. + /// + BreakSpaces } } diff --git a/src/AngleSharp.Css/Extensions/CssOmExtensions.cs b/src/AngleSharp.Css/Extensions/CssOmExtensions.cs index 5fe65851..3ccbc7a1 100644 --- a/src/AngleSharp.Css/Extensions/CssOmExtensions.cs +++ b/src/AngleSharp.Css/Extensions/CssOmExtensions.cs @@ -91,6 +91,11 @@ public static ICssStyleDeclaration Compute(this ICssStyleDeclaration style, ICss { var computedStyle = new CssStyleDeclaration(context.Context); + if (context is IFontComputeContext fonts) + { + computedStyle.RootFontSize = fonts.RootFontSize; + } + foreach (var property in style) { var computed = property.Compute(context); diff --git a/src/AngleSharp.Css/Factories/DefaultDeclarationFactory.cs b/src/AngleSharp.Css/Factories/DefaultDeclarationFactory.cs index bff400f8..505ab6e2 100644 --- a/src/AngleSharp.Css/Factories/DefaultDeclarationFactory.cs +++ b/src/AngleSharp.Css/Factories/DefaultDeclarationFactory.cs @@ -1083,6 +1083,34 @@ public class DefaultDeclarationFactory : IDeclarationFactory flags: FontVariantCapsDeclaration.Flags, shorthands: FontVariantCapsDeclaration.Shorthands) }, + { + FontVariantNumericDeclaration.Name, new DeclarationInfo( + name: FontVariantNumericDeclaration.Name, + converter: FontVariantNumericDeclaration.Converter, + initialValue: FontVariantNumericDeclaration.InitialValue, + flags: FontVariantNumericDeclaration.Flags) + }, + { + FontVariantLigaturesDeclaration.Name, new DeclarationInfo( + name: FontVariantLigaturesDeclaration.Name, + converter: FontVariantLigaturesDeclaration.Converter, + initialValue: FontVariantLigaturesDeclaration.InitialValue, + flags: FontVariantLigaturesDeclaration.Flags) + }, + { + TransformBoxDeclaration.Name, new DeclarationInfo( + name: TransformBoxDeclaration.Name, + converter: TransformBoxDeclaration.Converter, + initialValue: TransformBoxDeclaration.InitialValue, + flags: TransformBoxDeclaration.Flags) + }, + { + TransitionBehaviorDeclaration.Name, new DeclarationInfo( + name: TransitionBehaviorDeclaration.Name, + converter: TransitionBehaviorDeclaration.Converter, + initialValue: TransitionBehaviorDeclaration.InitialValue, + flags: TransitionBehaviorDeclaration.Flags) + }, { FontWeightDeclaration.Name, new DeclarationInfo( name: FontWeightDeclaration.Name, diff --git a/src/AngleSharp.Css/Parser/Micro/CalcParser.cs b/src/AngleSharp.Css/Parser/Micro/CalcParser.cs index 7a72f4bf..208236b3 100644 --- a/src/AngleSharp.Css/Parser/Micro/CalcParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/CalcParser.cs @@ -5,6 +5,7 @@ namespace AngleSharp.Css.Parser using AngleSharp.Css.Values; using AngleSharp.Text; using System; + using System.Collections.Generic; using System.Globalization; /// @@ -33,6 +34,53 @@ public static CssCalcValue ParseCalc(this StringSource source) return null; } + /// + /// Parses a comparison function value (min, max, or clamp), if any. + /// + internal static ICssValue ParseMath(this StringSource source) + { + var pos = source.Index; + var name = source.IsFunction(FunctionNames.Min) ? FunctionNames.Min : + source.IsFunction(FunctionNames.Max) ? FunctionNames.Max : + source.IsFunction(FunctionNames.Clamp) ? FunctionNames.Clamp : + null; + + if (name != null) + { + var arguments = new List(); + + while (true) + { + var argument = source.ParseAddExpression(); + + if (argument == null) + { + break; + } + + arguments.Add(argument); + + if (source.Current == Symbols.Comma) + { + source.SkipCurrentAndSpaces(); + } + else if (source.Current == Symbols.RoundBracketClose) + { + source.SkipCurrentAndSpaces(); + var valid = name == FunctionNames.Clamp ? arguments.Count == 3 : arguments.Count > 0; + return valid ? new CssMathValue(name, arguments.ToArray()) : null; + } + else + { + break; + } + } + } + + source.BackTo(pos); + return null; + } + private static ICssValue ParseExpression(this StringSource source) { var expression = source.ParseAddExpression(); @@ -106,7 +154,7 @@ private static ICssValue ParseBracketExpression(this StringSource source) return new CssCalcBracketExpression(content); } - return source.ParseAtomicExpression(); + return source.ParseMath() ?? source.ParseAtomicExpression(); } /// diff --git a/src/AngleSharp.Css/Parser/Micro/ColorParser.cs b/src/AngleSharp.Css/Parser/Micro/ColorParser.cs index bc5bd43a..6e423bad 100644 --- a/src/AngleSharp.Css/Parser/Micro/ColorParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/ColorParser.cs @@ -25,6 +25,8 @@ static class ColorParser { FunctionNames.Lch, ParseLch }, { FunctionNames.Oklab, ParseOklab}, { FunctionNames.Oklch, ParseOklch }, + { FunctionNames.LightDark, ParseLightDark }, + { FunctionNames.ColorMix, ParseColorMix }, }; /// @@ -174,6 +176,20 @@ static class ColorParser } private static CssColorValue? ParseHsla(StringSource source) + { + var pos = source.Index; + var color = ParseHslaLegacy(source); + + if (!color.HasValue) + { + source.BackTo(pos); + return ParseHueModern(source, CssColorValue.FromHsla); + } + + return color.Value; + } + + private static CssColorValue? ParseHslaLegacy(StringSource source) { var h = ParseAngle(source); var c1 = source.SkipGetSkip(); @@ -203,6 +219,94 @@ static class ColorParser return null; } + private static CssColorValue? ParseLightDark(StringSource source) + { + var light = Start(source); + var c1 = source.SkipGetSkip(); + var dark = Start(source); + var c2 = source.SkipGetSkip(); + + if (light != null && dark != null && c1 == Symbols.Comma && c2 == Symbols.RoundBracketClose) + { + // The used color scheme is unknown while parsing, i.e., the light one is taken. + return light; + } + + return null; + } + + private static CssColorValue? ParseColorMix(StringSource source) + { + if (!source.IsIdentifier(CssKeywords.In)) + { + return null; + } + + source.SkipSpacesAndComments(); + + // The colors are always interpolated in sRGB, independent of the given color space + // or hue interpolation method. + while (source.Current != Symbols.Comma && source.ParseIdent() != null) + { + source.SkipSpacesAndComments(); + } + + if (source.Current != Symbols.Comma) + { + return null; + } + + source.SkipCurrentAndSpaces(); + var first = ParseMixComponent(source, out var p1); + var c1 = source.SkipGetSkip(); + var second = ParseMixComponent(source, out var p2); + var c2 = source.SkipGetSkip(); + + if (first == null || second == null || c1 != Symbols.Comma || c2 != Symbols.RoundBracketClose) + { + return null; + } + + p1 ??= 1.0 - (p2 ?? 0.5); + p2 ??= 1.0 - p1.Value; + var sum = p1.Value + p2.Value; + + if (p1 < 0 || p2 < 0 || sum <= 0) + { + return null; + } + + var w1 = p1.Value / sum; + var w2 = p2.Value / sum; + var a1 = first.Value.A / 255.0; + var a2 = second.Value.A / 255.0; + var alpha = a1 * w1 + a2 * w2; + + // Mixes with premultiplied alpha; percentages summing to less than 100% scale the alpha. + Byte Channel(Byte x, Byte y) => alpha > 0 ? (Byte)Math.Round(Math.Max(Math.Min((x * a1 * w1 + y * a2 * w2) / alpha, 255.0), 0.0)) : (Byte)0; + + return CssColorValue.FromRgba( + Channel(first.Value.R, second.Value.R), + Channel(first.Value.G, second.Value.G), + Channel(first.Value.B, second.Value.B), + alpha * Math.Min(sum, 1.0)); + } + + private static CssColorValue? ParseMixComponent(StringSource source, out Double? percentage) + { + percentage = source.ParsePercent(); + source.SkipSpacesAndComments(); + var color = Start(source); + source.SkipSpacesAndComments(); + + if (!percentage.HasValue) + { + percentage = source.ParsePercent(); + } + + return color; + } + private static CssColorValue? ParseGray(StringSource source) { var n = ParseRgbComponent(source); @@ -355,6 +459,56 @@ static class ColorParser } private static CssColorValue? ParseHwba(StringSource source) + { + var pos = source.Index; + var color = ParseHwbaLegacy(source); + + if (!color.HasValue) + { + source.BackTo(pos); + return ParseHueModern(source, CssColorValue.FromHwba); + } + + return color.Value; + } + + /// + /// Parses the space-separated syntax of a hue-based color function (hsl, hwb), e.g., + /// hsl(120deg 100% 50% / 50%). Missing components (none) are taken as zero. + /// + private static CssColorValue? ParseHueModern(StringSource source, Func create) + { + var h = ParseAngleOrNone(source); + source.SkipSpacesAndComments(); + var x = ParsePercentOrNone(source); + source.SkipSpacesAndComments(); + var y = ParsePercentOrNone(source); + source.SkipSpacesAndComments(); + var c = source.Current; + var a = new Nullable(1.0); + + if (h != null && x != null && y != null) + { + source.SkipCurrentAndSpaces(); + + if (c == Symbols.Solidus) + { + a = ParseAlphaOrNone(source); + source.SkipSpacesAndComments(); + c = source.Current; + source.SkipCurrentAndSpaces(); + } + + if (a != null && c == Symbols.RoundBracketClose) + { + return create(h.Value, x.Value, y.Value, a.Value); + } + } + + return null; + } + + private static CssColorValue? ParseHwbaLegacy(StringSource source) { var h = ParseAngle(source); var c1 = source.SkipGetSkip(); @@ -452,6 +606,33 @@ static class ColorParser return null; } + private static Double? ParseAngleOrNone(StringSource source) => + source.IsIdentifier(CssKeywords.None) ? 0.0 : ParseAngle(source); + + private static Double? ParseAlphaOrNone(StringSource source) => + source.IsIdentifier(CssKeywords.None) ? 0.0 : ParseAlpha(source); + + private static Double? ParsePercentOrNone(StringSource source) + { + if (source.IsIdentifier(CssKeywords.None)) + { + return 0.0; + } + + var pos = source.Index; + var unit = source.ParseUnit(); + + // The modern syntax also allows plain numbers, which are interpreted as percentages. + if (unit != null && (unit.Dimension == "%" || unit.Dimension == String.Empty) && + Double.TryParse(unit.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)) + { + return value * 0.01; + } + + source.BackTo(pos); + return null; + } + private static Double? ParseAlpha(StringSource source) { var unit = source.ParseUnit(); diff --git a/src/AngleSharp.Css/Parser/Micro/CompoundParser.cs b/src/AngleSharp.Css/Parser/Micro/CompoundParser.cs index 194a0a9e..ca21f624 100644 --- a/src/AngleSharp.Css/Parser/Micro/CompoundParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/CompoundParser.cs @@ -128,12 +128,80 @@ public static ICssImageValue ParseImageSource(this StringSource source) if (url == null) { - return source.ParseGradient(); + return source.ParseGradient() ?? source.ParseImageSet(); } return url; } + /// + /// Parses an image-set function, where each option is an image (or a string with its + /// URL) followed by an optional resolution and an optional type(). + /// + internal static ICssImageValue ParseImageSet(this StringSource source) + { + var pos = source.Index; + + if (source.IsFunction(FunctionNames.ImageSet)) + { + var options = new List(); + + while (true) + { + var parts = new List(); + var image = source.ParseUri() ?? source.ParseGradient() ?? (ICssValue)ParseStringValue(source); + + if (image is null) + { + break; + } + + parts.Add(image); + source.SkipSpacesAndComments(); + var resolution = source.ParseResolution(); + + if (resolution.HasValue) + { + parts.Add(resolution.Value); + source.SkipSpacesAndComments(); + } + + if (source.IsFunction(FunctionNames.Type)) + { + var type = ParseStringValue(source); + + if (type is null || source.SkipGetSkip() != Symbols.RoundBracketClose) + { + break; + } + + parts.Add(new CssImageSetValue.TypeValue(type)); + } + + options.Add(new CssTupleValue(parts.ToArray())); + var c = source.SkipGetSkip(); + + if (c == Symbols.RoundBracketClose) + { + return new CssImageSetValue(options.ToArray()); + } + else if (c != Symbols.Comma) + { + break; + } + } + } + + source.BackTo(pos); + return null; + } + + private static ICssValue ParseStringValue(StringSource source) + { + var str = source.ParseString(); + return str is not null ? new CssStringValue(str) : null; + } + /// /// Parse for a generic periodic [u, y, d, x] value. /// diff --git a/src/AngleSharp.Css/Parser/Micro/GridParser.cs b/src/AngleSharp.Css/Parser/Micro/GridParser.cs index f240e20e..1b9831cf 100644 --- a/src/AngleSharp.Css/Parser/Micro/GridParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/GridParser.cs @@ -107,6 +107,36 @@ public static ICssValue ParseGridTemplate(this StringSource source) return null; } + /// + /// Parses a subgrid value, i.e., subgrid followed by optional line names. + /// https://drafts.csswg.org/css-grid-2/#subgrid-listing + /// + internal static ICssValue ParseSubgrid(this StringSource source) + { + if (source.IsIdentifier(CssKeywords.Subgrid)) + { + var values = new List { new CssIdentifierValue(CssKeywords.Subgrid) }; + source.SkipSpacesAndComments(); + + while (!source.IsDone) + { + var names = source.ParseLineNames(); + + if (!names.HasValue) + { + break; + } + + values.Add(names.Value); + source.SkipSpacesAndComments(); + } + + return new CssTupleValue(values.ToArray()); + } + + return null; + } + /// /// Parses a grid line name value. /// diff --git a/src/AngleSharp.Css/Parser/Micro/PointParser.cs b/src/AngleSharp.Css/Parser/Micro/PointParser.cs index cc2a932b..2f09f2a6 100644 --- a/src/AngleSharp.Css/Parser/Micro/PointParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/PointParser.cs @@ -43,7 +43,8 @@ private static ICssValue ParsePointDir(this StringSource source, Predicate public static CssOriginValue ParseOrigin(this StringSource source) { - var pt = source.ParsePoint(); + // An origin has no edge offsets, i.e., a trailing length is the z-component. + var pt = source.ParseKeywordPoint(); source.SkipSpacesAndComments(); var z = source.ParseLengthOrCalc(); @@ -70,7 +72,13 @@ public static CssOriginValue ParseOrigin(this StringSource source) /// /// Parses a point value. /// - public static CssPoint2D? ParsePoint(this StringSource source) + public static CssPoint2D? ParsePoint(this StringSource source) => + source.ParseEdgeOffsetPoint() ?? source.ParseKeywordPoint(); + + /// + /// Parses a point value given by one or two components, i.e., without edge offsets. + /// + private static CssPoint2D? ParseKeywordPoint(this StringSource source) { var pos = source.Index; var x = new CssLengthValue(50f, CssLengthValue.Unit.Percent); @@ -185,6 +193,69 @@ public static CssBackgroundSizeValue ParseSize(this StringSource source) } } + /// + /// Parses the three- and four-value syntax of a position, e.g., right 10px bottom 20px, + /// where at least one of the edges has an offset. + /// + private static CssPoint2D? ParseEdgeOffsetPoint(this StringSource source) + { + var pos = source.Index; + var first = source.ParseIdent(); + var firstOffset = first is not null ? source.ParseEdgeOffset(first) : null; + source.SkipSpacesAndComments(); + var second = first is not null ? source.ParseIdent() : null; + var secondOffset = second is not null ? source.ParseEdgeOffset(second) : null; + + if (second is not null && (firstOffset is not null || secondOffset is not null)) + { + if (!IsHorizontal(first) || !IsVertical(second)) + { + var edge = first; + var offset = firstOffset; + first = second; + firstOffset = secondOffset; + second = edge; + secondOffset = offset; + } + + var valid = IsHorizontal(first) && IsVertical(second) && + (firstOffset is null || !first.Isi(CssKeywords.Center)) && + (secondOffset is null || !second.Isi(CssKeywords.Center)); + + if (valid) + { + var x = firstOffset is not null ? new CssEdgeOffsetValue(first.ToLowerFast(), firstOffset) : (ICssValue)KeywordToLength(first).Value; + var y = secondOffset is not null ? new CssEdgeOffsetValue(second.ToLowerFast(), secondOffset) : (ICssValue)KeywordToLength(second).Value; + return new CssPoint2D(x, y); + } + } + + source.BackTo(pos); + return null; + } + + /// + /// Parses the optional offset following an edge keyword (not center). + /// + private static ICssValue ParseEdgeOffset(this StringSource source, String edge) + { + if (!edge.Isi(CssKeywords.Center)) + { + var pos = source.Index; + source.SkipSpacesAndComments(); + var offset = source.ParseDistanceOrCalc(); + + if (offset is not null) + { + return offset; + } + + source.BackTo(pos); + } + + return null; + } + private static Boolean IsHorizontal(String str) => str.Isi(CssKeywords.Left) || str.Isi(CssKeywords.Right) || str.Isi(CssKeywords.Center); diff --git a/src/AngleSharp.Css/Parser/Micro/ShadowParser.cs b/src/AngleSharp.Css/Parser/Micro/ShadowParser.cs index fcc5959e..d0b89bf0 100644 --- a/src/AngleSharp.Css/Parser/Micro/ShadowParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/ShadowParser.cs @@ -60,7 +60,7 @@ public static CssShadowValue ParseShadow(this StringSource source) if (!color.HasValue) { - color = source.ParseColor(); + color = source.ParseCurrentColor(); source.SkipSpacesAndComments(); } } diff --git a/src/AngleSharp.Css/Parser/Micro/TimingFunctionParser.cs b/src/AngleSharp.Css/Parser/Micro/TimingFunctionParser.cs index 96e88e0f..81252146 100644 --- a/src/AngleSharp.Css/Parser/Micro/TimingFunctionParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/TimingFunctionParser.cs @@ -1,6 +1,7 @@ #nullable disable namespace AngleSharp.Css.Parser { + using AngleSharp.Css.Dom; using AngleSharp.Css.Values; using AngleSharp.Text; using System; @@ -15,6 +16,17 @@ public static class TimingFunctionParser { { FunctionNames.Steps, ParseSteps }, { FunctionNames.CubicBezier, ParseCubicBezier }, + { CssKeywords.Linear, ParseLinear }, + }; + + private static readonly HashSet StepPositions = new(StringComparer.OrdinalIgnoreCase) + { + CssKeywords.Start, + CssKeywords.End, + CssKeywords.JumpStart, + CssKeywords.JumpEnd, + CssKeywords.JumpNone, + CssKeywords.JumpBoth, }; /// @@ -59,25 +71,73 @@ private static ICssTimingFunctionValue ParseSteps(StringSource source) if (intervals.HasValue) { - var start = false; - var end = true; + var position = default(String); if (c == Symbols.Comma) { - start = source.IsIdentifier(CssKeywords.Start); - end = source.IsIdentifier(CssKeywords.End); + position = source.ParseIdent()?.ToLowerFast(); c = source.SkipGetSkip(); + + if (position is null || !StepPositions.Contains(position)) + { + return null; + } } - if (start != end && c == Symbols.RoundBracketClose) + // jump-none requires at least two intervals + if (c == Symbols.RoundBracketClose && (intervals.Value.IntValue > 1 || (intervals.Value.IntValue == 1 && !CssKeywords.JumpNone.Is(position)))) { - return new CssStepsValue(intervals.Value, start); + return new CssStepsValue(intervals.Value, position); } } return null; } + private static ICssTimingFunctionValue ParseLinear(StringSource source) + { + var stops = new List(); + + while (true) + { + var output = source.ParseNumber(); + + if (!output.HasValue) + { + return null; + } + + var parts = new List { output.Value }; + source.SkipSpacesAndComments(); + + // Each stop has an optional input progress, given as one or two percentages. + for (var i = 0; i < 2; i++) + { + var input = source.ParsePercent(); + + if (!input.HasValue) + { + break; + } + + parts.Add(new CssPercentageValue(input.Value * 100.0)); + source.SkipSpacesAndComments(); + } + + stops.Add(new CssTupleValue(parts.ToArray())); + var c = source.SkipGetSkip(); + + if (c == Symbols.RoundBracketClose) + { + return stops.Count > 1 ? new CssLinearEasingValue(stops.ToArray()) : null; + } + else if (c != Symbols.Comma) + { + return null; + } + } + } + private static ICssTimingFunctionValue ParseCubicBezier(StringSource source) { var p1 = source.ParseNumber(); diff --git a/src/AngleSharp.Css/Parser/Micro/UnitParser.cs b/src/AngleSharp.Css/Parser/Micro/UnitParser.cs index cc0b0740..3353674f 100644 --- a/src/AngleSharp.Css/Parser/Micro/UnitParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/UnitParser.cs @@ -200,7 +200,24 @@ public static ICssValue ParseAngleOrCalc(this StringSource source) => /// public static ICssValue ParseFontSize(this StringSource source) => source.ParseDistanceOrCalc() ?? - source.ParseConstant(Map.FontSizes); + source.ParseFontSizeKeyword(); + + /// + /// Parses a font size keyword, e.g., medium or larger. + /// + internal static CssFontSizeKeywordValue ParseFontSizeKeyword(this StringSource source) + { + var pos = source.Index; + var ident = source.ParseIdent(); + + if (ident != null && Map.FontSizes.TryGetValue(ident, out var size)) + { + return new CssFontSizeKeywordValue(ident.ToLowerFast(), size); + } + + source.BackTo(pos); + return null; + } /// /// Parses a track breadth value. diff --git a/src/AngleSharp.Css/ValueConverters.cs b/src/AngleSharp.Css/ValueConverters.cs index 45a60cdb..75e6cd59 100644 --- a/src/AngleSharp.Css/ValueConverters.cs +++ b/src/AngleSharp.Css/ValueConverters.cs @@ -95,7 +95,7 @@ static class ValueConverters /// Represents a calculated number. /// https://developer.mozilla.org/en-US/docs/Web/CSS/calc /// - public static readonly IValueConverter CalcConverter = FromParser(CalcParser.ParseCalc); + public static readonly IValueConverter CalcConverter = Or(FromParser(CalcParser.ParseCalc), FromParser(CalcParser.ParseMath)); /// /// Represents a length object. @@ -175,6 +175,11 @@ static class ValueConverters /// public static readonly IValueConverter WeightIntegerConverter = new StructValueConverter(NumberParser.ParseWeightInteger); + /// + /// Represents a font weight number in the range [1, 1000] (CSS Fonts 4). + /// + public static readonly IValueConverter WeightNumberConverter = new StructValueConverter(ParseWeightNumber); + /// /// Represents an integer object that is greater tha zero. /// @@ -196,6 +201,12 @@ static class ValueConverters /// public static readonly IValueConverter NumberConverter = Or(OnlyNumberConverter, CalcConverter); + /// + /// Represents an alpha value, i.e., a number or a percentage. + /// https://drafts.csswg.org/css-color/#typedef-alpha-value + /// + public static readonly IValueConverter AlphaValueConverter = new AlphaValueConverter(NumberConverter); + /// /// Represents a (calculated) length object. /// @@ -310,7 +321,8 @@ static class ValueConverters Assign(CssKeywords.Saturation, CssKeywords.Saturation), Assign(CssKeywords.Color, CssKeywords.Color), Assign(CssKeywords.Luminosity, CssKeywords.Luminosity), - Assign(CssKeywords.Add, CssKeywords.Add)); + Assign(CssKeywords.Add, CssKeywords.Add), + Assign(CssKeywords.PlusLighter, CssKeywords.PlusLighter)); /// /// Represents a converter for the mix-blend-mode property. @@ -367,7 +379,8 @@ static class ValueConverters Assign(CssKeywords.Auto, CssKeywords.Auto), Assign(CssKeywords.CrispEdges, CssKeywords.CrispEdges), Assign(CssKeywords.OptimizeQuality, CssKeywords.OptimizeQuality), - Assign(CssKeywords.OptimizeSpeed, CssKeywords.OptimizeSpeed)); + Assign(CssKeywords.OptimizeSpeed, CssKeywords.OptimizeSpeed), + Assign(CssKeywords.Pixelated, CssKeywords.Pixelated)); /// /// Represents a converter for the image-orientation property. @@ -855,6 +868,11 @@ static class ValueConverters /// public static readonly IValueConverter LineStyleConverter = Map.LineStyles.ToConverter(); + /// + /// Represents a converter for the outline-style property, i.e., a line style or auto. + /// + public static readonly IValueConverter OutlineStyleConverter = Or(Assign(CssKeywords.Auto, CssKeywords.Auto), LineStyleConverter); + /// /// Represents a converter for the BackgroundAttachment enumeration. /// @@ -870,6 +888,11 @@ static class ValueConverters /// public static readonly IValueConverter BoxModelConverter = Map.BoxModels.ToConverter(); + /// + /// Represents a converter for the background-clip property, i.e., a box or text. + /// + public static readonly IValueConverter BackgroundClipConverter = Or(BoxModelConverter, Assign(CssKeywords.Text, CssKeywords.Text)); + /// /// Represents a converter for the AnimationDirection enumeration. /// @@ -1017,6 +1040,51 @@ static class ValueConverters Assign(CssKeywords.Unicase, CssKeywords.Unicase), Assign(CssKeywords.TitlingCaps, CssKeywords.TitlingCaps)); + /// + /// Represents a converter for the font-variant-numeric property. + /// https://www.w3.org/TR/css-fonts-4/#font-variant-numeric-prop + /// + public static readonly IValueConverter FontVariantNumericConverter = Or( + Assign(CssKeywords.Normal, CssKeywords.Normal), + WithAny( + Or(Assign(CssKeywords.LiningNums, CssKeywords.LiningNums), Assign(CssKeywords.OldstyleNums, CssKeywords.OldstyleNums)), + Or(Assign(CssKeywords.ProportionalNums, CssKeywords.ProportionalNums), Assign(CssKeywords.TabularNums, CssKeywords.TabularNums)), + Or(Assign(CssKeywords.DiagonalFractions, CssKeywords.DiagonalFractions), Assign(CssKeywords.StackedFractions, CssKeywords.StackedFractions)), + Assign(CssKeywords.Ordinal, CssKeywords.Ordinal), + Assign(CssKeywords.SlashedZero, CssKeywords.SlashedZero))); + + /// + /// Represents a converter for the font-variant-ligatures property. + /// https://www.w3.org/TR/css-fonts-4/#font-variant-ligatures-prop + /// + public static readonly IValueConverter FontVariantLigaturesConverter = Or( + Assign(CssKeywords.Normal, CssKeywords.Normal), + Assign(CssKeywords.None, CssKeywords.None), + WithAny( + Or(Assign(CssKeywords.CommonLigatures, CssKeywords.CommonLigatures), Assign(CssKeywords.NoCommonLigatures, CssKeywords.NoCommonLigatures)), + Or(Assign(CssKeywords.DiscretionaryLigatures, CssKeywords.DiscretionaryLigatures), Assign(CssKeywords.NoDiscretionaryLigatures, CssKeywords.NoDiscretionaryLigatures)), + Or(Assign(CssKeywords.HistoricalLigatures, CssKeywords.HistoricalLigatures), Assign(CssKeywords.NoHistoricalLigatures, CssKeywords.NoHistoricalLigatures)), + Or(Assign(CssKeywords.Contextual, CssKeywords.Contextual), Assign(CssKeywords.NoContextual, CssKeywords.NoContextual)))); + + /// + /// Represents a converter for the transform-box property. + /// https://www.w3.org/TR/css-transforms-1/#transform-box + /// + public static readonly IValueConverter TransformBoxConverter = Or( + Assign(CssKeywords.ContentBox, CssKeywords.ContentBox), + Assign(CssKeywords.BorderBox, CssKeywords.BorderBox), + Assign(CssKeywords.FillBox, CssKeywords.FillBox), + Assign(CssKeywords.StrokeBox, CssKeywords.StrokeBox), + Assign(CssKeywords.ViewBox, CssKeywords.ViewBox)); + + /// + /// Represents a converter for a single transition-behavior value. + /// https://www.w3.org/TR/css-transitions-2/#transition-behavior-property + /// + public static readonly IValueConverter TransitionBehaviorConverter = Or( + Assign(CssKeywords.Normal, CssKeywords.Normal), + Assign(CssKeywords.AllowDiscrete, CssKeywords.AllowDiscrete)); + /// /// Represents a converter for the DirectionMode enumeration. /// @@ -1085,7 +1153,7 @@ static class ValueConverters /// /// Represents a converter for the DisplayMode enumeration. /// - public static readonly IValueConverter DisplayModeConverter = Map.DisplayModes.ToConverter(); + public static readonly IValueConverter DisplayModeConverter = new DisplayValueConverter(); /// /// Represents a converter for the ClearMode enumeration. @@ -1102,7 +1170,7 @@ static class ValueConverters /// /// Represents a converter for the FontStyle enumeration. /// - public static readonly IValueConverter FontStyleConverter = Map.FontStyles.ToConverter(); + public static readonly IValueConverter FontStyleConverter = new FontStyleValueConverter(); /// /// Represents a converter for the FontWeight enumeration. @@ -1271,7 +1339,7 @@ static class ValueConverters /// /// Represents a converter for the TextOverflow enumeration. /// - public static readonly IValueConverter TextOverflowConverter = Map.TextOverflows.ToConverter(); + public static readonly IValueConverter TextOverflowConverter = Map.TextOverflows.ToConverter().Many(1, 2); /// /// Represents a converter for the BorderImageRepeat property. @@ -1296,32 +1364,32 @@ static class ValueConverters /// /// Represents a converter for the JustifyContent enumeration. /// - public static readonly IValueConverter JustifyContentConverter = Map.JustifyContentModes.ToConverter(); + public static readonly IValueConverter JustifyContentConverter = new BoxAlignmentValueConverter(Map.JustifyContentModes); /// /// Represents a converter for the AlignContent enumeration. /// - public static readonly IValueConverter AlignContentConverter = Map.AlignContentModes.ToConverter(); + public static readonly IValueConverter AlignContentConverter = new BoxAlignmentValueConverter(Map.AlignContentModes); /// /// Represents a converter for the AlignSelf enumeration. /// - public static readonly IValueConverter AlignSelfConverter = Map.AlignSelfModes.ToConverter(); + public static readonly IValueConverter AlignSelfConverter = new BoxAlignmentValueConverter(Map.AlignSelfModes); /// /// Represents a converter for the AlignItems enumeration. /// - public static readonly IValueConverter AlignItemsConverter = Map.AlignItemsModes.ToConverter(); + public static readonly IValueConverter AlignItemsConverter = new BoxAlignmentValueConverter(Map.AlignItemsModes); /// /// Represents a converter for the JustifyItems enumeration. /// - public static readonly IValueConverter JustifyItemsConverter = Map.JustifyItemsModes.ToConverter(); + public static readonly IValueConverter JustifyItemsConverter = new BoxAlignmentValueConverter(Map.JustifyItemsModes); /// /// Represents a converter for the JustifySelf enumeration. /// - public static readonly IValueConverter JustifySelfConverter = Map.JustifySelfModes.ToConverter(); + public static readonly IValueConverter JustifySelfConverter = new BoxAlignmentValueConverter(Map.JustifySelfModes); /// /// Represents a converter for the FlexDirection enumeration. @@ -1349,9 +1417,12 @@ static class ValueConverters public static readonly IValueConverter FootnotePolicyConverter = Map.FootnotePolicies.ToConverter(); /// - /// Represents a converter for the Visibility enumeration. + /// Represents a converter for the content-visibility property. /// - public static readonly IValueConverter ContentVisibilityConverter = Map.Visibilities.ToConverter(); + public static readonly IValueConverter ContentVisibilityConverter = Or( + Assign(CssKeywords.Visible, CssKeywords.Visible), + Assign(CssKeywords.Auto, CssKeywords.Auto), + Assign(CssKeywords.Hidden, CssKeywords.Hidden)); /// /// Represents a converter for the ScrollSnapAlignment enumeration. @@ -1426,12 +1497,33 @@ static class ValueConverters /// public static readonly IValueConverter ScrollPaddingConverter = AutoLengthOrPercentConverter; + /// + /// Represents a converter for the text-indent property. + /// https://drafts.csswg.org/css-text-3/#text-indent-property + /// + public static readonly IValueConverter TextIndentConverter = Or(FromParser(ParseTextIndent), LengthOrPercentConverter); + + /// + /// Represents a fit-content(length-percentage) function. + /// + public static readonly IValueConverter FitContentFunctionConverter = FromParser(ParseFitContent); + /// /// Represents a value for a width. /// public static readonly IValueConverter WidthConverter = Or( LengthOrPercentConverter, Auto, + FitContentFunctionConverter, + Map.Sizings.ToConverter()); + + /// + /// Represents a value for a maximum width. + /// + public static readonly IValueConverter MaxWidthConverter = Or( + LengthOrPercentConverter, + None, + FitContentFunctionConverter, Map.Sizings.ToConverter()); /// @@ -1439,7 +1531,7 @@ static class ValueConverters /// public static readonly IValueConverter FontSizeConverter = Or( LengthOrPercentConverter, - Map.FontSizes.ToConverter()); + FromParser(UnitParser.ParseFontSizeKeyword)); #endregion @@ -1557,7 +1649,7 @@ static class ValueConverters /// Represents a converter for the aspect-ratio property (auto | <ratio>). /// https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio /// - public static readonly IValueConverter AspectRatioConverter = Or(Auto, RatioConverter); + public static readonly IValueConverter AspectRatioConverter = Or(FromParser(ParseAutoRatio), Auto, RatioConverter); /// /// Represents a converter for the overflow-anchor property (auto | none). @@ -1586,6 +1678,7 @@ static class ValueConverters public static readonly IValueConverter OptionalImageSourceConverter = Or( UrlConverter, GradientConverter, + FromParser(CompoundParser.ParseImageSet), None); /// @@ -1746,7 +1839,7 @@ public static IValueConverter WithBorderSide(ICssValue lineWidth, ICssValue line Or(LineStyleConverter, VarConverter).Option(lineStyle), Or(CurrentColorConverter, VarConverter).Option(lineColor))); - public static readonly IValueConverter GridTemplateConverter = Or(None, TrackListConverter.Exclusive(), AutoTrackListConverter.Exclusive()); + public static readonly IValueConverter GridTemplateConverter = Or(None, FromParser(GridParser.ParseSubgrid), TrackListConverter.Exclusive(), AutoTrackListConverter.Exclusive()); public static readonly IValueConverter GridAutoConverter = TrackSizeConverter.Many(); @@ -1770,6 +1863,112 @@ private static IValueConverter FromParser(Func converter) private static IValueConverter FromParser(Func converter) where T : class, ICssValue => new ClassValueConverter(converter); + private static CssNumberValue? ParseWeightNumber(this StringSource source) + { + var pos = source.Index; + var number = source.ParseNumber(); + + if (number.HasValue && number.Value.Value >= 1.0 && number.Value.Value <= 1000.0) + { + return number; + } + + source.BackTo(pos); + return null; + } + + private static CssTupleValue ParseTextIndent(this StringSource source) + { + var pos = source.Index; + var length = default(ICssValue); + var hanging = default(ICssValue); + var eachLine = default(ICssValue); + + for (var i = 0; i < 3; i++) + { + var value = length is null ? LengthOrPercentConverter.Convert(source) : null; + + if (value is not null) + { + length = value; + } + else if (hanging is null && source.IsIdentifier(CssKeywords.Hanging)) + { + hanging = new CssIdentifierValue(CssKeywords.Hanging); + } + else if (eachLine is null && source.IsIdentifier(CssKeywords.EachLine)) + { + eachLine = new CssIdentifierValue(CssKeywords.EachLine); + } + else + { + break; + } + + source.SkipSpacesAndComments(); + } + + // A plain length is handled by the length converter. + if (length is not null && (hanging is not null || eachLine is not null)) + { + return new CssTupleValue(new[] { length, hanging, eachLine }.Where(m => m is not null).ToArray()); + } + + source.BackTo(pos); + return null; + } + + private static CssTupleValue ParseAutoRatio(this StringSource source) + { + var pos = source.Index; + var ratio = default(CssRatioValue?); + + if (source.IsIdentifier(CssKeywords.Auto)) + { + source.SkipSpacesAndComments(); + ratio = source.ParseRatio(); + } + else + { + ratio = source.ParseRatio(); + source.SkipSpacesAndComments(); + + if (!source.IsIdentifier(CssKeywords.Auto)) + { + ratio = null; + } + } + + if (ratio.HasValue) + { + var auto = new CssConstantValue(CssKeywords.Auto, CssLengthValue.Auto); + return new CssTupleValue(new ICssValue[] { auto, ratio.Value }); + } + + source.BackTo(pos); + return null; + } + + private static CssFitContentValue ParseFitContent(this StringSource source) + { + var pos = source.Index; + + if (source.IsFunction(FunctionNames.FitContent)) + { + var dim = source.ParseDistanceOrCalc(); + var c = source.SkipSpacesAndComments(); + + if (dim != null && c == Symbols.RoundBracketClose) + { + source.Next(); + return new CssFitContentValue(dim); + } + } + + source.BackTo(pos); + return null; + } + private static CssPercentageValue? ParseOnlyPercent(this StringSource source) { var pos = source.Index; diff --git a/src/AngleSharp.Css/Values/Composites/CssEdgeOffsetValue.cs b/src/AngleSharp.Css/Values/Composites/CssEdgeOffsetValue.cs new file mode 100644 index 00000000..19bed1d3 --- /dev/null +++ b/src/AngleSharp.Css/Values/Composites/CssEdgeOffsetValue.cs @@ -0,0 +1,62 @@ +#nullable disable +namespace AngleSharp.Css.Values +{ + using AngleSharp.Css.Dom; + using AngleSharp.Text; + using System; + + /// + /// Represents a position component given as an offset from an edge, e.g., right 10px. + /// https://drafts.csswg.org/css-values-4/#position + /// + sealed class CssEdgeOffsetValue : ICssCompositeValue, IEquatable + { + #region Fields + + private readonly String _edge; + private readonly ICssValue _offset; + + #endregion + + #region ctor + + public CssEdgeOffsetValue(String edge, ICssValue offset) + { + _edge = edge; + _offset = offset; + } + + #endregion + + #region Properties + + /// + /// Gets the edge keyword, i.e., left, right, top, or bottom. + /// + public String Edge => _edge; + + /// + /// Gets the offset from the edge. + /// + public ICssValue Offset => _offset; + + public String CssText => String.Concat(_edge, " ", _offset.CssText); + + #endregion + + #region Methods + + public Boolean Equals(CssEdgeOffsetValue other) => + other is not null && _edge.Is(other._edge) && _offset.Equals(other._offset); + + Boolean IEquatable.Equals(ICssValue other) => other is CssEdgeOffsetValue value && Equals(value); + + ICssValue ICssValue.Compute(ICssComputeContext context) + { + var offset = _offset.Compute(context); + return offset is null ? null : new CssEdgeOffsetValue(_edge, offset); + } + + #endregion + } +} diff --git a/src/AngleSharp.Css/Values/Composites/CssShadowValue.cs b/src/AngleSharp.Css/Values/Composites/CssShadowValue.cs index f7c20d14..adc09d00 100644 --- a/src/AngleSharp.Css/Values/Composites/CssShadowValue.cs +++ b/src/AngleSharp.Css/Values/Composites/CssShadowValue.cs @@ -146,7 +146,7 @@ ICssValue ICssValue.Compute(ICssComputeContext context) { var offsetX = _offsetX.Compute(context); var offsetY = _offsetY.Compute(context); - var blurRadius = _blurRadius.Compute(context); + var blurRadius = _blurRadius?.Compute(context); var spreadRadius = _spreadRadius?.Compute(context); var color = (CssColorValue)((ICssValue)_color).Compute(context); return new CssShadowValue(_inset, offsetX, offsetY, blurRadius, spreadRadius, color); diff --git a/src/AngleSharp.Css/Values/CssComputeContext.cs b/src/AngleSharp.Css/Values/CssComputeContext.cs index 4b27e530..88951ece 100644 --- a/src/AngleSharp.Css/Values/CssComputeContext.cs +++ b/src/AngleSharp.Css/Values/CssComputeContext.cs @@ -4,12 +4,13 @@ namespace AngleSharp.Css.Values using System; using System.Linq; - sealed class CssComputeContext : ICssComputeContext, ILocalComputeContext, IInheritingComputeContext + sealed class CssComputeContext : ICssComputeContext, ILocalComputeContext, IInheritingComputeContext, IFontComputeContext { private readonly IRenderDevice _device; private readonly IBrowsingContext? _context; private readonly CssCustomPropertyResolver _variables; private readonly ICssProperties? _parent; + private Double? _fontSize; public CssComputeContext(IRenderDevice device, IBrowsingContext? context, ICssProperties properties, ICssProperties? parent = null) { @@ -31,6 +32,31 @@ public CssComputeContext(IRenderDevice device, IBrowsingContext? context, ICssPr public ICssValue? Resolve(String name) => _variables.Resolve(name); public ICssValue? InheritedValue(String name) => _parent?.FirstOrDefault(m => m.Name == name)?.RawValue; + + public Double ParentFontSize => ToPixels(InheritedValue(PropertyNames.FontSize)) ?? DefaultFontSize; + + public Double FontSize + { + get + { + if (!_fontSize.HasValue) + { + // The font size of the element is computed against the font size of the parent. + var declared = Properties.FirstOrDefault(m => m.Name == PropertyNames.FontSize); + var computed = declared?.Compute(this).RawValue; + _fontSize = computed is CssInitialValue ? DefaultFontSize : ToPixels(computed) ?? ParentFontSize; + } + + return _fontSize.Value; + } + } + + public Double RootFontSize => _parent is null ? FontSize : (_parent as CssStyleDeclaration)?.RootFontSize ?? DefaultFontSize; + + private Double DefaultFontSize => _device.FontSize > 0 ? _device.FontSize : 16.0; + + private static Double? ToPixels(ICssValue? value) => + value is CssLengthValue length && length.Type == CssLengthValue.Unit.Px ? length.Value : null; } } diff --git a/src/AngleSharp.Css/Values/Expressions/CssCalcAddExpression.cs b/src/AngleSharp.Css/Values/Expressions/CssCalcAddExpression.cs index ed1bd7f2..8d56f1f5 100644 --- a/src/AngleSharp.Css/Values/Expressions/CssCalcAddExpression.cs +++ b/src/AngleSharp.Css/Values/Expressions/CssCalcAddExpression.cs @@ -63,7 +63,8 @@ ICssValue ICssValue.Compute(ICssComputeContext context) return x.WithValue(result); } - return null; + // The operands cannot be combined yet, e.g., a percentage of an unknown box with a length. + return left is null || right is null ? null : new CssCalcAddExpression(left, right); } Boolean IEquatable.Equals(ICssValue other) => Object.ReferenceEquals(this, other); diff --git a/src/AngleSharp.Css/Values/Expressions/CssCalcBracketExpression.cs b/src/AngleSharp.Css/Values/Expressions/CssCalcBracketExpression.cs index b8218fe8..60186958 100644 --- a/src/AngleSharp.Css/Values/Expressions/CssCalcBracketExpression.cs +++ b/src/AngleSharp.Css/Values/Expressions/CssCalcBracketExpression.cs @@ -44,7 +44,13 @@ public CssCalcBracketExpression(ICssValue content) #region Methods - ICssValue ICssValue.Compute(ICssComputeContext context) => _content.Compute(context); + ICssValue ICssValue.Compute(ICssComputeContext context) + { + var content = _content.Compute(context); + + // An expression that could not be resolved keeps its brackets. + return content is null || content is ICssMetricValue ? content : new CssCalcBracketExpression(content); + } Boolean IEquatable.Equals(ICssValue other) => Object.ReferenceEquals(this, other); diff --git a/src/AngleSharp.Css/Values/Expressions/CssCalcDivExpression.cs b/src/AngleSharp.Css/Values/Expressions/CssCalcDivExpression.cs index 68245cc6..976b034a 100644 --- a/src/AngleSharp.Css/Values/Expressions/CssCalcDivExpression.cs +++ b/src/AngleSharp.Css/Values/Expressions/CssCalcDivExpression.cs @@ -73,7 +73,8 @@ ICssValue ICssValue.Compute(ICssComputeContext context) } } - return null; + // The operands cannot be combined yet, e.g., a percentage of an unknown box with a length. + return left is null || right is null ? null : new CssCalcDivExpression(left, right); } private static ICssValue ComputeValue(ICssValue value, ICssComputeContext context) diff --git a/src/AngleSharp.Css/Values/Expressions/CssCalcMulExpression.cs b/src/AngleSharp.Css/Values/Expressions/CssCalcMulExpression.cs index a6d2858a..f80b697e 100644 --- a/src/AngleSharp.Css/Values/Expressions/CssCalcMulExpression.cs +++ b/src/AngleSharp.Css/Values/Expressions/CssCalcMulExpression.cs @@ -73,7 +73,8 @@ ICssValue ICssValue.Compute(ICssComputeContext context) return unitLeft.WithValue(unitLeft.Value * unitlessRight.Value); } - return null; + // The operands cannot be combined yet, e.g., a percentage of an unknown box with a length. + return left is null || right is null ? null : new CssCalcMulExpression(left, right); } private static ICssValue ComputeValue(ICssValue value, ICssComputeContext context) diff --git a/src/AngleSharp.Css/Values/Expressions/CssCalcSubExpression.cs b/src/AngleSharp.Css/Values/Expressions/CssCalcSubExpression.cs index 2aa2102f..8a20a11e 100644 --- a/src/AngleSharp.Css/Values/Expressions/CssCalcSubExpression.cs +++ b/src/AngleSharp.Css/Values/Expressions/CssCalcSubExpression.cs @@ -63,7 +63,8 @@ ICssValue ICssValue.Compute(ICssComputeContext context) return x.WithValue(result); } - return null; + // The operands cannot be combined yet, e.g., a percentage of an unknown box with a length. + return left is null || right is null ? null : new CssCalcSubExpression(left, right); } Boolean IEquatable.Equals(ICssValue other) => Object.ReferenceEquals(this, other); diff --git a/src/AngleSharp.Css/Values/Functions/CssCalcValue.cs b/src/AngleSharp.Css/Values/Functions/CssCalcValue.cs index 96a07e09..63d52412 100644 --- a/src/AngleSharp.Css/Values/Functions/CssCalcValue.cs +++ b/src/AngleSharp.Css/Values/Functions/CssCalcValue.cs @@ -60,7 +60,16 @@ public CssCalcValue(ICssValue expression) #region Methods - ICssValue ICssValue.Compute(ICssComputeContext context) => _expression.Compute(context); + ICssValue ICssValue.Compute(ICssComputeContext context) + { + var result = _expression.Compute(context); + + // An expression that could not be resolved (e.g., a percentage of an unknown box with a + // length) remains a calc() of its computed operands. + return result is CssCalcAddExpression or CssCalcSubExpression or CssCalcMulExpression or CssCalcDivExpression or CssCalcBracketExpression ? + new CssCalcValue(result) : + result; + } Boolean IEquatable.Equals(ICssValue other) => Object.ReferenceEquals(this, other); diff --git a/src/AngleSharp.Css/Values/Functions/CssImageSetValue.cs b/src/AngleSharp.Css/Values/Functions/CssImageSetValue.cs new file mode 100644 index 00000000..73354cdd --- /dev/null +++ b/src/AngleSharp.Css/Values/Functions/CssImageSetValue.cs @@ -0,0 +1,80 @@ +#nullable disable +namespace AngleSharp.Css.Values +{ + using AngleSharp.Css.Converters; + using AngleSharp.Css.Dom; + using AngleSharp.Text; + using System; + using System.Linq; + + /// + /// Represents the image-set function, i.e., a set of images for different resolutions. + /// https://drafts.csswg.org/css-images-4/#image-set-notation + /// + sealed class CssImageSetValue : ICssImageValue, IEquatable + { + #region Fields + + private readonly CssTupleValue[] _options; + + #endregion + + #region ctor + + /// + /// Creates a new image-set function. + /// + /// The options, each an image with an optional resolution and type. + public CssImageSetValue(CssTupleValue[] options) + { + _options = options; + } + + #endregion + + #region Properties + + public String Name => FunctionNames.ImageSet; + + public ICssValue[] Arguments => _options; + + public String CssText => Name.CssFunction(Arguments.Join(", ")); + + #endregion + + #region Methods + + public Boolean Equals(CssImageSetValue other) => + other is not null && _options.Length == other._options.Length && _options.Select((m, i) => m.CssText.Is(other._options[i].CssText)).All(m => m); + + Boolean IEquatable.Equals(ICssValue other) => other is CssImageSetValue value && Equals(value); + + ICssValue ICssValue.Compute(ICssComputeContext context) => + new CssImageSetValue(_options.Select(m => (CssTupleValue)((ICssValue)m).Compute(context)).ToArray()); + + #endregion + + #region Type + + /// + /// Represents the type() of an image-set option, i.e., the MIME type of the image. + /// + internal sealed class TypeValue : ICssValue + { + private readonly ICssValue _type; + + public TypeValue(ICssValue type) + { + _type = type; + } + + public String CssText => FunctionNames.Type.CssFunction(_type.CssText); + + public ICssValue Compute(ICssComputeContext context) => this; + + public Boolean Equals(ICssValue other) => other is TypeValue value && value._type.Equals(_type); + } + + #endregion + } +} diff --git a/src/AngleSharp.Css/Values/Functions/CssLinearEasingValue.cs b/src/AngleSharp.Css/Values/Functions/CssLinearEasingValue.cs new file mode 100644 index 00000000..ff50807b --- /dev/null +++ b/src/AngleSharp.Css/Values/Functions/CssLinearEasingValue.cs @@ -0,0 +1,56 @@ +#nullable disable +namespace AngleSharp.Css.Values +{ + using AngleSharp.Css.Converters; + using AngleSharp.Css.Dom; + using AngleSharp.Text; + using System; + using System.Linq; + + /// + /// Represents the linear() easing function, i.e., a piecewise linear timing function. + /// https://drafts.csswg.org/css-easing-2/#the-linear-easing-function + /// + sealed class CssLinearEasingValue : ICssTimingFunctionValue, IEquatable + { + #region Fields + + private readonly CssTupleValue[] _stops; + + #endregion + + #region ctor + + /// + /// Creates a new linear easing function. + /// + /// The stops, each an output number with up to two input percentages. + public CssLinearEasingValue(CssTupleValue[] stops) + { + _stops = stops; + } + + #endregion + + #region Properties + + public String Name => CssKeywords.Linear; + + public ICssValue[] Arguments => _stops; + + public String CssText => Name.CssFunction(Arguments.Join(", ")); + + #endregion + + #region Methods + + public Boolean Equals(CssLinearEasingValue other) => + other is not null && _stops.Length == other._stops.Length && _stops.Select((m, i) => m.CssText.Is(other._stops[i].CssText)).All(m => m); + + Boolean IEquatable.Equals(ICssValue other) => other is CssLinearEasingValue value && Equals(value); + + ICssValue ICssValue.Compute(ICssComputeContext context) => this; + + #endregion + } +} diff --git a/src/AngleSharp.Css/Values/Functions/CssMathValue.cs b/src/AngleSharp.Css/Values/Functions/CssMathValue.cs new file mode 100644 index 00000000..4d304109 --- /dev/null +++ b/src/AngleSharp.Css/Values/Functions/CssMathValue.cs @@ -0,0 +1,80 @@ +#nullable disable +namespace AngleSharp.Css.Values +{ + using AngleSharp.Css.Converters; + using AngleSharp.Css.Dom; + using AngleSharp.Text; + using System; + using System.Linq; + + /// + /// Represents a comparison function, i.e., min(), max(), or clamp(). + /// https://drafts.csswg.org/css-values-4/#comp-func + /// + sealed class CssMathValue : ICssFunctionValue + { + #region Fields + + private readonly String _name; + private readonly ICssValue[] _arguments; + + #endregion + + #region ctor + + public CssMathValue(String name, ICssValue[] arguments) + { + _name = name; + _arguments = arguments; + } + + #endregion + + #region Properties + + public String Name => _name; + + public ICssValue[] Arguments => _arguments; + + public String CssText => _name.CssFunction(_arguments.Join(", ")); + + #endregion + + #region Methods + + ICssValue ICssValue.Compute(ICssComputeContext context) + { + var arguments = _arguments.Select(m => m.Compute(context)).ToArray(); + + if (arguments.All(m => m is ICssMetricValue) && arguments.Length > 0) + { + var metrics = arguments.Cast().ToArray(); + var unit = metrics[0].UnitString; + + if (metrics.All(m => m.UnitString == unit)) + { + if (_name.Is(FunctionNames.Min)) + { + return metrics[0].WithValue(metrics.Min(m => m.Value)); + } + else if (_name.Is(FunctionNames.Max)) + { + return metrics[0].WithValue(metrics.Max(m => m.Value)); + } + else if (_name.Is(FunctionNames.Clamp) && metrics.Length == 3) + { + // clamp(MIN, VAL, MAX) is max(MIN, min(VAL, MAX)) + return metrics[0].WithValue(Math.Max(metrics[0].Value, Math.Min(metrics[1].Value, metrics[2].Value))); + } + } + } + + // The arguments cannot be compared yet (e.g., a percentage of an unknown box). + return arguments.Any(m => m is null) ? null : new CssMathValue(_name, arguments); + } + + Boolean IEquatable.Equals(ICssValue other) => Object.ReferenceEquals(this, other); + + #endregion + } +} diff --git a/src/AngleSharp.Css/Values/Functions/CssRadialGradientValue.cs b/src/AngleSharp.Css/Values/Functions/CssRadialGradientValue.cs index 204e04a8..764b9af6 100644 --- a/src/AngleSharp.Css/Values/Functions/CssRadialGradientValue.cs +++ b/src/AngleSharp.Css/Values/Functions/CssRadialGradientValue.cs @@ -102,7 +102,7 @@ public ICssValue[] Arguments size, CssKeywords.At, _center.CssText - }))); + }.Where(m => !String.IsNullOrEmpty(m))))); } foreach (var stop in _stops) diff --git a/src/AngleSharp.Css/Values/Functions/CssStepsValue.cs b/src/AngleSharp.Css/Values/Functions/CssStepsValue.cs index d05bdd27..9fd342a6 100644 --- a/src/AngleSharp.Css/Values/Functions/CssStepsValue.cs +++ b/src/AngleSharp.Css/Values/Functions/CssStepsValue.cs @@ -17,6 +17,7 @@ public sealed class CssStepsValue : ICssTimingFunctionValue, IEquatableIt must be a positive integer (greater than 0). /// Optional: If not specified then the change occurs at the end. public CssStepsValue(ICssValue intervals, Boolean start = false) + : this(intervals, start ? CssKeywords.Start : null) + { + } + + /// + /// Creates a new steps timing-function with the given step position keyword, e.g., + /// jump-none. A missing position is the same as end. + /// + internal CssStepsValue(ICssValue intervals, String position) { _intervals = intervals; - _start = start; + _position = position; + _start = position is not null && (position.Is(CssKeywords.Start) || position.Is(CssKeywords.JumpStart)); } #endregion @@ -56,9 +67,9 @@ public ICssValue[] Arguments _intervals }; - if (_start) + if (_position is not null && !_position.Is(CssKeywords.End) && !_position.Is(CssKeywords.JumpEnd)) { - args.Add(new CssIdentifierValue(CssKeywords.Start)); + args.Add(new CssIdentifierValue(_position)); } return args.ToArray(); @@ -72,7 +83,7 @@ public String CssText { get { - if (!_intervals.Equals(CssIntegerValue.One)) + if (!_intervals.Equals(CssIntegerValue.One) || (_position is not null && !_position.Is(CssKeywords.Start) && !_position.Is(CssKeywords.End))) { return Name.CssFunction(Arguments.Join(", ")); } @@ -111,7 +122,7 @@ public Boolean Equals(CssStepsValue other) if (other is not null) { var comparer = EqualityComparer.Default; - return _start == other._start && comparer.Equals(_intervals, other._intervals); + return _start == other._start && comparer.Equals(_intervals, other._intervals) && NormalizedPosition.Is(other.NormalizedPosition); } return false; @@ -119,6 +130,12 @@ public Boolean Equals(CssStepsValue other) Boolean IEquatable.Equals(ICssValue other) => other is CssStepsValue value && Equals(value); + /// + /// Gets the step position, where start and end are the same as jump-start and jump-end. + /// + private String NormalizedPosition => _start ? CssKeywords.Start : + _position is null || _position.Is(CssKeywords.JumpEnd) ? CssKeywords.End : _position; + ICssValue ICssValue.Compute(ICssComputeContext context) => this; #endregion diff --git a/src/AngleSharp.Css/Values/IFontComputeContext.cs b/src/AngleSharp.Css/Values/IFontComputeContext.cs new file mode 100644 index 00000000..89af48b8 --- /dev/null +++ b/src/AngleSharp.Css/Values/IFontComputeContext.cs @@ -0,0 +1,24 @@ +namespace AngleSharp.Css.Values; + +using System; + +/// +/// A compute context that knows the font sizes relevant for resolving relative lengths of an element. +/// +interface IFontComputeContext +{ + /// + /// Gets the computed font size (in px) of the parent element. + /// + Double ParentFontSize { get; } + + /// + /// Gets the computed font size (in px) of the element itself. + /// + Double FontSize { get; } + + /// + /// Gets the computed font size (in px) of the root element. + /// + Double RootFontSize { get; } +} diff --git a/src/AngleSharp.Css/Values/IRelativeComputeContext.cs b/src/AngleSharp.Css/Values/IRelativeComputeContext.cs new file mode 100644 index 00000000..3f92002c --- /dev/null +++ b/src/AngleSharp.Css/Values/IRelativeComputeContext.cs @@ -0,0 +1,30 @@ +namespace AngleSharp.Css.Values; + +using System; + +/// +/// A compute context that knows the references for resolving the relative lengths of a property. +/// +interface IRelativeComputeContext +{ + /// + /// Gets the font size (in px) em resolves against, if known. + /// + Double? EmBase { get; } + + /// + /// Gets the font size (in px) of the root element rem resolves against, if known. + /// + Double? RootFontSize { get; } + + /// + /// Gets if percentages refer to a box that is only known during layout, i.e., if they + /// must remain percentages in the computed value. + /// + Boolean KeepsPercentages { get; } + + /// + /// Gets the length (in px) percentages resolve against, if known. + /// + Double? PercentageBasis { get; } +} diff --git a/src/AngleSharp.Css/Values/Multiples/CssFlowRelativeValue.cs b/src/AngleSharp.Css/Values/Multiples/CssFlowRelativeValue.cs index 0c9c7c5e..8527496e 100644 --- a/src/AngleSharp.Css/Values/Multiples/CssFlowRelativeValue.cs +++ b/src/AngleSharp.Css/Values/Multiples/CssFlowRelativeValue.cs @@ -128,6 +128,12 @@ IEnumerator IEnumerable.GetEnumerator() ICssValue ICssValue.Compute(ICssComputeContext context) { var values = _values.Select(v => (T)v.Compute(context)).ToArray(); + // Keep the specific type, e.g., such that shorthands can merge the computed values. + if (this is CssFlowRelativeValue) + { + return new CssFlowRelativeValue((ICssValue[])(object)values); + } + return new CssFlowRelativeValue(values); } diff --git a/src/AngleSharp.Css/Values/Multiples/CssPeriodicValue.cs b/src/AngleSharp.Css/Values/Multiples/CssPeriodicValue.cs index f828861b..3d89f575 100644 --- a/src/AngleSharp.Css/Values/Multiples/CssPeriodicValue.cs +++ b/src/AngleSharp.Css/Values/Multiples/CssPeriodicValue.cs @@ -157,6 +157,12 @@ IEnumerator IEnumerable.GetEnumerator() ICssValue ICssValue.Compute(ICssComputeContext context) { var values = _values.Select(v => (T)v.Compute(context)).ToArray(); + // Keep the specific type, e.g., such that shorthands can merge the computed values. + if (this is CssPeriodicValue) + { + return new CssPeriodicValue((ICssValue[])(object)values); + } + return new CssPeriodicValue(values); } diff --git a/src/AngleSharp.Css/Values/Multiples/CssRadiusValue.cs b/src/AngleSharp.Css/Values/Multiples/CssRadiusValue.cs index 1300b279..1a0681bf 100644 --- a/src/AngleSharp.Css/Values/Multiples/CssRadiusValue.cs +++ b/src/AngleSharp.Css/Values/Multiples/CssRadiusValue.cs @@ -133,6 +133,12 @@ ICssValue ICssValue.Compute(ICssComputeContext context) values[i] = (T)ComputeValue(_values[i], context); } + // Keep the specific type, e.g., such that shorthands can merge the computed radii. + if (this is CssRadiusValue) + { + return new CssRadiusValue((ICssValue[])(object)values); + } + return new CssRadiusValue(values); } diff --git a/src/AngleSharp.Css/Values/Multiples/CssTupleValue.cs b/src/AngleSharp.Css/Values/Multiples/CssTupleValue.cs index b57aa821..d02ae7bf 100644 --- a/src/AngleSharp.Css/Values/Multiples/CssTupleValue.cs +++ b/src/AngleSharp.Css/Values/Multiples/CssTupleValue.cs @@ -102,6 +102,12 @@ ICssValue ICssValue.Compute(ICssComputeContext context) // line) - calling .Compute() on it unconditionally threw a NullReferenceException for // that ordinary case. var items = _items.Select(v => v == null ? v : (T)v.Compute(context)).ToArray(); + // Keep the specific type, e.g., such that shorthands can merge the computed tuple. + if (this is CssTupleValue) + { + return new CssTupleValue((ICssValue[])(object)items, _separator); + } + return new CssTupleValue(items, _separator); } diff --git a/src/AngleSharp.Css/Values/Primitives/CssCounterDefinitionValue.cs b/src/AngleSharp.Css/Values/Primitives/CssCounterDefinitionValue.cs index e11994dc..36aad9a8 100644 --- a/src/AngleSharp.Css/Values/Primitives/CssCounterDefinitionValue.cs +++ b/src/AngleSharp.Css/Values/Primitives/CssCounterDefinitionValue.cs @@ -42,7 +42,7 @@ public CssCounterDefinitionValue(String identifier, String listStyle, String sep /// public String CssText => _separator == null ? FunctionNames.Counter.CssFunction(Combine(_identifier)) : - FunctionNames.Counters.CssFunction(Combine(String.Concat(_identifier, " ", _separator))); + FunctionNames.Counters.CssFunction(Combine(String.Concat(_identifier, ", ", _separator.CssString()))); /// /// Gets the identifier of the counter. diff --git a/src/AngleSharp.Css/Values/Primitives/CssFontSizeKeywordValue.cs b/src/AngleSharp.Css/Values/Primitives/CssFontSizeKeywordValue.cs new file mode 100644 index 00000000..2dd6ee21 --- /dev/null +++ b/src/AngleSharp.Css/Values/Primitives/CssFontSizeKeywordValue.cs @@ -0,0 +1,71 @@ +#nullable enable +namespace AngleSharp.Css.Values +{ + using AngleSharp.Css.Dom; + using AngleSharp.Text; + using System; + + /// + /// Represents a font-size keyword, e.g., medium or larger. An absolute keyword computes + /// to a multiple of the device's default font size, a relative keyword to a multiple of + /// the parent's font size. + /// + sealed class CssFontSizeKeywordValue : ICssPrimitiveValue, IEquatable + { + #region Fields + + private readonly String _keyword; + private readonly CssLengthValue _size; + + #endregion + + #region ctor + + public CssFontSizeKeywordValue(String keyword, CssLengthValue size) + { + _keyword = keyword; + _size = size; + } + + #endregion + + #region Properties + + public String CssText => _keyword; + + /// + /// Gets the size relative to the default (em) or the parent's (%) font size. + /// + public CssLengthValue Size => _size; + + #endregion + + #region Methods + + public Boolean Equals(CssFontSizeKeywordValue? other) => other is not null && _keyword.Is(other._keyword); + + Boolean IEquatable.Equals(ICssValue? other) => other is CssFontSizeKeywordValue value && Equals(value); + + public override Boolean Equals(Object? obj) => obj is CssFontSizeKeywordValue value && Equals(value); + + public override Int32 GetHashCode() => _keyword.GetHashCode(); + + ICssValue ICssValue.Compute(ICssComputeContext context) + { + var basis = _size.Type == CssLengthValue.Unit.Percent ? + (context as IRelativeComputeContext)?.PercentageBasis : + context.Device?.FontSize; + + if (basis.HasValue && basis.Value > 0) + { + var factor = _size.Type == CssLengthValue.Unit.Percent ? _size.Value * 0.01 : _size.Value; + ICssValue length = new CssLengthValue(basis.Value * factor, CssLengthValue.Unit.Px); + return length.Compute(context); + } + + return this; + } + + #endregion + } +} diff --git a/src/AngleSharp.Css/Values/Primitives/CssLengthValue.cs b/src/AngleSharp.Css/Values/Primitives/CssLengthValue.cs index b95c62ae..fe08e1cd 100644 --- a/src/AngleSharp.Css/Values/Primitives/CssLengthValue.cs +++ b/src/AngleSharp.Css/Values/Primitives/CssLengthValue.cs @@ -174,6 +174,18 @@ public String UnitString Unit.Vmin => UnitNames.Vmin, Unit.Vmax => UnitNames.Vmax, Unit.Percent => UnitNames.Percent, + Unit.Svw => UnitNames.Svw, + Unit.Svh => UnitNames.Svh, + Unit.Svmin => UnitNames.Svmin, + Unit.Svmax => UnitNames.Svmax, + Unit.Lvw => UnitNames.Lvw, + Unit.Lvh => UnitNames.Lvh, + Unit.Lvmin => UnitNames.Lvmin, + Unit.Lvmax => UnitNames.Lvmax, + Unit.Dvw => UnitNames.Dvw, + Unit.Dvh => UnitNames.Dvh, + Unit.Dvmin => UnitNames.Dvmin, + Unit.Dvmax => UnitNames.Dvmax, _ => String.Empty, }; } @@ -252,6 +264,24 @@ ICssValue ICssValue.Compute(ICssComputeContext context) return this; } + if (context is IRelativeComputeContext relative) + { + switch (_unit) + { + case Unit.Percent when relative.PercentageBasis.HasValue: + return new CssLengthValue(_value * 0.01 * relative.PercentageBasis.Value, Unit.Px, true); + case Unit.Percent when relative.KeepsPercentages: + return this; + case Unit.Em when relative.EmBase.HasValue: + return new CssLengthValue(_value * relative.EmBase.Value, Unit.Px, true); + case Unit.Ex or Unit.Ch when relative.EmBase.HasValue: + // Without font metrics, 1ex and 1ch are taken as 0.5em. + return new CssLengthValue(_value * 0.5 * relative.EmBase.Value, Unit.Px, true); + case Unit.Rem when relative.RootFontSize.HasValue: + return new CssLengthValue(_value * relative.RootFontSize.Value, Unit.Px, true); + } + } + if (_unit != CssLengthValue.Unit.Px) { var px = ToPixel(context.Device); @@ -310,6 +340,18 @@ public static Unit GetUnit(String s) "vmax" => Unit.Vmax, "vmin" => Unit.Vmin, "vw" => Unit.Vw, + "svw" => Unit.Svw, + "svh" => Unit.Svh, + "svmin" => Unit.Svmin, + "svmax" => Unit.Svmax, + "lvw" => Unit.Lvw, + "lvh" => Unit.Lvh, + "lvmin" => Unit.Lvmin, + "lvmax" => Unit.Lvmax, + "dvw" => Unit.Dvw, + "dvh" => Unit.Dvh, + "dvmin" => Unit.Dvmin, + "dvmax" => Unit.Dvmax, "%" => Unit.Percent, _ => Unit.None, }; @@ -332,7 +374,7 @@ public static Unit GetUnit(String s) /// The number of pixels represented by the current length. public Double ToPixel(IRenderDimensions renderDimensions, RenderMode mode) { - switch (_unit) + switch (GetViewportUnit(_unit)) { case Unit.In: // 1 in = 2.54 cm return _value * 96.0; @@ -352,6 +394,11 @@ public Double ToPixel(IRenderDimensions renderDimensions, RenderMode mode) case Unit.Em: CheckForValidRenderDimensionsForFont(renderDimensions); return _value * renderDimensions.FontSize; + case Unit.Ex: + case Unit.Ch: + // without font metrics 1ex and 1ch are taken as 0.5em + CheckForValidRenderDimensionsForFont(renderDimensions); + return _value * 0.5 * renderDimensions.FontSize; case Unit.Rem: // here we dont actually know the root font size but currently the only IRenderDimensions used is // the IRenderDevice meaning its always the root font size @@ -388,7 +435,7 @@ public Double To(Unit unit, IRenderDimensions renderDimensions, RenderMode mode) { var value = ToPixel(renderDimensions, mode); - switch (unit) + switch (GetViewportUnit(unit)) { case Unit.In: // 1 in = 2.54 cm return value / 96.0; @@ -432,6 +479,19 @@ public Double To(Unit unit, IRenderDimensions renderDimensions, RenderMode mode) } } + /// + /// Maps the small, large, and dynamic viewport units to the plain viewport units. A + /// render device only knows a single viewport, i.e., they are all the same. + /// + private static Unit GetViewportUnit(Unit unit) => unit switch + { + Unit.Svw or Unit.Lvw or Unit.Dvw => Unit.Vw, + Unit.Svh or Unit.Lvh or Unit.Dvh => Unit.Vh, + Unit.Svmin or Unit.Lvmin or Unit.Dvmin => Unit.Vmin, + Unit.Svmax or Unit.Lvmax or Unit.Dvmax => Unit.Vmax, + _ => unit, + }; + private void CheckForValidRenderDimensions(IRenderDimensions renderDimensions, RenderMode mode) { if (renderDimensions == null || mode == RenderMode.Undefined || (mode == RenderMode.Horizontal ? renderDimensions.RenderWidth : renderDimensions.RenderHeight) <= 0) @@ -525,7 +585,67 @@ public enum Unit : byte /// The value is relative to a fixed (external) value, that is context /// dependent. 1% = 1/100 of the external value. /// - Percent + Percent, + /// + /// The value is relative to the small viewport width. + /// 1svw = 1/100 of the small viewport width. + /// + Svw, + /// + /// The value is relative to the small viewport height. + /// 1svh = 1/100 of the small viewport height. + /// + Svh, + /// + /// The value is relative to the small minimum viewport dimension. + /// 1svmin = 1/100 of the small minimum viewport dimension. + /// + Svmin, + /// + /// The value is relative to the small maximum viewport dimension. + /// 1svmax = 1/100 of the small maximum viewport dimension. + /// + Svmax, + /// + /// The value is relative to the large viewport width. + /// 1lvw = 1/100 of the large viewport width. + /// + Lvw, + /// + /// The value is relative to the large viewport height. + /// 1lvh = 1/100 of the large viewport height. + /// + Lvh, + /// + /// The value is relative to the large minimum viewport dimension. + /// 1lvmin = 1/100 of the large minimum viewport dimension. + /// + Lvmin, + /// + /// The value is relative to the large maximum viewport dimension. + /// 1lvmax = 1/100 of the large maximum viewport dimension. + /// + Lvmax, + /// + /// The value is relative to the dynamic viewport width. + /// 1dvw = 1/100 of the dynamic viewport width. + /// + Dvw, + /// + /// The value is relative to the dynamic viewport height. + /// 1dvh = 1/100 of the dynamic viewport height. + /// + Dvh, + /// + /// The value is relative to the dynamic minimum viewport dimension. + /// 1dvmin = 1/100 of the dynamic minimum viewport dimension. + /// + Dvmin, + /// + /// The value is relative to the dynamic maximum viewport dimension. + /// 1dvmax = 1/100 of the dynamic maximum viewport dimension. + /// + Dvmax } #endregion diff --git a/src/AngleSharp.Css/Values/Primitives/CssResolutionValue.cs b/src/AngleSharp.Css/Values/Primitives/CssResolutionValue.cs index 7887132b..00bf456a 100644 --- a/src/AngleSharp.Css/Values/Primitives/CssResolutionValue.cs +++ b/src/AngleSharp.Css/Values/Primitives/CssResolutionValue.cs @@ -69,6 +69,7 @@ public String UnitString Unit.Dpcm => UnitNames.Dpcm, Unit.Dpi => UnitNames.Dpi, Unit.Dppx => UnitNames.Dppx, + Unit.X => UnitNames.X, _ => String.Empty, }; } @@ -80,7 +81,7 @@ public String UnitString ICssValue ICssValue.Compute(ICssComputeContext context) { - if (_unit != Unit.Dppx) + if (_unit != Unit.Dppx && _unit != Unit.X) { var dots = ToDotsPerPixel(); return new CssResolutionValue(dots, Unit.Dppx); @@ -121,6 +122,7 @@ public static Unit GetUnit(String s) "dpcm" => Unit.Dpcm, "dpi" => Unit.Dpi, "dppx" => Unit.Dppx, + "x" => Unit.X, _ => Unit.None, }; } @@ -196,6 +198,10 @@ public enum Unit : byte /// The value is a resolution (dots per px). /// Dppx, + /// + /// The value is a resolution (dots per px), given by its alias x. + /// + X, } #endregion diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 947e64f6..627d54f6 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.1.5 + 1.2.0 enable latest true From cc8b56c5966fc2cfceb3856ca25cf5e3d8a8d73f Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Tue, 20 Oct 2026 20:02:57 +0200 Subject: [PATCH 3/4] Further fixes and improvements --- CHANGELOG.md | 5 +- .../FlexShorthandComputedStyleTests.cs | 50 ++++++++++++++++++ .../Styling/WritingModeComputedStyleTests.cs | 52 +++++++++++++++++++ src/AngleSharp.Css/Constants/PropertyNames.cs | 5 ++ .../Declarations/FlexDeclaration.cs | 36 +++++++++++-- .../TextOrientationDeclaration.cs | 20 +++++++ .../Declarations/WritingModeDeclaration.cs | 16 +++++- .../Factories/DefaultDeclarationFactory.cs | 7 +++ 8 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 src/AngleSharp.Css.Tests/Styling/FlexShorthandComputedStyleTests.cs create mode 100644 src/AngleSharp.Css.Tests/Styling/WritingModeComputedStyleTests.cs create mode 100644 src/AngleSharp.Css/Declarations/TextOrientationDeclaration.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 04fc3c34..a600b32e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ Released on Wednesday, October 21 2026. - Fixed `counters()` losing the comma and the quotes of its separator string - Fixed `color: currentcolor` not computing to the inherited color - Fixed `flex: none` not expanding to `0 0 auto` +- Fixed `flex` shorthand leaving omitted longhands unset instead of `1` for the factors and `0%` for the basis (e.g., `flex: 1` is `1 1 0%`) +- Fixed computed `flex` shorthand dropping longhands at their initial value (e.g., `flex-basis: 40px` reading back as `40px` instead of `0 1 40px`) +- Fixed legacy `writing-mode` values (`lr`, `lr-tb`, `rl`, `rl-tb`, `tb`, `tb-rl`) being rejected instead of mapping to `horizontal-tb` / `vertical-rl` - Fixed `content-visibility` accepting `collapse` instead of `auto` - Fixed `min-width`, `min-height`, and their logical counterparts rejecting `auto` - Fixed radial gradients without a size serializing a double space @@ -36,7 +39,7 @@ Released on Wednesday, October 21 2026. - Added support for font weights in the range 1 to 1000 (also in `@font-face`) and `font-style: oblique ` - Added support for the `jump-*` positions of `steps()` and the `linear()` easing function - Added support for `plus-lighter`, `image-rendering: pixelated`, `background-clip: text`, `border-image-repeat: space`, `outline-style: auto`, `overflow-wrap: anywhere`, `white-space: break-spaces`, two-value `text-overflow`, and `float` / `clear` with `inline-start` / `inline-end` -- Added `font-variant-numeric`, `font-variant-ligatures`, `transform-box`, and `transition-behavior` properties +- Added `font-variant-numeric`, `font-variant-ligatures`, `text-orientation`, `transform-box`, and `transition-behavior` properties # 1.1.4 diff --git a/src/AngleSharp.Css.Tests/Styling/FlexShorthandComputedStyleTests.cs b/src/AngleSharp.Css.Tests/Styling/FlexShorthandComputedStyleTests.cs new file mode 100644 index 00000000..0756daa1 --- /dev/null +++ b/src/AngleSharp.Css.Tests/Styling/FlexShorthandComputedStyleTests.cs @@ -0,0 +1,50 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Styling +{ + using AngleSharp.Css.Dom; + using AngleSharp.Dom; + using NUnit.Framework; + using static CssConstructionFunctions; + + /// + /// The `flex` shorthand does not expand into the longhands CSS Flexbox 1 (7.1.1) prescribes: + /// a unitless `flex: <grow> [<shrink>]` must set `flex-basis` to `0%` (it is left + /// unset, so `flex: 1` sizes items from their content), a basis-only `flex: <basis>` must + /// set `flex-grow` to 1 (left unset, so the item never grows), and omitted factors must take + /// `1`. The computed shorthand, rebuilt from the longhands, is also lossy: a lone + /// `flex-basis: 40px` reads back as `flex: 40px`, which would mean `1 1 40px`. Found in a + /// downstream renderer (AngleSharp.Renderer), which therefore has to read the longhands only. + /// + [TestFixture] + public class FlexShorthandComputedStyleTests + { + [TestCase("1", "1", "1", "0%")] + [TestCase("2 3", "2", "3", "0%")] + [TestCase("1 50px", "1", "1", "50px")] + [TestCase("100px", "1", "1", "100px")] + [TestCase("auto", "1", "1", "auto")] + [TestCase("initial", "0", "1", "auto")] + public void FlexShorthandExpandsIntoItsLonghands(string flex, string grow, string shrink, string basis) + { + var style = Compute($"flex: {flex}"); + + Assert.AreEqual(grow, style.GetPropertyValue("flex-grow")); + Assert.AreEqual(shrink, style.GetPropertyValue("flex-shrink")); + Assert.AreEqual(basis, style.GetPropertyValue("flex-basis")); + } + + [TestCase("flex-basis: 40px", "0 1 40px")] + [TestCase("flex-grow: 2", "2 1 auto")] + [TestCase("flex-shrink: 0", "0 0 auto")] + public void ComputedFlexShorthandKeepsTheLonghandsMeaning(string declaration, string expected) + { + Assert.AreEqual(expected, Compute(declaration).GetPropertyValue("flex")); + } + + private static ICssStyleDeclaration Compute(string declarations) + { + var document = ParseDocument($"
    x
    "); + return document.GetElementById("target").ComputeCurrentStyle(); + } + } +} diff --git a/src/AngleSharp.Css.Tests/Styling/WritingModeComputedStyleTests.cs b/src/AngleSharp.Css.Tests/Styling/WritingModeComputedStyleTests.cs new file mode 100644 index 00000000..9a6c89b7 --- /dev/null +++ b/src/AngleSharp.Css.Tests/Styling/WritingModeComputedStyleTests.cs @@ -0,0 +1,52 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Styling +{ + using AngleSharp.Css.Dom; + using AngleSharp.Dom; + using NUnit.Framework; + using static CssConstructionFunctions; + + /// + /// CSS Writing Modes 3/4: `text-orientation` (mixed | upright | sideways, inherited) is not + /// known at all, so it computes as empty and vertical text cannot be set upright; the legacy + /// SVG `writing-mode` values, which CSS Writing Modes 3 (section 10.1) requires user agents to + /// accept as aliases (lr, lr-tb, rl, rl-tb compute to horizontal-tb; tb, tb-rl to + /// vertical-rl), are dropped. Found in a downstream renderer (AngleSharp.Renderer). + /// + [TestFixture] + public class WritingModeComputedStyleTests + { + [TestCase("mixed")] + [TestCase("upright")] + [TestCase("sideways")] + public void TextOrientationIsComputed(string value) + { + Assert.AreEqual(value, Compute($"text-orientation: {value}").GetPropertyValue("text-orientation")); + } + + [Test] + public void TextOrientationIsInherited() + { + var document = ParseDocument("

    x

    "); + + Assert.AreEqual("upright", document.GetElementById("target").ComputeCurrentStyle().GetPropertyValue("text-orientation")); + } + + [TestCase("lr", "horizontal-tb")] + [TestCase("lr-tb", "horizontal-tb")] + [TestCase("rl", "horizontal-tb")] + [TestCase("rl-tb", "horizontal-tb")] + [TestCase("tb", "vertical-rl")] + [TestCase("tb-rl", "vertical-rl")] + public void LegacyWritingModeValuesComputeToTheirModernEquivalent(string value, string expected) + { + Assert.AreEqual(expected, Compute($"writing-mode: {value}").GetPropertyValue("writing-mode")); + } + + private static ICssStyleDeclaration Compute(string declarations) + { + var document = ParseDocument($"
    x
    "); + return document.GetElementById("target").ComputeCurrentStyle(); + } + } +} diff --git a/src/AngleSharp.Css/Constants/PropertyNames.cs b/src/AngleSharp.Css/Constants/PropertyNames.cs index beccccce..81dd0a19 100644 --- a/src/AngleSharp.Css/Constants/PropertyNames.cs +++ b/src/AngleSharp.Css/Constants/PropertyNames.cs @@ -2052,6 +2052,11 @@ public static class PropertyNames ///
    public static readonly String TextOverflow = "text-overflow"; + /// + /// The text-orientation declaration. + /// + public static readonly String TextOrientation = "text-orientation"; + /// /// The text-shadow declaration. /// diff --git a/src/AngleSharp.Css/Declarations/FlexDeclaration.cs b/src/AngleSharp.Css/Declarations/FlexDeclaration.cs index ce5760ad..a9584ddc 100644 --- a/src/AngleSharp.Css/Declarations/FlexDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/FlexDeclaration.cs @@ -26,6 +26,11 @@ static class FlexDeclaration sealed class FlexAggregator : IValueAggregator, IValueConverter { + // Shared (boxed) instances, such that Merge can tell omitted components apart; computing + // creates new instances, hence a computed shorthand is always serialized in full. + private static readonly ICssValue OmittedFactor = new CssLengthValue(1, CssLengthValue.Unit.None); + private static readonly ICssValue OmittedBasis = new CssLengthValue(0, CssLengthValue.Unit.Percent); + private static readonly IValueConverter converter = Or(None, WithAny( Or(FlexGrowDeclaration.Converter, VarConverter), Or(FlexShrinkDeclaration.Converter, VarConverter), @@ -44,7 +49,15 @@ public ICssValue Merge(ICssValue[] values) if (grow != null || shrink != null || basis != null) { - return new CssTupleValue(new[] { grow, shrink, basis }); + // Components omitted in the shorthand are left out again, such that it serializes as + // written; missing longhands (e.g., a lone computed `flex-basis: 40px`) must still be + // serialized, otherwise `flex: 40px` would mean `1 1 40px`. + return new CssTupleValue(new[] + { + Restore(grow, InitialValues.FlexGrowDecl), + Restore(shrink, InitialValues.FlexShrinkDecl), + Restore(basis, InitialValues.FlexBasisDecl), + }); } return null; @@ -56,11 +69,12 @@ public ICssValue[] Split(ICssValue value) if (options != null) { + // Omitted factors default to 1, an omitted basis to 0% (CSS Flexbox 1, 7.1.1) return new[] { - options.Items[0], - options.Items[1], - options.Items[2], + options.Items[0] ?? OmittedFactor, + options.Items[1] ?? OmittedFactor, + options.Items[2] ?? OmittedBasis, }; } else if (value != null && value.CssText.Isi(CssKeywords.None)) @@ -76,6 +90,20 @@ public ICssValue[] Split(ICssValue value) return null; } + + private static ICssValue Restore(ICssValue value, ICssValue initialValue) + { + if (ReferenceEquals(value, OmittedFactor) || ReferenceEquals(value, OmittedBasis)) + { + return null; + } + else if (value is CssInitialValue initial) + { + return initial.Value ?? initialValue; + } + + return value ?? initialValue; + } } } } diff --git a/src/AngleSharp.Css/Declarations/TextOrientationDeclaration.cs b/src/AngleSharp.Css/Declarations/TextOrientationDeclaration.cs new file mode 100644 index 00000000..4105a0fd --- /dev/null +++ b/src/AngleSharp.Css/Declarations/TextOrientationDeclaration.cs @@ -0,0 +1,20 @@ +namespace AngleSharp.Css.Declarations +{ + using AngleSharp.Css.Dom; + using System; + using static ValueConverters; + + static class TextOrientationDeclaration + { + public static String Name = PropertyNames.TextOrientation; + + public static IValueConverter Converter = Or( + Assign(CssKeywords.Mixed, CssKeywords.Mixed), + Assign("upright", "upright"), + Assign("sideways", "sideways")); + + public static ICssValue InitialValue = InitialValues.TextOrientationDecl; + + public static PropertyFlags Flags = PropertyFlags.Inherited; + } +} diff --git a/src/AngleSharp.Css/Declarations/WritingModeDeclaration.cs b/src/AngleSharp.Css/Declarations/WritingModeDeclaration.cs index b417e7d8..de4312fc 100644 --- a/src/AngleSharp.Css/Declarations/WritingModeDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/WritingModeDeclaration.cs @@ -1,6 +1,8 @@ namespace AngleSharp.Css.Declarations { + using AngleSharp.Css.Converters; using AngleSharp.Css.Dom; + using AngleSharp.Css.Parser; using System; using static ValueConverters; @@ -13,10 +15,20 @@ static class WritingModeDeclaration Assign("vertical-rl", "vertical-rl"), Assign("vertical-lr", "vertical-lr"), Assign("sideways-rl", "sideways-rl"), - Assign("sideways-lr", "sideways-lr")); + Assign("sideways-lr", "sideways-lr"), + // Legacy SVG values, see CSS Writing Modes 3 (10.1) + Alias("lr", "horizontal-tb"), + Alias("lr-tb", "horizontal-tb"), + Alias("rl", "horizontal-tb"), + Alias("rl-tb", "horizontal-tb"), + Alias("tb", "vertical-rl"), + Alias("tb-rl", "vertical-rl")); public static ICssValue InitialValue = InitialValues.WritingModeDecl; public static PropertyFlags Flags = PropertyFlags.Inherited; + + private static IValueConverter Alias(String legacy, String value) => + new IdentifierValueConverter(source => source.IsIdentifier(legacy) ? value : null); } -} \ No newline at end of file +} diff --git a/src/AngleSharp.Css/Factories/DefaultDeclarationFactory.cs b/src/AngleSharp.Css/Factories/DefaultDeclarationFactory.cs index 505ab6e2..07bfaa98 100644 --- a/src/AngleSharp.Css/Factories/DefaultDeclarationFactory.cs +++ b/src/AngleSharp.Css/Factories/DefaultDeclarationFactory.cs @@ -459,6 +459,13 @@ public class DefaultDeclarationFactory : IDeclarationFactory initialValue: TextOverflowDeclaration.InitialValue, flags: TextOverflowDeclaration.Flags) }, + { + TextOrientationDeclaration.Name, new DeclarationInfo( + name: TextOrientationDeclaration.Name, + converter: TextOrientationDeclaration.Converter, + initialValue: TextOrientationDeclaration.InitialValue, + flags: TextOrientationDeclaration.Flags) + }, { WritingModeDeclaration.Name, new DeclarationInfo( name: WritingModeDeclaration.Name, From 1b265f7023a82e5a1be02d15e457c8b745faa6e2 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Wed, 21 Oct 2026 10:26:55 +0200 Subject: [PATCH 4/4] Further fixes --- .../Declarations/CssTransitionProperty.cs | 10 ++++++++ .../Styling/AccentColorInheritanceTests.cs | 24 +++++++++++++++++++ .../Styling/Configuration.cs | 4 +++- .../DefaultStyleSheetRenderingSectionTests.cs | 21 ++++++++++++++++ .../Styling/HtmlCssIntegration.cs | 11 +++++++++ .../CssDefaultStyleSheetProvider.cs | 4 +++- .../Declarations/AccentColorDeclaration.cs | 2 +- .../Dom/Internal/CssStyleDeclaration.cs | 7 +++++- .../Parser/Micro/IdentParser.cs | 3 +++ 9 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 src/AngleSharp.Css.Tests/Styling/AccentColorInheritanceTests.cs diff --git a/src/AngleSharp.Css.Tests/Declarations/CssTransitionProperty.cs b/src/AngleSharp.Css.Tests/Declarations/CssTransitionProperty.cs index b329d602..db5ac62c 100644 --- a/src/AngleSharp.Css.Tests/Declarations/CssTransitionProperty.cs +++ b/src/AngleSharp.Css.Tests/Declarations/CssTransitionProperty.cs @@ -42,6 +42,16 @@ public void CssTransitionPropertyWidthHeightLegal() Assert.AreEqual("width, height", property.Value); } + [TestCase("translate")] + [TestCase("rotate")] + [TestCase("scale")] + public void CssTransitionPropertyIndividualTransformLegal(string name) + { + var property = ParseDeclaration("transition-property : " + name); + Assert.IsTrue(property.HasValue); + Assert.AreEqual(name, property.Value); + } + [Test] public void CssTransitionPropertyDashSpecificIllegal() { diff --git a/src/AngleSharp.Css.Tests/Styling/AccentColorInheritanceTests.cs b/src/AngleSharp.Css.Tests/Styling/AccentColorInheritanceTests.cs new file mode 100644 index 00000000..c45a8a1e --- /dev/null +++ b/src/AngleSharp.Css.Tests/Styling/AccentColorInheritanceTests.cs @@ -0,0 +1,24 @@ +#nullable disable +namespace AngleSharp.Css.Tests.Styling +{ + using AngleSharp.Dom; + using NUnit.Framework; + using static CssConstructionFunctions; + + /// + /// `accent-color` is an inherited property (CSS Basic User Interface 4), but a child reports it + /// as empty. Found in a downstream renderer (AngleSharp.Renderer), where `accent-color` on a form + /// does not reach the checkboxes inside it. + /// + [TestFixture] + public class AccentColorInheritanceTests + { + [Test] + public void AccentColorIsInherited() + { + var document = ParseDocument("
    "); + + Assert.AreEqual("rgba(200, 0, 100, 1)", document.GetElementById("target").ComputeCurrentStyle().GetPropertyValue("accent-color")); + } + } +} diff --git a/src/AngleSharp.Css.Tests/Styling/Configuration.cs b/src/AngleSharp.Css.Tests/Styling/Configuration.cs index 4ce512d0..949238cb 100644 --- a/src/AngleSharp.Css.Tests/Styling/Configuration.cs +++ b/src/AngleSharp.Css.Tests/Styling/Configuration.cs @@ -44,7 +44,9 @@ public void ObtainDefaultSheet() // 70, not 60: the UA stylesheet now also follows the HTML Standard's rendering // section for bidi isolation, `summary`, `fieldset`, `mark`, `abbr[title]`, links, // and the block margins / list padding. - Assert.AreEqual(70, sheet.Rules.Length); + // 72, not 70: the UA stylesheet now also generates quotes for `q::before` and + // `q::after`. + Assert.AreEqual(72, sheet.Rules.Length); } } } diff --git a/src/AngleSharp.Css.Tests/Styling/DefaultStyleSheetRenderingSectionTests.cs b/src/AngleSharp.Css.Tests/Styling/DefaultStyleSheetRenderingSectionTests.cs index 78ba4c83..8d188611 100644 --- a/src/AngleSharp.Css.Tests/Styling/DefaultStyleSheetRenderingSectionTests.cs +++ b/src/AngleSharp.Css.Tests/Styling/DefaultStyleSheetRenderingSectionTests.cs @@ -51,6 +51,27 @@ public void HrHasMargins(string property, string expected) Assert.AreEqual(expected, Computed("
    ", property)); } + [TestCase("::before", "open-quote")] + [TestCase("::after", "close-quote")] + public void QuotesAreGenerated(string pseudo, string expected) + { + var document = ParseDocument("

    x

    "); + var element = document.GetElementById("target"); + Assert.AreEqual(expected, document.DefaultView.GetComputedStyle(element, pseudo).GetPropertyValue("content")); + } + + [Test] + public void TablesAreBorderBoxSized() + { + Assert.AreEqual("border-box", Computed("
    x
    ", "box-sizing")); + } + + [Test] + public void TablesDoNotInheritTextIndent() + { + Assert.AreEqual("0px", Computed("
    x
    ", "text-indent")); + } + [TestCase("td")] [TestCase("th")] public void TableCellsHavePadding(string tagName) diff --git a/src/AngleSharp.Css.Tests/Styling/HtmlCssIntegration.cs b/src/AngleSharp.Css.Tests/Styling/HtmlCssIntegration.cs index f5e2810d..8e83a77e 100644 --- a/src/AngleSharp.Css.Tests/Styling/HtmlCssIntegration.cs +++ b/src/AngleSharp.Css.Tests/Styling/HtmlCssIntegration.cs @@ -471,6 +471,17 @@ public void DefaultStyleSheetTest_Issue21() Assert.AreEqual("700", boldStyle.GetFontWeight()); } + [Test] + public void InitialKeywordIsNotReplacedByInheritedValue() + { + var browsingContext = BrowsingContext.New(Configuration.Default.WithCss()); + var htmlParser = browsingContext.GetService(); + var document = htmlParser.ParseDocument("

    x

    "); + var style = document.QuerySelector("p").ComputeCurrentStyle(); + Assert.AreEqual("0px", style.GetPropertyValue("text-indent")); + Assert.AreEqual("rgba(0, 0, 0, 1)", style.GetPropertyValue("color")); + } + [Test] public void MediaRuleCssCausesException_Issue20() { diff --git a/src/AngleSharp.Css/CssDefaultStyleSheetProvider.cs b/src/AngleSharp.Css/CssDefaultStyleSheetProvider.cs index cf7e3179..21609244 100644 --- a/src/AngleSharp.Css/CssDefaultStyleSheetProvider.cs +++ b/src/AngleSharp.Css/CssDefaultStyleSheetProvider.cs @@ -143,7 +143,7 @@ private static ICssStyleSheet Parse(String source) small, sub, sup { font-size: .83em } sub { vertical-align: sub } sup { vertical-align: super } -table { border-spacing: 2px; } +table { border-spacing: 2px; box-sizing: border-box; text-indent: 0 } thead, tbody, tfoot { vertical-align: middle } td, th, tr { vertical-align: inherit } @@ -160,6 +160,8 @@ private static ICssStyleSheet Parse(String source) acronym[title] { text-decoration: dotted underline } mark { background-color: yellow; color: black } br::before { content: '\A'; white-space: pre-line } +q::before { content: open-quote } +q::after { content: close-quote } *::before, *::after { display: inline } center { text-align: center } :link { color: #0000ee } diff --git a/src/AngleSharp.Css/Declarations/AccentColorDeclaration.cs b/src/AngleSharp.Css/Declarations/AccentColorDeclaration.cs index 9f9ccc0e..efb73de4 100644 --- a/src/AngleSharp.Css/Declarations/AccentColorDeclaration.cs +++ b/src/AngleSharp.Css/Declarations/AccentColorDeclaration.cs @@ -12,6 +12,6 @@ static class AccentColorDeclaration public static ICssValue InitialValue = InitialValues.AccentColorDecl; - public static PropertyFlags Flags = PropertyFlags.None; + public static PropertyFlags Flags = PropertyFlags.Inherited; } } diff --git a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs index 07029b34..a079ed15 100644 --- a/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs +++ b/src/AngleSharp.Css/Dom/Internal/CssStyleDeclaration.cs @@ -350,7 +350,12 @@ internal void SetDeclarations(IEnumerable decls) => ChangeDeclarations(decls, m => false, (o, n) => !o.IsImportant || n.IsImportant); internal void UpdateDeclarations(IEnumerable decls) => - ChangeDeclarations(decls, m => !m.CanBeInherited, (o, n) => o.IsInherited); + ChangeDeclarations(decls, m => !m.CanBeInherited, (o, n) => o.IsInherited && !IsExplicitInitial(o)); + + // IsInherited also holds for an explicit `initial` on an inherited property, + // which must still resolve to the initial value rather than the parent's. + private static Boolean IsExplicitInitial(ICssProperty property) => + property.RawValue is not null && property.RawValue.CssText.Is(CssKeywords.Initial); #endregion diff --git a/src/AngleSharp.Css/Parser/Micro/IdentParser.cs b/src/AngleSharp.Css/Parser/Micro/IdentParser.cs index 53f458de..aefed3bf 100644 --- a/src/AngleSharp.Css/Parser/Micro/IdentParser.cs +++ b/src/AngleSharp.Css/Parser/Micro/IdentParser.cs @@ -424,6 +424,8 @@ private static String Rest(StringSource source, Char current, StringBuilder buff "perspective", "perspective-origin", "right", + "rotate", + "scale", "scroll-snap-coordinate", "scroll-snap-destination", "shape-image-threshold", @@ -438,6 +440,7 @@ private static String Rest(StringSource source, Char current, StringBuilder buff "top", "transform", "transform-origin", + "translate", "vertical-align", "visibility", "width",