Merge branch 'develop' into renovate/typescript-5.x

This commit is contained in:
Elian Doran 2025-04-05 17:51:34 +03:00 committed by GitHub
commit cfcc4740a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 89 additions and 7 deletions

View File

@ -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 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. 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.

View File

@ -176,10 +176,7 @@ describe("Markdown export", () => {
> [!IMPORTANT] > [!IMPORTANT]
> This is a very important information. > This is a very important information.
>${space} >${space}
> | | | > <figure class="table"><table><tbody><tr><td>1</td><td>2</td></tr><tr><td>3</td><td>4</td></tr></tbody></table></figure>
> | --- | --- |
> | 1 | 2 |
> | 3 | 4 |
> [!CAUTION] > [!CAUTION]
> This is a caution. > This is a caution.
@ -279,4 +276,16 @@ describe("Markdown export", () => {
expect(markdownExportService.toMarkdown(html)).toBe(expected); expect(markdownExportService.toMarkdown(html)).toBe(expected);
}); });
it("converts inline math expressions into proper Markdown syntax", () => {
const html = /*html*/`<p>The equation is&nbsp;<span class="math-tex">\\(e=mc^{2}\\)</span>.</p>`;
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*/`<span class="math-tex">\\[\sqrt{x^{2}+1}\\]</span>`;
const expected = `$$\sqrt{x^{2}+1}$$`;
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});
}); });

View File

@ -46,6 +46,7 @@ function toMarkdown(content: string) {
instance.addRule("admonition", buildAdmonitionFilter()); instance.addRule("admonition", buildAdmonitionFilter());
instance.addRule("inlineLink", buildInlineLinkFilter()); instance.addRule("inlineLink", buildInlineLinkFilter());
instance.addRule("figure", buildFigureFilter()); instance.addRule("figure", buildFigureFilter());
instance.addRule("math", buildMathFilter());
instance.use(gfm); instance.use(gfm);
instance.keep([ "kbd" ]); 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. // Taken from upstream since it's not exposed.
// https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js // https://github.com/mixmark-io/turndown/blob/master/src/commonmark-rules.js
function cleanAttribute(attribute: string | null | undefined) { function cleanAttribute(attribute: string | null | undefined) {

View File

@ -149,7 +149,8 @@ function sanitize(dirtyHtml: string) {
allowedTags, allowedTags,
allowedAttributes: { allowedAttributes: {
"*": ["class", "style", "title", "src", "href", "hash", "disabled", "align", "alt", "center", "data-*"], "*": ["class", "style", "title", "src", "href", "hash", "disabled", "align", "alt", "center", "data-*"],
input: ["type", "checked"] input: ["type", "checked"],
img: ["width", "height"]
}, },
allowedStyles: { allowedStyles: {
"*": { "*": {
@ -161,6 +162,9 @@ function sanitize(dirtyHtml: string) {
width: sizeRegex, width: sizeRegex,
height: sizeRegex height: sizeRegex
}, },
img: {
"aspect-ratio": [ /^\d+\/\d+$/ ],
},
table: { table: {
"border-color": colorRegex, "border-color": colorRegex,
"border-style": [/^\s*(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)\s*$/] "border-style": [/^\s*(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)\s*$/]

View File

@ -163,4 +163,32 @@ second line 2</code></pre><ul><li>Hello</li><li>world</li></ul><ol><li>Hello</li
expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected); expect(markdownService.renderToHtml(input, "Title")).toStrictEqual(expected);
}); });
it("preserves figures", () => {
const input = `<figure class="image image-style-align-center image_resized" style="width:50%;"><img style="aspect-ratio:991/403;" src="Jump to Note_image.png" width="991" height="403"></figure>`;
const expected = /*html*/`<figure class="image image-style-align-center image_resized" style="width:50%;"><img style="aspect-ratio:991/403;" src="Jump to Note_image.png" width="991" height="403"></figure>`;
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*/`<p>The equation is&nbsp;<span class="math-tex">\\(e=mc^{2}\\)</span>.</p>`;
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>`;
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(`<p>${scenario}</p>`);
}
});
}); });

View File

@ -12,7 +12,19 @@ class CustomMarkdownRenderer extends Renderer {
} }
paragraph(data: Tokens.Paragraph): string { paragraph(data: Tokens.Paragraph): string {
return super.paragraph(data).trimEnd(); 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;
} }
code({ text, lang }: Tokens.Code): string { code({ text, lang }: Tokens.Code): string {
@ -75,6 +87,9 @@ import { ADMONITION_TYPE_MAPPINGS } from "../export/markdown.js";
import utils from "../utils.js"; import utils from "../utils.js";
function renderToHtml(content: string, title: string) { 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, { let html = parse(content, {
async: false, async: false,
renderer: renderer renderer: renderer
@ -84,6 +99,9 @@ function renderToHtml(content: string, title: string) {
html = importUtils.handleH1(html, title); html = importUtils.handleH1(html, title);
html = htmlSanitizer.sanitize(html); 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. // Remove slash for self-closing tags to match CKEditor's approach.
html = html.replace(/<(\w+)([^>]*)\s+\/>/g, "<$1$2>"); html = html.replace(/<(\w+)([^>]*)\s+\/>/g, "<$1$2>");