@terrazzo/parser 0.7.3 → 0.8.1
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/CHANGELOG.md +20 -0
- package/dist/index.d.ts +705 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4603 -12
- package/dist/index.js.map +1 -1
- package/package.json +11 -7
- package/src/build/index.ts +8 -6
- package/src/index.ts +76 -1
- package/dist/build/index.d.ts +0 -20
- package/dist/build/index.d.ts.map +0 -1
- package/dist/build/index.js +0 -166
- package/dist/build/index.js.map +0 -1
- package/dist/config.d.ts +0 -8
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -290
- package/dist/config.js.map +0 -1
- package/dist/lib/code-frame.d.ts +0 -31
- package/dist/lib/code-frame.d.ts.map +0 -1
- package/dist/lib/code-frame.js +0 -108
- package/dist/lib/code-frame.js.map +0 -1
- package/dist/lint/index.d.ts +0 -12
- package/dist/lint/index.d.ts.map +0 -1
- package/dist/lint/index.js +0 -105
- package/dist/lint/index.js.map +0 -1
- package/dist/lint/plugin-core/index.d.ts +0 -13
- package/dist/lint/plugin-core/index.d.ts.map +0 -1
- package/dist/lint/plugin-core/index.js +0 -40
- package/dist/lint/plugin-core/index.js.map +0 -1
- package/dist/lint/plugin-core/lib/docs.d.ts +0 -2
- package/dist/lint/plugin-core/lib/docs.d.ts.map +0 -1
- package/dist/lint/plugin-core/lib/docs.js +0 -4
- package/dist/lint/plugin-core/lib/docs.js.map +0 -1
- package/dist/lint/plugin-core/rules/a11y-min-contrast.d.ts +0 -40
- package/dist/lint/plugin-core/rules/a11y-min-contrast.d.ts.map +0 -1
- package/dist/lint/plugin-core/rules/a11y-min-contrast.js +0 -58
- package/dist/lint/plugin-core/rules/a11y-min-contrast.js.map +0 -1
- package/dist/lint/plugin-core/rules/a11y-min-font-size.d.ts +0 -14
- package/dist/lint/plugin-core/rules/a11y-min-font-size.d.ts.map +0 -1
- package/dist/lint/plugin-core/rules/a11y-min-font-size.js +0 -45
- package/dist/lint/plugin-core/rules/a11y-min-font-size.js.map +0 -1
- package/dist/lint/plugin-core/rules/colorspace.d.ts +0 -15
- package/dist/lint/plugin-core/rules/colorspace.d.ts.map +0 -1
- package/dist/lint/plugin-core/rules/colorspace.js +0 -85
- package/dist/lint/plugin-core/rules/colorspace.js.map +0 -1
- package/dist/lint/plugin-core/rules/consistent-naming.d.ts +0 -12
- package/dist/lint/plugin-core/rules/consistent-naming.d.ts.map +0 -1
- package/dist/lint/plugin-core/rules/consistent-naming.js +0 -49
- package/dist/lint/plugin-core/rules/consistent-naming.js.map +0 -1
- package/dist/lint/plugin-core/rules/descriptions.d.ts +0 -10
- package/dist/lint/plugin-core/rules/descriptions.d.ts.map +0 -1
- package/dist/lint/plugin-core/rules/descriptions.js +0 -32
- package/dist/lint/plugin-core/rules/descriptions.js.map +0 -1
- package/dist/lint/plugin-core/rules/duplicate-values.d.ts +0 -10
- package/dist/lint/plugin-core/rules/duplicate-values.d.ts.map +0 -1
- package/dist/lint/plugin-core/rules/duplicate-values.js +0 -65
- package/dist/lint/plugin-core/rules/duplicate-values.js.map +0 -1
- package/dist/lint/plugin-core/rules/max-gamut.d.ts +0 -15
- package/dist/lint/plugin-core/rules/max-gamut.d.ts.map +0 -1
- package/dist/lint/plugin-core/rules/max-gamut.js +0 -101
- package/dist/lint/plugin-core/rules/max-gamut.js.map +0 -1
- package/dist/lint/plugin-core/rules/required-children.d.ts +0 -19
- package/dist/lint/plugin-core/rules/required-children.d.ts.map +0 -1
- package/dist/lint/plugin-core/rules/required-children.js +0 -78
- package/dist/lint/plugin-core/rules/required-children.js.map +0 -1
- package/dist/lint/plugin-core/rules/required-modes.d.ts +0 -14
- package/dist/lint/plugin-core/rules/required-modes.d.ts.map +0 -1
- package/dist/lint/plugin-core/rules/required-modes.js +0 -52
- package/dist/lint/plugin-core/rules/required-modes.js.map +0 -1
- package/dist/lint/plugin-core/rules/required-typography-properties.d.ts +0 -11
- package/dist/lint/plugin-core/rules/required-typography-properties.d.ts.map +0 -1
- package/dist/lint/plugin-core/rules/required-typography-properties.js +0 -38
- package/dist/lint/plugin-core/rules/required-typography-properties.js.map +0 -1
- package/dist/logger.d.ts +0 -77
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -136
- package/dist/logger.js.map +0 -1
- package/dist/parse/alias.d.ts +0 -34
- package/dist/parse/alias.d.ts.map +0 -1
- package/dist/parse/alias.js +0 -302
- package/dist/parse/alias.js.map +0 -1
- package/dist/parse/index.d.ts +0 -41
- package/dist/parse/index.d.ts.map +0 -1
- package/dist/parse/index.js +0 -273
- package/dist/parse/index.js.map +0 -1
- package/dist/parse/json.d.ts +0 -52
- package/dist/parse/json.d.ts.map +0 -1
- package/dist/parse/json.js +0 -168
- package/dist/parse/json.js.map +0 -1
- package/dist/parse/normalize.d.ts +0 -24
- package/dist/parse/normalize.d.ts.map +0 -1
- package/dist/parse/normalize.js +0 -176
- package/dist/parse/normalize.js.map +0 -1
- package/dist/parse/validate.d.ts +0 -90
- package/dist/parse/validate.d.ts.map +0 -1
- package/dist/parse/validate.js +0 -787
- package/dist/parse/validate.js.map +0 -1
- package/dist/types.d.ts +0 -265
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,4604 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { isAlias, isTokenMatch, parseAlias, parseColor, pluralize, splitID, tokenToCulori } from "@terrazzo/token-tools";
|
|
2
|
+
import wcmatch from "wildcard-match";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { merge } from "merge-anything";
|
|
5
|
+
import { clampChroma, wcagContrast } from "culori";
|
|
6
|
+
|
|
7
|
+
//#region src/lib/code-frame.ts
|
|
8
|
+
/**
|
|
9
|
+
* Extract what lines should be marked and highlighted.
|
|
10
|
+
*/
|
|
11
|
+
function getMarkerLines(loc, source, opts = {}) {
|
|
12
|
+
const startLoc = {
|
|
13
|
+
column: 0,
|
|
14
|
+
line: -1,
|
|
15
|
+
...loc.start
|
|
16
|
+
};
|
|
17
|
+
const endLoc = {
|
|
18
|
+
...startLoc,
|
|
19
|
+
...loc.end
|
|
20
|
+
};
|
|
21
|
+
const { linesAbove = 2, linesBelow = 3 } = opts || {};
|
|
22
|
+
const startLine = startLoc.line;
|
|
23
|
+
const startColumn = startLoc.column;
|
|
24
|
+
const endLine = endLoc.line;
|
|
25
|
+
const endColumn = endLoc.column;
|
|
26
|
+
let start = Math.max(startLine - (linesAbove + 1), 0);
|
|
27
|
+
let end = Math.min(source.length, endLine + linesBelow);
|
|
28
|
+
if (startLine === -1) start = 0;
|
|
29
|
+
if (endLine === -1) end = source.length;
|
|
30
|
+
const lineDiff = endLine - startLine;
|
|
31
|
+
const markerLines = {};
|
|
32
|
+
if (lineDiff) for (let i = 0; i <= lineDiff; i++) {
|
|
33
|
+
const lineNumber = i + startLine;
|
|
34
|
+
if (!startColumn) markerLines[lineNumber] = true;
|
|
35
|
+
else if (i === 0) {
|
|
36
|
+
const sourceLength = source[lineNumber - 1].length;
|
|
37
|
+
markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];
|
|
38
|
+
} else if (i === lineDiff) markerLines[lineNumber] = [0, endColumn];
|
|
39
|
+
else {
|
|
40
|
+
const sourceLength = source[lineNumber - i].length;
|
|
41
|
+
markerLines[lineNumber] = [0, sourceLength];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (startColumn === endColumn) if (startColumn) markerLines[startLine] = [startColumn, 0];
|
|
45
|
+
else markerLines[startLine] = true;
|
|
46
|
+
else markerLines[startLine] = [startColumn, endColumn - startColumn];
|
|
47
|
+
return {
|
|
48
|
+
start,
|
|
49
|
+
end,
|
|
50
|
+
markerLines
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* RegExp to test for newlines in terminal.
|
|
55
|
+
*/
|
|
56
|
+
const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
|
|
57
|
+
function codeFrameColumns(rawLines, loc, opts = {}) {
|
|
58
|
+
const lines = rawLines.split(NEWLINE);
|
|
59
|
+
const { start, end, markerLines } = getMarkerLines(loc, lines, opts);
|
|
60
|
+
const hasColumns = loc.start && typeof loc.start.column === "number";
|
|
61
|
+
const numberMaxWidth = String(end).length;
|
|
62
|
+
let frame = rawLines.split(NEWLINE, end).slice(start, end).map((line, index) => {
|
|
63
|
+
const number = start + 1 + index;
|
|
64
|
+
const paddedNumber = ` ${number}`.slice(-numberMaxWidth);
|
|
65
|
+
const gutter = ` ${paddedNumber} |`;
|
|
66
|
+
const hasMarker = markerLines[number];
|
|
67
|
+
const lastMarkerLine = !markerLines[number + 1];
|
|
68
|
+
if (hasMarker) {
|
|
69
|
+
let markerLine = "";
|
|
70
|
+
if (Array.isArray(hasMarker)) {
|
|
71
|
+
const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, " ");
|
|
72
|
+
const numberOfMarkers = hasMarker[1] || 1;
|
|
73
|
+
markerLine = [
|
|
74
|
+
"\n ",
|
|
75
|
+
gutter.replace(/\d/g, " "),
|
|
76
|
+
" ",
|
|
77
|
+
markerSpacing,
|
|
78
|
+
"^".repeat(numberOfMarkers)
|
|
79
|
+
].join("");
|
|
80
|
+
if (lastMarkerLine && opts.message) markerLine += ` ${opts.message}`;
|
|
81
|
+
}
|
|
82
|
+
return [
|
|
83
|
+
">",
|
|
84
|
+
gutter,
|
|
85
|
+
line.length > 0 ? ` ${line}` : "",
|
|
86
|
+
markerLine
|
|
87
|
+
].join("");
|
|
88
|
+
} else return ` ${gutter}${line.length > 0 ? ` ${line}` : ""}`;
|
|
89
|
+
}).join("\n");
|
|
90
|
+
if (opts.message && !hasColumns) frame = `${" ".repeat(numberMaxWidth + 1)}${opts.message}\n${frame}`;
|
|
91
|
+
return frame;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/logger.ts
|
|
96
|
+
const LOG_ORDER = [
|
|
97
|
+
"error",
|
|
98
|
+
"warn",
|
|
99
|
+
"info",
|
|
100
|
+
"debug"
|
|
101
|
+
];
|
|
102
|
+
const MESSAGE_COLOR = {
|
|
103
|
+
error: pc.red,
|
|
104
|
+
warn: pc.yellow
|
|
105
|
+
};
|
|
106
|
+
const timeFormatter = new Intl.DateTimeFormat("en-us", {
|
|
107
|
+
hour: "numeric",
|
|
108
|
+
hour12: false,
|
|
109
|
+
minute: "numeric",
|
|
110
|
+
second: "numeric",
|
|
111
|
+
fractionalSecondDigits: 3
|
|
112
|
+
});
|
|
113
|
+
/**
|
|
114
|
+
* @param {Entry} entry
|
|
115
|
+
* @param {Severity} severity
|
|
116
|
+
* @return {string}
|
|
117
|
+
*/
|
|
118
|
+
function formatMessage(entry, severity) {
|
|
119
|
+
let message = entry.message;
|
|
120
|
+
message = `[${entry.group}${entry.label ? `:${entry.label}` : ""}] ${message}`;
|
|
121
|
+
if (severity in MESSAGE_COLOR) message = MESSAGE_COLOR[severity](message);
|
|
122
|
+
if (entry.src) {
|
|
123
|
+
const start = entry.node?.loc?.start ?? {
|
|
124
|
+
line: 0,
|
|
125
|
+
column: 0
|
|
126
|
+
};
|
|
127
|
+
const loc = entry.filename ? `${entry.filename?.href.replace(/^file:\/\//, "")}:${start?.line ?? 0}:${start?.column ?? 0}\n\n` : "";
|
|
128
|
+
const codeFrame = codeFrameColumns(entry.src, { start }, { highlightCode: false });
|
|
129
|
+
message = `${message}\n\n${loc}${codeFrame}`;
|
|
130
|
+
}
|
|
131
|
+
return message;
|
|
132
|
+
}
|
|
133
|
+
var Logger = class {
|
|
134
|
+
level = "info";
|
|
135
|
+
debugScope = "*";
|
|
136
|
+
errorCount = 0;
|
|
137
|
+
warnCount = 0;
|
|
138
|
+
infoCount = 0;
|
|
139
|
+
debugCount = 0;
|
|
140
|
+
constructor(options) {
|
|
141
|
+
if (options?.level) this.level = options.level;
|
|
142
|
+
if (options?.debugScope) this.debugScope = options.debugScope;
|
|
143
|
+
}
|
|
144
|
+
setLevel(level) {
|
|
145
|
+
this.level = level;
|
|
146
|
+
}
|
|
147
|
+
/** Log an error message (always; can’t be silenced) */
|
|
148
|
+
error(entry) {
|
|
149
|
+
this.errorCount++;
|
|
150
|
+
const message = formatMessage(entry, "error");
|
|
151
|
+
if (entry.continueOnError) {
|
|
152
|
+
console.error(message);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (entry.node) throw new TokensJSONError(message);
|
|
156
|
+
else throw new Error(message);
|
|
157
|
+
}
|
|
158
|
+
/** Log an info message (if logging level permits) */
|
|
159
|
+
info(entry) {
|
|
160
|
+
this.infoCount++;
|
|
161
|
+
if (this.level === "silent" || LOG_ORDER.indexOf(this.level) < LOG_ORDER.indexOf("info")) return;
|
|
162
|
+
const message = formatMessage(entry, "info");
|
|
163
|
+
console.log(message);
|
|
164
|
+
}
|
|
165
|
+
/** Log a warning message (if logging level permits) */
|
|
166
|
+
warn(entry) {
|
|
167
|
+
this.warnCount++;
|
|
168
|
+
if (this.level === "silent" || LOG_ORDER.indexOf(this.level) < LOG_ORDER.indexOf("warn")) return;
|
|
169
|
+
const message = formatMessage(entry, "warn");
|
|
170
|
+
console.warn(message);
|
|
171
|
+
}
|
|
172
|
+
/** Log a diagnostics message (if logging level permits) */
|
|
173
|
+
debug(entry) {
|
|
174
|
+
if (this.level === "silent" || LOG_ORDER.indexOf(this.level) < LOG_ORDER.indexOf("debug")) return;
|
|
175
|
+
this.debugCount++;
|
|
176
|
+
let message = formatMessage(entry, "debug");
|
|
177
|
+
const debugPrefix = entry.label ? `${entry.group}:${entry.label}` : entry.group;
|
|
178
|
+
if (this.debugScope !== "*" && !wcmatch(this.debugScope)(debugPrefix)) return;
|
|
179
|
+
message.replace(/\[config[^\]]+\]/, (match) => pc.green(match)).replace(/\[parser[^\]]+\]/, (match) => pc.magenta(match)).replace(/\[lint[^\]]+\]/, (match) => pc.yellow(match)).replace(/\[plugin[^\]]+\]/, (match) => pc.cyan(match));
|
|
180
|
+
message = `${pc.dim(timeFormatter.format(performance.now()))} ${message}`;
|
|
181
|
+
if (typeof entry.timing === "number") {
|
|
182
|
+
let timing = "";
|
|
183
|
+
if (entry.timing < 1e3) timing = `${Math.round(entry.timing * 100) / 100}ms`;
|
|
184
|
+
else if (entry.timing < 6e4) timing = `${Math.round(entry.timing * 100) / 1e5}s`;
|
|
185
|
+
message = `${message} ${pc.dim(`[${timing}]`)}`;
|
|
186
|
+
}
|
|
187
|
+
console.log(message);
|
|
188
|
+
}
|
|
189
|
+
/** Get stats for current logger instance */
|
|
190
|
+
stats() {
|
|
191
|
+
return {
|
|
192
|
+
errorCount: this.errorCount,
|
|
193
|
+
warnCount: this.warnCount,
|
|
194
|
+
infoCount: this.infoCount,
|
|
195
|
+
debugCount: this.debugCount
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
var TokensJSONError = class extends Error {
|
|
200
|
+
constructor(message) {
|
|
201
|
+
super(message);
|
|
202
|
+
this.name = "TokensJSONError";
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/build/index.ts
|
|
208
|
+
const SINGLE_VALUE = "SINGLE_VALUE";
|
|
209
|
+
const MULTI_VALUE = "MULTI_VALUE";
|
|
210
|
+
/** Validate plugin setTransform() calls for immediate feedback */
|
|
211
|
+
function validateTransformParams({ params, logger, pluginName }) {
|
|
212
|
+
const baseMessage = {
|
|
213
|
+
group: "plugin",
|
|
214
|
+
label: pluginName,
|
|
215
|
+
message: ""
|
|
216
|
+
};
|
|
217
|
+
if (!params.value || typeof params.value !== "string" && typeof params.value !== "object" || Array.isArray(params.value)) logger.error({
|
|
218
|
+
...baseMessage,
|
|
219
|
+
message: `setTransform() value expected string or object of strings, received ${Array.isArray(params.value) ? "Array" : typeof params.value}`
|
|
220
|
+
});
|
|
221
|
+
if (typeof params.value === "object" && Object.values(params.value).some((v) => typeof v !== "string")) logger.error({
|
|
222
|
+
...baseMessage,
|
|
223
|
+
message: "setTransform() value expected object of strings, received some non-string values"
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/** Run build stage */
|
|
227
|
+
async function build(tokens, { sources, logger = new Logger(), config }) {
|
|
228
|
+
const formats = {};
|
|
229
|
+
const result = { outputFiles: [] };
|
|
230
|
+
function getTransforms(params) {
|
|
231
|
+
if (!params?.format) {
|
|
232
|
+
logger.warn({
|
|
233
|
+
group: "plugin",
|
|
234
|
+
message: "\"format\" missing from getTransforms(), no tokens returned."
|
|
235
|
+
});
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
return (formats[params.format] ?? []).filter((token) => {
|
|
239
|
+
if (params.$type) {
|
|
240
|
+
if (typeof params.$type === "string" && token.token.$type !== params.$type) return false;
|
|
241
|
+
else if (Array.isArray(params.$type) && !params.$type.some(($type) => token.token.$type === $type)) return false;
|
|
242
|
+
}
|
|
243
|
+
if (params.id && params.id !== "*" && !isTokenMatch(token.token.id, Array.isArray(params.id) ? params.id : [params.id])) return false;
|
|
244
|
+
if (params.mode && !wcmatch(params.mode)(token.mode)) return false;
|
|
245
|
+
return true;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
let transformsLocked = false;
|
|
249
|
+
const startTransform = performance.now();
|
|
250
|
+
for (const plugin of config.plugins) if (typeof plugin.transform === "function") await plugin.transform({
|
|
251
|
+
tokens,
|
|
252
|
+
sources,
|
|
253
|
+
getTransforms,
|
|
254
|
+
setTransform(id, params) {
|
|
255
|
+
if (transformsLocked) {
|
|
256
|
+
logger.warn({
|
|
257
|
+
message: "Attempted to call setTransform() after transform step has completed.",
|
|
258
|
+
group: "plugin",
|
|
259
|
+
label: plugin.name
|
|
260
|
+
});
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const token = tokens[id];
|
|
264
|
+
const cleanValue = typeof params.value === "string" ? params.value : { ...params.value };
|
|
265
|
+
if (typeof cleanValue === "object") {
|
|
266
|
+
for (const k of Object.keys(cleanValue)) if (cleanValue[k] === void 0) delete cleanValue[k];
|
|
267
|
+
}
|
|
268
|
+
validateTransformParams({
|
|
269
|
+
logger,
|
|
270
|
+
params: {
|
|
271
|
+
...params,
|
|
272
|
+
value: cleanValue
|
|
273
|
+
},
|
|
274
|
+
pluginName: plugin.name
|
|
275
|
+
});
|
|
276
|
+
if (!formats[params.format]) formats[params.format] = [];
|
|
277
|
+
const foundTokenI = formats[params.format].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && (!params.mode || params.mode === t.mode));
|
|
278
|
+
if (foundTokenI === -1) formats[params.format].push({
|
|
279
|
+
...params,
|
|
280
|
+
id,
|
|
281
|
+
value: cleanValue,
|
|
282
|
+
type: typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE,
|
|
283
|
+
mode: params.mode || ".",
|
|
284
|
+
token: structuredClone(token)
|
|
285
|
+
});
|
|
286
|
+
else {
|
|
287
|
+
formats[params.format][foundTokenI].value = cleanValue;
|
|
288
|
+
formats[params.format][foundTokenI].type = typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
transformsLocked = true;
|
|
293
|
+
logger.debug({
|
|
294
|
+
group: "parser",
|
|
295
|
+
label: "transform",
|
|
296
|
+
message: "transform() step",
|
|
297
|
+
timing: performance.now() - startTransform
|
|
298
|
+
});
|
|
299
|
+
const startBuild = performance.now();
|
|
300
|
+
for (const plugin of config.plugins) if (typeof plugin.build === "function") {
|
|
301
|
+
const pluginBuildStart = performance.now();
|
|
302
|
+
await plugin.build({
|
|
303
|
+
tokens,
|
|
304
|
+
sources,
|
|
305
|
+
getTransforms,
|
|
306
|
+
outputFile(filename, contents) {
|
|
307
|
+
const resolved = new URL(filename, config.outDir);
|
|
308
|
+
if (result.outputFiles.some((f) => new URL(f.filename, config.outDir).href === resolved.href)) logger.error({
|
|
309
|
+
group: "plugin",
|
|
310
|
+
message: `Can’t overwrite file "${filename}"`,
|
|
311
|
+
label: plugin.name
|
|
312
|
+
});
|
|
313
|
+
result.outputFiles.push({
|
|
314
|
+
filename,
|
|
315
|
+
contents,
|
|
316
|
+
plugin: plugin.name,
|
|
317
|
+
time: performance.now() - pluginBuildStart
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
logger.debug({
|
|
323
|
+
group: "parser",
|
|
324
|
+
label: "build",
|
|
325
|
+
message: "build() step",
|
|
326
|
+
timing: performance.now() - startBuild
|
|
327
|
+
});
|
|
328
|
+
const startBuildEnd = performance.now();
|
|
329
|
+
for (const plugin of config.plugins) if (typeof plugin.buildEnd === "function") await plugin.buildEnd({ outputFiles: structuredClone(result.outputFiles) });
|
|
330
|
+
logger.debug({
|
|
331
|
+
group: "parser",
|
|
332
|
+
label: "build",
|
|
333
|
+
message: "buildEnd() step",
|
|
334
|
+
timing: performance.now() - startBuildEnd
|
|
335
|
+
});
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
//#endregion
|
|
340
|
+
//#region src/lint/plugin-core/lib/docs.ts
|
|
341
|
+
function docsLink(ruleName) {
|
|
342
|
+
return `https://terrazzo.app/docs/cli/lint#${ruleName.replaceAll("/", "")}`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
//#endregion
|
|
346
|
+
//#region src/lint/plugin-core/rules/a11y-min-contrast.ts
|
|
347
|
+
const A11Y_MIN_CONTRAST = "a11y/min-contrast";
|
|
348
|
+
const WCAG2_MIN_CONTRAST = {
|
|
349
|
+
AA: {
|
|
350
|
+
default: 4.5,
|
|
351
|
+
large: 3
|
|
352
|
+
},
|
|
353
|
+
AAA: {
|
|
354
|
+
default: 7,
|
|
355
|
+
large: 4.5
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
const ERROR_INSUFFICIENT_CONTRAST = "INSUFFICIENT_CONTRAST";
|
|
359
|
+
const rule$9 = {
|
|
360
|
+
meta: {
|
|
361
|
+
messages: { [ERROR_INSUFFICIENT_CONTRAST]: "Pair {{ index }} failed; expected {{ expected }}, got {{ actual }} ({{ level }})" },
|
|
362
|
+
docs: {
|
|
363
|
+
description: "Enforce colors meet minimum contrast checks for WCAG 2.",
|
|
364
|
+
url: docsLink(A11Y_MIN_CONTRAST)
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
defaultOptions: {
|
|
368
|
+
level: "AA",
|
|
369
|
+
pairs: []
|
|
370
|
+
},
|
|
371
|
+
create({ tokens, options, report }) {
|
|
372
|
+
for (let i = 0; i < options.pairs.length; i++) {
|
|
373
|
+
const { foreground, background, largeText } = options.pairs[i];
|
|
374
|
+
if (!tokens[foreground]) throw new Error(`Token ${foreground} does not exist`);
|
|
375
|
+
if (tokens[foreground].$type !== "color") throw new Error(`Token ${foreground} isn’t a color`);
|
|
376
|
+
if (!tokens[background]) throw new Error(`Token ${background} does not exist`);
|
|
377
|
+
if (tokens[background].$type !== "color") throw new Error(`Token ${background} isn’t a color`);
|
|
378
|
+
const a = tokenToCulori(tokens[foreground].$value);
|
|
379
|
+
const b = tokenToCulori(tokens[background].$value);
|
|
380
|
+
const contrast = wcagContrast(a, b);
|
|
381
|
+
const min = WCAG2_MIN_CONTRAST[options.level ?? "AA"][largeText ? "large" : "default"];
|
|
382
|
+
if (contrast < min) report({
|
|
383
|
+
messageId: ERROR_INSUFFICIENT_CONTRAST,
|
|
384
|
+
data: {
|
|
385
|
+
index: i + 1,
|
|
386
|
+
expected: min,
|
|
387
|
+
actual: Math.round(contrast * 100) / 100,
|
|
388
|
+
level: options.level
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
var a11y_min_contrast_default = rule$9;
|
|
395
|
+
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/lint/plugin-core/rules/a11y-min-font-size.ts
|
|
398
|
+
const A11Y_MIN_FONT_SIZE = "a11y/min-font-size";
|
|
399
|
+
const ERROR_TOO_SMALL = "TOO_SMALL";
|
|
400
|
+
const rule$8 = {
|
|
401
|
+
meta: {
|
|
402
|
+
messages: { [ERROR_TOO_SMALL]: "{{ id }} font size too small. Expected minimum of {{ min }}" },
|
|
403
|
+
docs: {
|
|
404
|
+
description: "Enforce font sizes are no smaller than the given value.",
|
|
405
|
+
url: docsLink(A11Y_MIN_FONT_SIZE)
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
defaultOptions: {},
|
|
409
|
+
create({ tokens, options, report }) {
|
|
410
|
+
if (!options.minSizePx && !options.minSizeRem) throw new Error("Must specify at least one of minSizePx or minSizeRem");
|
|
411
|
+
for (const t of Object.values(tokens)) {
|
|
412
|
+
if (options.ignore && isTokenMatch(t.id, options.ignore)) continue;
|
|
413
|
+
if (t.aliasOf) continue;
|
|
414
|
+
if (t.$type === "typography" && "fontSize" in t.$value) {
|
|
415
|
+
const fontSize = t.$value.fontSize;
|
|
416
|
+
if (fontSize.unit === "px" && options.minSizePx && fontSize.value < options.minSizePx || fontSize.unit === "rem" && options.minSizeRem && fontSize.value < options.minSizeRem) report({
|
|
417
|
+
messageId: ERROR_TOO_SMALL,
|
|
418
|
+
data: {
|
|
419
|
+
id: t.id,
|
|
420
|
+
min: options.minSizePx ? `${options.minSizePx}px` : `${options.minSizeRem}rem`
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
var a11y_min_font_size_default = rule$8;
|
|
428
|
+
|
|
429
|
+
//#endregion
|
|
430
|
+
//#region src/lint/plugin-core/rules/colorspace.ts
|
|
431
|
+
const COLORSPACE = "core/colorspace";
|
|
432
|
+
const ERROR_COLOR$1 = "COLOR";
|
|
433
|
+
const ERROR_BORDER$1 = "BORDER";
|
|
434
|
+
const ERROR_GRADIENT$1 = "GRADIENT";
|
|
435
|
+
const ERROR_SHADOW$1 = "SHADOW";
|
|
436
|
+
const rule$7 = {
|
|
437
|
+
meta: {
|
|
438
|
+
messages: {
|
|
439
|
+
[ERROR_COLOR$1]: "Color {{ id }} not in colorspace {{ colorSpace }}",
|
|
440
|
+
[ERROR_BORDER$1]: "Border {{ id }} not in colorspace {{ colorSpace }}",
|
|
441
|
+
[ERROR_GRADIENT$1]: "Gradient {{ id }} not in colorspace {{ colorSpace }}",
|
|
442
|
+
[ERROR_SHADOW$1]: "Shadow {{ id }} not in colorspace {{ colorSpace }}"
|
|
443
|
+
},
|
|
444
|
+
docs: {
|
|
445
|
+
description: "Enforce that all colors are in a specific colorspace.",
|
|
446
|
+
url: docsLink(COLORSPACE)
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
defaultOptions: { colorSpace: "srgb" },
|
|
450
|
+
create({ tokens, options, report }) {
|
|
451
|
+
if (!options.colorSpace) return;
|
|
452
|
+
for (const t of Object.values(tokens)) {
|
|
453
|
+
if (options?.ignore && isTokenMatch(t.id, options.ignore)) continue;
|
|
454
|
+
if (t.aliasOf) continue;
|
|
455
|
+
switch (t.$type) {
|
|
456
|
+
case "color": {
|
|
457
|
+
if (t.$value.colorSpace !== options.colorSpace) report({
|
|
458
|
+
messageId: ERROR_COLOR$1,
|
|
459
|
+
data: {
|
|
460
|
+
id: t.id,
|
|
461
|
+
colorSpace: options.colorSpace
|
|
462
|
+
},
|
|
463
|
+
node: t.source.node
|
|
464
|
+
});
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
case "border": {
|
|
468
|
+
if (!t.partialAliasOf?.color && t.$value.color.colorSpace !== options.colorSpace) report({
|
|
469
|
+
messageId: ERROR_BORDER$1,
|
|
470
|
+
data: {
|
|
471
|
+
id: t.id,
|
|
472
|
+
colorSpace: options.colorSpace
|
|
473
|
+
},
|
|
474
|
+
node: t.source.node
|
|
475
|
+
});
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
case "gradient": {
|
|
479
|
+
for (let stopI = 0; stopI < t.$value.length; stopI++) if (!t.partialAliasOf?.[stopI]?.color && t.$value[stopI].color.colorSpace !== options.colorSpace) report({
|
|
480
|
+
messageId: ERROR_GRADIENT$1,
|
|
481
|
+
data: {
|
|
482
|
+
id: t.id,
|
|
483
|
+
colorSpace: options.colorSpace
|
|
484
|
+
},
|
|
485
|
+
node: t.source.node
|
|
486
|
+
});
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
case "shadow": {
|
|
490
|
+
for (let shadowI = 0; shadowI < t.$value.length; shadowI++) if (!t.partialAliasOf?.[shadowI]?.color && t.$value[shadowI].color.colorSpace !== options.colorSpace) report({
|
|
491
|
+
messageId: ERROR_SHADOW$1,
|
|
492
|
+
data: {
|
|
493
|
+
id: t.id,
|
|
494
|
+
colorSpace: options.colorSpace
|
|
495
|
+
},
|
|
496
|
+
node: t.source.node
|
|
497
|
+
});
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
var colorspace_default = rule$7;
|
|
505
|
+
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region ../../node_modules/.pnpm/scule@1.3.0/node_modules/scule/dist/index.mjs
|
|
508
|
+
const NUMBER_CHAR_RE = /\d/;
|
|
509
|
+
const STR_SPLITTERS = [
|
|
510
|
+
"-",
|
|
511
|
+
"_",
|
|
512
|
+
"/",
|
|
513
|
+
"."
|
|
514
|
+
];
|
|
515
|
+
function isUppercase(char = "") {
|
|
516
|
+
if (NUMBER_CHAR_RE.test(char)) return void 0;
|
|
517
|
+
return char !== char.toLowerCase();
|
|
518
|
+
}
|
|
519
|
+
function splitByCase(str, separators) {
|
|
520
|
+
const splitters = separators ?? STR_SPLITTERS;
|
|
521
|
+
const parts = [];
|
|
522
|
+
if (!str || typeof str !== "string") return parts;
|
|
523
|
+
let buff = "";
|
|
524
|
+
let previousUpper;
|
|
525
|
+
let previousSplitter;
|
|
526
|
+
for (const char of str) {
|
|
527
|
+
const isSplitter = splitters.includes(char);
|
|
528
|
+
if (isSplitter === true) {
|
|
529
|
+
parts.push(buff);
|
|
530
|
+
buff = "";
|
|
531
|
+
previousUpper = void 0;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const isUpper = isUppercase(char);
|
|
535
|
+
if (previousSplitter === false) {
|
|
536
|
+
if (previousUpper === false && isUpper === true) {
|
|
537
|
+
parts.push(buff);
|
|
538
|
+
buff = char;
|
|
539
|
+
previousUpper = isUpper;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (previousUpper === true && isUpper === false && buff.length > 1) {
|
|
543
|
+
const lastChar = buff.at(-1);
|
|
544
|
+
parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
|
|
545
|
+
buff = lastChar + char;
|
|
546
|
+
previousUpper = isUpper;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
buff += char;
|
|
551
|
+
previousUpper = isUpper;
|
|
552
|
+
previousSplitter = isSplitter;
|
|
553
|
+
}
|
|
554
|
+
parts.push(buff);
|
|
555
|
+
return parts;
|
|
556
|
+
}
|
|
557
|
+
function upperFirst(str) {
|
|
558
|
+
return str ? str[0].toUpperCase() + str.slice(1) : "";
|
|
559
|
+
}
|
|
560
|
+
function lowerFirst(str) {
|
|
561
|
+
return str ? str[0].toLowerCase() + str.slice(1) : "";
|
|
562
|
+
}
|
|
563
|
+
function pascalCase(str, opts) {
|
|
564
|
+
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)).join("") : "";
|
|
565
|
+
}
|
|
566
|
+
function camelCase(str, opts) {
|
|
567
|
+
return lowerFirst(pascalCase(str || "", opts));
|
|
568
|
+
}
|
|
569
|
+
function kebabCase(str, joiner) {
|
|
570
|
+
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
|
|
571
|
+
}
|
|
572
|
+
function snakeCase(str) {
|
|
573
|
+
return kebabCase(str || "", "_");
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/lint/plugin-core/rules/consistent-naming.ts
|
|
578
|
+
const CONSISTENT_NAMING = "core/consistent-naming";
|
|
579
|
+
const ERROR_WRONG_FORMAT = "ERROR_WRONG_FORMAT";
|
|
580
|
+
const rule$6 = {
|
|
581
|
+
meta: {
|
|
582
|
+
messages: { [ERROR_WRONG_FORMAT]: "{{ id }} doesn’t match format {{ format }}" },
|
|
583
|
+
docs: {
|
|
584
|
+
description: "Enforce consistent naming for tokens.",
|
|
585
|
+
url: docsLink(CONSISTENT_NAMING)
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
defaultOptions: { format: "kebab-case" },
|
|
589
|
+
create({ tokens, options, report }) {
|
|
590
|
+
const basicFormatter = {
|
|
591
|
+
"kebab-case": kebabCase,
|
|
592
|
+
camelCase,
|
|
593
|
+
PascalCase: pascalCase,
|
|
594
|
+
snake_case: snakeCase,
|
|
595
|
+
SCREAMING_SNAKE_CASE: (name) => snakeCase(name).toLocaleUpperCase()
|
|
596
|
+
}[String(options.format)];
|
|
597
|
+
for (const t of Object.values(tokens)) if (basicFormatter) {
|
|
598
|
+
const parts = t.id.split(".");
|
|
599
|
+
if (!parts.every((part) => basicFormatter(part) === part)) report({
|
|
600
|
+
messageId: ERROR_WRONG_FORMAT,
|
|
601
|
+
data: {
|
|
602
|
+
id: t.id,
|
|
603
|
+
format: options.format
|
|
604
|
+
},
|
|
605
|
+
node: t.source.node
|
|
606
|
+
});
|
|
607
|
+
} else if (typeof options.format === "function") {
|
|
608
|
+
const result = options.format(t.id);
|
|
609
|
+
if (result) report({
|
|
610
|
+
messageId: ERROR_WRONG_FORMAT,
|
|
611
|
+
data: {
|
|
612
|
+
id: t.id,
|
|
613
|
+
format: "(custom)"
|
|
614
|
+
},
|
|
615
|
+
node: t.source.node
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
var consistent_naming_default = rule$6;
|
|
621
|
+
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/lint/plugin-core/rules/descriptions.ts
|
|
624
|
+
const DESCRIPTIONS = "core/descriptions";
|
|
625
|
+
const ERROR_MISSING_DESCRIPTION = "MISSING_DESCRIPTION";
|
|
626
|
+
const rule$5 = {
|
|
627
|
+
meta: {
|
|
628
|
+
messages: { [ERROR_MISSING_DESCRIPTION]: "{{ id }} missing description" },
|
|
629
|
+
docs: {
|
|
630
|
+
description: "Enforce tokens have descriptions.",
|
|
631
|
+
url: docsLink(DESCRIPTIONS)
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
defaultOptions: {},
|
|
635
|
+
create({ tokens, options, report }) {
|
|
636
|
+
for (const t of Object.values(tokens)) {
|
|
637
|
+
if (options.ignore && isTokenMatch(t.id, options.ignore)) continue;
|
|
638
|
+
if (!t.$description) report({
|
|
639
|
+
messageId: ERROR_MISSING_DESCRIPTION,
|
|
640
|
+
data: { id: t.id },
|
|
641
|
+
node: t.source.node
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
var descriptions_default = rule$5;
|
|
647
|
+
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region src/lint/plugin-core/rules/duplicate-values.ts
|
|
650
|
+
const DUPLICATE_VALUES = "core/duplicate-values";
|
|
651
|
+
const ERROR_DUPLICATE_VALUE = "ERROR_DUPLICATE_VALUE";
|
|
652
|
+
const rule$4 = {
|
|
653
|
+
meta: {
|
|
654
|
+
messages: { [ERROR_DUPLICATE_VALUE]: "{{ id }} declared a duplicate value" },
|
|
655
|
+
docs: {
|
|
656
|
+
description: "Enforce tokens can’t redeclare the same value (excludes aliases).",
|
|
657
|
+
url: docsLink(DUPLICATE_VALUES)
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
defaultOptions: {},
|
|
661
|
+
create({ report, tokens, options }) {
|
|
662
|
+
const values = {};
|
|
663
|
+
for (const t of Object.values(tokens)) {
|
|
664
|
+
if (options.ignore && isTokenMatch(t.id, options.ignore)) continue;
|
|
665
|
+
if (!values[t.$type]) values[t.$type] = /* @__PURE__ */ new Set();
|
|
666
|
+
if (t.$type === "boolean" || t.$type === "duration" || t.$type === "fontWeight" || t.$type === "link" || t.$type === "number" || t.$type === "string") {
|
|
667
|
+
if (typeof t.aliasOf === "string" && isAlias(t.aliasOf)) continue;
|
|
668
|
+
if (values[t.$type]?.has(t.$value)) report({
|
|
669
|
+
messageId: ERROR_DUPLICATE_VALUE,
|
|
670
|
+
data: { id: t.id },
|
|
671
|
+
node: t.source.node
|
|
672
|
+
});
|
|
673
|
+
values[t.$type]?.add(t.$value);
|
|
674
|
+
} else {
|
|
675
|
+
for (const v of values[t.$type].values() ?? []) if (JSON.stringify(t.$value) === JSON.stringify(v)) {
|
|
676
|
+
report({
|
|
677
|
+
messageId: ERROR_DUPLICATE_VALUE,
|
|
678
|
+
data: { id: t.id },
|
|
679
|
+
node: t.source.node
|
|
680
|
+
});
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
values[t.$type].add(t.$value);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
var duplicate_values_default = rule$4;
|
|
689
|
+
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region src/lint/plugin-core/rules/max-gamut.ts
|
|
692
|
+
const MAX_GAMUT = "core/max-gamut";
|
|
693
|
+
const TOLERANCE = 1e-6;
|
|
694
|
+
/** is a Culori-parseable color within the specified gamut? */
|
|
695
|
+
function isWithinGamut(color, gamut) {
|
|
696
|
+
const parsed = tokenToCulori(color);
|
|
697
|
+
if (!parsed) return false;
|
|
698
|
+
if ([
|
|
699
|
+
"rgb",
|
|
700
|
+
"hsl",
|
|
701
|
+
"hsv",
|
|
702
|
+
"hwb"
|
|
703
|
+
].includes(parsed.mode)) return true;
|
|
704
|
+
const clamped = clampChroma(parsed, parsed.mode, gamut === "srgb" ? "rgb" : gamut);
|
|
705
|
+
return isWithinThreshold(parsed, clamped);
|
|
706
|
+
}
|
|
707
|
+
/** is Color A close enough to Color B? */
|
|
708
|
+
function isWithinThreshold(a, b, tolerance = TOLERANCE) {
|
|
709
|
+
for (const k in a) {
|
|
710
|
+
if (k === "mode" || k === "alpha") continue;
|
|
711
|
+
if (!(k in b)) throw new Error(`Can’t compare ${a.mode} to ${b.mode}`);
|
|
712
|
+
if (Math.abs(a[k] - b[k]) > tolerance) return false;
|
|
713
|
+
}
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
const ERROR_COLOR = "COLOR";
|
|
717
|
+
const ERROR_BORDER = "BORDER";
|
|
718
|
+
const ERROR_GRADIENT = "GRADIENT";
|
|
719
|
+
const ERROR_SHADOW = "SHADOW";
|
|
720
|
+
const rule$3 = {
|
|
721
|
+
meta: {
|
|
722
|
+
messages: {
|
|
723
|
+
[ERROR_COLOR]: "Color {{ id }} is outside {{ gamut }} gamut",
|
|
724
|
+
[ERROR_BORDER]: "Border {{ id }} is outside {{ gamut }} gamut",
|
|
725
|
+
[ERROR_GRADIENT]: "Gradient {{ id }} is outside {{ gamut }} gamut",
|
|
726
|
+
[ERROR_SHADOW]: "Shadow {{ id }} is outside {{ gamut }} gamut"
|
|
727
|
+
},
|
|
728
|
+
docs: {
|
|
729
|
+
description: "Enforce colors are within the specified gamut.",
|
|
730
|
+
url: docsLink(MAX_GAMUT)
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
defaultOptions: { gamut: "rec2020" },
|
|
734
|
+
create({ tokens, options, report }) {
|
|
735
|
+
if (!options?.gamut) return;
|
|
736
|
+
if (options.gamut !== "srgb" && options.gamut !== "p3" && options.gamut !== "rec2020") throw new Error(`Unknown gamut "${options.gamut}". Options are "srgb", "p3", or "rec2020"`);
|
|
737
|
+
for (const t of Object.values(tokens)) {
|
|
738
|
+
if (options.ignore && isTokenMatch(t.id, options.ignore)) continue;
|
|
739
|
+
if (t.aliasOf) continue;
|
|
740
|
+
switch (t.$type) {
|
|
741
|
+
case "color": {
|
|
742
|
+
if (!isWithinGamut(t.$value, options.gamut)) report({
|
|
743
|
+
messageId: ERROR_COLOR,
|
|
744
|
+
data: {
|
|
745
|
+
id: t.id,
|
|
746
|
+
gamut: options.gamut
|
|
747
|
+
},
|
|
748
|
+
node: t.source.node
|
|
749
|
+
});
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
case "border": {
|
|
753
|
+
if (!t.partialAliasOf?.color && !isWithinGamut(t.$value.color, options.gamut)) report({
|
|
754
|
+
messageId: ERROR_BORDER,
|
|
755
|
+
data: {
|
|
756
|
+
id: t.id,
|
|
757
|
+
gamut: options.gamut
|
|
758
|
+
},
|
|
759
|
+
node: t.source.node
|
|
760
|
+
});
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
case "gradient": {
|
|
764
|
+
for (let stopI = 0; stopI < t.$value.length; stopI++) if (!t.partialAliasOf?.[stopI]?.color && !isWithinGamut(t.$value[stopI].color, options.gamut)) report({
|
|
765
|
+
messageId: ERROR_GRADIENT,
|
|
766
|
+
data: {
|
|
767
|
+
id: t.id,
|
|
768
|
+
gamut: options.gamut
|
|
769
|
+
},
|
|
770
|
+
node: t.source.node
|
|
771
|
+
});
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
case "shadow": {
|
|
775
|
+
for (let shadowI = 0; shadowI < t.$value.length; shadowI++) if (!t.partialAliasOf?.[shadowI]?.color && !isWithinGamut(t.$value[shadowI].color, options.gamut)) report({
|
|
776
|
+
messageId: ERROR_SHADOW,
|
|
777
|
+
data: {
|
|
778
|
+
id: t.id,
|
|
779
|
+
gamut: options.gamut
|
|
780
|
+
},
|
|
781
|
+
node: t.source.node
|
|
782
|
+
});
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
var max_gamut_default = rule$3;
|
|
790
|
+
|
|
791
|
+
//#endregion
|
|
792
|
+
//#region src/lint/plugin-core/rules/required-children.ts
|
|
793
|
+
const REQUIRED_CHILDREN = "core/required-children";
|
|
794
|
+
const ERROR_EMPTY_MATCH = "EMPTY_MATCH";
|
|
795
|
+
const ERROR_MISSING_REQUIRED_TOKENS = "MISSING_REQUIRED_TOKENS";
|
|
796
|
+
const ERROR_MISSING_REQUIRED_GROUP = "MISSING_REQUIRED_GROUP";
|
|
797
|
+
const rule$2 = {
|
|
798
|
+
meta: {
|
|
799
|
+
messages: {
|
|
800
|
+
[ERROR_EMPTY_MATCH]: "No tokens matched {{ matcher }}",
|
|
801
|
+
[ERROR_MISSING_REQUIRED_TOKENS]: "Match {{ index }}: some groups missing required token \"{{ token }}\"",
|
|
802
|
+
[ERROR_MISSING_REQUIRED_GROUP]: "Match {{ index }}: some tokens missing required group \"{{ group }}\""
|
|
803
|
+
},
|
|
804
|
+
docs: {
|
|
805
|
+
description: "Enforce token groups have specific children, whether tokens and/or groups.",
|
|
806
|
+
url: docsLink(REQUIRED_CHILDREN)
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
defaultOptions: { matches: [] },
|
|
810
|
+
create({ tokens, options, report }) {
|
|
811
|
+
if (!options.matches?.length) throw new Error("Invalid config. Missing `matches: […]`");
|
|
812
|
+
for (let matchI = 0; matchI < options.matches.length; matchI++) {
|
|
813
|
+
const { match, requiredTokens, requiredGroups } = options.matches[matchI];
|
|
814
|
+
if (!match.length) throw new Error(`Match ${matchI}: must declare \`match: […]\``);
|
|
815
|
+
if (!requiredTokens?.length && !requiredGroups?.length) throw new Error(`Match ${matchI}: must declare either \`requiredTokens: […]\` or \`requiredGroups: […]\``);
|
|
816
|
+
const matchGroups = [];
|
|
817
|
+
const matchTokens = [];
|
|
818
|
+
let tokensMatched = false;
|
|
819
|
+
for (const t of Object.values(tokens)) {
|
|
820
|
+
if (!isTokenMatch(t.id, match)) continue;
|
|
821
|
+
tokensMatched = true;
|
|
822
|
+
const groups = t.id.split(".");
|
|
823
|
+
matchTokens.push(groups.pop());
|
|
824
|
+
matchGroups.push(...groups);
|
|
825
|
+
}
|
|
826
|
+
if (!tokensMatched) {
|
|
827
|
+
report({
|
|
828
|
+
messageId: ERROR_EMPTY_MATCH,
|
|
829
|
+
data: { matcher: JSON.stringify(match) }
|
|
830
|
+
});
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (requiredTokens) {
|
|
834
|
+
for (const id of requiredTokens) if (!matchTokens.includes(id)) report({
|
|
835
|
+
messageId: ERROR_MISSING_REQUIRED_TOKENS,
|
|
836
|
+
data: {
|
|
837
|
+
index: matchI,
|
|
838
|
+
token: id
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
if (requiredGroups) {
|
|
843
|
+
for (const groupName of requiredGroups) if (!matchGroups.includes(groupName)) report({
|
|
844
|
+
messageId: ERROR_MISSING_REQUIRED_GROUP,
|
|
845
|
+
data: {
|
|
846
|
+
index: matchI,
|
|
847
|
+
group: groupName
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
var required_children_default = rule$2;
|
|
855
|
+
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/lint/plugin-core/rules/required-modes.ts
|
|
858
|
+
const REQUIRED_MODES = "core/required-modes";
|
|
859
|
+
const rule$1 = {
|
|
860
|
+
meta: { docs: {
|
|
861
|
+
description: "Enforce certain tokens have specific modes.",
|
|
862
|
+
url: docsLink(REQUIRED_MODES)
|
|
863
|
+
} },
|
|
864
|
+
defaultOptions: { matches: [] },
|
|
865
|
+
create({ tokens, options, report }) {
|
|
866
|
+
if (!options?.matches?.length) throw new Error("Invalid config. Missing `matches: […]`");
|
|
867
|
+
for (let matchI = 0; matchI < options.matches.length; matchI++) {
|
|
868
|
+
const { match, modes } = options.matches[matchI];
|
|
869
|
+
if (!match.length) throw new Error(`Match ${matchI}: must declare \`match: […]\``);
|
|
870
|
+
if (!modes?.length) throw new Error(`Match ${matchI}: must declare \`modes: […]\``);
|
|
871
|
+
let tokensMatched = false;
|
|
872
|
+
for (const t of Object.values(tokens)) {
|
|
873
|
+
if (!isTokenMatch(t.id, match)) continue;
|
|
874
|
+
tokensMatched = true;
|
|
875
|
+
for (const mode of modes) if (!t.mode?.[mode]) report({
|
|
876
|
+
message: `Token ${t.id}: missing required mode "${mode}"`,
|
|
877
|
+
node: t.source.node
|
|
878
|
+
});
|
|
879
|
+
if (!tokensMatched) report({
|
|
880
|
+
message: `Match "${matchI}": no tokens matched ${JSON.stringify(match)}`,
|
|
881
|
+
node: t.source.node
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
var required_modes_default = rule$1;
|
|
888
|
+
|
|
889
|
+
//#endregion
|
|
890
|
+
//#region src/lint/plugin-core/rules/required-typography-properties.ts
|
|
891
|
+
const REQUIRED_TYPOGRAPHY_PROPERTIES = "core/required-typography-properties";
|
|
892
|
+
const rule = {
|
|
893
|
+
meta: { docs: {
|
|
894
|
+
description: "Enforce typography tokens have required properties.",
|
|
895
|
+
url: docsLink(REQUIRED_TYPOGRAPHY_PROPERTIES)
|
|
896
|
+
} },
|
|
897
|
+
defaultOptions: { properties: [] },
|
|
898
|
+
create({ tokens, options, report }) {
|
|
899
|
+
if (!options) return;
|
|
900
|
+
if (!options.properties.length) throw new Error(`"properties" can’t be empty`);
|
|
901
|
+
for (const t of Object.values(tokens)) {
|
|
902
|
+
if (options.ignore && isTokenMatch(t.id, options.ignore)) continue;
|
|
903
|
+
if (t.$type !== "typography") continue;
|
|
904
|
+
if (t.aliasOf) continue;
|
|
905
|
+
for (const p of options.properties) if (!t.partialAliasOf?.[p] && !(p in t.$value)) report({
|
|
906
|
+
message: `${t.id} missing required typographic property "${p}"`,
|
|
907
|
+
node: t.source.node
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
var required_typography_properties_default = rule;
|
|
913
|
+
|
|
914
|
+
//#endregion
|
|
915
|
+
//#region src/lint/plugin-core/index.ts
|
|
916
|
+
function coreLintPlugin() {
|
|
917
|
+
return {
|
|
918
|
+
name: "@terrazzo/plugin-lint-core",
|
|
919
|
+
lint() {
|
|
920
|
+
return {
|
|
921
|
+
[COLORSPACE]: colorspace_default,
|
|
922
|
+
[CONSISTENT_NAMING]: consistent_naming_default,
|
|
923
|
+
[DESCRIPTIONS]: descriptions_default,
|
|
924
|
+
[DUPLICATE_VALUES]: duplicate_values_default,
|
|
925
|
+
[MAX_GAMUT]: max_gamut_default,
|
|
926
|
+
[REQUIRED_CHILDREN]: required_children_default,
|
|
927
|
+
[REQUIRED_MODES]: required_modes_default,
|
|
928
|
+
[REQUIRED_TYPOGRAPHY_PROPERTIES]: required_typography_properties_default,
|
|
929
|
+
[A11Y_MIN_CONTRAST]: a11y_min_contrast_default,
|
|
930
|
+
[A11Y_MIN_FONT_SIZE]: a11y_min_font_size_default
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
//#endregion
|
|
937
|
+
//#region src/config.ts
|
|
938
|
+
const TRAILING_SLASH_RE = /\/*$/;
|
|
939
|
+
/**
|
|
940
|
+
* Validate and normalize a config
|
|
941
|
+
*/
|
|
942
|
+
function defineConfig(rawConfig, { logger = new Logger(), cwd } = {}) {
|
|
943
|
+
const configStart = performance.now();
|
|
944
|
+
if (!cwd) logger.error({
|
|
945
|
+
group: "config",
|
|
946
|
+
label: "core",
|
|
947
|
+
message: "defineConfig() missing `cwd` for JS API"
|
|
948
|
+
});
|
|
949
|
+
const config = merge({}, rawConfig);
|
|
950
|
+
normalizeTokens({
|
|
951
|
+
rawConfig,
|
|
952
|
+
config,
|
|
953
|
+
logger,
|
|
954
|
+
cwd
|
|
955
|
+
});
|
|
956
|
+
normalizeOutDir({
|
|
957
|
+
config,
|
|
958
|
+
cwd,
|
|
959
|
+
logger
|
|
960
|
+
});
|
|
961
|
+
normalizePlugins({
|
|
962
|
+
config,
|
|
963
|
+
logger
|
|
964
|
+
});
|
|
965
|
+
normalizeLint({
|
|
966
|
+
config,
|
|
967
|
+
logger
|
|
968
|
+
});
|
|
969
|
+
normalizeIgnore({
|
|
970
|
+
config,
|
|
971
|
+
logger
|
|
972
|
+
});
|
|
973
|
+
for (const plugin of config.plugins) plugin.config?.({ ...config });
|
|
974
|
+
logger.debug({
|
|
975
|
+
group: "parser",
|
|
976
|
+
label: "config",
|
|
977
|
+
message: "Finish config validation",
|
|
978
|
+
timing: performance.now() - configStart
|
|
979
|
+
});
|
|
980
|
+
return config;
|
|
981
|
+
}
|
|
982
|
+
/** Normalize config.tokens */
|
|
983
|
+
function normalizeTokens({ rawConfig, config, logger, cwd }) {
|
|
984
|
+
if (rawConfig.tokens === void 0) config.tokens = ["./tokens.json"];
|
|
985
|
+
else if (typeof rawConfig.tokens === "string") config.tokens = [rawConfig.tokens];
|
|
986
|
+
else if (Array.isArray(rawConfig.tokens)) {
|
|
987
|
+
config.tokens = [];
|
|
988
|
+
for (const file of rawConfig.tokens) if (typeof file === "string" || file instanceof URL) config.tokens.push(file);
|
|
989
|
+
else logger.error({
|
|
990
|
+
group: "config",
|
|
991
|
+
label: "tokens",
|
|
992
|
+
message: `Expected array of strings, encountered ${JSON.stringify(file)}`
|
|
993
|
+
});
|
|
994
|
+
} else logger.error({
|
|
995
|
+
group: "config",
|
|
996
|
+
label: "tokens",
|
|
997
|
+
message: `Expected string or array of strings, received ${typeof rawConfig.tokens}`
|
|
998
|
+
});
|
|
999
|
+
for (let i = 0; i < config.tokens.length; i++) {
|
|
1000
|
+
const filepath = config.tokens[i];
|
|
1001
|
+
if (filepath instanceof URL) continue;
|
|
1002
|
+
try {
|
|
1003
|
+
config.tokens[i] = new URL(filepath, cwd);
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
logger.error({
|
|
1006
|
+
group: "config",
|
|
1007
|
+
label: "tokens",
|
|
1008
|
+
message: `Invalid URL ${filepath}`
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
/** Normalize config.outDir */
|
|
1014
|
+
function normalizeOutDir({ config, cwd, logger }) {
|
|
1015
|
+
if (config.outDir instanceof URL) {} else if (typeof config.outDir === "undefined") config.outDir = new URL("./tokens/", cwd);
|
|
1016
|
+
else if (typeof config.outDir !== "string") logger.error({
|
|
1017
|
+
group: "config",
|
|
1018
|
+
label: "outDir",
|
|
1019
|
+
message: `Expected string, received ${JSON.stringify(config.outDir)}`
|
|
1020
|
+
});
|
|
1021
|
+
else {
|
|
1022
|
+
config.outDir = new URL(config.outDir, cwd);
|
|
1023
|
+
config.outDir = new URL(config.outDir.href.replace(TRAILING_SLASH_RE, "/"));
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/** Normalize config.plugins */
|
|
1027
|
+
function normalizePlugins({ config, logger }) {
|
|
1028
|
+
if (typeof config.plugins === "undefined") config.plugins = [];
|
|
1029
|
+
if (!Array.isArray(config.plugins)) logger.error({
|
|
1030
|
+
group: "config",
|
|
1031
|
+
label: "plugins",
|
|
1032
|
+
message: `Expected array of plugins, received ${JSON.stringify(config.plugins)}`
|
|
1033
|
+
});
|
|
1034
|
+
config.plugins.push(coreLintPlugin());
|
|
1035
|
+
for (let n = 0; n < config.plugins.length; n++) {
|
|
1036
|
+
const plugin = config.plugins[n];
|
|
1037
|
+
if (typeof plugin !== "object") logger.error({
|
|
1038
|
+
group: "config",
|
|
1039
|
+
label: `plugin[${n}]`,
|
|
1040
|
+
message: `Expected output plugin, received ${JSON.stringify(plugin)}`
|
|
1041
|
+
});
|
|
1042
|
+
else if (!plugin.name) logger.error({
|
|
1043
|
+
group: "config",
|
|
1044
|
+
label: `plugin[${n}]`,
|
|
1045
|
+
message: `Missing "name"`
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
config.plugins.sort((a, b) => {
|
|
1049
|
+
if (a.enforce === "pre" && b.enforce !== "pre") return -1;
|
|
1050
|
+
else if (a.enforce === "post" && b.enforce !== "post") return 1;
|
|
1051
|
+
return 0;
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
function normalizeLint({ config, logger }) {
|
|
1055
|
+
if (config.lint !== void 0) {
|
|
1056
|
+
if (config.lint === null || typeof config.lint !== "object" || Array.isArray(config.lint)) logger.error({
|
|
1057
|
+
group: "config",
|
|
1058
|
+
label: "lint",
|
|
1059
|
+
message: "Must be an object"
|
|
1060
|
+
});
|
|
1061
|
+
if (!config.lint.build) config.lint.build = { enabled: true };
|
|
1062
|
+
if (config.lint.build.enabled !== void 0) {
|
|
1063
|
+
if (typeof config.lint.build.enabled !== "boolean") logger.error({
|
|
1064
|
+
group: "config",
|
|
1065
|
+
label: "lint › build › enabled",
|
|
1066
|
+
message: `Expected boolean, received ${JSON.stringify(config.lint.build)}`
|
|
1067
|
+
});
|
|
1068
|
+
} else config.lint.build.enabled = true;
|
|
1069
|
+
if (config.lint.rules === void 0) config.lint.rules = {};
|
|
1070
|
+
else {
|
|
1071
|
+
if (config.lint.rules === null || typeof config.lint.rules !== "object" || Array.isArray(config.lint.rules)) {
|
|
1072
|
+
logger.error({
|
|
1073
|
+
group: "config",
|
|
1074
|
+
label: "lint › rules",
|
|
1075
|
+
message: `Expected object, received ${JSON.stringify(config.lint.rules)}`
|
|
1076
|
+
});
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const allRules = /* @__PURE__ */ new Map();
|
|
1080
|
+
for (const plugin of config.plugins) {
|
|
1081
|
+
if (typeof plugin.lint !== "function") continue;
|
|
1082
|
+
const pluginRules = plugin.lint();
|
|
1083
|
+
if (!pluginRules || Array.isArray(pluginRules) || typeof pluginRules !== "object") {
|
|
1084
|
+
logger.error({
|
|
1085
|
+
group: "config",
|
|
1086
|
+
label: `plugin › ${plugin.name}`,
|
|
1087
|
+
message: `Expected object for lint() received ${JSON.stringify(pluginRules)}`
|
|
1088
|
+
});
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
for (const rule$10 of Object.keys(pluginRules)) {
|
|
1092
|
+
if (allRules.get(rule$10) && allRules.get(rule$10) !== plugin.name) logger.error({
|
|
1093
|
+
group: "config",
|
|
1094
|
+
label: `plugin › ${plugin.name}`,
|
|
1095
|
+
message: `Duplicate rule ${rule$10} already registered by plugin ${allRules.get(rule$10)}`
|
|
1096
|
+
});
|
|
1097
|
+
allRules.set(rule$10, plugin.name);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
for (const id of Object.keys(config.lint.rules)) {
|
|
1101
|
+
if (!allRules.has(id)) logger.error({
|
|
1102
|
+
group: "config",
|
|
1103
|
+
label: `lint › rule › ${id}`,
|
|
1104
|
+
message: "Unknown rule. Is the plugin installed?"
|
|
1105
|
+
});
|
|
1106
|
+
const value = config.lint.rules[id];
|
|
1107
|
+
let severity = "off";
|
|
1108
|
+
let options;
|
|
1109
|
+
if (typeof value === "number" || typeof value === "string") severity = value;
|
|
1110
|
+
else if (Array.isArray(value)) {
|
|
1111
|
+
severity = value[0];
|
|
1112
|
+
options = value[1];
|
|
1113
|
+
} else if (value !== void 0) logger.error({
|
|
1114
|
+
group: "config",
|
|
1115
|
+
label: `lint › rule › ${id}`,
|
|
1116
|
+
message: `Invalid eyntax. Expected \`string | number | Array\`, received ${JSON.stringify(value)}}`
|
|
1117
|
+
});
|
|
1118
|
+
config.lint.rules[id] = [severity, options];
|
|
1119
|
+
if (typeof severity === "number") {
|
|
1120
|
+
if (severity !== 0 && severity !== 1 && severity !== 2) logger.error({
|
|
1121
|
+
group: "config",
|
|
1122
|
+
label: `lint › rule › ${id}`,
|
|
1123
|
+
message: `Invalid number ${severity}. Specify 0 (off), 1 (warn), or 2 (error).`
|
|
1124
|
+
});
|
|
1125
|
+
config.lint.rules[id][0] = [
|
|
1126
|
+
"off",
|
|
1127
|
+
"warn",
|
|
1128
|
+
"error"
|
|
1129
|
+
][severity];
|
|
1130
|
+
} else if (typeof severity === "string") {
|
|
1131
|
+
if (severity !== "off" && severity !== "warn" && severity !== "error") logger.error({
|
|
1132
|
+
group: "config",
|
|
1133
|
+
label: `lint › rule › ${id}`,
|
|
1134
|
+
message: `Invalid string ${JSON.stringify(severity)}. Specify "off", "warn", or "error".`
|
|
1135
|
+
});
|
|
1136
|
+
} else if (value !== null) logger.error({
|
|
1137
|
+
group: "config",
|
|
1138
|
+
label: `lint › rule › ${id}`,
|
|
1139
|
+
message: `Expected string or number, received ${JSON.stringify(value)}`
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
} else config.lint = {
|
|
1144
|
+
build: { enabled: true },
|
|
1145
|
+
rules: {}
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
function normalizeIgnore({ config, logger }) {
|
|
1149
|
+
if (!config.ignore) config.ignore = {};
|
|
1150
|
+
config.ignore.tokens ??= [];
|
|
1151
|
+
config.ignore.deprecated ??= false;
|
|
1152
|
+
if (!Array.isArray(config.ignore.tokens) || config.ignore.tokens.some((x) => typeof x !== "string")) logger.error({
|
|
1153
|
+
group: "config",
|
|
1154
|
+
label: "ignore › tokens",
|
|
1155
|
+
message: `Expected array of strings, received ${JSON.stringify(config.ignore.tokens)}`
|
|
1156
|
+
});
|
|
1157
|
+
if (typeof config.ignore.deprecated !== "boolean") logger.error({
|
|
1158
|
+
group: "config",
|
|
1159
|
+
label: "ignore › deprecated",
|
|
1160
|
+
message: `Expected boolean, received ${JSON.stringify(config.ignore.deprecated)}`
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
/** Merge configs */
|
|
1164
|
+
function mergeConfigs(a, b) {
|
|
1165
|
+
return merge(a, b);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
//#endregion
|
|
1169
|
+
//#region src/lint/index.ts
|
|
1170
|
+
const listFormat$1 = new Intl.ListFormat("en-us");
|
|
1171
|
+
async function lintRunner({ tokens, filename, config = {}, src, logger }) {
|
|
1172
|
+
const { plugins = [], lint } = config;
|
|
1173
|
+
const unusedLintRules = Object.keys(lint?.rules ?? {});
|
|
1174
|
+
for (const plugin of plugins) if (typeof plugin.lint === "function") {
|
|
1175
|
+
const s = performance.now();
|
|
1176
|
+
const linter = plugin.lint();
|
|
1177
|
+
const errors = [];
|
|
1178
|
+
const warnings = [];
|
|
1179
|
+
await Promise.all(Object.entries(linter).map(async ([id, rule$10]) => {
|
|
1180
|
+
if (!(id in lint.rules) || lint.rules[id] === null) return;
|
|
1181
|
+
const [severity, options] = lint.rules[id];
|
|
1182
|
+
if (severity === "off") return;
|
|
1183
|
+
await rule$10.create({
|
|
1184
|
+
id,
|
|
1185
|
+
report(descriptor) {
|
|
1186
|
+
let message = "";
|
|
1187
|
+
if (!descriptor.message && !descriptor.messageId) logger.error({
|
|
1188
|
+
group: "lint",
|
|
1189
|
+
label: `${plugin.name} › lint › ${id}`,
|
|
1190
|
+
message: "Unable to report error: missing message or messageId"
|
|
1191
|
+
});
|
|
1192
|
+
if (descriptor.message) message = descriptor.message;
|
|
1193
|
+
else {
|
|
1194
|
+
if (!(descriptor.messageId in (rule$10.meta?.messages ?? {}))) logger.error({
|
|
1195
|
+
group: "lint",
|
|
1196
|
+
label: `${plugin.name} › lint › ${id}`,
|
|
1197
|
+
message: `messageId "${descriptor.messageId}" does not exist`
|
|
1198
|
+
});
|
|
1199
|
+
message = rule$10.meta?.messages?.[descriptor.messageId] ?? "";
|
|
1200
|
+
}
|
|
1201
|
+
if (descriptor.data && typeof descriptor.data === "object") for (const [k, v] of Object.entries(descriptor.data)) {
|
|
1202
|
+
const formatted = [
|
|
1203
|
+
"string",
|
|
1204
|
+
"number",
|
|
1205
|
+
"boolean"
|
|
1206
|
+
].includes(typeof v) ? String(v) : JSON.stringify(v);
|
|
1207
|
+
message = message.replace(/{{[^}]+}}/g, (inner) => {
|
|
1208
|
+
const key = inner.substring(2, inner.length - 2).trim();
|
|
1209
|
+
return key === k ? formatted : inner;
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
(severity === "error" ? errors : warnings).push({
|
|
1213
|
+
group: "lint",
|
|
1214
|
+
label: id,
|
|
1215
|
+
message,
|
|
1216
|
+
filename,
|
|
1217
|
+
node: descriptor.node,
|
|
1218
|
+
src: descriptor.source?.src
|
|
1219
|
+
});
|
|
1220
|
+
},
|
|
1221
|
+
tokens,
|
|
1222
|
+
filename,
|
|
1223
|
+
src,
|
|
1224
|
+
options: merge(rule$10.meta?.defaultOptions ?? [], rule$10.defaultOptions ?? [], options)
|
|
1225
|
+
});
|
|
1226
|
+
const unusedLintRuleI = unusedLintRules.indexOf(id);
|
|
1227
|
+
if (unusedLintRuleI !== -1) unusedLintRules.splice(unusedLintRuleI, 1);
|
|
1228
|
+
}));
|
|
1229
|
+
for (const error of errors) logger.error({
|
|
1230
|
+
...error,
|
|
1231
|
+
continueOnError: true
|
|
1232
|
+
});
|
|
1233
|
+
for (const warning of warnings) logger.warn(warning);
|
|
1234
|
+
logger.debug({
|
|
1235
|
+
group: "lint",
|
|
1236
|
+
label: plugin.name,
|
|
1237
|
+
message: "Finished",
|
|
1238
|
+
timing: performance.now() - s
|
|
1239
|
+
});
|
|
1240
|
+
if (errors.length) {
|
|
1241
|
+
const counts = [pluralize(errors.length, "error", "errors")];
|
|
1242
|
+
if (warnings.length) counts.push(pluralize(warnings.length, "warning", "warnings"));
|
|
1243
|
+
logger.error({
|
|
1244
|
+
group: "lint",
|
|
1245
|
+
message: `Lint failed with ${listFormat$1.format(counts)}`,
|
|
1246
|
+
label: plugin.name,
|
|
1247
|
+
continueOnError: false
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
for (const unusedRule of unusedLintRules) logger.warn({
|
|
1252
|
+
group: "lint",
|
|
1253
|
+
label: "lint",
|
|
1254
|
+
message: `Unknown lint rule "${unusedRule}"`
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
//#endregion
|
|
1259
|
+
//#region ../../node_modules/.pnpm/@humanwhocodes+momoa@3.3.8/node_modules/@humanwhocodes/momoa/dist/momoa.js
|
|
1260
|
+
/**
|
|
1261
|
+
* @fileoverview Character codes.
|
|
1262
|
+
* @author Nicholas C. Zakas
|
|
1263
|
+
*/
|
|
1264
|
+
const CHAR_0 = 48;
|
|
1265
|
+
const CHAR_1 = 49;
|
|
1266
|
+
const CHAR_9 = 57;
|
|
1267
|
+
const CHAR_BACKSLASH = 92;
|
|
1268
|
+
const CHAR_DOLLAR = 36;
|
|
1269
|
+
const CHAR_DOT = 46;
|
|
1270
|
+
const CHAR_DOUBLE_QUOTE = 34;
|
|
1271
|
+
const CHAR_LOWER_A = 97;
|
|
1272
|
+
const CHAR_LOWER_E = 101;
|
|
1273
|
+
const CHAR_LOWER_F = 102;
|
|
1274
|
+
const CHAR_LOWER_N = 110;
|
|
1275
|
+
const CHAR_LOWER_T = 116;
|
|
1276
|
+
const CHAR_LOWER_U = 117;
|
|
1277
|
+
const CHAR_LOWER_X = 120;
|
|
1278
|
+
const CHAR_LOWER_Z = 122;
|
|
1279
|
+
const CHAR_MINUS = 45;
|
|
1280
|
+
const CHAR_NEWLINE = 10;
|
|
1281
|
+
const CHAR_PLUS = 43;
|
|
1282
|
+
const CHAR_RETURN = 13;
|
|
1283
|
+
const CHAR_SINGLE_QUOTE = 39;
|
|
1284
|
+
const CHAR_SLASH = 47;
|
|
1285
|
+
const CHAR_SPACE = 32;
|
|
1286
|
+
const CHAR_TAB = 9;
|
|
1287
|
+
const CHAR_UNDERSCORE = 95;
|
|
1288
|
+
const CHAR_UPPER_A = 65;
|
|
1289
|
+
const CHAR_UPPER_E = 69;
|
|
1290
|
+
const CHAR_UPPER_F = 70;
|
|
1291
|
+
const CHAR_UPPER_N = 78;
|
|
1292
|
+
const CHAR_UPPER_X = 88;
|
|
1293
|
+
const CHAR_UPPER_Z = 90;
|
|
1294
|
+
const CHAR_LOWER_B = 98;
|
|
1295
|
+
const CHAR_LOWER_R = 114;
|
|
1296
|
+
const CHAR_LOWER_V = 118;
|
|
1297
|
+
const CHAR_LINE_SEPARATOR = 8232;
|
|
1298
|
+
const CHAR_PARAGRAPH_SEPARATOR = 8233;
|
|
1299
|
+
const CHAR_UPPER_I = 73;
|
|
1300
|
+
const CHAR_STAR = 42;
|
|
1301
|
+
const CHAR_VTAB = 11;
|
|
1302
|
+
const CHAR_FORM_FEED = 12;
|
|
1303
|
+
const CHAR_NBSP = 160;
|
|
1304
|
+
const CHAR_BOM = 65279;
|
|
1305
|
+
const CHAR_NON_BREAKING_SPACE = 160;
|
|
1306
|
+
const CHAR_EN_QUAD = 8192;
|
|
1307
|
+
const CHAR_EM_QUAD = 8193;
|
|
1308
|
+
const CHAR_EN_SPACE = 8194;
|
|
1309
|
+
const CHAR_EM_SPACE = 8195;
|
|
1310
|
+
const CHAR_THREE_PER_EM_SPACE = 8196;
|
|
1311
|
+
const CHAR_FOUR_PER_EM_SPACE = 8197;
|
|
1312
|
+
const CHAR_SIX_PER_EM_SPACE = 8198;
|
|
1313
|
+
const CHAR_FIGURE_SPACE = 8199;
|
|
1314
|
+
const CHAR_PUNCTUATION_SPACE = 8200;
|
|
1315
|
+
const CHAR_THIN_SPACE = 8201;
|
|
1316
|
+
const CHAR_HAIR_SPACE = 8202;
|
|
1317
|
+
const CHAR_NARROW_NO_BREAK_SPACE = 8239;
|
|
1318
|
+
const CHAR_MEDIUM_MATHEMATICAL_SPACE = 8287;
|
|
1319
|
+
const CHAR_IDEOGRAPHIC_SPACE = 12288;
|
|
1320
|
+
/**
|
|
1321
|
+
* @fileoverview JSON syntax helpers
|
|
1322
|
+
* @author Nicholas C. Zakas
|
|
1323
|
+
*/
|
|
1324
|
+
/** @typedef {import("./typedefs.js").TokenType} TokenType */
|
|
1325
|
+
const LBRACKET = "[";
|
|
1326
|
+
const RBRACKET = "]";
|
|
1327
|
+
const LBRACE = "{";
|
|
1328
|
+
const RBRACE = "}";
|
|
1329
|
+
const COLON = ":";
|
|
1330
|
+
const COMMA = ",";
|
|
1331
|
+
const TRUE = "true";
|
|
1332
|
+
const FALSE = "false";
|
|
1333
|
+
const NULL = "null";
|
|
1334
|
+
const NAN$1 = "NaN";
|
|
1335
|
+
const INFINITY$1 = "Infinity";
|
|
1336
|
+
const QUOTE = "\"";
|
|
1337
|
+
const escapeToChar = new Map([
|
|
1338
|
+
[CHAR_DOUBLE_QUOTE, QUOTE],
|
|
1339
|
+
[CHAR_BACKSLASH, "\\"],
|
|
1340
|
+
[CHAR_SLASH, "/"],
|
|
1341
|
+
[CHAR_LOWER_B, "\b"],
|
|
1342
|
+
[CHAR_LOWER_N, "\n"],
|
|
1343
|
+
[CHAR_LOWER_F, "\f"],
|
|
1344
|
+
[CHAR_LOWER_R, "\r"],
|
|
1345
|
+
[CHAR_LOWER_T, " "]
|
|
1346
|
+
]);
|
|
1347
|
+
const json5EscapeToChar = new Map([
|
|
1348
|
+
...escapeToChar,
|
|
1349
|
+
[CHAR_LOWER_V, "\v"],
|
|
1350
|
+
[CHAR_0, "\0"]
|
|
1351
|
+
]);
|
|
1352
|
+
const charToEscape = new Map([
|
|
1353
|
+
[QUOTE, QUOTE],
|
|
1354
|
+
["\\", "\\"],
|
|
1355
|
+
["/", "/"],
|
|
1356
|
+
["\b", "b"],
|
|
1357
|
+
["\n", "n"],
|
|
1358
|
+
["\f", "f"],
|
|
1359
|
+
["\r", "r"],
|
|
1360
|
+
[" ", "t"]
|
|
1361
|
+
]);
|
|
1362
|
+
const json5CharToEscape = new Map([
|
|
1363
|
+
...charToEscape,
|
|
1364
|
+
["\v", "v"],
|
|
1365
|
+
["\0", "0"],
|
|
1366
|
+
["\u2028", "u2028"],
|
|
1367
|
+
["\u2029", "u2029"]
|
|
1368
|
+
]);
|
|
1369
|
+
/** @type {Map<string,TokenType>} */
|
|
1370
|
+
const knownTokenTypes = new Map([
|
|
1371
|
+
[LBRACKET, "LBracket"],
|
|
1372
|
+
[RBRACKET, "RBracket"],
|
|
1373
|
+
[LBRACE, "LBrace"],
|
|
1374
|
+
[RBRACE, "RBrace"],
|
|
1375
|
+
[COLON, "Colon"],
|
|
1376
|
+
[COMMA, "Comma"],
|
|
1377
|
+
[TRUE, "Boolean"],
|
|
1378
|
+
[FALSE, "Boolean"],
|
|
1379
|
+
[NULL, "Null"]
|
|
1380
|
+
]);
|
|
1381
|
+
/** @type {Map<string,TokenType>} */
|
|
1382
|
+
const knownJSON5TokenTypes = new Map([
|
|
1383
|
+
...knownTokenTypes,
|
|
1384
|
+
[NAN$1, "Number"],
|
|
1385
|
+
[INFINITY$1, "Number"]
|
|
1386
|
+
]);
|
|
1387
|
+
const json5LineTerminators = new Set([
|
|
1388
|
+
CHAR_NEWLINE,
|
|
1389
|
+
CHAR_RETURN,
|
|
1390
|
+
CHAR_LINE_SEPARATOR,
|
|
1391
|
+
CHAR_PARAGRAPH_SEPARATOR
|
|
1392
|
+
]);
|
|
1393
|
+
/**
|
|
1394
|
+
* @fileoverview JSON tokenization/parsing errors
|
|
1395
|
+
* @author Nicholas C. Zakas
|
|
1396
|
+
*/
|
|
1397
|
+
/** @typedef {import("./typedefs.js").Location} Location */
|
|
1398
|
+
/** @typedef {import("./typedefs.js").Token} Token */
|
|
1399
|
+
/**
|
|
1400
|
+
* Base class that attaches location to an error.
|
|
1401
|
+
*/
|
|
1402
|
+
var ErrorWithLocation = class extends Error {
|
|
1403
|
+
/**
|
|
1404
|
+
* Creates a new instance.
|
|
1405
|
+
* @param {string} message The error message to report.
|
|
1406
|
+
* @param {Location} loc The location information for the error.
|
|
1407
|
+
*/
|
|
1408
|
+
constructor(message, { line, column, offset }) {
|
|
1409
|
+
super(`${message} (${line}:${column})`);
|
|
1410
|
+
/**
|
|
1411
|
+
* The line on which the error occurred.
|
|
1412
|
+
* @type {number}
|
|
1413
|
+
*/
|
|
1414
|
+
this.line = line;
|
|
1415
|
+
/**
|
|
1416
|
+
* The column on which the error occurred.
|
|
1417
|
+
* @type {number}
|
|
1418
|
+
*/
|
|
1419
|
+
this.column = column;
|
|
1420
|
+
/**
|
|
1421
|
+
* The index into the string where the error occurred.
|
|
1422
|
+
* @type {number}
|
|
1423
|
+
*/
|
|
1424
|
+
this.offset = offset;
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
/**
|
|
1428
|
+
* Error thrown when an unexpected character is found during tokenizing.
|
|
1429
|
+
*/
|
|
1430
|
+
var UnexpectedChar = class extends ErrorWithLocation {
|
|
1431
|
+
/**
|
|
1432
|
+
* Creates a new instance.
|
|
1433
|
+
* @param {number} unexpected The character that was found.
|
|
1434
|
+
* @param {Location} loc The location information for the found character.
|
|
1435
|
+
*/
|
|
1436
|
+
constructor(unexpected, loc) {
|
|
1437
|
+
super(`Unexpected character '${String.fromCharCode(unexpected)}' found.`, loc);
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
/**
|
|
1441
|
+
* Error thrown when an unexpected identifier is found during tokenizing.
|
|
1442
|
+
*/
|
|
1443
|
+
var UnexpectedIdentifier = class extends ErrorWithLocation {
|
|
1444
|
+
/**
|
|
1445
|
+
* Creates a new instance.
|
|
1446
|
+
* @param {string} unexpected The character that was found.
|
|
1447
|
+
* @param {Location} loc The location information for the found character.
|
|
1448
|
+
*/
|
|
1449
|
+
constructor(unexpected, loc) {
|
|
1450
|
+
super(`Unexpected identifier '${unexpected}' found.`, loc);
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
/**
|
|
1454
|
+
* Error thrown when an unexpected token is found during parsing.
|
|
1455
|
+
*/
|
|
1456
|
+
var UnexpectedToken = class extends ErrorWithLocation {
|
|
1457
|
+
/**
|
|
1458
|
+
* Creates a new instance.
|
|
1459
|
+
* @param {Token} token The token that was found.
|
|
1460
|
+
*/
|
|
1461
|
+
constructor(token) {
|
|
1462
|
+
super(`Unexpected token ${token.type} found.`, token.loc.start);
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
/**
|
|
1466
|
+
* Error thrown when the end of input is found where it isn't expected.
|
|
1467
|
+
*/
|
|
1468
|
+
var UnexpectedEOF = class extends ErrorWithLocation {
|
|
1469
|
+
/**
|
|
1470
|
+
* Creates a new instance.
|
|
1471
|
+
* @param {Location} loc The location information for the found character.
|
|
1472
|
+
*/
|
|
1473
|
+
constructor(loc) {
|
|
1474
|
+
super("Unexpected end of input found.", loc);
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
const ID_Start = /[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE83\uDE86-\uDE89\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]/;
|
|
1478
|
+
const ID_Continue = /[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u08D4-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u09FC\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D00-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1CD0-\u1CD2\u1CD4-\u1CF9\u1D00-\u1DF9\u1DFB-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE3E\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC00-\uDC4A\uDC50-\uDC59\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDE00-\uDE3E\uDE47\uDE50-\uDE83\uDE86-\uDE99\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC40\uDC50-\uDC59\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD47\uDD50-\uDD59]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6\uDD00-\uDD4A\uDD50-\uDD59]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF]/;
|
|
1479
|
+
/**
|
|
1480
|
+
* @fileoverview A charactor code reader.
|
|
1481
|
+
* @author Nicholas C. Zakas
|
|
1482
|
+
*/
|
|
1483
|
+
const CHAR_CR = 13;
|
|
1484
|
+
const CHAR_LF = 10;
|
|
1485
|
+
/**
|
|
1486
|
+
* A reader that reads character codes from a string.
|
|
1487
|
+
*/
|
|
1488
|
+
var CharCodeReader = class {
|
|
1489
|
+
/**
|
|
1490
|
+
* The text to read from.
|
|
1491
|
+
* @type {string}
|
|
1492
|
+
*/
|
|
1493
|
+
#text = "";
|
|
1494
|
+
/**
|
|
1495
|
+
* The current line number.
|
|
1496
|
+
* @type {number}
|
|
1497
|
+
*/
|
|
1498
|
+
#line = 1;
|
|
1499
|
+
/**
|
|
1500
|
+
* The current column number.
|
|
1501
|
+
* @type {number}
|
|
1502
|
+
*/
|
|
1503
|
+
#column = 0;
|
|
1504
|
+
/**
|
|
1505
|
+
* The current offset in the text.
|
|
1506
|
+
* @type {number}
|
|
1507
|
+
*/
|
|
1508
|
+
#offset = -1;
|
|
1509
|
+
/**
|
|
1510
|
+
* Whether the last character read was a new line.
|
|
1511
|
+
* @type {boolean}
|
|
1512
|
+
*/
|
|
1513
|
+
#newLine = false;
|
|
1514
|
+
/**
|
|
1515
|
+
* The last character code read.
|
|
1516
|
+
* @type {number}
|
|
1517
|
+
*/
|
|
1518
|
+
#last = -1;
|
|
1519
|
+
/**
|
|
1520
|
+
* Whether the reader has ended.
|
|
1521
|
+
* @type {boolean}
|
|
1522
|
+
*/
|
|
1523
|
+
#ended = false;
|
|
1524
|
+
/**
|
|
1525
|
+
* Creates a new instance.
|
|
1526
|
+
* @param {string} text The text to read from
|
|
1527
|
+
*/
|
|
1528
|
+
constructor(text) {
|
|
1529
|
+
this.#text = text;
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Ends the reader.
|
|
1533
|
+
* @returns {void}
|
|
1534
|
+
*/
|
|
1535
|
+
#end() {
|
|
1536
|
+
if (this.#ended) return;
|
|
1537
|
+
this.#column++;
|
|
1538
|
+
this.#offset++;
|
|
1539
|
+
this.#last = -1;
|
|
1540
|
+
this.#ended = true;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Returns the current position of the reader.
|
|
1544
|
+
* @returns {Location} An object with line, column, and offset properties.
|
|
1545
|
+
*/
|
|
1546
|
+
locate() {
|
|
1547
|
+
return {
|
|
1548
|
+
line: this.#line,
|
|
1549
|
+
column: this.#column,
|
|
1550
|
+
offset: this.#offset
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Reads the next character code in the text.
|
|
1555
|
+
* @returns {number} The next character code, or -1 if there are no more characters.
|
|
1556
|
+
*/
|
|
1557
|
+
next() {
|
|
1558
|
+
if (this.#offset >= this.#text.length - 1) {
|
|
1559
|
+
this.#end();
|
|
1560
|
+
return -1;
|
|
1561
|
+
}
|
|
1562
|
+
this.#offset++;
|
|
1563
|
+
const charCode = this.#text.charCodeAt(this.#offset);
|
|
1564
|
+
if (this.#newLine) {
|
|
1565
|
+
this.#line++;
|
|
1566
|
+
this.#column = 1;
|
|
1567
|
+
this.#newLine = false;
|
|
1568
|
+
} else this.#column++;
|
|
1569
|
+
if (charCode === CHAR_CR) {
|
|
1570
|
+
this.#newLine = true;
|
|
1571
|
+
if (this.peek() === CHAR_LF) this.#offset++;
|
|
1572
|
+
} else if (charCode === CHAR_LF) this.#newLine = true;
|
|
1573
|
+
this.#last = charCode;
|
|
1574
|
+
return charCode;
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Peeks at the next character code in the text.
|
|
1578
|
+
* @returns {number} The next character code, or -1 if there are no more characters.
|
|
1579
|
+
*/
|
|
1580
|
+
peek() {
|
|
1581
|
+
if (this.#offset === this.#text.length - 1) return -1;
|
|
1582
|
+
return this.#text.charCodeAt(this.#offset + 1);
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Determines if the next character code in the text matches a specific character code.
|
|
1586
|
+
* @param {(number) => boolean} fn A function to call on the next character.
|
|
1587
|
+
* @returns {boolean} True if the next character code matches, false if not.
|
|
1588
|
+
*/
|
|
1589
|
+
match(fn) {
|
|
1590
|
+
if (fn(this.peek())) {
|
|
1591
|
+
this.next();
|
|
1592
|
+
return true;
|
|
1593
|
+
}
|
|
1594
|
+
return false;
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Returns the last character code read.
|
|
1598
|
+
* @returns {number} The last character code read.
|
|
1599
|
+
*/
|
|
1600
|
+
current() {
|
|
1601
|
+
return this.#last;
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
/**
|
|
1605
|
+
* @fileoverview JSON tokenizer
|
|
1606
|
+
* @author Nicholas C. Zakas
|
|
1607
|
+
*/
|
|
1608
|
+
/** @typedef {import("./typedefs.js").Range} Range */
|
|
1609
|
+
/** @typedef {import("./typedefs.js").TokenizeOptions} TokenizeOptions */
|
|
1610
|
+
const INFINITY = "Infinity";
|
|
1611
|
+
const NAN = "NaN";
|
|
1612
|
+
const keywordStarts = new Set([
|
|
1613
|
+
CHAR_LOWER_T,
|
|
1614
|
+
CHAR_LOWER_F,
|
|
1615
|
+
CHAR_LOWER_N
|
|
1616
|
+
]);
|
|
1617
|
+
const whitespace = new Set([
|
|
1618
|
+
CHAR_SPACE,
|
|
1619
|
+
CHAR_TAB,
|
|
1620
|
+
CHAR_NEWLINE,
|
|
1621
|
+
CHAR_RETURN
|
|
1622
|
+
]);
|
|
1623
|
+
const json5Whitespace = new Set([
|
|
1624
|
+
...whitespace,
|
|
1625
|
+
CHAR_VTAB,
|
|
1626
|
+
CHAR_FORM_FEED,
|
|
1627
|
+
CHAR_NBSP,
|
|
1628
|
+
CHAR_LINE_SEPARATOR,
|
|
1629
|
+
CHAR_PARAGRAPH_SEPARATOR,
|
|
1630
|
+
CHAR_BOM,
|
|
1631
|
+
CHAR_NON_BREAKING_SPACE,
|
|
1632
|
+
CHAR_EN_QUAD,
|
|
1633
|
+
CHAR_EM_QUAD,
|
|
1634
|
+
CHAR_EN_SPACE,
|
|
1635
|
+
CHAR_EM_SPACE,
|
|
1636
|
+
CHAR_THREE_PER_EM_SPACE,
|
|
1637
|
+
CHAR_FOUR_PER_EM_SPACE,
|
|
1638
|
+
CHAR_SIX_PER_EM_SPACE,
|
|
1639
|
+
CHAR_FIGURE_SPACE,
|
|
1640
|
+
CHAR_PUNCTUATION_SPACE,
|
|
1641
|
+
CHAR_THIN_SPACE,
|
|
1642
|
+
CHAR_HAIR_SPACE,
|
|
1643
|
+
CHAR_NARROW_NO_BREAK_SPACE,
|
|
1644
|
+
CHAR_MEDIUM_MATHEMATICAL_SPACE,
|
|
1645
|
+
CHAR_IDEOGRAPHIC_SPACE
|
|
1646
|
+
]);
|
|
1647
|
+
/** @type {TokenizeOptions} */
|
|
1648
|
+
const DEFAULT_OPTIONS$1 = {
|
|
1649
|
+
mode: "json",
|
|
1650
|
+
ranges: false
|
|
1651
|
+
};
|
|
1652
|
+
const jsonKeywords = new Set([
|
|
1653
|
+
"true",
|
|
1654
|
+
"false",
|
|
1655
|
+
"null"
|
|
1656
|
+
]);
|
|
1657
|
+
const tt = {
|
|
1658
|
+
EOF: 0,
|
|
1659
|
+
Number: 1,
|
|
1660
|
+
String: 2,
|
|
1661
|
+
Boolean: 3,
|
|
1662
|
+
Null: 4,
|
|
1663
|
+
NaN: 5,
|
|
1664
|
+
Infinity: 6,
|
|
1665
|
+
Identifier: 7,
|
|
1666
|
+
Colon: 20,
|
|
1667
|
+
LBrace: 21,
|
|
1668
|
+
RBrace: 22,
|
|
1669
|
+
LBracket: 23,
|
|
1670
|
+
RBracket: 24,
|
|
1671
|
+
Comma: 25,
|
|
1672
|
+
LineComment: 40,
|
|
1673
|
+
BlockComment: 41
|
|
1674
|
+
};
|
|
1675
|
+
/**
|
|
1676
|
+
* Determines if a given character is a decimal digit.
|
|
1677
|
+
* @param {number} c The character to check.
|
|
1678
|
+
* @returns {boolean} `true` if the character is a digit.
|
|
1679
|
+
*/
|
|
1680
|
+
function isDigit(c) {
|
|
1681
|
+
return c >= CHAR_0 && c <= CHAR_9;
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Determines if a given character is a hexadecimal digit.
|
|
1685
|
+
* @param {number} c The character to check.
|
|
1686
|
+
* @returns {boolean} `true` if the character is a hexadecimal digit.
|
|
1687
|
+
*/
|
|
1688
|
+
function isHexDigit(c) {
|
|
1689
|
+
return isDigit(c) || c >= CHAR_UPPER_A && c <= CHAR_UPPER_F || c >= CHAR_LOWER_A && c <= CHAR_LOWER_F;
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Determines if a given character is a positive digit (1-9).
|
|
1693
|
+
* @param {number} c The character to check.
|
|
1694
|
+
* @returns {boolean} `true` if the character is a positive digit.
|
|
1695
|
+
*/
|
|
1696
|
+
function isPositiveDigit(c) {
|
|
1697
|
+
return c >= CHAR_1 && c <= CHAR_9;
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Determines if a given character is the start of a keyword.
|
|
1701
|
+
* @param {number} c The character to check.
|
|
1702
|
+
* @returns {boolean} `true` if the character is the start of a keyword.
|
|
1703
|
+
*/
|
|
1704
|
+
function isKeywordStart(c) {
|
|
1705
|
+
return keywordStarts.has(c);
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Determines if a given character is the start of a number.
|
|
1709
|
+
* @param {number} c The character to check.
|
|
1710
|
+
* @returns {boolean} `true` if the character is the start of a number.
|
|
1711
|
+
*/
|
|
1712
|
+
function isNumberStart(c) {
|
|
1713
|
+
return isDigit(c) || c === CHAR_DOT || c === CHAR_MINUS;
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Determines if a given character is the start of a JSON5 number.
|
|
1717
|
+
* @param {number} c The character to check.
|
|
1718
|
+
* @returns {boolean} `true` if the character is the start of a JSON5 number.
|
|
1719
|
+
*/
|
|
1720
|
+
function isJSON5NumberStart(c) {
|
|
1721
|
+
return isNumberStart(c) || c === CHAR_PLUS;
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Determines if a given character is the start of a string.
|
|
1725
|
+
* @param {number} c The character to check.
|
|
1726
|
+
* @param {boolean} json5 `true` if JSON5 mode is enabled.
|
|
1727
|
+
* @returns {boolean} `true` if the character is the start of a string.
|
|
1728
|
+
*/
|
|
1729
|
+
function isStringStart(c, json5) {
|
|
1730
|
+
return c === CHAR_DOUBLE_QUOTE || json5 && c === CHAR_SINGLE_QUOTE;
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Tests that a given character is a valid first character of a
|
|
1734
|
+
* JSON5 identifier
|
|
1735
|
+
* @param {number} c The character to check.
|
|
1736
|
+
* @returns {boolean} `true` if the character is a valid first character.
|
|
1737
|
+
*/
|
|
1738
|
+
function isJSON5IdentifierStart(c) {
|
|
1739
|
+
if (c === CHAR_DOLLAR || c === CHAR_UNDERSCORE || c === CHAR_BACKSLASH) return true;
|
|
1740
|
+
if (c >= CHAR_LOWER_A && c <= CHAR_LOWER_Z || c >= CHAR_UPPER_A && c <= CHAR_UPPER_Z) return true;
|
|
1741
|
+
if (c === 8204 || c === 8205) return true;
|
|
1742
|
+
const ct = String.fromCharCode(c);
|
|
1743
|
+
return ID_Start.test(ct);
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Tests that a given character is a valid part of a JSON5 identifier.
|
|
1747
|
+
* @param {number} c The character to check.
|
|
1748
|
+
* @returns {boolean} `true` if the character is a valid part of an identifier.
|
|
1749
|
+
*/
|
|
1750
|
+
function isJSON5IdentifierPart(c) {
|
|
1751
|
+
if (isJSON5IdentifierStart(c) || isDigit(c)) return true;
|
|
1752
|
+
const ct = String.fromCharCode(c);
|
|
1753
|
+
return ID_Continue.test(ct);
|
|
1754
|
+
}
|
|
1755
|
+
var Tokenizer = class {
|
|
1756
|
+
/**
|
|
1757
|
+
* Options for the tokenizer.
|
|
1758
|
+
* @type {TokenizeOptions}
|
|
1759
|
+
*/
|
|
1760
|
+
#options;
|
|
1761
|
+
/**
|
|
1762
|
+
* The source text to tokenize.
|
|
1763
|
+
* @type {string}
|
|
1764
|
+
*/
|
|
1765
|
+
#text;
|
|
1766
|
+
/**
|
|
1767
|
+
* The reader for the source text.
|
|
1768
|
+
* @type {CharCodeReader}
|
|
1769
|
+
*/
|
|
1770
|
+
#reader;
|
|
1771
|
+
/**
|
|
1772
|
+
* Indicates if the tokenizer is in JSON5 mode.
|
|
1773
|
+
* @type {boolean}
|
|
1774
|
+
*/
|
|
1775
|
+
#json5;
|
|
1776
|
+
/**
|
|
1777
|
+
* Indicates if comments are allowed.
|
|
1778
|
+
* @type {boolean}
|
|
1779
|
+
*/
|
|
1780
|
+
#allowComments;
|
|
1781
|
+
/**
|
|
1782
|
+
* Indicates if ranges should be included in the tokens.
|
|
1783
|
+
* @type {boolean}
|
|
1784
|
+
*/
|
|
1785
|
+
#ranges;
|
|
1786
|
+
/**
|
|
1787
|
+
* The last token type read.
|
|
1788
|
+
* @type {Token}
|
|
1789
|
+
*/
|
|
1790
|
+
#token;
|
|
1791
|
+
/**
|
|
1792
|
+
* Determines if a character is an escaped character.
|
|
1793
|
+
* @type {(c:number) => boolean}
|
|
1794
|
+
*/
|
|
1795
|
+
#isEscapedCharacter;
|
|
1796
|
+
/**
|
|
1797
|
+
* Determines if a character is a JSON5 line terminator.
|
|
1798
|
+
* @type {(c:number) => boolean}
|
|
1799
|
+
*/
|
|
1800
|
+
#isJSON5LineTerminator;
|
|
1801
|
+
/**
|
|
1802
|
+
* Determines if a character is a JSON5 hex escape.
|
|
1803
|
+
* @type {(c:number) => boolean}
|
|
1804
|
+
*/
|
|
1805
|
+
#isJSON5HexEscape;
|
|
1806
|
+
/**
|
|
1807
|
+
* Determines if a character is whitespace.
|
|
1808
|
+
* @type {(c:number) => boolean}
|
|
1809
|
+
*/
|
|
1810
|
+
#isWhitespace;
|
|
1811
|
+
/**
|
|
1812
|
+
* Creates a new instance of the tokenizer.
|
|
1813
|
+
* @param {string} text The source text
|
|
1814
|
+
* @param {TokenizeOptions} [options] Options for the tokenizer.
|
|
1815
|
+
*/
|
|
1816
|
+
constructor(text, options) {
|
|
1817
|
+
this.#text = text;
|
|
1818
|
+
this.#options = {
|
|
1819
|
+
...DEFAULT_OPTIONS$1,
|
|
1820
|
+
...options
|
|
1821
|
+
};
|
|
1822
|
+
this.#reader = new CharCodeReader(text);
|
|
1823
|
+
this.#json5 = this.#options.mode === "json5";
|
|
1824
|
+
this.#allowComments = this.#options.mode !== "json";
|
|
1825
|
+
this.#ranges = this.#options.ranges;
|
|
1826
|
+
this.#isEscapedCharacter = this.#json5 ? json5EscapeToChar.has.bind(json5EscapeToChar) : escapeToChar.has.bind(escapeToChar);
|
|
1827
|
+
this.#isJSON5LineTerminator = this.#json5 ? json5LineTerminators.has.bind(json5LineTerminators) : () => false;
|
|
1828
|
+
this.#isJSON5HexEscape = this.#json5 ? (c) => c === CHAR_LOWER_X : () => false;
|
|
1829
|
+
this.#isWhitespace = this.#json5 ? json5Whitespace.has.bind(json5Whitespace) : whitespace.has.bind(whitespace);
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Convenience function for throwing unexpected character errors.
|
|
1833
|
+
* @param {number} c The unexpected character.
|
|
1834
|
+
* @param {Location} [loc] The location of the unexpected character.
|
|
1835
|
+
* @returns {never}
|
|
1836
|
+
* @throws {UnexpectedChar} always.
|
|
1837
|
+
*/
|
|
1838
|
+
#unexpected(c, loc = this.#reader.locate()) {
|
|
1839
|
+
throw new UnexpectedChar(c, loc);
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Convenience function for throwing unexpected identifier errors.
|
|
1843
|
+
* @param {string} identifier The unexpected identifier.
|
|
1844
|
+
* @param {Location} [loc] The location of the unexpected identifier.
|
|
1845
|
+
* @returns {never}
|
|
1846
|
+
* @throws {UnexpectedIdentifier} always.
|
|
1847
|
+
*/
|
|
1848
|
+
#unexpectedIdentifier(identifier, loc = this.#reader.locate()) {
|
|
1849
|
+
throw new UnexpectedIdentifier(identifier, loc);
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Convenience function for throwing unexpected EOF errors.
|
|
1853
|
+
* @returns {never}
|
|
1854
|
+
* @throws {UnexpectedEOF} always.
|
|
1855
|
+
*/
|
|
1856
|
+
#unexpectedEOF() {
|
|
1857
|
+
throw new UnexpectedEOF(this.#reader.locate());
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Creates a new token.
|
|
1861
|
+
* @param {TokenType} tokenType The type of token to create.
|
|
1862
|
+
* @param {number} length The length of the token.
|
|
1863
|
+
* @param {Location} startLoc The start location for the token.
|
|
1864
|
+
* @param {Location} [endLoc] The end location for the token.
|
|
1865
|
+
* @returns {Token} The token.
|
|
1866
|
+
*/
|
|
1867
|
+
#createToken(tokenType, length, startLoc, endLoc) {
|
|
1868
|
+
const endOffset = startLoc.offset + length;
|
|
1869
|
+
let range = this.#options.ranges ? { range: [startLoc.offset, endOffset] } : void 0;
|
|
1870
|
+
return {
|
|
1871
|
+
type: tokenType,
|
|
1872
|
+
loc: {
|
|
1873
|
+
start: startLoc,
|
|
1874
|
+
end: endLoc || {
|
|
1875
|
+
line: startLoc.line,
|
|
1876
|
+
column: startLoc.column + length,
|
|
1877
|
+
offset: endOffset
|
|
1878
|
+
}
|
|
1879
|
+
},
|
|
1880
|
+
...range
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Reads in a specific number of hex digits.
|
|
1885
|
+
* @param {number} count The number of hex digits to read.
|
|
1886
|
+
* @returns {string} The hex digits read.
|
|
1887
|
+
*/
|
|
1888
|
+
#readHexDigits(count) {
|
|
1889
|
+
let value = "";
|
|
1890
|
+
let c;
|
|
1891
|
+
for (let i = 0; i < count; i++) {
|
|
1892
|
+
c = this.#reader.peek();
|
|
1893
|
+
if (isHexDigit(c)) {
|
|
1894
|
+
this.#reader.next();
|
|
1895
|
+
value += String.fromCharCode(c);
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
this.#unexpected(c);
|
|
1899
|
+
}
|
|
1900
|
+
return value;
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Reads in a JSON5 identifier. Also used for JSON but we validate
|
|
1904
|
+
* the identifier later.
|
|
1905
|
+
* @param {number} c The first character of the identifier.
|
|
1906
|
+
* @returns {string} The identifier read.
|
|
1907
|
+
* @throws {UnexpectedChar} when the identifier cannot be read.
|
|
1908
|
+
*/
|
|
1909
|
+
#readIdentifier(c) {
|
|
1910
|
+
let value = "";
|
|
1911
|
+
do {
|
|
1912
|
+
value += String.fromCharCode(c);
|
|
1913
|
+
if (c === CHAR_BACKSLASH) {
|
|
1914
|
+
c = this.#reader.next();
|
|
1915
|
+
if (c !== CHAR_LOWER_U) this.#unexpected(c);
|
|
1916
|
+
value += String.fromCharCode(c);
|
|
1917
|
+
const hexDigits = this.#readHexDigits(4);
|
|
1918
|
+
const charCode = parseInt(hexDigits, 16);
|
|
1919
|
+
if (value.length === 2 && !isJSON5IdentifierStart(charCode)) {
|
|
1920
|
+
const loc = this.#reader.locate();
|
|
1921
|
+
this.#unexpected(CHAR_BACKSLASH, {
|
|
1922
|
+
line: loc.line,
|
|
1923
|
+
column: loc.column - 5,
|
|
1924
|
+
offset: loc.offset - 5
|
|
1925
|
+
});
|
|
1926
|
+
} else if (!isJSON5IdentifierPart(charCode)) {
|
|
1927
|
+
const loc = this.#reader.locate();
|
|
1928
|
+
this.#unexpected(charCode, {
|
|
1929
|
+
line: loc.line,
|
|
1930
|
+
column: loc.column - 5,
|
|
1931
|
+
offset: loc.offset - 5
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
value += hexDigits;
|
|
1935
|
+
}
|
|
1936
|
+
c = this.#reader.peek();
|
|
1937
|
+
if (!isJSON5IdentifierPart(c)) break;
|
|
1938
|
+
this.#reader.next();
|
|
1939
|
+
} while (true);
|
|
1940
|
+
return value;
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Reads in a string. Works for both JSON and JSON5.
|
|
1944
|
+
* @param {number} c The first character of the string (either " or ').
|
|
1945
|
+
* @returns {number} The length of the string.
|
|
1946
|
+
* @throws {UnexpectedChar} when the string cannot be read.
|
|
1947
|
+
* @throws {UnexpectedEOF} when EOF is reached before the string is finalized.
|
|
1948
|
+
*/
|
|
1949
|
+
#readString(c) {
|
|
1950
|
+
const delimiter = c;
|
|
1951
|
+
let length = 1;
|
|
1952
|
+
c = this.#reader.peek();
|
|
1953
|
+
while (c !== -1 && c !== delimiter) {
|
|
1954
|
+
this.#reader.next();
|
|
1955
|
+
length++;
|
|
1956
|
+
if (c === CHAR_BACKSLASH) {
|
|
1957
|
+
c = this.#reader.peek();
|
|
1958
|
+
if (this.#isEscapedCharacter(c) || this.#isJSON5LineTerminator(c)) {
|
|
1959
|
+
this.#reader.next();
|
|
1960
|
+
length++;
|
|
1961
|
+
} else if (c === CHAR_LOWER_U) {
|
|
1962
|
+
this.#reader.next();
|
|
1963
|
+
length++;
|
|
1964
|
+
const result = this.#readHexDigits(4);
|
|
1965
|
+
length += result.length;
|
|
1966
|
+
} else if (this.#isJSON5HexEscape(c)) {
|
|
1967
|
+
this.#reader.next();
|
|
1968
|
+
length++;
|
|
1969
|
+
const result = this.#readHexDigits(2);
|
|
1970
|
+
length += result.length;
|
|
1971
|
+
} else if (this.#json5) {
|
|
1972
|
+
this.#reader.next();
|
|
1973
|
+
length++;
|
|
1974
|
+
} else this.#unexpected(c);
|
|
1975
|
+
}
|
|
1976
|
+
c = this.#reader.peek();
|
|
1977
|
+
}
|
|
1978
|
+
if (c === -1) {
|
|
1979
|
+
this.#reader.next();
|
|
1980
|
+
this.#unexpectedEOF();
|
|
1981
|
+
}
|
|
1982
|
+
this.#reader.next();
|
|
1983
|
+
length++;
|
|
1984
|
+
return length;
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Reads a number. Works for both JSON and JSON5.
|
|
1988
|
+
* @param {number} c The first character of the number.
|
|
1989
|
+
* @returns {number} The length of the number.
|
|
1990
|
+
* @throws {UnexpectedChar} when the number cannot be read.
|
|
1991
|
+
* @throws {UnexpectedEOF} when EOF is reached before the number is finalized.
|
|
1992
|
+
*/
|
|
1993
|
+
#readNumber(c) {
|
|
1994
|
+
let length = 1;
|
|
1995
|
+
if (c === CHAR_MINUS || this.#json5 && c === CHAR_PLUS) {
|
|
1996
|
+
c = this.#reader.peek();
|
|
1997
|
+
if (this.#json5) {
|
|
1998
|
+
if (c === CHAR_UPPER_I || c === CHAR_UPPER_N) {
|
|
1999
|
+
this.#reader.next();
|
|
2000
|
+
const identifier = this.#readIdentifier(c);
|
|
2001
|
+
if (identifier !== INFINITY && identifier !== NAN) this.#unexpected(c);
|
|
2002
|
+
return length + identifier.length;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
if (!isDigit(c)) this.#unexpected(c);
|
|
2006
|
+
this.#reader.next();
|
|
2007
|
+
length++;
|
|
2008
|
+
}
|
|
2009
|
+
if (c === CHAR_0) {
|
|
2010
|
+
c = this.#reader.peek();
|
|
2011
|
+
if (this.#json5 && (c === CHAR_LOWER_X || c === CHAR_UPPER_X)) {
|
|
2012
|
+
this.#reader.next();
|
|
2013
|
+
length++;
|
|
2014
|
+
c = this.#reader.peek();
|
|
2015
|
+
if (!isHexDigit(c)) {
|
|
2016
|
+
this.#reader.next();
|
|
2017
|
+
this.#unexpected(c);
|
|
2018
|
+
}
|
|
2019
|
+
do {
|
|
2020
|
+
this.#reader.next();
|
|
2021
|
+
length++;
|
|
2022
|
+
c = this.#reader.peek();
|
|
2023
|
+
} while (isHexDigit(c));
|
|
2024
|
+
} else if (isDigit(c)) this.#unexpected(c);
|
|
2025
|
+
} else if (!this.#json5 || c !== CHAR_DOT) {
|
|
2026
|
+
if (!isPositiveDigit(c)) this.#unexpected(c);
|
|
2027
|
+
c = this.#reader.peek();
|
|
2028
|
+
while (isDigit(c)) {
|
|
2029
|
+
this.#reader.next();
|
|
2030
|
+
length++;
|
|
2031
|
+
c = this.#reader.peek();
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
if (c === CHAR_DOT) {
|
|
2035
|
+
let digitCount = -1;
|
|
2036
|
+
this.#reader.next();
|
|
2037
|
+
length++;
|
|
2038
|
+
digitCount++;
|
|
2039
|
+
c = this.#reader.peek();
|
|
2040
|
+
while (isDigit(c)) {
|
|
2041
|
+
this.#reader.next();
|
|
2042
|
+
length++;
|
|
2043
|
+
digitCount++;
|
|
2044
|
+
c = this.#reader.peek();
|
|
2045
|
+
}
|
|
2046
|
+
if (!this.#json5 && digitCount === 0) {
|
|
2047
|
+
this.#reader.next();
|
|
2048
|
+
if (c) this.#unexpected(c);
|
|
2049
|
+
else this.#unexpectedEOF();
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
if (c === CHAR_LOWER_E || c === CHAR_UPPER_E) {
|
|
2053
|
+
this.#reader.next();
|
|
2054
|
+
length++;
|
|
2055
|
+
c = this.#reader.peek();
|
|
2056
|
+
if (c === CHAR_PLUS || c === CHAR_MINUS) {
|
|
2057
|
+
this.#reader.next();
|
|
2058
|
+
length++;
|
|
2059
|
+
c = this.#reader.peek();
|
|
2060
|
+
}
|
|
2061
|
+
if (c === -1) {
|
|
2062
|
+
this.#reader.next();
|
|
2063
|
+
this.#unexpectedEOF();
|
|
2064
|
+
}
|
|
2065
|
+
if (!isDigit(c)) {
|
|
2066
|
+
this.#reader.next();
|
|
2067
|
+
this.#unexpected(c);
|
|
2068
|
+
}
|
|
2069
|
+
while (isDigit(c)) {
|
|
2070
|
+
this.#reader.next();
|
|
2071
|
+
length++;
|
|
2072
|
+
c = this.#reader.peek();
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return length;
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Reads a comment. Works for both JSON and JSON5.
|
|
2079
|
+
* @param {number} c The first character of the comment.
|
|
2080
|
+
* @returns {{length: number, multiline: boolean}} The length of the comment, and whether the comment is multi-line.
|
|
2081
|
+
* @throws {UnexpectedChar} when the comment cannot be read.
|
|
2082
|
+
* @throws {UnexpectedEOF} when EOF is reached before the comment is finalized.
|
|
2083
|
+
*/
|
|
2084
|
+
#readComment(c) {
|
|
2085
|
+
let length = 1;
|
|
2086
|
+
c = this.#reader.peek();
|
|
2087
|
+
if (c === CHAR_SLASH) {
|
|
2088
|
+
do {
|
|
2089
|
+
this.#reader.next();
|
|
2090
|
+
length += 1;
|
|
2091
|
+
c = this.#reader.peek();
|
|
2092
|
+
} while (c > -1 && c !== CHAR_RETURN && c !== CHAR_NEWLINE);
|
|
2093
|
+
return {
|
|
2094
|
+
length,
|
|
2095
|
+
multiline: false
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
if (c === CHAR_STAR) {
|
|
2099
|
+
this.#reader.next();
|
|
2100
|
+
length += 1;
|
|
2101
|
+
while (c > -1) {
|
|
2102
|
+
c = this.#reader.peek();
|
|
2103
|
+
if (c === CHAR_STAR) {
|
|
2104
|
+
this.#reader.next();
|
|
2105
|
+
length += 1;
|
|
2106
|
+
c = this.#reader.peek();
|
|
2107
|
+
if (c === CHAR_SLASH) {
|
|
2108
|
+
this.#reader.next();
|
|
2109
|
+
length += 1;
|
|
2110
|
+
return {
|
|
2111
|
+
length,
|
|
2112
|
+
multiline: true
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
} else {
|
|
2116
|
+
this.#reader.next();
|
|
2117
|
+
length += 1;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
this.#reader.next();
|
|
2121
|
+
this.#unexpectedEOF();
|
|
2122
|
+
}
|
|
2123
|
+
this.#reader.next();
|
|
2124
|
+
this.#unexpected(c);
|
|
2125
|
+
}
|
|
2126
|
+
/**
|
|
2127
|
+
* Returns the next token in the source text.
|
|
2128
|
+
* @returns {number} The code for the next token.
|
|
2129
|
+
*/
|
|
2130
|
+
next() {
|
|
2131
|
+
let c = this.#reader.next();
|
|
2132
|
+
while (this.#isWhitespace(c)) c = this.#reader.next();
|
|
2133
|
+
if (c === -1) return tt.EOF;
|
|
2134
|
+
const start = this.#reader.locate();
|
|
2135
|
+
const ct = String.fromCharCode(c);
|
|
2136
|
+
if (this.#json5) if (knownJSON5TokenTypes.has(ct)) this.#token = this.#createToken(knownJSON5TokenTypes.get(ct), 1, start);
|
|
2137
|
+
else if (isJSON5IdentifierStart(c)) {
|
|
2138
|
+
const value = this.#readIdentifier(c);
|
|
2139
|
+
if (knownJSON5TokenTypes.has(value)) this.#token = this.#createToken(knownJSON5TokenTypes.get(value), value.length, start);
|
|
2140
|
+
else this.#token = this.#createToken("Identifier", value.length, start);
|
|
2141
|
+
} else if (isJSON5NumberStart(c)) {
|
|
2142
|
+
const result = this.#readNumber(c);
|
|
2143
|
+
this.#token = this.#createToken("Number", result, start);
|
|
2144
|
+
} else if (isStringStart(c, this.#json5)) {
|
|
2145
|
+
const result = this.#readString(c);
|
|
2146
|
+
const lastCharLoc = this.#reader.locate();
|
|
2147
|
+
this.#token = this.#createToken("String", result, start, {
|
|
2148
|
+
line: lastCharLoc.line,
|
|
2149
|
+
column: lastCharLoc.column + 1,
|
|
2150
|
+
offset: lastCharLoc.offset + 1
|
|
2151
|
+
});
|
|
2152
|
+
} else if (c === CHAR_SLASH && this.#allowComments) {
|
|
2153
|
+
const result = this.#readComment(c);
|
|
2154
|
+
const lastCharLoc = this.#reader.locate();
|
|
2155
|
+
this.#token = this.#createToken(!result.multiline ? "LineComment" : "BlockComment", result.length, start, {
|
|
2156
|
+
line: lastCharLoc.line,
|
|
2157
|
+
column: lastCharLoc.column + 1,
|
|
2158
|
+
offset: lastCharLoc.offset + 1
|
|
2159
|
+
});
|
|
2160
|
+
} else this.#unexpected(c);
|
|
2161
|
+
else if (knownTokenTypes.has(ct)) this.#token = this.#createToken(knownTokenTypes.get(ct), 1, start);
|
|
2162
|
+
else if (isKeywordStart(c)) {
|
|
2163
|
+
const value = this.#readIdentifier(c);
|
|
2164
|
+
if (!jsonKeywords.has(value)) this.#unexpectedIdentifier(value, start);
|
|
2165
|
+
this.#token = this.#createToken(knownTokenTypes.get(value), value.length, start);
|
|
2166
|
+
} else if (isNumberStart(c)) {
|
|
2167
|
+
const result = this.#readNumber(c);
|
|
2168
|
+
this.#token = this.#createToken("Number", result, start);
|
|
2169
|
+
} else if (isStringStart(c, this.#json5)) {
|
|
2170
|
+
const result = this.#readString(c);
|
|
2171
|
+
this.#token = this.#createToken("String", result, start);
|
|
2172
|
+
} else if (c === CHAR_SLASH && this.#allowComments) {
|
|
2173
|
+
const result = this.#readComment(c);
|
|
2174
|
+
const lastCharLoc = this.#reader.locate();
|
|
2175
|
+
this.#token = this.#createToken(!result.multiline ? "LineComment" : "BlockComment", result.length, start, {
|
|
2176
|
+
line: lastCharLoc.line,
|
|
2177
|
+
column: lastCharLoc.column + 1,
|
|
2178
|
+
offset: lastCharLoc.offset + 1
|
|
2179
|
+
});
|
|
2180
|
+
} else this.#unexpected(c);
|
|
2181
|
+
return tt[this.#token.type];
|
|
2182
|
+
}
|
|
2183
|
+
/**
|
|
2184
|
+
* Returns the current token in the source text.
|
|
2185
|
+
* @returns {Token} The current token.
|
|
2186
|
+
*/
|
|
2187
|
+
get token() {
|
|
2188
|
+
return this.#token;
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
/**
|
|
2192
|
+
* @fileoverview JSON AST types
|
|
2193
|
+
* @author Nicholas C. Zakas
|
|
2194
|
+
*/
|
|
2195
|
+
/** @typedef {import("./typedefs.js").NodeParts} NodeParts */
|
|
2196
|
+
/** @typedef {import("./typedefs.js").DocumentNode} DocumentNode */
|
|
2197
|
+
/** @typedef {import("./typedefs.js").StringNode} StringNode */
|
|
2198
|
+
/** @typedef {import("./typedefs.js").NumberNode} NumberNode */
|
|
2199
|
+
/** @typedef {import("./typedefs.js").BooleanNode} BooleanNode */
|
|
2200
|
+
/** @typedef {import("./typedefs.js").MemberNode} MemberNode */
|
|
2201
|
+
/** @typedef {import("./typedefs.js").ObjectNode} ObjectNode */
|
|
2202
|
+
/** @typedef {import("./typedefs.js").ElementNode} ElementNode */
|
|
2203
|
+
/** @typedef {import("./typedefs.js").ArrayNode} ArrayNode */
|
|
2204
|
+
/** @typedef {import("./typedefs.js").NullNode} NullNode */
|
|
2205
|
+
/** @typedef {import("./typedefs.js").ValueNode} ValueNode */
|
|
2206
|
+
/** @typedef {import("./typedefs.js").IdentifierNode} IdentifierNode */
|
|
2207
|
+
/** @typedef {import("./typedefs.js").NaNNode} NaNNode */
|
|
2208
|
+
/** @typedef {import("./typedefs.js").InfinityNode} InfinityNode */
|
|
2209
|
+
/** @typedef {import("./typedefs.js").Sign} Sign */
|
|
2210
|
+
const types = {
|
|
2211
|
+
document(body, parts = {}) {
|
|
2212
|
+
return {
|
|
2213
|
+
type: "Document",
|
|
2214
|
+
body,
|
|
2215
|
+
loc: parts.loc,
|
|
2216
|
+
...parts
|
|
2217
|
+
};
|
|
2218
|
+
},
|
|
2219
|
+
string(value, parts = {}) {
|
|
2220
|
+
return {
|
|
2221
|
+
type: "String",
|
|
2222
|
+
value,
|
|
2223
|
+
loc: parts.loc,
|
|
2224
|
+
...parts
|
|
2225
|
+
};
|
|
2226
|
+
},
|
|
2227
|
+
number(value, parts = {}) {
|
|
2228
|
+
return {
|
|
2229
|
+
type: "Number",
|
|
2230
|
+
value,
|
|
2231
|
+
loc: parts.loc,
|
|
2232
|
+
...parts
|
|
2233
|
+
};
|
|
2234
|
+
},
|
|
2235
|
+
boolean(value, parts = {}) {
|
|
2236
|
+
return {
|
|
2237
|
+
type: "Boolean",
|
|
2238
|
+
value,
|
|
2239
|
+
loc: parts.loc,
|
|
2240
|
+
...parts
|
|
2241
|
+
};
|
|
2242
|
+
},
|
|
2243
|
+
null(parts = {}) {
|
|
2244
|
+
return {
|
|
2245
|
+
type: "Null",
|
|
2246
|
+
loc: parts.loc,
|
|
2247
|
+
...parts
|
|
2248
|
+
};
|
|
2249
|
+
},
|
|
2250
|
+
array(elements, parts = {}) {
|
|
2251
|
+
return {
|
|
2252
|
+
type: "Array",
|
|
2253
|
+
elements,
|
|
2254
|
+
loc: parts.loc,
|
|
2255
|
+
...parts
|
|
2256
|
+
};
|
|
2257
|
+
},
|
|
2258
|
+
element(value, parts = {}) {
|
|
2259
|
+
return {
|
|
2260
|
+
type: "Element",
|
|
2261
|
+
value,
|
|
2262
|
+
loc: parts.loc,
|
|
2263
|
+
...parts
|
|
2264
|
+
};
|
|
2265
|
+
},
|
|
2266
|
+
object(members, parts = {}) {
|
|
2267
|
+
return {
|
|
2268
|
+
type: "Object",
|
|
2269
|
+
members,
|
|
2270
|
+
loc: parts.loc,
|
|
2271
|
+
...parts
|
|
2272
|
+
};
|
|
2273
|
+
},
|
|
2274
|
+
member(name, value, parts = {}) {
|
|
2275
|
+
return {
|
|
2276
|
+
type: "Member",
|
|
2277
|
+
name,
|
|
2278
|
+
value,
|
|
2279
|
+
loc: parts.loc,
|
|
2280
|
+
...parts
|
|
2281
|
+
};
|
|
2282
|
+
},
|
|
2283
|
+
identifier(name, parts = {}) {
|
|
2284
|
+
return {
|
|
2285
|
+
type: "Identifier",
|
|
2286
|
+
name,
|
|
2287
|
+
loc: parts.loc,
|
|
2288
|
+
...parts
|
|
2289
|
+
};
|
|
2290
|
+
},
|
|
2291
|
+
nan(sign = "", parts = {}) {
|
|
2292
|
+
return {
|
|
2293
|
+
type: "NaN",
|
|
2294
|
+
sign,
|
|
2295
|
+
loc: parts.loc,
|
|
2296
|
+
...parts
|
|
2297
|
+
};
|
|
2298
|
+
},
|
|
2299
|
+
infinity(sign = "", parts = {}) {
|
|
2300
|
+
return {
|
|
2301
|
+
type: "Infinity",
|
|
2302
|
+
sign,
|
|
2303
|
+
loc: parts.loc,
|
|
2304
|
+
...parts
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
};
|
|
2308
|
+
/**
|
|
2309
|
+
* @fileoverview JSON parser
|
|
2310
|
+
* @author Nicholas C. Zakas
|
|
2311
|
+
*/
|
|
2312
|
+
/** @typedef {import("./typedefs.js").Node} Node */
|
|
2313
|
+
/** @typedef {import("./typedefs.js").Mode} Mode */
|
|
2314
|
+
/** @typedef {import("./typedefs.js").ParseOptions} ParseOptions */
|
|
2315
|
+
/** @type {ParseOptions} */
|
|
2316
|
+
const DEFAULT_OPTIONS = {
|
|
2317
|
+
mode: "json",
|
|
2318
|
+
ranges: false,
|
|
2319
|
+
tokens: false,
|
|
2320
|
+
allowTrailingCommas: false
|
|
2321
|
+
};
|
|
2322
|
+
const UNICODE_SEQUENCE = /\\u[\da-fA-F]{4}/gu;
|
|
2323
|
+
/**
|
|
2324
|
+
* Normalizes a JSON5 identifier by converting Unicode escape sequences into
|
|
2325
|
+
* their corresponding characters.
|
|
2326
|
+
* @param {string} identifier The identifier to normalize.
|
|
2327
|
+
* @returns {string} The normalized identifier.
|
|
2328
|
+
*/
|
|
2329
|
+
function normalizeIdentifier(identifier) {
|
|
2330
|
+
return identifier.replace(UNICODE_SEQUENCE, (unicodeEscape) => {
|
|
2331
|
+
return String.fromCharCode(parseInt(unicodeEscape.slice(2), 16));
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
/**
|
|
2335
|
+
* Converts a JSON-encoded string into a JavaScript string, interpreting each
|
|
2336
|
+
* escape sequence.
|
|
2337
|
+
* @param {string} value The text for the token.
|
|
2338
|
+
* @param {Token} token The string token to convert into a JavaScript string.
|
|
2339
|
+
* @param {boolean} json5 `true` if parsing JSON5, `false` otherwise.
|
|
2340
|
+
* @returns {string} A JavaScript string.
|
|
2341
|
+
*/
|
|
2342
|
+
function getStringValue(value, token, json5 = false) {
|
|
2343
|
+
let result = "";
|
|
2344
|
+
let escapeIndex = value.indexOf("\\");
|
|
2345
|
+
let lastIndex = 0;
|
|
2346
|
+
while (escapeIndex >= 0) {
|
|
2347
|
+
result += value.slice(lastIndex, escapeIndex);
|
|
2348
|
+
const escapeChar = value.charAt(escapeIndex + 1);
|
|
2349
|
+
const escapeCharCode = escapeChar.charCodeAt(0);
|
|
2350
|
+
if (json5 && json5EscapeToChar.has(escapeCharCode)) {
|
|
2351
|
+
result += json5EscapeToChar.get(escapeCharCode);
|
|
2352
|
+
lastIndex = escapeIndex + 2;
|
|
2353
|
+
} else if (escapeToChar.has(escapeCharCode)) {
|
|
2354
|
+
result += escapeToChar.get(escapeCharCode);
|
|
2355
|
+
lastIndex = escapeIndex + 2;
|
|
2356
|
+
} else if (escapeChar === "u") {
|
|
2357
|
+
const hexCode = value.slice(escapeIndex + 2, escapeIndex + 6);
|
|
2358
|
+
if (hexCode.length < 4 || /[^0-9a-f]/i.test(hexCode)) throw new ErrorWithLocation(`Invalid unicode escape \\u${hexCode}.`, {
|
|
2359
|
+
line: token.loc.start.line,
|
|
2360
|
+
column: token.loc.start.column + escapeIndex,
|
|
2361
|
+
offset: token.loc.start.offset + escapeIndex
|
|
2362
|
+
});
|
|
2363
|
+
result += String.fromCharCode(parseInt(hexCode, 16));
|
|
2364
|
+
lastIndex = escapeIndex + 6;
|
|
2365
|
+
} else if (json5 && escapeChar === "x") {
|
|
2366
|
+
const hexCode = value.slice(escapeIndex + 2, escapeIndex + 4);
|
|
2367
|
+
if (hexCode.length < 2 || /[^0-9a-f]/i.test(hexCode)) throw new ErrorWithLocation(`Invalid hex escape \\x${hexCode}.`, {
|
|
2368
|
+
line: token.loc.start.line,
|
|
2369
|
+
column: token.loc.start.column + escapeIndex,
|
|
2370
|
+
offset: token.loc.start.offset + escapeIndex
|
|
2371
|
+
});
|
|
2372
|
+
result += String.fromCharCode(parseInt(hexCode, 16));
|
|
2373
|
+
lastIndex = escapeIndex + 4;
|
|
2374
|
+
} else if (json5 && json5LineTerminators.has(escapeCharCode)) {
|
|
2375
|
+
lastIndex = escapeIndex + 2;
|
|
2376
|
+
if (escapeChar === "\r" && value.charAt(lastIndex) === "\n") lastIndex++;
|
|
2377
|
+
} else if (json5) {
|
|
2378
|
+
result += escapeChar;
|
|
2379
|
+
lastIndex = escapeIndex + 2;
|
|
2380
|
+
} else throw new ErrorWithLocation(`Invalid escape \\${escapeChar}.`, {
|
|
2381
|
+
line: token.loc.start.line,
|
|
2382
|
+
column: token.loc.start.column + escapeIndex,
|
|
2383
|
+
offset: token.loc.start.offset + escapeIndex
|
|
2384
|
+
});
|
|
2385
|
+
escapeIndex = value.indexOf("\\", lastIndex);
|
|
2386
|
+
}
|
|
2387
|
+
result += value.slice(lastIndex);
|
|
2388
|
+
return result;
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Gets the JavaScript value represented by a JSON token.
|
|
2392
|
+
* @param {string} value The text value of the token.
|
|
2393
|
+
* @param {Token} token The JSON token to get a value for.
|
|
2394
|
+
* @param {boolean} json5 `true` if parsing JSON5, `false` otherwise.
|
|
2395
|
+
* @returns {string|boolean|number} A number, string, or boolean.
|
|
2396
|
+
* @throws {TypeError} If an unknown token type is found.
|
|
2397
|
+
*/
|
|
2398
|
+
function getLiteralValue(value, token, json5 = false) {
|
|
2399
|
+
switch (token.type) {
|
|
2400
|
+
case "Boolean": return value === "true";
|
|
2401
|
+
case "Number":
|
|
2402
|
+
if (json5) {
|
|
2403
|
+
if (value.charCodeAt(0) === 45) return -Number(value.slice(1));
|
|
2404
|
+
if (value.charCodeAt(0) === 43) return Number(value.slice(1));
|
|
2405
|
+
}
|
|
2406
|
+
return Number(value);
|
|
2407
|
+
case "String": return getStringValue(value.slice(1, -1), token, json5);
|
|
2408
|
+
default: throw new TypeError(`Unknown token type "${token.type}.`);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
/**
|
|
2412
|
+
*
|
|
2413
|
+
* @param {string} text The text to parse.
|
|
2414
|
+
* @param {ParseOptions} [options] The options object.
|
|
2415
|
+
* @returns {DocumentNode} The AST representing the parsed JSON.
|
|
2416
|
+
* @throws {Error} When there is a parsing error.
|
|
2417
|
+
*/
|
|
2418
|
+
function parse$1(text, options) {
|
|
2419
|
+
options = Object.freeze({
|
|
2420
|
+
...DEFAULT_OPTIONS,
|
|
2421
|
+
...options
|
|
2422
|
+
});
|
|
2423
|
+
const tokens = [];
|
|
2424
|
+
const tokenizer = new Tokenizer(text, {
|
|
2425
|
+
mode: options.mode,
|
|
2426
|
+
ranges: options.ranges
|
|
2427
|
+
});
|
|
2428
|
+
const json5 = options.mode === "json5";
|
|
2429
|
+
const allowTrailingCommas = options.allowTrailingCommas || json5;
|
|
2430
|
+
/**
|
|
2431
|
+
* Returns the next token knowing there are no comments.
|
|
2432
|
+
* @returns {number} The next token type or 0 if no next token.
|
|
2433
|
+
*/
|
|
2434
|
+
function nextNoComments() {
|
|
2435
|
+
const nextType = tokenizer.next();
|
|
2436
|
+
if (nextType && options.tokens) tokens.push(tokenizer.token);
|
|
2437
|
+
return nextType;
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Returns the next token knowing there are comments to skip.
|
|
2441
|
+
* @returns {number} The next token type or 0 if no next token.
|
|
2442
|
+
*/
|
|
2443
|
+
function nextSkipComments() {
|
|
2444
|
+
const nextType = tokenizer.next();
|
|
2445
|
+
if (nextType && options.tokens) tokens.push(tokenizer.token);
|
|
2446
|
+
if (nextType >= tt.LineComment) return nextSkipComments();
|
|
2447
|
+
return nextType;
|
|
2448
|
+
}
|
|
2449
|
+
const next = options.mode === "json" ? nextNoComments : nextSkipComments;
|
|
2450
|
+
/**
|
|
2451
|
+
* Asserts a token has the given type.
|
|
2452
|
+
* @param {number} token The token to check.
|
|
2453
|
+
* @param {number} type The token type.
|
|
2454
|
+
* @throws {UnexpectedToken} If the token type isn't expected.
|
|
2455
|
+
* @returns {void}
|
|
2456
|
+
*/
|
|
2457
|
+
function assertTokenType(token, type) {
|
|
2458
|
+
if (token !== type) throw new UnexpectedToken(tokenizer.token);
|
|
2459
|
+
}
|
|
2460
|
+
/**
|
|
2461
|
+
* Asserts a token has one of the given types.
|
|
2462
|
+
* @param {number} token The token to check.
|
|
2463
|
+
* @param {number[]} types The token types.
|
|
2464
|
+
* @returns {void}
|
|
2465
|
+
* @throws {UnexpectedToken} If the token type isn't expected.
|
|
2466
|
+
*/
|
|
2467
|
+
function assertTokenTypes(token, types$1) {
|
|
2468
|
+
if (!types$1.includes(token)) throw new UnexpectedToken(tokenizer.token);
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* Creates a range only if ranges are specified.
|
|
2472
|
+
* @param {Location} start The start offset for the range.
|
|
2473
|
+
* @param {Location} end The end offset for the range.
|
|
2474
|
+
* @returns {{range:[number,number]}|undefined} An object with a
|
|
2475
|
+
*/
|
|
2476
|
+
function createRange(start, end) {
|
|
2477
|
+
return options.ranges ? { range: [start.offset, end.offset] } : void 0;
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Creates a node for a string, boolean, or number.
|
|
2481
|
+
* @param {number} tokenType The token representing the literal.
|
|
2482
|
+
* @returns {StringNode|NumberNode|BooleanNode} The node representing
|
|
2483
|
+
* the value.
|
|
2484
|
+
*/
|
|
2485
|
+
function createLiteralNode(tokenType) {
|
|
2486
|
+
const token = tokenizer.token;
|
|
2487
|
+
const range = createRange(token.loc.start, token.loc.end);
|
|
2488
|
+
const value = getLiteralValue(text.slice(token.loc.start.offset, token.loc.end.offset), token, json5);
|
|
2489
|
+
const loc = {
|
|
2490
|
+
start: { ...token.loc.start },
|
|
2491
|
+
end: { ...token.loc.end }
|
|
2492
|
+
};
|
|
2493
|
+
const parts = {
|
|
2494
|
+
loc,
|
|
2495
|
+
...range
|
|
2496
|
+
};
|
|
2497
|
+
switch (tokenType) {
|
|
2498
|
+
case tt.String: return types.string(value, parts);
|
|
2499
|
+
case tt.Number: return types.number(value, parts);
|
|
2500
|
+
case tt.Boolean: return types.boolean(value, parts);
|
|
2501
|
+
default: throw new TypeError(`Unknown token type ${token.type}.`);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
/**
|
|
2505
|
+
* Creates a node for a JSON5 identifier.
|
|
2506
|
+
* @param {Token} token The token representing the identifer.
|
|
2507
|
+
* @returns {NaNNode|InfinityNode|IdentifierNode} The node representing
|
|
2508
|
+
* the value.
|
|
2509
|
+
*/
|
|
2510
|
+
function createJSON5IdentifierNode(token) {
|
|
2511
|
+
const range = createRange(token.loc.start, token.loc.end);
|
|
2512
|
+
const identifier = text.slice(token.loc.start.offset, token.loc.end.offset);
|
|
2513
|
+
const loc = {
|
|
2514
|
+
start: { ...token.loc.start },
|
|
2515
|
+
end: { ...token.loc.end }
|
|
2516
|
+
};
|
|
2517
|
+
const parts = {
|
|
2518
|
+
loc,
|
|
2519
|
+
...range
|
|
2520
|
+
};
|
|
2521
|
+
if (token.type !== "Identifier") {
|
|
2522
|
+
let sign = "";
|
|
2523
|
+
if (identifier[0] === "+" || identifier[0] === "-") sign = identifier[0];
|
|
2524
|
+
return types[identifier.includes("NaN") ? "nan" : "infinity"](sign, parts);
|
|
2525
|
+
}
|
|
2526
|
+
return types.identifier(normalizeIdentifier(identifier), parts);
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* Creates a node for a null.
|
|
2530
|
+
* @param {Token} token The token representing null.
|
|
2531
|
+
* @returns {NullNode} The node representing null.
|
|
2532
|
+
*/
|
|
2533
|
+
function createNullNode(token) {
|
|
2534
|
+
const range = createRange(token.loc.start, token.loc.end);
|
|
2535
|
+
return types.null({
|
|
2536
|
+
loc: {
|
|
2537
|
+
start: { ...token.loc.start },
|
|
2538
|
+
end: { ...token.loc.end }
|
|
2539
|
+
},
|
|
2540
|
+
...range
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* Parses a property in an object.
|
|
2545
|
+
* @param {number} tokenType The token representing the property.
|
|
2546
|
+
* @returns {MemberNode} The node representing the property.
|
|
2547
|
+
* @throws {UnexpectedToken} When an unexpected token is found.
|
|
2548
|
+
* @throws {UnexpectedEOF} When the end of the file is reached.
|
|
2549
|
+
*/
|
|
2550
|
+
function parseProperty(tokenType) {
|
|
2551
|
+
if (json5) assertTokenTypes(tokenType, [
|
|
2552
|
+
tt.String,
|
|
2553
|
+
tt.Identifier,
|
|
2554
|
+
tt.Number
|
|
2555
|
+
]);
|
|
2556
|
+
else assertTokenType(tokenType, tt.String);
|
|
2557
|
+
const token = tokenizer.token;
|
|
2558
|
+
if (json5 && tokenType === tt.Number && /[+\-0-9]/.test(text[token.loc.start.offset])) throw new UnexpectedToken(token);
|
|
2559
|
+
let key = tokenType === tt.String ? createLiteralNode(tokenType) : createJSON5IdentifierNode(token);
|
|
2560
|
+
if (json5 && (key.type === "NaN" || key.type === "Infinity")) {
|
|
2561
|
+
if (key.sign !== "") throw new UnexpectedToken(tokenizer.token);
|
|
2562
|
+
key = types.identifier(key.type, {
|
|
2563
|
+
loc: key.loc,
|
|
2564
|
+
...createRange(key.loc.start, key.loc.end)
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
tokenType = next();
|
|
2568
|
+
assertTokenType(tokenType, tt.Colon);
|
|
2569
|
+
const value = parseValue();
|
|
2570
|
+
const range = createRange(key.loc.start, value.loc.end);
|
|
2571
|
+
return types.member(key, value, {
|
|
2572
|
+
loc: {
|
|
2573
|
+
start: { ...key.loc.start },
|
|
2574
|
+
end: { ...value.loc.end }
|
|
2575
|
+
},
|
|
2576
|
+
...range
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
/**
|
|
2580
|
+
* Parses an object literal.
|
|
2581
|
+
* @param {number} firstTokenType The first token type in the object.
|
|
2582
|
+
* @returns {ObjectNode} The object node.
|
|
2583
|
+
* @throws {UnexpectedEOF} When the end of the file is reached.
|
|
2584
|
+
* @throws {UnexpectedToken} When an unexpected token is found.
|
|
2585
|
+
*/
|
|
2586
|
+
function parseObject(firstTokenType) {
|
|
2587
|
+
assertTokenType(firstTokenType, tt.LBrace);
|
|
2588
|
+
const firstToken = tokenizer.token;
|
|
2589
|
+
const members = [];
|
|
2590
|
+
let tokenType = next();
|
|
2591
|
+
if (tokenType !== tt.RBrace) do {
|
|
2592
|
+
members.push(parseProperty(tokenType));
|
|
2593
|
+
tokenType = next();
|
|
2594
|
+
if (!tokenType) throw new UnexpectedEOF(members[members.length - 1].loc.end);
|
|
2595
|
+
if (tokenType === tt.Comma) {
|
|
2596
|
+
tokenType = next();
|
|
2597
|
+
if (allowTrailingCommas && tokenType === tt.RBrace) break;
|
|
2598
|
+
} else break;
|
|
2599
|
+
} while (tokenType);
|
|
2600
|
+
assertTokenType(tokenType, tt.RBrace);
|
|
2601
|
+
const lastToken = tokenizer.token;
|
|
2602
|
+
const range = createRange(firstToken.loc.start, lastToken.loc.end);
|
|
2603
|
+
return types.object(members, {
|
|
2604
|
+
loc: {
|
|
2605
|
+
start: { ...firstToken.loc.start },
|
|
2606
|
+
end: { ...lastToken.loc.end }
|
|
2607
|
+
},
|
|
2608
|
+
...range
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Parses an array literal.
|
|
2613
|
+
* @param {number} firstTokenType The first token in the array.
|
|
2614
|
+
* @returns {ArrayNode} The array node.
|
|
2615
|
+
* @throws {UnexpectedToken} When an unexpected token is found.
|
|
2616
|
+
* @throws {UnexpectedEOF} When the end of the file is reached.
|
|
2617
|
+
*/
|
|
2618
|
+
function parseArray(firstTokenType) {
|
|
2619
|
+
assertTokenType(firstTokenType, tt.LBracket);
|
|
2620
|
+
const firstToken = tokenizer.token;
|
|
2621
|
+
const elements = [];
|
|
2622
|
+
let tokenType = next();
|
|
2623
|
+
if (tokenType !== tt.RBracket) do {
|
|
2624
|
+
const value = parseValue(tokenType);
|
|
2625
|
+
elements.push(types.element(value, { loc: value.loc }));
|
|
2626
|
+
tokenType = next();
|
|
2627
|
+
if (tokenType === tt.Comma) {
|
|
2628
|
+
tokenType = next();
|
|
2629
|
+
if (allowTrailingCommas && tokenType === tt.RBracket) break;
|
|
2630
|
+
} else break;
|
|
2631
|
+
} while (tokenType);
|
|
2632
|
+
assertTokenType(tokenType, tt.RBracket);
|
|
2633
|
+
const lastToken = tokenizer.token;
|
|
2634
|
+
const range = createRange(firstToken.loc.start, lastToken.loc.end);
|
|
2635
|
+
return types.array(elements, {
|
|
2636
|
+
loc: {
|
|
2637
|
+
start: { ...firstToken.loc.start },
|
|
2638
|
+
end: { ...lastToken.loc.end }
|
|
2639
|
+
},
|
|
2640
|
+
...range
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Parses a JSON value.
|
|
2645
|
+
* @param {number} [tokenType] The token type to parse.
|
|
2646
|
+
* @returns {ValueNode|IdentifierNode} The node representing the value.
|
|
2647
|
+
*/
|
|
2648
|
+
function parseValue(tokenType) {
|
|
2649
|
+
tokenType = tokenType ?? next();
|
|
2650
|
+
const token = tokenizer.token;
|
|
2651
|
+
switch (tokenType) {
|
|
2652
|
+
case tt.String:
|
|
2653
|
+
case tt.Boolean: return createLiteralNode(tokenType);
|
|
2654
|
+
case tt.Number:
|
|
2655
|
+
if (json5) {
|
|
2656
|
+
let tokenText = text.slice(token.loc.start.offset, token.loc.end.offset);
|
|
2657
|
+
if (tokenText[0] === "+" || tokenText[0] === "-") tokenText = tokenText.slice(1);
|
|
2658
|
+
if (tokenText === "NaN" || tokenText === "Infinity") return createJSON5IdentifierNode(token);
|
|
2659
|
+
}
|
|
2660
|
+
return createLiteralNode(tokenType);
|
|
2661
|
+
case tt.Null: return createNullNode(token);
|
|
2662
|
+
case tt.LBrace: return parseObject(tokenType);
|
|
2663
|
+
case tt.LBracket: return parseArray(tokenType);
|
|
2664
|
+
default: throw new UnexpectedToken(token);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
const docBody = parseValue();
|
|
2668
|
+
const unexpectedToken = next();
|
|
2669
|
+
if (unexpectedToken) throw new UnexpectedToken(tokenizer.token);
|
|
2670
|
+
const docParts = { loc: {
|
|
2671
|
+
start: {
|
|
2672
|
+
line: 1,
|
|
2673
|
+
column: 1,
|
|
2674
|
+
offset: 0
|
|
2675
|
+
},
|
|
2676
|
+
end: { ...docBody.loc.end }
|
|
2677
|
+
} };
|
|
2678
|
+
if (options.tokens) docParts.tokens = tokens;
|
|
2679
|
+
if (options.ranges) docParts.range = [docParts.loc.start.offset, docParts.loc.end.offset];
|
|
2680
|
+
return types.document(docBody, docParts);
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* @fileoverview Evaluator for Momoa AST.
|
|
2684
|
+
* @author Nicholas C. Zakas
|
|
2685
|
+
*/
|
|
2686
|
+
/** @typedef {import("./typedefs.js").AnyNode} AnyNode */
|
|
2687
|
+
/** @typedef {import("./typedefs.js").JSONValue} JSONValue */
|
|
2688
|
+
/**
|
|
2689
|
+
* Evaluates a Momoa AST node into a JavaScript value.
|
|
2690
|
+
* @param {AnyNode} node The node to interpet.
|
|
2691
|
+
* @returns {JSONValue} The JavaScript value for the node.
|
|
2692
|
+
*/
|
|
2693
|
+
function evaluate(node) {
|
|
2694
|
+
switch (node.type) {
|
|
2695
|
+
case "String": return node.value;
|
|
2696
|
+
case "Number": return node.value;
|
|
2697
|
+
case "Boolean": return node.value;
|
|
2698
|
+
case "Null": return null;
|
|
2699
|
+
case "NaN": return NaN;
|
|
2700
|
+
case "Infinity": return node.sign === "-" ? -Infinity : Infinity;
|
|
2701
|
+
case "Identifier": return node.name;
|
|
2702
|
+
case "Array": return node.elements.map((element) => evaluate(element.value));
|
|
2703
|
+
case "Object": {
|
|
2704
|
+
/** @type {{[property: string]: JSONValue}} */
|
|
2705
|
+
const object = {};
|
|
2706
|
+
node.members.forEach((member) => {
|
|
2707
|
+
object[evaluate(member.name)] = evaluate(member.value);
|
|
2708
|
+
});
|
|
2709
|
+
return object;
|
|
2710
|
+
}
|
|
2711
|
+
case "Document": return evaluate(node.body);
|
|
2712
|
+
case "Element": throw new Error("Cannot evaluate array element outside of an array.");
|
|
2713
|
+
case "Member": throw new Error("Cannot evaluate object member outside of an object.");
|
|
2714
|
+
default: throw new Error(`Unknown node type ${node.type}.`);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* @fileoverview Printer for Momoa AST.
|
|
2719
|
+
* @author Nicholas C. Zakas
|
|
2720
|
+
*/
|
|
2721
|
+
/**
|
|
2722
|
+
* Prints the string representation of a Boolean node.
|
|
2723
|
+
* @param {BooleanNode} node The node to print.
|
|
2724
|
+
* @returns {string} The boolean value.
|
|
2725
|
+
*/
|
|
2726
|
+
function printBoolean(node) {
|
|
2727
|
+
return node.value ? "true" : "false";
|
|
2728
|
+
}
|
|
2729
|
+
/**
|
|
2730
|
+
* Prints the string representation of a null node.
|
|
2731
|
+
* @returns {string} The string "null".
|
|
2732
|
+
*/
|
|
2733
|
+
function printNull() {
|
|
2734
|
+
return "null";
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* Prints the string representation of a number node.
|
|
2738
|
+
* @param {NumberNode} node The node to print.
|
|
2739
|
+
* @returns {string} The number value.
|
|
2740
|
+
*/
|
|
2741
|
+
function printNumber(node) {
|
|
2742
|
+
return node.value.toString();
|
|
2743
|
+
}
|
|
2744
|
+
/**
|
|
2745
|
+
* Prints the string representation of a NaN node.
|
|
2746
|
+
* @returns {string} The string "NaN".
|
|
2747
|
+
*/
|
|
2748
|
+
function printNaN() {
|
|
2749
|
+
return "NaN";
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Prints the string representation of an Infinity node.
|
|
2753
|
+
* @param {InfinityNode} node The node to print.
|
|
2754
|
+
* @returns {string} The string "Infinity" or "-Infinity".
|
|
2755
|
+
*/
|
|
2756
|
+
function printInfinity(node) {
|
|
2757
|
+
return node.sign + "Infinity";
|
|
2758
|
+
}
|
|
2759
|
+
/**
|
|
2760
|
+
* Prints the string representation of a string node.
|
|
2761
|
+
* @param {StringNode} node The node to print.
|
|
2762
|
+
* @returns {string} The string value.
|
|
2763
|
+
*/
|
|
2764
|
+
function printString(node) {
|
|
2765
|
+
let result = "\"";
|
|
2766
|
+
for (const c of node.value) {
|
|
2767
|
+
const newChar = json5CharToEscape.get(c);
|
|
2768
|
+
if (newChar) {
|
|
2769
|
+
result += "\\" + newChar;
|
|
2770
|
+
continue;
|
|
2771
|
+
}
|
|
2772
|
+
if (c === "\"") {
|
|
2773
|
+
result += "\\\"";
|
|
2774
|
+
continue;
|
|
2775
|
+
}
|
|
2776
|
+
if (c < " " || c === "") {
|
|
2777
|
+
const hex = c.codePointAt(0).toString(16).toUpperCase();
|
|
2778
|
+
result += `\\u${"0000".substring(hex.length)}${hex}`;
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
result += c;
|
|
2782
|
+
}
|
|
2783
|
+
return result + "\"";
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Prints the string representation of an identifier node.
|
|
2787
|
+
* @param {IdentifierNode} node The node to print.
|
|
2788
|
+
* @returns {string} The identifier name.
|
|
2789
|
+
*/
|
|
2790
|
+
function printIdentifier(node) {
|
|
2791
|
+
return node.name;
|
|
2792
|
+
}
|
|
2793
|
+
/**
|
|
2794
|
+
* Prints the string representation of an array node.
|
|
2795
|
+
* @param {ArrayNode} node The node to print.
|
|
2796
|
+
* @param {string} indent The string to use for indentation.
|
|
2797
|
+
* @param {number} indentLevel The current level of indentation.
|
|
2798
|
+
* @returns {string} The array value.
|
|
2799
|
+
*/
|
|
2800
|
+
function printArray(node, indent, indentLevel) {
|
|
2801
|
+
const newLine = indent ? "\n" : "";
|
|
2802
|
+
const indentString = indent.repeat(indentLevel);
|
|
2803
|
+
const elementIndentString = indent.repeat(indentLevel + 1);
|
|
2804
|
+
return `[${newLine}${node.elements.map((element) => `${elementIndentString}${printValue(element.value, indent, indentLevel + 1)}`).join(`,${newLine}`)}${newLine}${indentString}]`;
|
|
2805
|
+
}
|
|
2806
|
+
/**
|
|
2807
|
+
* Prints the string representation of a member node.
|
|
2808
|
+
* @param {MemberNode} node The node to print.
|
|
2809
|
+
* @param {string} indent The string to use for indentation.
|
|
2810
|
+
* @param {number} indentLevel The current level of indentation.
|
|
2811
|
+
* @returns {string} The member value.
|
|
2812
|
+
*/
|
|
2813
|
+
function printMember(node, indent, indentLevel) {
|
|
2814
|
+
const space = indent ? " " : "";
|
|
2815
|
+
return `${printValue(node.name, indent, indentLevel)}:${space}${printValue(node.value, indent, indentLevel + 1)}`;
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2818
|
+
* Prints the string representation of an object node.
|
|
2819
|
+
* @param {ObjectNode} node The node to print.
|
|
2820
|
+
* @param {string} indent The string to use for indentation.
|
|
2821
|
+
* @param {number} indentLevel The current level of indentation.
|
|
2822
|
+
* @returns {string} The object value.
|
|
2823
|
+
*/
|
|
2824
|
+
function printObject(node, indent, indentLevel) {
|
|
2825
|
+
const newLine = indent ? "\n" : "";
|
|
2826
|
+
const indentString = indent.repeat(indentLevel);
|
|
2827
|
+
const memberIndentString = indent.repeat(indentLevel + 1);
|
|
2828
|
+
return `{${newLine}${node.members.map((member) => `${memberIndentString}${printMember(member, indent, indentLevel)}`).join(`,${newLine}`)}${newLine}${indentString}}`;
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* Prints the string representation of a node.
|
|
2832
|
+
* @param {AnyNode} node The node to print.
|
|
2833
|
+
* @param {string} indentString The string to use for indentation.
|
|
2834
|
+
* @param {number} indentLevel The current level of indentation.
|
|
2835
|
+
* @returns {string} The string representation of the node.
|
|
2836
|
+
* @throws {TypeError} If the node type is unknown.
|
|
2837
|
+
|
|
2838
|
+
*/
|
|
2839
|
+
function printValue(node, indentString, indentLevel) {
|
|
2840
|
+
switch (node.type) {
|
|
2841
|
+
case "String": return printString(node);
|
|
2842
|
+
case "Number": return printNumber(node);
|
|
2843
|
+
case "Boolean": return printBoolean(node);
|
|
2844
|
+
case "Null": return printNull();
|
|
2845
|
+
case "NaN": return printNaN();
|
|
2846
|
+
case "Infinity": return printInfinity(node);
|
|
2847
|
+
case "Identifier": return printIdentifier(node);
|
|
2848
|
+
case "Array": return printArray(node, indentString, indentLevel);
|
|
2849
|
+
case "Object": return printObject(node, indentString, indentLevel);
|
|
2850
|
+
case "Document": return printValue(node.body, indentString, indentLevel);
|
|
2851
|
+
default: throw new TypeError(`Unknown node type: ${node.type}`);
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
/**
|
|
2855
|
+
* Converts a Momoa AST back into a JSON string.
|
|
2856
|
+
* @param {AnyNode} node The node to print.
|
|
2857
|
+
* @param {Object} options Options for the print.
|
|
2858
|
+
* @param {number} [options.indent=0] The number of spaces to indent each line. If
|
|
2859
|
+
* greater than 0, then newlines and indents will be added to output.
|
|
2860
|
+
* @returns {string} The JSON representation of the AST.
|
|
2861
|
+
*/
|
|
2862
|
+
function print(node, { indent = 0 } = {}) {
|
|
2863
|
+
const indentLevel = 0;
|
|
2864
|
+
const indentString = " ".repeat(indent);
|
|
2865
|
+
return printValue(node, indentString, indentLevel);
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
//#endregion
|
|
2869
|
+
//#region src/parse/json.ts
|
|
2870
|
+
const CHILD_KEYS = {
|
|
2871
|
+
Document: ["body"],
|
|
2872
|
+
Object: ["members"],
|
|
2873
|
+
Member: ["name", "value"],
|
|
2874
|
+
Element: ["value"],
|
|
2875
|
+
Array: ["elements"],
|
|
2876
|
+
String: [],
|
|
2877
|
+
Number: [],
|
|
2878
|
+
Boolean: [],
|
|
2879
|
+
Null: [],
|
|
2880
|
+
Identifier: [],
|
|
2881
|
+
NaN: [],
|
|
2882
|
+
Infinity: []
|
|
2883
|
+
};
|
|
2884
|
+
/** Determines if a given value is an AST node. */
|
|
2885
|
+
function isNode(value) {
|
|
2886
|
+
return !!value && typeof value === "object" && "type" in value && typeof value.type === "string";
|
|
2887
|
+
}
|
|
2888
|
+
/** Get ObjectNode members as object */
|
|
2889
|
+
function getObjMembers(node) {
|
|
2890
|
+
const members = {};
|
|
2891
|
+
if (node.type !== "Object") return members;
|
|
2892
|
+
for (let i = 0; i < node.members.length; i++) {
|
|
2893
|
+
const m = node.members[i];
|
|
2894
|
+
if (m.name.type !== "String") continue;
|
|
2895
|
+
members[m.name.value] = {
|
|
2896
|
+
...m.value,
|
|
2897
|
+
index: i
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
return members;
|
|
2901
|
+
}
|
|
2902
|
+
/** Inject members to ObjectNode */
|
|
2903
|
+
function injectObjMembers(node, members = []) {
|
|
2904
|
+
if (node.type !== "Object") return;
|
|
2905
|
+
node.members.push(...members);
|
|
2906
|
+
}
|
|
2907
|
+
/** Replace an ObjectNode’s contents outright with another */
|
|
2908
|
+
function replaceObjMembers(a, b) {
|
|
2909
|
+
a.members = b.type === "Document" && b.body?.members || b.members;
|
|
2910
|
+
}
|
|
2911
|
+
/**
|
|
2912
|
+
* Variation of Momoa’s traverse(), which keeps track of global path.
|
|
2913
|
+
* Allows mutation of AST (along with any consequences)
|
|
2914
|
+
*/
|
|
2915
|
+
function traverse(root, visitor) {
|
|
2916
|
+
/**
|
|
2917
|
+
* Recursively visits a node.
|
|
2918
|
+
* @param {AnyNode} node The node to visit.
|
|
2919
|
+
* @param {AnyNode} [parent] The parent of the node to visit.
|
|
2920
|
+
* @return {void}
|
|
2921
|
+
*/
|
|
2922
|
+
function visitNode(node, parent, path = []) {
|
|
2923
|
+
const nextPath = [...path];
|
|
2924
|
+
if (node.type === "Member") {
|
|
2925
|
+
const { name } = node;
|
|
2926
|
+
nextPath.push("value" in name ? name.value : String(name));
|
|
2927
|
+
}
|
|
2928
|
+
visitor.enter?.(node, parent, nextPath);
|
|
2929
|
+
const childNode = CHILD_KEYS[node.type];
|
|
2930
|
+
for (const key of childNode ?? []) {
|
|
2931
|
+
const value = node[key];
|
|
2932
|
+
if (!value) continue;
|
|
2933
|
+
if (Array.isArray(value)) for (let i = 0; i < value.length; i++) visitNode(value[i], node, key === "elements" ? [...nextPath, String(i)] : nextPath);
|
|
2934
|
+
else if (isNode(value)) visitNode(value, node, nextPath);
|
|
2935
|
+
}
|
|
2936
|
+
visitor.exit?.(node, parent, nextPath);
|
|
2937
|
+
}
|
|
2938
|
+
visitNode(root, void 0, []);
|
|
2939
|
+
}
|
|
2940
|
+
/** Determine if an input is likely a JSON string */
|
|
2941
|
+
function maybeRawJSON(input) {
|
|
2942
|
+
return typeof input === "string" && input.trim().startsWith("{");
|
|
2943
|
+
}
|
|
2944
|
+
/** Find Momoa node by traversing paths */
|
|
2945
|
+
function findNode(node, path) {
|
|
2946
|
+
if (!path.length) return;
|
|
2947
|
+
let nextNode;
|
|
2948
|
+
switch (node.type) {
|
|
2949
|
+
case "Document": return findNode(node.body, path);
|
|
2950
|
+
case "Object": {
|
|
2951
|
+
const [member, ...rest] = path;
|
|
2952
|
+
nextNode = node.members.find((m) => m.name.type === "String" && m.name.value === member)?.value;
|
|
2953
|
+
if (nextNode && rest.length) return findNode(nextNode, path.slice(1));
|
|
2954
|
+
break;
|
|
2955
|
+
}
|
|
2956
|
+
case "Array": {
|
|
2957
|
+
const [_index, ...rest] = path;
|
|
2958
|
+
const index = Number.parseInt(_index, 10);
|
|
2959
|
+
nextNode = node.elements[index]?.value;
|
|
2960
|
+
if (nextNode && rest.length) return findNode(nextNode, path.slice(1));
|
|
2961
|
+
break;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
return nextNode;
|
|
2965
|
+
}
|
|
2966
|
+
function toMomoa(input, { continueOnError, filename, logger, yamlToMomoa }) {
|
|
2967
|
+
let src = "";
|
|
2968
|
+
if (typeof input === "string") src = input;
|
|
2969
|
+
let document = {};
|
|
2970
|
+
if (typeof input === "string" && !maybeRawJSON(input)) if (yamlToMomoa) try {
|
|
2971
|
+
document = yamlToMomoa(input);
|
|
2972
|
+
} catch (err) {
|
|
2973
|
+
logger.error({
|
|
2974
|
+
group: "parser",
|
|
2975
|
+
label: "json",
|
|
2976
|
+
message: String(err),
|
|
2977
|
+
filename,
|
|
2978
|
+
src: input,
|
|
2979
|
+
continueOnError
|
|
2980
|
+
});
|
|
2981
|
+
}
|
|
2982
|
+
else logger.error({
|
|
2983
|
+
group: "parser",
|
|
2984
|
+
label: "yaml",
|
|
2985
|
+
message: `Install \`yaml-to-momoa\` package to parse YAML, and pass in as option, e.g.:
|
|
2986
|
+
|
|
2987
|
+
import { parse } from '@terrazzo/parser';
|
|
2988
|
+
import yamlToMomoa from 'yaml-to-momoa';
|
|
2989
|
+
|
|
2990
|
+
parse(yamlString, { yamlToMomoa });`,
|
|
2991
|
+
continueOnError: false
|
|
2992
|
+
});
|
|
2993
|
+
else document = parseJSON(input);
|
|
2994
|
+
if (!src) src = print(document, { indent: 2 });
|
|
2995
|
+
return {
|
|
2996
|
+
src,
|
|
2997
|
+
document
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
/** Momoa, just with default options pre-set */
|
|
3001
|
+
function parseJSON(input, options) {
|
|
3002
|
+
return parse$1(typeof input === "string" ? input : JSON.stringify(input, void 0, 2), {
|
|
3003
|
+
mode: "jsonc",
|
|
3004
|
+
ranges: true,
|
|
3005
|
+
tokens: true,
|
|
3006
|
+
...options
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
//#endregion
|
|
3011
|
+
//#region src/parse/alias.ts
|
|
3012
|
+
/**
|
|
3013
|
+
* Resolve aliases and update the token nodes.
|
|
3014
|
+
*
|
|
3015
|
+
* Data structures are in an awkward in-between phase, where they have
|
|
3016
|
+
* placeholders for data but we still need to resolve everything. As such,
|
|
3017
|
+
* TypeScript will raise errors expecting the final shape.
|
|
3018
|
+
*
|
|
3019
|
+
* This is also a bit tricky because different token types alias slightly
|
|
3020
|
+
* differently. For example, color tokens and other “primitive” tokens behave
|
|
3021
|
+
* as-expected. But composite tokens like Typography, Gradient, Border, etc. can
|
|
3022
|
+
* either fully- or partially-alias their values. Then we add modes to the mix,
|
|
3023
|
+
* and we have to do the work all over again for each mode declared.
|
|
3024
|
+
*
|
|
3025
|
+
* All that to say, there are a generous amount of TypeScript overrides here rather
|
|
3026
|
+
* than try to codify indeterminate shapes.
|
|
3027
|
+
*/
|
|
3028
|
+
function applyAliases(token, options) {
|
|
3029
|
+
token.mode["."] ??= {};
|
|
3030
|
+
token.mode["."].$value = token.$value;
|
|
3031
|
+
token.mode["."].originalValue ??= token.originalValue.$value;
|
|
3032
|
+
token.mode["."].source ??= token.source;
|
|
3033
|
+
if (typeof token.$value === "string" && isAlias(token.$value)) {
|
|
3034
|
+
const { aliasChain, resolvedToken } = resolveAlias(token.$value, {
|
|
3035
|
+
...options,
|
|
3036
|
+
token
|
|
3037
|
+
});
|
|
3038
|
+
token.aliasOf = resolvedToken.id;
|
|
3039
|
+
token.aliasChain = aliasChain;
|
|
3040
|
+
token.$value = resolvedToken.$value;
|
|
3041
|
+
}
|
|
3042
|
+
for (const mode of Object.keys(token.mode)) {
|
|
3043
|
+
const modeValue = token.mode[mode].$value;
|
|
3044
|
+
if (typeof modeValue === "string" && isAlias(modeValue)) {
|
|
3045
|
+
const expectedType = [token.$type];
|
|
3046
|
+
const { aliasChain, resolvedToken } = resolveAlias(modeValue, {
|
|
3047
|
+
...options,
|
|
3048
|
+
token,
|
|
3049
|
+
expectedType,
|
|
3050
|
+
node: token.mode[mode].source?.node || options.node
|
|
3051
|
+
});
|
|
3052
|
+
token.mode[mode].aliasOf = resolvedToken.id;
|
|
3053
|
+
token.mode[mode].aliasChain = aliasChain;
|
|
3054
|
+
token.mode[mode].$value = resolvedToken.$value;
|
|
3055
|
+
continue;
|
|
3056
|
+
}
|
|
3057
|
+
if (typeof token.$value === "object" && typeof token.mode[mode].$value === "object" && !Array.isArray(token.$value)) {
|
|
3058
|
+
for (const [k, v] of Object.entries(token.$value)) if (!(k in token.mode[mode].$value)) token.mode[mode].$value[k] = v;
|
|
3059
|
+
}
|
|
3060
|
+
const node = getObjMembers(options.node).$value || options.node;
|
|
3061
|
+
switch (token.$type) {
|
|
3062
|
+
case "border": {
|
|
3063
|
+
applyBorderPartialAlias(token, mode, {
|
|
3064
|
+
...options,
|
|
3065
|
+
node
|
|
3066
|
+
});
|
|
3067
|
+
break;
|
|
3068
|
+
}
|
|
3069
|
+
case "gradient": {
|
|
3070
|
+
applyGradientPartialAlias(token, mode, {
|
|
3071
|
+
...options,
|
|
3072
|
+
node
|
|
3073
|
+
});
|
|
3074
|
+
break;
|
|
3075
|
+
}
|
|
3076
|
+
case "shadow": {
|
|
3077
|
+
applyShadowPartialAlias(token, mode, {
|
|
3078
|
+
...options,
|
|
3079
|
+
node
|
|
3080
|
+
});
|
|
3081
|
+
break;
|
|
3082
|
+
}
|
|
3083
|
+
case "strokeStyle": {
|
|
3084
|
+
applyStrokeStylePartialAlias(token, mode, {
|
|
3085
|
+
...options,
|
|
3086
|
+
node
|
|
3087
|
+
});
|
|
3088
|
+
break;
|
|
3089
|
+
}
|
|
3090
|
+
case "transition": {
|
|
3091
|
+
applyTransitionPartialAlias(token, mode, {
|
|
3092
|
+
...options,
|
|
3093
|
+
node
|
|
3094
|
+
});
|
|
3095
|
+
break;
|
|
3096
|
+
}
|
|
3097
|
+
case "typography": {
|
|
3098
|
+
applyTypographyPartialAlias(token, mode, {
|
|
3099
|
+
...options,
|
|
3100
|
+
node
|
|
3101
|
+
});
|
|
3102
|
+
break;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
const LIST_FORMAT = new Intl.ListFormat("en-us", { type: "disjunction" });
|
|
3108
|
+
/**
|
|
3109
|
+
* Resolve alias. Also add info on root node if it’s the root token (has .id)
|
|
3110
|
+
*/
|
|
3111
|
+
function resolveAlias(alias, options) {
|
|
3112
|
+
const baseMessage = {
|
|
3113
|
+
group: "parser",
|
|
3114
|
+
label: "alias",
|
|
3115
|
+
node: options?.node,
|
|
3116
|
+
filename: options.filename,
|
|
3117
|
+
src: options.src
|
|
3118
|
+
};
|
|
3119
|
+
const { logger, token, tokensSet } = options;
|
|
3120
|
+
const shallowAliasID = parseAlias(alias);
|
|
3121
|
+
const { token: resolvedToken, chain } = _resolveAliasInner(shallowAliasID, options);
|
|
3122
|
+
if (!tokensSet[token.id].$type) tokensSet[token.id].$type = resolvedToken.$type;
|
|
3123
|
+
const expectedType = [...options.expectedType ?? []];
|
|
3124
|
+
if (token.$type && !expectedType?.length) expectedType.push(token.$type);
|
|
3125
|
+
if (expectedType?.length && !expectedType.includes(resolvedToken.$type)) logger.error({
|
|
3126
|
+
...baseMessage,
|
|
3127
|
+
message: `Invalid alias: expected $type: ${LIST_FORMAT.format(expectedType)}, received $type: ${resolvedToken.$type}.`,
|
|
3128
|
+
node: options.node?.type === "Object" && getObjMembers(options.node).$value || baseMessage.node
|
|
3129
|
+
});
|
|
3130
|
+
if (chain?.length && resolvedToken) {
|
|
3131
|
+
let needsSort = false;
|
|
3132
|
+
for (const id of chain) {
|
|
3133
|
+
if (id !== resolvedToken.id && !resolvedToken.aliasedBy?.includes(id)) {
|
|
3134
|
+
resolvedToken.aliasedBy ??= [];
|
|
3135
|
+
resolvedToken.aliasedBy.push(id);
|
|
3136
|
+
needsSort = true;
|
|
3137
|
+
}
|
|
3138
|
+
if (token && !resolvedToken.aliasedBy?.includes(token.id)) {
|
|
3139
|
+
resolvedToken.aliasedBy ??= [];
|
|
3140
|
+
resolvedToken.aliasedBy.push(token.id);
|
|
3141
|
+
needsSort = true;
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
if (needsSort) resolvedToken.aliasedBy.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
|
|
3145
|
+
}
|
|
3146
|
+
return {
|
|
3147
|
+
resolvedToken,
|
|
3148
|
+
aliasChain: chain
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3151
|
+
function _resolveAliasInner(alias, { scanned = [],...options }) {
|
|
3152
|
+
const { logger, filename, src, node, tokensSet } = options;
|
|
3153
|
+
const baseMessage = {
|
|
3154
|
+
group: "parser",
|
|
3155
|
+
label: "alias",
|
|
3156
|
+
filename,
|
|
3157
|
+
src,
|
|
3158
|
+
node
|
|
3159
|
+
};
|
|
3160
|
+
const id = parseAlias(alias);
|
|
3161
|
+
if (!tokensSet[id]) logger.error({
|
|
3162
|
+
...baseMessage,
|
|
3163
|
+
message: `Alias {${alias}} not found.`
|
|
3164
|
+
});
|
|
3165
|
+
if (scanned.includes(id)) logger.error({
|
|
3166
|
+
...baseMessage,
|
|
3167
|
+
message: `Circular alias detected from ${alias}.`
|
|
3168
|
+
});
|
|
3169
|
+
const token = tokensSet[id];
|
|
3170
|
+
scanned.push(id);
|
|
3171
|
+
if (typeof token.originalValue.$value !== "string" || !isAlias(token.originalValue.$value)) return {
|
|
3172
|
+
token,
|
|
3173
|
+
chain: scanned
|
|
3174
|
+
};
|
|
3175
|
+
return _resolveAliasInner(token.originalValue.$value, {
|
|
3176
|
+
...options,
|
|
3177
|
+
scanned
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
function applyBorderPartialAlias(token, mode, options) {
|
|
3181
|
+
for (const [k, v] of Object.entries(token.mode[mode].$value)) if (typeof v === "string" && isAlias(v)) {
|
|
3182
|
+
token.mode[mode].partialAliasOf ??= {};
|
|
3183
|
+
const node = getObjMembers(options.node)[k] || options.node;
|
|
3184
|
+
const { resolvedToken } = resolveAlias(v, {
|
|
3185
|
+
...options,
|
|
3186
|
+
token,
|
|
3187
|
+
expectedType: {
|
|
3188
|
+
color: ["color"],
|
|
3189
|
+
width: ["dimension"],
|
|
3190
|
+
style: ["strokeStyle"]
|
|
3191
|
+
}[k],
|
|
3192
|
+
node
|
|
3193
|
+
});
|
|
3194
|
+
token.mode[mode].partialAliasOf[k] = parseAlias(v);
|
|
3195
|
+
if (mode === ".") {
|
|
3196
|
+
token.partialAliasOf ??= {};
|
|
3197
|
+
token.partialAliasOf[k] = parseAlias(v);
|
|
3198
|
+
}
|
|
3199
|
+
token.mode[mode].$value[k] = resolvedToken.$value;
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
function applyGradientPartialAlias(token, mode, options) {
|
|
3203
|
+
for (let i = 0; i < token.mode[mode].$value.length; i++) {
|
|
3204
|
+
const step = token.mode[mode].$value[i];
|
|
3205
|
+
for (const [k, v] of Object.entries(step)) if (typeof v === "string" && isAlias(v)) {
|
|
3206
|
+
token.mode[mode].partialAliasOf ??= [];
|
|
3207
|
+
token.mode[mode].partialAliasOf[i] ??= {};
|
|
3208
|
+
const expectedType = {
|
|
3209
|
+
color: ["color"],
|
|
3210
|
+
position: ["number"]
|
|
3211
|
+
}[k];
|
|
3212
|
+
let node = options.node?.elements?.[i]?.value || options.node;
|
|
3213
|
+
if (node.type === "Object") node = getObjMembers(node)[k] || node;
|
|
3214
|
+
const { resolvedToken } = resolveAlias(v, {
|
|
3215
|
+
...options,
|
|
3216
|
+
token,
|
|
3217
|
+
expectedType,
|
|
3218
|
+
node
|
|
3219
|
+
});
|
|
3220
|
+
token.mode[mode].partialAliasOf[i][k] = parseAlias(v);
|
|
3221
|
+
if (mode === ".") {
|
|
3222
|
+
token.partialAliasOf ??= [];
|
|
3223
|
+
token.partialAliasOf[i] ??= {};
|
|
3224
|
+
token.partialAliasOf[i][k] = parseAlias(v);
|
|
3225
|
+
}
|
|
3226
|
+
step[k] = resolvedToken.$value;
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
function applyShadowPartialAlias(token, mode, options) {
|
|
3231
|
+
if (!Array.isArray(token.mode[mode].$value)) token.mode[mode].$value = [token.mode[mode].$value];
|
|
3232
|
+
for (let i = 0; i < token.mode[mode].$value.length; i++) {
|
|
3233
|
+
const layer = token.mode[mode].$value[i];
|
|
3234
|
+
for (const [k, v] of Object.entries(layer)) if (typeof v === "string" && isAlias(v)) {
|
|
3235
|
+
token.mode[mode].partialAliasOf ??= [];
|
|
3236
|
+
token.mode[mode].partialAliasOf[i] ??= {};
|
|
3237
|
+
const expectedType = {
|
|
3238
|
+
offsetX: ["dimension"],
|
|
3239
|
+
offsetY: ["dimension"],
|
|
3240
|
+
blur: ["dimension"],
|
|
3241
|
+
spread: ["dimension"],
|
|
3242
|
+
color: ["color"],
|
|
3243
|
+
inset: ["boolean"]
|
|
3244
|
+
}[k];
|
|
3245
|
+
let node = options.node?.elements?.[i] || options.node;
|
|
3246
|
+
if (node.type === "Object") node = getObjMembers(node)[k] || node;
|
|
3247
|
+
const { resolvedToken } = resolveAlias(v, {
|
|
3248
|
+
...options,
|
|
3249
|
+
token,
|
|
3250
|
+
expectedType,
|
|
3251
|
+
node
|
|
3252
|
+
});
|
|
3253
|
+
token.mode[mode].partialAliasOf[i][k] = parseAlias(v);
|
|
3254
|
+
if (mode === ".") {
|
|
3255
|
+
token.partialAliasOf ??= [];
|
|
3256
|
+
token.partialAliasOf[i] ??= {};
|
|
3257
|
+
token.partialAliasOf[i][k] = parseAlias(v);
|
|
3258
|
+
}
|
|
3259
|
+
layer[k] = resolvedToken.$value;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
function applyStrokeStylePartialAlias(token, mode, options) {
|
|
3264
|
+
if (typeof token.mode[mode].$value !== "object" || !("dashArray" in token.mode[mode].$value)) return;
|
|
3265
|
+
for (let i = 0; i < token.mode[mode].$value.dashArray.length; i++) {
|
|
3266
|
+
const dash = token.mode[mode].$value.dashArray[i];
|
|
3267
|
+
if (typeof dash === "string" && isAlias(dash)) {
|
|
3268
|
+
let node = getObjMembers(options.node).dashArray || options.node;
|
|
3269
|
+
if (node.type === "Array") node = node?.elements?.[i]?.value || node;
|
|
3270
|
+
const { resolvedToken } = resolveAlias(dash, {
|
|
3271
|
+
...options,
|
|
3272
|
+
token,
|
|
3273
|
+
expectedType: ["dimension"],
|
|
3274
|
+
node
|
|
3275
|
+
});
|
|
3276
|
+
token.mode[mode].$value.dashArray[i] = resolvedToken.$value;
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
function applyTransitionPartialAlias(token, mode, options) {
|
|
3281
|
+
for (const [k, v] of Object.entries(token.mode[mode].$value)) if (typeof v === "string" && isAlias(v)) {
|
|
3282
|
+
token.mode[mode].partialAliasOf ??= {};
|
|
3283
|
+
const expectedType = {
|
|
3284
|
+
duration: ["duration"],
|
|
3285
|
+
delay: ["duration"],
|
|
3286
|
+
timingFunction: ["cubicBezier"]
|
|
3287
|
+
}[k];
|
|
3288
|
+
const node = getObjMembers(options.node)[k] || options.node;
|
|
3289
|
+
const { resolvedToken } = resolveAlias(v, {
|
|
3290
|
+
...options,
|
|
3291
|
+
token,
|
|
3292
|
+
expectedType,
|
|
3293
|
+
node
|
|
3294
|
+
});
|
|
3295
|
+
token.mode[mode].partialAliasOf[k] = parseAlias(v);
|
|
3296
|
+
if (mode === ".") {
|
|
3297
|
+
token.partialAliasOf ??= {};
|
|
3298
|
+
token.partialAliasOf[k] = parseAlias(v);
|
|
3299
|
+
}
|
|
3300
|
+
token.mode[mode].$value[k] = resolvedToken.$value;
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
function applyTypographyPartialAlias(token, mode, options) {
|
|
3304
|
+
for (const [k, v] of Object.entries(token.mode[mode].$value)) if (typeof v === "string" && isAlias(v)) {
|
|
3305
|
+
token.partialAliasOf ??= {};
|
|
3306
|
+
token.mode[mode].partialAliasOf ??= {};
|
|
3307
|
+
const expectedType = {
|
|
3308
|
+
fontFamily: ["fontFamily"],
|
|
3309
|
+
fontSize: ["dimension"],
|
|
3310
|
+
fontWeight: ["fontWeight"],
|
|
3311
|
+
letterSpacing: ["dimension"],
|
|
3312
|
+
lineHeight: ["dimension", "number"]
|
|
3313
|
+
}[k] || ["string"];
|
|
3314
|
+
const node = getObjMembers(options.node)[k] || options.node;
|
|
3315
|
+
const { resolvedToken } = resolveAlias(v, {
|
|
3316
|
+
...options,
|
|
3317
|
+
token,
|
|
3318
|
+
expectedType,
|
|
3319
|
+
node
|
|
3320
|
+
});
|
|
3321
|
+
token.mode[mode].partialAliasOf[k] = parseAlias(v);
|
|
3322
|
+
if (mode === ".") token.partialAliasOf[k] = parseAlias(v);
|
|
3323
|
+
token.mode[mode].$value[k] = resolvedToken.$value;
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
//#endregion
|
|
3328
|
+
//#region src/parse/normalize.ts
|
|
3329
|
+
const FONT_WEIGHT_MAP = {
|
|
3330
|
+
thin: 100,
|
|
3331
|
+
hairline: 100,
|
|
3332
|
+
"extra-light": 200,
|
|
3333
|
+
"ultra-light": 200,
|
|
3334
|
+
light: 300,
|
|
3335
|
+
normal: 400,
|
|
3336
|
+
regular: 400,
|
|
3337
|
+
book: 400,
|
|
3338
|
+
medium: 500,
|
|
3339
|
+
"semi-bold": 600,
|
|
3340
|
+
"demi-bold": 600,
|
|
3341
|
+
bold: 700,
|
|
3342
|
+
"extra-bold": 800,
|
|
3343
|
+
"ultra-bold": 800,
|
|
3344
|
+
black: 900,
|
|
3345
|
+
heavy: 900,
|
|
3346
|
+
"extra-black": 950,
|
|
3347
|
+
"ultra-black": 950
|
|
3348
|
+
};
|
|
3349
|
+
const NUMBER_WITH_UNIT_RE = /(-?\d*\.?\d+)(.*)/;
|
|
3350
|
+
/** Fill in defaults, and return predictable shapes for tokens */
|
|
3351
|
+
function normalizeValue(token) {
|
|
3352
|
+
if (typeof token.$value === "string" && isAlias(token.$value)) return token.$value;
|
|
3353
|
+
switch (token.$type) {
|
|
3354
|
+
case "boolean": return !!token.$value;
|
|
3355
|
+
case "border": {
|
|
3356
|
+
if (typeof token.$value === "string") return token.$value;
|
|
3357
|
+
return {
|
|
3358
|
+
color: normalizeValue({
|
|
3359
|
+
$type: "color",
|
|
3360
|
+
$value: token.$value.color ?? "#000000"
|
|
3361
|
+
}),
|
|
3362
|
+
style: normalizeValue({
|
|
3363
|
+
$type: "strokeStyle",
|
|
3364
|
+
$value: token.$value.style ?? "solid"
|
|
3365
|
+
}),
|
|
3366
|
+
width: normalizeValue({
|
|
3367
|
+
$type: "dimension",
|
|
3368
|
+
$value: token.$value.width
|
|
3369
|
+
})
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
case "color": {
|
|
3373
|
+
if (typeof token.$value === "string") return parseColor(token.$value);
|
|
3374
|
+
const newValue = {
|
|
3375
|
+
colorSpace: token.$value.colorSpace,
|
|
3376
|
+
components: token.$value.components ?? token.$value.channels,
|
|
3377
|
+
alpha: token.$value.alpha ?? 1
|
|
3378
|
+
};
|
|
3379
|
+
if ("hex" in token.$value) newValue.hex = token.$value.hex;
|
|
3380
|
+
return newValue;
|
|
3381
|
+
}
|
|
3382
|
+
case "cubicBezier": {
|
|
3383
|
+
if (typeof token.$value === "string") return token.$value;
|
|
3384
|
+
return token.$value.map((value) => typeof value === "number" ? normalizeValue({
|
|
3385
|
+
$type: "number",
|
|
3386
|
+
$value: value
|
|
3387
|
+
}) : value);
|
|
3388
|
+
}
|
|
3389
|
+
case "dimension": {
|
|
3390
|
+
if (token.$value === 0) return {
|
|
3391
|
+
value: 0,
|
|
3392
|
+
unit: "px"
|
|
3393
|
+
};
|
|
3394
|
+
if (typeof token.$value === "string") {
|
|
3395
|
+
const match = token.$value.match(NUMBER_WITH_UNIT_RE);
|
|
3396
|
+
return {
|
|
3397
|
+
value: Number.parseFloat(match?.[1] || token.$value),
|
|
3398
|
+
unit: match?.[2] || "px"
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
return token.$value;
|
|
3402
|
+
}
|
|
3403
|
+
case "duration": {
|
|
3404
|
+
if (token.$value === 0) return {
|
|
3405
|
+
value: 0,
|
|
3406
|
+
unit: "ms"
|
|
3407
|
+
};
|
|
3408
|
+
if (typeof token.$value === "string") {
|
|
3409
|
+
const match = token.$value.match(NUMBER_WITH_UNIT_RE);
|
|
3410
|
+
return {
|
|
3411
|
+
value: Number.parseFloat(match?.[1] || token.$value),
|
|
3412
|
+
unit: match?.[2] || "ms"
|
|
3413
|
+
};
|
|
3414
|
+
}
|
|
3415
|
+
return token.$value;
|
|
3416
|
+
}
|
|
3417
|
+
case "fontFamily": return Array.isArray(token.$value) ? token.$value : [token.$value];
|
|
3418
|
+
case "fontWeight": {
|
|
3419
|
+
if (typeof token.$value === "string" && FONT_WEIGHT_MAP[token.$value]) return FONT_WEIGHT_MAP[token.$value];
|
|
3420
|
+
return Math.min(999, Math.max(1, typeof token.$value === "string" ? Number.parseInt(token.$value) : token.$value));
|
|
3421
|
+
}
|
|
3422
|
+
case "gradient": {
|
|
3423
|
+
if (typeof token.$value === "string") return token.$value;
|
|
3424
|
+
const output = [];
|
|
3425
|
+
for (let i = 0; i < token.$value.length; i++) {
|
|
3426
|
+
const stop = structuredClone(token.$value[i]);
|
|
3427
|
+
stop.color = normalizeValue({
|
|
3428
|
+
$type: "color",
|
|
3429
|
+
$value: stop.color
|
|
3430
|
+
});
|
|
3431
|
+
if (stop.position === void 0) stop.position = i / (token.$value.length - 1);
|
|
3432
|
+
output.push(stop);
|
|
3433
|
+
}
|
|
3434
|
+
return output;
|
|
3435
|
+
}
|
|
3436
|
+
case "number": return typeof token.$value === "number" ? token.$value : Number.parseFloat(token.$value);
|
|
3437
|
+
case "shadow": {
|
|
3438
|
+
if (typeof token.$value === "string") return token.$value;
|
|
3439
|
+
return (Array.isArray(token.$value) ? token.$value : [token.$value]).map((layer) => ({
|
|
3440
|
+
color: normalizeValue({
|
|
3441
|
+
$type: "color",
|
|
3442
|
+
$value: layer.color
|
|
3443
|
+
}),
|
|
3444
|
+
offsetX: normalizeValue({
|
|
3445
|
+
$type: "dimension",
|
|
3446
|
+
$value: layer.offsetX ?? {
|
|
3447
|
+
value: 0,
|
|
3448
|
+
unit: "px"
|
|
3449
|
+
}
|
|
3450
|
+
}),
|
|
3451
|
+
offsetY: normalizeValue({
|
|
3452
|
+
$type: "dimension",
|
|
3453
|
+
$value: layer.offsetY ?? {
|
|
3454
|
+
value: 0,
|
|
3455
|
+
unit: "px"
|
|
3456
|
+
}
|
|
3457
|
+
}),
|
|
3458
|
+
blur: normalizeValue({
|
|
3459
|
+
$type: "dimension",
|
|
3460
|
+
$value: layer.blur ?? {
|
|
3461
|
+
value: 0,
|
|
3462
|
+
unit: "px"
|
|
3463
|
+
}
|
|
3464
|
+
}),
|
|
3465
|
+
spread: normalizeValue({
|
|
3466
|
+
$type: "dimension",
|
|
3467
|
+
$value: layer.spread ?? {
|
|
3468
|
+
value: 0,
|
|
3469
|
+
unit: "px"
|
|
3470
|
+
}
|
|
3471
|
+
}),
|
|
3472
|
+
inset: layer.inset === true
|
|
3473
|
+
}));
|
|
3474
|
+
}
|
|
3475
|
+
case "strokeStyle": return token.$value;
|
|
3476
|
+
case "string": return String(token.$value);
|
|
3477
|
+
case "transition": {
|
|
3478
|
+
if (typeof token.$value === "string") return token.$value;
|
|
3479
|
+
return {
|
|
3480
|
+
duration: normalizeValue({
|
|
3481
|
+
$type: "duration",
|
|
3482
|
+
$value: token.$value.duration ?? 0
|
|
3483
|
+
}),
|
|
3484
|
+
delay: normalizeValue({
|
|
3485
|
+
$type: "duration",
|
|
3486
|
+
$value: token.$value.delay ?? 0
|
|
3487
|
+
}),
|
|
3488
|
+
timingFunction: normalizeValue({
|
|
3489
|
+
$type: "cubicBezier",
|
|
3490
|
+
$value: token.$value.timingFunction
|
|
3491
|
+
})
|
|
3492
|
+
};
|
|
3493
|
+
}
|
|
3494
|
+
case "typography": {
|
|
3495
|
+
if (typeof token.$value === "string") return token.$value;
|
|
3496
|
+
const output = {};
|
|
3497
|
+
for (const [k, $value] of Object.entries(token.$value)) switch (k) {
|
|
3498
|
+
case "fontFamily": {
|
|
3499
|
+
output[k] = normalizeValue({
|
|
3500
|
+
$type: "fontFamily",
|
|
3501
|
+
$value
|
|
3502
|
+
});
|
|
3503
|
+
break;
|
|
3504
|
+
}
|
|
3505
|
+
case "fontSize":
|
|
3506
|
+
case "letterSpacing": {
|
|
3507
|
+
output[k] = normalizeValue({
|
|
3508
|
+
$type: "dimension",
|
|
3509
|
+
$value
|
|
3510
|
+
});
|
|
3511
|
+
break;
|
|
3512
|
+
}
|
|
3513
|
+
case "lineHeight": {
|
|
3514
|
+
output[k] = normalizeValue({
|
|
3515
|
+
$type: typeof token.$value === "number" ? "number" : "dimension",
|
|
3516
|
+
$value
|
|
3517
|
+
});
|
|
3518
|
+
break;
|
|
3519
|
+
}
|
|
3520
|
+
default: {
|
|
3521
|
+
output[k] = $value;
|
|
3522
|
+
break;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
return output;
|
|
3526
|
+
}
|
|
3527
|
+
default: return token.$value;
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
//#endregion
|
|
3532
|
+
//#region src/parse/validate.ts
|
|
3533
|
+
const listFormat = new Intl.ListFormat("en-us", { type: "disjunction" });
|
|
3534
|
+
const VALID_COLORSPACES = new Set([
|
|
3535
|
+
"adobe-rgb",
|
|
3536
|
+
"display-p3",
|
|
3537
|
+
"hsl",
|
|
3538
|
+
"hwb",
|
|
3539
|
+
"lab",
|
|
3540
|
+
"lch",
|
|
3541
|
+
"oklab",
|
|
3542
|
+
"oklch",
|
|
3543
|
+
"prophoto",
|
|
3544
|
+
"rec2020",
|
|
3545
|
+
"srgb",
|
|
3546
|
+
"srgb-linear",
|
|
3547
|
+
"xyz",
|
|
3548
|
+
"xyz-d50",
|
|
3549
|
+
"xyz-d65"
|
|
3550
|
+
]);
|
|
3551
|
+
const FONT_WEIGHT_VALUES = new Set([
|
|
3552
|
+
"thin",
|
|
3553
|
+
"hairline",
|
|
3554
|
+
"extra-light",
|
|
3555
|
+
"ultra-light",
|
|
3556
|
+
"light",
|
|
3557
|
+
"normal",
|
|
3558
|
+
"regular",
|
|
3559
|
+
"book",
|
|
3560
|
+
"medium",
|
|
3561
|
+
"semi-bold",
|
|
3562
|
+
"demi-bold",
|
|
3563
|
+
"bold",
|
|
3564
|
+
"extra-bold",
|
|
3565
|
+
"ultra-bold",
|
|
3566
|
+
"black",
|
|
3567
|
+
"heavy",
|
|
3568
|
+
"extra-black",
|
|
3569
|
+
"ultra-black"
|
|
3570
|
+
]);
|
|
3571
|
+
const STROKE_STYLE_VALUES = new Set([
|
|
3572
|
+
"solid",
|
|
3573
|
+
"dashed",
|
|
3574
|
+
"dotted",
|
|
3575
|
+
"double",
|
|
3576
|
+
"groove",
|
|
3577
|
+
"ridge",
|
|
3578
|
+
"outset",
|
|
3579
|
+
"inset"
|
|
3580
|
+
]);
|
|
3581
|
+
const STROKE_STYLE_LINE_CAP_VALUES = new Set([
|
|
3582
|
+
"round",
|
|
3583
|
+
"butt",
|
|
3584
|
+
"square"
|
|
3585
|
+
]);
|
|
3586
|
+
/** Distinct from isAlias() in that this accepts malformed aliases */
|
|
3587
|
+
function isMaybeAlias(node) {
|
|
3588
|
+
if (node?.type === "String") return node.value.startsWith("{");
|
|
3589
|
+
return false;
|
|
3590
|
+
}
|
|
3591
|
+
/** Assert object members match given types */
|
|
3592
|
+
function validateMembersAs($value, properties, node, { filename, src, logger }) {
|
|
3593
|
+
const members = getObjMembers($value);
|
|
3594
|
+
for (const [name, value] of Object.entries(properties)) {
|
|
3595
|
+
const { validator, required } = value;
|
|
3596
|
+
if (!members[name]) {
|
|
3597
|
+
if (required) logger.error({
|
|
3598
|
+
group: "parser",
|
|
3599
|
+
label: "validate",
|
|
3600
|
+
message: `Missing required property "${name}"`,
|
|
3601
|
+
filename,
|
|
3602
|
+
node: $value,
|
|
3603
|
+
src
|
|
3604
|
+
});
|
|
3605
|
+
continue;
|
|
3606
|
+
}
|
|
3607
|
+
const memberValue = members[name];
|
|
3608
|
+
if (isMaybeAlias(memberValue)) validateAliasSyntax(memberValue, node, {
|
|
3609
|
+
filename,
|
|
3610
|
+
src,
|
|
3611
|
+
logger
|
|
3612
|
+
});
|
|
3613
|
+
else validator(memberValue, node, {
|
|
3614
|
+
filename,
|
|
3615
|
+
src,
|
|
3616
|
+
logger
|
|
3617
|
+
});
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
/** Verify an Alias $value is formatted correctly */
|
|
3621
|
+
function validateAliasSyntax($value, _node, { filename, src, logger }) {
|
|
3622
|
+
if ($value.type !== "String" || !isAlias($value.value)) logger.error({
|
|
3623
|
+
group: "parser",
|
|
3624
|
+
label: "validate",
|
|
3625
|
+
message: `Invalid alias: ${print($value)}`,
|
|
3626
|
+
filename,
|
|
3627
|
+
node: $value,
|
|
3628
|
+
src
|
|
3629
|
+
});
|
|
3630
|
+
}
|
|
3631
|
+
/** Verify a Border token is valid */
|
|
3632
|
+
function validateBorder($value, node, { filename, src, logger }) {
|
|
3633
|
+
if ($value.type !== "Object") logger.error({
|
|
3634
|
+
group: "parser",
|
|
3635
|
+
label: "validate",
|
|
3636
|
+
message: `Expected object, received ${$value.type}`,
|
|
3637
|
+
filename,
|
|
3638
|
+
node: $value,
|
|
3639
|
+
src
|
|
3640
|
+
});
|
|
3641
|
+
else validateMembersAs($value, {
|
|
3642
|
+
color: {
|
|
3643
|
+
validator: validateColor,
|
|
3644
|
+
required: true
|
|
3645
|
+
},
|
|
3646
|
+
style: {
|
|
3647
|
+
validator: validateStrokeStyle,
|
|
3648
|
+
required: true
|
|
3649
|
+
},
|
|
3650
|
+
width: {
|
|
3651
|
+
validator: validateDimension,
|
|
3652
|
+
required: true
|
|
3653
|
+
}
|
|
3654
|
+
}, node, {
|
|
3655
|
+
filename,
|
|
3656
|
+
src,
|
|
3657
|
+
logger
|
|
3658
|
+
});
|
|
3659
|
+
}
|
|
3660
|
+
/** Verify a Color token is valid */
|
|
3661
|
+
function validateColor($value, node, { filename, src, logger }) {
|
|
3662
|
+
const baseMessage = {
|
|
3663
|
+
group: "parser",
|
|
3664
|
+
label: "validate",
|
|
3665
|
+
filename,
|
|
3666
|
+
node: $value,
|
|
3667
|
+
src
|
|
3668
|
+
};
|
|
3669
|
+
if ($value.type === "String") {
|
|
3670
|
+
if ($value.value === "") logger.error({
|
|
3671
|
+
...baseMessage,
|
|
3672
|
+
message: "Expected color, received empty string"
|
|
3673
|
+
});
|
|
3674
|
+
} else if ($value.type === "Object") {
|
|
3675
|
+
const channelMemberI = $value.members.findIndex((m) => m.name.type === "String" && m.name.value === "channels");
|
|
3676
|
+
if (channelMemberI !== -1) {
|
|
3677
|
+
logger.warn({
|
|
3678
|
+
...baseMessage,
|
|
3679
|
+
message: "\"channels\" is deprecated; rename \"channels\" to \"components\""
|
|
3680
|
+
});
|
|
3681
|
+
$value.members[channelMemberI].name.value = "components";
|
|
3682
|
+
}
|
|
3683
|
+
validateMembersAs($value, {
|
|
3684
|
+
colorSpace: {
|
|
3685
|
+
validator: (v) => {
|
|
3686
|
+
if (v.type !== "String") logger.error({
|
|
3687
|
+
...baseMessage,
|
|
3688
|
+
message: `Expected string, received ${print(v)}`,
|
|
3689
|
+
node: v
|
|
3690
|
+
});
|
|
3691
|
+
if (!VALID_COLORSPACES.has(v.value)) logger.error({
|
|
3692
|
+
...baseMessage,
|
|
3693
|
+
message: `Unsupported colorspace ${print(v)}`,
|
|
3694
|
+
node: v
|
|
3695
|
+
});
|
|
3696
|
+
},
|
|
3697
|
+
required: true
|
|
3698
|
+
},
|
|
3699
|
+
components: {
|
|
3700
|
+
validator: (v) => {
|
|
3701
|
+
if (v.type !== "Array") logger.error({
|
|
3702
|
+
...baseMessage,
|
|
3703
|
+
message: `Expected array, received ${print(v)}`,
|
|
3704
|
+
node: v
|
|
3705
|
+
});
|
|
3706
|
+
else {
|
|
3707
|
+
if (v.elements?.length !== 3) logger.error({
|
|
3708
|
+
...baseMessage,
|
|
3709
|
+
message: `Expected 3 components, received ${v.elements?.length ?? 0}`,
|
|
3710
|
+
node: v
|
|
3711
|
+
});
|
|
3712
|
+
for (const element of v.elements) if (element.value.type !== "Number") logger.error({
|
|
3713
|
+
...baseMessage,
|
|
3714
|
+
message: `Expected number, received ${print(element.value)}`,
|
|
3715
|
+
node: element
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
},
|
|
3719
|
+
required: true
|
|
3720
|
+
},
|
|
3721
|
+
hex: { validator: (v) => {
|
|
3722
|
+
if (v.type !== "String" || v.value.length === 6 || v.value.length === 8 || !/^#[a-f0-9]{3,8}$/i.test(v.value)) logger.error({
|
|
3723
|
+
...baseMessage,
|
|
3724
|
+
message: `Invalid hex color ${print(v)}`,
|
|
3725
|
+
node: v
|
|
3726
|
+
});
|
|
3727
|
+
} },
|
|
3728
|
+
alpha: { validator: validateNumber }
|
|
3729
|
+
}, node, {
|
|
3730
|
+
filename,
|
|
3731
|
+
src,
|
|
3732
|
+
logger
|
|
3733
|
+
});
|
|
3734
|
+
} else logger.error({
|
|
3735
|
+
...baseMessage,
|
|
3736
|
+
message: `Expected object, received ${$value.type}`,
|
|
3737
|
+
node: $value
|
|
3738
|
+
});
|
|
3739
|
+
}
|
|
3740
|
+
/** Verify a Cubic Bézier token is valid */
|
|
3741
|
+
function validateCubicBezier($value, _node, { filename, src, logger }) {
|
|
3742
|
+
const baseMessage = {
|
|
3743
|
+
group: "parser",
|
|
3744
|
+
label: "validate",
|
|
3745
|
+
filename,
|
|
3746
|
+
node: $value,
|
|
3747
|
+
src
|
|
3748
|
+
};
|
|
3749
|
+
if ($value.type !== "Array") logger.error({
|
|
3750
|
+
...baseMessage,
|
|
3751
|
+
message: `Expected array of numbers, received ${print($value)}`
|
|
3752
|
+
});
|
|
3753
|
+
else if (!$value.elements.every((e) => e.value.type === "Number")) logger.error({
|
|
3754
|
+
...baseMessage,
|
|
3755
|
+
message: "Expected an array of 4 numbers, received some non-numbers"
|
|
3756
|
+
});
|
|
3757
|
+
else if ($value.elements.length !== 4) logger.error({
|
|
3758
|
+
...baseMessage,
|
|
3759
|
+
message: `Expected an array of 4 numbers, received ${$value.elements.length}`
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
/** Verify a Dimension token is valid */
|
|
3763
|
+
function validateDimension($value, _node, { filename, src, logger }) {
|
|
3764
|
+
if ($value.type === "Number" && $value.value === 0) return;
|
|
3765
|
+
const baseMessage = {
|
|
3766
|
+
group: "parser",
|
|
3767
|
+
label: "validate",
|
|
3768
|
+
filename,
|
|
3769
|
+
node: $value,
|
|
3770
|
+
src
|
|
3771
|
+
};
|
|
3772
|
+
if ($value.type === "Object") {
|
|
3773
|
+
const { value: value$1, unit: unit$1 } = getObjMembers($value);
|
|
3774
|
+
if (!value$1) logger.error({
|
|
3775
|
+
...baseMessage,
|
|
3776
|
+
message: "Missing required property \"value\"."
|
|
3777
|
+
});
|
|
3778
|
+
if (!unit$1) logger.error({
|
|
3779
|
+
...baseMessage,
|
|
3780
|
+
message: "Missing required property \"unit\"."
|
|
3781
|
+
});
|
|
3782
|
+
if (value$1.type !== "Number") logger.error({
|
|
3783
|
+
...baseMessage,
|
|
3784
|
+
message: `Expected number, received ${value$1.type}`,
|
|
3785
|
+
node: value$1
|
|
3786
|
+
});
|
|
3787
|
+
if (![
|
|
3788
|
+
"px",
|
|
3789
|
+
"em",
|
|
3790
|
+
"rem"
|
|
3791
|
+
].includes(unit$1.value)) logger.error({
|
|
3792
|
+
...baseMessage,
|
|
3793
|
+
message: `Expected unit "px", "em", or "rem", received ${print(unit$1)}`,
|
|
3794
|
+
node: unit$1
|
|
3795
|
+
});
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
if ($value.type !== "String") logger.error({
|
|
3799
|
+
...baseMessage,
|
|
3800
|
+
message: `Expected string, received ${$value.type}`
|
|
3801
|
+
});
|
|
3802
|
+
const value = $value.value.match(/^-?[0-9.]+/)?.[0];
|
|
3803
|
+
const unit = $value.value.replace(value, "");
|
|
3804
|
+
if ($value.value === "") logger.error({
|
|
3805
|
+
...baseMessage,
|
|
3806
|
+
message: "Expected dimension, received empty string"
|
|
3807
|
+
});
|
|
3808
|
+
else if (![
|
|
3809
|
+
"px",
|
|
3810
|
+
"em",
|
|
3811
|
+
"rem"
|
|
3812
|
+
].includes(unit)) logger.error({
|
|
3813
|
+
...baseMessage,
|
|
3814
|
+
message: `Expected unit "px", "em", or "rem", received ${JSON.stringify(unit || $value.value)}`
|
|
3815
|
+
});
|
|
3816
|
+
else if (!Number.isFinite(Number.parseFloat(value))) logger.error({
|
|
3817
|
+
...baseMessage,
|
|
3818
|
+
message: `Expected dimension with units, received ${print($value)}`
|
|
3819
|
+
});
|
|
3820
|
+
}
|
|
3821
|
+
/** Verify a Duration token is valid */
|
|
3822
|
+
function validateDuration($value, _node, { filename, src, logger }) {
|
|
3823
|
+
if ($value.type === "Number" && $value.value === 0) return;
|
|
3824
|
+
const baseMessage = {
|
|
3825
|
+
group: "parser",
|
|
3826
|
+
label: "validate",
|
|
3827
|
+
filename,
|
|
3828
|
+
node: $value,
|
|
3829
|
+
src
|
|
3830
|
+
};
|
|
3831
|
+
if ($value.type === "Object") {
|
|
3832
|
+
const { value: value$1, unit: unit$1 } = getObjMembers($value);
|
|
3833
|
+
if (!value$1) logger.error({
|
|
3834
|
+
...baseMessage,
|
|
3835
|
+
message: "Missing required property \"value\"."
|
|
3836
|
+
});
|
|
3837
|
+
if (!unit$1) logger.error({
|
|
3838
|
+
...baseMessage,
|
|
3839
|
+
message: "Missing required property \"unit\"."
|
|
3840
|
+
});
|
|
3841
|
+
if (value$1?.type !== "Number") logger.error({
|
|
3842
|
+
...baseMessage,
|
|
3843
|
+
message: `Expected number, received ${value$1?.type}`,
|
|
3844
|
+
node: value$1
|
|
3845
|
+
});
|
|
3846
|
+
if (!["ms", "s"].includes(unit$1.value)) logger.error({
|
|
3847
|
+
...baseMessage,
|
|
3848
|
+
message: `Expected unit "ms" or "s", received ${print(unit$1)}`,
|
|
3849
|
+
node: unit$1
|
|
3850
|
+
});
|
|
3851
|
+
return;
|
|
3852
|
+
}
|
|
3853
|
+
if ($value.type !== "String") logger.error({
|
|
3854
|
+
...baseMessage,
|
|
3855
|
+
message: `Expected string, received ${$value.type}`
|
|
3856
|
+
});
|
|
3857
|
+
const value = $value.value.match(/^-?[0-9.]+/)?.[0];
|
|
3858
|
+
const unit = $value.value.replace(value, "");
|
|
3859
|
+
if ($value.value === "") logger.error({
|
|
3860
|
+
...baseMessage,
|
|
3861
|
+
message: "Expected duration, received empty string"
|
|
3862
|
+
});
|
|
3863
|
+
else if (!["ms", "s"].includes(unit)) logger.error({
|
|
3864
|
+
...baseMessage,
|
|
3865
|
+
message: `Expected unit "ms" or "s", received ${JSON.stringify(unit || $value.value)}`
|
|
3866
|
+
});
|
|
3867
|
+
else if (!Number.isFinite(Number.parseFloat(value))) logger.error({
|
|
3868
|
+
...baseMessage,
|
|
3869
|
+
message: `Expected duration with units, received ${print($value)}`
|
|
3870
|
+
});
|
|
3871
|
+
}
|
|
3872
|
+
/** Verify a Font Family token is valid */
|
|
3873
|
+
function validateFontFamily($value, _node, { filename, src, logger }) {
|
|
3874
|
+
const baseMessage = {
|
|
3875
|
+
group: "parser",
|
|
3876
|
+
label: "validate",
|
|
3877
|
+
filename,
|
|
3878
|
+
node: $value,
|
|
3879
|
+
src
|
|
3880
|
+
};
|
|
3881
|
+
if ($value.type !== "String" && $value.type !== "Array") logger.error({
|
|
3882
|
+
...baseMessage,
|
|
3883
|
+
message: `Expected string or array of strings, received ${$value.type}`
|
|
3884
|
+
});
|
|
3885
|
+
if ($value.type === "String" && $value.value === "") logger.error({
|
|
3886
|
+
...baseMessage,
|
|
3887
|
+
message: "Expected font family name, received empty string"
|
|
3888
|
+
});
|
|
3889
|
+
if ($value.type === "Array" && !$value.elements.every((e) => e.value.type === "String" && e.value.value !== "")) logger.error({
|
|
3890
|
+
...baseMessage,
|
|
3891
|
+
message: "Expected an array of strings, received some non-strings or empty strings"
|
|
3892
|
+
});
|
|
3893
|
+
}
|
|
3894
|
+
/** Verify a Font Weight token is valid */
|
|
3895
|
+
function validateFontWeight($value, _node, { filename, src, logger }) {
|
|
3896
|
+
const baseMessage = {
|
|
3897
|
+
group: "parser",
|
|
3898
|
+
label: "validate",
|
|
3899
|
+
filename,
|
|
3900
|
+
node: $value,
|
|
3901
|
+
src
|
|
3902
|
+
};
|
|
3903
|
+
if ($value.type !== "String" && $value.type !== "Number") logger.error({
|
|
3904
|
+
...baseMessage,
|
|
3905
|
+
message: `Expected a font weight name or number 0–1000, received ${$value.type}`
|
|
3906
|
+
});
|
|
3907
|
+
if ($value.type === "String" && !FONT_WEIGHT_VALUES.has($value.value)) logger.error({
|
|
3908
|
+
...baseMessage,
|
|
3909
|
+
message: `Unknown font weight ${print($value)}. Expected one of: ${listFormat.format([...FONT_WEIGHT_VALUES])}.`
|
|
3910
|
+
});
|
|
3911
|
+
if ($value.type === "Number" && ($value.value < 0 || $value.value > 1e3)) logger.error({
|
|
3912
|
+
...baseMessage,
|
|
3913
|
+
message: `Expected number 0–1000, received ${print($value)}`
|
|
3914
|
+
});
|
|
3915
|
+
}
|
|
3916
|
+
/** Verify a Gradient token is valid */
|
|
3917
|
+
function validateGradient($value, _node, { filename, src, logger }) {
|
|
3918
|
+
const baseMessage = {
|
|
3919
|
+
group: "parser",
|
|
3920
|
+
label: "validate",
|
|
3921
|
+
filename,
|
|
3922
|
+
node: $value,
|
|
3923
|
+
src
|
|
3924
|
+
};
|
|
3925
|
+
if ($value.type !== "Array") logger.error({
|
|
3926
|
+
...baseMessage,
|
|
3927
|
+
message: `Expected array of gradient stops, received ${$value.type}`
|
|
3928
|
+
});
|
|
3929
|
+
else for (let i = 0; i < $value.elements.length; i++) {
|
|
3930
|
+
const element = $value.elements[i];
|
|
3931
|
+
if (element.value.type !== "Object") {
|
|
3932
|
+
logger.error({
|
|
3933
|
+
...baseMessage,
|
|
3934
|
+
message: `Stop #${i + 1}: Expected gradient stop, received ${element.value.type}`,
|
|
3935
|
+
node: element
|
|
3936
|
+
});
|
|
3937
|
+
break;
|
|
3938
|
+
}
|
|
3939
|
+
validateMembersAs(element.value, {
|
|
3940
|
+
color: {
|
|
3941
|
+
validator: validateColor,
|
|
3942
|
+
required: true
|
|
3943
|
+
},
|
|
3944
|
+
position: {
|
|
3945
|
+
validator: validateNumber,
|
|
3946
|
+
required: true
|
|
3947
|
+
}
|
|
3948
|
+
}, element, {
|
|
3949
|
+
filename,
|
|
3950
|
+
src,
|
|
3951
|
+
logger
|
|
3952
|
+
});
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
/** Verify a Number token is valid */
|
|
3956
|
+
function validateNumber($value, _node, { filename, src, logger }) {
|
|
3957
|
+
if ($value.type !== "Number") logger.error({
|
|
3958
|
+
group: "parser",
|
|
3959
|
+
label: "validate",
|
|
3960
|
+
message: `Expected number, received ${$value.type}`,
|
|
3961
|
+
filename,
|
|
3962
|
+
node: $value,
|
|
3963
|
+
src
|
|
3964
|
+
});
|
|
3965
|
+
}
|
|
3966
|
+
/** Verify a Boolean token is valid */
|
|
3967
|
+
function validateBoolean($value, _node, { filename, src, logger }) {
|
|
3968
|
+
if ($value.type !== "Boolean") logger.error({
|
|
3969
|
+
group: "parser",
|
|
3970
|
+
label: "validate",
|
|
3971
|
+
message: `Expected boolean, received ${$value.type}`,
|
|
3972
|
+
filename,
|
|
3973
|
+
node: $value,
|
|
3974
|
+
src
|
|
3975
|
+
});
|
|
3976
|
+
}
|
|
3977
|
+
/** Verify a Shadow token’s value is valid */
|
|
3978
|
+
function validateShadowLayer($value, node, { filename, src, logger }) {
|
|
3979
|
+
if ($value.type !== "Object") logger.error({
|
|
3980
|
+
group: "parser",
|
|
3981
|
+
label: "validate",
|
|
3982
|
+
message: `Expected Object, received ${$value.type}`,
|
|
3983
|
+
filename,
|
|
3984
|
+
node: $value,
|
|
3985
|
+
src
|
|
3986
|
+
});
|
|
3987
|
+
else validateMembersAs($value, {
|
|
3988
|
+
color: {
|
|
3989
|
+
validator: validateColor,
|
|
3990
|
+
required: true
|
|
3991
|
+
},
|
|
3992
|
+
offsetX: {
|
|
3993
|
+
validator: validateDimension,
|
|
3994
|
+
required: true
|
|
3995
|
+
},
|
|
3996
|
+
offsetY: {
|
|
3997
|
+
validator: validateDimension,
|
|
3998
|
+
required: true
|
|
3999
|
+
},
|
|
4000
|
+
blur: { validator: validateDimension },
|
|
4001
|
+
spread: { validator: validateDimension },
|
|
4002
|
+
inset: { validator: validateBoolean }
|
|
4003
|
+
}, node, {
|
|
4004
|
+
filename,
|
|
4005
|
+
src,
|
|
4006
|
+
logger
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
/** Verify a Stroke Style token is valid. */
|
|
4010
|
+
function validateStrokeStyle($value, node, { filename, src, logger }) {
|
|
4011
|
+
const baseMessage = {
|
|
4012
|
+
group: "parser",
|
|
4013
|
+
label: "validate",
|
|
4014
|
+
filename,
|
|
4015
|
+
node: $value,
|
|
4016
|
+
src
|
|
4017
|
+
};
|
|
4018
|
+
if ($value.type === "String") {
|
|
4019
|
+
if (!STROKE_STYLE_VALUES.has($value.value)) logger.error({
|
|
4020
|
+
...baseMessage,
|
|
4021
|
+
message: `Unknown stroke style ${print($value)}. Expected one of: ${listFormat.format([...STROKE_STYLE_VALUES])}.`
|
|
4022
|
+
});
|
|
4023
|
+
} else if ($value.type === "Object") {
|
|
4024
|
+
const strokeMembers = getObjMembers($value);
|
|
4025
|
+
for (const property of ["dashArray", "lineCap"]) if (!strokeMembers[property]) logger.error({
|
|
4026
|
+
...baseMessage,
|
|
4027
|
+
message: `Missing required property "${property}"`
|
|
4028
|
+
});
|
|
4029
|
+
const { lineCap, dashArray } = strokeMembers;
|
|
4030
|
+
if (lineCap?.type !== "String" || !STROKE_STYLE_LINE_CAP_VALUES.has(lineCap.value)) logger.error({
|
|
4031
|
+
...baseMessage,
|
|
4032
|
+
message: `Unknown lineCap value ${print(lineCap)}. Expected one of: ${listFormat.format([...STROKE_STYLE_LINE_CAP_VALUES])}.`,
|
|
4033
|
+
node
|
|
4034
|
+
});
|
|
4035
|
+
if (dashArray?.type === "Array") for (const element of dashArray.elements) if (element.value.type === "String" && element.value.value !== "") if (isMaybeAlias(element.value)) validateAliasSyntax(element.value, node, {
|
|
4036
|
+
logger,
|
|
4037
|
+
src
|
|
4038
|
+
});
|
|
4039
|
+
else validateDimension(element.value, node, {
|
|
4040
|
+
logger,
|
|
4041
|
+
src
|
|
4042
|
+
});
|
|
4043
|
+
else logger.error({
|
|
4044
|
+
...baseMessage,
|
|
4045
|
+
message: "Expected array of strings, recieved some non-strings or empty strings.",
|
|
4046
|
+
node: element
|
|
4047
|
+
});
|
|
4048
|
+
else logger.error({
|
|
4049
|
+
...baseMessage,
|
|
4050
|
+
message: `Expected array of strings, received ${dashArray.type}`
|
|
4051
|
+
});
|
|
4052
|
+
} else logger.error({
|
|
4053
|
+
...baseMessage,
|
|
4054
|
+
message: `Expected string or object, received ${$value.type}`
|
|
4055
|
+
});
|
|
4056
|
+
}
|
|
4057
|
+
/** Verify a Transition token is valid */
|
|
4058
|
+
function validateTransition($value, node, { filename, src, logger }) {
|
|
4059
|
+
if ($value.type !== "Object") logger.error({
|
|
4060
|
+
group: "parser",
|
|
4061
|
+
label: "validate",
|
|
4062
|
+
message: `Expected object, received ${$value.type}`,
|
|
4063
|
+
filename,
|
|
4064
|
+
node: $value,
|
|
4065
|
+
src
|
|
4066
|
+
});
|
|
4067
|
+
else validateMembersAs($value, {
|
|
4068
|
+
duration: {
|
|
4069
|
+
validator: validateDuration,
|
|
4070
|
+
required: true
|
|
4071
|
+
},
|
|
4072
|
+
delay: {
|
|
4073
|
+
validator: validateDuration,
|
|
4074
|
+
required: false
|
|
4075
|
+
},
|
|
4076
|
+
timingFunction: {
|
|
4077
|
+
validator: validateCubicBezier,
|
|
4078
|
+
required: true
|
|
4079
|
+
}
|
|
4080
|
+
}, node, {
|
|
4081
|
+
filename,
|
|
4082
|
+
src,
|
|
4083
|
+
logger
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
4086
|
+
/**
|
|
4087
|
+
* Validate a MemberNode (the entire token object, plus its key in the parent
|
|
4088
|
+
* object) to see if it’s a valid DTCG token or not. Keeping the parent key
|
|
4089
|
+
* really helps in debug messages.
|
|
4090
|
+
*/
|
|
4091
|
+
function validateTokenMemberNode(node, { filename, src, logger }) {
|
|
4092
|
+
const baseMessage = {
|
|
4093
|
+
group: "parser",
|
|
4094
|
+
label: "validate",
|
|
4095
|
+
filename,
|
|
4096
|
+
node,
|
|
4097
|
+
src
|
|
4098
|
+
};
|
|
4099
|
+
if (node.type !== "Member" && node.type !== "Object") logger.error({
|
|
4100
|
+
...baseMessage,
|
|
4101
|
+
message: `Expected Object, received ${JSON.stringify(node.type)}`
|
|
4102
|
+
});
|
|
4103
|
+
const rootMembers = node.value.type === "Object" ? getObjMembers(node.value) : {};
|
|
4104
|
+
const $value = rootMembers.$value;
|
|
4105
|
+
const $type = rootMembers.$type;
|
|
4106
|
+
if (!$value) logger.error({
|
|
4107
|
+
...baseMessage,
|
|
4108
|
+
message: "Token missing $value"
|
|
4109
|
+
});
|
|
4110
|
+
if (isMaybeAlias($value)) {
|
|
4111
|
+
validateAliasSyntax($value, node, {
|
|
4112
|
+
logger,
|
|
4113
|
+
src
|
|
4114
|
+
});
|
|
4115
|
+
return;
|
|
4116
|
+
}
|
|
4117
|
+
if (!$type) logger.error({
|
|
4118
|
+
...baseMessage,
|
|
4119
|
+
message: "Token missing $type"
|
|
4120
|
+
});
|
|
4121
|
+
switch ($type.value) {
|
|
4122
|
+
case "color": {
|
|
4123
|
+
validateColor($value, node, {
|
|
4124
|
+
logger,
|
|
4125
|
+
src
|
|
4126
|
+
});
|
|
4127
|
+
break;
|
|
4128
|
+
}
|
|
4129
|
+
case "cubicBezier": {
|
|
4130
|
+
validateCubicBezier($value, node, {
|
|
4131
|
+
logger,
|
|
4132
|
+
src
|
|
4133
|
+
});
|
|
4134
|
+
break;
|
|
4135
|
+
}
|
|
4136
|
+
case "dimension": {
|
|
4137
|
+
validateDimension($value, node, {
|
|
4138
|
+
logger,
|
|
4139
|
+
src
|
|
4140
|
+
});
|
|
4141
|
+
break;
|
|
4142
|
+
}
|
|
4143
|
+
case "duration": {
|
|
4144
|
+
validateDuration($value, node, {
|
|
4145
|
+
logger,
|
|
4146
|
+
src
|
|
4147
|
+
});
|
|
4148
|
+
break;
|
|
4149
|
+
}
|
|
4150
|
+
case "fontFamily": {
|
|
4151
|
+
validateFontFamily($value, node, {
|
|
4152
|
+
logger,
|
|
4153
|
+
src
|
|
4154
|
+
});
|
|
4155
|
+
break;
|
|
4156
|
+
}
|
|
4157
|
+
case "fontWeight": {
|
|
4158
|
+
validateFontWeight($value, node, {
|
|
4159
|
+
logger,
|
|
4160
|
+
src
|
|
4161
|
+
});
|
|
4162
|
+
break;
|
|
4163
|
+
}
|
|
4164
|
+
case "number": {
|
|
4165
|
+
validateNumber($value, node, {
|
|
4166
|
+
logger,
|
|
4167
|
+
src
|
|
4168
|
+
});
|
|
4169
|
+
break;
|
|
4170
|
+
}
|
|
4171
|
+
case "shadow": {
|
|
4172
|
+
if ($value.type === "Object") validateShadowLayer($value, node, {
|
|
4173
|
+
logger,
|
|
4174
|
+
src
|
|
4175
|
+
});
|
|
4176
|
+
else if ($value.type === "Array") for (const element of $value.elements) validateShadowLayer(element.value, $value, {
|
|
4177
|
+
logger,
|
|
4178
|
+
src
|
|
4179
|
+
});
|
|
4180
|
+
else logger.error({
|
|
4181
|
+
...baseMessage,
|
|
4182
|
+
message: `Expected shadow object or array of shadow objects, received ${$value.type}`,
|
|
4183
|
+
node: $value
|
|
4184
|
+
});
|
|
4185
|
+
break;
|
|
4186
|
+
}
|
|
4187
|
+
case "boolean": {
|
|
4188
|
+
if ($value.type !== "Boolean") logger.error({
|
|
4189
|
+
...baseMessage,
|
|
4190
|
+
message: `Expected boolean, received ${$value.type}`,
|
|
4191
|
+
node: $value
|
|
4192
|
+
});
|
|
4193
|
+
break;
|
|
4194
|
+
}
|
|
4195
|
+
case "link": {
|
|
4196
|
+
if ($value.type !== "String") logger.error({
|
|
4197
|
+
...baseMessage,
|
|
4198
|
+
message: `Expected string, received ${$value.type}`,
|
|
4199
|
+
node: $value
|
|
4200
|
+
});
|
|
4201
|
+
else if ($value.value === "") logger.error({
|
|
4202
|
+
...baseMessage,
|
|
4203
|
+
message: "Expected URL, received empty string",
|
|
4204
|
+
node: $value
|
|
4205
|
+
});
|
|
4206
|
+
break;
|
|
4207
|
+
}
|
|
4208
|
+
case "string": {
|
|
4209
|
+
if ($value.type !== "String") logger.error({
|
|
4210
|
+
...baseMessage,
|
|
4211
|
+
message: `Expected string, received ${$value.type}`,
|
|
4212
|
+
node: $value
|
|
4213
|
+
});
|
|
4214
|
+
break;
|
|
4215
|
+
}
|
|
4216
|
+
case "border": {
|
|
4217
|
+
validateBorder($value, node, {
|
|
4218
|
+
filename,
|
|
4219
|
+
src,
|
|
4220
|
+
logger
|
|
4221
|
+
});
|
|
4222
|
+
break;
|
|
4223
|
+
}
|
|
4224
|
+
case "gradient": {
|
|
4225
|
+
validateGradient($value, node, {
|
|
4226
|
+
filename,
|
|
4227
|
+
src,
|
|
4228
|
+
logger
|
|
4229
|
+
});
|
|
4230
|
+
break;
|
|
4231
|
+
}
|
|
4232
|
+
case "strokeStyle": {
|
|
4233
|
+
validateStrokeStyle($value, node, {
|
|
4234
|
+
filename,
|
|
4235
|
+
src,
|
|
4236
|
+
logger
|
|
4237
|
+
});
|
|
4238
|
+
break;
|
|
4239
|
+
}
|
|
4240
|
+
case "transition": {
|
|
4241
|
+
validateTransition($value, node, {
|
|
4242
|
+
filename,
|
|
4243
|
+
src,
|
|
4244
|
+
logger
|
|
4245
|
+
});
|
|
4246
|
+
break;
|
|
4247
|
+
}
|
|
4248
|
+
case "typography": {
|
|
4249
|
+
if ($value.type !== "Object") {
|
|
4250
|
+
logger.error({
|
|
4251
|
+
...baseMessage,
|
|
4252
|
+
message: `Expected object, received ${$value.type}`,
|
|
4253
|
+
node: $value
|
|
4254
|
+
});
|
|
4255
|
+
break;
|
|
4256
|
+
}
|
|
4257
|
+
if ($value.members.length === 0) logger.error({
|
|
4258
|
+
...baseMessage,
|
|
4259
|
+
message: "Empty typography token. Must contain at least 1 property.",
|
|
4260
|
+
node: $value
|
|
4261
|
+
});
|
|
4262
|
+
validateMembersAs($value, {
|
|
4263
|
+
fontFamily: { validator: validateFontFamily },
|
|
4264
|
+
fontWeight: { validator: validateFontWeight }
|
|
4265
|
+
}, node, {
|
|
4266
|
+
filename,
|
|
4267
|
+
src,
|
|
4268
|
+
logger
|
|
4269
|
+
});
|
|
4270
|
+
break;
|
|
4271
|
+
}
|
|
4272
|
+
default: break;
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
/** Return any token node with its inherited $type */
|
|
4276
|
+
function getInheritedType(node, { subpath, $typeInheritance }) {
|
|
4277
|
+
if (node.value.type !== "Object") return;
|
|
4278
|
+
const $type = node.value.members.find((m) => m.name.type === "String" && m.name.value === "$type");
|
|
4279
|
+
const $value = node.value.members.find((m) => m.name.type === "String" && m.name.value === "$value");
|
|
4280
|
+
if ($typeInheritance && $type && !$value) $typeInheritance[subpath.join(".") || "."] = $type;
|
|
4281
|
+
const id = subpath.join(".");
|
|
4282
|
+
let parent$type;
|
|
4283
|
+
let longestPath = "";
|
|
4284
|
+
for (const [k, v] of Object.entries($typeInheritance ?? {})) if (k === "." || id.startsWith(k)) {
|
|
4285
|
+
if (k.length > longestPath.length) {
|
|
4286
|
+
parent$type = v;
|
|
4287
|
+
longestPath = k;
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
return parent$type;
|
|
4291
|
+
}
|
|
4292
|
+
/**
|
|
4293
|
+
* Validate does a little more than validate; it also converts to TokenNormalized
|
|
4294
|
+
* and sets up the basic data structure. But aliases are unresolved, and we need
|
|
4295
|
+
* a 2nd normalization pass afterward.
|
|
4296
|
+
*/
|
|
4297
|
+
function validateTokenNode(node, { config, filename, logger, parent, inheritedTypeNode, src, subpath }) {
|
|
4298
|
+
if (subpath.includes("$value") || node.value.type !== "Object") return;
|
|
4299
|
+
const members = getObjMembers(node.value);
|
|
4300
|
+
if (!members.$value || subpath.includes("$extensions") || subpath.includes("$deps")) return;
|
|
4301
|
+
const id = subpath.join(".");
|
|
4302
|
+
if (!subpath.includes(".$value") && members.value) logger.warn({
|
|
4303
|
+
group: "parser",
|
|
4304
|
+
label: "validate",
|
|
4305
|
+
message: `Group ${id} has "value". Did you mean "$value"?`,
|
|
4306
|
+
filename,
|
|
4307
|
+
node,
|
|
4308
|
+
src
|
|
4309
|
+
});
|
|
4310
|
+
const nodeWithType = structuredClone(node);
|
|
4311
|
+
let $type = members.$type?.type === "String" && members.$type.value || void 0;
|
|
4312
|
+
if (inheritedTypeNode && !members.$type) {
|
|
4313
|
+
injectObjMembers(nodeWithType.value, [inheritedTypeNode]);
|
|
4314
|
+
$type = inheritedTypeNode.value.value;
|
|
4315
|
+
}
|
|
4316
|
+
validateTokenMemberNode(nodeWithType, {
|
|
4317
|
+
filename,
|
|
4318
|
+
src,
|
|
4319
|
+
logger
|
|
4320
|
+
});
|
|
4321
|
+
const $deprecated = members.$deprecated && evaluate(members.$deprecated);
|
|
4322
|
+
if (config.ignore.deprecated && $deprecated || config.ignore.tokens && isTokenMatch(id, config.ignore.tokens)) return;
|
|
4323
|
+
const group = {
|
|
4324
|
+
id: splitID(id).group,
|
|
4325
|
+
tokens: []
|
|
4326
|
+
};
|
|
4327
|
+
if (inheritedTypeNode && inheritedTypeNode.value.type === "String") group.$type = inheritedTypeNode.value.value;
|
|
4328
|
+
const groupMembers = getObjMembers(parent);
|
|
4329
|
+
if (groupMembers.$description) group.$description = evaluate(groupMembers.$description);
|
|
4330
|
+
if (groupMembers.$extensions) group.$extensions = evaluate(groupMembers.$extensions);
|
|
4331
|
+
const $value = evaluate(members.$value);
|
|
4332
|
+
const token = {
|
|
4333
|
+
$type,
|
|
4334
|
+
$value,
|
|
4335
|
+
id,
|
|
4336
|
+
mode: {},
|
|
4337
|
+
originalValue: evaluate(node.value),
|
|
4338
|
+
group,
|
|
4339
|
+
source: {
|
|
4340
|
+
loc: filename?.href,
|
|
4341
|
+
node: nodeWithType.value
|
|
4342
|
+
}
|
|
4343
|
+
};
|
|
4344
|
+
if (members.$description?.type === "String" && members.$description.value) token.$description = members.$description.value;
|
|
4345
|
+
const extensions = members.$extensions ? getObjMembers(members.$extensions) : void 0;
|
|
4346
|
+
const modeValues = extensions?.mode ? getObjMembers(extensions.mode) : {};
|
|
4347
|
+
for (const mode of [".", ...Object.keys(modeValues)]) {
|
|
4348
|
+
const modeValue = mode === "." ? token.$value : evaluate(modeValues[mode]);
|
|
4349
|
+
token.mode[mode] = {
|
|
4350
|
+
$value: modeValue,
|
|
4351
|
+
originalValue: modeValue,
|
|
4352
|
+
source: {
|
|
4353
|
+
loc: filename?.href,
|
|
4354
|
+
node: modeValues[mode]
|
|
4355
|
+
}
|
|
4356
|
+
};
|
|
4357
|
+
}
|
|
4358
|
+
return token;
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4361
|
+
//#endregion
|
|
4362
|
+
//#region src/parse/index.ts
|
|
4363
|
+
/** Parse */
|
|
4364
|
+
async function parse(_input, { logger = new Logger(), skipLint = false, config = {}, continueOnError = false, yamlToMomoa, transform, _sources = {} } = {}) {
|
|
4365
|
+
const input = Array.isArray(_input) ? _input : [_input];
|
|
4366
|
+
let tokensSet = {};
|
|
4367
|
+
if (!Array.isArray(input)) logger.error({
|
|
4368
|
+
group: "parser",
|
|
4369
|
+
label: "init",
|
|
4370
|
+
message: "Input must be an array of input objects."
|
|
4371
|
+
});
|
|
4372
|
+
await Promise.all(input.map(async (src, i) => {
|
|
4373
|
+
if (!src || typeof src !== "object") logger.error({
|
|
4374
|
+
group: "parser",
|
|
4375
|
+
label: "init",
|
|
4376
|
+
message: `Input (${i}) must be an object.`
|
|
4377
|
+
});
|
|
4378
|
+
if (!src.src || typeof src.src !== "string" && typeof src.src !== "object") logger.error({
|
|
4379
|
+
message: `Input (${i}) missing "src" with a JSON/YAML string, or JSON object.`,
|
|
4380
|
+
group: "parser",
|
|
4381
|
+
label: "init"
|
|
4382
|
+
});
|
|
4383
|
+
if (src.filename) {
|
|
4384
|
+
if (!(src.filename instanceof URL)) logger.error({
|
|
4385
|
+
message: `Input (${i}) "filename" must be a URL (remote or file URL).`,
|
|
4386
|
+
group: "parser",
|
|
4387
|
+
label: "init"
|
|
4388
|
+
});
|
|
4389
|
+
if (_sources[src.filename.href]) return;
|
|
4390
|
+
}
|
|
4391
|
+
const result = await parseSingle(src.src, {
|
|
4392
|
+
filename: src.filename,
|
|
4393
|
+
logger,
|
|
4394
|
+
config,
|
|
4395
|
+
skipLint,
|
|
4396
|
+
continueOnError,
|
|
4397
|
+
yamlToMomoa,
|
|
4398
|
+
transform
|
|
4399
|
+
});
|
|
4400
|
+
tokensSet = Object.assign(tokensSet, result.tokens);
|
|
4401
|
+
if (src.filename) _sources[src.filename.href] = {
|
|
4402
|
+
filename: src.filename,
|
|
4403
|
+
src: result.src,
|
|
4404
|
+
document: result.document
|
|
4405
|
+
};
|
|
4406
|
+
}));
|
|
4407
|
+
const totalStart = performance.now();
|
|
4408
|
+
const aliasesStart = performance.now();
|
|
4409
|
+
let aliasCount = 0;
|
|
4410
|
+
for (const [id, token] of Object.entries(tokensSet)) {
|
|
4411
|
+
applyAliases(token, {
|
|
4412
|
+
tokensSet,
|
|
4413
|
+
filename: _sources[token.source.loc]?.filename,
|
|
4414
|
+
src: _sources[token.source.loc]?.src,
|
|
4415
|
+
node: getObjMembers(token.source.node).$value || token.source.node,
|
|
4416
|
+
logger
|
|
4417
|
+
});
|
|
4418
|
+
aliasCount++;
|
|
4419
|
+
const { group: parentGroup } = splitID(id);
|
|
4420
|
+
if (parentGroup) for (const siblingID of Object.keys(tokensSet)) {
|
|
4421
|
+
const { group: siblingGroup } = splitID(siblingID);
|
|
4422
|
+
if (siblingGroup?.startsWith(parentGroup)) token.group.tokens.push(siblingID);
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
logger.debug({
|
|
4426
|
+
message: `Resolved ${aliasCount} aliases`,
|
|
4427
|
+
group: "parser",
|
|
4428
|
+
label: "alias",
|
|
4429
|
+
timing: performance.now() - aliasesStart
|
|
4430
|
+
});
|
|
4431
|
+
logger.debug({
|
|
4432
|
+
message: "Finish all parser tasks",
|
|
4433
|
+
group: "parser",
|
|
4434
|
+
label: "core",
|
|
4435
|
+
timing: performance.now() - totalStart
|
|
4436
|
+
});
|
|
4437
|
+
if (continueOnError) {
|
|
4438
|
+
const { errorCount } = logger.stats();
|
|
4439
|
+
if (errorCount > 0) logger.error({
|
|
4440
|
+
group: "parser",
|
|
4441
|
+
message: `Parser encountered ${errorCount} ${pluralize(errorCount, "error", "errors")}. Exiting.`
|
|
4442
|
+
});
|
|
4443
|
+
}
|
|
4444
|
+
return {
|
|
4445
|
+
tokens: tokensSet,
|
|
4446
|
+
sources: Object.values(_sources)
|
|
4447
|
+
};
|
|
4448
|
+
}
|
|
4449
|
+
/** Parse a single input */
|
|
4450
|
+
async function parseSingle(input, { filename, logger, config, skipLint, continueOnError = false, transform, yamlToMomoa }) {
|
|
4451
|
+
const startParsing = performance.now();
|
|
4452
|
+
let { src, document } = toMomoa(input, {
|
|
4453
|
+
filename,
|
|
4454
|
+
logger,
|
|
4455
|
+
continueOnError,
|
|
4456
|
+
yamlToMomoa
|
|
4457
|
+
});
|
|
4458
|
+
logger.debug({
|
|
4459
|
+
group: "parser",
|
|
4460
|
+
label: "json",
|
|
4461
|
+
message: "Finish JSON parsing",
|
|
4462
|
+
timing: performance.now() - startParsing
|
|
4463
|
+
});
|
|
4464
|
+
const tokensSet = {};
|
|
4465
|
+
if (transform?.root) {
|
|
4466
|
+
const json = typeof input === "string" ? JSON.parse(input) : input;
|
|
4467
|
+
const result = transform?.root(json, ".", document);
|
|
4468
|
+
if (result) {
|
|
4469
|
+
const reRunResult = toMomoa(result, {
|
|
4470
|
+
filename,
|
|
4471
|
+
logger,
|
|
4472
|
+
continueOnError
|
|
4473
|
+
});
|
|
4474
|
+
src = reRunResult.src;
|
|
4475
|
+
document = reRunResult.document;
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
let tokenCount = 0;
|
|
4479
|
+
const startValidate = performance.now();
|
|
4480
|
+
const $typeInheritance = {};
|
|
4481
|
+
traverse(document, { enter(node, parent, subpath) {
|
|
4482
|
+
if (node.type === "Document" && node.body.type === "Object" && node.body.members) {
|
|
4483
|
+
const { members: rootMembers } = node.body;
|
|
4484
|
+
const root$type = rootMembers.find((m) => m.name.type === "String" && m.name.value === "$type");
|
|
4485
|
+
const root$value = rootMembers.find((m) => m.name.type === "String" && m.name.value === "$value");
|
|
4486
|
+
if (root$type && !root$value) $typeInheritance["."] = root$type;
|
|
4487
|
+
}
|
|
4488
|
+
if (node.type === "Object" && subpath.length && !node.members.some((m) => m.name.type === "String" && m.name.value === "$value") && !subpath.includes("$value") && !subpath.includes("$extensions")) {
|
|
4489
|
+
if (transform?.group) {
|
|
4490
|
+
const newJSON = transform?.group(evaluate(node), subpath.join("."), node);
|
|
4491
|
+
if (newJSON) replaceObjMembers(node, parseJSON(newJSON));
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
if (node.type === "Member") {
|
|
4495
|
+
const inheritedTypeNode = getInheritedType(node, {
|
|
4496
|
+
subpath,
|
|
4497
|
+
$typeInheritance
|
|
4498
|
+
});
|
|
4499
|
+
if (node.value.type === "Object") {
|
|
4500
|
+
const $type = node.value.members.find((m) => m.name.type === "String" && m.name.value === "$type") || inheritedTypeNode;
|
|
4501
|
+
const $value = node.value.members.find((m) => m.name.type === "String" && m.name.value === "$value");
|
|
4502
|
+
if ($value && $type?.value.type === "String" && transform?.[$type.value.value]) {
|
|
4503
|
+
const result = transform[$type.value.value]?.(evaluate(node.value), subpath.join("."), node);
|
|
4504
|
+
if (result) node.value = parseJSON(result).body;
|
|
4505
|
+
}
|
|
4506
|
+
const token = validateTokenNode(node, {
|
|
4507
|
+
filename,
|
|
4508
|
+
src,
|
|
4509
|
+
config,
|
|
4510
|
+
logger,
|
|
4511
|
+
parent,
|
|
4512
|
+
subpath,
|
|
4513
|
+
transform,
|
|
4514
|
+
inheritedTypeNode
|
|
4515
|
+
});
|
|
4516
|
+
if (token) {
|
|
4517
|
+
tokensSet[token.id] = token;
|
|
4518
|
+
tokenCount++;
|
|
4519
|
+
}
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
} });
|
|
4523
|
+
logger.debug({
|
|
4524
|
+
message: `Validated ${tokenCount} tokens`,
|
|
4525
|
+
group: "parser",
|
|
4526
|
+
label: "validate",
|
|
4527
|
+
timing: performance.now() - startValidate
|
|
4528
|
+
});
|
|
4529
|
+
const normalizeStart = performance.now();
|
|
4530
|
+
for (const [id, token] of Object.entries(tokensSet)) {
|
|
4531
|
+
try {
|
|
4532
|
+
tokensSet[id].$value = normalizeValue(token);
|
|
4533
|
+
} catch (err) {
|
|
4534
|
+
let { node } = token.source;
|
|
4535
|
+
const members = getObjMembers(node);
|
|
4536
|
+
if (members.$value) node = members.$value;
|
|
4537
|
+
logger.error({
|
|
4538
|
+
group: "parser",
|
|
4539
|
+
label: "normalize",
|
|
4540
|
+
message: err.message,
|
|
4541
|
+
filename,
|
|
4542
|
+
src,
|
|
4543
|
+
node,
|
|
4544
|
+
continueOnError
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
4547
|
+
for (const [mode, modeValue] of Object.entries(token.mode)) {
|
|
4548
|
+
if (mode === ".") continue;
|
|
4549
|
+
try {
|
|
4550
|
+
tokensSet[id].mode[mode].$value = normalizeValue({
|
|
4551
|
+
$type: token.$type,
|
|
4552
|
+
...modeValue
|
|
4553
|
+
});
|
|
4554
|
+
} catch (err) {
|
|
4555
|
+
let { node } = token.source;
|
|
4556
|
+
const members = getObjMembers(node);
|
|
4557
|
+
if (members.$value) node = members.$value;
|
|
4558
|
+
logger.error({
|
|
4559
|
+
group: "parser",
|
|
4560
|
+
label: "normalize",
|
|
4561
|
+
message: err.message,
|
|
4562
|
+
filename,
|
|
4563
|
+
src,
|
|
4564
|
+
node: modeValue.source.node,
|
|
4565
|
+
continueOnError
|
|
4566
|
+
});
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
logger.debug({
|
|
4571
|
+
message: `Normalized ${tokenCount} tokens`,
|
|
4572
|
+
group: "parser",
|
|
4573
|
+
label: "normalize",
|
|
4574
|
+
timing: performance.now() - normalizeStart
|
|
4575
|
+
});
|
|
4576
|
+
if (!skipLint && config?.plugins?.length) {
|
|
4577
|
+
const lintStart = performance.now();
|
|
4578
|
+
await lintRunner({
|
|
4579
|
+
tokens: tokensSet,
|
|
4580
|
+
src,
|
|
4581
|
+
config,
|
|
4582
|
+
logger
|
|
4583
|
+
});
|
|
4584
|
+
logger.debug({
|
|
4585
|
+
message: `Linted ${tokenCount} tokens`,
|
|
4586
|
+
group: "parser",
|
|
4587
|
+
label: "lint",
|
|
4588
|
+
timing: performance.now() - lintStart
|
|
4589
|
+
});
|
|
4590
|
+
} else logger.debug({
|
|
4591
|
+
message: "Linting skipped",
|
|
4592
|
+
group: "parser",
|
|
4593
|
+
label: "lint"
|
|
4594
|
+
});
|
|
4595
|
+
return {
|
|
4596
|
+
tokens: tokensSet,
|
|
4597
|
+
document,
|
|
4598
|
+
src
|
|
4599
|
+
};
|
|
4600
|
+
}
|
|
4601
|
+
|
|
4602
|
+
//#endregion
|
|
4603
|
+
export { CHILD_KEYS, FONT_WEIGHT_MAP, FONT_WEIGHT_VALUES, LOG_ORDER, Logger, MULTI_VALUE, SINGLE_VALUE, STROKE_STYLE_LINE_CAP_VALUES, STROKE_STYLE_VALUES, TokensJSONError, VALID_COLORSPACES, build, defineConfig, findNode, formatMessage, getInheritedType, getObjMembers, injectObjMembers, isNode, lintRunner, maybeRawJSON, mergeConfigs, normalizeValue as normalize, parse, parseJSON, replaceObjMembers, toMomoa, traverse, validateAliasSyntax, validateBoolean, validateBorder, validateColor, validateCubicBezier, validateDimension, validateDuration, validateFontFamily, validateFontWeight, validateGradient, validateNumber, validateShadowLayer, validateStrokeStyle, validateTokenMemberNode, validateTokenNode, validateTransition };
|
|
13
4604
|
//# sourceMappingURL=index.js.map
|