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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
# 1.1.2

Released on Friday, September 11 2026

- Fixed issue with calculation of CSS grid style
- Fixed default value of CSS gradients
- Fixed issue with Point2D style computations
- Fixed issue with style computation of `border-image-slice` declarations
- Fixed handling of `::before` and `::after` w.r.t. `content` declarations
- Added support for more `text-overflow` keywords
- Added `alignment-baseline` declaration
- Added `baseline-shift` declaration
- Added `color-interpolation-filters` declaration
- Added `clip-path` and `clip-rule` declarations
- Added `dominant-baseline` declaration
- Added `fill-opacity` and `fill-rule` declarations
- Added `marker-start`, `marker-mid`, and `marker-end` declarations
- Added `mask` declaration
- Added `text-underline-position` declaration
- Added `writing-mode` declaration

# 1.1.1

Released on Wednesday, September 9 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.1",
"version": "1.1.2",
"preview": true,
"description": "The doclet for the AngleSharp.Css documentation.",
"keywords": [
Expand Down
7 changes: 5 additions & 2 deletions src/AngleSharp.Css.Tests/Declarations/CssVariables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,13 @@ public void BackgroundShorthandWithVariableKeepsLonghands()
[Test]
public void GridGapShorthandWithVariableKeepsLonghands()
{
// gap's two-value form is <row-gap> <column-gap> (the first value is the row gap) -
// this test previously encoded the reverse, matching a real, now-fixed bug in
// GapDeclaration's own Longhands ordering (see GapShorthandComputedStyleTests).
var style = ParseDeclarations(@"gap: 12px var(--gap-x)");

Assert.AreEqual("12px", style.GetProperty("column-gap").Value);
Assert.AreEqual("var(--gap-x)", style.GetProperty("row-gap").Value);
Assert.AreEqual("12px", style.GetProperty("row-gap").Value);
Assert.AreEqual("var(--gap-x)", style.GetProperty("column-gap").Value);
}

[Test]
Expand Down
13 changes: 10 additions & 3 deletions src/AngleSharp.Css.Tests/Extensions/AnalysisWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,10 @@ public void GetComputedStylePseudoInitialScenarioSingleColon()

var style = window.GetComputedStyle(element, ":after");
Assert.IsNotNull(style);
Assert.AreEqual(2, style.Length);
// 3, not 2: `color` (the rule's own) + `font-weight` (inherited from .bold) + `display`
// (the UA stylesheet's `*:before, *:after { display: inline }` default, giving generated
// content its spec-correct display instead of none at all).
Assert.AreEqual(3, style.Length);
}

[Test]
Expand All @@ -219,7 +222,9 @@ public void GetComputedStylePseudoInitialScenarioDoubleColon()

var style = window.GetComputedStyle(element, "::after");
Assert.IsNotNull(style);
Assert.AreEqual(2, style.Length);
// See GetComputedStylePseudoInitialScenarioSingleColon's comment above - same rule, `::`
// syntax instead of `:`.
Assert.AreEqual(3, style.Length);
}

[Test]
Expand All @@ -243,7 +248,9 @@ public void GetComputedStyleMixedTrivialAndPseudoScenario()

var stylePseudo = window.GetComputedStyle(element, ":before");
Assert.IsNotNull(stylePseudo);
Assert.AreEqual(3, stylePseudo.Length);
// 4, not 3: `color` + `content` (the rule's own) + `font-weight` (inherited) + `display`
// (the UA stylesheet's `*:before, *:after { display: inline }` default).
Assert.AreEqual(4, stylePseudo.Length);
}

