Skip to content

fix(dtcg): emit required typography lineHeight and letterSpacing - #1

Closed
Arshgill01 wants to merge 37 commits into
mainfrom
cursor/fix-dtcg-typography-required-props-71e8
Closed

Arshgill01 wants to merge 37 commits into
mainfrom
cursor/fix-dtcg-typography-required-props-71e8

Conversation

@Arshgill01

Copy link
Copy Markdown
Owner

Fixes google-labs-code#172.

What changed

export --format dtcg advertised the 2025.10 DTCG schema but omitted two of the five required typography properties (lineHeight and letterSpacing), so every typography token failed validation.

Two separate gaps:

  1. lineHeight was in the source but dropped. YAML parses unquoted lineHeight: 1.6 as a number. The typography model only accepted string dimensions, so the value never reached the DTCG emitter. Quoted strings ("1.6") already worked. Finite numeric lineHeight is now kept as a unitless multiplier (the DTCG meaning). Bare numbers on fontSize / letterSpacing still error because those properties require an explicit unit.

  2. letterSpacing has no DESIGN.md source. The DTCG 2025.10 typography value schema requires letterSpacing. When the role omits it, the exporter now emits { "value": 0, "unit": "px" } so the token validates. Explicit source values are unchanged.

How this differs from closed google-labs-code#174

google-labs-code#174 (unmerged) only kept YAML numeric lineHeight and explicitly did not invent a letterSpacing default. That left the schema-required field missing, so DTCG output still would not validate.

This PR includes that lineHeight model fix and emits the letterSpacing default so option 1 from google-labs-code#172 is complete.

Verification

bun install
bun run lint
bun run test
bun run build

Targeted:

bun test src/linter/model/handler.test.ts src/linter/dtcg/handler.test.ts

AI assistance

Cursor Grok assisted with investigation, TDD, and drafting this PR. I reviewed the change and ran the tests above.

Open in Web Open in Cursor 

dalmaer and others added 30 commits May 8, 2026 15:35
…sue google-labs-code#75 tests (google-labs-code#94)

The component property loop only checked for typeof 'number' before
passing values to string-only helpers (isTokenReference, isValidColor,
isParseableDimension). Boolean YAML values (e.g. visible: true) fell
through to those helpers and only survived by accident due to the typeof
guards added in PR google-labs-code#79. This change handles booleans explicitly alongside
numbers so the intent is clear.

Adds three tests covering the exact reproducer from Issue google-labs-code#75
(opacity: 0.9), boolean properties (visible: true), and a mixed case
with numbers, booleans, and strings in the same component.
…google-labs-code#95)

formatAsMarkdown template-literaled obj.summary directly, which coerced
the lint summary object { errors, warnings, infos } to its toString
representation '[object Object]'.

Detect the lint output shape (findings array + numeric summary) and
render a proper markdown report with severity counts and a bulleted
findings list. The legacy string-summary path for fixer/diff shapes
is preserved.

Adds 6 tests covering the regression, findings with and without paths,
empty findings, the --format md alias, and the legacy fixer shape.

Fixes the bug originally reported in PR google-labs-code#56.
…-labs-code#84)

* feat(linter): add unknown-key rule for unknown top-level keys

* refactor(linter): narrow unknown-key to typo detection via Levenshtein

DESIGN.md is intentionally extensible, so warning on every unknown
top-level key flags legitimate custom fields. Restrict the rule to
likely typos of known schema keys (edit distance ≤ 2, case-insensitive)
and stay silent for unrelated extension keys.

Per @davideast review feedback on google-labs-code#84:
- Add SCHEMA_KEYS / SchemaKey in parser/spec.ts as the single source
  of truth and reference it from the model handler.
- Add a zero-dependency Levenshtein helper.
- Suggest the closest known key in the warning message.

* test(linter): add unit tests for levenshtein helper

Cover empty strings, single edit operations (insert/delete/substitute),
symmetry, the classic kitten/sitting case, and the exact distances
used by the unknown-key typo threshold.
…oogle-labs-code#96)

