export interface TokenEntry { /** 그룹을 뗀 토큰 이름 (`--fox-color-background-default` → `background-default`) */ name: string; /** * 현재 테마·뷰포트에서 실제로 해석된 값 (`#FFFFFF`, `1.6rem`). * * 선언 원문이 아니라 계산값을 쓴다 — 색상은 Lightning CSS가 `light-dark()`를 * `var(--lightningcss-light, …) var(--lightningcss-dark, …)`로 폴리필해 내보내므로 * 원문이 읽기 어렵고, 크기는 alias가 `var(--fox-…)` 참조라 원문만으로는 값을 알 수 * 없다. 계산값은 지금 화면에 실제로 적용되는 값과 정확히 일치한다. */ value: string; /** 스타일에서 참조할 커스텀 프로퍼티 전체 이름 */ property: string; } export type TokenRegistry = Record; const PREFIX = "--fox-"; /** * 문서의 모든 스타일시트를 훑어 `--fox-*` 커스텀 프로퍼티를 그룹별로 모은다. * * 테스트 페이지가 토큰 목록을 따로 들고 있지 않게 하려는 것이 목적이다 — `tokens/`에 * 값을 추가하면 여기에 자동으로 나타나고, 목록이 두 곳에 존재해 어긋날 일이 없다. * CSSOM은 선언 순서를 보존하므로 SCSS map에 적은 순서가 그대로 유지된다. * * 브라우저에서만 동작한다(문서가 있어야 한다). */ export function readTokenRegistry(): TokenRegistry { const registry: TokenRegistry = {}; const seen = new Set(); // 이름·순서는 CSSOM 규칙에서, 값은 계산 스타일에서 가져온다. const computed = getComputedStyle(document.documentElement); for (const sheet of Array.from(document.styleSheets)) { let rules: CSSRuleList; try { rules = sheet.cssRules; } catch { // 교차 출처 스타일시트는 규칙을 읽을 수 없다 — 우리 토큰은 동일 출처이므로 건너뛴다. continue; } for (const rule of Array.from(rules)) { if (!(rule instanceof CSSStyleRule) || rule.selectorText !== ":root") { continue; } for (const property of Array.from(rule.style)) { if (!property.startsWith(PREFIX) || seen.has(property)) { continue; } seen.add(property); const rest = property.slice(PREFIX.length); const separator = rest.indexOf("-"); if (separator <= 0) { continue; } (registry[rest.slice(0, separator)] ??= []).push({ name: rest.slice(separator + 1), value: computed.getPropertyValue(property).trim() || rule.style.getPropertyValue(property).trim(), property, }); } } } return registry; }