Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,46 @@
# 1.2.0

Released on Wednesday, October 21 2026.

- 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 `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
- 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 <ratio>`, `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 <angle>`
- 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`, `text-orientation`, `transform-box`, and `transition-behavior` properties

# 1.1.4

Released on Tuesday, October 20 2026.
Expand Down
2 changes: 1 addition & 1 deletion src/AngleSharp.Css.Docs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@anglesharp/css",
"version": "1.1.4",
"version": "1.2.0",
"preview": true,
"description": "The doclet for the AngleSharp.Css documentation.",
"keywords": [
Expand Down
12 changes: 11 additions & 1 deletion src/AngleSharp.Css.Tests/Declarations/CssBackgroundProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
5 changes: 3 additions & 2 deletions src/AngleSharp.Css.Tests/Declarations/CssBoxModelProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/AngleSharp.Css.Tests/Declarations/CssFontProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand Down
103 changes: 103 additions & 0 deletions src/AngleSharp.Css.Tests/Declarations/CssModernValueSyntaxTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#nullable disable
namespace AngleSharp.Css.Tests.Declarations
{
using NUnit.Framework;
using static CssConstructionFunctions;

/// <summary>
/// Covers the parsing and serialization of value syntax from current CSS specifications,
/// next to the cases in <see cref="Styling.DroppedValidValuesComputedStyleTests"/>.
/// </summary>
[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);
}
}
6 changes: 3 additions & 3 deletions src/AngleSharp.Css.Tests/Declarations/CssTextProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
10 changes: 10 additions & 0 deletions src/AngleSharp.Css.Tests/Declarations/CssTransitionProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
28 changes: 25 additions & 3 deletions src/AngleSharp.Css.Tests/Rules/FontFace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
24 changes: 24 additions & 0 deletions src/AngleSharp.Css.Tests/Styling/AccentColorInheritanceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#nullable disable
namespace AngleSharp.Css.Tests.Styling
{
using AngleSharp.Dom;
using NUnit.Framework;
using static CssConstructionFunctions;

/// <summary>
/// `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.
/// </summary>
[TestFixture]
public class AccentColorInheritanceTests
{
[Test]
public void AccentColorIsInherited()
{
var document = ParseDocument("<form style=\"accent-color: rgb(200, 0, 100)\"><input id=target type=checkbox></form>");

Assert.AreEqual("rgba(200, 0, 100, 1)", document.GetElementById("target").ComputeCurrentStyle().GetPropertyValue("accent-color"));
}
}
}
50 changes: 50 additions & 0 deletions src/AngleSharp.Css.Tests/Styling/ComputedSerializationTests.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
[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<IHtmlParser>().ParseDocument($"<div id=target style='{declarations}'>x</div>");
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"));
}
}
}
Loading
Loading