@udt/parser-utils 0.2.0 → 0.3.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 +1 -1
- package/dist/parseData.d.ts +12 -1
- package/dist/parseData.js +20 -0
- package/package.json +16 -4
- package/CHANGELOG.md +0 -21
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/clover.xml +0 -129
- package/coverage/coverage-final.json +0 -5
- package/coverage/extractProperties.ts.html +0 -277
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -161
- package/coverage/index.ts.html +0 -100
- package/coverage/isJsonObject.ts.html +0 -142
- package/coverage/parseData.ts.html +0 -925
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -196
- package/src/extractProperties.test.ts +0 -33
- package/src/extractProperties.ts +0 -64
- package/src/index.ts +0 -5
- package/src/isJsonObject.test.ts +0 -32
- package/src/isJsonObject.ts +0 -19
- package/src/parseData.test.ts +0 -435
- package/src/parseData.ts +0 -280
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -6
package/src/parseData.test.ts
DELETED
|
@@ -1,435 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
parseData,
|
|
4
|
-
type ParseDesignTokenDataFn,
|
|
5
|
-
type ParseGroupDataFn,
|
|
6
|
-
type IsDesignTokenDataFn,
|
|
7
|
-
type ParserConfig,
|
|
8
|
-
InvalidDataError,
|
|
9
|
-
AddChildFn,
|
|
10
|
-
} from "./parseData.js";
|
|
11
|
-
|
|
12
|
-
interface TestGroup {
|
|
13
|
-
type: "group";
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface TestDesignToken {
|
|
17
|
-
type: "token";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface TestParentContext {
|
|
21
|
-
dummyData?: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface ParseDataCall {
|
|
25
|
-
type: "group" | "token";
|
|
26
|
-
path: string[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface AddChildCall {
|
|
30
|
-
type: "addChild";
|
|
31
|
-
name: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Used to track the order in which parseXyzData and addChild
|
|
35
|
-
// functions are called
|
|
36
|
-
let parseDataCalls: (ParseDataCall | AddChildCall)[] = [];
|
|
37
|
-
|
|
38
|
-
/*
|
|
39
|
-
* For the purpose of these tests, any object with a
|
|
40
|
-
* value property will be considered a design token.
|
|
41
|
-
*
|
|
42
|
-
* Intentionally not using (current) DTCG syntax, to
|
|
43
|
-
* demonstrate that parseData() could be used to parse
|
|
44
|
-
* other formats too, as long as they use the same
|
|
45
|
-
* nested object structure for groups and tokens.
|
|
46
|
-
*/
|
|
47
|
-
const mockIsDesignTokenData = vi.fn<IsDesignTokenDataFn>(
|
|
48
|
-
(data) => data.value !== undefined
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const mockParseGroupData = vi.fn<
|
|
52
|
-
ParseGroupDataFn<TestGroup, TestParentContext>
|
|
53
|
-
>((_data, path, contextFromParent) => {
|
|
54
|
-
parseDataCalls.push({
|
|
55
|
-
type: "group",
|
|
56
|
-
path,
|
|
57
|
-
});
|
|
58
|
-
return {
|
|
59
|
-
group: {
|
|
60
|
-
type: "group",
|
|
61
|
-
},
|
|
62
|
-
// pass through contextFromParent
|
|
63
|
-
contextForChildren: contextFromParent,
|
|
64
|
-
};
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const mockAddChildToGroup = vi.fn<AddChildFn<TestGroup, TestDesignToken>>(
|
|
68
|
-
(_parent, name, _child) => {
|
|
69
|
-
parseDataCalls.push({
|
|
70
|
-
type: "addChild",
|
|
71
|
-
name,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const mockParseDesignTokenData = vi.fn<
|
|
77
|
-
ParseDesignTokenDataFn<TestDesignToken, TestParentContext>
|
|
78
|
-
>((_data, path, _contextFromParent) => {
|
|
79
|
-
parseDataCalls.push({ type: "token", path });
|
|
80
|
-
const result: TestDesignToken = {
|
|
81
|
-
type: "token",
|
|
82
|
-
};
|
|
83
|
-
return result;
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const defaultParserConfig: ParserConfig<
|
|
87
|
-
TestDesignToken,
|
|
88
|
-
TestGroup,
|
|
89
|
-
TestParentContext
|
|
90
|
-
> = {
|
|
91
|
-
isDesignTokenData: mockIsDesignTokenData,
|
|
92
|
-
groupPropsToExtract: [],
|
|
93
|
-
parseGroupData: mockParseGroupData,
|
|
94
|
-
parseDesignTokenData: mockParseDesignTokenData,
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
describe("parseData()", () => {
|
|
98
|
-
let parserConfig: ParserConfig<TestDesignToken, TestGroup, TestParentContext>;
|
|
99
|
-
|
|
100
|
-
beforeEach(() => {
|
|
101
|
-
// Reset stuff
|
|
102
|
-
parserConfig = defaultParserConfig;
|
|
103
|
-
parseDataCalls = [];
|
|
104
|
-
parserConfig.groupPropsToExtract = [];
|
|
105
|
-
mockIsDesignTokenData.mockClear();
|
|
106
|
-
mockParseGroupData.mockClear();
|
|
107
|
-
mockParseDesignTokenData.mockClear();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe("parsing an empty group object", () => {
|
|
111
|
-
let parsedGroupOrToken: TestGroup | TestDesignToken | undefined;
|
|
112
|
-
|
|
113
|
-
beforeEach(() => {
|
|
114
|
-
parsedGroupOrToken = parseData({}, parserConfig);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("returns a group", () => {
|
|
118
|
-
expect(parsedGroupOrToken?.type).toBe("group");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("calls isDesignTokenData function once", () => {
|
|
122
|
-
expect(mockIsDesignTokenData).toHaveBeenCalledOnce();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("class isDesignTokenData function with input data", () => {
|
|
126
|
-
expect(mockIsDesignTokenData).toHaveBeenCalledWith({});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("calls parseGroupData function once", () => {
|
|
130
|
-
expect(mockParseGroupData).toHaveBeenCalledOnce();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("calls parseGroupData function with empty group and empty path array", () => {
|
|
134
|
-
expect(mockParseGroupData).toHaveBeenCalledWith({}, [], undefined);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("does not call parseDesignTokenData function", () => {
|
|
138
|
-
expect(mockParseDesignTokenData).not.toHaveBeenCalled();
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe("parsing a design token object", () => {
|
|
143
|
-
const testTokenData = {
|
|
144
|
-
value: "whatever", // <-- this makes it a token
|
|
145
|
-
other: "thing",
|
|
146
|
-
stuff: 123,
|
|
147
|
-
notAGroup: {},
|
|
148
|
-
};
|
|
149
|
-
let parsedGroupOrToken: TestGroup | TestDesignToken | undefined;
|
|
150
|
-
|
|
151
|
-
beforeEach(() => {
|
|
152
|
-
parsedGroupOrToken = parseData(testTokenData, parserConfig);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("returns a design token", () => {
|
|
156
|
-
expect(parsedGroupOrToken?.type).toBe("token");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("does not call parseGroupData function", () => {
|
|
160
|
-
expect(mockParseGroupData).not.toHaveBeenCalled();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("calls parseDesignTokenData function once", () => {
|
|
164
|
-
expect(mockParseDesignTokenData).toHaveBeenCalledOnce();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("calls parseDesignTokenData function with complete data and empty path array", () => {
|
|
168
|
-
expect(mockParseDesignTokenData).toHaveBeenCalledWith(
|
|
169
|
-
testTokenData,
|
|
170
|
-
[],
|
|
171
|
-
undefined
|
|
172
|
-
);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe("parsing a group object containing design tokens and nested groups", () => {
|
|
177
|
-
/*
|
|
178
|
-
Contains:
|
|
179
|
-
|
|
180
|
-
- 4 groups: <root>, emptyGroup,
|
|
181
|
-
groupWithTokens, nestedGroup
|
|
182
|
-
|
|
183
|
-
- 5 tokens: nestedToken, anotherNestedToken,
|
|
184
|
-
deeplyNestedToken, token, anotherToken
|
|
185
|
-
*/
|
|
186
|
-
const testData = {
|
|
187
|
-
specialGroupProp: {
|
|
188
|
-
value: "not a token value",
|
|
189
|
-
description: "This is not a group or token!",
|
|
190
|
-
},
|
|
191
|
-
emptyGroup: {},
|
|
192
|
-
groupWithTokens: {
|
|
193
|
-
nestedToken: {
|
|
194
|
-
value: "a",
|
|
195
|
-
},
|
|
196
|
-
anotherNestedToken: {
|
|
197
|
-
value: "b",
|
|
198
|
-
},
|
|
199
|
-
nestedGroup: {
|
|
200
|
-
deeplyNestedToken: {
|
|
201
|
-
value: "x",
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
token: {
|
|
206
|
-
value: 1,
|
|
207
|
-
},
|
|
208
|
-
anotherToken: {
|
|
209
|
-
value: 2,
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
beforeEach(() => {
|
|
214
|
-
parserConfig.groupPropsToExtract = ["specialGroupProp"];
|
|
215
|
-
parseData(testData, parserConfig);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("calls parseDesignTokenData function 5 times", () => {
|
|
219
|
-
expect(mockParseDesignTokenData).toHaveBeenCalledTimes(5);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("calls parseGroupData function 4 times", () => {
|
|
223
|
-
expect(mockParseGroupData).toHaveBeenCalledTimes(4);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("only passed extracted group props to the parseGroupData function", () => {
|
|
227
|
-
expect(mockParseGroupData).toHaveBeenNthCalledWith(
|
|
228
|
-
1,
|
|
229
|
-
{ specialGroupProp: testData.specialGroupProp },
|
|
230
|
-
[],
|
|
231
|
-
undefined
|
|
232
|
-
);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("traverses the data depth-first", () => {
|
|
236
|
-
// Start with root group
|
|
237
|
-
expect(parseDataCalls[0]).toStrictEqual({
|
|
238
|
-
type: "group",
|
|
239
|
-
path: [],
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// "emptyGroup" next
|
|
243
|
-
expect(parseDataCalls[1]).toStrictEqual({
|
|
244
|
-
type: "group",
|
|
245
|
-
path: ["emptyGroup"],
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// "groupWithTokens" next
|
|
249
|
-
expect(parseDataCalls[2]).toStrictEqual({
|
|
250
|
-
type: "group",
|
|
251
|
-
path: ["groupWithTokens"],
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
// "nestedToken" next
|
|
255
|
-
expect(parseDataCalls[3]).toStrictEqual({
|
|
256
|
-
type: "token",
|
|
257
|
-
path: ["groupWithTokens", "nestedToken"],
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// "anotherNestedToken" next
|
|
261
|
-
expect(parseDataCalls[4]).toStrictEqual({
|
|
262
|
-
type: "token",
|
|
263
|
-
path: ["groupWithTokens", "anotherNestedToken"],
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// "nestedGroup" next
|
|
267
|
-
expect(parseDataCalls[5]).toStrictEqual({
|
|
268
|
-
type: "group",
|
|
269
|
-
path: ["groupWithTokens", "nestedGroup"],
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
// "deeplyNestedToken" next
|
|
273
|
-
expect(parseDataCalls[6]).toStrictEqual({
|
|
274
|
-
type: "token",
|
|
275
|
-
path: ["groupWithTokens", "nestedGroup", "deeplyNestedToken"],
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
// "token" next
|
|
279
|
-
expect(parseDataCalls[7]).toStrictEqual({
|
|
280
|
-
type: "token",
|
|
281
|
-
path: ["token"],
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// "anotherToken" next
|
|
285
|
-
expect(parseDataCalls[8]).toStrictEqual({
|
|
286
|
-
type: "token",
|
|
287
|
-
path: ["anotherToken"],
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
describe("with context data", () => {
|
|
293
|
-
const testData = { group: {}, token: { value: 123 } };
|
|
294
|
-
const testContext: TestParentContext = { dummyData: 42 };
|
|
295
|
-
|
|
296
|
-
beforeEach(() => {
|
|
297
|
-
parseData(testData, parserConfig, testContext);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it("passes the context data to outermost parseGroupData call", () => {
|
|
301
|
-
// 1st call is the root group
|
|
302
|
-
expect(mockParseGroupData).toHaveBeenNthCalledWith(
|
|
303
|
-
1,
|
|
304
|
-
{},
|
|
305
|
-
[],
|
|
306
|
-
testContext
|
|
307
|
-
);
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it("passes context from parent group to parseGroupData calls for child groups", () => {
|
|
311
|
-
expect(mockParseGroupData).toHaveBeenNthCalledWith(
|
|
312
|
-
2,
|
|
313
|
-
{},
|
|
314
|
-
["group"],
|
|
315
|
-
testContext
|
|
316
|
-
);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("passes context from parent group to parseDesignTokenData calls for child tokens", () => {
|
|
320
|
-
expect(mockParseDesignTokenData).toHaveBeenCalledWith(
|
|
321
|
-
testData.token,
|
|
322
|
-
["token"],
|
|
323
|
-
testContext
|
|
324
|
-
);
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
describe("using addChildToGroup function", () => {
|
|
329
|
-
const testData = {
|
|
330
|
-
tokenA: { value: 1 },
|
|
331
|
-
tokenB: { value: 2 },
|
|
332
|
-
groupC: { tokenX: { value: 99 }, tokenY: { value: 100 } },
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
beforeEach(() => {
|
|
336
|
-
mockAddChildToGroup.mockClear();
|
|
337
|
-
parserConfig.addChildToGroup = mockAddChildToGroup;
|
|
338
|
-
parseData(testData, parserConfig);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it("calls addChildToGroup for every child of every group", () => {
|
|
342
|
-
// Root group contains 3 children: "tokenA", "tokenB" and "groupC"
|
|
343
|
-
// "groupC" contains 2 children: "tokenX" and "tokenY"
|
|
344
|
-
// 3 + 2 = 5
|
|
345
|
-
expect(mockAddChildToGroup).toHaveBeenCalledTimes(5);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it("adds a nested group to its parent before parsing its children", () => {
|
|
349
|
-
// Steps should be:
|
|
350
|
-
// 0 parse root group
|
|
351
|
-
expect(parseDataCalls[0]).toStrictEqual({ type: "group", path: [] });
|
|
352
|
-
|
|
353
|
-
// 1 parse token "tokenA"
|
|
354
|
-
expect(parseDataCalls[1]).toStrictEqual({
|
|
355
|
-
type: "token",
|
|
356
|
-
path: ["tokenA"],
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
// 2 addChild "tokenA" to root group
|
|
360
|
-
expect(parseDataCalls[2]).toStrictEqual({
|
|
361
|
-
type: "addChild",
|
|
362
|
-
name: "tokenA",
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
// 3 parse token "tokenB"
|
|
366
|
-
expect(parseDataCalls[3]).toStrictEqual({
|
|
367
|
-
type: "token",
|
|
368
|
-
path: ["tokenB"],
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// 4 addChild "tokenB" to root group
|
|
372
|
-
expect(parseDataCalls[4]).toStrictEqual({
|
|
373
|
-
type: "addChild",
|
|
374
|
-
name: "tokenB",
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// 5 parse group "groupC"
|
|
378
|
-
expect(parseDataCalls[5]).toStrictEqual({
|
|
379
|
-
type: "group",
|
|
380
|
-
path: ["groupC"],
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
// 6 addChild "groupC" to root group
|
|
384
|
-
// NOTE that this happens *before* any of groupC's
|
|
385
|
-
// children are parsed.
|
|
386
|
-
expect(parseDataCalls[6]).toStrictEqual({
|
|
387
|
-
type: "addChild",
|
|
388
|
-
name: "groupC",
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
// 7 parse token "tokenX"
|
|
392
|
-
expect(parseDataCalls[7]).toStrictEqual({
|
|
393
|
-
type: "token",
|
|
394
|
-
path: ["groupC", "tokenX"],
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// 8 addChild "tokenX" to "groupC"
|
|
398
|
-
expect(parseDataCalls[8]).toStrictEqual({
|
|
399
|
-
type: "addChild",
|
|
400
|
-
name: "tokenX",
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
// 9 parse token "tokenY"
|
|
404
|
-
expect(parseDataCalls[9]).toStrictEqual({
|
|
405
|
-
type: "token",
|
|
406
|
-
path: ["groupC", "tokenY"],
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
// 10 addChild "tokenY" to "groupC"
|
|
410
|
-
expect(parseDataCalls[10]).toStrictEqual({
|
|
411
|
-
type: "addChild",
|
|
412
|
-
name: "tokenY",
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it("throws an InvalidDataError when given a non-object input", () => {
|
|
418
|
-
expect(() => parseData(123, parserConfig)).toThrowError(InvalidDataError);
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
it("throws an InvalidDataError when encountering a non-object child", () => {
|
|
422
|
-
expect(() => parseData({ brokenThing: 123 }, parserConfig)).toThrowError(
|
|
423
|
-
InvalidDataError
|
|
424
|
-
);
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
it("includes the path and data when throwing an InvalidDataError", () => {
|
|
428
|
-
try {
|
|
429
|
-
parseData({ brokenThing: 123 }, parserConfig);
|
|
430
|
-
} catch (error) {
|
|
431
|
-
expect((error as InvalidDataError).path).toStrictEqual(["brokenThing"]);
|
|
432
|
-
expect((error as InvalidDataError).data).toBe(123);
|
|
433
|
-
}
|
|
434
|
-
});
|
|
435
|
-
});
|
package/src/parseData.ts
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { isPlainObject, type PlainObject } from "./isJsonObject.js";
|
|
2
|
-
import { extractProperties } from "./extractProperties.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* A function that checks whether an object is a design token
|
|
6
|
-
* or not (if not, it is assumed to be a group).
|
|
7
|
-
*
|
|
8
|
-
* E.g. for DTCG data, this could check for the presence of a
|
|
9
|
-
* `$value` property.
|
|
10
|
-
*
|
|
11
|
-
* @param data A plain object (guaranteed not to `null` or an
|
|
12
|
-
* array)
|
|
13
|
-
*
|
|
14
|
-
* @returns `true` if `data` is design token data, `false` if
|
|
15
|
-
* it is group data.
|
|
16
|
-
*/
|
|
17
|
-
export type IsDesignTokenDataFn = (data: PlainObject) => boolean;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* A function that parses design token data.
|
|
21
|
-
*
|
|
22
|
-
* @param data A plain object containing design token data
|
|
23
|
-
* (guaranteed not to be `null` or an array)
|
|
24
|
-
* @param path The path to the design token data.
|
|
25
|
-
* @param contextFromParent Context data (if any) that was
|
|
26
|
-
* returned by the `parseGroupData()` call that
|
|
27
|
-
* parsed the group containing this design token.
|
|
28
|
-
*
|
|
29
|
-
* @returns The parsed representation of the design token.
|
|
30
|
-
* May be `undefined` if there is no useful result
|
|
31
|
-
* to return from `parseData()` - e.g. if just
|
|
32
|
-
* logging design token info or something like that.
|
|
33
|
-
*/
|
|
34
|
-
export type ParseDesignTokenDataFn<ParsedDesignToken, T> = (
|
|
35
|
-
data: PlainObject,
|
|
36
|
-
path: string[],
|
|
37
|
-
contextFromParent?: T
|
|
38
|
-
) => ParsedDesignToken;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* A function that adds a parsed group or design token
|
|
42
|
-
* as a child of the given parsed group.
|
|
43
|
-
*
|
|
44
|
-
* @param parent The parent group to add a child to
|
|
45
|
-
* @param name The name of the child group or design token
|
|
46
|
-
* @param child The group or design token to add
|
|
47
|
-
*/
|
|
48
|
-
export type AddChildFn<ParsedGroup, ParsedDesignToken> = (
|
|
49
|
-
parent: ParsedGroup,
|
|
50
|
-
name: string,
|
|
51
|
-
child: ParsedGroup | ParsedDesignToken
|
|
52
|
-
) => void;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* The return value of a `ParseGroupDataFn`.
|
|
56
|
-
*/
|
|
57
|
-
export interface ParseGroupResult<ParsedGroup, T> {
|
|
58
|
-
/**
|
|
59
|
-
* The parsed representation of the group.
|
|
60
|
-
*
|
|
61
|
-
* May be omitted if there is no useful result to
|
|
62
|
-
* return and we only need to pass along context
|
|
63
|
-
* data.
|
|
64
|
-
*/
|
|
65
|
-
group?: ParsedGroup;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Optional context data to be passed into the
|
|
69
|
-
* `parseGroupData()` and `parseDesignTokenData()` calls
|
|
70
|
-
* for any nested group or design token data.
|
|
71
|
-
*
|
|
72
|
-
* Useful if those functions need access to some data from
|
|
73
|
-
* higher up in the original data structure. For example, if
|
|
74
|
-
* parsing DTCG data, this could be used to pass inherited
|
|
75
|
-
* properties like `$type` down.
|
|
76
|
-
*/
|
|
77
|
-
contextForChildren?: T;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* A function that parses group data.
|
|
82
|
-
*
|
|
83
|
-
* @param data A plain object containing group data
|
|
84
|
-
* (guaranteed not to be `null` or an array)
|
|
85
|
-
* @param path The path to the group data.
|
|
86
|
-
* @param contextFromParent Context data (if any) that was
|
|
87
|
-
* returned by the `parseGroupData()` call that
|
|
88
|
-
* parsed the group containing this group.
|
|
89
|
-
*
|
|
90
|
-
* @returns The parsed representation of the group and,
|
|
91
|
-
* optionally, some context data to pass down
|
|
92
|
-
* when child data is parsed.
|
|
93
|
-
*/
|
|
94
|
-
export type ParseGroupDataFn<ParsedGroup, T> = (
|
|
95
|
-
data: PlainObject,
|
|
96
|
-
path: string[],
|
|
97
|
-
contextFromParent?: T
|
|
98
|
-
) => ParseGroupResult<ParsedGroup, T>;
|
|
99
|
-
|
|
100
|
-
export interface ParserConfig<ParsedDesignToken, ParsedGroup, T> {
|
|
101
|
-
/**
|
|
102
|
-
* A function that determines whether an object in the input
|
|
103
|
-
* data is a design token or a group.
|
|
104
|
-
*/
|
|
105
|
-
isDesignTokenData: IsDesignTokenDataFn;
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Array of strings and/or RegExp which match
|
|
109
|
-
* properties of group objects that are NOT
|
|
110
|
-
* names of child design tokens or groups.
|
|
111
|
-
*
|
|
112
|
-
* E.g. for DTCG data, this could be a RegEx
|
|
113
|
-
* like /^$/ which would match all $-prefixed
|
|
114
|
-
* format properties
|
|
115
|
-
*/
|
|
116
|
-
groupPropsToExtract: (string | RegExp)[];
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Function which is called for each group data object
|
|
120
|
-
* that is encountered.
|
|
121
|
-
*
|
|
122
|
-
* Is given the extracted properties of that group and its
|
|
123
|
-
* path, and should parse that data into whatever structure
|
|
124
|
-
* is desired.
|
|
125
|
-
*/
|
|
126
|
-
parseGroupData?: ParseGroupDataFn<ParsedGroup, T>;
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Function which is called for each design token
|
|
130
|
-
*data object that is encountered.
|
|
131
|
-
*
|
|
132
|
-
* Is given the design token data and its path, and
|
|
133
|
-
* should parse that data into whatever structure is
|
|
134
|
-
* desired.
|
|
135
|
-
*/
|
|
136
|
-
parseDesignTokenData: ParseDesignTokenDataFn<ParsedDesignToken, T>;
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Optional function that will add parsed groups
|
|
140
|
-
* or design tokens as children of another parsed group.
|
|
141
|
-
*
|
|
142
|
-
* Intended for cases where the parsed representation
|
|
143
|
-
* of a group needs to contain its children. If not
|
|
144
|
-
* needed, this property can be omitted.
|
|
145
|
-
*/
|
|
146
|
-
addChildToGroup?: AddChildFn<ParsedGroup, ParsedDesignToken>;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Thrown when `parseData()` encounters group or design token
|
|
151
|
-
* data that is not a plain object.
|
|
152
|
-
*/
|
|
153
|
-
export class InvalidDataError extends Error {
|
|
154
|
-
/**
|
|
155
|
-
* Path to the value that is not a plain object
|
|
156
|
-
*/
|
|
157
|
-
public path: string[];
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* The offending value.
|
|
161
|
-
*/
|
|
162
|
-
public data: unknown;
|
|
163
|
-
|
|
164
|
-
constructor(path: string[], data: unknown) {
|
|
165
|
-
super(
|
|
166
|
-
`Expected object at path "${path.join(
|
|
167
|
-
"."
|
|
168
|
-
)}", but got ${typeof data} instead`
|
|
169
|
-
);
|
|
170
|
-
this.name = "InvalidDataError";
|
|
171
|
-
this.path = path;
|
|
172
|
-
this.data = data;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* The internal data parsing implementation.
|
|
178
|
-
*
|
|
179
|
-
* Recursively calls itself for nested group and
|
|
180
|
-
* design token data.
|
|
181
|
-
*
|
|
182
|
-
* @param data The input data to traverse and parse
|
|
183
|
-
* @param config Parser config
|
|
184
|
-
* @param contextFromParent Context data passed in from
|
|
185
|
-
* parent calls to this function.
|
|
186
|
-
* @param path The path to the input data
|
|
187
|
-
* @param addToParent A function to add the parsed data
|
|
188
|
-
* to the parent group.
|
|
189
|
-
* @returns The parsed design token or group.
|
|
190
|
-
*/
|
|
191
|
-
function parseDataImpl<ParsedDesignToken, ParsedGroup, T>(
|
|
192
|
-
data: unknown,
|
|
193
|
-
config: ParserConfig<ParsedDesignToken, ParsedGroup, T>,
|
|
194
|
-
contextFromParent?: T,
|
|
195
|
-
path: string[] = [],
|
|
196
|
-
parentGroup?: ParsedGroup
|
|
197
|
-
): ParsedDesignToken | ParsedGroup | undefined {
|
|
198
|
-
if (!isPlainObject(data)) {
|
|
199
|
-
throw new InvalidDataError(path, data);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const {
|
|
203
|
-
isDesignTokenData,
|
|
204
|
-
groupPropsToExtract,
|
|
205
|
-
parseGroupData,
|
|
206
|
-
parseDesignTokenData,
|
|
207
|
-
addChildToGroup,
|
|
208
|
-
} = config;
|
|
209
|
-
|
|
210
|
-
let groupOrToken: ParsedGroup | ParsedDesignToken | undefined = undefined;
|
|
211
|
-
if (isDesignTokenData(data)) {
|
|
212
|
-
// looks like a token
|
|
213
|
-
groupOrToken = parseDesignTokenData(data, path, contextFromParent);
|
|
214
|
-
if (addChildToGroup && path.length > 0 && parentGroup !== undefined) {
|
|
215
|
-
addChildToGroup(parentGroup, path[path.length - 1], groupOrToken);
|
|
216
|
-
}
|
|
217
|
-
} else {
|
|
218
|
-
// must be a group
|
|
219
|
-
const { extracted: groupData, rest: children } = extractProperties(
|
|
220
|
-
data,
|
|
221
|
-
groupPropsToExtract
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
let contextForChildren: T | undefined;
|
|
225
|
-
if (parseGroupData) {
|
|
226
|
-
const parseResult = parseGroupData(groupData, path, contextFromParent);
|
|
227
|
-
contextForChildren = parseResult.contextForChildren;
|
|
228
|
-
groupOrToken = parseResult.group;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (
|
|
232
|
-
addChildToGroup &&
|
|
233
|
-
path.length > 0 &&
|
|
234
|
-
parentGroup !== undefined &&
|
|
235
|
-
groupOrToken !== undefined
|
|
236
|
-
) {
|
|
237
|
-
addChildToGroup(parentGroup, path[path.length - 1], groupOrToken);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
for (const childName in children) {
|
|
241
|
-
parseDataImpl(
|
|
242
|
-
children[childName],
|
|
243
|
-
config,
|
|
244
|
-
contextForChildren,
|
|
245
|
-
[...path, childName],
|
|
246
|
-
groupOrToken
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return groupOrToken;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Parses a nested object structure representing groups
|
|
256
|
-
* and design tokens, such as the data obtained by reading
|
|
257
|
-
* and JSON-parsing a DTCG file.
|
|
258
|
-
*
|
|
259
|
-
* It will recursively traverse the input data (depth first)
|
|
260
|
-
* and, using the functions provided in the config:
|
|
261
|
-
*
|
|
262
|
-
* 1. Check if the object is design token or group data
|
|
263
|
-
* 2. Pass that data to the parsed or processed by the
|
|
264
|
-
* relevant function
|
|
265
|
-
* 3. Return the outermost parsed group or design token
|
|
266
|
-
*
|
|
267
|
-
* @param data The input data to traverse and parse
|
|
268
|
-
* @param config Configuration for this parser
|
|
269
|
-
* @param contextFromParent Optional context data to
|
|
270
|
-
* pass into the top-most design token
|
|
271
|
-
* or group parser function call.
|
|
272
|
-
* @returns The outermost parsed group or design token
|
|
273
|
-
*/
|
|
274
|
-
export function parseData<ParsedDesignToken, ParsedGroup, T>(
|
|
275
|
-
data: unknown,
|
|
276
|
-
config: ParserConfig<ParsedDesignToken, ParsedGroup, T>,
|
|
277
|
-
contextFromParent?: T
|
|
278
|
-
): ParsedDesignToken | ParsedGroup | undefined {
|
|
279
|
-
return parseDataImpl(data, config, contextFromParent);
|
|
280
|
-
}
|