@safe-ugc-ui/validator 0.5.1 → 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 +36 -9
- package/dist/index.js +890 -222
- 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) {
|
|
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(
|
|
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);
|
|
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) {
|
|
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);
|
|
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
|
}
|
|
@@ -287,8 +650,7 @@ var STRUCTURED_OBJECT_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
|
287
650
|
"boxShadow",
|
|
288
651
|
"textShadow"
|
|
289
652
|
]);
|
|
290
|
-
function validateNodeStyle(
|
|
291
|
-
const style = node.style;
|
|
653
|
+
function validateNodeStyle(style, path, errors) {
|
|
292
654
|
if (!style) {
|
|
293
655
|
return;
|
|
294
656
|
}
|
|
@@ -302,7 +664,7 @@ function validateNodeStyle(node, ctx, errors) {
|
|
|
302
664
|
createError(
|
|
303
665
|
"DYNAMIC_NOT_ALLOWED",
|
|
304
666
|
`Style property "${prop}" must be a static literal; $ref is not allowed.`,
|
|
305
|
-
`${
|
|
667
|
+
`${path}.${prop}`
|
|
306
668
|
)
|
|
307
669
|
);
|
|
308
670
|
}
|
|
@@ -311,23 +673,39 @@ function validateNodeStyle(node, ctx, errors) {
|
|
|
311
673
|
createError(
|
|
312
674
|
"DYNAMIC_NOT_ALLOWED",
|
|
313
675
|
`Style property "${prop}" must be an object literal; use $ref only inside its nested fields.`,
|
|
314
|
-
`${
|
|
676
|
+
`${path}.${prop}`
|
|
315
677
|
)
|
|
316
678
|
);
|
|
317
679
|
}
|
|
318
680
|
}
|
|
319
681
|
}
|
|
320
682
|
}
|
|
321
|
-
function validateValueTypes(views) {
|
|
683
|
+
function validateValueTypes(views, fragments) {
|
|
322
684
|
const errors = [];
|
|
323
|
-
|
|
324
|
-
|
|
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);
|
|
691
|
+
const responsive = node.responsive;
|
|
692
|
+
if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
|
|
693
|
+
const compact = responsive.compact;
|
|
694
|
+
if (compact != null && typeof compact === "object" && !Array.isArray(compact)) {
|
|
695
|
+
validateNodeStyle(
|
|
696
|
+
compact,
|
|
697
|
+
`${ctx.path}.responsive.compact`,
|
|
698
|
+
errors
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
325
702
|
});
|
|
326
703
|
return errors;
|
|
327
704
|
}
|
|
328
705
|
|
|
329
706
|
// src/style-validator.ts
|
|
330
707
|
import {
|
|
708
|
+
ASPECT_RATIO_PATTERN,
|
|
331
709
|
FORBIDDEN_STYLE_PROPERTIES,
|
|
332
710
|
DANGEROUS_CSS_FUNCTIONS,
|
|
333
711
|
ZINDEX_MIN,
|
|
@@ -353,7 +731,7 @@ import {
|
|
|
353
731
|
TRANSITION_DELAY_MAX,
|
|
354
732
|
TRANSITION_MAX_COUNT,
|
|
355
733
|
ALLOWED_TRANSITION_PROPERTIES,
|
|
356
|
-
isRef
|
|
734
|
+
isRef as isRef2
|
|
357
735
|
} from "@safe-ugc-ui/types";
|
|
358
736
|
var COLOR_PROPERTIES = /* @__PURE__ */ new Set(["backgroundColor", "color"]);
|
|
359
737
|
var LENGTH_PROPERTIES = /* @__PURE__ */ new Set([
|
|
@@ -409,7 +787,7 @@ function isLiteralString(value) {
|
|
|
409
787
|
return typeof value === "string";
|
|
410
788
|
}
|
|
411
789
|
function isDynamic(value) {
|
|
412
|
-
return
|
|
790
|
+
return isRef2(value);
|
|
413
791
|
}
|
|
414
792
|
function isValidColor(value) {
|
|
415
793
|
const lower = value.toLowerCase();
|
|
@@ -437,6 +815,22 @@ function parseLengthValue(value) {
|
|
|
437
815
|
}
|
|
438
816
|
return Number(match[1]);
|
|
439
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
|
+
}
|
|
440
834
|
function collectDangerousCssErrors(value, path, errors) {
|
|
441
835
|
if (typeof value === "string") {
|
|
442
836
|
const lower = value.toLowerCase();
|
|
@@ -454,7 +848,7 @@ function collectDangerousCssErrors(value, path, errors) {
|
|
|
454
848
|
}
|
|
455
849
|
return;
|
|
456
850
|
}
|
|
457
|
-
if (
|
|
851
|
+
if (isRef2(value)) {
|
|
458
852
|
return;
|
|
459
853
|
}
|
|
460
854
|
if (Array.isArray(value)) {
|
|
@@ -578,8 +972,15 @@ function collectNestedStyleRefErrors(value, path, errors) {
|
|
|
578
972
|
collectNestedStyleRefErrors(child, childPath, errors);
|
|
579
973
|
}
|
|
580
974
|
}
|
|
581
|
-
function validateSingleStyle(style, stylePath, errors, cardStyles,
|
|
582
|
-
const
|
|
975
|
+
function validateSingleStyle(style, stylePath, errors, cardStyles, options = {}) {
|
|
976
|
+
const {
|
|
977
|
+
allowHoverStyle = true,
|
|
978
|
+
allowTransition = true,
|
|
979
|
+
allowHoverStyleRefs = true
|
|
980
|
+
} = options;
|
|
981
|
+
const STRUCTURED_FIELDS = /* @__PURE__ */ new Set();
|
|
982
|
+
if (allowTransition) STRUCTURED_FIELDS.add("transition");
|
|
983
|
+
if (allowHoverStyle) STRUCTURED_FIELDS.add("hoverStyle");
|
|
583
984
|
for (const key of Object.keys(style)) {
|
|
584
985
|
if (!STRUCTURED_FIELDS.has(key) && FORBIDDEN_STYLE_PROPERTIES.includes(key)) {
|
|
585
986
|
errors.push(
|
|
@@ -591,6 +992,24 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverSty
|
|
|
591
992
|
);
|
|
592
993
|
}
|
|
593
994
|
}
|
|
995
|
+
if (!allowHoverStyle && "hoverStyle" in style) {
|
|
996
|
+
errors.push(
|
|
997
|
+
createError(
|
|
998
|
+
"INVALID_VALUE",
|
|
999
|
+
`hoverStyle is not allowed inside responsive overrides at "${stylePath}.hoverStyle"`,
|
|
1000
|
+
`${stylePath}.hoverStyle`
|
|
1001
|
+
)
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
if (!allowTransition && "transition" in style) {
|
|
1005
|
+
errors.push(
|
|
1006
|
+
createError(
|
|
1007
|
+
"INVALID_VALUE",
|
|
1008
|
+
`transition is not allowed inside responsive overrides at "${stylePath}.transition"`,
|
|
1009
|
+
`${stylePath}.transition`
|
|
1010
|
+
)
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
594
1013
|
if ("zIndex" in style && isLiteralNumber(style.zIndex)) {
|
|
595
1014
|
const v = style.zIndex;
|
|
596
1015
|
if (v < ZINDEX_MIN || v > ZINDEX_MAX) {
|
|
@@ -819,6 +1238,18 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverSty
|
|
|
819
1238
|
}
|
|
820
1239
|
}
|
|
821
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
|
+
}
|
|
822
1253
|
for (const [prop, range] of Object.entries(RANGE_LENGTH_PROPERTIES)) {
|
|
823
1254
|
if (prop in style && isLiteralString(style[prop]) && !isDynamic(style[prop])) {
|
|
824
1255
|
const numericValue = parseLengthValue(style[prop]);
|
|
@@ -872,7 +1303,7 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverSty
|
|
|
872
1303
|
}
|
|
873
1304
|
}
|
|
874
1305
|
}
|
|
875
|
-
if ("hoverStyle" in style && style.hoverStyle != null) {
|
|
1306
|
+
if (allowHoverStyle && "hoverStyle" in style && style.hoverStyle != null) {
|
|
876
1307
|
const hoverStyle = style.hoverStyle;
|
|
877
1308
|
const hoverPath = `${stylePath}.hoverStyle`;
|
|
878
1309
|
if (typeof hoverStyle !== "object" || Array.isArray(hoverStyle)) {
|
|
@@ -901,12 +1332,16 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverSty
|
|
|
901
1332
|
hoverPath,
|
|
902
1333
|
errors,
|
|
903
1334
|
cardStyles,
|
|
904
|
-
|
|
1335
|
+
{
|
|
1336
|
+
allowHoverStyle,
|
|
1337
|
+
allowTransition,
|
|
1338
|
+
allowHoverStyleRefs
|
|
1339
|
+
}
|
|
905
1340
|
);
|
|
906
1341
|
}
|
|
907
1342
|
}
|
|
908
1343
|
}
|
|
909
|
-
if ("transition" in style && style.transition != null) {
|
|
1344
|
+
if (allowTransition && "transition" in style && style.transition != null) {
|
|
910
1345
|
const transition = style.transition;
|
|
911
1346
|
const transPath = `${stylePath}.transition`;
|
|
912
1347
|
if (typeof transition === "string") {
|
|
@@ -977,7 +1412,7 @@ function mergeStyleWithRef(cardStyleEntry, inlineStyle) {
|
|
|
977
1412
|
}
|
|
978
1413
|
return merged;
|
|
979
1414
|
}
|
|
980
|
-
function validateStyles(views, cardStyles) {
|
|
1415
|
+
function validateStyles(views, cardStyles, fragments) {
|
|
981
1416
|
const errors = [];
|
|
982
1417
|
if (cardStyles) {
|
|
983
1418
|
for (const [styleName, styleEntry] of Object.entries(cardStyles)) {
|
|
@@ -992,18 +1427,66 @@ function validateStyles(views, cardStyles) {
|
|
|
992
1427
|
);
|
|
993
1428
|
}
|
|
994
1429
|
collectNestedStyleRefErrors(styleEntry, entryPath, errors);
|
|
995
|
-
validateSingleStyle(styleEntry, entryPath, errors, void 0,
|
|
1430
|
+
validateSingleStyle(styleEntry, entryPath, errors, void 0, {
|
|
1431
|
+
allowHoverStyleRefs: false
|
|
1432
|
+
});
|
|
996
1433
|
}
|
|
997
1434
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
if (style == null || typeof style !== "object") {
|
|
1435
|
+
walkRenderableCard(views, fragments, (node, ctx) => {
|
|
1436
|
+
if (!("type" in node) || typeof node.type !== "string") {
|
|
1001
1437
|
return;
|
|
1002
1438
|
}
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1439
|
+
const style = node.style != null && typeof node.style === "object" && !Array.isArray(node.style) ? node.style : void 0;
|
|
1440
|
+
if (style != null && typeof style === "object") {
|
|
1441
|
+
const stylePath = `${ctx.path}.style`;
|
|
1442
|
+
const mergedStyle = resolveStyleRef(style, stylePath, cardStyles, errors);
|
|
1443
|
+
if (mergedStyle) {
|
|
1444
|
+
validateSingleStyle(mergedStyle, stylePath, errors, cardStyles);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
const responsive = node.responsive;
|
|
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
|
+
}
|
|
1466
|
+
}
|
|
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
|
+
}
|
|
1007
1490
|
}
|
|
1008
1491
|
});
|
|
1009
1492
|
return errors;
|
|
@@ -1012,8 +1495,74 @@ function validateStyles(views, cardStyles) {
|
|
|
1012
1495
|
// src/security.ts
|
|
1013
1496
|
import {
|
|
1014
1497
|
PROTOTYPE_POLLUTION_SEGMENTS,
|
|
1015
|
-
isRef as
|
|
1498
|
+
isRef as isRef3
|
|
1016
1499
|
} from "@safe-ugc-ui/types";
|
|
1500
|
+
|
|
1501
|
+
// src/responsive-utils.ts
|
|
1502
|
+
var RESPONSIVE_MODES = [
|
|
1503
|
+
"default",
|
|
1504
|
+
"compact"
|
|
1505
|
+
];
|
|
1506
|
+
function mergeNamedStyle(style, cardStyles) {
|
|
1507
|
+
if (!style) return void 0;
|
|
1508
|
+
const rawStyleName = style.$style;
|
|
1509
|
+
const styleName = typeof rawStyleName === "string" ? rawStyleName.trim() : rawStyleName;
|
|
1510
|
+
if (!styleName || typeof styleName !== "string" || !cardStyles) {
|
|
1511
|
+
if (style.$style !== void 0) {
|
|
1512
|
+
const { $style: _2, ...rest } = style;
|
|
1513
|
+
return rest;
|
|
1514
|
+
}
|
|
1515
|
+
return style;
|
|
1516
|
+
}
|
|
1517
|
+
const baseStyle = cardStyles[styleName];
|
|
1518
|
+
if (!baseStyle) {
|
|
1519
|
+
const { $style: _2, ...rest } = style;
|
|
1520
|
+
return rest;
|
|
1521
|
+
}
|
|
1522
|
+
const { $style: _, ...inlineWithoutStyleRef } = style;
|
|
1523
|
+
return { ...baseStyle, ...inlineWithoutStyleRef };
|
|
1524
|
+
}
|
|
1525
|
+
function getCompactResponsiveStyle(node) {
|
|
1526
|
+
const responsive = node.responsive;
|
|
1527
|
+
if (responsive == null || typeof responsive !== "object" || Array.isArray(responsive)) {
|
|
1528
|
+
return void 0;
|
|
1529
|
+
}
|
|
1530
|
+
const compact = responsive.compact;
|
|
1531
|
+
if (compact == null || typeof compact !== "object" || Array.isArray(compact)) {
|
|
1532
|
+
return void 0;
|
|
1533
|
+
}
|
|
1534
|
+
return compact;
|
|
1535
|
+
}
|
|
1536
|
+
function stripResponsiveOnlyUnsupportedFields(style) {
|
|
1537
|
+
if (!style) return void 0;
|
|
1538
|
+
const {
|
|
1539
|
+
hoverStyle: _hoverStyle,
|
|
1540
|
+
transition: _transition,
|
|
1541
|
+
...rest
|
|
1542
|
+
} = style;
|
|
1543
|
+
return rest;
|
|
1544
|
+
}
|
|
1545
|
+
function getEffectiveStyleForMode(node, cardStyles, mode) {
|
|
1546
|
+
const baseStyle = mergeNamedStyle(node.style, cardStyles);
|
|
1547
|
+
if (mode === "default") {
|
|
1548
|
+
return baseStyle;
|
|
1549
|
+
}
|
|
1550
|
+
const compactStyle = stripResponsiveOnlyUnsupportedFields(
|
|
1551
|
+
mergeNamedStyle(getCompactResponsiveStyle(node), cardStyles)
|
|
1552
|
+
);
|
|
1553
|
+
if (!compactStyle) {
|
|
1554
|
+
return baseStyle;
|
|
1555
|
+
}
|
|
1556
|
+
return {
|
|
1557
|
+
...baseStyle ?? {},
|
|
1558
|
+
...compactStyle
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
function getMergedCompactResponsiveStyle(node, cardStyles) {
|
|
1562
|
+
return mergeNamedStyle(getCompactResponsiveStyle(node), cardStyles);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// src/security.ts
|
|
1017
1566
|
var FORBIDDEN_URL_PREFIXES = [
|
|
1018
1567
|
"http://",
|
|
1019
1568
|
"https://",
|
|
@@ -1025,7 +1574,7 @@ function scanForRefs(obj, path, errors) {
|
|
|
1025
1574
|
if (obj === null || obj === void 0 || typeof obj !== "object") {
|
|
1026
1575
|
return;
|
|
1027
1576
|
}
|
|
1028
|
-
if (
|
|
1577
|
+
if (isRef3(obj)) {
|
|
1029
1578
|
const refStr = obj.$ref;
|
|
1030
1579
|
const segments = refStr.split(/[.\[]/);
|
|
1031
1580
|
for (const segment of segments) {
|
|
@@ -1130,9 +1679,102 @@ function checkSrcValue(resolved, type, errorPath, errors) {
|
|
|
1130
1679
|
}
|
|
1131
1680
|
}
|
|
1132
1681
|
}
|
|
1682
|
+
function pushUniqueError(errors, seen, error) {
|
|
1683
|
+
const key = `${error.code}|${error.path}|${error.message}`;
|
|
1684
|
+
if (seen.has(key)) {
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
seen.add(key);
|
|
1688
|
+
errors.push(error);
|
|
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
|
+
}
|
|
1707
|
+
function scanStyleStringsForUrl(style, path, errors) {
|
|
1708
|
+
if (!style) {
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
for (const [prop, value] of Object.entries(style)) {
|
|
1712
|
+
if (typeof value === "string" && value.toLowerCase().includes("url(")) {
|
|
1713
|
+
errors.push(
|
|
1714
|
+
createError(
|
|
1715
|
+
"FORBIDDEN_CSS_FUNCTION",
|
|
1716
|
+
`CSS url() function is forbidden in style values. Found in "${prop}".`,
|
|
1717
|
+
`${path}.${prop}`
|
|
1718
|
+
)
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
function validateEffectiveStylesForMode(mode, views, cardStyles, fragments, errors, seen) {
|
|
1724
|
+
const styleResolver = (node) => getEffectiveStyleForMode(node, cardStyles, mode);
|
|
1725
|
+
traverseCard(views, (node, context) => {
|
|
1726
|
+
const effectiveStyle = styleResolver(node);
|
|
1727
|
+
if (effectiveStyle && typeof effectiveStyle.position === "string") {
|
|
1728
|
+
const position = effectiveStyle.position;
|
|
1729
|
+
if (position === "fixed") {
|
|
1730
|
+
pushUniqueError(
|
|
1731
|
+
errors,
|
|
1732
|
+
seen,
|
|
1733
|
+
createError(
|
|
1734
|
+
"POSITION_FIXED_FORBIDDEN",
|
|
1735
|
+
'CSS position "fixed" is not allowed.',
|
|
1736
|
+
`${context.path}.style.position`
|
|
1737
|
+
)
|
|
1738
|
+
);
|
|
1739
|
+
} else if (position === "sticky") {
|
|
1740
|
+
pushUniqueError(
|
|
1741
|
+
errors,
|
|
1742
|
+
seen,
|
|
1743
|
+
createError(
|
|
1744
|
+
"POSITION_STICKY_FORBIDDEN",
|
|
1745
|
+
'CSS position "sticky" is not allowed.',
|
|
1746
|
+
`${context.path}.style.position`
|
|
1747
|
+
)
|
|
1748
|
+
);
|
|
1749
|
+
} else if (position === "absolute" && context.parentType !== "Stack") {
|
|
1750
|
+
pushUniqueError(
|
|
1751
|
+
errors,
|
|
1752
|
+
seen,
|
|
1753
|
+
createError(
|
|
1754
|
+
"POSITION_ABSOLUTE_NOT_IN_STACK",
|
|
1755
|
+
'CSS position "absolute" is only allowed inside a Stack component.',
|
|
1756
|
+
`${context.path}.style.position`
|
|
1757
|
+
)
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
if (effectiveStyle && effectiveStyle.overflow === "auto" && context.overflowAutoAncestor) {
|
|
1762
|
+
pushUniqueError(
|
|
1763
|
+
errors,
|
|
1764
|
+
seen,
|
|
1765
|
+
createError(
|
|
1766
|
+
"OVERFLOW_AUTO_NESTED",
|
|
1767
|
+
"Nested overflow:auto is not allowed. An ancestor already has overflow:auto.",
|
|
1768
|
+
`${context.path}.style.overflow`
|
|
1769
|
+
)
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
}, styleResolver, fragments);
|
|
1773
|
+
}
|
|
1133
1774
|
function validateSecurity(card) {
|
|
1134
1775
|
const errors = [];
|
|
1135
|
-
const
|
|
1776
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1777
|
+
const { views, state, cardAssets, cardStyles, fragments } = card;
|
|
1136
1778
|
if (cardAssets) {
|
|
1137
1779
|
for (const [key, value] of Object.entries(cardAssets)) {
|
|
1138
1780
|
const assetError = validateAssetPath(value);
|
|
@@ -1155,18 +1797,20 @@ function validateSecurity(card) {
|
|
|
1155
1797
|
}
|
|
1156
1798
|
}
|
|
1157
1799
|
}
|
|
1158
|
-
|
|
1800
|
+
walkRenderableCard(views, fragments, (node, context) => {
|
|
1801
|
+
if (!("type" in node) || typeof node.type !== "string") {
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
const traversableNode = node;
|
|
1159
1805
|
const { path } = context;
|
|
1160
|
-
const
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
delete nodeFields.style;
|
|
1164
|
-
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);
|
|
1165
1809
|
if (type === "Image" || type === "Avatar") {
|
|
1166
1810
|
const src = node.src;
|
|
1167
1811
|
if (typeof src === "string") {
|
|
1168
1812
|
checkSrcValue(src, type, `${path}.src`, errors);
|
|
1169
|
-
} else if (
|
|
1813
|
+
} else if (isRef3(src)) {
|
|
1170
1814
|
if (state) {
|
|
1171
1815
|
const resolved = resolveRefFromState(
|
|
1172
1816
|
src.$ref,
|
|
@@ -1178,74 +1822,46 @@ function validateSecurity(card) {
|
|
|
1178
1822
|
}
|
|
1179
1823
|
}
|
|
1180
1824
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
}
|
|
1825
|
+
scanStyleStringsForUrl(style, `${path}.style`, errors);
|
|
1826
|
+
const responsive = traversableNode.responsive;
|
|
1827
|
+
if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
|
|
1828
|
+
const compact = responsive.compact;
|
|
1829
|
+
if (compact != null && typeof compact === "object" && !Array.isArray(compact)) {
|
|
1830
|
+
scanStyleStringsForUrl(
|
|
1831
|
+
compact,
|
|
1832
|
+
`${path}.responsive.compact`,
|
|
1833
|
+
errors
|
|
1834
|
+
);
|
|
1192
1835
|
}
|
|
1193
1836
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
merged[key] = value;
|
|
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;
|
|
1201
1843
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
errors.push(
|
|
1209
|
-
createError(
|
|
1210
|
-
"POSITION_FIXED_FORBIDDEN",
|
|
1211
|
-
'CSS position "fixed" is not allowed.',
|
|
1212
|
-
`${path}.style.position`
|
|
1213
|
-
)
|
|
1214
|
-
);
|
|
1215
|
-
} else if (position === "sticky") {
|
|
1216
|
-
errors.push(
|
|
1217
|
-
createError(
|
|
1218
|
-
"POSITION_STICKY_FORBIDDEN",
|
|
1219
|
-
'CSS position "sticky" is not allowed.',
|
|
1220
|
-
`${path}.style.position`
|
|
1221
|
-
)
|
|
1222
|
-
);
|
|
1223
|
-
} else if (position === "absolute") {
|
|
1224
|
-
if (context.parentType !== "Stack") {
|
|
1225
|
-
errors.push(
|
|
1226
|
-
createError(
|
|
1227
|
-
"POSITION_ABSOLUTE_NOT_IN_STACK",
|
|
1228
|
-
'CSS position "absolute" is only allowed inside a Stack component.',
|
|
1229
|
-
`${path}.style.position`
|
|
1230
|
-
)
|
|
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
|
|
1231
1850
|
);
|
|
1232
1851
|
}
|
|
1233
1852
|
}
|
|
1234
1853
|
}
|
|
1235
|
-
if (effectiveStyle && effectiveStyle.overflow === "auto" && context.overflowAutoAncestor) {
|
|
1236
|
-
errors.push(
|
|
1237
|
-
createError(
|
|
1238
|
-
"OVERFLOW_AUTO_NESTED",
|
|
1239
|
-
"Nested overflow:auto is not allowed. An ancestor already has overflow:auto.",
|
|
1240
|
-
`${path}.style.overflow`
|
|
1241
|
-
)
|
|
1242
|
-
);
|
|
1243
|
-
}
|
|
1244
1854
|
scanForRefs(nodeFields, path, errors);
|
|
1245
1855
|
if (style) {
|
|
1246
1856
|
scanForRefs(style, `${path}.style`, errors);
|
|
1247
1857
|
}
|
|
1858
|
+
if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
|
|
1859
|
+
scanForRefs(responsive, `${path}.responsive`, errors);
|
|
1860
|
+
}
|
|
1248
1861
|
});
|
|
1862
|
+
for (const mode of RESPONSIVE_MODES) {
|
|
1863
|
+
validateEffectiveStylesForMode(mode, views, cardStyles, fragments, errors, seen);
|
|
1864
|
+
}
|
|
1249
1865
|
return errors;
|
|
1250
1866
|
}
|
|
1251
1867
|
|
|
@@ -1258,8 +1874,7 @@ import {
|
|
|
1258
1874
|
MAX_NESTED_LOOPS,
|
|
1259
1875
|
MAX_OVERFLOW_AUTO_COUNT,
|
|
1260
1876
|
MAX_STACK_NESTING,
|
|
1261
|
-
PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2
|
|
1262
|
-
isRef as isRef3
|
|
1877
|
+
PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2
|
|
1263
1878
|
} from "@safe-ugc-ui/types";
|
|
1264
1879
|
function utf8ByteLength(str) {
|
|
1265
1880
|
let bytes = 0;
|
|
@@ -1278,6 +1893,54 @@ function utf8ByteLength(str) {
|
|
|
1278
1893
|
}
|
|
1279
1894
|
return bytes;
|
|
1280
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
|
+
}
|
|
1281
1944
|
function resolveRefFromState2(refPath, state) {
|
|
1282
1945
|
const path = refPath.startsWith("$") ? refPath.slice(1) : refPath;
|
|
1283
1946
|
const dotSegments = path.split(".");
|
|
@@ -1312,58 +1975,49 @@ function resolveRefFromState2(refPath, state) {
|
|
|
1312
1975
|
}
|
|
1313
1976
|
return current;
|
|
1314
1977
|
}
|
|
1315
|
-
function countTemplateMetrics(template, cardStyles) {
|
|
1316
|
-
const result = {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
const merged = { ...cardStyles[refName] };
|
|
1333
|
-
for (const [key, value] of Object.entries(style)) {
|
|
1334
|
-
if (key !== "$style") merged[key] = value;
|
|
1978
|
+
function countTemplateMetrics(template, cardStyles, fragments) {
|
|
1979
|
+
const result = {
|
|
1980
|
+
nodes: 0,
|
|
1981
|
+
textBytes: 0,
|
|
1982
|
+
styleBytes: 0,
|
|
1983
|
+
overflowAutoCount: {
|
|
1984
|
+
default: 0,
|
|
1985
|
+
compact: 0
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
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);
|
|
1335
1995
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
effectiveOverflow = cardStyles[refName].overflow;
|
|
1996
|
+
const baseStyleForBytes = getEffectiveStyleForMode(
|
|
1997
|
+
node,
|
|
1998
|
+
cardStyles,
|
|
1999
|
+
"default"
|
|
2000
|
+
);
|
|
2001
|
+
if (baseStyleForBytes) {
|
|
2002
|
+
result.styleBytes += utf8ByteLength(JSON.stringify(baseStyleForBytes));
|
|
1344
2003
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
result.nodes += innerMetrics.nodes;
|
|
1363
|
-
result.textBytes += innerMetrics.textBytes;
|
|
1364
|
-
result.styleBytes += innerMetrics.styleBytes;
|
|
1365
|
-
result.overflowAutoCount += innerMetrics.overflowAutoCount;
|
|
1366
|
-
}
|
|
2004
|
+
const compactStyleForBytes = getMergedCompactResponsiveStyle(
|
|
2005
|
+
node,
|
|
2006
|
+
cardStyles
|
|
2007
|
+
);
|
|
2008
|
+
if (compactStyleForBytes) {
|
|
2009
|
+
result.styleBytes += utf8ByteLength(JSON.stringify(compactStyleForBytes));
|
|
2010
|
+
}
|
|
2011
|
+
for (const mode of RESPONSIVE_MODES) {
|
|
2012
|
+
const effectiveStyle = getEffectiveStyleForMode(node, cardStyles, mode);
|
|
2013
|
+
if (effectiveStyle?.overflow === "auto") {
|
|
2014
|
+
result.overflowAutoCount[mode]++;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
},
|
|
2018
|
+
void 0,
|
|
2019
|
+
fragments
|
|
2020
|
+
);
|
|
1367
2021
|
return result;
|
|
1368
2022
|
}
|
|
1369
2023
|
function validateLimits(card) {
|
|
@@ -1371,29 +2025,32 @@ function validateLimits(card) {
|
|
|
1371
2025
|
let nodeCount = 0;
|
|
1372
2026
|
let textContentBytes = 0;
|
|
1373
2027
|
let styleObjectsBytes = 0;
|
|
1374
|
-
|
|
2028
|
+
const overflowAutoCount = {
|
|
2029
|
+
default: 0,
|
|
2030
|
+
compact: 0
|
|
2031
|
+
};
|
|
1375
2032
|
traverseCard(card.views, (node, context) => {
|
|
1376
2033
|
nodeCount++;
|
|
1377
2034
|
if (node.type === "Text") {
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
textContentBytes += utf8ByteLength(content);
|
|
1381
|
-
}
|
|
2035
|
+
textContentBytes += countTextNodeLiteralBytes(node);
|
|
2036
|
+
styleObjectsBytes += countTextSpanStyleBytes(node);
|
|
1382
2037
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
2038
|
+
{
|
|
2039
|
+
const baseStyleForBytes = getEffectiveStyleForMode(
|
|
2040
|
+
node,
|
|
2041
|
+
card.cardStyles,
|
|
2042
|
+
"default"
|
|
2043
|
+
);
|
|
2044
|
+
if (baseStyleForBytes) {
|
|
2045
|
+
styleObjectsBytes += utf8ByteLength(JSON.stringify(baseStyleForBytes));
|
|
2046
|
+
}
|
|
2047
|
+
const compactStyleForBytes = getMergedCompactResponsiveStyle(
|
|
2048
|
+
node,
|
|
2049
|
+
card.cardStyles
|
|
2050
|
+
);
|
|
2051
|
+
if (compactStyleForBytes) {
|
|
2052
|
+
styleObjectsBytes += utf8ByteLength(JSON.stringify(compactStyleForBytes));
|
|
1394
2053
|
}
|
|
1395
|
-
const serialized = JSON.stringify(styleForBytes);
|
|
1396
|
-
styleObjectsBytes += utf8ByteLength(serialized);
|
|
1397
2054
|
}
|
|
1398
2055
|
const children = node.children;
|
|
1399
2056
|
if (children != null && typeof children === "object" && !Array.isArray(children) && "for" in children && "in" in children && "template" in children) {
|
|
@@ -1431,12 +2088,14 @@ function validateLimits(card) {
|
|
|
1431
2088
|
)
|
|
1432
2089
|
);
|
|
1433
2090
|
} else if (source.length > 1) {
|
|
1434
|
-
const tplMetrics = countTemplateMetrics(forLoop.template, card.cardStyles);
|
|
2091
|
+
const tplMetrics = countTemplateMetrics(forLoop.template, card.cardStyles, card.fragments);
|
|
1435
2092
|
const multiplier = source.length - 1;
|
|
1436
2093
|
nodeCount += tplMetrics.nodes * multiplier;
|
|
1437
2094
|
textContentBytes += tplMetrics.textBytes * multiplier;
|
|
1438
2095
|
styleObjectsBytes += tplMetrics.styleBytes * multiplier;
|
|
1439
|
-
|
|
2096
|
+
for (const mode of RESPONSIVE_MODES) {
|
|
2097
|
+
overflowAutoCount[mode] += tplMetrics.overflowAutoCount[mode] * multiplier;
|
|
2098
|
+
}
|
|
1440
2099
|
}
|
|
1441
2100
|
}
|
|
1442
2101
|
}
|
|
@@ -1450,16 +2109,10 @@ function validateLimits(card) {
|
|
|
1450
2109
|
);
|
|
1451
2110
|
}
|
|
1452
2111
|
}
|
|
1453
|
-
{
|
|
1454
|
-
|
|
1455
|
-
if (
|
|
1456
|
-
|
|
1457
|
-
if (!("overflow" in node.style) || node.style.overflow === void 0) {
|
|
1458
|
-
effectiveOverflow = card.cardStyles[refName].overflow;
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
if (effectiveOverflow === "auto") {
|
|
1462
|
-
overflowAutoCount++;
|
|
2112
|
+
for (const mode of RESPONSIVE_MODES) {
|
|
2113
|
+
const effectiveStyle = getEffectiveStyleForMode(node, card.cardStyles, mode);
|
|
2114
|
+
if (effectiveStyle?.overflow === "auto") {
|
|
2115
|
+
overflowAutoCount[mode]++;
|
|
1463
2116
|
}
|
|
1464
2117
|
}
|
|
1465
2118
|
if (node.type === "Stack" && context.stackDepth >= MAX_STACK_NESTING) {
|
|
@@ -1471,7 +2124,7 @@ function validateLimits(card) {
|
|
|
1471
2124
|
)
|
|
1472
2125
|
);
|
|
1473
2126
|
}
|
|
1474
|
-
});
|
|
2127
|
+
}, void 0, card.fragments);
|
|
1475
2128
|
if (nodeCount > MAX_NODE_COUNT) {
|
|
1476
2129
|
errors.push(
|
|
1477
2130
|
createError(
|
|
@@ -1499,11 +2152,15 @@ function validateLimits(card) {
|
|
|
1499
2152
|
)
|
|
1500
2153
|
);
|
|
1501
2154
|
}
|
|
1502
|
-
|
|
2155
|
+
const maxOverflowAutoCount = Math.max(
|
|
2156
|
+
overflowAutoCount.default,
|
|
2157
|
+
overflowAutoCount.compact
|
|
2158
|
+
);
|
|
2159
|
+
if (maxOverflowAutoCount > MAX_OVERFLOW_AUTO_COUNT) {
|
|
1503
2160
|
errors.push(
|
|
1504
2161
|
createError(
|
|
1505
2162
|
"OVERFLOW_AUTO_COUNT_EXCEEDED",
|
|
1506
|
-
`Card has ${
|
|
2163
|
+
`Card has ${maxOverflowAutoCount} elements with overflow:auto in at least one responsive mode, max is ${MAX_OVERFLOW_AUTO_COUNT}`,
|
|
1507
2164
|
"views"
|
|
1508
2165
|
)
|
|
1509
2166
|
);
|
|
@@ -1533,17 +2190,26 @@ function runAllChecks(input) {
|
|
|
1533
2190
|
const obj = input;
|
|
1534
2191
|
const views = obj.views;
|
|
1535
2192
|
const cardStyles = obj.styles;
|
|
2193
|
+
const fragments = obj.fragments;
|
|
1536
2194
|
const errors = [];
|
|
1537
|
-
errors.push(...
|
|
1538
|
-
errors.push(...
|
|
1539
|
-
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));
|
|
1540
2200
|
errors.push(...validateSecurity({
|
|
1541
2201
|
views,
|
|
1542
2202
|
state: obj.state,
|
|
1543
2203
|
cardAssets: obj.assets,
|
|
1544
|
-
cardStyles
|
|
2204
|
+
cardStyles,
|
|
2205
|
+
fragments
|
|
2206
|
+
}));
|
|
2207
|
+
errors.push(...validateLimits({
|
|
2208
|
+
state: obj.state,
|
|
2209
|
+
views,
|
|
2210
|
+
cardStyles,
|
|
2211
|
+
fragments
|
|
1545
2212
|
}));
|
|
1546
|
-
errors.push(...validateLimits({ state: obj.state, views, cardStyles }));
|
|
1547
2213
|
return errors;
|
|
1548
2214
|
}
|
|
1549
2215
|
function validate(input) {
|
|
@@ -1586,6 +2252,8 @@ export {
|
|
|
1586
2252
|
traverseNode,
|
|
1587
2253
|
validResult,
|
|
1588
2254
|
validate,
|
|
2255
|
+
validateConditions,
|
|
2256
|
+
validateFragments,
|
|
1589
2257
|
validateLimits,
|
|
1590
2258
|
validateNodes,
|
|
1591
2259
|
validateRaw,
|