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

Released on Wednesday, September 9 2026

- Fixed unresolved converter re-entry (#243) @sebastienros
- Fixed issue with computation of `transform` functions
- Fixed computed `animation` longhands retaining unresolved `initial` values
- Fixed percentage `border-radius` components resolving against the wrong axis
- Fixed default list styling for unordered lists
- Added shorthand decomposition and `clip` support for `overflow`
- Added explicit pseudo class handling via `SetPseudoClass`
- Added support for parsing `filter` declarations

# 1.1.0

Released on Saturday, September 5 2026
Expand Down
27 changes: 27 additions & 0 deletions docs/general/04-Core-Interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,33 @@ var config = Configuration.Default

These are central for extensibility and custom property handling.

## Pseudo-Class Forcing

AngleSharp has no notion of pointer/keyboard interaction state, so pseudo-classes such as `:hover` and `:active` never match by default. `WithCss()` adds extension methods on `IElement` that let a caller force a pseudo-class to match (or not match) for a specific element, both in `Element.Matches(...)` and in computed style (`ComputeCurrentStyle()`/`ComputeDeclarations(...)`):

- `SetPseudoClass(pseudoClass, value = true)`
: Forces the given pseudo-class on (or off, with `value: false`).
- `GetPseudoClass(pseudoClass)`
: Returns the forced value, or `null` if nothing was forced for that element.
- `RemovePseudoClass(pseudoClass)`
: Clears a single forced pseudo-class, reverting to normal matching.
- `ClearPseudoClasses()`
: Clears every forced pseudo-class for the element.

```cs
using AngleSharp.Dom;

var target = document.QuerySelector("#target");
target.SetPseudoClass("hover");

Console.WriteLine(target.Matches(":hover")); // True
Console.WriteLine(target.ComputeCurrentStyle().GetPropertyValue("background-color"));

target.RemovePseudoClass("hover");
```

This applies to any pseudo-class the selector engine recognizes (`:hover`, `:active`, `:visited`, ...), works per element only (forcing a child does not propagate to its ancestors), and is not the mechanism for `:focus`: that pseudo-class already has a real, settable state in AngleSharp core, so `SetPseudoClass("focus", ...)` delegates to `IHtmlElement.DoFocus()`/`DoBlur()` instead of a separate forced flag.

## Rule Of Thumb

- Use parser and CSSOM interfaces for analysis/transforms.
Expand Down
1 change: 1 addition & 0 deletions docs/general/06-Provided-Services.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ From the default configuration extension:
- `ICssParser` -> parser instance using optional `CssParserOptions`,
- `IStylingService` -> `CssStylingService` for CSS MIME handling,
- CSS observer service (`Factory.Observer`) for style mutation integration.
- `IPseudoClassSelectorFactory` -> wrapped so any recognized pseudo-class can be forced per element via `SetPseudoClass(...)` (see [Core Interfaces](04-Core-Interfaces.md)).

## Quick Service Retrieval

Expand Down
28 changes: 27 additions & 1 deletion docs/tutorials/02-Examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,33 @@ Console.WriteLine(serialized);

This preserves comments, but not their exact original positions in every case. Depending on where a comment was placed, it may be moved when the stylesheet is serialized again.

## 11. Where To Go Next
## 11. Preview A `:hover` State Without Real Interaction

Useful for headless rendering/screenshot tools that need to show interactive states.

```cs
using AngleSharp;
using AngleSharp.Dom;

var html = @"<!doctype html>
<style>
#btn { background-color: rgb(0, 0, 255); }
#btn:hover { background-color: rgb(255, 0, 0); }
</style>
<button id='btn'>Hover me</button>";

var context = BrowsingContext.New(Configuration.Default.WithCss());
var document = await context.OpenAsync(req => req.Content(html));

var button = document.QuerySelector("#btn");
button.SetPseudoClass("hover");

Console.WriteLine(button.ComputeCurrentStyle().GetPropertyValue("background-color")); // rgba(255, 0, 0, 1)

button.RemovePseudoClass("hover");
```

## 12. Where To Go Next

- Read [API Documentation](01-API.md) for deeper CSSOM details.
- Read [Render Tree Examples](03-Render-Tree.md) for style-aware tree traversal and resource download workflows.
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.0",
"version": "1.1.1",
"preview": true,
"description": "The doclet for the AngleSharp.Css documentation.",
"keywords": [
Expand Down
6 changes: 5 additions & 1 deletion src/AngleSharp.Css.Tests/Extensions/Elements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ public async Task DownloadResources()
};
var config = Configuration.Default
.WithDefaultLoader(loaderOptions)
.WithRenderDevice()
.WithRenderDevice(new DefaultRenderDevice
{
ViewPortWidth = 800,
ViewPortHeight = 600,
})
.WithCss();
var document = "<style>div { background: url('https://avatars1.githubusercontent.com/u/10828168?s=200&v=4'); }</style><div></div>".ToHtmlDocument(config);
var tree = document.DefaultView!.Render();
Expand Down
59 changes: 59 additions & 0 deletions src/AngleSharp.Css.Tests/Styling/AnimationComputedStyleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#nullable disable
namespace AngleSharp.Css.Tests.Styling
{
using AngleSharp.Dom;
using NUnit.Framework;
using static CssConstructionFunctions;

/// <summary>
/// Per the CSS cascade, a computed style is supposed to fully resolve every longhand to a
/// concrete value - the CSS-wide keyword `initial` is itself resolved away during computation,
/// never present verbatim in the computed declaration. Confirmed empirically while building
/// CSS `animation` support in a downstream renderer (AngleSharp.Renderer): any `animation`
/// longhand the `animation` shorthand does not explicitly set reports the literal string
/// `"initial"` instead of that property's own real initial value (`normal` for
/// `animation-direction`, `none` for `animation-fill-mode`, `0s` for `animation-delay`,
/// `running` for `animation-play-state`) - unlike `transition`'s equivalent longhands, which
/// report an empty string instead in the same situation (see BasicStyling.cs's sibling
/// investigation for `transition`, not affected by this).
/// </summary>
[TestFixture]
public class AnimationComputedStyleTests
{
[Test]
public void UnsetAnimationDirectionResolvesToNormal()
{
var document = ParseDocument("<div id=target style=\"animation: spin 2s linear;\"></div>");
var target = document.GetElementById("target");

Assert.AreEqual("normal", target.ComputeCurrentStyle().GetPropertyValue("animation-direction"));
}

[Test]
public void UnsetAnimationFillModeResolvesToNone()
{
var document = ParseDocument("<div id=target style=\"animation: spin 2s linear;\"></div>");
var target = document.GetElementById("target");

Assert.AreEqual("none", target.ComputeCurrentStyle().GetPropertyValue("animation-fill-mode"));
}

[Test]
public void UnsetAnimationDelayResolvesToZeroSeconds()
{
var document = ParseDocument("<div id=target style=\"animation: spin 2s linear;\"></div>");
var target = document.GetElementById("target");

Assert.AreEqual("0s", target.ComputeCurrentStyle().GetPropertyValue("animation-delay"));
}

[Test]
public void UnsetAnimationPlayStateResolvesToRunning()
{
var document = ParseDocument("<div id=target style=\"animation: spin 2s linear;\"></div>");
var target = document.GetElementById("target");

Assert.AreEqual("running", target.ComputeCurrentStyle().GetPropertyValue("animation-play-state"));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#nullable disable
namespace AngleSharp.Css.Tests.Styling
{
using AngleSharp.Dom;
using AngleSharp.Html.Parser;
using NUnit.Framework;

/// <summary>
/// Per https://www.w3.org/TR/css-backgrounds-3/#corner-overlap, a percentage `border-radius`
/// resolves its horizontal component against the border box's own *width* and its vertical
/// component against the border box's own *height* - independently. Confirmed empirically
/// while building `border-radius` support in a downstream renderer (AngleSharp.Renderer) that
/// this does not happen: both components resolve against the containing block's/element's
/// *width* alone, so on a box whose width and height differ, the vertical radius comes out
/// wrong (tracking the wrong axis's dimension entirely, not merely imprecise).
/// </summary>
[TestFixture]
public class BorderRadiusPercentageResolutionTests
{
private static IDocument ParseWithRenderDevice(string html, int viewPortWidth = 1000)
{
var config = Configuration.Default
.WithCss()
.WithRenderDevice(new DefaultRenderDevice { ViewPortWidth = viewPortWidth });
var browsingContext = BrowsingContext.New(config);
var htmlParser = browsingContext.GetService<IHtmlParser>();
return htmlParser.ParseDocument(html);
}

[Test]
public void VerticalPercentageComponentResolvesAgainstTheElementsOwnHeight()
{
// 200x100 box, `border-radius: 10% / 30%` - the horizontal component (10%) should
// resolve against the 200px width (20px); the vertical component (30%) should resolve
// against the 100px height (30px), not against the 200px width (which would give the
// wrong value, 60px).
var document = ParseWithRenderDevice("<div id=target style=\"width:200px; height:100px; border-radius: 10% / 30%;\"></div>");
var target = document.GetElementById("target");
var style = target.ComputeCurrentStyle();

var horizontal = style.GetPropertyValue("border-top-left-radius");
Assert.IsTrue(horizontal.Contains("30px"), $"expected the vertical 30% component to resolve to 30px (30% of the 100px height); got '{horizontal}'");
}

[Test]
public void VerticalPercentageComponentTracksHeightAcrossDifferentElementHeights()
{
// The same vertical percentage against two different heights (but the same width)
// must resolve to two different pixel values if it is genuinely tracking height - if
// it were (incorrectly) tracking width instead, both would resolve identically despite
// the different heights.
var shortDocument = ParseWithRenderDevice("<div id=target style=\"width:200px; height:100px; border-radius: 0 / 30%;\"></div>");
var shortTarget = shortDocument.GetElementById("target");
var shortRadius = shortTarget.ComputeCurrentStyle().GetPropertyValue("border-top-left-radius");

var tallDocument = ParseWithRenderDevice("<div id=target style=\"width:200px; height:200px; border-radius: 0 / 30%;\"></div>");
var tallTarget = tallDocument.GetElementById("target");
var tallRadius = tallTarget.ComputeCurrentStyle().GetPropertyValue("border-top-left-radius");

Assert.AreNotEqual(shortRadius, tallRadius, "a 30% vertical radius against a 100px-tall box and a 200px-tall box must resolve to different pixel values.");
}
}
}
54 changes: 54 additions & 0 deletions src/AngleSharp.Css.Tests/Styling/FilterPropertyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#nullable disable
namespace AngleSharp.Css.Tests.Styling
{
using AngleSharp.Css.Dom;
using AngleSharp.Dom;
using AngleSharp.Css.Values;
using AngleSharp.Html.Dom;
using NUnit.Framework;
using static CssConstructionFunctions;

/// <summary>
/// Verifies that filter function lists are parsed into structured values and preserved by
/// computed style, while the original inline style remains available through the DOM.
/// </summary>
[TestFixture]
public class FilterPropertyTests
{
[Test]
public void FilterComputedValuePreservesAuthoredFunctions()
{
var document = ParseDocument("<div id=target style=\"filter: grayscale(0.9) blur(2px);\"></div>");
var target = document.GetElementById("target");

Assert.AreEqual("grayscale(0.9) blur(2px)", target.ComputeCurrentStyle().GetPropertyValue("filter"));
}

[Test]
public void FilterFunctionsExposeNamesAndArguments()
{
var document = ParseDocument("<div id=target style=\"filter: grayscale(0.9) blur(2px);\"></div>");
var target = document.GetElementById("target");
var value = target.GetStyle().GetProperty("filter").RawValue as CssFilterValue;

Assert.IsNotNull(value);
Assert.AreEqual(2, value.Functions.Length);
Assert.AreEqual("grayscale", value.Functions[0].Name);
Assert.AreEqual("0.9", value.Functions[0].Arguments[0].CssText);
Assert.AreEqual("blur", value.Functions[1].Name);
Assert.AreEqual("2px", value.Functions[1].Arguments[0].CssText);
}

[Test]
public void RawFilterTextIsStillReadableFromTheInlineStyleAttributeItself()
{
// Confirms the gap is specifically in AngleSharp.Css's own computed-style/cascade
// pipeline, not in the HTML/attribute layer - the text is right there, just never
// parsed into a structured value or even echoed back through computed style.
var document = ParseDocument("<div id=target style=\"filter: grayscale(0.9) blur(2px);\"></div>");
var target = document.GetElementById("target");

Assert.AreEqual("filter: grayscale(0.9) blur(2px);", target.GetAttribute("style"));
}
}
}
57 changes: 57 additions & 0 deletions src/AngleSharp.Css.Tests/Styling/HoverPseudoClass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#nullable disable
namespace AngleSharp.Css.Tests.Styling
{
using AngleSharp.Dom;
using NUnit.Framework;
using static CssConstructionFunctions;

/// <summary>
/// AngleSharp has no notion of pointer/interaction state, so `:hover` is unconditionally
/// non-matching unless explicitly forced via <see cref="ElementExtensions.SetPseudoClass"/>
/// (see PseudoClassForcing.cs for the general mechanism this relies on). These tests pin down
/// the specific `:hover` case that first surfaced the gap while building CSS `transition`
/// support in a downstream renderer (AngleSharp.Renderer).
/// </summary>
[TestFixture]
public class HoverPseudoClassTests
{
[Test]
public void HoverDoesNotMatchByDefault()
{
var document = ParseDocument("<div id=target></div>");
var target = document.GetElementById("target");

Assert.IsFalse(target.Matches(":hover"));
Assert.IsTrue(target.Matches(":not(:hover)"));
}

[Test]
public void ForcingHoverMakesItMatchInTheSelectorEngine()
{
var document = ParseDocument("<div id=target></div>");
var target = document.GetElementById("target");

target.SetPseudoClass("hover");

Assert.IsTrue(target.Matches(":hover"));
Assert.IsFalse(target.Matches(":not(:hover)"));
}

[Test]
public void ForcingHoverLetsTheMoreSpecificHoverRuleWinTheCascade()
{
// #target:hover is more specific than #target and declared after it, so once :hover
// is forced to match, the cascade should resolve to red instead of blue.
var document = ParseDocument(@"<html><head><style>
#target { background-color: rgb(0, 0, 255); }
#target:hover { background-color: rgb(255, 0, 0); }
</style></head><body><div id=target></div></body></html>");
var target = document.GetElementById("target");

target.SetPseudoClass("hover");
var backgroundColor = target.ComputeCurrentStyle().GetPropertyValue("background-color");

Assert.AreEqual("rgba(255, 0, 0, 1)", backgroundColor);
}
}
}
Loading
Loading