Files
rust_browser/_bmad-output/implementation-artifacts/1-10-font-properties.md
Zachary D. Rowitsch 8a97aae651 Implement CSS 2.1 font properties with code review fixes (§15.4, §15.8, §4.3.2)
Add font-variant property (normal/small-caps) with correct per-character-case
rendering: lowercase chars rendered uppercased at 0.8x size, uppercase/non-letter
chars at full size. Fix font shorthand to validate mandatory font-family and reject
invalid line-height after /. Wire up actual x-height from font metrics in layout
measurement. Add font-variant to ::first-line pseudo-element support. Deduplicate
rasterizer text rendering. Add ex unit to grid template parsing tables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 10:41:50 -04:00

288 lines
19 KiB
Markdown

# Story 1.10: Font Properties
Status: done
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
## Story
As a web user,
I want font styles to render correctly including shorthand declarations and variant styles,
So that typography on web pages matches the designer's intent.
## Acceptance Criteria
1. **Given** an element with the `font` shorthand property
**When** the page is rendered
**Then** all font sub-properties (style, variant, weight, size, line-height, family) are parsed and applied correctly per CSS 2.1 §15.8
2. **Given** an element with `font-variant: small-caps`
**When** the page is rendered
**Then** lowercase characters are rendered as small capital letters
3. **Given** a CSS value using `ex` units
**When** the page is rendered
**Then** the unit resolves to the x-height of the element's font per CSS 2.1 §4.3.2
4. **Given** font metrics from the bundled font set
**When** computing line-height and baseline alignment
**Then** the correct ascent, descent, and x-height values are used for layout calculations
5. Golden tests cover font shorthand, small-caps, and ex units, checklist is updated, and `just ci` passes.
## Tasks / Subtasks
- [x] Task 1: Implement `font-variant` property (AC: #2)
- [x] 1.1 Add `FontVariant` PropertyId variant to `crates/css/src/types.rs`
- [x] 1.2 Add `font-variant` keyword parsing in CSS parser (values: `normal`, `small-caps`)
- [x] 1.3 Add `font_variant: FontVariant` field to `ComputedStyles` in `crates/style/src/types/computed.rs`
- [x] 1.4 Wire up `apply_declared` for `PropertyId::FontVariant`
- [x] 1.5 Wire up inheritance in `inherit_from_parent()` (font-variant is inherited per CSS 2.1 §15.5)
- [x] 1.6 Add unit tests for parsing and style computation
- [x] Task 2: Implement small-caps rendering (AC: #2)
- [x] 2.1 In layout/display_list: when `font-variant: small-caps`, transform lowercase text to uppercase and render at reduced size (~80% of font-size per convention)
- [x] 2.2 Propagate `font_variant` through `DisplayItem::Text` (add field or transform text before display list)
- [x] 2.3 In `crates/render/src/rasterizer/`: handle small-caps glyph rendering
- [x] 2.4 Add unit and golden tests for small-caps rendering
- [x] Task 3: Complete `font` shorthand parsing (AC: #1)
- [x] 3.1 Audit current shorthand in `crates/css/src/parser/shorthands/` — verify it expands to all 6 sub-properties: font-style, font-variant, font-weight, font-size, line-height, font-family
- [x] 3.2 Ensure `font-variant` is included in shorthand expansion (currently missing since `font-variant` doesn't exist yet)
- [x] 3.3 Ensure shorthand resets omitted sub-properties to initial values per CSS 2.1 §15.8
- [x] 3.4 Add tests for full shorthand parsing with all combinations
- [x] Task 4: Implement `ex` unit resolution using font metrics (AC: #3, #4)
- [x] 4.1 Add x-height extraction to `crates/fonts/` — use `ab_glyph` to compute x-height from the 'x' glyph bounding box
- [x] 4.2 Expose font metrics (ascent, descent, x-height) from `fonts` crate API
- [x] 4.3 In `crates/style/` or `crates/layout/`: resolve `ex` units using actual x-height (currently `ex` likely falls back to `0.5em` or is unsupported)
- [x] 4.4 Verify `em` unit resolution uses computed font-size (should already work)
- [x] 4.5 Add unit and golden tests for `ex` unit resolution
- [x] Task 5: Verify font metrics for line-height and baseline alignment (AC: #4)
- [x] 5.1 Verify `crates/fonts/` exposes correct ascent/descent for bundled Noto Sans
- [x] 5.2 Verify layout uses font metrics (not hardcoded values) for line-height computation
- [x] 5.3 Add golden tests verifying correct baseline alignment with different font sizes
- [x] Task 6: Golden tests and documentation (AC: #5)
- [x] 6.1 Create golden test fixtures covering: font shorthand, small-caps, ex units
- [x] 6.2 Generate expected outputs and verify correctness
- [x] 6.3 Update `docs/CSS2.1_Implementation_Checklist.md` Phase 10 — check off completed items
- [x] 6.4 Run `just ci` and fix any issues
## Dev Notes
### CSS 2.1 Spec References
- **§15.3** `font-style`: values `normal | italic | oblique` (already implemented)
- **§15.4** `font-variant`: values `normal | small-caps` (NOT implemented — primary work item)
- **§15.5** `font-weight`: values `normal | bold | bolder | lighter | 100-900` (already implemented including bolder/lighter)
- **§15.6** `font-size`: absolute-size, relative-size, length, percentage (partially implemented — keyword mapping status needs verification)
- **§15.7** `font-family`: generic families + specific names (parsing works, fallback selection needs verification)
- **§15.8** `font` shorthand: `[ font-style || font-variant || font-weight ]? font-size [ / line-height ]? font-family` (partially implemented — missing font-variant in expansion)
- **§4.3.2** `ex` unit: x-height of the element's font (NOT implemented — needs font metrics integration)
### Current Font Property Pipeline State
**Already working (DO NOT reimplement):**
- `font-family``PropertyId::FontFamily`, parsing via `CssValue::FontFamilies`, computed as `Vec<String>`, inherited
- `font-size``PropertyId::FontSize`, `to_px()` resolution, computed as `f32`, inherited
- `font-weight``PropertyId::FontWeight`, numeric 100-900 + keywords + `bolder()`/`lighter()`, computed as `FontWeight(u16)`, inherited
- `font-style``PropertyId::FontStyle`, normal/italic/oblique enum, computed as `FontStyle`, inherited
- `font` shorthand — basic parsing exists, expands to sub-properties
- `@font-face` — web font loading through full pipeline
- `DisplayItem::Text` — includes `font_size`, `font_weight`, `font_style`, `font_family` fields
- Text dump format: `font_weight=700`, `font_style=italic`, `font_family=...` (defaults omitted)
**Must be added:**
- `FontVariant` PropertyId + parsing + computed field + inheritance
- `font-variant` in `font` shorthand expansion
- Small-caps text transformation in layout/render
- `ex` unit resolution using actual x-height from font metrics
- Font metrics (ascent, descent, x-height) exposed from `fonts` crate
### Key Code Locations
| Component | File | What to modify |
|---|---|---|
| PropertyId enum | `crates/css/src/types.rs` | Add `FontVariant` variant (~line 417) |
| CSS keyword→PropertyId map | `crates/css/src/types.rs` | Add `"font-variant" => FontVariant` |
| Property parsing | `crates/css/src/parser/` | Add `font-variant` value parsing |
| Font shorthand | `crates/css/src/parser/shorthands/` (likely `typography.rs`) | Add font-variant to expansion |
| ComputedStyles | `crates/style/src/types/computed.rs` | Add `font_variant` field (~line 120) |
| apply_declared | `crates/style/src/types/computed.rs` (~line 842) | Add `FontVariant` match arm |
| inherit_from_parent | `crates/style/src/types/computed.rs` (~line 1686) | Add `FontVariant` inheritance |
| Font metrics | `crates/fonts/src/lib.rs` | Expose x-height, verify ascent/descent |
| Font rasterization | `crates/fonts/src/rasterize.rs` | May need small-caps glyph support |
| ex unit resolution | `crates/style/` or `crates/layout/` | `to_px()` or layout-level resolution |
| Display list text | `crates/display_list/src/builder.rs` | Small-caps text transformation |
| Text dump | `crates/display_list/src/tests/font_property_tests.rs` | Add font-variant dump tests |
| Golden fixtures | `tests/goldens/fixtures/` | New HTML fixtures for AC items |
| Golden expected | `tests/goldens/expected/` | Expected layout/display list outputs |
| Checklist | `docs/CSS2.1_Implementation_Checklist.md` | Update Phase 10 items |
### Architecture Constraints
- **No unsafe code** — `fonts` crate must use safe Rust. `ab_glyph` API is safe.
- **Layer 1 only** — all changes in `css`, `style`, `layout`, `display_list`, `render`, `fonts` (all Layer 1). No upward dependencies.
- **Phase-based mutation** — font metrics computed in style/layout phase, consumed in display list/render phase. No back-references.
- **Arena IDs** — font metric data flows through the pipeline via computed styles and layout boxes, not via shared mutable state.
### Implementation Pattern (Follow Exactly)
Follow the established CSS property implementation order from stories 1-7 through 1-9:
1. **Parse** in `css/` — add PropertyId variant, keyword-to-property mapping, value parser
2. **Compute** in `style/` — add field to ComputedStyles, apply_declared handler, inherit_from_parent wiring, initial value
3. **Layout effect** in `layout/` — small-caps text transformation, ex unit resolution
4. **Paint effect** in `display_list/` — propagate font-variant to display items
5. **Render** in `render/` — small-caps glyph rendering via `fonts` crate
6. **Golden tests** in `tests/goldens/` — fixtures + expected outputs
7. **Checklist update**`docs/CSS2.1_Implementation_Checklist.md` Phase 10
8. **CI validation**`just ci 2>&1 | tee /tmp/ci-output.txt`
### Small-Caps Implementation Strategy
CSS 2.1 §15.4 says `small-caps` renders lowercase letters as "small uppercase letters." Implementation approach:
- In the display list builder or layout phase, when `font-variant: small-caps`:
- Split text into runs of lowercase vs non-lowercase characters
- For lowercase runs: transform to uppercase and apply a reduced font-size (conventional ratio: ~0.8x of computed font-size)
- For non-lowercase runs: render at normal font-size
- This creates multiple text display items per original text node when small-caps is active
- Alternative: if `ab_glyph`/font supports OpenType `smcp` feature, use that instead (check first)
### `ex` Unit Implementation Strategy
CSS 2.1 §4.3.2: "`ex` refers to the x-height of the first available font." Implementation:
1. In `crates/fonts/`: add method to get x-height from `ab_glyph::Font::glyph_bounds('x')` — the height of the lowercase 'x' glyph
2. If the font has no 'x' glyph, fall back to `0.5em` per spec recommendation
3. Wire the x-height into `to_px()` or the layout resolution path so `1ex` resolves to the x-height value in pixels
4. Current `to_px()` in style likely handles `em` but not `ex` — add `ex` case
### Previous Story Learnings
From stories 1-8 (Borders) and 1-9 (Text Properties):
- Shorthand expansion goes in `crates/css/src/parser/shorthands/` — follow existing patterns (e.g., `typography.rs`)
- New enum types for CSS values go in `crates/style/src/types/text.rs` or `crates/shared/src/lib.rs`
- Golden test fixtures: `tests/goldens/fixtures/NNN-descriptive-name.html` (sequential numbering)
- Regen goldens: `cargo test -p rust_browser --test regen_goldens -- --nocapture`
- All font properties are inherited — don't forget `inherit_from_parent()` wiring
- Handle `inherit`, `initial`, `unset` keywords for the new property
### Git Intelligence
Recent relevant commits:
- `c3bcff4``font-weight: bolder/lighter` relative keywords (shows how relative font values are handled)
- `e319aa4` — CSS font shorthand + web font sharing with rasterizer (shows shorthand expansion pattern)
- `7f767b4`@font-face web font loading (shows fonts crate integration)
- `e74b167` — Real font support with bundled Noto Sans (shows font property pipeline foundation)
### Testing Strategy
1. **Unit tests** for `font-variant` parsing (in `crates/css/src/` test module)
2. **Unit tests** for `font-variant` computed style (in `crates/style/src/` test module)
3. **Unit tests** for font shorthand expansion including variant (in css parser tests)
4. **Unit tests** for x-height extraction from `ab_glyph` (in `crates/fonts/` test module)
5. **Unit tests** for `ex` unit resolution (in style or layout tests)
6. **Display list tests** for small-caps text transformation (in `crates/display_list/src/tests/font_property_tests.rs`)
7. **Golden tests** — minimum 3 fixtures:
- Font shorthand with all sub-properties including variant
- `font-variant: small-caps` rendering
- `ex` unit in various properties (width, height, margin, padding)
8. **Regression tests** — verify existing font properties still work after changes
### Project Structure Notes
- All changes span Layer 1 crates only — no cross-layer violations
- `fonts` crate at `crates/fonts/` — font metrics API changes here
- CSS property pipeline: `css/``style/``layout/``display_list/``render/`
- Bundled fonts in `crates/fonts/data/` — Noto Sans (3.1 MB)
- No new external dependencies needed — `ab_glyph` already provides x-height capability
### References
- [Source: CSS 2.1 §15.3-§15.8 — Font properties specification]
- [Source: CSS 2.1 §4.3.2 — ex unit definition]
- [Source: docs/CSS2.1_Implementation_Checklist.md#Phase 10 — Current implementation status]
- [Source: crates/css/src/types.rs — PropertyId enum, font property variants]
- [Source: crates/style/src/types/computed.rs — ComputedStyles, apply_declared, inherit_from_parent]
- [Source: crates/fonts/src/lib.rs — Font loading and metrics API]
- [Source: crates/display_list/src/tests/font_property_tests.rs — Existing font display tests]
- [Source: crates/css/src/parser/shorthands/ — Shorthand expansion patterns]
- [Source: _bmad-output/project-context.md — Full project rules and patterns]
## Dev Agent Record
### Agent Model Used
Claude Opus 4.6 (1M context)
### Debug Log References
### Completion Notes List
- Implemented `FontVariant` enum (Normal, SmallCaps) in `style::types::text`
- Added `PropertyId::FontVariant` with full CSS pipeline: parsing, apply_declared, reset_to_initial, copy_property_from_parent, inherit_from, dump
- `font-variant` is inherited per CSS 2.1 §15.5
- Propagated `font_variant` through entire rendering pipeline: LayoutBox, InlineFragment, InlineItem, ActiveInlineElement, DisplayItem::Text
- Small-caps rendering: text is split by character case — lowercase chars rendered uppercased at 0.8x size, uppercase/non-letter chars at full size (CSS 2.1 §15.4)
- Small-caps layout measurement matches rendering: inline engine measures segments separately
- Font shorthand now expands to 6 sub-properties including font-variant; captures `small-caps` keyword
- Font shorthand validates font-family is present (returns empty for invalid shorthand per §15.8)
- Font shorthand rejects invalid line-height after `/` delimiter
- Added `Ex` unit to CSS `Unit` enum; resolves to 0.5em in `to_px()` (CSS 2.1 §4.3.2 fallback at style time)
- Added `ex` unit to grid template parsing tables for consistency
- Added x-height extraction to `ScaledFontMetrics` using `ab_glyph` outline glyph bounds
- Fixed layout text_measure to use actual x_height from ScaledFontMetrics (was using ascent*0.5 approximation)
- Added `font_variant` to `FirstLineOverrideStyles` for `::first-line` pseudo-element support (CSS 2.1 §5.12.1)
- Rasterizer Text rendering deduplicated via `render_text_item` helper (shared between `rasterize()` and `rasterize_with_images()`)
- Display list `dump()` uses enum match for font_variant instead of hardcoded string
- Created 3 golden test fixtures (260-262): font shorthand, small-caps, ex units
- Added tests: font-variant initial parsing, ex unit CSS text parsing, small-caps display list integration
- Updated CSS2.1 Implementation Checklist Phase 10
- All CI checks pass: fmt, lint, test, policy, metrics
### Change Log
- 2026-03-14: Implemented font-variant property, small-caps rendering, font shorthand expansion, ex unit support, and font metrics x-height
- 2026-03-14: Code review fixes — small-caps case splitting, font shorthand validation, ::first-line support, rasterizer dedup, x_height wiring, dump enum match, test coverage
### File List
- crates/css/src/types.rs — Added FontVariant PropertyId variant, Ex unit, is_inherited, from_name, name mappings
- crates/css/src/parser/values.rs — Added parse_font_variant method
- crates/css/src/parser/property_dispatch.rs — Added font-variant dispatch and ex unit mapping
- crates/css/src/parser/shorthands/typography.rs — Added font-variant to font shorthand expansion
- crates/css/src/parser/shorthands/flex_grid.rs — Added ex unit mapping
- crates/css/src/tests/value_tests.rs — Added font-variant and ex unit tests
- crates/style/src/types/text.rs — Added FontVariant enum
- crates/style/src/types/mod.rs — Re-exported FontVariant
- crates/style/src/types/computed.rs — Added font_variant field, apply_declared, inheritance, dump, FirstLineOverrideStyles font_variant
- crates/style/src/types/tests.rs — Added font-variant style computation tests
- crates/style/src/lib.rs — Re-exported FontVariant
- crates/layout/src/types.rs — Added font_variant to LayoutBox and InlineFragment
- crates/layout/src/engine/box_tree.rs — Propagated font_variant from styles to layout boxes
- crates/layout/src/engine/inline.rs — Added font_variant to InlineItem, ActiveInlineElement, InlineFragment; small-caps measurement; ::first-line override
- crates/display_list/src/lib.rs — Added font_variant to DisplayItem::Text, dump, and scale_item
- crates/display_list/src/builder.rs — Added font_variant to Text item creation
- crates/display_list/src/tests/font_property_tests.rs — Updated tests for font_variant field
- crates/display_list/src/tests/edge_case_tests.rs — Updated tests for font_variant field
- crates/display_list/src/tests/item_tests.rs — Updated tests for font_variant field
- crates/fonts/src/lib.rs — Added x_height to ScaledFontMetrics, computed from ab_glyph outline bounds
- crates/render/src/rasterizer/mod.rs — Small-caps segment rendering via render_text_item helper, deduped rasterize/rasterize_with_images
- crates/render/src/rasterizer/tests/text_tests.rs — Updated tests for font_variant field
- crates/render/src/rasterizer/tests/display_list_tests.rs — Updated tests for font_variant field
- crates/app_browser/src/tests/hit_test_tests.rs — Updated tests for font_variant field
- tests/goldens.rs — Added 3 new golden test functions
- tests/goldens/fixtures/260-font-shorthand-complete.html — New golden fixture
- tests/goldens/fixtures/261-font-variant-small-caps.html — New golden fixture
- tests/goldens/fixtures/262-ex-units.html — New golden fixture
- tests/goldens/expected/260-font-shorthand-complete.{layout,dl}.txt — Generated expected outputs
- tests/goldens/expected/261-font-variant-small-caps.{layout,dl}.txt — Generated expected outputs
- tests/goldens/expected/262-ex-units.{layout,dl}.txt — Generated expected outputs
- crates/style/src/context.rs — Added FontVariant to is_first_line_applicable_property and compute_first_line_styles overlay
- crates/layout/src/text_measure.rs — Fixed font_metrics_styled to use actual x_height from ScaledFontMetrics
- crates/css/src/parser/values.rs — Added ex unit to grid template and single track size parsing tables
- tests/goldens/expected/094-table-vertical-align.{layout,dl}.txt — Updated for corrected x_height in vertical-align: middle
- docs/CSS2.1_Implementation_Checklist.md — Updated Phase 10 items