@hankchen/canvas-editor 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -501,6 +501,7 @@ function normalizeSchemaType(value) {
501
501
  const primary = value.find((entry) => entry !== "null" && entry !== "");
502
502
  return primary;
503
503
  }
504
+ var FORMAT_ANNOTATIONS = /* @__PURE__ */ new Set(["date", "date-time"]);
504
505
  function annotationsOf(raw) {
505
506
  const node = {};
506
507
  const rawType = raw["type"];
@@ -515,6 +516,10 @@ function annotationsOf(raw) {
515
516
  if (typeof rawDescription === "string") node.description = rawDescription;
516
517
  const rawTitle = raw["title"];
517
518
  if (typeof rawTitle === "string") node.title = rawTitle;
519
+ const rawFormat = raw["format"];
520
+ if (node.type === "string" && typeof rawFormat === "string" && FORMAT_ANNOTATIONS.has(rawFormat)) {
521
+ node.format = rawFormat;
522
+ }
518
523
  return node;
519
524
  }
520
525
  function pointerEscape(key) {
@@ -692,15 +697,170 @@ function parseExpressionPath(expr) {
692
697
  return { ok: true, head: { kind: "bare", name: head }, rest };
693
698
  }
694
699
 
700
+ // src/shared/expressionCall.ts
701
+ var TRIM_CHARS = " \n\r\0\v";
702
+ var WS_CHARS = " \n\r";
703
+ var HEAD_STOP_CHARS = `()',.${TRIM_CHARS}`;
704
+ function phpTrim(value) {
705
+ let start = 0;
706
+ let end = value.length;
707
+ while (start < end && TRIM_CHARS.includes(value[start])) start += 1;
708
+ while (end > start && TRIM_CHARS.includes(value[end - 1])) end -= 1;
709
+ return value.slice(start, end);
710
+ }
711
+ function skipWs(expr, pos) {
712
+ while (pos < expr.length && WS_CHARS.includes(expr[pos])) pos += 1;
713
+ return pos;
714
+ }
715
+ function isCallFragment(expr) {
716
+ let head = 0;
717
+ while (head < expr.length && !HEAD_STOP_CHARS.includes(expr[head])) head += 1;
718
+ if (head === 0) return false;
719
+ let pos = head;
720
+ while (pos < expr.length && TRIM_CHARS.includes(expr[pos])) pos += 1;
721
+ return expr[pos] === "(";
722
+ }
723
+ function parseExpressionCall(expr) {
724
+ let head = 0;
725
+ while (head < expr.length && !HEAD_STOP_CHARS.includes(expr[head])) head += 1;
726
+ if (head === 0) return { ok: false, error: "expression_syntax_error" };
727
+ const name = expr.slice(0, head);
728
+ let pos = skipWs(expr, head);
729
+ if (pos >= expr.length || expr[pos] !== "(") return { ok: false, error: "expression_syntax_error" };
730
+ pos = skipWs(expr, pos + 1);
731
+ const args = [];
732
+ if (pos < expr.length && expr[pos] === ")") {
733
+ pos += 1;
734
+ } else {
735
+ for (; ; ) {
736
+ if (pos >= expr.length) return { ok: false, error: "expression_syntax_error" };
737
+ const arg = parseCallArg(expr, pos);
738
+ if (!arg.ok) return arg;
739
+ args.push(arg.value);
740
+ pos = skipWs(expr, arg.end);
741
+ if (pos >= expr.length) return { ok: false, error: "expression_syntax_error" };
742
+ if (expr[pos] === ",") {
743
+ pos = skipWs(expr, pos + 1);
744
+ if (pos < expr.length && expr[pos] === ")") {
745
+ return { ok: false, error: "expression_syntax_error" };
746
+ }
747
+ continue;
748
+ }
749
+ if (expr[pos] === ")") {
750
+ pos += 1;
751
+ break;
752
+ }
753
+ return { ok: false, error: "expression_syntax_error" };
754
+ }
755
+ }
756
+ if (skipWs(expr, pos) !== expr.length) return { ok: false, error: "expression_syntax_error" };
757
+ if (args[0] !== void 0 && args[0].type !== "path") return { ok: false, error: "expression_syntax_error" };
758
+ for (const arg of args) {
759
+ if (arg.type === "path" && !parseExpressionPath(arg.value).ok) {
760
+ return { ok: false, error: "expression_syntax_error" };
761
+ }
762
+ }
763
+ return { ok: true, name, args };
764
+ }
765
+ function parseCallArg(expr, pos) {
766
+ const len = expr.length;
767
+ const ch = expr[pos];
768
+ if (ch === "'") {
769
+ let cursor = pos + 1;
770
+ let buf = "";
771
+ for (; ; ) {
772
+ if (cursor >= len) return { ok: false, error: "expression_syntax_error" };
773
+ if (expr[cursor] === "'") {
774
+ if (cursor + 1 < len && expr[cursor + 1] === "'") {
775
+ buf += "'";
776
+ cursor += 2;
777
+ continue;
778
+ }
779
+ cursor += 1;
780
+ break;
781
+ }
782
+ buf += expr[cursor];
783
+ cursor += 1;
784
+ }
785
+ return { ok: true, value: { type: "string", value: buf }, end: cursor };
786
+ }
787
+ if (ch >= "0" && ch <= "9") {
788
+ const start2 = pos;
789
+ const isDigitAt = (index) => {
790
+ const c = expr[index];
791
+ return c !== void 0 && c >= "0" && c <= "9";
792
+ };
793
+ while (isDigitAt(pos)) pos += 1;
794
+ if (pos < len && expr[pos] === "." && isDigitAt(pos + 1)) {
795
+ pos += 1;
796
+ while (isDigitAt(pos)) pos += 1;
797
+ }
798
+ return { ok: true, value: { type: "number", value: expr.slice(start2, pos) }, end: pos };
799
+ }
800
+ const start = pos;
801
+ while (pos < len && expr[pos] !== "," && expr[pos] !== ")") pos += 1;
802
+ const raw = phpTrim(expr.slice(start, pos));
803
+ if (raw === "") return { ok: false, error: "expression_syntax_error" };
804
+ return { ok: true, value: { type: "path", value: raw }, end: pos };
805
+ }
806
+ function expressionCallSlotAtEnd(expr) {
807
+ let head = 0;
808
+ while (head < expr.length && !HEAD_STOP_CHARS.includes(expr[head])) head += 1;
809
+ if (head === 0) return null;
810
+ let pos = skipWs(expr, head);
811
+ if (pos >= expr.length || expr[pos] !== "(") return null;
812
+ pos = skipWs(expr, pos + 1);
813
+ let index = 0;
814
+ for (; ; ) {
815
+ if (pos >= expr.length) return { name: expr.slice(0, head), index, text: "", stringSlot: false };
816
+ const ch = expr[pos];
817
+ if (ch === "'") {
818
+ let cursor = pos + 1;
819
+ for (; ; ) {
820
+ if (cursor >= expr.length) {
821
+ return { name: expr.slice(0, head), index, text: expr.slice(pos + 1), stringSlot: true };
822
+ }
823
+ if (expr[cursor] === "'") {
824
+ if (cursor + 1 < expr.length && expr[cursor + 1] === "'") {
825
+ cursor += 2;
826
+ continue;
827
+ }
828
+ cursor += 1;
829
+ break;
830
+ }
831
+ cursor += 1;
832
+ }
833
+ pos = cursor;
834
+ } else if (ch !== "," && ch !== ")") {
835
+ const argStart = pos;
836
+ while (pos < expr.length && expr[pos] !== "," && expr[pos] !== ")") pos += 1;
837
+ if (pos >= expr.length) {
838
+ return { name: expr.slice(0, head), index, text: phpTrim(expr.slice(argStart)), stringSlot: false };
839
+ }
840
+ }
841
+ if (pos >= expr.length) return { name: expr.slice(0, head), index: index + 1, text: "", stringSlot: false };
842
+ if (expr[pos] === ")") return null;
843
+ index += 1;
844
+ pos = skipWs(expr, pos + 1);
845
+ }
846
+ }
847
+
695
848
  // src/shared/expressionScan.ts
696
849
  var PHP_TRIM_CHARS = " \n\r\0\v";
697
- function phpTrim(value) {
850
+ function phpTrim2(value) {
698
851
  let start = 0;
699
852
  let end = value.length;
700
853
  while (start < end && PHP_TRIM_CHARS.includes(value[start])) start += 1;
701
854
  while (end > start && PHP_TRIM_CHARS.includes(value[end - 1])) end -= 1;
702
855
  return value.slice(start, end);
703
856
  }
857
+ function fragmentSyntaxError(expr) {
858
+ if (expr === "") return "expression_syntax_error";
859
+ if (isCallFragment(expr)) {
860
+ return parseExpressionCall(expr).ok ? null : "expression_syntax_error";
861
+ }
862
+ return parseExpressionPath(expr).ok ? null : "expression_syntax_error";
863
+ }
704
864
  function scanExpressionFragments(template) {
705
865
  const parts = [];
706
866
  let pos = 0;
@@ -712,15 +872,15 @@ function scanExpressionFragments(template) {
712
872
  parts.push({ kind: "literal", start: pos, end: open, text: template.slice(pos, open) });
713
873
  }
714
874
  const text = template.slice(open);
715
- parts.push({ kind: "open", start: open, end: template.length, text, expr: phpTrim(text.slice(2)) });
875
+ parts.push({ kind: "open", start: open, end: template.length, text, expr: phpTrim2(text.slice(2)) });
716
876
  return { template, parts };
717
877
  }
718
878
  if (open > pos) {
719
879
  parts.push({ kind: "literal", start: pos, end: open, text: template.slice(pos, open) });
720
880
  }
721
881
  const raw = template.slice(open + 2, close);
722
- const expr = phpTrim(raw);
723
- const error = parseExpressionPath(expr).ok ? null : "expression_syntax_error";
882
+ const expr = phpTrim2(raw);
883
+ const error = fragmentSyntaxError(expr);
724
884
  parts.push({ kind: "fragment", start: open, end: close + 2, raw, expr, error });
725
885
  pos = close + 2;
726
886
  open = template.indexOf("{{", pos);
@@ -938,7 +1098,35 @@ function buildLayerOutline(doc, lockedPaths = []) {
938
1098
  }
939
1099
  return roots;
940
1100
  }
941
- function createDefaultLayer(type) {
1101
+ var TEXT_PRESETS = [
1102
+ { id: "heading", fontSize: 40 },
1103
+ { id: "subheading", fontSize: 28 },
1104
+ { id: "body", fontSize: 16 }
1105
+ ];
1106
+ function defaultNextTextStyle() {
1107
+ const base = createDefaultLayer("TextLayer");
1108
+ return {
1109
+ font: base.font,
1110
+ fontSize: base.fontSize,
1111
+ fontColor: base.fontColor,
1112
+ autowrap: base.autowrap,
1113
+ lineHeight: base.shape.lineHeight
1114
+ };
1115
+ }
1116
+ function pickNextTextStylePatch(raw) {
1117
+ const out = {};
1118
+ if (raw === null || typeof raw !== "object") return out;
1119
+ const source = raw;
1120
+ if (typeof source.font === "string") out.font = source.font;
1121
+ if (typeof source.fontSize === "number" && Number.isFinite(source.fontSize)) out.fontSize = source.fontSize;
1122
+ if (typeof source.fontColor === "string") out.fontColor = source.fontColor;
1123
+ if (typeof source.autowrap === "boolean") out.autowrap = source.autowrap;
1124
+ if (typeof source.lineHeight === "number" && Number.isFinite(source.lineHeight) && source.lineHeight > 0) {
1125
+ out.lineHeight = source.lineHeight;
1126
+ }
1127
+ return out;
1128
+ }
1129
+ function createDefaultLayer(type, textDefaults) {
942
1130
  const base = {
943
1131
  // name/visible 缺省态(layer-panel-ux 工单 01):未命名 + 可见,wire 键省略
944
1132
  name: "",
@@ -958,19 +1146,26 @@ function createDefaultLayer(type) {
958
1146
  position: { anchor: "top-left", x: 0, y: 0 }
959
1147
  };
960
1148
  switch (type) {
961
- case "TextLayer":
962
- return {
1149
+ case "TextLayer": {
1150
+ const layer = {
963
1151
  ...base,
964
1152
  type,
965
- shape: { ...base.shape, width: 200, height: 60 },
1153
+ shape: {
1154
+ ...base.shape,
1155
+ width: 200,
1156
+ height: 60,
1157
+ lineHeight: textDefaults?.lineHeight ?? base.shape.lineHeight
1158
+ },
966
1159
  text: "\u6587\u672C",
967
1160
  expression: null,
968
- font: "",
969
- fontSize: 24,
970
- fontColor: "#111827",
1161
+ font: textDefaults?.font ?? "",
1162
+ fontSize: textDefaults?.fontSize ?? 24,
1163
+ fontColor: textDefaults?.fontColor ?? "#111827",
971
1164
  angle: 0,
972
- autowrap: false
1165
+ autowrap: textDefaults?.autowrap ?? false
973
1166
  };
1167
+ return layer;
1168
+ }
974
1169
  case "ImageLayer":
975
1170
  return { ...base, type, shape: { ...base.shape, width: 200, height: 150 }, src: null, expression: null };
976
1171
  case "QrCodeLayer":
@@ -1051,8 +1246,8 @@ function moveTableRowInDraft(draft, tablePath, fromRow, toRow) {
1051
1246
  rows.splice(reduced, 0, moved);
1052
1247
  return { from: fromRow, to: reduced };
1053
1248
  }
1054
- function addRootLayerInDraft(draft, type) {
1055
- return insertRootLayerInDraft(draft, createDefaultLayer(type));
1249
+ function addRootLayerInDraft(draft, type, textDefaults) {
1250
+ return insertRootLayerInDraft(draft, createDefaultLayer(type, textDefaults));
1056
1251
  }
1057
1252
  function insertRootLayerInDraft(draft, layer) {
1058
1253
  let min = Number.POSITIVE_INFINITY;
@@ -1407,6 +1602,9 @@ function resourceStatusesEqual(a, b) {
1407
1602
  }
1408
1603
  return true;
1409
1604
  }
1605
+ function nextTextStyleEqual(a, b) {
1606
+ return a.font === b.font && a.fontSize === b.fontSize && a.fontColor === b.fontColor && a.autowrap === b.autowrap && a.lineHeight === b.lineHeight;
1607
+ }
1410
1608
  var EditorStore = class {
1411
1609
  docValue = null;
1412
1610
  uiValue = {
@@ -1420,6 +1618,7 @@ var EditorStore = class {
1420
1618
  editing: null,
1421
1619
  renaming: null,
1422
1620
  anchorExpanded: false,
1621
+ nextTextStyle: defaultNextTextStyle(),
1423
1622
  dataSourceSchema: null,
1424
1623
  guides: [],
1425
1624
  rulersVisible: true,
@@ -1451,7 +1650,7 @@ var EditorStore = class {
1451
1650
  }
1452
1651
  /** 打开/替换文档:ui 选择/编辑/重命名会话与双向历史一并重置(新文档不继承旧路径/旧事务);
1453
1652
  * 参考线/锁定集合/查找会话/建层武装与画拉会话随当次会话清空(对位轴/文档内容防护/
1454
- * 查询词/待命层型不跨文档),标尺显隐作为偏好保留 */
1653
+ * 查询词/待命层型不跨文档),标尺显隐/锚点折叠/新层样式作为偏好保留 */
1455
1654
  openDocument(canvas) {
1456
1655
  this.docValue = canvas;
1457
1656
  this.undoSteps = [];
@@ -1585,6 +1784,16 @@ var EditorStore = class {
1585
1784
  this.uiValue = { ...this.uiValue, anchorExpanded: open };
1586
1785
  this.notify({ scope: "ui", branch: "anchorExpanded" });
1587
1786
  }
1787
+ /**
1788
+ * 新层样式槽逐字段合并(text-defaults 工单 01:捕获/预设/prefs 恢复的统一
1789
+ * 写入口):patch 并入现值,五字段值等短路(重复写同值零通知),不进历史。
1790
+ */
1791
+ setNextTextStyle(patch) {
1792
+ const next = { ...this.uiValue.nextTextStyle, ...patch };
1793
+ if (nextTextStyleEqual(this.uiValue.nextTextStyle, next)) return;
1794
+ this.uiValue = { ...this.uiValue, nextTextStyle: next };
1795
+ this.notify({ scope: "ui", branch: "nextTextStyle" });
1796
+ }
1588
1797
  /**
1589
1798
  * 数据源 schema 声明替换(归一产物或降级 null,校验收口在注入缝不在这里):
1590
1799
  * 同引用短路(重复注入同一归一产物不惊动订阅方);仅 ui 通知,不参与重绘
@@ -1952,6 +2161,12 @@ function clampFindCursor(cursor, count) {
1952
2161
  if (count <= 0) return 0;
1953
2162
  return Math.min(Math.max(cursor, 0), count - 1);
1954
2163
  }
2164
+ function captureFieldOfKey(key) {
2165
+ const single = key.length === 1 ? key[0] : null;
2166
+ if (single === "font" || single === "fontSize" || single === "fontColor" || single === "autowrap") return single;
2167
+ if (key.length === 2 && key[0] === "shape" && key[1] === "lineHeight") return "lineHeight";
2168
+ return null;
2169
+ }
1955
2170
  function hasValidExpressionFragment(template) {
1956
2171
  return scanExpressionFragments(template).parts.some(
1957
2172
  (part) => part.kind === "fragment" && part.error === null
@@ -2536,8 +2751,17 @@ var EditorSession = class {
2536
2751
  * 武装建层(面板新增项/层型快捷键的统一入口,CONTEXT「武装」词条):置 ui
2537
2752
  * 分支待命层型,下一次画布按下即开画拉;建层提交或 Esc 即解除——一次性
2538
2753
  * 待命态,无粘性工具(spec 决策 2)。重复武装同型值等短路零通知。
2754
+ *
2755
+ * preset(text-defaults 工单 01,CONTEXT「文本预设」词条):文本预设 = 写
2756
+ * 新层样式槽的快捷方式——命中档位先把档位值并入槽(v1 只 fontSize),再
2757
+ * 武装;仅 TextLayer 生效;未知 id = 跳过写槽、武装照常(评审口径统一:
2758
+ * 「空转」只指写槽,武装层型本身仍合法)。
2539
2759
  */
2540
- armLayerCreate(type) {
2760
+ armLayerCreate(type, preset) {
2761
+ if (preset !== void 0 && type === "TextLayer") {
2762
+ const entry = TEXT_PRESETS.find((candidate) => candidate.id === preset);
2763
+ if (entry) this.store.setNextTextStyle({ fontSize: entry.fontSize });
2764
+ }
2541
2765
  this.store.setArmedCreate(type);
2542
2766
  }
2543
2767
  /** 解除建层武装(取消入口):仅清待命态,零副作用。 */
@@ -2615,7 +2839,7 @@ var EditorSession = class {
2615
2839
  const clicked = Math.hypot(dx, dy) <= deadZoneScene;
2616
2840
  const index = { v: -1 };
2617
2841
  this.store.transact((draft) => {
2618
- const layer = createDefaultLayer(gesture.type);
2842
+ const layer = createDefaultLayer(gesture.type, this.store.ui.nextTextStyle);
2619
2843
  if (clicked) {
2620
2844
  layer.position = { ...layer.position, x: gesture.startScene.x, y: gesture.startScene.y };
2621
2845
  } else {
@@ -2801,15 +3025,22 @@ var EditorSession = class {
2801
3025
  */
2802
3026
  getEditorPrefs() {
2803
3027
  const ui = this.store.ui;
2804
- return { rulersVisible: ui.rulersVisible, anchorExpanded: ui.anchorExpanded };
3028
+ return {
3029
+ rulersVisible: ui.rulersVisible,
3030
+ anchorExpanded: ui.anchorExpanded,
3031
+ nextTextStyle: ui.nextTextStyle
3032
+ };
2805
3033
  }
2806
3034
  /**
2807
3035
  * 应用偏好(会话建立时一次性,跨会话恢复 prefs 的写入口):整体替换
2808
3036
  * rulersVisible/anchorExpanded,不进历史;值等写入经 store 短路零通知。
3037
+ * 新层样式(text-defaults 工单 01)走宽容读逐字段合并(pickNextTextStylePatch
3038
+ * 共享谓词——与捕获同门,缺字/类型不符保留现值)。
2809
3039
  */
2810
3040
  applyEditorPrefs(prefs) {
2811
3041
  this.store.setRulersVisible(prefs.rulersVisible);
2812
3042
  this.store.setAnchorExpanded(prefs.anchorExpanded);
3043
+ this.store.setNextTextStyle(pickNextTextStylePatch(prefs.nextTextStyle));
2813
3044
  }
2814
3045
  // ---- 对齐画布(layer-align-snap 工单 01):图层盒整体对齐画布几何 ----
2815
3046
  /**
@@ -3037,6 +3268,23 @@ var EditorSession = class {
3037
3268
  renormalizeAutoHeightInDraft(layer);
3038
3269
  canonicalizeTableSyncInDraft(draft, path, key, this.textPolicies);
3039
3270
  }, options);
3271
+ this.captureNextTextStyle(path, key, value);
3272
+ }
3273
+ /**
3274
+ * 新层样式捕获(text-defaults 工单 01,CONTEXT「新层样式」词条):属性面板
3275
+ * 对 TextLayer 文本族五字段的权威写顺带更新偏好槽——「用户改过一次,下一个
3276
+ * 新层沿用」。捕获口唯一(updateSpec 漏斗):内容写(updateData)、样式粘贴
3277
+ * (搬运不是偏好)、撤销重做都不经过此口。字段合法性走 editing 域共享谓词
3278
+ * pickNextTextStylePatch(与 prefs 宽容读同门,评审合流不再双写):目标层非
3279
+ * TextLayer、键不命中或值类型不符即空转,符合字段经值等短路入槽。
3280
+ */
3281
+ captureNextTextStyle(path, key, value) {
3282
+ const layer = this.store.doc ? resolveLayer(this.store.doc, path) : null;
3283
+ if (!layer || layer.type !== "TextLayer") return;
3284
+ const field = captureFieldOfKey(key);
3285
+ if (field === null) return;
3286
+ const patch = pickNextTextStylePatch({ [field]: value });
3287
+ if (patch[field] !== void 0) this.store.setNextTextStyle(patch);
3040
3288
  }
3041
3289
  /**
3042
3290
  * 更新图层数据字段(wire data.value 的领域展开,按 type 分派):
@@ -3223,7 +3471,7 @@ var EditorSession = class {
3223
3471
  if (!this.store.doc) return;
3224
3472
  const index = { v: -1 };
3225
3473
  this.store.transact((draft) => {
3226
- index.v = addRootLayerInDraft(draft, type);
3474
+ index.v = addRootLayerInDraft(draft, type, this.store.ui.nextTextStyle);
3227
3475
  });
3228
3476
  if (index.v >= 0) this.store.setSelection(["layers", index.v]);
3229
3477
  }
@@ -3993,6 +4241,22 @@ function classifyWheel(input) {
3993
4241
  return "pan";
3994
4242
  }
3995
4243
 
4244
+ // src/shared/expressionFunctions.ts
4245
+ var EXPRESSION_FUNCTIONS = [
4246
+ { name: "date", signature: "date(path[, format])", summary: "\u65E5\u671F\u683C\u5F0F\u5316\uFF08moment \u98CE\u683C token\uFF0C\u9884\u8BBE date/datetime\uFF09" },
4247
+ { name: "number", signature: "number(path[, n])", summary: "\u5343\u5206\u4F4D\u5206\u7EC4" },
4248
+ { name: "fixed", signature: "fixed(path[, n])", summary: "\u5B9A\u70B9\u8865\u96F6\uFF08\u7F3A\u7701 2 \u4F4D\uFF09" },
4249
+ { name: "percent", signature: "percent(path[, n])", summary: "\u767E\u5206\u6BD4\uFF08\u4E58 100\uFF09" }
4250
+ ];
4251
+ var NUMBER_FUNCTION_SUGGESTIONS = EXPRESSION_FUNCTIONS.filter(
4252
+ (fn) => fn.name !== "date"
4253
+ );
4254
+ var DATE_FORMAT_SUGGESTIONS = [
4255
+ { segment: "date", description: "\u9884\u8BBE \u2192 YYYY-MM-DD" },
4256
+ { segment: "datetime", description: "\u9884\u8BBE \u2192 YYYY-MM-DD HH:mm:ss" },
4257
+ { segment: "YYYY\u5E74MM\u6708DD\u65E5", description: "\u4E2D\u6587\u6A21\u677F\uFF08\u5199\u5373\u6240\u5F97\uFF09" }
4258
+ ];
4259
+
3996
4260
  // src/shared/expressionCandidates.ts
3997
4261
  function headName(head) {
3998
4262
  if (head.kind === "row") return "row";
@@ -4010,9 +4274,57 @@ function childCandidate(entry, prefix) {
4010
4274
  const path = prefix === "" ? entry.key : `${prefix}.${entry.key}`;
4011
4275
  return { path, segment: entry.key, ...candidateMeta(entry.node) };
4012
4276
  }
4013
- function headCandidates(schema, context, rowSchema) {
4277
+ function callCandidatesFor(entry, path, exprLen) {
4278
+ const extras = [];
4279
+ if (entry.node.format === "date" || entry.node.format === "date-time") {
4280
+ extras.push({
4281
+ path: `date(${path}, '')`,
4282
+ segment: `date(${path}, '')`,
4283
+ cursorBack: 2,
4284
+ replaceLen: exprLen,
4285
+ filterKey: entry.key,
4286
+ description: "\u65E5\u671F\u683C\u5F0F\u5316",
4287
+ type: "\u51FD\u6570"
4288
+ });
4289
+ } else if (entry.node.type === "number") {
4290
+ for (const fn of NUMBER_FUNCTION_SUGGESTIONS) {
4291
+ extras.push({
4292
+ path: `${fn.name}(${path})`,
4293
+ segment: `${fn.name}(${path})`,
4294
+ replaceLen: exprLen,
4295
+ filterKey: entry.key,
4296
+ description: fn.summary,
4297
+ type: "\u51FD\u6570"
4298
+ });
4299
+ }
4300
+ }
4301
+ return extras;
4302
+ }
4303
+ function childCandidates(entry, prefix, exprLen, withBuckets) {
4304
+ const base = childCandidate(entry, prefix);
4305
+ if (!withBuckets) return [base];
4306
+ return [base, ...callCandidatesFor(entry, base.path, exprLen)];
4307
+ }
4308
+ function functionCandidates() {
4309
+ return EXPRESSION_FUNCTIONS.map((fn) => ({
4310
+ path: fn.signature,
4311
+ segment: `${fn.name}()`,
4312
+ cursorBack: 1,
4313
+ description: fn.summary,
4314
+ type: "\u51FD\u6570"
4315
+ }));
4316
+ }
4317
+ function dateFormatCandidates() {
4318
+ return DATE_FORMAT_SUGGESTIONS.map((suggestion) => ({
4319
+ path: suggestion.segment,
4320
+ segment: suggestion.segment,
4321
+ description: suggestion.description,
4322
+ type: "\u683C\u5F0F"
4323
+ }));
4324
+ }
4325
+ function headCandidates(schema, context, rowSchema, exprLen, withBuckets) {
4014
4326
  const view = schemaChildEntries(schema);
4015
- const candidates = view.entries.map((entry) => childCandidate(entry, ""));
4327
+ const candidates = view.entries.flatMap((entry) => childCandidates(entry, "", exprLen, withBuckets));
4016
4328
  candidates.push({ path: "$root", segment: "$root" });
4017
4329
  if (context === "row") {
4018
4330
  candidates.push(rowSchema ? { path: "row", segment: "row", ...candidateMeta(rowSchema) } : { path: "row", segment: "row" });
@@ -4020,15 +4332,35 @@ function headCandidates(schema, context, rowSchema) {
4020
4332
  }
4021
4333
  return view.open === true ? { candidates, open: true } : { candidates };
4022
4334
  }
4023
- function enumerateExpressionCandidates(schema, query) {
4335
+ function callSlotResult(schema, query) {
4336
+ const slot = expressionCallSlotAtEnd(query.expr);
4337
+ if (slot === null) {
4338
+ return { ok: true, prefix: "", partial: "", candidates: [] };
4339
+ }
4340
+ if (slot.stringSlot && slot.index >= 1 && slot.name === "date") {
4341
+ return { ok: true, prefix: "", partial: slot.text, candidates: dateFormatCandidates() };
4342
+ }
4343
+ if (slot.index === 0 && !isCallFragment(slot.text)) {
4344
+ const sub = enumeratePathCandidates(schema, query, slot.text, false);
4345
+ if (!sub.ok) return { ok: true, prefix: "", partial: "", candidates: [] };
4346
+ return sub;
4347
+ }
4348
+ return { ok: true, prefix: "", partial: "", candidates: [] };
4349
+ }
4350
+ function enumeratePathCandidates(schema, query, expr, allowCallSuggestions) {
4024
4351
  if (schema === null) {
4025
4352
  return { ok: true, prefix: "", partial: "", candidates: [] };
4026
4353
  }
4027
- const hadTrailingDot = query.expr.endsWith(".");
4028
- const stripped = hadTrailingDot ? query.expr.slice(0, -1) : query.expr;
4354
+ const hadTrailingDot = expr.endsWith(".");
4355
+ const stripped = hadTrailingDot ? expr.slice(0, -1) : expr;
4029
4356
  if (stripped === "") {
4030
- if (query.expr === "") {
4031
- return { ok: true, prefix: "", partial: "", ...headCandidates(schema, query.context, query.rowSchema) };
4357
+ if (expr === "") {
4358
+ return {
4359
+ ok: true,
4360
+ prefix: "",
4361
+ partial: "",
4362
+ ...headCandidates(schema, query.context, query.rowSchema, expr.length, false)
4363
+ };
4032
4364
  }
4033
4365
  return { ok: false, reason: "expression_syntax_error" };
4034
4366
  }
@@ -4049,8 +4381,17 @@ function enumerateExpressionCandidates(schema, query) {
4049
4381
  prefixPath = [];
4050
4382
  partial = headName(head);
4051
4383
  }
4384
+ const withBuckets = allowCallSuggestions && partial !== "";
4052
4385
  if (prefixPath.length === 0) {
4053
- return { ok: true, prefix: "", partial, ...headCandidates(schema, query.context, query.rowSchema) };
4386
+ const view2 = headCandidates(schema, query.context, query.rowSchema, expr.length, withBuckets);
4387
+ const fns = allowCallSuggestions && expr !== "" ? functionCandidates() : [];
4388
+ return {
4389
+ ok: true,
4390
+ prefix: "",
4391
+ partial,
4392
+ candidates: [...view2.candidates, ...fns],
4393
+ ...view2.open === true ? { open: true } : {}
4394
+ };
4054
4395
  }
4055
4396
  let base;
4056
4397
  let segments;
@@ -4079,10 +4420,19 @@ function enumerateExpressionCandidates(schema, query) {
4079
4420
  ok: true,
4080
4421
  prefix,
4081
4422
  partial,
4082
- candidates: view.entries.map((entry) => childCandidate(entry, prefix)),
4423
+ candidates: view.entries.flatMap((entry) => childCandidates(entry, prefix, expr.length, withBuckets)),
4083
4424
  ...view.open === true ? { open: true } : {}
4084
4425
  };
4085
4426
  }
4427
+ function enumerateExpressionCandidates(schema, query) {
4428
+ if (schema === null) {
4429
+ return { ok: true, prefix: "", partial: "", candidates: [] };
4430
+ }
4431
+ if (isCallFragment(query.expr)) {
4432
+ return callSlotResult(schema, query);
4433
+ }
4434
+ return enumeratePathCandidates(schema, query, query.expr, true);
4435
+ }
4086
4436
 
4087
4437
  // src/shared/rowsPathCandidates.ts
4088
4438
  function isArrayNode(node) {
@@ -4232,15 +4582,18 @@ export {
4232
4582
  ANCHORS,
4233
4583
  ARM_CREATE_LAYER_TYPES,
4234
4584
  CREATE_DEAD_ZONE_SCREEN_PX,
4585
+ DATE_FORMAT_SUGGESTIONS,
4235
4586
  DEFAULT_EDITOR_SHORTCUTS,
4236
4587
  DEFAULT_ZOOM_BOUNDS,
4237
4588
  EDITOR_STATE_SCHEMA_VERSION,
4589
+ EXPRESSION_FUNCTIONS,
4238
4590
  EditorSession,
4239
4591
  EditorStore,
4240
4592
  FontCatalog,
4241
4593
  HORIZONTAL_ALIGNS,
4242
4594
  LAYER_TYPES,
4243
4595
  MAX_HISTORY_STEPS,
4596
+ NUMBER_FUNCTION_SUGGESTIONS,
4244
4597
  PACKAGE_NAME,
4245
4598
  PAN_TO_BOX_MARGIN_PX,
4246
4599
  PASTE_OFFSET_PX,
@@ -4250,6 +4603,7 @@ export {
4250
4603
  RESIZE_MIN_SIZE_PX,
4251
4604
  SNAP_THRESHOLD_SCREEN_PX,
4252
4605
  STYLE_FIELD_KEYS_BY_TYPE,
4606
+ TEXT_PRESETS,
4253
4607
  UploadHandlerMissingError,
4254
4608
  VERTICAL_ALIGNS,
4255
4609
  WHEEL_ZOOM_STEP,
@@ -4273,9 +4627,11 @@ export {
4273
4627
  createDefaultLayer,
4274
4628
  createRubberBandRect,
4275
4629
  createTemplateTable,
4630
+ defaultNextTextStyle,
4276
4631
  deleteLayerInDraft,
4277
4632
  enumerateExpressionCandidates,
4278
4633
  enumerateRowsPathCandidates,
4634
+ expressionCallSlotAtEnd,
4279
4635
  expressionFragmentAtCursor,
4280
4636
  fitRect,
4281
4637
  fitViewport,
@@ -4285,6 +4641,7 @@ export {
4285
4641
  hitTest,
4286
4642
  insertRootLayerAdjacentInDraft,
4287
4643
  insertRootLayerInDraft,
4644
+ isCallFragment,
4288
4645
  isImageContentUndrawn,
4289
4646
  isLayerPath,
4290
4647
  isLockedPath,
@@ -4303,11 +4660,14 @@ export {
4303
4660
  normalizeExpressionSchemaSource,
4304
4661
  panBy,
4305
4662
  panToBox,
4663
+ parseExpressionCall,
4306
4664
  parseExpressionPath,
4307
4665
  parseExpressionSchema,
4308
4666
  pastePosition,
4309
4667
  pathStartsWith,
4310
4668
  pathsEqual,
4669
+ phpTrim2 as phpTrim,
4670
+ pickNextTextStylePatch,
4311
4671
  prepareRootPaste,
4312
4672
  readSpecField,
4313
4673
  remapPathAfterSplice,