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, "&").replace(/ 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, "&").replace(/ 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 + * `>` that re-imports as literal `>` text — the `>` corruption. + * `<` and `&` still need encoding to avoid being parsed as tags/entities. + */ +function encodeLimitedHtmlEntities(text: string): string { + return text.replace(/&/g, '&').replace(/` 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 `>` that re-imports as literal text — the root cause of + * the `>` 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(