@reforma/project-tokens 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -0
- package/dist/authoring.d.ts +42 -0
- package/dist/authoring.d.ts.map +1 -0
- package/dist/authoring.js +161 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +142 -0
- package/dist/color-value.d.ts +13 -0
- package/dist/color-value.d.ts.map +1 -0
- package/dist/color-value.js +71 -0
- package/dist/compilation/compiler.d.ts +61 -0
- package/dist/compilation/compiler.d.ts.map +1 -0
- package/dist/compilation/compiler.js +254 -0
- package/dist/compilation/definitions.d.ts +17 -0
- package/dist/compilation/definitions.d.ts.map +1 -0
- package/dist/compilation/definitions.js +103 -0
- package/dist/compilation/scope-validation.d.ts +12 -0
- package/dist/compilation/scope-validation.d.ts.map +1 -0
- package/dist/compilation/scope-validation.js +105 -0
- package/dist/compilation/validation.d.ts +4 -0
- package/dist/compilation/validation.d.ts.map +1 -0
- package/dist/compilation/validation.js +179 -0
- package/dist/compilation/web-projection.d.ts +31 -0
- package/dist/compilation/web-projection.d.ts.map +1 -0
- package/dist/compilation/web-projection.js +136 -0
- package/dist/generation.d.ts +6 -0
- package/dist/generation.d.ts.map +1 -0
- package/dist/generation.js +186 -0
- package/dist/json-pointer.d.ts +5 -0
- package/dist/json-pointer.d.ts.map +1 -0
- package/dist/json-pointer.js +36 -0
- package/dist/mutation/documents.d.ts +20 -0
- package/dist/mutation/documents.d.ts.map +1 -0
- package/dist/mutation/documents.js +77 -0
- package/dist/mutation/layout.d.ts +17 -0
- package/dist/mutation/layout.d.ts.map +1 -0
- package/dist/mutation/layout.js +43 -0
- package/dist/mutation/mode-operations.d.ts +15 -0
- package/dist/mutation/mode-operations.d.ts.map +1 -0
- package/dist/mutation/mode-operations.js +124 -0
- package/dist/mutation/mutations.d.ts +11 -0
- package/dist/mutation/mutations.d.ts.map +1 -0
- package/dist/mutation/mutations.js +41 -0
- package/dist/mutation/planner.d.ts +11 -0
- package/dist/mutation/planner.d.ts.map +1 -0
- package/dist/mutation/planner.js +67 -0
- package/dist/mutation/reference-renamer.d.ts +25 -0
- package/dist/mutation/reference-renamer.d.ts.map +1 -0
- package/dist/mutation/reference-renamer.js +186 -0
- package/dist/mutation/token-operations.d.ts +31 -0
- package/dist/mutation/token-operations.d.ts.map +1 -0
- package/dist/mutation/token-operations.js +309 -0
- package/dist/mutation/types.d.ts +78 -0
- package/dist/mutation/types.d.ts.map +1 -0
- package/dist/mutation/types.js +9 -0
- package/dist/mutation/validation.d.ts +4 -0
- package/dist/mutation/validation.d.ts.map +1 -0
- package/dist/mutation/validation.js +48 -0
- package/dist/source/composer.d.ts +17 -0
- package/dist/source/composer.d.ts.map +1 -0
- package/dist/source/composer.js +226 -0
- package/dist/source/documents.d.ts +18 -0
- package/dist/source/documents.d.ts.map +1 -0
- package/dist/source/documents.js +68 -0
- package/dist/source/load.d.ts +8 -0
- package/dist/source/load.d.ts.map +1 -0
- package/dist/source/load.js +13 -0
- package/dist/source/resolver.d.ts +14 -0
- package/dist/source/resolver.d.ts.map +1 -0
- package/dist/source/resolver.js +180 -0
- package/dist/source/values.d.ts +17 -0
- package/dist/source/values.d.ts.map +1 -0
- package/dist/source/values.js +21 -0
- package/package.json +66 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { TokenMutationLayout } from './layout.js';
|
|
2
|
+
import type { DtcgTokenType } from '../authoring.js';
|
|
3
|
+
import type { TokenCompilation, TokenCompilationOptions } from '../compilation/compiler.js';
|
|
4
|
+
import type { TokenMutation } from './types.js';
|
|
5
|
+
export declare class TokenOperations {
|
|
6
|
+
private readonly options;
|
|
7
|
+
private readonly compilation;
|
|
8
|
+
private readonly layout;
|
|
9
|
+
constructor(options: Omit<TokenCompilationOptions, 'documents'>, compilation: TokenCompilation, layout: TokenMutationLayout);
|
|
10
|
+
rename(mutation: Extract<TokenMutation, {
|
|
11
|
+
kind: 'token.rename' | 'group.rename';
|
|
12
|
+
}>, sourcePath: string[]): void;
|
|
13
|
+
duplicate(path: string[], destination: string[]): void;
|
|
14
|
+
createGroup(path: string[], type?: DtcgTokenType): void;
|
|
15
|
+
deleteGroup(mutation: Extract<TokenMutation, {
|
|
16
|
+
kind: 'group.delete';
|
|
17
|
+
}>, path: string[]): void;
|
|
18
|
+
deleteToken(tokenPath: string, path: string[]): void;
|
|
19
|
+
private materializeTokenReferences;
|
|
20
|
+
edit(mutation: Extract<TokenMutation, {
|
|
21
|
+
kind: 'token.create' | 'token.reset' | 'token.update';
|
|
22
|
+
}>, path: string[]): Promise<boolean>;
|
|
23
|
+
private createToken;
|
|
24
|
+
private resetToken;
|
|
25
|
+
private updateToken;
|
|
26
|
+
private assertScopeDestination;
|
|
27
|
+
private assertTokenParents;
|
|
28
|
+
private assertGroupPath;
|
|
29
|
+
}
|
|
30
|
+
export declare function validateTokenPath(path: string): string[];
|
|
31
|
+
//# sourceMappingURL=token-operations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-operations.d.ts","sourceRoot":"","sources":["../../src/mutation/token-operations.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAIlD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC5F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,qBAAa,eAAe;IAEpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAH3B,YACqB,OAAO,EAAE,IAAI,CAAC,uBAAuB,EAAE,WAAW,CAAC,EACnD,WAAW,EAAE,gBAAgB,EAC7B,MAAM,EAAE,mBAAmB,EAC5C;IAEJ,MAAM,CACF,QAAQ,EAAE,OAAO,CAAC,aAAa,EAAE;QAAE,IAAI,EAAE,cAAc,GAAG,cAAc,CAAA;KAAE,CAAC,EAC3E,UAAU,EAAE,MAAM,EAAE,QAsBvB;IAED,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,QAoB9C;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,aAAa,QAqB/C;IAED,WAAW,CACP,QAAQ,EAAE,OAAO,CAAC,aAAa,EAAE;QAAE,IAAI,EAAE,cAAc,CAAA;KAAE,CAAC,EAC1D,IAAI,EAAE,MAAM,EAAE,QAkDjB;IAED,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QA0C5C;IAED,OAAO,CAAC,0BAA0B;IAoG5B,IAAI,CACN,QAAQ,EAAE,OAAO,CAAC,aAAa,EAAE;QAC7B,IAAI,EAAE,cAAc,GAAG,aAAa,GAAG,cAAc,CAAC;KACzD,CAAC,EACF,IAAI,EAAE,MAAM,EAAE,oBA+DjB;IAED,OAAO,CAAC,WAAW;IA2BnB,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,WAAW;IAyBnB,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,eAAe;CAQ1B;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,YAgB7C"}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { relative, resolve, sep } from 'node:path';
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
|
+
import { isDtcgTokenType } from '../compilation/scope-validation.js';
|
|
4
|
+
import { decodeJsonPointer, encodeJsonPointer } from '../json-pointer.js';
|
|
5
|
+
import { TokenReferenceRenamer } from './reference-renamer.js';
|
|
6
|
+
import { TokenMutationError } from './types.js';
|
|
7
|
+
export class TokenOperations {
|
|
8
|
+
options;
|
|
9
|
+
compilation;
|
|
10
|
+
layout;
|
|
11
|
+
constructor(options, compilation, layout) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.compilation = compilation;
|
|
14
|
+
this.layout = layout;
|
|
15
|
+
}
|
|
16
|
+
rename(mutation, sourcePath) {
|
|
17
|
+
const destinationPath = validateTokenPath(mutation.to);
|
|
18
|
+
if (mutation.kind === 'token.rename' && destinationPath.at(-1) === '$root' && sourcePath.at(-1) !== '$root') {
|
|
19
|
+
throw new TokenMutationError('INVALID_PATH', 'Choose a non-reserved variable name.');
|
|
20
|
+
}
|
|
21
|
+
if (mutation.kind === 'group.rename'
|
|
22
|
+
&& (sourcePath.includes('$root') || destinationPath.includes('$root'))) {
|
|
23
|
+
throw new TokenMutationError('INVALID_PATH', '$root is a token, not a group.');
|
|
24
|
+
}
|
|
25
|
+
new TokenReferenceRenamer(this.options, this.compilation, this.layout.documents, this.layout.modeFiles).rename(mutation, sourcePath, destinationPath);
|
|
26
|
+
}
|
|
27
|
+
duplicate(path, destination) {
|
|
28
|
+
this.assertScopeDestination(path);
|
|
29
|
+
this.assertScopeDestination(destination);
|
|
30
|
+
if (destination.includes('$root') || path.slice(0, -1).join('.') !== destination.slice(0, -1).join('.')) {
|
|
31
|
+
throw new TokenMutationError('INVALID_PATH', 'Duplicate a variable under a new sibling name.');
|
|
32
|
+
}
|
|
33
|
+
const { documents, modeFiles, profile } = this.layout;
|
|
34
|
+
const source = documents.read(profile.baseFile, path);
|
|
35
|
+
if (!source || !Object.hasOwn(source, '$value')) {
|
|
36
|
+
throw new TokenMutationError('TOKEN_NOT_FOUND', 'Choose a variable defined in the base source.');
|
|
37
|
+
}
|
|
38
|
+
for (const file of new Set(modeFiles.values())) {
|
|
39
|
+
if (documents.read(file, destination) !== undefined) {
|
|
40
|
+
throw new TokenMutationError('PATH_EXISTS', 'Token or group already exists.');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const file of new Set(modeFiles.values())) {
|
|
44
|
+
const definition = documents.read(file, path);
|
|
45
|
+
if (definition !== undefined)
|
|
46
|
+
documents.edit(file, destination, definition);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
createGroup(path, type) {
|
|
50
|
+
this.assertGroupPath(path);
|
|
51
|
+
if (type !== undefined) {
|
|
52
|
+
if (path.length !== 1 || !isDtcgTokenType(type)) {
|
|
53
|
+
throw new TokenMutationError('INVALID_SCOPE', 'Create a top-level scope with a standard DTCG type.');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
this.assertScopeDestination(path);
|
|
58
|
+
}
|
|
59
|
+
if ([...this.layout.modeFiles.values()].some(file => (this.layout.documents.read(file, path) !== undefined))) {
|
|
60
|
+
throw new TokenMutationError('PATH_EXISTS', 'Token or group already exists.');
|
|
61
|
+
}
|
|
62
|
+
this.layout.documents.edit(this.layout.profile.baseFile, path, type ? { $type: type } : {});
|
|
63
|
+
}
|
|
64
|
+
deleteGroup(mutation, path) {
|
|
65
|
+
this.assertGroupPath(path);
|
|
66
|
+
const { documents, modeFiles } = this.layout;
|
|
67
|
+
const files = [...modeFiles.values()].filter(file => (documents.read(file, path) !== undefined));
|
|
68
|
+
if (files.length === 0) {
|
|
69
|
+
throw new TokenMutationError('GROUP_NOT_FOUND', 'Group does not exist.');
|
|
70
|
+
}
|
|
71
|
+
const affectedTokens = [
|
|
72
|
+
...new Set(this.compilation.permutations
|
|
73
|
+
.flatMap(permutation => permutation.tokens)
|
|
74
|
+
.filter(token => token.path.startsWith(`${mutation.path}.`))
|
|
75
|
+
.map(token => token.path)),
|
|
76
|
+
].sort();
|
|
77
|
+
const confirmedTokens = mutation.tokens
|
|
78
|
+
? [...new Set(mutation.tokens)].sort()
|
|
79
|
+
: null;
|
|
80
|
+
if (confirmedTokens && JSON.stringify(confirmedTokens) !== JSON.stringify(affectedTokens)) {
|
|
81
|
+
throw new TokenMutationError('REVISION_CONFLICT', 'The confirmed group token inventory changed.');
|
|
82
|
+
}
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
const group = documents.read(file, path);
|
|
85
|
+
const hasChildren = Object.keys(group).some(key => (!key.startsWith('$') || key === '$root'));
|
|
86
|
+
const mustBeEmpty = !mutation.tokens && hasChildren;
|
|
87
|
+
if ('$value' in group || '$extends' in group || mustBeEmpty) {
|
|
88
|
+
throw new TokenMutationError('GROUP_NOT_EMPTY', 'Only empty groups can be deleted.');
|
|
89
|
+
}
|
|
90
|
+
documents.edit(file, path, undefined);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
deleteToken(tokenPath, path) {
|
|
94
|
+
const tokens = this.compilation.permutations.flatMap(permutation => (permutation.tokens.filter(token => token.path === tokenPath)));
|
|
95
|
+
if (tokens.length === 0) {
|
|
96
|
+
throw new TokenMutationError('TOKEN_NOT_FOUND', 'Token does not exist.');
|
|
97
|
+
}
|
|
98
|
+
const sourceFiles = [...this.layout.modeFiles.values()];
|
|
99
|
+
const expectedPointer = encodeJsonPointer(path);
|
|
100
|
+
const ambiguousSource = tokens.some(token => (token.source.pointer !== expectedPointer
|
|
101
|
+
|| !sourceFiles.includes(token.source.file)));
|
|
102
|
+
if (ambiguousSource) {
|
|
103
|
+
this.layout.throwAmbiguous();
|
|
104
|
+
}
|
|
105
|
+
const defaultToken = this.compilation.permutations.find(permutation => (permutation.input.mode === this.layout.profile.defaultMode))?.tokens.find(token => token.path === tokenPath);
|
|
106
|
+
if (!defaultToken) {
|
|
107
|
+
throw new TokenMutationError('TOKEN_NOT_FOUND', 'Token does not exist in the default mode.');
|
|
108
|
+
}
|
|
109
|
+
this.materializeTokenReferences(tokenPath, path, defaultToken.value, sourceFiles);
|
|
110
|
+
for (const file of this.layout.modeFiles.values()) {
|
|
111
|
+
if (this.layout.documents.read(file, path) !== undefined) {
|
|
112
|
+
this.layout.documents.edit(file, path, undefined);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
materializeTokenReferences(tokenPath, path, value, sourceFiles) {
|
|
117
|
+
const { documents } = this.layout;
|
|
118
|
+
for (const file of documents.files) {
|
|
119
|
+
if (file === documents.entryFile)
|
|
120
|
+
continue;
|
|
121
|
+
const resolveReference = (reference) => {
|
|
122
|
+
const url = new URL(reference, pathToFileURL(resolve(this.options.workspaceRoot, file)));
|
|
123
|
+
const target = new URL(url);
|
|
124
|
+
target.hash = '';
|
|
125
|
+
const targetFile = relative(resolve(this.options.workspaceRoot), fileURLToPath(target)).split(sep).join('/');
|
|
126
|
+
const isInternal = reference.startsWith('#');
|
|
127
|
+
if ((isInternal && !sourceFiles.includes(file))
|
|
128
|
+
|| (!isInternal && !sourceFiles.includes(targetFile))) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const fragment = decodeURIComponent(url.hash.slice(1));
|
|
132
|
+
if (!fragment.startsWith('/'))
|
|
133
|
+
return undefined;
|
|
134
|
+
const referencePath = decodeJsonPointer(fragment);
|
|
135
|
+
if (!path.every((part, index) => referencePath[index] === part)) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
const componentPath = referencePath.slice(path.length);
|
|
139
|
+
if (componentPath[0] === '$value')
|
|
140
|
+
componentPath.shift();
|
|
141
|
+
else if (componentPath.length > 0)
|
|
142
|
+
return undefined;
|
|
143
|
+
let resolved = value;
|
|
144
|
+
for (const part of componentPath) {
|
|
145
|
+
if (!resolved || typeof resolved !== 'object' || !Object.hasOwn(resolved, part)) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
resolved = resolved[part];
|
|
149
|
+
}
|
|
150
|
+
return structuredClone(resolved);
|
|
151
|
+
};
|
|
152
|
+
const visitValue = (node, nodePath) => {
|
|
153
|
+
if (node === `{${tokenPath}}`) {
|
|
154
|
+
documents.edit(file, nodePath, structuredClone(value));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!node || typeof node !== 'object')
|
|
158
|
+
return;
|
|
159
|
+
if ('$ref' in node && typeof node.$ref === 'string') {
|
|
160
|
+
const resolved = resolveReference(node.$ref);
|
|
161
|
+
if (resolved !== undefined) {
|
|
162
|
+
documents.edit(file, nodePath, resolved);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const [key, child] of Object.entries(node)) {
|
|
167
|
+
if (key === '$extensions')
|
|
168
|
+
continue;
|
|
169
|
+
visitValue(child, [...nodePath, Array.isArray(node) ? Number(key) : key]);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const visitGroups = (node, nodePath = []) => {
|
|
173
|
+
if (!node || typeof node !== 'object')
|
|
174
|
+
return;
|
|
175
|
+
if ('$value' in node) {
|
|
176
|
+
const reference = '$ref' in node && typeof node.$ref === 'string'
|
|
177
|
+
? resolveReference(node.$ref)
|
|
178
|
+
: undefined;
|
|
179
|
+
if (reference !== undefined) {
|
|
180
|
+
const definition = { ...node, $value: reference };
|
|
181
|
+
delete definition.$ref;
|
|
182
|
+
documents.edit(file, nodePath, definition);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
visitValue(node.$value, [...nodePath, '$value']);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (const [key, child] of Object.entries(node)) {
|
|
189
|
+
if (!key.startsWith('$') || key === '$root') {
|
|
190
|
+
visitGroups(child, [...nodePath, key]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
visitGroups(documents.parse(file));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async edit(mutation, path) {
|
|
198
|
+
this.assertScopeDestination(path);
|
|
199
|
+
if (mutation.kind === 'token.create' && mutation.mode !== this.layout.profile.defaultMode) {
|
|
200
|
+
throw new TokenMutationError('INVALID_MUTATION', 'Create tokens in the base mode; other modes override existing tokens.');
|
|
201
|
+
}
|
|
202
|
+
if (mutation.kind === 'token.create' && path.includes('$root')) {
|
|
203
|
+
throw new TokenMutationError('INVALID_PATH', 'Choose a non-reserved variable name.');
|
|
204
|
+
}
|
|
205
|
+
if (mutation.kind === 'token.update' && Object.hasOwn(mutation.properties, '$type')) {
|
|
206
|
+
throw new TokenMutationError('INVALID_MUTATION', 'Token type belongs to its scope and cannot be edited.');
|
|
207
|
+
}
|
|
208
|
+
const selected = this.compilation.permutations.find(permutation => (permutation.input.mode === mutation.mode));
|
|
209
|
+
if (!selected) {
|
|
210
|
+
throw new TokenMutationError('MODE_NOT_FOUND', 'Mode does not exist.');
|
|
211
|
+
}
|
|
212
|
+
const token = selected.tokens.find(candidate => candidate.path === mutation.path);
|
|
213
|
+
if (mutation.kind === 'token.reset'
|
|
214
|
+
&& token
|
|
215
|
+
&& !this.layout.modeFiles.has(mutation.mode)) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
const file = await this.layout.ensureModeFile(mutation.mode);
|
|
219
|
+
this.assertTokenParents(file, path);
|
|
220
|
+
if (mutation.kind === 'token.create') {
|
|
221
|
+
this.createToken(file, path, token, mutation.token);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (!token) {
|
|
225
|
+
throw new TokenMutationError('TOKEN_NOT_FOUND', 'Token does not exist.');
|
|
226
|
+
}
|
|
227
|
+
const sourceFiles = [...this.layout.modeFiles.values()];
|
|
228
|
+
const sourceOwned = token.source.pointer === encodeJsonPointer(path)
|
|
229
|
+
&& sourceFiles.includes(token.source.file);
|
|
230
|
+
if (!sourceOwned) {
|
|
231
|
+
this.layout.throwAmbiguous();
|
|
232
|
+
}
|
|
233
|
+
if (mutation.kind === 'token.reset') {
|
|
234
|
+
this.resetToken(file, path, mutation.mode);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
this.updateToken(file, path, token.source.file, mutation.properties);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
createToken(file, path, existing, definition) {
|
|
241
|
+
if (existing || this.layout.documents.read(file, path) !== undefined) {
|
|
242
|
+
throw new TokenMutationError('PATH_EXISTS', 'Token or group already exists.');
|
|
243
|
+
}
|
|
244
|
+
if (!Object.hasOwn(definition, '$value')) {
|
|
245
|
+
throw new TokenMutationError('INVALID_MUTATION', 'A token must have a value.');
|
|
246
|
+
}
|
|
247
|
+
if (Object.hasOwn(definition, '$type')) {
|
|
248
|
+
throw new TokenMutationError('INVALID_MUTATION', 'Tokens inherit their scope type; omit $type.');
|
|
249
|
+
}
|
|
250
|
+
this.layout.documents.edit(file, path, definition);
|
|
251
|
+
}
|
|
252
|
+
resetToken(file, path, mode) {
|
|
253
|
+
if (mode === this.layout.mode.default) {
|
|
254
|
+
throw new TokenMutationError('DEFAULT_RESET', 'The base value cannot be reset.');
|
|
255
|
+
}
|
|
256
|
+
if (this.layout.documents.read(file, path) !== undefined) {
|
|
257
|
+
this.layout.documents.edit(file, path, undefined);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
updateToken(file, path, sourceFile, properties) {
|
|
261
|
+
const { documents } = this.layout;
|
|
262
|
+
if (file !== sourceFile) {
|
|
263
|
+
documents.edit(file, path, {
|
|
264
|
+
...documents.read(sourceFile, path),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (properties.$value !== undefined) {
|
|
268
|
+
documents.edit(file, [...path, '$ref'], undefined);
|
|
269
|
+
}
|
|
270
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
271
|
+
if (value !== undefined) {
|
|
272
|
+
documents.edit(file, [...path, key], value);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
assertScopeDestination(path) {
|
|
277
|
+
const scope = this.layout.documents.read(this.layout.profile.baseFile, path.slice(0, 1));
|
|
278
|
+
if (path.length < 2 || !scope || '$value' in scope || !isDtcgTokenType(scope.$type)) {
|
|
279
|
+
throw new TokenMutationError('INVALID_SCOPE', 'Choose an existing typed scope as the destination.');
|
|
280
|
+
}
|
|
281
|
+
this.assertTokenParents(this.layout.profile.baseFile, path);
|
|
282
|
+
if (!this.layout.documents.read(this.layout.profile.baseFile, path.slice(0, -1))) {
|
|
283
|
+
throw new TokenMutationError('INVALID_SCOPE', 'The destination group no longer exists.');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
assertTokenParents(file, path) {
|
|
287
|
+
for (let index = 1; index < path.length; index++) {
|
|
288
|
+
const parent = this.layout.documents.read(file, path.slice(0, index));
|
|
289
|
+
if (parent && ('$value' in parent || '$extends' in parent)) {
|
|
290
|
+
this.layout.throwAmbiguous();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
assertGroupPath(path) {
|
|
295
|
+
if (path.includes('$root')) {
|
|
296
|
+
throw new TokenMutationError('INVALID_PATH', '$root is a token, not a group.');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
export function validateTokenPath(path) {
|
|
301
|
+
const parts = path.split('.');
|
|
302
|
+
const invalid = parts.some((part, index) => (!part.trim() || part !== part.trim()
|
|
303
|
+
|| /[{}]/.test(part)
|
|
304
|
+
|| part.startsWith('$') && !(part === '$root' && index === parts.length - 1)));
|
|
305
|
+
if (invalid) {
|
|
306
|
+
throw new TokenMutationError('INVALID_PATH', 'Expected a canonical token path.');
|
|
307
|
+
}
|
|
308
|
+
return parts;
|
|
309
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { DtcgTokenType } from '../authoring.js';
|
|
2
|
+
import type { TokenCompilation } from '../compilation/compiler.js';
|
|
3
|
+
export type TokenMutation = {
|
|
4
|
+
kind: 'token.update';
|
|
5
|
+
path: string;
|
|
6
|
+
mode: string;
|
|
7
|
+
properties: {
|
|
8
|
+
$value?: unknown;
|
|
9
|
+
$description?: string;
|
|
10
|
+
$deprecated?: boolean | string;
|
|
11
|
+
};
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'token.create';
|
|
14
|
+
path: string;
|
|
15
|
+
mode: string;
|
|
16
|
+
token: Record<string, unknown> & {
|
|
17
|
+
$value: unknown;
|
|
18
|
+
$type?: never;
|
|
19
|
+
};
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'token.duplicate';
|
|
22
|
+
path: string;
|
|
23
|
+
to: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'token.delete';
|
|
26
|
+
path: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'token.reset';
|
|
29
|
+
path: string;
|
|
30
|
+
mode: string;
|
|
31
|
+
} | {
|
|
32
|
+
kind: 'token.rename';
|
|
33
|
+
path: string;
|
|
34
|
+
to: string;
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'group.rename';
|
|
37
|
+
path: string;
|
|
38
|
+
to: string;
|
|
39
|
+
} | {
|
|
40
|
+
kind: 'mode.create';
|
|
41
|
+
mode: string;
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'mode.rename';
|
|
44
|
+
mode: string;
|
|
45
|
+
to: string;
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'mode.reset';
|
|
48
|
+
mode: string;
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'mode.default';
|
|
51
|
+
mode: string;
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'mode.delete';
|
|
54
|
+
mode: string;
|
|
55
|
+
replacement?: string;
|
|
56
|
+
} | {
|
|
57
|
+
kind: 'scope.create';
|
|
58
|
+
path: string;
|
|
59
|
+
type: DtcgTokenType;
|
|
60
|
+
} | {
|
|
61
|
+
kind: 'group.create';
|
|
62
|
+
path: string;
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'group.delete';
|
|
65
|
+
path: string;
|
|
66
|
+
tokens?: string[];
|
|
67
|
+
};
|
|
68
|
+
export interface TokenFileChange {
|
|
69
|
+
path: string;
|
|
70
|
+
before: string | null;
|
|
71
|
+
after: string;
|
|
72
|
+
}
|
|
73
|
+
export declare class TokenMutationError extends Error {
|
|
74
|
+
code: string;
|
|
75
|
+
compilation?: TokenCompilation | undefined;
|
|
76
|
+
constructor(code: string, message: string, compilation?: TokenCompilation | undefined);
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/mutation/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAEnE,MAAM,MAAM,aAAa,GAAG;IACxB,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE;QACR,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;KAClC,CAAC;CACL,GAAG;IACA,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE,CAAC;CACvE,GAAG;IACA,IAAI,EAAE,iBAAiB,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACd,GAAG;IACA,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;CAChB,GAAG;IACA,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAChB,GAAG;IACA,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACd,GAAG;IACA,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACd,GAAG;IACA,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CAChB,GAAG;IACA,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACd,GAAG;IACA,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CAChB,GAAG;IACA,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;CAChB,GAAG;IACA,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG;IACA,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,aAAa,CAAC;CACvB,GAAG;IACA,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;CAChB,GAAG;IACA,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAEF,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAE9B,IAAI,EAAE,MAAM;IAEZ,WAAW,CAAC,EAAE,gBAAgB;IAHzC,YACW,IAAI,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACR,WAAW,CAAC,EAAE,gBAAgB,YAAA,EAGxC;CACJ"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type TokenMutation } from './types.js';
|
|
2
|
+
import type { TokenCompilation } from '../compilation/compiler.js';
|
|
3
|
+
export declare function validateMutationResult(previous: TokenCompilation, next: TokenCompilation, mutation: TokenMutation): void;
|
|
4
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/mutation/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEpE,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAEjF,wBAAgB,sBAAsB,CAClC,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,gBAAgB,EACtB,QAAQ,EAAE,aAAa,QAY1B"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { TokenMutationError } from './types.js';
|
|
2
|
+
export function validateMutationResult(previous, next, mutation) {
|
|
3
|
+
switch (mutation.kind) {
|
|
4
|
+
case 'token.rename':
|
|
5
|
+
case 'group.rename':
|
|
6
|
+
validateRename(previous, next, mutation);
|
|
7
|
+
break;
|
|
8
|
+
case 'mode.default':
|
|
9
|
+
case 'mode.delete':
|
|
10
|
+
validateModeValues(previous, next);
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function validateRename(previous, next, mutation) {
|
|
15
|
+
for (const nextPermutation of next.permutations) {
|
|
16
|
+
const previousPermutation = previous.permutations.find(candidate => (candidate.input.mode === nextPermutation.input.mode));
|
|
17
|
+
const before = comparableRenameTokens(previousPermutation.tokens, mutation, true);
|
|
18
|
+
const after = comparableRenameTokens(nextPermutation.tokens, mutation, false);
|
|
19
|
+
if (before !== after) {
|
|
20
|
+
throw new TokenMutationError('AMBIGUOUS_SOURCE', 'This refactor would change effective values. Edit this layout in JSON.');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function comparableRenameTokens(tokens, mutation, rename) {
|
|
25
|
+
const values = tokens.map(token => {
|
|
26
|
+
const renamed = rename && (token.path === mutation.path
|
|
27
|
+
|| mutation.kind === 'group.rename' && token.path.startsWith(`${mutation.path}.`));
|
|
28
|
+
const path = renamed
|
|
29
|
+
? mutation.to + token.path.slice(mutation.path.length)
|
|
30
|
+
: token.path;
|
|
31
|
+
return [path, token.type, token.value, token.deprecated];
|
|
32
|
+
});
|
|
33
|
+
values.sort((left, right) => String(left[0]).localeCompare(String(right[0])));
|
|
34
|
+
return JSON.stringify(values);
|
|
35
|
+
}
|
|
36
|
+
function validateModeValues(previous, next) {
|
|
37
|
+
for (const nextPermutation of next.permutations) {
|
|
38
|
+
const previousPermutation = previous.permutations.find(candidate => (candidate.input.mode === nextPermutation.input.mode));
|
|
39
|
+
const before = comparableModeTokens(previousPermutation.tokens);
|
|
40
|
+
const after = comparableModeTokens(nextPermutation.tokens);
|
|
41
|
+
if (before !== after) {
|
|
42
|
+
throw new TokenMutationError('AMBIGUOUS_SOURCE', 'This source layout cannot be rebased without changing effective mode values. Edit it in JSON.');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function comparableModeTokens(tokens) {
|
|
47
|
+
return JSON.stringify(tokens.map(token => [token.path, token.type, token.css]));
|
|
48
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TokenSourceDocuments } from './documents.js';
|
|
2
|
+
import type { SourceItem } from './values.js';
|
|
3
|
+
export declare class TokenSourceComposer {
|
|
4
|
+
private readonly documents;
|
|
5
|
+
constructor(documents: TokenSourceDocuments);
|
|
6
|
+
compose(entry: URL, items: SourceItem[], input: Record<string, string>): Promise<any>;
|
|
7
|
+
private annotate;
|
|
8
|
+
private annotateToken;
|
|
9
|
+
private rebaseValueReferences;
|
|
10
|
+
private compilerSource;
|
|
11
|
+
private expandGroups;
|
|
12
|
+
private resolveValuePointers;
|
|
13
|
+
private resolveValuePointer;
|
|
14
|
+
private validateAliases;
|
|
15
|
+
private expandArrayAliases;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=composer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/source/composer.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAMtD,OAAO,KAAK,EAER,UAAU,EAEb,MAAM,aAAa,CAAC;AAErB,qBAAa,mBAAmB;IAChB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAAtC,YAA6B,SAAS,EAAE,oBAAoB,EAAI;IAE1D,OAAO,CACT,KAAK,EAAE,GAAG,EACV,KAAK,EAAE,UAAU,EAAE,EACnB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,gBAmBhC;IAED,OAAO,CAAC,QAAQ;IA2DhB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,qBAAqB;IAgB7B,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,YAAY;YA6EN,oBAAoB;YA4BpB,mBAAmB;IAmDjC,OAAO,CAAC,eAAe;IAuBvB,OAAO,CAAC,kBAAkB;CA2C7B"}
|