The linter already supports oklch, oklab, lab, lch, rgb, hsl, hwb,
named colors, color-mix, and 8-digit hex alpha. But the spec, README,
and error messages all said only hex was accepted. This was blocking
adoption for teams using modern CSS color spaces (Issue google-labs-code#53).

Update the Color type definition in the spec, the token types table in
the README, the spec.mdx source, and the linter error message to
reflect the full range of supported formats.

Closes google-labs-code#53
…abs-code#98)

- Remove dead runtime dependencies with zero imports: ink, react,
  @json-render/core, @json-render/ink, mdast (type-only import,
  covered by @types/mdast)
- Remove @types/react from devDependencies
- Add package-level lint script (tsc --noEmit --skipLibCheck) so
  turbo lint actually runs
- Fix 10 pre-existing type errors in handler.test.ts where
  properties.get() returns ResolvedValue but tests compared against
  raw primitives
- Pin CI bun version to 1.3.9 to match root packageManager field
  instead of floating on latest
- Use root-level turbo commands in CI instead of cd-ing into
  packages/cli
- Add lint step to CI pipeline
- Regenerate bun.lock after dependency removal

bun install now completes with no peer warnings.
bun run build, bun run test, and bun run lint all pass at the root.

Closes google-labs-code#30
google-labs-code#104)

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
…de#103)

* fix: support nested token declarations in frontmatter (google-labs-code#102)

* fix: support numeric/boolean properties and limit nesting depth

---------

Co-authored-by: vikks <imvikks@gmail.com>
* release: 0.3.0

* fix: remove hardcoded local path from DTCG conformance test

* test: skip DTCG conformance test (upstream @terrazzo/token-types removed from npm)
…er keys (google-labs-code#105)

Closes google-labs-code#100

Add a new `token-like-ignored` lint rule that warns when a top-level YAML
key is not part of the recognized export schema and its value looks like a
design-token map (hex colors, CSS dimensions, or typography property names).
These keys are silently dropped by `design.md export`, misleading users into
thinking their color palette or type scale was exported.

- Extend `ParsedDesignSystem` with `rawValues` so the parser carries all
  raw YAML values through to the model layer
- Extend `DesignSystemState` with `unknownKeyValues` populated by `ModelHandler`
- New rule `token-like-ignored` in `linter/rules/token-like-ignored.ts`
- 10 tests covering hex color maps, font maps, dimension maps, flat scalars,
  non-token objects, nested maps, and multi-key scenarios
- Register rule in `DEFAULT_RULE_DESCRIPTORS` and re-export from public API
- Update hardcoded rule-count assertions in `types.test.ts` and `spec.test.ts`

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
…-labs-code#118)

`npx @google/design.md spec` failed with "Failed to load spec.md" in
installed/published packages. The bundler emits the CLI to dist/index.js,
so getSpecContent() resolves spec.md alongside it at dist/spec.md, but the
build only copied docs/spec.md to dist/linter/. The dev-path fallback then
resolves outside the package, so the command threw.

Copy spec.md to dist/ as well, mirroring how spec-config.yaml is already
copied to both dist/ and dist/linter/. Add a `spec` invocation to the
tarball smoke test so the bundled-spec.md resolution is exercised in CI.
…ogle-labs-code#151)

serializeTailwindV4() already appends a trailing newline to the
@theme block. Using console.log() adds a second trailing newline,
making checked-in generated CSS fail strict whitespace gates like
git diff --check.

Fixes google-labs-code#139

Change-Id: I3a0150b2c0f8e6a7b9d4c3e2f1a0b9c8d7e6f5a4
…mix weights (google-labs-code#120)

parseHue tested `endsWith('rad')` before `endsWith('grad')`, so a gradian
angle matched the radians branch first: `100grad` resolved to ~329.58deg
instead of 90deg, silently producing the wrong color, luminance, contrast
result, and exports. Test `grad` before `rad`; deg/rad/turn/unitless
behavior is unchanged.

parseColorWithWeight treated any bare number as a weight and multiplied it
by 100, so `color-mix(in srgb, red 20, blue)` produced a wildly wrong blend
instead of being rejected. CSS color-mix weights are percentages only;
require a `%` suffix and otherwise return null (invalid color).
…ode#121)

Three small guards so a hostile DESIGN.md cannot pin CPU or exhaust the
call stack. All inputs are at the documented untrusted boundary (arbitrary
file/stdin), and none of the changes alter results for legitimate input.

- parseDimensionParts (and token-like-ignored's CSS_DIMENSION_RE) backtrack
  quadratically on long all-digit strings. Cap value length to 64 chars
  before matching; real CSS dimensions are far shorter.
- unknown-key runs an O(n*m) Levenshtein DP against every schema key for
  each unknown key. Skip a schema key whose length differs by more than the
  typo threshold — edit distance is at least the length difference, so the
  set of suggestions is unchanged.
- parseCssColor recurses for nested color-mix() with no depth bound. Thread
  a depth counter and stop at 32, so an over-deep value resolves to an
  invalid color (a precise error finding) instead of a RangeError that
  collapses the whole model build.
…de#132) (google-labs-code#145)

* fix: replace ENOENT stack trace with FileReadError and human-readable stderr (google-labs-code#132)

Introduce a typed `FileReadError` class in `readInput` so the function
throws instead of calling `process.exit` directly. Each command handler
catches it and writes a plain-text error to stderr:

  Error: "DESIGN.md" not found.
  Create a DESIGN.md file or pass "-" to read from stdin.

This replaces the unhandled Node.js stack trace dump reported in google-labs-code#132.
`readInput` is now unit-testable without mocking `process.exit`, and
`FileReadError.filePath` identifies the specific missing file (important
for `diff`, which reads two files).

* fix: use error code to generate accurate FileReadError message

Replace the hardcoded "not found" string in all command handlers with
a friendlyMessage getter on FileReadError that checks the OS error code:
- ENOENT → "not found. Create a DESIGN.md file or pass '-' for stdin."
- EACCES → "could not be read: permission denied."
- other  → "could not be read: <raw message>"

This prevents a misleading "not found" message when the file exists
but cannot be read due to permissions or other I/O errors.
…bs-code#146)

When a user runs a command with "-" as the file path from an interactive
terminal (e.g. design.md lint -), the process blocks silently waiting
for EOF with no indication of what to do.

Add a TTY check before the stdin read loop. If stdin is attached to a
terminal, write to stderr:

  Reading from stdin… Press Ctrl+D when done.

The stdin stream is accepted as an optional second parameter on readInput
(defaulting to process.stdin), making the TTY path fully testable via
dependency injection without touching process.stdin directly.

Also exports StdinStream so callers can type mock streams without
duplicating the definition.
…on input failure (google-labs-code#123)

On a missing or unreadable input file, readInput printed a structured
FILE_READ_ERROR JSON to stderr and then re-threw. The throw let the CLI
framework print a second, stack-trace error on top of the JSON and override
the exit code with 1 instead of the intended 2. Exit cleanly with code 2
right after the JSON, matching the function's documented "exits with error
JSON" contract.

Also unify the export command's stderr error envelope with readInput's
`{ error: <CODE>, message }` shape: an unknown --format now reports
`INVALID_FORMAT` (the human text moves to `message`), and emitter failures
forward the emitter's structured code (e.g. INVALID_TOKEN_NAME) instead of
discarding it.
…findings (google-labs-code#126)

The export command derived its exit code from the source's lint summary, so
exporting a file that had any lint *error* exited 1 even though the export
produced correct output. That conflates "the source has lint findings"
(which `lint` already reports by exiting 1) with "the export failed."

Decouple them: a successful export exits 0; only an invalid --format or an
emitter failure exits 1 (and an unreadable input exits 2, via readInput).
Document the export exit codes in the README.
…ogle-labs-code#109)

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
…#152)

The success branches (css-tailwind, json-tailwind, dtcg, css-vars) write
their output but never assign process.exitCode, so a successful export
left the code at its incoming value instead of an explicit 0. That is the
success-path counterpart to google-labs-code#126, which decoupled the exit code from
source lint findings; the subprocess test there only exercises json-tailwind
and a real process happens to exit 0 when exitCode is unset, so the gap went
unnoticed. Set process.exitCode = 0 after the format branches (every error
branch already returns first).

The in-process export.test.ts added in google-labs-code#109 asserts process.exitCode === 0
after a css-vars export, so it was failing on main. Also fix a companion
assertion in that file: it read error.error for the human message, but the
error envelope is { error: CODE, message: TEXT }, so the text lives on
error.message.

Signed-off-by: Arpit Jain <arpitjain099@gmail.com>
…google-labs-code#122)

cssStringLiteral escaped only backslash and double-quote. A font-family value
containing a raw newline, carriage return, or form feed (legal via a YAML
quoted or block scalar) was emitted verbatim inside the CSS string literal,
where raw line terminators are illegal and can break the value out of the
@theme token. Emit them as CSS hex escapes (e.g. `\a `).
* fix: render primitive types from spec config

* chore: retrigger cla check

---------

Co-authored-by: Julio César Suástegui <juliosuas@users.noreply.github.com>
* fix(cli): add defensive type guard to parseDimension to prevent runtime crashes

* fix(cli): resolve spec.md path resolution issue in compiled CLI
Adds duplicate and collision detection to the model builder for nested and flat token keys, preventing silent overwrites in the symbol table.
…code#125)

A typography token's sub-properties outside the schema (fontFamily,
fontSize, fontWeight, lineHeight, letterSpacing, fontFeature, fontVariation)
were silently dropped by the model — never resolved, never exported, and
with no diagnostic, so a typo like `fontwight` or an unsupported property
like `textTransform` vanished without a trace. Emit a warning for each,
mirroring how unknown component sub-tokens are already reported.
…labs-code#97) (google-labs-code#117)

* feat: data-driven Color and Dimension type definitions (fixes google-labs-code#97)

* fix(export): align export test expectations with structured error formats

* fix(ts): eliminate duplicate typeDefinitions identifiers and properties for clean CI compilation

---------

Co-authored-by: David East <deast@google.com>
drmikecrypto and others added 7 commits July 27, 2026 12:59
…4 serializer (google-labs-code#140)

Use stdout.write because serializeTailwindV4 already ends with one
newline; console.log added a second byte that broke git diff --check.

Also exit cleanly on missing DESIGN.md with a friendly message instead
of dumping a Node stack trace after the JSON error.

Fixes google-labs-code#139
Fixes google-labs-code#132

Co-authored-by: David East <deast@google.com>
…ing newline (google-labs-code#144)

* fix: avoid extra css-tailwind newline

* chore: retrigger cla check

---------

Co-authored-by: David East <deast@google.com>
…arnings (google-labs-code#155)

Implement support for an optional omitted frontmatter configuration key in DESIGN.md, allowing design system authors to explicitly declare token categories that are intentionally skipped or absent (Issue google-labs-code#78).

* Update Parser: Add 'omitted' to the known schema keys and types. Implement frontmatter parsing supporting both bare strings (e.g. - spacing) and object mappings with reasons (e.g. section: rounded, reason: "No rounded corners").
* Update Model: Forward the parsed omitted sections to the compiled DesignSystemState. Add optional rule property to linter Findings.
* Implement Omission Validation & Suppression:
  - Create omittedRule to validate the omitted configuration, warning on unknown or redundant sections (if tokens exist for a section listed in omitted).
  - Update missing-sections and missing-typography rules to skip warnings if the targets are explicitly listed as omitted.
  - Register the new rule in the default linter list.
* Test Coverage: Add test suites verifying frontmatter parsing, model mapping, linter warnings (declared-omission, redundant-omission, unknown-omission), and rule suppressions.
* Docs: Document the omitted frontmatter key in README.md and spec.mdx, update active rule counts, and regenerate docs/spec.md.

---------

Co-authored-by: David East <deast@google.com>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Sudarshan sunil hadmode <sudarshan.deve@gmail.com>
…gle-labs-code#119)

- lint `summary` uses the key `infos`, not `info`
- the `diff` JSON includes all token categories plus a `findings`
  block (before/after/delta) and `regression`; the example previously
  showed only two token categories and omitted `findings`
- the linter runs ten rules — add the missing `token-like-ignored` row

Co-authored-by: David East <deast@google.com>
YAML unquoted lineHeight values were dropped because the typography
model only accepted string dimensions. Keep numeric lineHeight as a
unitless multiplier, and emit letterSpacing {value: 0, unit: "px"}
when DESIGN.md omits it so DTCG export satisfies the 2025.10 schema.

Fixes google-labs-code#172

Co-authored-by: Arshdeep singh <arshgill6120@gmail.com>
@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

Opened against this fork by mistake. The upstream PR is google-labs-code#177

@Arshgill01 Arshgill01 closed this Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dtcg export omits required typography properties, so output fails the schema it declares