@hankchen/canvas-editor 0.6.0 → 0.7.1

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
@@ -1004,6 +1004,20 @@ function expressionFragmentAtCursor(scan, cursor) {
1004
1004
  return null;
1005
1005
  }
1006
1006
 
1007
+ // src/shared/resourcePath.ts
1008
+ function wholeTemplateDiagnostic(template, kind) {
1009
+ return { kind, start: 0, end: template.length, at: { start: 0, end: template.length } };
1010
+ }
1011
+ function resourcePathDiagnostics(template) {
1012
+ const anchored = template.endsWith("\n") ? template.slice(0, -1) : template;
1013
+ const matched = /^\{\{([^{}]*)\}\}$/.exec(anchored);
1014
+ if (matched === null) return [wholeTemplateDiagnostic(template, "resource_not_single_ref")];
1015
+ const path = phpTrim2(matched[1] ?? "");
1016
+ if (path === "") return [wholeTemplateDiagnostic(template, "empty_fragment")];
1017
+ if (path.includes("(")) return [wholeTemplateDiagnostic(template, "resource_function_call")];
1018
+ return [];
1019
+ }
1020
+
1007
1021
  // src/editing/alignCanvas.ts
1008
1022
  var ALIGN_CORNER_MARGIN_PX = 40;
1009
1023
  function alignToCanvasTarget(mode, box, canvasWidth, canvasHeight, margin) {
@@ -1226,15 +1240,14 @@ var RATIO_PRESETS = [
1226
1240
  { id: "16:9", ratio: { w: 16, h: 9 } },
1227
1241
  { id: "9:16", ratio: { w: 9, h: 16 } }
1228
1242
  ];
1229
- function defaultNextLayerRatio() {
1230
- return "free";
1231
- }
1232
1243
  function ratioValue(id) {
1233
1244
  const entry = RATIO_PRESETS.find((candidate) => candidate.id === id);
1234
1245
  return entry?.ratio ? { ...entry.ratio } : null;
1235
1246
  }
1236
- function pickNextLayerRatio(raw) {
1237
- return RATIO_PRESETS.some((candidate) => candidate.id === raw) ? raw : "free";
1247
+ function findPresetEntry(type, presetId) {
1248
+ if (type === "TextLayer") return TEXT_PRESETS.find((candidate) => candidate.id === presetId);
1249
+ if (type === "ImageLayer") return RATIO_PRESETS.find((candidate) => candidate.id === presetId);
1250
+ return void 0;
1238
1251
  }
1239
1252
  function defaultSizeForRatio(ratio) {
1240
1253
  return ratio.w >= ratio.h ? { width: 200, height: Math.round(200 * ratio.h / ratio.w) } : { width: Math.round(200 * ratio.w / ratio.h), height: 200 };
@@ -1742,13 +1755,14 @@ var EditorStore = class {
1742
1755
  resize: null,
1743
1756
  armedCreate: null,
1744
1757
  armedCreateRowsPath: "",
1758
+ armedCreatePreset: "",
1745
1759
  create: null,
1746
1760
  editing: null,
1747
1761
  renaming: null,
1748
1762
  anchorExpanded: false,
1749
1763
  nextTextStyle: defaultNextTextStyle(),
1750
- nextLayerRatio: defaultNextLayerRatio(),
1751
1764
  dataSourceSchema: null,
1765
+ dataSourceSchemaRejection: null,
1752
1766
  guides: [],
1753
1767
  rulersVisible: true,
1754
1768
  snapAxes: [],
@@ -1779,7 +1793,7 @@ var EditorStore = class {
1779
1793
  }
1780
1794
  /** 打开/替换文档:ui 选择/编辑/重命名会话与双向历史一并重置(新文档不继承旧路径/旧事务);
1781
1795
  * 参考线/锁定集合/查找会话/建层武装与画拉会话随当次会话清空(对位轴/文档内容防护/
1782
- * 查询词/待命层型不跨文档),标尺显隐/锚点折叠/新层样式/建层比例作为偏好保留 */
1796
+ * 查询词/待命层型不跨文档),标尺显隐/锚点折叠/新层样式作为偏好保留 */
1783
1797
  openDocument(canvas) {
1784
1798
  this.docValue = canvas;
1785
1799
  this.undoSteps = [];
@@ -1792,6 +1806,7 @@ var EditorStore = class {
1792
1806
  resize: null,
1793
1807
  armedCreate: null,
1794
1808
  armedCreateRowsPath: "",
1809
+ armedCreatePreset: "",
1795
1810
  create: null,
1796
1811
  editing: null,
1797
1812
  renaming: null,
@@ -1891,12 +1906,20 @@ var EditorStore = class {
1891
1906
  /**
1892
1907
  * 建层武装置/解除(null,drag-create 工单 01);标量值等短路(重复武装同型
1893
1908
  * 不通知)。rowsPath(template-table-drag-create 工单 01):模板表风味载荷
1894
- * 与层型一体写——同层型同载荷才短路(重复武装同型换载荷要更新),解除
1895
- * (null)一律清载荷。
1909
+ * 与层型一体写;preset(canvas-web-preset-submenu 工单 01):预设载荷与层型
1910
+ * 一体写——同层型同载荷才短路(重复武装同型换载荷要更新),解除(null)
1911
+ * 一律清载荷(rowsPath 与 preset 双清)。
1896
1912
  */
1897
- setArmedCreate(type, rowsPath = "") {
1898
- if (this.uiValue.armedCreate === type && this.uiValue.armedCreateRowsPath === rowsPath) return;
1899
- this.uiValue = { ...this.uiValue, armedCreate: type, armedCreateRowsPath: type === null ? "" : rowsPath };
1913
+ setArmedCreate(type, rowsPath = "", preset = "") {
1914
+ if (this.uiValue.armedCreate === type && this.uiValue.armedCreateRowsPath === rowsPath && this.uiValue.armedCreatePreset === preset) {
1915
+ return;
1916
+ }
1917
+ this.uiValue = {
1918
+ ...this.uiValue,
1919
+ armedCreate: type,
1920
+ armedCreateRowsPath: type === null ? "" : rowsPath,
1921
+ armedCreatePreset: type === null ? "" : preset
1922
+ };
1900
1923
  this.notify({ scope: "ui", branch: "armedCreate" });
1901
1924
  }
1902
1925
  /** 画拉橡皮筋会话开始/更新/结束(null);会话对象住 ui 分支,落库在 endCreate 单事务 */
@@ -1920,8 +1943,9 @@ var EditorStore = class {
1920
1943
  this.notify({ scope: "ui", branch: "anchorExpanded" });
1921
1944
  }
1922
1945
  /**
1923
- * 新层样式槽逐字段合并(text-defaults 工单 01:捕获/预设/prefs 恢复的统一
1924
- * 写入口):patch 并入现值,五字段值等短路(重复写同值零通知),不进历史。
1946
+ * 新层样式槽逐字段合并(text-defaults 工单 01:捕获/prefs 恢复的统一写入口;
1947
+ * canvas-web-preset-submenu 工单 01 起预设不再写槽):patch 并入现值,五字段
1948
+ * 值等短路(重复写同值零通知),不进历史。
1925
1949
  */
1926
1950
  setNextTextStyle(patch) {
1927
1951
  const next = { ...this.uiValue.nextTextStyle, ...patch };
@@ -1929,23 +1953,15 @@ var EditorStore = class {
1929
1953
  this.uiValue = { ...this.uiValue, nextTextStyle: next };
1930
1954
  this.notify({ scope: "ui", branch: "nextTextStyle" });
1931
1955
  }
1932
- /**
1933
- * 建层比例槽写入(image-ratio-preset 工单 01:比例预设应用 / prefs 恢复的
1934
- * 统一写入口):值等短路(重复写同档零通知),仅 ui 通知、不进历史。
1935
- */
1936
- setNextLayerRatio(id) {
1937
- if (this.uiValue.nextLayerRatio === id) return;
1938
- this.uiValue = { ...this.uiValue, nextLayerRatio: id };
1939
- this.notify({ scope: "ui", branch: "nextLayerRatio" });
1940
- }
1941
1956
  /**
1942
1957
  * 数据源 schema 声明替换(归一产物或降级 null,校验收口在注入缝不在这里):
1943
- * 同引用短路(重复注入同一归一产物不惊动订阅方);仅 ui 通知,不参与重绘
1958
+ * 声明与拒绝详情成对置换(rejection 仅被拒注入在场,2b 呈现面),同引用且
1959
+ * 同拒绝态短路(重复注入同一归一产物不惊动订阅方);仅 ui 通知,不参与重绘
1944
1960
  * 脏标(onStoreChange 对该分支短路)。
1945
1961
  */
1946
- setDataSourceSchema(schema) {
1947
- if (this.uiValue.dataSourceSchema === schema) return;
1948
- this.uiValue = { ...this.uiValue, dataSourceSchema: schema };
1962
+ setDataSourceSchema(schema, rejection = null) {
1963
+ if (this.uiValue.dataSourceSchema === schema && this.uiValue.dataSourceSchemaRejection === rejection) return;
1964
+ this.uiValue = { ...this.uiValue, dataSourceSchema: schema, dataSourceSchemaRejection: rejection };
1949
1965
  this.notify({ scope: "ui", branch: "dataSourceSchema" });
1950
1966
  }
1951
1967
  /** 参考线列表替换(增删动作在会话门面收口);仅 ui 通知,不参与历史 */
@@ -2912,29 +2928,23 @@ var EditorSession = class {
2912
2928
  /**
2913
2929
  * 武装建层(面板新增项/层型快捷键的统一入口,CONTEXT「武装」词条):置 ui
2914
2930
  * 分支待命层型,下一次画布按下即开画拉;建层提交或 Esc 即解除——一次性
2915
- * 待命态,无粘性工具(spec 决策 2)。重复武装同型值等短路零通知。
2931
+ * 待命态,无粘性工具(spec 决策 2)。重复武装同型同载荷值等短路零通知。
2916
2932
  *
2917
- * preset(text-defaults 工单 01 + image-ratio-preset 工单 01):层型相关预设 =
2918
- * 写槽的快捷方式——命中档位先把档位值并入对应偏好槽再武装;未知 id = 跳过
2919
- * 写槽、武装照常(评审口径统一:「空转」只指写槽,武装层型本身仍合法)。
2920
- * - TextLayer + 文本预设档:写新层样式槽(v1 只 fontSize);
2921
- * - ImageLayer + 比例预设档:写建层比例槽('free' 即显式重置档);QR 恒方
2922
- * 是层型硬钳、无比例槽(spec 豁免面);
2923
- * - 交叉类型(文本层带比例档/图片层带文本档)各在对方清单查无此档、天然
2924
- * 跳写槽。
2933
+ * preset(canvas-web-preset-submenu 工单 01,ADR 0021):层型相关预设随武装
2934
+ * 载荷一次性携带、**不再写任何偏好槽**——命中档位把档位 id 写进
2935
+ * ui.armedCreatePreset(endCreate 落库一次性消费,取消路零残留);未知 id =
2936
+ * 不落载荷、武装照常(评审口径统一:「空转」只指载荷,武装层型本身仍合法)。
2937
+ * - TextLayer + 文本预设档:载荷命中 TEXT_PRESETS(endCreate 档位字号直接
2938
+ * 生效,不经新层样式槽——普通武装文本按槽的语义不受染指);
2939
+ * - ImageLayer + 比例预设档:载荷命中 RATIO_PRESETS('free' 档载荷行为等价
2940
+ * 自由;QR 恒方是层型硬钳、无比例语义,spec 豁免面);
2941
+ * - 交叉类型(文本层带比例档/图片层带文本档)各在对方清单查无此档、不落载荷。
2925
2942
  */
2926
2943
  armLayerCreate(type, preset) {
2927
- if (preset !== void 0 && type === "TextLayer") {
2928
- const entry = TEXT_PRESETS.find((candidate) => candidate.id === preset);
2929
- if (entry) this.store.setNextTextStyle({ fontSize: entry.fontSize });
2930
- }
2931
- if (preset !== void 0 && type === "ImageLayer") {
2932
- const entry = RATIO_PRESETS.find((candidate) => candidate.id === preset);
2933
- if (entry) this.store.setNextLayerRatio(entry.id);
2934
- }
2935
- this.store.setArmedCreate(type);
2944
+ const entry = preset === void 0 ? void 0 : findPresetEntry(type, preset);
2945
+ this.store.setArmedCreate(type, "", entry !== void 0 ? entry.id : "");
2936
2946
  }
2937
- /** 解除建层武装(取消入口):仅清待命态(含模板表风味载荷),零副作用。 */
2947
+ /** 解除建层武装(取消入口):仅清待命态(含模板表风味与预设载荷),零副作用。 */
2938
2948
  cancelArmLayerCreate() {
2939
2949
  this.store.setArmedCreate(null);
2940
2950
  }
@@ -2963,9 +2973,11 @@ var EditorSession = class {
2963
2973
  if (!this.store.doc) return false;
2964
2974
  const point = { x: sceneX, y: sceneY };
2965
2975
  const templateRowsPath = armed === "TableLayer" && this.store.ui.armedCreateRowsPath !== "" ? this.store.ui.armedCreateRowsPath : void 0;
2976
+ const preset = (armed === "TextLayer" || armed === "ImageLayer") && this.store.ui.armedCreatePreset !== "" ? this.store.ui.armedCreatePreset : void 0;
2966
2977
  this.store.setCreate({
2967
2978
  type: armed,
2968
2979
  templateRowsPath,
2980
+ preset,
2969
2981
  startScene: point,
2970
2982
  currentScene: point,
2971
2983
  rect: { x: sceneX, y: sceneY, width: 0, height: 0 }
@@ -2981,17 +2993,19 @@ var EditorSession = class {
2981
2993
  * 全部可见根层供轴,copyMode 首移同门)。零文档事务:修正只进会话几何,
2982
2994
  * 落库在 endCreate。
2983
2995
  *
2984
- * 建层比例槽(image-ratio-preset 工单 01,spec 决策 3/4):仅 ImageLayer 按槽
2985
- * 钳制(QR 层不传 ratio、恒方语义不动,两钳互斥;其余层型无槽);Shift 瞬时
2986
- * 反转槽态——槽非自由按 Shift = 临时自由、槽自由按 Shift = 临时钳 1:1,按住
2987
- * 生效松开还原(逐帧求位、不写槽、不改武装态,Figma 惯例的手势瞬时语义)。
2996
+ * 建层预设档(canvas-web-preset-submenu 工单 01,ADR 0021,取代建层比例槽):
2997
+ * 仅 ImageLayer 按会话载荷档钳制(QR 层不传 ratio、恒方语义不动,两钳互斥;
2998
+ * 其余层型无比例语义);无载荷或 free 档 = 自由钳(普通武装恒自由)。
2999
+ * Shift 瞬时反转源 = 生效档(载荷档或自由):档具名按 Shift = 临时自由、
3000
+ * 生效档自由(含无载荷)按 Shift = 临时钳 1:1,按住生效松开还原(逐帧求位、
3001
+ * 不写载荷不改武装态,Figma 惯例的手势瞬时语义)。
2988
3002
  */
2989
3003
  createTo(sceneX, sceneY, shift = false) {
2990
3004
  const gesture = this.store.ui.create;
2991
3005
  const doc = this.store.doc;
2992
3006
  if (!gesture || !doc) return;
2993
- const slotRatio = gesture.type === "ImageLayer" ? ratioValue(this.store.ui.nextLayerRatio) : null;
2994
- const ratio = gesture.type === "ImageLayer" ? shift ? slotRatio === null ? { w: 1, h: 1 } : null : slotRatio : null;
3007
+ const presetRatio = gesture.type === "ImageLayer" && gesture.preset !== void 0 ? ratioValue(gesture.preset) : null;
3008
+ const ratio = gesture.type === "ImageLayer" ? shift ? presetRatio === null ? { w: 1, h: 1 } : null : presetRatio : null;
2995
3009
  const resolution = createRubberBandRect(
2996
3010
  gesture.type,
2997
3011
  gesture.startScene,
@@ -3013,15 +3027,20 @@ var EditorSession = class {
3013
3027
  * - 死区外:单事务写入「createDefaultLayer 缺省形态 + position 左上角对准
3014
3028
  * 橡皮筋矩形(含吸附修正)+ shape.width/height」并 insertRootLayerInDraft
3015
3029
  * 置顶(priority min−1、push 数组尾)——尺寸取手势末态钳制结果(含 Shift
3016
- * 反转末态,image-ratio-preset 工单 01 spec 决策 4);
3030
+ * 反转末态,钳制源 = 会话预设档,canvas-web-preset-submenu 工单 01);
3017
3031
  * - 死区内(点击兜底,位移按 CREATE_DEAD_ZONE_SCREEN_PX 屏幕 px 随 zoom 折算,
3018
- * ALT_DRAG_DEAD_ZONE_SCREEN_PX 同门):左上角对准点击点;尺寸按槽派生缺省
3019
- * 盒(image-ratio-preset 工单 01:ImageLayer 长边 200 定短边,自由 = 工厂
3020
- * 200×150 现状;忽略 shift——无位移可供等比约束;其余层型保持工厂缺省)。
3032
+ * ALT_DRAG_DEAD_ZONE_SCREEN_PX 同门):左上角对准点击点;尺寸按会话预设档
3033
+ * 派生缺省盒(ImageLayer 长边 200 定短边,自由/无载荷 = 工厂 200×150 现状;
3034
+ * 忽略 shift——无位移可供等比约束;其余层型保持工厂缺省)。
3021
3035
  * 落库后自动选中新层并解除武装;返回新层路径(绑定层消费:文本层据此自动
3022
3036
  * 进文本编辑,spec 决策 6)。无会话/无文档空转返回 null。点击兜底不产生
3023
3037
  * 零尺寸层(缺省形态自带可见尺寸)。
3024
3038
  *
3039
+ * 预设档一次性消费(canvas-web-preset-submenu 工单 01,ADR 0021):文本档
3040
+ * 字号直接生效(不经新层样式槽——字体/行高等槽外字段仍按槽,普通武装语义
3041
+ * 保留);图片档比供死区缺省盒派生。档位随会话捕获,落库后武装与载荷双清
3042
+ * (一次性语义,取消路零残留)。
3043
+ *
3025
3044
  * 模板表风味(template-table-drag-create 工单 01):会话载荷 templateRowsPath
3026
3045
  * 非空即分派 createTemplateTable 工厂(死区外 shape = 手势正规化矩形,死区内
3027
3046
  * 缺省壳 400×120、左上角对准点击点);模板层不自动进文本编辑(文本层专属)。
@@ -3039,21 +3058,21 @@ var EditorSession = class {
3039
3058
  const clicked = Math.hypot(dx, dy) <= deadZoneScene;
3040
3059
  const index = { v: -1 };
3041
3060
  const templateRowsPath = gesture.templateRowsPath;
3061
+ const presetTextEntry = gesture.type === "TextLayer" && gesture.preset !== void 0 ? findPresetEntry(gesture.type, gesture.preset) : void 0;
3062
+ const textDefaults = presetTextEntry ? { ...this.store.ui.nextTextStyle, fontSize: presetTextEntry.fontSize } : this.store.ui.nextTextStyle;
3063
+ const presetRatio = gesture.type === "ImageLayer" && gesture.preset !== void 0 ? ratioValue(gesture.preset) : null;
3042
3064
  this.store.transact((draft) => {
3043
3065
  const layer = templateRowsPath !== void 0 ? createTemplateTable(
3044
3066
  clicked ? { rowsPath: templateRowsPath } : {
3045
3067
  rowsPath: templateRowsPath,
3046
3068
  shape: { width: gesture.rect.width, height: gesture.rect.height }
3047
3069
  }
3048
- ) : createDefaultLayer(gesture.type, this.store.ui.nextTextStyle);
3070
+ ) : createDefaultLayer(gesture.type, textDefaults);
3049
3071
  if (clicked) {
3050
- if (gesture.type === "ImageLayer") {
3051
- const ratio = ratioValue(this.store.ui.nextLayerRatio);
3052
- if (ratio) {
3053
- const size = defaultSizeForRatio(ratio);
3054
- layer.shape.width = size.width;
3055
- layer.shape.height = size.height;
3056
- }
3072
+ if (gesture.type === "ImageLayer" && presetRatio) {
3073
+ const size = defaultSizeForRatio(presetRatio);
3074
+ layer.shape.width = size.width;
3075
+ layer.shape.height = size.height;
3057
3076
  }
3058
3077
  layer.position = { ...layer.position, x: gesture.startScene.x, y: gesture.startScene.y };
3059
3078
  } else {
@@ -3069,9 +3088,9 @@ var EditorSession = class {
3069
3088
  return path;
3070
3089
  }
3071
3090
  /**
3072
- * 取消画拉(Esc 语义):清橡皮筋会话、命中轴回显与武装态——零文档写入、
3073
- * 零历史步(拖拽期纯视觉因此取消零回退成本,spec 决策 4)。幂等:无会话时
3074
- * 仅解除武装(待命态 Esc 同走此口)。
3091
+ * 取消画拉(Esc 语义):清橡皮筋会话、命中轴回显与武装态(含模板表风味与
3092
+ * 预设载荷)——零文档写入、零历史步(拖拽期纯视觉因此取消零回退成本,spec
3093
+ * 决策 4)。幂等:无会话时仅解除武装(待命态 Esc 同走此口)。
3075
3094
  */
3076
3095
  cancelCreate() {
3077
3096
  this.store.setCreate(null);
@@ -3235,30 +3254,30 @@ var EditorSession = class {
3235
3254
  /**
3236
3255
  * 只读偏好(保存 prefs 的读取口):与 store.ui 同源,给宿主一个不用下钻
3237
3256
  * store 的口。读 = getEditorPrefs,写 = applyEditorPrefs,对称成对;帧切片
3238
- * 走 restoreEditorState,两不相混。
3257
+ * 走 restoreEditorState,两不相混。建层比例(nextLayerRatio)已退役不在此
3258
+ * 带出(canvas-web-preset-submenu 工单 01,ADR 0021——预设随武装载荷一次性
3259
+ * 携带,prefs 停写)。
3239
3260
  */
3240
3261
  getEditorPrefs() {
3241
3262
  const ui = this.store.ui;
3242
3263
  return {
3243
3264
  rulersVisible: ui.rulersVisible,
3244
3265
  anchorExpanded: ui.anchorExpanded,
3245
- nextTextStyle: ui.nextTextStyle,
3246
- nextLayerRatio: ui.nextLayerRatio
3266
+ nextTextStyle: ui.nextTextStyle
3247
3267
  };
3248
3268
  }
3249
3269
  /**
3250
3270
  * 应用偏好(会话建立时一次性,跨会话恢复 prefs 的写入口):整体替换
3251
3271
  * rulersVisible/anchorExpanded,不进历史;值等写入经 store 短路零通知。
3252
3272
  * 新层样式(text-defaults 工单 01)走宽容读逐字段合并(pickNextTextStylePatch
3253
- * 共享谓词——与捕获同门,缺字/类型不符保留现值);建层比例(image-ratio-preset
3254
- * 工单 01)走宽容读枚举收口(pickNextLayerRatio——非六档枚举值/旧快照缺字段
3255
- * 回落 'free')。
3273
+ * 共享谓词——与捕获同门,缺字/类型不符保留现值)。旧快照的 nextLayerRatio
3274
+ * 遗留键宽容忽略(canvas-web-preset-submenu 工单 01:槽退役停读,载荷上多余
3275
+ * 的键无害;schemaVersion 加法演进)。
3256
3276
  */
3257
3277
  applyEditorPrefs(prefs) {
3258
3278
  this.store.setRulersVisible(prefs.rulersVisible);
3259
3279
  this.store.setAnchorExpanded(prefs.anchorExpanded);
3260
3280
  this.store.setNextTextStyle(pickNextTextStylePatch(prefs.nextTextStyle));
3261
- this.store.setNextLayerRatio(pickNextLayerRatio(prefs.nextLayerRatio));
3262
3281
  }
3263
3282
  // ---- 对齐画布(layer-align-snap 工单 01):图层盒整体对齐画布几何 ----
3264
3283
  /**
@@ -3579,8 +3598,11 @@ var EditorSession = class {
3579
3598
  }
3580
3599
  /**
3581
3600
  * 数据字段取值方式切换(静态 ⇆ 表达式,工单 02 自面板收编内核):表达式现态
3582
- * → 字面接管(updateData,值保持现镜像);静态现态 → 打标(updateDataExpression,
3583
- * 初值 = 值字段原文)。各为独立一步历史(无 mergeKey)。三内容层分派各取值
3601
+ * → 字面接管(updateData,值保持现镜像);静态现态 → 打标,初值分内容面——
3602
+ * 文本层镜像原文(字面可含合法插值混排),资源层(Image/Qr)只镜像**已合法
3603
+ * 的单引用字面**(静态 ⇆ 表达式往返无损),违例字面(URL/拼接文案)落
3604
+ * 「未选择」空态不镜像——切换自身不得制造必违例初值(死端态整改条目 4;旧
3605
+ * 值可经撤销一步找回)。各为独立一步历史(无 mergeKey)。三内容层分派各取值
3584
3606
  * 字段,表/行/格/悬空路径空转。
3585
3607
  *
3586
3608
  * 活标志防御路由(ADR 0019):路径即编辑中的层时只翻会话活标志(零文档变更
@@ -3602,16 +3624,20 @@ var EditorSession = class {
3602
3624
  return;
3603
3625
  case "ImageLayer":
3604
3626
  if (layer.expression !== null) this.updateData(path, layer.src ?? "");
3605
- else this.updateDataExpression(path, layer.src ?? "");
3627
+ else this.updateDataExpression(path, this.resourceToggleInitial(layer.src ?? ""));
3606
3628
  return;
3607
3629
  case "QrCodeLayer":
3608
3630
  if (layer.expression !== null) this.updateData(path, layer.value);
3609
- else this.updateDataExpression(path, layer.value);
3631
+ else this.updateDataExpression(path, this.resourceToggleInitial(layer.value));
3610
3632
  return;
3611
3633
  default:
3612
3634
  return;
3613
3635
  }
3614
3636
  }
3637
+ /** 资源面切换初值:字面已是合法单引用(含空串为「未选择」)才镜像,违例落空态 */
3638
+ resourceToggleInitial(literal) {
3639
+ return resourcePathDiagnostics(literal).length === 0 ? literal : "";
3640
+ }
3615
3641
  /**
3616
3642
  * 内容写事务的统一形(会话内容独占工单 02):transact 落库 + 命中编辑层的
3617
3643
  * 草稿接管收口——updateData/updateDataExpression/replaceOne/replaceAll 四个
@@ -3655,9 +3681,20 @@ var EditorSession = class {
3655
3681
  * 换文档不重置,重注入/清除走同一入口。签名与「注入即整体替换」语义不变(D6)。
3656
3682
  */
3657
3683
  setDataSourceSchema(raw) {
3658
- this.store.setDataSourceSchema(
3659
- raw === null || raw === void 0 ? null : normalizeExpressionSchemaSource(raw)
3660
- );
3684
+ if (raw === null || raw === void 0) {
3685
+ this.store.setDataSourceSchema(null);
3686
+ return;
3687
+ }
3688
+ const schema = normalizeExpressionSchemaSource(raw);
3689
+ if (schema === null) {
3690
+ const parsed = parseExpressionSchema(raw);
3691
+ this.store.setDataSourceSchema(
3692
+ null,
3693
+ parsed.ok ? null : { reason: parsed.reason, detail: parsed.detail }
3694
+ );
3695
+ return;
3696
+ }
3697
+ this.store.setDataSourceSchema(schema);
3661
3698
  }
3662
3699
  // ---- 上传(工单 13):本机资源 → 可物化引用,实现经 uploadHandler 注入 ----
3663
3700
  /** 上传可用性:未注入 uploadHandler 时为 false(宿主据此隐藏/禁用上传入口) */
@@ -4523,20 +4560,6 @@ function expressionDiagnostics(template) {
4523
4560
  return found;
4524
4561
  }
4525
4562
 
4526
- // src/shared/resourcePath.ts
4527
- function wholeTemplateDiagnostic(template, kind) {
4528
- return { kind, start: 0, end: template.length, at: { start: 0, end: template.length } };
4529
- }
4530
- function resourcePathDiagnostics(template) {
4531
- const anchored = template.endsWith("\n") ? template.slice(0, -1) : template;
4532
- const matched = /^\{\{([^{}]*)\}\}$/.exec(anchored);
4533
- if (matched === null) return [wholeTemplateDiagnostic(template, "resource_not_single_ref")];
4534
- const path = phpTrim2(matched[1] ?? "");
4535
- if (path === "") return [wholeTemplateDiagnostic(template, "empty_fragment")];
4536
- if (path.includes("(")) return [wholeTemplateDiagnostic(template, "resource_function_call")];
4537
- return [];
4538
- }
4539
-
4540
4563
  // src/shared/expressionFunctions.ts
4541
4564
  var EXPRESSION_FUNCTIONS = [
4542
4565
  { name: "date", signature: "date(path[, format])", summary: "\u65E5\u671F\u683C\u5F0F\u5316\uFF08moment \u98CE\u683C token\uFF0C\u9884\u8BBE date/datetime\uFF09" },
@@ -4921,7 +4944,6 @@ export {
4921
4944
  createDefaultLayer,
4922
4945
  createRubberBandRect,
4923
4946
  createTemplateTable,
4924
- defaultNextLayerRatio,
4925
4947
  defaultNextTextStyle,
4926
4948
  defaultSizeForRatio,
4927
4949
  deleteLayerInDraft,
@@ -4930,6 +4952,7 @@ export {
4930
4952
  expressionCallSlotAtEnd,
4931
4953
  expressionDiagnostics,
4932
4954
  expressionFragmentAtCursor,
4955
+ findPresetEntry,
4933
4956
  fitRect,
4934
4957
  fitViewport,
4935
4958
  growRowToCellInDraft,
@@ -4964,7 +4987,6 @@ export {
4964
4987
  pathStartsWith,
4965
4988
  pathsEqual,
4966
4989
  phpTrim2 as phpTrim,
4967
- pickNextLayerRatio,
4968
4990
  pickNextTextStylePatch,
4969
4991
  prepareRootPaste,
4970
4992
  proportionalDrivingAxis,