@safe-ugc-ui/validator 0.6.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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);
227
+ }
228
+ }
229
+
230
+ // src/renderable-walk.ts
231
+ function isForLoopLike(value) {
232
+ return typeof value === "object" && value !== null && "for" in value && "in" in value && "template" in value;
233
+ }
234
+ function walkRenderableNode(node, path, inFragments, visitor) {
235
+ visitor(node, { path, inFragments });
236
+ if (!isTraversableNode(node)) {
237
+ return;
238
+ }
239
+ const children = node.children;
240
+ if (Array.isArray(children)) {
241
+ for (let i = 0; i < children.length; i++) {
242
+ const child = children[i];
243
+ if (!isTraversableNode(child) && !isFragmentUseLike(child)) {
244
+ continue;
245
+ }
246
+ walkRenderableNode(
247
+ child,
248
+ `${path}.children[${i}]`,
249
+ inFragments,
250
+ visitor
251
+ );
252
+ }
253
+ }
254
+ if (isForLoopLike(children)) {
255
+ const template = children.template;
256
+ if (isTraversableNode(template) || isFragmentUseLike(template)) {
257
+ walkRenderableNode(
258
+ template,
259
+ `${path}.children.template`,
260
+ inFragments,
261
+ visitor
262
+ );
263
+ }
264
+ }
265
+ for (const entry of getEmbeddedRenderables(node)) {
266
+ walkRenderableNode(
267
+ entry.renderable,
268
+ `${path}.${entry.pathSuffix}`,
269
+ inFragments,
270
+ visitor
271
+ );
272
+ }
273
+ }
274
+ function walkRenderableCard(views, fragments, visitor) {
275
+ for (const [viewName, rootNode] of Object.entries(views)) {
276
+ if (!isTraversableNode(rootNode) && !isFragmentUseLike(rootNode)) {
277
+ continue;
278
+ }
279
+ walkRenderableNode(
280
+ rootNode,
281
+ `views.${viewName}`,
282
+ false,
283
+ visitor
284
+ );
285
+ }
286
+ if (!fragments) {
287
+ return;
288
+ }
289
+ for (const [fragmentName, fragmentRoot] of Object.entries(fragments)) {
290
+ if (!isTraversableNode(fragmentRoot) && !isFragmentUseLike(fragmentRoot)) {
291
+ continue;
292
+ }
293
+ walkRenderableNode(
294
+ fragmentRoot,
295
+ `fragments.${fragmentName}`,
296
+ true,
297
+ visitor
298
+ );
165
299
  }
166
300
  }
167
301
 
302
+ // src/fragment-validator.ts
303
+ var FRAGMENT_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
304
+ function validateFragments(views, fragments) {
305
+ const errors = [];
306
+ if (fragments) {
307
+ for (const fragmentName of Object.keys(fragments)) {
308
+ if (!FRAGMENT_NAME_PATTERN.test(fragmentName)) {
309
+ errors.push(
310
+ createError(
311
+ "INVALID_FRAGMENT_NAME",
312
+ `Fragment name "${fragmentName}" is invalid; must match /^[A-Za-z][A-Za-z0-9_-]*$/.`,
313
+ `fragments.${fragmentName}`
314
+ )
315
+ );
316
+ }
317
+ }
318
+ }
319
+ walkRenderableCard(views, fragments, (node, context) => {
320
+ if (!isFragmentUseLike(node)) {
321
+ return;
322
+ }
323
+ if (context.inFragments) {
324
+ errors.push(
325
+ createError(
326
+ "FRAGMENT_NESTED_USE",
327
+ `Fragments may not contain "$use" references. Found at "${context.path}".`,
328
+ context.path
329
+ )
330
+ );
331
+ return;
332
+ }
333
+ if (!fragments || !(node.$use in fragments)) {
334
+ errors.push(
335
+ createError(
336
+ "FRAGMENT_REF_NOT_FOUND",
337
+ `Fragment "${node.$use}" was not found.`,
338
+ context.path
339
+ )
340
+ );
341
+ }
342
+ });
343
+ return errors;
344
+ }
345
+
168
346
  // src/node-validator.ts
