@yaoxiu/marketing-dsl 1.5.2 → 2.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.cjs CHANGED
@@ -327,13 +327,13 @@ function safeImageUrl(input) {
327
327
  // src/actions.ts
328
328
  function createDispatcher(options) {
329
329
  const { setState, emit, openView, closeTop, closeAll } = options;
330
- function dispatch(action, context) {
330
+ function run(action, context, interaction) {
331
331
  if (!action || !action.type) return;
332
332
  const handlers = options.handlers || {};
333
333
  const editMode = !!options.editMode;
334
334
  switch (action.type) {
335
335
  case "sequence":
336
- (action.actions || []).forEach((item) => dispatch(item, context));
336
+ (action.actions || []).forEach((item) => run(item, context, interaction));
337
337
  return;
338
338
  case "navigate": {
339
339
  const url = safeUrl(interpolate(action.url, context));
@@ -343,6 +343,7 @@ function createDispatcher(options) {
343
343
  }
344
344
  const target = action.target || "_blank";
345
345
  emit("navigate", { url, target });
346
+ if (!interaction.navigate) interaction.navigate = { url, target };
346
347
  if (editMode) return;
347
348
  if (typeof window === "undefined") return;
348
349
  if (target === "_self") window.location.href = url;
@@ -356,20 +357,25 @@ function createDispatcher(options) {
356
357
  return;
357
358
  // 关掉栈顶那层;只剩一层时就是整体关闭
358
359
  case "close":
360
+ interaction.closes = true;
359
361
  closeTop(action.reason);
360
362
  return;
361
363
  case "closeAll":
364
+ interaction.closes = true;
362
365
  closeAll(action.reason);
363
366
  return;
364
367
  case "setState":
365
368
  setState(action.key, interpolate(action.value, context));
366
369
  return;
367
- case "track":
368
- emit("track", {
370
+ case "track": {
371
+ const payload = {
369
372
  event: String(interpolate(action.event, context)),
370
373
  params: interpolateDeep(action.params || {}, context)
371
- });
374
+ };
375
+ emit("track", payload);
376
+ if (!interaction.track) interaction.track = payload;
372
377
  return;
378
+ }
373
379
  case "call": {
374
380
  const handler = handlers[action.name];
375
381
  if (typeof handler !== "function") {
@@ -389,6 +395,13 @@ function createDispatcher(options) {
389
395
  });
390
396
  }
391
397
  }
398
+ function dispatch(action, context, meta) {
399
+ if (!action || !action.type) return;
400
+ const interaction = { trigger: (meta == null ? void 0 : meta.trigger) || "action" };
401
+ if ((meta == null ? void 0 : meta.reason) !== void 0) interaction.reason = meta.reason;
402
+ run(action, context, interaction);
403
+ emit("interaction", interaction);
404
+ }
392
405
  return dispatch;
393
406
  }
394
407
 
@@ -619,9 +632,7 @@ function resolveNodeStyle(node, layout) {
619
632
  function resolveTree(input) {
620
633
  const { normalized, viewStack, ready } = input;
621
634
  const layers = viewStack.map((name) => ({ name, view: normalized.views[name] })).filter((item) => !!item.view);
622
- const hasPopup = Object.keys(normalized.views).some(
623
- (name) => normalized.views[name].type === "popup"
624
- );
635
+ const hasPopup = layers.some((item) => item.view.type === "popup");
625
636
  const countdowns = { endTimes: [], precision: "s" };
626
637
  const renderLayers = ready ? layers.map(
627
638
  (item, index) => resolveLayer(
@@ -635,8 +646,10 @@ function resolveTree(input) {
635
646
  return {
636
647
  ready: ready && layers.length > 0,
637
648
  hasPopup,
638
- // 配置里有弹窗视图时,根容器要撑满宿主容器,弹窗层才有地方铺
639
- rootStyle: hasPopup ? { position: "absolute", top: "0", right: "0", bottom: "0", left: "0" } : { position: "relative" },
649
+ // 根容器恒为 relative:它要待在宿主给的位置上(banner / 公告坑位是流式的普通 div,
650
+ // 根容器一脱流坑位就塌成 0、内容按最近定位祖先飘走)。弹窗铺满视口由弹窗层自己
651
+ // `position: fixed` 完成,不再靠根容器撑开,见 layerStyle。
652
+ rootStyle: { position: "relative" },
640
653
  layers: renderLayers,
641
654
  countdownEndTimes: countdowns.endTimes,
642
655
  countdownPrecision: countdowns.precision
@@ -682,7 +695,15 @@ function resolveLayer(name, view, isTop, input, countdowns) {
682
695
  };
683
696
  }
684
697
  function layerStyle(isPopup, isTop) {
685
- const style = isPopup ? { position: "absolute", top: "0", right: "0", bottom: "0", left: "0" } : {};
698
+ const style = isPopup ? {
699
+ position: "fixed",
700
+ top: "0",
701
+ right: "0",
702
+ bottom: "0",
703
+ left: "0",
704
+ // 弹窗层可能挂在页面中段的坑位里,不提层会被后面的内容盖住
705
+ zIndex: "1000"
706
+ } : {};
686
707
  if (!isTop) style.pointerEvents = "none";
687
708
  return style;
688
709
  }
@@ -710,7 +731,7 @@ function resolveStageStyle(stage, isPopup, context) {
710
731
  base.margin = "auto";
711
732
  base.flex = "none";
712
733
  }
713
- return Object.assign(
734
+ const style = Object.assign(
714
735
  base,
715
736
  {
716
737
  // width / height 支持数字(px)、'100%' / 'auto' / calc(...) 等相对单位,也支持 {{ }}
@@ -724,6 +745,11 @@ function resolveStageStyle(stage, isPopup, context) {
724
745
  // stage.style 也走一遍插值,这样切 tab 时弹窗本身的背景能跟着变
725
746
  toCssStyle(interpolateDeep(stage.style, context))
726
747
  );
748
+ if (!isPopup && style.maxWidth !== void 0 && style.margin === void 0) {
749
+ style.marginLeft = "auto";
750
+ style.marginRight = "auto";
751
+ }
752
+ return style;
727
753
  }
728
754
  function resolveCloseButton(config, layerName, closeTop) {
729
755
  if (!config || config.show === false) return void 0;
@@ -1187,7 +1213,11 @@ function createRuntime(dsl, options = {}) {
1187
1213
  const view = normalized.views[viewName];
1188
1214
  const action = view && view.stage && view.stage[hook];
1189
1215
  if (!action) return;
1190
- dispatch(action, Object.assign({}, buildContext(), extra));
1216
+ const reason = extra && extra.closeReason;
1217
+ dispatch(action, Object.assign({}, buildContext(), extra), {
1218
+ trigger: hook === "onShow" ? "show" : "close",
1219
+ reason: typeof reason === "string" ? reason : void 0
1220
+ });
1191
1221
  }
1192
1222
  function loadData() {
1193
1223
  const data = dsl.data || {};
@@ -1218,8 +1248,8 @@ function createRuntime(dsl, options = {}) {
1218
1248
  resolvedData = resolved;
1219
1249
  loading = false;
1220
1250
  notify();
1221
- emit("ready", { keys: Object.keys(resolved) });
1222
1251
  fireLifecycle(normalized.entry, "onShow");
1252
+ emit("ready", { keys: Object.keys(resolved) });
1223
1253
  }).catch((error) => {
1224
1254
  if (destroyed) return;
1225
1255
  loading = false;
@@ -1266,6 +1296,120 @@ function createRuntime(dsl, options = {}) {
1266
1296
  };
1267
1297
  }
1268
1298
 
1299
+ // src/user-fields.ts
1300
+ var DEFAULT_USER_FIELDS = [
1301
+ "shopId",
1302
+ "shopName",
1303
+ "collectCount",
1304
+ "albbCollectCount",
1305
+ "isNewUser",
1306
+ "levelId",
1307
+ "levelName",
1308
+ "serviceExpireTime",
1309
+ "remainDays",
1310
+ "userTypeNew",
1311
+ "serviceInfo"
1312
+ ];
1313
+ var USER_FIELD_LABELS = {
1314
+ shopId: "\u5E97\u94FA ID",
1315
+ shopName: "\u5E97\u94FA\u540D\u79F0",
1316
+ collectCount: "\u5269\u4F59\u91C7\u96C6\u6B21\u6570",
1317
+ albbCollectCount: "\u5269\u4F59 1688 \u91C7\u96C6\u6B21\u6570",
1318
+ isNewUser: "\u662F\u5426\u65B0\u7528\u6237\uFF080 / 1\uFF0C\u53EF\u76F4\u63A5\u7528\u4E8E visibleWhen\uFF09",
1319
+ levelId: "\u7248\u672C ID",
1320
+ levelName: "\u7248\u672C\u540D\u79F0\uFF0C\u5982\u300C\u4E13\u4E1A\u7248\u300D",
1321
+ serviceExpireTime: "\u670D\u52A1\u5230\u671F\u65F6\u95F4\uFF0C\u53EF\u76F4\u63A5\u5582\u7ED9 countdown \u7684 to",
1322
+ remainDays: "\u670D\u52A1\u5269\u4F59\u5929\u6570",
1323
+ userTypeNew: "\u65B0\u5546\u7528\u6237\u7C7B\u578B\uFF080 \u672A\u8BBE\u7F6E / 1 \u6709\u8D27\u6E90 / 2 \u65E0\u8D27\u6E90 / 3 \u4E24\u8005\u517C\u5907\uFF09",
1324
+ serviceInfo: "\u5BA2\u670D\u4E0E\u793E\u7FA4\u4FE1\u606F\uFF08\u5BF9\u8C61\uFF0C\u6309\u9700\u53D6\u5B50\u5B57\u6BB5\uFF09"
1325
+ };
1326
+ var USER_FIELD_RE = /\buser\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)/g;
1327
+ var INTERPOLATION_RE2 = /\{\{([\s\S]*?)\}\}/g;
1328
+ var RAW_EXPRESSION_KEYS = ["visibleWhen"];
1329
+ function editDistance(a, b) {
1330
+ const prev = [];
1331
+ for (let j = 0; j <= b.length; j++) prev[j] = j;
1332
+ for (let i = 1; i <= a.length; i++) {
1333
+ let diagonal = prev[0];
1334
+ prev[0] = i;
1335
+ for (let j = 1; j <= b.length; j++) {
1336
+ const temp = prev[j];
1337
+ prev[j] = Math.min(
1338
+ prev[j] + 1,
1339
+ prev[j - 1] + 1,
1340
+ diagonal + (a[i - 1] === b[j - 1] ? 0 : 1)
1341
+ );
1342
+ diagonal = temp;
1343
+ }
1344
+ }
1345
+ return prev[b.length];
1346
+ }
1347
+ function closestField(name, allowed) {
1348
+ const lower = name.toLowerCase();
1349
+ let best = "";
1350
+ let bestScore = Infinity;
1351
+ allowed.forEach((field) => {
1352
+ const score = editDistance(lower, field.toLowerCase());
1353
+ if (score < bestScore) {
1354
+ bestScore = score;
1355
+ best = field;
1356
+ }
1357
+ });
1358
+ return bestScore <= Math.max(3, Math.ceil(name.length / 2)) ? best : "";
1359
+ }
1360
+ function collectFromExpression(source, into) {
1361
+ USER_FIELD_RE.lastIndex = 0;
1362
+ let matched = USER_FIELD_RE.exec(source);
1363
+ while (matched) {
1364
+ into.push(matched[1]);
1365
+ matched = USER_FIELD_RE.exec(source);
1366
+ }
1367
+ }
1368
+ function collectFromValue(value, key, into) {
1369
+ if (RAW_EXPRESSION_KEYS.indexOf(key) > -1) {
1370
+ collectFromExpression(value, into);
1371
+ return;
1372
+ }
1373
+ INTERPOLATION_RE2.lastIndex = 0;
1374
+ let matched = INTERPOLATION_RE2.exec(value);
1375
+ while (matched) {
1376
+ collectFromExpression(matched[1], into);
1377
+ matched = INTERPOLATION_RE2.exec(value);
1378
+ }
1379
+ }
1380
+ function validateUserFields(dsl, allowed) {
1381
+ const issues = [];
1382
+ const seen = [];
1383
+ const walk = (node, path, key) => {
1384
+ if (typeof node === "string") {
1385
+ const names = [];
1386
+ collectFromValue(node, key, names);
1387
+ names.forEach((name) => {
1388
+ if (allowed.indexOf(name) > -1) return;
1389
+ const guess = closestField(name, allowed);
1390
+ issues.push({
1391
+ path,
1392
+ message: `\u7528\u6237\u4FE1\u606F\u91CC\u6CA1\u6709 "user.${name}" \u8FD9\u4E2A\u5B57\u6BB5` + (guess ? `\uFF0C\u662F\u4E0D\u662F\u60F3\u5199 "user.${guess}"\uFF1F` : "") + `\u3002\u53EF\u7528\u5B57\u6BB5\uFF1A${allowed.join(" / ")}`
1393
+ });
1394
+ });
1395
+ return;
1396
+ }
1397
+ if (!node || typeof node !== "object") return;
1398
+ if (seen.indexOf(node) > -1) return;
1399
+ seen.push(node);
1400
+ if (Array.isArray(node)) {
1401
+ node.forEach((item, index) => walk(item, `${path}[${index}]`, key));
1402
+ return;
1403
+ }
1404
+ Object.keys(node).forEach((childKey) => {
1405
+ const childPath = path ? `${path}.${childKey}` : childKey;
1406
+ walk(node[childKey], childPath, childKey);
1407
+ });
1408
+ };
1409
+ walk(dsl, "", "");
1410
+ return issues;
1411
+ }
1412
+
1269
1413
  // src/validate.ts
1270
1414
  var LENGTH_STYLE_KEYS = [
1271
1415
  "width",
@@ -1317,7 +1461,7 @@ var CLOSE_POSITIONS = [
1317
1461
  "bottom-center"
1318
1462
  ];
1319
1463
  var CONTAINER_TYPES = ["box", "flex"];
1320
- function validate(dsl) {
1464
+ function validate(dsl, options) {
1321
1465
  const errors = [];
1322
1466
  const warnings = [];
1323
1467
  const add = (path, message) => errors.push({ path, message });
@@ -1347,6 +1491,11 @@ function validate(dsl) {
1347
1491
  viewNames.forEach((name) => {
1348
1492
  validateView(views[name], isMulti ? `views.${name}` : "", ctx);
1349
1493
  });
1494
+ if (options && options.userFields) {
1495
+ validateUserFields(doc, options.userFields).forEach(
1496
+ (issue) => add(issue.path, issue.message)
1497
+ );
1498
+ }
1350
1499
  return { valid: errors.length === 0, errors, warnings };
1351
1500
  }
1352
1501
  function validateMultiView(dsl, add) {
@@ -1379,6 +1528,7 @@ function validateView(view, prefix, ctx) {
1379
1528
  if (["popup", "banner", "notice"].indexOf(view.type) === -1) {
1380
1529
  add(at("type"), "type \u5FC5\u987B\u662F popup / banner / notice \u4E4B\u4E00");
1381
1530
  }
1531
+ ctx.viewType = view.type;
1382
1532
  validateStage(
1383
1533
  view.stage,
1384
1534
  (path, message) => add(prefix ? `${prefix}.${path}` : path, message),
@@ -1402,6 +1552,16 @@ function checkLength(value) {
1402
1552
  if (isLength(value)) return true;
1403
1553
  return value.trim().split(/\s+/).every(isLength);
1404
1554
  }
1555
+ var BANNER_MIN_ASPECT_RATIO = 12;
1556
+ function parseAspectRatio(value) {
1557
+ if (typeof value === "number") return isFinite(value) && value > 0 ? value : null;
1558
+ if (typeof value !== "string" || value.indexOf("{{") > -1) return null;
1559
+ const parts = value.split("/");
1560
+ const width = Number(parts[0]);
1561
+ const height = parts.length > 1 ? Number(parts[1]) : 1;
1562
+ if (!isFinite(width) || !isFinite(height) || width <= 0 || height <= 0) return null;
1563
+ return width / height;
1564
+ }
1405
1565
  function checkAspectRatio(value) {
1406
1566
  if (typeof value === "string" && value.indexOf("{{") > -1) return true;
1407
1567
  if (typeof value === "number") return isFinite(value) && value > 0;
@@ -1569,11 +1729,21 @@ function validateNode(node, path, layout, ctx) {
1569
1729
  `"${node.style[key]}" \u4E0D\u662F\u5408\u6CD5\u957F\u5EA6\uFF0C\u652F\u6301\u6570\u5B57(px) / '100%' / 'auto' \u7B49`
1570
1730
  );
1571
1731
  }
1572
- if (key === "aspectRatio" && !checkAspectRatio(node.style[key])) {
1573
- add(
1574
- `${path}.style.aspectRatio`,
1575
- "aspectRatio \u8981\u5199\u6210 '750 / 200' \u6216 1.5 \u8FD9\u6837\u7684\u5BBD\u9AD8\u6BD4"
1576
- );
1732
+ if (key === "aspectRatio") {
1733
+ if (!checkAspectRatio(node.style[key])) {
1734
+ add(
1735
+ `${path}.style.aspectRatio`,
1736
+ "aspectRatio \u8981\u5199\u6210 '750 / 200' \u6216 1.5 \u8FD9\u6837\u7684\u5BBD\u9AD8\u6BD4"
1737
+ );
1738
+ } else if (ctx.viewType === "banner") {
1739
+ const ratio = parseAspectRatio(node.style[key]);
1740
+ if (ratio !== null && ratio < BANNER_MIN_ASPECT_RATIO) {
1741
+ add(
1742
+ `${path}.style.aspectRatio`,
1743
+ `banner \u7684\u5BBD\u9AD8\u6BD4\u4E0D\u80FD\u5C0F\u4E8E ${BANNER_MIN_ASPECT_RATIO}:1\uFF08\u5F53\u524D\u7EA6 ${ratio.toFixed(1)}:1\uFF09\u3002banner \u5751\u4F4D\u5E38\u9A7B\u9875\u9762\u9876\u90E8\uFF0C\u6BD4\u4F8B\u8D8A\u65B9\u5728\u5BBD\u5C4F\u4E0A\u8D8A\u9AD8 \u2014\u2014 1800px \u5BBD\u7684\u5751\u4F4D\u91CC\uFF0C${ratio.toFixed(1)}:1 \u4F1A\u7B97\u51FA ${Math.round(1800 / ratio)}px \u9AD8\uFF0C\u5403\u6389\u9996\u5C4F\u4E00\u5927\u5757`
1744
+ );
1745
+ }
1746
+ }
1577
1747
  }
1578
1748
  });
1579
1749
  }
@@ -1706,11 +1876,14 @@ function formatIssues(issues) {
1706
1876
 
1707
1877
  exports.ACTION_TYPES = ACTION_TYPES;
1708
1878
  exports.ALLOWED_STYLE_KEYS = ALLOWED_STYLE_KEYS;
1879
+ exports.BANNER_MIN_ASPECT_RATIO = BANNER_MIN_ASPECT_RATIO;
1709
1880
  exports.CLOSE_POSITIONS = CLOSE_POSITIONS;
1881
+ exports.DEFAULT_USER_FIELDS = DEFAULT_USER_FIELDS;
1710
1882
  exports.DSL_VERSION = DSL_VERSION;
1711
1883
  exports.NODE_TYPES = NODE_TYPES;
1712
1884
  exports.SINGLE_VIEW_NAME = SINGLE_VIEW_NAME;
1713
1885
  exports.TRACK_EVENT_PATTERN = TRACK_EVENT_PATTERN;
1886
+ exports.USER_FIELD_LABELS = USER_FIELD_LABELS;
1714
1887
  exports.check = check;
1715
1888
  exports.computeParts = computeParts;
1716
1889
  exports.createRuntime = createRuntime;
@@ -1728,3 +1901,4 @@ exports.safeUrl = safeUrl;
1728
1901
  exports.toCssStyle = toCssStyle;
1729
1902
  exports.toLength = toLength;
1730
1903
  exports.validate = validate;
1904
+ exports.validateUserFields = validateUserFields;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { D as Dsl, R as RuntimeOptions, a as RenderTree, b as DslView, c as DslStyle, C as CssStyle } from './types-BgUaJUAu.cjs';
2
- export { q as DslAction, h as DslActionBase, o as DslCallAction, j as DslCloseAction, k as DslCloseAllAction, t as DslCloseButton, s as DslClosePosition, w as DslDerived, A as DslHandler, f as DslLength, i as DslNavigateAction, r as DslNode, e as DslNodeType, l as DslOpenAction, g as DslRect, p as DslSequenceAction, m as DslSetStateAction, z as DslSource, v as DslSourceRef, u as DslStage, n as DslTrackAction, d as DslViewType, x as RenderElement, y as RenderLayer, F as RuntimeEmit, E as RuntimeEventName, B as RuntimeEvents } from './types-BgUaJUAu.cjs';
1
+ import { D as Dsl, R as RuntimeOptions, a as RenderTree, b as DslView, c as DslStyle, C as CssStyle } from './types-Bj0xyWnx.cjs';
2
+ export { q as DslAction, h as DslActionBase, o as DslCallAction, j as DslCloseAction, k as DslCloseAllAction, t as DslCloseButton, s as DslClosePosition, w as DslDerived, A as DslHandler, E as DslInteractionEvent, B as DslInteractionTrigger, f as DslLength, i as DslNavigateAction, r as DslNode, e as DslNodeType, l as DslOpenAction, g as DslRect, p as DslSequenceAction, m as DslSetStateAction, z as DslSource, v as DslSourceRef, u as DslStage, n as DslTrackAction, d as DslViewType, x as RenderElement, y as RenderLayer, H as RuntimeEmit, G as RuntimeEventName, F as RuntimeEvents } from './types-Bj0xyWnx.cjs';
3
3
 
4
4
  /**
5
5
  * 运行时。
@@ -46,6 +46,17 @@ interface Issue {
46
46
  path: string;
47
47
  message: string;
48
48
  }
49
+ /** 校验选项,全部可选;不传时行为与旧版完全一致 */
50
+ interface ValidateOptions {
51
+ /**
52
+ * 允许出现在 `{{ user.xxx }}` 里的字段白名单。
53
+ *
54
+ * **不传就不校验**(宿主注入什么字段由宿主决定,核心包不该替它们写死)。
55
+ * 传了则所有插值位置里引用的 `user` 字段都必须在表内,否则报 error。
56
+ * 默认可用的一套见 `DEFAULT_USER_FIELDS`。
57
+ */
58
+ userFields?: string[];
59
+ }
49
60
  interface ValidateResult {
50
61
  valid: boolean;
51
62
  errors: Issue[];
@@ -62,10 +73,70 @@ declare const ACTION_TYPES: string[];
62
73
  */
63
74
  declare const TRACK_EVENT_PATTERN: RegExp;
64
75
  declare const CLOSE_POSITIONS: string[];
65
- declare function validate(dsl: Dsl | unknown): ValidateResult;
76
+ /**
77
+ * 校验一份 DSL。
78
+ *
79
+ * @param dsl 待校验的配置
80
+ * @param options 可选项;不传时与旧版行为完全一致
81
+ */
82
+ declare function validate(dsl: Dsl | unknown, options?: ValidateOptions): ValidateResult;
83
+ /** 宽高比:'750 / 200'、'16/9' 或纯数字 */
84
+ /**
85
+ * banner 允许的最小宽高比(宽 ÷ 高)。
86
+ *
87
+ * banner 坑位常驻页面顶部、不跟随内容滚动,比例越方在宽屏上越高:
88
+ * 同样 1800px 宽的坑位,17:1 算出来是 105px,4:1(1200×300 那种大图)直接 451px,
89
+ * 首屏一眼看过去全是广告。所以比例必须够扁——**这是坑位的设计尺寸,不是配置的自由度**。
90
+ *
91
+ * 约束比例而不是约束固定高度,是因为整图 banner 必须靠 aspectRatio 维持等比:
92
+ * 写死高度的话坑位一宽,图就被横向拉变形了。
93
+ */
94
+ declare const BANNER_MIN_ASPECT_RATIO = 12;
66
95
  /** 把校验结果拼成可以直接展示的文本 */
67
96
  declare function formatIssues(issues: Issue[]): string;
68
97
 
98
+ /**
99
+ * `{{ user.xxx }}` 字段白名单校验
100
+ *
101
+ * `user` 是宿主注入的登录用户信息对象,字段集由宿主决定、前端 DSL 只能读。
102
+ * 运营写错字段名(`user.expireDays` 而不是 `user.remainDays`)时,
103
+ * 运行时只会求值为空——页面上就是一处空白 / 倒计时归零,
104
+ * 投到线上才发现。所以在保存前就按白名单拦一道。
105
+ *
106
+ * 白名单本身是**可选**的:`validate(dsl)` 不传就完全不查(向后兼容),
107
+ * 调用方(后台调试台 / 后端保存)自己决定用哪套字段。
108
+ */
109
+ /** 一条校验问题,结构与 `validate.ts` 的 `Issue` 一致(避免两个文件循环依赖类型) */
110
+ interface FieldIssue {
111
+ /** 出问题的位置,如 `views.main.nodes[0].content` */
112
+ path: string;
113
+ /** 给运营看的中文说明 */
114
+ message: string;
115
+ }
116
+ /**
117
+ * 默认可用的 `user` 字段白名单。
118
+ *
119
+ * 取自各平台统一的登录用户信息模型(`UserInfo`),这套字段名是全平台通用契约。
120
+ * 刻意排除两个字段:
121
+ * - `sourceData`:后端原始响应整包,结构随平台漂移,不对运营开放
122
+ * - `mcToken`:令牌,不该出现在运营配置里,更不该渲染到页面上
123
+ */
124
+ declare const DEFAULT_USER_FIELDS: string[];
125
+ /** 白名单字段的中文含义,文档和编辑器提示共用一份,避免说明各写各的 */
126
+ declare const USER_FIELD_LABELS: Record<string, string>;
127
+ /**
128
+ * 深度遍历整份 DSL,校验所有 `{{ user.xxx }}` 引用的字段是否在白名单内。
129
+ *
130
+ * 遍历的是**全部字符串值**而不是某几个已知字段(content / to / src …),
131
+ * 因为能写插值的位置太多(样式值、动作 params、stage 尺寸…),
132
+ * 正向列举必漏;反过来「凡是字符串就查一遍 `{{ }}`」不会漏。
133
+ *
134
+ * @param dsl 待校验的 DSL(任意结构,坏数据交给主校验流程报错,这里只负责不崩)
135
+ * @param allowed 允许的字段名列表
136
+ * @returns 每个非法引用一条 error 级问题
137
+ */
138
+ declare function validateUserFields(dsl: unknown, allowed: string[]): FieldIssue[];
139
+
69
140
  /**
70
141
  * 视图归一化。
71
142
  *
@@ -177,4 +248,4 @@ declare function computeParts(endTime: number, now?: number): CountdownParts;
177
248
  /** 不给 children 时退化成一行文本,占位符 {d} {h} {hAll} {m} {s} {cs} */
178
249
  declare function formatParts(parts: CountdownParts, format?: string): string;
179
250
 
180
- export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, CssStyle, DSL_VERSION, Dsl, type DslRuntime, DslStyle, DslView, type Issue, NODE_TYPES, type NormalizedViews, RenderTree, RuntimeOptions, SINGLE_VIEW_NAME, TRACK_EVENT_PATTERN, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
251
+ export { ACTION_TYPES, ALLOWED_STYLE_KEYS, BANNER_MIN_ASPECT_RATIO, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, CssStyle, DEFAULT_USER_FIELDS, DSL_VERSION, Dsl, type DslRuntime, DslStyle, DslView, type Issue, NODE_TYPES, type NormalizedViews, RenderTree, RuntimeOptions, SINGLE_VIEW_NAME, TRACK_EVENT_PATTERN, USER_FIELD_LABELS, type ValidateOptions, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate, validateUserFields };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { D as Dsl, R as RuntimeOptions, a as RenderTree, b as DslView, c as DslStyle, C as CssStyle } from './types-BgUaJUAu.js';
2
- export { q as DslAction, h as DslActionBase, o as DslCallAction, j as DslCloseAction, k as DslCloseAllAction, t as DslCloseButton, s as DslClosePosition, w as DslDerived, A as DslHandler, f as DslLength, i as DslNavigateAction, r as DslNode, e as DslNodeType, l as DslOpenAction, g as DslRect, p as DslSequenceAction, m as DslSetStateAction, z as DslSource, v as DslSourceRef, u as DslStage, n as DslTrackAction, d as DslViewType, x as RenderElement, y as RenderLayer, F as RuntimeEmit, E as RuntimeEventName, B as RuntimeEvents } from './types-BgUaJUAu.js';
1
+ import { D as Dsl, R as RuntimeOptions, a as RenderTree, b as DslView, c as DslStyle, C as CssStyle } from './types-Bj0xyWnx.js';
2
+ export { q as DslAction, h as DslActionBase, o as DslCallAction, j as DslCloseAction, k as DslCloseAllAction, t as DslCloseButton, s as DslClosePosition, w as DslDerived, A as DslHandler, E as DslInteractionEvent, B as DslInteractionTrigger, f as DslLength, i as DslNavigateAction, r as DslNode, e as DslNodeType, l as DslOpenAction, g as DslRect, p as DslSequenceAction, m as DslSetStateAction, z as DslSource, v as DslSourceRef, u as DslStage, n as DslTrackAction, d as DslViewType, x as RenderElement, y as RenderLayer, H as RuntimeEmit, G as RuntimeEventName, F as RuntimeEvents } from './types-Bj0xyWnx.js';
3
3
 
4
4
  /**
5
5
  * 运行时。
@@ -46,6 +46,17 @@ interface Issue {
46
46
  path: string;
47
47
  message: string;
48
48
  }
49
+ /** 校验选项,全部可选;不传时行为与旧版完全一致 */
50
+ interface ValidateOptions {
51
+ /**
52
+ * 允许出现在 `{{ user.xxx }}` 里的字段白名单。
53
+ *
54
+ * **不传就不校验**(宿主注入什么字段由宿主决定,核心包不该替它们写死)。
55
+ * 传了则所有插值位置里引用的 `user` 字段都必须在表内,否则报 error。
56
+ * 默认可用的一套见 `DEFAULT_USER_FIELDS`。
57
+ */
58
+ userFields?: string[];
59
+ }
49
60
  interface ValidateResult {
50
61
  valid: boolean;
51
62
  errors: Issue[];
@@ -62,10 +73,70 @@ declare const ACTION_TYPES: string[];
62
73
  */
63
74
  declare const TRACK_EVENT_PATTERN: RegExp;
64
75
  declare const CLOSE_POSITIONS: string[];
65
- declare function validate(dsl: Dsl | unknown): ValidateResult;
76
+ /**
77
+ * 校验一份 DSL。
78
+ *
79
+ * @param dsl 待校验的配置
80
+ * @param options 可选项;不传时与旧版行为完全一致
81
+ */
82
+ declare function validate(dsl: Dsl | unknown, options?: ValidateOptions): ValidateResult;
83
+ /** 宽高比:'750 / 200'、'16/9' 或纯数字 */
84
+ /**
85
+ * banner 允许的最小宽高比(宽 ÷ 高)。
86
+ *
87
+ * banner 坑位常驻页面顶部、不跟随内容滚动,比例越方在宽屏上越高:
88
+ * 同样 1800px 宽的坑位,17:1 算出来是 105px,4:1(1200×300 那种大图)直接 451px,
89
+ * 首屏一眼看过去全是广告。所以比例必须够扁——**这是坑位的设计尺寸,不是配置的自由度**。
90
+ *
91
+ * 约束比例而不是约束固定高度,是因为整图 banner 必须靠 aspectRatio 维持等比:
92
+ * 写死高度的话坑位一宽,图就被横向拉变形了。
93
+ */
94
+ declare const BANNER_MIN_ASPECT_RATIO = 12;
66
95
  /** 把校验结果拼成可以直接展示的文本 */
67
96
  declare function formatIssues(issues: Issue[]): string;
68
97
 
98
+ /**
99
+ * `{{ user.xxx }}` 字段白名单校验
100
+ *
101
+ * `user` 是宿主注入的登录用户信息对象,字段集由宿主决定、前端 DSL 只能读。
102
+ * 运营写错字段名(`user.expireDays` 而不是 `user.remainDays`)时,
103
+ * 运行时只会求值为空——页面上就是一处空白 / 倒计时归零,
104
+ * 投到线上才发现。所以在保存前就按白名单拦一道。
105
+ *
106
+ * 白名单本身是**可选**的:`validate(dsl)` 不传就完全不查(向后兼容),
107
+ * 调用方(后台调试台 / 后端保存)自己决定用哪套字段。
108
+ */
109
+ /** 一条校验问题,结构与 `validate.ts` 的 `Issue` 一致(避免两个文件循环依赖类型) */
110
+ interface FieldIssue {
111
+ /** 出问题的位置,如 `views.main.nodes[0].content` */
112
+ path: string;
113
+ /** 给运营看的中文说明 */
114
+ message: string;
115
+ }
116
+ /**
117
+ * 默认可用的 `user` 字段白名单。
118
+ *
119
+ * 取自各平台统一的登录用户信息模型(`UserInfo`),这套字段名是全平台通用契约。
120
+ * 刻意排除两个字段:
121
+ * - `sourceData`:后端原始响应整包,结构随平台漂移,不对运营开放
122
+ * - `mcToken`:令牌,不该出现在运营配置里,更不该渲染到页面上
123
+ */
124
+ declare const DEFAULT_USER_FIELDS: string[];
125
+ /** 白名单字段的中文含义,文档和编辑器提示共用一份,避免说明各写各的 */
126
+ declare const USER_FIELD_LABELS: Record<string, string>;
127
+ /**
128
+ * 深度遍历整份 DSL,校验所有 `{{ user.xxx }}` 引用的字段是否在白名单内。
129
+ *
130
+ * 遍历的是**全部字符串值**而不是某几个已知字段(content / to / src …),
131
+ * 因为能写插值的位置太多(样式值、动作 params、stage 尺寸…),
132
+ * 正向列举必漏;反过来「凡是字符串就查一遍 `{{ }}`」不会漏。
133
+ *
134
+ * @param dsl 待校验的 DSL(任意结构,坏数据交给主校验流程报错,这里只负责不崩)
135
+ * @param allowed 允许的字段名列表
136
+ * @returns 每个非法引用一条 error 级问题
137
+ */
138
+ declare function validateUserFields(dsl: unknown, allowed: string[]): FieldIssue[];
139
+
69
140
  /**
70
141
  * 视图归一化。
71
142
  *
@@ -177,4 +248,4 @@ declare function computeParts(endTime: number, now?: number): CountdownParts;
177
248
  /** 不给 children 时退化成一行文本,占位符 {d} {h} {hAll} {m} {s} {cs} */
178
249
  declare function formatParts(parts: CountdownParts, format?: string): string;
179
250
 
180
- export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, CssStyle, DSL_VERSION, Dsl, type DslRuntime, DslStyle, DslView, type Issue, NODE_TYPES, type NormalizedViews, RenderTree, RuntimeOptions, SINGLE_VIEW_NAME, TRACK_EVENT_PATTERN, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
251
+ export { ACTION_TYPES, ALLOWED_STYLE_KEYS, BANNER_MIN_ASPECT_RATIO, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, CssStyle, DEFAULT_USER_FIELDS, DSL_VERSION, Dsl, type DslRuntime, DslStyle, DslView, type Issue, NODE_TYPES, type NormalizedViews, RenderTree, RuntimeOptions, SINGLE_VIEW_NAME, TRACK_EVENT_PATTERN, USER_FIELD_LABELS, type ValidateOptions, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate, validateUserFields };