Files
rust_browser/_bmad-output/implementation-artifacts/1-4-generated-content.md
Zachary D. Rowitsch f17269dbfc Implement CSS 2.1 generated content: counters and quote keywords (§12.3, §12.4)
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>
2026-03-13 21:51:08 -04:00

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

  1. Given a CSS rule with ::before or ::after and a content property 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
  2. Given a content value using attr(), When the page is rendered, Then the attribute value from the HTML element is inserted as generated content
  3. Given a content value using counter() with counter-reset and counter-increment, When the page is rendered, Then the counter value is computed and displayed per CSS 2.1 §12.4
  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
  5. Golden tests cover string content, attr(), counter(), checklist is updated, and just ci passes

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-reset and counter-increment CSS property parsing (AC: #3)

    • 1.1 Add PropertyId::CounterReset and PropertyId::CounterIncrement variants in crates/css/src/types.rs (~line 348-487, PropertyId enum)
    • 1.2 Add CssValue variants for counter properties: CssValue::CounterReset(Vec<(String, i32)>) and CssValue::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() in crates/css/src/parser/mod.rs — syntax: counter-reset: <identifier> <integer>? (default 0), supports multiple counters, none keyword
    • 1.4 Implement parse_counter_increment() in crates/css/src/parser/mod.rs — syntax: counter-increment: <identifier> <integer>? (default 1), supports multiple counters, none keyword
    • 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
  • Task 2: counter() and counters() in content property parsing (AC: #3)

    • 2.1 Add ContentItem::Counter(String, ListStyleType) variant in crates/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() in crates/css/src/parser/mod.rs (~line 1670-1730) to parse counter(<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
  • Task 3: Counter state tracking in style computation (AC: #3)

    • 3.1 Add counter_reset: Vec<(String, i32)> and counter_increment: Vec<(String, i32)> fields to ComputedStyles in crates/style/src/types/computed.rs
    • 3.2 Wire cascade resolution for counter-reset and counter-increment in crates/style/src/types/computed.rs (apply_declaration)
    • 3.3 Implement counter scope tracking: create a CounterContext struct in crates/layout/src/engine/counter.rs that maintains a stack of counter scopes. Per CSS 2.1 §12.4: counter-reset creates a new counter instance in the current scope, counter-increment increments the innermost counter with that name
    • 3.4 CounterContext lives in layout crate as a RefCell field on LayoutEngine — counters are evaluated during box tree construction in document order
    • 3.5 Unit tests for counter scope creation, increment, and nesting
  • 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 processing ContentItem::Counter(name, style), look up the counter value from CounterContext and 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 ListStyleType formatting logic from list marker generation via format_list_marker in crates/layout/src/engine/list_marker.rs
    • 4.4 Wire counter context through build_box_tree() and build_pseudo_element_box() in box_tree.rs
    • 4.5 Unit tests for counter value lookup and formatting
  • Task 5: Quote keywords in content property (AC: #1, completes content support)

    • 5.1 Add ContentItem::OpenQuote, ContentItem::CloseQuote, ContentItem::NoOpenQuote, ContentItem::NoCloseQuote variants in crates/css/src/types.rs
    • 5.2 Extend parse_content_value() to recognize open-quote, close-quote, no-open-quote, no-close-quote keywords
    • 5.3 Implement quote depth tracking (integer depth, starts at 0) — open-quote inserts \u201C at even depth, \u2018 at odd depth
    • 5.4 Wire through style computation and box tree building
    • 5.5 Unit tests for quote keyword parsing and depth tracking
  • Task 6: Golden tests and checklist update (AC: #5)

    • 6.1 Add golden test 227: counter-reset + counter-increment with content: 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 ci and all tests pass (2 WPT tests promoted from known_fail to pass)

Dev Notes

Current Implementation Status

Generated content is substantially implemented. What works end-to-end:

  • ::before and ::after pseudo-elements — full pipeline: selector parsing → matching → style computation → box generation → layout → paint
  • content: "string" — single and multiple strings, concatenation
  • content: attr(name) — attribute value resolution at style computation time, multiple attr() items
  • content: "" — 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-line and ::first-letter — implemented with proper property filtering

What is missing (this story's scope):

  • counter-reset — PropertyId not defined, no parsing, no cascade
  • counter-increment — PropertyId not defined, no parsing, no cascade
  • counter() function — parser rejects it (drops declaration)
  • counters() function — parser rejects it
  • Quote keywordsopen-quote, close-quote, no-open-quote, no-close-quote not 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-reset creates a new counter in the element's scope
  • counter-increment increments 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:

  1. If it has counter-reset: foo 0, push a new scope for "foo" with value 0
  2. If it has counter-increment: foo 1, add 1 to the innermost "foo"
  3. 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-increment in css/ → add to ComputedStyles in style/ → resolve counter values during box tree build in layout/ → golden tests → checklist → just ci
  • Reuse existing infrastructure: Use ListStyleType enum 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 in tests/goldens/expected/
  • Unit tests pattern: construct DOM + style manually, run layout, assert positions
  • Checklist update at docs/CSS2.1_Implementation_Checklist.md is mandatory
  • just ci is 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:

  1. Scope creation: counter-reset on 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).
  2. Incrementing: counter-increment affects the innermost counter with that name. If no counter exists, one is implicitly created at the element with counter-reset: name 0 and then incremented.
  3. 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.
  4. 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".
  5. counters() function: Collects ALL instances of a named counter from outermost to innermost scope. With separator ".", produces "1.2.3" for nested lists.
  6. 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

  1. CSS parser tests in crates/css/src/tests/content_tests.rs — extend existing test file with counter()/counters() parsing tests
  2. Counter property tests — new tests for counter-reset and counter-increment parsing
  3. Counter scope tests — unit tests for CounterContext scope creation, increment, nesting, popping
  4. 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; }
  5. Regression check: Verify golden test 207 (attr()) and all existing pseudo-element tests still pass
  6. Run just ci at the end

CSS 2.1 Spec References

  • §12.1 — The content property: values, applicability to ::before/::after
  • §12.2 — The quotes property: 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 in css/ → computed in style/ → resolve in layout/
  • CounterContext struct created in crates/layout/ (not style/) because counter values resolve during box tree construction
  • New ContentItem variants added to crates/css/src/types.rs
  • Reuse ListStyleType from crates/style/src/types/primitives.rs for 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 CounterContext struct 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/\u201D for outer, \u2018/\u2019 for inner
  • Modified style computation to preserve ContentValue::Items through to layout when items contain dynamic content (counters/quotes)
  • Re-exported ContentItem from style crate for layout crate access
  • 2 WPT tests promoted from known_fail to pass (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 ci passes 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 — Added PropertyId::CounterReset, PropertyId::CounterIncrement, CssValue::CounterReset, CssValue::CounterIncrement, ContentItem::Counter, ContentItem::Counters, ContentItem::OpenQuote, ContentItem::CloseQuote, ContentItem::NoOpenQuote, ContentItem::NoCloseQuote
  • crates/css/src/parser/mod.rs — Added parse_counter_reset(), parse_counter_increment(), parse_counter_property(), parse_content_counter(), parse_content_counters(), extended parse_content_value() for counter/quote keywords, wired counter properties into parse_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_declaration
  • crates/style/src/types/computed.rs — Added counter_reset and counter_increment fields to ComputedStyles, wired cascade in apply_declaration()
  • crates/style/src/context.rs — Updated compute_pseudo_element_styles() to handle ContentValue::Items with dynamic content, return pseudo-element styles for Items
  • crates/style/src/lib.rs — Re-exported ContentItem from css crate
  • crates/layout/src/engine/counter.rs — New file: CounterContext struct with scope tracking, format_counter_value(), quote_char(), unit tests
  • crates/layout/src/engine/mod.rs — Added counter_context: RefCell<CounterContext> to LayoutEngine, reset on each layout pass
  • crates/layout/src/engine/box_tree.rs — Process counter-reset/increment during box tree construction, resolve counter/quote content items in resolve_content_items(), handle ContentValue::Items in build_pseudo_element_box()
  • tests/goldens.rs — Added golden tests 227-230
  • tests/goldens/fixtures/227-counter-basic.html — New fixture
  • tests/goldens/fixtures/228-counters-nested.html — New fixture
  • tests/goldens/fixtures/229-counter-upper-roman.html — New fixture
  • tests/goldens/fixtures/230-quote-keywords.html — New fixture
  • tests/goldens/expected/227-counter-basic.layout.txt — New expected output
  • tests/goldens/expected/227-counter-basic.dl.txt — New expected output
  • tests/goldens/expected/228-counters-nested.layout.txt — New expected output
  • tests/goldens/expected/228-counters-nested.dl.txt — New expected output
  • tests/goldens/expected/229-counter-upper-roman.layout.txt — New expected output
  • tests/goldens/expected/229-counter-upper-roman.dl.txt — New expected output
  • tests/goldens/expected/230-quote-keywords.layout.txt — New expected output
  • tests/goldens/expected/230-quote-keywords.dl.txt — New expected output
  • docs/CSS2.1_Implementation_Checklist.md — Checked off counter and generated content items
  • tests/external/wpt/wpt_manifest.toml — Promoted 2 counter WPT tests from known_fail to pass