347
+ import { ALL_COMPONENT_TYPES, MAX_INTERACTIVE_ITEMS } from "@safe-ugc-ui/types";
169
348
  var REQUIRED_FIELDS = {
170
- Text: ["content"],
171
349
  Image: ["src"],
172
350
  ProgressBar: ["value", "max"],
173
351
  Avatar: ["src"],
@@ -175,7 +353,9 @@ var REQUIRED_FIELDS = {
175
353
  Badge: ["label"],
176
354
  Chip: ["label"],
177
355
  Button: ["label", "action"],
178
- Toggle: ["value", "onToggle"]
356
+ Toggle: ["value", "onToggle"],
357
+ Accordion: ["items"],
358
+ Tabs: ["tabs"]
179
359
  };
180
360
  var KNOWN_TYPES = new Set(ALL_COMPONENT_TYPES);
181
361
  function looksLikeForLoop(value) {
@@ -203,17 +383,42 @@ function validateForLoop(children, path) {
203
383
  );
204
384
  }
205
385
  const template = children["template"];
206
- if (typeof template !== "object" || template === null || !("type" in template)) {
386
+ if (typeof template !== "object" || template === null || !("type" in template) && !isFragmentUseLike(template)) {
207
387
  errors.push(
208
388
  createError(
209
389
  "INVALID_VALUE",
210
- 'ForLoop "template" must be an object with a "type" property.',
390
+ 'ForLoop "template" must be an object with a "type" property or "$use" reference.',
211
391
  `${path}.children.template`
212
392
  )
213
393
  );
214
394
  }
215
395
  return errors;
216
396
  }
397
+ function collectUniqueInteractiveItemIds(items, nodeType, path, errors) {
398
+ const itemIds = /* @__PURE__ */ new Set();
399
+ for (let i = 0; i < items.length; i++) {
400
+ const item = items[i];
401
+ if (item == null || typeof item !== "object" || Array.isArray(item)) {
402
+ continue;
403
+ }
404
+ const itemId = item.id;
405
+ if (typeof itemId !== "string" || itemId.length === 0) {
406
+ continue;
407
+ }
408
+ if (itemIds.has(itemId)) {
409
+ errors.push(
410
+ createError(
411
+ "INVALID_VALUE",
412
+ `"${nodeType}" item ids must be unique. Duplicate id "${itemId}".`,
413
+ `${path}[${i}].id`
414
+ )
415
+ );
416
+ continue;
417
+ }
418
+ itemIds.add(itemId);
419
+ }
420
+ return itemIds;
421
+ }
217
422
  function validateNode(node, context) {
218
423
  const errors = [];
219
424
  const { path } = context;
@@ -227,6 +432,28 @@ function validateNode(node, context) {
227
432
  );
228
433
  return errors;
229
434
  }
435
+ if (node.type === "Text") {
436
+ const hasContent = "content" in node && node.content !== void 0;
437
+ const hasSpans = "spans" in node && node.spans !== void 0;
438
+ if (!hasContent && !hasSpans) {
439
+ errors.push(
440
+ createError(
441
+ "MISSING_FIELD",
442
+ '"Text" node must define either "content" or "spans".',
443
+ `${path}.content`
444
+ )
445
+ );
446
+ }
447
+ if (hasContent && hasSpans) {
448
+ errors.push(
449
+ createError(
450
+ "INVALID_VALUE",
451
+ '"Text" node cannot define both "content" and "spans".',
452
+ path
453
+ )
454
+ );
455
+ }
456
+ }
230
457
  const requiredFields = REQUIRED_FIELDS[node.type];
231
458
  if (requiredFields && requiredFields.length > 0) {
232
459
  for (const field of requiredFields) {
@@ -249,12 +476,148 @@ function validateNode(node, context) {
249
476
  )
250
477
  );
251
478
  }
479
+ if (node.type === "Accordion") {
480
+ const items = Array.isArray(node.items) ? node.items : void 0;
481
+ if (items) {
482
+ if (items.length > MAX_INTERACTIVE_ITEMS) {
483
+ errors.push(
484
+ createError(
485
+ "INVALID_VALUE",
486
+ `"Accordion" node may define at most ${MAX_INTERACTIVE_ITEMS} items.`,
487
+ `${path}.items`
488
+ )
489
+ );
490
+ }
491
+ const itemIds = collectUniqueInteractiveItemIds(
492
+ items,
493
+ "Accordion",
494
+ `${path}.items`,
495
+ errors
496
+ );
497
+ if (Array.isArray(node.defaultExpanded)) {
498
+ if (node.allowMultiple !== true && node.defaultExpanded.length > 1) {
499
+ errors.push(
500
+ createError(
501
+ "INVALID_VALUE",
502
+ '"Accordion" cannot define multiple defaultExpanded ids unless "allowMultiple" is true.',
503
+ `${path}.defaultExpanded`
504
+ )
505
+ );
506
+ }
507
+ for (let i = 0; i < node.defaultExpanded.length; i++) {
508
+ const itemId = node.defaultExpanded[i];
509
+ if (typeof itemId !== "string" || !itemIds.has(itemId)) {
510
+ errors.push(
511
+ createError(
512
+ "INVALID_VALUE",
513
+ `"Accordion" defaultExpanded id "${String(itemId)}" was not found in items.`,
514
+ `${path}.defaultExpanded[${i}]`
515
+ )
516
+ );
517
+ }
518
+ }
519
+ }
520
+ }
521
+ }
522
+ if (node.type === "Tabs") {
523
+ const tabs = Array.isArray(node.tabs) ? node.tabs : void 0;
524
+ if (tabs) {
525
+ if (tabs.length > MAX_INTERACTIVE_ITEMS) {
526
+ errors.push(
527
+ createError(
528
+ "INVALID_VALUE",
529
+ `"Tabs" node may define at most ${MAX_INTERACTIVE_ITEMS} tabs.`,
530
+ `${path}.tabs`
531
+ )
532
+ );
533
+ }
534
+ const tabIds = collectUniqueInteractiveItemIds(
535
+ tabs,
536
+ "Tabs",
537
+ `${path}.tabs`,
538
+ errors
539
+ );
540
+ if ("defaultTab" in node) {
541
+ const defaultTab = node.defaultTab;
542
+ if (typeof defaultTab !== "string" || !tabIds.has(defaultTab)) {
543
+ errors.push(
544
+ createError(
545
+ "INVALID_VALUE",
546
+ `"Tabs" defaultTab "${String(defaultTab)}" was not found in tabs.`,
547
+ `${path}.defaultTab`
548
+ )
549
+ );
550
+ }
551
+ }
552
+ }
553
+ }
252
554
  return errors;
253
555
  }
254
- function validateNodes(views) {
556
+ function validateNodes(views, fragments) {
255
557
  const errors = [];
256
- 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
  }
@@ -285,8 +648,10 @@ var STRUCTURED_OBJECT_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
285
648
  "borderLeft",
286
649
  // Shadow
287
650
  "boxShadow",
288
- "textShadow"
651
+ "textShadow",
652
+ "clipPath"
289
653
  ]);