[Test]
Expand Down
182 changes: 182 additions & 0 deletions src/AngleSharp.Css.Tests/Extensions/PseudoElementRenderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#nullable disable
namespace AngleSharp.Css.Tests.Extensions
{
using System.Linq;
using AngleSharp.Css.Dom;
using AngleSharp.Css.RenderTree;
using AngleSharp.Css.Tests.Mocks;
using AngleSharp.Dom;
using NUnit.Framework;
using static CssConstructionFunctions;

/// <summary>
/// `::before`/`::after` were entirely absent from the render tree `WindowExtensions.Render`/
/// `RenderTreeBuilder.RenderElement` produce - the tree only ever walked `element.ChildNodes`
/// for real DOM elements/text, with no awareness of `IElement.Pseudo(...)` at all, even though
/// `window.GetPseudoElements` (a separate, independent API) already resolved `::before`/`::after`
/// selector matching and `content` correctly. Found while adding pseudo-element support to a
/// downstream renderer.
/// </summary>
[TestFixture]
public class PseudoElementRenderTests
{
private static ElementRenderNode Render(string html)
{
var document = ParseDocument(html);
var window = document.DefaultView;
return (ElementRenderNode)window.Render(new PlainRenderDevice());
}

private static ElementRenderNode FindElement(ElementRenderNode node, string id)
{
if (node.Ref.Id == id)
{
return node;
}

foreach (var child in node.Children.OfType<ElementRenderNode>())
{
var found = FindElement(child, id);

if (found is not null)
{
return found;
}
}

return null;
}

[Test]
public void BeforePseudoElementIsTheFirstChildWithItsGeneratedTextContent()
{
var root = Render("<html><head><style>#target::before { content: \"PREFIX-\"; }</style></head>" +
"<body><div id=target>middle</div></body></html>");
var target = FindElement(root, "target");
var children = target.Children.ToArray();

Assert.AreEqual(2, children.Length);
var before = (ElementRenderNode)children[0];
Assert.IsInstanceOf<IPseudoElement>(before.Ref);
Assert.AreEqual("before", ((IPseudoElement)before.Ref).PseudoName);
var beforeText = (TextRenderNode)before.Children.Single();
Assert.AreEqual("PREFIX-", beforeText.Ref.Data);
var realText = (TextRenderNode)children[1];
Assert.AreEqual("middle", realText.Ref.Data);
}

[Test]
public void AfterPseudoElementIsTheLastChildWithItsGeneratedTextContent()
{
var root = Render("<html><head><style>#target::after { content: \"-SUFFIX\"; }</style></head>" +
"<body><div id=target>middle</div></body></html>");
var target = FindElement(root, "target");
var children = target.Children.ToArray();

Assert.AreEqual(2, children.Length);
var realText = (TextRenderNode)children[0];
Assert.AreEqual("middle", realText.Ref.Data);
var after = (ElementRenderNode)children[1];
Assert.AreEqual("after", ((IPseudoElement)after.Ref).PseudoName);
var afterText = (TextRenderNode)after.Children.Single();
Assert.AreEqual("-SUFFIX", afterText.Ref.Data);
}

[Test]
public void PseudoElementWithNoContentDeclarationGeneratesNoNode()
{
var root = Render("<html><body><div id=target>middle</div></body></html>");
var target = FindElement(root, "target");

Assert.AreEqual(1, target.Children.Count());
Assert.IsInstanceOf<TextRenderNode>(target.Children.Single());
}

[Test]
public void ExplicitContentNoneGeneratesNoNode()
{
var root = Render("<html><head><style>#target::before { content: none; }</style></head>" +
"<body><div id=target>middle</div></body></html>");
var target = FindElement(root, "target");

Assert.AreEqual(1, target.Children.Count());
Assert.IsInstanceOf<TextRenderNode>(target.Children.Single());
}

[Test]
public void ContentAttrResolvesTheHostsOwnAttribute()
{
var root = Render("<html><head><style>#target::before { content: attr(data-label); }</style></head>" +
"<body><div id=target data-label=\"Hello\">middle</div></body></html>");
var target = FindElement(root, "target");
var before = (ElementRenderNode)target.Children.First();
var beforeText = (TextRenderNode)before.Children.Single();

Assert.AreEqual("Hello", beforeText.Ref.Data);
}

[Test]
public void MultiplePartsConcatenateInDeclaredOrder()
{
var root = Render("<html><head><style>#target::before { content: \"[\" attr(data-label) \"]\"; }</style></head>" +
"<body><div id=target data-label=\"X\">middle</div></body></html>");
var target = FindElement(root, "target");
var before = (ElementRenderNode)target.Children.First();
var beforeText = (TextRenderNode)before.Children.Single();

Assert.AreEqual("[X]", beforeText.Ref.Data);
}

[Test]
public void PseudoElementInheritsFromItsHostWhenNotItselfSet()
{
var root = Render("<html><head><style>" +
"#target { color: rgb(10, 20, 30); }" +
"#target::before { content: \"x\"; }" +
"</style></head><body><div id=target>middle</div></body></html>");
var target = FindElement(root, "target");
var before = (ElementRenderNode)target.Children.First();

Assert.AreEqual("rgba(10, 20, 30, 1)", before.ComputedStyle.GetPropertyValue("color"));
}

[Test]
public void BothBeforeAndAfterCanCoexistBracketingRealContent()
{
var root = Render("<html><head><style>" +
"#target::before { content: \"<\"; }" +
"#target::after { content: \">\"; }" +
"</style></head><body><div id=target>middle</div></body></html>");
var target = FindElement(root, "target");
var children = target.Children.ToArray();

Assert.AreEqual(3, children.Length);
Assert.AreEqual("before", ((IPseudoElement)((ElementRenderNode)children[0]).Ref).PseudoName);
Assert.IsInstanceOf<TextRenderNode>(children[1]);
Assert.AreEqual("after", ((IPseudoElement)((ElementRenderNode)children[2]).Ref).PseudoName);
}

/// <summary>
/// A generated-content pseudo-element defaults to `display: inline` per spec (the same as a
/// real browser's own UA stylesheet) unless the author overrides it - AngleSharp.Css's UA
/// stylesheet had no rule for `::before`/`::after` at all, so an unstyled one computed
/// `display: block` (this project's general "unset display defaults to block" fallback,
/// correct for most real elements but wrong for generated content specifically), which would
/// have made every `::before`/`::after` start its own new block line instead of flowing
/// inline with the rest of its host's content - the overwhelmingly common real-world case
/// (an icon, a required-field marker, a breadcrumb separator, ...). Fixed with a targeted
/// `*:before, *:after { display: inline }` UA-stylesheet addition, the same "give the right
/// initial value via a UA rule" precedent already used for `list-style-type`.
/// </summary>
[Test]
public void PseudoElementDefaultsToInlineDisplay()
{
var root = Render("<html><head><style>#target::before { content: \"x\"; }</style></head>" +
"<body><div id=target>middle</div></body></html>");
var target = FindElement(root, "target");
var before = (ElementRenderNode)target.Children.First();

Assert.AreEqual("inline", before.ComputedStyle.GetDisplay());
}
}
}
6 changes: 5 additions & 1 deletion src/AngleSharp.Css.Tests/Styling/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ public void ObtainDefaultSheet()
Assert.IsNotNull(service.Default);
var sheet = service.Default;
Assert.IsNotNull(sheet);
Assert.AreEqual(49, sheet.Rules.Length);
// 50, not 49: the UA stylesheet now also carries a `*:before, *:after { display: inline }`
// rule, giving generated-content pseudo-elements their spec-correct default display
// (previously they had none at all, so this renderer's general "unset display defaults
// to block" fallback made every ::before/::after start its own new block line).
Assert.AreEqual(50, sheet.Rules.Length);
}
}
}
41 changes: 41 additions & 0 deletions src/AngleSharp.Css.Tests/Styling/GapShorthandComputedStyleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#nullable disable
namespace AngleSharp.Css.Tests.Styling
{
using AngleSharp.Dom;
using NUnit.Framework;
using static CssConstructionFunctions;

/// <summary>
/// `gap`/`grid-gap`'s two-value form is `&lt;row-gap&gt; &lt;column-gap&gt;` per
/// https://drafts.csswg.org/css-align-3/#gap-shorthand - `GapDeclaration`/`GridGapDeclaration`'s
/// own `Longhands` arrays listed `column-gap` before `row-gap`, the reverse of what their own
/// `Merge()`/`Split()` methods already assumed (`values[0]`/`Items[0]` is always the row value),
/// so the two longhands' computed values came out swapped. Found while adding CSS Grid track
/// sizing support to a downstream renderer.
/// </summary>
[TestFixture]
public class GapShorthandComputedStyleTests
{
[Test]
public void GapShorthandDecomposesRowFirstColumnSecond()
{
var document = ParseDocument("<div id=target style=\"gap: 10px 20px;\"></div>");
var target = document.GetElementById("target");
var style = target.ComputeCurrentStyle();

Assert.AreEqual("10px", style.GetPropertyValue("row-gap"));
Assert.AreEqual("20px", style.GetPropertyValue("column-gap"));
}

[Test]
public void GridGapShorthandDecomposesRowFirstColumnSecond()
{
var document = ParseDocument("<div id=target style=\"grid-gap: 10px 20px;\"></div>");
var target = document.GetElementById("target");
var style = target.ComputeCurrentStyle();

Assert.AreEqual("10px", style.GetPropertyValue("grid-row-gap"));
Assert.AreEqual("20px", style.GetPropertyValue("grid-column-gap"));
}
}
}
52 changes: 52 additions & 0 deletions src/AngleSharp.Css.Tests/Styling/TextOverflowComputedStyleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#nullable disable
namespace AngleSharp.Css.Tests.Styling
{
using AngleSharp.Dom;
using NUnit.Framework;
using static CssConstructionFunctions;

/// <summary>
/// `text-overflow` had no registered declaration at all (no `TextOverflowDeclaration`, never
/// wired into `DefaultDeclarationFactory`) - `ComputeCurrentStyle().GetPropertyValue("text-overflow")`
/// always reported an empty string, even for an explicitly authored `text-overflow: ellipsis`.
/// Its own placeholder initial-value constant (`InitialValues.TextOverflowDecl`) also used the
/// wrong enum entirely (`OverflowMode`, `visible`/`hidden`/`scroll`/`auto`/`clip`) paired with
/// the wrong keyword (`auto`) for a property whose real initial value is the keyword `clip`.
/// Found while adding `text-overflow: ellipsis` support to a downstream renderer.
/// </summary>
[TestFixture]
public class TextOverflowComputedStyleTests
{
[Test]
public void TextOverflowEllipsisIsRecognizedAndComputed()
{
var document = ParseDocument("<div id=target style=\"text-overflow: ellipsis;\"></div>");
var target = document.GetElementById("target");

Assert.AreEqual("ellipsis", target.ComputeCurrentStyle().GetPropertyValue("text-overflow"));
}

[Test]
public void TextOverflowClipIsRecognizedAndComputed()
{
var document = ParseDocument("<div id=target style=\"text-overflow: clip;\"></div>");
var target = document.GetElementById("target");

Assert.AreEqual("clip", target.ComputeCurrentStyle().GetPropertyValue("text-overflow"));
}

[Test]
public void UnsetTextOverflowComputesToEmptyNotItsInitialValue()
{
// Matches the same "never serialized when nothing in the cascade set it explicitly"
// behavior already established for white-space/list-style-type elsewhere in this
// project - an unset property reports empty rather than resolving to its own CSS
// initial value, so a consumer must supply that default itself (see
// AngleSharp.Renderer's ParseTextOverflow).
var document = ParseDocument("<div id=target></div>");
var target = document.GetElementById("target");

Assert.AreEqual("", target.ComputeCurrentStyle().GetPropertyValue("text-overflow"));
}
}
}
Loading
Loading