@yaoxiu/marketing-dsl 1.2.0 → 1.3.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
@@ -400,10 +400,24 @@ function parseEndTime(to) {
400
400
  if (typeof to === "number") return to;
401
401
  if (to === void 0 || to === null || to === "") return 0;
402
402
  const raw = String(to).trim();
403
+ if (!raw) return 0;
403
404
  if (/^\d+$/.test(raw)) return Number(raw);
404
- const time = new Date(raw.replace(/-/g, "/")).getTime();
405
+ if (/^\d{4}年/.test(raw)) {
406
+ const normalizedZh = raw.replace(/年|月/g, "/").replace(/日/g, " ").replace(/时|点|分/g, ":").replace(/秒/g, "").replace(/:+/g, ":").replace(/\s+/g, " ").replace(/[:\s]+$/, "").replace(/(\s\d{1,2})$/, "$1:00").trim();
407
+ const time2 = new Date(normalizedZh).getTime();
408
+ return isNaN(time2) ? 0 : time2;
409
+ }
410
+ const isIso = /^\d{4}-\d{2}-\d{2}T/.test(raw);
411
+ const normalized = isIso ? raw : raw.replace(/-/g, "/");
412
+ const time = new Date(normalized).getTime();
405
413
  return isNaN(time) ? 0 : time;
406
414
  }
