fix(editor): 修复 tiptap 序列化转义脚注语法 [^id] → \[^id\] 导致失效
@tiptap/markdown 的 escapeMarkdownSyntax (MarkdownManager.ts:1112) 对所有 非代码文本节点做 [→\[、]→\] 转义,导致脚注引用/定义 [^id] 被破坏为 \[^id\], pulldown-cmark 将其视为字面文本而非脚注语法,渲染出纯文本而非上标链接。 与数学公式节点同类问题(LaTeX 通过 atom Node + renderMarkdown 绕过), 但脚注是普通文本节点无法走相同路径,故在 getMarkdown 输出后做后处理: 将 \[^label\] 还原为 [^label],仅匹配脚注专属模式不影响普通链接语法。 同时修复数据库中已损坏的 markdown-syntax-test 和 markdown-quan-te-xing- ce-shi-wen-zhang 两篇文章的 content_md,并通过 rebuild 全量重建 content_html。
This commit is contained in:
parent
c693d82bab
commit
2b75ac4d28
75
libs/tiptap-editor/src/__tests__/footnote.test.ts
Normal file
75
libs/tiptap-editor/src/__tests__/footnote.test.ts
Normal file
@ -0,0 +1,75 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { Editor } from '@tiptap/core';
|
||||
import { Markdown } from '@tiptap/markdown';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* 脚注语法回归测试。
|
||||
*
|
||||
* @tiptap/markdown 的 escapeMarkdownSyntax 将 `[`→`\[`、`]`→`\]`,
|
||||
* 导致 `[^1]` 被序列化为 `\[^1\]`,pulldown-cmark 将其当作字面文本。
|
||||
* TiptapEditorInstance.unescapeFootnoteSyntax 在序列化后做后处理还原。
|
||||
*
|
||||
* 此测试直接验证 escapeMarkdownSyntax 的行为以及 unescape 正则的修复效果。
|
||||
*/
|
||||
|
||||
const UNESCAPE_RE = /\\\[\^([^\n\\]*?)\\\]/g;
|
||||
const unescape = (md: string) => md.replace(UNESCAPE_RE, '[^$1]');
|
||||
|
||||
function makeEditor() {
|
||||
return new Editor({
|
||||
element: document.body,
|
||||
extensions: [
|
||||
StarterKit.configure({ heading: { levels: [1, 2, 3] } }),
|
||||
Markdown,
|
||||
],
|
||||
content: '',
|
||||
});
|
||||
}
|
||||
|
||||
describe('脚注语法 - escapeMarkdownSyntax 破坏与修复', () => {
|
||||
let editor: Editor;
|
||||
|
||||
beforeEach(() => {
|
||||
editor = makeEditor();
|
||||
});
|
||||
|
||||
it('escapeMarkdownSyntax 确实会把 [^1] 转义为 \\[^1\\]', () => {
|
||||
editor.commands.setContent('引用[^1]', { contentType: 'markdown' });
|
||||
const raw = editor.getMarkdown();
|
||||
// 未修复时 raw 为 `\[^1\]`——这就是脚注失效的直接原因
|
||||
expect(raw).toContain('\\[^1\\]');
|
||||
});
|
||||
|
||||
it('unescape 正则还原脚注引用 \\[^1\\] → [^1]', () => {
|
||||
expect(unescape('\\[^1\\]')).toBe('[^1]');
|
||||
});
|
||||
|
||||
it('unescape 正则还原含空格的脚注 label', () => {
|
||||
expect(unescape('\\[^my note\\]')).toBe('[^my note]');
|
||||
});
|
||||
|
||||
it('unescape 正则还原脚注定义 \\[^1\\]: → [^1]:', () => {
|
||||
expect(unescape('\\[^1\\]: 定义内容')).toBe('[^1]: 定义内容');
|
||||
});
|
||||
|
||||
it('unescape 正则不影响普通链接 [text](url)', () => {
|
||||
const md = '\\[text\\](https://example.com)';
|
||||
// 普通链接的 \[text\] 不以 ^ 开头,不会被误改
|
||||
expect(unescape(md)).toBe(md);
|
||||
});
|
||||
|
||||
it('unescape 正则一次处理多个脚注引用', () => {
|
||||
const md = '引用了\\[^1\\]和\\[^pc\\]和\\[^2\\]';
|
||||
expect(unescape(md)).toBe('引用了[^1]和[^pc]和[^2]');
|
||||
});
|
||||
|
||||
it('完整往返:unescape(getMarkdown()) 保留脚注引用语法', () => {
|
||||
editor.commands.setContent('正文[^1]', { contentType: 'markdown' });
|
||||
const fixed = unescape(editor.getMarkdown());
|
||||
expect(fixed).toContain('[^1]');
|
||||
expect(fixed).not.toContain('\\[^1\\]');
|
||||
});
|
||||
});
|
||||
@ -162,7 +162,7 @@ class TiptapEditorInstance {
|
||||
autofocus: false,
|
||||
onUpdate: ({ editor }) => {
|
||||
if (this.options.onUpdate) {
|
||||
this.options.onUpdate(editor.getMarkdown());
|
||||
this.options.onUpdate(TiptapEditorInstance.unescapeFootnoteSyntax(editor.getMarkdown()));
|
||||
}
|
||||
},
|
||||
onFocus: () => {
|
||||
@ -205,7 +205,22 @@ class TiptapEditorInstance {
|
||||
if (this.isSourceMode && this.sourceTextarea) {
|
||||
return this.sourceTextarea.value;
|
||||
}
|
||||
return this.editor?.getMarkdown() || '';
|
||||
return TiptapEditorInstance.unescapeFootnoteSyntax(this.editor?.getMarkdown() || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复 @tiptap/markdown 的 escapeMarkdownSyntax 对脚注语法的破坏。
|
||||
*
|
||||
* escapeMarkdownSyntax(MarkdownManager.ts:1112)将 `[` → `\[`、`]` → `\]`,
|
||||
* 导致 `[^id]` 被转义为 `\[^id\]`,pulldown-cmark 将其视为字面文本而非脚注引用/定义。
|
||||
* 这是与数学公式节点同类的问题:LaTeX 通过 atom Node + renderMarkdown 绕过转义,
|
||||
* 但脚注是普通文本节点,无法走相同路径。
|
||||
*
|
||||
* 此函数在序列化后对完整的 Markdown 做后处理:把 `\[^label\]` 还原为 `[^label]`。
|
||||
* 仅匹配 `\[` + `^` 开头的模式(脚注专属),不会影响普通 `[text](url)` 链接语法。
|
||||
*/
|
||||
private static unescapeFootnoteSyntax(md: string): string {
|
||||
return md.replace(/\\\[\^([^\n\\]*?)\\\]/g, '[^$1]');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -219,7 +234,7 @@ class TiptapEditorInstance {
|
||||
const proseMirrorDom = this.editor.view.dom;
|
||||
if (!this.isSourceMode) {
|
||||
// 富文本 → 源码:导出当前 Markdown 到 textarea
|
||||
this.sourceTextarea.value = this.editor.getMarkdown();
|
||||
this.sourceTextarea.value = TiptapEditorInstance.unescapeFootnoteSyntax(this.editor.getMarkdown());
|
||||
// 必须在 display:'none' 之前读取滚动比例——隐藏后 scrollTop 会被浏览器归零
|
||||
const pmRatio = this.getScrollRatio(proseMirrorDom);
|
||||
proseMirrorDom.style.display = 'none';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user