Merge remote-tracking branch 'origin/develop' into feature/server_esbuild

This commit is contained in:
Elian Doran 2025-05-22 12:24:02 +03:00
commit edc180189c
No known key found for this signature in database
14 changed files with 686 additions and 490 deletions

View File

@ -63,7 +63,7 @@
"@types/leaflet": "1.9.18",
"@types/leaflet-gpx": "1.3.7",
"@types/mark.js": "8.11.12",
"@types/react": "19.1.4",
"@types/react": "19.1.5",
"@types/react-dom": "19.1.5",
"copy-webpack-plugin": "13.0.0",
"happy-dom": "17.4.7",

View File

@ -15,6 +15,14 @@
src: url(../fonts/JetBrainsMono-Light.woff2) format("woff");
}
:root {
--admonition-note-accent-color: #69c7ff;
--admonition-tip-accent-color: #40c025;
--admonition-important-accent-color: #9839f7;
--admonition-caution-accent-color: #ff2e2e;
--admonition-warning-accent-color: #e2aa03;
}
.table {
--bs-table-bg: transparent !important;
}
@ -2008,11 +2016,11 @@ footer.file-footer button {
left: 1em;
}
.admonition.note { --accent-color: #69c7ff; }
.admonition.tip { --accent-color: #40c025; }
.admonition.important { --accent-color: #9839f7; }
.admonition.caution { --accent-color: #ff2e2e; }
.admonition.warning { --accent-color: #e2aa03; }
.admonition.note { --accent-color: var(--admonition-note-accent-color); }
.admonition.tip { --accent-color: var(--admonition-tip-accent-color); }
.admonition.important { --accent-color: var(--admonition-important-accent-color); }
.admonition.caution { --accent-color: var(--admonition-caution-accent-color); }
.admonition.warning { --accent-color: var(--admonition-warning-accent-color); }
.admonition.note::before { content: "\eb21"; }
.admonition.tip::before { content: "\ea0d"; }

View File

@ -89,6 +89,7 @@
--menu-item-arrow-color: #ffffffa3;
--menu-item-delimiter-color: #ffffff1c;
--menu-item-group-header-color: #ffffff91;
--menu-section-background-color: #fefefe08;
--modal-backdrop-color: #000;
--modal-shadow-color: rgba(0, 0, 0, .5);
@ -236,6 +237,11 @@
--help-code-background: #565656;
--ck-editor-popup-border-color: var(--modal-border-color);
--ck-editor-toolbar-button-on-background: #ffffff3b;
--ck-editor-toolbar-button-on-color: white;
--ck-editor-toolbar-button-on-shadow: 1px 1px 2px rgba(0, 0, 0, .75);
--ck-editor-toolbar-dropdown-button-open-background: #ffffff14;
}
/*

View File

@ -83,6 +83,7 @@
--menu-item-arrow-color: #00000080;
--menu-item-delimiter-color: #00000030;
--menu-item-group-header-color: #00000061;
--menu-section-background-color: #00000006;
--modal-backdrop-color: #7c7c7c;
--modal-shadow-color: #00000033;
@ -236,4 +237,9 @@
--help-code-background: #d7d5d5;
--ck-editor-popup-border-color: var(--dropdown-border-color);
--ck-editor-toolbar-button-on-background: #00000017;
--ck-editor-toolbar-button-on-color: black;
--ck-editor-toolbar-button-on-shadow: 1px 1px 1px rgba(0, 0, 0, .35);
--ck-editor-toolbar-dropdown-button-open-background: #0000000f;
}

View File

@ -27,6 +27,19 @@
--ck-border-radius: 6px;
}
/* Toolbar button in on state */
.ck.ck-toolbar .ck-button.ck-on:not(.ck-dropdown__button):not(.ck-list-item-button):not(.ck-button_with-text) {
--ck-color-button-on-background: var(--ck-editor-toolbar-button-on-background);
--ck-color-button-on-color: var(--ck-editor-toolbar-button-on-color);
box-shadow: var(--ck-editor-toolbar-button-on-shadow);
}
/* Toolbar button with its dropdown open */
.ck.ck-toolbar .ck-button.ck-dropdown__button {
--ck-color-button-on-background: var(--ck-editor-toolbar-dropdown-button-open-background);
--ck-color-button-on-color: currentColor;
}
/* Disabled button */
:root .classic-toolbar-widget .ck.ck-button.ck-disabled {
opacity: .75;
@ -73,7 +86,8 @@
}
/* Dropdown list item */
:root ul.ck.ck-list button.ck-button {
:root ul.ck.ck-list button.ck-button,
:root .ck.ck-collapsible > button.ck-button {
padding: 2px 16px;
background: transparent;
border-radius: 6px !important;
@ -83,7 +97,9 @@
/* Checked list item */
:root ul.ck.ck-list button.ck-button:hover,
:root ul.ck.ck-list button.ck-button.ck-on:hover {
:root ul.ck.ck-list button.ck-button.ck-on:hover,
:root .ck.ck-collapsible > button.ck-button:not(.ck-disabled):hover,
:root .ck.ck-collapsible > button.ck-button:not(.ck-disabled):not(:focus):hover {
background: var(--hover-item-background-color);
color: var(--hover-item-color);
}
@ -96,6 +112,42 @@
background: var(--menu-item-delimiter-color);
}
/* Collapsible section */
.ck.ck-collapsible {
position: relative;
border: unset !important;
padding-top: var(--ck-editor-popup-padding);
}
.ck.ck-collapsible::before {
/* Adds a background shade which overlaps the dropdown's padding */
--negative-padding: calc(0px - var(--ck-editor-popup-padding));
display: block;
content: "";
position: absolute;
top: 0;
bottom: var(--negative-padding);
left: var(--negative-padding);
right: var(--negative-padding);
border-top: 1px solid var(--ck-editor-popup-border-color);
background: var(--menu-section-background-color);
}
.ck.ck-collapsible:last-child::before {
border-radius: 0 0 var(--dropdown-border-radius) var(--dropdown-border-radius);
}
.ck.ck-collapsible.ck-collapsible_collapsed > button.ck-button {
font-weight: normal !important;
}
.ck.ck-collapsible .ck-collapsible__children {
padding-top: 1em;
}
/* Font size dropdown */
.ck-fontsize-option {
@ -121,15 +173,55 @@
border-bottom-right-radius: var(--ck-border-radius);
}
/* Current color checkmark */
:root .ck.ck-color-selector .ck-icon {
color: white;
}
:root .ck.ck-color-selector .ck-icon__fill {
fill: black !important;
}
/* Table dropdown */
.ck-insert-table-dropdown__grid {
--ck-insert-table-dropdown-box-width: 16px;
--ck-insert-table-dropdown-box-height: 16px;
--ck-insert-table-dropdown-box-margin: 2px;
--ck-color-base-border: var(--ck-color-panel-border); /* Cell box color */
--ck-color-focus-border: var(--hover-item-text-color); /* Selected cell box border color */
--ck-color-focus-outer-shadow: var(--hover-item-background-color); /* Selected cell box background color */
--ck-border-radius: 0;
}
/* Admonitions dropdown */
.ck-tn-admonition-note { --icon: "\eb21"; --accent: var(--admonition-note-accent-color);}
.ck-tn-admonition-tip { --icon: "\ea0d"; --accent: var(--admonition-tip-accent-color);}
.ck-tn-admonition-important { --icon: "\ea7c"; --accent: var(--admonition-important-accent-color);}
.ck-tn-admonition-caution { --icon: "\eac7"; --accent: var(--admonition-caution-accent-color);}
.ck-tn-admonition-warning { --icon: "\eac5"; --accent: var(--admonition-warning-accent-color);}
:root .ck.ck-tn-admonition-option .ck-button__label {
display: inline-flex;
align-items: center;
width: 100%;
margin: 4px;
padding-right: 2em;
border: 1px solid var(--accent);
border-radius: 6px;
}
:root .ck.ck-tn-admonition-option .ck-button__label::before {
display: inline-block;
content: var(--icon);
width: 2em;
text-align: center;
font-size: 1.4em;
font-family: boxicons;
color: var(--accent);
}
/* Action buttons */
:root .ck-link-actions button.ck-button,

View File

@ -1,4 +1,4 @@
FROM node:22.15.1-bullseye-slim AS builder
FROM node:22.16.0-bullseye-slim AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@ -7,7 +7,7 @@ FROM node:22.15.1-bullseye-slim AS builder
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:22.15.1-bullseye-slim
FROM node:22.16.0-bullseye-slim
# Install only runtime dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \

View File

@ -1,4 +1,4 @@
FROM node:22.15.1-alpine AS builder
FROM node:22.16.0-alpine AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@ -7,7 +7,7 @@ FROM node:22.15.1-alpine AS builder
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:22.15.1-alpine
FROM node:22.16.0-alpine
# Install runtime dependencies
RUN apk add --no-cache su-exec shadow

View File

@ -11,7 +11,7 @@
"@types/archiver": "6.0.3",
"@types/better-sqlite3": "7.6.13",
"@types/cls-hooked": "4.3.9",
"@types/compression": "1.7.5",
"@types/compression": "1.8.0",
"@types/cookie-parser": "1.4.8",
"@types/debounce": "1.2.4",
"@types/ejs": "3.1.5",
@ -88,7 +88,7 @@
"multer": "2.0.0",
"normalize-strings": "1.1.1",
"ollama": "0.5.15",
"openai": "4.100.0",
"openai": "4.102.0",
"rand-token": "1.0.1",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",

View File

@ -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);
});
@ -240,7 +274,7 @@ second line 2</code></pre><ul><li>Hello</li><li>world</li></ul><ol><li>Hello</li
});
it("imports todo lists properly", () => {
const input = trimIndentation`\
const input = trimIndentation`\
- [x] Hello
- [ ] World`;
const expected = `<ul class="todo-list"><li><label class="todo-list__label"><input type="checkbox" checked="checked" disabled="disabled"><span class="todo-list__label__description">Hello</span></label></li><li><label class="todo-list__label"><input type="checkbox" disabled="disabled"><span class="todo-list__label__description">World</span></label></li></ul>`;

View File

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

View File

@ -22,6 +22,7 @@
* [Incorrect import of multiple inline math](https://github.com/TriliumNext/Notes/pull/1906) by @SiriusXT
* [Random EPERM: operation not permitted on Windows](https://github.com/TriliumNext/Notes/issues/249)
* [The update button is sometimes blank](https://github.com/TriliumNext/Notes/pull/1975) by @SiriusXT
* [Unable to handle multi line mathematical formulas when importing markdown](https://github.com/TriliumNext/Notes/pull/1984) by @SiriusXT
## ✨ Improvements

View File

@ -27,17 +27,17 @@
"private": true,
"devDependencies": {
"@electron/rebuild": "4.0.1",
"@nx/devkit": "21.1.0",
"@nx/esbuild": "21.1.0",
"@nx/eslint": "21.1.0",
"@nx/eslint-plugin": "21.1.0",
"@nx/express": "21.1.0",
"@nx/js": "21.1.0",
"@nx/node": "21.1.0",
"@nx/playwright": "21.1.0",
"@nx/vite": "21.1.0",
"@nx/web": "21.1.0",
"@nx/webpack": "21.1.0",
"@nx/devkit": "21.1.1",
"@nx/esbuild": "21.1.1",
"@nx/eslint": "21.1.1",
"@nx/eslint-plugin": "21.1.1",
"@nx/express": "21.1.1",
"@nx/js": "21.1.1",
"@nx/node": "21.1.1",
"@nx/playwright": "21.1.1",
"@nx/vite": "21.1.1",
"@nx/web": "21.1.1",
"@nx/webpack": "21.1.1",
"@playwright/test": "^1.36.0",
"@svgr/webpack": "^8.0.1",
"@swc-node/register": "~1.10.0",
@ -59,7 +59,7 @@
"jiti": "2.4.2",
"jsdom": "~26.1.0",
"jsonc-eslint-parser": "^2.1.0",
"nx": "21.1.0",
"nx": "21.1.1",
"react-refresh": "^0.17.0",
"swc-loader": "0.2.6",
"tslib": "^2.3.0",

View File

@ -113,6 +113,7 @@ export default class AdmonitionUI extends Plugin {
model: new ViewModel({
commandParam: type,
label: admonition.title,
class: `ck-tn-admonition-option ck-tn-admonition-${type}`,
role: 'menuitemradio',
withText: true
})

897
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff