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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ env:
# Snapshot baselines are per-platform and committed; a missing one is a gap, never a pass.
ANGLESHARP_SNAPSHOT_STRICT: 1
ANGLESHARP_VERSION: 1.8.1
ANGLESHARP_CSS_VERSION: 1.1.2
ANGLESHARP_CSS_VERSION: 1.2.0

jobs:
can_document:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ permissions:

env:
ANGLESHARP_VERSION: 1.8.1
ANGLESHARP_CSS_VERSION: 1.1.2
ANGLESHARP_CSS_VERSION: 1.2.0

jobs:
# Keep this matrix identical to the `test` matrix in ci.yml, images included - a baseline
Expand Down
525 changes: 144 additions & 381 deletions AGENTS.md

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,61 @@
# 0.8.0

Released on Wednesday, October 21 2026.

- Improved inline-blocks, floats and absolutely positioned boxes with an auto width, which now shrink to fit their content
- Improved inline backgrounds and borders, which are painted per line fragment
- Improved images, which are now inline boxes sized from their natural size, with the background color painted under background images
- Updated the required AngleSharp.Css version to 1.2.0
- Updated styling to come only from the computed style: removed the tag-name fallbacks for inline display, heading weight, hidden `<script>`/`<style>`/`input[type=hidden]`, `<textarea>` white-space and the form-control look (the missing default-stylesheet rules are reported to AngleSharp.Css)
- Updated `FillRectCommand` to carry an optional `PaintArea` (the box a gradient or image paint is sized in), with a matching `DisplayList.FillRect` overload
- Updated table layout to detect rows and cells by their `display` value instead of tag names
- Updated `RenderGradientPaint` and `RenderImagePaint` to carry optional tiling and repeat modes
- Removed a hard-coded `justify-content` for elements with the `nested` class
- Fixed inline layout: inline content now forms real line boxes, so inline siblings directly in `<body>` share a line, adjacent inline elements are no longer separated by a spurious space, mixed sizes share one baseline, nested inline styles are kept, and `white-space` preservation, `word-break`, `overflow-wrap` and `text-align` apply to mixed inline content
- Fixed `font-weight: bolder`/`lighter` being ignored (so `<b>`, `<strong>` and headings render bold via the default stylesheet)
- Fixed percentage `border-radius` values, which now resolve against the border box
- Fixed `vertical-align` on a box shifting the text inside it
- Fixed `min-width`/`max-width` being ignored for block widths
- Added the border styles `dotted`, `groove`, `ridge`, `inset` and `outset`
- Added correct rounded borders with differing widths, colors or styles (new `FillRingCommand`)
- Added multiple background layers, `background-repeat: space | round`, and sized/positioned/repeated gradient backgrounds
- Added `border-image` (new `DrawBorderImageCommand`)
- Added CSS masks via `mask-image` and its companion properties (new `PushMaskCommand`/`PopMaskCommand`)
- Added `backdrop-filter` (new `DrawBackdropFilterCommand`)
- Added the individual transform properties `translate`, `rotate` and `scale`
- Added `float: right` and `clear`, with floats shared across a block formatting context: text in following blocks wraps around them line by line, and `overflow`/`flow-root` containers enclose their floats
- Added `text-transform` and `word-spacing`
- Added `outline-offset`, `background-clip` and `background-origin` (images and gradients now start inside the border by default, as `background-origin: padding-box` specifies)
- Added `mix-blend-mode`, `isolation` and `background-blend-mode`
- Added intrinsic grid column sizing: `auto`, `min-content`, `max-content`, `fit-content()` and `minmax()` columns are sized from their items' min/max-content widths instead of a character-count estimate, and `auto` columns stretch into leftover space unless `justify-content` packs them
- Added quote generation in `content` (`open-quote`, `close-quote`, `no-open-quote`, `no-close-quote`) with the `quotes` property and language-dependent defaults, and images in `content: url()`, also mixed with text
- Added transitions and animations of `transform`, `translate`, `rotate`, `scale`, `filter` and `backdrop-filter`
- Added `input[type=image]`, the date/time input family (`date`, `time`, `datetime-local`, `month`, `week`) with formatted values and picker icons, and `accent-color`
- Added page CSS for inline SVG (including inheritance from the surrounding HTML) and the remaining SVG filter primitives (`feTurbulence`, `feDiffuseLighting`, `feSpecularLighting`, `feDisplacementMap`, `feConvolveMatrix`, `feTile`, `feImage`, `feBlend`, all `feComponentTransfer` functions, `SourceAlpha`), with filter regions, primitive subregions and `color-interpolation-filters` (linearRGB by default)
- Fixed `feFlood` to fill its whole region instead of recoloring the input
- Added multi-column layout (`column-count`, `column-width`, `column-gap`, `column-rule`)
- Added the Unicode Bidirectional Algorithm (UAX #9) with `unicode-bidi`, replacing the word-order reversal for `direction: rtl`
- Added WOFF and WOFF2 web fonts in `@font-face`
- Added `text-align: justify` and `text-align-last`
- Added soft-hyphen line breaking and `hyphens` (`auto` behaves like `manual`)
- Added `tab-size`
- Added synthesized small capitals for `font-variant: small-caps`/`all-small-caps`
- Added `font-stretch` for installed and `@font-face` fonts (new optional `FontStretch` on `RenderFont`, `DrawTextCommand` and `DrawTextShadowCommand`)
- Added `text-overflow: ellipsis` and `-webkit-line-clamp` for mixed inline content
- Fixed `text-indent` not being inherited by nested blocks
- Updated table layout: the table box (margins, `auto` centering, `clear`, border, padding, background), `border-spacing`, shrink-to-fit auto widths from per-column min/max-content widths, `table-layout: fixed`, cells laid out as real flow content with their own computed borders, padding and backgrounds (no more hard-coded black borders and 4px padding), `vertical-align: baseline` for cells, row/row-group/column backgrounds, captions with `caption-side`, header and footer groups, `empty-cells`, `visibility: collapse` for rows and columns, percentage row and cell heights, a collapsing border model in which the widest border wins each grid line, and tables inside shrink-to-fit boxes
- Fixed percentage `column-gap`/`row-gap`/`gap` in grid and flex containers being ignored
- Added `writing-mode` (`vertical-rl`, `vertical-lr`, `sideways-rl`, `sideways-lr`) with orthogonal flows in either direction, physical box properties mapped onto the logical ones, and `text-orientation` (CJK upright by default under `mixed`)
- Added `position: sticky` with `bottom`, `left` and `right`, confined to the containing block, and painted over later siblings at any nesting depth
- Added named grid lines (including the implicit lines of `grid-template-areas`), negative line numbers, `justify-self`, start/center/end alignment for grid items, and `fr` rows in grids without a definite height
- Added `transform`, `opacity`, `filter`, `mix-blend-mode`, `clip-path`, masks and `backdrop-filter` on flex, grid and table containers
- Improved flex sizing to follow the spec's flexible length resolution: definite `flex-basis` values are no longer widened to the content, items do not shrink below their content (`min-width: auto`), shrinking is weighted by base size, items frozen at `min`/`max` sizes leave the space to the others, and item heights come from a real layout
- Fixed grid containers growing past their authored height
- Fixed percentage widths of children inflating the intrinsic width of shrink-to-fit boxes and flex items
- Fixed the `gap` shorthand being left out of the intrinsic width of flex containers
- Fixed `visibility: hidden` removing elements from layout: hidden boxes now keep their space and only skip painting, and descendants with `visibility: visible` are painted
- Fixed boxes with a decided width of zero (empty table cells, atomic inlines) losing their height

# 0.7.0

Released on Monday, October 19 2026.
Expand Down
7 changes: 4 additions & 3 deletions docs/general/01-Basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,11 @@ This implementation is intentionally lightweight and bitmap-backed, but it suppo

## Text And Fonts

The renderer supports basic text styling such as font size, weight, italic, alignment, spacing, and decoration.
Generic font families like `serif`, `sans-serif`, and `monospace` are mapped to installed local fonts so they render as visibly different faces.
The renderer supports text styling such as font size, weight, italic, alignment, letter and word spacing, `text-transform`, decorations, shadows, `white-space`, `word-break`, `overflow-wrap`, and `text-overflow`.
Inline content is laid out in line boxes, so nested inline elements keep their own styles, share a baseline with the surrounding text, and wrap together with it.

If you want predictable output in visual tests, specify an explicit font family and keep the host environment consistent.
Generic font families like `serif`, `sans-serif`, and `monospace` always map to fonts bundled with the renderer, so they render the same on every machine.
Named families resolve only when installed on the host (or declared via `@font-face`), so prefer generic families when you need predictable output in visual tests.

## Visual Testing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@
<ProjectReference Include="..\AngleSharp.Renderer\AngleSharp.Renderer.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="fonts\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

</Project>
49 changes: 47 additions & 2 deletions src/AngleSharp.Renderer.Tests/FontFaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,56 @@ public async Task EmbeddedFace_TakesPrecedenceOverTheFallbackEntry()
Assert.Equal(await MeasureAsync(string.Empty, "serif"), width, tolerance: 0.01f);
}

