@terrazzo/parser
Version:
Parser/validator for the Design Tokens Community Group (DTCG) standard.
4,617 lines • 153 kB
JavaScript
import wcmatch from "wildcard-match";
import pc from "picocolors";
import { merge } from "merge-anything";
import { isAlias, parseAlias, parseColor, pluralize, splitID, tokenToCulori } from "@terrazzo/token-tools";
import { clampChroma, wcagContrast } from "culori";
//#region src/lib/code-frame.ts
/**
* Extract what lines should be marked and highlighted.
*/
function getMarkerLines(loc, source, opts = {}) {
const startLoc = {
column: 0,
line: -1,
...loc.start
};
const endLoc = {
...startLoc,
...loc.end
};
const { linesAbove = 2, linesBelow = 3 } = opts || {};
const startLine = startLoc.line;
const startColumn = startLoc.column;
const endLine = endLoc.line;
const endColumn = endLoc.column;
let start = Math.max(startLine - (linesAbove + 1), 0);
let end = Math.min(source.length, endLine + linesBelow);
if (startLine === -1) start = 0;
if (endLine === -1) end = source.length;
const lineDiff = endLine - startLine;
const markerLines = {};
if (lineDiff) for (let i = 0; i <= lineDiff; i++) {
const lineNumber = i + startLine;
if (!startColumn) markerLines[lineNumber] = true;
else if (i === 0) {
const sourceLength = source[lineNumber - 1].length;
markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];
} else if (i === lineDiff) markerLines[lineNumber] = [0, endColumn];
else {
const sourceLength = source[lineNumber - i].length;
markerLines[lineNumber] = [0, sourceLength];
}
}
else if (startColumn === endColumn) if (startColumn) markerLines[startLine] = [startColumn, 0];
else markerLines[startLine] = true;
else markerLines[startLine] = [startColumn, endColumn - startColumn];
return {
start,
end,
markerLines
};
}
/**
* RegExp to test for newlines in terminal.
*/
const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
function codeFrameColumns(rawLines, loc, opts = {}) {
const lines = rawLines.split(NEWLINE);
const { start, end, markerLines } = getMarkerLines(loc, lines, opts);
const hasColumns = loc.start && typeof loc.start.column === "number";
const numberMaxWidth = String(end).length;
let frame = rawLines.split(NEWLINE, end).slice(start, end).map((line, index) => {
const number = start + 1 + index;
const paddedNumber = ` ${number}`.slice(-numberMaxWidth);
const gutter = ` ${paddedNumber} |`;
const hasMarker = markerLines[number];
const lastMarkerLine = !markerLines[number + 1];
if (hasMarker) {
let markerLine = "";
if (Array.isArray(hasMarker)) {
const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, " ");
const numberOfMarkers = hasMarker[1] || 1;
markerLine = [
"\n ",
gutter.replace(/\d/g, " "),
" ",
markerSpacing,
"^".repeat(numberOfMarkers)
].join("");
if (lastMarkerLine && opts.message) markerLine += ` ${opts.message}`;
}
return [
">",
gutter,
line.length > 0 ? ` ${line}` : "",
markerLine
].join("");
} else return ` ${gutter}${line.length > 0 ? ` ${line}` : ""}`;
}).join("\n");
if (opts.message && !hasColumns) frame = `${" ".repeat(numberMaxWidth + 1)}${opts.message}\n${frame}`;
return frame;
}
//#endregion
//#region src/logger.ts
const LOG_ORDER = [
"error",
"warn",
"info",
"debug"
];
const MESSAGE_COLOR = {
error: pc.red,
warn: pc.yellow
};
const timeFormatter = new Intl.DateTimeFormat("en-us", {
hour: "numeric",
hour12: false,
minute: "numeric",
second: "numeric",
fractionalSecondDigits: 3
});
/**
* @param {Entry} entry
* @param {Severity} severity
* @return {string}
*/
function formatMessage(entry, severity) {
let message = entry.message;
message = `[${entry.group}${entry.label ? `:${entry.label}` : ""}] ${message}`;
if (severity in MESSAGE_COLOR) message = MESSAGE_COLOR[severity](message);
if (entry.src) {
const start = entry.node?.loc?.start ?? {
line: 0,
column: 0
};
const loc = entry.filename ? `${entry.filename?.href.replace(/^file:\/\//, "")}:${start?.line ?? 0}:${start?.column ?? 0}\n\n` : "";
const codeFrame = codeFrameColumns(entry.src, { start }, { highlightCode: false });
message = `${message}\n\n${loc}${codeFrame}`;
}
return message;
}
var Logger = class {
level = "info";
debugScope = "*";
errorCount = 0;
warnCount = 0;
infoCount = 0;
debugCount = 0;
constructor(options) {
if (options?.level) this.level = options.level;
if (options?.debugScope) this.debugScope = options.debugScope;
}
setLevel(level) {
this.level = level;
}
/** Log an error message (always; can’t be silenced) */
error(entry) {
this.errorCount++;
const message = formatMessage(entry, "error");
if (entry.continueOnError) {
console.error(message);
return;
}
if (entry.node) throw new TokensJSONError(message);
else throw new Error(message);
}
/** Log an info message (if logging level permits) */
info(entry) {
this.infoCount++;
if (this.level === "silent" || LOG_ORDER.indexOf(this.level) < LOG_ORDER.indexOf("info")) return;
const message = formatMessage(entry, "info");
console.log(message);
}
/** Log a warning message (if logging level permits) */
warn(entry) {
this.warnCount++;
if (this.level === "silent" || LOG_ORDER.indexOf(this.level) < LOG_ORDER.indexOf("warn")) return;
const message = formatMessage(entry, "warn");
console.warn(message);
}
/** Log a diagnostics message (if logging level permits) */
debug(entry) {
if (this.level === "silent" || LOG_ORDER.indexOf(this.level) < LOG_ORDER.indexOf("debug")) return;
this.debugCount++;
let message = formatMessage(entry, "debug");
const debugPrefix = entry.label ? `${entry.group}:${entry.label}` : entry.group;
if (this.debugScope !== "*" && !wcmatch(this.debugScope)(debugPrefix)) return;
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));
message = `${pc.dim(timeFormatter.format(performance.now()))} ${message}`;
if (typeof entry.timing === "number") {
let timing = "";
if (entry.timing < 1e3) timing = `${Math.round(entry.timing * 100) / 100}ms`;
else if (entry.timing < 6e4) timing = `${Math.round(entry.timing * 100) / 1e5}s`;
message = `${message} ${pc.dim(`[${timing}]`)}`;
}
console.log(message);
}
/** Get stats for current logger instance */
stats() {
return {
errorCount: this.errorCount,
warnCount: this.warnCount,
infoCount: this.infoCount,
debugCount: this.debugCount
};
}
};
var TokensJSONError = class extends Error {
constructor(message) {
super(message);
this.name = "TokensJSONError";
}
};
//#endregion
//#region src/build/index.ts
const SINGLE_VALUE = "SINGLE_VALUE";
const MULTI_VALUE = "MULTI_VALUE";
/** Validate plugin setTransform() calls for immediate feedback */
function validateTransformParams({ params, logger, pluginName }) {
const baseMessage = {
group: "plugin",
label: pluginName,
message: ""
};
if (!params.value || typeof params.value !== "string" && typeof params.value !== "object" || Array.isArray(params.value)) logger.error({
...baseMessage,
message: `setTransform() value expected string or object of strings, received ${Array.isArray(params.value) ? "Array" : typeof params.value}`
});
if (typeof params.value === "object" && Object.values(params.value).some((v) => typeof v !== "string")) logger.error({
...baseMessage,
message: "setTransform() value expected object of strings, received some non-string values"
});
}
/** Run build stage */
async function build(tokens, { sources, logger = new Logger(), config }) {
const formats = {};
const result = { outputFiles: [] };
function getTransforms(params) {
if (!params?.format) {
logger.warn({
group: "plugin",
message: "\"format\" missing from getTransforms(), no tokens returned."
});
return [];
}
const tokenMatcher = params.id ? wcmatch(Array.isArray(params.id) ? params.id : [params.id]) : null;
const modeMatcher = params.mode ? wcmatch(params.mode) : null;
return (formats[params.format] ?? []).filter((token) => {
if (params.$type) {
if (typeof params.$type === "string" && token.token.$type !== params.$type) return false;
else if (Array.isArray(params.$type) && !params.$type.some(($type) => token.token.$type === $type)) return false;
}
if (params.id && params.id !== "*" && tokenMatcher && !tokenMatcher(token.token.id)) return false;
if (modeMatcher && !modeMatcher(token.mode)) return false;
return true;
});
}
let transformsLocked = false;
const startTransform = performance.now();
for (const plugin of config.plugins) if (typeof plugin.transform === "function") await plugin.transform({
tokens,
sources,
getTransforms,
setTransform(id, params) {
if (transformsLocked) {
logger.warn({
message: "Attempted to call setTransform() after transform step has completed.",
group: "plugin",
label: plugin.name
});
return;
}
const token = tokens[id];
const cleanValue = typeof params.value === "string" ? params.value : { ...params.value };
if (typeof cleanValue === "object") {
for (const k of Object.keys(cleanValue)) if (cleanValue[k] === void 0) delete cleanValue[k];
}
validateTransformParams({
logger,
params: {
...params,
value: cleanValue
},
pluginName: plugin.name
});
if (!formats[params.format]) formats[params.format] = [];
const foundTokenI = formats[params.format].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && (!params.mode || params.mode === t.mode));
if (foundTokenI === -1) formats[params.format].push({
...params,
id,
value: cleanValue,
type: typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE,
mode: params.mode || ".",
token: structuredClone(token)
});
else {
formats[params.format][foundTokenI].value = cleanValue;
formats[params.format][foundTokenI].type = typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE;
}
}
});
transformsLocked = true;
logger.debug({
group: "parser",
label: "transform",
message: "transform() step",
timing: performance.now() - startTransform
});
const startBuild = performance.now();
for (const plugin of config.plugins) if (typeof plugin.build === "function") {
const pluginBuildStart = performance.now();
await plugin.build({
tokens,
sources,
getTransforms,
outputFile(filename, contents) {
const resolved = new URL(filename, config.outDir);
if (result.outputFiles.some((f) => new URL(f.filename, config.outDir).href === resolved.href)) logger.error({
group: "plugin",
message: `Can’t overwrite file "${filename}"`,
label: plugin.name
});
result.outputFiles.push({
filename,
contents,
plugin: plugin.name,
time: performance.now() - pluginBuildStart
});
}
});
}
logger.debug({
group: "parser",
label: "build",
message: "build() step",
timing: performance.now() - startBuild
});
const startBuildEnd = performance.now();
for (const plugin of config.plugins) if (typeof plugin.buildEnd === "function") await plugin.buildEnd({ outputFiles: structuredClone(result.outputFiles) });
logger.debug({
group: "parser",
label: "build",
message: "buildEnd() step",
timing: performance.now() - startBuildEnd
});
return result;
}
//#endregion
//#region src/lint/plugin-core/lib/docs.ts
function docsLink(ruleName) {
return `https://terrazzo.app/docs/cli/lint#${ruleName.replaceAll("/", "")}`;
}
//#endregion
//#region src/lint/plugin-core/rules/a11y-min-contrast.ts
const A11Y_MIN_CONTRAST = "a11y/min-contrast";
const WCAG2_MIN_CONTRAST = {
AA: {
default: 4.5,
large: 3
},
AAA: {
default: 7,
large: 4.5
}
};
const ERROR_INSUFFICIENT_CONTRAST = "INSUFFICIENT_CONTRAST";
const rule$9 = {
meta: {
messages: { [ERROR_INSUFFICIENT_CONTRAST]: "Pair {{ index }} failed; expected {{ expected }}, got {{ actual }} ({{ level }})" },
docs: {
description: "Enforce colors meet minimum contrast checks for WCAG 2.",
url: docsLink(A11Y_MIN_CONTRAST)
}
},
defaultOptions: {
level: "AA",
pairs: []
},
create({ tokens, options, report }) {
for (let i = 0; i < options.pairs.length; i++) {
const { foreground, background, largeText } = options.pairs[i];
if (!tokens[foreground]) throw new Error(`Token ${foreground} does not exist`);
if (tokens[foreground].$type !== "color") throw new Error(`Token ${foreground} isn’t a color`);
if (!tokens[background]) throw new Error(`Token ${background} does not exist`);
if (tokens[background].$type !== "color") throw new Error(`Token ${background} isn’t a color`);
const a = tokenToCulori(tokens[foreground].$value);
const b = tokenToCulori(tokens[background].$value);
const contrast = wcagContrast(a, b);
const min = WCAG2_MIN_CONTRAST[options.level ?? "AA"][largeText ? "large" : "default"];
if (contrast < min) report({
messageId: ERROR_INSUFFICIENT_CONTRAST,
data: {
index: i + 1,
expected: min,
actual: Math.round(contrast * 100) / 100,
level: options.level
}
});
}
}
};
var a11y_min_contrast_default = rule$9;
//#endregion
//#region src/lint/plugin-core/rules/a11y-min-font-size.ts
const A11Y_MIN_FONT_SIZE = "a11y/min-font-size";
const ERROR_TOO_SMALL = "TOO_SMALL";
const rule$8 = {
meta: {
messages: { [ERROR_TOO_SMALL]: "{{ id }} font size too small. Expected minimum of {{ min }}" },
docs: {
description: "Enforce font sizes are no smaller than the given value.",
url: docsLink(A11Y_MIN_FONT_SIZE)
}
},
defaultOptions: {},
create({ tokens, options, report }) {
if (!options.minSizePx && !options.minSizeRem) throw new Error("Must specify at least one of minSizePx or minSizeRem");
const shouldIgnore = options.ignore ? wcmatch(options.ignore) : null;
for (const t of Object.values(tokens)) {
if (shouldIgnore?.(t.id)) continue;
if (t.aliasOf) continue;
if (t.$type === "typography" && "fontSize" in t.$value) {
const fontSize = t.$value.fontSize;
if (fontSize.unit === "px" && options.minSizePx && fontSize.value < options.minSizePx || fontSize.unit === "rem" && options.minSizeRem && fontSize.value < options.minSizeRem) report({
messageId: ERROR_TOO_SMALL,
data: {
id: t.id,
min: options.minSizePx ? `${options.minSizePx}px` : `${options.minSizeRem}rem`
}
});
}
}
}
};
var a11y_min_font_size_default = rule$8;
//#endregion
//#region src/lint/plugin-core/rules/colorspace.ts
const COLORSPACE = "core/colorspace";
const ERROR_COLOR$1 = "COLOR";
const ERROR_BORDER$1 = "BORDER";
const ERROR_GRADIENT$1 = "GRADIENT";
const ERROR_SHADOW$1 = "SHADOW";
const rule$7 = {
meta: {
messages: {
[ERROR_COLOR$1]: "Color {{ id }} not in colorspace {{ colorSpace }}",
[ERROR_BORDER$1]: "Border {{ id }} not in colorspace {{ colorSpace }}",
[ERROR_GRADIENT$1]: "Gradient {{ id }} not in colorspace {{ colorSpace }}",
[ERROR_SHADOW$1]: "Shadow {{ id }} not in colorspace {{ colorSpace }}"
},
docs: {
description: "Enforce that all colors are in a specific colorspace.",
url: docsLink(COLORSPACE)
}
},
defaultOptions: { colorSpace: "srgb" },
create({ tokens, options, report }) {
if (!options.colorSpace) return;
const shouldIgnore = options.ignore ? wcmatch(options.ignore) : null;
for (const t of Object.values(tokens)) {
if (shouldIgnore?.(t.id)) continue;
if (t.aliasOf) continue;
switch (t.$type) {
case "color": {
if (t.$value.colorSpace !== options.colorSpace) report({
messageId: ERROR_COLOR$1,
data: {
id: t.id,
colorSpace: options.colorSpace
},
node: t.source.node
});
break;
}
case "border": {
if (!t.partialAliasOf?.color && t.$value.color.colorSpace !== options.colorSpace) report({
messageId: ERROR_BORDER$1,
data: {
id: t.id,
colorSpace: options.colorSpace
},
node: t.source.node
});
break;
}
case "gradient": {
for (let stopI = 0; stopI < t.$value.length; stopI++) if (!t.partialAliasOf?.[stopI]?.color && t.$value[stopI].color.colorSpace !== options.colorSpace) report({
messageId: ERROR_GRADIENT$1,
data: {
id: t.id,
colorSpace: options.colorSpace
},
node: t.source.node
});
break;
}
case "shadow": {
for (let shadowI = 0; shadowI < t.$value.length; shadowI++) if (!t.partialAliasOf?.[shadowI]?.color && t.$value[shadowI].color.colorSpace !== options.colorSpace) report({
messageId: ERROR_SHADOW$1,
data: {
id: t.id,
colorSpace: options.colorSpace
},
node: t.source.node
});
break;
}
}
}
}
};
var colorspace_default = rule$7;
//#endregion
//#region ../../node_modules/.pnpm/scule@1.3.0/node_modules/scule/dist/index.mjs
const NUMBER_CHAR_RE = /\d/;
const STR_SPLITTERS = [
"-",
"_",
"/",
"."
];
function isUppercase(char = "") {
if (NUMBER_CHAR_RE.test(char)) return void 0;
return char !== char.toLowerCase();
}
function splitByCase(str, separators) {
const splitters = separators ?? STR_SPLITTERS;
const parts = [];
if (!str || typeof str !== "string") return parts;
let buff = "";
let previousUpper;
let previousSplitter;
for (const char of str) {
const isSplitter = splitters.includes(char);
if (isSplitter === true) {
parts.push(buff);
buff = "";
previousUpper = void 0;
continue;
}
const isUpper = isUppercase(char);
if (previousSplitter === false) {
if (previousUpper === false && isUpper === true) {
parts.push(buff);
buff = char;
previousUpper = isUpper;
continue;
}
if (previousUpper === true && isUpper === false && buff.length > 1) {
const lastChar = buff.at(-1);
parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
buff = lastChar + char;
previousUpper = isUpper;
continue;
}
}
buff += char;
previousUpper = isUpper;
previousSplitter = isSplitter;
}
parts.push(buff);
return parts;
}
function upperFirst(str) {
return str ? str[0].toUpperCase() + str.slice(1) : "";
}
function lowerFirst(str) {
return str ? str[0].toLowerCase() + str.slice(1) : "";
}
function pascalCase(str, opts) {
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)).join("") : "";
}
function camelCase(str, opts) {
return lowerFirst(pascalCase(str || "", opts));
}
function kebabCase(str, joiner) {
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
}
function snakeCase(str) {
return kebabCase(str || "", "_");
}
//#endregion
//#region src/lint/plugin-core/rules/consistent-naming.ts
const CONSISTENT_NAMING = "core/consistent-naming";
const ERROR_WRONG_FORMAT = "ERROR_WRONG_FORMAT";
const rule$6 = {
meta: {
messages: { [ERROR_WRONG_FORMAT]: "{{ id }} doesn’t match format {{ format }}" },
docs: {
description: "Enforce consistent naming for tokens.",
url: docsLink(CONSISTENT_NAMING)
}
},
defaultOptions: { format: "kebab-case" },
create({ tokens, options, report }) {
const basicFormatter = {
"kebab-case": kebabCase,
camelCase,
PascalCase: pascalCase,
snake_case: snakeCase,
SCREAMING_SNAKE_CASE: (name) => snakeCase(name).toLocaleUpperCase()
}[String(options.format)];
for (const t of Object.values(tokens)) if (basicFormatter) {
const parts = t.id.split(".");
if (!parts.every((part) => basicFormatter(part) === part)) report({
messageId: ERROR_WRONG_FORMAT,
data: {
id: t.id,
format: options.format
},
node: t.source.node
});
} else if (typeof options.format === "function") {
const result = options.format(t.id);
if (result) report({
messageId: ERROR_WRONG_FORMAT,
data: {
id: t.id,
format: "(custom)"
},
node: t.source.node
});
}
}
};
var consistent_naming_default = rule$6;
//#endregion
//#region src/lint/plugin-core/rules/descriptions.ts
const DESCRIPTIONS = "core/descriptions";
const ERROR_MISSING_DESCRIPTION = "MISSING_DESCRIPTION";
const rule$5 = {
meta: {
messages: { [ERROR_MISSING_DESCRIPTION]: "{{ id }} missing description" },
docs: {
description: "Enforce tokens have descriptions.",
url: docsLink(DESCRIPTIONS)
}
},
defaultOptions: {},
create({ tokens, options, report }) {
const shouldIgnore = options.ignore ? wcmatch(options.ignore) : null;
for (const t of Object.values(tokens)) {
if (shouldIgnore?.(t.id)) continue;
if (!t.$description) report({
messageId: ERROR_MISSING_DESCRIPTION,
data: { id: t.id },
node: t.source.node
});
}
}
};
var descriptions_default = rule$5;
//#endregion
//#region src/lint/plugin-core/rules/duplicate-values.ts
const DUPLICATE_VALUES = "core/duplicate-values";
const ERROR_DUPLICATE_VALUE = "ERROR_DUPLICATE_VALUE";
const rule$4 = {
meta: {
messages: { [ERROR_DUPLICATE_VALUE]: "{{ id }} declared a duplicate value" },
docs: {
description: "Enforce tokens can’t redeclare the same value (excludes aliases).",
url: docsLink(DUPLICATE_VALUES)
}
},
defaultOptions: {},
create({ report, tokens, options }) {
const values = {};
const shouldIgnore = options.ignore ? wcmatch(options.ignore) : null;
for (const t of Object.values(tokens)) {
if (shouldIgnore?.(t.id)) continue;
if (!values[t.$type]) values[t.$type] = /* @__PURE__ */ new Set();
if (t.$type === "boolean" || t.$type === "duration" || t.$type === "fontWeight" || t.$type === "link" || t.$type === "number" || t.$type === "string") {
if (typeof t.aliasOf === "string" && isAlias(t.aliasOf)) continue;
if (values[t.$type]?.has(t.$value)) report({
messageId: ERROR_DUPLICATE_VALUE,
data: { id: t.id },
node: t.source.node
});
values[t.$type]?.add(t.$value);
} else {
for (const v of values[t.$type].values() ?? []) if (JSON.stringify(t.$value) === JSON.stringify(v)) {
report({
messageId: ERROR_DUPLICATE_VALUE,
data: { id: t.id },
node: t.source.node
});
break;
}
values[t.$type].add(t.$value);
}
}
}
};
var duplicate_values_default = rule$4;
//#endregion
//#region src/lint/plugin-core/rules/max-gamut.ts
const MAX_GAMUT = "core/max-gamut";
const TOLERANCE = 1e-6;
/** is a Culori-parseable color within the specified gamut? */
function isWithinGamut(color, gamut) {
const parsed = tokenToCulori(color);
if (!parsed) return false;
if ([
"rgb",
"hsl",
"hwb"
].includes(parsed.mode)) return true;
const clamped = clampChroma(parsed, parsed.mode, gamut === "srgb" ? "rgb" : gamut);
return isWithinThreshold(parsed, clamped);
}
/** is Color A close enough to Color B? */
function isWithinThreshold(a, b, tolerance = TOLERANCE) {
for (const k in a) {
if (k === "mode" || k === "alpha") continue;
if (!(k in b)) throw new Error(`Can’t compare ${a.mode} to ${b.mode}`);
if (Math.abs(a[k] - b[k]) > tolerance) return false;
}
return true;
}
const ERROR_COLOR = "COLOR";
const ERROR_BORDER = "BORDER";
const ERROR_GRADIENT = "GRADIENT";
const ERROR_SHADOW = "SHADOW";
const rule$3 = {
meta: {
messages: {
[ERROR_COLOR]: "Color {{ id }} is outside {{ gamut }} gamut",
[ERROR_BORDER]: "Border {{ id }} is outside {{ gamut }} gamut",
[ERROR_GRADIENT]: "Gradient {{ id }} is outside {{ gamut }} gamut",
[ERROR_SHADOW]: "Shadow {{ id }} is outside {{ gamut }} gamut"
},
docs: {
description: "Enforce colors are within the specified gamut.",
url: docsLink(MAX_GAMUT)
}
},
defaultOptions: { gamut: "rec2020" },
create({ tokens, options, report }) {
if (!options?.gamut) return;
if (options.gamut !== "srgb" && options.gamut !== "p3" && options.gamut !== "rec2020") throw new Error(`Unknown gamut "${options.gamut}". Options are "srgb", "p3", or "rec2020"`);
const shouldIgnore = options.ignore ? wcmatch(options.ignore) : null;
for (const t of Object.values(tokens)) {
if (shouldIgnore?.(t.id)) continue;
if (t.aliasOf) continue;
switch (t.$type) {
case "color": {
if (!isWithinGamut(t.$value, options.gamut)) report({
messageId: ERROR_COLOR,
data: {
id: t.id,
gamut: options.gamut
},
node: t.source.node
});
break;
}
case "border": {
if (!t.partialAliasOf?.color && !isWithinGamut(t.$value.color, options.gamut)) report({
messageId: ERROR_BORDER,
data: {
id: t.id,
gamut: options.gamut
},
node: t.source.node
});
break;
}
case "gradient": {
for (let stopI = 0; stopI < t.$value.length; stopI++) if (!t.partialAliasOf?.[stopI]?.color && !isWithinGamut(t.$value[stopI].color, options.gamut)) report({
messageId: ERROR_GRADIENT,
data: {
id: t.id,
gamut: options.gamut
},
node: t.source.node
});
break;
}
case "shadow": {
for (let shadowI = 0; shadowI < t.$value.length; shadowI++) if (!t.partialAliasOf?.[shadowI]?.color && !isWithinGamut(t.$value[shadowI].color, options.gamut)) report({
messageId: ERROR_SHADOW,
data: {
id: t.id,
gamut: options.gamut
},
node: t.source.node
});
break;
}
}
}
}
};
var max_gamut_default = rule$3;
//#endregion
//#region src/lint/plugin-core/rules/required-children.ts
const REQUIRED_CHILDREN = "core/required-children";
const ERROR_EMPTY_MATCH = "EMPTY_MATCH";
const ERROR_MISSING_REQUIRED_TOKENS = "MISSING_REQUIRED_TOKENS";
const ERROR_MISSING_REQUIRED_GROUP = "MISSING_REQUIRED_GROUP";
const rule$2 = {
meta: {
messages: {
[ERROR_EMPTY_MATCH]: "No tokens matched {{ matcher }}",
[ERROR_MISSING_REQUIRED_TOKENS]: "Match {{ index }}: some groups missing required token \"{{ token }}\"",
[ERROR_MISSING_REQUIRED_GROUP]: "Match {{ index }}: some tokens missing required group \"{{ group }}\""
},
docs: {
description: "Enforce token groups have specific children, whether tokens and/or groups.",
url: docsLink(REQUIRED_CHILDREN)
}
},
defaultOptions: { matches: [] },
create({ tokens, options, report }) {
if (!options.matches?.length) throw new Error("Invalid config. Missing `matches: […]`");
for (let matchI = 0; matchI < options.matches.length; matchI++) {
const { match, requiredTokens, requiredGroups } = options.matches[matchI];
if (!match.length) throw new Error(`Match ${matchI}: must declare \`match: […]\``);
if (!requiredTokens?.length && !requiredGroups?.length) throw new Error(`Match ${matchI}: must declare either \`requiredTokens: […]\` or \`requiredGroups: […]\``);
const matcher = wcmatch(match);
const matchGroups = [];
const matchTokens = [];
let tokensMatched = false;
for (const t of Object.values(tokens)) {
if (!matcher(t.id)) continue;
tokensMatched = true;
const groups = t.id.split(".");
matchTokens.push(groups.pop());
matchGroups.push(...groups);
}
if (!tokensMatched) {
report({
messageId: ERROR_EMPTY_MATCH,
data: { matcher: JSON.stringify(match) }
});
continue;
}
if (requiredTokens) {
for (const id of requiredTokens) if (!matchTokens.includes(id)) report({
messageId: ERROR_MISSING_REQUIRED_TOKENS,
data: {
index: matchI,
token: id
}
});
}
if (requiredGroups) {
for (const groupName of requiredGroups) if (!matchGroups.includes(groupName)) report({
messageId: ERROR_MISSING_REQUIRED_GROUP,
data: {
index: matchI,
group: groupName
}
});
}
}
}
};
var required_children_default = rule$2;
//#endregion
//#region src/lint/plugin-core/rules/required-modes.ts
const REQUIRED_MODES = "core/required-modes";
const rule$1 = {
meta: { docs: {
description: "Enforce certain tokens have specific modes.",
url: docsLink(REQUIRED_MODES)
} },
defaultOptions: { matches: [] },
create({ tokens, options, report }) {
if (!options?.matches?.length) throw new Error("Invalid config. Missing `matches: […]`");
for (let matchI = 0; matchI < options.matches.length; matchI++) {
const { match, modes } = options.matches[matchI];
if (!match.length) throw new Error(`Match ${matchI}: must declare \`match: […]\``);
if (!modes?.length) throw new Error(`Match ${matchI}: must declare \`modes: […]\``);
const matcher = wcmatch(match);
let tokensMatched = false;
for (const t of Object.values(tokens)) {
if (!matcher(t.id)) continue;
tokensMatched = true;
for (const mode of modes) if (!t.mode?.[mode]) report({
message: `Token ${t.id}: missing required mode "${mode}"`,
node: t.source.node
});
if (!tokensMatched) report({
message: `Match "${matchI}": no tokens matched ${JSON.stringify(match)}`,
node: t.source.node
});
}
}
}
};
var required_modes_default = rule$1;
//#endregion
//#region src/lint/plugin-core/rules/required-typography-properties.ts
const REQUIRED_TYPOGRAPHY_PROPERTIES = "core/required-typography-properties";
const rule = {
meta: { docs: {
description: "Enforce typography tokens have required properties.",
url: docsLink(REQUIRED_TYPOGRAPHY_PROPERTIES)
} },
defaultOptions: { properties: [] },
create({ tokens, options, report }) {
if (!options) return;
if (!options.properties.length) throw new Error(`"properties" can’t be empty`);
const shouldIgnore = options.ignore ? wcmatch(options.ignore) : null;
for (const t of Object.values(tokens)) {
if (shouldIgnore?.(t.id)) continue;
if (t.$type !== "typography") continue;
if (t.aliasOf) continue;
for (const p of options.properties) if (!t.partialAliasOf?.[p] && !(p in t.$value)) report({
message: `${t.id} missing required typographic property "${p}"`,
node: t.source.node
});
}
}
};
var required_typography_properties_default = rule;
//#endregion
//#region src/lint/plugin-core/index.ts
function coreLintPlugin() {
return {
name: "@terrazzo/plugin-lint-core",
lint() {
return {
[COLORSPACE]: colorspace_default,
[CONSISTENT_NAMING]: consistent_naming_default,
[DESCRIPTIONS]: descriptions_default,
[DUPLICATE_VALUES]: duplicate_values_default,
[MAX_GAMUT]: max_gamut_default,
[REQUIRED_CHILDREN]: required_children_default,
[REQUIRED_MODES]: required_modes_default,
[REQUIRED_TYPOGRAPHY_PROPERTIES]: required_typography_properties_default,
[A11Y_MIN_CONTRAST]: a11y_min_contrast_default,
[A11Y_MIN_FONT_SIZE]: a11y_min_font_size_default
};
}
};
}
//#endregion
//#region src/config.ts
const TRAILING_SLASH_RE = /\/*$/;
/**
* Validate and normalize a config
*/
function defineConfig(rawConfig, { logger = new Logger(), cwd } = {}) {
const configStart = performance.now();
if (!cwd) logger.error({
group: "config",
label: "core",
message: "defineConfig() missing `cwd` for JS API"
});
const config = merge({}, rawConfig);
normalizeTokens({
rawConfig,
config,
logger,
cwd
});
normalizeOutDir({
config,
cwd,
logger
});
normalizePlugins({
config,
logger
});
normalizeLint({
config,
logger
});
normalizeIgnore({
config,
logger
});
for (const plugin of config.plugins) plugin.config?.({ ...config });
logger.debug({
group: "parser",
label: "config",
message: "Finish config validation",
timing: performance.now() - configStart
});
return config;
}
/** Normalize config.tokens */
function normalizeTokens({ rawConfig, config, logger, cwd }) {
if (rawConfig.tokens === void 0) config.tokens = ["./tokens.json"];
else if (typeof rawConfig.tokens === "string") config.tokens = [rawConfig.tokens];
else if (Array.isArray(rawConfig.tokens)) {
config.tokens = [];
for (const file of rawConfig.tokens) if (typeof file === "string" || file instanceof URL) config.tokens.push(file);
else logger.error({
group: "config",
label: "tokens",
message: `Expected array of strings, encountered ${JSON.stringify(file)}`
});
} else logger.error({
group: "config",
label: "tokens",
message: `Expected string or array of strings, received ${typeof rawConfig.tokens}`
});
for (let i = 0; i < config.tokens.length; i++) {
const filepath = config.tokens[i];
if (filepath instanceof URL) continue;
try {
config.tokens[i] = new URL(filepath, cwd);
} catch {
logger.error({
group: "config",
label: "tokens",
message: `Invalid URL ${filepath}`
});
}
}
}
/** Normalize config.outDir */
function normalizeOutDir({ config, cwd, logger }) {
if (config.outDir instanceof URL) {} else if (typeof config.outDir === "undefined") config.outDir = new URL("./tokens/", cwd);
else if (typeof config.outDir !== "string") logger.error({
group: "config",
label: "outDir",
message: `Expected string, received ${JSON.stringify(config.outDir)}`
});
else {
config.outDir = new URL(config.outDir, cwd);
config.outDir = new URL(config.outDir.href.replace(TRAILING_SLASH_RE, "/"));
}
}
/** Normalize config.plugins */
function normalizePlugins({ config, logger }) {
if (typeof config.plugins === "undefined") config.plugins = [];
if (!Array.isArray(config.plugins)) logger.error({
group: "config",
label: "plugins",
message: `Expected array of plugins, received ${JSON.stringify(config.plugins)}`
});
config.plugins.push(coreLintPlugin());
for (let n = 0; n < config.plugins.length; n++) {
const plugin = config.plugins[n];
if (typeof plugin !== "object") logger.error({
group: "config",
label: `plugin[${n}]`,
message: `Expected output plugin, received ${JSON.stringify(plugin)}`
});
else if (!plugin.name) logger.error({
group: "config",
label: `plugin[${n}]`,
message: `Missing "name"`
});
}
config.plugins.sort((a, b) => {
if (a.enforce === "pre" && b.enforce !== "pre") return -1;
else if (a.enforce === "post" && b.enforce !== "post") return 1;
return 0;
});
}
function normalizeLint({ config, logger }) {
if (config.lint !== void 0) {
if (config.lint === null || typeof config.lint !== "object" || Array.isArray(config.lint)) logger.error({
group: "config",
label: "lint",
message: "Must be an object"
});
if (!config.lint.build) config.lint.build = { enabled: true };
if (config.lint.build.enabled !== void 0) {
if (typeof config.lint.build.enabled !== "boolean") logger.error({
group: "config",
label: "lint › build › enabled",
message: `Expected boolean, received ${JSON.stringify(config.lint.build)}`
});
} else config.lint.build.enabled = true;
if (config.lint.rules === void 0) config.lint.rules = {};
else {
if (config.lint.rules === null || typeof config.lint.rules !== "object" || Array.isArray(config.lint.rules)) {
logger.error({
group: "config",
label: "lint › rules",
message: `Expected object, received ${JSON.stringify(config.lint.rules)}`
});
return;
}
const allRules = /* @__PURE__ */ new Map();
for (const plugin of config.plugins) {
if (typeof plugin.lint !== "function") continue;
const pluginRules = plugin.lint();
if (!pluginRules || Array.isArray(pluginRules) || typeof pluginRules !== "object") {
logger.error({
group: "config",
label: `plugin › ${plugin.name}`,
message: `Expected object for lint() received ${JSON.stringify(pluginRules)}`
});
continue;
}
for (const rule$10 of Object.keys(pluginRules)) {
if (allRules.get(rule$10) && allRules.get(rule$10) !== plugin.name) logger.error({
group: "config",
label: `plugin › ${plugin.name}`,
message: `Duplicate rule ${rule$10} already registered by plugin ${allRules.get(rule$10)}`
});
allRules.set(rule$10, plugin.name);
}
}
for (const id of Object.keys(config.lint.rules)) {
if (!allRules.has(id)) logger.error({
group: "config",
label: `lint › rule › ${id}`,
message: "Unknown rule. Is the plugin installed?"
});
const value = config.lint.rules[id];
let severity = "off";
let options;
if (typeof value === "number" || typeof value === "string") severity = value;
else if (Array.isArray(value)) {
severity = value[0];
options = value[1];
} else if (value !== void 0) logger.error({
group: "config",
label: `lint › rule › ${id}`,
message: `Invalid eyntax. Expected \`string | number | Array\`, received ${JSON.stringify(value)}}`
});
config.lint.rules[id] = [severity, options];
if (typeof severity === "number") {
if (severity !== 0 && severity !== 1 && severity !== 2) logger.error({
group: "config",
label: `lint › rule › ${id}`,
message: `Invalid number ${severity}. Specify 0 (off), 1 (warn), or 2 (error).`
});
config.lint.rules[id][0] = [
"off",
"warn",
"error"
][severity];
} else if (typeof severity === "string") {
if (severity !== "off" && severity !== "warn" && severity !== "error") logger.error({
group: "config",
label: `lint › rule › ${id}`,
message: `Invalid string ${JSON.stringify(severity)}. Specify "off", "warn", or "error".`
});
} else if (value !== null) logger.error({
group: "config",
label: `lint › rule › ${id}`,
message: `Expected string or number, received ${JSON.stringify(value)}`
});
}
}
} else config.lint = {
build: { enabled: true },
rules: {}
};
}
function normalizeIgnore({ config, logger }) {
if (!config.ignore) config.ignore = {};
config.ignore.tokens ??= [];
config.ignore.deprecated ??= false;
if (!Array.isArray(config.ignore.tokens) || config.ignore.tokens.some((x) => typeof x !== "string")) logger.error({
group: "config",
label: "ignore › tokens",
message: `Expected array of strings, received ${JSON.stringify(config.ignore.tokens)}`
});
if (typeof config.ignore.deprecated !== "boolean") logger.error({
group: "config",
label: "ignore › deprecated",
message: `Expected boolean, received ${JSON.stringify(config.ignore.deprecated)}`
});
}
/** Merge configs */
function mergeConfigs(a, b) {
return merge(a, b);
}
//#endregion
//#region src/lint/index.ts
const listFormat$1 = new Intl.ListFormat("en-us");
async function lintRunner({ tokens, filename, config = {}, src, logger }) {
const { plugins = [], lint } = config;
const unusedLintRules = Object.keys(lint?.rules ?? {});
for (const plugin of plugins) if (typeof plugin.lint === "function") {
const s = performance.now();
const linter = plugin.lint();
const errors = [];
const warnings = [];
await Promise.all(Object.entries(linter).map(async ([id, rule$10]) => {
if (!(id in lint.rules) || lint.rules[id] === null) return;
const [severity, options] = lint.rules[id];
if (severity === "off") return;
await rule$10.create({
id,
report(descriptor) {
let message = "";
if (!descriptor.message && !descriptor.messageId) logger.error({
group: "lint",
label: `${plugin.name} › lint › ${id}`,
message: "Unable to report error: missing message or messageId"
});
if (descriptor.message) message = descriptor.message;
else {
if (!(descriptor.messageId in (rule$10.meta?.messages ?? {}))) logger.error({
group: "lint",
label: `${plugin.name} › lint › ${id}`,
message: `messageId "${descriptor.messageId}" does not exist`
});
message = rule$10.meta?.messages?.[descriptor.messageId] ?? "";
}
if (descriptor.data && typeof descriptor.data === "object") for (const [k, v] of Object.entries(descriptor.data)) {
const formatted = [
"string",
"number",
"boolean"
].includes(typeof v) ? String(v) : JSON.stringify(v);
message = message.replace(/{{[^}]+}}/g, (inner) => {
const key = inner.substring(2, inner.length - 2).trim();
return key === k ? formatted : inner;
});
}
(severity === "error" ? errors : warnings).push({
group: "lint",
label: id,
message,
filename,
node: descriptor.node,
src: descriptor.source?.src
});
},
tokens,
filename,
src,
options: merge(rule$10.meta?.defaultOptions ?? [], rule$10.defaultOptions ?? [], options)
});
const unusedLintRuleI = unusedLintRules.indexOf(id);
if (unusedLintRuleI !== -1) unusedLintRules.splice(unusedLintRuleI, 1);
}));
for (const error of errors) logger.error({
...error,
continueOnError: true
});
for (const warning of warnings) logger.warn(warning);
logger.debug({
group: "lint",
label: plugin.name,
message: "Finished",
timing: performance.now() - s
});
if (errors.length) {
const counts = [pluralize(errors.length, "error", "errors")];
if (warnings.length) counts.push(pluralize(warnings.length, "warning", "warnings"));
logger.error({
group: "lint",
message: `Lint failed with ${listFormat$1.format(counts)}`,
label: plugin.name,
continueOnError: false
});
}
}
for (const unusedRule of unusedLintRules) logger.warn({
group: "lint",
label: "lint",
message: `Unknown lint rule "${unusedRule}"`
});
}
//#endregion
//#region ../../node_modules/.pnpm/@humanwhocodes+momoa@3.3.8/node_modules/@humanwhocodes/momoa/dist/momoa.js
/**
* @fileoverview Character codes.
* @author Nicholas C. Zakas
*/
const CHAR_0 = 48;
const CHAR_1 = 49;
const CHAR_9 = 57;
const CHAR_BACKSLASH = 92;
const CHAR_DOLLAR = 36;
const CHAR_DOT = 46;
const CHAR_DOUBLE_QUOTE = 34;
const CHAR_LOWER_A = 97;
const CHAR_LOWER_E = 101;
const CHAR_LOWER_F = 102;
const CHAR_LOWER_N = 110;
const CHAR_LOWER_T = 116;
const CHAR_LOWER_U = 117;
const CHAR_LOWER_X = 120;
const CHAR_LOWER_Z = 122;
const CHAR_MINUS = 45;
const CHAR_NEWLINE = 10;
const CHAR_PLUS = 43;
const CHAR_RETURN = 13;
const CHAR_SINGLE_QUOTE = 39;
const CHAR_SLASH = 47;
const CHAR_SPACE = 32;
const CHAR_TAB = 9;
const CHAR_UNDERSCORE = 95;
const CHAR_UPPER_A = 65;
const CHAR_UPPER_E = 69;
const CHAR_UPPER_F = 70;
const CHAR_UPPER_N = 78;
const CHAR_UPPER_X = 88;
const CHAR_UPPER_Z = 90;
const CHAR_LOWER_B = 98;
const CHAR_LOWER_R = 114;
const CHAR_LOWER_V = 118;
const CHAR_LINE_SEPARATOR = 8232;
const CHAR_PARAGRAPH_SEPARATOR = 8233;
const CHAR_UPPER_I = 73;
const CHAR_STAR = 42;
const CHAR_VTAB = 11;
const CHAR_FORM_FEED = 12;
const CHAR_NBSP = 160;
const CHAR_BOM = 65279;
const CHAR_NON_BREAKING_SPACE = 160;
const CHAR_EN_QUAD = 8192;
const CHAR_EM_QUAD = 8193;
const CHAR_EN_SPACE = 8194;
const CHAR_EM_SPACE = 8195;
const CHAR_THREE_PER_EM_SPACE = 8196;
const CHAR_FOUR_PER_EM_SPACE = 8197;
const CHAR_SIX_PER_EM_SPACE = 8198;
const CHAR_FIGURE_SPACE = 8199;
const CHAR_PUNCTUATION_SPACE = 8200;
const CHAR_THIN_SPACE = 8201;
const CHAR_HAIR_SPACE = 8202;
const CHAR_NARROW_NO_BREAK_SPACE = 8239;
const CHAR_MEDIUM_MATHEMATICAL_SPACE = 8287;
const CHAR_IDEOGRAPHIC_SPACE = 12288;
/**
* @fileoverview JSON syntax helpers
* @author Nicholas C. Zakas
*/
/** @typedef {import("./typedefs.js").TokenType} TokenType */
const LBRACKET = "[";
const RBRACKET = "]";
const LBRACE = "{";
const RBRACE = "}";
const COLON = ":";
const COMMA = ",";
const TRUE = "true";
const FALSE = "false";
const NULL = "null";
const NAN$1 = "NaN";
const INFINITY$1 = "Infinity";
const QUOTE = "\"";
const escapeToChar = new Map([
[CHAR_DOUBLE_QUOTE, QUOTE],
[CHAR_BACKSLASH, "\\"],
[CHAR_SLASH, "/"],
[CHAR_LOWER_B, "\b"],
[CHAR_LOWER_N, "\n"],
[CHAR_LOWER_F, "\f"],
[CHAR_LOWER_R, "\r"],
[CHAR_LOWER_T, " "]
]);
const json5EscapeToChar = new Map([
...escapeToChar,
[CHAR_LOWER_V, "\v"],
[CHAR_0, "\0"]
]);
const charToEscape = new Map([
[QUOTE, QUOTE],
["\\", "\\"],
["/", "/"],
["\b", "b"],
["\n", "n"],
["\f", "f"],
["\r", "r"],
[" ", "t"]
]);
const json5CharToEscape = new Map([
...charToEscape,
["\v", "v"],
["\0", "0"],
["\u2028", "u2028"],
["\u2029", "u2029"]
]);
/** @type {Map<string,TokenType>} */
const knownTokenTypes = new Map([
[LBRACKET, "LBracket"],
[RBRACKET, "RBracket"],
[LBRACE, "LBrace"],
[RBRACE, "RBrace"],
[COLON, "Colon"],
[COMMA, "Comma"],
[TRUE, "Boolean"],
[FALSE, "Boolean"],
[NULL, "Null"]
]);
/** @type {Map<string,TokenType>} */
const knownJSON5TokenTypes = new Map([
...knownTokenTypes,
[NAN$1, "Number"],
[INFINITY$1, "Number"]
]);
const json5LineTerminators = new Set([
CHAR_NEWLINE,
CHAR_RETURN,
CHAR_LINE_SEPARATOR,
CHAR_PARAGRAPH_SEPARATOR
]);
/**
* @fileoverview JSON tokenization/parsing errors
* @author Nicholas C. Zakas
*/
/** @typedef {import("./typedefs.js").Location} Location */
/** @typedef {import("./typedefs.js").Token} Token */
/**
* Base class that attaches location to an error.
*/
var ErrorWithLocation = class extends Error {
/**
* Creates a new instance.
* @param {string} message The error message to report.
* @param {Location} loc The location information for the error.
*/
constructor(message, { line, column, offset }) {
super(`${message} (${line}:${column})`);
/**
* The line on which the error occurred.
* @type {number}
*/
this.line = line;
/**
* The column on which the error occurred.
* @type {number}
*/
this.column = column;
/**
* The index into the string where the error occurred.
* @type {number}
*/
this.offset = offset;
}
};
/**
* Error thrown when an unexpected character is found during tokenizing.
*/
var UnexpectedChar = class extends ErrorWithLocation {
/**
* Creates a new instance.
* @param {number} unexpected The character that was found.
* @param {Location} loc The location information for the found character.
*/
constructor(unexpected, loc) {
super(`Unexpected character '${String.fromCharCode(unexpected)}' found.`, loc);
}
};
/**
* Error thrown when an unexpected identifier is found during tokenizing.
*/
var UnexpectedIdentifier = class extends ErrorWithLocation {
/**
* Creates a new instance.
* @param {string} unexpected The character that was found.
* @param {Location} loc The location information for the found character.
*/
constructor(unexpected, loc) {
super(`Unexpected identifier '${unexpected}' found.`, loc);
}
};
/**
* Error thrown when an unexpected token is found during parsing.
*/
var UnexpectedToken = class extends ErrorWithLocation {
/**
* Creates a new instance.
* @param {Token} token The token that was found.
*/
constructor(token) {
super(`Unexpected token ${token.type} found.`, token.loc.start);
}
};
/**
* Error thrown when the end of input is found where it isn't expected.
*/
var UnexpectedEOF = class extends ErrorWithLocation {
/**
* Creates a new instance.
* @param {Location} loc The location information for the found character.
*/
constructor(loc) {
super("Unexpected end of input found.", loc);
}
};
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]/;
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]/;
/**
* @fileoverview A charactor code reader.
* @author Nicholas C. Zakas
*/
const CHAR_CR = 13;
const CHAR_LF = 10;
/**
* A reader that reads character codes from a string.
*/
var CharCodeReader = class {
/**
* The text to read from.
* @type {string}
*/
#text = "";
/**
* The current line number.
* @type {number}
*/
#line = 1;
/**
* The current column number.
* @type {number}
*/
#column = 0;
/**
* The current offset in the text.
* @type {number}
*/
#offset = -1;
/**
* Whether the last character read was a new line.
* @type {boolean}
*/
#newLine = false;
/**
* The last character code read.
* @type {number}
*/
#last = -1;
/**
* Whether the reader has ended.
* @type {boolean}
*/
#ended = false;
/**
* Creates a new instance.
* @param {string} text The text to read from
*/
constructor(text) {
this.#text = text;
}
/**
* Ends the reader.
* @returns {void}
*/
#end() {
if (this.#ended) return;
this.#column++;
this.#offset++;
this.#last = -1;
this.#ended = true;
}
/**
* Returns the current position of the reader.
* @returns {Location} An object with line, column, and offset properties.
*/
locate() {
return {
line: this.#line,
column: this.#column,
offset: this.#offset
};
}
/**
* Reads the next character code in the text.
* @returns {number} The next character code, or -1 if there are no more characters.
*/
next() {
if (this.#offset >= this.#text.length - 1) {
this.#end();
return -1;
}
this.#offset++;
const charCode = this.#text.charCodeAt(this.#offset);
if (this.#newLine) {
this.#line++;
this.#column = 1;
this.#newLine = false;
} else this.#column++;
if (charCode === CHAR_CR) {
this.#newLine = true;
if (this.peek() === CHAR_LF) this.#offset++;
} else if (charCode === CHAR_LF) this.#newLine = true;
this.#last = charCode;
return charCode;
}
/**
* Peeks at the next character code in the text.
* @returns {number} The next character code, or -1 if there are no more characters.
*/
peek() {
if (this.#offset === this.#text.length - 1) return -1;
return this.#text.charCodeAt(this.#offset + 1);
}
/**
* Determines if the next character code in the text matches a specific character code.
* @param {(number) => boolean} fn A function to call on the next character.
* @returns {boolean} True if the next character code matches, false if not.
*/
match(fn) {
if (fn(this.peek())) {
this.next();
return true;
}
return false;
}
/**
* Returns the last character code read.
* @returns {number} The last character code read.
*/
current() {
return this.#last;
}
};
/**
* @fileoverview JSON tokenizer
* @author Nicholas C. Zakas
*/
/** @typedef {import("./typedefs.js").Range} Range */
/** @typedef {import("./typedefs.js").TokenizeOptions} TokenizeOptions */
const INFINITY = "Infinity";
const NAN = "NaN";
const keywordStarts = new Set([
CHAR_LOWER_T,
CHAR_LOWER_F,
CHAR_LOWER_N
]);
const whitespace = new Set([
CHAR_SPACE,
CHAR_TAB,
CHAR_NEWLINE,
CHAR_RETURN
]);
const json5Whitespace = new Set([
...whitespace,
CHAR_VTAB,
CHAR_FORM_FEED,
CHAR_NBSP,
CHAR_LINE_SEPARATOR,
CHAR_PARAGRAPH_SEPARATOR,
CHAR_BOM,
CHAR_NON_BREAKING_SPACE,
CHAR_EN_QUAD,
CHAR_EM_QUAD,
CHAR_EN_SPACE,
CHAR_EM_SPACE,
CHAR_THREE_PER_EM_SPACE,
CHAR_FOUR_PER_EM_SPACE,
CHAR_SIX_PER_EM_SPACE,
CHAR_FIGURE_SPACE,
CHAR_PUNCTUATION_SPACE,
CHAR_THIN_SPACE,
CHAR_HAIR_SPACE,
CHAR_NARROW_NO_BREAK_SPACE,
CHAR_MEDIUM_MATHEMATICAL_SPACE,
CHAR_IDEOGRAPHIC_SPACE
]);
/** @type {TokenizeOptions} */
const DEFAULT_OPTIONS$1 = {
mode: "json",
ranges: false
};
const jsonKeywords = new Set([
"true",
"false",
"null"
]);
const tt = {
EOF: 0,
Number: 1,
String: 2,
Boolean: 3,
Null: 4,
NaN: 5,
Infinity: 6,
Identifier: 7,
Colon: 20,
LBrace: 21,
RBrace: 22,
LBracket: 23,
RBracket: 24,
Comma: 25,
LineComment: 40,
BlockComment: 41
};
/**
* Determines if a given character is a decimal digit.
* @param {number} c The character to check.
* @returns {boolean} `true` if the character is a digit.
*/
function isDigit(c) {
return c >= CHAR_0 && c <= CHAR_9;
}
/**
* Determines if a given character is a hexadecimal digit.
* @param {number} c The character to check.
* @returns {boolean} `true` if the character is a hexadecimal digit.
*/
function isHexDigit(c) {
return isDigit(c) || c >= CHAR_UPPER_A && c <= CHAR_UPPER_F || c >= CHAR_LOWER_A && c <= CHAR_LOWER_F;
}
/**
* Determines if a given character is a positive digit (1-9).
* @param {number} c The character to check.
* @returns {boolean} `true` if the character is a positive digit.
*/
function isPositiveDigit(c) {
return c >= CHAR_1 && c <= CHAR_9;
}
/**
* Determines if a given character is the start of a keyword.
* @param {number} c The character to check.
* @returns {boolean} `true` if the character is the start of a keyword.
*/
function isKeywordStart(c) {
return keywordStarts.has(c);
}
/**
* Determines if a given character is the start of a number.
* @param {number} c The character to check.
* @returns {boolean} `true` if the character is the start of a number.
*/
function isNumberStart(c) {
return isDigit(c) || c === CHAR_DOT || c === CHAR_MINUS;
}
/**
* Determines if a given character is the start of a JSON5 number.
* @param {number} c The character to check.
* @returns {boolean} `true` if the character is the start of a JSON5 number.
*/
function isJSON5NumberStart(c) {
return isNumberStart(c) || c === CHAR_PLUS;
}
/**
* Determines if a given character is the start of a string.
* @param {number} c The character to check.
* @param {boolean} json5 `true` if JSON5 mode is enabled.
* @returns {boolean} `true` if the character is the start of a string.
*/
function isStringStart(c, json5) {
return c === CHAR_DOUBLE_QUOTE || json5 && c === CHAR_SINGLE_QUOTE;
}
/**
* Tests that a given character is a valid first character of a
* JSON5 identifier
* @param {number} c The character to check.
* @returns {boolean} `true` if the character is a valid first character.
*/
function isJSON5IdentifierStart(c) {
if (c === CHAR_DOLLAR || c === CHAR_UNDERSCORE || c === CHAR_BACKSLASH) return true;
if (c >= CHAR_LOWER_A && c <= CHAR_LOWER_Z || c >= CHAR_UPPER_A && c <= CHAR_UPPER_Z) return true;
if (c === 8204 || c === 8205) return true;
const ct = String.fromCharCode(c);
return ID_Start.test(ct);
}
/**
* Tests that a given character is a valid part of a JSON5 identifier.
* @param {number} c The character to check.
* @returns {boolean} `true` if the character is a valid part of an identifier.
*/
function isJSON5IdentifierPart(c) {
if (isJSON5IdentifierStart(c) || isDigit(c)) return true;
const ct = String.fromCharCode(c);
return ID_Continue.test(ct);
}
var Tokenizer = class {
/**
* Options for the tokenizer.
* @type {TokenizeOptions}
*/
#options;
/**
* The source text to tokenize.
* @type {string}
*/
#text;
/**
* The reader for the source text.
* @type {CharCodeReader}
*/
#reader;
/**
* Indicates if the tokenizer is in JSON5 mode.
* @type {boolean}
*/
#json5;
/**
* Indicates if comments are allowed.
* @type {boolean}
*/
#allowComments;
/**
* Indicates if ranges should be included in the tokens.
* @type {boolean}
*/
#ranges;
/**
* The last token type read.
* @type {Token}
*/
#token;
/**
* Determines if a character is an escaped character.
* @type {(c:number) => boolean}
*/
#isEscapedCharacter;
/**
* Determines if a character is a JSON5 line terminator.
* @type {(c:number) => boolean}
*/
#isJSON5LineTerminator;
/**
* Determines if a character is a JSON5 hex escape.
* @type {(c:number) => boolean}
*/
#isJSON5HexEscape;
/**
* Determines if a character is whitespace.
* @type {(c:number) => boolean}
*/
#isWhitespace;
/**
* Creates a new instance of the tokenizer.
* @param {string} text The source text
* @param {TokenizeOptions} [options] Options for the tokenizer.
*/
constructor(text, options) {
this.#text = text;
this.#options = {
...DEFAULT_OPTIONS$1,
...options
};
this.#reader = new CharCodeReader(text);
this.#json5 = this.#options.mode === "json5";
this.#allowComments = this.#options.mode !== "json";
this.#ranges = this.#options.ranges;
this.#isEscapedCharacter = this.#json5 ? json5EscapeToChar.has.bind(json5EscapeToChar) : escapeToChar.has.bind(escapeToChar);
this.#isJSON5LineTerminator = this.#json5 ? json5LineTerminators.has.bind(json5LineTerminators) : () => false;
this.#isJSON5HexEscape = this.#json5 ? (c) => c === CHAR_LOWER_X : () => false;
this.#isWhitespace = this.#json5 ? json5Whitespace.has.bind(json5Whitespace) : whitespace.has.bind(whitespace);
}
/**
* Convenience function for throwing unexpected character errors.
* @param {number} c The unexpected character.
* @param {Location} [loc] The location of the unexpected character.
* @returns {never}
* @throws {UnexpectedChar} always.
*/
#unexpected(c, loc = this.#reader.locate()) {
throw new UnexpectedChar(c, loc);
}
/**
* Convenience function for throwing unexpected identifier errors.
* @param {string} identifier The unexpected identifier.
* @param {Location} [loc] The location of the unexpected identifier.
* @returns {never}
* @throws {UnexpectedIdentifier} always.
*/
#unexpectedIdentifier(identifier, loc = this.#reader.locate()) {
throw new UnexpectedIdentifier(identifier, loc);
}
/**
* Convenience function for throwing unexpected EOF errors.
* @returns {never}
* @throws {UnexpectedEOF} always.
*/
#unexpectedEOF() {
throw new UnexpectedEOF(this.#reader.locate());
}
/**
* Creates a new token.
* @param {TokenType} tokenType The type of token to create.
* @param {number} length The length of the token.
* @param {Location} startLoc The start location for the token.
* @param {Location} [endLoc] The end location for the token.
* @returns {Token} The token.
*/
#createToken(tokenType, length, startLoc, endLoc) {
const endOffset = startLoc.offset + length;
let range = this.#options.ranges ? { range: [startLoc.offset, endOffset] } : void 0;
return {
type: tokenType,
loc: {
start: startLoc,
end: endLoc || {
line: startLoc.line,
column: startLoc.column + length,
offset: endOffset
}
},
...range
};
}
/**
* Reads in a specific number of hex digits.
* @param {number} count The number of hex digits to read.
* @returns {string} The hex digits read.
*/
#readHexDigits(count) {
let value = "";
let c;
for (let i = 0; i < count; i++) {
c = this.#reader.peek();
if (isHexDigit(c)) {
this.#reader.next();
value += String.fromCharCode(c);
continue;
}
this.#unexpected(c);
}
return value;
}
/**
* Reads in a JSON5 identifier. Also used for JSON but we validate
* the identifier later.
* @param {number} c The first character of the identifier.
* @returns {string} The identifier read.
* @throws {UnexpectedChar} when the identifier cannot be read.
*/
#readIdentifier(c) {
let value = "";
do {
value += String.fromCharCode(c);
if (c === CHAR_BACKSLASH) {
c = this.#reader.next();
if (c !== CHAR_LOWER_U) this.#unexpected(c);
value += String.fromCharCode(c);
const hexDigits = this.#readHexDigits(4);
const charCode = parseInt(hexDigits, 16);
if (value.length === 2 && !isJSON5IdentifierStart(charCode)) {
const loc = this.#reader.locate();
this.#unexpected(CHAR_BACKSLASH, {
line: loc.line,
column: loc.column - 5,
offset: loc.offset - 5
});
} else if (!isJSON5IdentifierPart(charCode)) {
const loc = this.#reader.locate();
this.#unexpected(charCode, {
line: loc.line,
column: loc.column - 5,
offset: loc.offset - 5
});
}
value += hexDigits;
}
c = this.#reader.peek();
if (!isJSON5IdentifierPart(c)) break;
this.#reader.next();
} while (true);
return value;
}
/**
* Reads in a string. Works for both JSON and JSON5.
* @param {number} c The first character of the string (either " or ').
* @returns {number} The length of the string.
* @throws {UnexpectedChar} when the string cannot be read.
* @throws {UnexpectedEOF} when EOF is reached before the string is finalized.
*/
#readString(c) {
const delimiter = c;
let length = 1;
c = this.#reader.peek();
while (c !== -1 && c !== delimiter) {
this.#reader.next();
length++;
if (c === CHAR_BACKSLASH) {
c = this.#reader.peek();
if (this.#isEscapedCharacter(c) || this.#isJSON5LineTerminator(c)) {
this.#reader.next();
length++;
} else if (c === CHAR_LOWER_U) {
this.#reader.next();
length++;
const result = this.#readHexDigits(4);
length += result.length;
} else if (this.#isJSON5HexEscape(c)) {
this.#reader.next();
length++;
const result = this.#readHexDigits(2);
length += result.length;
} else if (this.#json5) {
this.#reader.next();
length++;
} else this.#unexpected(c);
}
c = this.#reader.peek();
}
if (c === -1) {
this.#reader.next();
this.#unexpectedEOF();
}
this.#reader.next();
length++;
return length;
}
/**
* Reads a number. Works for both JSON and JSON5.
* @param {number} c The first character of the number.
* @returns {number} The length of the number.
* @throws {UnexpectedChar} when the number cannot be read.
* @throws {UnexpectedEOF} when EOF is reached before the number is finalized.
*/
#readNumber(c) {
let length = 1;
if (c === CHAR_MINUS || this.#json5 && c === CHAR_PLUS) {
c = this.#reader.peek();
if (this.#json5) {
if (c === CHAR_UPPER_I || c === CHAR_UPPER_N) {
this.#reader.next();
const identifier = this.#readIdentifier(c);
if (identifier !== INFINITY && identifier !== NAN) this.#unexpected(c);
return length + identifier.length;
}
}
if (!isDigit(c)) this.#unexpected(c);
this.#reader.next();
length++;
}
if (c === CHAR_0) {
c = this.#reader.peek();
if (this.#json5 && (c === CHAR_LOWER_X || c === CHAR_UPPER_X)) {
this.#reader.next();
length++;
c = this.#reader.peek();
if (!isHexDigit(c)) {
this.#reader.next();
this.#unexpected(c);
}
do {
this.#reader.next();
length++;
c = this.#reader.peek();
} while (isHexDigit(c));
} else if (isDigit(c)) this.#unexpected(c);
} else if (!this.#json5 || c !== CHAR_DOT) {
if (!isPositiveDigit(c)) this.#unexpected(c);
c = this.#reader.peek();
while (isDigit(c)) {
this.#reader.next();
length++;
c = this.#reader.peek();
}
}
if (c === CHAR_DOT) {
let digitCount = -1;
this.#reader.next();
length++;
digitCount++;
c = this.#reader.peek();
while (isDigit(c)) {
this.#reader.next();
length++;
digitCount++;
c = this.#reader.peek();
}
if (!this.#json5 && digitCount === 0) {
this.#reader.next();
if (c) this.#unexpected(c);
else this.#unexpectedEOF();
}
}
if (c === CHAR_LOWER_E || c === CHAR_UPPER_E) {
this.#reader.next();
length++;
c = this.#reader.peek();
if (c === CHAR_PLUS || c === CHAR_MINUS) {
this.#reader.next();
length++;
c = this.#reader.peek();
}
if (c === -1) {
this.#reader.next();
this.#unexpectedEOF();
}
if (!isDigit(c)) {
this.#reader.next();
this.#unexpected(c);
}
while (isDigit(c)) {
this.#reader.next();
length++;
c = this.#reader.peek();
}
}
return length;
}
/**
* Reads a comment. Works for both JSON and JSON5.
* @param {number} c The first character of the comment.
* @returns {{length: number, multiline: boolean}} The length of the comment, and whether the comment is multi-line.
* @throws {UnexpectedChar} when the comment cannot be read.
* @throws {UnexpectedEOF} when EOF is reached before the comment is finalized.
*/
#readComment(c) {
let length = 1;
c = this.#reader.peek();
if (c === CHAR_SLASH) {
do {
this.#reader.next();
length += 1;
c = this.#reader.peek();
} while (c > -1 && c !== CHAR_RETURN && c !== CHAR_NEWLINE);
return {
length,
multiline: false
};
}
if (c === CHAR_STAR) {
this.#reader.next();
length += 1;
while (c > -1) {
c = this.#reader.peek();
if (c === CHAR_STAR) {
this.#reader.next();
length += 1;
c = this.#reader.peek();
if (c === CHAR_SLASH) {
this.#reader.next();
length += 1;
return {
length,
multiline: true
};
}
} else {
this.#reader.next();
length += 1;
}
}
this.#reader.next();
this.#unexpectedEOF();
}
this.#reader.next();
this.#unexpected(c);
}
/**
* Returns the next token in the source text.
* @returns {number} The code for the next token.
*/
next() {
let c = this.#reader.next();
while (this.#isWhitespace(c)) c = this.#reader.next();
if (c === -1) return tt.EOF;
const start = this.#reader.locate();
const ct = String.fromCharCode(c);
if (this.#json5) if (knownJSON5TokenTypes.has(ct)) this.#token = this.#createToken(knownJSON5TokenTypes.get(ct), 1, start);
else if (isJSON5IdentifierStart(c)) {
const value = this.#readIdentifier(c);
if (knownJSON5TokenTypes.has(value)) this.#token = this.#createToken(knownJSON5TokenTypes.get(value), value.length, start);
else this.#token = this.#createToken("Identifier", value.length, start);
} else if (isJSON5NumberStart(c)) {
const result = this.#readNumber(c);
this.#token = this.#createToken("Number", result, start);
} else if (isStringStart(c, this.#json5)) {
const result = this.#readString(c);
const lastCharLoc = this.#reader.locate();
this.#token = this.#createToken("String", result, start, {
line: lastCharLoc.line,
column: lastCharLoc.column + 1,
offset: lastCharLoc.offset + 1
});
} else if (c === CHAR_SLASH && this.#allowComments) {
const result = this.#readComment(c);
const lastCharLoc = this.#reader.locate();
this.#token = this.#createToken(!result.multiline ? "LineComment" : "BlockComment", result.length, start, {
line: lastCharLoc.line,
column: lastCharLoc.column + 1,
offset: lastCharLoc.offset + 1
});
} else this.#unexpected(c);
else if (knownTokenTypes.has(ct)) this.#token = this.#createToken(knownTokenTypes.get(ct), 1, start);
else if (isKeywordStart(c)) {
const value = this.#readIdentifier(c);
if (!jsonKeywords.has(value)) this.#unexpectedIdentifier(value, start);
this.#token = this.#createToken(knownTokenTypes.get(value), value.length, start);
} else if (isNumberStart(c)) {
const result = this.#readNumber(c);
this.#token = this.#createToken("Number", result, start);
} else if (isStringStart(c, this.#json5)) {
const result = this.#readString(c);
this.#token = this.#createToken("String", result, start);
} else if (c === CHAR_SLASH && this.#allowComments) {
const result = this.#readComment(c);
const lastCharLoc = this.#reader.locate();
this.#token = this.#createToken(!result.multiline ? "LineComment" : "BlockComment", result.length, start, {
line: lastCharLoc.line,
column: lastCharLoc.column + 1,
offset: lastCharLoc.offset + 1
});
} else this.#unexpected(c);
return tt[this.#token.type];
}
/**
* Returns the current token in the source text.
* @returns {Token} The current token.
*/
get token() {
return this.#token;
}
};
/**
* @fileoverview JSON AST types
* @author Nicholas C. Zakas
*/
/** @typedef {import("./typedefs.js").NodeParts} NodeParts */
/** @typedef {import("./typedefs.js").DocumentNode} DocumentNode */
/** @typedef {import("./typedefs.js").StringNode} StringNode */
/** @typedef {import("./typedefs.js").NumberNode} NumberNode */
/** @typedef {import("./typedefs.js").BooleanNode} BooleanNode */
/** @typedef {import("./typedefs.js").MemberNode} MemberNode */
/** @typedef {import("./typedefs.js").ObjectNode} ObjectNode */
/** @typedef {import("./typedefs.js").ElementNode} ElementNode */
/** @typedef {import("./typedefs.js").ArrayNode} ArrayNode */
/** @typedef {import("./typedefs.js").NullNode} NullNode */
/** @typedef {import("./typedefs.js").ValueNode} ValueNode */
/** @typedef {import("./typedefs.js").IdentifierNode} IdentifierNode */
/** @typedef {import("./typedefs.js").NaNNode} NaNNode */
/** @typedef {import("./typedefs.js").InfinityNode} InfinityNode */
/** @typedef {import("./typedefs.js").Sign} Sign */
const types = {
document(body, parts = {}) {
return {
type: "Document",
body,
loc: parts.loc,
...parts
};
},
string(value, parts = {}) {
return {
type: "String",
value,
loc: parts.loc,
...parts
};
},
number(value, parts = {}) {
return {
type: "Number",
value,
loc: parts.loc,
...parts
};
},
boolean(value, parts = {}) {
return {
type: "Boolean",
value,
loc: parts.loc,
...parts
};
},
null(parts = {}) {
return {
type: "Null",
loc: parts.loc,
...parts
};
},
array(elements, parts = {}) {
return {
type: "Array",
elements,
loc: parts.loc,
...parts
};
},
element(value, parts = {}) {
return {
type: "Element",
value,
loc: parts.loc,
...parts
};
},
object(members, parts = {}) {
return {
type: "Object",
members,
loc: parts.loc,
...parts
};
},
member(name, value, parts = {}) {
return {
type: "Member",
name,
value,
loc: parts.loc,
...parts
};
},
identifier(name, parts = {}) {
return {
type: "Identifier",
name,
loc: parts.loc,
...parts
};
},
nan(sign = "", parts = {}) {
return {
type: "NaN",
sign,
loc: parts.loc,
...parts
};
},
infinity(sign = "", parts = {}) {
return {
type: "Infinity",
sign,
loc: parts.loc,
...parts
};
}
};
/**
* @fileoverview JSON parser
* @author Nicholas C. Zakas
*/
/** @typedef {import("./typedefs.js").Node} Node */
/** @typedef {import("./typedefs.js").Mode} Mode */
/** @typedef {import("./typedefs.js").ParseOptions} ParseOptions */
/** @type {ParseOptions} */
const DEFAULT_OPTIONS = {
mode: "json",
ranges: false,
tokens: false,
allowTrailingCommas: false
};
const UNICODE_SEQUENCE = /\\u[\da-fA-F]{4}/gu;
/**
* Normalizes a JSON5 identifier by converting Unicode escape sequences into
* their corresponding characters.
* @param {string} identifier The identifier to normalize.
* @returns {string} The normalized identifier.
*/
function normalizeIdentifier(identifier) {
return identifier.replace(UNICODE_SEQUENCE, (unicodeEscape) => {
return String.fromCharCode(parseInt(unicodeEscape.slice(2), 16));
});
}
/**
* Converts a JSON-encoded string into a JavaScript string, interpreting each
* escape sequence.
* @param {string} value The text for the token.
* @param {Token} token The string token to convert into a JavaScript string.
* @param {boolean} json5 `true` if parsing JSON5, `false` otherwise.
* @returns {string} A JavaScript string.
*/
function getStringValue(value, token, json5 = false) {
let result = "";
let escapeIndex = value.indexOf("\\");
let lastIndex = 0;
while (escapeIndex >= 0) {
result += value.slice(lastIndex, escapeIndex);
const escapeChar = value.charAt(escapeIndex + 1);
const escapeCharCode = escapeChar.charCodeAt(0);
if (json5 && json5EscapeToChar.has(escapeCharCode)) {
result += json5EscapeToChar.get(escapeCharCode);
lastIndex = escapeIndex + 2;
} else if (escapeToChar.has(escapeCharCode)) {
result += escapeToChar.get(escapeCharCode);
lastIndex = escapeIndex + 2;
} else if (escapeChar === "u") {
const hexCode = value.slice(escapeIndex + 2, escapeIndex + 6);
if (hexCode.length < 4 || /[^0-9a-f]/i.test(hexCode)) throw new ErrorWithLocation(`Invalid unicode escape \\u${hexCode}.`, {
line: token.loc.start.line,
column: token.loc.start.column + escapeIndex,
offset: token.loc.start.offset + escapeIndex
});
result += String.fromCharCode(parseInt(hexCode, 16));
lastIndex = escapeIndex + 6;
} else if (json5 && escapeChar === "x") {
const hexCode = value.slice(escapeIndex + 2, escapeIndex + 4);
if (hexCode.length < 2 || /[^0-9a-f]/i.test(hexCode)) throw new ErrorWithLocation(`Invalid hex escape \\x${hexCode}.`, {
line: token.loc.start.line,
column: token.loc.start.column + escapeIndex,
offset: token.loc.start.offset + escapeIndex
});
result += String.fromCharCode(parseInt(hexCode, 16));
lastIndex = escapeIndex + 4;
} else if (json5 && json5LineTerminators.has(escapeCharCode)) {
lastIndex = escapeIndex + 2;
if (escapeChar === "\r" && value.charAt(lastIndex) === "\n") lastIndex++;
} else if (json5) {
result += escapeChar;
lastIndex = escapeIndex + 2;
} else throw new ErrorWithLocation(`Invalid escape \\${escapeChar}.`, {
line: token.loc.start.line,
column: token.loc.start.column + escapeIndex,
offset: token.loc.start.offset + escapeIndex
});
escapeIndex = value.indexOf("\\", lastIndex);
}
result += value.slice(lastIndex);
return result;
}
/**
* Gets the JavaScript value represented by a JSON token.
* @param {string} value The text value of the token.
* @param {Token} token The JSON token to get a value for.
* @param {boolean} json5 `true` if parsing JSON5, `false` otherwise.
* @returns {string|boolean|number} A number, string, or boolean.
* @throws {TypeError} If an unknown token type is found.
*/
function getLiteralValue(value, token, json5 = false) {
switch (token.type) {
case "Boolean": return value === "true";
case "Number":
if (json5) {
if (value.charCodeAt(0) === 45) return -Number(value.slice(1));
if (value.charCodeAt(0) === 43) return Number(value.slice(1));
}
return Number(value);
case "String": return getStringValue(value.slice(1, -1), token, json5);
default: throw new TypeError(`Unknown token type "${token.type}.`);
}
}
/**
*
* @param {string} text The text to parse.
* @param {ParseOptions} [options] The options object.
* @returns {DocumentNode} The AST representing the parsed JSON.
* @throws {Error} When there is a parsing error.
*/
function parse$1(text, options) {
options = Object.freeze({
...DEFAULT_OPTIONS,
...options
});
const tokens = [];
const tokenizer = new Tokenizer(text, {
mode: options.mode,
ranges: options.ranges
});
const json5 = options.mode === "json5";
const allowTrailingCommas = options.allowTrailingCommas || json5;
/**
* Returns the next token knowing there are no comments.
* @returns {number} The next token type or 0 if no next token.
*/
function nextNoComments() {
const nextType = tokenizer.next();
if (nextType && options.tokens) tokens.push(tokenizer.token);
return nextType;
}
/**
* Returns the next token knowing there are comments to skip.
* @returns {number} The next token type or 0 if no next token.
*/
function nextSkipComments() {
const nextType = tokenizer.next();
if (nextType && options.tokens) tokens.push(tokenizer.token);
if (nextType >= tt.LineComment) return nextSkipComments();
return nextType;
}
const next = options.mode === "json" ? nextNoComments : nextSkipComments;
/**
* Asserts a token has the given type.
* @param {number} token The token to check.
* @param {number} type The token type.
* @throws {UnexpectedToken} If the token type isn't expected.
* @returns {void}
*/
function assertTokenType(token, type) {
if (token !== type) throw new UnexpectedToken(tokenizer.token);
}
/**
* Asserts a token has one of the given types.
* @param {number} token The token to check.
* @param {number[]} types The token types.
* @returns {void}
* @throws {UnexpectedToken} If the token type isn't expected.
*/
function assertTokenTypes(token, types$1) {
if (!types$1.includes(token)) throw new UnexpectedToken(tokenizer.token);
}
/**
* Creates a range only if ranges are specified.
* @param {Location} start The start offset for the range.
* @param {Location} end The end offset for the range.
* @returns {{range:[number,number]}|undefined} An object with a
*/
function createRange(start, end) {
return options.ranges ? { range: [start.offset, end.offset] } : void 0;
}
/**
* Creates a node for a string, boolean, or number.
* @param {number} tokenType The token representing the literal.
* @returns {StringNode|NumberNode|BooleanNode} The node representing
* the value.
*/
function createLiteralNode(tokenType) {
const token = tokenizer.token;
const range = createRange(token.loc.start, token.loc.end);
const value = getLiteralValue(text.slice(token.loc.start.offset, token.loc.end.offset), token, json5);
const loc = {
start: { ...token.loc.start },
end: { ...token.loc.end }
};
const parts = {
loc,
...range
};
switch (tokenType) {
case tt.String: return types.string(value, parts);
case tt.Number: return types.number(value, parts);
case tt.Boolean: return types.boolean(value, parts);
default: throw new TypeError(`Unknown token type ${token.type}.`);
}
}
/**
* Creates a node for a JSON5 identifier.
* @param {Token} token The token representing the identifer.
* @returns {NaNNode|InfinityNode|IdentifierNode} The node representing
* the value.
*/
function createJSON5IdentifierNode(token) {
const range = createRange(token.loc.start, token.loc.end);
const identifier = text.slice(token.loc.start.offset, token.loc.end.offset);
const loc = {
start: { ...token.loc.start },
end: { ...token.loc.end }
};
const parts = {
loc,
...range
};
if (token.type !== "Identifier") {
let sign = "";
if (identifier[0] === "+" || identifier[0] === "-") sign = identifier[0];
return types[identifier.includes("NaN") ? "nan" : "infinity"](sign, parts);
}
return types.identifier(normalizeIdentifier(identifier), parts);
}
/**
* Creates a node for a null.
* @param {Token} token The token representing null.
* @returns {NullNode} The node representing null.
*/
function createNullNode(token) {
const range = createRange(token.loc.start, token.loc.end);
return types.null({
loc: {
start: { ...token.loc.start },
end: { ...token.loc.end }
},
...range
});
}
/**
* Parses a property in an object.
* @param {number} tokenType The token representing the property.
* @returns {MemberNode} The node representing the property.
* @throws {UnexpectedToken} When an unexpected token is found.
* @throws {UnexpectedEOF} When the end of the file is reached.
*/
function parseProperty(tokenType) {
if (json5) assertTokenTypes(tokenType, [
tt.String,
tt.Identifier,
tt.Number
]);
else assertTokenType(tokenType, tt.String);
const token = tokenizer.token;
if (json5 && tokenType === tt.Number && /[+\-0-9]/.test(text[token.loc.start.offset])) throw new UnexpectedToken(token);
let key = tokenType === tt.String ? createLiteralNode(tokenType) : createJSON5IdentifierNode(token);
if (json5 && (key.type === "NaN" || key.type === "Infinity")) {
if (key.sign !== "") throw new UnexpectedToken(tokenizer.token);
key = types.identifier(key.type, {
loc: key.loc,
...createRange(key.loc.start, key.loc.end)
});
}
tokenType = next();
assertTokenType(tokenType, tt.Colon);
const value = parseValue();
const range = createRange(key.loc.start, value.loc.end);
return types.member(key, value, {
loc: {
start: { ...key.loc.start },
end: { ...value.loc.end }
},
...range
});
}
/**
* Parses an object literal.
* @param {number} firstTokenType The first token type in the object.
* @returns {ObjectNode} The object node.
* @throws {UnexpectedEOF} When the end of the file is reached.
* @throws {UnexpectedToken} When an unexpected token is found.
*/
function parseObject(firstTokenType) {
assertTokenType(firstTokenType, tt.LBrace);
const firstToken = tokenizer.token;
const members = [];
let tokenType = next();
if (tokenType !== tt.RBrace) do {
members.push(parseProperty(tokenType));
tokenType = next();
if (!tokenType) throw new UnexpectedEOF(members[members.length - 1].loc.end);
if (tokenType === tt.Comma) {
tokenType = next();
if (allowTrailingCommas && tokenType === tt.RBrace) break;
} else break;
} while (tokenType);
assertTokenType(tokenType, tt.RBrace);
const lastToken = tokenizer.token;
const range = createRange(firstToken.loc.start, lastToken.loc.end);
return types.object(members, {
loc: {
start: { ...firstToken.loc.start },
end: { ...lastToken.loc.end }
},
...range
});
}
/**
* Parses an array literal.
* @param {number} firstTokenType The first token in the array.
* @returns {ArrayNode} The array node.
* @throws {UnexpectedToken} When an unexpected token is found.
* @throws {UnexpectedEOF} When the end of the file is reached.
*/
function parseArray(firstTokenType) {
assertTokenType(firstTokenType, tt.LBracket);
const firstToken = tokenizer.token;
const elements = [];
let tokenType = next();
if (tokenType !== tt.RBracket) do {
const value = parseValue(tokenType);
elements.push(types.element(value, { loc: value.loc }));
tokenType = next();
if (tokenType === tt.Comma) {
tokenType = next();
if (allowTrailingCommas && tokenType === tt.RBracket) break;
} else break;
} while (tokenType);
assertTokenType(tokenType, tt.RBracket);
const lastToken = tokenizer.token;
const range = createRange(firstToken.loc.start, lastToken.loc.end);
return types.array(elements, {
loc: {
start: { ...firstToken.loc.start },
end: { ...lastToken.loc.end }
},
...range
});
}
/**
* Parses a JSON value.
* @param {number} [tokenType] The token type to parse.
* @returns {ValueNode|IdentifierNode} The node representing the value.
*/
function parseValue(tokenType) {
tokenType = tokenType ?? next();
const token = tokenizer.token;
switch (tokenType) {
case tt.String:
case tt.Boolean: return createLiteralNode(tokenType);
case tt.Number:
if (json5) {
let tokenText = text.slice(token.loc.start.offset, token.loc.end.offset);
if (tokenText[0] === "+" || tokenText[0] === "-") tokenText = tokenText.slice(1);
if (tokenText === "NaN" || tokenText === "Infinity") return createJSON5IdentifierNode(token);
}
return createLiteralNode(tokenType);
case tt.Null: return createNullNode(token);
case tt.LBrace: return parseObject(tokenType);
case tt.LBracket: return parseArray(tokenType);
default: throw new UnexpectedToken(token);
}
}
const docBody = parseValue();
const unexpectedToken = next();
if (unexpectedToken) throw new UnexpectedToken(tokenizer.token);
const docParts = { loc: {
start: {
line: 1,
column: 1,
offset: 0
},
end: { ...docBody.loc.end }
} };
if (options.tokens) docParts.tokens = tokens;
if (options.ranges) docParts.range = [docParts.loc.start.offset, docParts.loc.end.offset];
return types.document(docBody, docParts);
}
/**
* @fileoverview Evaluator for Momoa AST.
* @author Nicholas C. Zakas
*/
/** @typedef {import("./typedefs.js").AnyNode} AnyNode */
/** @typedef {import("./typedefs.js").JSONValue} JSONValue */
/**
* Evaluates a Momoa AST node into a JavaScript value.
* @param {AnyNode} node The node to interpet.
* @returns {JSONValue} The JavaScript value for the node.
*/
function evaluate(node) {
switch (node.type) {
case "String": return node.value;
case "Number": return node.value;
case "Boolean": return node.value;
case "Null": return null;
case "NaN": return NaN;
case "Infinity": return node.sign === "-" ? -Infinity : Infinity;
case "Identifier": return node.name;
case "Array": return node.elements.map((element) => evaluate(element.value));
case "Object": {
/** @type {{[property: string]: JSONValue}} */
const object = {};
node.members.forEach((member) => {
object[evaluate(member.name)] = evaluate(member.value);
});
return object;
}
case "Document": return evaluate(node.body);
case "Element": throw new Error("Cannot evaluate array element outside of an array.");
case "Member": throw new Error("Cannot evaluate object member outside of an object.");
default: throw new Error(`Unknown node type ${node.type}.`);
}
}
/**
* @fileoverview Printer for Momoa AST.
* @author Nicholas C. Zakas
*/
/**
* Prints the string representation of a Boolean node.
* @param {BooleanNode} node The node to print.
* @returns {string} The boolean value.
*/
function printBoolean(node) {
return node.value ? "true" : "false";
}
/**
* Prints the string representation of a null node.
* @returns {string} The string "null".
*/
function printNull() {
return "null";
}
/**
* Prints the string representation of a number node.
* @param {NumberNode} node The node to print.
* @returns {string} The number value.
*/
function printNumber(node) {
return node.value.toString();
}
/**
* Prints the string representation of a NaN node.
* @returns {string} The string "NaN".
*/
function printNaN() {
return "NaN";
}
/**
* Prints the string representation of an Infinity node.
* @param {InfinityNode} node The node to print.
* @returns {string} The string "Infinity" or "-Infinity".
*/
function printInfinity(node) {
return node.sign + "Infinity";
}
/**
* Prints the string representation of a string node.
* @param {StringNode} node The node to print.
* @returns {string} The string value.
*/
function printString(node) {
let result = "\"";
for (const c of node.value) {
const newChar = json5CharToEscape.get(c);
if (newChar) {
result += "\\" + newChar;
continue;
}
if (c === "\"") {
result += "\\\"";
continue;
}
if (c < " " || c === "") {
const hex = c.codePointAt(0).toString(16).toUpperCase();
result += `\\u${"0000".substring(hex.length)}${hex}`;
continue;
}
result += c;
}
return result + "\"";
}
/**
* Prints the string representation of an identifier node.
* @param {IdentifierNode} node The node to print.
* @returns {string} The identifier name.
*/
function printIdentifier(node) {
return node.name;
}
/**
* Prints the string representation of an array node.
* @param {ArrayNode} node The node to print.
* @param {string} indent The string to use for indentation.
* @param {number} indentLevel The current level of indentation.
* @returns {string} The array value.
*/
function printArray(node, indent, indentLevel) {
const newLine = indent ? "\n" : "";
const indentString = indent.repeat(indentLevel);
const elementIndentString = indent.repeat(indentLevel + 1);
return `[${newLine}${node.elements.map((element) => `${elementIndentString}${printValue(element.value, indent, indentLevel + 1)}`).join(`,${newLine}`)}${newLine}${indentString}]`;
}
/**
* Prints the string representation of a member node.
* @param {MemberNode} node The node to print.
* @param {string} indent The string to use for indentation.
* @param {number} indentLevel The current level of indentation.
* @returns {string} The member value.
*/
function printMember(node, indent, indentLevel) {
const space = indent ? " " : "";
return `${printValue(node.name, indent, indentLevel)}:${space}${printValue(node.value, indent, indentLevel + 1)}`;
}
/**
* Prints the string representation of an object node.
* @param {ObjectNode} node The node to print.
* @param {string} indent The string to use for indentation.
* @param {number} indentLevel The current level of indentation.
* @returns {string} The object value.
*/
function printObject(node, indent, indentLevel) {
const newLine = indent ? "\n" : "";
const indentString = indent.repeat(indentLevel);
const memberIndentString = indent.repeat(indentLevel + 1);
return `{${newLine}${node.members.map((member) => `${memberIndentString}${printMember(member, indent, indentLevel)}`).join(`,${newLine}`)}${newLine}${indentString}}`;
}
/**
* Prints the string representation of a node.
* @param {AnyNode} node The node to print.
* @param {string} indentString The string to use for indentation.
* @param {number} indentLevel The current level of indentation.
* @returns {string} The string representation of the node.
* @throws {TypeError} If the node type is unknown.
*/
function printValue(node, indentString, indentLevel) {
switch (node.type) {
case "String": return printString(node);
case "Number": return printNumber(node);
case "Boolean": return printBoolean(node);
case "Null": return printNull();
case "NaN": return printNaN();
case "Infinity": return printInfinity(node);
case "Identifier": return printIdentifier(node);
case "Array": return printArray(node, indentString, indentLevel);
case "Object": return printObject(node, indentString, indentLevel);
case "Document": return printValue(node.body, indentString, indentLevel);
default: throw new TypeError(`Unknown node type: ${node.type}`);
}
}
/**
* Converts a Momoa AST back into a JSON string.
* @param {AnyNode} node The node to print.
* @param {Object} options Options for the print.
* @param {number} [options.indent=0] The number of spaces to indent each line. If
* greater than 0, then newlines and indents will be added to output.
* @returns {string} The JSON representation of the AST.
*/
function print(node, { indent = 0 } = {}) {
const indentLevel = 0;
const indentString = " ".repeat(indent);
return printValue(node, indentString, indentLevel);
}
//#endregion
//#region src/parse/json.ts
const CHILD_KEYS = {
Document: ["body"],
Object: ["members"],
Member: ["name", "value"],
Element: ["value"],
Array: ["elements"],
String: [],
Number: [],
Boolean: [],
Null: [],
Identifier: [],
NaN: [],
Infinity: []
};
/** Determines if a given value is an AST node. */
function isNode(value) {
return !!value && typeof value === "object" && "type" in value && typeof value.type === "string";
}
/** Get ObjectNode members as object */
function getObjMembers(node) {
const members = {};
if (node.type !== "Object") return members;
for (let i = 0; i < node.members.length; i++) {
const m = node.members[i];
if (m.name.type !== "String") continue;
members[m.name.value] = {
...m.value,
index: i
};
}
return members;
}
/** Inject members to ObjectNode */
function injectObjMembers(node, members = []) {
if (node.type !== "Object") return;
node.members.push(...members);
}
/** Replace an ObjectNode’s contents outright with another */
function replaceObjMembers(a, b) {
a.members = b.type === "Document" && b.body?.members || b.members;
}
/**
* Variation of Momoa’s traverse(), which keeps track of global path.
* Allows mutation of AST (along with any consequences)
*/
function traverse(root, visitor) {
/**
* Recursively visits a node.
* @param {AnyNode} node The node to visit.
* @param {AnyNode} [parent] The parent of the node to visit.
* @return {void}
*/
function visitNode(node, parent, path = []) {
const nextPath = [...path];
if (node.type === "Member") {
const { name } = node;
nextPath.push("value" in name ? name.value : String(name));
}
visitor.enter?.(node, parent, nextPath);
const childNode = CHILD_KEYS[node.type];
for (const key of childNode ?? []) {
const value = node[key];
if (!value) continue;
if (Array.isArray(value)) for (let i = 0; i < value.length; i++) visitNode(value[i], node, key === "elements" ? [...nextPath, String(i)] : nextPath);
else if (isNode(value)) visitNode(value, node, nextPath);
}
visitor.exit?.(node, parent, nextPath);
}
visitNode(root, void 0, []);
}
/** Determine if an input is likely a JSON string */
function maybeRawJSON(input) {
return typeof input === "string" && input.trim().startsWith("{");
}
/** Find Momoa node by traversing paths */
function findNode(node, path) {
if (!path.length) return;
let nextNode;
switch (node.type) {
case "Document": return findNode(node.body, path);
case "Object": {
const [member, ...rest] = path;
nextNode = node.members.find((m) => m.name.type === "String" && m.name.value === member)?.value;
if (nextNode && rest.length) return findNode(nextNode, path.slice(1));
break;
}
case "Array": {
const [_index, ...rest] = path;
const index = Number.parseInt(_index, 10);
nextNode = node.elements[index]?.value;
if (nextNode && rest.length) return findNode(nextNode, path.slice(1));
break;
}
}
return nextNode;
}
function toMomoa(input, { continueOnError, filename, logger, yamlToMomoa }) {
let src = "";
if (typeof input === "string") src = input;
let document = {};
if (typeof input === "string" && !maybeRawJSON(input)) if (yamlToMomoa) try {
document = yamlToMomoa(input);
} catch (err) {
logger.error({
group: "parser",
label: "json",
message: String(err),
filename,
src: input,
continueOnError
});
}
else logger.error({
group: "parser",
label: "yaml",
message: `Install \`yaml-to-momoa\` package to parse YAML, and pass in as option, e.g.:
import { parse } from '@terrazzo/parser';
import yamlToMomoa from 'yaml-to-momoa';
parse(yamlString, { yamlToMomoa });`,
continueOnError: false
});
else document = parseJSON(input);
if (!src) src = print(document, { indent: 2 });
return {
src,
document
};
}
/** Momoa, just with default options pre-set */
function parseJSON(input, options) {
return parse$1(typeof input === "string" ? input : JSON.stringify(input, void 0, 2), {
mode: "jsonc",
ranges: true,
tokens: true,
...options
});
}
//#endregion
//#region src/parse/alias.ts
/**
* Resolve aliases and update the token nodes.
*
* Data structures are in an awkward in-between phase, where they have
* placeholders for data but we still need to resolve everything. As such,
* TypeScript will raise errors expecting the final shape.
*
* This is also a bit tricky because different token types alias slightly
* differently. For example, color tokens and other “primitive” tokens behave
* as-expected. But composite tokens like Typography, Gradient, Border, etc. can
* either fully- or partially-alias their values. Then we add modes to the mix,
* and we have to do the work all over again for each mode declared.
*
* All that to say, there are a generous amount of TypeScript overrides here rather
* than try to codify indeterminate shapes.
*/
function applyAliases(token, options) {
token.mode["."] ??= {};
token.mode["."].$value = token.$value;
token.mode["."].originalValue ??= token.originalValue.$value;
token.mode["."].source ??= token.source;
if (typeof token.$value === "string" && isAlias(token.$value)) {
const { aliasChain, resolvedToken } = resolveAlias(token.$value, {
...options,
token
});
token.aliasOf = resolvedToken.id;
token.aliasChain = aliasChain;
token.$value = resolvedToken.$value;
}
for (const mode of Object.keys(token.mode)) {
const modeValue = token.mode[mode].$value;
if (typeof modeValue === "string" && isAlias(modeValue)) {
const expectedType = [token.$type];
const { aliasChain, resolvedToken } = resolveAlias(modeValue, {
...options,
token,
expectedType,
node: token.mode[mode].source?.node || options.node
});
token.mode[mode].aliasOf = resolvedToken.id;
token.mode[mode].aliasChain = aliasChain;
token.mode[mode].$value = resolvedToken.$value;
continue;
}
if (typeof token.$value === "object" && typeof token.mode[mode].$value === "object" && !Array.isArray(token.$value)) {
for (const [k, v] of Object.entries(token.$value)) if (!(k in token.mode[mode].$value)) token.mode[mode].$value[k] = v;
}
const node = getObjMembers(options.node).$value || options.node;
switch (token.$type) {
case "border": {
applyBorderPartialAlias(token, mode, {
...options,
node
});
break;
}
case "gradient": {
applyGradientPartialAlias(token, mode, {
...options,
node
});
break;
}
case "shadow": {
applyShadowPartialAlias(token, mode, {
...options,
node
});
break;
}
case "strokeStyle": {
applyStrokeStylePartialAlias(token, mode, {
...options,
node
});
break;
}
case "transition": {
applyTransitionPartialAlias(token, mode, {
...options,
node
});
break;
}
case "typography": {
applyTypographyPartialAlias(token, mode, {
...options,
node
});
break;
}
}
}
}
const LIST_FORMAT = new Intl.ListFormat("en-us", { type: "disjunction" });
/**
* Resolve alias. Also add info on root node if it’s the root token (has .id)
*/
function resolveAlias(alias, options) {
const baseMessage = {
group: "parser",
label: "alias",
node: options?.node,
filename: options.filename,
src: options.src
};
const { logger, token, tokensSet } = options;
const shallowAliasID = parseAlias(alias);
const { token: resolvedToken, chain } = _resolveAliasInner(shallowAliasID, options);
if (!tokensSet[token.id].$type) tokensSet[token.id].$type = resolvedToken.$type;
const expectedType = [...options.expectedType ?? []];
if (token.$type && !expectedType?.length) expectedType.push(token.$type);
if (expectedType?.length && !expectedType.includes(resolvedToken.$type)) logger.error({
...baseMessage,
message: `Invalid alias: expected $type: ${LIST_FORMAT.format(expectedType)}, received $type: ${resolvedToken.$type}.`,
node: options.node?.type === "Object" && getObjMembers(options.node).$value || baseMessage.node
});
if (chain?.length && resolvedToken) {
let needsSort = false;
for (const id of chain) {
if (id !== resolvedToken.id && !resolvedToken.aliasedBy?.includes(id)) {
resolvedToken.aliasedBy ??= [];
resolvedToken.aliasedBy.push(id);
needsSort = true;
}
if (token && !resolvedToken.aliasedBy?.includes(token.id)) {
resolvedToken.aliasedBy ??= [];
resolvedToken.aliasedBy.push(token.id);
needsSort = true;
}
}
if (needsSort) resolvedToken.aliasedBy.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
}
return {
resolvedToken,
aliasChain: chain
};
}
function _resolveAliasInner(alias, { scanned = [],...options }) {
const { logger, filename, src, node, tokensSet } = options;
const baseMessage = {
group: "parser",
label: "alias",
filename,
src,
node
};
const id = parseAlias(alias);
if (!tokensSet[id]) logger.error({
...baseMessage,
message: `Alias {${alias}} not found.`
});
if (scanned.includes(id)) logger.error({
...baseMessage,
message: `Circular alias detected from ${alias}.`
});
const token = tokensSet[id];
scanned.push(id);
if (typeof token.originalValue.$value !== "string" || !isAlias(token.originalValue.$value)) return {
token,
chain: scanned
};
return _resolveAliasInner(token.originalValue.$value, {
...options,
scanned
});
}
function applyBorderPartialAlias(token, mode, options) {
for (const [k, v] of Object.entries(token.mode[mode].$value)) if (typeof v === "string" && isAlias(v)) {
token.mode[mode].partialAliasOf ??= {};
const node = getObjMembers(options.node)[k] || options.node;
const { resolvedToken } = resolveAlias(v, {
...options,
token,
expectedType: {
color: ["color"],
width: ["dimension"],
style: ["strokeStyle"]
}[k],
node
});
token.mode[mode].partialAliasOf[k] = parseAlias(v);
if (mode === ".") {
token.partialAliasOf ??= {};
token.partialAliasOf[k] = parseAlias(v);
}
token.mode[mode].$value[k] = resolvedToken.$value;
}
}
function applyGradientPartialAlias(token, mode, options) {
for (let i = 0; i < token.mode[mode].$value.length; i++) {
const step = token.mode[mode].$value[i];
for (const [k, v] of Object.entries(step)) if (typeof v === "string" && isAlias(v)) {
token.mode[mode].partialAliasOf ??= [];
token.mode[mode].partialAliasOf[i] ??= {};
const expectedType = {
color: ["color"],
position: ["number"]
}[k];
let node = options.node?.elements?.[i]?.value || options.node;
if (node.type === "Object") node = getObjMembers(node)[k] || node;
const { resolvedToken } = resolveAlias(v, {
...options,
token,
expectedType,
node
});
token.mode[mode].partialAliasOf[i][k] = parseAlias(v);
if (mode === ".") {
token.partialAliasOf ??= [];
token.partialAliasOf[i] ??= {};
token.partialAliasOf[i][k] = parseAlias(v);
}
step[k] = resolvedToken.$value;
}
}
}
function applyShadowPartialAlias(token, mode, options) {
if (!Array.isArray(token.mode[mode].$value)) token.mode[mode].$value = [token.mode[mode].$value];
for (let i = 0; i < token.mode[mode].$value.length; i++) {
const layer = token.mode[mode].$value[i];
for (const [k, v] of Object.entries(layer)) if (typeof v === "string" && isAlias(v)) {
token.mode[mode].partialAliasOf ??= [];
token.mode[mode].partialAliasOf[i] ??= {};
const expectedType = {
offsetX: ["dimension"],
offsetY: ["dimension"],
blur: ["dimension"],
spread: ["dimension"],
color: ["color"],
inset: ["boolean"]
}[k];
let node = options.node?.elements?.[i] || options.node;
if (node.type === "Object") node = getObjMembers(node)[k] || node;
const { resolvedToken } = resolveAlias(v, {
...options,
token,
expectedType,
node
});
token.mode[mode].partialAliasOf[i][k] = parseAlias(v);
if (mode === ".") {
token.partialAliasOf ??= [];
token.partialAliasOf[i] ??= {};
token.partialAliasOf[i][k] = parseAlias(v);
}
layer[k] = resolvedToken.$value;
}
}
}
function applyStrokeStylePartialAlias(token, mode, options) {
if (typeof token.mode[mode].$value !== "object" || !("dashArray" in token.mode[mode].$value)) return;
for (let i = 0; i < token.mode[mode].$value.dashArray.length; i++) {
const dash = token.mode[mode].$value.dashArray[i];
if (typeof dash === "string" && isAlias(dash)) {
let node = getObjMembers(options.node).dashArray || options.node;
if (node.type === "Array") node = node?.elements?.[i]?.value || node;
const { resolvedToken } = resolveAlias(dash, {
...options,
token,
expectedType: ["dimension"],
node
});
token.mode[mode].$value.dashArray[i] = resolvedToken.$value;
}
}
}
function applyTransitionPartialAlias(token, mode, options) {
for (const [k, v] of Object.entries(token.mode[mode].$value)) if (typeof v === "string" && isAlias(v)) {
token.mode[mode].partialAliasOf ??= {};
const expectedType = {
duration: ["duration"],
delay: ["duration"],
timingFunction: ["cubicBezier"]
}[k];
const node = getObjMembers(options.node)[k] || options.node;
const { resolvedToken } = resolveAlias(v, {
...options,
token,
expectedType,
node
});
token.mode[mode].partialAliasOf[k] = parseAlias(v);
if (mode === ".") {
token.partialAliasOf ??= {};
token.partialAliasOf[k] = parseAlias(v);
}
token.mode[mode].$value[k] = resolvedToken.$value;
}
}
function applyTypographyPartialAlias(token, mode, options) {
for (const [k, v] of Object.entries(token.mode[mode].$value)) if (typeof v === "string" && isAlias(v)) {
token.partialAliasOf ??= {};
token.mode[mode].partialAliasOf ??= {};
const expectedType = {
fontFamily: ["fontFamily"],
fontSize: ["dimension"],
fontWeight: ["fontWeight"],
letterSpacing: ["dimension"],
lineHeight: ["dimension", "number"]
}[k] || ["string"];
const node = getObjMembers(options.node)[k] || options.node;
const { resolvedToken } = resolveAlias(v, {
...options,
token,
expectedType,
node
});
token.mode[mode].partialAliasOf[k] = parseAlias(v);
if (mode === ".") token.partialAliasOf[k] = parseAlias(v);
token.mode[mode].$value[k] = resolvedToken.$value;
}
}
//#endregion
//#region src/parse/normalize.ts
const FONT_WEIGHT_MAP = {
thin: 100,
hairline: 100,
"extra-light": 200,
"ultra-light": 200,
light: 300,
normal: 400,
regular: 400,
book: 400,
medium: 500,
"semi-bold": 600,
"demi-bold": 600,
bold: 700,
"extra-bold": 800,
"ultra-bold": 800,
black: 900,
heavy: 900,
"extra-black": 950,
"ultra-black": 950
};
const NUMBER_WITH_UNIT_RE = /(-?\d*\.?\d+)(.*)/;
/** Fill in defaults, and return predictable shapes for tokens */
function normalizeValue(token) {
if (typeof token.$value === "string" && isAlias(token.$value)) return token.$value;
switch (token.$type) {
case "boolean": return !!token.$value;
case "border": {
if (typeof token.$value === "string") return token.$value;
return {
color: normalizeValue({
$type: "color",
$value: token.$value.color ?? "#000000"
}),
style: normalizeValue({
$type: "strokeStyle",
$value: token.$value.style ?? "solid"
}),
width: normalizeValue({
$type: "dimension",
$value: token.$value.width
})
};
}
case "color": {
if (typeof token.$value === "string") return parseColor(token.$value);
const newValue = {
colorSpace: token.$value.colorSpace,
components: token.$value.components ?? token.$value.channels,
alpha: token.$value.alpha ?? 1
};
if ("hex" in token.$value) newValue.hex = token.$value.hex;
return newValue;
}
case "cubicBezier": {
if (typeof token.$value === "string") return token.$value;
return token.$value.map((value) => typeof value === "number" ? normalizeValue({
$type: "number",
$value: value
}) : value);
}
case "dimension": {
if (token.$value === 0) return {
value: 0,
unit: "px"
};
if (typeof token.$value === "string") {
const match = token.$value.match(NUMBER_WITH_UNIT_RE);
return {
value: Number.parseFloat(match?.[1] || token.$value),
unit: match?.[2] || "px"
};
}
return token.$value;
}
case "duration": {
if (token.$value === 0) return {
value: 0,
unit: "ms"
};
if (typeof token.$value === "string") {
const match = token.$value.match(NUMBER_WITH_UNIT_RE);
return {
value: Number.parseFloat(match?.[1] || token.$value),
unit: match?.[2] || "ms"
};
}
return token.$value;
}
case "fontFamily": return Array.isArray(token.$value) ? token.$value : [token.$value];
case "fontWeight": {
if (typeof token.$value === "string" && FONT_WEIGHT_MAP[token.$value]) return FONT_WEIGHT_MAP[token.$value];
return Math.min(999, Math.max(1, typeof token.$value === "string" ? Number.parseInt(token.$value) : token.$value));
}
case "gradient": {
if (typeof token.$value === "string") return token.$value;
const output = [];
for (let i = 0; i < token.$value.length; i++) {
const stop = structuredClone(token.$value[i]);
stop.color = normalizeValue({
$type: "color",
$value: stop.color
});
if (stop.position === void 0) stop.position = i / (token.$value.length - 1);
output.push(stop);
}
return output;
}
case "number": return typeof token.$value === "number" ? token.$value : Number.parseFloat(token.$value);
case "shadow": {
if (typeof token.$value === "string") return token.$value;
return (Array.isArray(token.$value) ? token.$value : [token.$value]).map((layer) => ({
color: normalizeValue({
$type: "color",
$value: layer.color
}),
offsetX: normalizeValue({
$type: "dimension",
$value: layer.offsetX ?? {
value: 0,
unit: "px"
}
}),
offsetY: normalizeValue({
$type: "dimension",
$value: layer.offsetY ?? {
value: 0,
unit: "px"
}
}),
blur: normalizeValue({
$type: "dimension",
$value: layer.blur ?? {
value: 0,
unit: "px"
}
}),
spread: normalizeValue({
$type: "dimension",
$value: layer.spread ?? {
value: 0,
unit: "px"
}
}),
inset: layer.inset === true
}));
}
case "strokeStyle": return token.$value;
case "string": return String(token.$value);
case "transition": {
if (typeof token.$value === "string") return token.$value;
return {
duration: normalizeValue({
$type: "duration",
$value: token.$value.duration ?? 0
}),
delay: normalizeValue({
$type: "duration",
$value: token.$value.delay ?? 0
}),
timingFunction: normalizeValue({
$type: "cubicBezier",
$value: token.$value.timingFunction
})
};
}
case "typography": {
if (typeof token.$value === "string") return token.$value;
const output = {};
for (const [k, $value] of Object.entries(token.$value)) switch (k) {
case "fontFamily": {
output[k] = normalizeValue({
$type: "fontFamily",
$value
});
break;
}
case "fontSize":
case "letterSpacing": {
output[k] = normalizeValue({
$type: "dimension",
$value
});
break;
}
case "lineHeight": {
output[k] = normalizeValue({
$type: typeof token.$value === "number" ? "number" : "dimension",
$value
});
break;
}
default: {
output[k] = $value;
break;
}
}
return output;
}
default: return token.$value;
}
}
//#endregion
//#region src/parse/validate.ts
const listFormat = new Intl.ListFormat("en-us", { type: "disjunction" });
const VALID_COLORSPACES = new Set([
"adobe-rgb",
"display-p3",
"hsl",
"hwb",
"lab",
"lch",
"oklab",
"oklch",
"prophoto",
"rec2020",
"srgb",
"srgb-linear",
"xyz",
"xyz-d50",
"xyz-d65"
]);
const FONT_WEIGHT_VALUES = new Set([
"thin",
"hairline",
"extra-light",
"ultra-light",
"light",
"normal",
"regular",
"book",
"medium",
"semi-bold",
"demi-bold",
"bold",
"extra-bold",
"ultra-bold",
"black",
"heavy",
"extra-black",
"ultra-black"
]);
const STROKE_STYLE_VALUES = new Set([
"solid",
"dashed",
"dotted",
"double",
"groove",
"ridge",
"outset",
"inset"
]);
const STROKE_STYLE_LINE_CAP_VALUES = new Set([
"round",
"butt",
"square"
]);
/** Distinct from isAlias() in that this accepts malformed aliases */
function isMaybeAlias(node) {
if (node?.type === "String") return node.value.startsWith("{");
return false;
}
/** Assert object members match given types */
function validateMembersAs($value, properties, node, { filename, src, logger }) {
const members = getObjMembers($value);
for (const [name, value] of Object.entries(properties)) {
const { validator, required } = value;
if (!members[name]) {
if (required) logger.error({
group: "parser",
label: "validate",
message: `Missing required property "${name}"`,
filename,
node: $value,
src
});
continue;
}
const memberValue = members[name];
if (isMaybeAlias(memberValue)) validateAliasSyntax(memberValue, node, {
filename,
src,
logger
});
else validator(memberValue, node, {
filename,
src,
logger
});
}
}
/** Verify an Alias $value is formatted correctly */
function validateAliasSyntax($value, _node, { filename, src, logger }) {
if ($value.type !== "String" || !isAlias($value.value)) logger.error({
group: "parser",
label: "validate",
message: `Invalid alias: ${print($value)}`,
filename,
node: $value,
src
});
}
/** Verify a Border token is valid */
function validateBorder($value, node, { filename, src, logger }) {
if ($value.type !== "Object") logger.error({
group: "parser",
label: "validate",
message: `Expected object, received ${$value.type}`,
filename,
node: $value,
src
});
else validateMembersAs($value, {
color: {
validator: validateColor,
required: true
},
style: {
validator: validateStrokeStyle,
required: true
},
width: {
validator: validateDimension,
required: true
}
}, node, {
filename,
src,
logger
});
}
/** Verify a Color token is valid */
function validateColor($value, node, { filename, src, logger }) {
const baseMessage = {
group: "parser",
label: "validate",
filename,
node: $value,
src
};
if ($value.type === "String") {
if ($value.value === "") logger.error({
...baseMessage,
message: "Expected color, received empty string"
});
} else if ($value.type === "Object") {
const channelMemberI = $value.members.findIndex((m) => m.name.type === "String" && m.name.value === "channels");
if (channelMemberI !== -1) {
logger.warn({
...baseMessage,
message: "\"channels\" is deprecated; rename \"channels\" to \"components\""
});
$value.members[channelMemberI].name.value = "components";
}
validateMembersAs($value, {
colorSpace: {
validator: (v) => {
if (v.type !== "String") logger.error({
...baseMessage,
message: `Expected string, received ${print(v)}`,
node: v
});
if (!VALID_COLORSPACES.has(v.value)) logger.error({
...baseMessage,
message: `Unsupported colorspace ${print(v)}`,
node: v
});
},
required: true
},
components: {
validator: (v) => {
if (v.type !== "Array") logger.error({
...baseMessage,
message: `Expected array, received ${print(v)}`,
node: v
});
else {
if (v.elements?.length !== 3) logger.error({
...baseMessage,
message: `Expected 3 components, received ${v.elements?.length ?? 0}`,
node: v
});
for (const element of v.elements) if (element.value.type !== "Number") logger.error({
...baseMessage,
message: `Expected number, received ${print(element.value)}`,
node: element
});
}
},
required: true
},
hex: { validator: (v) => {
if (v.type !== "String" || v.value.length === 6 || v.value.length === 8 || !/^#[a-f0-9]{3,8}$/i.test(v.value)) logger.error({
...baseMessage,
message: `Invalid hex color ${print(v)}`,
node: v
});
} },
alpha: { validator: validateNumber }
}, node, {
filename,
src,
logger
});
} else logger.error({
...baseMessage,
message: `Expected object, received ${$value.type}`,
node: $value
});
}
/** Verify a Cubic Bézier token is valid */
function validateCubicBezier($value, _node, { filename, src, logger }) {
const baseMessage = {
group: "parser",
label: "validate",
filename,
node: $value,
src
};
if ($value.type !== "Array") logger.error({
...baseMessage,
message: `Expected array of numbers, received ${print($value)}`
});
else if (!$value.elements.every((e) => e.value.type === "Number")) logger.error({
...baseMessage,
message: "Expected an array of 4 numbers, received some non-numbers"
});
else if ($value.elements.length !== 4) logger.error({
...baseMessage,
message: `Expected an array of 4 numbers, received ${$value.elements.length}`
});
}
/** Verify a Dimension token is valid */
function validateDimension($value, _node, { filename, src, logger }) {
if ($value.type === "Number" && $value.value === 0) return;
const baseMessage = {
group: "parser",
label: "validate",
filename,
node: $value,
src
};
if ($value.type === "Object") {
const { value: value$1, unit: unit$1 } = getObjMembers($value);
if (!value$1) logger.error({
...baseMessage,
message: "Missing required property \"value\"."
});
if (!unit$1) logger.error({
...baseMessage,
message: "Missing required property \"unit\"."
});
if (value$1.type !== "Number") logger.error({
...baseMessage,
message: `Expected number, received ${value$1.type}`,
node: value$1
});
if (![
"px",
"em",
"rem"
].includes(unit$1.value)) logger.error({
...baseMessage,
message: `Expected unit "px", "em", or "rem", received ${print(unit$1)}`,
node: unit$1
});
return;
}
if ($value.type !== "String") logger.error({
...baseMessage,
message: `Expected string, received ${$value.type}`
});
const value = $value.value.match(/^-?[0-9.]+/)?.[0];
const unit = $value.value.replace(value, "");
if ($value.value === "") logger.error({
...baseMessage,
message: "Expected dimension, received empty string"
});
else if (![
"px",
"em",
"rem"
].includes(unit)) logger.error({
...baseMessage,
message: `Expected unit "px", "em", or "rem", received ${JSON.stringify(unit || $value.value)}`
});
else if (!Number.isFinite(Number.parseFloat(value))) logger.error({
...baseMessage,
message: `Expected dimension with units, received ${print($value)}`
});
}
/** Verify a Duration token is valid */
function validateDuration($value, _node, { filename, src, logger }) {
if ($value.type === "Number" && $value.value === 0) return;
const baseMessage = {
group: "parser",
label: "validate",
filename,
node: $value,
src
};
if ($value.type === "Object") {
const { value: value$1, unit: unit$1 } = getObjMembers($value);
if (!value$1) logger.error({
...baseMessage,
message: "Missing required property \"value\"."
});
if (!unit$1) logger.error({
...baseMessage,
message: "Missing required property \"unit\"."
});
if (value$1?.type !== "Number") logger.error({
...baseMessage,
message: `Expected number, received ${value$1?.type}`,
node: value$1
});
if (!["ms", "s"].includes(unit$1.value)) logger.error({
...baseMessage,
message: `Expected unit "ms" or "s", received ${print(unit$1)}`,
node: unit$1
});
return;
}
if ($value.type !== "String") logger.error({
...baseMessage,
message: `Expected string, received ${$value.type}`
});
const value = $value.value.match(/^-?[0-9.]+/)?.[0];
const unit = $value.value.replace(value, "");
if ($value.value === "") logger.error({
...baseMessage,
message: "Expected duration, received empty string"
});
else if (!["ms", "s"].includes(unit)) logger.error({
...baseMessage,
message: `Expected unit "ms" or "s", received ${JSON.stringify(unit || $value.value)}`
});
else if (!Number.isFinite(Number.parseFloat(value))) logger.error({
...baseMessage,
message: `Expected duration with units, received ${print($value)}`
});
}
/** Verify a Font Family token is valid */
function validateFontFamily($value, _node, { filename, src, logger }) {
const baseMessage = {
group: "parser",
label: "validate",
filename,
node: $value,
src
};
if ($value.type !== "String" && $value.type !== "Array") logger.error({
...baseMessage,
message: `Expected string or array of strings, received ${$value.type}`
});
if ($value.type === "String" && $value.value === "") logger.error({
...baseMessage,
message: "Expected font family name, received empty string"
});
if ($value.type === "Array" && !$value.elements.every((e) => e.value.type === "String" && e.value.value !== "")) logger.error({
...baseMessage,
message: "Expected an array of strings, received some non-strings or empty strings"
});
}
/** Verify a Font Weight token is valid */
function validateFontWeight($value, _node, { filename, src, logger }) {
const baseMessage = {
group: "parser",
label: "validate",
filename,
node: $value,
src
};
if ($value.type !== "String" && $value.type !== "Number") logger.error({
...baseMessage,
message: `Expected a font weight name or number 0–1000, received ${$value.type}`
});
if ($value.type === "String" && !FONT_WEIGHT_VALUES.has($value.value)) logger.error({
...baseMessage,
message: `Unknown font weight ${print($value)}. Expected one of: ${listFormat.format([...FONT_WEIGHT_VALUES])}.`
});
if ($value.type === "Number" && ($value.value < 0 || $value.value > 1e3)) logger.error({
...baseMessage,
message: `Expected number 0–1000, received ${print($value)}`
});
}
/** Verify a Gradient token is valid */
function validateGradient($value, _node, { filename, src, logger }) {
const baseMessage = {
group: "parser",
label: "validate",
filename,
node: $value,
src
};
if ($value.type !== "Array") logger.error({
...baseMessage,
message: `Expected array of gradient stops, received ${$value.type}`
});
else for (let i = 0; i < $value.elements.length; i++) {
const element = $value.elements[i];
if (element.value.type !== "Object") {
logger.error({
...baseMessage,
message: `Stop #${i + 1}: Expected gradient stop, received ${element.value.type}`,
node: element
});
break;
}
validateMembersAs(element.value, {
color: {
validator: validateColor,
required: true
},
position: {
validator: validateNumber,
required: true
}
}, element, {
filename,
src,
logger
});
}
}
/** Verify a Number token is valid */
function validateNumber($value, _node, { filename, src, logger }) {
if ($value.type !== "Number") logger.error({
group: "parser",
label: "validate",
message: `Expected number, received ${$value.type}`,
filename,
node: $value,
src
});
}
/** Verify a Boolean token is valid */
function validateBoolean($value, _node, { filename, src, logger }) {
if ($value.type !== "Boolean") logger.error({
group: "parser",
label: "validate",
message: `Expected boolean, received ${$value.type}`,
filename,
node: $value,
src
});
}
/** Verify a Shadow token’s value is valid */
function validateShadowLayer($value, node, { filename, src, logger }) {
if ($value.type !== "Object") logger.error({
group: "parser",
label: "validate",
message: `Expected Object, received ${$value.type}`,
filename,
node: $value,
src
});
else validateMembersAs($value, {
color: {
validator: validateColor,
required: true
},
offsetX: {
validator: validateDimension,
required: true
},
offsetY: {
validator: validateDimension,
required: true
},
blur: { validator: validateDimension },
spread: { validator: validateDimension },
inset: { validator: validateBoolean }
}, node, {
filename,
src,
logger
});
}
/** Verify a Stroke Style token is valid. */
function validateStrokeStyle($value, node, { filename, src, logger }) {
const baseMessage = {
group: "parser",
label: "validate",
filename,
node: $value,
src
};
if ($value.type === "String") {
if (!STROKE_STYLE_VALUES.has($value.value)) logger.error({
...baseMessage,
message: `Unknown stroke style ${print($value)}. Expected one of: ${listFormat.format([...STROKE_STYLE_VALUES])}.`
});
} else if ($value.type === "Object") {
const strokeMembers = getObjMembers($value);
for (const property of ["dashArray", "lineCap"]) if (!strokeMembers[property]) logger.error({
...baseMessage,
message: `Missing required property "${property}"`
});
const { lineCap, dashArray } = strokeMembers;
if (lineCap?.type !== "String" || !STROKE_STYLE_LINE_CAP_VALUES.has(lineCap.value)) logger.error({
...baseMessage,
message: `Unknown lineCap value ${print(lineCap)}. Expected one of: ${listFormat.format([...STROKE_STYLE_LINE_CAP_VALUES])}.`,
node
});
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, {
logger,
src
});
else validateDimension(element.value, node, {
logger,
src
});
else if (element.value.type === "Object") validateDimension(element.value, node, {
logger,
src
});
else logger.error({
...baseMessage,
message: `Expected array of dimensions, received ${element.value.type}.`,
node: element
});
else logger.error({
...baseMessage,
message: `Expected array of strings, received ${dashArray.type}`
});
} else logger.error({
...baseMessage,
message: `Expected string or object, received ${$value.type}`
});
}
/** Verify a Transition token is valid */
function validateTransition($value, node, { filename, src, logger }) {
if ($value.type !== "Object") logger.error({
group: "parser",
label: "validate",
message: `Expected object, received ${$value.type}`,
filename,
node: $value,
src
});
else validateMembersAs($value, {
duration: {
validator: validateDuration,
required: true
},
delay: {
validator: validateDuration,
required: false
},
timingFunction: {
validator: validateCubicBezier,
required: true
}
}, node, {
filename,
src,
logger
});
}
/**
* Validate a MemberNode (the entire token object, plus its key in the parent
* object) to see if it’s a valid DTCG token or not. Keeping the parent key
* really helps in debug messages.
*/
function validateTokenMemberNode(node, { filename, src, logger }) {
const baseMessage = {
group: "parser",
label: "validate",
filename,
node,
src
};
if (node.type !== "Member" && node.type !== "Object") logger.error({
...baseMessage,
message: `Expected Object, received ${JSON.stringify(node.type)}`
});
const rootMembers = node.value.type === "Object" ? getObjMembers(node.value) : {};
const $value = rootMembers.$value;
const $type = rootMembers.$type;
if (!$value) logger.error({
...baseMessage,
message: "Token missing $value"
});
if (isMaybeAlias($value)) {
validateAliasSyntax($value, node, {
logger,
src
});
return;
}
if (!$type) logger.error({
...baseMessage,
message: "Token missing $type"
});
switch ($type.value) {
case "color": {
validateColor($value, node, {
logger,
src
});
break;
}
case "cubicBezier": {
validateCubicBezier($value, node, {
logger,
src
});
break;
}
case "dimension": {
validateDimension($value, node, {
logger,
src
});
break;
}
case "duration": {
validateDuration($value, node, {
logger,
src
});
break;
}
case "fontFamily": {
validateFontFamily($value, node, {
logger,
src
});
break;
}
case "fontWeight": {
validateFontWeight($value, node, {
logger,
src
});
break;
}
case "number": {
validateNumber($value, node, {
logger,
src
});
break;
}
case "shadow": {
if ($value.type === "Object") validateShadowLayer($value, node, {
logger,
src
});
else if ($value.type === "Array") for (const element of $value.elements) validateShadowLayer(element.value, $value, {
logger,
src
});
else logger.error({
...baseMessage,
message: `Expected shadow object or array of shadow objects, received ${$value.type}`,
node: $value
});
break;
}
case "boolean": {
if ($value.type !== "Boolean") logger.error({
...baseMessage,
message: `Expected boolean, received ${$value.type}`,
node: $value
});
break;
}
case "link": {
if ($value.type !== "String") logger.error({
...baseMessage,
message: `Expected string, received ${$value.type}`,
node: $value
});
else if ($value.value === "") logger.error({
...baseMessage,
message: "Expected URL, received empty string",
node: $value
});
break;
}
case "string": {
if ($value.type !== "String") logger.error({
...baseMessage,
message: `Expected string, received ${$value.type}`,
node: $value
});
break;
}
case "border": {
validateBorder($value, node, {
filename,
src,
logger
});
break;
}
case "gradient": {
validateGradient($value, node, {
filename,
src,
logger
});
break;
}
case "strokeStyle": {
validateStrokeStyle($value, node, {
filename,
src,
logger
});
break;
}
case "transition": {
validateTransition($value, node, {
filename,
src,
logger
});
break;
}
case "typography": {
if ($value.type !== "Object") {
logger.error({
...baseMessage,
message: `Expected object, received ${$value.type}`,
node: $value
});
break;
}
if ($value.members.length === 0) logger.error({
...baseMessage,
message: "Empty typography token. Must contain at least 1 property.",
node: $value
});
validateMembersAs($value, {
fontFamily: { validator: validateFontFamily },
fontWeight: { validator: validateFontWeight }
}, node, {
filename,
src,
logger
});
break;
}
default: break;
}
}
/** Return any token node with its inherited $type */
function getInheritedType(node, { subpath, $typeInheritance }) {
if (node.value.type !== "Object") return;
const $type = node.value.members.find((m) => m.name.type === "String" && m.name.value === "$type");
const $value = node.value.members.find((m) => m.name.type === "String" && m.name.value === "$value");
if ($typeInheritance && $type && !$value) $typeInheritance[subpath.join(".") || "."] = $type;
const id = subpath.join(".");
let parent$type;
let longestPath = "";
for (const [k, v] of Object.entries($typeInheritance ?? {})) if (k === "." || id.startsWith(k)) {
if (k.length > longestPath.length) {
parent$type = v;
longestPath = k;
}
}
return parent$type;
}
/**
* Validate does a little more than validate; it also converts to TokenNormalized
* and sets up the basic data structure. But aliases are unresolved, and we need
* a 2nd normalization pass afterward.
*/
function validateTokenNode(node, { config, filename, logger, parent, inheritedTypeNode, src, subpath }) {
if (subpath.includes("$value") || node.value.type !== "Object") return;
const members = getObjMembers(node.value);
if (!members.$value || subpath.includes("$extensions") || subpath.includes("$deps")) return;
const id = subpath.join(".");
if (!subpath.includes(".$value") && members.value) logger.warn({
group: "parser",
label: "validate",
message: `Group ${id} has "value". Did you mean "$value"?`,
filename,
node,
src
});
const nodeWithType = structuredClone(node);
let $type = members.$type?.type === "String" && members.$type.value || void 0;
if (inheritedTypeNode && !members.$type) {
injectObjMembers(nodeWithType.value, [inheritedTypeNode]);
$type = inheritedTypeNode.value.value;
}
validateTokenMemberNode(nodeWithType, {
filename,
src,
logger
});
const $deprecated = members.$deprecated && evaluate(members.$deprecated);
if (config.ignore.deprecated && $deprecated || config.ignore.tokens && wcmatch(config.ignore.tokens)(id)) return;
const group = {
id: splitID(id).group,
tokens: []
};
if (inheritedTypeNode && inheritedTypeNode.value.type === "String") group.$type = inheritedTypeNode.value.value;
const groupMembers = getObjMembers(parent);
if (groupMembers.$description) group.$description = evaluate(groupMembers.$description);
if (groupMembers.$extensions) group.$extensions = evaluate(groupMembers.$extensions);
const $value = evaluate(members.$value);
const token = {
$type,
$value,
id,
mode: {},
originalValue: evaluate(node.value),
group,
source: {
loc: filename?.href,
node: nodeWithType.value
}
};
if (members.$description?.type === "String" && members.$description.value) token.$description = members.$description.value;
const extensions = members.$extensions ? getObjMembers(members.$extensions) : void 0;
const modeValues = extensions?.mode ? getObjMembers(extensions.mode) : {};
for (const mode of [".", ...Object.keys(modeValues)]) {
const modeValue = mode === "." ? token.$value : evaluate(modeValues[mode]);
token.mode[mode] = {
$value: modeValue,
originalValue: modeValue,
source: {
loc: filename?.href,
node: modeValues[mode]
}
};
}
return token;
}
//#endregion
//#region src/parse/index.ts
/** Parse */
async function parse(_input, { logger = new Logger(), skipLint = false, config = {}, continueOnError = false, yamlToMomoa, transform, _sources = {} } = {}) {
const input = Array.isArray(_input) ? _input : [_input];
let tokensSet = {};
if (!Array.isArray(input)) logger.error({
group: "parser",
label: "init",
message: "Input must be an array of input objects."
});
await Promise.all(input.map(async (src, i) => {
if (!src || typeof src !== "object") logger.error({
group: "parser",
label: "init",
message: `Input (${i}) must be an object.`
});
if (!src.src || typeof src.src !== "string" && typeof src.src !== "object") logger.error({
message: `Input (${i}) missing "src" with a JSON/YAML string, or JSON object.`,
group: "parser",
label: "init"
});
if (src.filename) {
if (!(src.filename instanceof URL)) logger.error({
message: `Input (${i}) "filename" must be a URL (remote or file URL).`,
group: "parser",
label: "init"
});
if (_sources[src.filename.href]) return;
}
const result = await parseSingle(src.src, {
filename: src.filename,
logger,
config,
skipLint,
continueOnError,
yamlToMomoa,
transform
});
tokensSet = Object.assign(tokensSet, result.tokens);
if (src.filename) _sources[src.filename.href] = {
filename: src.filename,
src: result.src,
document: result.document
};
}));
const totalStart = performance.now();
const aliasesStart = performance.now();
let aliasCount = 0;
for (const [id, token] of Object.entries(tokensSet)) {
applyAliases(token, {
tokensSet,
filename: _sources[token.source.loc]?.filename,
src: _sources[token.source.loc]?.src,
node: getObjMembers(token.source.node).$value || token.source.node,
logger
});
aliasCount++;
const { group: parentGroup } = splitID(id);
if (parentGroup) for (const siblingID of Object.keys(tokensSet)) {
const { group: siblingGroup } = splitID(siblingID);
if (siblingGroup?.startsWith(parentGroup)) token.group.tokens.push(siblingID);
}
}
logger.debug({
message: `Resolved ${aliasCount} aliases`,
group: "parser",
label: "alias",
timing: performance.now() - aliasesStart
});
logger.debug({
message: "Finish all parser tasks",
group: "parser",
label: "core",
timing: performance.now() - totalStart
});
if (continueOnError) {
const { errorCount } = logger.stats();
if (errorCount > 0) logger.error({
group: "parser",
message: `Parser encountered ${errorCount} ${pluralize(errorCount, "error", "errors")}. Exiting.`
});
}
return {
tokens: tokensSet,
sources: Object.values(_sources)
};
}
/** Parse a single input */
async function parseSingle(input, { filename, logger, config, skipLint, continueOnError = false, transform, yamlToMomoa }) {
const startParsing = performance.now();
let { src, document } = toMomoa(input, {
filename,
logger,
continueOnError,
yamlToMomoa
});
logger.debug({
group: "parser",
label: "json",
message: "Finish JSON parsing",
timing: performance.now() - startParsing
});
const tokensSet = {};
if (transform?.root) {
const json = typeof input === "string" ? JSON.parse(input) : input;
const result = transform?.root(json, ".", document);
if (result) {
const reRunResult = toMomoa(result, {
filename,
logger,
continueOnError
});
src = reRunResult.src;
document = reRunResult.document;
}
}
let tokenCount = 0;
const startValidate = performance.now();
const $typeInheritance = {};
traverse(document, { enter(node, parent, subpath) {
if (node.type === "Document" && node.body.type === "Object" && node.body.members) {
const { members: rootMembers } = node.body;
const root$type = rootMembers.find((m) => m.name.type === "String" && m.name.value === "$type");
const root$value = rootMembers.find((m) => m.name.type === "String" && m.name.value === "$value");
if (root$type && !root$value) $typeInheritance["."] = root$type;
}
if (node.type === "Object" && subpath.length && !node.members.some((m) => m.name.type === "String" && m.name.value === "$value") && !subpath.includes("$value") && !subpath.includes("$extensions")) {
if (transform?.group) {
const newJSON = transform?.group(evaluate(node), subpath.join("."), node);
if (newJSON) replaceObjMembers(node, parseJSON(newJSON));
}
}
if (node.type === "Member") {
const inheritedTypeNode = getInheritedType(node, {
subpath,
$typeInheritance
});
if (node.value.type === "Object") {
const $type = node.value.members.find((m) => m.name.type === "String" && m.name.value === "$type") || inheritedTypeNode;
const $value = node.value.members.find((m) => m.name.type === "String" && m.name.value === "$value");
if ($value && $type?.value.type === "String" && transform?.[$type.value.value]) {
const result = transform[$type.value.value]?.(evaluate(node.value), subpath.join("."), node);
if (result) node.value = parseJSON(result).body;
}
const token = validateTokenNode(node, {
filename,
src,
config,
logger,
parent,
subpath,
transform,
inheritedTypeNode
});
if (token) {
tokensSet[token.id] = token;
tokenCount++;
}
}
}
} });
logger.debug({
message: `Validated ${tokenCount} tokens`,
group: "parser",
label: "validate",
timing: performance.now() - startValidate
});
const normalizeStart = performance.now();
for (const [id, token] of Object.entries(tokensSet)) {
try {
tokensSet[id].$value = normalizeValue(token);
} catch (err) {
let { node } = token.source;
const members = getObjMembers(node);
if (members.$value) node = members.$value;
logger.error({
group: "parser",
label: "normalize",
message: err.message,
filename,
src,
node,
continueOnError
});
}
for (const [mode, modeValue] of Object.entries(token.mode)) {
if (mode === ".") continue;
try {
tokensSet[id].mode[mode].$value = normalizeValue({
$type: token.$type,
...modeValue
});
} catch (err) {
let { node } = token.source;
const members = getObjMembers(node);
if (members.$value) node = members.$value;
logger.error({
group: "parser",
label: "normalize",
message: err.message,
filename,
src,
node: modeValue.source.node,
continueOnError
});
}
}
}
logger.debug({
message: `Normalized ${tokenCount} tokens`,
group: "parser",
label: "normalize",
timing: performance.now() - normalizeStart
});
if (!skipLint && config?.plugins?.length) {
const lintStart = performance.now();
await lintRunner({
tokens: tokensSet,
src,
config,
logger
});
logger.debug({
message: `Linted ${tokenCount} tokens`,
group: "parser",
label: "lint",
timing: performance.now() - lintStart
});
} else logger.debug({
message: "Linting skipped",
group: "parser",
label: "lint"
});
return {
tokens: tokensSet,
document,
src
};
}
//#endregion
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 };
//# sourceMappingURL=index.js.map