Skip to content

Commit 4e30af1

Browse files
committed
fix(editor): 图片插入 RangeError——CM6 禁止插件产出 block decoration(内联 replace + 回归测试)
1 parent 869eff0 commit 4e30af1

2 files changed

Lines changed: 21 additions & 15 deletions

File tree

‎app/src/components/RichEditorView.test.tsx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,17 @@ describe("RichEditorView 核心编辑", () => {
7575
fireEvent.keyDown(cmContent(), { key: "3", ctrlKey: true });
7676
await waitFor(() => expect(cmContent().textContent).toContain("### 第一行"));
7777
});
78+
79+
// v0.16.1 回归:插件不得产出 block decoration(CM6 抛 RangeError「Block
80+
// decorations may not be specified via plugins」——插入独立行图片即崩)
81+
it("独立行图片渲染为 widget(不抛 RangeError,原语法被替换)", async () => {
82+
const note = { ...baseNote, content: "前文\n![图](notes-images/1/a.png)\n后文" };
83+
render(<RichEditorView note={note} onCancel={vi.fn()} />);
84+
await waitFor(() => expect(document.querySelector(".cm-note-image")).not.toBeNull());
85+
// 语法文本已被 widget 替换(不残留源码)
86+
expect(cmContent().textContent).not.toContain("![图]");
87+
expect(cmContent().textContent).toContain("前文");
88+
});
7889
});
7990

8091
describe("RichEditorView 草稿恢复层", () => {

‎app/src/components/imageDecorationPlugin.ts‎

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,31 @@
33
*
44
* @ai-context: 将 `![alt](url)` 以 Decoration.replace widget 替换为真实图片
55
* (NoteImageWidget 挂载 React NoteImage)。仅 docChanged 重算
6-
* decorations(spec §4.1);独立行图片 block 化(占整行,替换后
7-
* 不残留行内空隙)。noteId/onOpen 经工厂参数注入(组件层持有)。
6+
* decorations(spec §4.1)。
7+
* @ai-context: v0.16.1 修复:插件 decorations 一律 inline replace——CM6 明确
8+
* 禁止插件产出 block decoration(ViewPlugin 的 decorations 经
9+
* disallowBlockEffectsFor 校验,block:true 抛 RangeError
10+
* "Block decorations may not be specified via plugins",插入
11+
* 独立行图片即崩)。独立行图片所在行仅空白,inline replace 替换
12+
* 整段语法后自然独占一行(视觉等价,无行为回退)。
13+
* noteId/onOpen 经工厂参数注入(组件层持有)。
814
*/
915
import { RangeSetBuilder } from "@codemirror/state";
1016
import { Decoration, ViewPlugin, type DecorationSet, type EditorView, type ViewUpdate } from "@codemirror/view";
11-
import { scanImageRefs, type ImageRef } from "./imageDecoration";
17+
import { scanImageRefs } from "./imageDecoration";
1218
import { NoteImageWidget } from "./NoteImageWidget";
1319

1420
interface PluginContext {
1521
noteId: number;
1622
onOpen?: (url: string, title?: string) => void;
1723
}
1824

19-
/** 独立行图片:所在行除图片语法外仅空白 → block 占整行 */
20-
function isBlockImage(view: EditorView, ref: ImageRef): boolean {
21-
const line = view.state.doc.lineAt(ref.from);
22-
const before = line.text.slice(0, ref.from - line.from);
23-
const after = line.text.slice(ref.to - line.from);
24-
return before.trim() === "" && after.trim() === "";
25-
}
26-
2725
function computeDecorations(view: EditorView, ctx: PluginContext): DecorationSet {
2826
const refs = scanImageRefs(view.state.doc.toString());
2927
// RangeSetBuilder 是带位置的唯一构造途径(Decoration.replace 只接受 spec)
3028
const builder = new RangeSetBuilder<Decoration>();
3129
for (const r of refs) {
32-
builder.add(r.from, r.to, Decoration.replace({
33-
widget: new NoteImageWidget(r, ctx),
34-
block: isBlockImage(view, r),
35-
}));
30+
builder.add(r.from, r.to, Decoration.replace({ widget: new NoteImageWidget(r, ctx) }));
3631
}
3732
return builder.finish();
3833
}

0 commit comments

Comments
 (0)