415
+ function isValidEndTime(to) {
416
+ if (typeof to === "number") return isFinite(to) && to > 0;
417
+ if (typeof to !== "string") return false;
418
+ if (to.indexOf("{{") > -1) return true;
419
+ return parseEndTime(to) > 0;
420
+ }
407
421
  function computeParts(endTime, now = Date.now()) {
408
422
  const raw = endTime - now;
409
423
  const remain = raw > 0 ? raw : 0;
@@ -956,10 +970,13 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
956
970
  const parts = computeParts(endTime);
957
971
  countdowns.endTimes.push(endTime);
958
972
  if (node.precision === "cs") countdowns.precision = "cs";
959
- if (parts.ended && endTime && node.onEnd) {
973
+ if (endTime && node.onEnd) {
960
974
  const token = `${key}@${endTime}`;
961
- if (!input.firedEnds.has(token)) {
962
- input.firedEnds.add(token);
975
+ const status = input.countdownEnds.get(token);
976
+ if (status === void 0) {
977
+ input.countdownEnds.set(token, parts.ended ? "done" : "waiting");
978
+ } else if (status === "waiting" && parts.ended) {
979
+ input.countdownEnds.set(token, "done");
963
980
  input.dispatch(node.onEnd, context);
964
981
  }
965
982
  }
@@ -1019,7 +1036,7 @@ function createRuntime(dsl, options = {}) {
1019
1036
  let loadFailed = false;
1020
1037
  let destroyed = false;
1021
1038
  const listeners = /* @__PURE__ */ new Set();
1022
- const firedEnds = /* @__PURE__ */ new Set();
1039
+ const countdownEnds = /* @__PURE__ */ new Map();
1023
1040
  const notify = () => {
1024
1041
  if (destroyed) return;
1025
1042
  listeners.forEach((fn) => fn());
@@ -1153,7 +1170,7 @@ function createRuntime(dsl, options = {}) {
1153
1170
  dispatch,
1154
1171
  setState,
1155
1172
  closeTop,
1156
- firedEnds
1173
+ countdownEnds
1157
1174
  });
1158
1175
  ticker.sync(tree.countdownEndTimes, tree.countdownPrecision);
1159
1176
  return tree;
@@ -1523,7 +1540,14 @@ function validateNodeByType(node, path, ctx) {
1523
1540
  }
1524
1541
  }
1525
1542
  if (node.type === "countdown") {
1526
- if (!node.to) add(`${path}.to`, "countdown \u5FC5\u987B\u63D0\u4F9B to\uFF08\u7ED3\u675F\u65F6\u95F4\uFF09");
1543
+ if (!node.to) {
1544
+ add(`${path}.to`, "countdown \u5FC5\u987B\u63D0\u4F9B to\uFF08\u7ED3\u675F\u65F6\u95F4\uFF09");
1545
+ } else if (!isValidEndTime(node.to)) {
1546
+ add(
1547
+ `${path}.to`,
1548
+ `"${node.to}" \u4E0D\u662F\u80FD\u8BC6\u522B\u7684\u65F6\u95F4\u3002\u53EF\u4EE5\u5199 "2026-09-01 18:00:00" / "2026-09-01" / "2026\u5E749\u67081\u65E5 18\u70B9" / ISO \u683C\u5F0F / \u65F6\u95F4\u6233`
1549
+ );
1550
+ }
1527
1551
  if (node.precision !== void 0 && ["s", "cs"].indexOf(node.precision) === -1) {
1528
1552
  add(`${path}.precision`, "precision \u53EA\u80FD\u662F 's'\uFF08\u6BCF\u79D2\uFF09\u6216 'cs'\uFF08\u5398\u79D2\uFF0C\u9010\u5E27\u5237\u65B0\uFF09");
1529
1553
  }
@@ -1618,6 +1642,7 @@ exports.formatParts = formatParts;
1618
1642
  exports.interpolate = interpolate;
1619
1643
  exports.interpolateDeep = interpolateDeep;
1620
1644
  exports.isLength = isLength;
1645
+ exports.isValidEndTime = isValidEndTime;
1621
1646
  exports.normalizeViews = normalizeViews;
1622
1647
  exports.parseEndTime = parseEndTime;
1623
1648
  exports.safeImageUrl = safeImageUrl;
package/dist/index.d.cts CHANGED
@@ -438,9 +438,16 @@ interface CountdownParts {
438
438
  type CountdownPrecision = 's' | 'cs';
439
439
  /** 把 to 解析成时间戳。兼容 '2026-09-01 00:00:00' 这种 Safari 不认的格式 */
440
440
  declare function parseEndTime(to: unknown): number;
441
+ /**
442
+ * 判断一个 to 能不能解析。给校验用。
443
+ *
444
+ * 解析失败会当成「已结束」,页面上只会看到一行「已结束」,
445
+ * 运营根本意识不到是自己时间写错了——所以必须在保存前就拦下来。
446
+ */
447
+ declare function isValidEndTime(to: unknown): boolean;
441
448
  /** 算出某个时刻的时间片段 */
442
449
  declare function computeParts(endTime: number, now?: number): CountdownParts;
443
450
  /** 不给 children 时退化成一行文本,占位符 {d} {h} {hAll} {m} {s} {cs} */
444
451
  declare function formatParts(parts: CountdownParts, format?: string): string;
445
452
 
446
- export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, type CssStyle, DSL_VERSION, type Dsl, type DslAction, type DslActionBase, type DslCallAction, type DslCloseAction, type DslCloseAllAction, type DslCloseButton, type DslClosePosition, type DslDerived, type DslHandler, type DslLength, type DslNavigateAction, type DslNode, type DslNodeType, type DslOpenAction, type DslRect, type DslRuntime, type DslSequenceAction, type DslSetStateAction, type DslSource, type DslSourceRef, type DslStage, type DslStyle, type DslTrackAction, type DslView, type DslViewType, type Issue, NODE_TYPES, type NormalizedViews, type RenderElement, type RenderLayer, type RenderTree, type RuntimeEmit, type RuntimeEventName, type RuntimeEvents, type RuntimeOptions, SINGLE_VIEW_NAME, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
453
+ export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, type CssStyle, DSL_VERSION, type Dsl, type DslAction, type DslActionBase, type DslCallAction, type DslCloseAction, type DslCloseAllAction, type DslCloseButton, type DslClosePosition, type DslDerived, type DslHandler, type DslLength, type DslNavigateAction, type DslNode, type DslNodeType, type DslOpenAction, type DslRect, type DslRuntime, type DslSequenceAction, type DslSetStateAction, type DslSource, type DslSourceRef, type DslStage, type DslStyle, type DslTrackAction, type DslView, type DslViewType, type Issue, NODE_TYPES, type NormalizedViews, type RenderElement, type RenderLayer, type RenderTree, type RuntimeEmit, type RuntimeEventName, type RuntimeEvents, type RuntimeOptions, SINGLE_VIEW_NAME, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
package/dist/index.d.ts CHANGED
@@ -438,9 +438,16 @@ interface CountdownParts {
438
438
  type CountdownPrecision = 's' | 'cs';
439
439
  /** 把 to 解析成时间戳。兼容 '2026-09-01 00:00:00' 这种 Safari 不认的格式 */
440
440
  declare function parseEndTime(to: unknown): number;
441
+ /**
442
+ * 判断一个 to 能不能解析。给校验用。
443
+ *
444
+ * 解析失败会当成「已结束」,页面上只会看到一行「已结束」,
445
+ * 运营根本意识不到是自己时间写错了——所以必须在保存前就拦下来。
446
+ */
447
+ declare function isValidEndTime(to: unknown): boolean;
441
448
  /** 算出某个时刻的时间片段 */
442
449
  declare function computeParts(endTime: number, now?: number): CountdownParts;
443
450
  /** 不给 children 时退化成一行文本,占位符 {d} {h} {hAll} {m} {s} {cs} */
444
451
  declare function formatParts(parts: CountdownParts, format?: string): string;
445
452
 
446
- export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, type CssStyle, DSL_VERSION, type Dsl, type DslAction, type DslActionBase, type DslCallAction, type DslCloseAction, type DslCloseAllAction, type DslCloseButton, type DslClosePosition, type DslDerived, type DslHandler, type DslLength, type DslNavigateAction, type DslNode, type DslNodeType, type DslOpenAction, type DslRect, type DslRuntime, type DslSequenceAction, type DslSetStateAction, type DslSource, type DslSourceRef, type DslStage, type DslStyle, type DslTrackAction, type DslView, type DslViewType, type Issue, NODE_TYPES, type NormalizedViews, type RenderElement, type RenderLayer, type RenderTree, type RuntimeEmit, type RuntimeEventName, type RuntimeEvents, type RuntimeOptions, SINGLE_VIEW_NAME, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
453
+ export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, type CountdownParts, type CountdownPrecision, type CssStyle, DSL_VERSION, type Dsl, type DslAction, type DslActionBase, type DslCallAction, type DslCloseAction, type DslCloseAllAction, type DslCloseButton, type DslClosePosition, type DslDerived, type DslHandler, type DslLength, type DslNavigateAction, type DslNode, type DslNodeType, type DslOpenAction, type DslRect, type DslRuntime, type DslSequenceAction, type DslSetStateAction, type DslSource, type DslSourceRef, type DslStage, type DslStyle, type DslTrackAction, type DslView, type DslViewType, type Issue, NODE_TYPES, type NormalizedViews, type RenderElement, type RenderLayer, type RenderTree, type RuntimeEmit, type RuntimeEventName, type RuntimeEvents, type RuntimeOptions, SINGLE_VIEW_NAME, type ValidateResult, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
package/dist/index.js CHANGED
@@ -398,10 +398,24 @@ function parseEndTime(to) {
398
398
  if (typeof to === "number") return to;
399
399
  if (to === void 0 || to === null || to === "") return 0;
400
400
  const raw = String(to).trim();
401
+ if (!raw) return 0;
401
402
  if (/^\d+$/.test(raw)) return Number(raw);
402
- const time = new Date(raw.replace(/-/g, "/")).getTime();
403
+ if (/^\d{4}年/.test(raw)) {
404
+ const normalizedZh = raw.replace(/年|月/g, "/").replace(/日/g, " ").replace(/时|点|分/g, ":").replace(/秒/g, "").replace(/:+/g, ":").replace(/\s+/g, " ").replace(/[:\s]+$/, "").replace(/(\s\d{1,2})$/, "$1:00").trim();
405
+ const time2 = new Date(normalizedZh).getTime();
406
+ return isNaN(time2) ? 0 : time2;
407
+ }
408
+ const isIso = /^\d{4}-\d{2}-\d{2}T/.test(raw);
409
+ const normalized = isIso ? raw : raw.replace(/-/g, "/");
410
+ const time = new Date(normalized).getTime();
403
411
  return isNaN(time) ? 0 : time;
404
412
  }
413
+ function isValidEndTime(to) {
414
+ if (typeof to === "number") return isFinite(to) && to > 0;
415
+ if (typeof to !== "string") return false;
416
+ if (to.indexOf("{{") > -1) return true;
417
+ return parseEndTime(to) > 0;
418
+ }
405
419
  function computeParts(endTime, now = Date.now()) {
406
420
  const raw = endTime - now;
407
421
  const remain = raw > 0 ? raw : 0;
@@ -954,10 +968,13 @@ function resolveCountdown(node, key, style, context, input, countdowns) {
954
968
  const parts = computeParts(endTime);
955
969
  countdowns.endTimes.push(endTime);
956
970
  if (node.precision === "cs") countdowns.precision = "cs";
957
- if (parts.ended && endTime && node.onEnd) {
971
+ if (endTime && node.onEnd) {
958
972
  const token = `${key}@${endTime}`;
959
- if (!input.firedEnds.has(token)) {
960
- input.firedEnds.add(token);
973
+ const status = input.countdownEnds.get(token);
974
+ if (status === void 0) {
975
+ input.countdownEnds.set(token, parts.ended ? "done" : "waiting");
976
+ } else if (status === "waiting" && parts.ended) {
977
+ input.countdownEnds.set(token, "done");
961
978
  input.dispatch(node.onEnd, context);
962
979
  }
963
980
  }
@@ -1017,7 +1034,7 @@ function createRuntime(dsl, options = {}) {
1017
1034
  let loadFailed = false;
1018
1035
  let destroyed = false;
1019
1036
  const listeners = /* @__PURE__ */ new Set();
1020
- const firedEnds = /* @__PURE__ */ new Set();
1037
+ const countdownEnds = /* @__PURE__ */ new Map();
1021
1038
  const notify = () => {
1022
1039
  if (destroyed) return;
1023
1040
  listeners.forEach((fn) => fn());
@@ -1151,7 +1168,7 @@ function createRuntime(dsl, options = {}) {
1151
1168
  dispatch,
1152
1169
  setState,
1153
1170
  closeTop,
1154
- firedEnds
1171
+ countdownEnds
1155
1172
  });
1156
1173
  ticker.sync(tree.countdownEndTimes, tree.countdownPrecision);
1157
1174
  return tree;
@@ -1521,7 +1538,14 @@ function validateNodeByType(node, path, ctx) {
1521
1538
  }
1522
1539
  }
1523
1540
  if (node.type === "countdown") {
1524
- if (!node.to) add(`${path}.to`, "countdown \u5FC5\u987B\u63D0\u4F9B to\uFF08\u7ED3\u675F\u65F6\u95F4\uFF09");
1541
+ if (!node.to) {
1542
+ add(`${path}.to`, "countdown \u5FC5\u987B\u63D0\u4F9B to\uFF08\u7ED3\u675F\u65F6\u95F4\uFF09");
1543
+ } else if (!isValidEndTime(node.to)) {
1544
+ add(
1545
+ `${path}.to`,
1546
+ `"${node.to}" \u4E0D\u662F\u80FD\u8BC6\u522B\u7684\u65F6\u95F4\u3002\u53EF\u4EE5\u5199 "2026-09-01 18:00:00" / "2026-09-01" / "2026\u5E749\u67081\u65E5 18\u70B9" / ISO \u683C\u5F0F / \u65F6\u95F4\u6233`
1547
+ );
1548
+ }
1525
1549
  if (node.precision !== void 0 && ["s", "cs"].indexOf(node.precision) === -1) {
1526
1550
  add(`${path}.precision`, "precision \u53EA\u80FD\u662F 's'\uFF08\u6BCF\u79D2\uFF09\u6216 'cs'\uFF08\u5398\u79D2\uFF0C\u9010\u5E27\u5237\u65B0\uFF09");
1527
1551
  }
@@ -1601,4 +1625,4 @@ function formatIssues(issues) {
1601
1625
  return issues.map((item) => item.path ? `${item.path}: ${item.message}` : item.message).join("\n");
1602
1626
  }
1603
1627
 
1604
- export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, DSL_VERSION, NODE_TYPES, SINGLE_VIEW_NAME, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
1628
+ export { ACTION_TYPES, ALLOWED_STYLE_KEYS, CLOSE_POSITIONS, DSL_VERSION, NODE_TYPES, SINGLE_VIEW_NAME, check, computeParts, createRuntime, evaluate, formatIssues, formatParts, interpolate, interpolateDeep, isLength, isValidEndTime, normalizeViews, parseEndTime, safeImageUrl, safeUrl, toCssStyle, toLength, validate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaoxiu/marketing-dsl",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "营销弹窗 DSL 解释器核心,纯逻辑无框架依赖",
6
6
  "license": "MIT",