@udt/parser-utils 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.
@@ -0,0 +1,271 @@
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 a parsed group.
43
+ *
44
+ * @param name The name of the child group or design token
45
+ * @param child The group or desing token to add
46
+ */
47
+ export type AddChildFn<ParsedGroup, ParsedDesignToken> = (
48
+ name: string,
49
+ child: ParsedGroup | ParsedDesignToken
50
+ ) => void;
51
+
52
+ /**
53
+ * The return value of a `ParseGroupDataFn`.
54
+ */
55
+ export interface ParseGroupResult<ParsedGroup, ParsedDesignToken, T> {
56
+ /**
57
+ * The parsed representation of the group.
58
+ *
59
+ * May be `undefined` if there is no useful result
60
+ * to return from `parseData()` - e.g. if just
61
+ * logging group info or something like that.
62
+ */
63
+ group: ParsedGroup;
64
+
65
+ /**
66
+ * Optional function that will add other parsed groups
67
+ * or design tokens as children of this parsed group.
68
+ *
69
+ * Intended for cases where the parsed representation
70
+ * of a group needs to contain its children. If not
71
+ * needed, this property can be omitted.
72
+ */
73
+ addChild?: AddChildFn<ParsedGroup, ParsedDesignToken>;
74
+
75
+ /**
76
+ * Optional context data to be passed into the
77
+ * `parseGroupData()` and `parseDesignTokenData()` calls
78
+ * for any nested group or design token data.
79
+ *
80
+ * Useful if those functions need access to some data from
81
+ * higher up in the original data structure. For example, if
82
+ * parsing DTCG data, this could be used to pass inherited
83
+ * properties like `$type` down.
84
+ */
85
+ contextForChildren?: T;
86
+ }
87
+
88
+ /**
89
+ * A function that parses group data.
90
+ *
91
+ * @param data A plain object containing group data
92
+ * (guaranteed not to be `null` or an array)
93
+ * @param path The path to the group data.
94
+ * @param contextFromParent Context data (if any) that was
95
+ * returned by the `parseGroupData()` call that
96
+ * parsed the group containing this group.
97
+ *
98
+ * @returns The parsed representation of the group and,
99
+ * optionally, a function to add child groups or
100
+ * design tokens to it and some context data to
101
+ * pass down when child data is parsed.
102
+ */
103
+ export type ParseGroupDataFn<ParsedGroup, ParsedDesignToken, T> = (
104
+ data: PlainObject,
105
+ path: string[],
106
+ contextFromParent?: T
107
+ ) => ParseGroupResult<ParsedGroup, ParsedDesignToken, T>;
108
+
109
+ export interface ParserConfig<ParsedDesignToken, ParsedGroup, T> {
110
+ /**
111
+ * A function that determines whether an object in the input
112
+ * data is a design token or a group.
113
+ */
114
+ isDesignTokenData: IsDesignTokenDataFn;
115
+
116
+ /**
117
+ * Array of strings and/or RegExp which match
118
+ * properties of group objects that are NOT
119
+ * names of child design tokens or groups.
120
+ *
121
+ * E.g. for DTCG data, this could be a RegEx
122
+ * like /^$/ which would match all $-prefixed
123
+ * format properties
124
+ */
125
+ groupPropsToExtract: (string | RegExp)[];
126
+
127
+ /**
128
+ * Function which is called for each group data object
129
+ * that is encountered.
130
+ *
131
+ * Is given the extracted properties of that group and its
132
+ * path, and should parse that data into whatever structure
133
+ * is desired.
134
+ */
135
+ parseGroupData: ParseGroupDataFn<ParsedGroup, ParsedDesignToken, T>;
136
+
137
+ /**
138
+ * Function which is called for each design token
139
+ *data object that is encountered.
140
+ *
141
+ * Is given the design token data and its path, and
142
+ * should parse that data into whatever structure is
143
+ * desired.
144
+ */
145
+ parseDesignTokenData: ParseDesignTokenDataFn<ParsedDesignToken, T>;
146
+ }
147
+
148
+ /**
149
+ * Thrown when `parseData()` encounters group or design token
150
+ * data that is not a plain object.
151
+ */
152
+ export class InvalidDataError extends Error {
153
+ /**
154
+ * Path to the value that is not a plain object
155
+ */
156
+ public path: string[];
157
+
158
+ /**
159
+ * The offending value.
160
+ */
161
+ public data: unknown;
162
+
163
+ constructor(path: string[], data: unknown) {
164
+ super(
165
+ `Expected object at path "${path.join(
166
+ "."
167
+ )}", but got ${typeof data} instead`
168
+ );
169
+ this.name = "InvalidDataError";
170
+ this.path = path;
171
+ this.data = data;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * The internal data parsing implementation.
177
+ *
178
+ * Recursively calls itself for nested group and
179
+ * design token data.
180
+ *
181
+ * @param data The input data to traverse and parse
182
+ * @param config Parser config
183
+ * @param contextFromParent Context data passed in from
184
+ * parent calls to this function.
185
+ * @param path The path to the input data
186
+ * @param addToParent A function to add the parsed data
187
+ * to the parent group.
188
+ * @returns The parsed design token or group.
189
+ */
190
+ function parseDataImpl<ParsedDesignToken, ParsedGroup, T>(
191
+ data: unknown,
192
+ config: ParserConfig<ParsedDesignToken, ParsedGroup, T>,
193
+ contextFromParent?: T,
194
+ path: string[] = [],
195
+ addToParent?: AddChildFn<ParsedGroup, ParsedDesignToken>
196
+ ): ParsedDesignToken | ParsedGroup {
197
+ if (!isPlainObject(data)) {
198
+ throw new InvalidDataError(path, data);
199
+ }
200
+
201
+ const {
202
+ isDesignTokenData,
203
+ groupPropsToExtract,
204
+ parseGroupData,
205
+ parseDesignTokenData,
206
+ } = config;
207
+
208
+ let groupOrToken: ParsedGroup | ParsedDesignToken;
209
+ if (isDesignTokenData(data)) {
210
+ // looks like a token
211
+ groupOrToken = parseDesignTokenData(data, path, contextFromParent);
212
+ if (addToParent && path.length > 0) {
213
+ addToParent(path[path.length - 1], groupOrToken);
214
+ }
215
+ } else {
216
+ // must be a group
217
+ const { extracted: groupData, remainingProps: childNames } =
218
+ extractProperties(data, groupPropsToExtract);
219
+ const { group, addChild, contextForChildren } = parseGroupData(
220
+ groupData,
221
+ path,
222
+ contextFromParent
223
+ );
224
+
225
+ groupOrToken = group;
226
+
227
+ if (addToParent && path.length > 0) {
228
+ addToParent(path[path.length - 1], groupOrToken);
229
+ }
230
+
231
+ for (const childName of childNames) {
232
+ parseDataImpl(
233
+ data[childName],
234
+ config,
235
+ contextForChildren,
236
+ [...path, childName],
237
+ addChild
238
+ );
239
+ }
240
+ }
241
+
242
+ return groupOrToken;
243
+ }
244
+
245
+ /**
246
+ * Parses a nested object structure representing groups
247
+ * and design tokens, such as the data obtained by reading
248
+ * and JSON-parsing a DTCG file.
249
+ *
250
+ * It will recursively traverse the input data (depth first)
251
+ * and, using the functions provided in the config:
252
+ *
253
+ * 1. Check if the object is design token or group data
254
+ * 2. Pass that data to the parsed or processed by the
255
+ * relevant function
256
+ * 3. Return the outermost parsed group or design token
257
+ *
258
+ * @param data The input data to traverse and parse
259
+ * @param config Configuration for this parser
260
+ * @param contextFromParent Optional context data to
261
+ * pass into the top-most design token
262
+ * or group parser function call.
263
+ * @returns The outermost parsed group or design token
264
+ */
265
+ export function parseData<ParsedDesignToken, ParsedGroup, T>(
266
+ data: unknown,
267
+ config: ParserConfig<ParsedDesignToken, ParsedGroup, T>,
268
+ contextFromParent?: T
269
+ ): ParsedDesignToken | ParsedGroup {
270
+ return parseDataImpl(data, config, contextFromParent);
271
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist/",
5
+ "declaration": true
6
+ },
7
+ "exclude": [
8
+ "**/*.test.ts",
9
+ "**/test/*.ts"
10
+ ]
11
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "../../shared-configs/tsconfig-base.json",
3
+ "include": [
4
+ "src/"
5
+ ]
6
+ }