@hankchen/canvas-editor 0.3.0 → 0.5.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) {
@@ -597,7 +602,6 @@ function compileRefNode(raw, rawRef, declPath, ctx) {
597
602
  }
598
603
  function compileInlineNode(raw, declPath, ctx) {
599
604
  const node = annotationsOf(raw);
600
- if (raw["additionalProperties"] === true) node.open = true;
601
605
  const rawProperties = raw["properties"];
602
606
  if (isPlainObject(rawProperties)) {
603
607
  const properties = /* @__PURE__ */ new Map();
@@ -611,6 +615,23 @@ function compileInlineNode(raw, declPath, ctx) {
611
615
  if (isPlainObject(rawItems)) node.items = compileSchemaNode(rawItems, `${declPath}/items`, ctx);
612
616
  return node;
613
617
  }
618
+ var OPEN_MAP_DETAIL_SUFFIX = "\u2014\u2014\u5F00\u653E\u6620\u5C04\u5951\u7EA6\u5DF2\u5E9F\u9664\uFF0C\u52A8\u6001\u952E\u8BF7\u7531\u5BBF\u4E3B\u7EC4\u88C5\u6210\u5177\u4F53 properties \u540E\u6CE8\u5165";
619
+ function findOpenMapping(raw, path) {
620
+ if (Array.isArray(raw)) {
621
+ for (let i = 0; i < raw.length; i += 1) {
622
+ const hit = findOpenMapping(raw[i], `${path}/${i}`);
623
+ if (hit !== null) return hit;
624
+ }
625
+ return null;
626
+ }
627
+ if (!isPlainObject(raw)) return null;
628
+ if (raw["additionalProperties"] === true) return path;
629
+ for (const [key, value] of Object.entries(raw)) {
630
+ const hit = findOpenMapping(value, `${path}/${pointerEscape(key)}`);
631
+ if (hit !== null) return hit;
632
+ }
633
+ return null;
634
+ }
614
635
  function parseExpressionSchema(raw) {
615
636
  if (!isPlainObject(raw)) {
616
637
  return { ok: false, reason: "invalid_schema", detail: "schema \u6839\u5FC5\u987B\u662F\u5BF9\u8C61\u58F0\u660E" };
@@ -626,6 +647,10 @@ function parseExpressionSchema(raw) {
626
647
  if (!isPlainObject(rawProperties)) {
627
648
  return { ok: false, reason: "invalid_schema", detail: "\u6839\u58F0\u660E\u7F3A properties \u5C42\u7EA7\uFF08schema \u5373\u8F7D\u8377\u5F62\u72B6\uFF0C\u5FC5\u987B\u58F0\u660E\u9876\u5C42\u952E\uFF09" };
628
649
  }
650
+ const openPath = findOpenMapping(raw, "#");
651
+ if (openPath !== null) {
652
+ return { ok: false, reason: "open_map_unsupported", detail: `"${openPath}" \u58F0\u660E\u4E86 additionalProperties: true${OPEN_MAP_DETAIL_SUFFIX}` };
653
+ }
629
654
  const diagnostics = [];
630
655
  const schema = compileSchemaNode(raw, "#", { root: raw, diagnostics, activePath: /* @__PURE__ */ new Set() });
631
656
  return { ok: true, schema, diagnostics };
@@ -667,7 +692,7 @@ function schemaNodeAtPath(node, segments) {
667
692
  function schemaChildEntries(node) {
668
693
  if (node.type === "array" || node.items !== void 0) return { entries: [] };
669
694
  const entries = node.properties !== void 0 ? [...node.properties].map(([key, child]) => ({ key, node: child })) : [];
670
- return node.open === true ? { entries, open: true } : { entries };
695
+ return { entries };
671
696
  }
672
697
  function resolveRowSchema(root, rowsPath) {
673
698
  const segments = rowsPath.split(".");
@@ -692,15 +717,194 @@ function parseExpressionPath(expr) {
692
717
  return { ok: true, head: { kind: "bare", name: head }, rest };
693
718
  }
694
719
 
720
+ // src/shared/expressionCall.ts
721
+ var TRIM_CHARS = " \n\r\0\v";
722
+ var WS_CHARS = " \n\r";
723
+ var HEAD_STOP_CHARS = `()',.${TRIM_CHARS}`;
724
+ function phpTrim(value) {
725
+ let start = 0;
726
+ let end = value.length;
727
+ while (start < end && TRIM_CHARS.includes(value[start])) start += 1;
728
+ while (end > start && TRIM_CHARS.includes(value[end - 1])) end -= 1;
729
+ return value.slice(start, end);
730
+ }
731
+ function skipWs(expr, pos) {
732
+ while (pos < expr.length && WS_CHARS.includes(expr[pos])) pos += 1;
733
+ return pos;
734
+ }
735
+ function fail(kind, start, end) {
736
+ return { ok: false, error: "expression_syntax_error", kind, at: { start, end } };
737
+ }
738
+ function isCallFragment(expr) {
739
+ let head = 0;
740
+ while (head < expr.length && !HEAD_STOP_CHARS.includes(expr[head])) head += 1;
741
+ if (head === 0) return false;
742
+ let pos = head;
743
+ while (pos < expr.length && TRIM_CHARS.includes(expr[pos])) pos += 1;
744
+ return expr[pos] === "(";
745
+ }
746
+ function parseExpressionCall(expr) {
747
+ let head = 0;
748
+ while (head < expr.length && !HEAD_STOP_CHARS.includes(expr[head])) head += 1;
749
+ if (head === 0) return fail("invalid_path", 0, expr.length);
750
+ const name = expr.slice(0, head);
751
+ let pos = skipWs(expr, head);
752
+ if (pos >= expr.length || expr[pos] !== "(") return fail("missing_open_paren", head, expr.length);
753
+ const openParen = pos;
754
+ pos = skipWs(expr, pos + 1);
755
+ const args = [];
756
+ const spans = [];
757
+ if (pos < expr.length && expr[pos] === ")") {
758
+ pos += 1;
759
+ } else {
760
+ for (; ; ) {
761
+ if (pos >= expr.length) return fail("unbalanced_paren", openParen, expr.length);
762
+ const arg = parseCallArg(expr, pos);
763
+ if (!arg.ok) return arg;
764
+ args.push(arg.value);
765
+ spans.push({ start: arg.start, end: arg.end });
766
+ pos = skipWs(expr, arg.end);
767
+ if (pos >= expr.length) return fail("unbalanced_paren", openParen, expr.length);
768
+ if (expr[pos] === ",") {
769
+ const comma = pos;
770
+ pos = skipWs(expr, pos + 1);
771
+ if (pos < expr.length && expr[pos] === ")") {
772
+ return fail("empty_arg_slot", comma + 1, pos);
773
+ }
774
+ continue;
775
+ }
776
+ if (expr[pos] === ")") {
777
+ pos += 1;
778
+ break;
779
+ }
780
+ return fail("bad_arg_syntax", pos, pos + 1);
781
+ }
782
+ }
783
+ pos = skipWs(expr, pos);
784
+ if (pos !== expr.length) return fail("trailing_after_call", pos, expr.length);
785
+ if (args[0] !== void 0 && args[0].type !== "path") {
786
+ const span = spans[0];
787
+ return fail("first_arg_not_path", span.start, span.end);
788
+ }
789
+ for (let i = 0; i < args.length; i += 1) {
790
+ const arg = args[i];
791
+ const span = spans[i];
792
+ if (arg.type === "path" && !parseExpressionPath(arg.value).ok) {
793
+ return fail("invalid_path_arg", span.start, span.end);
794
+ }
795
+ }
796
+ return { ok: true, name, args };
797
+ }
798
+ function parseCallArg(expr, pos) {
799
+ const len = expr.length;
800
+ const start = pos;
801
+ const ch = expr[pos];
802
+ if (ch === "'") {
803
+ let cursor = pos + 1;
804
+ let buf = "";
805
+ for (; ; ) {
806
+ if (cursor >= len) {
807
+ return { ok: false, error: "expression_syntax_error", kind: "unterminated_string", at: { start, end: len } };
808
+ }
809
+ if (expr[cursor] === "'") {
810
+ if (cursor + 1 < len && expr[cursor + 1] === "'") {
811
+ buf += "'";
812
+ cursor += 2;
813
+ continue;
814
+ }
815
+ cursor += 1;
816
+ break;
817
+ }
818
+ buf += expr[cursor];
819
+ cursor += 1;
820
+ }
821
+ return { ok: true, value: { type: "string", value: buf }, start, end: cursor };
822
+ }
823
+ if (ch >= "0" && ch <= "9") {
824
+ const isDigitAt = (index) => {
825
+ const c = expr[index];
826
+ return c !== void 0 && c >= "0" && c <= "9";
827
+ };
828
+ while (isDigitAt(pos)) pos += 1;
829
+ if (pos < len && expr[pos] === "." && isDigitAt(pos + 1)) {
830
+ pos += 1;
831
+ while (isDigitAt(pos)) pos += 1;
832
+ }
833
+ return { ok: true, value: { type: "number", value: expr.slice(start, pos) }, start, end: pos };
834
+ }
835
+ while (pos < len && expr[pos] !== "," && expr[pos] !== ")") pos += 1;
836
+ const raw = phpTrim(expr.slice(start, pos));
837
+ if (raw === "") {
838
+ return { ok: false, error: "expression_syntax_error", kind: "empty_arg_slot", at: { start, end: pos } };
839
+ }
840
+ return { ok: true, value: { type: "path", value: raw }, start, end: pos };
841
+ }
842
+ function expressionCallSlotAtEnd(expr) {
843
+ let head = 0;
844
+ while (head < expr.length && !HEAD_STOP_CHARS.includes(expr[head])) head += 1;
845
+ if (head === 0) return null;
846
+ let pos = skipWs(expr, head);
847
+ if (pos >= expr.length || expr[pos] !== "(") return null;
848
+ pos = skipWs(expr, pos + 1);
849
+ let index = 0;
850
+ for (; ; ) {
851
+ if (pos >= expr.length) return { name: expr.slice(0, head), index, text: "", stringSlot: false };
852
+ const ch = expr[pos];
853
+ if (ch === "'") {
854
+ let cursor = pos + 1;
855
+ for (; ; ) {
856
+ if (cursor >= expr.length) {
857
+ return { name: expr.slice(0, head), index, text: expr.slice(pos + 1), stringSlot: true };
858
+ }
859
+ if (expr[cursor] === "'") {
860
+ if (cursor + 1 < expr.length && expr[cursor + 1] === "'") {
861
+ cursor += 2;
862
+ continue;
863
+ }
864
+ cursor += 1;
865
+ break;
866
+ }
867
+ cursor += 1;
868
+ }
869
+ pos = cursor;
870
+ } else if (ch !== "," && ch !== ")") {
871
+ const argStart = pos;
872
+ while (pos < expr.length && expr[pos] !== "," && expr[pos] !== ")") pos += 1;
873
+ if (pos >= expr.length) {
874
+ return { name: expr.slice(0, head), index, text: phpTrim(expr.slice(argStart)), stringSlot: false };
875
+ }
876
+ }
877
+ if (pos >= expr.length) return { name: expr.slice(0, head), index: index + 1, text: "", stringSlot: false };
878
+ if (expr[pos] === ")") return null;
879
+ index += 1;
880
+ pos = skipWs(expr, pos + 1);
881
+ }
882
+ }
883
+
695
884
  // src/shared/expressionScan.ts
696
885
  var PHP_TRIM_CHARS = " \n\r\0\v";
697
- function phpTrim(value) {
886
+ function phpTrim2(value) {
698
887
  let start = 0;
699
888
  let end = value.length;
700
889
  while (start < end && PHP_TRIM_CHARS.includes(value[start])) start += 1;
701
890
  while (end > start && PHP_TRIM_CHARS.includes(value[end - 1])) end -= 1;
702
891
  return value.slice(start, end);
703
892
  }
893
+ function fragmentDiagnostic(expr) {
894
+ if (expr === "") return { kind: "empty_fragment", at: { start: 0, end: 0 } };
895
+ if (isCallFragment(expr)) {
896
+ const parsed = parseExpressionCall(expr);
897
+ if (parsed.ok) return null;
898
+ return { kind: parsed.kind, at: parsed.at };
899
+ }
900
+ if (parseExpressionPath(expr).ok) return null;
901
+ return { kind: "invalid_path", at: { start: 0, end: expr.length } };
902
+ }
903
+ function leadingPhpTrim(value) {
904
+ let start = 0;
905
+ while (start < value.length && PHP_TRIM_CHARS.includes(value[start])) start += 1;
906
+ return start;
907
+ }
704
908
  function scanExpressionFragments(template) {
705
909
  const parts = [];
706
910
  let pos = 0;
@@ -712,16 +916,34 @@ function scanExpressionFragments(template) {
712
916
  parts.push({ kind: "literal", start: pos, end: open, text: template.slice(pos, open) });
713
917
  }
714
918
  const text = template.slice(open);
715
- parts.push({ kind: "open", start: open, end: template.length, text, expr: phpTrim(text.slice(2)) });
919
+ parts.push({ kind: "open", start: open, end: template.length, text, expr: phpTrim2(text.slice(2)) });
716
920
  return { template, parts };
717
921
  }
718
922
  if (open > pos) {
719
923
  parts.push({ kind: "literal", start: pos, end: open, text: template.slice(pos, open) });
720
924
  }
721
925
  const raw = template.slice(open + 2, close);
722
- const expr = phpTrim(raw);
723
- const error = parseExpressionPath(expr).ok ? null : "expression_syntax_error";
724
- parts.push({ kind: "fragment", start: open, end: close + 2, raw, expr, error });
926
+ const expr = phpTrim2(raw);
927
+ let diagnostic = null;
928
+ const found = fragmentDiagnostic(expr);
929
+ if (found) {
930
+ const exprStart = open + 2 + leadingPhpTrim(raw);
931
+ diagnostic = {
932
+ kind: found.kind,
933
+ start: open,
934
+ end: close + 2,
935
+ at: { start: exprStart + found.at.start, end: exprStart + found.at.end }
936
+ };
937
+ }
938
+ parts.push({
939
+ kind: "fragment",
940
+ start: open,
941
+ end: close + 2,
942
+ raw,
943
+ expr,
944
+ error: diagnostic !== null ? "expression_syntax_error" : null,
945
+ diagnostic
946
+ });
725
947
  pos = close + 2;
726
948
  open = template.indexOf("{{", pos);
727
949
  }
@@ -2924,11 +3146,10 @@ var EditorSession = class {
2924
3146
  * 进入文本编辑:路径必须解析到 TextLayer。会话只进 ui 分支(不进历史),
2925
3147
  * live 文本住在绑定层的 textarea,内容层随即跳绘该层文本(防重影)。
2926
3148
  *
2927
- * 内容类型会话标志(canvas-web-expression-editing 工单 01,spec 决策 2)按
2928
- * 进入瞬间 `layer.expression !== null` 锚定进会话快照——「进入时计、编辑中
2929
- * 稳定」(verticalAnchorY 同门):编辑中面板改标记不回写标志,提交去向由
2930
- * 快照裁决(打标是显式动作,内容判定制已否决)。同路径重复进入短路(快照
2931
- * 不重锚);换层进入按新层现状重锚。
3149
+ * 内容模式活标志(ADR 0019)初值按进入瞬间 `layer.expression !== null` 取定
3150
+ * (与文档现态一致),此后不再冻结——pill 切换与内核 toggleDataMode 会话期
3151
+ * 都只翻标志,提交去向由退出时现态裁决(见 commitTextEdit)。同路径重复进入
3152
+ * 短路(会话不重建,接管序号延续);换层进入按新层现状取初值。
2932
3153
  */
2933
3154
  beginTextEdit(path) {
2934
3155
  const layer = this.textLayerAt(path);
@@ -2944,14 +3165,14 @@ var EditorSession = class {
2944
3165
  * 提交与随后 blur 竞态),会话清空后后续调用幂等,保证一次退出恰一步历史。
2945
3166
  *
2946
3167
  * 提交矩阵(canvas-web-expression-editing 工单 01,spec 决策 3,空串判定先于
2947
- * 片段判定):
2948
- * - 串与文档一致:不进历史(进出编辑零噪声),返回 false——对比对象随会话
2949
- * 模式取:表达式会话对比 `layer.expression`、字面会话对比 `layer.text`(现行
2950
- * 语义);
3168
+ * 片段判定;对比对象与去向裁决按会话标志**退出时现态**——活标志语义,ADR
3169
+ * 0019,取代旧「进入时快照」):
3170
+ * - 串与文档一致:不进历史(进出编辑零噪声),返回 false——对比对象随退出时
3171
+ * 会话标志取:表达式现态对比 `layer.expression`、静态现态对比 `layer.text`;
2951
3172
  * - 空串:删除该图层(复用 deleteLayer 的 splice/重映射语义),任何模式同语义;
2952
- * - 表达式会话且串含 ≥1 语法有效闭合片段(hasValidExpressionFragment):
3173
+ * - 表达式现态且串含 ≥1 语法有效闭合片段(hasValidExpressionFragment):
2953
3174
  * updateDataExpression 标记写(保持标记、text 自动重镜像);
2954
- * - 其余(字面会话恒走此支;表达式会话无合法片段回落):updateData 字面写、
3175
+ * - 其余(静态现态恒走此支;表达式现态无合法片段回落):updateData 字面写、
2955
3176
  * 字面接管解标——含 `{{...}}` 按字面保留,杜绝无片段标记层。
2956
3177
  */
2957
3178
  commitTextEdit(text) {
@@ -2975,8 +3196,10 @@ var EditorSession = class {
2975
3196
  /**
2976
3197
  * 编辑 overlay 的布局描述:与绘制/命中同一套布局策略求出的盒几何与排版字段
2977
3198
  * (textarea 以盒左上对位、盒尺寸占位,字号/行高/内边距按场景像素交给 CSS
2978
- * transform 缩放)。verticalAnchorY 按进入编辑时的文档文本计算,编辑中保持
2979
- * 稳定不随键入跳动。非文本层/路径失效返回 null。
3199
+ * transform 缩放)。对当前文档现算(canvas-web-edit-overlay-toolbar 工单 01,
3200
+ * ADR 0019 布局现算):会话期样式/几何权威写实时反映到查询值;verticalAnchorY
3201
+ * 按当前文档文本计算——live 键入只住 textarea 草稿不落文档,键入不触发文档
3202
+ * 变更,锚点天然稳定不跳。非文本层/路径失效返回 null。
2980
3203
  */
2981
3204
  textEditLayout(path) {
2982
3205
  const layer = this.textLayerAt(path);
@@ -3053,7 +3276,8 @@ var EditorSession = class {
3053
3276
  * 原地不动——事务后派生列表在被替换处缩一位,原下标即「下一处」(「替换后
3054
3277
  * 自动跳下一处」靠派生天然成立,末处替换则钳位停在尾)。写值经 replaceHitsInDraft
3055
3278
  * 与文本编辑提交同门(text 直写 + expression 保持 null)。无文档/空 query/
3056
- * 无命中空转返回 false。
3279
+ * 无命中空转返回 false。命中编辑层时草稿接管(会话内容独占,工单 02——
3280
+ * 替换结果出现在正在编辑的框里)。
3057
3281
  */
3058
3282
  replaceOne() {
3059
3283
  const doc = this.store.doc;
@@ -3062,7 +3286,7 @@ var EditorSession = class {
3062
3286
  const matches = scanTextMatches(doc, query);
3063
3287
  const hit = matches[clampFindCursor(this.store.ui.find.cursor, matches.length)];
3064
3288
  if (!hit) return false;
3065
- this.store.transact((draft) => {
3289
+ this.transactContent([hit.path], (draft) => {
3066
3290
  replaceHitsInDraft(draft, [hit], this.store.ui.find.replacement);
3067
3291
  });
3068
3292
  return true;
@@ -3071,7 +3295,8 @@ var EditorSession = class {
3071
3295
  * 全部替换(查找条「全部替换」按钮):命中(path + 偏移对)在事务前按原文
3072
3296
  * 快照一次算定,单事务内按同字段偏移降序应用、替换产物不重扫(replacement
3073
3297
  * 含查询串不死循环)——一次调用 = 一步历史,undo 一次全回。返回应用处数
3074
- * (计数反馈「已替换 N 处」);无文档/空 query 空转返回 0。
3298
+ * (计数反馈「已替换 N 处」);无文档/空 query 空转返回 0。命中编辑层时
3299
+ * 草稿接管(会话内容独占,工单 02)。
3075
3300
  */
3076
3301
  replaceAll() {
3077
3302
  const doc = this.store.doc;
@@ -3080,7 +3305,7 @@ var EditorSession = class {
3080
3305
  const hits = scanTextMatches(doc, query);
3081
3306
  if (hits.length === 0) return 0;
3082
3307
  let applied = 0;
3083
- this.store.transact((draft) => {
3308
+ this.transactContent(hits.map((hit) => hit.path), (draft) => {
3084
3309
  applied = replaceHitsInDraft(draft, hits, this.store.ui.find.replacement);
3085
3310
  });
3086
3311
  return applied;
@@ -3134,9 +3359,13 @@ var EditorSession = class {
3134
3359
  * null,值与标记的镜像关系随之解除——标记态文档经字面编辑退化为字面态,
3135
3360
  * 不可自动恢复属预期(spec §3.7 兼容面)。
3136
3361
  * 表/行/格无数据字段,空转。
3362
+ *
3363
+ * 会话内容独占(工单 02,ADR 0019 内核底线面):命中编辑层的写落文档的同时
3364
+ * 草稿接管(draftSeq 递增,绑定层全量替换 textarea 草稿)——旁门程序化写的
3365
+ * 结果进编辑框草稿而非被退出提交覆盖,任何方向不丢字。
3137
3366
  */
3138
3367
  updateData(path, value, options = {}) {
3139
- this.store.transact((draft) => {
3368
+ this.transactContent([path], (draft) => {
3140
3369
  const layer = resolveLayer(draft, path);
3141
3370
  if (!layer) return;
3142
3371
  switch (layer.type) {
@@ -3167,9 +3396,11 @@ var EditorSession = class {
3167
3396
  * 字面态空值归一交给 updateData/decode 分支(这里不清值,字面接管语义)。
3168
3397
  * - 仅三内容层生效,表/行/格/行模板空转(与 updateData 同门)。
3169
3398
  * - updateData 语义不变:字面写解除标记仍成立(spec §1 明示两者并排)。
3399
+ * - 命中编辑层的旁门打标同走草稿接管(draftSeq 递增;会话标志不随动——
3400
+ * 活标志只归 pill/toggleDataMode 翻,见 toggleDataMode)。
3170
3401
  */
3171
3402
  updateDataExpression(path, expression, options = {}) {
3172
- this.store.transact((draft) => {
3403
+ this.transactContent([path], (draft) => {
3173
3404
  const layer = resolveLayer(draft, path);
3174
3405
  if (!layer) return;
3175
3406
  switch (layer.type) {
@@ -3190,6 +3421,63 @@ var EditorSession = class {
3190
3421
  }
3191
3422
  }, options);
3192
3423
  }
3424
+ /**
3425
+ * 数据字段取值方式切换(静态 ⇆ 表达式,工单 02 自面板收编内核):表达式现态
3426
+ * → 字面接管(updateData,值保持现镜像);静态现态 → 打标(updateDataExpression,
3427
+ * 初值 = 值字段原文)。各为独立一步历史(无 mergeKey)。三内容层分派各取值
3428
+ * 字段,表/行/格/悬空路径空转。
3429
+ *
3430
+ * 活标志防御路由(ADR 0019):路径即编辑中的层时只翻会话活标志(零文档变更
3431
+ * 零历史步)——会话期内容模式切换的唯一写入口是会话标志,文档面锁定归编辑框
3432
+ * 草稿;面板锁定段/pill 显式调用与宿主旁路调用都安全收敛到同一语义。
3433
+ */
3434
+ toggleDataMode(path) {
3435
+ const editing = this.store.ui.editing;
3436
+ if (editing && pathsEqual(editing.path, path)) {
3437
+ this.store.setEditing({ ...editing, expression: !editing.expression });
3438
+ return;
3439
+ }
3440
+ const layer = this.store.doc ? resolveLayer(this.store.doc, path) : null;
3441
+ if (!layer) return;
3442
+ switch (layer.type) {
3443
+ case "TextLayer":
3444
+ if (layer.expression !== null) this.updateData(path, layer.text);
3445
+ else this.updateDataExpression(path, layer.text);
3446
+ return;
3447
+ case "ImageLayer":
3448
+ if (layer.expression !== null) this.updateData(path, layer.src ?? "");
3449
+ else this.updateDataExpression(path, layer.src ?? "");
3450
+ return;
3451
+ case "QrCodeLayer":
3452
+ if (layer.expression !== null) this.updateData(path, layer.value);
3453
+ else this.updateDataExpression(path, layer.value);
3454
+ return;
3455
+ default:
3456
+ return;
3457
+ }
3458
+ }
3459
+ /**
3460
+ * 内容写事务的统一形(会话内容独占工单 02):transact 落库 + 命中编辑层的
3461
+ * 草稿接管收口——updateData/updateDataExpression/replaceOne/replaceAll 四个
3462
+ * 旁门写入口同门(接管判定见 takeoverDraftIfNeeded;零变更事务天然短路)。
3463
+ */
3464
+ transactContent(paths, recipe, options = {}) {
3465
+ const docBefore = this.store.doc;
3466
+ this.store.transact(recipe, options);
3467
+ this.takeoverDraftIfNeeded(paths, docBefore);
3468
+ }
3469
+ /**
3470
+ * 草稿接管收口(工单 02):paths 中有编辑层且文档确实变更(引用比较——零变更
3471
+ * 事务 transact 空转不通知,接管序号也不动)时递增会话 draftSeq 并广播
3472
+ * ui.editing。绑定层据此全量替换 textarea 草稿为文档新值并刷新表达式诊断。
3473
+ * commitTextEdit 先清会话再落库,天然不复入接管。
3474
+ */
3475
+ takeoverDraftIfNeeded(paths, docBefore) {
3476
+ const editing = this.store.ui.editing;
3477
+ if (!editing || this.store.doc === docBefore) return;
3478
+ if (!paths.some((path) => pathsEqual(path, editing.path))) return;
3479
+ this.store.setEditing({ ...editing, draftSeq: (editing.draftSeq ?? 0) + 1 });
3480
+ }
3193
3481
  /** 更新画布级字段(未选中图层时面板的宽/高写入口);键由类型收窄为 width/height */
3194
3482
  updateCanvasProp(key, value, options = {}) {
3195
3483
  this.store.transact((draft) => {
@@ -3201,12 +3489,14 @@ var EditorSession = class {
3201
3489
  * 注入数据源 schema 声明(载荷形态,D1 钉定:声明即 compile(canvas, dataset)
3202
3490
  * 收到的 data 载荷形状,根上下文候选 = 载荷顶层键)。声明经
3203
3491
  * normalizeExpressionSchemaSource 一次性编译收口——根级结构性问题(保留键
3204
- * row/$ 前缀,前移填充期 reserved_root_key;根非对象/缺 properties 层级)降级
3205
- * 为无候选 + console 警告,局部故障($ref 断链/外部指针/环引用/目标形态不符)
3206
- * 不拒绝:该节点降叶子 + 注入期汇总告警一次,树其余部分照常服务,不抛错不
3207
- * 弹错(辅助声明不作权威);null/undefined = 清除声明。声明只住 store ui 分支:
3208
- * 不进 graph、不落 localStorage、不进 wire;openDocument 换文档不重置,
3209
- * 重注入/清除走同一入口。签名与「注入即整体替换」语义不变(D6)。
3492
+ * row/$ 前缀,前移填充期 reserved_root_key;根非对象/缺 properties 层级)
3493
+ * 与开放映射契约废除(open_map_unsupported,ADR 0020:文档任意层
3494
+ * additionalProperties: true 显式拒绝,动态键归宿主组装成具体 properties 后
3495
+ * 注入)降级为无候选 + console 警告,局部故障($ref 断链/外部指针/环引用/
3496
+ * 目标形态不符)不拒绝:该节点降叶子 + 注入期汇总告警一次,树其余部分照常
3497
+ * 服务,不抛错不弹错(辅助声明不作权威);null/undefined = 清除声明。声明只
3498
+ * 住 store ui 分支:不进 graph、不落 localStorage、不进 wire;openDocument
3499
+ * 换文档不重置,重注入/清除走同一入口。签名与「注入即整体替换」语义不变(D6)。
3210
3500
  */
3211
3501
  setDataSourceSchema(raw) {
3212
3502
  this.store.setDataSourceSchema(
@@ -4081,6 +4371,45 @@ function classifyWheel(input) {
4081
4371
  return "pan";
4082
4372
  }
4083
4373
 
4374
+ // src/shared/expressionDiagnostics.ts
4375
+ function expressionDiagnostics(template) {
4376
+ const found = [];
4377
+ for (const part of scanExpressionFragments(template).parts) {
4378
+ if (part.kind === "fragment" && part.diagnostic !== null) found.push(part.diagnostic);
4379
+ }
4380
+ return found;
4381
+ }
4382
+
4383
+ // src/shared/resourcePath.ts
4384
+ function wholeTemplateDiagnostic(template, kind) {
4385
+ return { kind, start: 0, end: template.length, at: { start: 0, end: template.length } };
4386
+ }
4387
+ function resourcePathDiagnostics(template) {
4388
+ const anchored = template.endsWith("\n") ? template.slice(0, -1) : template;
4389
+ const matched = /^\{\{([^{}]*)\}\}$/.exec(anchored);
4390
+ if (matched === null) return [wholeTemplateDiagnostic(template, "resource_not_single_ref")];
4391
+ const path = phpTrim2(matched[1] ?? "");
4392
+ if (path === "") return [wholeTemplateDiagnostic(template, "empty_fragment")];
4393
+ if (path.includes("(")) return [wholeTemplateDiagnostic(template, "resource_function_call")];
4394
+ return [];
4395
+ }
4396
+
4397
+ // src/shared/expressionFunctions.ts
4398
+ var EXPRESSION_FUNCTIONS = [
4399
+ { name: "date", signature: "date(path[, format])", summary: "\u65E5\u671F\u683C\u5F0F\u5316\uFF08moment \u98CE\u683C token\uFF0C\u9884\u8BBE date/datetime\uFF09" },
4400
+ { name: "number", signature: "number(path[, n])", summary: "\u5343\u5206\u4F4D\u5206\u7EC4" },
4401
+ { name: "fixed", signature: "fixed(path[, n])", summary: "\u5B9A\u70B9\u8865\u96F6\uFF08\u7F3A\u7701 2 \u4F4D\uFF09" },
4402
+ { name: "percent", signature: "percent(path[, n])", summary: "\u767E\u5206\u6BD4\uFF08\u4E58 100\uFF09" }
4403
+ ];
4404
+ var NUMBER_FUNCTION_SUGGESTIONS = EXPRESSION_FUNCTIONS.filter(
4405
+ (fn) => fn.name !== "date"
4406
+ );
4407
+ var DATE_FORMAT_SUGGESTIONS = [
4408
+ { segment: "date", description: "\u9884\u8BBE \u2192 YYYY-MM-DD" },
4409
+ { segment: "datetime", description: "\u9884\u8BBE \u2192 YYYY-MM-DD HH:mm:ss" },
4410
+ { segment: "YYYY\u5E74MM\u6708DD\u65E5", description: "\u4E2D\u6587\u6A21\u677F\uFF08\u5199\u5373\u6240\u5F97\uFF09" }
4411
+ ];
4412
+
4084
4413
  // src/shared/expressionCandidates.ts
4085
4414
  function headName(head) {
4086
4415
  if (head.kind === "row") return "row";
@@ -4096,27 +4425,98 @@ function candidateMeta(node) {
4096
4425
  }
4097
4426
  function childCandidate(entry, prefix) {
4098
4427
  const path = prefix === "" ? entry.key : `${prefix}.${entry.key}`;
4099
- return { path, segment: entry.key, ...candidateMeta(entry.node) };
4100
- }
4101
- function headCandidates(schema, context, rowSchema) {
4428
+ return { path, segment: entry.key, kind: "path", ...candidateMeta(entry.node) };
4429
+ }
4430
+ function callCandidatesFor(entry, path, exprLen) {
4431
+ const extras = [];
4432
+ if (entry.node.format === "date" || entry.node.format === "date-time") {
4433
+ extras.push({
4434
+ path: `date(${path}, '')`,
4435
+ segment: `date(${path}, '')`,
4436
+ cursorBack: 2,
4437
+ replaceLen: exprLen,
4438
+ filterKey: entry.key,
4439
+ kind: "function",
4440
+ description: "\u65E5\u671F\u683C\u5F0F\u5316",
4441
+ type: "\u51FD\u6570"
4442
+ });
4443
+ } else if (entry.node.type === "number") {
4444
+ for (const fn of NUMBER_FUNCTION_SUGGESTIONS) {
4445
+ extras.push({
4446
+ path: `${fn.name}(${path})`,
4447
+ segment: `${fn.name}(${path})`,
4448
+ replaceLen: exprLen,
4449
+ filterKey: entry.key,
4450
+ kind: "function",
4451
+ description: fn.summary,
4452
+ type: "\u51FD\u6570"
4453
+ });
4454
+ }
4455
+ }
4456
+ return extras;
4457
+ }
4458
+ function childCandidates(entry, prefix, exprLen, withBuckets) {
4459
+ const base = childCandidate(entry, prefix);
4460
+ if (!withBuckets) return [base];
4461
+ return [base, ...callCandidatesFor(entry, base.path, exprLen)];
4462
+ }
4463
+ function functionCandidates() {
4464
+ return EXPRESSION_FUNCTIONS.map((fn) => ({
4465
+ path: fn.signature,
4466
+ segment: `${fn.name}()`,
4467
+ cursorBack: 1,
4468
+ kind: "function",
4469
+ description: fn.summary,
4470
+ type: "\u51FD\u6570"
4471
+ }));
4472
+ }
4473
+ function dateFormatCandidates() {
4474
+ return DATE_FORMAT_SUGGESTIONS.map((suggestion) => ({
4475
+ path: suggestion.segment,
4476
+ segment: suggestion.segment,
4477
+ description: suggestion.description,
4478
+ type: "\u683C\u5F0F"
4479
+ }));
4480
+ }
4481
+ function headCandidates(schema, context, rowSchema, exprLen, withBuckets) {
4102
4482
  const view = schemaChildEntries(schema);
4103
- const candidates = view.entries.map((entry) => childCandidate(entry, ""));
4104
- candidates.push({ path: "$root", segment: "$root" });
4483
+ const candidates = view.entries.flatMap((entry) => childCandidates(entry, "", exprLen, withBuckets));
4484
+ candidates.push({ path: "$root", segment: "$root", kind: "path" });
4105
4485
  if (context === "row") {
4106
- candidates.push(rowSchema ? { path: "row", segment: "row", ...candidateMeta(rowSchema) } : { path: "row", segment: "row" });
4107
- candidates.push({ path: "$index", segment: "$index" });
4486
+ candidates.push(rowSchema ? { path: "row", segment: "row", kind: "path", ...candidateMeta(rowSchema) } : { path: "row", segment: "row", kind: "path" });
4487
+ candidates.push({ path: "$index", segment: "$index", kind: "path" });
4108
4488
  }
4109
- return view.open === true ? { candidates, open: true } : { candidates };
4489
+ return candidates;
4110
4490
  }
4111
- function enumerateExpressionCandidates(schema, query) {
4491
+ function callSlotResult(schema, query) {
4492
+ const slot = expressionCallSlotAtEnd(query.expr);
4493
+ if (slot === null) {
4494
+ return { ok: true, prefix: "", partial: "", candidates: [] };
4495
+ }
4496
+ if (slot.stringSlot && slot.index >= 1 && slot.name === "date") {
4497
+ return { ok: true, prefix: "", partial: slot.text, candidates: dateFormatCandidates() };
4498
+ }
4499
+ if (slot.index === 0 && !isCallFragment(slot.text)) {
4500
+ const sub = enumeratePathCandidates(schema, query, slot.text, false);
4501
+ if (!sub.ok) return { ok: true, prefix: "", partial: "", candidates: [] };
4502
+ return sub;
4503
+ }
4504
+ return { ok: true, prefix: "", partial: "", candidates: [] };
4505
+ }
4506
+ function enumeratePathCandidates(schema, query, expr, allowCallSuggestions) {
4112
4507
  if (schema === null) {
4113
4508
  return { ok: true, prefix: "", partial: "", candidates: [] };
4114
4509
  }
4115
- const hadTrailingDot = query.expr.endsWith(".");
4116
- const stripped = hadTrailingDot ? query.expr.slice(0, -1) : query.expr;
4510
+ const hadTrailingDot = expr.endsWith(".");
4511
+ const stripped = hadTrailingDot ? expr.slice(0, -1) : expr;
4117
4512
  if (stripped === "") {
4118
- if (query.expr === "") {
4119
- return { ok: true, prefix: "", partial: "", ...headCandidates(schema, query.context, query.rowSchema) };
4513
+ if (expr === "") {
4514
+ return {
4515
+ ok: true,
4516
+ prefix: "",
4517
+ partial: "",
4518
+ candidates: headCandidates(schema, query.context, query.rowSchema, expr.length, false)
4519
+ };
4120
4520
  }
4121
4521
  return { ok: false, reason: "expression_syntax_error" };
4122
4522
  }
@@ -4137,8 +4537,16 @@ function enumerateExpressionCandidates(schema, query) {
4137
4537
  prefixPath = [];
4138
4538
  partial = headName(head);
4139
4539
  }
4540
+ const withBuckets = allowCallSuggestions && partial !== "";
4140
4541
  if (prefixPath.length === 0) {
4141
- return { ok: true, prefix: "", partial, ...headCandidates(schema, query.context, query.rowSchema) };
4542
+ const view2 = headCandidates(schema, query.context, query.rowSchema, expr.length, withBuckets);
4543
+ const fns = allowCallSuggestions && expr !== "" ? functionCandidates() : [];
4544
+ return {
4545
+ ok: true,
4546
+ prefix: "",
4547
+ partial,
4548
+ candidates: [...view2, ...fns]
4549
+ };
4142
4550
  }
4143
4551
  let base;
4144
4552
  let segments;
@@ -4167,17 +4575,25 @@ function enumerateExpressionCandidates(schema, query) {
4167
4575
  ok: true,
4168
4576
  prefix,
4169
4577
  partial,
4170
- candidates: view.entries.map((entry) => childCandidate(entry, prefix)),
4171
- ...view.open === true ? { open: true } : {}
4578
+ candidates: view.entries.flatMap((entry) => childCandidates(entry, prefix, expr.length, withBuckets))
4172
4579
  };
4173
4580
  }
4581
+ function enumerateExpressionCandidates(schema, query) {
4582
+ if (schema === null) {
4583
+ return { ok: true, prefix: "", partial: "", candidates: [] };
4584
+ }
4585
+ if (isCallFragment(query.expr)) {
4586
+ return callSlotResult(schema, query);
4587
+ }
4588
+ return enumeratePathCandidates(schema, query, query.expr, true);
4589
+ }
4174
4590
 
4175
4591
  // src/shared/rowsPathCandidates.ts
4176
4592
  function isArrayNode(node) {
4177
4593
  return node.type === "array" || node.items !== void 0;
4178
4594
  }
4179
4595
  function isScalarNode(node) {
4180
- return !isArrayNode(node) && node.properties === void 0 && node.open !== true;
4596
+ return !isArrayNode(node) && node.properties === void 0;
4181
4597
  }
4182
4598
  function childCandidate2(key, node, prefix) {
4183
4599
  const candidate = {
@@ -4197,7 +4613,7 @@ function levelCandidates(base, prefix) {
4197
4613
  candidates.push(childCandidate2(key, child, prefix));
4198
4614
  }
4199
4615
  }
4200
- return base.open === true ? { candidates, open: true } : { candidates };
4616
+ return candidates;
4201
4617
  }
4202
4618
  function enumerateRowsPathCandidates(startSchema, input) {
4203
4619
  if (startSchema === null) return null;
@@ -4213,11 +4629,7 @@ function enumerateRowsPathCandidates(startSchema, input) {
4213
4629
  }
4214
4630
  if (isArrayNode(base) || isScalarNode(base)) return null;
4215
4631
  const view = levelCandidates(base, completed.join("."));
4216
- return {
4217
- partial,
4218
- candidates: view.candidates,
4219
- ...view.open === true ? { open: true } : {}
4220
- };
4632
+ return { partial, candidates: view };
4221
4633
  }
4222
4634
  function resolveRowsPathStartSchema(doc, path, rootSchema) {
4223
4635
  if (rootSchema === null) return null;
@@ -4320,15 +4732,18 @@ export {
4320
4732
  ANCHORS,
4321
4733
  ARM_CREATE_LAYER_TYPES,
4322
4734
  CREATE_DEAD_ZONE_SCREEN_PX,
4735
+ DATE_FORMAT_SUGGESTIONS,
4323
4736
  DEFAULT_EDITOR_SHORTCUTS,
4324
4737
  DEFAULT_ZOOM_BOUNDS,
4325
4738
  EDITOR_STATE_SCHEMA_VERSION,
4739
+ EXPRESSION_FUNCTIONS,
4326
4740
  EditorSession,
4327
4741
  EditorStore,
4328
4742
  FontCatalog,
4329
4743
  HORIZONTAL_ALIGNS,
4330
4744
  LAYER_TYPES,
4331
4745
  MAX_HISTORY_STEPS,
4746
+ NUMBER_FUNCTION_SUGGESTIONS,
4332
4747
  PACKAGE_NAME,
4333
4748
  PAN_TO_BOX_MARGIN_PX,
4334
4749
  PASTE_OFFSET_PX,
@@ -4366,6 +4781,8 @@ export {
4366
4781
  deleteLayerInDraft,
4367
4782
  enumerateExpressionCandidates,
4368
4783
  enumerateRowsPathCandidates,
4784
+ expressionCallSlotAtEnd,
4785
+ expressionDiagnostics,
4369
4786
  expressionFragmentAtCursor,
4370
4787
  fitRect,
4371
4788
  fitViewport,
@@ -4375,6 +4792,7 @@ export {
4375
4792
  hitTest,
4376
4793
  insertRootLayerAdjacentInDraft,
4377
4794
  insertRootLayerInDraft,
4795
+ isCallFragment,
4378
4796
  isImageContentUndrawn,
4379
4797
  isLayerPath,
4380
4798
  isLockedPath,
@@ -4393,11 +4811,13 @@ export {
4393
4811
  normalizeExpressionSchemaSource,
4394
4812
  panBy,
4395
4813
  panToBox,
4814
+ parseExpressionCall,
4396
4815
  parseExpressionPath,
4397
4816
  parseExpressionSchema,
4398
4817
  pastePosition,
4399
4818
  pathStartsWith,
4400
4819
  pathsEqual,
4820
+ phpTrim2 as phpTrim,
4401
4821
  pickNextTextStylePatch,
4402
4822
  prepareRootPaste,
4403
4823
  readSpecField,
@@ -4414,6 +4834,7 @@ export {
4414
4834
  resolveRowsPathStartSchema,
4415
4835
  resolveSnap,
4416
4836
  resolveSnapPoints,
4837
+ resourcePathDiagnostics,
4417
4838
  rootLayerOf,
4418
4839
  scanExpressionFragments,
4419
4840
  scanTextMatches,