@safe-ugc-ui/validator 0.6.0 → 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/dist/index.d.ts +34 -9
- package/dist/index.js +640 -152
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -100,74 +100,252 @@ function parseCard(input) {
|
|
|
100
100
|
return result.success ? result.data : null;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
// src/node-validator.ts
|
|
104
|
-
import { ALL_COMPONENT_TYPES } from "@safe-ugc-ui/types";
|
|
105
|
-
|
|
106
103
|
// src/traverse.ts
|
|
107
104
|
function isForLoop(children) {
|
|
108
105
|
return typeof children === "object" && children !== null && "for" in children && "in" in children && "template" in children;
|
|
109
106
|
}
|
|
107
|
+
function isTraversableNode(value) {
|
|
108
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
109
|
+
}
|
|
110
|
+
function isFragmentUseLike(value) {
|
|
111
|
+
return typeof value === "object" && value !== null && "$use" in value && typeof value.$use === "string";
|
|
112
|
+
}
|
|
113
|
+
function getEmbeddedRenderables(node) {
|
|
114
|
+
const entries = [];
|
|
115
|
+
const interactiveField = node.type === "Accordion" ? "items" : node.type === "Tabs" ? "tabs" : null;
|
|
116
|
+
if (interactiveField) {
|
|
117
|
+
const items = node[interactiveField];
|
|
118
|
+
if (!Array.isArray(items)) {
|
|
119
|
+
return entries;
|
|
120
|
+
}
|
|
121
|
+
for (let i = 0; i < items.length; i++) {
|
|
122
|
+
const item = items[i];
|
|
123
|
+
if (item == null || typeof item !== "object" || Array.isArray(item)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const content = item.content;
|
|
127
|
+
if (isTraversableNode(content) || isFragmentUseLike(content)) {
|
|
128
|
+
entries.push({
|
|
129
|
+
pathSuffix: `${interactiveField}[${i}].content`,
|
|
130
|
+
renderable: content
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return entries;
|
|
136
|
+
}
|
|
137
|
+
function resolveRenderableNode(node, fragments, fragmentStack = []) {
|
|
138
|
+
if (isTraversableNode(node)) {
|
|
139
|
+
return node;
|
|
140
|
+
}
|
|
141
|
+
if (!isFragmentUseLike(node) || !fragments) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (fragmentStack.length > 0 || fragmentStack.includes(node.$use)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const target = fragments[node.$use];
|
|
148
|
+
return isTraversableNode(target) ? target : null;
|
|
149
|
+
}
|
|
110
150
|
function hasOverflowAuto(style) {
|
|
111
151
|
return style?.overflow === "auto";
|
|
112
152
|
}
|
|
113
|
-
function traverseNode(node, context, visitor, styleResolver) {
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
153
|
+
function traverseNode(node, context, visitor, styleResolver, fragments, fragmentStack = []) {
|
|
154
|
+
const resolvedNode = resolveRenderableNode(node, fragments, fragmentStack);
|
|
155
|
+
if (!resolvedNode) {
|
|
116
156
|
return;
|
|
117
157
|
}
|
|
118
|
-
const
|
|
119
|
-
|
|
158
|
+
const nextFragmentStack = isFragmentUseLike(node) ? [...fragmentStack, node.$use] : fragmentStack;
|
|
159
|
+
const result = visitor(resolvedNode, context);
|
|
160
|
+
if (result === false) {
|
|
120
161
|
return;
|
|
121
162
|
}
|
|
122
|
-
const nextStackDepth =
|
|
123
|
-
const nextOverflowAuto = context.overflowAutoAncestor || hasOverflowAuto(styleResolver ? styleResolver(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
163
|
+
const nextStackDepth = resolvedNode.type === "Stack" ? context.stackDepth + 1 : context.stackDepth;
|
|
164
|
+
const nextOverflowAuto = context.overflowAutoAncestor || hasOverflowAuto(styleResolver ? styleResolver(resolvedNode) : resolvedNode.style);
|
|
165
|
+
const children = resolvedNode.children;
|
|
166
|
+
if (children != null) {
|
|
167
|
+
if (isForLoop(children)) {
|
|
168
|
+
const childCtx = {
|
|
169
|
+
path: `${context.path}.children.template`,
|
|
170
|
+
depth: context.depth + 1,
|
|
171
|
+
parentType: resolvedNode.type,
|
|
172
|
+
loopDepth: context.loopDepth + 1,
|
|
173
|
+
overflowAutoAncestor: nextOverflowAuto,
|
|
174
|
+
stackDepth: nextStackDepth
|
|
175
|
+
};
|
|
176
|
+
traverseNode(children.template, childCtx, visitor, styleResolver, fragments, nextFragmentStack);
|
|
177
|
+
} else if (Array.isArray(children)) {
|
|
178
|
+
for (let i = 0; i < children.length; i++) {
|
|
179
|
+
const child = children[i];
|
|
180
|
+
if (isTraversableNode(child) || isFragmentUseLike(child)) {
|
|
181
|
+
const childCtx = {
|
|
182
|
+
path: `${context.path}.children[${i}]`,
|
|
183
|
+
depth: context.depth + 1,
|
|
184
|
+
parentType: resolvedNode.type,
|
|
185
|
+
loopDepth: context.loopDepth,
|
|
186
|
+
overflowAutoAncestor: nextOverflowAuto,
|
|
187
|
+
stackDepth: nextStackDepth
|
|
188
|
+
};
|
|
189
|
+
traverseNode(child, childCtx, visitor, styleResolver, fragments, nextFragmentStack);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const entry of getEmbeddedRenderables(resolvedNode)) {
|
|
195
|
+
const embeddedCtx = {
|
|
196
|
+
path: `${context.path}.${entry.pathSuffix}`,
|
|
127
197
|
depth: context.depth + 1,
|
|
128
|
-
parentType:
|
|
129
|
-
loopDepth: context.loopDepth
|
|
198
|
+
parentType: resolvedNode.type,
|
|
199
|
+
loopDepth: context.loopDepth,
|
|
130
200
|
overflowAutoAncestor: nextOverflowAuto,
|
|
131
201
|
stackDepth: nextStackDepth
|
|
132
202
|
};
|
|
133
|
-
traverseNode(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
parentType: node.type,
|
|
142
|
-
loopDepth: context.loopDepth,
|
|
143
|
-
overflowAutoAncestor: nextOverflowAuto,
|
|
144
|
-
stackDepth: nextStackDepth
|
|
145
|
-
};
|
|
146
|
-
traverseNode(child, childCtx, visitor, styleResolver);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
203
|
+
traverseNode(
|
|
204
|
+
entry.renderable,
|
|
205
|
+
embeddedCtx,
|
|
206
|
+
visitor,
|
|
207
|
+
styleResolver,
|
|
208
|
+
fragments,
|
|
209
|
+
nextFragmentStack
|
|
210
|
+
);
|
|
149
211
|
}
|
|
150
212
|
}
|
|
151
|
-
function traverseCard(views, visitor, styleResolver) {
|
|
213
|
+
function traverseCard(views, visitor, styleResolver, fragments, pathPrefix = "views") {
|
|
152
214
|
for (const [viewName, rootNode] of Object.entries(views)) {
|
|
153
|
-
if (rootNode
|
|
215
|
+
if (!isTraversableNode(rootNode) && !isFragmentUseLike(rootNode)) {
|
|
154
216
|
continue;
|
|
155
217
|
}
|
|
156
218
|
const context = {
|
|
157
|
-
path:
|
|
219
|
+
path: `${pathPrefix}.${viewName}`,
|
|
158
220
|
depth: 0,
|
|
159
221
|
parentType: null,
|
|
160
222
|
loopDepth: 0,
|
|
161
223
|
overflowAutoAncestor: false,
|
|
162
224
|
stackDepth: 0
|
|
163
225
|
};
|
|
164
|
-
traverseNode(rootNode, context, visitor, styleResolver);
|
|
226
|
+
traverseNode(rootNode, context, visitor, styleResolver, fragments);
|
|
165
227
|
}
|
|
166
228
|
}
|
|
167
229
|
|
|
230
|
+
// src/renderable-walk.ts
|
|
231
|
+
function isForLoopLike(value) {
|
|
232
|
+
return typeof value === "object" && value !== null && "for" in value && "in" in value && "template" in value;
|
|
233
|
+
}
|
|
234
|
+
function walkRenderableNode(node, path, inFragments, visitor) {
|
|
235
|
+
visitor(node, { path, inFragments });
|
|
236
|
+
if (!isTraversableNode(node)) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const children = node.children;
|
|
240
|
+
if (Array.isArray(children)) {
|
|
241
|
+
for (let i = 0; i < children.length; i++) {
|
|
242
|
+
const child = children[i];
|
|
243
|
+
if (!isTraversableNode(child) && !isFragmentUseLike(child)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
walkRenderableNode(
|
|
247
|
+
child,
|
|
248
|
+
`${path}.children[${i}]`,
|
|
249
|
+
inFragments,
|
|
250
|
+
visitor
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (isForLoopLike(children)) {
|
|
255
|
+
const template = children.template;
|
|
256
|
+
if (isTraversableNode(template) || isFragmentUseLike(template)) {
|
|
257
|
+
walkRenderableNode(
|
|
258
|
+
template,
|
|
259
|
+
`${path}.children.template`,
|
|
260
|
+
inFragments,
|
|
261
|
+
visitor
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const entry of getEmbeddedRenderables(node)) {
|
|
266
|
+
walkRenderableNode(
|
|
267
|
+
entry.renderable,
|
|
268
|
+
`${path}.${entry.pathSuffix}`,
|
|
269
|
+
inFragments,
|
|
270
|
+
visitor
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function walkRenderableCard(views, fragments, visitor) {
|
|
275
|
+
for (const [viewName, rootNode] of Object.entries(views)) {
|
|
276
|
+
if (!isTraversableNode(rootNode) && !isFragmentUseLike(rootNode)) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
walkRenderableNode(
|
|
280
|
+
rootNode,
|
|
281
|
+
`views.${viewName}`,
|
|
282
|
+
false,
|
|
283
|
+
visitor
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (!fragments) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
for (const [fragmentName, fragmentRoot] of Object.entries(fragments)) {
|
|
290
|
+
if (!isTraversableNode(fragmentRoot) && !isFragmentUseLike(fragmentRoot)) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
walkRenderableNode(
|
|
294
|
+
fragmentRoot,
|
|
295
|
+
`fragments.${fragmentName}`,
|
|
296
|
+
true,
|
|
297
|
+
visitor
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/fragment-validator.ts
|
|
303
|
+
var FRAGMENT_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
304
|
+
function validateFragments(views, fragments) {
|
|
305
|
+
const errors = [];
|
|
306
|
+
if (fragments) {
|
|
307
|
+
for (const fragmentName of Object.keys(fragments)) {
|
|
308
|
+
if (!FRAGMENT_NAME_PATTERN.test(fragmentName)) {
|
|
309
|
+
errors.push(
|
|
310
|
+
createError(
|
|
311
|
+
"INVALID_FRAGMENT_NAME",
|
|
312
|
+
`Fragment name "${fragmentName}" is invalid; must match /^[A-Za-z][A-Za-z0-9_-]*$/.`,
|
|
313
|
+
`fragments.${fragmentName}`
|
|
314
|
+
)
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
walkRenderableCard(views, fragments, (node, context) => {
|
|
320
|
+
if (!isFragmentUseLike(node)) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (context.inFragments) {
|
|
324
|
+
errors.push(
|
|
325
|
+
createError(
|
|
326
|
+
"FRAGMENT_NESTED_USE",
|
|
327
|
+
`Fragments may not contain "$use" references. Found at "${context.path}".`,
|
|
328
|
+
context.path
|
|
329
|
+
)
|
|
330
|
+
);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (!fragments || !(node.$use in fragments)) {
|
|
334
|
+
errors.push(
|
|
335
|
+
createError(
|
|
336
|
+
"FRAGMENT_REF_NOT_FOUND",
|
|
337
|
+
`Fragment "${node.$use}" was not found.`,
|
|
338
|
+
context.path
|
|
339
|
+
)
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
return errors;
|
|
344
|
+
}
|
|
345
|
+
|
|
168
346
|
// src/node-validator.ts
|
|
347
|
+
import { ALL_COMPONENT_TYPES, MAX_INTERACTIVE_ITEMS } from "@safe-ugc-ui/types";
|
|
169
348
|
var REQUIRED_FIELDS = {
|
|
170
|
-
Text: ["content"],
|
|
171
349
|
Image: ["src"],
|
|
172
350
|
ProgressBar: ["value", "max"],
|
|
173
351
|
Avatar: ["src"],
|
|
@@ -175,7 +353,9 @@ var REQUIRED_FIELDS = {
|
|
|
175
353
|
Badge: ["label"],
|
|
176
354
|
Chip: ["label"],
|
|
177
355
|
Button: ["label", "action"],
|
|
178
|
-
Toggle: ["value", "onToggle"]
|
|
356
|
+
Toggle: ["value", "onToggle"],
|
|
357
|
+
Accordion: ["items"],
|
|
358
|
+
Tabs: ["tabs"]
|
|
179
359
|
};
|
|
180
360
|
var KNOWN_TYPES = new Set(ALL_COMPONENT_TYPES);
|
|
181
361
|
function looksLikeForLoop(value) {
|
|
@@ -203,17 +383,42 @@ function validateForLoop(children, path) {
|
|
|
203
383
|
);
|
|
204
384
|
}
|
|
205
385
|
const template = children["template"];
|
|
206
|
-
if (typeof template !== "object" || template === null || !("type" in template)) {
|
|
386
|
+
if (typeof template !== "object" || template === null || !("type" in template) && !isFragmentUseLike(template)) {
|
|
207
387
|
errors.push(
|
|
208
388
|
createError(
|
|
209
389
|
"INVALID_VALUE",
|
|
210
|
-
'ForLoop "template" must be an object with a "type" property.',
|
|
390
|
+
'ForLoop "template" must be an object with a "type" property or "$use" reference.',
|
|
211
391
|
`${path}.children.template`
|
|
212
392
|
)
|
|
213
393
|
);
|
|
214
394
|
}
|
|
215
395
|
return errors;
|
|
216
396
|
}
|
|
397
|
+
function collectUniqueInteractiveItemIds(items, nodeType, path, errors) {
|
|
398
|
+
const itemIds = /* @__PURE__ */ new Set();
|
|
399
|
+
for (let i = 0; i < items.length; i++) {
|
|
400
|
+
const item = items[i];
|
|
401
|
+
if (item == null || typeof item !== "object" || Array.isArray(item)) {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const itemId = item.id;
|
|
405
|
+
if (typeof itemId !== "string" || itemId.length === 0) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (itemIds.has(itemId)) {
|
|
409
|
+
errors.push(
|
|
410
|
+
createError(
|
|
411
|
+
"INVALID_VALUE",
|
|
412
|
+
`"${nodeType}" item ids must be unique. Duplicate id "${itemId}".`,
|
|
413
|
+
`${path}[${i}].id`
|
|
414
|
+
)
|
|
415
|
+
);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
itemIds.add(itemId);
|
|
419
|
+
}
|
|
420
|
+
return itemIds;
|
|
421
|
+
}
|
|
217
422
|
function validateNode(node, context) {
|
|
218
423
|
const errors = [];
|
|
219
424
|
const { path } = context;
|
|
@@ -227,6 +432,28 @@ function validateNode(node, context) {
|
|
|
227
432
|
);
|
|
228
433
|
return errors;
|
|
229
434
|
}
|
|
435
|
+
if (node.type === "Text") {
|
|
436
|
+
const hasContent = "content" in node && node.content !== void 0;
|
|
437
|
+
const hasSpans = "spans" in node && node.spans !== void 0;
|
|
438
|
+
if (!hasContent && !hasSpans) {
|
|
439
|
+
errors.push(
|
|
440
|
+
createError(
|
|
441
|
+
"MISSING_FIELD",
|
|
442
|
+
'"Text" node must define either "content" or "spans".',
|
|
443
|
+
`${path}.content`
|
|
444
|
+
)
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
if (hasContent && hasSpans) {
|
|
448
|
+
errors.push(
|
|
449
|
+
createError(
|
|
450
|
+
"INVALID_VALUE",
|
|
451
|
+
'"Text" node cannot define both "content" and "spans".',
|
|
452
|
+
path
|
|
453
|
+
)
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
230
457
|
const requiredFields = REQUIRED_FIELDS[node.type];
|
|
231
458
|
if (requiredFields && requiredFields.length > 0) {
|
|
232
459
|
for (const field of requiredFields) {
|
|
@@ -249,12 +476,148 @@ function validateNode(node, context) {
|
|
|
249
476
|
)
|
|
250
477
|
);
|
|
251
478
|
}
|
|
479
|
+
if (node.type === "Accordion") {
|
|
480
|
+
const items = Array.isArray(node.items) ? node.items : void 0;
|
|
481
|
+
if (items) {
|
|
482
|
+
if (items.length > MAX_INTERACTIVE_ITEMS) {
|
|
483
|
+
errors.push(
|
|
484
|
+
createError(
|
|
485
|
+
"INVALID_VALUE",
|
|
486
|
+
`"Accordion" node may define at most ${MAX_INTERACTIVE_ITEMS} items.`,
|
|
487
|
+
`${path}.items`
|
|
488
|
+
)
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
const itemIds = collectUniqueInteractiveItemIds(
|
|
492
|
+
items,
|
|
493
|
+
"Accordion",
|
|
494
|
+
`${path}.items`,
|
|
495
|
+
errors
|
|
496
|
+
);
|
|
497
|
+
if (Array.isArray(node.defaultExpanded)) {
|
|
498
|
+
if (node.allowMultiple !== true && node.defaultExpanded.length > 1) {
|
|
499
|
+
errors.push(
|
|
500
|
+
createError(
|
|
501
|
+
"INVALID_VALUE",
|
|
502
|
+
'"Accordion" cannot define multiple defaultExpanded ids unless "allowMultiple" is true.',
|
|
503
|
+
`${path}.defaultExpanded`
|
|
504
|
+
)
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
for (let i = 0; i < node.defaultExpanded.length; i++) {
|
|
508
|
+
const itemId = node.defaultExpanded[i];
|
|
509
|
+
if (typeof itemId !== "string" || !itemIds.has(itemId)) {
|
|
510
|
+
errors.push(
|
|
511
|
+
createError(
|
|
512
|
+
"INVALID_VALUE",
|
|
513
|
+
`"Accordion" defaultExpanded id "${String(itemId)}" was not found in items.`,
|
|
514
|
+
`${path}.defaultExpanded[${i}]`
|
|
515
|
+
)
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (node.type === "Tabs") {
|
|
523
|
+
const tabs = Array.isArray(node.tabs) ? node.tabs : void 0;
|
|
524
|
+
if (tabs) {
|
|
525
|
+
if (tabs.length > MAX_INTERACTIVE_ITEMS) {
|
|
526
|
+
errors.push(
|
|
527
|
+
createError(
|
|
528
|
+
"INVALID_VALUE",
|
|
529
|
+
`"Tabs" node may define at most ${MAX_INTERACTIVE_ITEMS} tabs.`,
|
|
530
|
+
`${path}.tabs`
|
|
531
|
+
)
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
const tabIds = collectUniqueInteractiveItemIds(
|
|
535
|
+
tabs,
|
|
536
|
+
"Tabs",
|
|
537
|
+
`${path}.tabs`,
|
|
538
|
+
errors
|
|
539
|
+
);
|
|
540
|
+
if ("defaultTab" in node) {
|
|
541
|
+
const defaultTab = node.defaultTab;
|
|
542
|
+
if (typeof defaultTab !== "string" || !tabIds.has(defaultTab)) {
|
|
543
|
+
errors.push(
|
|
544
|
+
createError(
|
|
545
|
+
"INVALID_VALUE",
|
|
546
|
+
`"Tabs" defaultTab "${String(defaultTab)}" was not found in tabs.`,
|
|
547
|
+
`${path}.defaultTab`
|
|
548
|
+
)
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
252
554
|
return errors;
|
|
253
555
|
}
|
|
254
|
-
function validateNodes(views) {
|
|
556
|
+
function validateNodes(views, fragments) {
|
|
255
557
|
const errors = [];
|
|
256
|
-
|
|
257
|
-
|
|
558
|
+
walkRenderableCard(views, fragments, (node, context) => {
|
|
559
|
+
if (!("type" in node) || typeof node.type !== "string") {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
errors.push(
|
|
563
|
+
...validateNode(
|
|
564
|
+
node,
|
|
565
|
+
{
|
|
566
|
+
path: context.path,
|
|
567
|
+
depth: 0,
|
|
568
|
+
parentType: null,
|
|
569
|
+
loopDepth: 0,
|
|
570
|
+
overflowAutoAncestor: false,
|
|
571
|
+
stackDepth: 0
|
|
572
|
+
}
|
|
573
|
+
)
|
|
574
|
+
);
|
|
575
|
+
});
|
|
576
|
+
return errors;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/condition-validator.ts
|
|
580
|
+
import { MAX_CONDITION_DEPTH, isRef } from "@safe-ugc-ui/types";
|
|
581
|
+
function validateConditionDepth(condition, path, errors, depth = 1) {
|
|
582
|
+
if (depth > MAX_CONDITION_DEPTH) {
|
|
583
|
+
errors.push(
|
|
584
|
+
createError(
|
|
585
|
+
"CONDITION_DEPTH_EXCEEDED",
|
|
586
|
+
`$if condition exceeds maximum depth of ${MAX_CONDITION_DEPTH} at "${path}"`,
|
|
587
|
+
path
|
|
588
|
+
)
|
|
589
|
+
);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (typeof condition === "boolean" || isRef(condition)) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (typeof condition !== "object" || condition === null || Array.isArray(condition)) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const conditionObj = condition;
|
|
599
|
+
const op = conditionObj.op;
|
|
600
|
+
if (op === "not") {
|
|
601
|
+
validateConditionDepth(conditionObj.value, `${path}.value`, errors, depth + 1);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if ((op === "and" || op === "or") && Array.isArray(conditionObj.values)) {
|
|
605
|
+
for (let i = 0; i < conditionObj.values.length; i++) {
|
|
606
|
+
validateConditionDepth(
|
|
607
|
+
conditionObj.values[i],
|
|
608
|
+
`${path}.values[${i}]`,
|
|
609
|
+
errors,
|
|
610
|
+
depth + 1
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function validateConditions(views, fragments) {
|
|
616
|
+
const errors = [];
|
|
617
|
+
walkRenderableCard(views, fragments, (node, ctx) => {
|
|
618
|
+
if ("$if" in node) {
|
|
619
|
+
validateConditionDepth(node.$if, `${ctx.path}.$if`, errors);
|
|
620
|
+
}
|
|
258
621
|
});
|
|
259
622
|
return errors;
|
|
260
623
|
}
|
|
@@ -317,10 +680,14 @@ function validateNodeStyle(style, path, errors) {
|
|
|
317
680
|
}
|
|
318
681
|
}
|
|
319
682
|
}
|
|
320
|
-
function validateValueTypes(views) {
|
|
683
|
+
function validateValueTypes(views, fragments) {
|
|
321
684
|
const errors = [];
|
|
322
|
-
|
|
323
|
-
|
|
685
|
+
walkRenderableCard(views, fragments, (node, ctx) => {
|
|
686
|
+
if (!("type" in node) || typeof node.type !== "string") {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const style = node.style != null && typeof node.style === "object" && !Array.isArray(node.style) ? node.style : void 0;
|
|
690
|
+
validateNodeStyle(style, `${ctx.path}.style`, errors);
|
|
324
691
|
const responsive = node.responsive;
|
|
325
692
|
if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
|
|
326
693
|
const compact = responsive.compact;
|
|
@@ -338,6 +705,7 @@ function validateValueTypes(views) {
|
|
|
338
705
|
|
|
339
706
|
// src/style-validator.ts
|
|
340
707
|
import {
|
|
708
|
+
ASPECT_RATIO_PATTERN,
|
|
341
709
|
FORBIDDEN_STYLE_PROPERTIES,
|
|
342
710
|
DANGEROUS_CSS_FUNCTIONS,
|
|
343
711
|
ZINDEX_MIN,
|
|
@@ -363,7 +731,7 @@ import {
|
|
|
363
731
|
TRANSITION_DELAY_MAX,
|
|
364
732
|
TRANSITION_MAX_COUNT,
|
|
365
733
|
ALLOWED_TRANSITION_PROPERTIES,
|
|
366
|
-
isRef
|
|
734
|
+
isRef as isRef2
|
|
367
735
|
} from "@safe-ugc-ui/types";
|
|
368
736
|
var COLOR_PROPERTIES = /* @__PURE__ */ new Set(["backgroundColor", "color"]);
|
|
369
737
|
var LENGTH_PROPERTIES = /* @__PURE__ */ new Set([
|
|
@@ -419,7 +787,7 @@ function isLiteralString(value) {
|
|
|
419
787
|
return typeof value === "string";
|
|
420
788
|
}
|
|
421
789
|
function isDynamic(value) {
|
|
422
|
-
return
|
|
790
|
+
return isRef2(value);
|
|
423
791
|
}
|
|
424
792
|
function isValidColor(value) {
|
|
425
793
|
const lower = value.toLowerCase();
|
|
@@ -447,6 +815,22 @@ function parseLengthValue(value) {
|
|
|
447
815
|
}
|
|
448
816
|
return Number(match[1]);
|
|
449
817
|
}
|
|
818
|
+
function isValidAspectRatioLiteral(value) {
|
|
819
|
+
if (typeof value === "number") {
|
|
820
|
+
return Number.isFinite(value) && value > 0;
|
|
821
|
+
}
|
|
822
|
+
const match = value.match(ASPECT_RATIO_PATTERN);
|
|
823
|
+
if (!match) {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
const parts = value.split("/");
|
|
827
|
+
if (parts.length !== 2) {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
const width = Number(parts[0].trim());
|
|
831
|
+
const height = Number(parts[1].trim());
|
|
832
|
+
return Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0;
|
|
833
|
+
}
|
|
450
834
|
function collectDangerousCssErrors(value, path, errors) {
|
|
451
835
|
if (typeof value === "string") {
|
|
452
836
|
const lower = value.toLowerCase();
|
|
@@ -464,7 +848,7 @@ function collectDangerousCssErrors(value, path, errors) {
|
|
|
464
848
|
}
|
|
465
849
|
return;
|
|
466
850
|
}
|
|
467
|
-
if (
|
|
851
|
+
if (isRef2(value)) {
|
|
468
852
|
return;
|
|
469
853
|
}
|
|
470
854
|
if (Array.isArray(value)) {
|
|
@@ -854,6 +1238,18 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, options = {})
|
|
|
854
1238
|
}
|
|
855
1239
|
}
|
|
856
1240
|
}
|
|
1241
|
+
if ("aspectRatio" in style && style.aspectRatio != null && !isDynamic(style.aspectRatio)) {
|
|
1242
|
+
const aspectRatio = style.aspectRatio;
|
|
1243
|
+
if (typeof aspectRatio !== "number" && typeof aspectRatio !== "string" || !isValidAspectRatioLiteral(aspectRatio)) {
|
|
1244
|
+
errors.push(
|
|
1245
|
+
createError(
|
|
1246
|
+
"INVALID_VALUE",
|
|
1247
|
+
`Invalid aspectRatio "${String(aspectRatio)}" at "${stylePath}.aspectRatio"`,
|
|
1248
|
+
`${stylePath}.aspectRatio`
|
|
1249
|
+
)
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
857
1253
|
for (const [prop, range] of Object.entries(RANGE_LENGTH_PROPERTIES)) {
|
|
858
1254
|
if (prop in style && isLiteralString(style[prop]) && !isDynamic(style[prop])) {
|
|
859
1255
|
const numericValue = parseLengthValue(style[prop]);
|
|
@@ -1016,7 +1412,7 @@ function mergeStyleWithRef(cardStyleEntry, inlineStyle) {
|
|
|
1016
1412
|
}
|
|
1017
1413
|
return merged;
|
|
1018
1414
|
}
|
|
1019
|
-
function validateStyles(views, cardStyles) {
|
|
1415
|
+
function validateStyles(views, cardStyles, fragments) {
|
|
1020
1416
|
const errors = [];
|
|
1021
1417
|
if (cardStyles) {
|
|
1022
1418
|
for (const [styleName, styleEntry] of Object.entries(cardStyles)) {
|
|
@@ -1036,8 +1432,11 @@ function validateStyles(views, cardStyles) {
|
|
|
1036
1432
|
});
|
|
1037
1433
|
}
|
|
1038
1434
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1435
|
+
walkRenderableCard(views, fragments, (node, ctx) => {
|
|
1436
|
+
if (!("type" in node) || typeof node.type !== "string") {
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
const style = node.style != null && typeof node.style === "object" && !Array.isArray(node.style) ? node.style : void 0;
|
|
1041
1440
|
if (style != null && typeof style === "object") {
|
|
1042
1441
|
const stylePath = `${ctx.path}.style`;
|
|
1043
1442
|
const mergedStyle = resolveStyleRef(style, stylePath, cardStyles, errors);
|
|
@@ -1046,26 +1445,48 @@ function validateStyles(views, cardStyles) {
|
|
|
1046
1445
|
}
|
|
1047
1446
|
}
|
|
1048
1447
|
const responsive = node.responsive;
|
|
1049
|
-
if (responsive
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1448
|
+
if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
|
|
1449
|
+
const compact = responsive.compact;
|
|
1450
|
+
if (compact != null && typeof compact === "object" && !Array.isArray(compact)) {
|
|
1451
|
+
const compactPath = `${ctx.path}.responsive.compact`;
|
|
1452
|
+
const mergedCompact = resolveStyleRef(
|
|
1453
|
+
compact,
|
|
1454
|
+
compactPath,
|
|
1455
|
+
cardStyles,
|
|
1456
|
+
errors
|
|
1457
|
+
);
|
|
1458
|
+
if (mergedCompact) {
|
|
1459
|
+
validateSingleStyle(mergedCompact, compactPath, errors, cardStyles, {
|
|
1460
|
+
allowHoverStyle: false,
|
|
1461
|
+
allowTransition: false,
|
|
1462
|
+
allowHoverStyleRefs: false
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1055
1466
|
}
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1467
|
+
const spans = Array.isArray(node.spans) ? node.spans : void 0;
|
|
1468
|
+
if (node.type === "Text" && spans) {
|
|
1469
|
+
for (let i = 0; i < spans.length; i++) {
|
|
1470
|
+
const span = spans[i];
|
|
1471
|
+
if (span == null || typeof span !== "object" || Array.isArray(span)) {
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
const spanStyle = span.style;
|
|
1475
|
+
if (spanStyle == null || typeof spanStyle !== "object" || Array.isArray(spanStyle)) {
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
validateSingleStyle(
|
|
1479
|
+
spanStyle,
|
|
1480
|
+
`${ctx.path}.spans[${i}].style`,
|
|
1481
|
+
errors,
|
|
1482
|
+
void 0,
|
|
1483
|
+
{
|
|
1484
|
+
allowHoverStyle: false,
|
|
1485
|
+
allowTransition: false,
|
|
1486
|
+
allowHoverStyleRefs: false
|
|
1487
|
+
}
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1069
1490
|
}
|
|
1070
1491
|
});
|
|
1071
1492
|
return errors;
|
|
@@ -1074,7 +1495,7 @@ function validateStyles(views, cardStyles) {
|
|
|
1074
1495
|
// src/security.ts
|
|
1075
1496
|
import {
|
|
1076
1497
|
PROTOTYPE_POLLUTION_SEGMENTS,
|
|
1077
|
-
isRef as
|
|
1498
|
+
isRef as isRef3
|
|
1078
1499
|
} from "@safe-ugc-ui/types";
|
|
1079
1500
|
|
|
1080
1501
|
// src/responsive-utils.ts
|
|
@@ -1153,7 +1574,7 @@ function scanForRefs(obj, path, errors) {
|
|
|
1153
1574
|
if (obj === null || obj === void 0 || typeof obj !== "object") {
|
|
1154
1575
|
return;
|
|
1155
1576
|
}
|
|
1156
|
-
if (
|
|
1577
|
+
if (isRef3(obj)) {
|
|
1157
1578
|
const refStr = obj.$ref;
|
|
1158
1579
|
const segments = refStr.split(/[.\[]/);
|
|
1159
1580
|
for (const segment of segments) {
|
|
@@ -1266,6 +1687,23 @@ function pushUniqueError(errors, seen, error) {
|
|
|
1266
1687
|
seen.add(key);
|
|
1267
1688
|
errors.push(error);
|
|
1268
1689
|
}
|
|
1690
|
+
function getScannableNodeFields(node) {
|
|
1691
|
+
const nodeFields = { ...node };
|
|
1692
|
+
delete nodeFields.type;
|
|
1693
|
+
delete nodeFields.style;
|
|
1694
|
+
delete nodeFields.children;
|
|
1695
|
+
const interactiveField = node.type === "Accordion" ? "items" : node.type === "Tabs" ? "tabs" : null;
|
|
1696
|
+
if (interactiveField && Array.isArray(node[interactiveField])) {
|
|
1697
|
+
nodeFields[interactiveField] = node[interactiveField].map((item) => {
|
|
1698
|
+
if (item == null || typeof item !== "object" || Array.isArray(item)) {
|
|
1699
|
+
return item;
|
|
1700
|
+
}
|
|
1701
|
+
const { content: _content, ...rest } = item;
|
|
1702
|
+
return rest;
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
return nodeFields;
|
|
1706
|
+
}
|
|
1269
1707
|
function scanStyleStringsForUrl(style, path, errors) {
|
|
1270
1708
|
if (!style) {
|
|
1271
1709
|
return;
|
|
@@ -1282,7 +1720,7 @@ function scanStyleStringsForUrl(style, path, errors) {
|
|
|
1282
1720
|
}
|
|
1283
1721
|
}
|
|
1284
1722
|
}
|
|
1285
|
-
function validateEffectiveStylesForMode(mode, views, cardStyles, errors, seen) {
|
|
1723
|
+
function validateEffectiveStylesForMode(mode, views, cardStyles, fragments, errors, seen) {
|
|
1286
1724
|
const styleResolver = (node) => getEffectiveStyleForMode(node, cardStyles, mode);
|
|
1287
1725
|
traverseCard(views, (node, context) => {
|
|
1288
1726
|
const effectiveStyle = styleResolver(node);
|
|
@@ -1331,12 +1769,12 @@ function validateEffectiveStylesForMode(mode, views, cardStyles, errors, seen) {
|
|
|
1331
1769
|
)
|
|
1332
1770
|
);
|
|
1333
1771
|
}
|
|
1334
|
-
}, styleResolver);
|
|
1772
|
+
}, styleResolver, fragments);
|
|
1335
1773
|
}
|
|
1336
1774
|
function validateSecurity(card) {
|
|
1337
1775
|
const errors = [];
|
|
1338
1776
|
const seen = /* @__PURE__ */ new Set();
|
|
1339
|
-
const { views, state, cardAssets, cardStyles } = card;
|
|
1777
|
+
const { views, state, cardAssets, cardStyles, fragments } = card;
|
|
1340
1778
|
if (cardAssets) {
|
|
1341
1779
|
for (const [key, value] of Object.entries(cardAssets)) {
|
|
1342
1780
|
const assetError = validateAssetPath(value);
|
|
@@ -1359,18 +1797,20 @@ function validateSecurity(card) {
|
|
|
1359
1797
|
}
|
|
1360
1798
|
}
|
|
1361
1799
|
}
|
|
1362
|
-
|
|
1800
|
+
walkRenderableCard(views, fragments, (node, context) => {
|
|
1801
|
+
if (!("type" in node) || typeof node.type !== "string") {
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
const traversableNode = node;
|
|
1363
1805
|
const { path } = context;
|
|
1364
|
-
const
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1367
|
-
delete nodeFields.style;
|
|
1368
|
-
delete nodeFields.children;
|
|
1806
|
+
const style = traversableNode.style != null && typeof traversableNode.style === "object" && !Array.isArray(traversableNode.style) ? traversableNode.style : void 0;
|
|
1807
|
+
const type = traversableNode.type;
|
|
1808
|
+
const nodeFields = getScannableNodeFields(traversableNode);
|
|
1369
1809
|
if (type === "Image" || type === "Avatar") {
|
|
1370
1810
|
const src = node.src;
|
|
1371
1811
|
if (typeof src === "string") {
|
|
1372
1812
|
checkSrcValue(src, type, `${path}.src`, errors);
|
|
1373
|
-
} else if (
|
|
1813
|
+
} else if (isRef3(src)) {
|
|
1374
1814
|
if (state) {
|
|
1375
1815
|
const resolved = resolveRefFromState(
|
|
1376
1816
|
src.$ref,
|
|
@@ -1383,7 +1823,7 @@ function validateSecurity(card) {
|
|
|
1383
1823
|
}
|
|
1384
1824
|
}
|
|
1385
1825
|
scanStyleStringsForUrl(style, `${path}.style`, errors);
|
|
1386
|
-
const responsive =
|
|
1826
|
+
const responsive = traversableNode.responsive;
|
|
1387
1827
|
if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
|
|
1388
1828
|
const compact = responsive.compact;
|
|
1389
1829
|
if (compact != null && typeof compact === "object" && !Array.isArray(compact)) {
|
|
@@ -1394,16 +1834,33 @@ function validateSecurity(card) {
|
|
|
1394
1834
|
);
|
|
1395
1835
|
}
|
|
1396
1836
|
}
|
|
1837
|
+
if (type === "Text" && Array.isArray(traversableNode.spans)) {
|
|
1838
|
+
const spans = traversableNode.spans;
|
|
1839
|
+
for (let i = 0; i < spans.length; i++) {
|
|
1840
|
+
const span = spans[i];
|
|
1841
|
+
if (span == null || typeof span !== "object" || Array.isArray(span)) {
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
const spanStyle = span.style;
|
|
1845
|
+
if (spanStyle != null && typeof spanStyle === "object" && !Array.isArray(spanStyle)) {
|
|
1846
|
+
scanStyleStringsForUrl(
|
|
1847
|
+
spanStyle,
|
|
1848
|
+
`${path}.spans[${i}].style`,
|
|
1849
|
+
errors
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1397
1854
|
scanForRefs(nodeFields, path, errors);
|
|
1398
1855
|
if (style) {
|
|
1399
1856
|
scanForRefs(style, `${path}.style`, errors);
|
|
1400
1857
|
}
|
|
1401
|
-
if (
|
|
1402
|
-
scanForRefs(
|
|
1858
|
+
if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
|
|
1859
|
+
scanForRefs(responsive, `${path}.responsive`, errors);
|
|
1403
1860
|
}
|
|
1404
1861
|
});
|
|
1405
1862
|
for (const mode of RESPONSIVE_MODES) {
|
|
1406
|
-
validateEffectiveStylesForMode(mode, views, cardStyles, errors, seen);
|
|
1863
|
+
validateEffectiveStylesForMode(mode, views, cardStyles, fragments, errors, seen);
|
|
1407
1864
|
}
|
|
1408
1865
|
return errors;
|
|
1409
1866
|
}
|
|
@@ -1417,8 +1874,7 @@ import {
|
|
|
1417
1874
|
MAX_NESTED_LOOPS,
|
|
1418
1875
|
MAX_OVERFLOW_AUTO_COUNT,
|
|
1419
1876
|
MAX_STACK_NESTING,
|
|
1420
|
-
PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2
|
|
1421
|
-
isRef as isRef3
|
|
1877
|
+
PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2
|
|
1422
1878
|
} from "@safe-ugc-ui/types";
|
|
1423
1879
|
function utf8ByteLength(str) {
|
|
1424
1880
|
let bytes = 0;
|
|
@@ -1437,6 +1893,54 @@ function utf8ByteLength(str) {
|
|
|
1437
1893
|
}
|
|
1438
1894
|
return bytes;
|
|
1439
1895
|
}
|
|
1896
|
+
function isTemplateObject(value) {
|
|
1897
|
+
return typeof value === "object" && value !== null && "$template" in value && Array.isArray(value.$template);
|
|
1898
|
+
}
|
|
1899
|
+
function countLiteralTemplatedStringBytes(value) {
|
|
1900
|
+
if (typeof value === "string") {
|
|
1901
|
+
return utf8ByteLength(value);
|
|
1902
|
+
}
|
|
1903
|
+
if (!isTemplateObject(value)) {
|
|
1904
|
+
return 0;
|
|
1905
|
+
}
|
|
1906
|
+
return value.$template.reduce((total, part) => {
|
|
1907
|
+
if (typeof part === "string") {
|
|
1908
|
+
return total + utf8ByteLength(part);
|
|
1909
|
+
}
|
|
1910
|
+
if (typeof part === "number" || typeof part === "boolean" || part === null) {
|
|
1911
|
+
return total + utf8ByteLength(String(part));
|
|
1912
|
+
}
|
|
1913
|
+
return total;
|
|
1914
|
+
}, 0);
|
|
1915
|
+
}
|
|
1916
|
+
function countTextNodeLiteralBytes(node) {
|
|
1917
|
+
if (Array.isArray(node.spans)) {
|
|
1918
|
+
return node.spans.reduce((total, span) => {
|
|
1919
|
+
if (span == null || typeof span !== "object" || Array.isArray(span)) {
|
|
1920
|
+
return total;
|
|
1921
|
+
}
|
|
1922
|
+
return total + countLiteralTemplatedStringBytes(
|
|
1923
|
+
span.text
|
|
1924
|
+
);
|
|
1925
|
+
}, 0);
|
|
1926
|
+
}
|
|
1927
|
+
return countLiteralTemplatedStringBytes(node.content);
|
|
1928
|
+
}
|
|
1929
|
+
function countTextSpanStyleBytes(node) {
|
|
1930
|
+
if (!Array.isArray(node.spans)) {
|
|
1931
|
+
return 0;
|
|
1932
|
+
}
|
|
1933
|
+
return node.spans.reduce((total, span) => {
|
|
1934
|
+
if (span == null || typeof span !== "object" || Array.isArray(span)) {
|
|
1935
|
+
return total;
|
|
1936
|
+
}
|
|
1937
|
+
const style = span.style;
|
|
1938
|
+
if (style == null || typeof style !== "object" || Array.isArray(style)) {
|
|
1939
|
+
return total;
|
|
1940
|
+
}
|
|
1941
|
+
return total + utf8ByteLength(JSON.stringify(style));
|
|
1942
|
+
}, 0);
|
|
1943
|
+
}
|
|
1440
1944
|
function resolveRefFromState2(refPath, state) {
|
|
1441
1945
|
const path = refPath.startsWith("$") ? refPath.slice(1) : refPath;
|
|
1442
1946
|
const dotSegments = path.split(".");
|
|
@@ -1471,7 +1975,7 @@ function resolveRefFromState2(refPath, state) {
|
|
|
1471
1975
|
}
|
|
1472
1976
|
return current;
|
|
1473
1977
|
}
|
|
1474
|
-
function countTemplateMetrics(template, cardStyles) {
|
|
1978
|
+
function countTemplateMetrics(template, cardStyles, fragments) {
|
|
1475
1979
|
const result = {
|
|
1476
1980
|
nodes: 0,
|
|
1477
1981
|
textBytes: 0,
|
|
@@ -1481,64 +1985,39 @@ function countTemplateMetrics(template, cardStyles) {
|
|
|
1481
1985
|
compact: 0
|
|
1482
1986
|
}
|
|
1483
1987
|
};
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
}
|
|
1494
|
-
{
|
|
1495
|
-
const baseStyleForBytes = getEffectiveStyleForMode(
|
|
1496
|
-
node,
|
|
1497
|
-
cardStyles,
|
|
1498
|
-
"default"
|
|
1499
|
-
);
|
|
1500
|
-
if (baseStyleForBytes) {
|
|
1501
|
-
result.styleBytes += utf8ByteLength(JSON.stringify(baseStyleForBytes));
|
|
1502
|
-
}
|
|
1503
|
-
const compactStyleForBytes = getMergedCompactResponsiveStyle(
|
|
1504
|
-
node,
|
|
1505
|
-
cardStyles
|
|
1506
|
-
);
|
|
1507
|
-
if (compactStyleForBytes) {
|
|
1508
|
-
result.styleBytes += utf8ByteLength(JSON.stringify(compactStyleForBytes));
|
|
1509
|
-
}
|
|
1510
|
-
for (const mode of RESPONSIVE_MODES) {
|
|
1511
|
-
const effectiveStyle = getEffectiveStyleForMode(
|
|
1988
|
+
traverseCard(
|
|
1989
|
+
{ __template: template },
|
|
1990
|
+
(node) => {
|
|
1991
|
+
result.nodes += 1;
|
|
1992
|
+
if (node.type === "Text") {
|
|
1993
|
+
result.textBytes += countTextNodeLiteralBytes(node);
|
|
1994
|
+
result.styleBytes += countTextSpanStyleBytes(node);
|
|
1995
|
+
}
|
|
1996
|
+
const baseStyleForBytes = getEffectiveStyleForMode(
|
|
1512
1997
|
node,
|
|
1513
1998
|
cardStyles,
|
|
1514
|
-
|
|
1999
|
+
"default"
|
|
1515
2000
|
);
|
|
1516
|
-
if (
|
|
1517
|
-
result.
|
|
2001
|
+
if (baseStyleForBytes) {
|
|
2002
|
+
result.styleBytes += utf8ByteLength(JSON.stringify(baseStyleForBytes));
|
|
2003
|
+
}
|
|
2004
|
+
const compactStyleForBytes = getMergedCompactResponsiveStyle(
|
|
2005
|
+
node,
|
|
2006
|
+
cardStyles
|
|
2007
|
+
);
|
|
2008
|
+
if (compactStyleForBytes) {
|
|
2009
|
+
result.styleBytes += utf8ByteLength(JSON.stringify(compactStyleForBytes));
|
|
1518
2010
|
}
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
const children = node.children;
|
|
1522
|
-
if (Array.isArray(children)) {
|
|
1523
|
-
for (const child of children) {
|
|
1524
|
-
const childMetrics = countTemplateMetrics(child, cardStyles);
|
|
1525
|
-
result.nodes += childMetrics.nodes;
|
|
1526
|
-
result.textBytes += childMetrics.textBytes;
|
|
1527
|
-
result.styleBytes += childMetrics.styleBytes;
|
|
1528
2011
|
for (const mode of RESPONSIVE_MODES) {
|
|
1529
|
-
|
|
2012
|
+
const effectiveStyle = getEffectiveStyleForMode(node, cardStyles, mode);
|
|
2013
|
+
if (effectiveStyle?.overflow === "auto") {
|
|
2014
|
+
result.overflowAutoCount[mode]++;
|
|
2015
|
+
}
|
|
1530
2016
|
}
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
result.nodes += innerMetrics.nodes;
|
|
1536
|
-
result.textBytes += innerMetrics.textBytes;
|
|
1537
|
-
result.styleBytes += innerMetrics.styleBytes;
|
|
1538
|
-
for (const mode of RESPONSIVE_MODES) {
|
|
1539
|
-
result.overflowAutoCount[mode] += innerMetrics.overflowAutoCount[mode];
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
2017
|
+
},
|
|
2018
|
+
void 0,
|
|
2019
|
+
fragments
|
|
2020
|
+
);
|
|
1542
2021
|
return result;
|
|
1543
2022
|
}
|
|
1544
2023
|
function validateLimits(card) {
|
|
@@ -1553,10 +2032,8 @@ function validateLimits(card) {
|
|
|
1553
2032
|
traverseCard(card.views, (node, context) => {
|
|
1554
2033
|
nodeCount++;
|
|
1555
2034
|
if (node.type === "Text") {
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
textContentBytes += utf8ByteLength(content);
|
|
1559
|
-
}
|
|
2035
|
+
textContentBytes += countTextNodeLiteralBytes(node);
|
|
2036
|
+
styleObjectsBytes += countTextSpanStyleBytes(node);
|
|
1560
2037
|
}
|
|
1561
2038
|
{
|
|
1562
2039
|
const baseStyleForBytes = getEffectiveStyleForMode(
|
|
@@ -1611,7 +2088,7 @@ function validateLimits(card) {
|
|
|
1611
2088
|
)
|
|
1612
2089
|
);
|
|
1613
2090
|
} else if (source.length > 1) {
|
|
1614
|
-
const tplMetrics = countTemplateMetrics(forLoop.template, card.cardStyles);
|
|
2091
|
+
const tplMetrics = countTemplateMetrics(forLoop.template, card.cardStyles, card.fragments);
|
|
1615
2092
|
const multiplier = source.length - 1;
|
|
1616
2093
|
nodeCount += tplMetrics.nodes * multiplier;
|
|
1617
2094
|
textContentBytes += tplMetrics.textBytes * multiplier;
|
|
@@ -1647,7 +2124,7 @@ function validateLimits(card) {
|
|
|
1647
2124
|
)
|
|
1648
2125
|
);
|
|
1649
2126
|
}
|
|
1650
|
-
});
|
|
2127
|
+
}, void 0, card.fragments);
|
|
1651
2128
|
if (nodeCount > MAX_NODE_COUNT) {
|
|
1652
2129
|
errors.push(
|
|
1653
2130
|
createError(
|
|
@@ -1713,17 +2190,26 @@ function runAllChecks(input) {
|
|
|
1713
2190
|
const obj = input;
|
|
1714
2191
|
const views = obj.views;
|
|
1715
2192
|
const cardStyles = obj.styles;
|
|
2193
|
+
const fragments = obj.fragments;
|
|
1716
2194
|
const errors = [];
|
|
1717
|
-
errors.push(...
|
|
1718
|
-
errors.push(...
|
|
1719
|
-
errors.push(...
|
|
2195
|
+
errors.push(...validateFragments(views, fragments));
|
|
2196
|
+
errors.push(...validateNodes(views, fragments));
|
|
2197
|
+
errors.push(...validateConditions(views, fragments));
|
|
2198
|
+
errors.push(...validateValueTypes(views, fragments));
|
|
2199
|
+
errors.push(...validateStyles(views, cardStyles, fragments));
|
|
1720
2200
|
errors.push(...validateSecurity({
|
|
1721
2201
|
views,
|
|
1722
2202
|
state: obj.state,
|
|
1723
2203
|
cardAssets: obj.assets,
|
|
1724
|
-
cardStyles
|
|
2204
|
+
cardStyles,
|
|
2205
|
+
fragments
|
|
2206
|
+
}));
|
|
2207
|
+
errors.push(...validateLimits({
|
|
2208
|
+
state: obj.state,
|
|
2209
|
+
views,
|
|
2210
|
+
cardStyles,
|
|
2211
|
+
fragments
|
|
1725
2212
|
}));
|
|
1726
|
-
errors.push(...validateLimits({ state: obj.state, views, cardStyles }));
|
|
1727
2213
|
return errors;
|
|
1728
2214
|
}
|
|
1729
2215
|
function validate(input) {
|
|
@@ -1766,6 +2252,8 @@ export {
|
|
|
1766
2252
|
traverseNode,
|
|
1767
2253
|
validResult,
|
|
1768
2254
|
validate,
|
|
2255
|
+
validateConditions,
|
|
2256
|
+
validateFragments,
|
|
1769
2257
|
validateLimits,
|
|
1770
2258
|
validateNodes,
|
|
1771
2259
|
validateRaw,
|