CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Failing after 20s
CI & Build / Python tests (push) Successful in 44s
CI & Build / Build & push image (push) Skipped
Last piece of the architecture in #2296. The operator: "the prose doesn't have to live as one offs, there's a central system for managing it." Two fields, both free-form: design_systems.guidance the narrative a token table cannot hold — aesthetic, voice and tone, what is deliberately out of scope. design_tokens.rationale WHY a token is this value, which is a different question from `purpose` (what it is FOR). "Success equals Moss, aligned by design" is a rationale; "page bg, deepest surface" is a purpose. Rules carry the first routinely and a token row had nowhere to put it. Free-form rather than a column per category, deliberately. A schema with `voice`, `aesthetic` and `scope` columns would bake one rulebook's table of contents into every install (rule #115), leaving the next install three empty columns and nowhere for what it actually cares about. Both nullable: a design system with no prose at all is complete, not a draft. `rationale` cascades like `purpose` — deepest non-empty wins — so an app overriding a colour keeps the family's reasoning rather than blanking it. Same argument as `supersedes`: the override was about the value, not the meaning. In the generated sheet the inline comment prefers `purpose` and falls back to `rationale`, so a token carrying only the why still says something instead of rendering bare.
302 lines
12 KiB
Python
302 lines
12 KiB
Python
"""Render a design system's resolved tokens as its master CSS sheet.
|
|
|
|
Pure, like `design_cascade` and for the same reasons: no database import, so the
|
|
rendering rule is testable without one and the preview surface can reuse it.
|
|
|
|
WHAT THIS SHEET IS, AND DELIBERATELY IS NOT
|
|
-------------------------------------------
|
|
It declares **purpose tokens only** — custom properties, grouped by what they
|
|
mean. It contains no rules for elements or classes: no `.btn-primary`, no
|
|
`table`, no `input`.
|
|
|
|
That is the point rather than a limitation. A sheet that styled every element
|
|
would restate the same handful of values once per element and grow with the UI;
|
|
a sheet of purpose-named values states each once and is reused. Components —
|
|
buttons, tables, input schemes — live as SNIPPETS that reference these names and
|
|
carry prose about the idea, which is a surface that already exists and already
|
|
has recall, locations, drift checks and merge.
|
|
|
|
So the division is: this sheet says what the values MEAN; snippets say what
|
|
things LOOK LIKE, in terms of those values. A token named after an element
|
|
(`--fs-button-bg`) is the smell that the two have been mixed — it multiplies
|
|
with every new element, where a purpose name (`--fs-action-primary`) is reused.
|
|
|
|
SAFETY
|
|
------
|
|
Values are interpolated into a stylesheet, and design systems are shareable
|
|
records (rule #47). A value of `red; } body { display: none` in a system someone
|
|
shared with you would otherwise inject CSS into your page. Everything rendered
|
|
here is validated or dropped — never escaped-and-hoped.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from collections.abc import Sequence
|
|
|
|
BASE_MODE = "base"
|
|
|
|
# A custom property name, strictly. Anything else is dropped rather than
|
|
# sanitised: a name is an identifier, and a "cleaned up" identifier is a
|
|
# different token than the one the operator recorded.
|
|
_VALID_NAME = re.compile(r"^--[A-Za-z0-9_-]+$")
|
|
|
|
# Characters that would end the declaration, open a block, start an at-rule, or
|
|
# begin a tag. A value containing any of them is not a value.
|
|
_UNSAFE_VALUE = re.compile(r"[{};@<>]|\*/|/\*|\n|\r")
|
|
|
|
|
|
def is_valid_token_name(name: str) -> bool:
|
|
return bool(_VALID_NAME.match((name or "").strip()))
|
|
|
|
|
|
def safe_value(value: str) -> str | None:
|
|
"""A CSS value that cannot escape its declaration, or None.
|
|
|
|
Rejects rather than strips. A partially-sanitised value is a value the
|
|
operator did not write, and silently rendering a different colour than the
|
|
record holds is worse than rendering none — the sheet's whole claim is that
|
|
it IS the record.
|
|
"""
|
|
candidate = (value or "").strip()
|
|
if not candidate or _UNSAFE_VALUE.search(candidate):
|
|
return None
|
|
return candidate
|
|
|
|
|
|
def safe_comment(text: str) -> str:
|
|
"""Comment text that cannot close its comment or break the line."""
|
|
return re.sub(r"\*/|/\*|[\n\r]", " ", (text or "")).strip()
|
|
|
|
|
|
def selector_for_mode(mode: str, root_selector: str = ":root") -> str:
|
|
"""Which selector a mode's declarations belong under.
|
|
|
|
`base` gets the caller's root selector; every other mode gets a bare
|
|
attribute selector, matching the convention already in the codebase.
|
|
|
|
The root selector is a PARAMETER because a container-scoped preview cannot
|
|
use `:root` — #251 recorded that mode scoping is one-way (light lives on
|
|
`:root`, dark layers over it), so a generator that hardcoded `:root` could
|
|
not serve a preview at all.
|
|
"""
|
|
if mode == BASE_MODE:
|
|
return root_selector
|
|
safe_mode = re.sub(r"[^A-Za-z0-9_-]", "", mode)
|
|
return f'[data-theme="{safe_mode}"]' if safe_mode else root_selector
|
|
|
|
|
|
def _grouped(tokens: Sequence) -> list[tuple[str | None, list]]:
|
|
"""Tokens by group, preserving the order they arrive in (already sorted)."""
|
|
groups: dict[str | None, list] = {}
|
|
for token in tokens:
|
|
groups.setdefault(getattr(token, "group_name", None), []).append(token)
|
|
return list(groups.items())
|
|
|
|
|
|
def _modes_present(tokens: Sequence) -> list[str]:
|
|
"""Every mode any token declares, base first then the rest alphabetically."""
|
|
modes = {
|
|
mode
|
|
for token in tokens
|
|
for mode in (getattr(token, "value_by_mode", None) or {})
|
|
}
|
|
rest = sorted(modes - {BASE_MODE})
|
|
return ([BASE_MODE] if BASE_MODE in modes else []) + rest
|
|
|
|
|
|
def render_stylesheet(
|
|
tokens: Sequence,
|
|
*,
|
|
root_selector: str = ":root",
|
|
title: str = "",
|
|
design_system_id: int | None = None,
|
|
) -> str:
|
|
"""The master sheet for a resolved token set.
|
|
|
|
One block per mode. Within a block, only the tokens that declare a value for
|
|
that mode — so a mode block is an override layer, exactly as the storage
|
|
model has it.
|
|
|
|
Tokens with no value at all are emitted as COMMENTED-OUT declarations in
|
|
their group, not dropped. The rulebook named them, so their absence is a
|
|
finding, and a commented line puts that finding where the reader is already
|
|
looking.
|
|
"""
|
|
header = [
|
|
"/*",
|
|
f" * {safe_comment(title) or 'Design system'} — generated stylesheet",
|
|
]
|
|
if design_system_id is not None:
|
|
header.append(f" * Source: design system {int(design_system_id)}.")
|
|
header += [
|
|
" *",
|
|
" * Purpose tokens only. This sheet declares what values MEAN; it styles",
|
|
" * no elements. Buttons, tables and input schemes live as snippets that",
|
|
" * reference these names, so each value is stated once and reused rather",
|
|
" * than restated per element.",
|
|
" *",
|
|
" * Generated — edit the design system, not this file.",
|
|
" */",
|
|
"",
|
|
]
|
|
|
|
lines = list(header)
|
|
valueless = [
|
|
t for t in tokens
|
|
if is_valid_token_name(getattr(t, "name", ""))
|
|
and not (getattr(t, "value_by_mode", None) or {})
|
|
]
|
|
|
|
for mode in _modes_present(tokens):
|
|
block: list[str] = []
|
|
for group, group_tokens in _grouped(tokens):
|
|
entries: list[str] = []
|
|
for token in group_tokens:
|
|
name = (getattr(token, "name", "") or "").strip()
|
|
if not is_valid_token_name(name):
|
|
continue
|
|
raw = (getattr(token, "value_by_mode", None) or {}).get(mode)
|
|
if raw is None:
|
|
continue
|
|
value = safe_value(str(raw))
|
|
if value is None:
|
|
entries.append(
|
|
f" /* {name}: value rejected — not a safe CSS value */"
|
|
)
|
|
continue
|
|
# Purpose first — what the token is FOR is what a reader of the
|
|
# stylesheet needs. Rationale is the fallback so a token that
|
|
# only carries the why still says something.
|
|
purpose = safe_comment(
|
|
getattr(token, "purpose", "")
|
|
or getattr(token, "rationale", "")
|
|
or ""
|
|
)
|
|
comment = f" /* {purpose} */" if purpose and mode == BASE_MODE else ""
|
|
entries.append(f" {name}: {value};{comment}")
|
|
if entries:
|
|
if group:
|
|
block.append(f" /* {safe_comment(group)} */")
|
|
block.extend(entries)
|
|
block.append("")
|
|
|
|
# Declared-but-valueless tokens belong to the base layer: they have no
|
|
# mode to sit under, and repeating them per mode would triple the noise.
|
|
if mode == BASE_MODE and valueless:
|
|
block.append(" /* Declared by the design system, no value set yet: */")
|
|
block.extend(f" /* {t.name}: ; */" for t in valueless)
|
|
block.append("")
|
|
|
|
if not block:
|
|
continue
|
|
lines.append(f"{selector_for_mode(mode, root_selector)} {{")
|
|
lines.extend(block[:-1] if block[-1] == "" else block)
|
|
lines.append("}")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines).rstrip() + "\n"
|
|
|
|
|
|
def duplicate_values(tokens: Sequence) -> dict[str, list[str]]:
|
|
"""Values declared by more than one token, mapped to the names declaring them.
|
|
|
|
Two names for one value are either a deliberate alias or the same idea
|
|
recorded twice — the token-level form of the duplicated-definition shape.
|
|
Reported rather than refused: a design system legitimately aligns colours on
|
|
purpose ("Success = Moss, by design"), and a generator that rejected that
|
|
would be wrong about the operator's intent.
|
|
|
|
Compares the BASE value only. Two tokens agreeing in one mode and diverging
|
|
in another are not duplicates of each other — they are a near-miss, which is
|
|
a different and less interesting finding.
|
|
"""
|
|
by_value: dict[str, list[str]] = {}
|
|
for token in tokens:
|
|
name = getattr(token, "name", "")
|
|
base = (getattr(token, "value_by_mode", None) or {}).get(BASE_MODE)
|
|
if not name or not base:
|
|
continue
|
|
by_value.setdefault(str(base).strip().lower(), []).append(name)
|
|
return {value: names for value, names in by_value.items() if len(names) > 1}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Reading a sheet from the other side: does this code use it correctly?
|
|
# ---------------------------------------------------------------------------
|
|
#
|
|
# "The snippets use the tags from the sheet" is a verifiable relation, and
|
|
# nothing checked it before. Three questions, each a different failure:
|
|
#
|
|
# var(--x) where no --x exists -> renders as NOTHING. No error, no test
|
|
# failure, no visual clue beyond the thing
|
|
# silently not being styled.
|
|
# a superseded literal in code -> the value the sheet said to stop writing,
|
|
# and the sheet knows what to write instead.
|
|
# --x: declared inside a snippet -> a component minting its own token is the
|
|
# bloat a shared sheet exists to prevent.
|
|
#
|
|
# The first is not hypothetical: `--color-accent` was used throughout a new view
|
|
# in this codebase and does not exist.
|
|
|
|
_VAR_REFERENCE = re.compile(r"var\(\s*(--[A-Za-z0-9_-]+)")
|
|
_LOCAL_DEFINITION = re.compile(r"(?<![\w-])(--[A-Za-z0-9_-]+)\s*:")
|
|
|
|
|
|
def referenced_tokens(code: str) -> set[str]:
|
|
"""Every custom property the code reads through `var()`."""
|
|
return set(_VAR_REFERENCE.findall(code or ""))
|
|
|
|
|
|
def defined_tokens(code: str) -> set[str]:
|
|
"""Every custom property the code declares itself.
|
|
|
|
Excludes names it also reads: `--x: var(--x, fallback)` is a redeclaration
|
|
of something the sheet owns, which the unknown-reference check already
|
|
covers more precisely.
|
|
"""
|
|
return set(_LOCAL_DEFINITION.findall(code or "")) - referenced_tokens(code or "")
|
|
|
|
|
|
def _literal_pattern(literal: str) -> re.Pattern:
|
|
"""Match a literal value without matching a longer one that contains it.
|
|
|
|
`#fff` must not match inside `#ffffff` — they are different colours, and a
|
|
finding that fired on the wrong one would send someone to change code that
|
|
was already correct.
|
|
"""
|
|
escaped = re.escape(literal)
|
|
lead = r"(?<![0-9A-Za-z_#-])"
|
|
trail = r"(?![0-9A-Za-z_-])" if literal.startswith("#") else r"(?![0-9A-Za-z_-])"
|
|
return re.compile(lead + escaped + trail, re.IGNORECASE)
|
|
|
|
|
|
def check_code_against_tokens(code: str, tokens) -> dict:
|
|
"""What this code gets wrong about that token set.
|
|
|
|
`tokens` is any sequence with `.name`, `.value_by_mode` and `.supersedes` —
|
|
resolved tokens, or stored ones.
|
|
|
|
Reports rather than scores. Every finding here has a legitimate exception:
|
|
a snippet may target a system it isn't being checked against, and a literal
|
|
may be deliberate in a context the token doesn't cover. What it removes is
|
|
the SILENCE — all three currently fail with no signal at all.
|
|
"""
|
|
known = {getattr(t, "name", "") for t in tokens}
|
|
referenced = referenced_tokens(code)
|
|
|
|
superseded: list[dict] = []
|
|
for token in tokens:
|
|
for literal in getattr(token, "supersedes", None) or ():
|
|
if _literal_pattern(str(literal)).search(code or ""):
|
|
superseded.append({
|
|
"literal": literal,
|
|
"use_instead": getattr(token, "name", ""),
|
|
})
|
|
|
|
return {
|
|
"used": sorted(referenced & known),
|
|
"unknown": sorted(referenced - known),
|
|
"superseded_literals": superseded,
|
|
"local_definitions": sorted(defined_tokens(code)),
|
|
}
|