@safe-ugc-ui/validator 0.6.0 → 1.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.
- package/dist/index.d.ts +34 -9
- package/dist/index.js +759 -181
- 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);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
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
|
+
);
|
|
165
299
|
}
|
|
166
300
|
}
|
|
167
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
|
}
|
|
@@ -285,8 +648,10 @@ var STRUCTURED_OBJECT_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
|
285
648
|
"borderLeft",
|
|
286
649
|
// Shadow
|
|
287
650
|
"boxShadow",
|
|
288
|
-
"textShadow"
|
|
651
|
+
"textShadow",
|
|
652
|
+
"clipPath"
|
|
289
653
|
]);
|
|
654
|
+
var RESPONSIVE_OVERRIDE_KEYS = ["medium", "compact"];
|
|
290
655
|
function validateNodeStyle(style, path, errors) {
|
|
291
656
|
if (!style) {
|
|
292
657
|
return;
|
|
@@ -317,19 +682,25 @@ function validateNodeStyle(style, path, errors) {
|
|
|
317
682
|
}
|
|
318
683
|
}
|
|
319
684
|
}
|
|
320
|
-
function validateValueTypes(views) {
|
|
685
|
+
function validateValueTypes(views, fragments) {
|
|
321
686
|
const errors = [];
|
|
322
|
-
|
|
323
|
-
|
|
687
|
+
walkRenderableCard(views, fragments, (node, ctx) => {
|
|
688
|
+
if (!("type" in node) || typeof node.type !== "string") {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const style = node.style != null && typeof node.style === "object" && !Array.isArray(node.style) ? node.style : void 0;
|
|
692
|
+
validateNodeStyle(style, `${ctx.path}.style`, errors);
|
|
324
693
|
const responsive = node.responsive;
|
|
325
694
|
if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
695
|
+
for (const mode of RESPONSIVE_OVERRIDE_KEYS) {
|
|
696
|
+
const override = responsive[mode];
|
|
697
|
+
if (override != null && typeof override === "object" && !Array.isArray(override)) {
|
|
698
|
+
validateNodeStyle(
|
|
699
|
+
override,
|
|
700
|
+
`${ctx.path}.responsive.${mode}`,
|
|
701
|
+
errors
|
|
702
|
+
);
|
|
703
|
+
}
|
|
333
704
|
}
|
|
334
705
|
}
|
|
335
706
|
});
|
|
@@ -338,6 +709,7 @@ function validateValueTypes(views) {
|
|
|
338
709
|
|
|
339
710
|
// src/style-validator.ts
|
|
340
711
|
import {
|
|
712
|
+
ASPECT_RATIO_PATTERN,
|
|
341
713
|
FORBIDDEN_STYLE_PROPERTIES,
|
|
342
714
|
DANGEROUS_CSS_FUNCTIONS,
|
|
343
715
|
ZINDEX_MIN,
|
|
@@ -358,12 +730,13 @@ import {
|
|
|
358
730
|
LETTER_SPACING_MAX,
|
|
359
731
|
OPACITY_MIN,
|
|
360
732
|
OPACITY_MAX,
|
|
733
|
+
BACKDROP_BLUR_MAX,
|
|
361
734
|
CSS_NAMED_COLORS,
|
|
362
735
|
TRANSITION_DURATION_MAX,
|
|
363
736
|
TRANSITION_DELAY_MAX,
|
|
364
737
|
TRANSITION_MAX_COUNT,
|
|
365
738
|
ALLOWED_TRANSITION_PROPERTIES,
|
|
366
|
-
isRef
|
|
739
|
+
isRef as isRef2
|
|
367
740
|
} from "@safe-ugc-ui/types";
|
|
368
741
|
var COLOR_PROPERTIES = /* @__PURE__ */ new Set(["backgroundColor", "color"]);
|
|
369
742
|
var LENGTH_PROPERTIES = /* @__PURE__ */ new Set([
|
|
@@ -403,6 +776,7 @@ var LENGTH_AUTO_ALLOWED = /* @__PURE__ */ new Set([
|
|
|
403
776
|
"marginBottom",
|
|
404
777
|
"marginLeft"
|
|
405
778
|
]);
|
|
779
|
+
var RESPONSIVE_OVERRIDE_KEYS2 = ["medium", "compact"];
|
|
406
780
|
var RANGE_LENGTH_PROPERTIES = {
|
|
407
781
|
fontSize: { min: FONT_SIZE_MIN, max: FONT_SIZE_MAX },
|
|
408
782
|
letterSpacing: { min: LETTER_SPACING_MIN, max: LETTER_SPACING_MAX },
|
|
@@ -419,7 +793,7 @@ function isLiteralString(value) {
|
|
|
419
793
|
return typeof value === "string";
|
|
420
794
|
}
|
|
421
795
|
function isDynamic(value) {
|
|
422
|
-
return
|
|
796
|
+
return isRef2(value);
|
|
423
797
|
}
|
|
424
798
|
function isValidColor(value) {
|
|
425
799
|
const lower = value.toLowerCase();
|
|
@@ -447,6 +821,22 @@ function parseLengthValue(value) {
|
|
|
447
821
|
}
|
|
448
822
|
return Number(match[1]);
|
|
449
823
|
}
|
|
824
|
+
function isValidAspectRatioLiteral(value) {
|
|
825
|
+
if (typeof value === "number") {
|
|
826
|
+
return Number.isFinite(value) && value > 0;
|
|
827
|
+
}
|
|
828
|
+
const match = value.match(ASPECT_RATIO_PATTERN);
|
|
829
|
+
if (!match) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
const parts = value.split("/");
|
|
833
|
+
if (parts.length !== 2) {
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
const width = Number(parts[0].trim());
|
|
837
|
+
const height = Number(parts[1].trim());
|
|
838
|
+
return Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0;
|
|
839
|
+
}
|
|
450
840
|
function collectDangerousCssErrors(value, path, errors) {
|
|
451
841
|
if (typeof value === "string") {
|
|
452
842
|
const lower = value.toLowerCase();
|
|
@@ -464,7 +854,7 @@ function collectDangerousCssErrors(value, path, errors) {
|
|
|
464
854
|
}
|
|
465
855
|
return;
|
|
466
856
|
}
|
|
467
|
-
if (
|
|
857
|
+
if (isRef2(value)) {
|
|
468
858
|
return;
|
|
469
859
|
}
|
|
470
860
|
if (Array.isArray(value)) {
|
|
@@ -528,6 +918,44 @@ function validateTextShadowObject(shadow, path, errors) {
|
|
|
528
918
|
);
|
|
529
919
|
}
|
|
530
920
|
}
|
|
921
|
+
function validateClipPathLength(value, path, errors) {
|
|
922
|
+
if (value == null || isDynamic(value)) {
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (typeof value === "number") {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (typeof value === "string" && isValidLength(value)) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
errors.push(
|
|
932
|
+
createError(
|
|
933
|
+
"INVALID_LENGTH",
|
|
934
|
+
`Invalid length "${String(value)}" at "${path}"`,
|
|
935
|
+
path
|
|
936
|
+
)
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
function validateClipPathObject(clipPath, path, errors) {
|
|
940
|
+
switch (clipPath.type) {
|
|
941
|
+
case "circle":
|
|
942
|
+
validateClipPathLength(clipPath.radius, `${path}.radius`, errors);
|
|
943
|
+
return;
|
|
944
|
+
case "ellipse":
|
|
945
|
+
validateClipPathLength(clipPath.rx, `${path}.rx`, errors);
|
|
946
|
+
validateClipPathLength(clipPath.ry, `${path}.ry`, errors);
|
|
947
|
+
return;
|
|
948
|
+
case "inset":
|
|
949
|
+
validateClipPathLength(clipPath.top, `${path}.top`, errors);
|
|
950
|
+
validateClipPathLength(clipPath.right, `${path}.right`, errors);
|
|
951
|
+
validateClipPathLength(clipPath.bottom, `${path}.bottom`, errors);
|
|
952
|
+
validateClipPathLength(clipPath.left, `${path}.left`, errors);
|
|
953
|
+
validateClipPathLength(clipPath.round, `${path}.round`, errors);
|
|
954
|
+
return;
|
|
955
|
+
default:
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
531
959
|
var STYLE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
532
960
|
function reportStyleRefError(rawRef, stylePath, cardStyles, errors) {
|
|
533
961
|
const trimmedRef = rawRef.trim();
|
|
@@ -662,6 +1090,18 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, options = {})
|
|
|
662
1090
|
);
|
|
663
1091
|
}
|
|
664
1092
|
}
|
|
1093
|
+
if ("backdropBlur" in style && isLiteralNumber(style.backdropBlur)) {
|
|
1094
|
+
const v = style.backdropBlur;
|
|
1095
|
+
if (v < 0 || v > BACKDROP_BLUR_MAX) {
|
|
1096
|
+
errors.push(
|
|
1097
|
+
createError(
|
|
1098
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
1099
|
+
`backdropBlur (${v}) must be between 0 and ${BACKDROP_BLUR_MAX} at "${stylePath}.backdropBlur"`,
|
|
1100
|
+
`${stylePath}.backdropBlur`
|
|
1101
|
+
)
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
665
1105
|
if ("letterSpacing" in style && isLiteralNumber(style.letterSpacing)) {
|
|
666
1106
|
const v = style.letterSpacing;
|
|
667
1107
|
if (v < LETTER_SPACING_MIN || v > LETTER_SPACING_MAX) {
|
|
@@ -854,6 +1294,25 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, options = {})
|
|
|
854
1294
|
}
|
|
855
1295
|
}
|
|
856
1296
|
}
|
|
1297
|
+
if ("aspectRatio" in style && style.aspectRatio != null && !isDynamic(style.aspectRatio)) {
|
|
1298
|
+
const aspectRatio = style.aspectRatio;
|
|
1299
|
+
if (typeof aspectRatio !== "number" && typeof aspectRatio !== "string" || !isValidAspectRatioLiteral(aspectRatio)) {
|
|
1300
|
+
errors.push(
|
|
1301
|
+
createError(
|
|
1302
|
+
"INVALID_VALUE",
|
|
1303
|
+
`Invalid aspectRatio "${String(aspectRatio)}" at "${stylePath}.aspectRatio"`,
|
|
1304
|
+
`${stylePath}.aspectRatio`
|
|
1305
|
+
)
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if ("clipPath" in style && style.clipPath != null && typeof style.clipPath === "object" && !Array.isArray(style.clipPath) && !isDynamic(style.clipPath)) {
|
|
1310
|
+
validateClipPathObject(
|
|
1311
|
+
style.clipPath,
|
|
1312
|
+
`${stylePath}.clipPath`,
|
|
1313
|
+
errors
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
857
1316
|
for (const [prop, range] of Object.entries(RANGE_LENGTH_PROPERTIES)) {
|
|
858
1317
|
if (prop in style && isLiteralString(style[prop]) && !isDynamic(style[prop])) {
|
|
859
1318
|
const numericValue = parseLengthValue(style[prop]);
|
|
@@ -1016,7 +1475,7 @@ function mergeStyleWithRef(cardStyleEntry, inlineStyle) {
|
|
|
1016
1475
|
}
|
|
1017
1476
|
return merged;
|
|
1018
1477
|
}
|
|
1019
|
-
function validateStyles(views, cardStyles) {
|
|
1478
|
+
function validateStyles(views, cardStyles, fragments) {
|
|
1020
1479
|
const errors = [];
|
|
1021
1480
|
if (cardStyles) {
|
|
1022
1481
|
for (const [styleName, styleEntry] of Object.entries(cardStyles)) {
|
|
@@ -1036,8 +1495,11 @@ function validateStyles(views, cardStyles) {
|
|
|
1036
1495
|
});
|
|
1037
1496
|
}
|
|
1038
1497
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1498
|
+
walkRenderableCard(views, fragments, (node, ctx) => {
|
|
1499
|
+
if (!("type" in node) || typeof node.type !== "string") {
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
const style = node.style != null && typeof node.style === "object" && !Array.isArray(node.style) ? node.style : void 0;
|
|
1041
1503
|
if (style != null && typeof style === "object") {
|
|
1042
1504
|
const stylePath = `${ctx.path}.style`;
|
|
1043
1505
|
const mergedStyle = resolveStyleRef(style, stylePath, cardStyles, errors);
|
|
@@ -1046,26 +1508,50 @@ function validateStyles(views, cardStyles) {
|
|
|
1046
1508
|
}
|
|
1047
1509
|
}
|
|
1048
1510
|
const responsive = node.responsive;
|
|
1049
|
-
if (responsive
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1511
|
+
if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
|
|
1512
|
+
for (const mode of RESPONSIVE_OVERRIDE_KEYS2) {
|
|
1513
|
+
const override = responsive[mode];
|
|
1514
|
+
if (override != null && typeof override === "object" && !Array.isArray(override)) {
|
|
1515
|
+
const overridePath = `${ctx.path}.responsive.${mode}`;
|
|
1516
|
+
const mergedOverride = resolveStyleRef(
|
|
1517
|
+
override,
|
|
1518
|
+
overridePath,
|
|
1519
|
+
cardStyles,
|
|
1520
|
+
errors
|
|
1521
|
+
);
|
|
1522
|
+
if (mergedOverride) {
|
|
1523
|
+
validateSingleStyle(mergedOverride, overridePath, errors, cardStyles, {
|
|
1524
|
+
allowHoverStyle: false,
|
|
1525
|
+
allowTransition: false,
|
|
1526
|
+
allowHoverStyleRefs: false
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1055
1531
|
}
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1532
|
+
const spans = Array.isArray(node.spans) ? node.spans : void 0;
|
|
1533
|
+
if (node.type === "Text" && spans) {
|
|
1534
|
+
for (let i = 0; i < spans.length; i++) {
|
|
1535
|
+
const span = spans[i];
|
|
1536
|
+
if (span == null || typeof span !== "object" || Array.isArray(span)) {
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
const spanStyle = span.style;
|
|
1540
|
+
if (spanStyle == null || typeof spanStyle !== "object" || Array.isArray(spanStyle)) {
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
validateSingleStyle(
|
|
1544
|
+
spanStyle,
|
|
1545
|
+
`${ctx.path}.spans[${i}].style`,
|
|
1546
|
+
errors,
|
|
1547
|
+
void 0,
|
|
1548
|
+
{
|
|
1549
|
+
allowHoverStyle: false,
|
|
1550
|
+
allowTransition: false,
|
|
1551
|
+
allowHoverStyleRefs: false
|
|
1552
|
+
}
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1069
1555
|
}
|
|
1070
1556
|
});
|
|
1071
1557
|
return errors;
|
|
@@ -1074,12 +1560,13 @@ function validateStyles(views, cardStyles) {
|
|
|
1074
1560
|
// src/security.ts
|
|
1075
1561
|
import {
|
|
1076
1562
|
PROTOTYPE_POLLUTION_SEGMENTS,
|
|
1077
|
-
isRef as
|
|
1563
|
+
isRef as isRef3
|
|
1078
1564
|
} from "@safe-ugc-ui/types";
|
|
1079
1565
|
|
|
1080
1566
|
// src/responsive-utils.ts
|
|
1081
1567
|
var RESPONSIVE_MODES = [
|
|
1082
1568
|
"default",
|
|
1569
|
+
"medium",
|
|
1083
1570
|
"compact"
|
|
1084
1571
|
];
|
|
1085
1572
|
function mergeNamedStyle(style, cardStyles) {
|
|
@@ -1101,16 +1588,16 @@ function mergeNamedStyle(style, cardStyles) {
|
|
|
1101
1588
|
const { $style: _, ...inlineWithoutStyleRef } = style;
|
|
1102
1589
|
return { ...baseStyle, ...inlineWithoutStyleRef };
|
|
1103
1590
|
}
|
|
1104
|
-
function
|
|
1591
|
+
function getResponsiveStyle(node, mode) {
|
|
1105
1592
|
const responsive = node.responsive;
|
|
1106
1593
|
if (responsive == null || typeof responsive !== "object" || Array.isArray(responsive)) {
|
|
1107
1594
|
return void 0;
|
|
1108
1595
|
}
|
|
1109
|
-
const
|
|
1110
|
-
if (
|
|
1596
|
+
const override = responsive[mode];
|
|
1597
|
+
if (override == null || typeof override !== "object" || Array.isArray(override)) {
|
|
1111
1598
|
return void 0;
|
|
1112
1599
|
}
|
|
1113
|
-
return
|
|
1600
|
+
return override;
|
|
1114
1601
|
}
|
|
1115
1602
|
function stripResponsiveOnlyUnsupportedFields(style) {
|
|
1116
1603
|
if (!style) return void 0;
|
|
@@ -1126,22 +1613,36 @@ function getEffectiveStyleForMode(node, cardStyles, mode) {
|
|
|
1126
1613
|
if (mode === "default") {
|
|
1127
1614
|
return baseStyle;
|
|
1128
1615
|
}
|
|
1616
|
+
const mediumStyle = stripResponsiveOnlyUnsupportedFields(
|
|
1617
|
+
mergeNamedStyle(getResponsiveStyle(node, "medium"), cardStyles)
|
|
1618
|
+
);
|
|
1619
|
+
if (mode === "medium") {
|
|
1620
|
+
if (!mediumStyle) {
|
|
1621
|
+
return baseStyle;
|
|
1622
|
+
}
|
|
1623
|
+
return {
|
|
1624
|
+
...baseStyle ?? {},
|
|
1625
|
+
...mediumStyle
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1129
1628
|
const compactStyle = stripResponsiveOnlyUnsupportedFields(
|
|
1130
|
-
mergeNamedStyle(
|
|
1629
|
+
mergeNamedStyle(getResponsiveStyle(node, "compact"), cardStyles)
|
|
1131
1630
|
);
|
|
1132
|
-
if (!compactStyle) {
|
|
1631
|
+
if (!mediumStyle && !compactStyle) {
|
|
1133
1632
|
return baseStyle;
|
|
1134
1633
|
}
|
|
1135
1634
|
return {
|
|
1136
1635
|
...baseStyle ?? {},
|
|
1636
|
+
...mediumStyle ?? {},
|
|
1137
1637
|
...compactStyle
|
|
1138
1638
|
};
|
|
1139
1639
|
}
|
|
1140
|
-
function
|
|
1141
|
-
return mergeNamedStyle(
|
|
1640
|
+
function getMergedResponsiveStyleOverride(node, cardStyles, mode) {
|
|
1641
|
+
return mergeNamedStyle(getResponsiveStyle(node, mode), cardStyles);
|
|
1142
1642
|
}
|
|
1143
1643
|
|
|
1144
1644
|
// src/security.ts
|
|
1645
|
+
var RESPONSIVE_OVERRIDE_KEYS3 = ["medium", "compact"];
|
|
1145
1646
|
var FORBIDDEN_URL_PREFIXES = [
|
|
1146
1647
|
"http://",
|
|
1147
1648
|
"https://",
|
|
@@ -1153,7 +1654,7 @@ function scanForRefs(obj, path, errors) {
|
|
|
1153
1654
|
if (obj === null || obj === void 0 || typeof obj !== "object") {
|
|
1154
1655
|
return;
|
|
1155
1656
|
}
|
|
1156
|
-
if (
|
|
1657
|
+
if (isRef3(obj)) {
|
|
1157
1658
|
const refStr = obj.$ref;
|
|
1158
1659
|
const segments = refStr.split(/[.\[]/);
|
|
1159
1660
|
for (const segment of segments) {
|
|
@@ -1266,6 +1767,23 @@ function pushUniqueError(errors, seen, error) {
|
|
|
1266
1767
|
seen.add(key);
|
|
1267
1768
|
errors.push(error);
|
|
1268
1769
|
}
|
|
1770
|
+
function getScannableNodeFields(node) {
|
|
1771
|
+
const nodeFields = { ...node };
|
|
1772
|
+
delete nodeFields.type;
|
|
1773
|
+
delete nodeFields.style;
|
|
1774
|
+
delete nodeFields.children;
|
|
1775
|
+
const interactiveField = node.type === "Accordion" ? "items" : node.type === "Tabs" ? "tabs" : null;
|
|
1776
|
+
if (interactiveField && Array.isArray(node[interactiveField])) {
|
|
1777
|
+
nodeFields[interactiveField] = node[interactiveField].map((item) => {
|
|
1778
|
+
if (item == null || typeof item !== "object" || Array.isArray(item)) {
|
|
1779
|
+
return item;
|
|
1780
|
+
}
|
|
1781
|
+
const { content: _content, ...rest } = item;
|
|
1782
|
+
return rest;
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
return nodeFields;
|
|
1786
|
+
}
|
|
1269
1787
|
function scanStyleStringsForUrl(style, path, errors) {
|
|
1270
1788
|
if (!style) {
|
|
1271
1789
|
return;
|
|
@@ -1282,7 +1800,7 @@ function scanStyleStringsForUrl(style, path, errors) {
|
|
|
1282
1800
|
}
|
|
1283
1801
|
}
|
|
1284
1802
|
}
|
|
1285
|
-
function validateEffectiveStylesForMode(mode, views, cardStyles, errors, seen) {
|
|
1803
|
+
function validateEffectiveStylesForMode(mode, views, cardStyles, fragments, errors, seen) {
|
|
1286
1804
|
const styleResolver = (node) => getEffectiveStyleForMode(node, cardStyles, mode);
|
|
1287
1805
|
traverseCard(views, (node, context) => {
|
|
1288
1806
|
const effectiveStyle = styleResolver(node);
|
|
@@ -1331,12 +1849,12 @@ function validateEffectiveStylesForMode(mode, views, cardStyles, errors, seen) {
|
|
|
1331
1849
|
)
|
|
1332
1850
|
);
|
|
1333
1851
|
}
|
|
1334
|
-
}, styleResolver);
|
|
1852
|
+
}, styleResolver, fragments);
|
|
1335
1853
|
}
|
|
1336
1854
|
function validateSecurity(card) {
|
|
1337
1855
|
const errors = [];
|
|
1338
1856
|
const seen = /* @__PURE__ */ new Set();
|
|
1339
|
-
const { views, state, cardAssets, cardStyles } = card;
|
|
1857
|
+
const { views, state, cardAssets, cardStyles, fragments } = card;
|
|
1340
1858
|
if (cardAssets) {
|
|
1341
1859
|
for (const [key, value] of Object.entries(cardAssets)) {
|
|
1342
1860
|
const assetError = validateAssetPath(value);
|
|
@@ -1359,18 +1877,20 @@ function validateSecurity(card) {
|
|
|
1359
1877
|
}
|
|
1360
1878
|
}
|
|
1361
1879
|
}
|
|
1362
|
-
|
|
1880
|
+
walkRenderableCard(views, fragments, (node, context) => {
|
|
1881
|
+
if (!("type" in node) || typeof node.type !== "string") {
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const traversableNode = node;
|
|
1363
1885
|
const { path } = context;
|
|
1364
|
-
const
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1367
|
-
delete nodeFields.style;
|
|
1368
|
-
delete nodeFields.children;
|
|
1886
|
+
const style = traversableNode.style != null && typeof traversableNode.style === "object" && !Array.isArray(traversableNode.style) ? traversableNode.style : void 0;
|
|
1887
|
+
const type = traversableNode.type;
|
|
1888
|
+
const nodeFields = getScannableNodeFields(traversableNode);
|
|
1369
1889
|
if (type === "Image" || type === "Avatar") {
|
|
1370
1890
|
const src = node.src;
|
|
1371
1891
|
if (typeof src === "string") {
|
|
1372
1892
|
checkSrcValue(src, type, `${path}.src`, errors);
|
|
1373
|
-
} else if (
|
|
1893
|
+
} else if (isRef3(src)) {
|
|
1374
1894
|
if (state) {
|
|
1375
1895
|
const resolved = resolveRefFromState(
|
|
1376
1896
|
src.$ref,
|
|
@@ -1383,27 +1903,46 @@ function validateSecurity(card) {
|
|
|
1383
1903
|
}
|
|
1384
1904
|
}
|
|
1385
1905
|
scanStyleStringsForUrl(style, `${path}.style`, errors);
|
|
1386
|
-
const responsive =
|
|
1906
|
+
const responsive = traversableNode.responsive;
|
|
1387
1907
|
if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
|
|
1388
|
-
const
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1908
|
+
for (const mode of RESPONSIVE_OVERRIDE_KEYS3) {
|
|
1909
|
+
const override = responsive[mode];
|
|
1910
|
+
if (override != null && typeof override === "object" && !Array.isArray(override)) {
|
|
1911
|
+
scanStyleStringsForUrl(
|
|
1912
|
+
override,
|
|
1913
|
+
`${path}.responsive.${mode}`,
|
|
1914
|
+
errors
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
if (type === "Text" && Array.isArray(traversableNode.spans)) {
|
|
1920
|
+
const spans = traversableNode.spans;
|
|
1921
|
+
for (let i = 0; i < spans.length; i++) {
|
|
1922
|
+
const span = spans[i];
|
|
1923
|
+
if (span == null || typeof span !== "object" || Array.isArray(span)) {
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
const spanStyle = span.style;
|
|
1927
|
+
if (spanStyle != null && typeof spanStyle === "object" && !Array.isArray(spanStyle)) {
|
|
1928
|
+
scanStyleStringsForUrl(
|
|
1929
|
+
spanStyle,
|
|
1930
|
+
`${path}.spans[${i}].style`,
|
|
1931
|
+
errors
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1395
1934
|
}
|
|
1396
1935
|
}
|
|
1397
1936
|
scanForRefs(nodeFields, path, errors);
|
|
1398
1937
|
if (style) {
|
|
1399
1938
|
scanForRefs(style, `${path}.style`, errors);
|
|
1400
1939
|
}
|
|
1401
|
-
if (
|
|
1402
|
-
scanForRefs(
|
|
1940
|
+
if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
|
|
1941
|
+
scanForRefs(responsive, `${path}.responsive`, errors);
|
|
1403
1942
|
}
|
|
1404
1943
|
});
|
|
1405
1944
|
for (const mode of RESPONSIVE_MODES) {
|
|
1406
|
-
validateEffectiveStylesForMode(mode, views, cardStyles, errors, seen);
|
|
1945
|
+
validateEffectiveStylesForMode(mode, views, cardStyles, fragments, errors, seen);
|
|
1407
1946
|
}
|
|
1408
1947
|
return errors;
|
|
1409
1948
|
}
|
|
@@ -1417,8 +1956,7 @@ import {
|
|
|
1417
1956
|
MAX_NESTED_LOOPS,
|
|
1418
1957
|
MAX_OVERFLOW_AUTO_COUNT,
|
|
1419
1958
|
MAX_STACK_NESTING,
|
|
1420
|
-
PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2
|
|
1421
|
-
isRef as isRef3
|
|
1959
|
+
PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2
|
|
1422
1960
|
} from "@safe-ugc-ui/types";
|
|
1423
1961
|
function utf8ByteLength(str) {
|
|
1424
1962
|
let bytes = 0;
|
|
@@ -1437,6 +1975,54 @@ function utf8ByteLength(str) {
|
|
|
1437
1975
|
}
|
|
1438
1976
|
return bytes;
|
|
1439
1977
|
}
|
|
1978
|
+
function isTemplateObject(value) {
|
|
1979
|
+
return typeof value === "object" && value !== null && "$template" in value && Array.isArray(value.$template);
|
|
1980
|
+
}
|
|
1981
|
+
function countLiteralTemplatedStringBytes(value) {
|
|
1982
|
+
if (typeof value === "string") {
|
|
1983
|
+
return utf8ByteLength(value);
|
|
1984
|
+
}
|
|
1985
|
+
if (!isTemplateObject(value)) {
|
|
1986
|
+
return 0;
|
|
1987
|
+
}
|
|
1988
|
+
return value.$template.reduce((total, part) => {
|
|
1989
|
+
if (typeof part === "string") {
|
|
1990
|
+
return total + utf8ByteLength(part);
|
|
1991
|
+
}
|
|
1992
|
+
if (typeof part === "number" || typeof part === "boolean" || part === null) {
|
|
1993
|
+
return total + utf8ByteLength(String(part));
|
|
1994
|
+
}
|
|
1995
|
+
return total;
|
|
1996
|
+
}, 0);
|
|
1997
|
+
}
|
|
1998
|
+
function countTextNodeLiteralBytes(node) {
|
|
1999
|
+
if (Array.isArray(node.spans)) {
|
|
2000
|
+
return node.spans.reduce((total, span) => {
|
|
2001
|
+
if (span == null || typeof span !== "object" || Array.isArray(span)) {
|
|
2002
|
+
return total;
|
|
2003
|
+
}
|
|
2004
|
+
return total + countLiteralTemplatedStringBytes(
|
|
2005
|
+
span.text
|
|
2006
|
+
);
|
|
2007
|
+
}, 0);
|
|
2008
|
+
}
|
|
2009
|
+
return countLiteralTemplatedStringBytes(node.content);
|
|
2010
|
+
}
|
|
2011
|
+
function countTextSpanStyleBytes(node) {
|
|
2012
|
+
if (!Array.isArray(node.spans)) {
|
|
2013
|
+
return 0;
|
|
2014
|
+
}
|
|
2015
|
+
return node.spans.reduce((total, span) => {
|
|
2016
|
+
if (span == null || typeof span !== "object" || Array.isArray(span)) {
|
|
2017
|
+
return total;
|
|
2018
|
+
}
|
|
2019
|
+
const style = span.style;
|
|
2020
|
+
if (style == null || typeof style !== "object" || Array.isArray(style)) {
|
|
2021
|
+
return total;
|
|
2022
|
+
}
|
|
2023
|
+
return total + utf8ByteLength(JSON.stringify(style));
|
|
2024
|
+
}, 0);
|
|
2025
|
+
}
|
|
1440
2026
|
function resolveRefFromState2(refPath, state) {
|
|
1441
2027
|
const path = refPath.startsWith("$") ? refPath.slice(1) : refPath;
|
|
1442
2028
|
const dotSegments = path.split(".");
|
|
@@ -1471,74 +2057,53 @@ function resolveRefFromState2(refPath, state) {
|
|
|
1471
2057
|
}
|
|
1472
2058
|
return current;
|
|
1473
2059
|
}
|
|
1474
|
-
function countTemplateMetrics(template, cardStyles) {
|
|
2060
|
+
function countTemplateMetrics(template, cardStyles, fragments) {
|
|
1475
2061
|
const result = {
|
|
1476
2062
|
nodes: 0,
|
|
1477
2063
|
textBytes: 0,
|
|
1478
2064
|
styleBytes: 0,
|
|
1479
2065
|
overflowAutoCount: {
|
|
1480
2066
|
default: 0,
|
|
2067
|
+
medium: 0,
|
|
1481
2068
|
compact: 0
|
|
1482
2069
|
}
|
|
1483
2070
|
};
|
|
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(
|
|
2071
|
+
traverseCard(
|
|
2072
|
+
{ __template: template },
|
|
2073
|
+
(node) => {
|
|
2074
|
+
result.nodes += 1;
|
|
2075
|
+
if (node.type === "Text") {
|
|
2076
|
+
result.textBytes += countTextNodeLiteralBytes(node);
|
|
2077
|
+
result.styleBytes += countTextSpanStyleBytes(node);
|
|
2078
|
+
}
|
|
2079
|
+
const baseStyleForBytes = getEffectiveStyleForMode(
|
|
1512
2080
|
node,
|
|
1513
2081
|
cardStyles,
|
|
1514
|
-
|
|
2082
|
+
"default"
|
|
1515
2083
|
);
|
|
1516
|
-
if (
|
|
1517
|
-
result.
|
|
2084
|
+
if (baseStyleForBytes) {
|
|
2085
|
+
result.styleBytes += utf8ByteLength(JSON.stringify(baseStyleForBytes));
|
|
2086
|
+
}
|
|
2087
|
+
for (const mode of ["medium", "compact"]) {
|
|
2088
|
+
const responsiveStyleForBytes = getMergedResponsiveStyleOverride(
|
|
2089
|
+
node,
|
|
2090
|
+
cardStyles,
|
|
2091
|
+
mode
|
|
2092
|
+
);
|
|
2093
|
+
if (responsiveStyleForBytes) {
|
|
2094
|
+
result.styleBytes += utf8ByteLength(JSON.stringify(responsiveStyleForBytes));
|
|
2095
|
+
}
|
|
1518
2096
|
}
|
|
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
2097
|
for (const mode of RESPONSIVE_MODES) {
|
|
1529
|
-
|
|
2098
|
+
const effectiveStyle = getEffectiveStyleForMode(node, cardStyles, mode);
|
|
2099
|
+
if (effectiveStyle?.overflow === "auto") {
|
|
2100
|
+
result.overflowAutoCount[mode]++;
|
|
2101
|
+
}
|
|
1530
2102
|
}
|
|
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
|
-
}
|
|
2103
|
+
},
|
|
2104
|
+
void 0,
|
|
2105
|
+
fragments
|
|
2106
|
+
);
|
|
1542
2107
|
return result;
|
|
1543
2108
|
}
|
|
1544
2109
|
function validateLimits(card) {
|
|
@@ -1548,15 +2113,14 @@ function validateLimits(card) {
|
|
|
1548
2113
|
let styleObjectsBytes = 0;
|
|
1549
2114
|
const overflowAutoCount = {
|
|
1550
2115
|
default: 0,
|
|
2116
|
+
medium: 0,
|
|
1551
2117
|
compact: 0
|
|
1552
2118
|
};
|
|
1553
2119
|
traverseCard(card.views, (node, context) => {
|
|
1554
2120
|
nodeCount++;
|
|
1555
2121
|
if (node.type === "Text") {
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
textContentBytes += utf8ByteLength(content);
|
|
1559
|
-
}
|
|
2122
|
+
textContentBytes += countTextNodeLiteralBytes(node);
|
|
2123
|
+
styleObjectsBytes += countTextSpanStyleBytes(node);
|
|
1560
2124
|
}
|
|
1561
2125
|
{
|
|
1562
2126
|
const baseStyleForBytes = getEffectiveStyleForMode(
|
|
@@ -1567,12 +2131,15 @@ function validateLimits(card) {
|
|
|
1567
2131
|
if (baseStyleForBytes) {
|
|
1568
2132
|
styleObjectsBytes += utf8ByteLength(JSON.stringify(baseStyleForBytes));
|
|
1569
2133
|
}
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
2134
|
+
for (const mode of ["medium", "compact"]) {
|
|
2135
|
+
const responsiveStyleForBytes = getMergedResponsiveStyleOverride(
|
|
2136
|
+
node,
|
|
2137
|
+
card.cardStyles,
|
|
2138
|
+
mode
|
|
2139
|
+
);
|
|
2140
|
+
if (responsiveStyleForBytes) {
|
|
2141
|
+
styleObjectsBytes += utf8ByteLength(JSON.stringify(responsiveStyleForBytes));
|
|
2142
|
+
}
|
|
1576
2143
|
}
|
|
1577
2144
|
}
|
|
1578
2145
|
const children = node.children;
|
|
@@ -1611,7 +2178,7 @@ function validateLimits(card) {
|
|
|
1611
2178
|
)
|
|
1612
2179
|
);
|
|
1613
2180
|
} else if (source.length > 1) {
|
|
1614
|
-
const tplMetrics = countTemplateMetrics(forLoop.template, card.cardStyles);
|
|
2181
|
+
const tplMetrics = countTemplateMetrics(forLoop.template, card.cardStyles, card.fragments);
|
|
1615
2182
|
const multiplier = source.length - 1;
|
|
1616
2183
|
nodeCount += tplMetrics.nodes * multiplier;
|
|
1617
2184
|
textContentBytes += tplMetrics.textBytes * multiplier;
|
|
@@ -1647,7 +2214,7 @@ function validateLimits(card) {
|
|
|
1647
2214
|
)
|
|
1648
2215
|
);
|
|
1649
2216
|
}
|
|
1650
|
-
});
|
|
2217
|
+
}, void 0, card.fragments);
|
|
1651
2218
|
if (nodeCount > MAX_NODE_COUNT) {
|
|
1652
2219
|
errors.push(
|
|
1653
2220
|
createError(
|
|
@@ -1713,17 +2280,26 @@ function runAllChecks(input) {
|
|
|
1713
2280
|
const obj = input;
|
|
1714
2281
|
const views = obj.views;
|
|
1715
2282
|
const cardStyles = obj.styles;
|
|
2283
|
+
const fragments = obj.fragments;
|
|
1716
2284
|
const errors = [];
|
|
1717
|
-
errors.push(...
|
|
1718
|
-
errors.push(...
|
|
1719
|
-
errors.push(...
|
|
2285
|
+
errors.push(...validateFragments(views, fragments));
|
|
2286
|
+
errors.push(...validateNodes(views, fragments));
|
|
2287
|
+
errors.push(...validateConditions(views, fragments));
|
|
2288
|
+
errors.push(...validateValueTypes(views, fragments));
|
|
2289
|
+
errors.push(...validateStyles(views, cardStyles, fragments));
|
|
1720
2290
|
errors.push(...validateSecurity({
|
|
1721
2291
|
views,
|
|
1722
2292
|
state: obj.state,
|
|
1723
2293
|
cardAssets: obj.assets,
|
|
1724
|
-
cardStyles
|
|
2294
|
+
cardStyles,
|
|
2295
|
+
fragments
|
|
2296
|
+
}));
|
|
2297
|
+
errors.push(...validateLimits({
|
|
2298
|
+
state: obj.state,
|
|
2299
|
+
views,
|
|
2300
|
+
cardStyles,
|
|
2301
|
+
fragments
|
|
1725
2302
|
}));
|
|
1726
|
-
errors.push(...validateLimits({ state: obj.state, views, cardStyles }));
|
|
1727
2303
|
return errors;
|
|
1728
2304
|
}
|
|
1729
2305
|
function validate(input) {
|
|
@@ -1766,6 +2342,8 @@ export {
|
|
|
1766
2342
|
traverseNode,
|
|
1767
2343
|
validResult,
|
|
1768
2344
|
validate,
|
|
2345
|
+
validateConditions,
|
|
2346
|
+
validateFragments,
|
|
1769
2347
|
validateLimits,
|
|
1770
2348
|
validateNodes,
|
|
1771
2349
|
validateRaw,
|