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

270 lines
22 KiB
Markdown

# 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.
- [x] Task 1: `counter-reset` and `counter-increment` CSS property parsing (AC: #3)
- [x] 1.1 Add `PropertyId::CounterReset` and `PropertyId::CounterIncrement` variants in `crates/css/src/types.rs` (~line 348-487, PropertyId enum)
- [x] 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
- [x] 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
- [x] 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
- [x] 1.5 Wire both properties into the main property dispatch in parser
- [x] 1.6 Unit tests for counter property parsing: single counter, counter with value, multiple counters, `none`, invalid values
- [x] Task 2: `counter()` and `counters()` in content property parsing (AC: #3)
- [x] 2.1 Add `ContentItem::Counter(String, ListStyleType)` variant in `crates/css/src/types.rs` — counter name + optional list-style-type (default decimal)
- [x] 2.2 Add `ContentItem::Counters(String, String, ListStyleType)` variant — counter name + separator string + optional list-style-type
- [x] 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>)`
- [x] 2.4 Unit tests for counter/counters parsing in `crates/css/src/tests/content_tests.rs`
- [x] Task 3: Counter state tracking in style computation (AC: #3)
- [x] 3.1 Add `counter_reset: Vec<(String, i32)>` and `counter_increment: Vec<(String, i32)>` fields to `ComputedStyles` in `crates/style/src/types/computed.rs`
- [x] 3.2 Wire cascade resolution for `counter-reset` and `counter-increment` in `crates/style/src/types/computed.rs` (apply_declaration)
- [x] 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
- [x] 3.4 `CounterContext` lives in layout crate as a `RefCell` field on `LayoutEngine` — counters are evaluated during box tree construction in document order
- [x] 3.5 Unit tests for counter scope creation, increment, and nesting
- [x] Task 4: Counter value resolution in generated content (AC: #3)
- [x] 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
- [x] 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
- [x] 4.3 Reuse existing `ListStyleType` formatting logic from list marker generation via `format_list_marker` in `crates/layout/src/engine/list_marker.rs`
- [x] 4.4 Wire counter context through `build_box_tree()` and `build_pseudo_element_box()` in `box_tree.rs`
- [x] 4.5 Unit tests for counter value lookup and formatting
- [x] Task 5: Quote keywords in content property (AC: #1, completes content support)
- [x] 5.1 Add `ContentItem::OpenQuote`, `ContentItem::CloseQuote`, `ContentItem::NoOpenQuote`, `ContentItem::NoCloseQuote` variants in `crates/css/src/types.rs`
- [x] 5.2 Extend `parse_content_value()` to recognize `open-quote`, `close-quote`, `no-open-quote`, `no-close-quote` keywords
- [x] 5.3 Implement quote depth tracking (integer depth, starts at 0) — `open-quote` inserts `\u201C` at even depth, `\u2018` at odd depth
- [x] 5.4 Wire through style computation and box tree building
- [x] 5.5 Unit tests for quote keyword parsing and depth tracking
- [x] Task 6: Golden tests and checklist update (AC: #5)
- [x] 6.1 Add golden test 227: `counter-reset` + `counter-increment` with `content: counter(name)` — numbered headings
- [x] 6.2 Add golden test 228: nested counters with `counters()` and separator — "1.1", "1.2" section numbering
- [x] 6.3 Add golden test 229: counters with `list-style-type: upper-roman` — I, II, III, IV
- [x] 6.4 Add golden test 230: quote keywords generating Unicode quote characters
- [x] 6.5 Verify existing golden test 207 (content: attr()) still passes
- [x] 6.6 Update `docs/CSS2.1_Implementation_Checklist.md` — checked off: counter-reset, counter-increment, counter(), counters(), quote keywords in content
- [x] 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 keywords** — `open-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