@hankchen/canvas-editor 0.4.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
@@ -602,7 +602,6 @@ function compileRefNode(raw, rawRef, declPath, ctx) {
602
602
  }
603
603
  function compileInlineNode(raw, declPath, ctx) {
604
604
  const node = annotationsOf(raw);
605
- if (raw["additionalProperties"] === true) node.open = true;
606
605
  const rawProperties = raw["properties"];
607
606
  if (isPlainObject(rawProperties)) {
608
607
  const properties = /* @__PURE__ */ new Map();
@@ -616,6 +615,23 @@ function compileInlineNode(raw, declPath, ctx) {
616
615
  if (isPlainObject(rawItems)) node.items = compileSchemaNode(rawItems, `${declPath}/items`, ctx);
617
616
  return node;
618
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
+ }
619
635
  function parseExpressionSchema(raw) {
620
636
  if (!isPlainObject(raw)) {
621
637
  return { ok: false, reason: "invalid_schema", detail: "schema \u6839\u5FC5\u987B\u662F\u5BF9\u8C61\u58F0\u660E" };
@@ -631,6 +647,10 @@ function parseExpressionSchema(raw) {
631
647
  if (!isPlainObject(rawProperties)) {
632
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" };
633
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
+ }
634
654
  const diagnostics = [];
635
655
  const schema = compileSchemaNode(raw, "#", { root: raw, diagnostics, activePath: /* @__PURE__ */ new Set() });
636
656
  return { ok: true, schema, diagnostics };
@@ -672,7 +692,7 @@ function schemaNodeAtPath(node, segments) {
672
692
  function schemaChildEntries(node) {
673
693
  if (node.type === "array" || node.items !== void 0) return { entries: [] };
674
694
  const entries = node.properties !== void 0 ? [...node.properties].map(([key, child]) => ({ key, node: child })) : [];
675
- return node.open === true ? { entries, open: true } : { entries };
695
+ return { entries };
676
696
  }
677
697
  function resolveRowSchema(root, rowsPath) {
678
698
  const segments = rowsPath.split(".");
@@ -712,6 +732,9 @@ function skipWs(expr, pos) {
712
732
  while (pos < expr.length && WS_CHARS.includes(expr[pos])) pos += 1;
713
733
  return pos;
714
734
  }
735
+ function fail(kind, start, end) {
736
+ return { ok: false, error: "expression_syntax_error", kind, at: { start, end } };
737
+ }
715
738
  function isCallFragment(expr) {
716
739
  let head = 0;
717
740
  while (head < expr.length && !HEAD_STOP_CHARS.includes(expr[head])) head += 1;
@@ -723,26 +746,30 @@ function isCallFragment(expr) {
723
746
  function parseExpressionCall(expr) {
724
747
  let head = 0;
725
748
  while (head < expr.length && !HEAD_STOP_CHARS.includes(expr[head])) head += 1;
726
- if (head === 0) return { ok: false, error: "expression_syntax_error" };
749
+ if (head === 0) return fail("invalid_path", 0, expr.length);
727
750
  const name = expr.slice(0, head);
728
751
  let pos = skipWs(expr, head);
729
- if (pos >= expr.length || expr[pos] !== "(") return { ok: false, error: "expression_syntax_error" };
752
+ if (pos >= expr.length || expr[pos] !== "(") return fail("missing_open_paren", head, expr.length);
753
+ const openParen = pos;
730
754
  pos = skipWs(expr, pos + 1);
731
755
  const args = [];
756
+ const spans = [];
732
757
  if (pos < expr.length && expr[pos] === ")") {
733
758
  pos += 1;
734
759
  } else {
735
760
  for (; ; ) {
736
- if (pos >= expr.length) return { ok: false, error: "expression_syntax_error" };
761
+ if (pos >= expr.length) return fail("unbalanced_paren", openParen, expr.length);
737
762
  const arg = parseCallArg(expr, pos);
738
763
  if (!arg.ok) return arg;
739
764
  args.push(arg.value);
765
+ spans.push({ start: arg.start, end: arg.end });
740
766
  pos = skipWs(expr, arg.end);
741
- if (pos >= expr.length) return { ok: false, error: "expression_syntax_error" };
767
+ if (pos >= expr.length) return fail("unbalanced_paren", openParen, expr.length);
742
768
  if (expr[pos] === ",") {
769
+ const comma = pos;
743
770
  pos = skipWs(expr, pos + 1);
744
771
  if (pos < expr.length && expr[pos] === ")") {
745
- return { ok: false, error: "expression_syntax_error" };
772
+ return fail("empty_arg_slot", comma + 1, pos);
746
773
  }
747
774
  continue;
748
775
  }
@@ -750,26 +777,35 @@ function parseExpressionCall(expr) {
750
777
  pos += 1;
751
778
  break;
752
779
  }
753
- return { ok: false, error: "expression_syntax_error" };
780
+ return fail("bad_arg_syntax", pos, pos + 1);
754
781
  }
755
782
  }
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) {
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];
759
792
  if (arg.type === "path" && !parseExpressionPath(arg.value).ok) {
760
- return { ok: false, error: "expression_syntax_error" };
793
+ return fail("invalid_path_arg", span.start, span.end);
761
794
  }
762
795
  }
763
796
  return { ok: true, name, args };
764
797
  }
765
798
  function parseCallArg(expr, pos) {
766
799
  const len = expr.length;
800
+ const start = pos;
767
801
  const ch = expr[pos];
768
802
  if (ch === "'") {
769
803
  let cursor = pos + 1;
770
804
  let buf = "";
771
805
  for (; ; ) {
772
- if (cursor >= len) return { ok: false, error: "expression_syntax_error" };
806
+ if (cursor >= len) {
807
+ return { ok: false, error: "expression_syntax_error", kind: "unterminated_string", at: { start, end: len } };
808
+ }
773
809
  if (expr[cursor] === "'") {
774
810
  if (cursor + 1 < len && expr[cursor + 1] === "'") {
775
811
  buf += "'";
@@ -782,10 +818,9 @@ function parseCallArg(expr, pos) {
782
818
  buf += expr[cursor];
783
819
  cursor += 1;
784
820
  }
785
- return { ok: true, value: { type: "string", value: buf }, end: cursor };
821
+ return { ok: true, value: { type: "string", value: buf }, start, end: cursor };
786
822
  }
787
823
  if (ch >= "0" && ch <= "9") {
788
- const start2 = pos;
789
824
  const isDigitAt = (index) => {
790
825
  const c = expr[index];
791
826
  return c !== void 0 && c >= "0" && c <= "9";
@@ -795,13 +830,14 @@ function parseCallArg(expr, pos) {
795
830
  pos += 1;
796
831
  while (isDigitAt(pos)) pos += 1;
797
832
  }
798
- return { ok: true, value: { type: "number", value: expr.slice(start2, pos) }, end: pos };
833
+ return { ok: true, value: { type: "number", value: expr.slice(start, pos) }, start, end: pos };
799
834
  }
800
- const start = pos;
801
835
  while (pos < len && expr[pos] !== "," && expr[pos] !== ")") pos += 1;
802
836
  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 };
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 };
805
841
  }
806
842
  function expressionCallSlotAtEnd(expr) {
807
843
  let head = 0;
@@ -854,12 +890,20 @@ function phpTrim2(value) {
854
890
  while (end > start && PHP_TRIM_CHARS.includes(value[end - 1])) end -= 1;
855
891
  return value.slice(start, end);
856
892
  }
857
- function fragmentSyntaxError(expr) {
858
- if (expr === "") return "expression_syntax_error";
893
+ function fragmentDiagnostic(expr) {
894
+ if (expr === "") return { kind: "empty_fragment", at: { start: 0, end: 0 } };
859
895
  if (isCallFragment(expr)) {
860
- return parseExpressionCall(expr).ok ? null : "expression_syntax_error";
896
+ const parsed = parseExpressionCall(expr);
897
+ if (parsed.ok) return null;
898
+ return { kind: parsed.kind, at: parsed.at };
861
899
  }
862
- return parseExpressionPath(expr).ok ? null : "expression_syntax_error";
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;
863
907
  }
864
908
  function scanExpressionFragments(template) {
865
909
  const parts = [];
@@ -880,8 +924,26 @@ function scanExpressionFragments(template) {
880
924
  }
881
925
  const raw = template.slice(open + 2, close);
882
926
  const expr = phpTrim2(raw);
883
- const error = fragmentSyntaxError(expr);
884
- parts.push({ kind: "fragment", start: open, end: close + 2, raw, expr, error });
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
+ });
885
947
  pos = close + 2;
886
948
  open = template.indexOf("{{", pos);
887
949
  }
@@ -3084,11 +3146,10 @@ var EditorSession = class {
3084
3146
  * 进入文本编辑:路径必须解析到 TextLayer。会话只进 ui 分支(不进历史),
3085
3147
  * live 文本住在绑定层的 textarea,内容层随即跳绘该层文本(防重影)。
3086
3148
  *
3087
- * 内容类型会话标志(canvas-web-expression-editing 工单 01,spec 决策 2)按
3088
- * 进入瞬间 `layer.expression !== null` 锚定进会话快照——「进入时计、编辑中
3089
- * 稳定」(verticalAnchorY 同门):编辑中面板改标记不回写标志,提交去向由
3090
- * 快照裁决(打标是显式动作,内容判定制已否决)。同路径重复进入短路(快照
3091
- * 不重锚);换层进入按新层现状重锚。
3149
+ * 内容模式活标志(ADR 0019)初值按进入瞬间 `layer.expression !== null` 取定
3150
+ * (与文档现态一致),此后不再冻结——pill 切换与内核 toggleDataMode 会话期
3151
+ * 都只翻标志,提交去向由退出时现态裁决(见 commitTextEdit)。同路径重复进入
3152
+ * 短路(会话不重建,接管序号延续);换层进入按新层现状取初值。
3092
3153
  */
3093
3154
  beginTextEdit(path) {
3094
3155
  const layer = this.textLayerAt(path);
@@ -3104,14 +3165,14 @@ var EditorSession = class {
3104
3165
  * 提交与随后 blur 竞态),会话清空后后续调用幂等,保证一次退出恰一步历史。
3105
3166
  *
3106
3167
  * 提交矩阵(canvas-web-expression-editing 工单 01,spec 决策 3,空串判定先于
3107
- * 片段判定):
3108
- * - 串与文档一致:不进历史(进出编辑零噪声),返回 false——对比对象随会话
3109
- * 模式取:表达式会话对比 `layer.expression`、字面会话对比 `layer.text`(现行
3110
- * 语义);
3168
+ * 片段判定;对比对象与去向裁决按会话标志**退出时现态**——活标志语义,ADR
3169
+ * 0019,取代旧「进入时快照」):
3170
+ * - 串与文档一致:不进历史(进出编辑零噪声),返回 false——对比对象随退出时
3171
+ * 会话标志取:表达式现态对比 `layer.expression`、静态现态对比 `layer.text`;
3111
3172
  * - 空串:删除该图层(复用 deleteLayer 的 splice/重映射语义),任何模式同语义;
3112
- * - 表达式会话且串含 ≥1 语法有效闭合片段(hasValidExpressionFragment):
3173
+ * - 表达式现态且串含 ≥1 语法有效闭合片段(hasValidExpressionFragment):
3113
3174
  * updateDataExpression 标记写(保持标记、text 自动重镜像);
3114
- * - 其余(字面会话恒走此支;表达式会话无合法片段回落):updateData 字面写、
3175
+ * - 其余(静态现态恒走此支;表达式现态无合法片段回落):updateData 字面写、
3115
3176
  * 字面接管解标——含 `{{...}}` 按字面保留,杜绝无片段标记层。
3116
3177
  */
3117
3178
  commitTextEdit(text) {
@@ -3135,8 +3196,10 @@ var EditorSession = class {
3135
3196
  /**
3136
3197
  * 编辑 overlay 的布局描述:与绘制/命中同一套布局策略求出的盒几何与排版字段
3137
3198
  * (textarea 以盒左上对位、盒尺寸占位,字号/行高/内边距按场景像素交给 CSS
3138
- * transform 缩放)。verticalAnchorY 按进入编辑时的文档文本计算,编辑中保持
3139
- * 稳定不随键入跳动。非文本层/路径失效返回 null。
3199
+ * transform 缩放)。对当前文档现算(canvas-web-edit-overlay-toolbar 工单 01,
3200
+ * ADR 0019 布局现算):会话期样式/几何权威写实时反映到查询值;verticalAnchorY
3201
+ * 按当前文档文本计算——live 键入只住 textarea 草稿不落文档,键入不触发文档
3202
+ * 变更,锚点天然稳定不跳。非文本层/路径失效返回 null。
3140
3203
  */
3141
3204
  textEditLayout(path) {
3142
3205
  const layer = this.textLayerAt(path);
@@ -3213,7 +3276,8 @@ var EditorSession = class {
3213
3276
  * 原地不动——事务后派生列表在被替换处缩一位,原下标即「下一处」(「替换后
3214
3277
  * 自动跳下一处」靠派生天然成立,末处替换则钳位停在尾)。写值经 replaceHitsInDraft
3215
3278
  * 与文本编辑提交同门(text 直写 + expression 保持 null)。无文档/空 query/
3216
- * 无命中空转返回 false。
3279
+ * 无命中空转返回 false。命中编辑层时草稿接管(会话内容独占,工单 02——
3280
+ * 替换结果出现在正在编辑的框里)。
3217
3281
  */
3218
3282
  replaceOne() {
3219
3283
  const doc = this.store.doc;
@@ -3222,7 +3286,7 @@ var EditorSession = class {
3222
3286
  const matches = scanTextMatches(doc, query);
3223
3287
  const hit = matches[clampFindCursor(this.store.ui.find.cursor, matches.length)];
3224
3288
  if (!hit) return false;
3225
- this.store.transact((draft) => {
3289
+ this.transactContent([hit.path], (draft) => {
3226
3290
  replaceHitsInDraft(draft, [hit], this.store.ui.find.replacement);
3227
3291
  });
3228
3292
  return true;
@@ -3231,7 +3295,8 @@ var EditorSession = class {
3231
3295
  * 全部替换(查找条「全部替换」按钮):命中(path + 偏移对)在事务前按原文
3232
3296
  * 快照一次算定,单事务内按同字段偏移降序应用、替换产物不重扫(replacement
3233
3297
  * 含查询串不死循环)——一次调用 = 一步历史,undo 一次全回。返回应用处数
3234
- * (计数反馈「已替换 N 处」);无文档/空 query 空转返回 0。
3298
+ * (计数反馈「已替换 N 处」);无文档/空 query 空转返回 0。命中编辑层时
3299
+ * 草稿接管(会话内容独占,工单 02)。
3235
3300
  */
3236
3301
  replaceAll() {
3237
3302
  const doc = this.store.doc;
@@ -3240,7 +3305,7 @@ var EditorSession = class {
3240
3305
  const hits = scanTextMatches(doc, query);
3241
3306
  if (hits.length === 0) return 0;
3242
3307
  let applied = 0;
3243
- this.store.transact((draft) => {
3308
+ this.transactContent(hits.map((hit) => hit.path), (draft) => {
3244
3309
  applied = replaceHitsInDraft(draft, hits, this.store.ui.find.replacement);
3245
3310
  });
3246
3311
  return applied;
@@ -3294,9 +3359,13 @@ var EditorSession = class {
3294
3359
  * null,值与标记的镜像关系随之解除——标记态文档经字面编辑退化为字面态,
3295
3360
  * 不可自动恢复属预期(spec §3.7 兼容面)。
3296
3361
  * 表/行/格无数据字段,空转。
3362
+ *
3363
+ * 会话内容独占(工单 02,ADR 0019 内核底线面):命中编辑层的写落文档的同时
3364
+ * 草稿接管(draftSeq 递增,绑定层全量替换 textarea 草稿)——旁门程序化写的
3365
+ * 结果进编辑框草稿而非被退出提交覆盖,任何方向不丢字。
3297
3366
  */
3298
3367
  updateData(path, value, options = {}) {
3299
- this.store.transact((draft) => {
3368
+ this.transactContent([path], (draft) => {
3300
3369
  const layer = resolveLayer(draft, path);
3301
3370
  if (!layer) return;
3302
3371
  switch (layer.type) {
@@ -3327,9 +3396,11 @@ var EditorSession = class {
3327
3396
  * 字面态空值归一交给 updateData/decode 分支(这里不清值,字面接管语义)。
3328
3397
  * - 仅三内容层生效,表/行/格/行模板空转(与 updateData 同门)。
3329
3398
  * - updateData 语义不变:字面写解除标记仍成立(spec §1 明示两者并排)。
3399
+ * - 命中编辑层的旁门打标同走草稿接管(draftSeq 递增;会话标志不随动——
3400
+ * 活标志只归 pill/toggleDataMode 翻,见 toggleDataMode)。
3330
3401
  */
3331
3402
  updateDataExpression(path, expression, options = {}) {
3332
- this.store.transact((draft) => {
3403
+ this.transactContent([path], (draft) => {
3333
3404
  const layer = resolveLayer(draft, path);
3334
3405
  if (!layer) return;
3335
3406
  switch (layer.type) {
@@ -3350,6 +3421,63 @@ var EditorSession = class {
3350
3421
  }
3351
3422
  }, options);
3352
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
+ }
3353
3481
  /** 更新画布级字段(未选中图层时面板的宽/高写入口);键由类型收窄为 width/height */
3354
3482
  updateCanvasProp(key, value, options = {}) {
3355
3483
  this.store.transact((draft) => {
@@ -3361,12 +3489,14 @@ var EditorSession = class {
3361
3489
  * 注入数据源 schema 声明(载荷形态,D1 钉定:声明即 compile(canvas, dataset)
3362
3490
  * 收到的 data 载荷形状,根上下文候选 = 载荷顶层键)。声明经
3363
3491
  * normalizeExpressionSchemaSource 一次性编译收口——根级结构性问题(保留键
3364
- * row/$ 前缀,前移填充期 reserved_root_key;根非对象/缺 properties 层级)降级
3365
- * 为无候选 + console 警告,局部故障($ref 断链/外部指针/环引用/目标形态不符)
3366
- * 不拒绝:该节点降叶子 + 注入期汇总告警一次,树其余部分照常服务,不抛错不
3367
- * 弹错(辅助声明不作权威);null/undefined = 清除声明。声明只住 store ui 分支:
3368
- * 不进 graph、不落 localStorage、不进 wire;openDocument 换文档不重置,
3369
- * 重注入/清除走同一入口。签名与「注入即整体替换」语义不变(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)。
3370
3500
  */
3371
3501
  setDataSourceSchema(raw) {
3372
3502
  this.store.setDataSourceSchema(
@@ -4241,6 +4371,29 @@ function classifyWheel(input) {
4241
4371
  return "pan";
4242
4372
  }
4243
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
+
4244
4397
  // src/shared/expressionFunctions.ts
4245
4398
  var EXPRESSION_FUNCTIONS = [
4246
4399
  { name: "date", signature: "date(path[, format])", summary: "\u65E5\u671F\u683C\u5F0F\u5316\uFF08moment \u98CE\u683C token\uFF0C\u9884\u8BBE date/datetime\uFF09" },
@@ -4272,7 +4425,7 @@ function candidateMeta(node) {
4272
4425
  }
4273
4426
  function childCandidate(entry, prefix) {
4274
4427
  const path = prefix === "" ? entry.key : `${prefix}.${entry.key}`;
4275
- return { path, segment: entry.key, ...candidateMeta(entry.node) };
4428
+ return { path, segment: entry.key, kind: "path", ...candidateMeta(entry.node) };
4276
4429
  }
4277
4430
  function callCandidatesFor(entry, path, exprLen) {
4278
4431
  const extras = [];
@@ -4283,6 +4436,7 @@ function callCandidatesFor(entry, path, exprLen) {
4283
4436
  cursorBack: 2,
4284
4437
  replaceLen: exprLen,
4285
4438
  filterKey: entry.key,
4439
+ kind: "function",
4286
4440
  description: "\u65E5\u671F\u683C\u5F0F\u5316",
4287
4441
  type: "\u51FD\u6570"
4288
4442
  });
@@ -4293,6 +4447,7 @@ function callCandidatesFor(entry, path, exprLen) {
4293
4447
  segment: `${fn.name}(${path})`,
4294
4448
  replaceLen: exprLen,
4295
4449
  filterKey: entry.key,
4450
+ kind: "function",
4296
4451
  description: fn.summary,
4297
4452
  type: "\u51FD\u6570"
4298
4453
  });
@@ -4310,6 +4465,7 @@ function functionCandidates() {
4310
4465
  path: fn.signature,
4311
4466
  segment: `${fn.name}()`,
4312
4467
  cursorBack: 1,
4468
+ kind: "function",
4313
4469
  description: fn.summary,
4314
4470
  type: "\u51FD\u6570"
4315
4471
  }));
@@ -4325,12 +4481,12 @@ function dateFormatCandidates() {
4325
4481
  function headCandidates(schema, context, rowSchema, exprLen, withBuckets) {
4326
4482
  const view = schemaChildEntries(schema);
4327
4483
  const candidates = view.entries.flatMap((entry) => childCandidates(entry, "", exprLen, withBuckets));
4328
- candidates.push({ path: "$root", segment: "$root" });
4484
+ candidates.push({ path: "$root", segment: "$root", kind: "path" });
4329
4485
  if (context === "row") {
4330
- candidates.push(rowSchema ? { path: "row", segment: "row", ...candidateMeta(rowSchema) } : { path: "row", segment: "row" });
4331
- 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" });
4332
4488
  }
4333
- return view.open === true ? { candidates, open: true } : { candidates };
4489
+ return candidates;
4334
4490
  }
4335
4491
  function callSlotResult(schema, query) {
4336
4492
  const slot = expressionCallSlotAtEnd(query.expr);
@@ -4359,7 +4515,7 @@ function enumeratePathCandidates(schema, query, expr, allowCallSuggestions) {
4359
4515
  ok: true,
4360
4516
  prefix: "",
4361
4517
  partial: "",
4362
- ...headCandidates(schema, query.context, query.rowSchema, expr.length, false)
4518
+ candidates: headCandidates(schema, query.context, query.rowSchema, expr.length, false)
4363
4519
  };
4364
4520
  }
4365
4521
  return { ok: false, reason: "expression_syntax_error" };
@@ -4389,8 +4545,7 @@ function enumeratePathCandidates(schema, query, expr, allowCallSuggestions) {
4389
4545
  ok: true,
4390
4546
  prefix: "",
4391
4547
  partial,
4392
- candidates: [...view2.candidates, ...fns],
4393
- ...view2.open === true ? { open: true } : {}
4548
+ candidates: [...view2, ...fns]
4394
4549
  };
4395
4550
  }
4396
4551
  let base;
@@ -4420,8 +4575,7 @@ function enumeratePathCandidates(schema, query, expr, allowCallSuggestions) {
4420
4575
  ok: true,
4421
4576
  prefix,
4422
4577
  partial,
4423
- candidates: view.entries.flatMap((entry) => childCandidates(entry, prefix, expr.length, withBuckets)),
4424
- ...view.open === true ? { open: true } : {}
4578
+ candidates: view.entries.flatMap((entry) => childCandidates(entry, prefix, expr.length, withBuckets))
4425
4579
  };
4426
4580
  }
4427
4581
  function enumerateExpressionCandidates(schema, query) {
@@ -4439,7 +4593,7 @@ function isArrayNode(node) {
4439
4593
  return node.type === "array" || node.items !== void 0;
4440
4594
  }
4441
4595
  function isScalarNode(node) {
4442
- return !isArrayNode(node) && node.properties === void 0 && node.open !== true;
4596
+ return !isArrayNode(node) && node.properties === void 0;
4443
4597
  }
4444
4598
  function childCandidate2(key, node, prefix) {
4445
4599
  const candidate = {
@@ -4459,7 +4613,7 @@ function levelCandidates(base, prefix) {
4459
4613
  candidates.push(childCandidate2(key, child, prefix));
4460
4614
  }
4461
4615
  }
4462
- return base.open === true ? { candidates, open: true } : { candidates };
4616
+ return candidates;
4463
4617
  }
4464
4618
  function enumerateRowsPathCandidates(startSchema, input) {
4465
4619
  if (startSchema === null) return null;
@@ -4475,11 +4629,7 @@ function enumerateRowsPathCandidates(startSchema, input) {
4475
4629
  }
4476
4630
  if (isArrayNode(base) || isScalarNode(base)) return null;
4477
4631
  const view = levelCandidates(base, completed.join("."));
4478
- return {
4479
- partial,
4480
- candidates: view.candidates,
4481
- ...view.open === true ? { open: true } : {}
4482
- };
4632
+ return { partial, candidates: view };
4483
4633
  }
4484
4634
  function resolveRowsPathStartSchema(doc, path, rootSchema) {
4485
4635
  if (rootSchema === null) return null;
@@ -4632,6 +4782,7 @@ export {
4632
4782
  enumerateExpressionCandidates,
4633
4783
  enumerateRowsPathCandidates,
4634
4784
  expressionCallSlotAtEnd,
4785
+ expressionDiagnostics,
4635
4786
  expressionFragmentAtCursor,
4636
4787
  fitRect,
4637
4788
  fitViewport,
@@ -4683,6 +4834,7 @@ export {
4683
4834
  resolveRowsPathStartSchema,
4684
4835
  resolveSnap,
4685
4836
  resolveSnapPoints,
4837
+ resourcePathDiagnostics,
4686
4838
  rootLayerOf,
4687
4839
  scanExpressionFragments,
4688
4840
  scanTextMatches,