@terrazzo/parser 2.0.0-alpha.7 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/dist/index.d.ts +39 -6
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1989 -1921
  5. package/dist/index.js.map +1 -1
  6. package/package.json +3 -3
  7. package/src/build/index.ts +0 -209
  8. package/src/config.ts +0 -304
  9. package/src/index.ts +0 -95
  10. package/src/lib/code-frame.ts +0 -177
  11. package/src/lib/momoa.ts +0 -10
  12. package/src/lib/resolver-utils.ts +0 -35
  13. package/src/lint/index.ts +0 -142
  14. package/src/lint/plugin-core/index.ts +0 -103
  15. package/src/lint/plugin-core/lib/docs.ts +0 -3
  16. package/src/lint/plugin-core/rules/a11y-min-contrast.ts +0 -91
  17. package/src/lint/plugin-core/rules/a11y-min-font-size.ts +0 -66
  18. package/src/lint/plugin-core/rules/colorspace.ts +0 -108
  19. package/src/lint/plugin-core/rules/consistent-naming.ts +0 -65
  20. package/src/lint/plugin-core/rules/descriptions.ts +0 -43
  21. package/src/lint/plugin-core/rules/duplicate-values.ts +0 -85
  22. package/src/lint/plugin-core/rules/max-gamut.ts +0 -144
  23. package/src/lint/plugin-core/rules/required-children.ts +0 -106
  24. package/src/lint/plugin-core/rules/required-modes.ts +0 -75
  25. package/src/lint/plugin-core/rules/required-type.ts +0 -28
  26. package/src/lint/plugin-core/rules/required-typography-properties.ts +0 -65
  27. package/src/lint/plugin-core/rules/valid-boolean.ts +0 -41
  28. package/src/lint/plugin-core/rules/valid-border.ts +0 -57
  29. package/src/lint/plugin-core/rules/valid-color.ts +0 -265
  30. package/src/lint/plugin-core/rules/valid-cubic-bezier.ts +0 -83
  31. package/src/lint/plugin-core/rules/valid-dimension.ts +0 -199
  32. package/src/lint/plugin-core/rules/valid-duration.ts +0 -123
  33. package/src/lint/plugin-core/rules/valid-font-family.ts +0 -68
  34. package/src/lint/plugin-core/rules/valid-font-weight.ts +0 -89
  35. package/src/lint/plugin-core/rules/valid-gradient.ts +0 -79
  36. package/src/lint/plugin-core/rules/valid-link.ts +0 -41
  37. package/src/lint/plugin-core/rules/valid-number.ts +0 -63
  38. package/src/lint/plugin-core/rules/valid-shadow.ts +0 -67
  39. package/src/lint/plugin-core/rules/valid-string.ts +0 -41
  40. package/src/lint/plugin-core/rules/valid-stroke-style.ts +0 -104
  41. package/src/lint/plugin-core/rules/valid-transition.ts +0 -61
  42. package/src/lint/plugin-core/rules/valid-typography.ts +0 -67
  43. package/src/logger.ts +0 -213
  44. package/src/parse/index.ts +0 -124
  45. package/src/parse/load.ts +0 -172
  46. package/src/parse/normalize.ts +0 -163
  47. package/src/parse/process.ts +0 -251
  48. package/src/parse/token.ts +0 -553
  49. package/src/resolver/create-synthetic-resolver.ts +0 -86
  50. package/src/resolver/index.ts +0 -7
  51. package/src/resolver/load.ts +0 -215
  52. package/src/resolver/normalize.ts +0 -133
  53. package/src/resolver/validate.ts +0 -375
  54. package/src/types.ts +0 -468
package/dist/index.js CHANGED
@@ -230,29 +230,35 @@ function validateTransformParams({ params, logger, pluginName }) {
230
230
  message: "setTransform() value expected object of strings, received some non-string values"
231
231
  });
232
232
  }
233
+ const FALLBACK_PERMUTATION_ID = JSON.stringify({ tzMode: "*" });
233
234
  /** Run build stage */
