@yaoxiu/marketing-dsl 1.5.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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) {
@@ -1707,10 +1856,12 @@ function formatIssues(issues) {
1707
1856
  exports.ACTION_TYPES = ACTION_TYPES;
1708
1857
  exports.ALLOWED_STYLE_KEYS = ALLOWED_STYLE_KEYS;
1709
1858
  exports.CLOSE_POSITIONS = CLOSE_POSITIONS;
1859
+ exports.DEFAULT_USER_FIELDS = DEFAULT_USER_FIELDS;
1710
1860
  exports.DSL_VERSION = DSL_VERSION;
1711
1861
  exports.NODE_TYPES = NODE_TYPES;
1712
1862
  exports.SINGLE_VIEW_NAME = SINGLE_VIEW_NAME;
1713
1863
  exports.TRACK_EVENT_PATTERN = TRACK_EVENT_PATTERN;
1864
+ exports.USER_FIELD_LABELS = USER_FIELD_LABELS;
1714
1865
  exports.check = check;
1715
1866
  exports.computeParts = computeParts;
1716
1867
  exports.createRuntime = createRuntime;
@@ -1728,3 +1879,4 @@ exports.safeUrl = safeUrl;
1728
1879
  exports.toCssStyle = toCssStyle;
1729
1880
  exports.toLength = toLength;
1730
1881
  exports.validate = validate;
1882
+ 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,58 @@ 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;
66
83
  /** 把校验结果拼成可以直接展示的文本 */
67
84
  declare function formatIssues(issues: Issue[]): string;
68
85
 
86
+ /**
87
+ * `{{ user.xxx }}` 字段白名单校验
88
+ *
89
+ * `user` 是宿主注入的登录用户信息对象,字段集由宿主决定、前端 DSL 只能读。
90
+ * 运营写错字段名(`user.expireDays` 而不是 `user.remainDays`)时,
91
+ * 运行时只会求值为空——页面上就是一处空白 / 倒计时归零,
92
+ * 投到线上才发现。所以在保存前就按白名单拦一道。
93
+ *
94
+ * 白名单本身是**可选**的:`validate(dsl)` 不传就完全不查(向后兼容),
95
+ * 调用方(后台调试台 / 后端保存)自己决定用哪套字段。
96
+ */
97
+ /** 一条校验问题,结构与 `validate.ts` 的 `Issue` 一致(避免两个文件循环依赖类型) */
98
+ interface FieldIssue {
99
+ /** 出问题的位置,如 `views.main.nodes[0].content` */
100
+ path: string;
101
+ /** 给运营看的中文说明 */
102
+ message: string;
103
+ }
104
+ /**
105
+ * 默认可用的 `user` 字段白名单。
106
+ *
107
+ * 取自各平台统一的登录用户信息模型(`UserInfo`),这套字段名是全平台通用契约。
108
+ * 刻意排除两个字段:
109
+ * - `sourceData`:后端原始响应整包,结构随平台漂移,不对运营开放
110
+ * - `mcToken`:令牌,不该出现在运营配置里,更不该渲染到页面上
111
+ */
112
+ declare const DEFAULT_USER_FIELDS: string[];
113
+ /** 白名单字段的中文含义,文档和编辑器提示共用一份,避免说明各写各的 */
114
+ declare const USER_FIELD_LABELS: Record<string, string>;
115
+ /**
116
+ * 深度遍历整份 DSL,校验所有 `{{ user.xxx }}` 引用的字段是否在白名单内。
117
+ *
118
+ * 遍历的是**全部字符串值**而不是某几个已知字段(content / to / src …),
119
+ * 因为能写插值的位置太多(样式值、动作 params、stage 尺寸…),
120
+ * 正向列举必漏;反过来「凡是字符串就查一遍 `{{ }}`」不会漏。
121
+ *
122
+ * @param dsl 待校验的 DSL(任意结构,坏数据交给主校验流程报错,这里只负责不崩)
123
+ * @param allowed 允许的字段名列表
124
+ * @returns 每个非法引用一条 error 级问题
125
+ */
126
+ declare function validateUserFields(dsl: unknown, allowed: string[]): FieldIssue[];
127
+
69
128
  /**
70
129
  * 视图归一化。
71
130
  *
@@ -177,4 +236,4 @@ declare function computeParts(endTime: number, now?: number): CountdownParts;
177
236
  /** 不给 children 时退化成一行文本,占位符 {d} {h} {hAll} {m} {s} {cs} */
178
237
  declare function formatParts(parts: CountdownParts, format?: string): string;
179
238
 
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 };
239
+ export { ACTION_TYPES, ALLOWED_STYLE_KEYS, 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,58 @@ 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;
66
83
  /** 把校验结果拼成可以直接展示的文本 */
67
84
  declare function formatIssues(issues: Issue[]): string;
68
85
 
86
+ /**
87
+ * `{{ user.xxx }}` 字段白名单校验
88
+ *
89
+ * `user` 是宿主注入的登录用户信息对象,字段集由宿主决定、前端 DSL 只能读。
90
+ * 运营写错字段名(`user.expireDays` 而不是 `user.remainDays`)时,
91
+ * 运行时只会求值为空——页面上就是一处空白 / 倒计时归零,
92
+ * 投到线上才发现。所以在保存前就按白名单拦一道。
93
+ *
94
+ * 白名单本身是**可选**的:`validate(dsl)` 不传就完全不查(向后兼容),
95
+ * 调用方(后台调试台 / 后端保存)自己决定用哪套字段。
96
+ */
97
+ /** 一条校验问题,结构与 `validate.ts` 的 `Issue` 一致(避免两个文件循环依赖类型) */
98
+ interface FieldIssue {
99
+ /** 出问题的位置,如 `views.main.nodes[0].content` */
100
+ path: string;
101
+ /** 给运营看的中文说明 */
102
+ message: string;
103
+ }
104
+ /**
105
+ * 默认可用的 `user` 字段白名单。
106
+ *
107
+ * 取自各平台统一的登录用户信息模型(`UserInfo`),这套字段名是全平台通用契约。
108
+ * 刻意排除两个字段:
109
+ * - `sourceData`:后端原始响应整包,结构随平台漂移,不对运营开放
110
+ * - `mcToken`:令牌,不该出现在运营配置里,更不该渲染到页面上
111
+ */
112
+ declare const DEFAULT_USER_FIELDS: string[];
113
+ /** 白名单字段的中文含义,文档和编辑器提示共用一份,避免说明各写各的 */
114
+ declare const USER_FIELD_LABELS: Record<string, string>;
115
+ /**
116
+ * 深度遍历整份 DSL,校验所有 `{{ user.xxx }}` 引用的字段是否在白名单内。
117
+ *
118
+ * 遍历的是**全部字符串值**而不是某几个已知字段(content / to / src …),
119
+ * 因为能写插值的位置太多(样式值、动作 params、stage 尺寸…),
120
+ * 正向列举必漏;反过来「凡是字符串就查一遍 `{{ }}`」不会漏。
121
+ *
122
+ * @param dsl 待校验的 DSL(任意结构,坏数据交给主校验流程报错,这里只负责不崩)
123
+ * @param allowed 允许的字段名列表
124
+ * @returns 每个非法引用一条 error 级问题
125
+ */
126
+ declare function validateUserFields(dsl: unknown, allowed: string[]): FieldIssue[];
127
+
69
128
  /**
70
129
  * 视图归一化。
71
130
  *
@@ -177,4 +236,4 @@ declare function computeParts(endTime: number, now?: number): CountdownParts;
177
236
  /** 不给 children 时退化成一行文本,占位符 {d} {h} {hAll} {m} {s} {cs} */
178
237
  declare function formatParts(parts: CountdownParts, format?: string): string;
179
238
 
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 };
239
+ export { ACTION_TYPES, ALLOWED_STYLE_KEYS, 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.js CHANGED
@@ -1,5 +1,5 @@
1
- import { normalizeViews, createTicker, interpolateDeep, interpolate, evaluate, resolveNodeStyle, toLength, toCssStyle, parseEndTime, computeParts, formatParts } from './chunk-FKZKLCVV.js';
2
- export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, DSL_VERSION, NODE_TYPES, SINGLE_VIEW_NAME, TRACK_EVENT_PATTERN, check, computeParts, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, toCssStyle, toLength, validate } from './chunk-FKZKLCVV.js';
1
+ import { normalizeViews, createTicker, evaluate, resolveNodeStyle, interpolateDeep, interpolate, toLength, toCssStyle, parseEndTime, computeParts, formatParts } from './chunk-BSJDNS6C.js';
2
+ export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, DEFAULT_USER_FIELDS, DSL_VERSION, NODE_TYPES, SINGLE_VIEW_NAME, TRACK_EVENT_PATTERN, USER_FIELD_LABELS, check, computeParts, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, toCssStyle, toLength, validate, validateUserFields } from './chunk-BSJDNS6C.js';
3
3
 
