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
39 changes: 20 additions & 19 deletions AGENTS.md

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
# 0.9.0

Released on Friday, October 30 2026.

- Fixed `-webkit-line-clamp` counting lines per inline run instead of across the nested blocks of the clamped box (e.g. paragraphs in a clamped card), which now ends after its last line
- Fixed gaps with font- or viewport-relative units (e.g. Tailwind's `gap-4`, i.e. `gap: 1rem`) being ignored in grid and flex containers
- Fixed images (and other replaced elements) with `border-radius` not being clipped to its curve (e.g. a `rounded-full` avatar painted square)
- Fixed `line-height` lengths (e.g. `16px`, or `1rem` as in Tailwind's `text-xs`) being ignored in favor of the inherited (or default) line height
- Fixed inline-level flex items and non-stretched grid items (e.g. a `<span>`) being as tall as their inline box instead of their line boxes, which let text overflow them at the bottom (e.g. tag pills with padding)
- Fixed words breaking after a hyphen or slash in a box sized to fit them (e.g. a flex item with the max-content width of `c-sharp`), as the width came out of the box arithmetic a fraction of a pixel short
- Added grid track alignment: `justify-content` and `align-content` (`start`, `end`, `center`, `space-between`, `space-around`, `space-evenly`, with `safe`) position the tracks of a grid whose tracks do not fill it
- Added lines moving down past floats: a line whose first word does not fit beside the floats moves down to where one of them ends, until it fits or no float is beside it
- Added `writing-mode` on flex, grid and table containers: a container whose writing mode differs from its parent's lays out its items along its own axes (e.g. a `vertical-rl` flex container runs its main axis downwards)
- Added nested writing modes in the same orientation: a `vertical-lr` element in `vertical-rl` content (or the reverse) stacks its blocks in its own direction
- Added `position: sticky` inside orthogonal flows (e.g. in a `vertical-rl` box), which used to stay static
- Added anonymous table objects: stray `display: table-cell` boxes (e.g. `<div style="display:table-cell">` used for layout) form a row of an anonymous table, rows outside a table get one, and text or other content directly inside a table or row is wrapped in an anonymous row or cell instead of being dropped
- Added percentage table heights (e.g. `table { height: 50% }`), resolved against a definite containing block height
- Added `column-span: all` in multi-column containers: the spanner is laid out across all columns, and the content on each side of it is balanced separately
- Added text shaping with HarfBuzz (via `SkiaSharp.HarfBuzz`): kerning and ligatures (e.g. `fi`, `fl`), Arabic joining and complex scripts such as Devanagari, measured and painted from the same shaped glyphs, so line breaking, alignment and table widths use the shaped advances
- Added `font-feature-settings`, `font-kerning`, `font-variant-ligatures` and `font-variant-numeric` (e.g. `font-feature-settings: "liga" 0` turns ligatures off, `tabular-nums` selects tabular figures); a non-zero `letter-spacing` turns optional ligatures off
- Added Noto Sans Devanagari as a bundled fallback font (its OFL license is included in the package), so Devanagari renders without an installed font
- Changed right-to-left text in the display list: `DrawTextCommand.Text` is now in logical order with `IsRightToLeft` set, and shaping reverses and mirrors it (it used to be reversed and mirrored by layout)
- Changed `text-overflow: ellipsis` to cut at glyph clusters (`ITextMeasurer.GetClusterBoundaries`, which defaults to grapheme clusters for custom measurers), so it never splits a ligature or conjunct
- Improved SVG `text`/`tspan`, which is shaped the same way as HTML text, reordered by the bidi algorithm with `direction` as its paragraph direction (which also decides where `text-anchor: start` and `end` are), and in its `lang`
- Added `lang` to text shaping, so fonts with language-specific forms use them (e.g. the Marathi forms of Noto Sans Devanagari for `lang="mr"`)
- Added joining across element boundaries: an Arabic word split by an inline element (e.g. one letter in `<b>`) keeps its joined forms
- Changed Canvas 2D `fillText` to use the context's `font` (family, size, weight, style) with the same fonts, shaping and bidi reordering as HTML text instead of the host's default typeface
- Added fragmentation in multi-column containers: `break-before`/`break-after` (`column` forces a new column, `avoid`), `break-inside: avoid`, `orphans`/`widows` (lines of a paragraph are no longer left alone at the top or bottom of a column), `column-fill: auto|balance` with a definite height (balanced by default), and blocks without lines splitting across columns

# 0.8.2

Released on Sunday, October 25 2026.
Expand Down
4 changes: 4 additions & 0 deletions docs/general/01-Basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ Inline content is laid out in line boxes, so nested inline elements keep their o
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.

Text is shaped with HarfBuzz, so kerning and ligatures (`fi`, `fl`) apply, Arabic letters join, and complex scripts such as Devanagari form their conjuncts.
`font-feature-settings`, `font-kerning`, `font-variant-ligatures` and `font-variant-numeric` select the OpenType features of the font; a non-zero `letter-spacing` turns optional ligatures off.
The bundled fonts cover Latin, Greek, Cyrillic, Hebrew and Arabic (DejaVu) and Devanagari (Noto Sans Devanagari, used only as a fallback); other scripts need an installed font or an `@font-face` that covers them.

## Visual Testing

The test suite uses PNG snapshots under `verification-assets/`. Missing baselines are created automatically unless strict mode is enabled with `ANGLESHARP_SNAPSHOT_STRICT=1`.
8 changes: 6 additions & 2 deletions docs/tutorials/03-Questions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ section: "AngleSharp.Renderer"

## Why do web-safe fonts look the same on my machine?

The renderer maps generic families such as `serif`, `sans-serif`, and `monospace` to installed local fonts. If your system does not have distinct serif/sans/monospace faces available, Skia may fall back to similar-looking defaults.
The renderer maps generic families such as `serif`, `sans-serif`, and `monospace` to fonts bundled with it (DejaVu), not to installed fonts, so they look the same on every machine.

For stable results, install a font set with visibly different faces or use explicit family names that are present on the target machine.
Named families (e.g. `Arial`) resolve only when installed on the host or declared via `@font-face`; otherwise the next entry of the `font-family` list is used.

## Why does my text in another script show boxes?

Characters the resolved font has no glyph for fall back to the next `font-family` entry, then the bundled fonts (Latin, Greek, Cyrillic, Hebrew, Arabic, Devanagari), then whatever the host has installed. For other scripts (e.g. Thai or CJK) declare an `@font-face` or install a font that covers them.

## Why does a snapshot fail when I add a new baseline?

Expand Down
38 changes: 38 additions & 0 deletions src/AngleSharp.Renderer.Benchmarks/BASELINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ Against `AngleSharp` 1.8.1, `AngleSharp.Css` 1.1.2 (both via `PackageReference`)
| **Layout only: `BuildDisplayList`** | **364 ms** | **194.14 MB** |
| Full pipeline: layout + Skia rasterization + PNG encoding (`RenderToPng`) | 527 ms | 199.41 MB |

> **Note (2026-10-28):** the tables above predate HarfBuzz text shaping and were measured on the
> Apple M5. The shaping change was measured before/after on a different machine (see the last
> History entry); re-run these tables on the M5 to refresh them.

**Cumulative improvement over the original, pre-optimization baseline: ~9.5x faster, ~7.5x less
allocation** on `BuildDisplayList` (net10.0: 2,415 ms / 1,456 MB &rarr; 255 ms / 194 MB). The
net8.0 mean above carries a wide error margin in this particular short-job run (~285ms on a 364ms
Expand Down Expand Up @@ -206,3 +210,37 @@ BenchmarkDotNet refuses to run outside `Release` and warns loudly if it detects
unoptimized build - always use `-c Release`. Update the tables above (and the date) after a
meaningful optimization lands, so this file keeps tracking the current baseline rather than one
frozen the day it was first written.

### 2026-10-28, HarfBuzz text shaping (cost, plus a width cache in `SkiaTextMeasurer`)

Measured on a different machine than the tables above, so only the before/after pair is
comparable: AMD Ryzen 7 3700X (16 threads), Linux (WSL2), .NET SDK 10.0.100, `short` job.
"Before" is commit `59b892a` (unshaped `canvas.DrawText`), "shaped" is HarfBuzz shaping in
`SkiaTextShaping` without any caching, "shaped + cache" adds the per-thread width cache that
`SkiaTextMeasurer.MeasureWidth` now keeps (keyed by text and `RenderFont`, cleared at 16,384
entries).

| `BuildDisplayList` | net10.0 Mean | net8.0 Mean | Allocated |
|---|---:|---:|---:|
| before | 423 ms | 401 ms | 119.9 MB |
| shaped | 443 ms | 509 ms | 142.4 MB |
| shaped + cache | 409 ms | 355 ms | 121.7 MB |

| `RenderToPng` | net10.0 Mean | net8.0 Mean | Allocated |
|---|---:|---:|---:|
| before | 2,630 ms | 2,586 ms | 124.7 MB |
| shaped | 2,923 ms | 2,988 ms | 148.1 MB |
| shaped + cache | 2,867 ms | 2,963 ms | 127.3 MB |

The short job's error margins are wide on this machine (up to +/-30 ms StdDev for layout), so the
layout means are best read as "back to parity" with the cache; allocation is deterministic and
ends up +1.8 MB (+1.5%). Painting keeps a real cost of roughly +9-15% on the full pipeline: every
painted run is shaped once more when it is drawn (lines are mostly unique, so a paint-side cache
would rarely hit).

Nested trial layouts: a probe laying out 20 copies of inline-blocks nested 1-5 deep (each with a
12-word sentence) in a 300px box. The number of `MeasureWidth` calls is identical before and after
(trial layouts multiply it by ~2-2.5x per level: 1,280 / 4,020 / 9,760 / 21,500 / 41,946), and
with the width cache the repeated calls cost a dictionary lookup instead of a shaping pass, so the
time per layout did not grow (before: 17 / 36 / 89 / 160 / 154 ms, after: 19 / 45 / 76 / 133 /
123 ms for depths 1-5; single runs of 5 layouts, so +/-20%).
2 changes: 1 addition & 1 deletion src/AngleSharp.Renderer.Docs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@anglesharp/renderer",
"version": "0.8.2",
"version": "0.9.0",
"preview": true,
"description": "The doclet for the AngleSharp.Renderer documentation.",
"keywords": [
Expand Down
Loading
Loading