mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 18:12:29 +08:00
Merge branch 'develop' into renovate/typescript-5.x
This commit is contained in:
commit
cfcc4740a1
@ -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.
|
@ -176,10 +176,7 @@ describe("Markdown export", () => {
|
||||
> [!IMPORTANT]
|
||||
> This is a very important information.
|
||||
>${space}
|
||||
> | | |
|
||||
> | --- | --- |
|
||||
> | 1 | 2 |
|
||||
> | 3 | 4 |
|
||||
> <figure class="table"><table><tbody><tr><td>1</td><td>2</td></tr><tr><td>3</td><td>4</td></tr></tbody></table></figure>
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a caution.
|
||||
@ -279,4 +276,16 @@ describe("Markdown export", () => {
|
||||
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
||||
});
|
||||
|
||||
it("converts inline math expressions into proper Markdown syntax", () => {
|
||||
const html = /*html*/`<p>The equation is <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);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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*$/]
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
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 <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>`);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -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(/(?<!\\)\$\$(.+)\$\$/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 {
|
||||
@ -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>");
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user