@safe-ugc-ui/validator 0.6.0 → 1.0.0

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