[Theory]
[InlineData("DejaVuSerif-Latin1.woff", "woff")]
[InlineData("DejaVuSerif-Latin1.woff2", "woff2")]
[InlineData("DejaVuSerif-Latin1-hmtx.woff2", "woff2")]
public async Task WebFontFace_MeasuresLikeTheFontItWraps(string fileName, string format)
{
// The fixtures are an ASCII/Latin-1 subset of the bundled DejaVu Serif, saved as TTF,
// WOFF, and WOFF2 by fontTools (with the transformed glyf/loca tables, and in the -hmtx
// file also the transformed hmtx table).
var width = await MeasureAsync(
$"@font-face {{ font-family: 'Embedded'; src: url({FixtureDataUri(fileName, format)}) format('{format}'); }}",
"Embedded");

Assert.Equal(await MeasureAsync(string.Empty, "serif"), width, tolerance: 0.01f);
}

[Theory]
[InlineData("DejaVuSerif-Latin1.woff", "woff")]
[InlineData("DejaVuSerif-Latin1.woff2", "woff2")]
[InlineData("DejaVuSerif-Latin1-hmtx.woff2", "woff2")]
public async Task WebFontFace_RendersTheSameGlyphsAsTheFontItWraps(string fileName, string format)
{
// Identical pixels mean the decoded outlines, hinting instructions and metrics match the
// original font, including the composite glyphs of the accented letters.
var reference = await RenderSampleAsync(FixtureDataUri("DejaVuSerif-Latin1.ttf", "truetype"), "truetype");
var decoded = await RenderSampleAsync(FixtureDataUri(fileName, format), format);

Assert.Equal(reference, decoded);
}

