Add counter-reset, counter-increment properties with scope-based counter tracking during box tree construction. Implement counter(), counters(), and quote keywords (open-quote, close-quote, no-open-quote, no-close-quote) in the content property. Includes code review fixes: removed dead code in counter parser, eliminated wasteful allocation in counter formatting, added counter properties to ComputedStyles::dump(). 4 golden tests added, 2 WPT tests promoted to pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
22 KiB
Story 1.4: Generated Content
Status: done
Story
As a web user,
I want CSS-generated content (bullets, quotes, labels) to render correctly,
so that pages using ::before and ::after pseudo-elements display as intended.
Acceptance Criteria
- Given a CSS rule with
::beforeor::afterand acontentproperty with a string value, When the page is rendered, Then the string content is inserted as an inline box before/after the element's content per CSS 2.1 §12.1 - Given a
contentvalue usingattr(), When the page is rendered, Then the attribute value from the HTML element is inserted as generated content - Given a
contentvalue usingcounter()withcounter-resetandcounter-increment, When the page is rendered, Then the counter value is computed and displayed per CSS 2.1 §12.4 - Given generated content with styling (color, font-size, display), When the page is rendered, Then the pseudo-element is styled independently from its originating element
- Golden tests cover string content, attr(), counter(), checklist is updated, and
just cipasses
Tasks / Subtasks
NOTE: AC #1, #2, and #4 are already implemented and passing. This story focuses on completing AC #3 (counters) and adding missing content value types.
-
Task 1:
counter-resetandcounter-incrementCSS property parsing (AC: #3)- 1.1 Add
PropertyId::CounterResetandPropertyId::CounterIncrementvariants incrates/css/src/types.rs(~line 348-487, PropertyId enum) - 1.2 Add
CssValuevariants for counter properties:CssValue::CounterReset(Vec<(String, i32)>)andCssValue::CounterIncrement(Vec<(String, i32)>)— each is a list of (counter-name, integer) pairs per CSS 2.1 §12.4 - 1.3 Implement
parse_counter_reset()incrates/css/src/parser/mod.rs— syntax:counter-reset: <identifier> <integer>?(default 0), supports multiple counters,nonekeyword - 1.4 Implement
parse_counter_increment()incrates/css/src/parser/mod.rs— syntax:counter-increment: <identifier> <integer>?(default 1), supports multiple counters,nonekeyword - 1.5 Wire both properties into the main property dispatch in parser
- 1.6 Unit tests for counter property parsing: single counter, counter with value, multiple counters,
none, invalid values
- 1.1 Add
-
Task 2:
counter()andcounters()in content property parsing (AC: #3)- 2.1 Add
ContentItem::Counter(String, ListStyleType)variant incrates/css/src/types.rs— counter name + optional list-style-type (default decimal) - 2.2 Add
ContentItem::Counters(String, String, ListStyleType)variant — counter name + separator string + optional list-style-type - 2.3 Extend
parse_content_value()incrates/css/src/parser/mod.rs(~line 1670-1730) to parsecounter(<identifier>),counter(<identifier>, <list-style-type>),counters(<identifier>, <string>),counters(<identifier>, <string>, <list-style-type>) - 2.4 Unit tests for counter/counters parsing in
crates/css/src/tests/content_tests.rs
- 2.1 Add
-
Task 3: Counter state tracking in style computation (AC: #3)
- 3.1 Add
counter_reset: Vec<(String, i32)>andcounter_increment: Vec<(String, i32)>fields toComputedStylesincrates/style/src/types/computed.rs - 3.2 Wire cascade resolution for
counter-resetandcounter-incrementincrates/style/src/types/computed.rs(apply_declaration) - 3.3 Implement counter scope tracking: create a
CounterContextstruct incrates/layout/src/engine/counter.rsthat maintains a stack of counter scopes. Per CSS 2.1 §12.4:counter-resetcreates a new counter instance in the current scope,counter-incrementincrements the innermost counter with that name - 3.4
CounterContextlives in layout crate as aRefCellfield onLayoutEngine— counters are evaluated during box tree construction in document order - 3.5 Unit tests for counter scope creation, increment, and nesting
- 3.1 Add
-
Task 4: Counter value resolution in generated content (AC: #3)
- 4.1 During box tree construction in
crates/layout/src/engine/box_tree.rs, when processingContentItem::Counter(name, style), look up the counter value fromCounterContextand format it using the list-style-type formatter - 4.2 For
ContentItem::Counters(name, separator, style), collect all counter instances with that name (from outermost to innermost scope), format each, and join with separator - 4.3 Reuse existing
ListStyleTypeformatting logic from list marker generation viaformat_list_markerincrates/layout/src/engine/list_marker.rs - 4.4 Wire counter context through
build_box_tree()andbuild_pseudo_element_box()inbox_tree.rs - 4.5 Unit tests for counter value lookup and formatting
- 4.1 During box tree construction in
-
Task 5: Quote keywords in content property (AC: #1, completes content support)
- 5.1 Add
ContentItem::OpenQuote,ContentItem::CloseQuote,ContentItem::NoOpenQuote,ContentItem::NoCloseQuotevariants incrates/css/src/types.rs - 5.2 Extend
parse_content_value()to recognizeopen-quote,close-quote,no-open-quote,no-close-quotekeywords - 5.3 Implement quote depth tracking (integer depth, starts at 0) —
open-quoteinserts\u201Cat even depth,\u2018at odd depth - 5.4 Wire through style computation and box tree building
- 5.5 Unit tests for quote keyword parsing and depth tracking
- 5.1 Add
-
Task 6: Golden tests and checklist update (AC: #5)
- 6.1 Add golden test 227:
counter-reset+counter-incrementwithcontent: counter(name)— numbered headings - 6.2 Add golden test 228: nested counters with
counters()and separator — "1.1", "1.2" section numbering - 6.3 Add golden test 229: counters with
list-style-type: upper-roman— I, II, III, IV - 6.4 Add golden test 230: quote keywords generating Unicode quote characters
- 6.5 Verify existing golden test 207 (content: attr()) still passes
- 6.6 Update
docs/CSS2.1_Implementation_Checklist.md— checked off: counter-reset, counter-increment, counter(), counters(), quote keywords in content - 6.7 Run
just ciand all tests pass (2 WPT tests promoted from known_fail to pass)
- 6.1 Add golden test 227:
Dev Notes
Current Implementation Status
Generated content is substantially implemented. What works end-to-end:
::beforeand::afterpseudo-elements — full pipeline: selector parsing → matching → style computation → box generation → layout → paintcontent: "string"— single and multiple strings, concatenationcontent: attr(name)— attribute value resolution at style computation time, multiple attr() itemscontent: ""— empty string generates box with no text child (clearfix pattern)content: normal/content: none— correctly suppress box generation- Pseudo-element styling — all CSS properties applied independently, cascade/specificity correct
- Display types — pseudo-elements support block, inline, inline-block, flex, table, etc.
- Replaced element exclusion — img, input, etc. properly excluded from pseudo-element generation
::first-lineand::first-letter— implemented with proper property filtering
What is missing (this story's scope):
counter-reset— PropertyId not defined, no parsing, no cascadecounter-increment— PropertyId not defined, no parsing, no cascadecounter()function — parser rejects it (drops declaration)counters()function — parser rejects it- Quote keywords —
open-quote,close-quote,no-open-quote,no-close-quotenot parsed url()in content — not parsed (low priority, rarely used in CSS 2.1)
Key Code Locations
| Component | File | Key Functions/Lines |
|---|---|---|
| PseudoElement enum | crates/selectors/src/types.rs:162-172 |
Before, After, FirstLine, FirstLetter |
| ContentValue/ContentItem types | crates/css/src/types.rs:230-249 |
ContentValue { Normal, None, String, Items }, ContentItem { String, Attr } |
| PropertyId enum | crates/css/src/types.rs:348-487 |
Needs CounterReset, CounterIncrement added |
| Content parsing | crates/css/src/parser/mod.rs:1670-1730 |
parse_content_value() — extend for counter()/counters()/quotes |
| Content tests | crates/css/src/tests/content_tests.rs |
610 lines, comprehensive string/attr tests |
| ComputedStyles struct | crates/style/src/types/computed.rs:154-162 |
content, before_styles, after_styles fields |
| Pseudo-element style computation | crates/style/src/context.rs:1100-1120 |
compute_pseudo_element_styles() — attr() resolution here |
| Style tests | crates/style/src/tests/pseudo_element.rs |
1011 lines of cascade/inheritance/attr tests |
| Box tree pseudo-element insertion | crates/layout/src/engine/box_tree.rs:160-200 |
::before as first child, ::after as last child |
build_pseudo_element_box() |
crates/layout/src/engine/box_tree.rs:435-506 |
Display type conversion, text content creation, style copying |
| Layout pseudo-element tests | crates/layout/src/tests/pseudo_element_tests.rs |
729 lines |
| ListStyleType enum | crates/style/src/types/primitives.rs |
Existing enum for list markers — reuse for counter formatting |
| List marker generation | crates/layout/src/engine/box_tree.rs |
Existing list-item marker box generation — pattern for counter rendering |
| CSS 2.1 Checklist | docs/CSS2.1_Implementation_Checklist.md:184-192 |
Phase 14 (Lists & Counters) — counter items unchecked |
| Golden test 207 | tests/goldens/fixtures/207-content-attr.html |
Existing ::after with attr() test |
Implementation Approach
Task 1+2 (Counter property + content function parsing):
Follow the CSS Property Implementation Order: parse first. Add PropertyId::CounterReset and PropertyId::CounterIncrement to the PropertyId enum. Add parsing functions similar to existing property parsers in crates/css/src/parser/mod.rs. The syntax is: counter-reset: <ident> <integer>? [, <ident> <integer>?]* where default reset value is 0 and default increment is 1. For content parsing, extend parse_content_value() to handle counter(<ident>[, <list-style-type>]) and counters(<ident>, <string>[, <list-style-type>]).
Task 3 (Counter state tracking): This is the most architecturally complex part. CSS counters have scope rules (§12.4):
counter-resetcreates a new counter in the element's scopecounter-incrementincrements the innermost counter with that name- Counters are inherited — children see parent counters
- The tree traversal order matters (document order)
Best approach: Create a CounterContext that is passed through box tree construction. It maintains a HashMap<String, Vec<i32>> where the Vec represents nested scopes (stack). When processing an element:
- If it has
counter-reset: foo 0, push a new scope for "foo" with value 0 - If it has
counter-increment: foo 1, add 1 to the innermost "foo" - When leaving the element's subtree, pop its counter scopes
This should live in crates/layout/src/engine/box_tree.rs since it's needed during box construction where content values are resolved.
Task 4 (Counter value resolution):
When build_pseudo_element_box() encounters ContentItem::Counter(name, style), look up the current value of counter name from the CounterContext. Format using the ListStyleType — the codebase already has list marker formatting. For ContentItem::Counters(name, sep, style), collect all scope values for that counter name and join with the separator string.
Task 5 (Quote keywords):
Simpler than counters. Track a quote_depth: i32 in the context. open-quote inserts " (depth 0, 2, 4...) or ' (depth 1, 3, 5...) and increments depth. close-quote decrements depth and inserts the closing character. no-open-quote / no-close-quote affect depth without inserting text.
Architecture Constraints
- Layer rule: Changes span
css(Layer 1),style(Layer 1),layout(Layer 1) — all horizontal Layer 1 deps - No unsafe: All affected crates have
unsafe_code = "forbid" - Pipeline order: CSS parse → style compute → layout (box tree build) → display list → render. Counter values resolve during box tree construction (layout phase), not during style computation.
- CSS Property Implementation Order: Parse
counter-reset/counter-incrementincss/→ add toComputedStylesinstyle/→ resolve counter values during box tree build inlayout/→ golden tests → checklist →just ci - Reuse existing infrastructure: Use
ListStyleTypeenum and formatting from list marker code for counter value formatting. Do NOT create a parallel formatting system.
Previous Story Intelligence
From Story 1.1 (Margin Collapsing):
- Golden test infrastructure: fixtures in
tests/goldens/fixtures/, expected intests/goldens/expected/ - Unit tests pattern: construct DOM + style manually, run layout, assert positions
- Checklist update at
docs/CSS2.1_Implementation_Checklist.mdis mandatory just ciis the single validation gate
From Story 1.2 (Stacking Contexts & Z-Index):
- Display list builder is paint order engine — generated content boxes participate normally in paint
- Pseudo-element boxes are regular LayoutBox children — they follow all normal paint rules
From Story 1.3 (Positioning):
- Positioned pseudo-elements (
::before { position: absolute; }) follow normal positioning rules - Pseudo-element boxes go through the same layout dispatch as regular boxes
Counter Scope Rules (CSS 2.1 §12.4 — Critical)
These rules MUST be implemented correctly:
- Scope creation:
counter-reseton an element creates a new counter instance. If a counter with that name already exists in the current scope, a NEW instance is created (nesting). - Incrementing:
counter-incrementaffects the innermost counter with that name. If no counter exists, one is implicitly created at the element withcounter-reset: name 0and then incremented. - Inheritance: Child elements see parent counters. The counter value at any point is the sum of all increments since the last reset in the current scope.
- Self-nesting:
<ol>elements automatically nest counters — each<ol>resets, each<li>increments. This produces "1, 2, 3" at level 1 and "1, 2" at level 2, not "4, 5". counters()function: Collects ALL instances of a named counter from outermost to innermost scope. With separator ".", produces "1.2.3" for nested lists.- Processing order: Elements are processed in document order (depth-first tree traversal). The counter-reset and counter-increment on an element are processed BEFORE the element's content (::before sees the incremented value).
Testing Strategy
- CSS parser tests in
crates/css/src/tests/content_tests.rs— extend existing test file with counter()/counters() parsing tests - Counter property tests — new tests for
counter-resetandcounter-incrementparsing - Counter scope tests — unit tests for
CounterContextscope creation, increment, nesting, popping - Integration golden tests — key scenarios:
- Simple numbered headings:
h2 { counter-increment: section; } h2::before { content: counter(section) ". "; } - Nested counters:
ol { counter-reset: item; } li { counter-increment: item; } li::before { content: counters(item, ".") " "; } - Counter with non-decimal format:
content: counter(section, upper-roman) - Quote characters:
q::before { content: open-quote; } q::after { content: close-quote; }
- Simple numbered headings:
- Regression check: Verify golden test 207 (attr()) and all existing pseudo-element tests still pass
- Run
just ciat the end
CSS 2.1 Spec References
- §12.1 — The
contentproperty: values, applicability to ::before/::after - §12.2 — The
quotesproperty: specifying quote pairs for each nesting level - §12.3 — Inserting quotes with
content: open-quote, close-quote, no-open-quote, no-close-quote - §12.4 — Automatic counters and numbering: counter-reset, counter-increment, counter(), counters()
- §12.4.1 — Nested counters and scope
- §12.4.2 — Counter styles (list-style-type values for formatting)
- §12.5 — Lists: list-style-type, list-style-position, list-style-image (related but separate story 1.6)
Project Structure Notes
- New CSS properties (
counter-reset,counter-increment) follow: parse incss/→ computed instyle/→ resolve inlayout/ CounterContextstruct created incrates/layout/(notstyle/) because counter values resolve during box tree construction- New
ContentItemvariants added tocrates/css/src/types.rs - Reuse
ListStyleTypefromcrates/style/src/types/primitives.rsfor counter formatting - New golden test fixtures go in
tests/goldens/fixtures/with next available number - Checklist update in
docs/CSS2.1_Implementation_Checklist.md(Phase 14 counter items + Phase 15 content items)
References
- [Source: crates/css/src/types.rs#ContentValue] — Content property value types (extend with counter/quote variants)
- [Source: crates/css/src/parser/mod.rs#parse_content_value] — Content property parser (~L1670-1730)
- [Source: crates/css/src/tests/content_tests.rs] — Existing content parsing tests (610 lines)
- [Source: crates/style/src/types/computed.rs#ComputedStyles] — Computed styles struct (add counter fields)
- [Source: crates/style/src/context.rs#compute_pseudo_element_styles] — Pseudo-element style computation
- [Source: crates/style/src/tests/pseudo_element.rs] — Pseudo-element style tests (1011 lines)
- [Source: crates/layout/src/engine/box_tree.rs#build_pseudo_element_box] — Pseudo-element box generation (~L435-506)
- [Source: crates/layout/src/tests/pseudo_element_tests.rs] — Layout pseudo-element tests (729 lines)
- [Source: crates/selectors/src/types.rs#PseudoElement] — PseudoElement enum definition
- [Source: docs/CSS2.1_Implementation_Checklist.md#Phase-14] — Lists & Counters checklist items
- [Source: tests/goldens/fixtures/207-content-attr.html] — Existing generated content golden test
Dev Agent Record
Agent Model Used
Claude Opus 4.6 (1M context)
Debug Log References
None — clean implementation with no blocking issues.
Completion Notes List
- Implemented full CSS 2.1 §12.4 counter support:
counter-reset,counter-increment,counter(),counters()functions - Implemented CSS 2.1 §12.3 quote keywords:
open-quote,close-quote,no-open-quote,no-close-quote - Created
CounterContextstruct in layout crate for scope-based counter tracking during document-order traversal - Counter values resolve during box tree construction (layout phase), not style computation
- Reused existing
format_list_marker()for counter value formatting (decimal, lower-alpha, upper-alpha, lower-roman, upper-roman) - Quote depth tracking uses Unicode quotation marks:
\u201C/\u201Dfor outer,\u2018/\u2019for inner - Modified style computation to preserve
ContentValue::Itemsthrough to layout when items contain dynamic content (counters/quotes) - Re-exported
ContentItemfrom style crate for layout crate access - 2 WPT tests promoted from
known_failtopass(counter-increment-applies-to-011, counter-reset-applies-to-011) - All 744 CSS tests pass, 672 layout tests pass, 4 new golden tests pass, existing golden test 207 passes
just cipasses fully
Change Log
- 2026-03-13: Implemented CSS counters, quote keywords, and generated content enhancements (Story 1.4)
File List
crates/css/src/types.rs— AddedPropertyId::CounterReset,PropertyId::CounterIncrement,CssValue::CounterReset,CssValue::CounterIncrement,ContentItem::Counter,ContentItem::Counters,ContentItem::OpenQuote,ContentItem::CloseQuote,ContentItem::NoOpenQuote,ContentItem::NoCloseQuotecrates/css/src/parser/mod.rs— Addedparse_counter_reset(),parse_counter_increment(),parse_counter_property(),parse_content_counter(),parse_content_counters(), extendedparse_content_value()for counter/quote keywords, wired counter properties intoparse_value_for_property()crates/css/src/tests/content_tests.rs— Added 18 new tests for counter/counters/quote parsing; updated test_parse_content_unknown_ident_ignored → test_parse_content_counter_produces_declarationcrates/style/src/types/computed.rs— Addedcounter_resetandcounter_incrementfields toComputedStyles, wired cascade inapply_declaration()crates/style/src/context.rs— Updatedcompute_pseudo_element_styles()to handleContentValue::Itemswith dynamic content, return pseudo-element styles for Itemscrates/style/src/lib.rs— Re-exportedContentItemfrom css cratecrates/layout/src/engine/counter.rs— New file:CounterContextstruct with scope tracking,format_counter_value(),quote_char(), unit testscrates/layout/src/engine/mod.rs— Addedcounter_context: RefCell<CounterContext>toLayoutEngine, reset on each layout passcrates/layout/src/engine/box_tree.rs— Process counter-reset/increment during box tree construction, resolve counter/quote content items inresolve_content_items(), handleContentValue::Itemsinbuild_pseudo_element_box()tests/goldens.rs— Added golden tests 227-230tests/goldens/fixtures/227-counter-basic.html— New fixturetests/goldens/fixtures/228-counters-nested.html— New fixturetests/goldens/fixtures/229-counter-upper-roman.html— New fixturetests/goldens/fixtures/230-quote-keywords.html— New fixturetests/goldens/expected/227-counter-basic.layout.txt— New expected outputtests/goldens/expected/227-counter-basic.dl.txt— New expected outputtests/goldens/expected/228-counters-nested.layout.txt— New expected outputtests/goldens/expected/228-counters-nested.dl.txt— New expected outputtests/goldens/expected/229-counter-upper-roman.layout.txt— New expected outputtests/goldens/expected/229-counter-upper-roman.dl.txt— New expected outputtests/goldens/expected/230-quote-keywords.layout.txt— New expected outputtests/goldens/expected/230-quote-keywords.dl.txt— New expected outputdocs/CSS2.1_Implementation_Checklist.md— Checked off counter and generated content itemstests/external/wpt/wpt_manifest.toml— Promoted 2 counter WPT tests from known_fail to pass