234
235
  async function build(tokens, { resolver, sources, logger = new Logger(), config }) {
235
236
  const formats = {};
236
237
  const result = { outputFiles: [] };
237
- function getTransforms(params) {
238
- if (!params?.format) {
239
- logger.warn({
240
- group: "plugin",
241
- message: "\"format\" missing from getTransforms(), no tokens returned."
242
- });
243
- return [];
244
- }
245
- const tokenMatcher = params.id ? wcmatch(Array.isArray(params.id) ? params.id : [params.id]) : null;
246
- const modeMatcher = params.mode ? wcmatch(params.mode) : null;
247
- return (formats[params.format] ?? []).filter((token) => {
248
- if (params.$type) {
249
- if (typeof params.$type === "string" && token.token.$type !== params.$type) return false;
250
- else if (Array.isArray(params.$type) && !params.$type.some(($type) => token.token.$type === $type)) return false;
238
+ function getTransforms(plugin) {
239
+ return function getTransforms$1(params) {
240
+ if (!params?.format) {
241
+ logger.warn({
242
+ group: "plugin",
243
+ label: plugin,
244
+ message: "\"format\" missing from getTransforms(), no tokens returned."
245
+ });
246
+ return [];
251
247
  }
252
- if (params.id && params.id !== "*" && tokenMatcher && !tokenMatcher(token.token.id)) return false;
253
- if (modeMatcher && !modeMatcher(token.mode)) return false;
254
- return true;
255
- });
248
+ const tokenMatcher = params.id && params.id !== "*" ? wcmatch(params.id) : null;
249
+ const modeMatcher = params.mode ? wcmatch(params.mode) : null;
250
+ const permutationID = params.input ? resolver.getPermutationID(params.input) : JSON.stringify({ tzMode: "*" });
251
+ return (formats[params.format]?.[permutationID] ?? []).filter((token) => {
252
+ if (params.$type) {
253
+ if (typeof params.$type === "string" && token.token.$type !== params.$type) return false;
254
+ else if (Array.isArray(params.$type) && !params.$type.some(($type) => token.token.$type === $type)) return false;
255
+ }
256
+ if (tokenMatcher && !tokenMatcher(token.token.id)) return false;
257
+ if (params.input && token.permutationID !== resolver.getPermutationID(params.input)) return false;
258
+ if (modeMatcher && !modeMatcher(token.mode)) return false;
259
+ return true;
260
+ });
261
+ };
256
262
  }
257
263
  let transformsLocked = false;
258
264
  const startTransform = performance.now();
@@ -260,7 +266,7 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
260
266
  context: { logger },
261
267
  tokens,
262
268
  sources,
263
- getTransforms,
269
+ getTransforms: getTransforms(plugin.name),
264
270
  setTransform(id, params) {
265
271
  if (transformsLocked) {
266
272
  logger.warn({
@@ -271,6 +277,7 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
271
277
  return;
272
278
  }
273
279
  const token = tokens[id];
280
+ const permutationID = params.input ? resolver.getPermutationID(params.input) : FALLBACK_PERMUTATION_ID;
274
281
  const cleanValue = typeof params.value === "string" ? params.value : { ...params.value };
275
282
  validateTransformParams({
276
283
  logger,
@@ -280,19 +287,27 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
280
287
  },
281
288
  pluginName: plugin.name
282
289
  });
283
- if (!formats[params.format]) formats[params.format] = [];
284
- const foundTokenI = formats[params.format].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && (!params.mode || params.mode === t.mode));
285
- if (foundTokenI === -1) formats[params.format].push({
290
+ if (!formats[params.format]) formats[params.format] = {};
291
+ if (!formats[params.format][permutationID]) formats[params.format][permutationID] = [];
292
+ let foundTokenI = -1;
293
+ if (params.mode) foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && params.mode === t.mode);
294
+ else if (params.input) {
295
+ if (!formats[params.format][permutationID]) formats[params.format][permutationID] = [];
296
+ foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID) && permutationID === t.permutationID);
297
+ } else foundTokenI = formats[params.format][permutationID].findIndex((t) => id === t.id && (!params.localID || params.localID === t.localID));
298
+ if (foundTokenI === -1) formats[params.format][permutationID].push({
286
299
  ...params,
287
300
  id,
288
301
  value: cleanValue,
289
302
  type: typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE,
290
303
  mode: params.mode || ".",
291
- token: structuredClone(token)
304
+ token: structuredClone(token),
305
+ permutationID,
306
+ input: JSON.parse(permutationID)
292
307
  });
293
308
  else {
294
- formats[params.format][foundTokenI].value = cleanValue;
295
- formats[params.format][foundTokenI].type = typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE;
309
+ formats[params.format][permutationID][foundTokenI].value = cleanValue;
310
+ formats[params.format][permutationID][foundTokenI].type = typeof cleanValue === "string" ? SINGLE_VALUE : MULTI_VALUE;
296
311
  }
297
312
  },
298
313
  resolver
@@ -312,7 +327,7 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
312
327
  context: { logger },
313
328
  tokens,
314
329
  sources,
315
- getTransforms,
330
+ getTransforms: getTransforms(plugin.name),
316
331
  resolver,
317
332
  outputFile(filename, contents) {
318
333
  const resolved = new URL(filename, config.outDir);
@@ -341,7 +356,7 @@ async function build(tokens, { resolver, sources, logger = new Logger(), config
341
356
  await Promise.all(config.plugins.map(async (plugin) => plugin.buildEnd?.({
342
357
  context: { logger },
343
358
  tokens,
344
- getTransforms,
359
+ getTransforms: getTransforms(plugin.name),
345
360
  sources,
346
361
  outputFiles: structuredClone(result.outputFiles)
347
362
  })));
@@ -897,262 +912,134 @@ const rule$16 = {
897
912
  var required_typography_properties_default = rule$16;
898
913
 
899
914
  //#endregion
900
- //#region src/lint/plugin-core/rules/valid-boolean.ts
901
- const VALID_BOOLEAN = "core/valid-boolean";
915
+ //#region src/lint/plugin-core/rules/valid-font-family.ts
916
+ const VALID_FONT_FAMILY = "core/valid-font-family";
902
917
  const ERROR$9 = "ERROR";
903
918
  const rule$15 = {
904
919
  meta: {
905
- messages: { [ERROR$9]: "Must be a boolean." },
920
+ messages: { [ERROR$9]: "Must be a string, or array of strings." },
906
921
  docs: {
907
- description: "Require boolean tokens to follow the Terrazzo extension.",
908
- url: docsLink(VALID_BOOLEAN)
922
+ description: "Require fontFamily tokens to follow the format.",
923
+ url: docsLink(VALID_FONT_FAMILY)
909
924
  }
910
925
  },
911
926
  defaultOptions: {},
912
927
  create({ tokens, report }) {
913
928
  for (const t of Object.values(tokens)) {
914
- if (t.aliasOf || !t.originalValue || t.$type !== "boolean") continue;
915
- validateBoolean(t.originalValue.$value, {
916
- node: getObjMember(t.source.node, "$value"),
917
- filename: t.source.filename
918
- });
919
- function validateBoolean(value, { node, filename }) {
920
- if (typeof value !== "boolean") report({
929
+ if (t.aliasOf || !t.originalValue) continue;
930
+ switch (t.$type) {
931
+ case "fontFamily":
932
+ validateFontFamily(t.originalValue.$value, {
933
+ node: getObjMember(t.source.node, "$value"),
934
+ filename: t.source.filename
935
+ });
936
+ break;
937
+ case "typography":
938
+ if (typeof t.originalValue.$value === "object" && t.originalValue.$value.fontFamily) {
939
+ if (t.partialAliasOf?.fontFamily) continue;
940
+ const properties = getObjMembers(getObjMember(t.source.node, "$value"));
941
+ validateFontFamily(t.originalValue.$value.fontFamily, {
942
+ node: properties.fontFamily,
943
+ filename: t.source.filename
944
+ });
945
+ }
946
+ break;
947
+ }
948
+ function validateFontFamily(value, { node, filename }) {
949
+ if (typeof value === "string") {
950
+ if (!value) report({
951
+ messageId: ERROR$9,
952
+ node,
953
+ filename
954
+ });
955
+ } else if (Array.isArray(value)) {
956
+ if (!value.every((v) => v && typeof v === "string")) report({
957
+ messageId: ERROR$9,
958
+ node,
959
+ filename
960
+ });
961
+ } else report({
921
962
  messageId: ERROR$9,
922
- filename,
923
- node
963
+ node,
964
+ filename
924
965
  });
925
966
  }
926
967
  }
927
968
  }
928
969
  };
929
- var valid_boolean_default = rule$15;
970
+ var valid_font_family_default = rule$15;
930
971
 
931
972
  //#endregion
932
- //#region src/lint/plugin-core/rules/valid-border.ts
933
- const VALID_BORDER = "core/valid-border";
973
+ //#region src/lint/plugin-core/rules/valid-font-weight.ts
974
+ const VALID_FONT_WEIGHT = "core/valid-font-weight";
934
975
  const ERROR$8 = "ERROR";
935
- const ERROR_INVALID_PROP$7 = "ERROR_INVALID_PROP";
976
+ const ERROR_STYLE = "ERROR_STYLE";
936
977
  const rule$14 = {
937
978
  meta: {
938
979
  messages: {
939
- [ERROR$8]: `Border token missing required properties: ${new Intl.ListFormat("en-us", { type: "conjunction" }).format(BORDER_REQUIRED_PROPERTIES)}.`,
940
- [ERROR_INVALID_PROP$7]: "Unknown property: {{ key }}."
941
- },
942
- docs: {
943
- description: "Require border tokens to follow the format.",
944
- url: docsLink(VALID_BORDER)
945
- }
946
- },
947
- defaultOptions: {},
948
- create({ tokens, report }) {
949
- for (const t of Object.values(tokens)) {
950
- if (t.aliasOf || !t.originalValue || t.$type !== "border") continue;
951
- validateBorder(t.originalValue.$value, {
952
- node: getObjMember(t.source.node, "$value"),
953
- filename: t.source.filename
954
- });
955
- }
956
- function validateBorder(value, { node, filename }) {
957
- if (!value || typeof value !== "object" || !BORDER_REQUIRED_PROPERTIES.every((property) => property in value)) report({
958
- messageId: ERROR$8,
959
- filename,
960
- node
961
- });
962
- else for (const key of Object.keys(value)) if (!BORDER_REQUIRED_PROPERTIES.includes(key)) report({
963
- messageId: ERROR_INVALID_PROP$7,
964
- data: { key: JSON.stringify(key) },
965
- node: getObjMember(node, key) ?? node,
966
- filename
967
- });
968
- }
969
- }
970
- };
971
- var valid_border_default = rule$14;
972
-
973
- //#endregion
974
- //#region src/lint/plugin-core/rules/valid-color.ts
975
- const VALID_COLOR = "core/valid-color";
976
- const ERROR_ALPHA = "ERROR_ALPHA";
977
- const ERROR_INVALID_COLOR = "ERROR_INVALID_COLOR";
978
- const ERROR_INVALID_COLOR_SPACE = "ERROR_INVALID_COLOR_SPACE";
979
- const ERROR_INVALID_COMPONENT_LENGTH = "ERROR_INVALID_COMPONENT_LENGTH";
980
- const ERROR_INVALID_HEX8 = "ERROR_INVALID_HEX8";
981
- const ERROR_INVALID_PROP$6 = "ERROR_INVALID_PROP";
982
- const ERROR_MISSING_COMPONENTS = "ERROR_MISSING_COMPONENTS";
983
- const ERROR_OBJ_FORMAT = "ERROR_OBJ_FORMAT";
984
- const ERROR_OUT_OF_RANGE = "ERROR_OUT_OF_RANGE";
985
- const rule$13 = {
986
- meta: {
987
- messages: {
988
- [ERROR_ALPHA]: `Alpha {{ alpha }} not in range 0 – 1.`,
989
- [ERROR_INVALID_COLOR_SPACE]: `Invalid color space: {{ colorSpace }}. Expected ${new Intl.ListFormat("en-us", { type: "disjunction" }).format(Object.keys(COLORSPACE))}`,
990
- [ERROR_INVALID_COLOR]: `Could not parse color {{ color }}.`,
991
- [ERROR_INVALID_COMPONENT_LENGTH]: "Expected {{ expected }} components, received {{ got }}.",
992
- [ERROR_INVALID_HEX8]: `Hex value can’t be semi-transparent.`,
993
- [ERROR_INVALID_PROP$6]: `Unknown property {{ key }}.`,
994
- [ERROR_MISSING_COMPONENTS]: "Expected components to be array of numbers, received {{ got }}.",
995
- [ERROR_OBJ_FORMAT]: "Migrate to the new object format, e.g. \"#ff0000\" → { \"colorSpace\": \"srgb\", \"components\": [1, 0, 0] } }",
996
- [ERROR_OUT_OF_RANGE]: `Invalid range for color space {{ colorSpace }}. Expected {{ range }}.`
980
+ [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))}.`,
981
+ [ERROR_STYLE]: "Expected style {{ style }}, received {{ value }}."
997
982
  },
998
983
  docs: {
999
- description: "Require color tokens to follow the format.",
1000
- url: docsLink(VALID_COLOR)
984
+ description: "Require number tokens to follow the format.",
985
+ url: docsLink(VALID_FONT_WEIGHT)
1001
986
  }
1002
987
  },
1003
- defaultOptions: {
1004
- legacyFormat: false,
1005
- ignoreRanges: false
1006
- },
988
+ defaultOptions: { style: void 0 },
1007
989
  create({ tokens, options, report }) {
1008
990
  for (const t of Object.values(tokens)) {
1009
991
  if (t.aliasOf || !t.originalValue) continue;
1010
992
  switch (t.$type) {
1011
- case "color":
1012
- validateColor(t.originalValue.$value, {
993
+ case "fontWeight":
994
+ validateFontWeight(t.originalValue.$value, {
1013
995
  node: getObjMember(t.source.node, "$value"),
1014
996
  filename: t.source.filename
1015
997
  });
1016
998
  break;
1017
- case "border":
1018
- if (t.originalValue.$value.color && !isAlias(t.originalValue.$value.color)) validateColor(t.originalValue.$value.color, {
1019
- node: getObjMember(getObjMember(t.source.node, "$value"), "color"),
1020
- filename: t.source.filename
1021
- });
1022
- break;
1023
- case "gradient": {
1024
- const $valueNode = getObjMember(t.source.node, "$value");
1025
- for (let i = 0; i < t.originalValue.$value.length; i++) {
1026
- const stop = t.originalValue.$value[i];
1027
- if (!stop.color || isAlias(stop.color)) continue;
1028
- validateColor(stop.color, {
1029
- node: getObjMember($valueNode.elements[i].value, "color"),
1030
- filename: t.source.filename
1031
- });
1032
- }
1033
- break;
1034
- }
1035
- case "shadow": {
1036
- const $value = Array.isArray(t.originalValue.$value) ? t.originalValue.$value : [t.originalValue.$value];
1037
- const $valueNode = getObjMember(t.source.node, "$value");
1038
- for (let i = 0; i < $value.length; i++) {
1039
- const layer = $value[i];
1040
- if (!layer.color || isAlias(layer.color)) continue;
1041
- validateColor(layer.color, {
1042
- node: $valueNode.type === "Object" ? getObjMember($valueNode, "color") : getObjMember($valueNode.elements[i].value, "color"),
999
+ case "typography":
1000
+ if (typeof t.originalValue.$value === "object" && t.originalValue.$value.fontWeight) {
1001
+ if (t.partialAliasOf?.fontWeight) continue;
1002
+ const properties = getObjMembers(getObjMember(t.source.node, "$value"));
1003
+ validateFontWeight(t.originalValue.$value.fontWeight, {
1004
+ node: properties.fontWeight,
1043
1005
  filename: t.source.filename
1044
1006
  });
1045
1007
  }
1046
1008
  break;
1047
- }
1048
1009
  }
1049
- function validateColor(value, { node, filename }) {
1050
- if (!value) report({
1051
- messageId: ERROR_INVALID_COLOR,
1052
- data: { color: JSON.stringify(value) },
1053
- node,
1054
- filename
1055
- });
1056
- else if (typeof value === "object") {
1057
- for (const key of Object.keys(value)) if (![
1058
- "colorSpace",
1059
- "components",
1060
- "channels",
1061
- "hex",
1062
- "alpha"
1063
- ].includes(key)) report({
1064
- messageId: ERROR_INVALID_PROP$6,
1065
- data: { key: JSON.stringify(key) },
1066
- node: getObjMember(node, key) ?? node,
1067
- filename
1068
- });
1069
- const colorSpace = "colorSpace" in value && typeof value.colorSpace === "string" ? value.colorSpace : void 0;
1070
- const csData = COLORSPACE[colorSpace] || void 0;
1071
- if (!("colorSpace" in value) || !csData) {
1072
- report({
1073
- messageId: ERROR_INVALID_COLOR_SPACE,
1074
- data: { colorSpace },
1075
- node: getObjMember(node, "colorSpace") ?? node,
1076
- filename
1077
- });
1078
- return;
1079
- }
1080
- const components = "components" in value ? value.components : void 0;
1081
- if (Array.isArray(components)) if (csData?.ranges && components?.length === csData.ranges.length) {
1082
- for (let i = 0; i < components.length; i++) if (!Number.isFinite(components[i]) || components[i] < csData.ranges[i][0] || components[i] > csData.ranges[i][1]) {
1083
- if (!(colorSpace === "hsl" && components[0] === null) && !(colorSpace === "hwb" && components[0] === null) && !(colorSpace === "lch" && components[2] === null) && !(colorSpace === "oklch" && components[2] === null)) report({
1084
- messageId: ERROR_OUT_OF_RANGE,
1085
- data: {
1086
- colorSpace,
1087
- range: `[${csData.ranges.map((r) => `${r[0]}–${r[1]}`).join(", ")}]`
1088
- },
1089
- node: getObjMember(node, "components") ?? node,
1090
- filename
1091
- });
1092
- }
1093
- } else report({
1094
- messageId: ERROR_INVALID_COMPONENT_LENGTH,
1010
+ function validateFontWeight(value, { node, filename }) {
1011
+ if (typeof value === "string") {
1012
+ if (options.style === "numbers") report({
1013
+ messageId: ERROR_STYLE,
1095
1014
  data: {
1096
- expected: csData?.ranges.length,
1097
- got: components?.length ?? 0
1015
+ style: "numbers",
1016
+ value
1098
1017
  },
1099
- node: getObjMember(node, "components") ?? node,
1018
+ node,
1100
1019
  filename
1101
1020
  });
1102
- else report({
1103
- messageId: ERROR_MISSING_COMPONENTS,
1104
- data: { got: JSON.stringify(components) },
1105
- node: getObjMember(node, "components") ?? node,
1021
+ else if (!(value in FONT_WEIGHTS)) report({
1022
+ messageId: ERROR$8,
1023
+ node,
1106
1024
  filename
1107
1025
  });
1108
- const alpha = "alpha" in value ? value.alpha : void 0;
1109
- if (alpha !== void 0 && (typeof alpha !== "number" || alpha < 0 || alpha > 1)) report({
1110
- messageId: ERROR_ALPHA,
1111
- data: { alpha },
1112
- node: getObjMember(node, "alpha") ?? node,
1026
+ } else if (typeof value === "number") {
1027
+ if (options.style === "names") report({
1028
+ messageId: ERROR_STYLE,
1029
+ data: {
1030
+ style: "names",
1031
+ value
1032
+ },
1033
+ node,
1113
1034
  filename
1114
1035
  });
1115
- const hex = "hex" in value ? value.hex : void 0;
1116
- if (hex) {
1117
- let color;
1118
- try {
1119
- color = parseColor(hex);
1120
- } catch {
1121
- report({
1122
- messageId: ERROR_INVALID_COLOR,
1123
- data: { color: hex },
1124
- node: getObjMember(node, "hex") ?? node,
1125
- filename
1126
- });
1127
- return;
1128
- }
1129
- if (color.alpha !== 1) report({
1130
- messageId: ERROR_INVALID_HEX8,
1131
- data: { color: hex },
1132
- node: getObjMember(node, "hex") ?? node,
1133
- filename
1134
- });
1135
- }
1136
- } else if (typeof value === "string") {
1137
- if (isAlias(value)) return;
1138
- if (!options.legacyFormat) report({
1139
- messageId: ERROR_OBJ_FORMAT,
1140
- data: { color: JSON.stringify(value) },
1036
+ else if (!(value >= 0 && value < 1e3)) report({
1037
+ messageId: ERROR$8,
1141
1038
  node,
1142
1039
  filename
1143
1040
  });
1144
- else try {
1145
- parseColor(value);
1146
- } catch {
1147
- report({
1148
- messageId: ERROR_INVALID_COLOR,
1149
- data: { color: JSON.stringify(value) },
1150
- node,
1151
- filename
1152
- });
1153
- }
1154
1041
  } else report({
1155
- messageId: ERROR_INVALID_COLOR,
1042
+ messageId: ERROR$8,
1156
1043
  node,
1157
1044
  filename
1158
1045
  });
@@ -1160,64 +1047,96 @@ const rule$13 = {
1160
1047
  }
1161
1048
  }
1162
1049
  };
1163
- var valid_color_default = rule$13;
1050
+ var valid_font_weight_default = rule$14;
1164
1051
 
1165
1052
  //#endregion
1166
- //#region src/lint/plugin-core/rules/valid-cubic-bezier.ts
1167
- const VALID_CUBIC_BEZIER = "core/valid-cubic-bezier";
1168
- const ERROR$7 = "ERROR";
1169
- const ERROR_X = "ERROR_X";
1170
- const ERROR_Y = "ERROR_Y";
1171
- const rule$12 = {
1053
+ //#region src/lint/plugin-core/rules/valid-gradient.ts
1054
+ const VALID_GRADIENT = "core/valid-gradient";
1055
+ const ERROR_MISSING$1 = "ERROR_MISSING";
1056
+ const ERROR_POSITION = "ERROR_POSITION";
1057
+ const ERROR_INVALID_PROP$7 = "ERROR_INVALID_PROP";
1058
+ const rule$13 = {
1172
1059
  meta: {
1173
1060
  messages: {
1174
- [ERROR$7]: "Expected [number, number, number, number].",
1175
- [ERROR_X]: "x values must be between 0-1.",
1176
- [ERROR_Y]: "y values must be a valid number."
1061
+ [ERROR_MISSING$1]: "Must be an array of { color, position } objects.",
1062
+ [ERROR_POSITION]: "Expected number 0-1, received {{ value }}.",
1063
+ [ERROR_INVALID_PROP$7]: "Unknown property {{ key }}."
1177
1064
  },
1178
1065
  docs: {
1179
- description: "Require cubicBezier tokens to follow the format.",
1180
- url: docsLink(VALID_CUBIC_BEZIER)
1066
+ description: "Require gradient tokens to follow the format.",
1067
+ url: docsLink(VALID_GRADIENT)
1181
1068
  }
1182
1069
  },
1183
1070
  defaultOptions: {},
1184
1071
  create({ tokens, report }) {
1185
1072
  for (const t of Object.values(tokens)) {
1186
- if (t.aliasOf || !t.originalValue) continue;
1187
- switch (t.$type) {
1188
- case "cubicBezier":
1189
- validateCubicBezier(t.originalValue.$value, {
1190
- node: getObjMember(t.source.node, "$value"),
1191
- filename: t.source.filename
1073
+ if (t.aliasOf || !t.originalValue || t.$type !== "gradient") continue;
1074
+ validateGradient(t.originalValue.$value, {
1075
+ node: getObjMember(t.source.node, "$value"),
1076
+ filename: t.source.filename
1077
+ });
1078
+ function validateGradient(value, { node, filename }) {
1079
+ if (Array.isArray(value)) for (let i = 0; i < value.length; i++) {
1080
+ const stop = value[i];
1081
+ if (!stop || typeof stop !== "object") {
1082
+ report({
1083
+ messageId: ERROR_MISSING$1,
1084
+ node,
1085
+ filename
1086
+ });
1087
+ continue;
1088
+ }
1089
+ for (const property of GRADIENT_REQUIRED_STOP_PROPERTIES) if (!(property in stop)) report({
1090
+ messageId: ERROR_MISSING$1,
1091
+ node: node.elements[i],
1092
+ filename
1192
1093
  });
1193
- break;
1194
- case "transition": if (typeof t.originalValue.$value === "object" && t.originalValue.$value.timingFunction && !isAlias(t.originalValue.$value.timingFunction)) {
1195
- const $valueNode = getObjMember(t.source.node, "$value");
1196
- validateCubicBezier(t.originalValue.$value.timingFunction, {
1197
- node: getObjMember($valueNode, "timingFunction"),
1198
- filename: t.source.filename
1094
+ for (const key of Object.keys(stop)) if (!GRADIENT_REQUIRED_STOP_PROPERTIES.includes(key)) report({
1095
+ messageId: ERROR_INVALID_PROP$7,
1096
+ data: { key: JSON.stringify(key) },
1097
+ node: node.elements[i],
1098
+ filename
1099
+ });
1100
+ if ("position" in stop && typeof stop.position !== "number" && !isAlias(stop.position)) report({
1101
+ messageId: ERROR_POSITION,
1102
+ data: { value: stop.position },
1103
+ node: getObjMember(node.elements[i].value, "position"),
1104
+ filename
1199
1105
  });
1200
1106
  }
1107
+ else report({
1108
+ messageId: ERROR_MISSING$1,
1109
+ node,
1110
+ filename
1111
+ });
1201
1112
  }
1202
- function validateCubicBezier(value, { node, filename }) {
1203
- if (Array.isArray(value) && value.length === 4) {
1204
- for (const pos of [0, 2]) {
1205
- if (isAlias(value[pos]) || isPure$ref(value[pos])) continue;
1206
- if (!(value[pos] >= 0 && value[pos] <= 1)) report({
1207
- messageId: ERROR_X,
1208
- node: node.elements[pos],
1209
- filename
1210
- });
1211
- }
1212
- for (const pos of [1, 3]) {
1213
- if (isAlias(value[pos]) || isPure$ref(value[pos])) continue;
1214
- if (typeof value[pos] !== "number") report({
1215
- messageId: ERROR_Y,
1216
- node: node.elements[pos],
1217
- filename
1218
- });
1219
- }
1220
- } else report({
1113
+ }
1114
+ }
1115
+ };
1116
+ var valid_gradient_default = rule$13;
1117
+
1118
+ //#endregion
1119
+ //#region src/lint/plugin-core/rules/valid-link.ts
1120
+ const VALID_LINK = "core/valid-link";
1121
+ const ERROR$7 = "ERROR";
1122
+ const rule$12 = {
1123
+ meta: {
1124
+ messages: { [ERROR$7]: "Must be a string." },
1125
+ docs: {
1126
+ description: "Require link tokens to follow the Terrazzo extension.",
1127
+ url: docsLink(VALID_LINK)
1128
+ }
1129
+ },
1130
+ defaultOptions: {},
1131
+ create({ tokens, report }) {
1132
+ for (const t of Object.values(tokens)) {
1133
+ if (t.aliasOf || !t.originalValue || t.$type !== "link") continue;
1134
+ validateLink(t.originalValue.$value, {
1135
+ node: getObjMember(t.source.node, "$value"),
1136
+ filename: t.source.filename
1137
+ });
1138
+ function validateLink(value, { node, filename }) {
1139
+ if (!value || typeof value !== "string") report({
1221
1140
  messageId: ERROR$7,
1222
1141
  node,
1223
1142
  filename
@@ -1226,160 +1145,44 @@ const rule$12 = {
1226
1145
  }
1227
1146
  }
1228
1147
  };
1229
- var valid_cubic_bezier_default = rule$12;
1148
+ var valid_link_default = rule$12;
1230
1149
 
1231
1150
  //#endregion
1232
- //#region src/lint/plugin-core/rules/valid-dimension.ts
1233
- const VALID_DIMENSION = "core/valid-dimension";
1234
- const ERROR_FORMAT$1 = "ERROR_FORMAT";
1235
- const ERROR_INVALID_PROP$5 = "ERROR_INVALID_PROP";
1236
- const ERROR_LEGACY$1 = "ERROR_LEGACY";
1237
- const ERROR_UNIT$1 = "ERROR_UNIT";
1238
- const ERROR_VALUE$1 = "ERROR_VALUE";
1151
+ //#region src/lint/plugin-core/rules/valid-number.ts
1152
+ const VALID_NUMBER = "core/valid-number";
1153
+ const ERROR_NAN = "ERROR_NAN";
1239
1154
  const rule$11 = {
1240
1155
  meta: {
1241
- messages: {
1242
- [ERROR_FORMAT$1]: "Invalid dimension: {{ value }}. Expected object with \"value\" and \"unit\".",
1243
- [ERROR_LEGACY$1]: "Migrate to the new object format: { \"value\": 10, \"unit\": \"px\" }.",
1244
- [ERROR_UNIT$1]: "Unit {{ unit }} not allowed. Expected {{ allowed }}.",
1245
- [ERROR_INVALID_PROP$5]: "Unknown property {{ key }}.",
1246
- [ERROR_VALUE$1]: "Expected number, received {{ value }}."
1247
- },
1156
+ messages: { [ERROR_NAN]: "Must be a number." },
1248
1157
  docs: {
1249
- description: "Require dimension tokens to follow the format",
1250
- url: docsLink(VALID_DIMENSION)
1158
+ description: "Require number tokens to follow the format.",
1159
+ url: docsLink(VALID_NUMBER)
1251
1160
  }
1252
1161
  },
1253
- defaultOptions: {
1254
- legacyFormat: false,
1255
- allowedUnits: [
1256
- "px",
1257
- "em",
1258
- "rem"
1259
- ]
1260
- },
1261
- create({ tokens, options, report }) {
1162
+ defaultOptions: {},
1163
+ create({ tokens, report }) {
1262
1164
  for (const t of Object.values(tokens)) {
1263
1165
  if (t.aliasOf || !t.originalValue) continue;
1264
1166
  switch (t.$type) {
1265
- case "dimension":
1266
- validateDimension(t.originalValue.$value, {
1167
+ case "number":
1168
+ validateNumber(t.originalValue.$value, {
1267
1169
  node: getObjMember(t.source.node, "$value"),
1268
1170
  filename: t.source.filename
1269
1171
  });
1270
1172
  break;
1271
- case "strokeStyle":
1272
- if (typeof t.originalValue.$value === "object" && Array.isArray(t.originalValue.$value.dashArray)) {
1273
- const dashArray = getObjMember(getObjMember(t.source.node, "$value"), "dashArray");
1274
- for (let i = 0; i < t.originalValue.$value.dashArray.length; i++) {
1275
- if (isAlias(t.originalValue.$value.dashArray[i])) continue;
1276
- validateDimension(t.originalValue.$value.dashArray[i], {
1277
- node: dashArray.elements[i].value,
1278
- filename: t.source.filename
1279
- });
1280
- }
1281
- }
1282
- break;
1283
- case "border": {
1173
+ case "typography": {
1284
1174
  const $valueNode = getObjMember(t.source.node, "$value");
1285
1175
  if (typeof t.originalValue.$value === "object") {
1286
- if (t.originalValue.$value.width && !isAlias(t.originalValue.$value.width)) validateDimension(t.originalValue.$value.width, {
1287
- node: getObjMember($valueNode, "width"),
1176
+ if (t.originalValue.$value.lineHeight && !isAlias(t.originalValue.$value.lineHeight) && typeof t.originalValue.$value.lineHeight !== "object") validateNumber(t.originalValue.$value.lineHeight, {
1177
+ node: getObjMember($valueNode, "lineHeight"),
1288
1178
  filename: t.source.filename
1289
1179
  });
1290
- if (typeof t.originalValue.$value.style === "object" && Array.isArray(t.originalValue.$value.style.dashArray)) {
1291
- const dashArray = getObjMember(getObjMember($valueNode, "style"), "dashArray");
1292
- for (let i = 0; i < t.originalValue.$value.style.dashArray.length; i++) {
1293
- if (isAlias(t.originalValue.$value.style.dashArray[i])) continue;
1294
- validateDimension(t.originalValue.$value.style.dashArray[i], {
1295
- node: dashArray.elements[i].value,
1296
- filename: t.source.filename
1297
- });
1298
- }
1299
- }
1300
- }
1301
- break;
1302
- }
1303
- case "shadow":
1304
- if (t.originalValue.$value && typeof t.originalValue.$value === "object") {
1305
- const $valueNode = getObjMember(t.source.node, "$value");
1306
- const valueArray = Array.isArray(t.originalValue.$value) ? t.originalValue.$value : [t.originalValue.$value];
1307
- for (let i = 0; i < valueArray.length; i++) {
1308
- const node = $valueNode.type === "Array" ? $valueNode.elements[i].value : $valueNode;
1309
- for (const property of [
1310
- "offsetX",
1311
- "offsetY",
1312
- "blur",
1313
- "spread"
1314
- ]) {
1315
- if (isAlias(valueArray[i][property])) continue;
1316
- validateDimension(valueArray[i][property], {
1317
- node: getObjMember(node, property),
1318
- filename: t.source.filename
1319
- });
1320
- }
1321
- }
1322
- }
1323
- break;
1324
- case "typography": {
1325
- const $valueNode = getObjMember(t.source.node, "$value");
1326
- if (typeof t.originalValue.$value === "object") {
1327
- for (const property of [
1328
- "fontSize",
1329
- "lineHeight",
1330
- "letterSpacing"
1331
- ]) if (property in t.originalValue.$value) {
1332
- if (isAlias(t.originalValue.$value[property]) || property === "lineHeight" && typeof t.originalValue.$value[property] === "number") continue;
1333
- validateDimension(t.originalValue.$value[property], {
1334
- node: getObjMember($valueNode, property),
1335
- filename: t.source.filename
1336
- });
1337
- }
1338
1180
  }
1339
- break;
1340
1181
  }
1341
1182
  }
1342
- function validateDimension(value, { node, filename }) {
1343
- if (value && typeof value === "object") {
1344
- for (const key of Object.keys(value)) if (!["value", "unit"].includes(key)) report({
1345
- messageId: ERROR_INVALID_PROP$5,
1346
- data: { key: JSON.stringify(key) },
1347
- node: getObjMember(node, key) ?? node,
1348
- filename
1349
- });
1350
- const { unit, value: numValue } = value;
1351
- if (!("value" in value || "unit" in value)) {
1352
- report({
1353
- messageId: ERROR_FORMAT$1,
1354
- data: { value },
1355
- node,
1356
- filename
1357
- });
1358
- return;
1359
- }
1360
- if (!options.allowedUnits.includes(unit)) report({
1361
- messageId: ERROR_UNIT$1,
1362
- data: {
1363
- unit,
1364
- allowed: new Intl.ListFormat("en-us", { type: "disjunction" }).format(options.allowedUnits)
1365
- },
1366
- node: getObjMember(node, "unit") ?? node,
1367
- filename
1368
- });
1369
- if (!Number.isFinite(numValue)) report({
1370
- messageId: ERROR_VALUE$1,
1371
- data: { value },
1372
- node: getObjMember(node, "value") ?? node,
1373
- filename
1374
- });
1375
- } else if (typeof value === "string" && !options.legacyFormat) report({
1376
- messageId: ERROR_LEGACY$1,
1377
- node,
1378
- filename
1379
- });
1380
- else report({
1381
- messageId: ERROR_FORMAT$1,
1382
- data: { value },
1183
+ function validateNumber(value, { node, filename }) {
1184
+ if (typeof value !== "number" || Number.isNaN(value)) report({
1185
+ messageId: ERROR_NAN,
1383
1186
  node,
1384
1187
  filename
1385
1188
  });
@@ -1387,150 +1190,74 @@ const rule$11 = {
1387
1190
  }
1388
1191
  }
1389
1192
  };
1390
- var valid_dimension_default = rule$11;
1193
+ var valid_number_default = rule$11;
1391
1194
 
1392
1195
  //#endregion
1393
- //#region src/lint/plugin-core/rules/valid-duration.ts
1394
- const VALID_DURATION = "core/valid-duration";
1395
- const ERROR_FORMAT = "ERROR_FORMAT";
1396
- const ERROR_INVALID_PROP$4 = "ERROR_INVALID_PROP";
1397
- const ERROR_LEGACY = "ERROR_LEGACY";
1398
- const ERROR_UNIT = "ERROR_UNIT";
1399
- const ERROR_VALUE = "ERROR_VALUE";
1196
+ //#region src/lint/plugin-core/rules/valid-shadow.ts
1197
+ const VALID_SHADOW = "core/valid-shadow";
1198
+ const ERROR$6 = "ERROR";
1199
+ const ERROR_INVALID_PROP$6 = "ERROR_INVALID_PROP";
1400
1200
  const rule$10 = {
1401
1201
  meta: {
1402
1202
  messages: {
1403
- [ERROR_FORMAT]: "Migrate to the new object format: { \"value\": 2, \"unit\": \"ms\" }.",
1404
- [ERROR_LEGACY]: "Migrate to the new object format: { \"value\": 10, \"unit\": \"px\" }.",
1405
- [ERROR_INVALID_PROP$4]: "Unknown property: {{ key }}.",
1406
- [ERROR_UNIT]: "Unknown unit {{ unit }}. Expected \"ms\" or \"s\".",
1407
- [ERROR_VALUE]: "Expected number, received {{ value }}."
1203
+ [ERROR$6]: `Missing required properties: ${new Intl.ListFormat("en-us", { type: "conjunction" }).format(SHADOW_REQUIRED_PROPERTIES)}.`,
1204
+ [ERROR_INVALID_PROP$6]: "Unknown property {{ key }}."
1408
1205
  },
1409
1206
  docs: {
1410
- description: "Require duration tokens to follow the format",
1411
- url: docsLink(VALID_DURATION)
1207
+ description: "Require shadow tokens to follow the format.",
1208
+ url: docsLink(VALID_SHADOW)
1412
1209
  }
1413
1210
  },
1414
- defaultOptions: {
1415
- legacyFormat: false,
1416
- unknownUnits: false
1417
- },
1418
- create({ tokens, options, report }) {
1211
+ defaultOptions: {},
1212
+ create({ tokens, report }) {
1419
1213
  for (const t of Object.values(tokens)) {
1420
- if (t.aliasOf || !t.originalValue) continue;
1421
- switch (t.$type) {
1422
- case "duration":
1423
- validateDuration(t.originalValue.$value, {
1424
- node: getObjMember(t.source.node, "$value"),
1425
- filename: t.source.filename
1426
- });
1427
- break;
1428
- case "transition":
1429
- if (typeof t.originalValue.$value === "object") {
1430
- const $valueNode = getObjMember(t.source.node, "$value");
1431
- for (const property of ["duration", "delay"]) if (t.originalValue.$value[property] && !isAlias(t.originalValue.$value[property])) validateDuration(t.originalValue.$value[property], {
1432
- node: getObjMember($valueNode, property),
1433
- filename: t.source.filename
1434
- });
1435
- }
1436
- break;
1437
- }
1438
- function validateDuration(value, { node, filename }) {
1439
- if (value && typeof value === "object") {
1440
- for (const key of Object.keys(value)) if (!["value", "unit"].includes(key)) report({
1441
- messageId: ERROR_INVALID_PROP$4,
1442
- data: { key: JSON.stringify(key) },
1443
- node: getObjMember(node, key) ?? node,
1444
- filename
1445
- });
1446
- const { unit, value: numValue } = value;
1447
- if (!("value" in value || "unit" in value)) {
1448
- report({
1449
- messageId: ERROR_FORMAT,
1450
- data: { value },
1451
- node,
1452
- filename
1453
- });
1454
- return;
1455
- }
1456
- if (!options.unknownUnits && !["ms", "s"].includes(unit)) report({
1457
- messageId: ERROR_UNIT,
1458
- data: { unit },
1459
- node: getObjMember(node, "unit") ?? node,
1460
- filename
1461
- });
1462
- if (!Number.isFinite(numValue)) report({
1463
- messageId: ERROR_VALUE,
1464
- data: { value },
1465
- node: getObjMember(node, "value") ?? node,
1466
- filename
1467
- });
1468
- } else if (typeof value === "string" && !options.legacyFormat) report({
1469
- messageId: ERROR_FORMAT,
1214
+ if (t.aliasOf || !t.originalValue || t.$type !== "shadow") continue;
1215
+ validateShadow(t.originalValue.$value, {
1216
+ node: getObjMember(t.source.node, "$value"),
1217
+ filename: t.source.filename
1218
+ });
1219
+ function validateShadow(value, { node, filename }) {
1220
+ const wrappedValue = Array.isArray(value) ? value : [value];
1221
+ 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({
1222
+ messageId: ERROR$6,
1470
1223
  node,
1471
1224
  filename
1472
1225
  });
1473
- else report({
1474
- messageId: ERROR_FORMAT,
1475
- data: { value },
1476
- node,
1226
+ else for (const key of Object.keys(wrappedValue[i])) if (![...SHADOW_REQUIRED_PROPERTIES, "inset"].includes(key)) report({
1227
+ messageId: ERROR_INVALID_PROP$6,
1228
+ data: { key: JSON.stringify(key) },
1229
+ node: getObjMember(node.type === "Array" ? node.elements[i].value : node, key),
1477
1230
  filename
1478
1231
  });
1479
1232
  }
1480
1233
  }
1481
1234
  }
1482
1235
  };
1483
- var valid_duration_default = rule$10;
1236
+ var valid_shadow_default = rule$10;
1484
1237
 
1485
1238
  //#endregion
1486
- //#region src/lint/plugin-core/rules/valid-font-family.ts
1487
- const VALID_FONT_FAMILY = "core/valid-font-family";
1488
- const ERROR$6 = "ERROR";
1239
+ //#region src/lint/plugin-core/rules/valid-string.ts
1240
+ const VALID_STRING = "core/valid-string";
1241
+ const ERROR$5 = "ERROR";
1489
1242
  const rule$9 = {
1490
1243
  meta: {
1491
- messages: { [ERROR$6]: "Must be a string, or array of strings." },
1244
+ messages: { [ERROR$5]: "Must be a string." },
1492
1245
  docs: {
1493
- description: "Require fontFamily tokens to follow the format.",
1494
- url: docsLink(VALID_FONT_FAMILY)
1246
+ description: "Require string tokens to follow the Terrazzo extension.",
1247
+ url: docsLink(VALID_STRING)
1495
1248
  }
1496
1249
  },
1497
1250
  defaultOptions: {},
1498
1251
  create({ tokens, report }) {
1499
1252
  for (const t of Object.values(tokens)) {
1500
- if (t.aliasOf || !t.originalValue) continue;
1501
- switch (t.$type) {
1502
- case "fontFamily":
1503
- validateFontFamily(t.originalValue.$value, {
1504
- node: getObjMember(t.source.node, "$value"),
1505
- filename: t.source.filename
1506
- });
1507
- break;
1508
- case "typography":
1509
- if (typeof t.originalValue.$value === "object" && t.originalValue.$value.fontFamily) {
1510
- if (t.partialAliasOf?.fontFamily) continue;
1511
- const properties = getObjMembers(getObjMember(t.source.node, "$value"));
1512
- validateFontFamily(t.originalValue.$value.fontFamily, {
1513
- node: properties.fontFamily,
1514
- filename: t.source.filename
1515
- });
1516
- }
1517
- break;
1518
- }
1519
- function validateFontFamily(value, { node, filename }) {
1520
- if (typeof value === "string") {
1521
- if (!value) report({
1522
- messageId: ERROR$6,
1523
- node,
1524
- filename
1525
- });
1526
- } else if (Array.isArray(value)) {
1527
- if (!value.every((v) => v && typeof v === "string")) report({
1528
- messageId: ERROR$6,
1529
- node,
1530
- filename
1531
- });
1532
- } else report({
1533
- messageId: ERROR$6,
1253
+ if (t.aliasOf || !t.originalValue || t.$type !== "string") continue;
1254
+ validateString(t.originalValue.$value, {
1255
+ node: getObjMember(t.source.node, "$value"),
1256
+ filename: t.source.filename
1257
+ });
1258
+ function validateString(value, { node, filename }) {
1259
+ if (typeof value !== "string") report({
1260
+ messageId: ERROR$5,
1534
1261
  node,
1535
1262
  filename
1536
1263
  });
@@ -1538,79 +1265,83 @@ const rule$9 = {
1538
1265
  }
1539
1266
  }
1540
1267
  };
1541
- var valid_font_family_default = rule$9;
1268
+ var valid_string_default = rule$9;
1542
1269
 
1543
1270
  //#endregion
1544
- //#region src/lint/plugin-core/rules/valid-font-weight.ts
1545
- const VALID_FONT_WEIGHT = "core/valid-font-weight";
1546
- const ERROR$5 = "ERROR";
1547
- const ERROR_STYLE = "ERROR_STYLE";
1271
+ //#region src/lint/plugin-core/rules/valid-stroke-style.ts
1272
+ const VALID_STROKE_STYLE = "core/valid-stroke-style";
1273
+ const ERROR_STR = "ERROR_STR";
1274
+ const ERROR_OBJ = "ERROR_OBJ";
1275
+ const ERROR_LINE_CAP = "ERROR_LINE_CAP";
1276
+ const ERROR_INVALID_PROP$5 = "ERROR_INVALID_PROP";
1548
1277
  const rule$8 = {
1549
1278
  meta: {
1550
1279
  messages: {
1551
- [ERROR$5]: `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))}.`,
1552
- [ERROR_STYLE]: "Expected style {{ style }}, received {{ value }}."
1280
+ [ERROR_STR]: `Value most be one of ${new Intl.ListFormat("en-us", { type: "disjunction" }).format(STROKE_STYLE_STRING_VALUES)}.`,
1281
+ [ERROR_OBJ]: `Missing required properties: ${new Intl.ListFormat("en-us", { type: "conjunction" }).format(TRANSITION_REQUIRED_PROPERTIES)}.`,
1282
+ [ERROR_LINE_CAP]: `lineCap must be one of ${new Intl.ListFormat("en-us", { type: "disjunction" }).format(STROKE_STYLE_LINE_CAP_VALUES)}.`,
1283
+ [ERROR_INVALID_PROP$5]: "Unknown property: {{ key }}."
1553
1284
  },
1554
1285
  docs: {
1555
- description: "Require number tokens to follow the format.",
1556
- url: docsLink(VALID_FONT_WEIGHT)
1286
+ description: "Require strokeStyle tokens to follow the format.",
1287
+ url: docsLink(VALID_STROKE_STYLE)
1557
1288
  }
1558
1289
  },
1559
- defaultOptions: { style: void 0 },
1560
- create({ tokens, options, report }) {
1290
+ defaultOptions: {},
1291
+ create({ tokens, report }) {
1561
1292
  for (const t of Object.values(tokens)) {
1562
1293
  if (t.aliasOf || !t.originalValue) continue;
1563
1294
  switch (t.$type) {
1564
- case "fontWeight":
1565
- validateFontWeight(t.originalValue.$value, {
1295
+ case "strokeStyle":
1296
+ validateStrokeStyle(t.originalValue.$value, {
1566
1297
  node: getObjMember(t.source.node, "$value"),
1567
1298
  filename: t.source.filename
1568
1299
  });
1569
1300
  break;
1570
- case "typography":
1571
- if (typeof t.originalValue.$value === "object" && t.originalValue.$value.fontWeight) {
1572
- if (t.partialAliasOf?.fontWeight) continue;
1573
- const properties = getObjMembers(getObjMember(t.source.node, "$value"));
1574
- validateFontWeight(t.originalValue.$value.fontWeight, {
1575
- node: properties.fontWeight,
1301
+ case "border":
1302
+ if (t.originalValue.$value && typeof t.originalValue.$value === "object") {
1303
+ const $valueNode = getObjMember(t.source.node, "$value");
1304
+ if (t.originalValue.$value.style) validateStrokeStyle(t.originalValue.$value.style, {
1305
+ node: getObjMember($valueNode, "style"),
1576
1306
  filename: t.source.filename
1577
1307
  });
1578
1308
  }
1579
1309
  break;
1580
1310
  }
1581
- function validateFontWeight(value, { node, filename }) {
1311
+ function validateStrokeStyle(value, { node, filename }) {
1582
1312
  if (typeof value === "string") {
1583
- if (options.style === "numbers") report({
1584
- messageId: ERROR_STYLE,
1585
- data: {
1586
- style: "numbers",
1587
- value
1588
- },
1313
+ if (!isAlias(value) && !STROKE_STYLE_STRING_VALUES.includes(value)) {
1314
+ report({
1315
+ messageId: ERROR_STR,
1316
+ node,
1317
+ filename
1318
+ });
1319
+ return;
1320
+ }
1321
+ } else if (value && typeof value === "object") {
1322
+ if (!STROKE_STYLE_OBJECT_REQUIRED_PROPERTIES.every((property) => property in value)) report({
1323
+ messageId: ERROR_OBJ,
1589
1324
  node,
1590
1325
  filename
1591
1326
  });
1592
- else if (!(value in FONT_WEIGHTS)) report({
1593
- messageId: ERROR$5,
1594
- node,
1327
+ if (!Array.isArray(value.dashArray)) report({
1328
+ messageId: ERROR_OBJ,
1329
+ node: getObjMember(node, "dashArray"),
1595
1330
  filename
1596
1331
  });
1597
- } else if (typeof value === "number") {
1598
- if (options.style === "names") report({
1599
- messageId: ERROR_STYLE,
1600
- data: {
1601
- style: "names",
1602
- value
1603
- },
1604
- node,
1332
+ if (!STROKE_STYLE_LINE_CAP_VALUES.includes(value.lineCap)) report({
1333
+ messageId: ERROR_OBJ,
1334
+ node: getObjMember(node, "lineCap"),
1605
1335
  filename
1606
1336
  });
1607
- else if (!(value >= 0 && value < 1e3)) report({
1608
- messageId: ERROR$5,
1609
- node,
1337
+ for (const key of Object.keys(value)) if (!["dashArray", "lineCap"].includes(key)) report({
1338
+ messageId: ERROR_INVALID_PROP$5,
1339
+ data: { key: JSON.stringify(key) },
1340
+ node: getObjMember(node, key),
1610
1341
  filename
1611
1342
  });
1612
1343
  } else report({
1613
- messageId: ERROR$5,
1344
+ messageId: ERROR_OBJ,
1614
1345
  node,
1615
1346
  filename
1616
1347
  });
@@ -1618,97 +1349,92 @@ const rule$8 = {
1618
1349
  }
1619
1350
  }
1620
1351
  };
1621
- var valid_font_weight_default = rule$8;
1352
+ var valid_stroke_style_default = rule$8;
1622
1353
 
1623
1354
  //#endregion
1624
- //#region src/lint/plugin-core/rules/valid-gradient.ts
1625
- const VALID_GRADIENT = "core/valid-gradient";
1626
- const ERROR_MISSING$1 = "ERROR_MISSING";
1627
- const ERROR_POSITION = "ERROR_POSITION";
1628
- const ERROR_INVALID_PROP$3 = "ERROR_INVALID_PROP";
1355
+ //#region src/lint/plugin-core/rules/valid-transition.ts
1356
+ const VALID_TRANSITION = "core/valid-transition";
1357
+ const ERROR$4 = "ERROR";
1358
+ const ERROR_INVALID_PROP$4 = "ERROR_INVALID_PROP";
1629
1359
  const rule$7 = {
1630
1360
  meta: {
1631
1361
  messages: {
1632
- [ERROR_MISSING$1]: "Must be an array of { color, position } objects.",
1633
- [ERROR_POSITION]: "Expected number 0-1, received {{ value }}.",
1634
- [ERROR_INVALID_PROP$3]: "Unknown property {{ key }}."
1362
+ [ERROR$4]: `Missing required properties: ${new Intl.ListFormat("en-us", { type: "conjunction" }).format(TRANSITION_REQUIRED_PROPERTIES)}.`,
1363
+ [ERROR_INVALID_PROP$4]: "Unknown property: {{ key }}."
1635
1364
  },
1636
1365
  docs: {
1637
- description: "Require gradient tokens to follow the format.",
1638
- url: docsLink(VALID_GRADIENT)
1366
+ description: "Require transition tokens to follow the format.",
1367
+ url: docsLink(VALID_TRANSITION)
1639
1368
  }
1640
1369
  },
1641
1370
  defaultOptions: {},
1642
1371
  create({ tokens, report }) {
1643
1372
  for (const t of Object.values(tokens)) {
1644
- if (t.aliasOf || !t.originalValue || t.$type !== "gradient") continue;
1645
- validateGradient(t.originalValue.$value, {
1373
+ if (t.aliasOf || !t.originalValue || t.$type !== "transition") continue;
1374
+ validateTransition(t.originalValue.$value, {
1646
1375
  node: getObjMember(t.source.node, "$value"),
1647
1376
  filename: t.source.filename
1648
1377
  });
1649
- function validateGradient(value, { node, filename }) {
1650
- if (Array.isArray(value)) for (let i = 0; i < value.length; i++) {
1651
- const stop = value[i];
1652
- if (!stop || typeof stop !== "object") {
1653
- report({
1654
- messageId: ERROR_MISSING$1,
1655
- node,
1656
- filename
1657
- });
1658
- continue;
1659
- }
1660
- for (const property of GRADIENT_REQUIRED_STOP_PROPERTIES) if (!(property in stop)) report({
1661
- messageId: ERROR_MISSING$1,
1662
- node: node.elements[i],
1663
- filename
1664
- });
1665
- for (const key of Object.keys(stop)) if (!GRADIENT_REQUIRED_STOP_PROPERTIES.includes(key)) report({
1666
- messageId: ERROR_INVALID_PROP$3,
1667
- data: { key: JSON.stringify(key) },
1668
- node: node.elements[i],
1669
- filename
1670
- });
1671
- if ("position" in stop && typeof stop.position !== "number" && !isAlias(stop.position)) report({
1672
- messageId: ERROR_POSITION,
1673
- data: { value: stop.position },
1674
- node: getObjMember(node.elements[i].value, "position"),
1675
- filename
1676
- });
1677
- }
1678
- else report({
1679
- messageId: ERROR_MISSING$1,
1680
- node,
1681
- filename
1682
- });
1683
- }
1378
+ }
1379
+ function validateTransition(value, { node, filename }) {
1380
+ if (!value || typeof value !== "object" || !TRANSITION_REQUIRED_PROPERTIES.every((property) => property in value)) report({
1381
+ messageId: ERROR$4,
1382
+ node,
1383
+ filename
1384
+ });
1385
+ else for (const key of Object.keys(value)) if (!TRANSITION_REQUIRED_PROPERTIES.includes(key)) report({
1386
+ messageId: ERROR_INVALID_PROP$4,
1387
+ data: { key: JSON.stringify(key) },
1388
+ node: getObjMember(node, key),
1389
+ filename
1390
+ });
1684
1391
  }
1685
1392
  }
1686
1393
  };
1687
- var valid_gradient_default = rule$7;
1394
+ var valid_transition_default = rule$7;
1688
1395
 
1689
1396
  //#endregion
1690
- //#region src/lint/plugin-core/rules/valid-link.ts
1691
- const VALID_LINK = "core/valid-link";
1692
- const ERROR$4 = "ERROR";
1397
+ //#region src/lint/plugin-core/rules/valid-typography.ts
1398
+ const VALID_TYPOGRAPHY = "core/valid-typography";
1399
+ const ERROR$3 = "ERROR";
1400
+ const ERROR_MISSING = "ERROR_MISSING";
1693
1401
  const rule$6 = {
1694
1402
  meta: {
1695
- messages: { [ERROR$4]: "Must be a string." },
1403
+ messages: {
1404
+ [ERROR$3]: `Expected object, received {{ value }}.`,
1405
+ [ERROR_MISSING]: `Missing required property "{{ property }}".`
1406
+ },
1696
1407
  docs: {
1697
- description: "Require link tokens to follow the Terrazzo extension.",
1698
- url: docsLink(VALID_LINK)
1408
+ description: "Require typography tokens to follow the format.",
1409
+ url: docsLink(VALID_TYPOGRAPHY)
1699
1410
  }
1700
1411
  },
1701
- defaultOptions: {},
1702
- create({ tokens, report }) {
1412
+ defaultOptions: { requiredProperties: [
1413
+ "fontFamily",
1414
+ "fontSize",
1415
+ "fontWeight",
1416
+ "letterSpacing",
1417
+ "lineHeight"
1418
+ ] },
1419
+ create({ tokens, options, report }) {
1420
+ const isIgnored = options.ignore ? wcmatch(options.ignore) : () => false;
1703
1421
  for (const t of Object.values(tokens)) {
1704
- if (t.aliasOf || !t.originalValue || t.$type !== "link") continue;
1705
- validateLink(t.originalValue.$value, {
1422
+ if (t.aliasOf || !t.originalValue || t.$type !== "typography" || isIgnored(t.id)) continue;
1423
+ validateTypography(t.originalValue.$value, {
1706
1424
  node: getObjMember(t.source.node, "$value"),
1707
1425
  filename: t.source.filename
1708
1426
  });
1709
- function validateLink(value, { node, filename }) {
1710
- if (!value || typeof value !== "string") report({
1711
- messageId: ERROR$4,
1427
+ function validateTypography(value, { node, filename }) {
1428
+ if (value && typeof value === "object") {
1429
+ for (const property of options.requiredProperties) if (!(property in value)) report({
1430
+ messageId: ERROR_MISSING,
1431
+ data: { property },
1432
+ node,
1433
+ filename
1434
+ });
1435
+ } else report({
1436
+ messageId: ERROR$3,
1437
+ data: { value: JSON.stringify(value) },
1712
1438
  node,
1713
1439
  filename
1714
1440
  });
@@ -1716,203 +1442,265 @@ const rule$6 = {
1716
1442
  }
1717
1443
  }
1718
1444
  };
1719
- var valid_link_default = rule$6;
1445
+ var valid_typography_default = rule$6;
1720
1446
 
1721
1447
  //#endregion
1722
- //#region src/lint/plugin-core/rules/valid-number.ts
1723
- const VALID_NUMBER = "core/valid-number";
1724
- const ERROR_NAN = "ERROR_NAN";
1448
+ //#region src/lint/plugin-core/rules/valid-boolean.ts
1449
+ const VALID_BOOLEAN = "core/valid-boolean";
1450
+ const ERROR$2 = "ERROR";
1725
1451
  const rule$5 = {
1726
1452
  meta: {
1727
- messages: { [ERROR_NAN]: "Must be a number." },
1453
+ messages: { [ERROR$2]: "Must be a boolean." },
1728
1454
  docs: {
1729
- description: "Require number tokens to follow the format.",
1730
- url: docsLink(VALID_NUMBER)
1455
+ description: "Require boolean tokens to follow the Terrazzo extension.",
1456
+ url: docsLink(VALID_BOOLEAN)
1731
1457
  }
1732
1458
  },
1733
1459
  defaultOptions: {},
1734
1460
  create({ tokens, report }) {
1735
1461
  for (const t of Object.values(tokens)) {
1736
- if (t.aliasOf || !t.originalValue) continue;
1737
- switch (t.$type) {
1738
- case "number":
1739
- validateNumber(t.originalValue.$value, {
1740
- node: getObjMember(t.source.node, "$value"),
1741
- filename: t.source.filename
1742
- });
1743
- break;
1744
- case "typography": {
1745
- const $valueNode = getObjMember(t.source.node, "$value");
1746
- if (typeof t.originalValue.$value === "object") {
1747
- if (t.originalValue.$value.lineHeight && !isAlias(t.originalValue.$value.lineHeight) && typeof t.originalValue.$value.lineHeight !== "object") validateNumber(t.originalValue.$value.lineHeight, {
1748
- node: getObjMember($valueNode, "lineHeight"),
1749
- filename: t.source.filename
1750
- });
1751
- }
1752
- }
1753
- }
1754
- function validateNumber(value, { node, filename }) {
1755
- if (typeof value !== "number" || Number.isNaN(value)) report({
1756
- messageId: ERROR_NAN,
1757
- node,
1758
- filename
1462
+ if (t.aliasOf || !t.originalValue || t.$type !== "boolean") continue;
1463
+ validateBoolean(t.originalValue.$value, {
1464
+ node: getObjMember(t.source.node, "$value"),
1465
+ filename: t.source.filename
1466
+ });
1467
+ function validateBoolean(value, { node, filename }) {
1468
+ if (typeof value !== "boolean") report({
1469
+ messageId: ERROR$2,
1470
+ filename,
1471
+ node
1759
1472
  });
1760
1473
  }
1761
1474
  }
1762
1475
  }
1763
1476
  };
1764
- var valid_number_default = rule$5;
1477
+ var valid_boolean_default = rule$5;
1765
1478
 
1766
1479
  //#endregion
1767
- //#region src/lint/plugin-core/rules/valid-shadow.ts
1768
- const VALID_SHADOW = "core/valid-shadow";
1769
- const ERROR$3 = "ERROR";
1770
- const ERROR_INVALID_PROP$2 = "ERROR_INVALID_PROP";
1480
+ //#region src/lint/plugin-core/rules/valid-border.ts
1481
+ const VALID_BORDER = "core/valid-border";
1482
+ const ERROR$1 = "ERROR";
1483
+ const ERROR_INVALID_PROP$3 = "ERROR_INVALID_PROP";
1771
1484
  const rule$4 = {
1772
1485
  meta: {
1773
1486
  messages: {
1774
- [ERROR$3]: `Missing required properties: ${new Intl.ListFormat("en-us", { type: "conjunction" }).format(SHADOW_REQUIRED_PROPERTIES)}.`,
1775
- [ERROR_INVALID_PROP$2]: "Unknown property {{ key }}."
1487
+ [ERROR$1]: `Border token missing required properties: ${new Intl.ListFormat("en-us", { type: "conjunction" }).format(BORDER_REQUIRED_PROPERTIES)}.`,
1488
+ [ERROR_INVALID_PROP$3]: "Unknown property: {{ key }}."
1776
1489
  },
1777
1490
  docs: {
1778
- description: "Require shadow tokens to follow the format.",
1779
- url: docsLink(VALID_SHADOW)
1491
+ description: "Require border tokens to follow the format.",
1492
+ url: docsLink(VALID_BORDER)
1780
1493
  }
1781
1494
  },
1782
1495
  defaultOptions: {},
1783
1496
  create({ tokens, report }) {
1784
1497
  for (const t of Object.values(tokens)) {
1785
- if (t.aliasOf || !t.originalValue || t.$type !== "shadow") continue;
1786
- validateShadow(t.originalValue.$value, {
1498
+ if (t.aliasOf || !t.originalValue || t.$type !== "border") continue;
1499
+ validateBorder(t.originalValue.$value, {
1787
1500
  node: getObjMember(t.source.node, "$value"),
1788
1501
  filename: t.source.filename
1789
1502
  });
1790
- function validateShadow(value, { node, filename }) {
1791
- const wrappedValue = Array.isArray(value) ? value : [value];
1792
- 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({
1793
- messageId: ERROR$3,
1794
- node,
1795
- filename
1796
- });
1797
- else for (const key of Object.keys(wrappedValue[i])) if (![...SHADOW_REQUIRED_PROPERTIES, "inset"].includes(key)) report({
1798
- messageId: ERROR_INVALID_PROP$2,
1799
- data: { key: JSON.stringify(key) },
1800
- node: getObjMember(node.type === "Array" ? node.elements[i].value : node, key),
1801
- filename
1802
- });
1803
- }
1804
- }
1805
- }
1806
- };
1807
- var valid_shadow_default = rule$4;
1808
-
1809
- //#endregion
1810
- //#region src/lint/plugin-core/rules/valid-string.ts
1811
- const VALID_STRING = "core/valid-string";
1812
- const ERROR$2 = "ERROR";
1813
- const rule$3 = {
1814
- meta: {
1815
- messages: { [ERROR$2]: "Must be a string." },
1816
- docs: {
1817
- description: "Require string tokens to follow the Terrazzo extension.",
1818
- url: docsLink(VALID_STRING)
1819
1503
  }
1820
- },
1821
- defaultOptions: {},
1822
- create({ tokens, report }) {
1823
- for (const t of Object.values(tokens)) {
1824
- if (t.aliasOf || !t.originalValue || t.$type !== "string") continue;
1825
- validateString(t.originalValue.$value, {
1826
- node: getObjMember(t.source.node, "$value"),
1827
- filename: t.source.filename
1504
+ function validateBorder(value, { node, filename }) {
1505
+ if (!value || typeof value !== "object" || !BORDER_REQUIRED_PROPERTIES.every((property) => property in value)) report({
1506
+ messageId: ERROR$1,
1507
+ filename,
1508
+ node
1509
+ });
1510
+ else for (const key of Object.keys(value)) if (!BORDER_REQUIRED_PROPERTIES.includes(key)) report({
1511
+ messageId: ERROR_INVALID_PROP$3,
1512
+ data: { key: JSON.stringify(key) },
1513
+ node: getObjMember(node, key) ?? node,
1514
+ filename
1828
1515
  });
1829
- function validateString(value, { node, filename }) {
1830
- if (typeof value !== "string") report({
1831
- messageId: ERROR$2,
1832
- node,
1833
- filename
1834
- });
1835
- }
1836
1516
  }
1837
1517
  }
1838
1518
  };
1839
- var valid_string_default = rule$3;
1519
+ var valid_border_default = rule$4;
1840
1520
 
1841
1521
  //#endregion
1842
- //#region src/lint/plugin-core/rules/valid-stroke-style.ts
1843
- const VALID_STROKE_STYLE = "core/valid-stroke-style";
1844
- const ERROR_STR = "ERROR_STR";
1845
- const ERROR_OBJ = "ERROR_OBJ";
1846
- const ERROR_LINE_CAP = "ERROR_LINE_CAP";
1847
- const ERROR_INVALID_PROP$1 = "ERROR_INVALID_PROP";
1848
- const rule$2 = {
1522
+ //#region src/lint/plugin-core/rules/valid-color.ts
1523
+ const VALID_COLOR = "core/valid-color";
1524
+ const ERROR_ALPHA = "ERROR_ALPHA";
1525
+ const ERROR_INVALID_COLOR = "ERROR_INVALID_COLOR";
1526
+ const ERROR_INVALID_COLOR_SPACE = "ERROR_INVALID_COLOR_SPACE";
1527
+ const ERROR_INVALID_COMPONENT_LENGTH = "ERROR_INVALID_COMPONENT_LENGTH";
1528
+ const ERROR_INVALID_HEX8 = "ERROR_INVALID_HEX8";
1529
+ const ERROR_INVALID_PROP$2 = "ERROR_INVALID_PROP";
1530
+ const ERROR_MISSING_COMPONENTS = "ERROR_MISSING_COMPONENTS";
1531
+ const ERROR_OBJ_FORMAT = "ERROR_OBJ_FORMAT";
1532
+ const ERROR_OUT_OF_RANGE = "ERROR_OUT_OF_RANGE";
1533
+ const rule$3 = {
1849
1534
  meta: {
1850
1535
  messages: {
1851
- [ERROR_STR]: `Value most be one of ${new Intl.ListFormat("en-us", { type: "disjunction" }).format(STROKE_STYLE_STRING_VALUES)}.`,
1852
- [ERROR_OBJ]: `Missing required properties: ${new Intl.ListFormat("en-us", { type: "conjunction" }).format(TRANSITION_REQUIRED_PROPERTIES)}.`,
1853
- [ERROR_LINE_CAP]: `lineCap must be one of ${new Intl.ListFormat("en-us", { type: "disjunction" }).format(STROKE_STYLE_LINE_CAP_VALUES)}.`,
1854
- [ERROR_INVALID_PROP$1]: "Unknown property: {{ key }}."
1536
+ [ERROR_ALPHA]: `Alpha {{ alpha }} not in range 0 1.`,
1537
+ [ERROR_INVALID_COLOR_SPACE]: `Invalid color space: {{ colorSpace }}. Expected ${new Intl.ListFormat("en-us", { type: "disjunction" }).format(Object.keys(COLORSPACE))}`,
1538
+ [ERROR_INVALID_COLOR]: `Could not parse color {{ color }}.`,
1539
+ [ERROR_INVALID_COMPONENT_LENGTH]: "Expected {{ expected }} components, received {{ got }}.",
1540
+ [ERROR_INVALID_HEX8]: `Hex value can’t be semi-transparent.`,
1541
+ [ERROR_INVALID_PROP$2]: `Unknown property {{ key }}.`,
1542
+ [ERROR_MISSING_COMPONENTS]: "Expected components to be array of numbers, received {{ got }}.",
1543
+ [ERROR_OBJ_FORMAT]: "Migrate to the new object format, e.g. \"#ff0000\" → { \"colorSpace\": \"srgb\", \"components\": [1, 0, 0] } }",
1544
+ [ERROR_OUT_OF_RANGE]: `Invalid range for color space {{ colorSpace }}. Expected {{ range }}.`
1855
1545
  },
1856
1546
  docs: {
1857
- description: "Require strokeStyle tokens to follow the format.",
1858
- url: docsLink(VALID_STROKE_STYLE)
1547
+ description: "Require color tokens to follow the format.",
1548
+ url: docsLink(VALID_COLOR)
1859
1549
  }
1860
1550
  },
1861
- defaultOptions: {},
1862
- create({ tokens, report }) {
1551
+ defaultOptions: {
1552
+ legacyFormat: false,
1553
+ ignoreRanges: false
1554
+ },
1555
+ create({ tokens, options, report }) {
1863
1556
  for (const t of Object.values(tokens)) {
1864
1557
  if (t.aliasOf || !t.originalValue) continue;
1865
1558
  switch (t.$type) {
1866
- case "strokeStyle":
1867
- validateStrokeStyle(t.originalValue.$value, {
1559
+ case "color":
1560
+ validateColor(t.originalValue.$value, {
1868
1561
  node: getObjMember(t.source.node, "$value"),
1869
1562
  filename: t.source.filename
1870
1563
  });
1871
1564
  break;
1872
1565
  case "border":
1873
- if (t.originalValue.$value && typeof t.originalValue.$value === "object") {
1874
- const $valueNode = getObjMember(t.source.node, "$value");
1875
- if (t.originalValue.$value.style) validateStrokeStyle(t.originalValue.$value.style, {
1876
- node: getObjMember($valueNode, "style"),
1877
- filename: t.source.filename
1878
- });
1566
+ if (t.originalValue.$value.color && !isAlias(t.originalValue.$value.color)) validateColor(t.originalValue.$value.color, {
1567
+ node: getObjMember(getObjMember(t.source.node, "$value"), "color"),
1568
+ filename: t.source.filename
1569
+ });
1570
+ break;
1571
+ case "gradient": {
1572
+ const $valueNode = getObjMember(t.source.node, "$value");
1573
+ for (let i = 0; i < t.originalValue.$value.length; i++) {
1574
+ const stop = t.originalValue.$value[i];
1575
+ if (!stop.color || isAlias(stop.color)) continue;
1576
+ validateColor(stop.color, {
1577
+ node: getObjMember($valueNode.elements[i].value, "color"),
1578
+ filename: t.source.filename
1579
+ });
1580
+ }
1581
+ break;
1582
+ }
1583
+ case "shadow": {
1584
+ const $value = Array.isArray(t.originalValue.$value) ? t.originalValue.$value : [t.originalValue.$value];
1585
+ const $valueNode = getObjMember(t.source.node, "$value");
1586
+ for (let i = 0; i < $value.length; i++) {
1587
+ const layer = $value[i];
1588
+ if (!layer.color || isAlias(layer.color)) continue;
1589
+ validateColor(layer.color, {
1590
+ node: $valueNode.type === "Object" ? getObjMember($valueNode, "color") : getObjMember($valueNode.elements[i].value, "color"),
1591
+ filename: t.source.filename
1592
+ });
1879
1593
  }
1880
1594
  break;
1595
+ }
1881
1596
  }
1882
- function validateStrokeStyle(value, { node, filename }) {
1883
- if (typeof value === "string") {
1884
- if (!isAlias(value) && !STROKE_STYLE_STRING_VALUES.includes(value)) {
1597
+ function validateColor(value, { node, filename }) {
1598
+ if (!value) report({
1599
+ messageId: ERROR_INVALID_COLOR,
1600
+ data: { color: JSON.stringify(value) },
1601
+ node,
1602
+ filename
1603
+ });
1604
+ else if (typeof value === "object") {
1605
+ for (const key of Object.keys(value)) if (![
1606
+ "colorSpace",
1607
+ "components",
1608
+ "channels",
1609
+ "hex",
1610
+ "alpha"
1611
+ ].includes(key)) report({
1612
+ messageId: ERROR_INVALID_PROP$2,
1613
+ data: { key: JSON.stringify(key) },
1614
+ node: getObjMember(node, key) ?? node,
1615
+ filename
1616
+ });
1617
+ const colorSpace = "colorSpace" in value && typeof value.colorSpace === "string" ? value.colorSpace : void 0;
1618
+ const csData = COLORSPACE[colorSpace] || void 0;
1619
+ if (!("colorSpace" in value) || !csData) {
1885
1620
  report({
1886
- messageId: ERROR_STR,
1887
- node,
1621
+ messageId: ERROR_INVALID_COLOR_SPACE,
1622
+ data: { colorSpace },
1623
+ node: getObjMember(node, "colorSpace") ?? node,
1888
1624
  filename
1889
1625
  });
1890
1626
  return;
1891
1627
  }
1892
- } else if (value && typeof value === "object") {
1893
- if (!STROKE_STYLE_OBJECT_REQUIRED_PROPERTIES.every((property) => property in value)) report({
1894
- messageId: ERROR_OBJ,
1895
- node,
1628
+ const components = "components" in value ? value.components : void 0;
1629
+ if (Array.isArray(components)) if (csData?.ranges && components?.length === csData.ranges.length) {
1630
+ for (let i = 0; i < components.length; i++) if (!Number.isFinite(components[i]) || components[i] < csData.ranges[i][0] || components[i] > csData.ranges[i][1]) {
1631
+ if (!(colorSpace === "hsl" && components[0] === null) && !(colorSpace === "hwb" && components[0] === null) && !(colorSpace === "lch" && components[2] === null) && !(colorSpace === "oklch" && components[2] === null)) report({
1632
+ messageId: ERROR_OUT_OF_RANGE,
1633
+ data: {
1634
+ colorSpace,
1635
+ range: `[${csData.ranges.map((r) => `${r[0]}–${r[1]}`).join(", ")}]`
1636
+ },
1637
+ node: getObjMember(node, "components") ?? node,
1638
+ filename
1639
+ });
1640
+ }
1641
+ } else report({
1642
+ messageId: ERROR_INVALID_COMPONENT_LENGTH,
1643
+ data: {
1644
+ expected: csData?.ranges.length,
1645
+ got: components?.length ?? 0
1646
+ },
1647
+ node: getObjMember(node, "components") ?? node,
1896
1648
  filename
1897
1649
  });
1898
- if (!Array.isArray(value.dashArray)) report({
1899
- messageId: ERROR_OBJ,
1900
- node: getObjMember(node, "dashArray"),
1650
+ else report({
1651
+ messageId: ERROR_MISSING_COMPONENTS,
1652
+ data: { got: JSON.stringify(components) },
1653
+ node: getObjMember(node, "components") ?? node,
1901
1654
  filename
1902
1655
  });
1903
- if (!STROKE_STYLE_LINE_CAP_VALUES.includes(value.lineCap)) report({
1904
- messageId: ERROR_OBJ,
1905
- node: getObjMember(node, "lineCap"),
1656
+ const alpha = "alpha" in value ? value.alpha : void 0;
1657
+ if (alpha !== void 0 && (typeof alpha !== "number" || alpha < 0 || alpha > 1)) report({
1658
+ messageId: ERROR_ALPHA,
1659
+ data: { alpha },
1660
+ node: getObjMember(node, "alpha") ?? node,
1906
1661
  filename
1907
1662
  });
1908
- for (const key of Object.keys(value)) if (!["dashArray", "lineCap"].includes(key)) report({
1909
- messageId: ERROR_INVALID_PROP$1,
1910
- data: { key: JSON.stringify(key) },
1911
- node: getObjMember(node, key),
1663
+ const hex = "hex" in value ? value.hex : void 0;
1664
+ if (hex) {
1665
+ let color;
1666
+ try {
1667
+ color = parseColor(hex);
1668
+ } catch {
1669
+ report({
1670
+ messageId: ERROR_INVALID_COLOR,
1671
+ data: { color: hex },
1672
+ node: getObjMember(node, "hex") ?? node,
1673
+ filename
1674
+ });
1675
+ return;
1676
+ }
1677
+ if (color.alpha !== 1) report({
1678
+ messageId: ERROR_INVALID_HEX8,
1679
+ data: { color: hex },
1680
+ node: getObjMember(node, "hex") ?? node,
1681
+ filename
1682
+ });
1683
+ }
1684
+ } else if (typeof value === "string") {
1685
+ if (isAlias(value)) return;
1686
+ if (!options.legacyFormat) report({
1687
+ messageId: ERROR_OBJ_FORMAT,
1688
+ data: { color: JSON.stringify(value) },
1689
+ node,
1912
1690
  filename
1913
1691
  });
1692
+ else try {
1693
+ parseColor(value);
1694
+ } catch {
1695
+ report({
1696
+ messageId: ERROR_INVALID_COLOR,
1697
+ data: { color: JSON.stringify(value) },
1698
+ node,
1699
+ filename
1700
+ });
1701
+ }
1914
1702
  } else report({
1915
- messageId: ERROR_OBJ,
1703
+ messageId: ERROR_INVALID_COLOR,
1916
1704
  node,
1917
1705
  filename
1918
1706
  });
@@ -1920,92 +1708,319 @@ const rule$2 = {
1920
1708
  }
1921
1709
  }
1922
1710
  };
1923
- var valid_stroke_style_default = rule$2;
1711
+ var valid_color_default = rule$3;
1924
1712
 
1925
1713
  //#endregion
1926
- //#region src/lint/plugin-core/rules/valid-transition.ts
1927
- const VALID_TRANSITION = "core/valid-transition";
1928
- const ERROR$1 = "ERROR";
1929
- const ERROR_INVALID_PROP = "ERROR_INVALID_PROP";
1930
- const rule$1 = {
1714
+ //#region src/lint/plugin-core/rules/valid-cubic-bezier.ts
1715
+ const VALID_CUBIC_BEZIER = "core/valid-cubic-bezier";
1716
+ const ERROR = "ERROR";
1717
+ const ERROR_X = "ERROR_X";
1718
+ const ERROR_Y = "ERROR_Y";
1719
+ const rule$2 = {
1931
1720
  meta: {
1932
1721
  messages: {
1933
- [ERROR$1]: `Missing required properties: ${new Intl.ListFormat("en-us", { type: "conjunction" }).format(TRANSITION_REQUIRED_PROPERTIES)}.`,
1934
- [ERROR_INVALID_PROP]: "Unknown property: {{ key }}."
1722
+ [ERROR]: "Expected [number, number, number, number].",
1723
+ [ERROR_X]: "x values must be between 0-1.",
1724
+ [ERROR_Y]: "y values must be a valid number."
1935
1725
  },
1936
1726
  docs: {
1937
- description: "Require transition tokens to follow the format.",
1938
- url: docsLink(VALID_TRANSITION)
1727
+ description: "Require cubicBezier tokens to follow the format.",
1728
+ url: docsLink(VALID_CUBIC_BEZIER)
1939
1729
  }
1940
1730
  },
1941
1731
  defaultOptions: {},
1942
1732
  create({ tokens, report }) {
1943
1733
  for (const t of Object.values(tokens)) {
1944
- if (t.aliasOf || !t.originalValue || t.$type !== "transition") continue;
1945
- validateTransition(t.originalValue.$value, {
1946
- node: getObjMember(t.source.node, "$value"),
1947
- filename: t.source.filename
1948
- });
1949
- }
1950
- function validateTransition(value, { node, filename }) {
1951
- if (!value || typeof value !== "object" || !TRANSITION_REQUIRED_PROPERTIES.every((property) => property in value)) report({
1952
- messageId: ERROR$1,
1953
- node,
1954
- filename
1955
- });
1956
- else for (const key of Object.keys(value)) if (!TRANSITION_REQUIRED_PROPERTIES.includes(key)) report({
1957
- messageId: ERROR_INVALID_PROP,
1958
- data: { key: JSON.stringify(key) },
1959
- node: getObjMember(node, key),
1960
- filename
1961
- });
1734
+ if (t.aliasOf || !t.originalValue) continue;
1735
+ switch (t.$type) {
1736
+ case "cubicBezier":
1737
+ validateCubicBezier(t.originalValue.$value, {
1738
+ node: getObjMember(t.source.node, "$value"),
1739
+ filename: t.source.filename
1740
+ });
1741
+ break;
1742
+ case "transition": if (typeof t.originalValue.$value === "object" && t.originalValue.$value.timingFunction && !isAlias(t.originalValue.$value.timingFunction)) {
1743
+ const $valueNode = getObjMember(t.source.node, "$value");
1744
+ validateCubicBezier(t.originalValue.$value.timingFunction, {
1745
+ node: getObjMember($valueNode, "timingFunction"),
1746
+ filename: t.source.filename
1747
+ });
1748
+ }
1749
+ }
1750
+ function validateCubicBezier(value, { node, filename }) {
1751
+ if (Array.isArray(value) && value.length === 4) {
1752
+ for (const pos of [0, 2]) {
1753
+ if (isAlias(value[pos]) || isPure$ref(value[pos])) continue;
1754
+ if (!(value[pos] >= 0 && value[pos] <= 1)) report({
1755
+ messageId: ERROR_X,
1756
+ node: node.elements[pos],
1757
+ filename
1758
+ });
1759
+ }
1760
+ for (const pos of [1, 3]) {
1761
+ if (isAlias(value[pos]) || isPure$ref(value[pos])) continue;
1762
+ if (typeof value[pos] !== "number") report({
1763
+ messageId: ERROR_Y,
1764
+ node: node.elements[pos],
1765
+ filename
1766
+ });
1767
+ }
1768
+ } else report({
1769
+ messageId: ERROR,
1770
+ node,
1771
+ filename
1772
+ });
1773
+ }
1962
1774
  }
1963
1775
  }
1964
1776
  };
1965
- var valid_transition_default = rule$1;
1777
+ var valid_cubic_bezier_default = rule$2;
1966
1778
 
1967
1779
  //#endregion
1968
- //#region src/lint/plugin-core/rules/valid-typography.ts
1969
- const VALID_TYPOGRAPHY = "core/valid-typography";
1970
- const ERROR = "ERROR";
1971
- const ERROR_MISSING = "ERROR_MISSING";
1972
- const rule = {
1780
+ //#region src/lint/plugin-core/rules/valid-dimension.ts
1781
+ const VALID_DIMENSION = "core/valid-dimension";
1782
+ const ERROR_FORMAT$1 = "ERROR_FORMAT";
1783
+ const ERROR_INVALID_PROP$1 = "ERROR_INVALID_PROP";
1784
+ const ERROR_LEGACY$1 = "ERROR_LEGACY";
1785
+ const ERROR_UNIT$1 = "ERROR_UNIT";
1786
+ const ERROR_VALUE$1 = "ERROR_VALUE";
1787
+ const rule$1 = {
1973
1788
  meta: {
1974
1789
  messages: {
1975
- [ERROR]: `Expected object, received {{ value }}.`,
1976
- [ERROR_MISSING]: `Missing required property "{{ property }}".`
1790
+ [ERROR_FORMAT$1]: "Invalid dimension: {{ value }}. Expected object with \"value\" and \"unit\".",
1791
+ [ERROR_LEGACY$1]: "Migrate to the new object format: { \"value\": 10, \"unit\": \"px\" }.",
1792
+ [ERROR_UNIT$1]: "Unit {{ unit }} not allowed. Expected {{ allowed }}.",
1793
+ [ERROR_INVALID_PROP$1]: "Unknown property {{ key }}.",
1794
+ [ERROR_VALUE$1]: "Expected number, received {{ value }}."
1977
1795
  },
1978
1796
  docs: {
1979
- description: "Require typography tokens to follow the format.",
1980
- url: docsLink(VALID_TYPOGRAPHY)
1797
+ description: "Require dimension tokens to follow the format",
1798
+ url: docsLink(VALID_DIMENSION)
1981
1799
  }
1982
1800
  },
1983
- defaultOptions: { requiredProperties: [
1984
- "fontFamily",
1985
- "fontSize",
1986
- "fontWeight",
1987
- "letterSpacing",
1988
- "lineHeight"
1989
- ] },
1801
+ defaultOptions: {
1802
+ legacyFormat: false,
1803
+ allowedUnits: [
1804
+ "px",
1805
+ "em",
1806
+ "rem"
1807
+ ]
1808
+ },
1990
1809
  create({ tokens, options, report }) {
1991
- const isIgnored = options.ignore ? wcmatch(options.ignore) : () => false;
1992
1810
  for (const t of Object.values(tokens)) {
1993
- if (t.aliasOf || !t.originalValue || t.$type !== "typography" || isIgnored(t.id)) continue;
1994
- validateTypography(t.originalValue.$value, {
1995
- node: getObjMember(t.source.node, "$value"),
1996
- filename: t.source.filename
1997
- });
1998
- function validateTypography(value, { node, filename }) {
1811
+ if (t.aliasOf || !t.originalValue) continue;
1812
+ switch (t.$type) {
1813
+ case "dimension":
1814
+ validateDimension(t.originalValue.$value, {
1815
+ node: getObjMember(t.source.node, "$value"),
1816
+ filename: t.source.filename
1817
+ });
1818
+ break;
1819
+ case "strokeStyle":
1820
+ if (typeof t.originalValue.$value === "object" && Array.isArray(t.originalValue.$value.dashArray)) {
1821
+ const dashArray = getObjMember(getObjMember(t.source.node, "$value"), "dashArray");
1822
+ for (let i = 0; i < t.originalValue.$value.dashArray.length; i++) {
1823
+ if (isAlias(t.originalValue.$value.dashArray[i])) continue;
1824
+ validateDimension(t.originalValue.$value.dashArray[i], {
1825
+ node: dashArray.elements[i].value,
1826
+ filename: t.source.filename
1827
+ });
1828
+ }
1829
+ }
1830
+ break;
1831
+ case "border": {
1832
+ const $valueNode = getObjMember(t.source.node, "$value");
1833
+ if (typeof t.originalValue.$value === "object") {
1834
+ if (t.originalValue.$value.width && !isAlias(t.originalValue.$value.width)) validateDimension(t.originalValue.$value.width, {
1835
+ node: getObjMember($valueNode, "width"),
1836
+ filename: t.source.filename
1837
+ });
1838
+ if (typeof t.originalValue.$value.style === "object" && Array.isArray(t.originalValue.$value.style.dashArray)) {
1839
+ const dashArray = getObjMember(getObjMember($valueNode, "style"), "dashArray");
1840
+ for (let i = 0; i < t.originalValue.$value.style.dashArray.length; i++) {
1841
+ if (isAlias(t.originalValue.$value.style.dashArray[i])) continue;
1842
+ validateDimension(t.originalValue.$value.style.dashArray[i], {
1843
+ node: dashArray.elements[i].value,
1844
+ filename: t.source.filename
1845
+ });
1846
+ }
1847
+ }
1848
+ }
1849
+ break;
1850
+ }
1851
+ case "shadow":
1852
+ if (t.originalValue.$value && typeof t.originalValue.$value === "object") {
1853
+ const $valueNode = getObjMember(t.source.node, "$value");
1854
+ const valueArray = Array.isArray(t.originalValue.$value) ? t.originalValue.$value : [t.originalValue.$value];
1855
+ for (let i = 0; i < valueArray.length; i++) {
1856
+ const node = $valueNode.type === "Array" ? $valueNode.elements[i].value : $valueNode;
1857
+ for (const property of [
1858
+ "offsetX",
1859
+ "offsetY",
1860
+ "blur",
1861
+ "spread"
1862
+ ]) {
1863
+ if (isAlias(valueArray[i][property])) continue;
1864
+ validateDimension(valueArray[i][property], {
1865
+ node: getObjMember(node, property),
1866
+ filename: t.source.filename
1867
+ });
1868
+ }
1869
+ }
1870
+ }
1871
+ break;
1872
+ case "typography": {
1873
+ const $valueNode = getObjMember(t.source.node, "$value");
1874
+ if (typeof t.originalValue.$value === "object") {
1875
+ for (const property of [
1876
+ "fontSize",
1877
+ "lineHeight",
1878
+ "letterSpacing"
1879
+ ]) if (property in t.originalValue.$value) {
1880
+ if (isAlias(t.originalValue.$value[property]) || property === "lineHeight" && typeof t.originalValue.$value[property] === "number") continue;
1881
+ validateDimension(t.originalValue.$value[property], {
1882
+ node: getObjMember($valueNode, property),
1883
+ filename: t.source.filename
1884
+ });
1885
+ }
1886
+ }
1887
+ break;
1888
+ }
1889
+ }
1890
+ function validateDimension(value, { node, filename }) {
1999
1891
  if (value && typeof value === "object") {
2000
- for (const property of options.requiredProperties) if (!(property in value)) report({
2001
- messageId: ERROR_MISSING,
2002
- data: { property },
2003
- node,
1892
+ for (const key of Object.keys(value)) if (!["value", "unit"].includes(key)) report({
1893
+ messageId: ERROR_INVALID_PROP$1,
1894
+ data: { key: JSON.stringify(key) },
1895
+ node: getObjMember(node, key) ?? node,
2004
1896
  filename
2005
1897
  });
2006
- } else report({
2007
- messageId: ERROR,
2008
- data: { value: JSON.stringify(value) },
1898
+ const { unit, value: numValue } = value;
1899
+ if (!("value" in value || "unit" in value)) {
1900
+ report({
1901
+ messageId: ERROR_FORMAT$1,
1902
+ data: { value },
1903
+ node,
1904
+ filename
1905
+ });
1906
+ return;
1907
+ }
1908
+ if (!options.allowedUnits.includes(unit)) report({
1909
+ messageId: ERROR_UNIT$1,
1910
+ data: {
1911
+ unit,
1912
+ allowed: new Intl.ListFormat("en-us", { type: "disjunction" }).format(options.allowedUnits)
1913
+ },
1914
+ node: getObjMember(node, "unit") ?? node,
1915
+ filename
1916
+ });
1917
+ if (!Number.isFinite(numValue)) report({
1918
+ messageId: ERROR_VALUE$1,
1919
+ data: { value },
1920
+ node: getObjMember(node, "value") ?? node,
1921
+ filename
1922
+ });
1923
+ } else if (typeof value === "string" && !options.legacyFormat) report({
1924
+ messageId: ERROR_LEGACY$1,
1925
+ node,
1926
+ filename
1927
+ });
1928
+ else report({
1929
+ messageId: ERROR_FORMAT$1,
1930
+ data: { value },
1931
+ node,
1932
+ filename
1933
+ });
1934
+ }
1935
+ }
1936
+ }
1937
+ };
1938
+ var valid_dimension_default = rule$1;
1939
+
1940
+ //#endregion
1941
+ //#region src/lint/plugin-core/rules/valid-duration.ts
1942
+ const VALID_DURATION = "core/valid-duration";
1943
+ const ERROR_FORMAT = "ERROR_FORMAT";
1944
+ const ERROR_INVALID_PROP = "ERROR_INVALID_PROP";
1945
+ const ERROR_LEGACY = "ERROR_LEGACY";
1946
+ const ERROR_UNIT = "ERROR_UNIT";
1947
+ const ERROR_VALUE = "ERROR_VALUE";
1948
+ const rule = {
1949
+ meta: {
1950
+ messages: {
1951
+ [ERROR_FORMAT]: "Migrate to the new object format: { \"value\": 2, \"unit\": \"ms\" }.",
1952
+ [ERROR_LEGACY]: "Migrate to the new object format: { \"value\": 10, \"unit\": \"px\" }.",
1953
+ [ERROR_INVALID_PROP]: "Unknown property: {{ key }}.",
1954
+ [ERROR_UNIT]: "Unknown unit {{ unit }}. Expected \"ms\" or \"s\".",
1955
+ [ERROR_VALUE]: "Expected number, received {{ value }}."
1956
+ },
1957
+ docs: {
1958
+ description: "Require duration tokens to follow the format",
1959
+ url: docsLink(VALID_DURATION)
1960
+ }
1961
+ },
1962
+ defaultOptions: {
1963
+ legacyFormat: false,
1964
+ unknownUnits: false
1965
+ },
1966
+ create({ tokens, options, report }) {
1967
+ for (const t of Object.values(tokens)) {
1968
+ if (t.aliasOf || !t.originalValue) continue;
1969
+ switch (t.$type) {
1970
+ case "duration":
1971
+ validateDuration(t.originalValue.$value, {
1972
+ node: getObjMember(t.source.node, "$value"),
1973
+ filename: t.source.filename
1974
+ });
1975
+ break;
1976
+ case "transition":
1977
+ if (typeof t.originalValue.$value === "object") {
1978
+ const $valueNode = getObjMember(t.source.node, "$value");
1979
+ for (const property of ["duration", "delay"]) if (t.originalValue.$value[property] && !isAlias(t.originalValue.$value[property])) validateDuration(t.originalValue.$value[property], {
1980
+ node: getObjMember($valueNode, property),
1981
+ filename: t.source.filename
1982
+ });
1983
+ }
1984
+ break;
1985
+ }
1986
+ function validateDuration(value, { node, filename }) {
1987
+ if (value && typeof value === "object") {
1988
+ for (const key of Object.keys(value)) if (!["value", "unit"].includes(key)) report({
1989
+ messageId: ERROR_INVALID_PROP,
1990
+ data: { key: JSON.stringify(key) },
1991
+ node: getObjMember(node, key) ?? node,
1992
+ filename
1993
+ });
1994
+ const { unit, value: numValue } = value;
1995
+ if (!("value" in value || "unit" in value)) {
1996
+ report({
1997
+ messageId: ERROR_FORMAT,
1998
+ data: { value },
1999
+ node,
2000
+ filename
2001
+ });
2002
+ return;
2003
+ }
2004
+ if (!options.unknownUnits && !["ms", "s"].includes(unit)) report({
2005
+ messageId: ERROR_UNIT,
2006
+ data: { unit },
2007
+ node: getObjMember(node, "unit") ?? node,
2008
+ filename
2009
+ });
2010
+ if (!Number.isFinite(numValue)) report({
2011
+ messageId: ERROR_VALUE,
2012
+ data: { value },
2013
+ node: getObjMember(node, "value") ?? node,
2014
+ filename
2015
+ });
2016
+ } else if (typeof value === "string" && !options.legacyFormat) report({
2017
+ messageId: ERROR_FORMAT,
2018
+ node,
2019
+ filename
2020
+ });
2021
+ else report({
2022
+ messageId: ERROR_FORMAT,
2023
+ data: { value },
2009
2024
  node,
2010
2025
  filename
2011
2026
  });
@@ -2013,7 +2028,7 @@ const rule = {
2013
2028
  }
2014
2029
  }
2015
2030
  };
2016
- var valid_typography_default = rule;
2031
+ var valid_duration_default = rule;
2017
2032
 
2018
2033
  //#endregion
2019
2034
  //#region src/lint/plugin-core/index.ts
@@ -2150,6 +2165,7 @@ function normalizeTokens({ rawConfig, config, logger, cwd }) {
2150
2165
  });
2151
2166
  }
2152
2167
  }
2168
+ config.alphabetize = rawConfig.alphabetize ?? true;
2153
2169
  }
2154
2170
  /** Normalize config.outDir */
2155
2171
  function normalizeOutDir({ config, cwd, logger }) {
@@ -2423,1179 +2439,1207 @@ function filterResolverPaths(path) {
2423
2439
  }
2424
2440
  return path;
2425
2441
  }
2426
- /**
2427
- * Make a deterministic string from an object
2428
- */
2429
- function makeInputKey(input) {
2430
- return JSON.stringify(Object.fromEntries(Object.entries(input).sort((a, b) => a[0].localeCompare(b[0], "en-us", { numeric: true }))));
2442
+ /** Make a deterministic string from an object */
2443
+ function getPermutationID(input) {
2444
+ const keys = Object.keys(input).sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
2445
+ return JSON.stringify(Object.fromEntries(keys.map((k) => [k, input[k]])));
2431
2446
  }
2432
2447
 
2433
2448
  //#endregion
2434
- //#region src/resolver/validate.ts
2449
+ //#region src/parse/assert.ts
2450
+ function assert(value, logger, entry) {
2451
+ if (!value) logger.error(entry);
2452
+ }
2453
+ function assertStringNode(value, logger, entry) {
2454
+ assert(value?.type === "String", logger, entry);
2455
+ }
2456
+ function assertObjectNode(value, logger, entry) {
2457
+ assert(value?.type === "Object", logger, entry);
2458
+ }
2459
+
2460
+ //#endregion
2461
+ //#region src/parse/normalize.ts
2435
2462
  /**
2436
- * Determine whether this is likely a resolver
2437
- * We use terms the word likelybecause this occurs before validation. Since
2438
- * we may be dealing with a doc _intended_ to be a resolver, but may be lacking
2439
- * some critical information, how can we determine intent? There’s a bit of
2440
- * guesswork here, but we try and find a reasonable edge case where we sniff out
2441
- * invalid DTCG syntax that a resolver doc would have.
2463
+ * Normalize token value.
2464
+ * The reason for the “anytyping is this aligns various user-provided inputs to the type
2442
2465
  */
2443
- function isLikelyResolver(doc) {
2444
- if (doc.body.type !== "Object") return false;
2445
- for (const member of doc.body.members) {
2446
- if (member.name.type !== "String") continue;
2447
- switch (member.name.value) {
2448
- case "name":
2449
- case "description":
2450
- case "version":
2451
- if (member.value.type === "String") return true;
2452
- break;
2453
- case "sets":
2454
- case "modifiers":
2455
- if (member.value.type !== "Object") continue;
2456
- if (getObjMember(member.value, "description")?.type === "String") return true;
2457
- if (member.name.value === "sets" && getObjMember(member.value, "sources")?.type === "Array") return true;
2458
- else if (member.name.value === "modifiers") {
2459
- const contexts = getObjMember(member.value, "contexts");
2460
- if (contexts?.type === "Object" && contexts.members.some((m) => m.value.type === "Array")) return true;
2461
- }
2462
- break;
2463
- case "resolutionOrder":
2464
- if (member.value.type === "Array") return true;
2465
- break;
2466
- }
2467
- }
2468
- return false;
2469
- }
2470
- const MESSAGE_EXPECTED = {
2471
- STRING: "Expected string.",
2472
- OBJECT: "Expected object.",
2473
- ARRAY: "Expected array."
2474
- };
2475
- /**
2476
- * Validate a resolver document.
2477
- * There’s a ton of boilerplate here, only to surface detailed code frames. Is there a better abstraction?
2478
- */
2479
- function validateResolver(node, { logger, src }) {
2466
+ function normalize(token, { logger, src }) {
2480
2467
  const entry = {
2481
2468
  group: "parser",
2482
- label: "resolver",
2469
+ label: "init",
2483
2470
  src
2484
2471
  };
2485
- if (node.body.type !== "Object") logger.error({
2486
- ...entry,
2487
- message: MESSAGE_EXPECTED.OBJECT,
2488
- node
2489
- });
2490
- const errors = [];
2491
- let hasVersion = false;
2492
- let hasResolutionOrder = false;
2493
- for (const member of node.body.members) {
2494
- if (member.name.type !== "String") continue;
2495
- switch (member.name.value) {
2496
- case "name":
2497
- case "description":
2498
- if (member.value.type !== "String") errors.push({
2499
- ...entry,
2500
- message: MESSAGE_EXPECTED.STRING
2501
- });
2502
- break;
2503
- case "version":
2504
- hasVersion = true;
2505
- if (member.value.type !== "String" || member.value.value !== "2025.10") errors.push({
2506
- ...entry,
2507
- message: `Expected "version" to be "2025.10".`,
2508
- node: member.value
2509
- });
2510
- break;
2511
- case "sets":
2512
- case "modifiers":
2513
- if (member.value.type !== "Object") errors.push({
2514
- ...entry,
2515
- message: MESSAGE_EXPECTED.OBJECT,
2516
- node: member.value
2517
- });
2518
- else for (const item of member.value.members) if (item.value.type !== "Object") errors.push({
2519
- ...entry,
2520
- message: MESSAGE_EXPECTED.OBJECT,
2521
- node: item.value
2522
- });
2523
- else {
2524
- const validator = member.name.value === "sets" ? validateSet : validateModifier;
2525
- errors.push(...validator(item.value, false, {
2526
- logger,
2527
- src
2528
- }));
2472
+ function normalizeFontFamily(value) {
2473
+ return typeof value === "string" ? [value] : value;
2474
+ }
2475
+ function normalizeFontWeight(value) {
2476
+ return typeof value === "string" && FONT_WEIGHTS[value] || value;
2477
+ }
2478
+ function normalizeColor(value, node) {
2479
+ if (typeof value === "string" && !isAlias(value)) {
2480
+ logger.warn({
2481
+ ...entry,
2482
+ node,
2483
+ message: `${token.id}: string colors will be deprecated in a future version. Please update to object notation`
2484
+ });
2485
+ try {
2486
+ return parseColor(value);
2487
+ } catch {
2488
+ return {
2489
+ colorSpace: "srgb",
2490
+ components: [
2491
+ 0,
2492
+ 0,
2493
+ 0
2494
+ ],
2495
+ alpha: 1
2496
+ };
2497
+ }
2498
+ } else if (value && typeof value === "object") {
2499
+ if (value.alpha === void 0) value.alpha = 1;
2500
+ }
2501
+ return value;
2502
+ }
2503
+ switch (token.$type) {
2504
+ case "color":
2505
+ for (const mode of Object.keys(token.mode)) token.mode[mode].$value = normalizeColor(token.mode[mode].$value, token.mode[mode].source.node);
2506
+ token.$value = token.mode["."].$value;
2507
+ break;
2508
+ case "fontFamily":
2509
+ for (const mode of Object.keys(token.mode)) token.mode[mode].$value = normalizeFontFamily(token.mode[mode].$value);
2510
+ token.$value = token.mode["."].$value;
2511
+ break;
2512
+ case "fontWeight":
2513
+ for (const mode of Object.keys(token.mode)) token.mode[mode].$value = normalizeFontWeight(token.mode[mode].$value);
2514
+ token.$value = token.mode["."].$value;
2515
+ break;
2516
+ case "border":
2517
+ for (const mode of Object.keys(token.mode)) {
2518
+ const border = token.mode[mode].$value;
2519
+ if (!border || typeof border !== "object") continue;
2520
+ if (border.color) border.color = normalizeColor(border.color, getObjMember(token.mode[mode].source.node, "color"));
2521
+ }
2522
+ token.$value = token.mode["."].$value;
2523
+ break;
2524
+ case "shadow":
2525
+ for (const mode of Object.keys(token.mode)) {
2526
+ if (!Array.isArray(token.mode[mode].$value)) token.mode[mode].$value = [token.mode[mode].$value];
2527
+ const $value = token.mode[mode].$value;
2528
+ for (let i = 0; i < $value.length; i++) {
2529
+ const shadow = $value[i];
2530
+ if (!shadow || typeof shadow !== "object") continue;
2531
+ const shadowNode = token.mode[mode].source.node.type === "Array" ? token.mode[mode].source.node.elements[i].value : token.mode[mode].source.node;
2532
+ if (shadow.color) shadow.color = normalizeColor(shadow.color, getObjMember(shadowNode, "color"));
2533
+ if (!("inset" in shadow)) shadow.inset = false;
2529
2534
  }
2530
- break;
2531
- case "resolutionOrder":
2532
- hasResolutionOrder = true;
2533
- if (member.value.type !== "Array") errors.push({
2534
- ...entry,
2535
- message: MESSAGE_EXPECTED.ARRAY,
2536
- node: member.value
2537
- });
2538
- else if (member.value.elements.length === 0) errors.push({
2539
- ...entry,
2540
- message: `"resolutionOrder" can’t be empty array.`,
2541
- node: member.value
2542
- });
2543
- else for (const item of member.value.elements) if (item.value.type !== "Object") errors.push({
2544
- ...entry,
2545
- message: MESSAGE_EXPECTED.OBJECT,
2546
- node: item.value
2547
- });
2548
- else {
2549
- const itemMembers = getObjMembers(item.value);
2550
- if (itemMembers.$ref?.type === "String") continue;
2551
- if (itemMembers.type?.type === "String") if (itemMembers.type.value === "set") validateSet(item.value, true, {
2552
- logger,
2553
- src
2554
- });
2555
- else if (itemMembers.type.value === "modifier") validateModifier(item.value, true, {
2556
- logger,
2557
- src
2558
- });
2559
- else errors.push({
2560
- ...entry,
2561
- message: `Unknown type ${JSON.stringify(itemMembers.type.value)}`,
2562
- node: itemMembers.type
2563
- });
2564
- if (itemMembers.sources?.type === "Array") validateSet(item.value, true, {
2565
- logger,
2566
- src
2567
- });
2568
- else if (itemMembers.contexts?.type === "Object") validateModifier(item.value, true, {
2569
- logger,
2570
- src
2571
- });
2572
- else if (itemMembers.name?.type === "String" || itemMembers.description?.type === "String") validateSet(item.value, true, {
2573
- logger,
2574
- src
2575
- });
2535
+ }
2536
+ token.$value = token.mode["."].$value;
2537
+ break;
2538
+ case "gradient":
2539
+ for (const mode of Object.keys(token.mode)) {
2540
+ if (!Array.isArray(token.mode[mode].$value)) continue;
2541
+ const $value = token.mode[mode].$value;
2542
+ for (let i = 0; i < $value.length; i++) {
2543
+ const stop = $value[i];
2544
+ if (!stop || typeof stop !== "object") continue;
2545
+ const stopNode = token.mode[mode].source.node?.elements?.[i]?.value;
2546
+ if (stop.color) stop.color = normalizeColor(stop.color, getObjMember(stopNode, "color"));
2576
2547
  }
2577
- break;
2578
- case "$defs":
2579
- case "$extensions":
2580
- if (member.value.type !== "Object") errors.push({
2581
- ...entry,
2582
- message: `Expected object`,
2583
- node: member.value
2584
- });
2585
- break;
2586
- case "$schema":
2587
- case "$ref":
2588
- if (member.value.type !== "String") errors.push({
2589
- ...entry,
2590
- message: `Expected string`,
2591
- node: member.value
2592
- });
2593
- break;
2594
- default:
2595
- errors.push({
2596
- ...entry,
2597
- message: `Unknown key ${JSON.stringify(member.name.value)}`,
2598
- node: member.name,
2599
- src
2600
- });
2601
- break;
2602
- }
2548
+ }
2549
+ token.$value = token.mode["."].$value;
2550
+ break;
2551
+ case "typography":
2552
+ for (const mode of Object.keys(token.mode)) {
2553
+ const $value = token.mode[mode].$value;
2554
+ if (typeof $value !== "object") return;
2555
+ for (const [k, v] of Object.entries($value)) switch (k) {
2556
+ case "fontFamily":
2557
+ $value[k] = normalizeFontFamily(v);
2558
+ break;
2559
+ case "fontWeight":
2560
+ $value[k] = normalizeFontWeight(v);
2561
+ break;
2562
+ }
2563
+ }
2564
+ token.$value = token.mode["."].$value;
2565
+ break;
2603
2566
  }
2604
- if (!hasVersion) errors.push({
2605
- ...entry,
2606
- message: `Missing "version".`,
2607
- node,
2608
- src
2609
- });
2610
- if (!hasResolutionOrder) errors.push({
2611
- ...entry,
2612
- message: `Missing "resolutionOrder".`,
2613
- node,
2614
- src
2615
- });
2616
- if (errors.length) logger.error(...errors);
2617
2567
  }
2618
- function validateSet(node, isInline = false, { src }) {
2619
- const entry = {
2620
- group: "parser",
2621
- label: "resolver",
2622
- src
2623
- };
2624
- const errors = [];
2625
- let hasName = !isInline;
2626
- let hasType = !isInline;
2627
- let hasSources = false;
2628
- for (const member of node.members) {
2629
- if (member.name.type !== "String") continue;
2630
- switch (member.name.value) {
2631
- case "name":
2632
- hasName = true;
2633
- if (member.value.type !== "String") errors.push({
2634
- ...entry,
2635
- message: MESSAGE_EXPECTED.STRING,
2636
- node: member.value
2637
- });
2638
- break;
2639
- case "description":
2640
- if (member.value.type !== "String") errors.push({
2641
- ...entry,
2642
- message: MESSAGE_EXPECTED.STRING,
2643
- node: member.value
2644
- });
2645
- break;
2646
- case "type":
2647
- hasType = true;
2648
- if (member.value.type !== "String") errors.push({
2649
- ...entry,
2650
- message: MESSAGE_EXPECTED.STRING,
2651
- node: member.value
2652
- });
2653
- else if (member.value.value !== "set") errors.push({
2654
- ...entry,
2655
- message: "\"type\" must be \"set\"."
2656
- });
2657
- break;
2658
- case "sources":
2659
- hasSources = true;
2660
- if (member.value.type !== "Array") errors.push({
2661
- ...entry,
2662
- message: MESSAGE_EXPECTED.ARRAY,
2663
- node: member.value
2664
- });
2665
- else if (member.value.elements.length === 0) errors.push({
2666
- ...entry,
2667
- message: `"sources" can’t be empty array.`,
2668
- node: member.value
2669
- });
2670
- break;
2671
- case "$defs":
2672
- case "$extensions":
2673
- if (member.value.type !== "Object") errors.push({
2674
- ...entry,
2675
- message: `Expected object`,
2676
- node: member.value
2677
- });
2678
- break;
2679
- case "$ref":
2680
- if (member.value.type !== "String") errors.push({
2681
- ...entry,
2682
- message: `Expected string`,
2683
- node: member.value
2684
- });
2685
- break;
2686
- default:
2687
- errors.push({
2688
- ...entry,
2689
- message: `Unknown key ${JSON.stringify(member.name.value)}`,
2690
- node: member.name
2691
- });
2692
- break;
2568
+
2569
+ //#endregion
2570
+ //#region src/parse/token.ts
2571
+ /** Convert valid DTCG alias to $ref */
2572
+ function aliasToGroupRef(alias) {
2573
+ const id = parseAlias(alias);
2574
+ if (id === alias) return;
2575
+ return { $ref: `#/${id.replace(/~/g, "~0").replace(/\//g, "~1").replace(/\./g, "/")}` };
2576
+ }
2577
+ /** Convert valid DTCG alias to $ref */
2578
+ function aliasToTokenRef(alias, mode) {
2579
+ const id = parseAlias(alias);
2580
+ if (id === alias) return;
2581
+ return { $ref: `#/${id.replace(/~/g, "~0").replace(/\//g, "~1").replace(/\./g, "/")}${mode && mode !== "." ? `/$extensions/mode/${mode}` : ""}/$value` };
2582
+ }
2583
+ /** Generate a TokenNormalized from a Momoa node */
2584
+ function tokenFromNode(node, { groups, path, source, ignore }) {
2585
+ if (!(node.type === "Object" && !!getObjMember(node, "$value") && !path.includes("$extensions"))) return;
2586
+ const jsonID = encodeFragment(path);
2587
+ const id = path.join(".").replace(/\.\$root$/, "");
2588
+ const originalToken = momoa.evaluate(node);
2589
+ const group = groups[encodeFragment(path.slice(0, -1))];
2590
+ if (group?.tokens && !group.tokens.includes(id)) group.tokens.push(id);
2591
+ const nodeSource = {
2592
+ filename: source.filename.href,
2593
+ node
2594
+ };
2595
+ const token = {
2596
+ id,
2597
+ $type: originalToken.$type || group.$type,
2598
+ $description: originalToken.$description || void 0,
2599
+ $deprecated: originalToken.$deprecated ?? group.$deprecated ?? void 0,
2600
+ $value: originalToken.$value,
2601
+ $extensions: originalToken.$extensions || void 0,
2602
+ $extends: originalToken.$extends || void 0,
2603
+ aliasChain: void 0,
2604
+ aliasedBy: void 0,
2605
+ aliasOf: void 0,
2606
+ partialAliasOf: void 0,
2607
+ dependencies: void 0,
2608
+ group,
2609
+ originalValue: void 0,
2610
+ source: nodeSource,
2611
+ jsonID,
2612
+ mode: { ".": {
2613
+ $value: originalToken.$value,
2614
+ aliasOf: void 0,
2615
+ aliasChain: void 0,
2616
+ partialAliasOf: void 0,
2617
+ aliasedBy: void 0,
2618
+ originalValue: void 0,
2619
+ dependencies: void 0,
2620
+ source: {
2621
+ ...nodeSource,
2622
+ node: getObjMember(nodeSource.node, "$value") ?? nodeSource.node
2623
+ }
2624
+ } }
2625
+ };
2626
+ if (ignore?.deprecated && token.$deprecated || ignore?.tokens && wcmatch(ignore.tokens)(token.id)) return;
2627
+ const $extensions = getObjMember(node, "$extensions");
2628
+ if ($extensions) {
2629
+ const modeNode = getObjMember($extensions, "mode");
2630
+ for (const mode of Object.keys(token.$extensions.mode ?? {})) {
2631
+ const modeValue = token.$extensions.mode[mode];
2632
+ token.mode[mode] = {
2633
+ $value: modeValue,
2634
+ aliasOf: void 0,
2635
+ aliasChain: void 0,
2636
+ partialAliasOf: void 0,
2637
+ aliasedBy: void 0,
2638
+ originalValue: void 0,
2639
+ dependencies: void 0,
2640
+ source: {
2641
+ ...nodeSource,
2642
+ node: getObjMember(modeNode, mode)
2643
+ }
2644
+ };
2693
2645
  }
2694
2646
  }
2695
- if (!hasName) errors.push({
2696
- ...entry,
2697
- message: `Missing "name".`,
2698
- node
2699
- });
2700
- if (!hasType) errors.push({
2701
- ...entry,
2702
- message: `"type": "set" missing.`,
2703
- node
2704
- });
2705
- if (!hasSources) errors.push({
2706
- ...entry,
2707
- message: `Missing "sources".`,
2708
- node
2709
- });
2710
- return errors;
2647
+ return token;
2711
2648
  }
2712
- function validateModifier(node, isInline = false, { src }) {
2713
- const errors = [];
2714
- const entry = {
2715
- group: "parser",
2716
- label: "resolver",
2717
- src
2649
+ /** Generate originalValue and source from node */
2650
+ function tokenRawValuesFromNode(node, { filename, path }) {
2651
+ if (!(node.type === "Object" && getObjMember(node, "$value") && !path.includes("$extensions"))) return;
2652
+ const rawValues = {
2653
+ jsonID: encodeFragment(path),
2654
+ originalValue: momoa.evaluate(node),
2655
+ source: {
2656
+ loc: filename,
2657
+ filename,
2658
+ node
2659
+ },
2660
+ mode: {}
2718
2661
  };
2719
- let hasName = !isInline;
2720
- let hasType = !isInline;
2721
- let hasContexts = false;
2722
- for (const member of node.members) {
2723
- if (member.name.type !== "String") continue;
2724
- switch (member.name.value) {
2725
- case "name":
2726
- hasName = true;
2727
- if (member.value.type !== "String") errors.push({
2728
- ...entry,
2729
- message: MESSAGE_EXPECTED.STRING,
2730
- node: member.value
2731
- });
2732
- break;
2733
- case "description":
2734
- if (member.value.type !== "String") errors.push({
2735
- ...entry,
2736
- message: MESSAGE_EXPECTED.STRING,
2737
- node: member.value
2738
- });
2739
- break;
2740
- case "type":
2741
- hasType = true;
2742
- if (member.value.type !== "String") errors.push({
2743
- ...entry,
2744
- message: MESSAGE_EXPECTED.STRING,
2745
- node: member.value
2746
- });
2747
- else if (member.value.value !== "modifier") errors.push({
2748
- ...entry,
2749
- message: "\"type\" must be \"modifier\"."
2750
- });
2751
- break;
2752
- case "contexts":
2753
- hasContexts = true;
2754
- if (member.value.type !== "Object") errors.push({
2755
- ...entry,
2756
- message: MESSAGE_EXPECTED.OBJECT,
2757
- node: member.value
2758
- });
2759
- else if (member.value.members.length === 0) errors.push({
2760
- ...entry,
2761
- message: `"contexts" can’t be empty object.`,
2762
- node: member.value
2763
- });
2764
- else for (const context of member.value.members) if (context.value.type !== "Array") errors.push({
2765
- ...entry,
2766
- message: MESSAGE_EXPECTED.ARRAY,
2767
- node: context.value
2768
- });
2769
- break;
2770
- case "default":
2771
- if (member.value.type !== "String") errors.push({
2772
- ...entry,
2773
- message: `Expected string`,
2774
- node: member.value
2775
- });
2776
- else {
2777
- const contexts = getObjMember(node, "contexts");
2778
- if (!contexts || !getObjMember(contexts, member.value.value)) errors.push({
2779
- ...entry,
2780
- message: "Invalid default context",
2781
- node: member.value
2782
- });
2662
+ rawValues.mode["."] = {
2663
+ originalValue: rawValues.originalValue.$value,
2664
+ source: {
2665
+ ...rawValues.source,
2666
+ node: getObjMember(node, "$value")
2667
+ }
2668
+ };
2669
+ const $extensions = getObjMember(node, "$extensions");
2670
+ if ($extensions) {
2671
+ const modes = getObjMember($extensions, "mode");
2672
+ if (modes) for (const modeMember of modes.members) {
2673
+ const mode = modeMember.name.value;
2674
+ rawValues.mode[mode] = {
2675
+ originalValue: momoa.evaluate(modeMember.value),
2676
+ source: {
2677
+ loc: filename,
2678
+ filename,
2679
+ node: modeMember.value
2680
+ }
2681
+ };
2682
+ }
2683
+ }
2684
+ return rawValues;
2685
+ }
2686
+ /** Arbitrary keys that should be associated with a token group */
2687
+ const GROUP_PROPERTIES = [
2688
+ "$deprecated",
2689
+ "$description",
2690
+ "$extensions",
2691
+ "$type"
2692
+ ];
2693
+ /**
2694
+ * Generate a group from a node.
2695
+ * This method mutates the groups index as it goes because of group inheritance.
2696
+ * As it encounters new groups it may have to update other groups.
2697
+ */
2698
+ function groupFromNode(node, { path, groups }) {
2699
+ const id = path.join(".");
2700
+ const jsonID = encodeFragment(path);
2701
+ if (!groups[jsonID]) groups[jsonID] = {
2702
+ id,
2703
+ $deprecated: void 0,
2704
+ $description: void 0,
2705
+ $extensions: void 0,
2706
+ $type: void 0,
2707
+ tokens: []
2708
+ };
2709
+ const groupIDs = Object.keys(groups);
2710
+ groupIDs.sort();
2711
+ for (const groupID of groupIDs) if (jsonID.startsWith(groupID) && groupID !== jsonID) {
2712
+ groups[jsonID].$deprecated = groups[groupID]?.$deprecated ?? groups[jsonID].$deprecated;
2713
+ groups[jsonID].$description = groups[groupID]?.$description ?? groups[jsonID].$description;
2714
+ groups[jsonID].$type = groups[groupID]?.$type ?? groups[jsonID].$type;
2715
+ }
2716
+ for (const m of node.members) {
2717
+ if (m.name.type !== "String" || !GROUP_PROPERTIES.includes(m.name.value)) continue;
2718
+ groups[jsonID][m.name.value] = momoa.evaluate(m.value);
2719
+ }
2720
+ return groups[jsonID];
2721
+ }
2722
+ /**
2723
+ * Link and reverse-link tokens in one pass.
2724
+ */
2725
+ function graphAliases(refMap, { tokens, logger, sources }) {
2726
+ const getTokenRef = (ref) => ref.replace(/\/(\$value|\$extensions)\/?.*/, "");
2727
+ for (const [jsonID, { refChain }] of Object.entries(refMap)) {
2728
+ if (!refChain.length) continue;
2729
+ const mode = jsonID.match(/\/\$extensions\/mode\/([^/]+)/)?.[1] || ".";
2730
+ const rootRef = getTokenRef(jsonID);
2731
+ const modeValue = tokens[rootRef]?.mode[mode];
2732
+ if (!modeValue) continue;
2733
+ if (!modeValue.dependencies) modeValue.dependencies = [];
2734
+ modeValue.dependencies.push(...refChain.filter((r) => !modeValue.dependencies.includes(r)));
2735
+ modeValue.dependencies.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
2736
+ if (jsonID.endsWith("/$value") || tokens[jsonID]) {
2737
+ modeValue.aliasOf = refToTokenID(refChain.at(-1));
2738
+ modeValue.aliasChain = [...refChain.map(refToTokenID)];
2739
+ }
2740
+ const partial = jsonID.replace(/.*\/\$value\/?/, "").split("/").filter(Boolean);
2741
+ if (partial.length && modeValue.$value && typeof modeValue.$value === "object") {
2742
+ let node = modeValue.$value;
2743
+ let sourceNode = modeValue.source.node;
2744
+ if (!modeValue.partialAliasOf) modeValue.partialAliasOf = Array.isArray(modeValue.$value) || tokens[rootRef]?.$type === "shadow" ? [] : {};
2745
+ let partialAliasOf = modeValue.partialAliasOf;
2746
+ if (tokens[rootRef]?.$type === "shadow" && !Array.isArray(node)) {
2747
+ if (Array.isArray(modeValue.partialAliasOf) && !modeValue.partialAliasOf.length) modeValue.partialAliasOf.push({});
2748
+ partialAliasOf = modeValue.partialAliasOf[0];
2749
+ }
2750
+ for (let i = 0; i < partial.length; i++) {
2751
+ let key = partial[i];
2752
+ if (String(Number(key)) === key) key = Number(key);
2753
+ if (key in node && typeof node[key] !== "undefined") {
2754
+ node = node[key];
2755
+ if (sourceNode.type === "Object") sourceNode = getObjMember(sourceNode, key) ?? sourceNode;
2756
+ else if (sourceNode.type === "Array") sourceNode = sourceNode.elements[key]?.value ?? sourceNode;
2757
+ }
2758
+ if (i === partial.length - 1) {
2759
+ const aliasedID = getTokenRef(refChain[0]);
2760
+ if (!(aliasedID in tokens)) {
2761
+ logger.error({
2762
+ group: "parser",
2763
+ label: "init",
2764
+ message: `Invalid alias: ${aliasedID}`,
2765
+ node: sourceNode,
2766
+ src: sources[tokens[rootRef].source.filename]?.src
2767
+ });
2768
+ break;
2769
+ }
2770
+ partialAliasOf[key] = refToTokenID(aliasedID);
2771
+ }
2772
+ if (!(key in partialAliasOf)) partialAliasOf[key] = Array.isArray(node) ? [] : {};
2773
+ partialAliasOf = partialAliasOf[key];
2774
+ }
2775
+ }
2776
+ const aliasedByRefs = [jsonID, ...refChain].reverse();
2777
+ for (let i = 0; i < aliasedByRefs.length; i++) {
2778
+ const baseRef = getTokenRef(aliasedByRefs[i]);
2779
+ const baseToken = tokens[baseRef]?.mode[mode] || tokens[baseRef];
2780
+ if (!baseToken) continue;
2781
+ const upstream = aliasedByRefs.slice(i + 1);
2782
+ if (!upstream.length) break;
2783
+ if (!baseToken.aliasedBy) baseToken.aliasedBy = [];
2784
+ for (let j = 0; j < upstream.length; j++) {
2785
+ const downstream = refToTokenID(upstream[j]);
2786
+ if (!baseToken.aliasedBy.includes(downstream)) {
2787
+ baseToken.aliasedBy.push(downstream);
2788
+ if (mode === ".") tokens[baseRef].aliasedBy = baseToken.aliasedBy;
2783
2789
  }
2784
- break;
2785
- case "$defs":
2786
- case "$extensions":
2787
- if (member.value.type !== "Object") errors.push({
2788
- ...entry,
2789
- message: `Expected object`,
2790
- node: member.value
2791
- });
2792
- break;
2793
- case "$ref":
2794
- if (member.value.type !== "String") errors.push({
2795
- ...entry,
2796
- message: `Expected string`,
2797
- node: member.value
2798
- });
2799
- break;
2800
- default:
2801
- errors.push({
2802
- ...entry,
2803
- message: `Unknown key ${JSON.stringify(member.name.value)}`,
2804
- node: member.name
2805
- });
2806
- break;
2790
+ }
2791
+ baseToken.aliasedBy.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
2792
+ }
2793
+ if (mode === ".") {
2794
+ tokens[rootRef].aliasChain = modeValue.aliasChain;
2795
+ tokens[rootRef].aliasedBy = modeValue.aliasedBy;
2796
+ tokens[rootRef].aliasOf = modeValue.aliasOf;
2797
+ tokens[rootRef].dependencies = modeValue.dependencies;
2798
+ tokens[rootRef].partialAliasOf = modeValue.partialAliasOf;
2807
2799
  }
2808
2800
  }
2809
- if (!hasName) errors.push({
2810
- ...entry,
2811
- message: `Missing "name".`,
2812
- node
2813
- });
2814
- if (!hasType) errors.push({
2815
- ...entry,
2816
- message: `"type": "modifier" missing.`,
2817
- node
2818
- });
2819
- if (!hasContexts) errors.push({
2820
- ...entry,
2821
- message: `Missing "contexts".`,
2822
- node
2823
- });
2824
- return errors;
2825
2801
  }
2826
-
2827
- //#endregion
2828
- //#region src/parse/normalize.ts
2829
2802
  /**
2830
- * Normalize token value.
2831
- * The reason for the “any” typing is this aligns various user-provided inputs to the type
2803
+ * Convert Reference Object to token ID.
2804
+ * This can then be turned into an alias by surrounding with { }
2805
+ * ⚠️ This is not mode-aware. This will flatten multiple modes into the same root token.
2832
2806
  */
2833
- function normalize(token, { logger, src }) {
2834
- const entry = {
2835
- group: "parser",
2836
- label: "init",
2837
- src
2838
- };
2839
- function normalizeFontFamily(value) {
2840
- return typeof value === "string" ? [value] : value;
2841
- }
2842
- function normalizeFontWeight(value) {
2843
- return typeof value === "string" && FONT_WEIGHTS[value] || value;
2844
- }
2845
- function normalizeColor(value, node) {
2846
- if (typeof value === "string" && !isAlias(value)) {
2847
- logger.warn({
2848
- ...entry,
2849
- node,
2850
- message: `${token.id}: string colors will be deprecated in a future version. Please update to object notation`
2851
- });
2852
- try {
2853
- return parseColor(value);
2854
- } catch {
2855
- return {
2856
- colorSpace: "srgb",
2857
- components: [
2858
- 0,
2859
- 0,
2860
- 0
2861
- ],
2862
- alpha: 1
2863
- };
2864
- }
2865
- } else if (value && typeof value === "object") {
2866
- if (value.alpha === void 0) value.alpha = 1;
2867
- }
2868
- return value;
2807
+ function refToTokenID($ref) {
2808
+ const path = typeof $ref === "object" ? $ref.$ref : $ref;
2809
+ if (typeof path !== "string") return;
2810
+ const { subpath } = parseRef(path);
2811
+ if (subpath?.[0] === "$defs") subpath.splice(0, 2);
2812
+ return subpath?.length && subpath.join(".").replace(/\.(\$root|\$value|\$extensions).*$/, "") || void 0;
2813
+ }
2814
+ const EXPECTED_NESTED_ALIAS = {
2815
+ border: {
2816
+ color: ["color"],
2817
+ stroke: ["strokeStyle"],
2818
+ width: ["dimension"]
2819
+ },
2820
+ gradient: {
2821
+ color: ["color"],
2822
+ position: ["number"]
2823
+ },
2824
+ shadow: {
2825
+ color: ["color"],
2826
+ offsetX: ["dimension"],
2827
+ offsetY: ["dimension"],
2828
+ blur: ["dimension"],
2829
+ spread: ["dimension"],
2830
+ inset: ["boolean"]
2831
+ },
2832
+ strokeStyle: { dashArray: ["dimension"] },
2833
+ transition: {
2834
+ duration: ["duration"],
2835
+ delay: ["duration"],
2836
+ timingFunction: ["cubicBezier"]
2837
+ },
2838
+ typography: {
2839
+ fontFamily: ["fontFamily"],
2840
+ fontWeight: ["fontWeight"],
2841
+ fontSize: ["dimension"],
2842
+ lineHeight: ["dimension", "number"],
2843
+ letterSpacing: ["dimension"],
2844
+ paragraphSpacing: ["dimension", "string"],
2845
+ wordSpacing: ["dimension", "string"]
2869
2846
  }
2870
- switch (token.$type) {
2871
- case "color":
2872
- for (const mode of Object.keys(token.mode)) token.mode[mode].$value = normalizeColor(token.mode[mode].$value, token.mode[mode].source.node);
2873
- token.$value = token.mode["."].$value;
2874
- break;
2875
- case "fontFamily":
2876
- for (const mode of Object.keys(token.mode)) token.mode[mode].$value = normalizeFontFamily(token.mode[mode].$value);
2877
- token.$value = token.mode["."].$value;
2878
- break;
2879
- case "fontWeight":
2880
- for (const mode of Object.keys(token.mode)) token.mode[mode].$value = normalizeFontWeight(token.mode[mode].$value);
2881
- token.$value = token.mode["."].$value;
2882
- break;
2883
- case "border":
2884
- for (const mode of Object.keys(token.mode)) {
2885
- const border = token.mode[mode].$value;
2886
- if (!border || typeof border !== "object") continue;
2887
- if (border.color) border.color = normalizeColor(border.color, getObjMember(token.mode[mode].source.node, "color"));
2888
- }
2889
- token.$value = token.mode["."].$value;
2890
- break;
2891
- case "shadow":
2892
- for (const mode of Object.keys(token.mode)) {
2893
- if (!Array.isArray(token.mode[mode].$value)) token.mode[mode].$value = [token.mode[mode].$value];
2894
- const $value = token.mode[mode].$value;
2895
- for (let i = 0; i < $value.length; i++) {
2896
- const shadow = $value[i];
2897
- if (!shadow || typeof shadow !== "object") continue;
2898
- const shadowNode = token.mode[mode].source.node.type === "Array" ? token.mode[mode].source.node.elements[i].value : token.mode[mode].source.node;
2899
- if (shadow.color) shadow.color = normalizeColor(shadow.color, getObjMember(shadowNode, "color"));
2900
- if (!("inset" in shadow)) shadow.inset = false;
2847
+ };
2848
+ /**
2849
+ * Resolve DTCG aliases, $extends, and $ref
2850
+ */
2851
+ function resolveAliases(tokens, { logger, refMap, sources }) {
2852
+ for (const token of Object.values(tokens)) {
2853
+ const aliasEntry = {
2854
+ group: "parser",
2855
+ label: "init",
2856
+ src: sources[token.source.filename]?.src,
2857
+ node: getObjMember(token.source.node, "$value")
2858
+ };
2859
+ for (const mode of Object.keys(token.mode)) {
2860
+ function resolveInner(alias, refChain) {
2861
+ const nextRef = aliasToTokenRef(alias, mode)?.$ref;
2862
+ if (!nextRef) {
2863
+ logger.error({
2864
+ ...aliasEntry,
2865
+ message: `Internal error resolving ${JSON.stringify(refChain)}`
2866
+ });
2867
+ throw new Error("Internal error");
2901
2868
  }
2869
+ if (refChain.includes(nextRef)) logger.error({
2870
+ ...aliasEntry,
2871
+ message: "Circular alias detected."
2872
+ });
2873
+ const nextJSONID = nextRef.replace(/\/(\$value|\$extensions).*/, "");
2874
+ const nextToken = tokens[nextJSONID]?.mode[mode] || tokens[nextJSONID]?.mode["."];
2875
+ if (!nextToken) logger.error({
2876
+ ...aliasEntry,
2877
+ message: `Could not resolve alias ${alias}.`
2878
+ });
2879
+ refChain.push(nextRef);
2880
+ if (isAlias(nextToken.originalValue)) return resolveInner(nextToken.originalValue, refChain);
2881
+ return nextJSONID;
2902
2882
  }
2903
- token.$value = token.mode["."].$value;
2904
- break;
2905
- case "gradient":
2906
- for (const mode of Object.keys(token.mode)) {
2907
- if (!Array.isArray(token.mode[mode].$value)) continue;
2908
- const $value = token.mode[mode].$value;
2909
- for (let i = 0; i < $value.length; i++) {
2910
- const stop = $value[i];
2911
- if (!stop || typeof stop !== "object") continue;
2912
- const stopNode = token.mode[mode].source.node?.elements?.[i]?.value;
2913
- if (stop.color) stop.color = normalizeColor(stop.color, getObjMember(stopNode, "color"));
2883
+ function traverseAndResolve(value, { node, expectedTypes, path }) {
2884
+ if (typeof value !== "string") {
2885
+ if (Array.isArray(value)) for (let i = 0; i < value.length; i++) {
2886
+ if (!value[i]) continue;
2887
+ value[i] = traverseAndResolve(value[i], {
2888
+ node: node.elements?.[i]?.value,
2889
+ expectedTypes: expectedTypes?.includes("cubicBezier") ? ["number"] : expectedTypes,
2890
+ path: [...path, i]
2891
+ }).$value;
2892
+ }
2893
+ else if (typeof value === "object") for (const key of Object.keys(value)) {
2894
+ if (!expectedTypes?.length || !EXPECTED_NESTED_ALIAS[expectedTypes[0]]) continue;
2895
+ value[key] = traverseAndResolve(value[key], {
2896
+ node: getObjMember(node, key),
2897
+ expectedTypes: EXPECTED_NESTED_ALIAS[expectedTypes[0]][key],
2898
+ path: [...path, key]
2899
+ }).$value;
2900
+ }
2901
+ return { $value: value };
2914
2902
  }
2915
- }
2916
- token.$value = token.mode["."].$value;
2917
- break;
2918
- case "typography":
2919
- for (const mode of Object.keys(token.mode)) {
2920
- const $value = token.mode[mode].$value;
2921
- if (typeof $value !== "object") return;
2922
- for (const [k, v] of Object.entries($value)) switch (k) {
2923
- case "fontFamily":
2924
- $value[k] = normalizeFontFamily(v);
2925
- break;
2926
- case "fontWeight":
2927
- $value[k] = normalizeFontWeight(v);
2928
- break;
2903
+ if (!isAlias(value)) {
2904
+ if (!expectedTypes?.includes("string") && (value.includes("{") || value.includes("}"))) logger.error({
2905
+ ...aliasEntry,
2906
+ message: "Invalid alias syntax.",
2907
+ node
2908
+ });
2909
+ return { $value: value };
2929
2910
  }
2911
+ const refChain = [];
2912
+ const resolvedID = resolveInner(value, refChain);
2913
+ if (expectedTypes?.length && !expectedTypes.includes(tokens[resolvedID].$type)) logger.error({
2914
+ ...aliasEntry,
2915
+ message: `Cannot alias to $type "${tokens[resolvedID].$type}" from $type "${expectedTypes.join(" / ")}".`,
2916
+ node
2917
+ });
2918
+ refMap[path.join("/")] = {
2919
+ filename: token.source.filename,
2920
+ refChain
2921
+ };
2922
+ return {
2923
+ $type: tokens[resolvedID].$type,
2924
+ $value: tokens[resolvedID].mode[mode]?.$value || tokens[resolvedID].$value
2925
+ };
2930
2926
  }
2931
- token.$value = token.mode["."].$value;
2932
- break;
2927
+ const pathBase = mode === "." ? token.jsonID : `${token.jsonID}/$extensions/mode/${mode}`;
2928
+ const { $type, $value } = traverseAndResolve(token.mode[mode].$value, {
2929
+ node: aliasEntry.node,
2930
+ expectedTypes: token.$type ? [token.$type] : void 0,
2931
+ path: [pathBase, "$value"]
2932
+ });
2933
+ if (!token.$type) token.$type = $type;
2934
+ if ($value) token.mode[mode].$value = $value;
2935
+ if (mode === ".") token.$value = token.mode[mode].$value;
2936
+ }
2933
2937
  }
2934
2938
  }
2935
2939
 
2936
2940
  //#endregion
2937
- //#region src/parse/token.ts
2938
- /** Convert valid DTCG alias to $ref */
2939
- function aliasToGroupRef(alias) {
2940
- const id = parseAlias(alias);
2941
- if (id === alias) return;
2942
- return { $ref: `#/${id.replace(/~/g, "~0").replace(/\//g, "~1").replace(/\./g, "/")}` };
2943
- }
2944
- /** Convert valid DTCG alias to $ref */
2945
- function aliasToTokenRef(alias, mode) {
2946
- const id = parseAlias(alias);
2947
- if (id === alias) return;
2948
- return { $ref: `#/${id.replace(/~/g, "~0").replace(/\//g, "~1").replace(/\./g, "/")}${mode && mode !== "." ? `/$extensions/mode/${mode}` : ""}/$value` };
2949
- }
2950
- /** Generate a TokenNormalized from a Momoa node */
2951
- function tokenFromNode(node, { groups, path, source, ignore }) {
2952
- if (!(node.type === "Object" && !!getObjMember(node, "$value") && !path.includes("$extensions"))) return;
2953
- const jsonID = encodeFragment(path);
2954
- const id = path.join(".");
2955
- const originalToken = momoa.evaluate(node);
2956
- const group = groups[encodeFragment(path.slice(0, -1))];
2957
- if (group?.tokens && !group.tokens.includes(id)) group.tokens.push(id);
2958
- const nodeSource = {
2959
- filename: source.filename.href,
2960
- node
2941
+ //#region src/parse/process.ts
2942
+ function processTokens(rootSource, { config, logger, sourceByFilename, isResolver }) {
2943
+ const entry = {
2944
+ group: "parser",
2945
+ label: "init"
2961
2946
  };
2962
- const token = {
2963
- id,
2964
- $type: originalToken.$type || group.$type,
2965
- $description: originalToken.$description || void 0,
2966
- $deprecated: originalToken.$deprecated ?? group.$deprecated ?? void 0,
2967
- $value: originalToken.$value,
2968
- $extensions: originalToken.$extensions || void 0,
2969
- $extends: originalToken.$extends || void 0,
2970
- aliasChain: void 0,
2971
- aliasedBy: void 0,
2972
- aliasOf: void 0,
2973
- partialAliasOf: void 0,
2974
- dependencies: void 0,
2975
- group,
2976
- originalValue: void 0,
2977
- source: nodeSource,
2978
- jsonID,
2979
- mode: { ".": {
2980
- $value: originalToken.$value,
2981
- aliasOf: void 0,
2982
- aliasChain: void 0,
2983
- partialAliasOf: void 0,
2984
- aliasedBy: void 0,
2985
- originalValue: void 0,
2986
- dependencies: void 0,
2987
- source: {
2988
- ...nodeSource,
2989
- node: getObjMember(nodeSource.node, "$value") ?? nodeSource.node
2947
+ const refMap = {};
2948
+ function resolveRef(node, chain) {
2949
+ const { subpath } = parseRef(node.value);
2950
+ assert(subpath, logger, {
2951
+ ...entry,
2952
+ message: "Can’t resolve $ref",
2953
+ node,
2954
+ src: rootSource.src
2955
+ });
2956
+ const next = findNode(rootSource.document, subpath);
2957
+ assert(next, logger, {
2958
+ ...entry,
2959
+ message: "Can't find $ref",
2960
+ node,
2961
+ src: rootSource.src
2962
+ });
2963
+ if (next?.type === "Object") {
2964
+ const next$ref = getObjMember(next, "$ref");
2965
+ if (next$ref && next$ref.type === "String") {
2966
+ if (chain.includes(next$ref.value)) logger.error({
2967
+ ...entry,
2968
+ message: `Circular $ref detected: ${JSON.stringify(next$ref.value)}`,
2969
+ node: next$ref,
2970
+ src: rootSource.src
2971
+ });
2972
+ chain.push(next$ref.value);
2973
+ return resolveRef(next$ref, chain);
2974
+ }
2975
+ }
2976
+ return next;
2977
+ }
2978
+ const inlineStart = performance.now();
2979
+ traverse(rootSource.document, { enter(node, _parent, rawPath) {
2980
+ if (rawPath.includes("$extensions") || node.type !== "Object") return;
2981
+ const $ref = node.type === "Object" ? getObjMember(node, "$ref") : void 0;
2982
+ if (!$ref) return;
2983
+ assertStringNode($ref, logger, {
2984
+ ...entry,
2985
+ message: "Invalid $ref. Expected string.",
2986
+ node: $ref,
2987
+ src: rootSource.src
2988
+ });
2989
+ const jsonID = encodeFragment(rawPath);
2990
+ refMap[jsonID] = {
2991
+ filename: rootSource.filename.href,
2992
+ refChain: [$ref.value]
2993
+ };
2994
+ const resolved = resolveRef($ref, refMap[jsonID].refChain);
2995
+ if (resolved.type === "Object") {
2996
+ node.members.splice(node.members.findIndex((m) => m.name.type === "String" && m.name.value === "$ref"), 1);
2997
+ replaceNode(node, mergeObjects(resolved, node));
2998
+ } else replaceNode(node, resolved);
2999
+ } });
3000
+ logger.debug({
3001
+ ...entry,
3002
+ message: "Inline aliases",
3003
+ timing: performance.now() - inlineStart
3004
+ });
3005
+ function flatten$extends(node, chain) {
3006
+ const memberKeys = node.members.map((m) => m.name.type === "String" && m.name.value).filter(Boolean);
3007
+ if (memberKeys.includes("$extends")) {
3008
+ const $extends = getObjMember(node, "$extends");
3009
+ assertStringNode($extends, logger, {
3010
+ ...entry,
3011
+ message: "$extends must be a string",
3012
+ node: $extends,
3013
+ src: rootSource.src
3014
+ });
3015
+ if (memberKeys.includes("$value")) logger.error({
3016
+ ...entry,
3017
+ message: "$extends can’t exist within a token",
3018
+ node: $extends,
3019
+ src: rootSource.src
3020
+ });
3021
+ const next = isAlias($extends.value) ? aliasToGroupRef($extends.value) : void 0;
3022
+ assert(next, logger, {
3023
+ ...entry,
3024
+ message: "$extends must be a valid alias",
3025
+ node: $extends,
3026
+ src: rootSource.src
3027
+ });
3028
+ if (chain.includes(next.$ref) || chain.some((value) => value.startsWith(next.$ref) || next.$ref.startsWith(value))) logger.error({
3029
+ ...entry,
3030
+ message: "Circular $extends detected",
3031
+ node: $extends,
3032
+ src: rootSource.src
3033
+ });
3034
+ chain.push(next.$ref);
3035
+ const extended = findNode(rootSource.document, parseRef(next.$ref).subpath ?? []);
3036
+ assert(extended, logger, {
3037
+ ...entry,
3038
+ message: "Could not resolve $extends",
3039
+ node: $extends,
3040
+ src: rootSource.src
3041
+ });
3042
+ assertObjectNode(extended, logger, {
3043
+ ...entry,
3044
+ message: "$extends must resolve to a group of tokens",
3045
+ node
3046
+ });
3047
+ flatten$extends(extended, chain);
3048
+ replaceNode(node, mergeObjects(extended, node));
3049
+ }
3050
+ for (const member of node.members) if (member.value.type === "Object" && member.name.type === "String" && !["$value", "$extensions"].includes(member.name.value)) traverse(member.value, { enter(subnode, _parent) {
3051
+ if (subnode.type === "Object") flatten$extends(subnode, chain);
3052
+ } });
3053
+ }
3054
+ const extendsStart = performance.now();
3055
+ flatten$extends(rootSource.document.body, []);
3056
+ logger.debug({
3057
+ ...entry,
3058
+ message: "Resolving $extends",
3059
+ timing: performance.now() - extendsStart
3060
+ });
3061
+ const firstPass = performance.now();
3062
+ const tokens = {};
3063
+ const tokenIDs = [];
3064
+ const groups = {};
3065
+ traverse(rootSource.document, { enter(node, _parent, rawPath) {
3066
+ if (node.type !== "Object") return;
3067
+ groupFromNode(node, {
3068
+ path: isResolver ? filterResolverPaths(rawPath) : rawPath,
3069
+ groups
3070
+ });
3071
+ const token = tokenFromNode(node, {
3072
+ groups,
3073
+ ignore: config.ignore,
3074
+ path: isResolver ? filterResolverPaths(rawPath) : rawPath,
3075
+ source: rootSource
3076
+ });
3077
+ if (token) {
3078
+ tokenIDs.push(token.jsonID);
3079
+ tokens[token.jsonID] = token;
3080
+ }
3081
+ } });
3082
+ logger.debug({
3083
+ ...entry,
3084
+ message: "Parsing: 1st pass",
3085
+ timing: performance.now() - firstPass
3086
+ });
3087
+ const secondPass = performance.now();
3088
+ for (const source of Object.values(sourceByFilename)) traverse(source.document, { enter(node, _parent, path) {
3089
+ if (node.type !== "Object") return;
3090
+ const tokenRawValues = tokenRawValuesFromNode(node, {
3091
+ filename: source.filename.href,
3092
+ path
3093
+ });
3094
+ if (tokenRawValues && tokens[tokenRawValues?.jsonID]) {
3095
+ tokens[tokenRawValues.jsonID].originalValue = tokenRawValues.originalValue;
3096
+ tokens[tokenRawValues.jsonID].source = tokenRawValues.source;
3097
+ for (const mode of Object.keys(tokenRawValues.mode)) {
3098
+ tokens[tokenRawValues.jsonID].mode[mode].originalValue = tokenRawValues.mode[mode].originalValue;
3099
+ tokens[tokenRawValues.jsonID].mode[mode].source = tokenRawValues.mode[mode].source;
2990
3100
  }
2991
- } }
2992
- };
2993
- if (ignore?.deprecated && token.$deprecated || ignore?.tokens && wcmatch(ignore.tokens)(token.id)) return;
2994
- const $extensions = getObjMember(node, "$extensions");
2995
- if ($extensions) {
2996
- const modeNode = getObjMember($extensions, "mode");
2997
- for (const mode of Object.keys(token.$extensions.mode ?? {})) {
2998
- const modeValue = token.$extensions.mode[mode];
2999
- token.mode[mode] = {
3000
- $value: modeValue,
3001
- aliasOf: void 0,
3002
- aliasChain: void 0,
3003
- partialAliasOf: void 0,
3004
- aliasedBy: void 0,
3005
- originalValue: void 0,
3006
- dependencies: void 0,
3007
- source: {
3008
- ...nodeSource,
3009
- node: getObjMember(modeNode, mode)
3010
- }
3011
- };
3012
3101
  }
3102
+ } });
3103
+ resolveAliases(tokens, {
3104
+ logger,
3105
+ sources: sourceByFilename,
3106
+ refMap
3107
+ });
3108
+ logger.debug({
3109
+ ...entry,
3110
+ message: "Parsing: 2nd pass",
3111
+ timing: performance.now() - secondPass
3112
+ });
3113
+ const aliasStart = performance.now();
3114
+ graphAliases(refMap, {
3115
+ tokens,
3116
+ logger,
3117
+ sources: sourceByFilename
3118
+ });
3119
+ logger.debug({
3120
+ ...entry,
3121
+ message: "Alias graph built",
3122
+ timing: performance.now() - aliasStart
3123
+ });
3124
+ const normalizeStart = performance.now();
3125
+ for (const id of tokenIDs) {
3126
+ const token = tokens[id];
3127
+ normalize(token, {
3128
+ logger,
3129
+ src: sourceByFilename[token.source.filename]?.src
3130
+ });
3013
3131
  }
3014
- return token;
3132
+ logger.debug({
3133
+ ...entry,
3134
+ message: "Normalized values",
3135
+ timing: performance.now() - normalizeStart
3136
+ });
3137
+ if (config.alphabetize === false) return tokens;
3138
+ const sortStart = performance.now();
3139
+ const tokensSorted = {};
3140
+ tokenIDs.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
3141
+ for (const path of tokenIDs) {
3142
+ const id = refToTokenID(path);
3143
+ tokensSorted[id] = tokens[path];
3144
+ }
3145
+ for (const group of Object.values(groups)) group.tokens.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
3146
+ logger.debug({
3147
+ ...entry,
3148
+ message: "Sorted tokens",
3149
+ timing: performance.now() - sortStart
3150
+ });
3151
+ return tokensSorted;
3015
3152
  }
3016
- /** Generate originalValue and source from node */
3017
- function tokenRawValuesFromNode(node, { filename, path }) {
3018
- if (!(node.type === "Object" && getObjMember(node, "$value") && !path.includes("$extensions"))) return;
3019
- const rawValues = {
3020
- jsonID: encodeFragment(path),
3021
- originalValue: momoa.evaluate(node),
3022
- source: {
3023
- loc: filename,
3153
+
3154
+ //#endregion
3155
+ //#region src/resolver/normalize.ts
3156
+ /** Normalize resolver (assuming it’s been validated) */
3157
+ async function normalizeResolver(document, { logger, filename, req, src, yamlToMomoa }) {
3158
+ const resolverBundle = await bundle([{
3159
+ filename,
3160
+ src
3161
+ }], {
3162
+ req,
3163
+ yamlToMomoa
3164
+ });
3165
+ const resolverSource = momoa.evaluate(resolverBundle.document);
3166
+ replaceNode(document, resolverBundle.document);
3167
+ for (const set of Object.values(resolverSource.sets ?? {})) for (const source of set.sources) resolvePartials(source, {
3168
+ resolver: resolverSource,
3169
+ logger
3170
+ });
3171
+ for (const modifier of Object.values(resolverSource.modifiers ?? {})) for (const context of Object.values(modifier.contexts)) for (const source of context) resolvePartials(source, {
3172
+ resolver: resolverSource,
3173
+ logger
3174
+ });
3175
+ for (const item of resolverSource.resolutionOrder ?? []) resolvePartials(item, {
3176
+ resolver: resolverSource,
3177
+ logger
3178
+ });
3179
+ return {
3180
+ name: resolverSource.name,
3181
+ version: resolverSource.version,
3182
+ description: resolverSource.description,
3183
+ sets: resolverSource.sets,
3184
+ modifiers: resolverSource.modifiers,
3185
+ resolutionOrder: resolverSource.resolutionOrder,
3186
+ _source: {
3024
3187
  filename,
3025
- node
3026
- },
3027
- mode: {}
3028
- };
3029
- rawValues.mode["."] = {
3030
- originalValue: rawValues.originalValue.$value,
3031
- source: {
3032
- ...rawValues.source,
3033
- node: getObjMember(node, "$value")
3188
+ document
3034
3189
  }
3035
3190
  };
3036
- const $extensions = getObjMember(node, "$extensions");
3037
- if ($extensions) {
3038
- const modes = getObjMember($extensions, "mode");
3039
- if (modes) for (const modeMember of modes.members) {
3040
- const mode = modeMember.name.value;
3041
- rawValues.mode[mode] = {
3042
- originalValue: momoa.evaluate(modeMember.value),
3043
- source: {
3044
- loc: filename,
3045
- filename,
3046
- node: modeMember.value
3047
- }
3048
- };
3049
- }
3050
- }
3051
- return rawValues;
3052
3191
  }
3053
- /** Arbitrary keys that should be associated with a token group */
3054
- const GROUP_PROPERTIES = [
3055
- "$deprecated",
3056
- "$description",
3057
- "$extensions",
3058
- "$type"
3059
- ];
3060
- /**
3061
- * Generate a group from a node.
3062
- * This method mutates the groups index as it goes because of group inheritance.
3063
- * As it encounters new groups it may have to update other groups.
3064
- */
3065
- function groupFromNode(node, { path, groups }) {
3066
- const id = path.join(".");
3067
- const jsonID = encodeFragment(path);
3068
- if (!groups[jsonID]) groups[jsonID] = {
3069
- id,
3070
- $deprecated: void 0,
3071
- $description: void 0,
3072
- $extensions: void 0,
3073
- $type: void 0,
3074
- tokens: []
3192
+ /** Resolve $refs for already-initialized JS */
3193
+ function resolvePartials(source, { resolver, logger }) {
3194
+ if (!source) return;
3195
+ const entry = {
3196
+ group: "parser",
3197
+ label: "resolver"
3075
3198
  };
3076
- const groupIDs = Object.keys(groups);
3077
- groupIDs.sort();
3078
- for (const groupID of groupIDs) if (jsonID.startsWith(groupID) && groupID !== jsonID) {
3079
- groups[jsonID].$deprecated = groups[groupID]?.$deprecated ?? groups[jsonID].$deprecated;
3080
- groups[jsonID].$description = groups[groupID]?.$description ?? groups[jsonID].$description;
3081
- groups[jsonID].$type = groups[groupID]?.$type ?? groups[jsonID].$type;
3082
- }
3083
- for (const m of node.members) {
3084
- if (m.name.type !== "String" || !GROUP_PROPERTIES.includes(m.name.value)) continue;
3085
- groups[jsonID][m.name.value] = momoa.evaluate(m.value);
3086
- }
3087
- return groups[jsonID];
3088
- }
3089
- /**
3090
- * Link and reverse-link tokens in one pass.
3091
- */
3092
- function graphAliases(refMap, { tokens, logger, sources }) {
3093
- const getTokenRef = (ref) => ref.replace(/\/(\$value|\$extensions)\/?.*/, "");
3094
- for (const [jsonID, { refChain }] of Object.entries(refMap)) {
3095
- if (!refChain.length) continue;
3096
- const mode = jsonID.match(/\/\$extensions\/mode\/([^/]+)/)?.[1] || ".";
3097
- const rootRef = getTokenRef(jsonID);
3098
- const modeValue = tokens[rootRef]?.mode[mode];
3099
- if (!modeValue) continue;
3100
- if (!modeValue.dependencies) modeValue.dependencies = [];
3101
- modeValue.dependencies.push(...refChain.filter((r) => !modeValue.dependencies.includes(r)));
3102
- modeValue.dependencies.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
3103
- if (jsonID.endsWith("/$value") || tokens[jsonID]) {
3104
- modeValue.aliasOf = refToTokenID(refChain.at(-1));
3105
- modeValue.aliasChain = [...refChain.map(refToTokenID)];
3106
- }
3107
- const partial = jsonID.replace(/.*\/\$value\/?/, "").split("/").filter(Boolean);
3108
- if (partial.length && modeValue.$value && typeof modeValue.$value === "object") {
3109
- let node = modeValue.$value;
3110
- let sourceNode = modeValue.source.node;
3111
- if (!modeValue.partialAliasOf) modeValue.partialAliasOf = Array.isArray(modeValue.$value) || tokens[rootRef]?.$type === "shadow" ? [] : {};
3112
- let partialAliasOf = modeValue.partialAliasOf;
3113
- if (tokens[rootRef]?.$type === "shadow" && !Array.isArray(node)) {
3114
- if (Array.isArray(modeValue.partialAliasOf) && !modeValue.partialAliasOf.length) modeValue.partialAliasOf.push({});
3115
- partialAliasOf = modeValue.partialAliasOf[0];
3116
- }
3117
- for (let i = 0; i < partial.length; i++) {
3118
- let key = partial[i];
3119
- if (String(Number(key)) === key) key = Number(key);
3120
- if (key in node && typeof node[key] !== "undefined") {
3121
- node = node[key];
3122
- if (sourceNode.type === "Object") sourceNode = getObjMember(sourceNode, key) ?? sourceNode;
3123
- else if (sourceNode.type === "Array") sourceNode = sourceNode.elements[key]?.value ?? sourceNode;
3124
- }
3125
- if (i === partial.length - 1) {
3126
- const aliasedID = getTokenRef(refChain[0]);
3127
- if (!(aliasedID in tokens)) {
3128
- logger.error({
3129
- group: "parser",
3130
- label: "init",
3131
- message: `Invalid alias: ${aliasedID}`,
3132
- node: sourceNode,
3133
- src: sources[tokens[rootRef].source.filename]?.src
3134
- });
3135
- break;
3136
- }
3137
- partialAliasOf[key] = refToTokenID(aliasedID);
3138
- }
3139
- if (!(key in partialAliasOf)) partialAliasOf[key] = Array.isArray(node) ? [] : {};
3140
- partialAliasOf = partialAliasOf[key];
3141
- }
3142
- }
3143
- const aliasedByRefs = [jsonID, ...refChain].reverse();
3144
- for (let i = 0; i < aliasedByRefs.length; i++) {
3145
- const baseRef = getTokenRef(aliasedByRefs[i]);
3146
- const baseToken = tokens[baseRef]?.mode[mode] || tokens[baseRef];
3147
- if (!baseToken) continue;
3148
- const upstream = aliasedByRefs.slice(i + 1);
3149
- if (!upstream.length) break;
3150
- if (!baseToken.aliasedBy) baseToken.aliasedBy = [];
3151
- for (let j = 0; j < upstream.length; j++) {
3152
- const downstream = refToTokenID(upstream[j]);
3153
- if (!baseToken.aliasedBy.includes(downstream)) {
3154
- baseToken.aliasedBy.push(downstream);
3155
- if (mode === ".") tokens[baseRef].aliasedBy = baseToken.aliasedBy;
3156
- }
3157
- }
3158
- baseToken.aliasedBy.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
3159
- }
3160
- if (mode === ".") {
3161
- tokens[rootRef].aliasChain = modeValue.aliasChain;
3162
- tokens[rootRef].aliasedBy = modeValue.aliasedBy;
3163
- tokens[rootRef].aliasOf = modeValue.aliasOf;
3164
- tokens[rootRef].dependencies = modeValue.dependencies;
3165
- tokens[rootRef].partialAliasOf = modeValue.partialAliasOf;
3199
+ if (Array.isArray(source)) for (const item of source) resolvePartials(item, {
3200
+ resolver,
3201
+ logger
3202
+ });
3203
+ else if (typeof source === "object") for (const k of Object.keys(source)) if (k === "$ref") {
3204
+ const $ref = source[k];
3205
+ const { url, subpath = [] } = parseRef($ref);
3206
+ if (url !== "." || !subpath.length) logger.error({
3207
+ ...entry,
3208
+ message: `Could not load $ref ${JSON.stringify($ref)}`
3209
+ });
3210
+ const found = findObject(resolver, subpath ?? [], logger);
3211
+ if (subpath[0] === "sets" || subpath[0] === "modifiers") {
3212
+ found.type = subpath[0].replace(/s$/, "");
3213
+ found.name = subpath[1];
3166
3214
  }
3215
+ if (found) {
3216
+ for (const k2 of Object.keys(found)) source[k2] = found[k2];
3217
+ delete source.$ref;
3218
+ } else logger.error({
3219
+ ...entry,
3220
+ message: `Could not find ${JSON.stringify($ref)}`
3221
+ });
3222
+ } else resolvePartials(source[k], {
3223
+ resolver,
3224
+ logger
3225
+ });
3226
+ }
3227
+ function findObject(dict, path, logger) {
3228
+ let node = dict;
3229
+ for (const idRaw of path) {
3230
+ const id = idRaw.replace(/~/g, "~0").replace(/\//g, "~1");
3231
+ if (!(id in node)) logger.error({
3232
+ group: "parser",
3233
+ label: "resolver",
3234
+ message: `Could not load $ref ${encodeFragment(path)}`
3235
+ });
3236
+ node = node[id];
3167
3237
  }
3238
+ return node;
3168
3239
  }
3240
+
3241
+ //#endregion
3242
+ //#region src/resolver/validate.ts
3169
3243
  /**
3170
- * Convert Reference Object to token ID.
3171
- * This can then be turned into an alias by surrounding with { … }
3172
- * ⚠️ This is not mode-aware. This will flatten multiple modes into the same root token.
3244
+ * Determine whether this is likely a resolver
3245
+ * We use terms the word “likely” because this occurs before validation. Since
3246
+ * we may be dealing with a doc _intended_ to be a resolver, but may be lacking
3247
+ * some critical information, how can we determine intent? There’s a bit of
3248
+ * guesswork here, but we try and find a reasonable edge case where we sniff out
3249
+ * invalid DTCG syntax that a resolver doc would have.
3173
3250
  */
3174
- function refToTokenID($ref) {
3175
- const path = typeof $ref === "object" ? $ref.$ref : $ref;
3176
- if (typeof path !== "string") return;
3177
- const { subpath } = parseRef(path);
3178
- if (subpath?.[0] === "$defs") subpath.splice(0, 2);
3179
- return subpath?.length && subpath.join(".").replace(/\.(\$value|\$extensions).*$/, "") || void 0;
3180
- }
3181
- const EXPECTED_NESTED_ALIAS = {
3182
- border: {
3183
- color: ["color"],
3184
- stroke: ["strokeStyle"],
3185
- width: ["dimension"]
3186
- },
3187
- gradient: {
3188
- color: ["color"],
3189
- position: ["number"]
3190
- },
3191
- shadow: {
3192
- color: ["color"],
3193
- offsetX: ["dimension"],
3194
- offsetY: ["dimension"],
3195
- blur: ["dimension"],
3196
- spread: ["dimension"],
3197
- inset: ["boolean"]
3198
- },
3199
- strokeStyle: { dashArray: ["dimension"] },
3200
- transition: {
3201
- duration: ["duration"],
3202
- delay: ["duration"],
3203
- timingFunction: ["cubicBezier"]
3204
- },
3205
- typography: {
3206
- fontFamily: ["fontFamily"],
3207
- fontWeight: ["fontWeight"],
3208
- fontSize: ["dimension"],
3209
- lineHeight: ["dimension", "number"],
3210
- letterSpacing: ["dimension"]
3251
+ function isLikelyResolver(doc) {
3252
+ if (doc.body.type !== "Object") return false;
3253
+ for (const member of doc.body.members) {
3254
+ if (member.name.type !== "String") continue;
3255
+ switch (member.name.value) {
3256
+ case "name":
3257
+ case "description":
3258
+ case "version":
3259
+ if (member.value.type === "String") return true;
3260
+ break;
3261
+ case "sets":
3262
+ case "modifiers":
3263
+ if (member.value.type !== "Object") continue;
3264
+ if (getObjMember(member.value, "description")?.type === "String") return true;
3265
+ if (member.name.value === "sets" && getObjMember(member.value, "sources")?.type === "Array") return true;
3266
+ else if (member.name.value === "modifiers") {
3267
+ const contexts = getObjMember(member.value, "contexts");
3268
+ if (contexts?.type === "Object" && contexts.members.some((m) => m.value.type === "Array")) return true;
3269
+ }
3270
+ break;
3271
+ case "resolutionOrder":
3272
+ if (member.value.type === "Array") return true;
3273
+ break;
3274
+ }
3211
3275
  }
3276
+ return false;
3277
+ }
3278
+ const MESSAGE_EXPECTED = {
3279
+ STRING: "Expected string.",
3280
+ OBJECT: "Expected object.",
3281
+ ARRAY: "Expected array."
3212
3282
  };
3213
3283
  /**
3214
- * Resolve DTCG aliases, $extends, and $ref
3284
+ * Validate a resolver document.
3285
+ * There’s a ton of boilerplate here, only to surface detailed code frames. Is there a better abstraction?
3215
3286
  */
3216
- function resolveAliases(tokens, { logger, refMap, sources }) {
3217
- for (const token of Object.values(tokens)) {
3218
- const aliasEntry = {
3219
- group: "parser",
3220
- label: "init",
3221
- src: sources[token.source.filename]?.src,
3222
- node: getObjMember(token.source.node, "$value")
3223
- };
3224
- for (const mode of Object.keys(token.mode)) {
3225
- function resolveInner(alias, refChain) {
3226
- const nextRef = aliasToTokenRef(alias, mode)?.$ref;
3227
- if (!nextRef) {
3228
- logger.error({
3229
- ...aliasEntry,
3230
- message: `Internal error resolving ${JSON.stringify(refChain)}`
3231
- });
3232
- throw new Error("Internal error");
3233
- }
3234
- if (refChain.includes(nextRef)) logger.error({
3235
- ...aliasEntry,
3236
- message: "Circular alias detected."
3287
+ function validateResolver(node, { logger, src }) {
3288
+ const entry = {
3289
+ group: "parser",
3290
+ label: "resolver",
3291
+ src
3292
+ };
3293
+ if (node.body.type !== "Object") logger.error({
3294
+ ...entry,
3295
+ message: MESSAGE_EXPECTED.OBJECT,
3296
+ node
3297
+ });
3298
+ const errors = [];
3299
+ let hasVersion = false;
3300
+ let hasResolutionOrder = false;
3301
+ for (const member of node.body.members) {
3302
+ if (member.name.type !== "String") continue;
3303
+ switch (member.name.value) {
3304
+ case "name":
3305
+ case "description":
3306
+ if (member.value.type !== "String") errors.push({
3307
+ ...entry,
3308
+ message: MESSAGE_EXPECTED.STRING
3237
3309
  });
3238
- const nextJSONID = nextRef.replace(/\/(\$value|\$extensions).*/, "");
3239
- const nextToken = tokens[nextJSONID]?.mode[mode] || tokens[nextJSONID]?.mode["."];
3240
- if (!nextToken) logger.error({
3241
- ...aliasEntry,
3242
- message: `Could not resolve alias ${alias}.`
3310
+ break;
3311
+ case "version":
3312
+ hasVersion = true;
3313
+ if (member.value.type !== "String" || member.value.value !== "2025.10") errors.push({
3314
+ ...entry,
3315
+ message: `Expected "version" to be "2025.10".`,
3316
+ node: member.value
3243
3317
  });
3244
- refChain.push(nextRef);
3245
- if (isAlias(nextToken.originalValue)) return resolveInner(nextToken.originalValue, refChain);
3246
- return nextJSONID;
3247
- }
3248
- function traverseAndResolve(value, { node, expectedTypes, path }) {
3249
- if (typeof value !== "string") {
3250
- if (Array.isArray(value)) for (let i = 0; i < value.length; i++) {
3251
- if (!value[i]) continue;
3252
- value[i] = traverseAndResolve(value[i], {
3253
- node: node.elements?.[i]?.value,
3254
- expectedTypes: expectedTypes?.includes("cubicBezier") ? ["number"] : expectedTypes,
3255
- path: [...path, i]
3256
- }).$value;
3257
- }
3258
- else if (typeof value === "object") for (const key of Object.keys(value)) {
3259
- if (!expectedTypes?.length || !EXPECTED_NESTED_ALIAS[expectedTypes[0]]) continue;
3260
- value[key] = traverseAndResolve(value[key], {
3261
- node: getObjMember(node, key),
3262
- expectedTypes: EXPECTED_NESTED_ALIAS[expectedTypes[0]][key],
3263
- path: [...path, key]
3264
- }).$value;
3265
- }
3266
- return { $value: value };
3318
+ break;
3319
+ case "sets":
3320
+ case "modifiers":
3321
+ if (member.value.type !== "Object") errors.push({
3322
+ ...entry,
3323
+ message: MESSAGE_EXPECTED.OBJECT,
3324
+ node: member.value
3325
+ });
3326
+ else for (const item of member.value.members) if (item.value.type !== "Object") errors.push({
3327
+ ...entry,
3328
+ message: MESSAGE_EXPECTED.OBJECT,
3329
+ node: item.value
3330
+ });
3331
+ else {
3332
+ const validator = member.name.value === "sets" ? validateSet : validateModifier;
3333
+ errors.push(...validator(item.value, false, {
3334
+ logger,
3335
+ src
3336
+ }));
3267
3337
  }
3268
- if (!isAlias(value)) {
3269
- if (!expectedTypes?.includes("string") && (value.includes("{") || value.includes("}"))) logger.error({
3270
- ...aliasEntry,
3271
- message: "Invalid alias syntax.",
3272
- node
3338
+ break;
3339
+ case "resolutionOrder":
3340
+ hasResolutionOrder = true;
3341
+ if (member.value.type !== "Array") errors.push({
3342
+ ...entry,
3343
+ message: MESSAGE_EXPECTED.ARRAY,
3344
+ node: member.value
3345
+ });
3346
+ else if (member.value.elements.length === 0) errors.push({
3347
+ ...entry,
3348
+ message: `"resolutionOrder" can’t be empty array.`,
3349
+ node: member.value
3350
+ });
3351
+ else for (const item of member.value.elements) if (item.value.type !== "Object") errors.push({
3352
+ ...entry,
3353
+ message: MESSAGE_EXPECTED.OBJECT,
3354
+ node: item.value
3355
+ });
3356
+ else {
3357
+ const itemMembers = getObjMembers(item.value);
3358
+ if (itemMembers.$ref?.type === "String") continue;
3359
+ if (itemMembers.type?.type === "String") if (itemMembers.type.value === "set") validateSet(item.value, true, {
3360
+ logger,
3361
+ src
3362
+ });
3363
+ else if (itemMembers.type.value === "modifier") validateModifier(item.value, true, {
3364
+ logger,
3365
+ src
3366
+ });
3367
+ else errors.push({
3368
+ ...entry,
3369
+ message: `Unknown type ${JSON.stringify(itemMembers.type.value)}`,
3370
+ node: itemMembers.type
3371
+ });
3372
+ if (itemMembers.sources?.type === "Array") validateSet(item.value, true, {
3373
+ logger,
3374
+ src
3375
+ });
3376
+ else if (itemMembers.contexts?.type === "Object") validateModifier(item.value, true, {
3377
+ logger,
3378
+ src
3379
+ });
3380
+ else if (itemMembers.name?.type === "String" || itemMembers.description?.type === "String") validateSet(item.value, true, {
3381
+ logger,
3382
+ src
3273
3383
  });
3274
- return { $value: value };
3275
3384
  }
3276
- const refChain = [];
3277
- const resolvedID = resolveInner(value, refChain);
3278
- if (expectedTypes?.length && !expectedTypes.includes(tokens[resolvedID].$type)) logger.error({
3279
- ...aliasEntry,
3280
- message: `Cannot alias to $type "${tokens[resolvedID].$type}" from $type "${expectedTypes.join(" / ")}".`,
3281
- node
3385
+ break;
3386
+ case "$defs":
3387
+ case "$extensions":
3388
+ if (member.value.type !== "Object") errors.push({
3389
+ ...entry,
3390
+ message: `Expected object`,
3391
+ node: member.value
3392
+ });
3393
+ break;
3394
+ case "$schema":
3395
+ case "$ref":
3396
+ if (member.value.type !== "String") errors.push({
3397
+ ...entry,
3398
+ message: `Expected string`,
3399
+ node: member.value
3282
3400
  });
3283
- refMap[path.join("/")] = {
3284
- filename: token.source.filename,
3285
- refChain
3286
- };
3287
- return {
3288
- $type: tokens[resolvedID].$type,
3289
- $value: tokens[resolvedID].mode[mode]?.$value || tokens[resolvedID].$value
3290
- };
3291
- }
3292
- const pathBase = mode === "." ? token.jsonID : `${token.jsonID}/$extensions/mode/${mode}`;
3293
- const { $type, $value } = traverseAndResolve(token.mode[mode].$value, {
3294
- node: aliasEntry.node,
3295
- expectedTypes: token.$type ? [token.$type] : void 0,
3296
- path: [pathBase, "$value"]
3297
- });
3298
- if (!token.$type) token.$type = $type;
3299
- if ($value) token.mode[mode].$value = $value;
3300
- if (mode === ".") token.$value = token.mode[mode].$value;
3301
- }
3302
- }
3303
- }
3304
-
3305
- //#endregion
3306
- //#region src/parse/process.ts
3307
- function processTokens(rootSource, { config, logger, sourceByFilename }) {
3308
- const entry = {
3309
- group: "parser",
3310
- label: "init"
3311
- };
3312
- const refMap = {};
3313
- function resolveRef(node, chain) {
3314
- const { subpath } = parseRef(node.value);
3315
- if (!subpath) logger.error({
3316
- ...entry,
3317
- message: "Can’t resolve $ref",
3318
- node,
3319
- src: rootSource.src
3320
- });
3321
- const next = findNode(rootSource.document, subpath);
3322
- if (next?.type === "Object") {
3323
- const next$ref = getObjMember(next, "$ref");
3324
- if (next$ref && next$ref.type === "String") {
3325
- if (chain.includes(next$ref.value)) logger.error({
3401
+ break;
3402
+ default:
3403
+ errors.push({
3326
3404
  ...entry,
3327
- message: `Circular $ref detected: ${JSON.stringify(next$ref.value)}`,
3328
- node: next$ref,
3329
- src: rootSource.src
3405
+ message: `Unknown key ${JSON.stringify(member.name.value)}`,
3406
+ node: member.name,
3407
+ src
3330
3408
  });
3331
- chain.push(next$ref.value);
3332
- return resolveRef(next$ref, chain);
3333
- }
3334
- }
3335
- return next;
3336
- }
3337
- const inlineStart = performance.now();
3338
- traverse(rootSource.document, { enter(node, _parent, rawPath) {
3339
- if (rawPath.includes("$extensions") || node.type !== "Object") return;
3340
- const $ref = node.type === "Object" ? getObjMember(node, "$ref") : void 0;
3341
- if (!$ref) return;
3342
- if ($ref.type !== "String") logger.error({
3343
- ...entry,
3344
- message: "Invalid $ref. Expected string.",
3345
- node: $ref,
3346
- src: rootSource.src
3347
- });
3348
- const jsonID = encodeFragment(rawPath);
3349
- refMap[jsonID] = {
3350
- filename: rootSource.filename.href,
3351
- refChain: [$ref.value]
3352
- };
3353
- const resolved = resolveRef($ref, refMap[jsonID].refChain);
3354
- if (resolved.type === "Object") {
3355
- node.members.splice(node.members.findIndex((m) => m.name.type === "String" && m.name.value === "$ref"), 1);
3356
- replaceNode(node, mergeObjects(resolved, node));
3357
- } else replaceNode(node, resolved);
3358
- } });
3359
- logger.debug({
3360
- ...entry,
3361
- message: "Inline aliases",
3362
- timing: performance.now() - inlineStart
3363
- });
3364
- function flatten$extends(node, chain) {
3365
- const memberKeys = node.members.map((m) => m.name.type === "String" && m.name.value).filter(Boolean);
3366
- let extended;
3367
- if (memberKeys.includes("$extends")) {
3368
- const $extends = getObjMember(node, "$extends");
3369
- if ($extends.type !== "String") logger.error({
3370
- ...entry,
3371
- message: "$extends must be a string",
3372
- node: $extends,
3373
- src: rootSource.src
3374
- });
3375
- if (memberKeys.includes("$value")) logger.error({
3376
- ...entry,
3377
- message: "$extends can’t exist within a token",
3378
- node: $extends,
3379
- src: rootSource.src
3380
- });
3381
- const next = isAlias($extends.value) ? aliasToGroupRef($extends.value) : void 0;
3382
- if (!next) logger.error({
3383
- ...entry,
3384
- message: "$extends must be a valid alias",
3385
- node: $extends,
3386
- src: rootSource.src
3387
- });
3388
- if (chain.includes(next.$ref) || chain.some((value) => value.startsWith(next.$ref) || next.$ref.startsWith(value))) logger.error({
3389
- ...entry,
3390
- message: "Circular $extends detected",
3391
- node: $extends,
3392
- src: rootSource.src
3393
- });
3394
- chain.push(next.$ref);
3395
- extended = findNode(rootSource.document, parseRef(next.$ref).subpath ?? []);
3396
- if (!extended) logger.error({
3397
- ...entry,
3398
- message: "Could not resolve $extends",
3399
- node: $extends,
3400
- src: rootSource.src
3401
- });
3402
- if (extended.type !== "Object") logger.error({
3403
- ...entry,
3404
- message: "$extends must resolve to a group of tokens",
3405
- node
3406
- });
3407
- flatten$extends(extended, chain);
3408
- replaceNode(node, mergeObjects(extended, node));
3409
+ break;
3409
3410
  }
3410
- for (const member of node.members) if (member.value.type === "Object" && member.name.type === "String" && !["$value", "$extensions"].includes(member.name.value)) traverse(member.value, { enter(subnode, _parent) {
3411
- if (subnode.type === "Object") flatten$extends(subnode, chain);
3412
- } });
3413
3411
  }
3414
- const extendsStart = performance.now();
3415
- flatten$extends(rootSource.document.body, []);
3416
- logger.debug({
3412
+ if (!hasVersion) errors.push({
3417
3413
  ...entry,
3418
- message: "Resolving $extends",
3419
- timing: performance.now() - extendsStart
3414
+ message: `Missing "version".`,
3415
+ node,
3416
+ src
3420
3417
  });
3421
- const firstPass = performance.now();
3422
- const tokens = {};
3423
- const tokenIDs = [];
3424
- const groups = {};
3425
- const isResolver = isLikelyResolver(rootSource.document);
3426
- traverse(rootSource.document, { enter(node, _parent, rawPath) {
3427
- if (node.type !== "Object") return;
3428
- groupFromNode(node, {
3429
- path: isResolver ? filterResolverPaths(rawPath) : rawPath,
3430
- groups
3431
- });
3432
- const token = tokenFromNode(node, {
3433
- groups,
3434
- ignore: config.ignore,
3435
- path: isResolver ? filterResolverPaths(rawPath) : rawPath,
3436
- source: rootSource
3437
- });
3438
- if (token) {
3439
- tokenIDs.push(token.jsonID);
3440
- tokens[token.jsonID] = token;
3441
- }
3442
- } });
3443
- logger.debug({
3418
+ if (!hasResolutionOrder) errors.push({
3444
3419
  ...entry,
3445
- message: "Parsing: 1st pass",
3446
- timing: performance.now() - firstPass
3420
+ message: `Missing "resolutionOrder".`,
3421
+ node,
3422
+ src
3447
3423
  });
3448
- const secondPass = performance.now();
3449
- for (const source of Object.values(sourceByFilename)) traverse(source.document, { enter(node, _parent, path) {
3450
- if (node.type !== "Object") return;
3451
- const tokenRawValues = tokenRawValuesFromNode(node, {
3452
- filename: source.filename.href,
3453
- path
3454
- });
3455
- if (tokenRawValues && tokens[tokenRawValues?.jsonID]) {
3456
- tokens[tokenRawValues.jsonID].originalValue = tokenRawValues.originalValue;
3457
- tokens[tokenRawValues.jsonID].source = tokenRawValues.source;
3458
- for (const mode of Object.keys(tokenRawValues.mode)) {
3459
- tokens[tokenRawValues.jsonID].mode[mode].originalValue = tokenRawValues.mode[mode].originalValue;
3460
- tokens[tokenRawValues.jsonID].mode[mode].source = tokenRawValues.mode[mode].source;
3461
- }
3424
+ if (errors.length) logger.error(...errors);
3425
+ }
3426
+ function validateSet(node, isInline = false, { src }) {
3427
+ const entry = {
3428
+ group: "parser",
3429
+ label: "resolver",
3430
+ src
3431
+ };
3432
+ const errors = [];
3433
+ let hasName = !isInline;
3434
+ let hasType = !isInline;
3435
+ let hasSources = false;
3436
+ for (const member of node.members) {
3437
+ if (member.name.type !== "String") continue;
3438
+ switch (member.name.value) {
3439
+ case "name":
3440
+ hasName = true;
3441
+ if (member.value.type !== "String") errors.push({
3442
+ ...entry,
3443
+ message: MESSAGE_EXPECTED.STRING,
3444
+ node: member.value
3445
+ });
3446
+ break;
3447
+ case "description":
3448
+ if (member.value.type !== "String") errors.push({
3449
+ ...entry,
3450
+ message: MESSAGE_EXPECTED.STRING,
3451
+ node: member.value
3452
+ });
3453
+ break;
3454
+ case "type":
3455
+ hasType = true;
3456
+ if (member.value.type !== "String") errors.push({
3457
+ ...entry,
3458
+ message: MESSAGE_EXPECTED.STRING,
3459
+ node: member.value
3460
+ });
3461
+ else if (member.value.value !== "set") errors.push({
3462
+ ...entry,
3463
+ message: "\"type\" must be \"set\"."
3464
+ });
3465
+ break;
3466
+ case "sources":
3467
+ hasSources = true;
3468
+ if (member.value.type !== "Array") errors.push({
3469
+ ...entry,
3470
+ message: MESSAGE_EXPECTED.ARRAY,
3471
+ node: member.value
3472
+ });
3473
+ else if (member.value.elements.length === 0) errors.push({
3474
+ ...entry,
3475
+ message: `"sources" can’t be empty array.`,
3476
+ node: member.value
3477
+ });
3478
+ else for (const source of member.value.elements) if (source.value.type !== "Object") errors.push({
3479
+ ...entry,
3480
+ message: MESSAGE_EXPECTED.OBJECT,
3481
+ node: source.value
3482
+ });
3483
+ break;
3484
+ case "$defs":
3485
+ case "$extensions":
3486
+ if (member.value.type !== "Object") errors.push({
3487
+ ...entry,
3488
+ message: `Expected object`,
3489
+ node: member.value
3490
+ });
3491
+ break;
3492
+ case "$ref":
3493
+ if (member.value.type !== "String") errors.push({
3494
+ ...entry,
3495
+ message: `Expected string`,
3496
+ node: member.value
3497
+ });
3498
+ break;
3499
+ default:
3500
+ errors.push({
3501
+ ...entry,
3502
+ message: `Unknown key ${JSON.stringify(member.name.value)}`,
3503
+ node: member.name
3504
+ });
3505
+ break;
3462
3506
  }
3463
- } });
3464
- resolveAliases(tokens, {
3465
- logger,
3466
- sources: sourceByFilename,
3467
- refMap
3468
- });
3469
- logger.debug({
3470
- ...entry,
3471
- message: "Parsing: 2nd pass",
3472
- timing: performance.now() - secondPass
3473
- });
3474
- const aliasStart = performance.now();
3475
- graphAliases(refMap, {
3476
- tokens,
3477
- logger,
3478
- sources: sourceByFilename
3479
- });
3480
- logger.debug({
3481
- ...entry,
3482
- message: "Alias graph built",
3483
- timing: performance.now() - aliasStart
3484
- });
3485
- const normalizeStart = performance.now();
3486
- for (const id of tokenIDs) {
3487
- const token = tokens[id];
3488
- normalize(token, {
3489
- logger,
3490
- src: sourceByFilename[token.source.filename]?.src
3491
- });
3492
3507
  }
3493
- logger.debug({
3508
+ if (!hasName) errors.push({
3494
3509
  ...entry,
3495
- message: "Normalized values",
3496
- timing: performance.now() - normalizeStart
3510
+ message: `Missing "name".`,
3511
+ node
3497
3512
  });
3498
- const sortStart = performance.now();
3499
- const tokensSorted = {};
3500
- tokenIDs.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
3501
- for (const path of tokenIDs) {
3502
- const id = refToTokenID(path);
3503
- tokensSorted[id] = tokens[path];
3504
- }
3505
- for (const group of Object.values(groups)) group.tokens.sort((a, b) => a.localeCompare(b, "en-us", { numeric: true }));
3506
- logger.debug({
3513
+ if (!hasType) errors.push({
3507
3514
  ...entry,
3508
- message: "Sorted tokens",
3509
- timing: performance.now() - sortStart
3510
- });
3511
- return tokensSorted;
3512
- }
3513
-
3514
- //#endregion
3515
- //#region src/resolver/normalize.ts
3516
- /** Normalize resolver (assuming it’s been validated) */
3517
- async function normalizeResolver(document, { logger, filename, req, src, yamlToMomoa }) {
3518
- const resolverBundle = await bundle([{
3519
- filename,
3520
- src
3521
- }], {
3522
- req,
3523
- yamlToMomoa
3524
- });
3525
- const resolverSource = momoa.evaluate(resolverBundle.document);
3526
- replaceNode(document, resolverBundle.document);
3527
- for (const set of Object.values(resolverSource.sets ?? {})) for (const source of set.sources) resolvePartials(source, {
3528
- resolver: resolverSource,
3529
- logger
3530
- });
3531
- for (const modifier of Object.values(resolverSource.modifiers ?? {})) for (const context of Object.values(modifier.contexts)) for (const source of context) resolvePartials(source, {
3532
- resolver: resolverSource,
3533
- logger
3515
+ message: `"type": "set" missing.`,
3516
+ node
3534
3517
  });
3535
- for (const item of resolverSource.resolutionOrder ?? []) resolvePartials(item, {
3536
- resolver: resolverSource,
3537
- logger
3518
+ if (!hasSources) errors.push({
3519
+ ...entry,
3520
+ message: `Missing "sources".`,
3521
+ node
3538
3522
  });
3539
- return {
3540
- name: resolverSource.name,
3541
- version: resolverSource.version,
3542
- description: resolverSource.description,
3543
- sets: resolverSource.sets,
3544
- modifiers: resolverSource.modifiers,
3545
- resolutionOrder: resolverSource.resolutionOrder,
3546
- _source: {
3547
- filename,
3548
- document
3549
- }
3550
- };
3523
+ return errors;
3551
3524
  }
3552
- /** Resolve $refs for already-initialized JS */
3553
- function resolvePartials(source, { resolver, logger }) {
3554
- if (!source) return;
3525
+ function validateModifier(node, isInline = false, { src }) {
3526
+ const errors = [];
3555
3527
  const entry = {
3556
3528
  group: "parser",
3557
- label: "resolver"
3529
+ label: "resolver",
3530
+ src
3558
3531
  };
3559
- if (Array.isArray(source)) for (const item of source) resolvePartials(item, {
3560
- resolver,
3561
- logger
3562
- });
3563
- else if (typeof source === "object") for (const k of Object.keys(source)) if (k === "$ref") {
3564
- const $ref = source[k];
3565
- const { url, subpath = [] } = parseRef($ref);
3566
- if (url !== "." || !subpath.length) logger.error({
3567
- ...entry,
3568
- message: `Could not load $ref ${JSON.stringify($ref)}`
3569
- });
3570
- const found = findObject(resolver, subpath ?? [], logger);
3571
- if (subpath[0] === "sets" || subpath[0] === "modifiers") {
3572
- found.type = subpath[0].replace(/s$/, "");
3573
- found.name = subpath[1];
3532
+ let hasName = !isInline;
3533
+ let hasType = !isInline;
3534
+ let hasContexts = false;
3535
+ for (const member of node.members) {
3536
+ if (member.name.type !== "String") continue;
3537
+ switch (member.name.value) {
3538
+ case "name":
3539
+ hasName = true;
3540
+ if (member.value.type !== "String") errors.push({
3541
+ ...entry,
3542
+ message: MESSAGE_EXPECTED.STRING,
3543
+ node: member.value
3544
+ });
3545
+ break;
3546
+ case "description":
3547
+ if (member.value.type !== "String") errors.push({
3548
+ ...entry,
3549
+ message: MESSAGE_EXPECTED.STRING,
3550
+ node: member.value
3551
+ });
3552
+ break;
3553
+ case "type":
3554
+ hasType = true;
3555
+ if (member.value.type !== "String") errors.push({
3556
+ ...entry,
3557
+ message: MESSAGE_EXPECTED.STRING,
3558
+ node: member.value
3559
+ });
3560
+ else if (member.value.value !== "modifier") errors.push({
3561
+ ...entry,
3562
+ message: "\"type\" must be \"modifier\"."
3563
+ });
3564
+ break;
3565
+ case "contexts":
3566
+ hasContexts = true;
3567
+ if (member.value.type !== "Object") errors.push({
3568
+ ...entry,
3569
+ message: MESSAGE_EXPECTED.OBJECT,
3570
+ node: member.value
3571
+ });
3572
+ else if (member.value.members.length === 0) errors.push({
3573
+ ...entry,
3574
+ message: `"contexts" can’t be empty object.`,
3575
+ node: member.value
3576
+ });
3577
+ else for (const context of member.value.members) if (context.value.type !== "Array") errors.push({
3578
+ ...entry,
3579
+ message: MESSAGE_EXPECTED.ARRAY,
3580
+ node: context.value
3581
+ });
3582
+ else for (const source of context.value.elements) if (source.value.type !== "Object") errors.push({
3583
+ ...entry,
3584
+ message: MESSAGE_EXPECTED.OBJECT,
3585
+ node: source.value
3586
+ });
3587
+ break;
3588
+ case "default":
3589
+ if (member.value.type !== "String") errors.push({
3590
+ ...entry,
3591
+ message: `Expected string`,
3592
+ node: member.value
3593
+ });
3594
+ else {
3595
+ const contexts = getObjMember(node, "contexts");
3596
+ if (!contexts || !getObjMember(contexts, member.value.value)) errors.push({
3597
+ ...entry,
3598
+ message: "Invalid default context",
3599
+ node: member.value
3600
+ });
3601
+ }
3602
+ break;
3603
+ case "$defs":
3604
+ case "$extensions":
3605
+ if (member.value.type !== "Object") errors.push({
3606
+ ...entry,
3607
+ message: `Expected object`,
3608
+ node: member.value
3609
+ });
3610
+ break;
3611
+ case "$ref":
3612
+ if (member.value.type !== "String") errors.push({
3613
+ ...entry,
3614
+ message: `Expected string`,
3615
+ node: member.value
3616
+ });
3617
+ break;
3618
+ default:
3619
+ errors.push({
3620
+ ...entry,
3621
+ message: `Unknown key ${JSON.stringify(member.name.value)}`,
3622
+ node: member.name
3623
+ });
3624
+ break;
3574
3625
  }
3575
- if (found) {
3576
- for (const k2 of Object.keys(found)) source[k2] = found[k2];
3577
- delete source.$ref;
3578
- } else logger.error({
3579
- ...entry,
3580
- message: `Could not find ${JSON.stringify($ref)}`
3581
- });
3582
- } else resolvePartials(source[k], {
3583
- resolver,
3584
- logger
3585
- });
3586
- }
3587
- function findObject(dict, path, logger) {
3588
- let node = dict;
3589
- for (const idRaw of path) {
3590
- const id = idRaw.replace(/~/g, "~0").replace(/\//g, "~1");
3591
- if (!(id in node)) logger.error({
3592
- group: "parser",
3593
- label: "resolver",
3594
- message: `Could not load $ref ${encodeFragment(path)}`
3595
- });
3596
- node = node[id];
3597
3626
  }
3598
- return node;
3627
+ if (!hasName) errors.push({
3628
+ ...entry,
3629
+ message: `Missing "name".`,
3630
+ node
3631
+ });
3632
+ if (!hasType) errors.push({
3633
+ ...entry,
3634
+ message: `"type": "modifier" missing.`,
3635
+ node
3636
+ });
3637
+ if (!hasContexts) errors.push({
3638
+ ...entry,
3639
+ message: `Missing "contexts".`,
3640
+ node
3641
+ });
3642
+ return errors;
3599
3643
  }
3600
3644
 
3601
3645
  //#endregion
@@ -3687,8 +3731,8 @@ function createResolver(resolverSource, { config, logger, sources }) {
3687
3731
  ...inputDefaults,
3688
3732
  ...inputRaw
3689
3733
  };
3690
- const inputKey = makeInputKey(input);
3691
- if (resolverCache[inputKey]) return resolverCache[inputKey];
3734
+ const permutationID = getPermutationID(input);
3735
+ if (resolverCache[permutationID]) return resolverCache[permutationID];
3692
3736
  for (const item of resolverSource.resolutionOrder) switch (item.type) {
3693
3737
  case "set":
3694
3738
  for (const s of item.sources) tokensRaw = merge(tokensRaw, s);
@@ -3697,8 +3741,7 @@ function createResolver(resolverSource, { config, logger, sources }) {
3697
3741
  const context = input[item.name];
3698
3742
  const sources$1 = item.contexts[context];
3699
3743
  if (!sources$1) logger.error({
3700
- group: "parser",
3701
- label: "resolver",
3744
+ group: "resolver",
3702
3745
  message: `Modifier ${item.name} has no context ${JSON.stringify(context)}.`
3703
3746
  });
3704
3747
  for (const s of sources$1 ?? []) tokensRaw = merge(tokensRaw, s);
@@ -3715,9 +3758,10 @@ function createResolver(resolverSource, { config, logger, sources }) {
3715
3758
  config,
3716
3759
  logger,
3717
3760
  sourceByFilename: { [resolverSource._source.filename.href]: rootSource },
3761
+ isResolver: true,
3718
3762
  sources
3719
3763
  });
3720
- resolverCache[inputKey] = tokens;
3764
+ resolverCache[permutationID] = tokens;
3721
3765
  return tokens;
3722
3766
  },
3723
3767
  source: resolverSource,
@@ -3725,17 +3769,41 @@ function createResolver(resolverSource, { config, logger, sources }) {
3725
3769
  if (!allPermutations.length) allPermutations.push(...calculatePermutations(Object.entries(validContexts)));
3726
3770
  return allPermutations;
3727
3771
  },
3728
- isValidInput(input) {
3772
+ isValidInput(input, throwError = false) {
3729
3773
  if (!input || typeof input !== "object") logger.error({
3730
- group: "parser",
3731
- label: "resolver",
3774
+ group: "resolver",
3732
3775
  message: `Invalid input: ${JSON.stringify(input)}.`
3733
3776
  });
3734
- if (!Object.keys(input).every((k) => k in validContexts)) return false;
3777
+ for (const k of Object.keys(input)) if (!(k in validContexts)) {
3778
+ if (throwError) logger.error({
3779
+ group: "resolver",
3780
+ message: `No such modifier ${JSON.stringify(k)}`
3781
+ });
3782
+ return false;
3783
+ }
3735
3784
  for (const [name, contexts] of Object.entries(validContexts)) if (name in input) {
3736
- if (!contexts.includes(input[name])) return false;
3737
- } else if (!(name in inputDefaults)) return false;
3785
+ if (!contexts.includes(input[name])) {
3786
+ if (throwError) logger.error({
3787
+ group: "resolver",
3788
+ message: `Modifier "${name}" has no context ${JSON.stringify(input[name])}.`
3789
+ });
3790
+ return false;
3791
+ }
3792
+ } else if (!(name in inputDefaults)) {
3793
+ if (throwError) logger.error({
3794
+ group: "resolver",
3795
+ message: `Modifier "${name}" missing value (no default set).`
3796
+ });
3797
+ return false;
3798
+ }
3738
3799
  return true;
3800
+ },
3801
+ getPermutationID(input) {
3802
+ this.isValidInput(input, true);
3803
+ return getPermutationID({
3804
+ ...inputDefaults,
3805
+ ...input
3806
+ });
3739
3807
  }
3740
3808
  };
3741
3809
  }