private static string FixtureDataUri(string fileName, string format) =>
$"data:font/{format};base64,{Convert.ToBase64String(File.ReadAllBytes(Path.Combine(AppContext.BaseDirectory, "fonts", fileName)))}";

private static async Task<byte[]> RenderSampleAsync(string dataUri, string format)
{
var html =
"<html><head><style>" +
$"@font-face {{ font-family: 'Embedded'; src: url({dataUri}) format('{format}'); }}" +
"body { margin: 0; font-family: 'Embedded'; font-size: 24px; }" +
"</style></head><body>Handgloves {}[] àéîõü ÅÇÑØ ß 0123</body></html>";
var document = await BrowsingContext.New(Configuration.Default.WithCss()).OpenAsync(request => request.Content(html));
var image = new HtmlRenderer().RenderToPng(document, new DefaultRenderDevice { ViewPortWidth = 420, ViewPortHeight = 40 });
return image.Data;
}

[Fact]
public async Task UnsupportedFormat_FallsThroughToTheNextSource()
{
// Skia cannot decode the compressed wrappers, so a woff2 source has to be skipped rather
// than claim the family and leave it unrenderable.
// A woff2 source that cannot be decoded has to be skipped rather than claim the family
// and leave it unrenderable.
var width = await MeasureAsync(
"@font-face { font-family: 'Embedded'; " +
$"src: url(data:font/woff2;base64,{Convert.ToBase64String("wOF2padding"u8.ToArray())}) format('woff2'), " +
Expand Down
134 changes: 134 additions & 0 deletions src/AngleSharp.Renderer.Tests/FormControlTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
namespace AngleSharp.Renderer.Tests;

using AngleSharp;
using AngleSharp.Css;
using AngleSharp.Renderer.Rendering;

/// <summary>
/// Image buttons, the date/time input family and <c>accent-color</c>.
/// </summary>
public sealed class FormControlTests
{
private static readonly RenderColor DefaultAccent = new(26, 115, 232);

[Fact]
public async Task ImageInput_PaintsItsImageAtItsNaturalSize()
{
var displayList = await LayoutAsync($"<input type=\"image\" alt=\"Go\" src=\"{GeneratedContentTests.PngDataUri(20, 14)}\">");

var image = Assert.Single(displayList.Commands.OfType<DrawImageCommand>());
Assert.Equal(20f, image.Rect.Width, precision: 2);
Assert.Equal(14f, image.Rect.Height, precision: 2);
Assert.Empty(displayList.Commands.OfType<DrawTextCommand>());
}

[Fact]
public async Task ImageInput_TakesTheAuthoredSize()
{
var displayList = await LayoutAsync($"<input type=\"image\" style=\"width:40px; height:28px\" src=\"{GeneratedContentTests.PngDataUri(20, 14)}\">");

var image = Assert.Single(displayList.Commands.OfType<DrawImageCommand>());
Assert.Equal(40f, image.Rect.Width, precision: 2);
Assert.Equal(28f, image.Rect.Height, precision: 2);
}

[Theory]
[InlineData("alt=\"Go\"", "Go")]
[InlineData("", "Submit")]
public async Task ImageInputWithoutImage_ShowsItsAltText(string alt, string expected)
{
var displayList = await LayoutAsync($"<input type=\"image\" src=\"missing.png\" {alt}>");

Assert.Empty(displayList.Commands.OfType<DrawImageCommand>());
Assert.Equal(expected, Assert.Single(displayList.Commands.OfType<DrawTextCommand>()).Text);
}

[Theory]
[InlineData("date", "", "mm/dd/yyyy")]
[InlineData("date", "2024-05-03", "05/03/2024")]
[InlineData("time", "", "--:-- --")]
[InlineData("time", "13:05", "01:05 PM")]
[InlineData("time", "00:30", "12:30 AM")]
[InlineData("datetime-local", "", "mm/dd/yyyy, --:-- --")]
[InlineData("datetime-local", "2024-05-03T13:05", "05/03/2024, 01:05 PM")]
[InlineData("month", "", "--------- ----")]
[InlineData("month", "2024-05", "May 2024")]
[InlineData("week", "", "Week --, ----")]
[InlineData("week", "2024-W18", "Week 18, 2024")]
[InlineData("date", "not-a-date", "mm/dd/yyyy")]
public async Task DateAndTimeInputs_ShowTheirValueOrPlaceholder(string type, string value, string expected)
{
var displayList = await LayoutAsync($"<input type=\"{type}\" value=\"{value}\">");

Assert.Equal(expected, Assert.Single(displayList.Commands.OfType<DrawTextCommand>()).Text);
}

[Theory]
[InlineData("date")]
[InlineData("time")]
public async Task DateAndTimeInputs_PaintAPickerIconAfterTheText(string type)
{
var displayList = await LayoutAsync($"<input type=\"{type}\" style=\"color:#00ff00\">");

var text = Assert.Single(displayList.Commands.OfType<DrawTextCommand>());
var icon = displayList.Commands.OfType<StrokeRoundedRectCommand>().Where(command => command.Color.Equals(new RenderColor(0, 255, 0))).ToList();
Assert.NotEmpty(icon);
Assert.All(icon, part => Assert.True(part.Rect.X > text.X, $"icon at {part.Rect.X} is not after the text at {text.X}"));
}

[Fact]
public async Task DateInput_IsWideEnoughForItsTextAndIcon()
{
var displayList = await LayoutAsync("<input type=\"date\" style=\"border:0; padding:0; background:#ff0000\">");

var box = Assert.Single(displayList.Commands.OfType<FillRectCommand>(), command => command.Color.Equals(new RenderColor(255, 0, 0)));
var icon = displayList.Commands.OfType<StrokeRoundedRectCommand>().ToList();
Assert.NotEmpty(icon);
Assert.True(icon.Max(part => part.Rect.X + part.Rect.Width) <= box.Rect.X + box.Rect.Width + 0.01f);
}

[Theory]
[InlineData("checkbox")]
[InlineData("radio")]
public async Task CheckedControls_UseTheDefaultAccentColor(string type)
{
var displayList = await LayoutAsync($"<input type=\"{type}\" checked>");

Assert.Contains(displayList.Commands.OfType<FillRectCommand>(), command => command.Color.Equals(DefaultAccent));
}

[Theory]
[InlineData("checkbox", "checked")]
[InlineData("radio", "checked")]
[InlineData("range", "")]
public async Task AccentColor_ColorsTheControl(string type, string attributes)
{
var displayList = await LayoutAsync($"<input type=\"{type}\" {attributes} style=\"accent-color: rgb(200, 0, 100)\">");

Assert.Contains(displayList.Commands.OfType<FillRectCommand>(), command => command.Color.Equals(new RenderColor(200, 0, 100)));
Assert.DoesNotContain(displayList.Commands.OfType<FillRectCommand>(), command => command.Color.Equals(DefaultAccent));
}

[Fact]
public async Task AccentColor_IsInherited()
{
var displayList = await LayoutAsync("<form style=\"accent-color: rgb(200, 0, 100)\"><input type=\"checkbox\" checked></form>");

Assert.Contains(displayList.Commands.OfType<FillRectCommand>(), command => command.Color.Equals(new RenderColor(200, 0, 100)));
}

[Fact]
public async Task AccentColorAuto_UsesTheDefault()
{
var displayList = await LayoutAsync("<input type=\"checkbox\" checked style=\"accent-color: auto\">");

Assert.Contains(displayList.Commands.OfType<FillRectCommand>(), command => command.Color.Equals(DefaultAccent));
}

private static async Task<DisplayList> LayoutAsync(string body)
{
var context = BrowsingContext.New(Configuration.Default.WithCss());
var document = await context.OpenAsync(request => request.Content($"<html><body>{body}</body></html>"));
return new HtmlRenderer().BuildDisplayList(document, new DefaultRenderDevice { ViewPortWidth = 400, ViewPortHeight = 300 });
}
}
Loading
Loading