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>
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
-
Given an element with the
fontshorthand 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 -
Given an element with
font-variant: small-capsWhen the page is rendered Then lowercase characters are rendered as small capital letters -
Given a CSS value using
exunits 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 -
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
-
Golden tests cover font shorthand, small-caps, and ex units, checklist is updated, and
just cipasses.
Tasks / Subtasks
-
Task 1: Implement
font-variantproperty (AC: #2)- 1.1 Add
FontVariantPropertyId variant tocrates/css/src/types.rs - 1.2 Add
font-variantkeyword parsing in CSS parser (values:normal,small-caps) - 1.3 Add
font_variant: FontVariantfield toComputedStylesincrates/style/src/types/computed.rs - 1.4 Wire up
apply_declaredforPropertyId::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
- 1.1 Add
-
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_variantthroughDisplayItem::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
- 2.1 In layout/display_list: when
-
Task 3: Complete
fontshorthand 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-variantis included in shorthand expansion (currently missing sincefont-variantdoesn'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
- 3.1 Audit current shorthand in
-
Task 4: Implement
exunit resolution using font metrics (AC: #3, #4)- 4.1 Add x-height extraction to
crates/fonts/— useab_glyphto compute x-height from the 'x' glyph bounding box - 4.2 Expose font metrics (ascent, descent, x-height) from
fontscrate API - 4.3 In
crates/style/orcrates/layout/: resolveexunits using actual x-height (currentlyexlikely falls back to0.5emor is unsupported) - 4.4 Verify
emunit resolution uses computed font-size (should already work) - 4.5 Add unit and golden tests for
exunit resolution
- 4.1 Add x-height extraction to
-
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
- 5.1 Verify
-
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.mdPhase 10 — check off completed items - 6.4 Run
just ciand fix any issues
Dev Notes
CSS 2.1 Spec References
- §15.3
font-style: valuesnormal | italic | oblique(already implemented) - §15.4
font-variant: valuesnormal | small-caps(NOT implemented — primary work item) - §15.5
font-weight: valuesnormal | 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
fontshorthand:[ font-style || font-variant || font-weight ]? font-size [ / line-height ]? font-family(partially implemented — missing font-variant in expansion) - §4.3.2
exunit: 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 viaCssValue::FontFamilies, computed asVec<String>, inheritedfont-size—PropertyId::FontSize,to_px()resolution, computed asf32, inheritedfont-weight—PropertyId::FontWeight, numeric 100-900 + keywords +bolder()/lighter(), computed asFontWeight(u16), inheritedfont-style—PropertyId::FontStyle, normal/italic/oblique enum, computed asFontStyle, inheritedfontshorthand — basic parsing exists, expands to sub-properties@font-face— web font loading through full pipelineDisplayItem::Text— includesfont_size,font_weight,font_style,font_familyfields- Text dump format:
font_weight=700,font_style=italic,font_family=...(defaults omitted)
Must be added:
FontVariantPropertyId + parsing + computed field + inheritancefont-variantinfontshorthand expansion- Small-caps text transformation in layout/render
exunit resolution using actual x-height from font metrics- Font metrics (ascent, descent, x-height) exposed from
fontscrate
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 —
fontscrate must use safe Rust.ab_glyphAPI 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:
- Parse in
css/— add PropertyId variant, keyword-to-property mapping, value parser - Compute in
style/— add field to ComputedStyles, apply_declared handler, inherit_from_parent wiring, initial value - Layout effect in
layout/— small-caps text transformation, ex unit resolution - Paint effect in
display_list/— propagate font-variant to display items - Render in
render/— small-caps glyph rendering viafontscrate - Golden tests in
tests/goldens/— fixtures + expected outputs - Checklist update —
docs/CSS2.1_Implementation_Checklist.mdPhase 10 - 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 OpenTypesmcpfeature, 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:
- In
crates/fonts/: add method to get x-height fromab_glyph::Font::glyph_bounds('x')— the height of the lowercase 'x' glyph - If the font has no 'x' glyph, fall back to
0.5emper spec recommendation - Wire the x-height into
to_px()or the layout resolution path so1exresolves to the x-height value in pixels - Current
to_px()in style likely handlesembut notex— addexcase
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.rsorcrates/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,unsetkeywords for the new property
Git Intelligence
Recent relevant commits:
c3bcff4—font-weight: bolder/lighterrelative 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
- Unit tests for
font-variantparsing (incrates/css/src/test module) - Unit tests for
font-variantcomputed style (incrates/style/src/test module) - Unit tests for font shorthand expansion including variant (in css parser tests)
- Unit tests for x-height extraction from
ab_glyph(incrates/fonts/test module) - Unit tests for
exunit resolution (in style or layout tests) - Display list tests for small-caps text transformation (in
crates/display_list/src/tests/font_property_tests.rs) - Golden tests — minimum 3 fixtures:
- Font shorthand with all sub-properties including variant
font-variant: small-capsrenderingexunit in various properties (width, height, margin, padding)
- Regression tests — verify existing font properties still work after changes
Project Structure Notes
- All changes span Layer 1 crates only — no cross-layer violations
fontscrate atcrates/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_glyphalready 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
FontVariantenum (Normal, SmallCaps) instyle::types::text - Added
PropertyId::FontVariantwith full CSS pipeline: parsing, apply_declared, reset_to_initial, copy_property_from_parent, inherit_from, dump font-variantis inherited per CSS 2.1 §15.5- Propagated
font_variantthrough 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-capskeyword - Font shorthand validates font-family is present (returns empty for invalid shorthand per §15.8)
- Font shorthand rejects invalid line-height after
/delimiter - Added
Exunit to CSSUnitenum; resolves to 0.5em into_px()(CSS 2.1 §4.3.2 fallback at style time) - Added
exunit to grid template parsing tables for consistency - Added x-height extraction to
ScaledFontMetricsusingab_glyphoutline glyph bounds - Fixed layout text_measure to use actual x_height from ScaledFontMetrics (was using ascent*0.5 approximation)
- Added
font_varianttoFirstLineOverrideStylesfor::first-linepseudo-element support (CSS 2.1 §5.12.1) - Rasterizer Text rendering deduplicated via
render_text_itemhelper (shared betweenrasterize()andrasterize_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