@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.
- package/lib/index.d.ts +132 -17
- package/lib/index.js +724 -624
- 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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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/
|
|
676
|
-
var
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
"
|
|
682
|
-
"
|
|
683
|
-
"
|
|
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
|
-
*
|
|
909
|
+
* Parses a DTCG 2025.10 JSON document into a {@link Dtcg}.
|
|
697
910
|
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
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
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
#
|
|
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
|
|
716
|
-
if (
|
|
717
|
-
|
|
718
|
-
|
|
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
|
|
937
|
+
type,
|
|
938
|
+
description,
|
|
939
|
+
deprecated,
|
|
940
|
+
extensions,
|
|
941
|
+
extends: extendsRef,
|
|
942
|
+
root,
|
|
736
943
|
children
|
|
737
944
|
});
|
|
738
945
|
}
|
|
739
|
-
#
|
|
740
|
-
|
|
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
|
-
#
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
|
|
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
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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/
|
|
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
|
-
*
|
|
1261
|
-
* Wraps URL, File or content.
|
|
1461
|
+
* Loads and validates token sources, assembling a {@link DtcgList}.
|
|
1262
1462
|
*
|
|
1263
|
-
*
|
|
1264
|
-
*
|
|
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
|
|
1267
|
-
#
|
|
1268
|
-
#
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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
|
-
|
|
1310
|
-
|
|
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
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
|
4105
|
-
if (
|
|
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
|
-
#
|
|
4354
|
+
#loader = new DtcgListLoader();
|
|
4251
4355
|
async validate(sources) {
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
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.#
|
|
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 };
|