654
+ var RESPONSIVE_OVERRIDE_KEYS = ["medium", "compact"];
290
655
  function validateNodeStyle(style, path, errors) {
291
656
  if (!style) {
292
657
  return;
@@ -317,19 +682,25 @@ function validateNodeStyle(style, path, errors) {
317
682
  }
318
683
  }
319
684
  }
320
- function validateValueTypes(views) {
685
+ function validateValueTypes(views, fragments) {
321
686
  const errors = [];
322
- traverseCard(views, (node, ctx) => {
323
- validateNodeStyle(node.style, `${ctx.path}.style`, errors);
687
+ walkRenderableCard(views, fragments, (node, ctx) => {
688
+ if (!("type" in node) || typeof node.type !== "string") {
689
+ return;
690
+ }
691
+ const style = node.style != null && typeof node.style === "object" && !Array.isArray(node.style) ? node.style : void 0;
692
+ validateNodeStyle(style, `${ctx.path}.style`, errors);
324
693
  const responsive = node.responsive;
325
694
  if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
326
- const compact = responsive.compact;
327
- if (compact != null && typeof compact === "object" && !Array.isArray(compact)) {
328
- validateNodeStyle(
329
- compact,
330
- `${ctx.path}.responsive.compact`,
331
- errors
332
- );
695
+ for (const mode of RESPONSIVE_OVERRIDE_KEYS) {
696
+ const override = responsive[mode];
697
+ if (override != null && typeof override === "object" && !Array.isArray(override)) {
698
+ validateNodeStyle(
699
+ override,
700
+ `${ctx.path}.responsive.${mode}`,
701
+ errors
702
+ );
703
+ }
333
704
  }
334
705
  }
335
706
  });
@@ -338,6 +709,7 @@ function validateValueTypes(views) {
338
709
 
339
710
  // src/style-validator.ts
340
711
  import {
712
+ ASPECT_RATIO_PATTERN,
341
713
  FORBIDDEN_STYLE_PROPERTIES,
342
714
  DANGEROUS_CSS_FUNCTIONS,
343
715
  ZINDEX_MIN,
@@ -358,12 +730,13 @@ import {
358
730
  LETTER_SPACING_MAX,
359
731
  OPACITY_MIN,
360
732
  OPACITY_MAX,
733
+ BACKDROP_BLUR_MAX,
361
734
  CSS_NAMED_COLORS,
362
735
  TRANSITION_DURATION_MAX,
363
736
  TRANSITION_DELAY_MAX,
364
737
  TRANSITION_MAX_COUNT,
365
738
  ALLOWED_TRANSITION_PROPERTIES,
366
- isRef
739
+ isRef as isRef2
367
740
  } from "@safe-ugc-ui/types";
368
741
  var COLOR_PROPERTIES = /* @__PURE__ */ new Set(["backgroundColor", "color"]);
369
742
  var LENGTH_PROPERTIES = /* @__PURE__ */ new Set([
@@ -403,6 +776,7 @@ var LENGTH_AUTO_ALLOWED = /* @__PURE__ */ new Set([
403
776
  "marginBottom",
404
777
  "marginLeft"
405
778
  ]);
779
+ var RESPONSIVE_OVERRIDE_KEYS2 = ["medium", "compact"];
406
780
  var RANGE_LENGTH_PROPERTIES = {
407
781
  fontSize: { min: FONT_SIZE_MIN, max: FONT_SIZE_MAX },
408
782
  letterSpacing: { min: LETTER_SPACING_MIN, max: LETTER_SPACING_MAX },
@@ -419,7 +793,7 @@ function isLiteralString(value) {
419
793
  return typeof value === "string";
420
794
  }
421
795
  function isDynamic(value) {
422
- return isRef(value);
796
+ return isRef2(value);
423
797
  }
424
798
  function isValidColor(value) {
425
799
  const lower = value.toLowerCase();
@@ -447,6 +821,22 @@ function parseLengthValue(value) {
447
821
  }
448
822
  return Number(match[1]);
449
823
  }
824
+ function isValidAspectRatioLiteral(value) {
825
+ if (typeof value === "number") {
826
+ return Number.isFinite(value) && value > 0;
827
+ }
828
+ const match = value.match(ASPECT_RATIO_PATTERN);
829
+ if (!match) {
830
+ return false;
831
+ }
832
+ const parts = value.split("/");
833
+ if (parts.length !== 2) {
834
+ return false;
835
+ }
836
+ const width = Number(parts[0].trim());
837
+ const height = Number(parts[1].trim());
838
+ return Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0;
839
+ }
450
840
  function collectDangerousCssErrors(value, path, errors) {
451
841
  if (typeof value === "string") {
452
842
  const lower = value.toLowerCase();
@@ -464,7 +854,7 @@ function collectDangerousCssErrors(value, path, errors) {
464
854
  }
465
855
  return;
466
856
  }
467
- if (isRef(value)) {
857
+ if (isRef2(value)) {
468
858
  return;
469
859
  }
470
860
  if (Array.isArray(value)) {
@@ -528,6 +918,44 @@ function validateTextShadowObject(shadow, path, errors) {
528
918
  );
529
919
  }
530
920
  }
921
+ function validateClipPathLength(value, path, errors) {
922
+ if (value == null || isDynamic(value)) {
923
+ return;
924
+ }
925
+ if (typeof value === "number") {
926
+ return;
927
+ }
928
+ if (typeof value === "string" && isValidLength(value)) {
929
+ return;
930
+ }
931
+ errors.push(
932
+ createError(
933
+ "INVALID_LENGTH",
934
+ `Invalid length "${String(value)}" at "${path}"`,
935
+ path
936
+ )
937
+ );
938
+ }
939
+ function validateClipPathObject(clipPath, path, errors) {
940
+ switch (clipPath.type) {
941
+ case "circle":
942
+ validateClipPathLength(clipPath.radius, `${path}.radius`, errors);
943
+ return;
944
+ case "ellipse":
945
+ validateClipPathLength(clipPath.rx, `${path}.rx`, errors);
946
+ validateClipPathLength(clipPath.ry, `${path}.ry`, errors);
947
+ return;
948
+ case "inset":
949
+ validateClipPathLength(clipPath.top, `${path}.top`, errors);
950
+ validateClipPathLength(clipPath.right, `${path}.right`, errors);
951
+ validateClipPathLength(clipPath.bottom, `${path}.bottom`, errors);
952
+ validateClipPathLength(clipPath.left, `${path}.left`, errors);
953
+ validateClipPathLength(clipPath.round, `${path}.round`, errors);
954
+ return;
955
+ default:
956
+ return;
957
+ }
958
+ }
531
959
  var STYLE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
532
960
  function reportStyleRefError(rawRef, stylePath, cardStyles, errors) {
533
961
  const trimmedRef = rawRef.trim();
@@ -662,6 +1090,18 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, options = {})
662
1090
  );
663
1091
  }
664
1092
  }
1093
+ if ("backdropBlur" in style && isLiteralNumber(style.backdropBlur)) {
1094
+ const v = style.backdropBlur;
1095
+ if (v < 0 || v > BACKDROP_BLUR_MAX) {
1096
+ errors.push(
1097
+ createError(
1098
+ "STYLE_VALUE_OUT_OF_RANGE",
1099
+ `backdropBlur (${v}) must be between 0 and ${BACKDROP_BLUR_MAX} at "${stylePath}.backdropBlur"`,
1100
+ `${stylePath}.backdropBlur`
1101
+ )
1102
+ );
1103
+ }
1104
+ }
665
1105
  if ("letterSpacing" in style && isLiteralNumber(style.letterSpacing)) {
666
1106
  const v = style.letterSpacing;
667
1107
  if (v < LETTER_SPACING_MIN || v > LETTER_SPACING_MAX) {
@@ -854,6 +1294,25 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, options = {})
854
1294
  }
855
1295
  }
856
1296
  }
1297
+ if ("aspectRatio" in style && style.aspectRatio != null && !isDynamic(style.aspectRatio)) {
1298
+ const aspectRatio = style.aspectRatio;
1299
+ if (typeof aspectRatio !== "number" && typeof aspectRatio !== "string" || !isValidAspectRatioLiteral(aspectRatio)) {
1300
+ errors.push(
1301
+ createError(
1302
+ "INVALID_VALUE",
1303
+ `Invalid aspectRatio "${String(aspectRatio)}" at "${stylePath}.aspectRatio"`,
1304
+ `${stylePath}.aspectRatio`
1305
+ )
1306
+ );
1307
+ }
1308
+ }
1309
+ if ("clipPath" in style && style.clipPath != null && typeof style.clipPath === "object" && !Array.isArray(style.clipPath) && !isDynamic(style.clipPath)) {
1310
+ validateClipPathObject(
1311
+ style.clipPath,
1312
+ `${stylePath}.clipPath`,
1313
+ errors
1314
+ );
1315
+ }
857
1316
  for (const [prop, range] of Object.entries(RANGE_LENGTH_PROPERTIES)) {
858
1317
  if (prop in style && isLiteralString(style[prop]) && !isDynamic(style[prop])) {
859
1318
  const numericValue = parseLengthValue(style[prop]);
@@ -1016,7 +1475,7 @@ function mergeStyleWithRef(cardStyleEntry, inlineStyle) {
1016
1475
  }
1017
1476
  return merged;
1018
1477
  }
1019
- function validateStyles(views, cardStyles) {
1478
+ function validateStyles(views, cardStyles, fragments) {
1020
1479
  const errors = [];
1021
1480
  if (cardStyles) {
1022
1481
  for (const [styleName, styleEntry] of Object.entries(cardStyles)) {
@@ -1036,8 +1495,11 @@ function validateStyles(views, cardStyles) {
1036
1495
  });
1037
1496
  }
1038
1497
  }
1039
- traverseCard(views, (node, ctx) => {
1040
- const style = node.style;
1498
+ walkRenderableCard(views, fragments, (node, ctx) => {
1499
+ if (!("type" in node) || typeof node.type !== "string") {
1500
+ return;
1501
+ }
1502
+ const style = node.style != null && typeof node.style === "object" && !Array.isArray(node.style) ? node.style : void 0;
1041
1503
  if (style != null && typeof style === "object") {
1042
1504
  const stylePath = `${ctx.path}.style`;
1043
1505
  const mergedStyle = resolveStyleRef(style, stylePath, cardStyles, errors);
@@ -1046,26 +1508,50 @@ function validateStyles(views, cardStyles) {
1046
1508
  }
1047
1509
  }
1048
1510
  const responsive = node.responsive;
1049
- if (responsive == 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;
1511
+ if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
1512
+ for (const mode of RESPONSIVE_OVERRIDE_KEYS2) {
1513
+ const override = responsive[mode];
1514
+ if (override != null && typeof override === "object" && !Array.isArray(override)) {
1515
+ const overridePath = `${ctx.path}.responsive.${mode}`;
1516
+ const mergedOverride = resolveStyleRef(
1517
+ override,
1518
+ overridePath,
1519
+ cardStyles,
1520
+ errors
1521
+ );
1522
+ if (mergedOverride) {
1523
+ validateSingleStyle(mergedOverride, overridePath, errors, cardStyles, {
1524
+ allowHoverStyle: false,
1525
+ allowTransition: false,
1526
+ allowHoverStyleRefs: false
1527
+ });
1528
+ }
1529
+ }
1530
+ }
1055
1531
  }
1056
- const 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
- });
1532
+ const spans = Array.isArray(node.spans) ? node.spans : void 0;
1533
+ if (node.type === "Text" && spans) {
1534
+ for (let i = 0; i < spans.length; i++) {
1535
+ const span = spans[i];
1536
+ if (span == null || typeof span !== "object" || Array.isArray(span)) {
1537
+ continue;
1538
+ }
1539
+ const spanStyle = span.style;
1540
+ if (spanStyle == null || typeof spanStyle !== "object" || Array.isArray(spanStyle)) {
1541
+ continue;
1542
+ }
1543
+ validateSingleStyle(
1544
+ spanStyle,
1545
+ `${ctx.path}.spans[${i}].style`,
1546
+ errors,
1547
+ void 0,
1548
+ {
1549
+ allowHoverStyle: false,
1550
+ allowTransition: false,
1551
+ allowHoverStyleRefs: false
1552
+ }
1553
+ );
1554
+ }
1069
1555
  }
1070
1556
  });
1071
1557
  return errors;
@@ -1074,12 +1560,13 @@ function validateStyles(views, cardStyles) {
1074
1560
  // src/security.ts
1075
1561
  import {
1076
1562
  PROTOTYPE_POLLUTION_SEGMENTS,
1077
- isRef as isRef2
1563
+ isRef as isRef3
1078
1564
  } from "@safe-ugc-ui/types";
1079
1565
 
1080
1566
  // src/responsive-utils.ts
1081
1567
  var RESPONSIVE_MODES = [
1082
1568
  "default",
1569
+ "medium",
1083
1570
  "compact"
1084
1571
  ];
1085
1572
  function mergeNamedStyle(style, cardStyles) {
@@ -1101,16 +1588,16 @@ function mergeNamedStyle(style, cardStyles) {
1101
1588
  const { $style: _, ...inlineWithoutStyleRef } = style;
1102
1589
  return { ...baseStyle, ...inlineWithoutStyleRef };
1103
1590
  }
1104
- function getCompactResponsiveStyle(node) {
1591
+ function getResponsiveStyle(node, mode) {
1105
1592
  const responsive = node.responsive;
1106
1593
  if (responsive == null || typeof responsive !== "object" || Array.isArray(responsive)) {
1107
1594
  return void 0;
1108
1595
  }
1109
- const compact = responsive.compact;
1110
- if (compact == null || typeof compact !== "object" || Array.isArray(compact)) {
1596
+ const override = responsive[mode];
1597
+ if (override == null || typeof override !== "object" || Array.isArray(override)) {
1111
1598
  return void 0;
1112
1599
  }
1113
- return compact;
1600
+ return override;
1114
1601
  }
1115
1602
  function stripResponsiveOnlyUnsupportedFields(style) {
1116
1603
  if (!style) return void 0;
@@ -1126,22 +1613,36 @@ function getEffectiveStyleForMode(node, cardStyles, mode) {
1126
1613
  if (mode === "default") {
1127
1614
  return baseStyle;
1128
1615
  }
1616
+ const mediumStyle = stripResponsiveOnlyUnsupportedFields(
1617
+ mergeNamedStyle(getResponsiveStyle(node, "medium"), cardStyles)
1618
+ );
1619
+ if (mode === "medium") {
1620
+ if (!mediumStyle) {
1621
+ return baseStyle;
1622
+ }
1623
+ return {
1624
+ ...baseStyle ?? {},
1625
+ ...mediumStyle
1626
+ };
1627
+ }
1129
1628
  const compactStyle = stripResponsiveOnlyUnsupportedFields(
1130
- mergeNamedStyle(getCompactResponsiveStyle(node), cardStyles)
1629
+ mergeNamedStyle(getResponsiveStyle(node, "compact"), cardStyles)
1131
1630
  );
1132
- if (!compactStyle) {
1631
+ if (!mediumStyle && !compactStyle) {
1133
1632
  return baseStyle;
1134
1633
  }
1135
1634
  return {
1136
1635
  ...baseStyle ?? {},
1636
+ ...mediumStyle ?? {},
1137
1637
  ...compactStyle
1138
1638
  };
1139
1639
  }
1140
- function getMergedCompactResponsiveStyle(node, cardStyles) {
1141
- return mergeNamedStyle(getCompactResponsiveStyle(node), cardStyles);
1640
+ function getMergedResponsiveStyleOverride(node, cardStyles, mode) {
1641
+ return mergeNamedStyle(getResponsiveStyle(node, mode), cardStyles);
1142
1642
  }
1143
1643
 
1144
1644
  // src/security.ts
1645
+ var RESPONSIVE_OVERRIDE_KEYS3 = ["medium", "compact"];
1145
1646
  var FORBIDDEN_URL_PREFIXES = [
1146
1647
  "http://",
1147
1648
  "https://",
@@ -1153,7 +1654,7 @@ function scanForRefs(obj, path, errors) {
1153
1654
  if (obj === null || obj === void 0 || typeof obj !== "object") {
1154
1655
  return;
1155
1656
  }
1156
- if (isRef2(obj)) {
1657
+ if (isRef3(obj)) {
1157
1658
  const refStr = obj.$ref;
1158
1659
  const segments = refStr.split(/[.\[]/);
1159
1660
  for (const segment of segments) {
@@ -1266,6 +1767,23 @@ function pushUniqueError(errors, seen, error) {
1266
1767
  seen.add(key);
1267
1768
  errors.push(error);
1268
1769
  }
1770
+ function getScannableNodeFields(node) {
1771
+ const nodeFields = { ...node };
1772
+ delete nodeFields.type;
1773
+ delete nodeFields.style;
1774
+ delete nodeFields.children;
1775
+ const interactiveField = node.type === "Accordion" ? "items" : node.type === "Tabs" ? "tabs" : null;
1776
+ if (interactiveField && Array.isArray(node[interactiveField])) {
1777
+ nodeFields[interactiveField] = node[interactiveField].map((item) => {
1778
+ if (item == null || typeof item !== "object" || Array.isArray(item)) {
1779
+ return item;
1780
+ }
1781
+ const { content: _content, ...rest } = item;
1782
+ return rest;
1783
+ });
1784
+ }
1785
+ return nodeFields;
1786
+ }
1269
1787
  function scanStyleStringsForUrl(style, path, errors) {
1270
1788
  if (!style) {
1271
1789
  return;
@@ -1282,7 +1800,7 @@ function scanStyleStringsForUrl(style, path, errors) {
1282
1800
  }
1283
1801
  }
1284
1802
  }
1285
- function validateEffectiveStylesForMode(mode, views, cardStyles, errors, seen) {
1803
+ function validateEffectiveStylesForMode(mode, views, cardStyles, fragments, errors, seen) {
1286
1804
  const styleResolver = (node) => getEffectiveStyleForMode(node, cardStyles, mode);
1287
1805
  traverseCard(views, (node, context) => {
1288
1806
  const effectiveStyle = styleResolver(node);
@@ -1331,12 +1849,12 @@ function validateEffectiveStylesForMode(mode, views, cardStyles, errors, seen) {
1331
1849
  )
1332
1850
  );
1333
1851
  }
1334
- }, styleResolver);
1852
+ }, styleResolver, fragments);
1335
1853
  }
1336
1854
  function validateSecurity(card) {
1337
1855
  const errors = [];
1338
1856
  const seen = /* @__PURE__ */ new Set();
1339
- const { views, state, cardAssets, cardStyles } = card;
1857
+ const { views, state, cardAssets, cardStyles, fragments } = card;
1340
1858
  if (cardAssets) {
1341
1859
  for (const [key, value] of Object.entries(cardAssets)) {
1342
1860
  const assetError = validateAssetPath(value);
@@ -1359,18 +1877,20 @@ function validateSecurity(card) {
1359
1877
  }
1360
1878
  }
1361
1879
  }
1362
- traverseCard(views, (node, context) => {
1880
+ walkRenderableCard(views, fragments, (node, context) => {
1881
+ if (!("type" in node) || typeof node.type !== "string") {
1882
+ return;
1883
+ }
1884
+ const traversableNode = node;
1363
1885
  const { path } = context;
1364
- const { style, type } = node;
1365
- const nodeFields = { ...node };
1366
- delete nodeFields.type;
1367
- delete nodeFields.style;
1368
- delete nodeFields.children;
1886
+ const style = traversableNode.style != null && typeof traversableNode.style === "object" && !Array.isArray(traversableNode.style) ? traversableNode.style : void 0;
1887
+ const type = traversableNode.type;
1888
+ const nodeFields = getScannableNodeFields(traversableNode);
1369
1889
  if (type === "Image" || type === "Avatar") {
1370
1890
  const src = node.src;
1371
1891
  if (typeof src === "string") {
1372
1892
  checkSrcValue(src, type, `${path}.src`, errors);
1373
- } else if (isRef2(src)) {
1893
+ } else if (isRef3(src)) {
1374
1894
  if (state) {
1375
1895
  const resolved = resolveRefFromState(
1376
1896
  src.$ref,
@@ -1383,27 +1903,46 @@ function validateSecurity(card) {
1383
1903
  }
1384
1904
  }
1385
1905
  scanStyleStringsForUrl(style, `${path}.style`, errors);
1386
- const responsive = node.responsive;
1906
+ const responsive = traversableNode.responsive;
1387
1907
  if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
1388
- const compact = responsive.compact;
1389
- if (compact != null && typeof compact === "object" && !Array.isArray(compact)) {
1390
- scanStyleStringsForUrl(
1391
- compact,
1392
- `${path}.responsive.compact`,
1393
- errors
1394
- );
1908
+ for (const mode of RESPONSIVE_OVERRIDE_KEYS3) {
1909
+ const override = responsive[mode];
1910
+ if (override != null && typeof override === "object" && !Array.isArray(override)) {
1911
+ scanStyleStringsForUrl(
1912
+ override,
1913
+ `${path}.responsive.${mode}`,
1914
+ errors
1915
+ );
1916
+ }
1917
+ }
1918
+ }
1919
+ if (type === "Text" && Array.isArray(traversableNode.spans)) {
1920
+ const spans = traversableNode.spans;
1921
+ for (let i = 0; i < spans.length; i++) {
1922
+ const span = spans[i];
1923
+ if (span == null || typeof span !== "object" || Array.isArray(span)) {
1924
+ continue;
1925
+ }
1926
+ const spanStyle = span.style;
1927
+ if (spanStyle != null && typeof spanStyle === "object" && !Array.isArray(spanStyle)) {
1928
+ scanStyleStringsForUrl(
1929
+ spanStyle,
1930
+ `${path}.spans[${i}].style`,
1931
+ errors
1932
+ );
1933
+ }
1395
1934
  }
1396
1935
  }
1397
1936
  scanForRefs(nodeFields, path, errors);
1398
1937
  if (style) {
1399
1938
  scanForRefs(style, `${path}.style`, errors);
1400
1939
  }
1401
- if (node.responsive != null && typeof node.responsive === "object" && !Array.isArray(node.responsive)) {
1402
- scanForRefs(node.responsive, `${path}.responsive`, errors);
1940
+ if (responsive != null && typeof responsive === "object" && !Array.isArray(responsive)) {
1941
+ scanForRefs(responsive, `${path}.responsive`, errors);
1403
1942
  }
1404
1943
  });
1405
1944
  for (const mode of RESPONSIVE_MODES) {
1406
- validateEffectiveStylesForMode(mode, views, cardStyles, errors, seen);
1945
+ validateEffectiveStylesForMode(mode, views, cardStyles, fragments, errors, seen);
1407
1946
  }
1408
1947
  return errors;
1409
1948
  }
@@ -1417,8 +1956,7 @@ import {
1417
1956
  MAX_NESTED_LOOPS,
1418
1957
  MAX_OVERFLOW_AUTO_COUNT,
1419
1958
  MAX_STACK_NESTING,
1420
- PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2,
1421
- isRef as isRef3
1959
+ PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2
1422
1960
  } from "@safe-ugc-ui/types";
1423
1961
  function utf8ByteLength(str) {
1424
1962
  let bytes = 0;
@@ -1437,6 +1975,54 @@ function utf8ByteLength(str) {
1437
1975
  }
1438
1976
  return bytes;
1439
1977
  }
1978
+ function isTemplateObject(value) {
1979
+ return typeof value === "object" && value !== null && "$template" in value && Array.isArray(value.$template);
1980
+ }
1981
+ function countLiteralTemplatedStringBytes(value) {
1982
+ if (typeof value === "string") {
1983
+ return utf8ByteLength(value);
1984
+ }
1985
+ if (!isTemplateObject(value)) {
1986
+ return 0;
1987
+ }
1988
+ return value.$template.reduce((total, part) => {
1989
+ if (typeof part === "string") {
1990
+ return total + utf8ByteLength(part);
1991
+ }
1992
+ if (typeof part === "number" || typeof part === "boolean" || part === null) {
1993
+ return total + utf8ByteLength(String(part));
1994
+ }
1995
+ return total;
1996
+ }, 0);
1997
+ }
1998
+ function countTextNodeLiteralBytes(node) {
1999
+ if (Array.isArray(node.spans)) {
2000
+ return node.spans.reduce((total, span) => {
2001
+ if (span == null || typeof span !== "object" || Array.isArray(span)) {
2002
+ return total;
2003
+ }
2004
+ return total + countLiteralTemplatedStringBytes(
2005
+ span.text
2006
+ );
2007
+ }, 0);
2008
+ }
2009
+ return countLiteralTemplatedStringBytes(node.content);
2010
+ }
2011
+ function countTextSpanStyleBytes(node) {
2012
+ if (!Array.isArray(node.spans)) {
2013
+ return 0;
2014
+ }
2015
+ return node.spans.reduce((total, span) => {
2016
+ if (span == null || typeof span !== "object" || Array.isArray(span)) {
2017
+ return total;
2018
+ }
2019
+ const style = span.style;
2020
+ if (style == null || typeof style !== "object" || Array.isArray(style)) {
2021
+ return total;
2022
+ }
2023
+ return total + utf8ByteLength(JSON.stringify(style));
2024
+ }, 0);
2025
+ }
1440
2026
  function resolveRefFromState2(refPath, state) {
1441
2027
  const path = refPath.startsWith("$") ? refPath.slice(1) : refPath;
1442
2028
  const dotSegments = path.split(".");
@@ -1471,74 +2057,53 @@ function resolveRefFromState2(refPath, state) {
1471
2057
  }
1472
2058
  return current;
1473
2059
  }
1474
- function countTemplateMetrics(template, cardStyles) {
2060
+ function countTemplateMetrics(template, cardStyles, fragments) {
1475
2061
  const result = {
1476
2062
  nodes: 0,
1477
2063
  textBytes: 0,
1478
2064
  styleBytes: 0,
1479
2065
  overflowAutoCount: {
1480
2066
  default: 0,
2067
+ medium: 0,
1481
2068
  compact: 0
1482
2069
  }
1483
2070
  };
1484
- 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(
2071
+ traverseCard(
2072
+ { __template: template },
2073
+ (node) => {
2074
+ result.nodes += 1;
2075
+ if (node.type === "Text") {
2076
+ result.textBytes += countTextNodeLiteralBytes(node);
2077
+ result.styleBytes += countTextSpanStyleBytes(node);
2078
+ }
2079
+ const baseStyleForBytes = getEffectiveStyleForMode(
1512
2080
  node,
1513
2081
  cardStyles,
1514
- mode
2082
+ "default"
1515
2083
  );
1516
- if (effectiveStyle?.overflow === "auto") {
1517
- result.overflowAutoCount[mode]++;
2084
+ if (baseStyleForBytes) {
2085
+ result.styleBytes += utf8ByteLength(JSON.stringify(baseStyleForBytes));
2086
+ }
2087
+ for (const mode of ["medium", "compact"]) {
2088
+ const responsiveStyleForBytes = getMergedResponsiveStyleOverride(
2089
+ node,
2090
+ cardStyles,
2091
+ mode
2092
+ );
2093
+ if (responsiveStyleForBytes) {
2094
+ result.styleBytes += utf8ByteLength(JSON.stringify(responsiveStyleForBytes));
2095
+ }
1518
2096
  }
1519
- }
1520
- }
1521
- const children = node.children;
1522
- if (Array.isArray(children)) {
1523
- for (const child of children) {
1524
- const childMetrics = countTemplateMetrics(child, cardStyles);
1525
- result.nodes += childMetrics.nodes;
1526
- result.textBytes += childMetrics.textBytes;
1527
- result.styleBytes += childMetrics.styleBytes;
1528
2097
  for (const mode of RESPONSIVE_MODES) {
1529
- result.overflowAutoCount[mode] += childMetrics.overflowAutoCount[mode];
2098
+ const effectiveStyle = getEffectiveStyleForMode(node, cardStyles, mode);
2099
+ if (effectiveStyle?.overflow === "auto") {
2100
+ result.overflowAutoCount[mode]++;
2101
+ }
1530
2102
  }
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
- }
2103
+ },
2104
+ void 0,
2105
+ fragments
2106
+ );
1542
2107
  return result;
1543
2108
  }
1544
2109
  function validateLimits(card) {
@@ -1548,15 +2113,14 @@ function validateLimits(card) {
1548
2113
  let styleObjectsBytes = 0;
1549
2114
  const overflowAutoCount = {
1550
2115
  default: 0,
2116
+ medium: 0,
1551
2117
  compact: 0
1552
2118
  };
1553
2119
  traverseCard(card.views, (node, context) => {
1554
2120
  nodeCount++;
1555
2121
  if (node.type === "Text") {
1556
- const content = node.content;
1557
- if (typeof content === "string" && !isRef3(content)) {
1558
- textContentBytes += utf8ByteLength(content);
1559
- }
2122
+ textContentBytes += countTextNodeLiteralBytes(node);
2123
+ styleObjectsBytes += countTextSpanStyleBytes(node);
1560
2124
  }
1561
2125
  {
1562
2126
  const baseStyleForBytes = getEffectiveStyleForMode(
@@ -1567,12 +2131,15 @@ function validateLimits(card) {
1567
2131
  if (baseStyleForBytes) {
1568
2132
  styleObjectsBytes += utf8ByteLength(JSON.stringify(baseStyleForBytes));
1569
2133
  }
1570
- const compactStyleForBytes = getMergedCompactResponsiveStyle(
1571
- node,
1572
- card.cardStyles
1573
- );
1574
- if (compactStyleForBytes) {
1575
- styleObjectsBytes += utf8ByteLength(JSON.stringify(compactStyleForBytes));
2134
+ for (const mode of ["medium", "compact"]) {
2135
+ const responsiveStyleForBytes = getMergedResponsiveStyleOverride(
2136
+ node,
2137
+ card.cardStyles,
2138
+ mode
2139
+ );
2140
+ if (responsiveStyleForBytes) {
2141
+ styleObjectsBytes += utf8ByteLength(JSON.stringify(responsiveStyleForBytes));
2142
+ }
1576
2143
  }
1577
2144
  }
1578
2145
  const children = node.children;
@@ -1611,7 +2178,7 @@ function validateLimits(card) {
1611
2178
  )
1612
2179
  );
1613
2180
  } else if (source.length > 1) {
1614
- const tplMetrics = countTemplateMetrics(forLoop.template, card.cardStyles);
2181
+ const tplMetrics = countTemplateMetrics(forLoop.template, card.cardStyles, card.fragments);
1615
2182
  const multiplier = source.length - 1;
1616
2183
  nodeCount += tplMetrics.nodes * multiplier;
1617
2184
  textContentBytes += tplMetrics.textBytes * multiplier;
@@ -1647,7 +2214,7 @@ function validateLimits(card) {
1647
2214
  )
1648
2215
  );
1649
2216
  }
1650
- });
2217
+ }, void 0, card.fragments);
1651
2218
  if (nodeCount > MAX_NODE_COUNT) {
1652
2219
  errors.push(
1653
2220
  createError(
@@ -1713,17 +2280,26 @@ function runAllChecks(input) {
1713
2280
  const obj = input;
1714
2281
  const views = obj.views;
1715
2282
  const cardStyles = obj.styles;
2283
+ const fragments = obj.fragments;
1716
2284
  const errors = [];
1717
- errors.push(...validateNodes(views));
1718
- errors.push(...validateValueTypes(views));
1719
- errors.push(...validateStyles(views, cardStyles));
2285
+ errors.push(...validateFragments(views, fragments));
2286
+ errors.push(...validateNodes(views, fragments));
2287
+ errors.push(...validateConditions(views, fragments));
2288
+ errors.push(...validateValueTypes(views, fragments));
2289
+ errors.push(...validateStyles(views, cardStyles, fragments));
1720
2290
  errors.push(...validateSecurity({
1721
2291
  views,
1722
2292
  state: obj.state,
1723
2293
  cardAssets: obj.assets,
1724
- cardStyles
2294
+ cardStyles,
2295
+ fragments
2296
+ }));
2297
+ errors.push(...validateLimits({
2298
+ state: obj.state,
2299
+ views,
2300
+ cardStyles,
2301
+ fragments
1725
2302
  }));
1726
- errors.push(...validateLimits({ state: obj.state, views, cardStyles }));
1727
2303
  return errors;
1728
2304
  }
1729
2305
  function validate(input) {
@@ -1766,6 +2342,8 @@ export {
1766
2342
  traverseNode,
1767
2343
  validResult,
1768
2344
  validate,
2345
+ validateConditions,
2346
+ validateFragments,
1769
2347
  validateLimits,
1770
2348
  validateNodes,
1771
2349
  validateRaw,