diff --git a/assets/controllers/editor/conversion.js b/assets/controllers/editor/conversion.js index 3068468..0048ad9 100644 --- a/assets/controllers/editor/conversion.js +++ b/assets/controllers/editor/conversion.js @@ -72,13 +72,55 @@ export function deltaToMarkdown(delta, opts = {}) { } }; - const escapeText = (s) => - String(s) - .replace(/\\/g, '\\\\') - .replace(/([*_`[\]~])/g, '\\$1'); + const escapeText = (s) => { + // Escape special markdown characters, but don't double-escape already escaped ones + // We process character by character to handle existing escapes properly + let result = ''; + const str = String(s); + for (let i = 0; i < str.length; i++) { + const char = str[i]; + if (char === '\\') { + // Check if next char is something that could be escaped + const next = str[i + 1]; + if (next && '\\*_`[]~'.includes(next)) { + // Already escaped, keep both backslash and next char + result += '\\' + next; + i++; // skip next char + } else { + // Lone backslash, escape it + result += '\\\\'; + } + } else if ('*_`[]~'.includes(char)) { + // Special char that needs escaping + result += '\\' + char; + } else { + result += char; + } + } + return result; + }; - const escapeLinkText = (s) => - String(s).replace(/\\/g, '\\\\').replace(/([\[\]])/g, '\\$1'); + const escapeLinkText = (s) => { + let result = ''; + const str = String(s); + for (let i = 0; i < str.length; i++) { + const char = str[i]; + if (char === '\\') { + const next = str[i + 1]; + if (next && '\\[]'.includes(next)) { + result += '\\' + next; + i++; + } else { + result += '\\\\'; + } + } else if ('[]'.includes(char)) { + result += '\\' + char; + } else { + result += char; + } + } + return result; + }; const escapeLinkUrl = (s) => String(s).replace(/\s/g, '%20'); @@ -86,7 +128,22 @@ export function deltaToMarkdown(delta, opts = {}) { if (!text) return ''; if (attrs.code) { - return `\`${String(text).replace(/`/g, '\\`')}\``; + let codeText = ''; + const str = String(text); + for (let i = 0; i < str.length; i++) { + const char = str[i]; + if (char === '\\' && i + 1 < str.length && str[i + 1] === '`') { + codeText += '\\`'; + i++; + } else if (char === '`') { + codeText += '\\`'; + } else if (char === '\\') { + codeText += '\\\\'; + } else { + codeText += char; + } + } + return `\`${codeText}\``; } let out = escapeText(text); @@ -339,7 +396,23 @@ function inlineMarkdownToOps(text) { const pushText = (t) => { if (t) ops.push({ insert: t }); }; + const unescapeText = (t) => t.replace(/\\([\\*_`[\]~])/g, '$1'); + while (i < text.length) { + // escaped character: \X + if (text[i] === '\\' && i + 1 < text.length) { + const next = text[i + 1]; + if ('\\*_`[]~'.includes(next)) { + pushText(next); + i += 2; + continue; + } + // not a recognized escape, treat backslash literally + pushText('\\'); + i += 1; + continue; + } + // inline code: `...` if (text[i] === '`') { const end = text.indexOf('`', i + 1); @@ -358,8 +431,8 @@ function inlineMarkdownToOps(text) { if (closeBracket !== -1 && text[closeBracket + 1] === '(') { const closeParen = text.indexOf(')', closeBracket + 2); if (closeParen !== -1) { - const label = text.slice(i + 1, closeBracket); - const url = text.slice(closeBracket + 2, closeParen); + const label = unescapeText(text.slice(i + 1, closeBracket)); + const url = text.slice(closeBracket + 2, closeParen).replace(/%20/g, ' '); if (label) ops.push({ insert: label, attributes: { link: url } }); i = closeParen + 1; continue; @@ -372,7 +445,7 @@ function inlineMarkdownToOps(text) { if (text.startsWith('**', i)) { const end = text.indexOf('**', i + 2); if (end !== -1) { - const content = text.slice(i + 2, end); + const content = unescapeText(text.slice(i + 2, end)); if (content) ops.push({ insert: content, attributes: { bold: true } }); i = end + 2; continue; @@ -384,7 +457,7 @@ function inlineMarkdownToOps(text) { if (text.startsWith('~~', i)) { const end = text.indexOf('~~', i + 2); if (end !== -1) { - const content = text.slice(i + 2, end); + const content = unescapeText(text.slice(i + 2, end)); if (content) ops.push({ insert: content, attributes: { strike: true } }); i = end + 2; continue; @@ -396,7 +469,7 @@ function inlineMarkdownToOps(text) { if (text[i] === '*') { const end = text.indexOf('*', i + 1); if (end !== -1) { - const content = text.slice(i + 1, end); + const content = unescapeText(text.slice(i + 1, end)); if (content) ops.push({ insert: content, attributes: { italic: true } }); i = end + 1; continue; @@ -414,7 +487,7 @@ function inlineMarkdownToOps(text) { } function nextSpecialIndex(text, start) { - const specials = ['`', '[', '*', '~']; + const specials = ['\\', '`', '[', '*', '~']; let min = text.length; for (const ch of specials) { const idx = text.indexOf(ch, start);