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 2a4e5fbf1..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 |
+ > 1 2 3 4
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 fd125d0f6..2fc4fe468 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,28 @@ 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(3, content.length - 3)}$`; + } + + // Display math + if (content.startsWith(String.raw`\\\[`) && content.endsWith(String.raw`\\\]`)) { + return `$$${content.substring(4, content.length - 4)}$$`; + } + + // 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) { 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 061e22f21..9ca6b6496 100644 --- a/src/services/import/markdown.spec.ts +++ b/src/services/import/markdown.spec.ts @@ -163,4 +163,32 @@ second line 2The 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); + }); + + it("preserves escaped math expressions", () => { + 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 57bf58dd6..a4b541979 100644 --- a/src/services/import/markdown.ts +++ b/src/services/import/markdown.ts @@ -12,7 +12,19 @@ class CustomMarkdownRenderer extends Renderer { } paragraph(data: Tokens.Paragraph): string { - return super.paragraph(data).trimEnd(); + let text = super.paragraph(data).trimEnd(); + + if (text.includes("$")) { + // Display math + text = text.replaceAll(/(?\\\[$1\\\]`); + + // Inline math + text = text.replaceAll(/(?\\\($1\\\)`); + } + + return text; } code({ text, lang }: Tokens.Code): string { @@ -75,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 @@ -84,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(/(<(img|figure).*?style=".*?)"/g, "$1;\""); + // Remove slash for self-closing tags to match CKEditor's approach. html = html.replace(/<(\w+)([^>]*)\s+\/>/g, "<$1$2>");