diff --git a/web/src/copy_and_paste.ts b/web/src/copy_and_paste.ts index 40ae732af5..0c111c45c9 100644 --- a/web/src/copy_and_paste.ts +++ b/web/src/copy_and_paste.ts @@ -362,7 +362,7 @@ function image_to_zulip_markdown( const src = node.getAttribute("src") ?? node.getAttribute("href") ?? ""; const title = deduplicate_newlines(node.getAttribute("title") ?? ""); // Using Zulip's link like syntax for images - return src ? "[" + title + "](" + src + ")" : (node.getAttribute("alt") ?? ""); + return src ? "[" + title + "](" + src + ")" : node.getAttribute("alt") ?? ""; } function within_single_element(html_fragment: HTMLElement): boolean { @@ -436,6 +436,36 @@ export function paste_handler_converter(paste_html: string): string { return "~~" + content + "~~"; }, }); + turndownService.addRule("latexMath", { + filter(node: Node) { + if (!(node instanceof Element)) { + return false; + } + const closest_display = node.closest(".katex-display"); + const has_mathml = node.querySelector(".katex-mathml") !== null; + return closest_display !== null && has_mathml; + }, + replacement(content: string, node: Node) { + if (!(node instanceof Element)) { + return content; + } + const display_element = node.closest(".katex-display") ?? node; + const mathml_element = display_element.querySelector(".katex-mathml"); + + assert(mathml_element, "Expected .katex-mathml element not found."); + + const annotation = mathml_element.querySelector( + 'annotation[encoding="application/x-tex"]', + ); + + assert(annotation?.textContent, "Expected LaTeX annotation not found."); + + const latex_content = annotation.textContent.trim(); + return latex_content.includes("\n") + ? `\`\`\`math\n${latex_content}\n\`\`\`\n` + : `$$${latex_content}$$`; + }, + }); turndownService.addRule("links", { filter: ["a"], replacement(content, node) { @@ -570,7 +600,7 @@ export function paste_handler_converter(paste_html: string): string { const className = codeElement.getAttribute("class") ?? ""; const language = node.parentElement?.classList.contains("zulip-code-block") - ? (node.closest(".codehilite")?.dataset?.codeLanguage ?? "") + ? node.closest(".codehilite")?.dataset?.codeLanguage ?? "" : (/language-(\S+)/.exec(className) ?? [null, ""])[1]; assert(options.fence !== undefined); diff --git a/web/tests/copy_and_paste.test.cjs b/web/tests/copy_and_paste.test.cjs index 1feb8a674e..19b9e3411c 100644 --- a/web/tests/copy_and_paste.test.cjs +++ b/web/tests/copy_and_paste.test.cjs @@ -2,6 +2,8 @@ const assert = require("node:assert/strict"); +const {JSDOM} = require("jsdom"); + const {zrequire} = require("./lib/namespace.cjs"); const {run_test} = require("./lib/test.cjs"); @@ -83,6 +85,44 @@ run_test("try_stream_topic_syntax_text", () => { } }); +run_test("latex_math_conversion", () => { + const {window} = new JSDOM(``); + global.document = window.document; + global.Element = window.Element; + // Test case for inline LaTeX math + let input = ` + + $$x^2 + y^2 = z^2$$ + `; + assert.equal(copy_and_paste.paste_handler_converter(input), "$$x^2 + y^2 = z^2$$"); + + // Test case for multiline LaTeX math + input = ` + + + \\begin{align} +x + y &= z \\\\ +a + b &= c +\\end{align} + + `; + assert.equal( + copy_and_paste.paste_handler_converter(input), + "\\begin{align} x + y &= z \\\\ a + b &= c \\end{align}", + ); + + // Test case where no math content exists + input = `

No math here

`; + assert.equal(copy_and_paste.paste_handler_converter(input), "No math here"); + + // Test case where closest display exists but no annotation + input = ` + + + { // Copied HTML from VS Code let paste_html = `
if ($preview_src.endsWith("&size=full"))
`;