yggdrasil/libs/patches/@tiptap__markdown@3.27.3.patch
xfy c6999763c0 fix(editor): 修复 tiptap-markdown 过度转义导致显示损坏
@tiptap/markdown 的 escapeMarkdownSyntax 对每个非代码文本节点的 */_/>
无差别转义,导致 content_html→content\_html、>→>、字面星号被反复
转义累积,前台表格/加粗/引用块显示为裸 markdown 文本。转义幂等且自增强
(已转义的 \* 被 marked 解析为 escape token→再转义→粘住),只有源码模式
保存原文才能清除。

pnpm patch 持久化修补上游包(提交进 libs/patches/,pnpm install 自动应用):
- escapeMarkdownSyntax 改为 CommonMark 风向感知:只在真正构成强调开/闭
  边界的位置转义 */_(一侧 space/punct、另一侧 other),词内位置不转义
- encodeHtmlEntities 降级为 encodeLimitedHtmlEntities:只编码 < 和 &,
  移除 > (文本节点内的 > 不构成 blockquote,编码只会产生 &gt; 损坏)
- 同步修补 src/dist ESM/dist CJS 三处
- 8 个往返单测覆盖词内下划线/行内>/字面星号/真实强调/多轮稳定
2026-07-23 12:04:25 +08:00

250 lines
11 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

