diff --git a/.agents/skills/fixtures/SKILL.md b/.agents/skills/fixtures/SKILL.md new file mode 100644 index 0000000000..bdb56308f2 --- /dev/null +++ b/.agents/skills/fixtures/SKILL.md @@ -0,0 +1,98 @@ +--- +description: Guides authoring, organizing, and referencing test fixtures across the project. Use when creating new fixtures, writing tests that depend on fixtures, or deciding what should be checked into git. +--- + +# Fixtures + +## What fixtures are + +Fixtures are static input files — HTML, SVG, CSS, JSON, `.grida`, `.fig`, +font binaries, images, text samples — used as **deterministic inputs** to +rendering, parsing, and I/O tests. They exist so that tests are reproducible, +self-contained, and don't depend on external services or generated data. + +## Why we keep them + +- **Regression detection** — render the same input, compare the output. +- **Spec coverage** — each fixture maps to a specific feature or property + being tested (one concept per file). +- **Onboarding** — new contributors can see exactly what the renderer handles + by browsing fixtures. +- **Cross-pipeline validation** — the same fixture can be consumed by unit + tests, golden tests, reftests, probe tests, and visual inspection. + +## What should be covered + +A fixture should exist for every **rendering behavior, format variant, or +edge case** that the codebase supports or intends to support. This includes: + +- Each CSS property / SVG element the htmlcss or SVG renderer handles +- Format I/O round-trips (Figma → Grida, SVG → Grida, clipboard paste) +- Edge cases: zero-size, empty content, deeply nested, degenerate inputs +- Unsupported-but-tracked features (the fixture documents the gap) + +## Best practices + +- **One concept per file.** Don't combine unrelated properties. +- **Self-contained.** No external resources, network fetches, or scripts. +- **Minimal.** Only enough structure to isolate the behavior under test. +- **Probe-friendly.** High-contrast palette (prefer B/W), round pixel values, + ≤ 3 colors. Designed for headless pixel probing, not human aesthetics. +- **Descriptive naming.** `-[-].` — the + filename alone should tell you what's being tested. +- **Labeled specimens.** Within a fixture, label each test case with the + value being exercised so both humans and heuristics can identify regions. +- **Don't duplicate.** Before adding a fixture, check if an existing one + already covers the behavior. Extend or split rather than duplicate. + +## Git inclusion policy + +### Checked in (`fixtures/`) + +All directories under `fixtures/` **except `fixtures/local/`** are committed +to the repository. These are small, purpose-built files that are part of the +test suite. + +``` +fixtures/ +├── css/ # CSS stylesheets +├── fonts/ # Bundled font binaries (deterministic text tests) +├── images/ # Test images +├── test-fig/ # Figma clipboard / REST fixtures +├── test-figma/ # Figma archive fixtures +├── test-grida/ # .grida format fixtures +├── test-html/ # HTML+CSS renderer fixtures (L0, etc.) +├── test-markdown/ # Markdown fixtures +├── test-svg/ # SVG fixtures +├── text/ # Plain text samples +└── local/ # ← gitignored, see below +``` + +### Not checked in (`fixtures/local/`) + +`fixtures/local/` is **gitignored**. It holds large, third-party, or +benchmark-only datasets that are meaningful for local development but too +large or license-restricted for the repository: + +- `W3C_SVG_11_TestSuite` — W3C SVG 1.1 conformance suite (~50 MB) +- `resvg-test-suite` — resvg's feature-focused SVG tests +- `oxygen-icons-5.116.0` — icon set for stress testing +- `perf` — large scenes for benchmarking + +These must be downloaded separately by developers who need them. + +## Referencing local-only fixtures + +**Never reference `fixtures/local/` paths in committed code, tests, or +documentation.** Local fixtures do not exist in CI or on other developers' +machines. Specifically: + +- Do not `include!()`, `read_to_string()`, or `fs::read()` a `local/` path + in any Rust test or example that runs in CI. +- Do not hardcode `fixtures/local/` paths in docs, READMEs, or AGENTS files + as if they are always available. +- If a doc needs to mention a local suite (e.g. for reftest instructions), + clearly mark it as **local-only** and note that the developer must download + it first. +- Tests that depend on local fixtures must be gated (e.g. `#[ignore]` with a + comment, or behind a feature flag) so they don't fail in CI. diff --git a/docs/wg/feat-2d/htmlcss.md b/docs/wg/feat-2d/htmlcss.md index c08c9ab6ca..31b3784e8a 100644 --- a/docs/wg/feat-2d/htmlcss.md +++ b/docs/wg/feat-2d/htmlcss.md @@ -104,19 +104,122 @@ Types from `cg::prelude` reused where they 100% align with CSS semantics: ### Text & Font -| CSS Property | Status | Notes | -| -------------------------------- | ------ | ----------------------------------------------------------- | -| `color` | ✅ | Inherited | -| `font-size` | ✅ | Computed px | -| `font-weight` | ✅ | 100–900 | -| `font-style` (italic) | ✅ | | -| `font-family` | ✅ | Generic families mapped to platform names | -| `line-height` | ✅ | normal, number, length | -| `letter-spacing`, `word-spacing` | ✅ | | -| `text-align` | ✅ | left, right, center, justify | -| `text-transform` | ✅ | uppercase, lowercase, capitalize | -| `text-decoration` | ✅ | underline, line-through, overline (bitfield — simultaneous) | -| `white-space` | ✅ | normal, pre, pre-wrap, pre-line, nowrap | +#### Color & Inheritance + +| CSS Property | Status | Notes | +| ------------ | ------ | --------- | +| `color` | ✅ | Inherited | + +#### Font Properties + +| CSS Property | Status | Notes | +| --------------------------- | ------ | ----------------------------------------- | +| `font` (shorthand) | ❌ | | +| `font-family` | ✅ | Generic families mapped to platform names | +| `font-size` | ✅ | Computed px | +| `font-weight` | ✅ | 100–900 | +| `font-style` | ✅ | italic | +| `font-stretch` | ❌ | | +| `font-size-adjust` | ❌ | | +| `font-kerning` | ❌ | | +| `font-optical-sizing` | ❌ | | +| `font-synthesis` | ❌ | Shorthand | +| `font-synthesis-weight` | ❌ | | +| `font-synthesis-style` | ❌ | | +| `font-synthesis-small-caps` | ❌ | | +| `font-variant` (shorthand) | ❌ | | +| `font-variant-ligatures` | ❌ | | +| `font-variant-caps` | ❌ | | +| `font-variant-numeric` | ❌ | | +| `font-variant-east-asian` | ❌ | | +| `font-variant-alternates` | ❌ | | +| `font-variant-position` | ❌ | | +| `font-variant-emoji` | ❌ | | +| `font-feature-settings` | ❌ | | +| `font-variation-settings` | ❌ | | +| `font-language-override` | ❌ | | + +#### Text Layout + +| CSS Property | Status | Notes | +| ----------------------------- | ------ | ------------------------------------------- | +| `line-height` | ✅ | normal, number, length | +| `letter-spacing` | ✅ | | +| `word-spacing` | ✅ | | +| `text-align` | ✅ | left, right, center, justify | +| `text-align-last` | ❌ | | +| `text-justify` | ❌ | | +| `text-indent` | ❌ | Field defined in FontProps, not extracted | +| `text-transform` | ✅ | uppercase, lowercase, capitalize | +| `white-space` | ✅ | normal, pre, pre-wrap, pre-line, nowrap | +| `word-break` | ❌ | | +| `overflow-wrap` / `word-wrap` | ❌ | | +| `line-break` | ❌ | | +| `hyphens` | ❌ | | +| `hyphenate-character` | ❌ | | +| `hyphenate-limit-chars` | ❌ | | +| `tab-size` | ❌ | | +| `text-overflow` | ❌ | Enum defined (Clip/Ellipsis), not extracted | +| `text-wrap` | ❌ | | +| `text-wrap-mode` | ❌ | | +| `text-wrap-style` | ❌ | | +| `hanging-punctuation` | ❌ | | +| `text-spacing-trim` | ❌ | | + +#### Text Decoration + +| CSS Property | Status | Notes | +| ----------------------------- | ------ | ----------------------------------------------------------- | +| `text-decoration` (shorthand) | ✅ | underline, line-through, overline (bitfield — simultaneous) | +| `text-decoration-line` | ✅ | | +| `text-decoration-style` | ⚠️ | Field defined, not extracted from Stylo | +| `text-decoration-color` | ⚠️ | Field defined, not extracted from Stylo | +| `text-decoration-thickness` | ❌ | | +| `text-decoration-skip-ink` | ❌ | | +| `text-underline-position` | ❌ | | +| `text-underline-offset` | ❌ | | + +#### Text Emphasis + +| CSS Property | Status | Notes | +| ------------------------ | ------ | ----- | +| `text-emphasis` | ❌ | | +| `text-emphasis-style` | ❌ | | +| `text-emphasis-color` | ❌ | | +| `text-emphasis-position` | ❌ | | + +#### Text Shadow + +| CSS Property | Status | Notes | +| ------------- | ------ | ------------------ | +| `text-shadow` | ❌ | Not in type schema | + +#### Writing Modes & BiDi + +| CSS Property | Status | Notes | +| ---------------------- | ------ | ----- | +| `direction` | ❌ | | +| `writing-mode` | ❌ | | +| `unicode-bidi` | ❌ | | +| `text-orientation` | ❌ | | +| `text-combine-upright` | ❌ | | + +#### Inline Layout & Alignment + +| CSS Property | Status | Notes | +| -------------------- | ------ | --------------------------- | +| `vertical-align` | ❌ | Enum defined, not extracted | +| `dominant-baseline` | ❌ | | +| `alignment-baseline` | ❌ | | +| `baseline-shift` | ❌ | | +| `initial-letter` | ❌ | | + +#### Ruby + +| CSS Property | Status | Notes | +| --------------- | ------ | ----- | +| `ruby-position` | ❌ | | +| `ruby-align` | ❌ | | ### Inline Elements @@ -160,19 +263,180 @@ Types from `cg::prelude` reused where they 100% align with CSS semantics: | `position: absolute` | ✅ | Via Taffy | | `z-index` | ⚠️ | Stored but not used for paint order | -### Not Yet Supported - -| Category | Properties | -| ----------------- | ------------------------------------------------------------------- | -| Background images | `background-image: url()`, `background-position`, `background-size` | -| Transform | `transform`, `transform-origin` | -| Box shadow inset | `box-shadow: inset` | -| Table layout | `display: table-row`, `table-cell` (proper grid) | -| Float | `float`, `clear` | -| Filter | `filter`, `backdrop-filter` | -| Clip/Mask | `clip-path`, `mask` | -| Outline | `outline` | -| Text | `text-indent`, `text-overflow`, `vertical-align` (sub/super) | +### Grid Layout + +| CSS Property | Status | Notes | +| ------------------------------------- | ------ | ---------------------------------------- | +| `display: grid` | ⚠️ | Taffy `Display::Grid`, no grid props yet | +| `grid-template-columns` | ❌ | | +| `grid-template-rows` | ❌ | | +| `grid-template-areas` | ❌ | | +| `grid-auto-columns`, `grid-auto-rows` | ❌ | | +| `grid-auto-flow` | ❌ | | +| `grid-column`, `grid-row` | ❌ | | +| `gap` (row-gap, column-gap) | ✅ | Flex/grid gap via Taffy | + +### Flexbox (detail) + +| CSS Property | Status | Notes | +| ----------------- | ------ | --------- | +| `flex-direction` | ✅ | Via Taffy | +| `flex-wrap` | ✅ | Via Taffy | +| `align-items` | ✅ | Via Taffy | +| `align-self` | ✅ | Via Taffy | +| `align-content` | ✅ | Via Taffy | +| `justify-content` | ✅ | Via Taffy | +| `justify-items` | ❌ | | +| `justify-self` | ❌ | | +| `flex-grow` | ✅ | Via Taffy | +| `flex-shrink` | ✅ | Via Taffy | +| `flex-basis` | ✅ | Via Taffy | +| `order` | ❌ | | + +### Sizing & Intrinsic Keywords + +| CSS Property | Status | Notes | +| ---------------------------- | ------ | -------------------------------- | +| `width`, `height` (%) | ⚠️ | px and auto only; % not resolved | +| `aspect-ratio` | ❌ | | +| `min-content`, `max-content` | ❌ | Intrinsic sizing keywords | +| `fit-content` | ❌ | | + +### Background (extended) + +| CSS Property | Status | Notes | +| ------------------------- | ------ | ----- | +| `background-position` | ❌ | | +| `background-size` | ❌ | | +| `background-repeat` | ❌ | | +| `background-origin` | ❌ | | +| `background-clip` | ❌ | | +| `background-attachment` | ❌ | | +| `background-image: url()` | ❌ | | + +### Box Shadow (detail) + +| CSS Property | Status | Notes | +| -------------------- | ------ | ----------------------------------- | +| `box-shadow` (outer) | ✅ | blur, spread, offset, border-radius | +| `box-shadow: inset` | ❌ | | +| Multiple shadows | ❌ | Only first shadow painted | + +### Positioning (extended) + +| CSS Property | Status | Notes | +| -------------------------------- | ------ | --------------------------------------- | +| `position: fixed` | ❌ | | +| `position: sticky` | ❌ | | +| `top`, `right`, `bottom`, `left` | ⚠️ | Stub in collect.rs, returns defaults | +| `z-index` | ⚠️ | Stored but not used for paint order | +| `float` | ❌ | Recognized in collect, no layout effect | +| `clear` | ❌ | Recognized in collect, no layout effect | + +### Transform & 3D + +| CSS Property | Status | Notes | +| --------------------- | ------ | ----- | +| `transform` | ❌ | | +| `transform-origin` | ❌ | | +| `perspective` | ❌ | | +| `backface-visibility` | ❌ | | + +### Filter & Effects + +| CSS Property | Status | Notes | +| ----------------- | ------ | ----- | +| `filter` | ❌ | | +| `backdrop-filter` | ❌ | | +| `clip-path` | ❌ | | +| `mask` | ❌ | | +| `mask-image` | ❌ | | + +### Outline + +| CSS Property | Status | Notes | +| ---------------- | ------ | ----- | +| `outline` | ❌ | | +| `outline-offset` | ❌ | | + +### Table Layout + +| CSS Property | Status | Notes | +| --------------------- | ------ | ------------------------------- | +| `display: table` | ⚠️ | Falls back to block flow | +| `display: table-row` | ⚠️ | Falls back to flex (faux-table) | +| `display: table-cell` | ⚠️ | Falls back to flex item | +| `border-collapse` | ❌ | | +| `border-spacing` | ❌ | | +| `table-layout` | ❌ | | +| `caption-side` | ❌ | | + +### Multi-column Layout + +| CSS Property | Status | Notes | +| -------------- | ------ | ------------------------------ | +| `columns` | ❌ | | +| `column-count` | ❌ | | +| `column-gap` | ✅ | Via Taffy gap (flex/grid only) | +| `column-rule` | ❌ | | +| `column-span` | ❌ | | +| `column-width` | ❌ | | + +### Generated Content & Counters + +| CSS Property | Status | Notes | +| -------------------------- | ------ | --------------------------------- | +| `content` (::before/after) | ❌ | Pseudo-elements not supported | +| `counter-reset` | ❌ | Internal counters for `
    ` only | +| `counter-increment` | ❌ | | +| `quotes` | ❌ | | + +### Interaction & UI + +| CSS Property | Status | Notes | +| ---------------- | ------ | ----------------------------- | +| `cursor` | ❌ | Not relevant for canvas embed | +| `pointer-events` | ❌ | Not relevant for canvas embed | +| `user-select` | ❌ | Not relevant for canvas embed | +| `resize` | ❌ | | +| `caret-color` | ❌ | | + +### Animation & Transition + +| CSS Property | Status | Notes | +| ------------ | ------ | ------------------ | +| `transition` | ❌ | Static render only | +| `animation` | ❌ | Static render only | + +### CSS Variables & Functions + +| Feature | Status | Notes | +| --------------------------- | ------ | -------------------- | +| Custom properties | ❌ | `var()` not resolved | +| `calc()` | ⚠️ | Stylo resolves to px | +| `clamp()`, `min()`, `max()` | ⚠️ | Stylo resolves to px | +| `env()` | ❌ | | + +### Replaced Elements + +| CSS Property | Status | Notes | +| --------------------- | ------ | ------------------------- | +| `object-fit` | ❌ | | +| `object-position` | ❌ | | +| `` rendering | ❌ | Images not loaded/painted | +| `` inline | ❌ | | +| ``, ``, ``, inline box decoration) | +| `list` | `
      `, `
        `, `list-style-type`, counters, nested lists | +| `table` | `display: table*`, `border-collapse`, `border-spacing` | +| `filter` | `filter`, `backdrop-filter` | +| `transform` | `transform`, `transform-origin`, `perspective` | +| `mask` | `clip-path`, `mask`, `mask-image` | +| `mixed` | Integration tests combining multiple domains | + +#### Naming examples -**Mixed** +``` +# --- paint --- +paint-background-solid.html background-color +paint-gradient-linear.html linear-gradient directions and multi-stop +paint-gradient-radial.html radial-gradient +paint-gradient-conic.html conic-gradient +paint-border-style.html solid, dashed, dotted +paint-border-style-double.html double, groove, ridge +paint-border-radius.html uniform, pill, circle, single-corner +paint-border-radius-elliptical.html elliptical (rx ≠ ry) +paint-shadow.html outer box-shadow +paint-shadow-inset.html inset box-shadow +paint-shadow-multiple.html multiple box-shadows +paint-opacity.html opacity values +paint-blend-mode.html mix-blend-mode values +paint-outline.html outline, outline-offset + +# --- layout --- +layout-block.html block flow +layout-display-none.html display: none skips elements +layout-position-relative.html position: relative with offsets +layout-position-absolute.html position: absolute +layout-overflow-hidden.html overflow: hidden clip +layout-visibility-hidden.html visibility: hidden/collapse + +# --- flex --- +flex-row.html flex-direction: row +flex-column.html flex-direction: column +flex-align-items.html align-items values +flex-gap.html row-gap, column-gap + +# --- box --- +box-dimensions.html width, height, min-*, max-* +box-margin.html margin all sides, collapsing, auto +box-padding.html padding all sides, shorthand + +# --- text --- +text-color.html color inheritance +text-align.html text-align values +text-font-properties.html font-size, weight, family, style +text-font-weight.html font-weight: 100–900 +text-line-height.html line-height: unitless, px +text-letter-spacing.html letter-spacing values +text-decoration.html underline, overline, line-through +text-decoration-full.html line + style + color combined +text-shadow.html text-shadow +text-whitespace-pre.html white-space: pre, pre-wrap, pre-line + +# --- inline --- +inline-elements.html , , + +# --- list --- +list-unordered.html
          disc/circle/square +list-ordered.html
            decimal, alpha +list-nested.html nested list counters + +# --- filter --- +filter-blur.html filter: blur() +filter-backdrop-blur.html backdrop-filter: blur() + +# --- transform --- +transform-2d.html translate, rotate, scale, skew +transform-origin.html transform-origin values + +# --- mask --- +mask-clip-path.html clip-path shapes + +# --- mixed --- +mixed-card.html realistic card combining many properties +mixed-inline-style.html style="" overriding stylesheet +``` -- `mixed-card.html` — realistic card with flex, padding, gap, bg, radius, border, shadow, typography -- `mixed-inline-style.html` — style="" attribute overriding stylesheet rules +--- ### Authoring Rules -1. **One concept per file** — each fixture targets a specific property group. -2. **Supported features only** — no margin, grid, tables, viewport units, position, or other unsupported CSS. See `crates/grida-canvas/src/html/TODO.md` for the unsupported list. -3. **Dark theme** — `background: #030712` on body, light text (`#e2e8f0`). -4. **Self-contained** — no external resources, no `