@safe-ugc-ui/validator 0.5.1 → 0.6.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 +5 -3
- package/dist/index.js +312 -132
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -101,6 +101,7 @@ interface TraversableNode {
|
|
|
101
101
|
type: string;
|
|
102
102
|
children?: TraversableNode[] | ForLoopLike;
|
|
103
103
|
style?: Record<string, unknown>;
|
|
104
|
+
responsive?: Record<string, unknown>;
|
|
104
105
|
[key: string]: unknown;
|
|
105
106
|
}
|
|
106
107
|
interface ForLoopLike {
|
|
@@ -113,17 +114,18 @@ interface ForLoopLike {
|
|
|
113
114
|
* Return `false` to skip traversing into this node's children.
|
|
114
115
|
*/
|
|
115
116
|
type NodeVisitor = (node: TraversableNode, context: TraversalContext) => void | false;
|
|
117
|
+
type StyleResolver = (node: TraversableNode) => Record<string, unknown> | undefined;
|
|
116
118
|
/**
|
|
117
119
|
* Recursively traverse a single node and its descendants.
|
|
118
120
|
*/
|
|
119
|
-
declare function traverseNode(node: TraversableNode, context: TraversalContext, visitor: NodeVisitor): void;
|
|
121
|
+
declare function traverseNode(node: TraversableNode, context: TraversalContext, visitor: NodeVisitor, styleResolver?: StyleResolver): void;
|
|
120
122
|
/**
|
|
121
123
|
* Traverse all nodes in every view of a card.
|
|
122
124
|
*
|
|
123
125
|
* @param views - The `views` object from a UGCCard (mapping view names to root nodes).
|
|
124
126
|
* @param visitor - Called for every node in every view.
|
|
125
127
|
*/
|
|
126
|
-
declare function traverseCard(views: Record<string, unknown>, visitor: NodeVisitor): void;
|
|
128
|
+
declare function traverseCard(views: Record<string, unknown>, visitor: NodeVisitor, styleResolver?: StyleResolver): void;
|
|
127
129
|
|
|
128
130
|
/**
|
|
129
131
|
* @safe-ugc-ui/validator — Schema (Structural) Validation
|
|
@@ -360,4 +362,4 @@ declare function validate(input: unknown): ValidationResult;
|
|
|
360
362
|
*/
|
|
361
363
|
declare function validateRaw(rawJson: string): ValidationResult;
|
|
362
364
|
|
|
363
|
-
export { type NodeVisitor, type TraversableNode, type TraversalContext, type ValidationError, type ValidationErrorCode, type ValidationResult, createError, invalidResult, merge, parseCard, toResult, traverseCard, traverseNode, validResult, validate, validateLimits, validateNodes, validateRaw, validateSchema, validateSecurity, validateStyles, validateValueTypes };
|
|
365
|
+
export { type NodeVisitor, type StyleResolver, type TraversableNode, type TraversalContext, type ValidationError, type ValidationErrorCode, type ValidationResult, createError, invalidResult, merge, parseCard, toResult, traverseCard, traverseNode, validResult, validate, validateLimits, validateNodes, validateRaw, validateSchema, validateSecurity, validateStyles, validateValueTypes };
|
package/dist/index.js
CHANGED
|
@@ -110,7 +110,7 @@ function isForLoop(children) {
|
|
|
110
110
|
function hasOverflowAuto(style) {
|
|
111
111
|
return style?.overflow === "auto";
|
|
112
112
|
}
|
|
113
|
-
function traverseNode(node, context, visitor) {
|
|
113
|
+
function traverseNode(node, context, visitor, styleResolver) {
|
|
114
114
|
const result = visitor(node, context);
|
|
115
115
|
if (result === false) {
|
|
116
116
|
return;
|
|
@@ -120,7 +120,7 @@ function traverseNode(node, context, visitor) {
|
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
122
|
const nextStackDepth = node.type === "Stack" ? context.stackDepth + 1 : context.stackDepth;
|
|
123
|
-
const nextOverflowAuto = context.overflowAutoAncestor || hasOverflowAuto(node.style);
|
|
123
|
+
const nextOverflowAuto = context.overflowAutoAncestor || hasOverflowAuto(styleResolver ? styleResolver(node) : node.style);
|
|
124
124
|
if (isForLoop(children)) {
|
|
125
125
|
const childCtx = {
|
|
126
126
|
path: `${context.path}.children.template`,
|
|
@@ -130,7 +130,7 @@ function traverseNode(node, context, visitor) {
|
|
|
130
130
|
overflowAutoAncestor: nextOverflowAuto,
|
|
131
131
|
stackDepth: nextStackDepth
|
|
132
132
|
};
|
|
133
|
-
traverseNode(children.template, childCtx, visitor);
|
|
133
|
+
traverseNode(children.template, childCtx, visitor, styleResolver);
|
|
134
134
|
} else if (Array.isArray(children)) {
|
|
135
135
|
for (let i = 0; i < children.length; i++) {
|
|
136
136
|
const child = children[i];
|
|
@@ -143,12 +143,12 @@ function traverseNode(node, context, visitor) {
|
|
|
143
143
|
overflowAutoAncestor: nextOverflowAuto,
|
|
144
144
|
stackDepth: nextStackDepth
|
|
145
145
|
};
|
|
146
|
-
traverseNode(child, childCtx, visitor);
|
|
146
|
+
traverseNode(child, childCtx, visitor, styleResolver);
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
function traverseCard(views, visitor) {
|
|
151
|
+
function traverseCard(views, visitor, styleResolver) {
|
|
152
152
|
for (const [viewName, rootNode] of Object.entries(views)) {
|
|
153
153
|
if (rootNode == null || typeof rootNode !== "object" || !("type" in rootNode)) {
|
|
154
154
|
continue;
|
|
@@ -161,7 +161,7 @@ function traverseCard(views, visitor) {
|
|
|
161
161
|
overflowAutoAncestor: false,
|
|
162
162
|
stackDepth: 0
|
|
163
163
|
};
|
|
164
|
-
traverseNode(rootNode, context, visitor);
|
|
164
|
+
traverseNode(rootNode, context, visitor, styleResolver);
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
@@ -287,8 +287,7 @@ var STRUCTURED_OBJECT_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
|
287
287
|
"boxShadow",
|
|
288
288
|
"textShadow"
|
|
289
289
|
]);
|
|
290
|
-
function validateNodeStyle(
|
|
291
|
-
const style = node.style;
|
|
290
|
+
function validateNodeStyle(style, path, errors) {
|
|
292
291
|
if (!style) {
|
|
293
292
|
return;
|
|
294
293
|
}
|
|
@@ -302,7 +301,7 @@ function validateNodeStyle(node, ctx, errors) {
|
|
|
302
301
|
createError(
|
|
303
302
|
"DYNAMIC_NOT_ALLOWED",
|
|
304
303
|
`Style property "${prop}" must be a static literal; $ref is not allowed.`,
|
|
305
|
-
`${
|
|
304
|
+
`${path}.${prop}`
|
|
306
305
|
)
|
|
307
306
|
);
|
|
308
307
|
}
|
|
@@ -311,7 +310,7 @@ function validateNodeStyle(node, ctx, errors) {
|
|
|
311
310
|
createError(
|
|
312
311
|
"DYNAMIC_NOT_ALLOWED",
|
|
313
312
|
`Style property "${prop}" must be an object literal; use $ref only inside its nested fields.`,
|
|
314
|
-
`${
|
|
313
|
+
`${path}.${prop}`
|
|
315
314
|
)
|
|
316
315
|
);
|
|
317
316
|
}
|
|
@@ -321,7 +320,18 @@ function validateNodeStyle(node, ctx, errors) {
|
|
|
321
320
|
function validateValueTypes(views) {
|
|
322
321
|
const errors = [];
|
|
323
322
|
traverseCard(views, (node, ctx) => {
|
|
324
|
-
validateNodeStyle(node, ctx
|
|
323
|
+
validateNodeStyle(node.style, `${ctx.path}.style`, errors);
|
|
324
|
+
const responsive = node.responsive;
|
|
325
|
+
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
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
325
335
|
});
|
|
326
336
|
return errors;
|
|
327
337
|
}
|
|
@@ -578,8 +588,15 @@ function collectNestedStyleRefErrors(value, path, errors) {
|
|
|
578
588
|
collectNestedStyleRefErrors(child, childPath, errors);
|
|
579
589
|
}
|
|
580
590
|
}
|
|
581
|
-
function validateSingleStyle(style, stylePath, errors, cardStyles,
|
|
582
|
-
const
|
|
591
|
+
function validateSingleStyle(style, stylePath, errors, cardStyles, options = {}) {
|
|
592
|
+
const {
|
|
593
|
+
allowHoverStyle = true,
|
|
594
|
+
allowTransition = true,
|
|
595
|
+
allowHoverStyleRefs = true
|
|
596
|
+
} = options;
|
|
597
|
+
const STRUCTURED_FIELDS = /* @__PURE__ */ new Set();
|
|
598
|
+
if (allowTransition) STRUCTURED_FIELDS.add("transition");
|
|
599
|
+
if (allowHoverStyle) STRUCTURED_FIELDS.add("hoverStyle");
|
|
583
600
|
for (const key of Object.keys(style)) {
|
|
584
601
|
if (!STRUCTURED_FIELDS.has(key) && FORBIDDEN_STYLE_PROPERTIES.includes(key)) {
|
|
585
602
|
errors.push(
|
|
@@ -591,6 +608,24 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverSty
|
|
|
591
608
|
);
|
|
592
609
|
}
|
|
593
610
|
}
|
|
611
|
+
if (!allowHoverStyle && "hoverStyle" in style) {
|
|
612
|
+
errors.push(
|
|
613
|
+
createError(
|
|
614
|
+
"INVALID_VALUE",
|
|
615
|
+
`hoverStyle is not allowed inside responsive overrides at "${stylePath}.hoverStyle"`,
|
|
616
|
+
`${stylePath}.hoverStyle`
|
|
617
|
+
)
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (!allowTransition && "transition" in style) {
|
|
621
|
+
errors.push(
|
|
622
|
+
createError(
|
|
623
|
+
"INVALID_VALUE",
|
|
624
|
+
`transition is not allowed inside responsive overrides at "${stylePath}.transition"`,
|
|
625
|
+
`${stylePath}.transition`
|
|
626
|
+
)
|
|
627
|
+
);
|
|
628
|
+
}
|
|
594
629
|
if ("zIndex" in style && isLiteralNumber(style.zIndex)) {
|
|
595
630
|
const v = style.zIndex;
|
|
596
631
|
if (v < ZINDEX_MIN || v > ZINDEX_MAX) {
|
|
@@ -872,7 +907,7 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverSty
|
|
|
872
907
|
}
|
|
873
908
|
}
|
|
874
909
|
}
|
|
875
|
-
if ("hoverStyle" in style && style.hoverStyle != null) {
|
|
910
|
+
if (allowHoverStyle && "hoverStyle" in style && style.hoverStyle != null) {
|
|
876
911
|
const hoverStyle = style.hoverStyle;
|
|
877
912
|
const hoverPath = `${stylePath}.hoverStyle`;
|
|
878
913
|
if (typeof hoverStyle !== "object" || Array.isArray(hoverStyle)) {
|
|
@@ -901,12 +936,16 @@ function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverSty
|
|
|
901
936
|
hoverPath,
|
|
902
937
|
errors,
|
|
903
938
|
cardStyles,
|
|
904
|
-
|
|
939
|
+
{
|
|
940
|
+
allowHoverStyle,
|
|
941
|
+
allowTransition,
|
|
942
|
+
allowHoverStyleRefs
|
|
943
|
+
}
|
|
905
944
|
);
|
|
906
945
|
}
|
|
907
946
|
}
|
|
908
947
|
}
|
|
909
|
-
if ("transition" in style && style.transition != null) {
|
|
948
|
+
if (allowTransition && "transition" in style && style.transition != null) {
|
|
910
949
|
const transition = style.transition;
|
|
911
950
|
const transPath = `${stylePath}.transition`;
|
|
912
951
|
if (typeof transition === "string") {
|
|
@@ -992,18 +1031,41 @@ function validateStyles(views, cardStyles) {
|
|
|
992
1031
|
);
|
|
993
1032
|
}
|
|
994
1033
|
collectNestedStyleRefErrors(styleEntry, entryPath, errors);
|
|
995
|
-
validateSingleStyle(styleEntry, entryPath, errors, void 0,
|
|
1034
|
+
validateSingleStyle(styleEntry, entryPath, errors, void 0, {
|
|
1035
|
+
allowHoverStyleRefs: false
|
|
1036
|
+
});
|
|
996
1037
|
}
|
|
997
1038
|
}
|
|
998
1039
|
traverseCard(views, (node, ctx) => {
|
|
999
1040
|
const style = node.style;
|
|
1000
|
-
if (style
|
|
1041
|
+
if (style != null && typeof style === "object") {
|
|
1042
|
+
const stylePath = `${ctx.path}.style`;
|
|
1043
|
+
const mergedStyle = resolveStyleRef(style, stylePath, cardStyles, errors);
|
|
1044
|
+
if (mergedStyle) {
|
|
1045
|
+
validateSingleStyle(mergedStyle, stylePath, errors, cardStyles);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
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)) {
|
|
1001
1054
|
return;
|
|
1002
1055
|
}
|
|
1003
|
-
const
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
+
});
|
|
1007
1069
|
}
|
|
1008
1070
|
});
|
|
1009
1071
|
return errors;
|
|
@@ -1014,6 +1076,72 @@ import {
|
|
|
1014
1076
|
PROTOTYPE_POLLUTION_SEGMENTS,
|
|
1015
1077
|
isRef as isRef2
|
|
1016
1078
|
} from "@safe-ugc-ui/types";
|
|
1079
|
+
|
|
1080
|
+
// src/responsive-utils.ts
|
|
1081
|
+
var RESPONSIVE_MODES = [
|
|
1082
|
+
"default",
|
|
1083
|
+
"compact"
|
|
1084
|
+
];
|
|
1085
|
+
function mergeNamedStyle(style, cardStyles) {
|
|
1086
|
+
if (!style) return void 0;
|
|
1087
|
+
const rawStyleName = style.$style;
|
|
1088
|
+
const styleName = typeof rawStyleName === "string" ? rawStyleName.trim() : rawStyleName;
|
|
1089
|
+
if (!styleName || typeof styleName !== "string" || !cardStyles) {
|
|
1090
|
+
if (style.$style !== void 0) {
|
|
1091
|
+
const { $style: _2, ...rest } = style;
|
|
1092
|
+
return rest;
|
|
1093
|
+
}
|
|
1094
|
+
return style;
|
|
1095
|
+
}
|
|
1096
|
+
const baseStyle = cardStyles[styleName];
|
|
1097
|
+
if (!baseStyle) {
|
|
1098
|
+
const { $style: _2, ...rest } = style;
|
|
1099
|
+
return rest;
|
|
1100
|
+
}
|
|
1101
|
+
const { $style: _, ...inlineWithoutStyleRef } = style;
|
|
1102
|
+
return { ...baseStyle, ...inlineWithoutStyleRef };
|
|
1103
|
+
}
|
|
1104
|
+
function getCompactResponsiveStyle(node) {
|
|
1105
|
+
const responsive = node.responsive;
|
|
1106
|
+
if (responsive == null || typeof responsive !== "object" || Array.isArray(responsive)) {
|
|
1107
|
+
return void 0;
|
|
1108
|
+
}
|
|
1109
|
+
const compact = responsive.compact;
|
|
1110
|
+
if (compact == null || typeof compact !== "object" || Array.isArray(compact)) {
|
|
1111
|
+
return void 0;
|
|
1112
|
+
}
|
|
1113
|
+
return compact;
|
|
1114
|
+
}
|
|
1115
|
+
function stripResponsiveOnlyUnsupportedFields(style) {
|
|
1116
|
+
if (!style) return void 0;
|
|
1117
|
+
const {
|
|
1118
|
+
hoverStyle: _hoverStyle,
|
|
1119
|
+
transition: _transition,
|
|
1120
|
+
...rest
|
|
1121
|
+
} = style;
|
|
1122
|
+
return rest;
|
|
1123
|
+
}
|
|
1124
|
+
function getEffectiveStyleForMode(node, cardStyles, mode) {
|
|
1125
|
+
const baseStyle = mergeNamedStyle(node.style, cardStyles);
|
|
1126
|
+
if (mode === "default") {
|
|
1127
|
+
return baseStyle;
|
|
1128
|
+
}
|
|
1129
|
+
const compactStyle = stripResponsiveOnlyUnsupportedFields(
|
|
1130
|
+
mergeNamedStyle(getCompactResponsiveStyle(node), cardStyles)
|
|
1131
|
+
);
|
|
1132
|
+
if (!compactStyle) {
|
|
1133
|
+
return baseStyle;
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
...baseStyle ?? {},
|
|
1137
|
+
...compactStyle
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
function getMergedCompactResponsiveStyle(node, cardStyles) {
|
|
1141
|
+
return mergeNamedStyle(getCompactResponsiveStyle(node), cardStyles);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/security.ts
|
|
1017
1145
|
var FORBIDDEN_URL_PREFIXES = [
|
|
1018
1146
|
"http://",
|
|
1019
1147
|
"https://",
|
|
@@ -1130,8 +1258,84 @@ function checkSrcValue(resolved, type, errorPath, errors) {
|
|
|
1130
1258
|
}
|
|
1131
1259
|
}
|
|
1132
1260
|
}
|
|
1261
|
+
function pushUniqueError(errors, seen, error) {
|
|
1262
|
+
const key = `${error.code}|${error.path}|${error.message}`;
|
|
1263
|
+
if (seen.has(key)) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
seen.add(key);
|
|
1267
|
+
errors.push(error);
|
|
1268
|
+
}
|
|
1269
|
+
function scanStyleStringsForUrl(style, path, errors) {
|
|
1270
|
+
if (!style) {
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
for (const [prop, value] of Object.entries(style)) {
|
|
1274
|
+
if (typeof value === "string" && value.toLowerCase().includes("url(")) {
|
|
1275
|
+
errors.push(
|
|
1276
|
+
createError(
|
|
1277
|
+
"FORBIDDEN_CSS_FUNCTION",
|
|
1278
|
+
`CSS url() function is forbidden in style values. Found in "${prop}".`,
|
|
1279
|
+
`${path}.${prop}`
|
|
1280
|
+
)
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
function validateEffectiveStylesForMode(mode, views, cardStyles, errors, seen) {
|
|
1286
|
+
const styleResolver = (node) => getEffectiveStyleForMode(node, cardStyles, mode);
|
|
1287
|
+
traverseCard(views, (node, context) => {
|
|
1288
|
+
const effectiveStyle = styleResolver(node);
|
|
1289
|
+
if (effectiveStyle && typeof effectiveStyle.position === "string") {
|
|
1290
|
+
const position = effectiveStyle.position;
|
|
1291
|
+
if (position === "fixed") {
|
|
1292
|
+
pushUniqueError(
|
|
1293
|
+
errors,
|
|
1294
|
+
seen,
|
|
1295
|
+
createError(
|
|
1296
|
+
"POSITION_FIXED_FORBIDDEN",
|
|
1297
|
+
'CSS position "fixed" is not allowed.',
|
|
1298
|
+
`${context.path}.style.position`
|
|
1299
|
+
)
|
|
1300
|
+
);
|
|
1301
|
+
} else if (position === "sticky") {
|
|
1302
|
+
pushUniqueError(
|
|
1303
|
+
errors,
|
|
1304
|
+
seen,
|
|
1305
|
+
createError(
|
|
1306
|
+
"POSITION_STICKY_FORBIDDEN",
|
|
1307
|
+
'CSS position "sticky" is not allowed.',
|
|
1308
|
+
`${context.path}.style.position`
|
|
1309
|
+
)
|
|
1310
|
+
);
|
|
1311
|
+
} else if (position === "absolute" && context.parentType !== "Stack") {
|
|
1312
|
+
pushUniqueError(
|
|
1313
|
+
errors,
|
|
1314
|
+
seen,
|
|
1315
|
+
createError(
|
|
1316
|
+
"POSITION_ABSOLUTE_NOT_IN_STACK",
|
|
1317
|
+
'CSS position "absolute" is only allowed inside a Stack component.',
|
|
1318
|
+
`${context.path}.style.position`
|
|
1319
|
+
)
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (effectiveStyle && effectiveStyle.overflow === "auto" && context.overflowAutoAncestor) {
|
|
1324
|
+
pushUniqueError(
|
|
1325
|
+
errors,
|
|
1326
|
+
seen,
|
|
1327
|
+
createError(
|
|
1328
|
+
"OVERFLOW_AUTO_NESTED",
|
|
1329
|
+
"Nested overflow:auto is not allowed. An ancestor already has overflow:auto.",
|
|
1330
|
+
`${context.path}.style.overflow`
|
|
1331
|
+
)
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
}, styleResolver);
|
|
1335
|
+
}
|
|
1133
1336
|
function validateSecurity(card) {
|
|
1134
1337
|
const errors = [];
|
|
1338
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1135
1339
|
const { views, state, cardAssets, cardStyles } = card;
|
|
1136
1340
|
if (cardAssets) {
|
|
1137
1341
|
for (const [key, value] of Object.entries(cardAssets)) {
|
|
@@ -1178,74 +1382,29 @@ function validateSecurity(card) {
|
|
|
1178
1382
|
}
|
|
1179
1383
|
}
|
|
1180
1384
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
let effectiveStyle = style;
|
|
1195
|
-
if (style && typeof style.$style === "string" && cardStyles && style.$style.trim() in cardStyles) {
|
|
1196
|
-
const refName = style.$style.trim();
|
|
1197
|
-
const merged = { ...cardStyles[refName] };
|
|
1198
|
-
for (const [key, value] of Object.entries(style)) {
|
|
1199
|
-
if (key !== "$style") {
|
|
1200
|
-
merged[key] = value;
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
effectiveStyle = merged;
|
|
1204
|
-
}
|
|
1205
|
-
if (effectiveStyle && typeof effectiveStyle.position === "string") {
|
|
1206
|
-
const position = effectiveStyle.position;
|
|
1207
|
-
if (position === "fixed") {
|
|
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
|
-
)
|
|
1385
|
+
scanStyleStringsForUrl(style, `${path}.style`, errors);
|
|
1386
|
+
const responsive = node.responsive;
|
|
1387
|
+
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
|
|
1222
1394
|
);
|
|
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
|
-
)
|
|
1231
|
-
);
|
|
1232
|
-
}
|
|
1233
1395
|
}
|
|
1234
1396
|
}
|
|
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
1397
|
scanForRefs(nodeFields, path, errors);
|
|
1245
1398
|
if (style) {
|
|
1246
1399
|
scanForRefs(style, `${path}.style`, errors);
|
|
1247
1400
|
}
|
|
1401
|
+
if (node.responsive != null && typeof node.responsive === "object" && !Array.isArray(node.responsive)) {
|
|
1402
|
+
scanForRefs(node.responsive, `${path}.responsive`, errors);
|
|
1403
|
+
}
|
|
1248
1404
|
});
|
|
1405
|
+
for (const mode of RESPONSIVE_MODES) {
|
|
1406
|
+
validateEffectiveStylesForMode(mode, views, cardStyles, errors, seen);
|
|
1407
|
+
}
|
|
1249
1408
|
return errors;
|
|
1250
1409
|
}
|
|
1251
1410
|
|
|
@@ -1313,7 +1472,15 @@ function resolveRefFromState2(refPath, state) {
|
|
|
1313
1472
|
return current;
|
|
1314
1473
|
}
|
|
1315
1474
|
function countTemplateMetrics(template, cardStyles) {
|
|
1316
|
-
const result = {
|
|
1475
|
+
const result = {
|
|
1476
|
+
nodes: 0,
|
|
1477
|
+
textBytes: 0,
|
|
1478
|
+
styleBytes: 0,
|
|
1479
|
+
overflowAutoCount: {
|
|
1480
|
+
default: 0,
|
|
1481
|
+
compact: 0
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1317
1484
|
if (template == null || typeof template !== "object") return result;
|
|
1318
1485
|
const node = template;
|
|
1319
1486
|
if (!node.type) return result;
|
|
@@ -1324,27 +1491,31 @@ function countTemplateMetrics(template, cardStyles) {
|
|
|
1324
1491
|
result.textBytes = utf8ByteLength(content);
|
|
1325
1492
|
}
|
|
1326
1493
|
}
|
|
1327
|
-
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
}
|
|
1336
|
-
styleForBytes = merged;
|
|
1337
|
-
}
|
|
1338
|
-
result.styleBytes = utf8ByteLength(JSON.stringify(styleForBytes));
|
|
1339
|
-
let effectiveOverflow = style.overflow;
|
|
1340
|
-
if (typeof style.$style === "string" && cardStyles && style.$style.trim() in cardStyles) {
|
|
1341
|
-
const refName = style.$style.trim();
|
|
1342
|
-
if (!("overflow" in style) || style.overflow === void 0) {
|
|
1343
|
-
effectiveOverflow = cardStyles[refName].overflow;
|
|
1344
|
-
}
|
|
1494
|
+
{
|
|
1495
|
+
const baseStyleForBytes = getEffectiveStyleForMode(
|
|
1496
|
+
node,
|
|
1497
|
+
cardStyles,
|
|
1498
|
+
"default"
|
|
1499
|
+
);
|
|
1500
|
+
if (baseStyleForBytes) {
|
|
1501
|
+
result.styleBytes += utf8ByteLength(JSON.stringify(baseStyleForBytes));
|
|
1345
1502
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
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(
|
|
1512
|
+
node,
|
|
1513
|
+
cardStyles,
|
|
1514
|
+
mode
|
|
1515
|
+
);
|
|
1516
|
+
if (effectiveStyle?.overflow === "auto") {
|
|
1517
|
+
result.overflowAutoCount[mode]++;
|
|
1518
|
+
}
|
|
1348
1519
|
}
|
|
1349
1520
|
}
|
|
1350
1521
|
const children = node.children;
|
|
@@ -1354,7 +1525,9 @@ function countTemplateMetrics(template, cardStyles) {
|
|
|
1354
1525
|
result.nodes += childMetrics.nodes;
|
|
1355
1526
|
result.textBytes += childMetrics.textBytes;
|
|
1356
1527
|
result.styleBytes += childMetrics.styleBytes;
|
|
1357
|
-
|
|
1528
|
+
for (const mode of RESPONSIVE_MODES) {
|
|
1529
|
+
result.overflowAutoCount[mode] += childMetrics.overflowAutoCount[mode];
|
|
1530
|
+
}
|
|
1358
1531
|
}
|
|
1359
1532
|
} else if (children != null && typeof children === "object" && !Array.isArray(children) && "template" in children) {
|
|
1360
1533
|
const innerTemplate = children.template;
|
|
@@ -1362,7 +1535,9 @@ function countTemplateMetrics(template, cardStyles) {
|
|
|
1362
1535
|
result.nodes += innerMetrics.nodes;
|
|
1363
1536
|
result.textBytes += innerMetrics.textBytes;
|
|
1364
1537
|
result.styleBytes += innerMetrics.styleBytes;
|
|
1365
|
-
|
|
1538
|
+
for (const mode of RESPONSIVE_MODES) {
|
|
1539
|
+
result.overflowAutoCount[mode] += innerMetrics.overflowAutoCount[mode];
|
|
1540
|
+
}
|
|
1366
1541
|
}
|
|
1367
1542
|
return result;
|
|
1368
1543
|
}
|
|
@@ -1371,7 +1546,10 @@ function validateLimits(card) {
|
|
|
1371
1546
|
let nodeCount = 0;
|
|
1372
1547
|
let textContentBytes = 0;
|
|
1373
1548
|
let styleObjectsBytes = 0;
|
|
1374
|
-
|
|
1549
|
+
const overflowAutoCount = {
|
|
1550
|
+
default: 0,
|
|
1551
|
+
compact: 0
|
|
1552
|
+
};
|
|
1375
1553
|
traverseCard(card.views, (node, context) => {
|
|
1376
1554
|
nodeCount++;
|
|
1377
1555
|
if (node.type === "Text") {
|
|
@@ -1380,20 +1558,22 @@ function validateLimits(card) {
|
|
|
1380
1558
|
textContentBytes += utf8ByteLength(content);
|
|
1381
1559
|
}
|
|
1382
1560
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1561
|
+
{
|
|
1562
|
+
const baseStyleForBytes = getEffectiveStyleForMode(
|
|
1563
|
+
node,
|
|
1564
|
+
card.cardStyles,
|
|
1565
|
+
"default"
|
|
1566
|
+
);
|
|
1567
|
+
if (baseStyleForBytes) {
|
|
1568
|
+
styleObjectsBytes += utf8ByteLength(JSON.stringify(baseStyleForBytes));
|
|
1569
|
+
}
|
|
1570
|
+
const compactStyleForBytes = getMergedCompactResponsiveStyle(
|
|
1571
|
+
node,
|
|
1572
|
+
card.cardStyles
|
|
1573
|
+
);
|
|
1574
|
+
if (compactStyleForBytes) {
|
|
1575
|
+
styleObjectsBytes += utf8ByteLength(JSON.stringify(compactStyleForBytes));
|
|
1394
1576
|
}
|
|
1395
|
-
const serialized = JSON.stringify(styleForBytes);
|
|
1396
|
-
styleObjectsBytes += utf8ByteLength(serialized);
|
|
1397
1577
|
}
|
|
1398
1578
|
const children = node.children;
|
|
1399
1579
|
if (children != null && typeof children === "object" && !Array.isArray(children) && "for" in children && "in" in children && "template" in children) {
|
|
@@ -1436,7 +1616,9 @@ function validateLimits(card) {
|
|
|
1436
1616
|
nodeCount += tplMetrics.nodes * multiplier;
|
|
1437
1617
|
textContentBytes += tplMetrics.textBytes * multiplier;
|
|
1438
1618
|
styleObjectsBytes += tplMetrics.styleBytes * multiplier;
|
|
1439
|
-
|
|
1619
|
+
for (const mode of RESPONSIVE_MODES) {
|
|
1620
|
+
overflowAutoCount[mode] += tplMetrics.overflowAutoCount[mode] * multiplier;
|
|
1621
|
+
}
|
|
1440
1622
|
}
|
|
1441
1623
|
}
|
|
1442
1624
|
}
|
|
@@ -1450,16 +1632,10 @@ function validateLimits(card) {
|
|
|
1450
1632
|
);
|
|
1451
1633
|
}
|
|
1452
1634
|
}
|
|
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++;
|
|
1635
|
+
for (const mode of RESPONSIVE_MODES) {
|
|
1636
|
+
const effectiveStyle = getEffectiveStyleForMode(node, card.cardStyles, mode);
|
|
1637
|
+
if (effectiveStyle?.overflow === "auto") {
|
|
1638
|
+
overflowAutoCount[mode]++;
|
|
1463
1639
|
}
|
|
1464
1640
|
}
|
|
1465
1641
|
if (node.type === "Stack" && context.stackDepth >= MAX_STACK_NESTING) {
|
|
@@ -1499,11 +1675,15 @@ function validateLimits(card) {
|
|
|
1499
1675
|
)
|
|
1500
1676
|
);
|
|
1501
1677
|
}
|
|
1502
|
-
|
|
1678
|
+
const maxOverflowAutoCount = Math.max(
|
|
1679
|
+
overflowAutoCount.default,
|
|
1680
|
+
overflowAutoCount.compact
|
|
1681
|
+
);
|
|
1682
|
+
if (maxOverflowAutoCount > MAX_OVERFLOW_AUTO_COUNT) {
|
|
1503
1683
|
errors.push(
|
|
1504
1684
|
createError(
|
|
1505
1685
|
"OVERFLOW_AUTO_COUNT_EXCEEDED",
|
|
1506
|
-
`Card has ${
|
|
1686
|
+
`Card has ${maxOverflowAutoCount} elements with overflow:auto in at least one responsive mode, max is ${MAX_OVERFLOW_AUTO_COUNT}`,
|
|
1507
1687
|
"views"
|
|
1508
1688
|
)
|
|
1509
1689
|
);
|