@navecss/tokens 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +55 -0
- package/README.md +122 -0
- package/TOKENS.md +180 -0
- package/dist/breakpoints.d.ts +25 -0
- package/dist/breakpoints.js +44 -0
- package/dist/build-record.json +77 -0
- package/dist/core-contract.json +30 -0
- package/dist/lib/bin.d.ts +2 -0
- package/dist/lib/bin.js +113 -0
- package/dist/lib/builder.d.ts +21 -0
- package/dist/lib/builder.js +41 -0
- package/dist/lib/collision.d.ts +12 -0
- package/dist/lib/collision.js +46 -0
- package/dist/lib/composite-value.d.ts +3 -0
- package/dist/lib/composite-value.js +152 -0
- package/dist/lib/core-version.d.ts +12 -0
- package/dist/lib/core-version.js +40 -0
- package/dist/lib/dtcg-descriptions.d.ts +2 -0
- package/dist/lib/dtcg-descriptions.js +23 -0
- package/dist/lib/dtcg-malformed.d.ts +10 -0
- package/dist/lib/dtcg-malformed.js +56 -0
- package/dist/lib/dtcg-outputs.d.ts +2 -0
- package/dist/lib/dtcg-outputs.js +32 -0
- package/dist/lib/dtcg-shape.d.ts +13 -0
- package/dist/lib/dtcg-shape.js +113 -0
- package/dist/lib/errors.d.ts +23 -0
- package/dist/lib/errors.js +34 -0
- package/dist/lib/facade.d.ts +26 -0
- package/dist/lib/facade.js +97 -0
- package/dist/lib/formats.d.ts +6 -0
- package/dist/lib/formats.js +236 -0
- package/dist/lib/json-pointer.d.ts +2 -0
- package/dist/lib/json-pointer.js +28 -0
- package/dist/lib/overrides.d.ts +2 -0
- package/dist/lib/overrides.js +59 -0
- package/dist/lib/package-root.d.ts +2 -0
- package/dist/lib/package-root.js +31 -0
- package/dist/lib/reader.d.ts +8 -0
- package/dist/lib/reader.js +136 -0
- package/dist/lib/theming/adjacency-source.d.ts +20 -0
- package/dist/lib/theming/adjacency-source.js +103 -0
- package/dist/lib/theming/adjacency.d.ts +17 -0
- package/dist/lib/theming/adjacency.js +133 -0
- package/dist/lib/theming/build-record.d.ts +45 -0
- package/dist/lib/theming/build-record.js +87 -0
- package/dist/lib/theming/build-step.d.ts +14 -0
- package/dist/lib/theming/build-step.js +91 -0
- package/dist/lib/theming/color-math.d.ts +33 -0
- package/dist/lib/theming/color-math.js +125 -0
- package/dist/lib/theming/comment-open-tracker.d.ts +1 -0
- package/dist/lib/theming/comment-open-tracker.js +49 -0
- package/dist/lib/theming/consumer-build.d.ts +18 -0
- package/dist/lib/theming/consumer-build.js +29 -0
- package/dist/lib/theming/contact-sheet.d.ts +22 -0
- package/dist/lib/theming/contact-sheet.js +123 -0
- package/dist/lib/theming/contrast.d.ts +31 -0
- package/dist/lib/theming/contrast.js +133 -0
- package/dist/lib/theming/copy-lint.d.ts +13 -0
- package/dist/lib/theming/copy-lint.js +98 -0
- package/dist/lib/theming/core-contract.d.ts +29 -0
- package/dist/lib/theming/core-contract.js +71 -0
- package/dist/lib/theming/core-source.d.ts +2 -0
- package/dist/lib/theming/core-source.js +18 -0
- package/dist/lib/theming/css-color-4.d.ts +19 -0
- package/dist/lib/theming/css-color-4.js +66 -0
- package/dist/lib/theming/css-named-colours.d.ts +1 -0
- package/dist/lib/theming/css-named-colours.js +151 -0
- package/dist/lib/theming/descriptions.d.ts +1 -0
- package/dist/lib/theming/descriptions.js +21 -0
- package/dist/lib/theming/emit.d.ts +10 -0
- package/dist/lib/theming/emit.js +62 -0
- package/dist/lib/theming/feedback-signal.d.ts +11 -0
- package/dist/lib/theming/feedback-signal.js +23 -0
- package/dist/lib/theming/guard-message-probes.d.ts +6 -0
- package/dist/lib/theming/guard-message-probes.js +96 -0
- package/dist/lib/theming/ladder.d.ts +15 -0
- package/dist/lib/theming/ladder.js +81 -0
- package/dist/lib/theming/neutral.d.ts +12 -0
- package/dist/lib/theming/neutral.js +76 -0
- package/dist/lib/theming/on-star.d.ts +11 -0
- package/dist/lib/theming/on-star.js +87 -0
- package/dist/lib/theming/palette-record.d.ts +2 -0
- package/dist/lib/theming/palette-record.js +15 -0
- package/dist/lib/theming/pipeline.d.ts +43 -0
- package/dist/lib/theming/pipeline.js +104 -0
- package/dist/lib/theming/ramp.d.ts +34 -0
- package/dist/lib/theming/ramp.js +92 -0
- package/dist/lib/theming/seed-channel-split.d.ts +8 -0
- package/dist/lib/theming/seed-channel-split.js +99 -0
- package/dist/lib/theming/seed-form-parsers-css-color-4.d.ts +8 -0
- package/dist/lib/theming/seed-form-parsers-css-color-4.js +85 -0
- package/dist/lib/theming/seed-form-parsers.d.ts +6 -0
- package/dist/lib/theming/seed-form-parsers.js +155 -0
- package/dist/lib/theming/seed-ingest.d.ts +15 -0
- package/dist/lib/theming/seed-ingest.js +112 -0
- package/dist/lib/theming/seed-input.d.ts +22 -0
- package/dist/lib/theming/seed-input.js +1 -0
- package/dist/lib/theming/seed-refusal.d.ts +21 -0
- package/dist/lib/theming/seed-refusal.js +77 -0
- package/dist/lib/theming/semantics.d.ts +19 -0
- package/dist/lib/theming/semantics.js +71 -0
- package/dist/lib/theming/shipped-seeds.d.ts +2 -0
- package/dist/lib/theming/shipped-seeds.js +5 -0
- package/dist/lib/theming/step-table.d.ts +5 -0
- package/dist/lib/theming/step-table.js +26 -0
- package/dist/lib/theming/third-party-provenance.d.ts +9 -0
- package/dist/lib/theming/third-party-provenance.js +35 -0
- package/dist/lib/theming/tokens-source.d.ts +3 -0
- package/dist/lib/theming/tokens-source.js +61 -0
- package/dist/lib/token-name.d.ts +2 -0
- package/dist/lib/token-name.js +28 -0
- package/dist/lib/validate-report.d.ts +19 -0
- package/dist/lib/validate-report.js +93 -0
- package/dist/lib/validate.d.ts +16 -0
- package/dist/lib/validate.js +65 -0
- package/dist/palette-record.json +293 -0
- package/dist/tokens.css +433 -0
- package/dist/tokens.d.ts +90 -0
- package/dist/tokens.js +88 -0
- package/package.json +85 -0
- package/tokens.json +938 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { srgbToOklch } from "./color-math.js";
|
|
2
|
+
import { isNoneKeyword, parseHueDeg, readNumericToken, splitChannelArgs, } from "./seed-channel-split.js";
|
|
3
|
+
import { badChannelValue, checkOpaqueAlpha, clamp01 } from "./seed-refusal.js";
|
|
4
|
+
export const RECOGNISED_ALIASED_FUNCTIONS = {
|
|
5
|
+
hsl: 'hsl',
|
|
6
|
+
hsla: 'hsl',
|
|
7
|
+
oklch: 'oklch',
|
|
8
|
+
rgb: 'rgb',
|
|
9
|
+
rgba: 'rgb',
|
|
10
|
+
};
|
|
11
|
+
function parseRgbChannel(token) {
|
|
12
|
+
if (isNoneKeyword(token))
|
|
13
|
+
return 0;
|
|
14
|
+
if (token.endsWith('%'))
|
|
15
|
+
return clamp01(readNumericToken(token.slice(0, -1)) / 100);
|
|
16
|
+
return clamp01(readNumericToken(token) / 255);
|
|
17
|
+
}
|
|
18
|
+
function parsePercent01(token) {
|
|
19
|
+
if (isNoneKeyword(token))
|
|
20
|
+
return 0;
|
|
21
|
+
const t = token.endsWith('%') ? token.slice(0, -1) : token;
|
|
22
|
+
return clamp01(readNumericToken(t) / 100);
|
|
23
|
+
}
|
|
24
|
+
function refuseNoneInLegacySyntax(input, form, parts, isLegacySyntax) {
|
|
25
|
+
if (!isLegacySyntax)
|
|
26
|
+
return;
|
|
27
|
+
if (parts.some((part) => isNoneKeyword(part))) {
|
|
28
|
+
throw badChannelValue(input, form, 'it uses the `none` keyword, which legacy comma-separated syntax does not allow; write the seed in modern space-separated syntax instead.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function hslSectorRgbPrime(hp, c, x) {
|
|
32
|
+
switch (Math.floor(hp) % 6) {
|
|
33
|
+
case 0: {
|
|
34
|
+
return { b: 0, g: x, r: c };
|
|
35
|
+
}
|
|
36
|
+
case 1: {
|
|
37
|
+
return { b: 0, g: c, r: x };
|
|
38
|
+
}
|
|
39
|
+
case 2: {
|
|
40
|
+
return { b: x, g: c, r: 0 };
|
|
41
|
+
}
|
|
42
|
+
case 3: {
|
|
43
|
+
return { b: c, g: x, r: 0 };
|
|
44
|
+
}
|
|
45
|
+
case 4: {
|
|
46
|
+
return { b: c, g: 0, r: x };
|
|
47
|
+
}
|
|
48
|
+
default: {
|
|
49
|
+
return { b: x, g: 0, r: c };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function hslToSrgb(h, s, l) {
|
|
54
|
+
const hue = ((h % 360) + 360) % 360;
|
|
55
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
56
|
+
const hp = hue / 60;
|
|
57
|
+
const x = c * (1 - Math.abs((hp % 2) - 1));
|
|
58
|
+
const m = l - c / 2;
|
|
59
|
+
const prime = hslSectorRgbPrime(hp, c, x);
|
|
60
|
+
return { b: prime.b + m, g: prime.g + m, r: prime.r + m };
|
|
61
|
+
}
|
|
62
|
+
export function ingestHex(input, digits) {
|
|
63
|
+
let r;
|
|
64
|
+
let g;
|
|
65
|
+
let b;
|
|
66
|
+
let alphaHex;
|
|
67
|
+
switch (digits.length) {
|
|
68
|
+
case 3: {
|
|
69
|
+
r = Number.parseInt(digits[0] + digits[0], 16);
|
|
70
|
+
g = Number.parseInt(digits[1] + digits[1], 16);
|
|
71
|
+
b = Number.parseInt(digits[2] + digits[2], 16);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case 4: {
|
|
75
|
+
r = Number.parseInt(digits[0] + digits[0], 16);
|
|
76
|
+
g = Number.parseInt(digits[1] + digits[1], 16);
|
|
77
|
+
b = Number.parseInt(digits[2] + digits[2], 16);
|
|
78
|
+
alphaHex = digits[3] + digits[3];
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case 6: {
|
|
82
|
+
r = Number.parseInt(digits.slice(0, 2), 16);
|
|
83
|
+
g = Number.parseInt(digits.slice(2, 4), 16);
|
|
84
|
+
b = Number.parseInt(digits.slice(4, 6), 16);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
default: {
|
|
88
|
+
r = Number.parseInt(digits.slice(0, 2), 16);
|
|
89
|
+
g = Number.parseInt(digits.slice(2, 4), 16);
|
|
90
|
+
b = Number.parseInt(digits.slice(4, 6), 16);
|
|
91
|
+
alphaHex = digits.slice(6, 8);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (alphaHex !== undefined)
|
|
95
|
+
checkOpaqueAlpha(input, Number.parseInt(alphaHex, 16) / 255, 'hex');
|
|
96
|
+
return srgbToOklch({ b: b / 255, g: g / 255, r: r / 255 });
|
|
97
|
+
}
|
|
98
|
+
export function ingestRgb(input, args) {
|
|
99
|
+
const { alpha, isLegacySyntax, parts } = splitChannelArgs(args);
|
|
100
|
+
if (parts.length !== 3) {
|
|
101
|
+
throw badChannelValue(input, 'rgb()', `it needs three channels and this has ${parts.length}.`);
|
|
102
|
+
}
|
|
103
|
+
refuseNoneInLegacySyntax(input, 'rgb()', parts, isLegacySyntax);
|
|
104
|
+
const r = parseRgbChannel(parts[0]);
|
|
105
|
+
const g = parseRgbChannel(parts[1]);
|
|
106
|
+
const b = parseRgbChannel(parts[2]);
|
|
107
|
+
if ([r, g, b].some((n) => !Number.isFinite(n))) {
|
|
108
|
+
throw badChannelValue(input, 'rgb()', 'one or more of its channel values could not be read as a number.');
|
|
109
|
+
}
|
|
110
|
+
checkOpaqueAlpha(input, alpha, 'rgb()');
|
|
111
|
+
return srgbToOklch({ b, g, r });
|
|
112
|
+
}
|
|
113
|
+
export function ingestHsl(input, args) {
|
|
114
|
+
const { alpha, isLegacySyntax, parts } = splitChannelArgs(args);
|
|
115
|
+
if (parts.length !== 3) {
|
|
116
|
+
throw badChannelValue(input, 'hsl()', `it needs three channels and this has ${parts.length}.`);
|
|
117
|
+
}
|
|
118
|
+
refuseNoneInLegacySyntax(input, 'hsl()', parts, isLegacySyntax);
|
|
119
|
+
const h = parseHueDeg(parts[0]);
|
|
120
|
+
const s = parsePercent01(parts[1]);
|
|
121
|
+
const l = parsePercent01(parts[2]);
|
|
122
|
+
if ([h, s, l].some((n) => !Number.isFinite(n))) {
|
|
123
|
+
throw badChannelValue(input, 'hsl()', 'one or more of its channel values could not be read as a number.');
|
|
124
|
+
}
|
|
125
|
+
checkOpaqueAlpha(input, alpha, 'hsl()');
|
|
126
|
+
return srgbToOklch(hslToSrgb(h, s, l));
|
|
127
|
+
}
|
|
128
|
+
function parseOklchLightness(token) {
|
|
129
|
+
if (isNoneKeyword(token))
|
|
130
|
+
return 0;
|
|
131
|
+
if (token.endsWith('%'))
|
|
132
|
+
return readNumericToken(token.slice(0, -1)) / 100;
|
|
133
|
+
return readNumericToken(token);
|
|
134
|
+
}
|
|
135
|
+
function parseOklchChroma(token) {
|
|
136
|
+
if (isNoneKeyword(token))
|
|
137
|
+
return 0;
|
|
138
|
+
if (token.endsWith('%'))
|
|
139
|
+
return (readNumericToken(token.slice(0, -1)) / 100) * 0.4;
|
|
140
|
+
return readNumericToken(token);
|
|
141
|
+
}
|
|
142
|
+
export function ingestOklch(input, args) {
|
|
143
|
+
const { alpha, parts } = splitChannelArgs(args);
|
|
144
|
+
if (parts.length !== 3) {
|
|
145
|
+
throw badChannelValue(input, 'oklch()', `it needs three channels and this has ${parts.length}.`);
|
|
146
|
+
}
|
|
147
|
+
const l = parseOklchLightness(parts[0]);
|
|
148
|
+
const c = parseOklchChroma(parts[1]);
|
|
149
|
+
const h = parseHueDeg(parts[2]);
|
|
150
|
+
if ([l, c, h].some((n) => !Number.isFinite(n))) {
|
|
151
|
+
throw badChannelValue(input, 'oklch()', 'one or more of its channel values could not be read as a number.');
|
|
152
|
+
}
|
|
153
|
+
checkOpaqueAlpha(input, alpha, 'oklch()');
|
|
154
|
+
return { c: Math.max(0, c), h, l: clamp01(l) };
|
|
155
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Oklch } from './color-math.ts';
|
|
2
|
+
import type { ConsumerBuildComposition, ConsumerBuildOptions } from './consumer-build.ts';
|
|
3
|
+
import type { SeedInput } from './seed-input.ts';
|
|
4
|
+
export { SeedIngestRefusal } from './seed-refusal.ts';
|
|
5
|
+
export declare function parseSeed(raw: string): SeedInput;
|
|
6
|
+
export declare function ingestSeed(raw: string): Oklch;
|
|
7
|
+
interface RawConsumerSeeds {
|
|
8
|
+
danger: string;
|
|
9
|
+
declaredTintHue: number;
|
|
10
|
+
primary: string;
|
|
11
|
+
}
|
|
12
|
+
export interface RawConsumerBuildOptions extends Omit<ConsumerBuildOptions, 'seedInputs' | 'seeds'> {
|
|
13
|
+
seeds: RawConsumerSeeds;
|
|
14
|
+
}
|
|
15
|
+
export declare function composeConsumerBuildFromRawSeeds(options: RawConsumerBuildOptions): ConsumerBuildComposition;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { composeConsumerBuild } from "./consumer-build.js";
|
|
2
|
+
import { CSS_NAMED_COLOURS } from "./css-named-colours.js";
|
|
3
|
+
import { ingestColor, ingestLab, ingestLch } from "./seed-form-parsers-css-color-4.js";
|
|
4
|
+
import { ingestHex, ingestHsl, ingestOklch, ingestRgb, RECOGNISED_ALIASED_FUNCTIONS, } from "./seed-form-parsers.js";
|
|
5
|
+
import { badChannelValue, CONTEXT_DEPENDENT_FIX, NAMED_COLOUR_FIX, SeedIngestRefusal, unrecognisedForm, } from "./seed-refusal.js";
|
|
6
|
+
export { SeedIngestRefusal } from "./seed-refusal.js";
|
|
7
|
+
function isContextDependentForm(input) {
|
|
8
|
+
if (/\bcurrentcolor\b/i.test(input))
|
|
9
|
+
return true;
|
|
10
|
+
if (/\bvar\(/i.test(input))
|
|
11
|
+
return true;
|
|
12
|
+
if (/\(\s*from\s+/i.test(input))
|
|
13
|
+
return true;
|
|
14
|
+
if (/\blight-dark\(/i.test(input))
|
|
15
|
+
return true;
|
|
16
|
+
if (/\benv\(/i.test(input))
|
|
17
|
+
return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
function contextDependentDetail(input) {
|
|
21
|
+
if (/\bcurrentcolor\b/i.test(input)) {
|
|
22
|
+
return 'this seed refers to `currentColor`, which resolves only where an element renders, not at build time.';
|
|
23
|
+
}
|
|
24
|
+
if (/\(\s*from\s+/i.test(input)) {
|
|
25
|
+
return 'this seed uses relative colour syntax, which substitutes at browser time against a live declaration, not against a build-time input.';
|
|
26
|
+
}
|
|
27
|
+
if (/\blight-dark\(/i.test(input)) {
|
|
28
|
+
return 'this seed calls `light-dark()`, which resolves against the active color-scheme at browser time, not at build time.';
|
|
29
|
+
}
|
|
30
|
+
if (/\benv\(/i.test(input)) {
|
|
31
|
+
return "this seed calls `env()`, which resolves against the user agent's environment at browser time, not at build time.";
|
|
32
|
+
}
|
|
33
|
+
return 'this seed refers to a custom property or another declaration, which resolves only at browser time, not at build time.';
|
|
34
|
+
}
|
|
35
|
+
function badHexRefusal(raw, input) {
|
|
36
|
+
const body = input.slice(1);
|
|
37
|
+
if (/^[\dA-Fa-f]+$/.test(body)) {
|
|
38
|
+
return badChannelValue(raw, 'hex', `it needs 3, 4, 6 or 8 digits and this has ${body.length}.`);
|
|
39
|
+
}
|
|
40
|
+
return badChannelValue(raw, 'hex', 'its digits could not be read as hexadecimal.');
|
|
41
|
+
}
|
|
42
|
+
function ingestFunctionForm(raw, name, args) {
|
|
43
|
+
if (name === 'lab')
|
|
44
|
+
return { form: 'lab', raw, value: ingestLab(raw, args) };
|
|
45
|
+
if (name === 'lch')
|
|
46
|
+
return { form: 'lch', raw, value: ingestLch(raw, args) };
|
|
47
|
+
if (name === 'color') {
|
|
48
|
+
const { space, value } = ingestColor(raw, args);
|
|
49
|
+
return { form: 'color', raw, space, value };
|
|
50
|
+
}
|
|
51
|
+
const recognised = RECOGNISED_ALIASED_FUNCTIONS[name];
|
|
52
|
+
if (recognised === 'rgb')
|
|
53
|
+
return { form: 'rgb', raw, value: ingestRgb(raw, args) };
|
|
54
|
+
if (recognised === 'hsl')
|
|
55
|
+
return { form: 'hsl', raw, value: ingestHsl(raw, args) };
|
|
56
|
+
if (recognised === 'oklch')
|
|
57
|
+
return { form: 'oklch', raw, value: ingestOklch(raw, args) };
|
|
58
|
+
throw unrecognisedForm(raw);
|
|
59
|
+
}
|
|
60
|
+
export function parseSeed(raw) {
|
|
61
|
+
const input = raw.trim();
|
|
62
|
+
if (isContextDependentForm(input)) {
|
|
63
|
+
throw new SeedIngestRefusal({
|
|
64
|
+
act: 'form-acceptance',
|
|
65
|
+
detail: contextDependentDetail(input),
|
|
66
|
+
fix: CONTEXT_DEPENDENT_FIX,
|
|
67
|
+
input: raw,
|
|
68
|
+
refusalClass: 'context-dependent-form',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const hexMatch = /^#([\dA-Fa-f]{3}|[\dA-Fa-f]{4}|[\dA-Fa-f]{6}|[\dA-Fa-f]{8})$/.exec(input);
|
|
72
|
+
if (hexMatch) {
|
|
73
|
+
const digits = hexMatch[1];
|
|
74
|
+
return {
|
|
75
|
+
form: 'hex',
|
|
76
|
+
raw,
|
|
77
|
+
value: ingestHex(raw, digits),
|
|
78
|
+
width: digits.length,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (input.startsWith('#'))
|
|
82
|
+
throw badHexRefusal(raw, input);
|
|
83
|
+
const fnMatch = /^([a-zA-Z-]+)\(([\s\S]*)\)$/.exec(input);
|
|
84
|
+
if (fnMatch)
|
|
85
|
+
return ingestFunctionForm(raw, fnMatch[1].toLowerCase(), fnMatch[2]);
|
|
86
|
+
if (/^[a-zA-Z]+$/.test(input) && CSS_NAMED_COLOURS.has(input.toLowerCase())) {
|
|
87
|
+
throw new SeedIngestRefusal({
|
|
88
|
+
act: 'form-acceptance',
|
|
89
|
+
detail: `\`${input}\` is a valid CSS named colour, not accepted as a seed form at 0.1.0 (a scope limit, not a defect in the input).`,
|
|
90
|
+
fix: NAMED_COLOUR_FIX,
|
|
91
|
+
input: raw,
|
|
92
|
+
refusalClass: 'named-colour',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
throw unrecognisedForm(raw);
|
|
96
|
+
}
|
|
97
|
+
export function ingestSeed(raw) {
|
|
98
|
+
return parseSeed(raw).value;
|
|
99
|
+
}
|
|
100
|
+
export function composeConsumerBuildFromRawSeeds(options) {
|
|
101
|
+
const primary = parseSeed(options.seeds.primary);
|
|
102
|
+
const danger = parseSeed(options.seeds.danger);
|
|
103
|
+
return composeConsumerBuild({
|
|
104
|
+
...options,
|
|
105
|
+
seedInputs: { danger, primary },
|
|
106
|
+
seeds: {
|
|
107
|
+
danger: danger.value,
|
|
108
|
+
declaredTintHue: options.seeds.declaredTintHue,
|
|
109
|
+
primary: primary.value,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Oklch } from './color-math.ts';
|
|
2
|
+
export type ColorFunctionSpace = 'display-p3' | 'srgb';
|
|
3
|
+
export type HexWidth = 3 | 4 | 6 | 8;
|
|
4
|
+
export type SeedInput = {
|
|
5
|
+
form: 'color';
|
|
6
|
+
raw: string;
|
|
7
|
+
space: ColorFunctionSpace;
|
|
8
|
+
value: Oklch;
|
|
9
|
+
} | {
|
|
10
|
+
form: 'hex';
|
|
11
|
+
raw: string;
|
|
12
|
+
value: Oklch;
|
|
13
|
+
width: HexWidth;
|
|
14
|
+
} | {
|
|
15
|
+
form: 'hsl' | 'lab' | 'lch' | 'oklch' | 'rgb';
|
|
16
|
+
raw: string;
|
|
17
|
+
value: Oklch;
|
|
18
|
+
} | {
|
|
19
|
+
form: 'oklch';
|
|
20
|
+
value: Oklch;
|
|
21
|
+
};
|
|
22
|
+
export type SeedInputs = Partial<Record<'danger' | 'primary', SeedInput>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type SeedRefusalAct = 'channel-value' | 'form-acceptance';
|
|
2
|
+
export type SeedRefusalClass = 'context-dependent-form' | 'named-colour' | 'non-opaque-alpha' | 'unrecognised-form';
|
|
3
|
+
export declare const CONTEXT_DEPENDENT_FIX = "replace this seed with a literal colour value the build can resolve on its own, with no reference to a custom property, the current colour, or another declaration.";
|
|
4
|
+
export declare const NAMED_COLOUR_FIX = "write the seed in one of the accepted forms below, never a CSS named colour.";
|
|
5
|
+
export declare class SeedIngestRefusal extends Error {
|
|
6
|
+
readonly act: SeedRefusalAct;
|
|
7
|
+
readonly input: string;
|
|
8
|
+
readonly refusalClass: SeedRefusalClass;
|
|
9
|
+
constructor(params: {
|
|
10
|
+
act: SeedRefusalAct;
|
|
11
|
+
detail: string;
|
|
12
|
+
fix: string;
|
|
13
|
+
input: string;
|
|
14
|
+
refusalClass: SeedRefusalClass;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export declare const clamp01: (n: number) => number;
|
|
18
|
+
export declare function unrecognisedForm(input: string): SeedIngestRefusal;
|
|
19
|
+
export declare function badChannelValue(input: string, form: string, detail: string): SeedIngestRefusal;
|
|
20
|
+
export declare function unsupportedColourSpace(input: string, space: string): SeedIngestRefusal;
|
|
21
|
+
export declare function checkOpaqueAlpha(input: string, alpha: number | undefined, form: string): void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const ACCEPTED_FORM_POINTER = 'Accepted seed forms: hex (3, 4, 6 or 8 digit), rgb(), hsl(), oklch(), lab(), lch(), color() (srgb, display-p3).';
|
|
2
|
+
const NON_OPAQUE_ALPHA_FIX = "write the seed's alpha channel as fully opaque: drop the alpha channel entirely, or set it to 1 / 100% / an opaque hex pair (f / ff).";
|
|
3
|
+
export const CONTEXT_DEPENDENT_FIX = 'replace this seed with a literal colour value the build can resolve on its own, with no reference to a custom property, the current colour, or another declaration.';
|
|
4
|
+
export const NAMED_COLOUR_FIX = 'write the seed in one of the accepted forms below, never a CSS named colour.';
|
|
5
|
+
const UNRECOGNISED_FORM_FIX = 'write the seed in one of the accepted forms below.';
|
|
6
|
+
const CHANNEL_VALUE_FIX = 'write every channel of this seed as a literal number or percentage, with no missing channel.';
|
|
7
|
+
const UNSUPPORTED_COLOUR_SPACE_FIX = 'write the colour in the `srgb` or `display-p3` colour space of `color()`, or in another accepted form below.';
|
|
8
|
+
export class SeedIngestRefusal extends Error {
|
|
9
|
+
act;
|
|
10
|
+
input;
|
|
11
|
+
refusalClass;
|
|
12
|
+
constructor(params) {
|
|
13
|
+
const actPhrase = params.act === 'channel-value' ? 'channel value' : 'form acceptance';
|
|
14
|
+
super(`${params.detail} Input as given: \`${params.input}\`. Refused at: ${actPhrase}. Fix: ${params.fix} ${ACCEPTED_FORM_POINTER}`);
|
|
15
|
+
this.name = 'SeedIngestRefusal';
|
|
16
|
+
this.act = params.act;
|
|
17
|
+
this.input = params.input;
|
|
18
|
+
this.refusalClass = params.refusalClass;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const ALPHA_EPSILON = 1e-6;
|
|
22
|
+
export const clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
23
|
+
const roundTo = (n, d) => {
|
|
24
|
+
const f = 10 ** d;
|
|
25
|
+
return Math.round(n * f) / f;
|
|
26
|
+
};
|
|
27
|
+
function unrecognisedFormDetail(input) {
|
|
28
|
+
if (input === '')
|
|
29
|
+
return 'The seed is empty: nothing was written where a seed is required.';
|
|
30
|
+
if (input.trim() === '') {
|
|
31
|
+
return 'The seed is whitespace only: nothing but spaces or blank characters was written where a seed is required.';
|
|
32
|
+
}
|
|
33
|
+
return `\`${input}\` is not an accepted seed form.`;
|
|
34
|
+
}
|
|
35
|
+
export function unrecognisedForm(input) {
|
|
36
|
+
return new SeedIngestRefusal({
|
|
37
|
+
act: 'form-acceptance',
|
|
38
|
+
detail: unrecognisedFormDetail(input),
|
|
39
|
+
fix: UNRECOGNISED_FORM_FIX,
|
|
40
|
+
input,
|
|
41
|
+
refusalClass: 'unrecognised-form',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function badChannelValue(input, form, detail) {
|
|
45
|
+
return new SeedIngestRefusal({
|
|
46
|
+
act: 'channel-value',
|
|
47
|
+
detail: `\`${form}\` is an accepted seed form, but ${detail}`,
|
|
48
|
+
fix: CHANNEL_VALUE_FIX,
|
|
49
|
+
input,
|
|
50
|
+
refusalClass: 'unrecognised-form',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function unsupportedColourSpace(input, space) {
|
|
54
|
+
return new SeedIngestRefusal({
|
|
55
|
+
act: 'channel-value',
|
|
56
|
+
detail: `\`color()\` is an accepted seed form, but its colour space \`${space}\` is not one this build converts; only \`srgb\` and \`display-p3\` are.`,
|
|
57
|
+
fix: UNSUPPORTED_COLOUR_SPACE_FIX,
|
|
58
|
+
input,
|
|
59
|
+
refusalClass: 'unrecognised-form',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export function checkOpaqueAlpha(input, alpha, form) {
|
|
63
|
+
if (alpha === undefined)
|
|
64
|
+
return;
|
|
65
|
+
if (!Number.isFinite(alpha)) {
|
|
66
|
+
throw badChannelValue(input, form, 'its alpha channel could not be read as a number.');
|
|
67
|
+
}
|
|
68
|
+
if (Math.abs(alpha - 1) > ALPHA_EPSILON) {
|
|
69
|
+
throw new SeedIngestRefusal({
|
|
70
|
+
act: 'channel-value',
|
|
71
|
+
detail: `the alpha channel resolves to ${roundTo(alpha, 4)}, which is not fully opaque.`,
|
|
72
|
+
fix: NON_OPAQUE_ALPHA_FIX,
|
|
73
|
+
input,
|
|
74
|
+
refusalClass: 'non-opaque-alpha',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type Scale = 'neutral' | 'primary' | 'danger';
|
|
2
|
+
export type StepRef = {
|
|
3
|
+
scale: Scale;
|
|
4
|
+
step: number;
|
|
5
|
+
} | {
|
|
6
|
+
alias: string;
|
|
7
|
+
};
|
|
8
|
+
export interface SlotMapping {
|
|
9
|
+
light: StepRef;
|
|
10
|
+
dark: StepRef;
|
|
11
|
+
}
|
|
12
|
+
export declare const ACHROMATIC_BRANCH_SLOTS: readonly ["action.primary", "action.primary.hover", "action.primary.active", "on-action.primary", "content.link", "border.focus"];
|
|
13
|
+
export declare const CHROMATIC_MAPPING: Record<string, SlotMapping>;
|
|
14
|
+
export declare const ACHROMATIC_MAPPING: Record<string, SlotMapping>;
|
|
15
|
+
export declare const ACTION_SECONDARY_FOREGROUND = "content.primary";
|
|
16
|
+
export declare const ACTION_SECONDARY_BORDER = "border.control";
|
|
17
|
+
export declare function resolveSlotMapping(slot: string, isAchromaticBranch: boolean): SlotMapping;
|
|
18
|
+
export declare const SEMANTIC_SLOTS: readonly string[];
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const ACHROMATIC_BRANCH_SLOTS = [
|
|
2
|
+
'action.primary',
|
|
3
|
+
'action.primary.hover',
|
|
4
|
+
'action.primary.active',
|
|
5
|
+
'on-action.primary',
|
|
6
|
+
'content.link',
|
|
7
|
+
'border.focus',
|
|
8
|
+
];
|
|
9
|
+
const ref = (scale, step) => ({ scale, step });
|
|
10
|
+
const aliasRef = (slot) => ({ alias: slot });
|
|
11
|
+
export const CHROMATIC_MAPPING = {
|
|
12
|
+
'surface.base': { light: ref('neutral', 0), dark: ref('neutral', 900) },
|
|
13
|
+
'surface.raised': { light: ref('neutral', 0), dark: ref('neutral', 850) },
|
|
14
|
+
'surface.overlay': { light: ref('neutral', 0), dark: ref('neutral', 800) },
|
|
15
|
+
'surface.sunken': { light: ref('neutral', 50), dark: ref('neutral', 950) },
|
|
16
|
+
'surface.inverse': { light: ref('neutral', 900), dark: ref('neutral', 100) },
|
|
17
|
+
'content.primary': { light: ref('neutral', 900), dark: ref('neutral', 100) },
|
|
18
|
+
'content.secondary': { light: ref('neutral', 700), dark: ref('neutral', 300) },
|
|
19
|
+
'content.tertiary': { light: ref('neutral', 600), dark: ref('neutral', 400) },
|
|
20
|
+
'content.disabled': { light: ref('neutral', 300), dark: ref('neutral', 700) },
|
|
21
|
+
'content.inverse': { light: ref('neutral', 100), dark: ref('neutral', 900) },
|
|
22
|
+
'content.link': { light: ref('primary', 600), dark: ref('primary', 300) },
|
|
23
|
+
'border.default': { light: ref('neutral', 200), dark: ref('neutral', 700) },
|
|
24
|
+
'border.strong': { light: ref('neutral', 400), dark: ref('neutral', 600) },
|
|
25
|
+
'border.control': { light: ref('neutral', 500), dark: ref('neutral', 500) },
|
|
26
|
+
'border.disabled': { light: ref('neutral', 150), dark: ref('neutral', 800) },
|
|
27
|
+
'border.focus': { light: ref('primary', 500), dark: ref('primary', 500) },
|
|
28
|
+
'action.primary': { light: ref('primary', 600), dark: ref('primary', 300) },
|
|
29
|
+
'action.primary.hover': { light: ref('primary', 700), dark: ref('primary', 200) },
|
|
30
|
+
'action.primary.active': { light: ref('primary', 800), dark: ref('primary', 150) },
|
|
31
|
+
'on-action.primary': { light: ref('neutral', 0), dark: ref('neutral', 900) },
|
|
32
|
+
'action.secondary': { light: ref('neutral', 100), dark: ref('neutral', 800) },
|
|
33
|
+
'feedback.danger': { light: ref('danger', 600), dark: ref('danger', 300) },
|
|
34
|
+
'feedback.danger.foreground': { light: ref('danger', 600), dark: ref('danger', 300) },
|
|
35
|
+
'on-feedback.danger': { light: ref('neutral', 0), dark: ref('neutral', 900) },
|
|
36
|
+
'feedback.warning': { light: ref('neutral', 600), dark: ref('neutral', 300) },
|
|
37
|
+
'feedback.warning.foreground': { light: ref('neutral', 600), dark: ref('neutral', 300) },
|
|
38
|
+
'on-feedback.warning': { light: ref('neutral', 0), dark: ref('neutral', 900) },
|
|
39
|
+
'feedback.success': { light: aliasRef('feedback.warning'), dark: aliasRef('feedback.warning') },
|
|
40
|
+
'feedback.success.foreground': {
|
|
41
|
+
light: aliasRef('feedback.warning.foreground'),
|
|
42
|
+
dark: aliasRef('feedback.warning.foreground'),
|
|
43
|
+
},
|
|
44
|
+
'on-feedback.success': { light: ref('neutral', 0), dark: ref('neutral', 900) },
|
|
45
|
+
'feedback.info': { light: aliasRef('feedback.warning'), dark: aliasRef('feedback.warning') },
|
|
46
|
+
'feedback.info.foreground': {
|
|
47
|
+
light: aliasRef('feedback.warning.foreground'),
|
|
48
|
+
dark: aliasRef('feedback.warning.foreground'),
|
|
49
|
+
},
|
|
50
|
+
'on-feedback.info': { light: ref('neutral', 0), dark: ref('neutral', 900) },
|
|
51
|
+
};
|
|
52
|
+
export const ACHROMATIC_MAPPING = {
|
|
53
|
+
'action.primary': { light: ref('neutral', 800), dark: ref('neutral', 200) },
|
|
54
|
+
'action.primary.hover': { light: ref('neutral', 900), dark: ref('neutral', 150) },
|
|
55
|
+
'action.primary.active': { light: ref('neutral', 950), dark: ref('neutral', 50) },
|
|
56
|
+
'on-action.primary': { light: ref('neutral', 0), dark: ref('neutral', 900) },
|
|
57
|
+
'content.link': { light: aliasRef('content.primary'), dark: aliasRef('content.primary') },
|
|
58
|
+
'border.focus': { light: ref('neutral', 500), dark: ref('neutral', 500) },
|
|
59
|
+
};
|
|
60
|
+
export const ACTION_SECONDARY_FOREGROUND = 'content.primary';
|
|
61
|
+
export const ACTION_SECONDARY_BORDER = 'border.control';
|
|
62
|
+
export function resolveSlotMapping(slot, isAchromaticBranch) {
|
|
63
|
+
const achromaticOverride = isAchromaticBranch ? ACHROMATIC_MAPPING[slot] : undefined;
|
|
64
|
+
if (achromaticOverride)
|
|
65
|
+
return achromaticOverride;
|
|
66
|
+
const mapping = CHROMATIC_MAPPING[slot];
|
|
67
|
+
if (!mapping)
|
|
68
|
+
throw new Error(`Unknown semantic slot: ${slot}`);
|
|
69
|
+
return mapping;
|
|
70
|
+
}
|
|
71
|
+
export const SEMANTIC_SLOTS = Object.keys(CHROMATIC_MAPPING);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const STEP_TABLE = [
|
|
2
|
+
{ step: 0, l: 1 },
|
|
3
|
+
{ step: 50, l: 0.985 },
|
|
4
|
+
{ step: 100, l: 0.965 },
|
|
5
|
+
{ step: 150, l: 0.94 },
|
|
6
|
+
{ step: 200, l: 0.905 },
|
|
7
|
+
{ step: 300, l: 0.835 },
|
|
8
|
+
{ step: 400, l: 0.72 },
|
|
9
|
+
{ step: 500, l: 0.59 },
|
|
10
|
+
{ step: 600, l: 0.47 },
|
|
11
|
+
{ step: 700, l: 0.37 },
|
|
12
|
+
{ step: 800, l: 0.27 },
|
|
13
|
+
{ step: 850, l: 0.225 },
|
|
14
|
+
{ step: 900, l: 0.18 },
|
|
15
|
+
{ step: 950, l: 0.12 },
|
|
16
|
+
{ step: 1000, l: 0 },
|
|
17
|
+
];
|
|
18
|
+
const STEP_LIGHTNESS = new Map(STEP_TABLE.map((r) => [r.step, r.l]));
|
|
19
|
+
export function stepLightness(step) {
|
|
20
|
+
const l = STEP_LIGHTNESS.get(step);
|
|
21
|
+
if (l === undefined) {
|
|
22
|
+
throw new Error(`Step ${step} is not defined in the shared step table. ` +
|
|
23
|
+
`Valid steps: ${STEP_TABLE.map((r) => r.step).join(', ')}. Open an issue.`);
|
|
24
|
+
}
|
|
25
|
+
return l;
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const R6_CONSUMER_ARTIFACTS: readonly ["tokens.css", "tokens.js", "tokens.d.ts", "breakpoints.js", "breakpoints.d.ts", "palette-record.json", "build-record.json"];
|
|
2
|
+
export interface ProvenanceArtifact {
|
|
3
|
+
path: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const THIRD_PARTY_RESIDUE_PATTERNS: readonly RegExp[];
|
|
7
|
+
export declare function findThirdPartyResidue(artifacts: readonly ProvenanceArtifact[]): string[];
|
|
8
|
+
export declare function findUnattributedHeader(artifacts: readonly ProvenanceArtifact[]): string[];
|
|
9
|
+
export declare const THIRD_PARTY_TEXT_ROUTING_POLICY: string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const R6_CONSUMER_ARTIFACTS = [
|
|
2
|
+
'tokens.css',
|
|
3
|
+
'tokens.js',
|
|
4
|
+
'tokens.d.ts',
|
|
5
|
+
'breakpoints.js',
|
|
6
|
+
'breakpoints.d.ts',
|
|
7
|
+
'palette-record.json',
|
|
8
|
+
'build-record.json',
|
|
9
|
+
];
|
|
10
|
+
const RESIDUE_FRAGMENTS = [
|
|
11
|
+
['style', '[- ]?', 'dictionar', 'y'],
|
|
12
|
+
['terra', 'zzo'],
|
|
13
|
+
['tokens', '[- ]', 'studio'],
|
|
14
|
+
['fig', 'ma'],
|
|
15
|
+
['cul', 'ori'],
|
|
16
|
+
['color', String.raw `js\.io`],
|
|
17
|
+
];
|
|
18
|
+
export const THIRD_PARTY_RESIDUE_PATTERNS = RESIDUE_FRAGMENTS.map((fragments) => new RegExp(fragments.join(''), 'i'));
|
|
19
|
+
const NAVE_ATTRIBUTION_PATTERN = /^\/\*\*?\s*\n?\s*\*?\s*Nave (Design System|breakpoints)\b/;
|
|
20
|
+
export function findThirdPartyResidue(artifacts) {
|
|
21
|
+
return artifacts
|
|
22
|
+
.filter((artifact) => THIRD_PARTY_RESIDUE_PATTERNS.some((pattern) => pattern.test(artifact.content)))
|
|
23
|
+
.map((artifact) => artifact.path);
|
|
24
|
+
}
|
|
25
|
+
export function findUnattributedHeader(artifacts) {
|
|
26
|
+
return artifacts
|
|
27
|
+
.filter((artifact) => !NAVE_ATTRIBUTION_PATTERN.test(artifact.content))
|
|
28
|
+
.map((artifact) => artifact.path);
|
|
29
|
+
}
|
|
30
|
+
export const THIRD_PARTY_TEXT_ROUTING_POLICY = 'Any proposal to vendor a colour-space or DTCG 2025.10 parser to make this entry point ' +
|
|
31
|
+
'self-contained, to reach for a bundler with `noExternal` or a bundled helper, to add a ' +
|
|
32
|
+
'dependency and then inline it so the tarball looks dependency-free, or to emit text ' +
|
|
33
|
+
"taken from a third party into a consumer's committed artifact, is routed through this " +
|
|
34
|
+
"project's own licence review before publication. These are examples, not an " +
|
|
35
|
+
'enumeration: a prohibition written as a list defaults to permitted.';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { findPackageRoot } from "../package-root.js";
|
|
4
|
+
export const TOKENS_JSON_PATH = path.join(findPackageRoot(import.meta.url), 'tokens.json');
|
|
5
|
+
function didCloseBlock(char, state) {
|
|
6
|
+
if (state.escaped) {
|
|
7
|
+
state.escaped = false;
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
if (char === '\\') {
|
|
11
|
+
state.escaped = state.inString;
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (char === '"') {
|
|
15
|
+
state.inString = !state.inString;
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (state.inString)
|
|
19
|
+
return false;
|
|
20
|
+
if (char === '{') {
|
|
21
|
+
state.depth += 1;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (char === '}') {
|
|
25
|
+
state.depth -= 1;
|
|
26
|
+
return state.depth === 0;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function adjacencyBlockText(rawText) {
|
|
31
|
+
const start = rawText.indexOf('{', rawText.indexOf('"adjacency"'));
|
|
32
|
+
if (start <= 0)
|
|
33
|
+
return undefined;
|
|
34
|
+
const state = { depth: 0, escaped: false, inString: false };
|
|
35
|
+
for (let i = start; i < rawText.length; i += 1) {
|
|
36
|
+
if (didCloseBlock(rawText[i], state))
|
|
37
|
+
return rawText.slice(start, i + 1);
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const SUBJECT_KEY_RE = /"([^"]+)"\s*:\s*\[/g;
|
|
42
|
+
export function assertNoDuplicateAdjacencySubjects(rawText) {
|
|
43
|
+
const block = adjacencyBlockText(rawText);
|
|
44
|
+
if (block === undefined)
|
|
45
|
+
return;
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
for (const [, subject] of block.matchAll(SUBJECT_KEY_RE)) {
|
|
48
|
+
if (seen.has(subject)) {
|
|
49
|
+
throw new Error(`Adjacency declaration: the adjacency block in @navecss/tokens' own bundled ` +
|
|
50
|
+
`tokens.json declares the subject "${subject}" twice. JSON.parse keeps only the ` +
|
|
51
|
+
"last block, so the first one's pairs would be dropped silently: merge the two " +
|
|
52
|
+
'into one subject key.');
|
|
53
|
+
}
|
|
54
|
+
seen.add(subject);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function loadShippedTokensSource() {
|
|
58
|
+
const rawText = readFileSync(TOKENS_JSON_PATH, 'utf8');
|
|
59
|
+
assertNoDuplicateAdjacencySubjects(rawText);
|
|
60
|
+
return JSON.parse(rawText);
|
|
61
|
+
}
|