mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	Merge pull request #1984 from TriliumNext/markdown-math
fix(import): Unable to handle multi line mathematical formulas when i…
This commit is contained in:
		| @@ -194,9 +194,43 @@ second line 2</code></pre><ul><li>Hello</li><li>world</li></ul><ol><li>Hello</li | ||||
|         expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); | ||||
|     }); | ||||
|  | ||||
|     it("converts multi-line display math expressions into Mathtex format", () => { | ||||
|         const input = `$$ | ||||
| \\sqrt{x^{2}+1} \\ | ||||
| + \\frac{1}{2} | ||||
| $$`; | ||||
|         const expected = /*html*/`<span class="math-tex">\\[ | ||||
| \\sqrt{x^{2}+1} \\ | ||||
| + \\frac{1}{2} | ||||
| \\]</span>`; | ||||
|         expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); | ||||
|     }); | ||||
|  | ||||
|     it("ignores math formulas inside code blocks and converts inline math expressions correctly", () => { | ||||
|         const result = markdownService.renderToHtml(trimIndentation`\ | ||||
|             \`\`\`unknownlanguage | ||||
|             $$a+b$$ | ||||
|             \`\`\` | ||||
|         `, "title"); | ||||
|         expect(result).toBe(trimIndentation`\ | ||||
|             <pre><code class="language-text-x-trilium-auto">$$a+b$$</code></pre>`); | ||||
|     }); | ||||
|  | ||||
|     it("converts specific inline math expression into Mathtex format", () => { | ||||
|         const input = `This is a formula: $\\mathcal{L}_{task} + \\mathcal{L}_{od}$ inside a sentence.`; | ||||
|         const expected = /*html*/`<p>This is a formula: <span class="math-tex">\\(\\mathcal{L}_{task} + \\mathcal{L}_{od}\\)</span> inside a sentence.</p>`; | ||||
|         expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); | ||||
|     }); | ||||
|  | ||||
|     it("converts math expressions inside list items into Mathtex format", () => { | ||||
|         const input = `- First item with formula: $E = mc^2$`; | ||||
|         const expected = /*html*/`<ul><li>First item with formula: <span class="math-tex">\\(E = mc^2\\)</span></li></ul>`; | ||||
|         expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); | ||||
|     }); | ||||
|  | ||||
|     it("converts display math expressions into Mathtex format", () => { | ||||
|         const input = `$$\sqrt{x^{2}+1}$$`; | ||||
|         const expected = /*html*/`<p><span class="math-tex">\\[\sqrt{x^{2}+1}\\]</span></p>`; | ||||
|         const expected = /*html*/`<span class="math-tex">\\[\sqrt{x^{2}+1}\\]</span>`; | ||||
|         expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -23,19 +23,7 @@ class CustomMarkdownRenderer extends Renderer { | ||||
|     } | ||||
|  | ||||
|     paragraph(data: Tokens.Paragraph): string { | ||||
|         let text = super.paragraph(data).trimEnd(); | ||||
|  | ||||
|         if (text.includes("$")) { | ||||
|             // Display math | ||||
|             text = text.replaceAll(/(?<!\\)\$\$(.+)\$\$/g, | ||||
|                 `<span class="math-tex">\\\[$1\\\]</span>`); | ||||
|  | ||||
|             // Inline math | ||||
|             text = text.replaceAll(/(?<!\\)\$(.+?)\$/g, | ||||
|                 `<span class="math-tex">\\\($1\\\)</span>`); | ||||
|         } | ||||
|  | ||||
|         return text; | ||||
|         return super.paragraph(data).trimEnd(); | ||||
|     } | ||||
|  | ||||
|     code({ text, lang }: Tokens.Code): string { | ||||
| @@ -133,11 +121,17 @@ function renderToHtml(content: string, title: string) { | ||||
|     // Double-escape slashes in math expression because they are otherwise consumed by the parser somewhere. | ||||
|     content = content.replaceAll("\\$", "\\\\$"); | ||||
|  | ||||
|     let html = parse(content, { | ||||
|     // Extract formulas and replace them with placeholders to prevent interference from Markdown rendering | ||||
|     const { processedText, placeholderMap: formulaMap } = extractFormulas(content); | ||||
|  | ||||
|     let html = parse(processedText, { | ||||
|         async: false, | ||||
|         renderer: renderer | ||||
|     }) as string; | ||||
|  | ||||
|     // After rendering, replace placeholders back with the formula HTML | ||||
|     html = restoreFromMap(html, formulaMap); | ||||
|  | ||||
|     // h1 handling needs to come before sanitization | ||||
|     html = importUtils.handleH1(html, title); | ||||
|     html = htmlSanitizer.sanitize(html); | ||||
| @@ -165,6 +159,59 @@ function getNormalizedMimeFromMarkdownLanguage(language: string | undefined) { | ||||
|     return MIME_TYPE_AUTO; | ||||
| } | ||||
|  | ||||
| function extractCodeBlocks(text: string): { processedText: string, placeholderMap: Map<string, string> } { | ||||
|     const codeMap = new Map<string, string>(); | ||||
|     let id = 0; | ||||
|     const timestamp = Date.now(); | ||||
|  | ||||
|     // Multi-line code block and Inline code | ||||
|     text = text.replace(/```[\s\S]*?```/g, (m) => { | ||||
|         const key = `<!--CODE_BLOCK_${timestamp}_${id++}-->`; | ||||
|         codeMap.set(key, m); | ||||
|         return key; | ||||
|     }).replace(/`[^`\n]+`/g, (m) => { | ||||
|         const key = `<!--INLINE_CODE_${timestamp}_${id++}-->`; | ||||
|         codeMap.set(key, m); | ||||
|         return key; | ||||
|     }); | ||||
|  | ||||
|     return { processedText: text, placeholderMap: codeMap }; | ||||
| } | ||||
|  | ||||
| function extractFormulas(text: string): { processedText: string, placeholderMap: Map<string, string> } { | ||||
|     // Protect the $ signs inside code blocks from being recognized as formulas. | ||||
|     const { processedText: noCodeText, placeholderMap: codeMap } = extractCodeBlocks(text); | ||||
|  | ||||
|     const formulaMap = new Map<string, string>(); | ||||
|     let id = 0; | ||||
|     const timestamp = Date.now(); | ||||
|  | ||||
|     // Display math and Inline math | ||||
|     let processedText = noCodeText.replace(/(?<!\\)\$\$((?:(?!\n{2,})[\s\S])+?)\$\$/g, (_, formula) => { | ||||
|         const key = `<!--FORMULA_BLOCK_${timestamp}_${id++}-->`; | ||||
|         const rendered = `<span class="math-tex">\\[${formula}\\]</span>`; | ||||
|         formulaMap.set(key, rendered); | ||||
|         return key; | ||||
|     }).replace(/(?<!\\)\$(.+?)\$/g, (_, formula) => { | ||||
|         const key = `<!--FORMULA_INLINE_${timestamp}_${id++}-->`; | ||||
|         const rendered = `<span class="math-tex">\\(${formula}\\)</span>`; | ||||
|         formulaMap.set(key, rendered); | ||||
|         return key; | ||||
|     }); | ||||
|  | ||||
|     processedText = restoreFromMap(processedText, codeMap); | ||||
|  | ||||
|     return { processedText, placeholderMap: formulaMap }; | ||||
| } | ||||
|  | ||||
| function restoreFromMap(text: string, map: Map<string, string>): string { | ||||
|     if (map.size === 0) return text; | ||||
|     const pattern = [...map.keys()] | ||||
|         .map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) | ||||
|         .join('|'); | ||||
|     return text.replace(new RegExp(pattern, 'g'), match => map.get(match) ?? match); | ||||
| } | ||||
|  | ||||
| const renderer = new CustomMarkdownRenderer({ async: false }); | ||||
|  | ||||
| export default { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user