@terrazzo/parser
Version:
Parser/validator for the Design Tokens Community Group (DTCG) standard.
1,505 lines • 137 kB
JavaScript
import { BORDER_REQUIRED_PROPERTIES, COLOR_SPACE, CachedWildcardMatcher, FONT_WEIGHTS, GRADIENT_REQUIRED_STOP_PROPERTIES, SHADOW_REQUIRED_PROPERTIES, STROKE_STYLE_LINE_CAP_VALUES, STROKE_STYLE_OBJECT_REQUIRED_PROPERTIES, STROKE_STYLE_STRING_VALUES, TRANSITION_REQUIRED_PROPERTIES, isAlias, parseAlias, parseColor, pluralize, tokenToColor } from "@terrazzo/token-tools";
import * as momoa from "@humanwhocodes/momoa";
import pc from "picocolors";
import { merge } from "merge-anything";
import { contrastWCAG21, inGamut } from "colorjs.io/fn";
import { camelCase, kebabCase, pascalCase, snakeCase } from "scule";
import { bundle, encodeFragment, findNode, getObjMember, getObjMembers, isPure$ref, maybeRawJSON, mergeObjects, parseRef, replaceNode, traverse } from "@terrazzo/json-schema-tools";
//#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) markerLines[lineNumber] = [startColumn, source[lineNumber - 1].length - startColumn + 1];
else if (i === lineDiff) markerLines[lineNumber] = [0, endColumn];
else markerLines[lineNumber] = [0, source[lineNumber - i].length];
}
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 = {}) {
if (typeof rawLines !== "string") throw new Error(`Expected string, got ${rawLines}`);
const { start, end, markerLines } = getMarkerLines(loc, rawLines.split(NEWLINE), 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 gutter = ` ${` ${number}`.slice(-numberMaxWidth)} |`;
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 GROUP_COLOR = {
config: pc.cyan,
import: pc.green,
lint: pc.yellowBright,
parser: pc.magenta,
plugin: pc.greenBright,
resolver: pc.magentaBright,
server: pc.gray
};
const MESSAGE_COLOR = {
error: pc.red,
warn: pc.yellow,
info: (msg) => msg,
debug: pc.gray
};
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) {
const groupColor = GROUP_COLOR[entry.group];
const messageColor = MESSAGE_COLOR[severity];
let message = entry.message;
message = `${groupColor(`${entry.group}${entry.label ? `:${entry.label}` : ""}:`)} ${messageColor(message)}`;
if (typeof entry.timing === "number") message = `${message} ${formatTiming(entry.timing)}`;
if (entry.node) {
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 ?? momoa.print(entry.node, { indent: 2 }), { start }, { highlightCode: false });
message = `${message}\n\n${loc}${codeFrame}`;
}
return message;
}
const debugMatch = new CachedWildcardMatcher();
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(...entries) {
const message = [];
let firstNode;
for (const entry of entries) {
this.errorCount++;
message.push(formatMessage(entry, "error"));
if (entry.node) firstNode = entry.node;
}
if (entries.every((e) => e.continueOnError)) console.error(message.join("\n\n"));
else throw firstNode ? new TokensJSONError(message.join("\n\n")) : new Error(message.join("\n\n"));
}
/** Log an info message (if logging level permits) */
info(...entries) {
for (const entry of entries) {
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(...entries) {
for (const entry of entries) {
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(...entries) {
for (const entry of entries) {
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 !== "*" && !debugMatch.match(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") message = `${message} ${formatTiming(entry.timing)}`;
console.log(message);
}
}
/** Get stats for current logger instance */
stats() {
return {
errorCount: this.errorCount,
warnCount: this.warnCount,
infoCount: this.infoCount,
debugCount: this.debugCount
};
}
};
function formatTiming(timing) {
let output = "";
if (timing < 1e3) output = `${Math.round(timing * 100) / 100}ms`;
else if (timing < 6e4) output = `${Math.round(timing) / 1e3}s`;
else output = `${Math.round(timing / 1e3) / 60}m`;
return pc.dim(`[${output}]`);
}
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"
});
}
const FALLBACK_PERMUTATION_ID = JSON.stringify({ tzMode: "*" });
const cachedMatcher$1 = new CachedWildcardMatcher();
/** Run build stage */
async function build(tokens, { resolver, sources, logger = new Logger(), config }) {
const formats = {};
const result = { outputFiles: [] };
function getTransforms(plugin) {
return function getTransforms(params) {
if (!params?.format) {
logger.warn({
group: "plugin",
label: plugin,
message: "\"format\" missing from getTransforms(), no tokens returned."
});
return [];
}
const isLegacyModes = params.input && Object.keys(params.input).length === 1 && "tzMode" in params.input;
const permutationID = params.input && !isLegacyModes ? resolver.getPermutationID(params.input) : FALLBACK_PERMUTATION_ID;
const mode = params.mode || isLegacyModes && params.input.tzMode || void 0;
const singleTokenID = typeof params.id === "string" && tokens[params.id]?.id || Array.isArray(params.id) && params.id.length === 1 && tokens[params.id[0]]?.id || void 0;
const $type = typeof params.$type === "string" && [params.$type] || Array.isArray(params.$type) && params.$type || void 0;
const idMatcher = params.id && !singleTokenID && !isFullWildcard(params.id) ? cachedMatcher$1.tokenIDMatch(params.id) : null;
const modeMatcher = mode && mode !== "." && !isFullWildcard(mode) ? cachedMatcher$1.match(mode) : null;
return (formats[params.format]?.[permutationID] ?? []).filter((token) => {
if (singleTokenID && token.id !== singleTokenID || idMatcher && !idMatcher(token.id)) return false;
if (params.$type && !$type?.some((value) => token.token.$type === value)) return false;
if (mode === "." && token.mode !== "." || 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") {
const pt = performance.now();
await plugin.transform({
context: { logger },
tokens,
sources,
getTransforms: getTransforms(plugin.name),
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];
if (!token) logger.error({
group: "plugin",
label: plugin.name,
message: `No token "${id}"`
});
const isLegacyModes = params.input && Object.keys(params.input).length === 1 && "tzMode" in params.input;
const permutationID = params.input && !isLegacyModes ? resolver.getPermutationID(params.input) : FALLBACK_PERMUTATION_ID;
const mode = params.mode || isLegacyModes && params.input.tzMode || void 0;
const cleanValue = typeof params.value === "string" ? params.value : { ...params.value };
validateTransformParams({
logger,
params: {
...params,
value: cleanValue
},
pluginName: plugin.name
});
if (!formats[params.format]) formats[params.format] = { [FALLBACK_PERMUTATION_ID]: [] };
if (!formats[params.format][permutationID]) formats[params.format][permutationID] = [];
const foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && (!mode || t.mode === mode));
if (foundTokenI === -1) {
const transformedToken = {
...params,
id,
value: cleanValue,
type: typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE,
mode: mode || ".",
token: makeReadOnlyToken(token),
permutationID,
input: JSON.parse(permutationID)
};
formats[params.format][permutationID].push(transformedToken);
if (params.input && !Object.keys(params.input).length && permutationID !== FALLBACK_PERMUTATION_ID) formats[params.format][FALLBACK_PERMUTATION_ID].push(transformedToken);
} else {
formats[params.format][permutationID][foundTokenI].value = cleanValue;
formats[params.format][permutationID][foundTokenI].type = typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE;
}
},
resolver
});
logger.debug({
group: "plugin",
label: plugin.name,
message: "transform()",
timing: performance.now() - pt
});
}
transformsLocked = true;
logger.debug({
group: "parser",
label: "transform",
message: "All plugins finished transform()",
timing: performance.now() - startTransform
});
const startBuild = performance.now();
await Promise.all(config.plugins.map(async (plugin) => {
if (typeof plugin.build === "function") {
const pb = performance.now();
await plugin.build({
context: { logger },
tokens,
sources,
getTransforms: getTransforms(plugin.name),
resolver,
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() - pb
});
}
});
logger.debug({
group: "plugin",
label: plugin.name,
message: "build()",
timing: performance.now() - pb
});
}
}));
logger.debug({
group: "parser",
label: "build",
message: "All plugins finished build()",
timing: performance.now() - startBuild
});
cachedMatcher$1.reset();
const startBuildEnd = performance.now();
await Promise.all(config.plugins.map(async (plugin) => plugin.buildEnd?.({
context: { logger },
tokens,
getTransforms: getTransforms(plugin.name),
sources,
outputFiles: structuredClone(result.outputFiles)
})));
logger.debug({
group: "parser",
label: "build",
message: "buildEnd() step",
timing: performance.now() - startBuildEnd
});
return result;
}
function isFullWildcard(value) {
return typeof value === "string" && (value === "*" || value === "**") || Array.isArray(value) && value.some((v) => v === "*" || v === "**");
}
/** Generate getters for transformed tokens. Reduces memory usage while improving accuracy. Provides some safety for read-only values. */
function makeReadOnlyToken(token) {
return {
get id() {
return token.id;
},
get $value() {
return token.$value;
},
get $type() {
return token.$type;
},
get $description() {
return token.$description;
},
get $deprecated() {
return token.$deprecated;
},
get $extends() {
return token.$extends;
},
get $extensions() {
return token.$extensions;
},
get mode() {
return token.mode;
},
get originalValue() {
return token.originalValue;
},
get aliasChain() {
return token.aliasChain;
},
get aliasOf() {
return token.aliasOf;
},
get partialAliasOf() {
return token.partialAliasOf;
},
get aliasedBy() {
return token.aliasedBy;
},
get group() {
return token.group;
},
get source() {
return token.source;
},
get jsonID() {
return token.jsonID;
},
get dependencies() {
return token.dependencies;
}
};
}
//#endregion
//#region src/lint/plugin-core/lib/docs.ts
function docsLink(ruleName) {
return `https://terrazzo.app/docs/linting#${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$26 = {
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 contrast = contrastWCAG21(tokenToColor(tokens[foreground].$value), tokenToColor(tokens[background].$value));
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
}
});
}
}
};
//#endregion
//#region src/lint/plugin-core/lib/matchers.ts
/**
* Share one cached matcher factory for all lint plugins.
*
* Creating matchers is CPU-intensive, however, if we made one matcher for very
* getTransform plugin query, we could end up with tens of thousands of
* matchers, all taking up space in memory, but without providing any caching
* benefits if a matcher is used only once. So a reasonable balance is we
* maintain one cache per task category, and we garbage-collect everything after
* it’s done. Lint tasks are likely to have frequently-occurring patterns. So
* we’d expect for most use cases a shared lint cache has benefits, but only
* so long as this doesn’t spread to other plugins and other task categories.
*/
const cachedLintMatcher = new CachedWildcardMatcher();
//#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$25 = {
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 ? cachedLintMatcher.tokenIDMatch(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`
}
});
}
}
}
};
//#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$24 = {
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 ? cachedLintMatcher.tokenIDMatch(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,
filename: t.source.filename
});
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,
filename: t.source.filename
});
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,
filename: t.source.filename
});
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,
filename: t.source.filename
});
break;
}
}
}
};
//#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$23 = {
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) {
if (!t.id.split(".").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") {
if (options.format(t.id)) report({
messageId: ERROR_WRONG_FORMAT,
data: {
id: t.id,
format: "(custom)"
},
node: t.source.node
});
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/descriptions.ts
const DESCRIPTIONS = "core/descriptions";
const ERROR_MISSING_DESCRIPTION = "MISSING_DESCRIPTION";
const rule$22 = {
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 ? cachedLintMatcher.tokenIDMatch(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
});
}
}
};
//#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$21 = {
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 ? cachedLintMatcher.tokenIDMatch(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,
filename: t.source.filename
});
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,
filename: t.source.filename
});
break;
}
values[t.$type].add(t.$value);
}
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/max-gamut.ts
const MAX_GAMUT = "core/max-gamut";
const ERROR_COLOR = "COLOR";
const ERROR_BORDER = "BORDER";
const ERROR_GRADIENT = "GRADIENT";
const ERROR_SHADOW = "SHADOW";
const rule$20 = {
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 ? cachedLintMatcher.tokenIDMatch(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 (!inGamut(tokenToColor(t.$value), options.gamut)) report({
messageId: ERROR_COLOR,
data: {
id: t.id,
gamut: options.gamut
},
node: t.source.node,
filename: t.source.filename
});
break;
case "border":
if (!t.partialAliasOf?.color && !inGamut(tokenToColor(t.$value.color), options.gamut)) report({
messageId: ERROR_BORDER,
data: {
id: t.id,
gamut: options.gamut
},
node: t.source.node,
filename: t.source.filename
});
break;
case "gradient":
for (let stopI = 0; stopI < t.$value.length; stopI++) if (!t.partialAliasOf?.[stopI]?.color && !inGamut(tokenToColor(t.$value[stopI].color), options.gamut)) report({
messageId: ERROR_GRADIENT,
data: {
id: t.id,
gamut: options.gamut
},
node: t.source.node,
filename: t.source.filename
});
break;
case "shadow":
for (let shadowI = 0; shadowI < t.$value.length; shadowI++) if (!t.partialAliasOf?.[shadowI]?.color && !inGamut(tokenToColor(t.$value[shadowI].color), options.gamut)) report({
messageId: ERROR_SHADOW,
data: {
id: t.id,
gamut: options.gamut
},
node: t.source.node,
filename: t.source.filename
});
break;
}
}
}
};
//#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$19 = {
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 = cachedLintMatcher.tokenIDMatch(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
}
});
}
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/required-modes.ts
const REQUIRED_MODES = "core/required-modes";
const rule$18 = {
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 = cachedLintMatcher.tokenIDMatch(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,
filename: t.source.filename
});
if (!tokensMatched) report({
message: `Match "${matchI}": no tokens matched ${JSON.stringify(match)}`,
node: t.source.node,
filename: t.source.filename
});
}
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/required-type.ts
const REQUIRED_TYPE = "core/required-type";
const ERROR$10 = "ERROR";
const rule$17 = {
meta: {
messages: { [ERROR$10]: "Token missing $type." },
docs: {
description: "Requiring every token to have $type, even aliases, simplifies computation.",
url: docsLink(REQUIRED_TYPE)
}
},
defaultOptions: {},
create({ tokens, report }) {
for (const t of Object.values(tokens)) if (!t.originalValue?.$type) report({
messageId: ERROR$10,
node: t.source.node,
filename: t.source.filename
});
}
};
//#endregion
//#region src/lint/plugin-core/rules/required-typography-properties.ts
const REQUIRED_TYPOGRAPHY_PROPERTIES = "core/required-typography-properties";
/** @deprecated Use core/valid-typography instead */
const rule$16 = {
meta: { docs: {
description: "Enforce typography tokens have required properties.",
url: docsLink(REQUIRED_TYPOGRAPHY_PROPERTIES)
} },
defaultOptions: { properties: [
"fontFamily",
"fontSize",
"fontWeight",
"letterSpacing",
"lineHeight"
] },
create({ tokens, options, report }) {
if (!options) return;
if (!options.properties.length) throw new Error(`"properties" can’t be empty`);
const shouldIgnore = options.ignore ? cachedLintMatcher.tokenIDMatch(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: `This rule is deprecated. Use core/valid-typography. Missing required typographic property "${p}"`,
node: t.source.node,
filename: t.source.filename
});
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/valid-font-family.ts
const VALID_FONT_FAMILY = "core/valid-font-family";
const ERROR$9 = "ERROR";
const rule$15 = {
meta: {
messages: { [ERROR$9]: "Must be a string, or array of strings." },
docs: {
description: "Require fontFamily tokens to follow the format.",
url: docsLink(VALID_FONT_FAMILY)
}
},
defaultOptions: {},
create({ tokens, report }) {
for (const t of Object.values(tokens)) {
if (t.aliasOf || !t.originalValue) continue;
switch (t.$type) {
case "fontFamily":
validateFontFamily(t.originalValue.$value, {
node: getObjMember(t.source.node, "$value"),
filename: t.source.filename
});
break;
case "typography":
if (typeof t.originalValue.$value === "object" && t.originalValue.$value.fontFamily) {
if (t.partialAliasOf?.fontFamily) continue;
const properties = getObjMembers(getObjMember(t.source.node, "$value"));
validateFontFamily(t.originalValue.$value.fontFamily, {
node: properties.fontFamily,
filename: t.source.filename
});
}
break;
}
function validateFontFamily(value, { node, filename }) {
if (typeof value === "string") {
if (!value) report({
messageId: ERROR$9,
node,
filename
});
} else if (Array.isArray(value)) {
if (!value.every((v) => v && typeof v === "string")) report({
messageId: ERROR$9,
node,
filename
});
} else report({
messageId: ERROR$9,
node,
filename
});
}
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/valid-font-weight.ts
const VALID_FONT_WEIGHT = "core/valid-font-weight";
const ERROR$8 = "ERROR";
const ERROR_STYLE = "ERROR_STYLE";
const rule$14 = {
meta: {
messages: {
[ERROR$8]: `Must either be a valid number (0 - 999) or a valid font weight: ${new Intl.ListFormat("en-us", { type: "disjunction" }).format(Object.keys(FONT_WEIGHTS))}.`,
[ERROR_STYLE]: "Expected style {{ style }}, received {{ value }}."
},
docs: {
description: "Require number tokens to follow the format.",
url: docsLink(VALID_FONT_WEIGHT)
}
},
defaultOptions: { style: void 0 },
create({ tokens, options, report }) {
for (const t of Object.values(tokens)) {
if (t.aliasOf || !t.originalValue) continue;
switch (t.$type) {
case "fontWeight":
validateFontWeight(t.originalValue.$value, {
node: getObjMember(t.source.node, "$value"),
filename: t.source.filename
});
break;
case "typography":
if (typeof t.originalValue.$value === "object" && t.originalValue.$value.fontWeight) {
if (t.partialAliasOf?.fontWeight) continue;
const properties = getObjMembers(getObjMember(t.source.node, "$value"));
validateFontWeight(t.originalValue.$value.fontWeight, {
node: properties.fontWeight,
filename: t.source.filename
});
}
break;
}
function validateFontWeight(value, { node, filename }) {
if (typeof value === "string") {
if (options.style === "numbers") report({
messageId: ERROR_STYLE,
data: {
style: "numbers",
value
},
node,
filename
});
else if (!(value in FONT_WEIGHTS)) report({
messageId: ERROR$8,
node,
filename
});
} else if (typeof value === "number") {
if (options.style === "names") report({
messageId: ERROR_STYLE,
data: {
style: "names",
value
},
node,
filename
});
else if (!(value >= 0 && value < 1e3)) report({
messageId: ERROR$8,
node,
filename
});
} else report({
messageId: ERROR$8,
node,
filename
});
}
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/valid-gradient.ts
const VALID_GRADIENT = "core/valid-gradient";
const ERROR_MISSING$1 = "ERROR_MISSING";
const ERROR_POSITION = "ERROR_POSITION";
const ERROR_INVALID_PROP$7 = "ERROR_INVALID_PROP";
const rule$13 = {
meta: {
messages: {
[ERROR_MISSING$1]: "Must be an array of { color, position } objects.",
[ERROR_POSITION]: "Expected number 0-1, received {{ value }}.",
[ERROR_INVALID_PROP$7]: "Unknown property {{ key }}."
},
docs: {
description: "Require gradient tokens to follow the format.",
url: docsLink(VALID_GRADIENT)
}
},
defaultOptions: {},
create({ tokens, report }) {
for (const t of Object.values(tokens)) {
if (t.aliasOf || !t.originalValue || t.$type !== "gradient") continue;
validateGradient(t.originalValue.$value, {
node: getObjMember(t.source.node, "$value"),
filename: t.source.filename
});
function validateGradient(value, { node, filename }) {
if (Array.isArray(value)) for (let i = 0; i < value.length; i++) {
const stop = value[i];
if (!stop || typeof stop !== "object") {
report({
messageId: ERROR_MISSING$1,
node,
filename
});
continue;
}
for (const property of GRADIENT_REQUIRED_STOP_PROPERTIES) if (!(property in stop)) report({
messageId: ERROR_MISSING$1,
node: node.elements[i],
filename
});
for (const key of Object.keys(stop)) if (!GRADIENT_REQUIRED_STOP_PROPERTIES.includes(key)) report({
messageId: ERROR_INVALID_PROP$7,
data: { key: JSON.stringify(key) },
node: node.elements[i],
filename
});
if ("position" in stop && typeof stop.position !== "number" && !isAlias(stop.position)) report({
messageId: ERROR_POSITION,
data: { value: stop.position },
node: getObjMember(node.elements[i].value, "position"),
filename
});
}
else report({
messageId: ERROR_MISSING$1,
node,
filename
});
}
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/valid-link.ts
const VALID_LINK = "core/valid-link";
const ERROR$7 = "ERROR";
const rule$12 = {
meta: {
messages: { [ERROR$7]: "Must be a string." },
docs: {
description: "Require link tokens to follow the Terrazzo extension.",
url: docsLink(VALID_LINK)
}
},
defaultOptions: {},
create({ tokens, report }) {
for (const t of Object.values(tokens)) {
if (t.aliasOf || !t.originalValue || t.$type !== "link") continue;
validateLink(t.originalValue.$value, {
node: getObjMember(t.source.node, "$value"),
filename: t.source.filename
});
function validateLink(value, { node, filename }) {
if (!value || typeof value !== "string") report({
messageId: ERROR$7,
node,
filename
});
}
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/valid-number.ts
const VALID_NUMBER = "core/valid-number";
const ERROR_NAN = "ERROR_NAN";
const rule$11 = {
meta: {
messages: { [ERROR_NAN]: "Must be a number." },
docs: {
description: "Require number tokens to follow the format.",
url: docsLink(VALID_NUMBER)
}
},
defaultOptions: {},
create({ tokens, report }) {
for (const t of Object.values(tokens)) {
if (t.aliasOf || !t.originalValue) continue;
switch (t.$type) {
case "number":
validateNumber(t.originalValue.$value, {
node: getObjMember(t.source.node, "$value"),
filename: t.source.filename
});
break;
case "typography": {
const $valueNode = getObjMember(t.source.node, "$value");
if (typeof t.originalValue.$value === "object") {
if (t.originalValue.$value.lineHeight && !isAlias(t.originalValue.$value.lineHeight) && typeof t.originalValue.$value.lineHeight !== "object") validateNumber(t.originalValue.$value.lineHeight, {
node: getObjMember($valueNode, "lineHeight"),
filename: t.source.filename
});
}
}
}
function validateNumber(value, { node, filename }) {
if (typeof value !== "number" || Number.isNaN(value)) report({
messageId: ERROR_NAN,
node,
filename
});
}
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/valid-shadow.ts
const VALID_SHADOW = "core/valid-shadow";
const ERROR$6 = "ERROR";
const ERROR_INVALID_PROP$6 = "ERROR_INVALID_PROP";
const rule$10 = {
meta: {
messages: {
[ERROR$6]: `Missing required properties: ${new Intl.ListFormat("en-us", { type: "conjunction" }).format(SHADOW_REQUIRED_PROPERTIES)}.`,
[ERROR_INVALID_PROP$6]: "Unknown property {{ key }}."
},
docs: {
description: "Require shadow tokens to follow the format.",
url: docsLink(VALID_SHADOW)
}
},
defaultOptions: {},
create({ tokens, report }) {
for (const t of Object.values(tokens)) {
if (t.aliasOf || !t.originalValue || t.$type !== "shadow") continue;
validateShadow(t.originalValue.$value, {
node: getObjMember(t.source.node, "$value"),
filename: t.source.filename
});
function validateShadow(value, { node, filename }) {
const wrappedValue = Array.isArray(value) ? value : [value];
for (let i = 0; i < wrappedValue.length; i++) if (!wrappedValue[i] || typeof wrappedValue[i] !== "object" || !SHADOW_REQUIRED_PROPERTIES.every((property) => property in wrappedValue[i])) report({
messageId: ERROR$6,
node,
filename
});
else for (const key of Object.keys(wrappedValue[i])) if (![...SHADOW_REQUIRED_PROPERTIES, "inset"].includes(key)) report({
messageId: ERROR_INVALID_PROP$6,
data: { key: JSON.stringify(key) },
node: getObjMember(node.type === "Array" ? node.elements[i].value : node, key),
filename
});
}
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/valid-string.ts
const VALID_STRING = "core/valid-string";
const ERROR$5 = "ERROR";
const rule$9 = {
meta: {
messages: { [ERROR$5]: "Must be a string." },
docs: {
description: "Require string tokens to follow the Terrazzo extension.",
url: docsLink(VALID_STRING)
}
},
defaultOptions: {},
create({ tokens, report }) {
for (const t of Object.values(tokens)) {
if (t.aliasOf || !t.originalValue || t.$type !== "string") continue;
validateString(t.originalValue.$value, {
node: getObjMember(t.source.node, "$value"),
filename: t.source.filename
});
function validateString(value, { node, filename }) {
if (typeof value !== "string") report({
messageId: ERROR$5,
node,
filename
});
}
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/valid-stroke-style.ts
const VALID_STROKE_STYLE = "core/valid-stroke-style";
const ERROR_STR = "ERROR_STR";
const ERROR_OBJ = "ERROR_OBJ";
const ERROR_LINE_CAP = "ERROR_LINE_CAP";
const ERROR_INVALID_PROP$5 = "ERROR_INVALID_PROP";
const rule$8 = {
meta: {
messages: {
[ERROR_STR]: `Value most be one of ${new Intl.ListFormat("en-us", { type: "disjunction" }).format(STROKE_STYLE_STRING_VALUES)}.`,
[ERROR_OBJ]: `Missing required properties: ${new Intl.ListFormat("en-us", { type: "conjunction" }).format(TRANSITION_REQUIRED_PROPERTIES)}.`,
[ERROR_LINE_CAP]: `lineCap must be one of ${new Intl.ListFormat("en-us", { type: "disjunction" }).format(STROKE_STYLE_LINE_CAP_VALUES)}.`,
[ERROR_INVALID_PROP$5]: "Unknown property: {{ key }}."
},
docs: {
description: "Require strokeStyle tokens to follow the format.",
url: docsLink(VALID_STROKE_STYLE)
}
},
defaultOptions: {},
create({ tokens, report }) {
for (const t of Object.values(tokens)) {
if (t.aliasOf || !t.originalValue) continue;
switch (t.$type) {
case "strokeStyle":
validateStrokeStyle(t.originalValue.$value, {
node: getObjMember(t.source.node, "$value"),
filename: t.source.filename
});
break;
case "border":
if (t.originalValue.$value && typeof t.originalValue.$value === "object") {
const $valueNode = getObjMember(t.source.node, "$value");
if (t.originalValue.$value.style) validateStrokeStyle(t.originalValue.$value.style, {
node: getObjMember($valueNode, "style"),
filename: t.source.filename
});
}
break;
}
function validateStrokeStyle(value, { node, filename }) {
if (typeof value === "string") {
if (!isAlias(value) && !STROKE_STYLE_STRING_VALUES.includes(value)) {
report({
messageId: ERROR_STR,
node,
filename
});
return;
}
} else if (value && typeof value === "object") {
if (!STROKE_STYLE_OBJECT_REQUIRED_PROPERTIES.every((property) => property in value)) report({
messageId: ERROR_OBJ,
node,
filename
});
if (!Array.isArray(value.dashArray)) report({
messageId: ERROR_OBJ,
node: getObjMember(node, "dashArray"),
filename
});
if (!STROKE_STYLE_LINE_CAP_VALUES.includes(value.lineCap)) report({
messageId: ERROR_OBJ,
node: getObjMember(node, "lineCap"),
filename
});
for (const key of Object.keys(value)) if (!["dashArray", "lineCap"].includes(key)) report({
messageId: ERROR_INVALID_PROP$5,
data: { key: JSON.stringify(key) },
node: getObjMember(node, key),
filename
});
} else report({
messageId: ERROR_OBJ,
node,
filename
});
}
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/valid-transition.ts
const VALID_TRANSITION = "core/valid-transition";
const ERROR$4 = "ERROR";
const ERROR_INVALID_PROP$4 = "ERROR_INVALID_PROP";
const rule$7 = {
meta: {
messages: {
[ERROR$4]: `Missing required properties: ${new Intl.ListFormat("en-us", { type: "conjunction" }).format(TRANSITION_REQUIRED_PROPERTIES)}.`,
[ERROR_INVALID_PROP$4]: "Unknown property: {{ key }}."
},
docs: {
description: "Require transition tokens to follow the format.",
url: docsLink(VALID_TRANSITION)
}
},
defaultOptions: {},
create({ tokens, report }) {
for (const t of Object.values(tokens)) {
if (t.aliasOf || !t.originalValue || t.$type !== "transition") continue;
validateTransition(t.originalValue.$value, {
node: getObjMember(t.source.node, "$value"),
filename: t.source.filename
});
}
function validateTransition(value, { node, filename }) {
if (!value || typeof value !== "object" || !TRANSITION_REQUIRED_PROPERTIES.every((property) => property in value)) report({
messageId: ERROR$4,
node,
filename
});
else for (const key of Object.keys(value)) if (!TRANSITION_REQUIRED_PROPERTIES.includes(key)) report({
messageId: ERROR_INVALID_PROP$4,
data: { key: JSON.stringify(key) },
node: getObjMember(node, key),
filename
});
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/valid-typography.ts
const VALID_TYPOGRAPHY = "core/valid-typography";
const ERROR$3 = "ERROR";
const ERROR_MISSING = "ERROR_MISSING";
const rule$6 = {
meta: {
messages: {
[ERROR$3]: `Expected object, received {{ value }}.`,
[ERROR_MISSING]: `Missing required property "{{ property }}".`
},
docs: {
description: "Require typography tokens to follow the format.",
url: docsLink(VALID_TYPOGRAPHY)
}
},
defaultOptions: { requiredProperties: [
"fontFamily",
"fontSize",
"fontWeight",
"letterSpacing",
"lineHeight"
] },
create({ tokens, options, report }) {
const isIgnored = options.ignore ? cachedLintMatcher.tokenIDMatch(options.ignore) : () => false;
for (const t of Object.values(tokens)) {
if (t.aliasOf || !t.originalValue || t.$type !== "typography" || isIgnored(t.id)) continue;
validateTypography(t.originalValue.$value, {
node: getObjMember(t.source.node, "$value"),
filename: t.source.filename
});
function validateTypography(value, { node, filename }) {
if (value && typeof value === "object") {
for (const property of options.requiredProperties) if (!(property in value)) report({
messageId: ERROR_MISSING,
data: { property },
node,
filename
});
} else report({
messageId: ERROR$3,
data: { value: JSON.stringify(value) },
node,
filename
});
}
}
}
};
//#endregion
//#region src/lint/plugin-core/rules/valid-boolean.ts
const VALID_BOOLEAN = "core/valid-boolean";
const ERROR$2 = "ERROR";
const rule$5 = {
meta: {
messages: { [ERROR$2]: "Must be a boolean." },
docs: {
description: "Require boolean tokens to follow the Terrazzo extension.",
url: docsLink(VALID_BOOLEAN)
}
},
defaultOptions: {},
create({ tokens, report }) {
for (const t of Object.values(tokens)) {
if (t.aliasOf || !t.originalValue || t.$type !== "boolean") continue;
validateBoolean(t.originalValue.$value, {
node: getObjMember(t.source.node, "$value"),
filename