@design-token-kit/core 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/lib/index.d.ts +132 -17
  2. package/lib/index.js +724 -624
  3. package/package.json +3 -2
package/lib/index.js CHANGED
@@ -1,11 +1,216 @@
1
- import { readFile, readdir, writeFile } from "node:fs/promises";
2
1
  import { existsSync } from "node:fs";
2
+ import { readFile, readdir, writeFile } from "node:fs/promises";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import path from "node:path";
5
5
  import { tmpdir } from "node:os";
6
+ import { parse, parseAllDocuments } from "yaml";
6
7
  import Ajv from "ajv";
7
8
  import addFormats from "ajv-formats";
8
9
  import { fileURLToPath } from "node:url";
10
+ //#region src/core/model/DtcgList.ts
11
+ /**
12
+ * An ordered collection of DTCG documents representing a base token set and its theme overrides.
13
+ *
14
+ * The base document defines all tokens. Each theme document overrides a subset of them.
15
+ * When validating a theme, references are resolved against the theme first, then the base.
16
+ *
17
+ * @see https://tr.designtokens.org/format/#file-format
18
+ */
19
+ var DtcgList = class {
20
+ base;
21
+ themes;
22
+ constructor(base, themes) {
23
+ this.base = base;
24
+ this.themes = themes ?? /* @__PURE__ */ new Map();
25
+ }
26
+ validate() {
27
+ const issues = this.base.validate();
28
+ for (const theme of this.themes.values()) issues.push(...theme.validate(this.base));
29
+ return issues;
30
+ }
31
+ };
32
+ //#endregion
33
+ //#region src/core/io/Format.ts
34
+ /**
35
+ * Token formats.
36
+ */
37
+ var Format = /* @__PURE__ */ function(Format) {
38
+ /**
39
+ * DTCG (Design Tokens Community Group) JSON token format.
40
+ *
41
+ * @see https://tr.designtokens.org/format/
42
+ */
43
+ Format["DTCG"] = "dtcg";
44
+ /**
45
+ * HRDT (Human-Readable Design Tokens) YAML token format.
46
+ */
47
+ Format["HRDT"] = "hrdt";
48
+ /**
49
+ * CSS custom properties.
50
+ */
51
+ Format["CSS"] = "css";
52
+ return Format;
53
+ }({});
54
+ //#endregion
55
+ //#region src/core/io/FormatDetector.ts
56
+ /**
57
+ * Detects the token format from raw content by inspecting the content
58
+ * structure rather than relying on file extensions.
59
+ *
60
+ * Supported formats: {@link Format.DTCG} (JSON), {@link Format.HRDT} (YAML),
61
+ * and {@link Format.CSS}.
62
+ */
63
+ var FormatDetector = class FormatDetector {
64
+ /**
65
+ * Detects the format of the given content.
66
+ *
67
+ * Detection rules:
68
+ * - If content starts with `{` and is valid JSON it is treated as DTCG.
69
+ * - If content contains CSS-specific patterns (`:root`, custom properties,
70
+ * `@layer`) it is treated as CSS.
71
+ * - Otherwise it is treated as HRDT.
72
+ */
73
+ static detect(content) {
74
+ const trimmed = content.trimStart();
75
+ if (trimmed.length === 0) return Format.HRDT;
76
+ if (FormatDetector.isDtcg(trimmed)) return Format.DTCG;
77
+ if (FormatDetector.isCss(trimmed)) return Format.CSS;
78
+ return Format.HRDT;
79
+ }
80
+ /**
81
+ * Returns `true` when the content looks like a DTCG JSON document.
82
+ *
83
+ * The check is structural: content must start with `{` and be parseable
84
+ * as JSON.
85
+ */
86
+ static isDtcg(content) {
87
+ if (!content.startsWith("{")) return false;
88
+ try {
89
+ JSON.parse(content);
90
+ return true;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+ /**
96
+ * Returns `true` when the content looks like CSS.
97
+ *
98
+ * Detects the presence of CSS custom properties (`--name:`),
99
+ * {@code :root} selector, or {@code @layer} at-rules.
100
+ */
101
+ static isCss(content) {
102
+ return /(^|\s)--[a-zA-Z0-9_-]+\s*:/.test(content) || /(^|\s):root\b/.test(content) || /(^|\s)@layer\b/.test(content);
103
+ }
104
+ };
105
+ //#endregion
106
+ //#region src/core/Stdin.ts
107
+ /**
108
+ * Singleton reader for {@code process.stdin}.
109
+ *
110
+ * The underlying stream can only be consumed once per process.
111
+ * After the first call to {@link get} the content is cached and
112
+ * subsequent calls return the cached buffer.
113
+ */
114
+ var Stdin = class {
115
+ #buffer;
116
+ /**
117
+ * Returns the cached stdin content, or reads the stream on the first
118
+ * call.
119
+ *
120
+ * @throws Error if stdin is a TTY (no data piped).
121
+ */
122
+ async get() {
123
+ if (this.#buffer === void 0) this.#buffer = await this.#read();
124
+ return this.#buffer;
125
+ }
126
+ /**
127
+ * Returns {@code true} when stdin is piped (not a TTY) and data is
128
+ * available to read.
129
+ */
130
+ hasData() {
131
+ return !process.stdin.isTTY;
132
+ }
133
+ async #read() {
134
+ if (!this.hasData()) throw new Error("Cannot read from stdin: no input. Pipe data via '< file' or provide file arguments.");
135
+ const chunks = [];
136
+ for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk);
137
+ if (chunks.length === 0) throw new Error("Cannot read from stdin: stream ended without data");
138
+ return Buffer.concat(chunks).toString("utf8");
139
+ }
140
+ };
141
+ var stdin = new Stdin();
142
+ //#endregion
143
+ //#region src/core/Source.ts
144
+ var SourceType = /* @__PURE__ */ function(SourceType) {
145
+ SourceType["URL"] = "url";
146
+ SourceType["FILE"] = "file";
147
+ SourceType["CONTENT"] = "content";
148
+ SourceType["STDIN"] = "stdin";
149
+ return SourceType;
150
+ }(SourceType || {});
151
+ /**
152
+ * Data source.
153
+ * Wraps URL, File, stdin ({@code "-"}) or raw content.
154
+ *
155
+ * Allows returning any of these data sources as a file.
156
+ * Used in tool implementations to simplify the interface.
157
+ */
158
+ var Source = class Source {
159
+ #type;
160
+ #input;
161
+ #content;
162
+ #format;
163
+ #filePath;
164
+ constructor(input) {
165
+ this.#input = input;
166
+ if (input === "-") this.#type = SourceType.STDIN;
167
+ else if (Source.#isUrl(input)) this.#type = SourceType.URL;
168
+ else if (existsSync(input)) this.#type = SourceType.FILE;
169
+ else this.#type = SourceType.CONTENT;
170
+ }
171
+ static #isUrl(value) {
172
+ try {
173
+ new URL(value);
174
+ return true;
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
179
+ getInput() {
180
+ return this.#input;
181
+ }
182
+ getType() {
183
+ return this.#type;
184
+ }
185
+ async getFormat() {
186
+ if (this.#format === void 0) this.#format = FormatDetector.detect(await this.getContent());
187
+ return this.#format;
188
+ }
189
+ async getContent() {
190
+ if (this.#content !== void 0) return this.#content;
191
+ if (this.#type === SourceType.STDIN) this.#content = await stdin.get();
192
+ else if (this.#type === SourceType.FILE) this.#content = await readFile(this.#input, "utf8");
193
+ else if (this.#type === SourceType.URL) {
194
+ const response = await fetch(this.#input);
195
+ if (!response.ok) throw new Error(`Unable to fetch "${this.#input}": HTTP ${response.status}`);
196
+ this.#content = await response.text();
197
+ } else if (this.#type === SourceType.CONTENT) this.#content = this.#input;
198
+ else throw new Error(`Unsupported source type "${this.#type}: ${this.#input}"`);
199
+ return this.#content;
200
+ }
201
+ async getFile() {
202
+ if (this.#filePath !== void 0) return this.#filePath;
203
+ if (this.#type === SourceType.FILE) this.#filePath = this.#input;
204
+ else this.#filePath = await Source.#writeTemp(await this.getContent());
205
+ return this.#filePath;
206
+ }
207
+ static async #writeTemp(content) {
208
+ const filePath = path.join(tmpdir(), `designtokens-${randomUUID()}.tmp`);
209
+ await writeFile(filePath, content, "utf8");
210
+ return filePath;
211
+ }
212
+ };
213
+ //#endregion
9
214
  //#region src/core/model/TokenGroup.ts
10
215
  /**
11
216
  * A named container for tokens and nested groups.
@@ -253,14 +458,28 @@ var TypographyValue = class {
253
458
  */
254
459
  var Dtcg = class {
255
460
  #root;
256
- constructor(root) {
461
+ /**
462
+ * The origin of this token document.
463
+ *
464
+ * Can be a file path ({@code "tokens.json"}), a URL, or
465
+ * {@code "-"} for stdin.
466
+ * Used in validation diagnostics as the {@code sourcePath}.
467
+ */
468
+ source;
469
+ constructor(root, source) {
257
470
  this.#root = root;
471
+ this.source = source;
258
472
  }
259
- /** Returns the top-level child token or group with the given name, or undefined. */
473
+ /**
474
+ * Returns the top-level child token or group with the given name,
475
+ * or undefined.
476
+ */
260
477
  get(name) {
261
478
  return this.#root.get(name);
262
479
  }
263
- /** Returns all top-level child names in insertion order. */
480
+ /**
481
+ * Returns all top-level child names in insertion order.
482
+ */
264
483
  keys() {
265
484
  return this.#root.keys();
266
485
  }
@@ -282,6 +501,10 @@ var Dtcg = class {
282
501
  const issues = [];
283
502
  const seen = /* @__PURE__ */ new Set();
284
503
  this.#validateGroup(this.#root, [], issues, seen, base);
504
+ for (const issue of issues) {
505
+ issue.name = "internal";
506
+ if (this.source !== void 0) issue.sourcePath = this.source;
507
+ }
285
508
  return issues;
286
509
  }
287
510
  #push(issues, seen, severity, tokenPath, message) {
@@ -291,7 +514,7 @@ var Dtcg = class {
291
514
  issues.push({
292
515
  tokenPath,
293
516
  severity,
294
- message
517
+ message: message.includes(tokenPath) ? message : `${tokenPath}: ${message}`
295
518
  });
296
519
  }
297
520
  #validateGroup(group, path, issues, seen, base) {
@@ -672,408 +895,75 @@ var DurationValue = class {
672
895
  }
673
896
  };
674
897
  //#endregion
675
- //#region src/core/io/HrdtTokenReader.ts
676
- var DIMENSION_RE = /^(-?\d+(?:\.\d+)?)(px|rem)$/;
677
- var DURATION_RE = /^(-?\d+(?:\.\d+)?)(ms|s)$/;
678
- var HEX_RE = /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
679
- var REFERENCE_RE = /^\{[^{}]+\}$/;
680
- var TOKEN_TYPES = new Set([
681
- "color",
682
- "dimension",
683
- "fontFamily",
684
- "fontWeight",
685
- "duration",
686
- "cubicBezier",
687
- "number",
688
- "strokeStyle",
689
- "border",
690
- "transition",
691
- "shadow",
692
- "gradient",
693
- "typography"
898
+ //#region src/core/io/DtcgJsonReader.ts
899
+ var GROUP_KEYS = new Set([
900
+ "$type",
901
+ "$description",
902
+ "$extensions",
903
+ "$deprecated",
904
+ "$extends",
905
+ "$root",
906
+ "$schema"
694
907
  ]);
695
908
  /**
696
- * Reads an HRDT token file and parses it directly into a {@link Dtcg} model.
909
+ * Parses a DTCG 2025.10 JSON document into a {@link Dtcg}.
697
910
  *
698
- * The HRDT format uses YAML syntax and group path to infer token types
699
- * (e.g. `primitive.color.*` -> color tokens). Non-primitive groups
700
- * contain alias references to primitive tokens.
911
+ * A child object is treated as a token when it contains `$value` or `$ref`,
912
+ * and as a group otherwise.
913
+ *
914
+ * @see https://tr.designtokens.org/format/
701
915
  */
702
- var HrdtTokenReader = class {
703
- parseRaw(hrdtContent) {
704
- return new SimpleHrdtParser(hrdtContent).parse();
705
- }
706
- parse(hrdtContent) {
707
- return new Dtcg(this.#parseRoot(new SimpleHrdtParser(hrdtContent).parse()));
708
- }
709
- async parseFile(filePath) {
710
- return this.parse(await readFile(filePath, "utf8"));
916
+ var DtcgJsonReader = class {
917
+ parse(content, source) {
918
+ const raw = JSON.parse(content);
919
+ return new Dtcg(this.#parseGroup(raw, void 0), source);
711
920
  }
712
- #parseRoot(raw) {
921
+ #parseGroup(raw, inheritedType) {
922
+ const type = this.#resolveType(raw["$type"], inheritedType);
923
+ const description = typeof raw["$description"] === "string" ? raw["$description"] : void 0;
924
+ const deprecated = this.#parseDeprecated(raw["$deprecated"]);
925
+ const extensions = this.#parseExtensions(raw["$extensions"]);
926
+ const extendsRef = this.#parseExtendsRef(raw["$extends"]);
927
+ const root = raw["$root"] != null ? this.#parseToken(raw["$root"], type) : void 0;
713
928
  const children = /* @__PURE__ */ new Map();
714
929
  for (const [key, value] of Object.entries(raw)) {
715
- if (key === "$schema") continue;
716
- if (!this.#isObject(value)) continue;
717
- if (key === "primitive") children.set(key, this.#parsePrimitiveRoot(value));
718
- else children.set(key, this.#parseReferenceGroup(value));
719
- }
720
- return new TokenGroup({ children });
721
- }
722
- #parsePrimitiveRoot(raw) {
723
- const children = /* @__PURE__ */ new Map();
724
- for (const [typeName, tokens] of Object.entries(raw)) {
725
- if (!TOKEN_TYPES.has(typeName)) throw new HrdtTokenReaderError(`Unknown primitive token type: "${typeName}"`);
726
- const tokenType = typeName;
727
- children.set(typeName, this.#parsePrimitiveTypeGroup(tokens, tokenType));
930
+ if (GROUP_KEYS.has(key)) continue;
931
+ if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
932
+ const child = value;
933
+ if (this.#isToken(child)) children.set(key, this.#parseToken(child, type));
934
+ else children.set(key, this.#parseGroup(child, type));
728
935
  }
729
- return new TokenGroup({ children });
730
- }
731
- #parsePrimitiveTypeGroup(raw, tokenType) {
732
- const children = /* @__PURE__ */ new Map();
733
- for (const [name, value] of Object.entries(raw)) children.set(name, this.#parsePrimitiveToken(value, tokenType));
734
936
  return new TokenGroup({
735
- type: tokenType,
937
+ type,
938
+ description,
939
+ deprecated,
940
+ extensions,
941
+ extends: extendsRef,
942
+ root,
736
943
  children
737
944
  });
738
945
  }
739
- #parsePrimitiveToken(value, tokenType) {
740
- if (typeof value === "string" && REFERENCE_RE.test(value)) return new AliasToken(new TokenReference(value.slice(1, -1)));
741
- switch (tokenType) {
742
- case "color": return new ColorToken(this.#parseColor(value));
743
- case "dimension": return new DimensionToken(this.#parseDimension(value));
744
- case "fontFamily": return new FontFamilyToken(this.#parseFontFamily(value));
745
- case "fontWeight": return new FontWeightToken(this.#parseFontWeight(value));
746
- case "number": return new NumberToken(this.#parseNumber(value));
747
- case "duration": return new DurationToken(this.#parseDuration(value));
748
- case "cubicBezier": return new CubicBezierToken(this.#parseCubicBezier(value));
749
- case "strokeStyle": return new StrokeStyleToken(this.#parseStrokeStyle(value));
750
- case "border": return new BorderToken(this.#parseBorder(value));
751
- case "transition": return new TransitionToken(this.#parseTransition(value));
752
- case "shadow": return new ShadowToken(this.#parseShadow(value));
753
- case "gradient": return new GradientToken(this.#parseGradient(value));
754
- case "typography": return new TypographyToken(this.#parseTypography(value));
755
- }
946
+ #isToken(raw) {
947
+ return "$value" in raw || "$ref" in raw;
756
948
  }
757
- #parseReferenceGroup(raw) {
758
- const children = /* @__PURE__ */ new Map();
759
- for (const [key, value] of Object.entries(raw)) if (this.#isLeaf(value)) children.set(key, this.#parseReferenceToken(value));
760
- else children.set(key, this.#parseReferenceGroup(value));
761
- return new TokenGroup({ children });
762
- }
763
- #isLeaf(value) {
764
- return !this.#isObject(value) || Array.isArray(value);
765
- }
766
- #parseReferenceToken(value) {
767
- if (typeof value === "string" && REFERENCE_RE.test(value)) return new AliasToken(new TokenReference(value.slice(1, -1)));
768
- throw new HrdtTokenReaderError(`Expected a reference in non-primitive group, got: ${JSON.stringify(value)}`);
769
- }
770
- #parseColor(value) {
771
- if (typeof value !== "string" || !HEX_RE.test(value)) throw new HrdtTokenReaderError(`Expected hex color, got: ${JSON.stringify(value)}`);
772
- const normalized = value.toLowerCase();
773
- const hex = normalized.slice(0, 7);
774
- const r = parseInt(normalized.slice(1, 3), 16);
775
- const g = parseInt(normalized.slice(3, 5), 16);
776
- const b = parseInt(normalized.slice(5, 7), 16);
777
- const alpha = normalized.length === 9 ? this.#round(parseInt(normalized.slice(7, 9), 16) / 255) : 1;
778
- return new ColorValue("srgb", [
779
- this.#round(r / 255),
780
- this.#round(g / 255),
781
- this.#round(b / 255)
782
- ], alpha, hex);
783
- }
784
- #parseDimension(value) {
785
- if (typeof value !== "string") throw new HrdtTokenReaderError(`Expected dimension string, got: ${JSON.stringify(value)}`);
786
- const match = value.match(DIMENSION_RE);
787
- if (!match) throw new HrdtTokenReaderError(`Expected dimension with px/rem unit, got: "${value}"`);
788
- return new DimensionValue(Number(match[1]), match[2]);
789
- }
790
- #parseFontFamily(value) {
791
- if (typeof value === "string") return value;
792
- if (Array.isArray(value) && value.every((v) => typeof v === "string")) return value;
793
- throw new HrdtTokenReaderError(`Expected fontFamily string or array, got: ${JSON.stringify(value)}`);
794
- }
795
- #parseFontWeight(value) {
796
- if (typeof value === "string" || typeof value === "number") return value;
797
- throw new HrdtTokenReaderError(`Expected fontWeight string or number, got: ${JSON.stringify(value)}`);
798
- }
799
- #parseNumber(value) {
800
- if (typeof value !== "number") throw new HrdtTokenReaderError(`Expected number, got: ${JSON.stringify(value)}`);
801
- return value;
802
- }
803
- #parseDuration(value) {
804
- if (typeof value !== "string") throw new HrdtTokenReaderError(`Expected duration string, got: ${JSON.stringify(value)}`);
805
- const match = value.match(DURATION_RE);
806
- if (!match) throw new HrdtTokenReaderError(`Expected duration with ms/s unit, got: "${value}"`);
807
- return new DurationValue(Number(match[1]), match[2]);
808
- }
809
- #parseCubicBezier(value) {
810
- if (!Array.isArray(value) || value.length !== 4 || !value.every((v) => typeof v === "number")) throw new HrdtTokenReaderError(`Expected cubicBezier [n, n, n, n], got: ${JSON.stringify(value)}`);
811
- return new CubicBezierValue(value[0], value[1], value[2], value[3]);
812
- }
813
- #parseStrokeStyle(value) {
814
- if (typeof value === "string") return value;
815
- if (this.#isObject(value)) {
816
- const obj = value;
817
- return new StrokeStyleObject(obj["dashArray"].map((d) => this.#parseDimension(d)), obj["lineCap"]);
818
- }
819
- throw new HrdtTokenReaderError(`Expected strokeStyle, got: ${JSON.stringify(value)}`);
820
- }
821
- #parseBorder(value) {
822
- if (!this.#isObject(value)) throw new HrdtTokenReaderError(`Expected border object, got: ${JSON.stringify(value)}`);
823
- const obj = value;
824
- return new BorderValue(this.#parseColor(obj["color"]), this.#parseDimension(obj["width"]), this.#parseStrokeStyle(obj["style"]));
825
- }
826
- #parseTransition(value) {
827
- if (!this.#isObject(value)) throw new HrdtTokenReaderError(`Expected transition object, got: ${JSON.stringify(value)}`);
828
- const obj = value;
829
- return new TransitionValue(this.#parseDuration(obj["duration"]), this.#parseDuration(obj["delay"]), this.#parseCubicBezier(obj["timingFunction"]));
830
- }
831
- #parseShadow(value) {
832
- if (Array.isArray(value)) return value.map((item) => this.#parseShadowLayer(item));
833
- return this.#parseShadowLayer(value);
834
- }
835
- #parseShadowLayer(value) {
836
- if (!this.#isObject(value)) throw new HrdtTokenReaderError(`Expected shadow layer object, got: ${JSON.stringify(value)}`);
837
- const obj = value;
838
- return new ShadowLayer(this.#parseColor(obj["color"]), this.#parseDimension(obj["offsetX"]), this.#parseDimension(obj["offsetY"]), this.#parseDimension(obj["blur"]), this.#parseDimension(obj["spread"]));
839
- }
840
- #parseGradient(value) {
841
- if (!Array.isArray(value)) throw new HrdtTokenReaderError(`Expected gradient stops array, got: ${JSON.stringify(value)}`);
842
- return value.map((item) => {
843
- if (!this.#isObject(item)) throw new HrdtTokenReaderError(`Expected gradient stop object, got: ${JSON.stringify(item)}`);
844
- const obj = item;
845
- return new GradientStop(this.#parseColor(obj["color"]), this.#parseNumber(obj["position"]));
846
- });
847
- }
848
- #parseTypography(value) {
849
- if (!this.#isObject(value)) throw new HrdtTokenReaderError(`Expected typography object, got: ${JSON.stringify(value)}`);
850
- const obj = value;
851
- return new TypographyValue(this.#parseFontFamily(obj["fontFamily"]), this.#parseDimension(obj["fontSize"]), this.#parseFontWeight(obj["fontWeight"]), this.#parseDimension(obj["letterSpacing"]), this.#parseNumber(obj["lineHeight"]));
852
- }
853
- #isObject(value) {
854
- return typeof value === "object" && value !== null && !Array.isArray(value);
855
- }
856
- #round(value) {
857
- return Number(value.toFixed(3));
858
- }
859
- };
860
- /**
861
- * Thrown when the YAML content does not conform to the HRDT token format.
862
- */
863
- var HrdtTokenReaderError = class extends Error {
864
- constructor(message) {
865
- super(message);
866
- this.name = "HrdtTokenReaderError";
867
- }
868
- };
869
- var SimpleHrdtParser = class {
870
- #lines;
871
- #index = 0;
872
- constructor(content) {
873
- this.#lines = content.replace(/^/, "").split(/\r?\n/g).map((line) => line.replace(/\s+$/u, "")).filter((line) => line.trim().length > 0 && !line.trimStart().startsWith("#")).map((line) => ({
874
- indent: line.length - line.trimStart().length,
875
- text: line.trimStart()
876
- }));
877
- }
878
- parse() {
879
- const result = this.#parseBlock(0);
880
- if (typeof result !== "object" || result === null || Array.isArray(result)) throw new HrdtTokenReaderError("YAML root must be an object.");
881
- return result;
882
- }
883
- #parseBlock(indent) {
884
- const line = this.#current();
885
- if (!line || line.indent < indent) return {};
886
- return line.text.startsWith("- ") ? this.#parseSequence(indent) : this.#parseMapping(indent);
887
- }
888
- #parseMapping(indent) {
889
- const output = {};
890
- while (this.#index < this.#lines.length) {
891
- const line = this.#current();
892
- if (line.indent < indent) break;
893
- if (line.indent > indent) throw new HrdtTokenReaderError(`Unexpected indentation near "${line.text}".`);
894
- if (line.text.startsWith("- ")) break;
895
- const { key, value } = this.#parseKeyValue(line.text);
896
- this.#index += 1;
897
- output[key] = value === void 0 ? this.#parseBlock(this.#nextIndent(indent)) : this.#parseScalar(value);
898
- }
899
- return output;
900
- }
901
- #parseSequence(indent) {
902
- const output = [];
903
- while (this.#index < this.#lines.length) {
904
- const line = this.#current();
905
- if (line.indent < indent) break;
906
- if (line.indent !== indent || !line.text.startsWith("- ")) break;
907
- const itemText = line.text.slice(2).trim();
908
- this.#index += 1;
909
- if (itemText.includes(":")) {
910
- const { key, value } = this.#parseKeyValue(itemText);
911
- const item = {};
912
- item[key] = value === void 0 ? this.#parseBlock(this.#nextIndent(indent)) : this.#parseScalar(value);
913
- if (this.#current()?.indent === indent + 2 && !this.#current()?.text.startsWith("- ")) Object.assign(item, this.#parseMapping(indent + 2));
914
- output.push(item);
915
- } else output.push(this.#parseScalar(itemText));
916
- }
917
- return output;
918
- }
919
- #parseKeyValue(text) {
920
- const delimiter = text.indexOf(":");
921
- if (delimiter < 0) throw new HrdtTokenReaderError(`Expected key-value pair, got "${text}".`);
922
- const rawKey = text.slice(0, delimiter).trim();
923
- if (!rawKey) throw new HrdtTokenReaderError(`Expected non-empty key in "${text}".`);
924
- const value = text.slice(delimiter + 1).trim();
925
- const key = rawKey.startsWith("\"") && rawKey.endsWith("\"") || rawKey.startsWith("'") && rawKey.endsWith("'") ? this.#unquote(rawKey) : rawKey;
926
- return value.length === 0 ? { key } : {
927
- key,
928
- value
929
- };
930
- }
931
- #parseScalar(value) {
932
- if (value.startsWith("[") && value.endsWith("]")) return this.#parseInlineArray(value);
933
- if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) return this.#unquote(value);
934
- if (value === "true") return true;
935
- if (value === "false") return false;
936
- if (value === "null") return null;
937
- if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
938
- return value;
939
- }
940
- #parseInlineArray(value) {
941
- const inner = value.slice(1, -1).trim();
942
- if (!inner) return [];
943
- return this.#splitInlineArray(inner).map((item) => this.#parseScalar(item.trim()));
944
- }
945
- #splitInlineArray(value) {
946
- const items = [];
947
- let current = "";
948
- let quote;
949
- for (let i = 0; i < value.length; i++) {
950
- const char = value[i];
951
- if (quote) {
952
- current += char;
953
- if (char === quote && value[i - 1] !== "\\") quote = void 0;
954
- continue;
955
- }
956
- if (char === "\"" || char === "'") {
957
- quote = char;
958
- current += char;
959
- continue;
960
- }
961
- if (char === ",") {
962
- items.push(current);
963
- current = "";
964
- continue;
965
- }
966
- current += char;
967
- }
968
- items.push(current);
969
- return items;
970
- }
971
- #unquote(value) {
972
- if (value.startsWith("\"")) return JSON.parse(value);
973
- return value.slice(1, -1).replace(/''/g, "'");
974
- }
975
- #current() {
976
- return this.#lines[this.#index];
977
- }
978
- #nextIndent(currentIndent) {
979
- const nextLine = this.#current();
980
- if (!nextLine || nextLine.indent <= currentIndent) return currentIndent + 2;
981
- return nextLine.indent;
982
- }
983
- };
984
- //#endregion
985
- //#region src/core/model/DtcgList.ts
986
- /**
987
- * An ordered collection of DTCG documents representing a base token set and its theme overrides.
988
- *
989
- * The base document defines all tokens. Each theme document overrides a subset of them.
990
- * When validating a theme, references are resolved against the theme first, then the base.
991
- *
992
- * @see https://tr.designtokens.org/format/#file-format
993
- */
994
- var DtcgList = class {
995
- base;
996
- themes;
997
- constructor(base, themes) {
998
- this.base = base;
999
- this.themes = themes ?? /* @__PURE__ */ new Map();
1000
- }
1001
- validate() {
1002
- const issues = this.base.validate();
1003
- for (const theme of this.themes.values()) issues.push(...theme.validate(this.base));
1004
- return issues;
1005
- }
1006
- };
1007
- //#endregion
1008
- //#region src/core/io/DtcgJsonReader.ts
1009
- var GROUP_KEYS = new Set([
1010
- "$type",
1011
- "$description",
1012
- "$extensions",
1013
- "$deprecated",
1014
- "$extends",
1015
- "$root",
1016
- "$schema"
1017
- ]);
1018
- /**
1019
- * Parses a DTCG 2025.10 JSON document into a {@link Dtcg}.
1020
- *
1021
- * A child object is treated as a token when it contains `$value` or `$ref`,
1022
- * and as a group otherwise.
1023
- *
1024
- * @see https://tr.designtokens.org/format/
1025
- */
1026
- var DtcgJsonReader = class {
1027
- parse(content) {
1028
- const raw = JSON.parse(content);
1029
- return new Dtcg(this.#parseGroup(raw, void 0));
1030
- }
1031
- #parseGroup(raw, inheritedType) {
1032
- const type = this.#resolveType(raw["$type"], inheritedType);
1033
- const description = typeof raw["$description"] === "string" ? raw["$description"] : void 0;
1034
- const deprecated = this.#parseDeprecated(raw["$deprecated"]);
1035
- const extensions = this.#parseExtensions(raw["$extensions"]);
1036
- const extendsRef = this.#parseExtendsRef(raw["$extends"]);
1037
- const root = raw["$root"] != null ? this.#parseToken(raw["$root"], type) : void 0;
1038
- const children = /* @__PURE__ */ new Map();
1039
- for (const [key, value] of Object.entries(raw)) {
1040
- if (GROUP_KEYS.has(key)) continue;
1041
- if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
1042
- const child = value;
1043
- if (this.#isToken(child)) children.set(key, this.#parseToken(child, type));
1044
- else children.set(key, this.#parseGroup(child, type));
1045
- }
1046
- return new TokenGroup({
1047
- type,
1048
- description,
1049
- deprecated,
1050
- extensions,
1051
- extends: extendsRef,
1052
- root,
1053
- children
1054
- });
1055
- }
1056
- #isToken(raw) {
1057
- return "$value" in raw || "$ref" in raw;
1058
- }
1059
- #parseToken(raw, inheritedType) {
1060
- const type = this.#resolveType(raw["$type"], inheritedType);
1061
- const description = typeof raw["$description"] === "string" ? raw["$description"] : void 0;
1062
- const deprecated = this.#parseDeprecated(raw["$deprecated"]);
1063
- const extensions = this.#parseExtensions(raw["$extensions"]);
1064
- if ("$ref" in raw) {
1065
- const ref = raw["$ref"];
1066
- if (typeof ref !== "string") throw new DtcgJsonReaderError("$ref must be a string");
1067
- return this.#makeToken(type, new TokenReference(ref), description, deprecated, extensions);
1068
- }
1069
- const rawValue = raw["$value"];
1070
- if (typeof rawValue === "string" && /^\{[^{}]+\}$/.test(rawValue)) {
1071
- const ref = new TokenReference(rawValue.slice(1, -1));
1072
- return this.#makeToken(type, ref, description, deprecated, extensions);
1073
- }
1074
- if (type == null) throw new DtcgJsonReaderError(`Token has no $type and no inherited type: ${JSON.stringify(raw)}`);
1075
- const value = this.#parseValue(type, rawValue);
1076
- return this.#makeToken(type, value, description, deprecated, extensions);
949
+ #parseToken(raw, inheritedType) {
950
+ const type = this.#resolveType(raw["$type"], inheritedType);
951
+ const description = typeof raw["$description"] === "string" ? raw["$description"] : void 0;
952
+ const deprecated = this.#parseDeprecated(raw["$deprecated"]);
953
+ const extensions = this.#parseExtensions(raw["$extensions"]);
954
+ if ("$ref" in raw) {
955
+ const ref = raw["$ref"];
956
+ if (typeof ref !== "string") throw new DtcgJsonReaderError("$ref must be a string");
957
+ return this.#makeToken(type, new TokenReference(ref), description, deprecated, extensions);
958
+ }
959
+ const rawValue = raw["$value"];
960
+ if (typeof rawValue === "string" && /^\{[^{}]+\}$/.test(rawValue)) {
961
+ const ref = new TokenReference(rawValue.slice(1, -1));
962
+ return this.#makeToken(type, ref, description, deprecated, extensions);
963
+ }
964
+ if (type == null) throw new DtcgJsonReaderError(`Token has no $type and no inherited type: ${JSON.stringify(raw)}`);
965
+ const value = this.#parseValue(type, rawValue);
966
+ return this.#makeToken(type, value, description, deprecated, extensions);
1077
967
  }
1078
968
  #makeToken(type, value, description, deprecated, extensions) {
1079
969
  switch (type) {
@@ -1233,92 +1123,445 @@ var DtcgJsonReader = class {
1233
1123
  #parseExtensions(raw) {
1234
1124
  if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) return raw;
1235
1125
  }
1236
- #parseExtendsRef(raw) {
1237
- if (typeof raw !== "string") return void 0;
1238
- if (/^\{[^{}]+\}$/.test(raw)) return new TokenReference(raw.slice(1, -1));
1239
- return raw;
1126
+ #parseExtendsRef(raw) {
1127
+ if (typeof raw !== "string") return void 0;
1128
+ if (/^\{[^{}]+\}$/.test(raw)) return new TokenReference(raw.slice(1, -1));
1129
+ return raw;
1130
+ }
1131
+ };
1132
+ /**
1133
+ * Thrown when the input JSON does not conform to the expected DTCG structure.
1134
+ */
1135
+ var DtcgJsonReaderError = class extends Error {
1136
+ constructor(message) {
1137
+ super(message);
1138
+ this.name = "DtcgJsonReaderError";
1139
+ }
1140
+ };
1141
+ //#endregion
1142
+ //#region src/core/io/HrdtTokenReader.ts
1143
+ var DIMENSION_RE = /^(-?\d+(?:\.\d+)?)(px|rem)$/;
1144
+ var DURATION_RE = /^(-?\d+(?:\.\d+)?)(ms|s)$/;
1145
+ var HEX_RE = /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
1146
+ var REFERENCE_RE = /^\{[^{}]+\}$/;
1147
+ var TOKEN_TYPES = new Set([
1148
+ "color",
1149
+ "dimension",
1150
+ "fontFamily",
1151
+ "fontWeight",
1152
+ "duration",
1153
+ "cubicBezier",
1154
+ "number",
1155
+ "strokeStyle",
1156
+ "border",
1157
+ "transition",
1158
+ "shadow",
1159
+ "gradient",
1160
+ "typography"
1161
+ ]);
1162
+ /**
1163
+ * Reads an HRDT token file and parses it directly into a {@link Dtcg} model.
1164
+ *
1165
+ * The HRDT format uses YAML syntax and group path to infer token types
1166
+ * (e.g. `primitive.color.*` -> color tokens). Non-primitive groups
1167
+ * contain alias references to primitive tokens.
1168
+ */
1169
+ var HrdtTokenReader = class {
1170
+ parseRaw(hrdtContent) {
1171
+ return parse(hrdtContent);
1172
+ }
1173
+ parse(hrdtContent, source) {
1174
+ const raw = parse(hrdtContent);
1175
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new HrdtTokenReaderError("YAML root must be an object.");
1176
+ return new Dtcg(this.#parseRoot(raw), source);
1177
+ }
1178
+ /**
1179
+ * Parses a YAML string that may contain multiple documents separated by
1180
+ * {@code ---} and returns a {@link Dtcg} for each document.
1181
+ *
1182
+ * If the content contains a single document the result is a single-element
1183
+ * array.
1184
+ */
1185
+ parseAll(hrdtContent, source) {
1186
+ const documents = parseAllDocuments(hrdtContent);
1187
+ if ("length" in documents === false) throw new HrdtTokenReaderError("Failed to parse YAML document.");
1188
+ return documents.map((doc) => {
1189
+ const raw = doc.toJS();
1190
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new HrdtTokenReaderError("YAML document root must be an object.");
1191
+ return new Dtcg(this.#parseRoot(raw), source);
1192
+ });
1193
+ }
1194
+ async parseFile(filePath) {
1195
+ return this.parse(await readFile(filePath, "utf8"));
1196
+ }
1197
+ #parseRoot(raw) {
1198
+ const children = /* @__PURE__ */ new Map();
1199
+ for (const [key, value] of Object.entries(raw)) {
1200
+ if (key === "$schema") continue;
1201
+ if (!this.#isObject(value)) continue;
1202
+ if (key === "primitive") children.set(key, this.#parsePrimitiveRoot(value));
1203
+ else children.set(key, this.#parseReferenceGroup(value));
1204
+ }
1205
+ return new TokenGroup({ children });
1206
+ }
1207
+ #parsePrimitiveRoot(raw) {
1208
+ const children = /* @__PURE__ */ new Map();
1209
+ for (const [typeName, tokens] of Object.entries(raw)) {
1210
+ if (!TOKEN_TYPES.has(typeName)) throw new HrdtTokenReaderError(`Unknown primitive token type: "${typeName}"`);
1211
+ const tokenType = typeName;
1212
+ children.set(typeName, this.#parsePrimitiveTypeGroup(tokens, tokenType));
1213
+ }
1214
+ return new TokenGroup({ children });
1215
+ }
1216
+ #parsePrimitiveTypeGroup(raw, tokenType) {
1217
+ const children = /* @__PURE__ */ new Map();
1218
+ for (const [name, value] of Object.entries(raw)) children.set(name, this.#parsePrimitiveToken(value, tokenType));
1219
+ return new TokenGroup({
1220
+ type: tokenType,
1221
+ children
1222
+ });
1223
+ }
1224
+ #parsePrimitiveToken(value, tokenType) {
1225
+ if (typeof value === "string" && REFERENCE_RE.test(value)) return new AliasToken(new TokenReference(value.slice(1, -1)));
1226
+ switch (tokenType) {
1227
+ case "color": return new ColorToken(this.#parseColor(value));
1228
+ case "dimension": return new DimensionToken(this.#parseDimension(value));
1229
+ case "fontFamily": return new FontFamilyToken(this.#parseFontFamily(value));
1230
+ case "fontWeight": return new FontWeightToken(this.#parseFontWeight(value));
1231
+ case "number": return new NumberToken(this.#parseNumber(value));
1232
+ case "duration": return new DurationToken(this.#parseDuration(value));
1233
+ case "cubicBezier": return new CubicBezierToken(this.#parseCubicBezier(value));
1234
+ case "strokeStyle": return new StrokeStyleToken(this.#parseStrokeStyle(value));
1235
+ case "border": return new BorderToken(this.#parseBorder(value));
1236
+ case "transition": return new TransitionToken(this.#parseTransition(value));
1237
+ case "shadow": return new ShadowToken(this.#parseShadow(value));
1238
+ case "gradient": return new GradientToken(this.#parseGradient(value));
1239
+ case "typography": return new TypographyToken(this.#parseTypography(value));
1240
+ }
1241
+ }
1242
+ #parseReferenceGroup(raw) {
1243
+ const children = /* @__PURE__ */ new Map();
1244
+ for (const [key, value] of Object.entries(raw)) if (this.#isLeaf(value)) children.set(key, this.#parseReferenceToken(value));
1245
+ else children.set(key, this.#parseReferenceGroup(value));
1246
+ return new TokenGroup({ children });
1247
+ }
1248
+ #isLeaf(value) {
1249
+ return !this.#isObject(value) || Array.isArray(value);
1250
+ }
1251
+ #parseReferenceToken(value) {
1252
+ if (typeof value === "string" && REFERENCE_RE.test(value)) return new AliasToken(new TokenReference(value.slice(1, -1)));
1253
+ throw new HrdtTokenReaderError(`Expected a reference in non-primitive group, got: ${JSON.stringify(value)}`);
1254
+ }
1255
+ #parseColor(value) {
1256
+ if (typeof value !== "string" || !HEX_RE.test(value)) throw new HrdtTokenReaderError(`Expected hex color, got: ${JSON.stringify(value)}`);
1257
+ const normalized = value.toLowerCase();
1258
+ const hex = normalized.slice(0, 7);
1259
+ const r = parseInt(normalized.slice(1, 3), 16);
1260
+ const g = parseInt(normalized.slice(3, 5), 16);
1261
+ const b = parseInt(normalized.slice(5, 7), 16);
1262
+ const alpha = normalized.length === 9 ? this.#round(parseInt(normalized.slice(7, 9), 16) / 255) : 1;
1263
+ return new ColorValue("srgb", [
1264
+ this.#round(r / 255),
1265
+ this.#round(g / 255),
1266
+ this.#round(b / 255)
1267
+ ], alpha, hex);
1268
+ }
1269
+ #parseDimension(value) {
1270
+ if (typeof value !== "string") throw new HrdtTokenReaderError(`Expected dimension string, got: ${JSON.stringify(value)}`);
1271
+ const match = value.match(DIMENSION_RE);
1272
+ if (!match) throw new HrdtTokenReaderError(`Expected dimension with px/rem unit, got: "${value}"`);
1273
+ return new DimensionValue(Number(match[1]), match[2]);
1274
+ }
1275
+ #parseFontFamily(value) {
1276
+ if (typeof value === "string") return value;
1277
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) return value;
1278
+ throw new HrdtTokenReaderError(`Expected fontFamily string or array, got: ${JSON.stringify(value)}`);
1279
+ }
1280
+ #parseFontWeight(value) {
1281
+ if (typeof value === "string" || typeof value === "number") return value;
1282
+ throw new HrdtTokenReaderError(`Expected fontWeight string or number, got: ${JSON.stringify(value)}`);
1283
+ }
1284
+ #parseNumber(value) {
1285
+ if (typeof value !== "number") throw new HrdtTokenReaderError(`Expected number, got: ${JSON.stringify(value)}`);
1286
+ return value;
1287
+ }
1288
+ #parseDuration(value) {
1289
+ if (typeof value !== "string") throw new HrdtTokenReaderError(`Expected duration string, got: ${JSON.stringify(value)}`);
1290
+ const match = value.match(DURATION_RE);
1291
+ if (!match) throw new HrdtTokenReaderError(`Expected duration with ms/s unit, got: "${value}"`);
1292
+ return new DurationValue(Number(match[1]), match[2]);
1293
+ }
1294
+ #parseCubicBezier(value) {
1295
+ if (!Array.isArray(value) || value.length !== 4 || !value.every((v) => typeof v === "number")) throw new HrdtTokenReaderError(`Expected cubicBezier [n, n, n, n], got: ${JSON.stringify(value)}`);
1296
+ return new CubicBezierValue(value[0], value[1], value[2], value[3]);
1297
+ }
1298
+ #parseStrokeStyle(value) {
1299
+ if (typeof value === "string") return value;
1300
+ if (this.#isObject(value)) {
1301
+ const obj = value;
1302
+ return new StrokeStyleObject(obj["dashArray"].map((d) => this.#parseDimension(d)), obj["lineCap"]);
1303
+ }
1304
+ throw new HrdtTokenReaderError(`Expected strokeStyle, got: ${JSON.stringify(value)}`);
1305
+ }
1306
+ #parseBorder(value) {
1307
+ if (!this.#isObject(value)) throw new HrdtTokenReaderError(`Expected border object, got: ${JSON.stringify(value)}`);
1308
+ const obj = value;
1309
+ return new BorderValue(this.#parseColor(obj["color"]), this.#parseDimension(obj["width"]), this.#parseStrokeStyle(obj["style"]));
1310
+ }
1311
+ #parseTransition(value) {
1312
+ if (!this.#isObject(value)) throw new HrdtTokenReaderError(`Expected transition object, got: ${JSON.stringify(value)}`);
1313
+ const obj = value;
1314
+ return new TransitionValue(this.#parseDuration(obj["duration"]), this.#parseDuration(obj["delay"]), this.#parseCubicBezier(obj["timingFunction"]));
1315
+ }
1316
+ #parseShadow(value) {
1317
+ if (Array.isArray(value)) return value.map((item) => this.#parseShadowLayer(item));
1318
+ return this.#parseShadowLayer(value);
1319
+ }
1320
+ #parseShadowLayer(value) {
1321
+ if (!this.#isObject(value)) throw new HrdtTokenReaderError(`Expected shadow layer object, got: ${JSON.stringify(value)}`);
1322
+ const obj = value;
1323
+ return new ShadowLayer(this.#parseColor(obj["color"]), this.#parseDimension(obj["offsetX"]), this.#parseDimension(obj["offsetY"]), this.#parseDimension(obj["blur"]), this.#parseDimension(obj["spread"]));
1324
+ }
1325
+ #parseGradient(value) {
1326
+ if (!Array.isArray(value)) throw new HrdtTokenReaderError(`Expected gradient stops array, got: ${JSON.stringify(value)}`);
1327
+ return value.map((item) => {
1328
+ if (!this.#isObject(item)) throw new HrdtTokenReaderError(`Expected gradient stop object, got: ${JSON.stringify(item)}`);
1329
+ const obj = item;
1330
+ return new GradientStop(this.#parseColor(obj["color"]), this.#parseNumber(obj["position"]));
1331
+ });
1332
+ }
1333
+ #parseTypography(value) {
1334
+ if (!this.#isObject(value)) throw new HrdtTokenReaderError(`Expected typography object, got: ${JSON.stringify(value)}`);
1335
+ const obj = value;
1336
+ return new TypographyValue(this.#parseFontFamily(obj["fontFamily"]), this.#parseDimension(obj["fontSize"]), this.#parseFontWeight(obj["fontWeight"]), this.#parseDimension(obj["letterSpacing"]), this.#parseNumber(obj["lineHeight"]));
1337
+ }
1338
+ #isObject(value) {
1339
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1340
+ }
1341
+ #round(value) {
1342
+ return Number(value.toFixed(3));
1343
+ }
1344
+ };
1345
+ /**
1346
+ * Thrown when the YAML content does not conform to the HRDT token format.
1347
+ */
1348
+ var HrdtTokenReaderError = class extends Error {
1349
+ constructor(message) {
1350
+ super(message);
1351
+ this.name = "HrdtTokenReaderError";
1352
+ }
1353
+ };
1354
+ //#endregion
1355
+ //#region src/core/validation/dtcg/DtcgSchemaValidator.ts
1356
+ var FORMAT_SCHEMA_ID = "https://www.designtokens.org/schemas/2025.10/format.json";
1357
+ /**
1358
+ * Validates DTCG JSON sources against the official DTCG JSON Schema.
1359
+ * Accepts DTCG JSON sources only.
1360
+ */
1361
+ var DtcgSchemaValidator = class {
1362
+ name = "dtcg";
1363
+ async validate(sources) {
1364
+ const validator = (await this.#createAjv()).getSchema(FORMAT_SCHEMA_ID);
1365
+ if (!validator) throw new Error(`AJV schema "${FORMAT_SCHEMA_ID}" was not loaded.`);
1366
+ const issues = [];
1367
+ for (const source of sources) {
1368
+ const content = await new Source(source).getContent();
1369
+ if (validator(JSON.parse(content))) continue;
1370
+ const errors = validator.errors ?? [];
1371
+ for (const error of errors) issues.push(this.#toValidationIssue(source, error));
1372
+ }
1373
+ return issues;
1374
+ }
1375
+ async #createAjv() {
1376
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
1377
+ const schemaDir = path.resolve(moduleDir, "schemas/2025.10");
1378
+ const ajv = new Ajv({
1379
+ allErrors: true,
1380
+ strict: false
1381
+ });
1382
+ addFormats(ajv);
1383
+ for (const schemaFilePath of await listJsonFiles(schemaDir)) {
1384
+ const rawSchema = await readFile(schemaFilePath, "utf8");
1385
+ const schema = JSON.parse(rawSchema);
1386
+ ajv.addSchema(schema, schema.$id ?? schemaFilePath);
1387
+ }
1388
+ return ajv;
1389
+ }
1390
+ #toValidationIssue(sourcePath, error) {
1391
+ const instancePath = error.instancePath || "/";
1392
+ const message = error.message ?? "Validation error.";
1393
+ return {
1394
+ name: this.name,
1395
+ sourcePath,
1396
+ severity: "error",
1397
+ message: `${instancePath}: ${message}`,
1398
+ raw: error
1399
+ };
1400
+ }
1401
+ };
1402
+ async function listJsonFiles(directoryPath) {
1403
+ const entries = await readdir(directoryPath, { withFileTypes: true });
1404
+ const files = [];
1405
+ for (const entry of entries) {
1406
+ const entryPath = path.join(directoryPath, entry.name);
1407
+ if (entry.isDirectory()) files.push(...await listJsonFiles(entryPath));
1408
+ else if (entry.isFile() && entry.name.toLowerCase().endsWith(".json")) files.push(entryPath);
1409
+ }
1410
+ return files;
1411
+ }
1412
+ //#endregion
1413
+ //#region src/core/validation/hrdt/HrdtTokenValidator.ts
1414
+ var SCHEMA_ID = "https://designtokens.local/schemas/hrdt-tokens.json";
1415
+ /**
1416
+ * Validator based on AJV and JSON Schema.
1417
+ * Accepts HRDT sources only.
1418
+ */
1419
+ var HrdtTokenValidator = class {
1420
+ name = "hrdt";
1421
+ async validate(sources) {
1422
+ const validator = (await this.#createAjv()).getSchema(SCHEMA_ID);
1423
+ if (!validator) throw new Error(`AJV schema "${SCHEMA_ID}" was not loaded.`);
1424
+ const issues = [];
1425
+ for (const source of sources) {
1426
+ const content = await new Source(source).getContent();
1427
+ if (validator(new HrdtTokenReader().parseRaw(content))) continue;
1428
+ const errors = validator.errors ?? [];
1429
+ for (const error of errors) issues.push(this.#toValidationIssue(source, error));
1430
+ }
1431
+ return issues;
1432
+ }
1433
+ async #createAjv() {
1434
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
1435
+ const schemaPath = path.resolve(moduleDir, "schemas/hrdt-tokens.json");
1436
+ const ajv = new Ajv({
1437
+ allErrors: true,
1438
+ strict: false
1439
+ });
1440
+ addFormats(ajv);
1441
+ const rawSchema = await readFile(schemaPath, "utf8");
1442
+ const schema = JSON.parse(rawSchema);
1443
+ ajv.addSchema(schema, schema.$id ?? schemaPath);
1444
+ return ajv;
1240
1445
  }
1241
- };
1242
- /**
1243
- * Thrown when the input JSON does not conform to the expected DTCG structure.
1244
- */
1245
- var DtcgJsonReaderError = class extends Error {
1246
- constructor(message) {
1247
- super(message);
1248
- this.name = "DtcgJsonReaderError";
1446
+ #toValidationIssue(sourcePath, error) {
1447
+ const instancePath = error.instancePath || "/";
1448
+ const message = error.message ?? "Validation error.";
1449
+ return {
1450
+ name: this.name,
1451
+ sourcePath,
1452
+ severity: "error",
1453
+ message: `${instancePath}: ${message}`,
1454
+ raw: error
1455
+ };
1249
1456
  }
1250
1457
  };
1251
1458
  //#endregion
1252
- //#region src/core/Source.ts
1253
- var SourceType = /* @__PURE__ */ function(SourceType) {
1254
- SourceType["URL"] = "url";
1255
- SourceType["FILE"] = "file";
1256
- SourceType["CONTENT"] = "content";
1257
- return SourceType;
1258
- }(SourceType || {});
1459
+ //#region src/core/io/DtcgListLoader.ts
1259
1460
  /**
1260
- * Data source.
1261
- * Wraps URL, File or content.
1461
+ * Loads and validates token sources, assembling a {@link DtcgList}.
1262
1462
  *
1263
- * Allows returning any of these data sources as a file.
1264
- * Used in tool implementations to simplify the interface.
1463
+ * The first document is the base token set, subsequent documents are theme
1464
+ * overrides.
1465
+ *
1466
+ * Sources in different formats may be mixed.
1265
1467
  */
1266
- var Source = class Source {
1267
- #type;
1268
- #input;
1269
- #filePath;
1270
- constructor(input) {
1271
- this.#input = input;
1272
- if (Source.#isHttpUrl(input)) this.#type = SourceType.URL;
1273
- else if (existsSync(input)) this.#type = SourceType.FILE;
1274
- else this.#type = SourceType.CONTENT;
1275
- }
1276
- static #isHttpUrl(value) {
1277
- try {
1278
- const url = new URL(value);
1279
- return url.protocol === "http:" || url.protocol === "https:";
1280
- } catch {
1281
- return false;
1468
+ var DtcgListLoader = class {
1469
+ #validators = new Map([[Format.HRDT, new HrdtTokenValidator()], [Format.DTCG, new DtcgSchemaValidator()]]);
1470
+ #parsers = new Map([[Format.HRDT, async (source) => new HrdtTokenReader().parseAll(await source.getContent(), source.getInput())], [Format.DTCG, async (source) => [new DtcgJsonReader().parse(await source.getContent(), source.getInput())]]]);
1471
+ /**
1472
+ * Validates and loads all sources into a {@link DtcgList}.
1473
+ *
1474
+ * @param sources - Paths to token files or {@code "-"} for stdin.
1475
+ * @param forcedFormat - When set, all sources are treated as this
1476
+ * format instead of auto-detecting from content.
1477
+ * @throws TokenSyntaxError when schema validation fails.
1478
+ */
1479
+ async load(sources, forcedFormat) {
1480
+ const sourceList = sources.map((s) => new Source(s));
1481
+ const issues = await this.#validate(sourceList, forcedFormat);
1482
+ if (issues.length > 0) throw new TokenSyntaxError(issues);
1483
+ const allDocs = await this.#parse(sourceList, forcedFormat);
1484
+ return this.#buildDtcgList(allDocs);
1485
+ }
1486
+ async #validate(sourceList, forcedFormat) {
1487
+ const issues = [];
1488
+ for (const source of sourceList) {
1489
+ const format = forcedFormat ?? await source.getFormat();
1490
+ const validator = this.#validators.get(format);
1491
+ issues.push(...await validator.validate([source.getInput()]));
1282
1492
  }
1493
+ return issues;
1283
1494
  }
1284
- getInput() {
1285
- return this.#input;
1286
- }
1287
- getType() {
1288
- return this.#type;
1495
+ async #parse(sourceList, forcedFormat) {
1496
+ const allDocs = new Array();
1497
+ for (const source of sourceList) {
1498
+ const format = forcedFormat ?? await source.getFormat();
1499
+ const parser = this.#parsers.get(format);
1500
+ for (const doc of await parser(source)) allDocs.push({
1501
+ source: source.getInput(),
1502
+ doc
1503
+ });
1504
+ }
1505
+ return allDocs;
1289
1506
  }
1290
- async getContent() {
1291
- if (this.#type === SourceType.FILE) return readFile(this.#input, "utf8");
1292
- else if (this.#type === SourceType.URL) {
1293
- const response = await fetch(this.#input);
1294
- if (!response.ok) throw new Error(`Unable to fetch "${this.#input}": HTTP ${response.status}`);
1295
- return response.text();
1296
- } else if (this.#type === SourceType.CONTENT) return this.#input;
1297
- else throw new Error(`Unsupported source type "${this.#type}: ${this.#input}"`);
1507
+ #buildDtcgList(allDocs) {
1508
+ const [baseEntry, ...themeEntries] = allDocs;
1509
+ const themes = new Map(themeEntries.map((entry, i) => [extractThemeName(entry.source, i), entry.doc]));
1510
+ return new DtcgList(baseEntry.doc, themes);
1298
1511
  }
1299
- async getFile() {
1300
- if (this.#filePath !== void 0) return this.#filePath;
1301
- if (this.#type === SourceType.FILE) this.#filePath = this.#input;
1302
- else if (this.#type === SourceType.URL) {
1303
- const response = await fetch(this.#input);
1304
- if (!response.ok) throw new Error(`Unable to fetch "${this.#input}": HTTP ${response.status}`);
1305
- this.#filePath = await Source.#writeTemp(await response.text());
1306
- } else this.#filePath = await Source.#writeTemp(this.#input);
1307
- return this.#filePath;
1512
+ };
1513
+ /**
1514
+ * Thrown by {@link DtcgListLoader.load} when schema validation fails.
1515
+ *
1516
+ * The {@link issues} field contains individual validation diagnostics.
1517
+ */
1518
+ var TokenSyntaxError = class extends Error {
1519
+ issues;
1520
+ constructor(issues) {
1521
+ super("Schema validation failed");
1522
+ this.name = "TokenSyntaxError";
1523
+ this.issues = issues;
1308
1524
  }
1309
- static async #writeTemp(content) {
1310
- const filePath = path.join(tmpdir(), `designtokens-${randomUUID()}.tmp`);
1311
- await writeFile(filePath, content, "utf8");
1312
- return filePath;
1525
+ formatIssues() {
1526
+ return this.issues.map((i) => `[${i.name}] ${i.sourcePath} - ${i.message}`).join("\n");
1313
1527
  }
1314
1528
  };
1315
- //#endregion
1316
- //#region src/core/css/DtcgTokenCssConverter.ts
1317
- function extractThemeName$1(filePath) {
1318
- const withoutExt = (filePath.split("/").at(-1)?.split("\\").at(-1) ?? filePath).replace(/\.json$/i, "");
1529
+ function extractThemeName(source, index) {
1530
+ if (source === "-") return index !== void 0 ? `stdin-${index + 1}` : "stdin";
1531
+ const withoutExt = (source.split("/").at(-1)?.split("\\").at(-1) ?? source).replace(/\.(json|ya?ml)$/i, "");
1319
1532
  const dotIndex = withoutExt.lastIndexOf(".");
1320
1533
  return dotIndex > 0 ? withoutExt.slice(dotIndex + 1) : withoutExt;
1321
1534
  }
1535
+ //#endregion
1536
+ //#region src/core/css/DtcgTokenCssConverter.ts
1537
+ /**
1538
+ * Converts token documents in any supported format to CSS custom properties.
1539
+ *
1540
+ * The first document is treated as the base token set, all subsequent
1541
+ * documents as theme overrides.
1542
+ * Base tokens are emitted under {@code :root}.
1543
+ * Theme tokens are emitted under {@code :root[data-theme="name"]}.
1544
+ */
1545
+ var DtcgTokenCssConverter = class {
1546
+ #loader = new DtcgListLoader();
1547
+ async convert(sources) {
1548
+ const list = await this.#loader.load(sources);
1549
+ return this.convertList(list);
1550
+ }
1551
+ convertDocument(doc) {
1552
+ return this.convertList(new DtcgList(doc, /* @__PURE__ */ new Map()));
1553
+ }
1554
+ convertList(list) {
1555
+ const blocks = [];
1556
+ const baseBlock = renderBlock(":root", collectFromDoc(list.base));
1557
+ if (baseBlock) blocks.push(baseBlock);
1558
+ for (const [themeName, theme] of list.themes) {
1559
+ const block = renderBlock(`:root[data-theme="${themeName}"]`, collectFromDoc(theme));
1560
+ if (block) blocks.push(block);
1561
+ }
1562
+ return blocks.join("\n\n");
1563
+ }
1564
+ };
1322
1565
  function tokenPathToCssVar(path) {
1323
1566
  return `--${path.replace(/\./g, "-")}`;
1324
1567
  }
@@ -1394,33 +1637,6 @@ function renderBlock(selector, declarations) {
1394
1637
  if (declarations.length === 0) return "";
1395
1638
  return `${selector} {\n${declarations.map(([prop, val]) => ` ${prop}: ${val};`).join("\n")}\n}`;
1396
1639
  }
1397
- async function readDoc$1(source) {
1398
- const content = await new Source(source).getContent();
1399
- return /\.(ya?ml)$/i.test(source) ? new HrdtTokenReader().parse(content) : new DtcgJsonReader().parse(content);
1400
- }
1401
- async function loadDtcgList(sources) {
1402
- const [base, ...rest] = await Promise.all(sources.map(readDoc$1));
1403
- return new DtcgList(base, new Map(rest.map((doc, i) => [extractThemeName$1(sources[i + 1]), doc])));
1404
- }
1405
- var DtcgTokenCssConverter = class {
1406
- async convert(sources) {
1407
- const list = await loadDtcgList(sources);
1408
- return this.convertList(list);
1409
- }
1410
- convertDocument(doc) {
1411
- return this.convertList(new DtcgList(doc, /* @__PURE__ */ new Map()));
1412
- }
1413
- convertList(list) {
1414
- const blocks = [];
1415
- const baseBlock = renderBlock(":root", collectFromDoc(list.base));
1416
- if (baseBlock) blocks.push(baseBlock);
1417
- for (const [themeName, theme] of list.themes) {
1418
- const block = renderBlock(`:root[data-theme="${themeName}"]`, collectFromDoc(theme));
1419
- if (block) blocks.push(block);
1420
- }
1421
- return blocks.join("\n\n");
1422
- }
1423
- };
1424
1640
  //#endregion
1425
1641
  //#region src/core/showcase/CssTokenParser.ts
1426
1642
  /**
@@ -4101,8 +4317,8 @@ var TokenHtmlShowcaseBuilder = class {
4101
4317
  async showcase(sources) {
4102
4318
  if (sources.length === 0) throw new Error("No token sources provided");
4103
4319
  if (sources.length === 1) {
4104
- const sourceContent = await readFile(sources[0], "utf8");
4105
- if (this.#isCssContent(sourceContent)) return this.#renderCss(sourceContent);
4320
+ const source = new Source(sources[0]);
4321
+ if (await source.getFormat() === Format.CSS) return this.#renderCss(await source.getContent());
4106
4322
  }
4107
4323
  return this.#showcaseFromSources(sources);
4108
4324
  }
@@ -4121,147 +4337,28 @@ var TokenHtmlShowcaseBuilder = class {
4121
4337
  #formatValidationIssues(issues) {
4122
4338
  return issues.filter((issue) => issue.severity === "error").map((issue) => `[${issue.name}] ${issue.sourcePath} - ${issue.message}`).join("\n");
4123
4339
  }
4124
- #isCssContent(content) {
4125
- return /(^|\s)--[a-zA-Z0-9_-]+\s*:/.test(content) || /(^|\s):root\b/.test(content) || /(^|\s)@layer\b/.test(content);
4126
- }
4127
- };
4128
- //#endregion
4129
- //#region src/core/validation/dtcg/DtcgSchemaValidator.ts
4130
- var FORMAT_SCHEMA_ID = "https://www.designtokens.org/schemas/2025.10/format.json";
4131
- /**
4132
- * Validates DTCG JSON sources against the official DTCG JSON Schema.
4133
- * Accepts DTCG JSON sources only.
4134
- */
4135
- var DtcgSchemaValidator = class {
4136
- name = "dtcg";
4137
- async validate(sources) {
4138
- const validator = (await this.#createAjv()).getSchema(FORMAT_SCHEMA_ID);
4139
- if (!validator) throw new Error(`AJV schema "${FORMAT_SCHEMA_ID}" was not loaded.`);
4140
- const issues = [];
4141
- for (const source of sources) {
4142
- const content = await new Source(source).getContent();
4143
- if (validator(JSON.parse(content))) continue;
4144
- const errors = validator.errors ?? [];
4145
- for (const error of errors) issues.push(this.#toValidationIssue(source, error));
4146
- }
4147
- return issues;
4148
- }
4149
- async #createAjv() {
4150
- const moduleDir = path.dirname(fileURLToPath(import.meta.url));
4151
- const schemaDir = path.resolve(moduleDir, "schemas/2025.10");
4152
- const ajv = new Ajv({
4153
- allErrors: true,
4154
- strict: false
4155
- });
4156
- addFormats(ajv);
4157
- for (const schemaFilePath of await listJsonFiles(schemaDir)) {
4158
- const rawSchema = await readFile(schemaFilePath, "utf8");
4159
- const schema = JSON.parse(rawSchema);
4160
- ajv.addSchema(schema, schema.$id ?? schemaFilePath);
4161
- }
4162
- return ajv;
4163
- }
4164
- #toValidationIssue(sourcePath, error) {
4165
- const instancePath = error.instancePath || "/";
4166
- const message = error.message ?? "Validation error.";
4167
- return {
4168
- name: this.name,
4169
- sourcePath,
4170
- severity: "error",
4171
- message: `${instancePath}: ${message}`,
4172
- raw: error
4173
- };
4174
- }
4175
- };
4176
- async function listJsonFiles(directoryPath) {
4177
- const entries = await readdir(directoryPath, { withFileTypes: true });
4178
- const files = [];
4179
- for (const entry of entries) {
4180
- const entryPath = path.join(directoryPath, entry.name);
4181
- if (entry.isDirectory()) files.push(...await listJsonFiles(entryPath));
4182
- else if (entry.isFile() && entry.name.toLowerCase().endsWith(".json")) files.push(entryPath);
4183
- }
4184
- return files;
4185
- }
4186
- //#endregion
4187
- //#region src/core/validation/hrdt/HrdtTokenValidator.ts
4188
- var SCHEMA_ID = "https://designtokens.local/schemas/hrdt-tokens.json";
4189
- /**
4190
- * Validator based on AJV and JSON Schema.
4191
- * Accepts HRDT sources only.
4192
- */
4193
- var HrdtTokenValidator = class {
4194
- name = "hrdt";
4195
- async validate(sources) {
4196
- const validator = (await this.#createAjv()).getSchema(SCHEMA_ID);
4197
- if (!validator) throw new Error(`AJV schema "${SCHEMA_ID}" was not loaded.`);
4198
- const issues = [];
4199
- for (const source of sources) {
4200
- const content = await new Source(source).getContent();
4201
- if (validator(new HrdtTokenReader().parseRaw(content))) continue;
4202
- const errors = validator.errors ?? [];
4203
- for (const error of errors) issues.push(this.#toValidationIssue(source, error));
4204
- }
4205
- return issues;
4206
- }
4207
- async #createAjv() {
4208
- const moduleDir = path.dirname(fileURLToPath(import.meta.url));
4209
- const schemaPath = path.resolve(moduleDir, "schemas/hrdt-tokens.json");
4210
- const ajv = new Ajv({
4211
- allErrors: true,
4212
- strict: false
4213
- });
4214
- addFormats(ajv);
4215
- const rawSchema = await readFile(schemaPath, "utf8");
4216
- const schema = JSON.parse(rawSchema);
4217
- ajv.addSchema(schema, schema.$id ?? schemaPath);
4218
- return ajv;
4219
- }
4220
- #toValidationIssue(sourcePath, error) {
4221
- const instancePath = error.instancePath || "/";
4222
- const message = error.message ?? "Validation error.";
4223
- return {
4224
- name: this.name,
4225
- sourcePath,
4226
- severity: "error",
4227
- message: `${instancePath}: ${message}`,
4228
- raw: error
4229
- };
4230
- }
4231
4340
  };
4232
4341
  //#endregion
4233
4342
  //#region src/core/validation/DtcgTokenValidator.ts
4234
- function extractThemeName(source) {
4235
- const withoutExt = (source.split("/").at(-1)?.split("\\").at(-1) ?? source).replace(/\.(json|ya?ml)$/i, "");
4236
- const dotIndex = withoutExt.lastIndexOf(".");
4237
- return dotIndex > 0 ? withoutExt.slice(dotIndex + 1) : withoutExt;
4238
- }
4239
- async function readDoc(source) {
4240
- const content = await new Source(source).getContent();
4241
- return /\.(ya?ml)$/i.test(source) ? new HrdtTokenReader().parse(content) : new DtcgJsonReader().parse(content);
4242
- }
4243
4343
  /**
4244
4344
  * Validates DTCG JSON or HRDT token files using the internal model.
4245
4345
  *
4246
4346
  * Accepts one base file plus optional theme files. Validates references,
4247
4347
  * cycles, type mismatches, and deprecated token usage.
4348
+ *
4349
+ * Format is detected from content, not from file extension.
4350
+ * HRDT sources may contain multiple YAML documents separated by {@code ---}.
4351
+ * The first document is the base, subsequent documents are themes.
4248
4352
  */
4249
4353
  var DtcgTokenValidator = class {
4250
- #name = "internal";
4354
+ #loader = new DtcgListLoader();
4251
4355
  async validate(sources) {
4252
- const hrdtSources = sources.filter((s) => /\.(ya?ml)$/i.test(s));
4253
- const hrdtIssues = hrdtSources.length > 0 ? await new HrdtTokenValidator().validate(hrdtSources) : [];
4254
- if (hrdtIssues.some((i) => i.severity === "error")) return hrdtIssues;
4255
- const jsonSources = sources.filter((s) => !/\.(ya?ml)$/i.test(s));
4256
- const schemaIssues = jsonSources.length > 0 ? await new DtcgSchemaValidator().validate(jsonSources) : [];
4257
- if (schemaIssues.some((i) => i.severity === "error")) return schemaIssues;
4258
- const [base, ...rest] = await Promise.all(sources.map(readDoc));
4259
- return new DtcgList(base, new Map(rest.map((doc, i) => [extractThemeName(sources[i + 1]), doc]))).validate().map((issue) => ({
4260
- name: this.#name,
4261
- sourcePath: sources.join(", "),
4262
- message: `${issue.tokenPath}: ${issue.message}`,
4263
- severity: issue.severity
4264
- }));
4356
+ try {
4357
+ return (await this.#loader.load(sources)).validate();
4358
+ } catch (error) {
4359
+ if (error instanceof TokenSyntaxError) return error.issues;
4360
+ throw error;
4361
+ }
4265
4362
  }
4266
4363
  };
4267
4364
  //#endregion
@@ -4616,6 +4713,9 @@ var HrdtTokenWriter = class {
4616
4713
  return String(value);
4617
4714
  }
4618
4715
  #serializeColor(color) {
4716
+ return `"${this.#colorToHex(color)}"`;
4717
+ }
4718
+ #colorToHex(color) {
4619
4719
  if (color.hex) {
4620
4720
  if (color.alpha === 1) return color.hex;
4621
4721
  const alphaHex = Math.round(color.alpha * 255).toString(16).padStart(2, "0");
@@ -4704,7 +4804,7 @@ var HrdtTokenWriter = class {
4704
4804
  return lines;
4705
4805
  }
4706
4806
  #serializeColorOrRef(value) {
4707
- return value instanceof TokenReference ? value.toString() : this.#serializeColor(value);
4807
+ return value instanceof TokenReference ? value.toString() : this.#colorToHex(value);
4708
4808
  }
4709
4809
  #serializeDimensionOrRef(value) {
4710
4810
  return value instanceof TokenReference ? `"${value}"` : this.#serializeDimension(value);
@@ -4734,4 +4834,4 @@ function createTokenHtmlShowcase() {
4734
4834
  return new TokenHtmlShowcaseBuilder(new DtcgTokenValidator(), new DtcgTokenCssConverter());
4735
4835
  }
4736
4836
  //#endregion
4737
- export { DesignToken, DesignTokens, Dtcg, DtcgJsonReader, DtcgJsonReaderError, DtcgJsonWriter, DtcgList, DtcgSchemaValidator, DtcgTokenCssConverter, DtcgTokenValidator, HrdtTokenReader, HrdtTokenReaderError, HrdtTokenValidator, HrdtTokenWriter, Source, TokenGroup, TokenHtmlShowcaseBuilder, TokenNode, TokenReference, createTokenCssConverter, createTokenHtmlShowcase };
4837
+ export { DesignToken, DesignTokens, Dtcg, DtcgJsonReader, DtcgJsonReaderError, DtcgJsonWriter, DtcgList, DtcgListLoader, DtcgSchemaValidator, DtcgTokenCssConverter, DtcgTokenValidator, Format, FormatDetector, HrdtTokenReader, HrdtTokenReaderError, HrdtTokenValidator, HrdtTokenWriter, Source, TokenGroup, TokenHtmlShowcaseBuilder, TokenNode, TokenReference, TokenSyntaxError, createTokenCssConverter, createTokenHtmlShowcase };