From 894cfe4f7a9537b45516724233c62ef05354dece Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 5 Apr 2025 09:28:18 +0300 Subject: [PATCH 01/11] feat(export/markdown): export in-line math properly --- src/services/export/markdown.spec.ts | 6 ++++++ src/services/export/markdown.ts | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/services/export/markdown.spec.ts b/src/services/export/markdown.spec.ts index 2a4e5fbf1..2726481af 100644 --- a/src/services/export/markdown.spec.ts +++ b/src/services/export/markdown.spec.ts @@ -279,4 +279,10 @@ describe("Markdown export", () => { expect(markdownExportService.toMarkdown(html)).toBe(expected); }); + it("converts inline math expressions to proper Markdown syntax", () => { + const html = /*html*/`

The equation is \(e=mc^{2}\).

`; + const expected = `The equation is\u00a0$e=mc^{2}$.`; + expect(markdownExportService.toMarkdown(html)).toBe(expected); + }); + }); diff --git a/src/services/export/markdown.ts b/src/services/export/markdown.ts index fd125d0f6..cbc480984 100644 --- a/src/services/export/markdown.ts +++ b/src/services/export/markdown.ts @@ -46,6 +46,7 @@ function toMarkdown(content: string) { instance.addRule("admonition", buildAdmonitionFilter()); instance.addRule("inlineLink", buildInlineLinkFilter()); instance.addRule("figure", buildFigureFilter()); + instance.addRule("math", buildMathFilter()); instance.use(gfm); instance.keep([ "kbd" ]); } @@ -207,6 +208,23 @@ function buildFigureFilter(): Rule { } } +function buildMathFilter(): Rule { + return { + filter(node) { + return node.nodeName === "SPAN" && node.classList.contains("math-tex"); + }, + replacement(content) { + // Inline math + if (content.startsWith("(") && content.endsWith(")")) { + return `$${content.substring(1, content.length - 1)}$`; + } + + // Unknown. + return content; + } + } +} + // Taken from upstream since it's not exposed. // https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js function cleanAttribute(attribute: string | null | undefined) { From fc4eb13e8d16167f2104152ca35b7933146542ca Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 5 Apr 2025 09:32:08 +0300 Subject: [PATCH 02/11] feat(export/markdown): export display math properly --- src/services/export/markdown.spec.ts | 8 +++++++- src/services/export/markdown.ts | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/services/export/markdown.spec.ts b/src/services/export/markdown.spec.ts index 2726481af..24aa77da2 100644 --- a/src/services/export/markdown.spec.ts +++ b/src/services/export/markdown.spec.ts @@ -279,10 +279,16 @@ describe("Markdown export", () => { expect(markdownExportService.toMarkdown(html)).toBe(expected); }); - it("converts inline math expressions to proper Markdown syntax", () => { + it("converts inline math expressions into proper Markdown syntax", () => { const html = /*html*/`

The equation is \(e=mc^{2}\).

`; const expected = `The equation is\u00a0$e=mc^{2}$.`; expect(markdownExportService.toMarkdown(html)).toBe(expected); }); + it("converts display math expressions into proper Markdown syntax", () => { + const html = /*html*/`\[\sqrt{x^{2}+1}\]`; + const expected = `$$\sqrt{x^{2}+1}$$`; + expect(markdownExportService.toMarkdown(html)).toBe(expected); + }); + }); diff --git a/src/services/export/markdown.ts b/src/services/export/markdown.ts index cbc480984..731024435 100644 --- a/src/services/export/markdown.ts +++ b/src/services/export/markdown.ts @@ -219,6 +219,11 @@ function buildMathFilter(): Rule { return `$${content.substring(1, content.length - 1)}$`; } + // Display math + if (content.startsWith("\\[") && content.endsWith("\\]")) { + return `$$${content.substring(2, content.length - 2)}$$`; + } + // Unknown. return content; } From 07b5cd3b05749073b4778f4850ee71b8ad04697c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 5 Apr 2025 09:57:44 +0300 Subject: [PATCH 03/11] feat(import/markdown): import in-display math properly --- src/services/import/markdown.spec.ts | 12 ++++++++++++ src/services/import/markdown.ts | 8 +++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/services/import/markdown.spec.ts b/src/services/import/markdown.spec.ts index 061e22f21..ea98eca96 100644 --- a/src/services/import/markdown.spec.ts +++ b/src/services/import/markdown.spec.ts @@ -163,4 +163,16 @@ second line 2
  • Hello
  • world
  1. Hello
  2. { + const input = `The equation is\u00a0$e=mc^{2}$.`; + const expected = /*html*/`

    The equation is \(e=mc^{2}\).

    `; + expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); + }); + + it("converts display math expressions into Mathtex format", () => { + const input = `$$\sqrt{x^{2}+1}$$`; + const expected = /*html*/`

    \\[\sqrt{x^{2}+1}\\]

    `; + expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); + }); + }); diff --git a/src/services/import/markdown.ts b/src/services/import/markdown.ts index 57bf58dd6..1613c2f57 100644 --- a/src/services/import/markdown.ts +++ b/src/services/import/markdown.ts @@ -12,7 +12,13 @@ class CustomMarkdownRenderer extends Renderer { } paragraph(data: Tokens.Paragraph): string { - return super.paragraph(data).trimEnd(); + let text = super.paragraph(data).trimEnd(); + + // Display math + text = text.replaceAll(/\$\$(.+)\$\$/g, + `\\\[$1\\\]`); + + return text; } code({ text, lang }: Tokens.Code): string { From e6b9ecda5ce62be9deff2da2c0703a6891f951b0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 5 Apr 2025 09:59:10 +0300 Subject: [PATCH 04/11] feat(import/markdown): import in-line math properly --- src/services/import/markdown.spec.ts | 2 +- src/services/import/markdown.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/import/markdown.spec.ts b/src/services/import/markdown.spec.ts index ea98eca96..d2d8ea694 100644 --- a/src/services/import/markdown.spec.ts +++ b/src/services/import/markdown.spec.ts @@ -165,7 +165,7 @@ second line 2
    • Hello
    • world
    1. Hello
    2. { const input = `The equation is\u00a0$e=mc^{2}$.`; - const expected = /*html*/`

      The equation is \(e=mc^{2}\).

      `; + const expected = /*html*/`

      The equation is \\(e=mc^{2}\\).

      `; expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); }); diff --git a/src/services/import/markdown.ts b/src/services/import/markdown.ts index 1613c2f57..6cc46caec 100644 --- a/src/services/import/markdown.ts +++ b/src/services/import/markdown.ts @@ -18,6 +18,10 @@ class CustomMarkdownRenderer extends Renderer { text = text.replaceAll(/\$\$(.+)\$\$/g, `\\\[$1\\\]`); + // Inline math + text = text.replaceAll(/\$(.+)\$/g, + `\\\($1\\\)`); + return text; } From 721bf455e12a1893ddeabcef7fe00680074d1416 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 5 Apr 2025 09:59:42 +0300 Subject: [PATCH 05/11] refactor(import/markdown): add guard condition for processing math --- src/services/import/markdown.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/services/import/markdown.ts b/src/services/import/markdown.ts index 6cc46caec..60f770bbd 100644 --- a/src/services/import/markdown.ts +++ b/src/services/import/markdown.ts @@ -14,13 +14,15 @@ class CustomMarkdownRenderer extends Renderer { paragraph(data: Tokens.Paragraph): string { let text = super.paragraph(data).trimEnd(); - // Display math - text = text.replaceAll(/\$\$(.+)\$\$/g, - `\\\[$1\\\]`); + if (text.includes("$")) { + // Display math + text = text.replaceAll(/\$\$(.+)\$\$/g, + `\\\[$1\\\]`); - // Inline math - text = text.replaceAll(/\$(.+)\$/g, - `\\\($1\\\)`); + // Inline math + text = text.replaceAll(/\$(.+)\$/g, + `\\\($1\\\)`); + } return text; } From 4bb767f8eec65b99066d0351824c050a5bc4b4c6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 5 Apr 2025 10:46:33 +0300 Subject: [PATCH 06/11] fix(import/markdown): preserve escaped math expressions --- src/services/import/markdown.spec.ts | 10 ++++++++++ src/services/import/markdown.ts | 7 +++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/services/import/markdown.spec.ts b/src/services/import/markdown.spec.ts index d2d8ea694..e25a8bc09 100644 --- a/src/services/import/markdown.spec.ts +++ b/src/services/import/markdown.spec.ts @@ -175,4 +175,14 @@ second line 2
      • Hello
      • world
      1. Hello
      2. { + const scenarios = [ + "\\$\\$\sqrt{x^{2}+1}\\$\\$", + "The equation is \\$e=mc^{2}\\$." + ]; + for (const scenario of scenarios) { + expect(markdownService.renderToHtml(scenario, "Title")).toStrictEqual(`

        ${scenario}

        `); + } + }); + }); diff --git a/src/services/import/markdown.ts b/src/services/import/markdown.ts index 60f770bbd..209d4afe5 100644 --- a/src/services/import/markdown.ts +++ b/src/services/import/markdown.ts @@ -16,11 +16,11 @@ class CustomMarkdownRenderer extends Renderer { if (text.includes("$")) { // Display math - text = text.replaceAll(/\$\$(.+)\$\$/g, + text = text.replaceAll(/(?\\\[$1\\\]`); // Inline math - text = text.replaceAll(/\$(.+)\$/g, + text = text.replaceAll(/(?\\\($1\\\)`); } @@ -87,6 +87,9 @@ import { ADMONITION_TYPE_MAPPINGS } from "../export/markdown.js"; import utils from "../utils.js"; 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, { async: false, renderer: renderer From 7293f59a80e7578ae84af24435f7e226aab124a1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 5 Apr 2025 11:04:40 +0300 Subject: [PATCH 07/11] fix(export/markdown): math expressions not working due to string escaping --- docs/User Guide/User Guide/Advanced Usage/Note ID.md | 2 +- src/services/export/markdown.spec.ts | 4 ++-- src/services/export/markdown.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/User Guide/User Guide/Advanced Usage/Note ID.md b/docs/User Guide/User Guide/Advanced Usage/Note ID.md index 5c0c8949b..f8f0dcd6c 100644 --- a/docs/User Guide/User Guide/Advanced Usage/Note ID.md +++ b/docs/User Guide/User Guide/Advanced Usage/Note ID.md @@ -11,6 +11,6 @@ When notes are exported, their note ID is kept in the metadata of the export. Ho Since the Note ID is a fixed-width randomly generated number, due to the [pigeonhole principle](https://en.wikipedia.org/wiki/Pigeonhole_principle), there is a possibility that a newly created note will have the same ID as an existing note. -Since the note ID is alphanumeric and the length is 12 we have \\(62^{12}\\) unique IDs. However since we are generating them randomly, we can use a collision calculator such as the one for [Nano ID](https://alex7kom.github.io/nano-nanoid-cc/?alphabet=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz&size=12&speed=1000&speedUnit=hour) to determine that we'd need to create 1000 notes per hour every hour for 9 centuries in order to have at least 1% probability of a note collision. +Since the note ID is alphanumeric and the length is 12 we have $62^{12}$ unique IDs. However since we are generating them randomly, we can use a collision calculator such as the one for [Nano ID](https://alex7kom.github.io/nano-nanoid-cc/?alphabet=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz&size=12&speed=1000&speedUnit=hour) to determine that we'd need to create 1000 notes per hour every hour for 9 centuries in order to have at least 1% probability of a note collision. As such, Trilium does not take any explicit action against potential note collisions, similar to other software that makes uses of unique hashes such as [Git](https://stackoverflow.com/questions/10434326/hash-collision-in-git). If one would theoretically occur, what would most likely happen is that the existing note will be replaced by the new one. \ No newline at end of file diff --git a/src/services/export/markdown.spec.ts b/src/services/export/markdown.spec.ts index 24aa77da2..077d91b41 100644 --- a/src/services/export/markdown.spec.ts +++ b/src/services/export/markdown.spec.ts @@ -280,13 +280,13 @@ describe("Markdown export", () => { }); it("converts inline math expressions into proper Markdown syntax", () => { - const html = /*html*/`

        The equation is \(e=mc^{2}\).

        `; + const html = /*html*/`

        The equation is \\(e=mc^{2}\\).

        `; const expected = `The equation is\u00a0$e=mc^{2}$.`; expect(markdownExportService.toMarkdown(html)).toBe(expected); }); it("converts display math expressions into proper Markdown syntax", () => { - const html = /*html*/`\[\sqrt{x^{2}+1}\]`; + const html = /*html*/`\\[\sqrt{x^{2}+1}\\]`; const expected = `$$\sqrt{x^{2}+1}$$`; expect(markdownExportService.toMarkdown(html)).toBe(expected); }); diff --git a/src/services/export/markdown.ts b/src/services/export/markdown.ts index 731024435..2fc4fe468 100644 --- a/src/services/export/markdown.ts +++ b/src/services/export/markdown.ts @@ -215,13 +215,13 @@ function buildMathFilter(): Rule { }, replacement(content) { // Inline math - if (content.startsWith("(") && content.endsWith(")")) { - return `$${content.substring(1, content.length - 1)}$`; + if (content.startsWith("\\\\(") && content.endsWith("\\\\)")) { + return `$${content.substring(3, content.length - 3)}$`; } // Display math - if (content.startsWith("\\[") && content.endsWith("\\]")) { - return `$$${content.substring(2, content.length - 2)}$$`; + if (content.startsWith(String.raw`\\\[`) && content.endsWith(String.raw`\\\]`)) { + return `$$${content.substring(4, content.length - 4)}$$`; } // Unknown. From 64ccea570283987b08d62f7b9c6e895f7bbc8be9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 5 Apr 2025 11:37:26 +0300 Subject: [PATCH 08/11] feat(import/markdown): preserve figure image size --- src/services/html_sanitizer.ts | 6 +++++- src/services/import/markdown.spec.ts | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/services/html_sanitizer.ts b/src/services/html_sanitizer.ts index c1e18bb41..b4b6dae32 100644 --- a/src/services/html_sanitizer.ts +++ b/src/services/html_sanitizer.ts @@ -149,7 +149,8 @@ function sanitize(dirtyHtml: string) { allowedTags, allowedAttributes: { "*": ["class", "style", "title", "src", "href", "hash", "disabled", "align", "alt", "center", "data-*"], - input: ["type", "checked"] + input: ["type", "checked"], + img: ["width", "height"] }, allowedStyles: { "*": { @@ -161,6 +162,9 @@ function sanitize(dirtyHtml: string) { width: sizeRegex, height: sizeRegex }, + img: { + "aspect-ratio": [ /^\d+\/\d+$/ ], + }, table: { "border-color": colorRegex, "border-style": [/^\s*(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)\s*$/] diff --git a/src/services/import/markdown.spec.ts b/src/services/import/markdown.spec.ts index e25a8bc09..cf75fc1af 100644 --- a/src/services/import/markdown.spec.ts +++ b/src/services/import/markdown.spec.ts @@ -163,6 +163,12 @@ second line 2
        • Hello
        • world
        1. Hello
        2. { + const input = `
          `; + const expected = /*html*/`
          `; + expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); + }); + it("converts inline math expressions into Mathtex format", () => { const input = `The equation is\u00a0$e=mc^{2}$.`; const expected = /*html*/`

          The equation is \\(e=mc^{2}\\).

          `; From 8cb10764b6d2800981cb53db4cac5802d390f28c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 5 Apr 2025 12:31:02 +0300 Subject: [PATCH 09/11] feat(import/markdown): preserve trailing semicolon in img --- src/services/import/markdown.spec.ts | 2 +- src/services/import/markdown.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/import/markdown.spec.ts b/src/services/import/markdown.spec.ts index cf75fc1af..91389350c 100644 --- a/src/services/import/markdown.spec.ts +++ b/src/services/import/markdown.spec.ts @@ -165,7 +165,7 @@ second line 2
          • Hello
          • world
          1. Hello
          2. { const input = `
            `; - const expected = /*html*/`
            `; + const expected = /*html*/`
            `; expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); }); diff --git a/src/services/import/markdown.ts b/src/services/import/markdown.ts index 209d4afe5..22dd9cab9 100644 --- a/src/services/import/markdown.ts +++ b/src/services/import/markdown.ts @@ -99,6 +99,9 @@ function renderToHtml(content: string, title: string) { html = importUtils.handleH1(html, title); html = htmlSanitizer.sanitize(html); + // Add a trailing semicolon to CSS styles. + html = html.replaceAll(/(]*)\s+\/>/g, "<$1$2>"); From cdb5ebb08076c9b739111f19d61109f940e138a7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 5 Apr 2025 12:37:06 +0300 Subject: [PATCH 10/11] feat(import/markdown): preserve trailing semicolon in figure style --- src/services/import/markdown.spec.ts | 4 ++-- src/services/import/markdown.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/import/markdown.spec.ts b/src/services/import/markdown.spec.ts index 91389350c..9ca6b6496 100644 --- a/src/services/import/markdown.spec.ts +++ b/src/services/import/markdown.spec.ts @@ -164,8 +164,8 @@ second line 2
            • Hello
            • world
            1. Hello
            2. { - const input = `
              `; - const expected = /*html*/`
              `; + const input = `
              `; + const expected = /*html*/`
              `; expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); }); diff --git a/src/services/import/markdown.ts b/src/services/import/markdown.ts index 22dd9cab9..a4b541979 100644 --- a/src/services/import/markdown.ts +++ b/src/services/import/markdown.ts @@ -100,7 +100,7 @@ function renderToHtml(content: string, title: string) { html = htmlSanitizer.sanitize(html); // Add a trailing semicolon to CSS styles. - html = html.replaceAll(/(]*)\s+\/>/g, "<$1$2>"); From 8977926c00cca7092086724263a22c4fd8016860 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 5 Apr 2025 17:51:03 +0300 Subject: [PATCH 11/11] fix(test): failed test due to change in figure handling --- src/services/export/markdown.spec.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/services/export/markdown.spec.ts b/src/services/export/markdown.spec.ts index 077d91b41..67dfef5e6 100644 --- a/src/services/export/markdown.spec.ts +++ b/src/services/export/markdown.spec.ts @@ -176,10 +176,7 @@ describe("Markdown export", () => { > [!IMPORTANT] > This is a very important information. >${space} - > | | | - > | --- | --- | - > | 1 | 2 | - > | 3 | 4 | + >
              12
              34
              > [!CAUTION] > This is a caution.