mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 10:02:59 +08:00
Merge remote-tracking branch 'origin/develop' into feature/server_esbuild
This commit is contained in:
commit
edc180189c
@ -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",
|
||||
|
@ -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"; }
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 \
|
||||
|
@ -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
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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>`;
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
24
package.json
24
package.json
@ -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",
|
||||
|
@ -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
897
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user