Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,19 +261,21 @@ Export DESIGN.md tokens to other formats.
npx @google/design.md export --format json-tailwind DESIGN.md > tailwind.theme.json
npx @google/design.md export --format css-tailwind DESIGN.md > theme.css
npx @google/design.md export --format dtcg DESIGN.md > tokens.json
npx @google/design.md export --format html DESIGN.md > preview.html
```

| Option | Type | Default | Description |
|:-------|:-----|:--------|:------------|
| `file` | positional | required | Path to DESIGN.md (or `-` for stdin) |
| `--format` | `json-tailwind` \| `css-tailwind` \| `tailwind` \| `dtcg` | required | Output format |
| `--format` | `json-tailwind` \| `css-tailwind` \| `tailwind` \| `dtcg` \| `html` | required | Output format |

| Format | Output | Description |
|:-------|:-------|:------------|
| `json-tailwind` | JSON | Tailwind v3 `theme.extend` config object |
| `css-tailwind` | CSS | Tailwind v4 `@theme { ... }` block with CSS custom properties |
| `tailwind` | JSON | Alias for `json-tailwind` |
| `dtcg` | JSON | W3C Design Tokens Format Module |
| `html` | HTML | Human-readable static preview site for reviewing the design settings in a browser |

### `spec`

Expand Down Expand Up @@ -327,6 +329,7 @@ DESIGN.md tokens are inspired by the [W3C Design Token Format](https://www.desig
- **Tailwind v3 config (JSON)** — `npx @google/design.md export --format json-tailwind DESIGN.md` — emits a `theme.extend` JSON object for `tailwind.config.js`. `--format tailwind` is a backwards-compatible alias.
- **Tailwind v4 theme (CSS)** — `npx @google/design.md export --format css-tailwind DESIGN.md` — emits a CSS `@theme { ... }` block using Tailwind v4's CSS-variable token namespaces (`--color-*`, `--font-*`, `--text-*`, `--leading-*`, `--tracking-*`, `--font-weight-*`, `--radius-*`, `--spacing-*`).
- **DTCG tokens.json** ([W3C Design Tokens Format Module](https://tr.designtokens.org/format/)) — `npx @google/design.md export --format dtcg DESIGN.md`
- **HTML preview** — `npx @google/design.md export --format html DESIGN.md > preview.html` — emits a polished static site that renders colors, type, components, spacing, and radius tokens for quick human review.

## Status

Expand All @@ -335,4 +338,4 @@ The DESIGN.md format is at version `alpha`. The spec, token schema, and CLI are
## Disclaimer

This project is not eligible for the [Google Open Source Software Vulnerability
Rewards Program](https://bughunters.google.com/open-source-security).
Rewards Program](https://bughunters.google.com/open-source-security).
2 changes: 2 additions & 0 deletions docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ DESIGN.md is a self-contained, plain-text representation of a design system. It

A DESIGN.md file contains two parts: An optional YAML frontmatter, and a markdown body. The YAML front matter contains machine-readable design tokens. The markdown body sections provide human-readable design rationale and guidance. Prose may use descriptive color names (e.g., "Midnight Forest Green") that correspond to systematic token names (e.g., `primary`). The tokens are the normative values; the prose provides context for how to apply them.

The CLI can also emit a human-readable HTML preview from the same tokens. This preview is intentionally static, so it can be opened directly in a browser or committed alongside example systems for visual review.

# Design Tokens

DESIGN.md may embed design tokens in a structured format. The system that we use to describe design tokens is inspired by the
Expand Down
17 changes: 14 additions & 3 deletions packages/cli/src/commands/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@
// limitations under the License.

import { defineCommand } from 'citty';
import { lint, TailwindEmitterHandler, TailwindV4EmitterHandler, serializeTailwindV4 } from '../linter/index.js';
import { lint, HtmlPreviewEmitterHandler, TailwindEmitterHandler, TailwindV4EmitterHandler, serializeTailwindV4 } from '../linter/index.js';
import { DtcgEmitterHandler } from '../linter/dtcg/handler.js';
import { readInput } from '../utils.js';

const FORMATS = ['css-tailwind', 'json-tailwind', 'tailwind', 'dtcg'] as const;
const FORMATS = ['css-tailwind', 'json-tailwind', 'tailwind', 'dtcg', 'html'] as const;
type ExportFormat = typeof FORMATS[number];

export default defineCommand({
meta: {
name: 'export',
description: 'Export DESIGN.md tokens to other formats. `css-tailwind` emits Tailwind v4 CSS @theme; `json-tailwind` emits Tailwind v3 theme.extend JSON; `tailwind` is an alias for `json-tailwind`; `dtcg` emits W3C Design Tokens.',
description: 'Export DESIGN.md tokens to other formats. `css-tailwind` emits Tailwind v4 CSS @theme; `json-tailwind` emits Tailwind v3 theme.extend JSON; `tailwind` is an alias for `json-tailwind`; `dtcg` emits W3C Design Tokens; `html` emits a readable static preview page.',
},
args: {
file: {
Expand Down Expand Up @@ -85,6 +85,17 @@ export default defineCommand({
}

console.log(JSON.stringify(result.data, null, 2));
} else if (format === 'html') {
const handler = new HtmlPreviewEmitterHandler();
const result = handler.execute(report.designSystem);

if (!result.success) {
console.error(JSON.stringify({ error: result.error.message }));
process.exitCode = 1;
return;
}

console.log(result.data);
}

process.exitCode = report.summary.errors > 0 ? 1 : 0;
Expand Down
84 changes: 84 additions & 0 deletions packages/cli/src/linter/html/handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { describe, expect, test } from 'bun:test';
import { HtmlPreviewEmitterHandler } from './handler.js';
import type { DesignSystemState, ResolvedColor, ResolvedDimension, ResolvedTypography } from '../model/spec.js';

function makeColor(hex: string, luminance = 0): ResolvedColor {
return { type: 'color', hex, r: 0, g: 0, b: 0, luminance };
}

function makeDim(value: number, unit: string): ResolvedDimension {
return { type: 'dimension', value, unit };
}

function emptyState(overrides?: Partial<DesignSystemState>): DesignSystemState {
return {
colors: new Map(),
typography: new Map(),
rounded: new Map(),
spacing: new Map(),
components: new Map(),
symbolTable: new Map(),
...overrides,
};
}

describe('HtmlPreviewEmitterHandler', () => {
test('renders a complete static HTML preview', () => {
const body: ResolvedTypography = {
type: 'typography',
fontFamily: 'Public Sans',
fontSize: makeDim(16, 'px'),
fontWeight: 400,
lineHeight: makeDim(24, 'px'),
};

const result = new HtmlPreviewEmitterHandler().execute(emptyState({
name: 'Heritage',
description: 'Editorial design system',
colors: new Map([
['primary', makeColor('#1a1c1e')],
['neutral', makeColor('#f7f5f2', 0.9)],
]),
typography: new Map([['body-md', body]]),
rounded: new Map([['sm', makeDim(4, 'px')]]),
spacing: new Map([['md', makeDim(16, 'px')]]),
}));

expect(result.success).toBe(true);
if (!result.success) return;
expect(result.data).toContain('<!doctype html>');
expect(result.data).toContain('<title>Heritage Preview</title>');
expect(result.data).toContain('Editorial design system');
expect(result.data).toContain('body-md');
expect(result.data).toContain('#1a1c1e');
});

test('escapes token names and descriptions before rendering HTML', () => {
const result = new HtmlPreviewEmitterHandler().execute(emptyState({
name: '<script>alert(1)</script>',
description: 'Use "quoted" text',
colors: new Map([['primary<script>', makeColor('#000000')]]),
}));

expect(result.success).toBe(true);
if (!result.success) return;
expect(result.data).not.toContain('<script>alert(1)</script>');
expect(result.data).toContain('&lt;script&gt;alert(1)&lt;/script&gt;');
expect(result.data).toContain('Use &quot;quoted&quot; text');
expect(result.data).toContain('primary&lt;script&gt;');
});
});
Loading