4
4
  // src/url.ts
5
5
  var SAFE_PROTOCOLS = ["http:", "https:", "mailto:", "tel:"];
@@ -22,13 +22,13 @@ function safeImageUrl(input) {
22
22
  // src/actions.ts
23
23
  function createDispatcher(options) {
24
24
  const { setState, emit, openView, closeTop, closeAll } = options;
25
- function dispatch(action, context) {
25
+ function run(action, context, interaction) {
26
26
  if (!action || !action.type) return;
27
27
  const handlers = options.handlers || {};
28
28
  const editMode = !!options.editMode;
29
29
  switch (action.type) {
30
30
  case "sequence":
31
- (action.actions || []).forEach((item) => dispatch(item, context));
31
+ (action.actions || []).forEach((item) => run(item, context, interaction));
32
32
  return;
33
33
  case "navigate": {
34
34
  const url = safeUrl(interpolate(action.url, context));
@@ -38,6 +38,7 @@ function createDispatcher(options) {
38
38
  }
39
39
  const target = action.target || "_blank";
40
40
  emit("navigate", { url, target });
41
+ if (!interaction.navigate) interaction.navigate = { url, target };
41
42
  if (editMode) return;
42
43
  if (typeof window === "undefined") return;
43
44
  if (target === "_self") window.location.href = url;
@@ -51,20 +52,25 @@ function createDispatcher(options) {
51
52
  return;
52
53
  // 关掉栈顶那层;只剩一层时就是整体关闭
53
54
  case "close":
55
+ interaction.closes = true;
54
56
  closeTop(action.reason);
55
57
  return;
56
58
  case "closeAll":
59
+ interaction.closes = true;
57
60
  closeAll(action.reason);
58
61
  return;
59
62
  case "setState":
60
63
  setState(action.key, interpolate(action.value, context));
61
64
  return;
62
- case "track":
63
- emit("track", {
65
+ case "track": {
66
+ const payload = {
64
67
  event: String(interpolate(action.event, context)),
65
68
  params: interpolateDeep(action.params || {}, context)
66
- });
69
+ };
70
+ emit("track", payload);
71
+ if (!interaction.track) interaction.track = payload;
67
72
  return;
73
+ }
68
74
  case "call": {
69
75
  const handler = handlers[action.name];
70
76
  if (typeof handler !== "function") {
@@ -84,6 +90,13 @@ function createDispatcher(options) {
84
90
  });
85
91
  }
86
92
  }
93
+ function dispatch(action, context, meta) {
94
+ if (!action || !action.type) return;
95
+ const interaction = { trigger: (meta == null ? void 0 : meta.trigger) || "action" };
96
+ if ((meta == null ? void 0 : meta.reason) !== void 0) interaction.reason = meta.reason;
97
+ run(action, context, interaction);
98
+ emit("interaction", interaction);
99
+ }
87
100
  return dispatch;
88
101
  }
89
102
 
@@ -91,9 +104,7 @@ function createDispatcher(options) {
91
104
  function resolveTree(input) {
92
105
  const { normalized, viewStack, ready } = input;
93
106
  const layers = viewStack.map((name) => ({ name, view: normalized.views[name] })).filter((item) => !!item.view);
94
- const hasPopup = Object.keys(normalized.views).some(
95
- (name) => normalized.views[name].type === "popup"
96
- );
107
+ const hasPopup = layers.some((item) => item.view.type === "popup");
97
108
  const countdowns = { endTimes: [], precision: "s" };
98
109
  const renderLayers = ready ? layers.map(
99
110
  (item, index) => resolveLayer(
@@ -107,8 +118,10 @@ function resolveTree(input) {
107
118
  return {
108
119
  ready: ready && layers.length > 0,
109
120
  hasPopup,
110
- // 配置里有弹窗视图时,根容器要撑满宿主容器,弹窗层才有地方铺
111
- rootStyle: hasPopup ? { position: "absolute", top: "0", right: "0", bottom: "0", left: "0" } : { position: "relative" },
121
+ // 根容器恒为 relative:它要待在宿主给的位置上(banner / 公告坑位是流式的普通 div,
122
+ // 根容器一脱流坑位就塌成 0、内容按最近定位祖先飘走)。弹窗铺满视口由弹窗层自己
123
+ // `position: fixed` 完成,不再靠根容器撑开,见 layerStyle。
124
+ rootStyle: { position: "relative" },
112
125
  layers: renderLayers,
113
126
  countdownEndTimes: countdowns.endTimes,
114
127
  countdownPrecision: countdowns.precision
@@ -154,7 +167,15 @@ function resolveLayer(name, view, isTop, input, countdowns) {
154
167
  };
155
168
  }
156
169
  function layerStyle(isPopup, isTop) {
157
- const style = isPopup ? { position: "absolute", top: "0", right: "0", bottom: "0", left: "0" } : {};
170
+ const style = isPopup ? {
171
+ position: "fixed",
172
+ top: "0",
173
+ right: "0",
174
+ bottom: "0",
175
+ left: "0",
176
+ // 弹窗层可能挂在页面中段的坑位里,不提层会被后面的内容盖住
177
+ zIndex: "1000"
178
+ } : {};
158
179
  if (!isTop) style.pointerEvents = "none";
159
180
  return style;
160
181
  }
@@ -182,7 +203,7 @@ function resolveStageStyle(stage, isPopup, context) {
182
203
  base.margin = "auto";
183
204
  base.flex = "none";
184
205
  }
185
- return Object.assign(
206
+ const style = Object.assign(
186
207
  base,
187
208
  {
188
209
  // width / height 支持数字(px)、'100%' / 'auto' / calc(...) 等相对单位,也支持 {{ }}
@@ -196,6 +217,11 @@ function resolveStageStyle(stage, isPopup, context) {
196
217
  // stage.style 也走一遍插值,这样切 tab 时弹窗本身的背景能跟着变
197
218
  toCssStyle(interpolateDeep(stage.style, context))
198
219
  );
220
+ if (!isPopup && style.maxWidth !== void 0 && style.margin === void 0) {
221
+ style.marginLeft = "auto";
222
+ style.marginRight = "auto";
223
+ }
224
+ return style;
199
225
  }
200
226
  function resolveCloseButton(config, layerName, closeTop) {
201
227
  if (!config || config.show === false) return void 0;
@@ -659,7 +685,11 @@ function createRuntime(dsl, options = {}) {
659
685
  const view = normalized.views[viewName];
660
686
  const action = view && view.stage && view.stage[hook];
661
687
  if (!action) return;
662
- dispatch(action, Object.assign({}, buildContext(), extra));
688
+ const reason = extra && extra.closeReason;
689
+ dispatch(action, Object.assign({}, buildContext(), extra), {
690
+ trigger: hook === "onShow" ? "show" : "close",
691
+ reason: typeof reason === "string" ? reason : void 0
692
+ });
663
693
  }
664
694
  function loadData() {
665
695
  const data = dsl.data || {};
@@ -690,8 +720,8 @@ function createRuntime(dsl, options = {}) {
690
720
  resolvedData = resolved;
691
721
  loading = false;
692
722
  notify();
693
- emit("ready", { keys: Object.keys(resolved) });
694
723
  fireLifecycle(normalized.entry, "onShow");
724
+ emit("ready", { keys: Object.keys(resolved) });
695
725
  }).catch((error) => {
696
726
  if (destroyed) return;
697
727
  loading = false;