Skip to content

CSS 2.1 test parity: generated content, counters, list markers (stacked on #264) - #265

Merged
eXpl0it3r merged 4 commits into
ArthurHub:masterfrom
jhaygood86:feature/css21-generated-content
Nov 9, 2026
Merged

eXpl0it3r merged 4 commits into
ArthurHub:masterfrom
jhaygood86:feature/css21-generated-content

Conversation

@jhaygood86

Copy link
Copy Markdown
Contributor

Part 2 of a CSS 2.1 test-parity sweep against PeachPDF's Integration test suite. Stacked on #264 (float/clear/positioning) and #263 - the diff below includes both until they merge. Only the last 4 commits (counter engine, list-marker fixes, one stale-test fix) are this PR's own.

Summary

Scope: chapter 12 (generated content, automatic numbering, lists) - previously zero behavioral coverage in either test project (only content/list-style CSS-OM parsing).

  • counter-reset/counter-increment: had zero effect anywhere - content: counter() was explicitly out of scope per CssContentEngine's own doc comment, and there was no counter-value tracking in Core at all. Implemented CssCounterEngine: a forward-order tree walk threading named counter values through document order (parent→first-child, sibling→sibling), covering ordinary sequential/nested counter usage. Deliberately scoped down from PeachPDF's full §12.4 algorithm - reversed() and counter-set aren't implemented; see the class's own remarks for the exact boundary. Ported PeachPDF's CssCounterEngineTests.cs (pure counter-style formatting, 17 tests) and the counter-related subset of CssContentEngineTests.cs, plus two new integration tests proving the document-order threading itself (sequential siblings, nested-ancestor visibility).
  • List markers: <li value="N"> had no effect (only <ol start>/reversed were consulted) - fixed. Also found and fixed list-style-type:square rendering "♠" (a card-suit glyph, not a square) while auditing this code.
  • Stale-Ignore audit: one existing [Ignore]d test claimed "this fork has no pseudo-element support at all" - false (::before clearly already worked, per the passing test right above it in the same file). Un-ignoring it surfaced a real bug in the test itself (a FirstOrDefault predicate ambiguous between the real text node and the ::after box), not the engine - fixed and passing.
  • PeachPDF gap filed: jhaygood86/PeachPDF#863 - the quotes property (and open-quote/close-quote nesting depth per CSS 2.1 §12.2/§15.3) has no test coverage, and doesn't appear to be implemented there either.

Test plan

  • HtmlRenderer.Test full suite: 2449 passed, 0 failed
  • HtmlRenderer.IntegrationTest full suite: 374 passed, 0 failed
  • HtmlRenderer.PdfSharp.Test full suite: 39 passed, 0 failed
  • Full solution builds clean across net8.0, net8.0-windows, netstandard2.0, net462
  • Re-approved the Bullets.png baseline for the corrected square-marker glyph - reviewed the diff image, confirmed it's the intended fix

@eXpl0it3r

Copy link
Copy Markdown
Collaborator

Rebased onto the updated #264 and fixed the counter scoping on top:

  • A counter-reset on a nested element leaked out of its parent, so with <ol><li>A<ol>…3 items…</ol></li><li>B</li></ol> the outer list continued at 4. CssCounterEngine now keeps a stack of instances per counter name, a reset is in scope for the box's following siblings and their descendants, and goes away once the parent ends (§12.4.1). A second reset among the same siblings replaces the instance instead of nesting.
  • Elements with display:none don't reset or increment counters anymore (§12.4.3).
  • A counter that isn't in scope reads as 0 instead of 1, and incrementing one behaves as if the box reset it to 0.

Since the scoping is a proper stack now, counters() should be quite easy to add on top of it at some point. The three cases have tests in CssContentEngineIntegrationTests.

@eXpl0it3r
eXpl0it3r force-pushed the feature/css21-generated-content branch from 0c5f63b to 9377e7f Compare November 9, 2026 08:09
jhaygood86 and others added 4 commits November 9, 2026 09:14
…counter()

counter-reset/counter-increment parsed at the CSS-OM level but had
zero effect anywhere - content: counter()/counters() were explicitly
out of scope in CssContentEngine's own doc comment, and there was no
counter-value tracking anywhere in Core.

Adds CssCounterEngine: a forward-order tree walk (hooked into
DomParser right after CascadeApplyStyles, before pseudo-element
content gets resolved) that threads named counter values through
document order - a box inherits its parent's counters for its first
child, and each child's resulting counters feed the next sibling -
covering ordinary sequential/nested counter usage (including the
common case content:counter() needs). Deliberately not the full CSS
2.1 §12.4 algorithm: reversed() and counter-set aren't implemented,
and a nested counter-reset for a name already in scope always starts
a fresh value rather than the spec's more precise scoping rule - see
the class's own remarks for the exact boundary.

FormatCounterValue is the single counter-style resolver (decimal,
decimal-leading-zero, roman, alpha, with CSS Counter Styles Level 3
§2's fallback-to-decimal for unknown/out-of-range styles), ported
from PeachPDF's CssCounterEngine and reusing this fork's existing
CommonUtils.ConvertToAlphaNumber for the alphabetic styles.

Ports PeachPDF's CssCounterEngineTests.cs (pure formatting, 17 tests)
and the counter-related subset of CssContentEngineTests.cs, adapted
to run through the real box tree (LayoutHarness + a ::before/::after
rule) rather than a hand-built box, since pseudo-element boxes here
are only constructible via real selector matching. Adds two new
integration tests proving the document-order threading itself works
(sequential sibling increments, nested-ancestor visibility) beyond
what PeachPDF's own box-local unit tests exercise.
<li value="N"> (a WHATWG HTML presentational hint) had no effect on
GetIndexForList's numbering - only <ol start>/reversed were consulted.
Later items without their own explicit value now continue counting
from a value-carrying item, matching every browser.

Also fixes list-style-type:square rendering "♠" (U+2660 BLACK SPADE
SUIT card-suit glyph) instead of an actual square - presumably a
copy/paste typo, noticed while auditing this code for the counter
port above. Re-approves the Bullets.png baseline for the corrected
marker glyph.

Ports the CSS2.1-relevant subset of PeachPDF's
ListItemCounterIntegrationTests.cs (ol start/reversed were already
correct; li value and the marker-glyph fix are new).
… test

CssContentWithCssEscapeInString_RendersLiterally was Ignore'd, citing
"this fork has no pseudo-element support at all" - false: ::before
already works (the CSS-escape test right above it in this same file
passes), and CssContentEngine/CssData's pseudo-element creation
predates this port entirely.

Un-ignoring it surfaced a real bug in the TEST itself, not the
engine: its box-finding predicate (HtmlTag == null && Text != null)
matches the real text node "text" (which also has no HtmlTag) before
it reaches the ::after box, since ::after is appended at the end of
p.Boxes while ::before is inserted at index 0 - the sibling test just
above happens to pass because ::before's insert-at-0 placement wins
the FirstOrDefault race by coincidence. Fixed to key off
IsAfterPseudoElement directly, and it passes.
- End a counter-reset's scope with its parent instead of leaking it to later siblings of the parent
- Skip display:none subtrees when resolving counters
- Read counters that aren't in scope as 0
@eXpl0it3r
eXpl0it3r force-pushed the feature/css21-generated-content branch from 9377e7f to 5c13e9a Compare November 9, 2026 08:14
@eXpl0it3r eXpl0it3r added the CSS label Nov 9, 2026
@eXpl0it3r
eXpl0it3r merged commit 8cdb55f into ArthurHub:master Nov 9, 2026
9 checks passed
@eXpl0it3r

Copy link
Copy Markdown
Collaborator

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants