figma-tokens-flattener 1.0.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/README.md +56 -0
- package/index.js +629 -0
- package/package.json +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# figma-tokens-flattener
|
|
2
|
+
|
|
3
|
+
A tool for transforming Ant Design tokens from Tokens Studio for Figma (Single file) into flat style mappings for light and dark themes.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```npm install figma-tokens-flattener --save-dev```
|
|
8
|
+
|
|
9
|
+
## Create Configuration
|
|
10
|
+
|
|
11
|
+
Create a figma-tokens-flattener-config.json file in your project root:
|
|
12
|
+
|
|
13
|
+
```{
|
|
14
|
+
"source": {
|
|
15
|
+
"tokensFile": "src/tokens/tokens.json"
|
|
16
|
+
},
|
|
17
|
+
"target": {
|
|
18
|
+
"jsonsDir": "src/tokens"
|
|
19
|
+
}
|
|
20
|
+
}```
|
|
21
|
+
|
|
22
|
+
## Run Transformation
|
|
23
|
+
|
|
24
|
+
```npx simple-token-transformer flatten```
|
|
25
|
+
|
|
26
|
+
## Alternative Way (Recommended)
|
|
27
|
+
|
|
28
|
+
Add a script to your package.json:
|
|
29
|
+
|
|
30
|
+
```{
|
|
31
|
+
"scripts": {
|
|
32
|
+
"flatten-tokens": "simple-token-transformer flatten"
|
|
33
|
+
}
|
|
34
|
+
}```
|
|
35
|
+
|
|
36
|
+
And run with:
|
|
37
|
+
|
|
38
|
+
```npm run flatten-tokens```
|
|
39
|
+
|
|
40
|
+
## How It Works
|
|
41
|
+
|
|
42
|
+
1. Reading Tokens - The tool reads the JSON file exported from Tokens Studio for Figma
|
|
43
|
+
|
|
44
|
+
2. Theme Separation - Automatically splits tokens into light and dark themes
|
|
45
|
+
|
|
46
|
+
3. Flat Structure Transformation - Converts nested token structure into flat mapping
|
|
47
|
+
|
|
48
|
+
4. Result Saving - Creates separate JSON files for each theme
|
|
49
|
+
|
|
50
|
+
## Output
|
|
51
|
+
After running the command, the following files will be created in the target directory:
|
|
52
|
+
|
|
53
|
+
light-theme-tokens.json - tokens for light theme
|
|
54
|
+
|
|
55
|
+
dark-theme-tokens.json - tokens for dark theme
|
|
56
|
+
|
package/index.js
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/* Color */
|
|
5
|
+
function flattenColorGroups(colorGroups) {
|
|
6
|
+
const flattened = {};
|
|
7
|
+
if (!colorGroups || typeof colorGroups !== 'object') {
|
|
8
|
+
console.warn('An object with groups of colors was expected, but received:', colorGroups);
|
|
9
|
+
return flattened;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
Object.keys(colorGroups).forEach(groupName => {
|
|
13
|
+
const shades = colorGroups[groupName];
|
|
14
|
+
if (shades && typeof shades === 'object') {
|
|
15
|
+
Object.keys(shades).forEach(shadeKey => {
|
|
16
|
+
const shadeObj = shades[shadeKey];
|
|
17
|
+
if (shadeObj && typeof shadeObj === 'object' && shadeObj.hasOwnProperty('value')) {
|
|
18
|
+
const flatKey = `${groupName}.${shadeKey}`;
|
|
19
|
+
flattened[flatKey] = shadeObj.value.toLowerCase();
|
|
20
|
+
} else {
|
|
21
|
+
console.warn(`The wrong structure for the color ${groupName}.${shadeKey}:`, shadeObj);
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
} else {
|
|
25
|
+
console.warn(`The wrong structure for a group of colors ${groupName}:`, shades);
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
return flattened;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Seed */
|
|
32
|
+
function flattenSeedTokens(seedTokens) {
|
|
33
|
+
const flattened = {};
|
|
34
|
+
if (!seedTokens || typeof seedTokens !== 'object') {
|
|
35
|
+
console.warn('An object with seed tokens was expected, but received:', seedTokens);
|
|
36
|
+
return flattened;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
Object.keys(seedTokens).forEach(tokenName => {
|
|
41
|
+
const tokenObj = seedTokens[tokenName];
|
|
42
|
+
|
|
43
|
+
if (!tokenObj || typeof tokenObj !== 'object') {
|
|
44
|
+
console.warn(`Incorrect structure for the seed token ${tokenName}:`, tokenObj);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let valueToUse;
|
|
49
|
+
|
|
50
|
+
if (tokenObj.hasOwnProperty('value')) {
|
|
51
|
+
valueToUse = tokenObj.value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
else if (tokenObj.hasOwnProperty('style') && tokenObj.style && typeof tokenObj.style === 'object' && tokenObj.style.hasOwnProperty('value')) {
|
|
55
|
+
valueToUse = tokenObj.style.value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
else {
|
|
59
|
+
console.warn(`Unsupported structure for the seed token ${tokenName}:`, tokenObj);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof valueToUse === 'string' && !isNaN(valueToUse) && !isNaN(parseFloat(valueToUse))) {
|
|
64
|
+
if (Number.isInteger(parseFloat(valueToUse))) {
|
|
65
|
+
valueToUse = parseInt(valueToUse, 10);
|
|
66
|
+
} else {
|
|
67
|
+
valueToUse = parseFloat(valueToUse);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
flattened[tokenName] = valueToUse;
|
|
72
|
+
})
|
|
73
|
+
return flattened;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Map */
|
|
77
|
+
function hexToRgb(hex) {
|
|
78
|
+
if (typeof hex !== 'string') return hex;
|
|
79
|
+
const hexMatch = hex.trim().toLowerCase().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
80
|
+
if (!hexMatch) return hex;
|
|
81
|
+
let hexValue = hexMatch[1];
|
|
82
|
+
if (hexValue.length === 3) {
|
|
83
|
+
hexValue = hexValue.split('').map(ch => ch + ch).join('');
|
|
84
|
+
}
|
|
85
|
+
const r = parseInt(hexValue.slice(0, 2), 16);
|
|
86
|
+
const g = parseInt(hexValue.slice(2, 4), 16);
|
|
87
|
+
const b = parseInt(hexValue.slice(4, 6), 16);
|
|
88
|
+
return `${r}, ${g}, ${b}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function flattenMapTokens(value, seedContext) {
|
|
92
|
+
if (typeof value !== 'string') return value;
|
|
93
|
+
const str = value.trim();
|
|
94
|
+
|
|
95
|
+
const NAME = '[a-zA-Z_][\\w.-]*';
|
|
96
|
+
const TOKEN_RE = new RegExp('(\\{(' + NAME + ')\\})|\\$(' + NAME + ')', 'g');
|
|
97
|
+
const SINGLE_TOKEN_BRACED = new RegExp('^\\{(' + NAME + ')\\}$');
|
|
98
|
+
const RGBA_RE = /^rgba\s*\((.*)\)\s*$/i;
|
|
99
|
+
|
|
100
|
+
const isHex = (colorString) => typeof colorString === 'string' && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(colorString);
|
|
101
|
+
|
|
102
|
+
const getTokenValue = (name) =>
|
|
103
|
+
Object.prototype.hasOwnProperty.call(seedContext, name) ? seedContext[name] : undefined;
|
|
104
|
+
|
|
105
|
+
// {token} — return the token value as it is
|
|
106
|
+
const tokenMatch = str.match(SINGLE_TOKEN_BRACED);
|
|
107
|
+
if (tokenMatch) {
|
|
108
|
+
const tokenValue = getTokenValue(tokenMatch[1]);
|
|
109
|
+
return tokenValue !== undefined ? tokenValue : str;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// rgba
|
|
113
|
+
const rgbaMatch = str.match(RGBA_RE);
|
|
114
|
+
if (rgbaMatch) {
|
|
115
|
+
const inside = rgbaMatch[1];
|
|
116
|
+
const replaced = inside.replace(TOKEN_RE, (match, _g1, nameBraced) => {
|
|
117
|
+
const name = nameBraced;
|
|
118
|
+
const tokenValue = getTokenValue(name);
|
|
119
|
+
if (tokenValue === undefined) return match;
|
|
120
|
+
if (isHex(tokenValue)) return String(hexToRgb(tokenValue)); // "r, g, b"
|
|
121
|
+
return String(tokenValue);
|
|
122
|
+
});
|
|
123
|
+
return 'rgba(' + replaced + ')';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// An attempt to calculate an arithmetic expression
|
|
127
|
+
let hadUnknown = false;
|
|
128
|
+
let hadNonNumeric = false;
|
|
129
|
+
|
|
130
|
+
const expressionWithResolvedTokens = str.replace(TOKEN_RE, (match, _g1, tokenName) => {
|
|
131
|
+
const tokenValue = getTokenValue(tokenName);
|
|
132
|
+
if (tokenValue === undefined) {
|
|
133
|
+
hadUnknown = true;
|
|
134
|
+
return match;
|
|
135
|
+
}
|
|
136
|
+
if (typeof v === 'number') return String(tokenValue);
|
|
137
|
+
|
|
138
|
+
const numericValue = typeof v === 'string' ? Number(tokenValue) : NaN;
|
|
139
|
+
if (Number.isFinite(numericValue)) return String(numericValue);
|
|
140
|
+
|
|
141
|
+
hadNonNumeric = true;
|
|
142
|
+
return match;
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
if (!hadUnknown && !hadNonNumeric) {
|
|
146
|
+
if (/[^0-9+\-*/().\s]/.test(expressionWithResolvedTokens)) {
|
|
147
|
+
return str;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const result = Function('"use strict"; return (' + expressionWithResolvedTokens + ');')();
|
|
151
|
+
return Number.isFinite(result) ? result : str;
|
|
152
|
+
} catch {
|
|
153
|
+
return str;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Universal substitution of tokens in any string
|
|
158
|
+
const generic = str.replace(TOKEN_RE, (match, g1, tokenName) => {
|
|
159
|
+
const tokenValue = getTokenValue(tokenName);
|
|
160
|
+
if (tokenValue === undefined) return match;
|
|
161
|
+
if (isHex(tokenValue)) return 'rgb(' + hexToRgb(tokenValue) + ')';
|
|
162
|
+
return String(tokenValue);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return generic;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function flattenMapTokensWrapper(mapTokens, seedContext) {
|
|
169
|
+
const flattened = {};
|
|
170
|
+
if (!mapTokens || typeof mapTokens !== 'object') {
|
|
171
|
+
console.warn('An object with map tokens was expected, but received:', mapTokens);
|
|
172
|
+
return flattened;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
Object.keys(mapTokens).forEach(tokenName => {
|
|
176
|
+
const tokenContent = mapTokens[tokenName];
|
|
177
|
+
|
|
178
|
+
if (tokenContent && typeof tokenContent === 'object' && tokenContent.hasOwnProperty('value')) {
|
|
179
|
+
const rawValue = tokenContent.value;
|
|
180
|
+
|
|
181
|
+
const computedValue = flattenMapTokens(rawValue, seedContext);
|
|
182
|
+
flattened[tokenName] = computedValue;
|
|
183
|
+
} else {
|
|
184
|
+
console.warn(`Incorrect structure for the map token ${tokenName}:`, tokenContent);
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
return flattened;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Alias */
|
|
191
|
+
function isDropShadowStructure(obj) {
|
|
192
|
+
if (!obj || typeof obj !== 'object') {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const keys = Object.keys(obj);
|
|
197
|
+
if (keys.length === 0) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const key of keys) {
|
|
202
|
+
if (isNaN(key)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const firstShadow = obj[keys[0]];
|
|
208
|
+
if (!firstShadow || typeof firstShadow !== 'object') {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const expectedProps = ['x', 'y', 'blur', 'spread', 'color', 'type'];
|
|
213
|
+
for (const prop of expectedProps) {
|
|
214
|
+
if (!firstShadow.hasOwnProperty(prop)) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const coordProp of ['x', 'y', 'blur', 'spread']) {
|
|
220
|
+
if (!firstShadow[coordProp] || typeof firstShadow[coordProp] !== 'object' || !firstShadow[coordProp].hasOwnProperty('value')) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!firstShadow.color || typeof firstShadow.color !== 'object' || !firstShadow.color.hasOwnProperty('value')) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (firstShadow.type?.value !== 'dropShadow' && firstShadow.type !== 'dropShadow') {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// If the first shadow has passed the test, we will check the rest.
|
|
234
|
+
for (const key of keys) {
|
|
235
|
+
const shadow = obj[key];
|
|
236
|
+
if (!shadow || typeof shadow !== 'object') {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
for (const coordProp of ['x', 'y', 'blur', 'spread']) {
|
|
240
|
+
if (!shadow[coordProp] || typeof shadow[coordProp] !== 'object' || !shadow[coordProp].hasOwnProperty('value')) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!shadow.color || typeof shadow.color !== 'object' || !shadow.color.hasOwnProperty('value')) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
if (shadow.type?.value !== 'dropShadow' && shadow.type !== 'dropShadow') {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* Checks whether the object is a single dropShadow structure. */
|
|
256
|
+
function isSingleDropShadowStructure(obj) {
|
|
257
|
+
if (!obj || typeof obj !== 'object') {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const expectedProps = ['x', 'y', 'blur', 'spread', 'color', 'type'];
|
|
262
|
+
for (const prop of expectedProps) {
|
|
263
|
+
if (!obj.hasOwnProperty(prop)) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
for (const coordProp of ['x', 'y', 'blur', 'spread']) {
|
|
269
|
+
if (!obj[coordProp] || typeof obj[coordProp] !== 'object' || !obj[coordProp].hasOwnProperty('value')) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!obj.color || typeof obj.color !== 'object' || !obj.color.hasOwnProperty('value')) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (obj.type?.value !== 'dropShadow' && obj.type !== 'dropShadow') {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* Collects the boxShadow string from the dropShadow structure (multiple). */
|
|
286
|
+
function buildBoxShadowString(shadowStructure, contextTokens) {
|
|
287
|
+
const shadowParts = [];
|
|
288
|
+
const keys = Object.keys(shadowStructure).sort((a, b) => parseInt(a) - parseInt(b));
|
|
289
|
+
|
|
290
|
+
for (const key of keys) {
|
|
291
|
+
const shadowDef = shadowStructure[key];
|
|
292
|
+
const shadowString = buildSingleShadowString(shadowDef, contextTokens);
|
|
293
|
+
if (shadowString) {
|
|
294
|
+
shadowParts.push(shadowString);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Combine all the shadows into one line
|
|
299
|
+
return shadowParts.join(', ');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* Collects a row for one shadow from the dropShadow structure. */
|
|
303
|
+
function buildSingleShadowString(singleShadowDef, contextTokens) {
|
|
304
|
+
// Extracting and calculating the values
|
|
305
|
+
const x = flattenMapTokens(singleShadowDef.x.value, contextTokens);
|
|
306
|
+
const y = flattenMapTokens(singleShadowDef.y.value, contextTokens);
|
|
307
|
+
const blur = flattenMapTokens(singleShadowDef.blur.value, contextTokens);
|
|
308
|
+
const spread = flattenMapTokens(singleShadowDef.spread.value, contextTokens);
|
|
309
|
+
const color = flattenMapTokens(singleShadowDef.color.value, contextTokens);
|
|
310
|
+
|
|
311
|
+
// Checking if all values are numeric (or color)
|
|
312
|
+
if (typeof x !== 'number' || typeof y !== 'number' || typeof blur !== 'number' || typeof spread !== 'number' || typeof color !== 'string') {
|
|
313
|
+
console.warn(`Invalid value type in dropShadow: x=${x}(${typeof x}), y=${y}(${typeof y}), blur=${blur}(${typeof blur}), spread=${spread}(${typeof spread}), color=${color}(${typeof color})`);
|
|
314
|
+
return undefined; // Returning undefined to skip this shadow
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Forming a row for one shadow
|
|
318
|
+
return `${x}px ${y}px ${blur}px ${spread}px ${color}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
/* An auxiliary function for converting the alias token structure. */
|
|
323
|
+
function flattenAliasTokens(aliasTokens, contextTokens) {
|
|
324
|
+
const flattened = {};
|
|
325
|
+
if (!aliasTokens || typeof aliasTokens !== 'object') {
|
|
326
|
+
console.warn('An object with alias tokens was expected, but received:', aliasTokens);
|
|
327
|
+
return flattened;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
Object.keys(aliasTokens).forEach((tokenName) => {
|
|
331
|
+
const tokenContent = aliasTokens[tokenName];
|
|
332
|
+
|
|
333
|
+
if (tokenName[0] === tokenName[0].toUpperCase()) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (tokenContent && typeof tokenContent === 'object' && tokenContent.hasOwnProperty('value')) {
|
|
338
|
+
const rawValue = tokenContent.value;
|
|
339
|
+
const processedValue = flattenMapTokens(rawValue, contextTokens);
|
|
340
|
+
|
|
341
|
+
flattened[tokenName] = processedValue;
|
|
342
|
+
} else {
|
|
343
|
+
// It can be a complex token, for example, a boxShadow (multiple or single)
|
|
344
|
+
if (isDropShadowStructure(tokenContent)) {
|
|
345
|
+
|
|
346
|
+
// boxShadow structure processing (multiple)
|
|
347
|
+
const boxShadowValue = buildBoxShadowString(tokenContent, contextTokens);
|
|
348
|
+
flattened[tokenName] = boxShadowValue;
|
|
349
|
+
|
|
350
|
+
} else if (isSingleDropShadowStructure(tokenContent)) {
|
|
351
|
+
// Processing the structure of a single shadow
|
|
352
|
+
const singleShadowValue = buildSingleShadowString(tokenContent, contextTokens);
|
|
353
|
+
|
|
354
|
+
if (singleShadowValue) {
|
|
355
|
+
flattened[tokenName] = singleShadowValue;
|
|
356
|
+
} else {
|
|
357
|
+
console.warn(`Couldn't form a line for a single shadow ${tokenName}:`, tokenContent);
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
console.warn(`Unsupported structure for alias token ${tokenName}:`, tokenContent);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
return flattened;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
function checkAndResolveVarValues(contextTokens) {
|
|
369
|
+
const resolved = {};
|
|
370
|
+
|
|
371
|
+
Object.keys(contextTokens).forEach(tokenName => {
|
|
372
|
+
const TOKEN_RE = /(\{([\w.-]+)\})|\$([\w.-]+)/g;
|
|
373
|
+
const currentValue = contextTokens[tokenName];
|
|
374
|
+
|
|
375
|
+
// Checking whether the value is a string containing a raw token.
|
|
376
|
+
if (typeof currentValue === 'string' && TOKEN_RE.test(currentValue)) {
|
|
377
|
+
const recomputedValue = flattenMapTokens(currentValue, contextTokens);
|
|
378
|
+
|
|
379
|
+
// If the value has changed after repeated calculation, update it.
|
|
380
|
+
if (recomputedValue !== currentValue) {
|
|
381
|
+
resolved[tokenName] = recomputedValue;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
return resolved;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
/* DefaultValues */
|
|
389
|
+
function flattenDefaultValueTokens(defaultTokens) {
|
|
390
|
+
if (!defaultTokens || typeof defaultTokens !== 'object') return {};
|
|
391
|
+
|
|
392
|
+
const resolved = {};
|
|
393
|
+
|
|
394
|
+
// 1) Basic numeric values
|
|
395
|
+
for (const [name, token] of Object.entries(defaultTokens)) {
|
|
396
|
+
if (token && typeof token === 'object' && Object.prototype.hasOwnProperty.call(token, 'value')) {
|
|
397
|
+
const val = token.value;
|
|
398
|
+
if (typeof val === 'number' && Number.isFinite(val)) {
|
|
399
|
+
resolved[name] = val;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const extractRefKey = (value) => {
|
|
405
|
+
if (typeof value !== 'string') return null;
|
|
406
|
+
const tokenMatch = value.match(/^\s*\{([^}]+)\}\s*$/);
|
|
407
|
+
return tokenMatch ? tokenMatch [1].trim() : null;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// 2) Iterative resolution of "{key}" links
|
|
411
|
+
const maxRounds = Object.keys(defaultTokens).length + 5; // защита от циклов
|
|
412
|
+
for (let round = 0; round < maxRounds; round++) {
|
|
413
|
+
let changed = false;
|
|
414
|
+
|
|
415
|
+
for (const [name, token] of Object.entries(defaultTokens)) {
|
|
416
|
+
if (!token || typeof token !== 'object' || !Object.prototype.hasOwnProperty.call(token, 'value')) continue;
|
|
417
|
+
|
|
418
|
+
const val = token.value;
|
|
419
|
+
|
|
420
|
+
if (typeof val === 'number' && Number.isFinite(val)) {
|
|
421
|
+
if (resolved[name] !== val) {
|
|
422
|
+
resolved[name] = val;
|
|
423
|
+
changed = true;
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const refKey = extractRefKey(val);
|
|
429
|
+
if (!refKey) continue;
|
|
430
|
+
|
|
431
|
+
if (Object.prototype.hasOwnProperty.call(resolved, refKey)) {
|
|
432
|
+
const num = resolved[refKey];
|
|
433
|
+
if (typeof num === 'number' && Number.isFinite(num) && resolved[name] !== num) {
|
|
434
|
+
resolved[name] = num;
|
|
435
|
+
changed = true;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!changed) break;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// 3) Returning numeric values only
|
|
444
|
+
const numericTokens = {};
|
|
445
|
+
for (const [tokenName, tokenValue] of Object.entries(resolved)) {
|
|
446
|
+
if (typeof tokenValue === 'number' && Number.isFinite(tokenValue)) numericTokens [tokenName] = tokenValue;
|
|
447
|
+
}
|
|
448
|
+
return numericTokens ;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/* Components */
|
|
452
|
+
function flattenComponentsTokens(componentsTokens, contextTokens) {
|
|
453
|
+
const flattened = {};
|
|
454
|
+
if (!componentsTokens || typeof componentsTokens !== 'object') {
|
|
455
|
+
console.warn('An object with components tokens was expected, but received:', componentsTokens);
|
|
456
|
+
return flattened;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
Object.keys(componentsTokens).forEach(componentName => {
|
|
460
|
+
const componentTokens = componentsTokens[componentName];
|
|
461
|
+
if (!componentTokens || typeof componentTokens !== 'object') {
|
|
462
|
+
console.warn(`Incorrect structure for the component ${componentName}:`, componentTokens);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Processing tokens for one component
|
|
467
|
+
const processedComponentTokens = {};
|
|
468
|
+
Object.keys(componentTokens).forEach(tokenName => {
|
|
469
|
+
const tokenDefinition = componentTokens[tokenName];
|
|
470
|
+
|
|
471
|
+
if (tokenDefinition && typeof tokenDefinition === 'object' && tokenDefinition.hasOwnProperty('value')) {
|
|
472
|
+
const rawValue = tokenDefinition.value;
|
|
473
|
+
|
|
474
|
+
const processedValue = flattenMapTokens(rawValue, contextTokens);
|
|
475
|
+
processedComponentTokens[tokenName] = processedValue;
|
|
476
|
+
} else {
|
|
477
|
+
// It can be a token with a nested structure, for example, lineType.style.value
|
|
478
|
+
if (tokenDefinition && typeof tokenDefinition === 'object' && tokenDefinition.style && typeof tokenDefinition.style === 'object' && tokenDefinition.style.hasOwnProperty('value')) {
|
|
479
|
+
const rawValue = tokenDefinition.style.value;
|
|
480
|
+
|
|
481
|
+
const processedValue = flattenMapTokens(rawValue, contextTokens);
|
|
482
|
+
processedComponentTokens[tokenName] = processedValue;
|
|
483
|
+
|
|
484
|
+
} else {
|
|
485
|
+
if (isDropShadowStructure(tokenDefinition)) {
|
|
486
|
+
// Processing of the boxShadowSecondary structure (multiple)
|
|
487
|
+
const boxShadowValue = buildBoxShadowString(tokenDefinition, contextTokens);
|
|
488
|
+
processedComponentTokens[tokenName] = boxShadowValue;
|
|
489
|
+
|
|
490
|
+
} else {
|
|
491
|
+
console.warn(`Unsupported token structure ${componentName}.${tokenName}:`, tokenDefinition);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
// Adding the processed component tokens to the final object
|
|
498
|
+
flattened[componentName] = processedComponentTokens;
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
return flattened;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function flatten() {
|
|
505
|
+
const configFilePath = path.resolve('./figma-tokens-flattener-config.json');
|
|
506
|
+
let config = {};
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
const configContent = fs.readFileSync(configFilePath, 'utf-8');
|
|
510
|
+
config = JSON.parse(configContent);
|
|
511
|
+
} catch (configError) {
|
|
512
|
+
if (configError.code === 'ENOENT') {
|
|
513
|
+
console.log('The configuration file is simple-token-transformer-config.json was not found. We use the path - the root directory.');
|
|
514
|
+
} else {
|
|
515
|
+
console.error('Error when reading or parsing the configuration file:', configError.message);
|
|
516
|
+
// Continue with an empty configuration, by default
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const inputFilePath = path.resolve(config.source?.tokensFile || './tokens.json');
|
|
521
|
+
const outputDir = path.resolve(config.target?.jsonsDir || './'); // Save it to the current default directory
|
|
522
|
+
|
|
523
|
+
const baseKeys = ['colors', 'seed', 'map', 'alias', 'components']; // The keys we need from the original token
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
const fileContent = fs.readFileSync(inputFilePath, 'utf-8');
|
|
527
|
+
const allTokensData = JSON.parse(fileContent);
|
|
528
|
+
|
|
529
|
+
let lightTokens = {};
|
|
530
|
+
let darkTokens = {};
|
|
531
|
+
|
|
532
|
+
for (const baseKey of baseKeys) {
|
|
533
|
+
const lightFullKey = `light/${baseKey}`;
|
|
534
|
+
const darkFullKey = `dark/${baseKey}`;
|
|
535
|
+
const lightDefaultKey = `Default/Light`;
|
|
536
|
+
const darkDefaultKey = `Default/Dark`;
|
|
537
|
+
|
|
538
|
+
// _____________________________Обработка light токенов
|
|
539
|
+
if (allTokensData.hasOwnProperty(lightFullKey)) {
|
|
540
|
+
// Special processing transformation of each collection into a flat structure
|
|
541
|
+
|
|
542
|
+
if (baseKey === 'colors') {
|
|
543
|
+
const flattenedColors = flattenColorGroups(allTokensData[lightFullKey]);
|
|
544
|
+
lightTokens = { ...lightTokens, ...flattenedColors }; // Объединяем с существующими токенами
|
|
545
|
+
}
|
|
546
|
+
else if (baseKey === 'seed') {
|
|
547
|
+
const flattenedSeeds = flattenSeedTokens(allTokensData[lightFullKey]);
|
|
548
|
+
lightTokens = { ...lightTokens, ...flattenedSeeds };
|
|
549
|
+
}
|
|
550
|
+
else if (baseKey === 'map') {
|
|
551
|
+
const flattenedMaps = flattenMapTokensWrapper(allTokensData[lightFullKey], lightTokens);
|
|
552
|
+
lightTokens = { ...lightTokens, ...flattenedMaps };
|
|
553
|
+
}
|
|
554
|
+
else if (baseKey === 'alias') {
|
|
555
|
+
const flattenedAliases = flattenAliasTokens(allTokensData[lightFullKey], lightTokens);
|
|
556
|
+
lightTokens = { ...lightTokens, ...flattenedAliases };
|
|
557
|
+
const resolved = checkAndResolveVarValues(lightTokens);
|
|
558
|
+
lightTokens = { ...lightTokens, ...resolved };
|
|
559
|
+
}
|
|
560
|
+
else if (baseKey === 'components') {
|
|
561
|
+
// We add the remaining default values. They may have nesting, so we put everything in a flat structure.
|
|
562
|
+
const flattenDefaultValues = flattenDefaultValueTokens(allTokensData[lightDefaultKey]);
|
|
563
|
+
lightTokens = { ...flattenDefaultValues, ...lightTokens };
|
|
564
|
+
|
|
565
|
+
const flattenedComponents = flattenComponentsTokens(allTokensData[lightFullKey], lightTokens);
|
|
566
|
+
lightTokens = { ...lightTokens, ...flattenedComponents };
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
console.warn(`Collection not found, collection key: ${lightFullKey}`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// _____________________________Обработка dark токенов
|
|
573
|
+
if (allTokensData.hasOwnProperty(darkFullKey)) {
|
|
574
|
+
|
|
575
|
+
if (baseKey === 'colors') {
|
|
576
|
+
const flattenedColors = flattenColorGroups(allTokensData[darkFullKey]);
|
|
577
|
+
darkTokens = { ...darkTokens, ...flattenedColors };
|
|
578
|
+
} else if (baseKey === 'seed') {
|
|
579
|
+
const flattenedSeeds = flattenSeedTokens(allTokensData[darkFullKey]);
|
|
580
|
+
darkTokens = { ...darkTokens, ...flattenedSeeds };
|
|
581
|
+
}
|
|
582
|
+
else if (baseKey === 'map') {
|
|
583
|
+
const flattenedMaps = flattenMapTokensWrapper(allTokensData[darkFullKey], darkTokens);
|
|
584
|
+
darkTokens = { ...darkTokens, ...flattenedMaps };
|
|
585
|
+
}
|
|
586
|
+
else if (baseKey === 'alias') {
|
|
587
|
+
const flattenedAliases = flattenAliasTokens(allTokensData[darkFullKey], darkTokens);
|
|
588
|
+
darkTokens = { ...darkTokens, ...flattenedAliases };
|
|
589
|
+
const resolved = checkAndResolveVarValues(darkTokens);
|
|
590
|
+
darkTokens = { ...darkTokens, ...resolved };
|
|
591
|
+
}
|
|
592
|
+
else if (baseKey === 'components') {
|
|
593
|
+
// We add the remaining default values. They may have nesting, so we put everything in a flat structure.
|
|
594
|
+
const flattenDefaultValues = flattenDefaultValueTokens(allTokensData[darkDefaultKey]);
|
|
595
|
+
darkTokens = { ...flattenDefaultValues, ...darkTokens };
|
|
596
|
+
|
|
597
|
+
const flattenedComponents = flattenComponentsTokens(allTokensData[darkFullKey], darkTokens);
|
|
598
|
+
darkTokens = { ...darkTokens, ...flattenedComponents };
|
|
599
|
+
}
|
|
600
|
+
} else {
|
|
601
|
+
console.warn(`Collection not found, collection key: ${darkFullKey}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const lightOutputPath = path.join(outputDir, 'light.json');
|
|
606
|
+
const darkOutputPath = path.join(outputDir, 'dark.json');
|
|
607
|
+
|
|
608
|
+
console.log(`Saving light and dark tokens in: ${outputDir}`);
|
|
609
|
+
fs.writeFileSync(lightOutputPath, JSON.stringify(lightTokens, null, 2));
|
|
610
|
+
|
|
611
|
+
fs.writeFileSync(darkOutputPath, JSON.stringify(darkTokens, null, 2));
|
|
612
|
+
|
|
613
|
+
console.log('\nReady! Light files.json and dark.json files have been created.');
|
|
614
|
+
|
|
615
|
+
} catch (error) {
|
|
616
|
+
if (error.code === 'ENOENT') {
|
|
617
|
+
console.error(`Error: The tokens file.json was not found on the path ${inputFilePath}`);
|
|
618
|
+
} else if (error instanceof SyntaxError) {
|
|
619
|
+
console.error('Error: The contents of the tokens file.json is not valid JSON.');
|
|
620
|
+
console.error(error.message);
|
|
621
|
+
} else {
|
|
622
|
+
console.error('Error when reading or parsing the tokens.json file:', error.message);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
module.exports = {
|
|
628
|
+
flatten
|
|
629
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "figma-tokens-flattener",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A tool for transforming Ant Design tokens from Tokens Studio for Figma (Single file) into flat style mappings for light and dark themes.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [],
|
|
10
|
+
"author": "Yurii Sudarskii",
|
|
11
|
+
"license": "ISC"
|
|
12
|
+
}
|