diff --git a/dist/index.cjs b/dist/index.cjs
index 617292de240bd244e51ab3b0690b8ece3353637e..c5421ddadc6aea87d7ba5f46f04d61ca36aa7834 100644
--- a/dist/index.cjs
+++ b/dist/index.cjs
@@ -274,6 +274,9 @@ function htmlContainsUnrecognizedTag(html, schemaTags) {
return !schemaTags.has(tagName);
});
}
+function encodeLimitedHtmlEntities(text) {
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;");
+}
// src/MarkdownManager.ts
var MarkdownManager = class {
@@ -1049,29 +1052,40 @@ var MarkdownManager = class {
return { type: "text", text };
}
/**
- * Encode HTML entities in text unless the node is inside a code context
- * (code mark or code-block parent) where literal characters should be preserved.
- * Also backslash-escape markdown-significant characters in non-code text to
- * prevent them from being misinterpreted as formatting delimiters.
+ * Encode HTML entities + backslash-escape markdown-significant chars.
+ * Patch: CommonMark direction-aware escape for * and _ (only escape
+ * emphasis-boundary positions, not intra-word); drop > from HTML encoding.
*/
encodeTextForMarkdown(text, node, parentNode) {
const isInsideCode = (parentNode == null ? void 0 : parentNode.type) != null && this.codeTypes.has(parentNode.type) || (node.marks || []).some((m) => this.codeTypes.has(typeof m === "string" ? m : m.type));
if (isInsideCode) {
return text;
}
- return this.escapeMarkdownSyntax(_core.encodeHtmlEntities.call(void 0, text));
+ return this.escapeMarkdownSyntax(encodeLimitedHtmlEntities(text));
}
- /**
- * Backslash-escape characters that have special meaning in markdown inline
- * syntax. This prevents literal characters in text nodes from being
- * misinterpreted as formatting delimiters when the output is parsed again.
- *
- * The set covers the most common inline markdown syntax characters.
- * Characters inside code blocks/code marks are skipped by the caller
- * (`encodeTextForMarkdown`) via the existing `isInsideCode` guard.
- */
escapeMarkdownSyntax(text) {
- return text.replace(/([\\`*_[\]~])/g, "\\$1");
+ const punct = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';
+ let result = "";
+ for (let i = 0; i < text.length; i++) {
+ const ch = text[i];
+ if (ch === "*" || ch === "_") {
+ const prev = i > 0 ? text[i - 1] : "";
+ const next = i < text.length - 1 ? text[i + 1] : "";
+ const leftSpace = prev === "" || /\s/.test(prev);
+ const leftPunct = punct.includes(prev);
+ const leftOther = !leftSpace && !leftPunct;
+ const rightSpace = next === "" || /\s/.test(next);
+ const rightPunct = punct.includes(next);
+ const rightOther = !rightSpace && !rightPunct;
+ const isBoundary = ((leftSpace || leftPunct) && rightOther) || ((rightSpace || rightPunct) && leftOther);
+ result += isBoundary ? "\\" + ch : ch;
+ } else if (ch === "`" || ch === "[" || ch === "]" || ch === "~" || ch === "\\") {
+ result += "\\" + ch;
+ } else {
+ result += ch;
+ }
+ }
+ return result;
}
renderNodeToMarkdown(node, parentNode, index = 0, level = 0, meta = {}) {
var _a;
diff --git a/dist/index.js b/dist/index.js
index 18088ba76ee44bcddc94fdf30502928249516f10..e57cf65f9ffabca7728de5a39637d309720394c5 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -9,7 +9,6 @@ import {
attrsEqual as attrsEqual2,
callOrReturn,
decodeHtmlEntities,
- encodeHtmlEntities,
flattenExtensions,
generateJSON,
getExtensionField,
@@ -274,6 +273,9 @@ function htmlContainsUnrecognizedTag(html, schemaTags) {
return !schemaTags.has(tagName);
});
}
+function encodeLimitedHtmlEntities(text) {
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;");
+}
// src/MarkdownManager.ts
var MarkdownManager = class {
@@ -1049,29 +1051,40 @@ var MarkdownManager = class {
return { type: "text", text };
}
/**
- * Encode HTML entities in text unless the node is inside a code context
- * (code mark or code-block parent) where literal characters should be preserved.
- * Also backslash-escape markdown-significant characters in non-code text to
- * prevent them from being misinterpreted as formatting delimiters.
+ * Encode HTML entities + backslash-escape markdown-significant chars.
+ * Patch: CommonMark direction-aware escape for * and _ (only escape
+ * emphasis-boundary positions, not intra-word); drop > from HTML encoding.
*/
encodeTextForMarkdown(text, node, parentNode) {
const isInsideCode = (parentNode == null ? void 0 : parentNode.type) != null && this.codeTypes.has(parentNode.type) || (node.marks || []).some((m) => this.codeTypes.has(typeof m === "string" ? m : m.type));
if (isInsideCode) {
return text;
}
- return this.escapeMarkdownSyntax(encodeHtmlEntities(text));
+ return this.escapeMarkdownSyntax(encodeLimitedHtmlEntities(text));
}
- /**
- * Backslash-escape characters that have special meaning in markdown inline
- * syntax. This prevents literal characters in text nodes from being
- * misinterpreted as formatting delimiters when the output is parsed again.
- *
- * The set covers the most common inline markdown syntax characters.
- * Characters inside code blocks/code marks are skipped by the caller
- * (`encodeTextForMarkdown`) via the existing `isInsideCode` guard.
- */
escapeMarkdownSyntax(text) {
- return text.replace(/([\\`*_[\]~])/g, "\\$1");
+ const punct = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';
+ let result = "";
+ for (let i = 0; i < text.length; i++) {
+ const ch = text[i];
+ if (ch === "*" || ch === "_") {
+ const prev = i > 0 ? text[i - 1] : "";
+ const next = i < text.length - 1 ? text[i + 1] : "";
+ const leftSpace = prev === "" || /\s/.test(prev);
+ const leftPunct = punct.includes(prev);
+ const leftOther = !leftSpace && !leftPunct;
+ const rightSpace = next === "" || /\s/.test(next);
+ const rightPunct = punct.includes(next);
+ const rightOther = !rightSpace && !rightPunct;
+ const isBoundary = ((leftSpace || leftPunct) && rightOther) || ((rightSpace || rightPunct) && leftOther);
+ result += isBoundary ? "\\" + ch : ch;
+ } else if (ch === "`" || ch === "[" || ch === "]" || ch === "~" || ch === "\\") {
+ result += "\\" + ch;
+ } else {
+ result += ch;
+ }
+ }
+ return result;
}
renderNodeToMarkdown(node, parentNode, index = 0, level = 0, meta = {}) {
var _a;
diff --git a/src/MarkdownManager.ts b/src/MarkdownManager.ts
index 5d6256f921567f53268827cc549bb83b77b3130d..146514f78f857951eebb1f776756b381cc5fc90b 100644
--- a/src/MarkdownManager.ts
+++ b/src/MarkdownManager.ts
@@ -13,7 +13,6 @@ import {
attrsEqual,
callOrReturn,
decodeHtmlEntities,
- encodeHtmlEntities,
flattenExtensions,
generateJSON,
getExtensionField,
@@ -34,6 +33,19 @@ import {
} from './utils.js'
import { htmlContainsUnrecognizedTag } from './utils/htmlTagDetection.js'
+/**
+ * Encode only `<` and `&` as HTML entities. Patch replacement for the
+ * `encodeHtmlEntities` from `@tiptap/core`, which also encodes `>`.
+ *
+ * `>` inside a text node is never a blockquote start (block tokenizer consumes
+ * line-leading `>` before text nodes exist), so encoding it only produces
+ * `&gt;` that re-imports as literal `&gt;` text — the `&gt;` corruption.
+ * `<` and `&` still need encoding to avoid being parsed as tags/entities.
+ */
+function encodeLimitedHtmlEntities(text: string): string {
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;')
+}
+
export class MarkdownManager {
private markedInstance: typeof marked
private activeParseLexer: Lexer | null = null
@@ -1087,6 +1099,12 @@ export class MarkdownManager {
* (code mark or code-block parent) where literal characters should be preserved.
* Also backslash-escape markdown-significant characters in non-code text to
* prevent them from being misinterpreted as formatting delimiters.
+ *
+ * Patch: drop `>` from HTML entity encoding (only encode `<` and `&`).
+ * A `>` inside a text node never starts a blockquote (line-start `>` is
+ * consumed by the block tokenizer before reaching text nodes), so encoding
+ * it only produces `&gt;` that re-imports as literal text — the root cause of
+ * the `&gt;` corruption.
*/
private encodeTextForMarkdown(text: string, node: JSONContent, parentNode?: JSONContent): string {
const isInsideCode =
@@ -1097,7 +1115,7 @@ export class MarkdownManager {
return text
}
- return this.escapeMarkdownSyntax(encodeHtmlEntities(text))
+ return this.escapeMarkdownSyntax(encodeLimitedHtmlEntities(text))
}
/**
@@ -1105,12 +1123,44 @@ export class MarkdownManager {
* syntax. This prevents literal characters in text nodes from being
* misinterpreted as formatting delimiters when the output is parsed again.
*
- * The set covers the most common inline markdown syntax characters.
+ * Patch: CommonMark direction-aware escaping for `*` and `_`. Instead of
+ * blindly escaping every `*`/`_` (which corrupts intra-word underscores like
+ * `content_html` → `content\_html`), only escape positions that would be
+ * consumed as emphasis open/close boundaries by a CommonMark parser. An
+ * emphasis boundary requires one flank to be {start, whitespace, punctuation}
+ * and the other to be a non-space non-punct char (letters / CJK / digits).
+ * Intra-word `*`/`_` (both flanks "other") is left alone.
+ * Structural characters (` ` [ ] ~ \) stay fully escaped — they are never
+ * emphasis boundaries but are structurally significant.
* Characters inside code blocks/code marks are skipped by the caller
* (`encodeTextForMarkdown`) via the existing `isInsideCode` guard.
*/
private escapeMarkdownSyntax(text: string): string {
- return text.replace(/([\\`*_[\]~])/g, '\\$1')
+ // CommonMark 标点集CJK/字母/数字属 "other"。
+ const punct = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
+ let result = ''
+ for (let i = 0; i < text.length; i++) {
+ const ch = text[i]
+ if (ch === '*' || ch === '_') {
+ const prev = i > 0 ? text[i - 1] : ''
+ const next = i < text.length - 1 ? text[i + 1] : ''
+ const leftSpace = prev === '' || /\s/.test(prev)
+ const leftPunct = punct.includes(prev)
+ const leftOther = !leftSpace && !leftPunct
+ const rightSpace = next === '' || /\s/.test(next)
+ const rightPunct = punct.includes(next)
+ const rightOther = !rightSpace && !rightPunct
+ // 可作开边界:(left∈{start,space,punct}) 且 (right∈other)
+ // 可作闭边界:(right∈{start,space,punct}) 且 (left∈other)
+ const isBoundary = ((leftSpace || leftPunct) && rightOther) || ((rightSpace || rightPunct) && leftOther)
+ result += isBoundary ? '\\' + ch : ch
+ } else if (ch === '`' || ch === '[' || ch === ']' || ch === '~' || ch === '\\') {
+ result += '\\' + ch
+ } else {
+ result += ch
+ }
+ }
+ return result
}
renderNodeToMarkdown(