Compare commits

...

103 Commits

Author SHA1 Message Date
Elian Doran
455edbfb5d chore(server): remove runtime from login 2026-01-07 23:31:28 +02:00
Elian Doran
7288b66d27 chore(client): address requested changes 2026-01-07 23:04:34 +02:00
Elian Doran
3d72ec80bb refactor(client): get rid of any 2026-01-07 22:01:35 +02:00
Elian Doran
f2a74df511 feat(client): use hashes for assets 2026-01-07 21:49:05 +02:00
Elian Doran
68c6052d10 chore(client): remove useless manual chunk 2026-01-07 21:39:35 +02:00
Elian Doran
c4edb56bd4 fix(server): not starting due to serving of assets 2026-01-07 21:38:44 +02:00
Elian Doran
b6a3fe7cfb chore(client): get rid of translation issue 2026-01-07 21:18:09 +02:00
Elian Doran
7a088c5b7d refactor(client): handle everything in bootstrap 2026-01-07 21:11:38 +02:00
Elian Doran
2e845a9faa refactor(client): get rid of runtime in favor of bootstrap script 2026-01-07 21:08:19 +02:00
Elian Doran
ac3ae0dbbe chore(client): fix type issues 2026-01-07 21:02:27 +02:00
Elian Doran
a3fc13de3a refactor(client): extract bootstrap script into separate file 2026-01-07 21:00:40 +02:00
Elian Doran
ee6cbc710c chore(server): remove font size globs 2026-01-07 20:52:27 +02:00
Elian Doran
18d701525e fix(client): print broken due to lack of query forwarding
; Conflicts:
;	apps/client/src/index.html
2026-01-07 20:52:04 +02:00
Elian Doran
e47c848ec8 chore(server): reintegrate mobile layout 2026-01-07 20:51:33 +02:00
Elian Doran
cd64548299 fix(client): load custom fonts 2026-01-07 20:51:29 +02:00
Elian Doran
8645d053de fix(client): ckeditor theme not loaded properly 2026-01-07 20:51:24 +02:00
Elian Doran
91f2dabed7 Merge remote-tracking branch 'origin/main' into lightweight/bootstrap_ejs
; Conflicts:
;	apps/client/src/widgets/layout/StatusBar.tsx
2026-01-07 20:51:17 +02:00
Elian Doran
716612680d Translations update from Hosted Weblate (#8293) 2026-01-07 19:35:42 +02:00
Michael
3800fb85eb Translated using Weblate (German)
Currently translated at 95.4% (1672 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-01-07 18:32:56 +01:00
Rafa Osuna
d807984be4 Translated using Weblate (Spanish)
Currently translated at 92.7% (1624 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-01-07 18:32:56 +01:00
Giovi
2c92ae8898 Translated using Weblate (Italian)
Currently translated at 100.0% (1751 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-01-07 18:32:55 +01:00
Argann Bonneau
3d8cbc81c4 Translated using Weblate (French)
Currently translated at 94.5% (1656 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/
2026-01-07 18:32:54 +01:00
Yatrik Patel
d747c94450 Translated using Weblate (Hindi)
Currently translated at 3.4% (4 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/hi/
2026-01-07 18:32:53 +01:00
pythaac
a627d1f96e Translated using Weblate (Korean)
Currently translated at 76.3% (116 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/
2026-01-07 18:32:53 +01:00
Yatrik Patel
869db5e478 Translated using Weblate (Hindi)
Currently translated at 0.9% (17 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-07 18:32:52 +01:00
Yatrik Patel
73e94d385e Translated using Weblate (Hindi)
Currently translated at 5.9% (23 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-07 18:32:51 +01:00
Kim Nøglegaard
8f4ebeb335 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-07 18:32:51 +01:00
Yatrik Patel
263ee864be Translated using Weblate (Hindi)
Currently translated at 9.2% (14 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-07 18:32:50 +01:00
Elian Doran
f078732624 fix(text): Title is not selected when creating a note via the launcher (#8292) 2026-01-07 19:32:37 +02:00
SngAbc
fac1f6b16c fix(text): Title is not focused when creating a note via the launcher
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-07 10:33:17 +08:00
SiriusXT
a5841c1423 fix(text): Title is not focused when creating a note via the launcher 2026-01-07 10:11:24 +08:00
Elian Doran
aaca18003d Translations update from Hosted Weblate (#8279) 2026-01-06 13:54:24 +02:00
Kim Nøglegaard
5ec521b024 Translated using Weblate (Norwegian Bokmål)
Currently translated at 68.4% (104 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-06 04:01:53 +01:00
Yatrik Patel
b3c0be7559 Translated using Weblate (Hindi)
Currently translated at 3.0% (12 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-06 04:01:51 +01:00
Máté Zsólya
d52b735b99 Translated using Weblate (Hungarian)
Currently translated at 1.9% (34 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hu/
2026-01-06 04:01:49 +01:00
Yatrik Patel
639b1f2863 Translated using Weblate (Hindi)
Currently translated at 5.9% (9 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-06 04:01:47 +01:00
Elian Doran
7f2cc885fe Feat(math): Improve legacy math input with MathLive (#7842) 2026-01-06 00:12:38 +02:00
Elian Doran
19a365a370 fix(sql_console): cannot copy table data (#8268) 2026-01-06 00:10:11 +02:00
Elian Doran
9a50da328e chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.9 (#8265) 2026-01-05 23:53:05 +02:00
Elian Doran
181e36a7c1 Merge remote-tracking branch 'origin/main' into Meinzzzz/main
; Conflicts:
;	.gitignore
2026-01-05 23:46:12 +02:00
Elian Doran
178508d245 Merge branch 'main' into fix/sql_select_text 2026-01-05 23:43:29 +02:00
Elian Doran
d132d084cf Merge branch 'main' into renovate/rollup-plugin-webpack-stats-2.x 2026-01-05 23:43:06 +02:00
Elian Doran
494b55d685 fix(ckeditor): missing pl locale 2026-01-05 23:39:36 +02:00
Elian Doran
51513d3779 fix(status_bar): count not refreshing properly after change 2026-01-05 21:03:32 +02:00
Elian Doran
688d197472 chore(client): set up body classes 2026-01-05 11:55:51 +02:00
Elian Doran
b745fb476e chore(client): get icons to load 2026-01-05 11:50:21 +02:00
Elian Doran
047b5a85d2 chore(client): load stylesheets 2026-01-05 11:48:19 +02:00
Elian Doran
370a0c6a05 feat(client): get desktop to start 2026-01-05 11:40:53 +02:00
Elian Doran
0d4558fee1 feat(client): get glob to be populated 2026-01-05 11:37:03 +02:00
Elian Doran
76526e0a96 feat(server): bootstrap route 2026-01-05 11:22:10 +02:00
Elian Doran
70093e0a7d feat(server): render static Vite HTML 2026-01-05 11:07:40 +02:00
SngAbc
458398f2ca Merge branch 'main' into fix/sql_select_text 2026-01-05 13:51:45 +08:00
SngAbc
7a6cc4f51e fix(sql_console): cannot copy table data
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-05 12:16:16 +08:00
SiriusXT
f4ccce7de5 fix(sql_console): cannot copy table data 2026-01-05 11:23:50 +08:00
renovate[bot]
f8b5417d6c chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.9 2026-01-05 01:03:52 +00:00
meinzzzz
87ab41c80c Fix shift+tab behavior in MathInputView 2025-12-23 18:02:40 +01:00
Meinzzzz
d2391f94c0 Fix offline math rendering by bundling local fonts 2025-12-15 21:32:50 +01:00
Meinzzzz
050ddb8c55 Improve css to fix tooltips 2025-12-15 20:17:58 +01:00
Meinzzzz
bc23e0984a Undo unnecessary formatting changes 2025-12-14 22:00:56 +01:00
Meinzzzz
07de353207 Adding comments and improving code quality in math input views 2025-12-14 20:21:42 +01:00
Meinzzzz
c02491d2e6 Remove unnecessary any casts in math plugin 2025-12-12 23:09:20 +01:00
Meinzzzz
a6ede8f905 Improve mathinputview 2025-12-12 21:33:59 +01:00
Meinzzzz
22941a9ce0 Fix sync issues 2025-12-12 19:48:09 +01:00
Meinzzzz
633a09d414 Fix sync bug 2025-12-11 23:06:13 +01:00
Meinzzzz
29f0881c5a Fix clicking issue in Mathfield 2025-12-10 22:44:02 +01:00
Meinzzzz
60debca37b Improve comments 2025-12-10 18:36:34 +01:00
Meinzzzz
30ea81d0fb Improve virtual keyboard logic and fix Tab issues 2025-12-08 22:59:08 +01:00
Meinzzzz
b1d92c4fe6 Fix Tab issues 2025-12-08 22:39:12 +01:00
Meinzzzz
70f46de2d8 MathLive virtual keyboard only appears when focusing the mathfield 2025-12-08 20:30:07 +01:00
Meinzzzz
f1b2d0b870 Increas Mathfield font size and ensure virtual keyboard appears above CKEditor 2025-12-08 20:22:52 +01:00
Meinzzzz
8a385972fc Close Virtual Keyboard when Mathinput is closed 2025-12-08 18:49:06 +01:00
Meinzzzz
28dd85c1d1 Merge upstream changes and resolve conflicts 2025-12-07 23:51:41 +01:00
meinzzzz
827c8e0e72 Refactor: Combine MathLive and LaTeX inputs into one single component 2025-12-07 23:19:48 +01:00
meinzzzz
162c076a14 Improve MathLive integration and lazy loading 2025-12-02 22:30:37 +01:00
meinzzzz
9386465de7 Added mathrender error class for better error handling in math rendering 2025-12-02 22:29:20 +01:00
meinzzzz
acca22f3a1 Improve Synchronization Between Mathlive and rawlatex input 2025-12-02 22:28:16 +01:00
meinzzzz
f8d84814e0 Fix differential d problems 2025-11-26 23:02:34 +01:00
meinzzzz
c46cf41842 Small improvements 2025-11-26 22:48:57 +01:00
meinzzzz
64ab1c4116 Imrovement for Latex 2025-11-26 22:29:29 +01:00
meinzzzz
a6de1041c7 Fix bug in math rendering where old content was not cleared 2025-11-26 21:59:33 +01:00
meinzzzz
c8d34e65ea Improve max window size 2025-11-26 21:49:09 +01:00
meinzzzz
51db729546 Improve and simplify Mathfield integration 2025-11-25 23:27:06 +01:00
meinzzzz
d2052ad236 Disable mathlive sound effects 2025-11-24 21:51:59 +01:00
meinzzzz
9c4301467f Remove unused icons from ckeditor5-math package 2025-11-24 19:46:04 +01:00
meinzzzz
e7355dc0e4 remove gitignore unneccesary changes 2025-11-24 18:43:52 +01:00
meinzzzz
4110fec94f Removed unnecessary declare keyboard 2025-11-24 18:28:59 +01:00
meinzzzz
d5e601eae9 Simpliyfied resize logic for math input form and improved css 2025-11-24 17:56:18 +01:00
meinzzzz
4f044c4a57 Use icons form CKEditor5 icons, instead of testing icons. 2025-11-23 22:43:07 +01:00
meinzzzz
5821c350e1 Fixing class property initialization order 2025-11-23 17:58:51 +01:00
meinzzzz
edba8188fe Fix dark selection colors in MathLive math-field 2025-11-23 13:44:28 +01:00
meinzzzz
1471a72633 refactor: avoid recursive updates in mathLiveInput by normalizing value before updateing 2025-11-23 13:34:22 +01:00
meinzzzz
56834cb88a Improve MathLive and Raw LaTeX input views to propagate mousedown events 2025-11-23 13:29:26 +01:00
meinzzzz
a0f16f9184 Fix typos in mathform.css 2025-11-23 13:09:56 +01:00
meinzzzz
de80eb4806 Improve mathform.css styling for better visual integration 2025-11-22 22:42:34 +01:00
meinzzzz
48a4b81fbe remove automated screenshot files 2025-11-22 21:40:55 +01:00
meinzzzz
e225794f72 Better window focus handling in MathFormView 2025-11-22 21:35:37 +01:00
meinzzzz
4eef30f8b5 Fix names 2025-11-22 00:20:20 +01:00
meinzzzz
569b09609d Remove mathlive dependency and chunking 2025-11-22 00:01:14 +01:00
meinzzzz
39838c25c2 Fixed chaching problems 2025-11-21 23:50:49 +01:00
meinzzzz
49e90c08a9 Better Names for Math UI Components 2025-11-20 22:45:21 +01:00
meinzzzz
e777b06fb8 Math 2025-11-20 18:53:39 +01:00
meinzzzz
497ec2ac74 Merge branch 'main' of https://github.com/Meinzzzz/Trilium-Mathlive 2025-11-20 18:00:18 +01:00
meinzzzz
c5d282d203 Mathlive 2025-11-20 00:09:10 +01:00
43 changed files with 1246 additions and 679 deletions

2
.gitignore vendored
View File

@@ -51,4 +51,4 @@ upload
# docs
site/
apps/*/coverage
scripts/translation/.language*.json
scripts/translation/.language*.json

View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<title>Trilium Notes</title>
</head>
<body id="trilium-app">
<noscript>Trilium requires JavaScript to be enabled.</noscript>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
<!-- Required to match the PWA's top bar color with the theme -->
<!-- This works even when the user directly changes --root-background in CSS -->
<div id="background-color-tracker" style="position: absolute; visibility: hidden; color: var(--root-background); transition: color 1ms;"></div>
<script src="./index.ts" type="module"></script>
<!-- Required for correct loading of scripts in Electron -->
<script>
if (typeof module === 'object') {window.module = module; module = undefined;}
</script>
</body>
</html>

View File

@@ -0,0 +1,110 @@
async function bootstrap() {
showSplash();
await setupGlob();
await Promise.all([
initJQuery(),
loadBootstrapCss()
]);
loadStylesheets();
loadIcons();
setBodyAttributes();
await loadScripts();
hideSplash();
}
async function initJQuery() {
const $ = (await import("jquery")).default;
window.$ = $;
window.jQuery = $;
}
async function setupGlob() {
const response = await fetch(`/bootstrap${window.location.search}`);
const json = await response.json();
window.global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.glob = {
...json,
activeDialog: null
};
}
async function loadBootstrapCss() {
// We have to selectively import Bootstrap CSS based on text direction.
if (glob.isRtl) {
await import("bootstrap/dist/css/bootstrap.rtl.min.css");
} else {
await import("bootstrap/dist/css/bootstrap.min.css");
}
}
function loadStylesheets() {
const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
const cssToLoad: string[] = [];
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
cssToLoad.push(`api/fonts`);
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
if (themeCssUrl) {
cssToLoad.push(themeCssUrl);
}
if (themeUseNextAsBase === "next") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
} else if (themeUseNextAsBase === "next-dark") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
} else if (themeUseNextAsBase === "next-light") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
}
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
for (const href of cssToLoad) {
const linkEl = document.createElement("link");
linkEl.href = href;
linkEl.rel = "stylesheet";
document.head.appendChild(linkEl);
}
}
function loadIcons() {
const styleEl = document.createElement("style");
styleEl.innerText = window.glob.iconPackCss;
document.head.appendChild(styleEl);
}
function setBodyAttributes() {
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
const classesToSet = [
device,
`heading-style-${headingStyle}`,
`layout-${layoutOrientation}`,
`platform-${platform}`,
isElectron && "electron",
hasNativeTitleBar && "native-titlebar",
hasBackgroundEffects && "background-effects"
].filter(Boolean) as string[];
for (const classToSet of classesToSet) {
document.body.classList.add(classToSet);
}
document.body.lang = currentLocale.id;
document.body.dir = currentLocale.rtl ? "rtl" : "ltr";
}
async function loadScripts() {
if (glob.device === "mobile") {
await import("./mobile.js");
} else {
await import("./desktop.js");
}
}
function showSplash() {
// hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide <noscript>
document.body.style.display = "none";
}
function hideSplash() {
document.body.style.display = "block";
}
bootstrap();

View File

@@ -1,3 +1,7 @@
import "bootstrap/dist/css/bootstrap.min.css";
// @ts-ignore - module = undefined
// Required for correct loading of scripts in Electron
if (typeof module === 'object') {window.module = module; module = undefined;}
document.body.style.display = "block";

View File

@@ -1,15 +0,0 @@
import $ from "jquery";
async function loadBootstrap() {
if (document.body.dir === "rtl") {
await import("bootstrap/dist/css/bootstrap.rtl.min.css");
} else {
await import("bootstrap/dist/css/bootstrap.min.css");
}
}
(window as any).$ = $;
(window as any).jQuery = $;
await loadBootstrap();
$("body").show();

View File

@@ -25,7 +25,8 @@
},
"widget-list-error": {
"title": "Abruf der Liste von Widgets vom Server ist fehlgeschlagen"
}
},
"open-script-note": "Script-Notiz öffnen"
},
"add_link": {
"add_link": "Link hinzufügen",
@@ -208,7 +209,8 @@
"info": {
"modalTitle": "Infonachricht",
"closeButton": "Schließen",
"okButton": "OK"
"okButton": "OK",
"copy_to_clipboard": "In die Zwischenablage kopieren"
},
"jump_to_note": {
"search_button": "Suche im Volltext",
@@ -695,7 +697,9 @@
"export_as_image": "Als Bild exportieren",
"export_as_image_png": "PNG (Raster)",
"export_as_image_svg": "SVG (Vektor)",
"note_map": "Notizen Karte"
"note_map": "Notizen Karte",
"view_revisions": "Notizrevisionen",
"advanced": "Erweitert"
},
"onclick_button": {
"no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"

View File

@@ -162,7 +162,8 @@
"other": "Otro",
"quickSearch": "centrarse en la entrada de búsqueda rápida",
"inPageSearch": "búsqueda en la página",
"title": "Hoja de ayuda"
"title": "Hoja de ayuda",
"editShortcuts": "Editar atajos de teclado"
},
"import": {
"importIntoNote": "Importar a nota",

View File

@@ -21,7 +21,7 @@
},
"bundle-error": {
"title": "Echec du chargement d'un script personnalisé",
"message": "Le script de la note avec l'ID \"{{id}}\", intitulé \"{{title}}\" n'a pas pu être exécuté à cause de\n\n{{message}}"
"message": "Le script n'a pas pu être exécuté à cause de\n\n{{message}}"
},
"widget-list-error": {
"title": "Impossible d'obtenir la liste des widgets depuis le serveur"

View File

@@ -31,5 +31,17 @@
},
"add_link": {
"note": "नोट"
},
"bulk_actions": {
"other": "अन्य"
},
"clone_to": {
"search_for_note_by_its_name": "नोट क नाम से नोट खोजें"
},
"confirm": {
"also_delete_note": "नोट भी डिलीट करें"
},
"delete_notes": {
"delete_notes_preview": "नोट्स प्रिव्यू डिलीट करें"
}
}

View File

@@ -21,7 +21,13 @@
},
"bundle-error": {
"title": "Nem sikerült betölteni az egyéni szkriptet",
"message": "A(z) \"{{id}}\" azonosítójú, \"{{title}}\" című jegyzetből származó szkript nem hajtható végre a következő ok miatt:\n\n{{message}}"
"message": "A skript nem hajtható végre a következő ok miatt:\n\n{{message}}"
},
"widget-list-error": {
"title": "A Widget-ek letöltése sikertelen volt"
},
"widget-render-error": {
"title": "Nem sikerült renderelni a React widget-et"
}
},
"add_link": {

View File

@@ -1895,7 +1895,11 @@
"create-child-note": "Crea nota figlio",
"unhoist": "Sganciare",
"toggle-sidebar": "Attiva/disattiva la barra laterale",
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione."
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione.",
"clone-indicator-tooltip": "Questa nota ha {{- count}} genitori: {{- parents}}",
"clone-indicator-tooltip-single": "Questa nota è stata clonata (1 genitore aggiuntivo: {{- parent}})",
"shared-indicator-tooltip": "Questa nota è condivisa pubblicamente",
"shared-indicator-tooltip-with-url": "Questa nota è condivisa pubblicamente all'indirizzo: {{- url}}"
},
"title_bar_buttons": {
"window-on-top": "Mantieni la finestra in primo piano"
@@ -2200,7 +2204,14 @@
"execute_sql_description": "Questa nota è una nota SQL. Clicca per eseguire la query SQL.",
"shared_copy_to_clipboard": "Copia link negli appunti",
"shared_open_in_browser": "Apri il link nel browser",
"shared_unshare": "Rimuovi condivisione"
"shared_unshare": "Rimuovi condivisione",
"save_status_saved": "Salvato",
"save_status_saving": "Salvataggio in corso...",
"save_status_unsaved": "Non salvato",
"save_status_error": "Salvataggio non riuscito",
"save_status_saving_tooltip": "Le modifiche sono state salvate.",
"save_status_unsaved_tooltip": "Ci sono modifiche non salvate. Verranno salvate automaticamente tra un attimo.",
"save_status_error_tooltip": "Si è verificato un errore durante il salvataggio della nota. Se possibile, prova a copiare il contenuto della nota altrove e a ricaricare l'applicazione."
},
"breadcrumb": {
"workspace_badge": "Area di lavoro",
@@ -2243,5 +2254,18 @@
"empty_button": "Nascondi il pannello",
"toggle": "Attiva/disattiva pannello destro",
"custom_widget_go_to_source": "Vai al codice sorgente"
},
"pdf": {
"attachments_one": "{{count}} allegato",
"attachments_many": "{{count}} allegati",
"attachments_other": "{{count}} allegati",
"layers_one": "{{count}} livello",
"layers_many": "{{count}} livelli",
"layers_other": "{{count}} livelli",
"pages_one": "{{count}} pagina",
"pages_many": "{{count}} pagine",
"pages_other": "{{count}} pagine",
"pages_alt": "Pagina {{pageNumber}}",
"pages_loading": "Caricamento in corso..."
}
}

View File

@@ -1,4 +1,4 @@
import { IconRegistry } from "@triliumnext/commons";
import { IconRegistry, Locale } from "@triliumnext/commons";
import appContext, { AppContext } from "./components/app_context";
import type FNote from "./entities/fnote";
@@ -47,14 +47,25 @@ interface CustomGlobals {
platform?: typeof process.platform;
linter: typeof lint;
hasNativeTitleBar: boolean;
hasBackgroundEffects: boolean;
isElectron: boolean;
isRtl: boolean;
iconRegistry: IconRegistry;
themeCssUrl: string;
themeUseNextAsBase?: "next" | "next-light" | "next-dark";
iconPackCss: string;
headingStyle: "plain" | "underline" | "markdown";
layoutOrientation: "vertical" | "horizontal";
currentLocale: Locale;
}
type RequireMethod = (moduleName: string) => any;
declare global {
interface Window {
$: JQueryStatic;
jQuery: JQueryStatic;
logError(message: string);
logInfo(message: string);

View File

@@ -5,7 +5,7 @@ import { Dropdown as BootstrapDropdown } from "bootstrap";
import clsx from "clsx";
import { type ComponentChildren, RefObject } from "preact";
import { createPortal } from "preact/compat";
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { CommandNames } from "../../components/app_context";
import NoteContext from "../../components/note_context";
@@ -338,15 +338,19 @@ interface AttributesProps extends StatusBarContext {
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
const [ count, setCount ] = useState(note.attributes.length);
const getAttributeCount = useCallback((note: FNote) => {
return note.getAttributes().filter(a => !a.isAutoLink).length;
}, []);
// React to note changes.
useEffect(() => {
setCount(note.getAttributes().filter(a => !a.isAutoLink).length);
}, [ note ]);
setCount(getAttributeCount(note));
}, [ note, getAttributeCount ]);
// React to changes in count.
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
setCount(note.attributes.length);
setCount(getAttributeCount(note));
}
}));

View File

@@ -23,7 +23,7 @@ export default function SqlResults() {
{t("sql_result.no_rows")}
</Alert>
) : (
<div class="sql-console-result-container">
<div className="sql-console-result-container selectable-text">
{results?.map(rows => {
// inserts, updates
if (typeof rows === "object" && !Array.isArray(rows)) {

View File

@@ -286,7 +286,7 @@ function useWatchdogCrashHandling() {
const currentState = watchdog.state;
logInfo(`CKEditor state changed to ${currentState}`);
if (currentState === "ready") {
if (currentState === "ready" && hasCrashed.current) {
hasCrashed.current = false;
watchdog.editor?.focus();
}

View File

@@ -1,9 +1,9 @@
/// <reference types='vitest' />
import { join, resolve } from 'path';
import { defineConfig, type Plugin } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy'
import webpackStatsPlugin from 'rollup-plugin-webpack-stats';
import preact from "@preact/preset-vite";
import { join } from 'path';
import webpackStatsPlugin from 'rollup-plugin-webpack-stats';
import { defineConfig } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy'
const assets = [ "assets", "stylesheets", "fonts", "translations" ];
@@ -70,21 +70,15 @@ export default defineConfig(() => ({
sourcemap: false,
rollupOptions: {
input: {
desktop: join(__dirname, "src", "desktop.ts"),
mobile: join(__dirname, "src", "mobile.ts"),
index: join(__dirname, "src", "index.html"),
login: join(__dirname, "src", "login.ts"),
setup: join(__dirname, "src", "setup.ts"),
set_password: join(__dirname, "src", "set_password.ts"),
runtime: join(__dirname, "src", "runtime.ts"),
print: join(__dirname, "src", "print.tsx")
},
output: {
entryFileNames: "src/[name].js",
chunkFileNames: "src/[name].js",
assetFileNames: "src/[name].[ext]",
manualChunks: {
"ckeditor5": [ "@triliumnext/ckeditor5" ],
"boxicons": [ "../../node_modules/boxicons/css/boxicons.min.css" ]
"ckeditor5": [ "@triliumnext/ckeditor5" ]
},
},
onwarn(warning, rollupWarn) {

View File

@@ -220,7 +220,6 @@
"password-confirmation": "Password confirmation",
"button": "Set password"
},
"javascript-required": "Trilium requires JavaScript to be enabled.",
"setup": {
"heading": "Trilium Notes setup",
"new-document": "I'm a new user, and I want to create a new Trilium document for my notes",

View File

@@ -10,6 +10,18 @@
"creating-and-moving-notes": "नोट्स बनाना और स्थानांतरित करना",
"move-note-up": "नोट को ऊपर ले जाएं",
"move-note-down": "नोट को नीचे ले जाएं",
"note-clipboard": "नोट क्लिपबोर्ड"
"note-clipboard": "नोट क्लिपबोर्ड",
"duplicate-subtree": "डुप्लिकेट सबट्री",
"open-new-tab": "नया टैब खोलें",
"second-tab": "लिस्ट में दूसरी टैब एक्टिवेट करें",
"third-tab": "लिस्ट में तीसरी टैब एक्टिवेट करें",
"fourth-tab": "लिस्ट में चौथी टैब एक्टिवेट करें",
"sixth-tab": "लिस्ट में छठी टैब एक्टिवेट करें",
"seventh-tab": "लिस्ट में सातवीं टैब एक्टिवेट करें",
"eight-tab": "लिस्ट में आठवीं टैब एक्टिवेट करें",
"ninth-tab": "लिस्ट में नौवीं टैब एक्टिवेट करें",
"last-tab": "लिस्ट में आखिरी टैब एक्टिवेट करें",
"show-sql-console": "\"SQL कंसोल\" पेज खोलें",
"show-backend-log": "\"बैकेंड लॉग\" पेज खोलें"
}
}

View File

@@ -1,60 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<title>Trilium Notes</title>
<style id="trilium-icon-packs">
<%- iconPackCss %>
</style>
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
</head>
<body
id="trilium-app"
class="desktop heading-style-<%= headingStyle %> layout-<%= layoutOrientation %> platform-<%= platform %> <%= isElectron ? 'electron' : '' %> <%= hasNativeTitleBar ? 'native-titlebar' : '' %> <%= hasBackgroundEffects ? 'background-effects' : '' %>"
lang="<%= currentLocale.id %>" dir="<%= currentLocale.rtl ? 'rtl' : 'ltr' %>"
>
<noscript><%= t("javascript-required") %></noscript>
<script>
// hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide <noscript>
document.getElementsByTagName("body")[0].style.display = "none";
</script>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
<%- include("./partials/windowGlobal.ejs", locals) %>
<!-- Required for match the PWA's top bar color with the theme -->
<!-- This works even when the user directly changes --root-background in CSS -->
<div id="background-color-tracker" style="position: absolute; visibility: hidden; color: var(--root-background); transition: color 1ms;"></div>
<!-- Required for correct loading of scripts in Electron -->
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<link href="<%= assetPath %>/stylesheets/ckeditor-theme.css" rel="stylesheet">
<link href="api/fonts" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet">
<% if (themeCssUrl) { %>
<link href="<%= themeCssUrl %>" rel="stylesheet">
<% } %>
<% if (themeUseNextAsBase === "next") { %>
<link href="<%= assetPath %>/stylesheets/theme-next.css" rel="stylesheet">
<% } else if (themeUseNextAsBase === "next-dark") { %>
<link href="<%= assetPath %>/stylesheets/theme-next-dark.css" rel="stylesheet">
<% } else if (themeUseNextAsBase === "next-light") { %>
<link href="<%= assetPath %>/stylesheets/theme-next-light.css" rel="stylesheet">
<% } %>
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
<script src="<%= appPath %>/desktop.js" crossorigin type="module"></script>
</body>
</html>

View File

@@ -15,7 +15,6 @@
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-light.css">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-next.css">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/style.css">
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
</head>
<body lang="<%= currentLocale.id %>" dir="<%= currentLocale.rtl ? 'rtl' : 'ltr' %>">
<div class="container login-page">

View File

@@ -1,137 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
<meta name="theme-color" content="#fff">
<title>Trilium Notes</title>
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<style>
.lds-roller {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-roller div {
animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
transform-origin: 40px 40px;
}
.lds-roller div:after {
content: " ";
display: block;
position: absolute;
width: 7px;
height: 7px;
border-radius: 50%;
background: #000;
margin: -4px 0 0 -4px;
}
.lds-roller div:nth-child(1) {
animation-delay: -0.036s;
}
.lds-roller div:nth-child(1):after {
top: 63px;
left: 63px;
}
.lds-roller div:nth-child(2) {
animation-delay: -0.072s;
}
.lds-roller div:nth-child(2):after {
top: 68px;
left: 56px;
}
.lds-roller div:nth-child(3) {
animation-delay: -0.108s;
}
.lds-roller div:nth-child(3):after {
top: 71px;
left: 48px;
}
.lds-roller div:nth-child(4) {
animation-delay: -0.144s;
}
.lds-roller div:nth-child(4):after {
top: 72px;
left: 40px;
}
.lds-roller div:nth-child(5) {
animation-delay: -0.18s;
}
.lds-roller div:nth-child(5):after {
top: 71px;
left: 32px;
}
.lds-roller div:nth-child(6) {
animation-delay: -0.216s;
}
.lds-roller div:nth-child(6):after {
top: 68px;
left: 24px;
}
.lds-roller div:nth-child(7) {
animation-delay: -0.252s;
}
.lds-roller div:nth-child(7):after {
top: 63px;
left: 17px;
}
.lds-roller div:nth-child(8) {
animation-delay: -0.288s;
}
.lds-roller div:nth-child(8):after {
top: 56px;
left: 12px;
}
@keyframes lds-roller {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<style id="trilium-icon-packs">
<%- iconPackCss %>
</style>
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
</head>
<body
class="mobile heading-style-<%= headingStyle %>"
lang="<%= currentLocale.id %>" dir="<%= currentLocale.rtl ? 'rtl' : 'ltr' %>"
>
<noscript><%= t("javascript-required") %></noscript>
<div id="context-menu-cover"></div>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
<%- include("./partials/windowGlobal.ejs", locals) %>
<script src="<%= appPath %>/mobile.js" crossorigin type="module"></script>
<link href="api/fonts" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/ckeditor-theme.css" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet">
<% if (themeCssUrl) { %>
<link href="<%= themeCssUrl %>" rel="stylesheet">
<% } %>
<% if (themeUseNextAsBase === "next") { %>
<link href="<%= assetPath %>/stylesheets/theme-next.css" rel="stylesheet">
<% } else if (themeUseNextAsBase === "next-dark") { %>
<link href="<%= assetPath %>/stylesheets/theme-next-dark.css" rel="stylesheet">
<% } else if (themeUseNextAsBase === "next-light") { %>
<link href="<%= assetPath %>/stylesheets/theme-next-light.css" rel="stylesheet">
<% } %>
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
</body>
</html>

View File

@@ -1,25 +0,0 @@
<script type="text/javascript">
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.glob = {
device: "<%= device %>",
baseApiUrl: "<%= baseApiUrl %>",
activeDialog: null,
maxEntityChangeIdAtLoad: <%= maxEntityChangeIdAtLoad %>,
maxEntityChangeSyncIdAtLoad: <%= maxEntityChangeSyncIdAtLoad %>,
instanceName: '<%= instanceName %>',
csrfToken: '<%= csrfToken %>',
isDev: <%= isDev %>,
appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>,
isMainWindow: <%= isMainWindow %>,
isProtectedSessionAvailable: <%= isProtectedSessionAvailable %>,
triliumVersion: "<%= triliumVersion %>",
assetPath: "<%= assetPath %>",
appPath: "<%= appPath %>",
platform: "<%= platform %>",
hasNativeTitleBar: <%= hasNativeTitleBar %>,
TRILIUM_SAFE_MODE: <%= !!process.env.TRILIUM_SAFE_MODE %>,
isRtl: <%= !!currentLocale.rtl %>,
iconRegistry: <%- JSON.stringify(iconRegistry) %>
};
</script>

View File

@@ -1,10 +1,13 @@
import express from "express";
import rateLimit from "express-rate-limit";
import { existsSync } from "fs";
import path from "path";
import type serveStatic from "serve-static";
import { assetUrlFragment } from "../services/asset_path.js";
import auth from "../services/auth.js";
import { getResourceDir, isDev } from "../services/utils.js";
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOptions<express.Response<unknown, Record<string, unknown>>>) => {
if (!isDev) {
@@ -20,19 +23,29 @@ async function register(app: express.Application) {
const srcRoot = path.join(__dirname, "..", "..");
const resourceDir = getResourceDir();
const rootLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
if (process.env.NODE_ENV === "development") {
const { createServer: createViteServer } = await import("vite");
const clientDir = path.join(srcRoot, "../client");
const vite = await createViteServer({
server: { middlewareMode: true },
appType: "custom",
appType: "spa",
cacheDir: path.join(srcRoot, "../../.cache/vite"),
base: `/${assetUrlFragment}/`,
root: clientDir,
css: { devSourcemap: true }
});
app.use(`/${assetUrlFragment}/`, (req, res, next) => {
req.url = `/${assetUrlFragment}${req.url}`;
app.use(`/${assetUrlFragment}/`, vite.middlewares);
app.get(`/`, [ rootLimiter, auth.checkAuth, csrfMiddleware ], (req, res, next) => {
req.url = `/${assetUrlFragment}/src/index.html`;
vite.middlewares(req, res, next);
});
app.get(`/index.ts`, (req, res, next) => {
req.url = `/${assetUrlFragment}/src/index.ts`;
vite.middlewares(req, res, next);
});
app.use(`/node_modules/@excalidraw/excalidraw/dist/prod`, persistentCacheStatic(path.join(srcRoot, "../../node_modules/@excalidraw/excalidraw/dist/prod")));
@@ -42,7 +55,14 @@ async function register(app: express.Application) {
throw new Error(`Public directory is missing at: ${path.resolve(publicDir)}`);
}
app.use(`/${assetUrlFragment}/src`, persistentCacheStatic(path.join(publicDir, "src")));
app.get(`/`, [ rootLimiter, auth.checkAuth, csrfMiddleware ], (_, res) => {
// We force the page to not be cached since on mobile the CSRF token can be
// broken when closing the browser and coming back in to the page.
// The page is restored from cache, but the API call fail.
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.sendFile(path.join(publicDir, "src", "index.html"));
});
app.use("/assets", persistentCacheStatic(path.join(publicDir, "assets")));
app.use(`/${assetUrlFragment}/stylesheets`, persistentCacheStatic(path.join(publicDir, "stylesheets")));
app.use(`/${assetUrlFragment}/fonts`, persistentCacheStatic(path.join(publicDir, "fonts")));
app.use(`/${assetUrlFragment}/translations/`, persistentCacheStatic(path.join(publicDir, "translations")));

View File

@@ -15,10 +15,10 @@ import sql from "../services/sql.js";
import { isDev, isElectron, isWindows11 } from "../services/utils.js";
import { generateToken as generateCsrfToken } from "./csrf_protection.js";
type View = "desktop" | "mobile" | "print";
function index(req: Request, res: Response) {
const view = getView(req);
export function bootstrap(req: Request, res: Response) {
const options = optionService.getOptionMap();
//'overwrite' set to false (default) => the existing token will be re-used and validated
@@ -26,17 +26,14 @@ function index(req: Request, res: Response) {
const csrfToken = generateCsrfToken(req, res, false, false);
log.info(`CSRF token generation: ${csrfToken ? "Successful" : "Failed"}`);
// We force the page to not be cached since on mobile the CSRF token can be
// broken when closing the browser and coming back in to the page.
// The page is restored from cache, but the API call fail.
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
const view = getView(req);
const theme = options.theme;
const themeNote = attributeService.getNoteWithLabel("appTheme", theme);
const nativeTitleBarVisible = options.nativeTitleBarVisible === "true";
const iconPacks = getIconPacks();
const currentLocale = getCurrentLocale();
res.render(view, {
res.send({
device: view,
csrfToken,
themeCssUrl: getThemeCssUrl(theme, themeNote),
@@ -47,9 +44,6 @@ function index(req: Request, res: Response) {
isElectron,
hasNativeTitleBar: isElectron && nativeTitleBarVisible,
hasBackgroundEffects: isElectron && isWindows11 && !nativeTitleBarVisible && options.backgroundEffects === "true",
mainFontSize: parseInt(options.mainFontSize),
treeFontSize: parseInt(options.treeFontSize),
detailFontSize: parseInt(options.detailFontSize),
maxEntityChangeIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes"),
maxEntityChangeSyncIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1"),
instanceName: config.General ? config.General.instanceName : null,
@@ -61,14 +55,16 @@ function index(req: Request, res: Response) {
assetPath,
appPath,
baseApiUrl: 'api/',
currentLocale: getCurrentLocale(),
currentLocale,
isRtl: !!currentLocale.rtl,
iconPackCss: iconPacks
.map(p => generateCss(p, p.builtin
? `${assetPath}/fonts/${p.fontAttachmentId}.${MIME_TO_EXTENSION_MAPPINGS[p.fontMime]}`
: `api/attachments/download/${p.fontAttachmentId}`))
.filter(Boolean)
.join("\n\n"),
iconRegistry: generateIconRegistry(iconPacks)
iconRegistry: generateIconRegistry(iconPacks),
TRILIUM_SAFE_MODE: !!process.env.TRILIUM_SAFE_MODE
});
}
@@ -133,7 +129,3 @@ function getThemeCssUrl(theme: string, themeNote: BNote | null) {
function getAppCssNoteIds() {
return attributeService.getNotesWithLabel("appCss").map((note) => note.noteId);
}
export default {
index
};

View File

@@ -1,76 +1,73 @@
import { isElectron } from "../services/utils.js";
import express from "express";
import auth from "../services/auth.js";
import openID from '../services/open_id.js';
import totp from './api/totp.js';
import recoveryCodes from './api/recovery_codes.js';
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
import { createPartialContentHandler } from "@triliumnext/express-partial-content";
import express from "express";
import rateLimit from "express-rate-limit";
// page routes
import setupRoute from "./setup.js";
import loginRoute from "./login.js";
import indexRoute from "./index.js";
// API routes
import treeApiRoute from "./api/tree.js";
import notesApiRoute from "./api/notes.js";
import branchesApiRoute from "./api/branches.js";
import attachmentsApiRoute from "./api/attachments.js";
import autocompleteApiRoute from "./api/autocomplete.js";
import cloningApiRoute from "./api/cloning.js";
import revisionsApiRoute from "./api/revisions.js";
import recentChangesApiRoute from "./api/recent_changes.js";
import optionsApiRoute from "./api/options.js";
import passwordApiRoute from "./api/password.js";
import syncApiRoute from "./api/sync.js";
import loginApiRoute from "./api/login.js";
import recentNotesRoute from "./api/recent_notes.js";
import appInfoRoute from "./api/app_info.js";
import exportRoute from "./api/export.js";
import importRoute from "./api/import.js";
import setupApiRoute from "./api/setup.js";
import sqlRoute from "./api/sql.js";
import databaseRoute from "./api/database.js";
import imageRoute from "./api/image.js";
import attributesRoute from "./api/attributes.js";
import scriptRoute from "./api/script.js";
import senderRoute from "./api/sender.js";
import filesRoute from "./api/files.js";
import searchRoute from "./api/search.js";
import bulkActionRoute from "./api/bulk_action.js";
import specialNotesRoute from "./api/special_notes.js";
import noteMapRoute from "./api/note_map.js";
import clipperRoute from "./api/clipper.js";
import similarNotesRoute from "./api/similar_notes.js";
import keysRoute from "./api/keys.js";
import backendLogRoute from "./api/backend_log.js";
import statsRoute from "./api/stats.js";
import fontsRoute from "./api/fonts.js";
import etapiTokensApiRoutes from "./api/etapi_tokens.js";
import relationMapApiRoute from "./api/relation-map.js";
import otherRoute from "./api/other.js";
import metricsRoute from "./api/metrics.js";
import shareRoutes from "../share/routes.js";
import ollamaRoute from "./api/ollama.js";
import openaiRoute from "./api/openai.js";
import anthropicRoute from "./api/anthropic.js";
import llmRoute from "./api/llm.js";
import systemInfoRoute from "./api/system_info.js";
import etapiAuthRoutes from "../etapi/auth.js";
import etapiAppInfoRoutes from "../etapi/app_info.js";
import etapiAttachmentRoutes from "../etapi/attachments.js";
import etapiAttributeRoutes from "../etapi/attributes.js";
import etapiBranchRoutes from "../etapi/branches.js";
import etapiNoteRoutes from "../etapi/notes.js";
import etapiSpecialNoteRoutes from "../etapi/special_notes.js";
import etapiSpecRoute from "../etapi/spec.js";
import etapiAuthRoutes from "../etapi/auth.js";
import etapiBackupRoute from "../etapi/backup.js";
import etapiBranchRoutes from "../etapi/branches.js";
import etapiMetricsRoute from "../etapi/metrics.js";
import etapiNoteRoutes from "../etapi/notes.js";
import etapiSpecRoute from "../etapi/spec.js";
import etapiSpecialNoteRoutes from "../etapi/special_notes.js";
import auth from "../services/auth.js";
import openID from '../services/open_id.js';
import { isElectron } from "../services/utils.js";
import shareRoutes from "../share/routes.js";
import anthropicRoute from "./api/anthropic.js";
import appInfoRoute from "./api/app_info.js";
import attachmentsApiRoute from "./api/attachments.js";
import attributesRoute from "./api/attributes.js";
import autocompleteApiRoute from "./api/autocomplete.js";
import backendLogRoute from "./api/backend_log.js";
import branchesApiRoute from "./api/branches.js";
import bulkActionRoute from "./api/bulk_action.js";
import clipperRoute from "./api/clipper.js";
import cloningApiRoute from "./api/cloning.js";
import databaseRoute from "./api/database.js";
import etapiTokensApiRoutes from "./api/etapi_tokens.js";
import exportRoute from "./api/export.js";
import filesRoute from "./api/files.js";
import fontsRoute from "./api/fonts.js";
import imageRoute from "./api/image.js";
import importRoute from "./api/import.js";
import keysRoute from "./api/keys.js";
import llmRoute from "./api/llm.js";
import loginApiRoute from "./api/login.js";
import metricsRoute from "./api/metrics.js";
import noteMapRoute from "./api/note_map.js";
import notesApiRoute from "./api/notes.js";
import ollamaRoute from "./api/ollama.js";
import openaiRoute from "./api/openai.js";
import optionsApiRoute from "./api/options.js";
import otherRoute from "./api/other.js";
import passwordApiRoute from "./api/password.js";
import recentChangesApiRoute from "./api/recent_changes.js";
import recentNotesRoute from "./api/recent_notes.js";
import recoveryCodes from './api/recovery_codes.js';
import relationMapApiRoute from "./api/relation-map.js";
import revisionsApiRoute from "./api/revisions.js";
import scriptRoute from "./api/script.js";
import searchRoute from "./api/search.js";
import senderRoute from "./api/sender.js";
import setupApiRoute from "./api/setup.js";
import similarNotesRoute from "./api/similar_notes.js";
import specialNotesRoute from "./api/special_notes.js";
import sqlRoute from "./api/sql.js";
import statsRoute from "./api/stats.js";
import syncApiRoute from "./api/sync.js";
import systemInfoRoute from "./api/system_info.js";
import totp from './api/totp.js';
// API routes
import treeApiRoute from "./api/tree.js";
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
import * as indexRoute from "./index.js";
import loginRoute from "./login.js";
import { apiResultHandler, apiRoute, asyncApiRoute, asyncRoute, route, router, uploadMiddlewareWithErrorHandling } from "./route_api.js";
// page routes
import setupRoute from "./setup.js";
const GET = "get",
PST = "post",
@@ -79,7 +76,6 @@ const GET = "get",
DEL = "delete";
function register(app: express.Application) {
route(GET, "/", [auth.checkAuth, csrfMiddleware], indexRoute.index);
route(GET, "/login", [auth.checkAppInitialized, auth.checkPasswordSet], loginRoute.loginPage);
route(GET, "/set-password", [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPasswordPage);
@@ -89,6 +85,7 @@ function register(app: express.Application) {
skipSuccessfulRequests: true // successful auth to rate-limited ETAPI routes isn't counted. However, successful auth to /login is still counted!
});
route(GET, "/bootstrap", [ auth.checkAuth ], indexRoute.bootstrap);
route(PST, "/login", [loginRateLimiter], loginRoute.login);
route(PST, "/logout", [csrfMiddleware, auth.checkAuth], loginRoute.logout);
route(PST, "/set-password", [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPassword);

View File

@@ -13,6 +13,18 @@
"note_structure_description": "नोटों को पदानुक्रमिक रूप से व्यवस्थित किया जा सकता है। फ़ोल्डर्स की कोई आवश्यकता नहीं है, क्योंकि प्रत्येक नोट में उप-नोट हो सकते हैं। एक एकल नोट को पदानुक्रम में कई स्थानों पर जोड़ा जा सकता है।"
},
"productivity_benefits": {
"protected_notes_title": "संरक्षित नोट्स"
"protected_notes_title": "संरक्षित नोट्स",
"web_clipper_title": "वेब क्लिपर"
},
"note_types": {
"canvas_title": "कैनवास",
"mindmap_title": "माइंडमैप"
},
"extensibility_benefits": {
"share_title": "वेब पर नोट्स शेयर करें"
},
"collections": {
"calendar_title": "कैलेंडर",
"table_title": "टेबल"
}
}

View File

@@ -130,6 +130,23 @@
"mobile_question": "모바일 앱이 있나요?",
"mobile_answer": "현재 공식적인 모바일 앱은 없습니다. 하지만, 서버 인스턴스를 가지고 있다면 웹 브라우저를 이용해 접근하거나 PWA로 설치할 수 있습니다. 안드로이드에는 (데스크탑 클라이언트처럼)오프라인에서도 작동하는 TriliumDroid라는 비공식 앱이 있습니다.",
"database_question": "어디에 데이터가 저장되나요?",
"server_question": "Trilium을 사용하기 위해 서버가 필요한가요?"
"server_question": "Trilium을 사용하기 위해 서버가 필요한가요?",
"title": "자주 묻는 질문",
"database_answer": "모든 노트는 애플리케이션 폴더의 SQLite 데이터베이스에 저장됩니다. Trilium이 텍스트 파일 대신 데이터베이스를 사용하는 이유는 성능과 기능 모두 구현하기 훨씬 어렵기 때문입니다(트리 여러 위치에 같은 노트를 두는 Clone과 같은 기능). 애플리케이션 폴더를 찾으려면 About 창으로 가세요.",
"server_answer": "아니요, 서버는 웹 브라우저를 통해 접속할 수 있도록 허용하며, 여러 기기를 사용하는 경우 동기화를 관리합니다. 시작하려면 데스크톱 애플리케이션을 다운로드하여 사용하기만 하면 됩니다.",
"scaling_question": "이 애플리케이션은 얼마나 많은 노트를 처리할 수 있나요?",
"scaling_answer": "사용량에 따라 다르겠지만, 이 애플리케이션은 최소 10만 개의 노트를 문제없이 처리할 수 있습니다. 다만, Trilium은 (NextCloud와 같은) 파일 저장소라기보다는 지식 기반 애플리케이션에 가깝기 때문에, 대용량 파일(파일당 1GB 이상)을 많이 업로드할 경우 동기화 과정이 실패할 수 있다는 점에 유의하십시오.",
"network_share_question": "내 데이터베이스를 네트워크 드라이브로 공유할 수 있나요?",
"network_share_answer": "아니요, 일반적으로 SQLite 데이터베이스를 네트워크 드라이브로 공유하는 것은 좋지 않습니다. 경우에 따라 작동할 수도 있지만, 네트워크를 통한 파일 잠금이 완벽하지 않아 데이터베이스가 손상될 가능성이 있습니다.",
"security_question": "내 데이터는 어떻게 보호되나요?",
"security_answer": "기본적으로 노트는 암호화되지 않으며 데이터베이스에서 직접 읽을 수 있습니다. 노트를 암호화 대상으로 표시하면, AES-128-CBC를 사용하여 암호화됩니다."
},
"final_cta": {
"title": "Trilium Notes를 시작할 준비가 되셨나요?",
"description": "강력한 기능과 완벽한 개인 정보 보호를 통해 나만의 지식 기반을 구축하세요.",
"get_started": "시작하기"
},
"components": {
"link_learn_more": "자세히 알아보기..."
}
}

View File

@@ -10,13 +10,18 @@
"title": "Organiser tankene dine. Bygg din personlige kunnskapsbase.",
"github": "GitHub",
"get_started": "Kom i gang",
"dockerhub": "Docker Hub"
"dockerhub": "Docker Hub",
"screenshot_alt": "Screenshot fra Trilium Notes skrivebordsprogram",
"subtitle": "Trilium er en open-source-løsning for å ta notater og organisere en personlig kunnskapsbase. Kan brukes lokalt på arbeidsstasjonen din eller synkroniseres med en selv-hostet løsning for å ha dine notater med deg overalt."
},
"organization_benefits": {
"title": "Organisering",
"note_structure_title": "Notatstruktur",
"hoisting_title": "Arbeidsflate og fokusering",
"attributes_description": "Bruk relasjoner mellom notater eller legg til etiketter for enkel kategorisering. Bruk fremhevede attributter for å legge inn strukturert informasjon som kan brukes i tabeller og tavler."
"attributes_description": "Bruk relasjoner mellom notater eller legg til etiketter for enkel kategorisering. Bruk fremhevede attributter for å legge inn strukturert informasjon som kan brukes i tabeller og tavler.",
"note_structure_description": "Notater kan arrangeres herarkisk. Det trengs ikke mapper, siden alle notater kan inneholde undernotater. Ett notat kan legges inn flere steder i herarkiet.",
"attributes_title": "Notatetiketter og -relasjoner",
"hoisting_description": "Du kan enkelt skille personlige og arbeidsnotater ved å gruppere de under arbeidsrom, som fokuserer notat-treet ditt på kun ønskede notater."
},
"productivity_benefits": {
"sync_title": "Synkronisering",
@@ -26,7 +31,12 @@
"protected_notes_title": "Beskyttede notater",
"title": "Produktivitet og sikkerhet",
"sync_content": "Bruk en selv-hostet eller cloud-instans for å enkelt synkronisere notater på tvers av enheter, og ha de tilgjengelige fra din mobiltelefon ved hjelp av progressiv web-app.",
"jump_to_content": "Hopp raskt til notater eller grensesnittkommandoer over hele hierarkiet ved å søke etter tittel, med \"fuzzy\" matching for å ta hensyn til skrivefeil eller små differanser."
"jump_to_content": "Hopp raskt til notater eller grensesnittkommandoer over hele hierarkiet ved å søke etter tittel, med \"fuzzy\" matching for å ta hensyn til skrivefeil eller små differanser.",
"revisions_content": "Notater lagres periodisk i bakgrunnen og revisjonshistorikk kan brukes for tilbakeblikk eller å omgjøre uønskede endringer. Revisjoner kan også lages manuelt.",
"protected_notes_content": "Beskytt sensitiv personlig informasjon ved å kryptere notater og låse de med en passordkryptert sesjon.",
"jump_to_title": "Hurtigsøk og kommandoer",
"search_content": "Eller søk etter tekst i notatene og finjuster søket ved å filtrere på foreldrenotat eller dybde.",
"web_clipper_content": "Hent nettsider (eller screenshots) og legg de direkte i Trilium ved hjelp av web clipper nettleserutvidelse."
},
"note_types": {
"canvas_title": "Kanvas",
@@ -34,13 +44,26 @@
"text_title": "Tekstnotat",
"code_title": "Kodenotat",
"file_title": "Filnotat",
"mermaid_title": "Mermaid diagrammer"
"mermaid_title": "Mermaid diagrammer",
"title": "Flere måter å presentere informasjonen din",
"text_description": "Notatene redigeres med en visuell editor (WYSIWYG), som støtter tabeller, bilder, matematiske uttrykk og kodeblokker med syntaksutheving. Formater tekst hurtig med Markdown-inspirert syntaks eller \"slash-kommandoer\".",
"code_description": "Store samlinger med kildekode eller skript bruker en dedikert editor med syntaksfremheving for mange programmeringsspråk og med flere fargetema.",
"file_description": "Integrer multimediafiler som PDFer, bilder og video med forhåndsvisning i programmet.",
"mermaid_description": "Lag diagrammer som flytskjema, klasse- og sekvensdiagrammer, Ganttdiagrammer og mye mer ved hjelp av Mermaidsyntaks.",
"mindmap_description": "Organiser dine tanker visuelt eller gjør en brainstorming.",
"others_list": "og andre: <0>notatkart</0>, <1>relasjonskart</1>, <2>lagrede søk</2>, <3>rendret notat</3>, og <4>web view</4>.",
"canvas_description": "Arranger figurer, bilder og tekst på et uendelig lerret som bruker samme teknologi som excalidraw.com. Ideelt for diagrammer, skisser og visuell planlegging."
},
"extensibility_benefits": {
"import_export_title": "Import/eksport",
"scripting_title": "Avansert skripting",
"api_title": "REST API",
"title": "Deling og utvidbarhet"
"title": "Deling og utvidbarhet",
"share_title": "Del notater på nett",
"share_description": "Hvis du har en server, kan den brukes til å dele valgfrie notater med andre.",
"scripting_description": "Lag dine egne integrasjoner i Trilium med egendefinerte widgets, eller serversidelogikk.",
"import_export_description": "Samhandle med andre programmer ved hjelp av Markdown, ENEX og OML.",
"api_description": "Ved hjelp av den innebygde REST-APIen kan du programmatisk samhandle med Trilium."
},
"collections": {
"title": "Samlinger",
@@ -49,7 +72,11 @@
"geomap_title": "Geokart",
"presentation_title": "Presentasjon",
"board_title": "Kanbantavle",
"geomap_description": "Planlegg ferien din eller merk deg dine interessepunkter på et geografisk kart ved hjelp av definerbare markører. Vis lagrede GPX-spor for å se reisen din."
"geomap_description": "Planlegg ferien din eller merk deg dine interessepunkter på et geografisk kart ved hjelp av definerbare markører. Vis lagrede GPX-spor for å se reisen din.",
"calendar_description": "Organiser dine personlige eller jobb-arrangement ved hjelp av kalender, med støtte for heldags- og flerdagsarrangement. Få rask oversikt over dine arrangementer med ukes- måneds- og årsvisning. Dra og slipp hendelser for enkelt å gjøre endringer.",
"table_description": "Vis og rediger informasjon om notater i tabellform, med ulike kolonnetyper som tekst, nummer, avkrysningsbokser, dato og tid, lenker, farger og støtte for relasjoner. Du kan også vise notater i et hierarkisk tre i tabellen.",
"board_description": "Organiser oppgaver eller prosjekter i en Kanbantavle hvor du enkelt kan lage nye elementer og kolonner, og endre status på elementer ved å dra de rundt på tavlen.",
"presentation_description": "Organiser informasjon i lysbilder og presenter dem i fullskjermmodus med myke overganger. Lysbildene kan også eksporteres til PDF for enkel deling."
},
"header": {
"documentation": "Dokumentasjon",
@@ -67,14 +94,19 @@
"title": "Støtt oss",
"financial_donations_title": "Finansiell donasjon",
"github_sponsors": "GitHub Sponsors",
"financial_donations_description": "Trilium er bygget og vedlikeholdt med <Link>flere hundre timers arbeid</Link>. Ditt bidrag hjelper å holde det åpen kildekode, forbedre funksjonalitet og dekker driftskostnader."
"financial_donations_description": "Trilium er bygget og vedlikeholdt med <Link>flere hundre timers arbeid</Link>. Ditt bidrag hjelper å holde det åpen kildekode, forbedre funksjonalitet og dekker driftskostnader.",
"financial_donations_cta": "Vurder gjerne å støtte hovedutvikleren (<Link>eliandoran</Link>) av programmet via:",
"buy_me_a_coffee": "Buy Me A Coffee"
},
"download_helper_desktop_windows": {
"download_scoop": "Scoop",
"title_x64": "Windows 64-bit",
"download_zip": "Portable (.zip)",
"title_arm64": "Windows på ARM",
"download_exe": "Last ned installasjonsprogram (.exe)"
"download_exe": "Last ned installasjonsprogram (.exe)",
"description_x64": "Kompatibel med Intel- eller AMD-enheter som kjører Windows 10 og 11.",
"description_arm64": "Kompatibel med ARM-enheter (for eksempel Qualcomm Snapdragon).",
"quick_start": "For å installere via Winget:"
},
"download_helper_desktop_linux": {
"download_deb": ".deb",
@@ -84,21 +116,31 @@
"download_aur": "AUR",
"title_x64": "Linux 64-bit",
"download_zip": "Portable (.zip)",
"title_arm64": "Linux på ARM"
"title_arm64": "Linux på ARM",
"description_x64": "For de fleste Linux-distribusjoner, kompatibelt med x86_64-arkitektur.",
"description_arm64": "For ARM-baserte Linux-distribusjoner, kompatibelt med aarch64-arkitektur.",
"quick_start": "Velg egnet pakkeformat avhengig av din distribusjon:"
},
"download_helper_server_docker": {
"download_ghcr": "ghcr.io",
"download_dockerhub": "Docker Hub",
"title": "Selv-hostet med Docker"
"title": "Selv-hostet med Docker",
"description": "Installer enkelt på Windows, Linux eller macOS ved bruk av en Docker-container."
},
"download_helper_desktop_macos": {
"download_homebrew_cask": "Homebrew Cask",
"download_zip": "Portable (.zip)",
"title_x64": "macOS for Intel",
"download_dmg": "Last ned installasjonsprogram (.dmg)"
"download_dmg": "Last ned installasjonsprogram (.dmg)",
"title_arm64": "macOS for Apple Silicon",
"description_x64": "For Intel-baserte Mac-er med macOS Monterey eller nyere.",
"description_arm64": "For Apple Silicon Mac-er som de med M1- og M2-chiper.",
"quick_start": "For å installere via Homebrew:"
},
"final_cta": {
"get_started": "Kom i gang"
"get_started": "Kom i gang",
"title": "Klar for å begynne med Trilium Notes?",
"description": "Skap din personlige kunnskapsbase med kraftig funksjonalitet og fullt personvern."
},
"components": {
"link_learn_more": "Lær mer..."
@@ -108,7 +150,8 @@
"platform_small": "for {{platform}}",
"linux_small": "for Linux",
"platform_big": "v{{version}} for {{platform}}",
"linux_big": "v{{version}} for Linux"
"linux_big": "v{{version}} for Linux",
"more_platforms": "Flere plattformer og serveroppsett"
},
"footer": {
"copyright_and_the": " og ",
@@ -118,16 +161,40 @@
"download_tar_x64": "x64 (.tar.xz)",
"download_tar_arm64": "ARM (.tar.xz)",
"download_nixos": "NixOS modul",
"title": "Selv-hostet på Linux"
"title": "Selv-hostet på Linux",
"description": "Installer Trilium Notes på din egen server eller VPS, kompatibel med de fleste distribusjoner."
},
"download_helper_server_hosted": {
"title": "Betalt hosting",
"download_triliumcc": "Alternativt sjekk trilium.cc"
"download_triliumcc": "Alternativt sjekk trilium.cc",
"description": "Trilium Notes driftet på PikaPods, en betalt tjeneste for enkel tilgang og administrasjon. Ikke direkte tilknyttet Trilium-teamet.",
"download_pikapod": "Installer på PikaPods"
},
"faq": {
"title": "Ofte stilte spørsmål"
"title": "Ofte stilte spørsmål",
"mobile_question": "Finnes det en mobil applikasjon?",
"mobile_answer": "Foreløpig er det ikke noe offisiell mobil applikasjon. Men hvis du har en serverinstans kan du koble til denne med en nettleser, og også installere den som en progressiv web-app. For Android finnes det en uoffisiell applikasjon med navn TriliumDroid som også fungerer offline (samme som en skrivebordsklient).",
"database_question": "Hvor lagres dataene?",
"database_answer": "Alle notater lagres i en SQLite-database i en programmappe. Årsaken til at Trilium bruker database i stedet for rene tekstfiler er både ytelse og at visse funksjoner ellers ville vært vanskelig å implementere, slik som klonede notater (samme notat flere steder). For å finne programmappen, åpne \"om\"-vinduet i programmet.",
"server_question": "Trenger jeg en server for å bruke Trilium?",
"server_answer": "Nei, serveren tillater tilgang via nettleser og håndterer synkronisering hvis du har flere enheter. For å komme i gang er det nok å laste ned skrivebordsprogrammet og begynne med det.",
"scaling_question": "Hvor godt skalerer programmet med store mengder notater?",
"scaling_answer": "Avhengig av bruk burde programmet kunne håndtere minst 100.000 notater uten problemer. Merk at synkroniseringen noen ganger kan feile ved opplasting av mange store filer (1GB per fil) siden Trilium er ment for å være en kunnskapsbase mer enn et fillager (som for eksempel NextCloud).",
"network_share_question": "Kan jeg dele databasen min over nettverksdeling?",
"network_share_answer": "Nei, det er stort sett ikke en god ide å dele en SQLite-database over nettverksdeling. Selv om det kan fungere, er det sjanser for at databasen kan bli ødelagt grunnet problemer med fillåsing over nettverk.",
"security_question": "Hvordan er mine data beskyttet?",
"security_answer": "Som standard blir ikke notater kryptert og kan leses direkte fra databasen. Når et notat er markert kryptert, blir det kryptert med AES-128-CBC."
},
"404": {
"title": "404: Siden ble ikke funnet"
"title": "404: Siden ble ikke funnet",
"description": "Siden ble ikke funnet. Den kan ha blitt slettet eller adressen er feil."
},
"contribute": {
"title": "Andre måter å bidra",
"way_translate": "Oversett programmet til ditt språk via <Link>Weblate</Link>.",
"way_community": "Ta del i felleskapet på <Discussions>GitHub Discussions</Discussions> eller på <Matrix>Matrix</Matrix>.",
"way_reports": "Meld feil via <Link>GitHub issues</Link>.",
"way_document": "Hjelp oss å forbedre dokumentasjonen ved å fortelle om mangler, eller bidra med veiledninger, Ofte Stilte Spørsmål eller tutorials.",
"way_market": "Spre ordet: Del Trilium Notes med venner, på blogger eller i sosiale media."
}
}

4
docs/README-hi.md vendored
View File

@@ -107,7 +107,7 @@ Our documentation is available in multiple formats:
maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and
[note/link maps](https://docs.triliumnotes.org/user-guide/note-types/note-map)
for visualizing notes and their relations
* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/)
* [Mind Elixir](https://docs.mind-elixir.com/) पर आधारित माइंड मैप्स
* [Geo maps](https://docs.triliumnotes.org/user-guide/collections/geomap) with
location pins and GPX tracks
* [Scripting](https://docs.triliumnotes.org/user-guide/scripts) - see [Advanced
@@ -157,7 +157,7 @@ compatible with the latest zadam/trilium version of
versions of TriliumNext/Trilium have their sync versions incremented which
prevents direct migration.
## 💬 Discuss with us
## 💬 हमारे साथ चर्चा करें
Feel free to join our official conversations. We would love to hear what
features, suggestions, or issues you may have!

View File

@@ -66,7 +66,7 @@
"jiti": "2.6.1",
"jsonc-eslint-parser": "2.4.2",
"react-refresh": "0.18.0",
"rollup-plugin-webpack-stats": "2.1.8",
"rollup-plugin-webpack-stats": "2.1.9",
"tslib": "2.8.1",
"tsx": "4.21.0",
"typescript": "~5.9.0",

View File

@@ -70,6 +70,7 @@
]
},
"dependencies": {
"@ckeditor/ckeditor5-icons": "47.3.0"
"@ckeditor/ckeditor5-icons": "47.3.0",
"mathlive": "0.108.2"
}
}

View File

@@ -1,6 +1,9 @@
import ckeditor from './../theme/icons/math.svg?raw';
import './augmentation.js';
import "../theme/mathform.css";
import 'mathlive';
import 'mathlive/fonts.css';
import 'mathlive/static.css';
export { default as Math } from './math.js';
export { default as MathUI } from './mathui.js';

View File

@@ -55,9 +55,9 @@ export default class MathUI extends Plugin {
this._balloon.showStack( 'main' );
requestAnimationFrame(() => {
this.formView?.mathInputView.fieldView.element?.focus();
});
requestAnimationFrame( () => {
this.formView?.mathInputView.focus();
} );
}
private _createFormView() {
@@ -71,31 +71,37 @@ export default class MathUI extends Plugin {
throw new CKEditorError( 'math-command' );
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const mathConfig = editor.config.get( 'math' )!;
const formView = new MainFormView(
editor.locale,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
mathConfig.engine!,
mathConfig.lazyLoad,
{
engine: mathConfig.engine!,
lazyLoad: mathConfig.lazyLoad,
previewUid: this._previewUid,
previewClassName: mathConfig.previewClassName!,
katexRenderOptions: mathConfig.katexRenderOptions!
},
mathConfig.enablePreview,
this._previewUid,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
mathConfig.previewClassName!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
mathConfig.popupClassName!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
mathConfig.katexRenderOptions!
mathConfig.popupClassName!
);
formView.mathInputView.bind( 'value' ).to( mathCommand, 'value' );
formView.displayButtonView.bind( 'isOn' ).to( mathCommand, 'display' );
// Form elements should be read-only when corresponding commands are disabled.
formView.mathInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', value => !value );
formView.saveButtonView.bind( 'isEnabled' ).to( mathCommand );
formView.displayButtonView.bind( 'isEnabled' ).to( mathCommand );
formView.mathInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', ( value: boolean ) => !value );
formView.saveButtonView.bind( 'isEnabled' ).to(
mathCommand,
'isEnabled',
formView.mathInputView,
'value',
( commandEnabled, equation ) => {
const normalizedEquation = ( equation ?? '' ).trim();
return commandEnabled && normalizedEquation.length > 0;
}
);
formView.displayButtonView.bind( 'isEnabled' ).to( mathCommand, 'isEnabled' );
// Listen to submit button click
this.listenTo( formView, 'submit', () => {
@@ -115,24 +121,12 @@ export default class MathUI extends Plugin {
} );
// Allow pressing Enter to submit changes, and use Shift+Enter to insert a new line
formView.keystrokes.set('enter', (data, cancel) => {
if (!data.shiftKey) {
formView.fire('submit');
formView.keystrokes.set( 'enter', ( data, cancel ) => {
if ( !data.shiftKey ) {
formView.fire( 'submit' );
cancel();
}
});
// Allow the textarea to be resizable
formView.mathInputView.fieldView.once('render', () => {
const textarea = formView.mathInputView.fieldView.element;
if (!textarea) return;
Object.assign(textarea.style, {
resize: 'both',
height: '100px',
width: '400px',
minWidth: '100%',
});
});
} );
return formView;
}
@@ -162,14 +156,12 @@ export default class MathUI extends Plugin {
} );
if ( this._balloon.visibleView === this.formView ) {
this.formView.mathInputView.fieldView.element?.select();
this.formView.mathInputView.focus();
}
// Show preview element
const previewEl = document.getElementById( this._previewUid );
if ( previewEl && this.formView.previewEnabled ) {
// Force refresh preview
this.formView.mathView?.updateMath();
if ( previewEl && this.formView.mathView ) {
this.formView.mathView.updateMath();
}
this.formView.equation = mathCommand.value ?? '';
@@ -206,8 +198,10 @@ export default class MathUI extends Plugin {
private _removeFormView() {
if ( this._isFormInPanel && this.formView ) {
this.formView.saveButtonView.focus();
// Hide virtual keyboard before removing the form
this.formView.hideKeyboard();
this.formView.saveButtonView.focus();
this._balloon.remove( this.formView );
// Hide preview element

View File

@@ -1,91 +1,59 @@
import { ButtonView, createLabeledTextarea, FocusCycler, LabelView, LabeledFieldView, submitHandler, SwitchButtonView, View, ViewCollection, type TextareaView, type FocusableView, Locale, FocusTracker, KeystrokeHandler } from 'ckeditor5';
import { ButtonView, FocusCycler, FocusTracker, KeystrokeHandler, LabelView, submitHandler, SwitchButtonView, View, ViewCollection, type FocusableView, type Locale } from 'ckeditor5';
import IconCheck from "@ckeditor/ckeditor5-icons/theme/icons/check.svg?raw";
import IconCancel from "@ckeditor/ckeditor5-icons/theme/icons/cancel.svg?raw";
import { extractDelimiters, hasDelimiters } from '../utils.js';
import MathView from './mathview.js';
import MathView, { type MathViewOptions } from './mathview.js';
import MathInputView from './mathinputview.js';
import '../../theme/mathform.css';
import type { KatexOptions } from '../typings-external.js';
class MathInputView extends LabeledFieldView<TextareaView> {
public value: null | string = null;
public isReadOnly = false;
constructor( locale: Locale ) {
super( locale, createLabeledTextarea );
}
}
export default class MainFormView extends View {
public saveButtonView: ButtonView;
public mathInputView: MathInputView;
public displayButtonView: SwitchButtonView;
public cancelButtonView: ButtonView;
public previewEnabled: boolean;
public previewLabel?: LabelView;
public displayButtonView: SwitchButtonView;
public mathInputView: MathInputView;
public mathView?: MathView;
public override locale: Locale = new Locale();
public lazyLoad: undefined | ( () => Promise<void> );
public focusTracker = new FocusTracker();
public keystrokes = new KeystrokeHandler();
private _focusables = new ViewCollection<FocusableView>();
private _focusCycler: FocusCycler;
constructor(
locale: Locale,
engine:
| 'mathjax'
| 'katex'
| ( (
equation: string,
element: HTMLElement,
display: boolean,
) => void ),
lazyLoad: undefined | ( () => Promise<void> ),
mathViewOptions: MathViewOptions,
previewEnabled = false,
previewUid: string,
previewClassName: Array<string>,
popupClassName: Array<string>,
katexRenderOptions: KatexOptions
popupClassName: Array<string> = []
) {
super( locale );
const t = locale.t;
// Submit button
this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', null );
this.saveButtonView.type = 'submit';
// Create views
this.mathInputView = new MathInputView( locale );
this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', 'submit' );
this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel' );
this.cancelButtonView.delegate( 'execute' ).to( this, 'cancel' );
this.displayButtonView = this._createDisplayButton( t );
// Equation input
this.mathInputView = this._createMathInput();
// Build children
// Display button
this.displayButtonView = this._createDisplayButton();
const children: Array<View> = [
this.mathInputView,
this.displayButtonView
];
// Cancel button
this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel', 'cancel' );
if ( previewEnabled ) {
const previewLabel = new LabelView( locale );
previewLabel.text = t( 'Equation preview' );
this.previewEnabled = previewEnabled;
let children = [];
if ( this.previewEnabled ) {
// Preview label
this.previewLabel = new LabelView( locale );
this.previewLabel.text = t( 'Equation preview' );
// Math element
this.mathView = new MathView( engine, lazyLoad, locale, previewUid, previewClassName, katexRenderOptions );
this.mathView = new MathView( locale, mathViewOptions );
this.mathView.bind( 'display' ).to( this.displayButtonView, 'isOn' );
children = [
this.mathInputView,
this.displayButtonView,
this.previewLabel,
this.mathView
];
} else {
children = [
this.mathInputView,
this.displayButtonView
];
children.push( previewLabel, this.mathView );
}
// Add UI elements to template
this._setupSync( previewEnabled );
this.setTemplate( {
tag: 'form',
attributes: {
@@ -107,10 +75,30 @@ export default class MainFormView extends View {
},
children
},
this.saveButtonView,
this.cancelButtonView
{
tag: 'div',
attributes: {
class: [
'ck-math-button-row'
]
},
children: [
this.saveButtonView,
this.cancelButtonView
]
}
]
} );
this._focusCycler = new FocusCycler( {
focusables: this._focusables,
focusTracker: this.focusTracker,
keystrokeHandler: this.keystrokes,
actions: {
focusPrevious: 'shift + tab',
focusNext: 'tab'
}
} );
}
public override render(): void {
@@ -121,103 +109,73 @@ export default class MainFormView extends View {
view: this
} );
// Register form elements to focusable elements
const childViews = [
this.mathInputView,
const focusableViews = [
this.mathInputView.latexTextAreaView,
this.displayButtonView,
this.saveButtonView,
this.cancelButtonView
];
childViews.forEach( v => {
focusableViews.forEach( v => {
this._focusables.add( v );
if ( v.element ) {
this._focusables.add( v );
this.focusTracker.add( v.element );
}
} );
// Listen to keypresses inside form element
this.mathInputView.on( 'mathfieldReady', () => {
const mathfieldView = this.mathInputView.mathFieldFocusableView;
if ( mathfieldView.element ) {
if ( this._focusables.has( mathfieldView ) ) {
this._focusables.remove( mathfieldView );
}
this._focusables.add( mathfieldView, 0 );
this.focusTracker.add( mathfieldView.element );
}
} );
if ( this.element ) {
this.keystrokes.listenTo( this.element );
}
}
public get equation(): string {
return this.mathInputView.value ?? '';
}
public set equation( equation: string ) {
const norm = equation.trim();
this.mathInputView.value = norm.length ? norm : null;
if ( this.mathView ) {
this.mathView.value = norm;
}
}
public focus(): void {
this._focusCycler.focusFirst();
}
public get equation(): string {
return this.mathInputView.fieldView.element?.value ?? '';
}
private _setupSync( previewEnabled: boolean ): void {
this.mathInputView.on( 'change:value', () => {
let eq = ( this.mathInputView.value ?? '' ).trim();
public set equation( equation: string ) {
if ( this.mathInputView.fieldView.element ) {
this.mathInputView.fieldView.element.value = equation;
}
if ( this.previewEnabled && this.mathView ) {
this.mathView.value = equation;
}
}
if ( hasDelimiters( eq ) ) {
const params = extractDelimiters( eq );
eq = params.equation;
this.displayButtonView.isOn = params.display;
public focusTracker: FocusTracker = new FocusTracker();
public keystrokes: KeystrokeHandler = new KeystrokeHandler();
private _focusables = new ViewCollection<FocusableView>();
private _focusCycler: FocusCycler = new FocusCycler( {
focusables: this._focusables,
focusTracker: this.focusTracker,
keystrokeHandler: this.keystrokes,
actions: {
focusPrevious: 'shift + tab',
focusNext: 'tab'
}
} );
private _createMathInput() {
const t = this.locale.t;
// Create equation input
const mathInput = new MathInputView( this.locale );
const fieldView = mathInput.fieldView;
mathInput.infoText = t( 'Insert equation in TeX format.' );
const onInput = () => {
if ( fieldView.element != null ) {
let equationInput = fieldView.element.value.trim();
// If input has delimiters
if ( hasDelimiters( equationInput ) ) {
// Get equation without delimiters
const params = extractDelimiters( equationInput );
// Remove delimiters from input field
fieldView.element.value = params.equation;
equationInput = params.equation;
// update display button and preview
this.displayButtonView.isOn = params.display;
if ( this.mathInputView.value !== eq ) {
this.mathInputView.value = eq.length ? eq : null;
}
if ( this.previewEnabled && this.mathView ) {
// Update preview view
this.mathView.value = equationInput;
}
this.saveButtonView.isEnabled = !!equationInput;
}
};
fieldView.on( 'render', onInput );
fieldView.on( 'input', onInput );
return mathInput;
if ( previewEnabled && this.mathView && this.mathView.value !== eq ) {
this.mathView.value = eq;
}
} );
}
private _createButton(
label: string,
icon: string,
className: string,
eventName: string | null
) {
private _createButton( label: string, icon: string, className: string, type?: 'submit' | 'button' ): ButtonView {
const button = new ButtonView( this.locale );
button.set( {
@@ -232,16 +190,14 @@ export default class MainFormView extends View {
}
} );
if ( eventName ) {
button.delegate( 'execute' ).to( this, eventName );
if ( type ) {
button.type = type;
}
return button;
}
private _createDisplayButton() {
const t = this.locale.t;
private _createDisplayButton( t: ( str: string ) => string ): SwitchButtonView {
const switchButton = new SwitchButtonView( this.locale );
switchButton.set( {
@@ -256,15 +212,13 @@ export default class MainFormView extends View {
} );
switchButton.on( 'execute', () => {
// Toggle state
switchButton.isOn = !switchButton.isOn;
if ( this.previewEnabled && this.mathView ) {
// Update preview view
this.mathView.display = switchButton.isOn;
}
} );
return switchButton;
}
public hideKeyboard(): void {
this.mathInputView.hideKeyboard();
}
}

View File

@@ -0,0 +1,268 @@
// Math input widget: wraps a MathLive <math-field> and a LaTeX textarea
// and keeps them in sync for the CKEditor 5 math dialog.
import { View, type Locale, type FocusableView } from 'ckeditor5';
import 'mathlive/fonts.css'; // Auto-bundles offline fonts
declare global {
interface Window {
mathVirtualKeyboard?: {
visible: boolean;
show: () => void;
hide: () => void;
addEventListener: ( event: string, cb: () => void ) => void;
removeEventListener: ( event: string, cb: () => void ) => void;
};
}
}
interface MathFieldElement extends HTMLElement {
value: string;
readOnly: boolean;
mathVirtualKeyboardPolicy: string;
inlineShortcuts?: Record<string, string>;
setValue?: ( value: string, options?: { silenceNotifications?: boolean } ) => void;
}
// Wrapper for the MathLive element to make it focusable in CKEditor's UI system
export class MathFieldFocusableView extends View implements FocusableView {
public declare element: HTMLElement | null;
private _view: MathInputView;
constructor( locale: Locale, view: MathInputView ) {
super( locale );
this._view = view;
}
public focus(): void {
this._view.mathfield?.focus();
}
public setElement( el: HTMLElement ): void {
this.element = el;
}
}
// Wrapper for the LaTeX textarea to make it focusable in CKEditor's UI system
export class LatexTextAreaView extends View implements FocusableView {
declare public element: HTMLTextAreaElement;
constructor( locale: Locale ) {
super( locale );
this.setTemplate( { tag: 'textarea', attributes: {
class: [ 'ck', 'ck-textarea', 'ck-latex-textarea' ], spellcheck: 'false', tabindex: 0
} } );
}
public focus(): void {
this.element?.focus();
}
}
// Main view class for the math input
export default class MathInputView extends View {
public declare value: string | null;
public declare isReadOnly: boolean;
public mathfield: MathFieldElement | null = null;
public readonly latexTextAreaView: LatexTextAreaView;
public readonly mathFieldFocusableView: MathFieldFocusableView;
private _destroyed = false;
private _vkGeometryHandler?: () => void;
private _updating = false;
private static _configured = false;
constructor( locale: Locale ) {
super( locale );
this.latexTextAreaView = new LatexTextAreaView( locale );
this.mathFieldFocusableView = new MathFieldFocusableView( locale, this );
this.set( 'value', null );
this.set( 'isReadOnly', false );
this.setTemplate( {
tag: 'div', attributes: { class: [ 'ck', 'ck-math-input' ] },
children: [
{ tag: 'div', attributes: { class: [ 'ck-mathlive-container' ] } },
{ tag: 'label', attributes: { class: [ 'ck-latex-label' ] }, children: [ locale.t( 'LaTeX' ) ] },
{ tag: 'div', attributes: { class: [ 'ck-latex-wrapper' ] }, children: [ this.latexTextAreaView ] }
]
} );
}
public override render(): void {
super.render();
const textarea = this.latexTextAreaView.element;
// Sync changes from the LaTeX textarea to the mathfield and model
this.listenTo( textarea, 'input', () => {
if ( this._updating ) {
return;
}
this._updating = true;
const val = textarea.value;
this.value = val || null;
if ( this.mathfield ) {
if ( val === '' ) {
this.mathfield.remove();
this.mathfield = null;
this._initMathField( false );
} else if ( this.mathfield.value.trim() !== val.trim() ) {
this._setMathfieldValue( val );
}
}
this._updating = false;
} );
// Sync changes from the model (this.value) to the UI elements
this.on( 'change:value', ( _e, _n, val ) => {
if ( this._updating ) {
return;
}
this._updating = true;
const newVal = val ?? '';
if ( textarea.value !== newVal ) {
textarea.value = newVal;
}
if ( this.mathfield ) {
if ( this.mathfield.value.trim() !== newVal.trim() ) {
this._setMathfieldValue( newVal );
}
} else if ( newVal !== '' ) {
this._initMathField( false );
}
this._updating = false;
} );
// Handle read-only state changes
this.on( 'change:isReadOnly', ( _e, _n, val ) => {
textarea.readOnly = val;
if ( this.mathfield ) {
this.mathfield.readOnly = val;
}
} );
// Handle virtual keyboard geometry changes
const vk = window.mathVirtualKeyboard;
if ( vk && !this._vkGeometryHandler ) {
this._vkGeometryHandler = () => {
if ( vk.visible && this.mathfield ) {
this.mathfield.focus();
}
};
vk.addEventListener( 'geometrychange', this._vkGeometryHandler );
}
const initial = this.value ?? '';
if ( textarea.value !== initial ) {
textarea.value = initial;
}
this._loadMathLive();
}
// Loads the MathLive library dynamically
private async _loadMathLive(): Promise<void> {
try {
await import( 'mathlive' );
await customElements.whenDefined( 'math-field' );
if ( this._destroyed ) {
return;
}
if ( !MathInputView._configured ) {
const MathfieldClass = customElements.get( 'math-field' ) as any;
if ( MathfieldClass ) {
MathfieldClass.soundsDirectory = null;
MathfieldClass.plonkSound = null;
MathInputView._configured = true;
}
}
if ( this.element && !this._destroyed ) {
this._initMathField( true );
}
} catch {
const c = this.element?.querySelector( '.ck-mathlive-container' );
if ( c ) {
c.textContent = 'Math editor unavailable';
}
}
}
// Initializes the <math-field> element
private _initMathField( shouldFocus: boolean ): void {
const container = this.element?.querySelector( '.ck-mathlive-container' );
if ( !container ) {
return;
}
if ( this.mathfield ) {
this._setMathfieldValue( this.value ?? '' );
return;
}
const mf = document.createElement( 'math-field' ) as MathFieldElement;
mf.mathVirtualKeyboardPolicy = 'auto';
mf.setAttribute( 'tabindex', '0' );
mf.value = this.value ?? '';
mf.readOnly = this.isReadOnly;
container.appendChild( mf );
// Set shortcuts after mounting (accessing inlineShortcuts requires mounted element)
try {
if ( mf.inlineShortcuts ) {
mf.inlineShortcuts = { ...mf.inlineShortcuts, dx: 'dx', dy: 'dy', dt: 'dt' };
}
} catch {
// Inline shortcut configuration is optional; ignore failures to avoid breaking the math field.
}
mf.addEventListener( 'keydown', ev => {
if ( ev.key === 'Tab' ) {
if ( ev.shiftKey ) {
ev.preventDefault();
} else {
ev.preventDefault();
ev.stopImmediatePropagation();
this.latexTextAreaView.focus();
}
}
}, { capture: true } );
mf.addEventListener( 'input', () => {
if ( this._updating ) {
return;
}
this._updating = true;
const textarea = this.latexTextAreaView.element;
if ( textarea.value.trim() !== mf.value.trim() ) {
textarea.value = mf.value;
}
this.value = mf.value || null;
this._updating = false;
} );
this.mathfield = mf;
this.mathFieldFocusableView.setElement( mf );
this.fire( 'mathfieldReady' );
if ( shouldFocus ) {
requestAnimationFrame( () => mf.focus() );
}
}
// Updates the mathfield value without triggering loops
private _setMathfieldValue( value: string ): void {
if ( !this.mathfield ) {
return;
}
if ( this.mathfield.setValue ) {
this.mathfield.setValue( value, { silenceNotifications: true } );
} else {
this.mathfield.value = value;
}
}
public hideKeyboard(): void {
window.mathVirtualKeyboard?.hide();
}
public focus(): void {
this.mathfield?.focus();
}
public override destroy(): void {
this._destroyed = true;
const vk = window.mathVirtualKeyboard;
if ( vk && this._vkGeometryHandler ) {
vk.removeEventListener( 'geometrychange', this._vkGeometryHandler );
this._vkGeometryHandler = undefined;
}
this.hideKeyboard();
this.mathfield?.remove();
this.mathfield = null;
super.destroy();
}
}

View File

@@ -2,44 +2,44 @@ import { View, type Locale } from 'ckeditor5';
import type { KatexOptions } from '../typings-external.js';
import { renderEquation } from '../utils.js';
/**
* Configuration options for the MathView.
*/
export interface MathViewOptions {
engine: 'mathjax' | 'katex' | ( ( equation: string, element: HTMLElement, display: boolean ) => void );
lazyLoad: undefined | ( () => Promise<void> );
previewUid: string;
previewClassName: Array<string>;
katexRenderOptions: KatexOptions;
}
export default class MathView extends View {
/**
* The LaTeX equation value to render.
* @observable
*/
public declare value: string;
/**
* Whether to render in display mode (centered) or inline.
* @observable
*/
public declare display: boolean;
public previewUid: string;
public previewClassName: Array<string>;
public katexRenderOptions: KatexOptions;
public engine:
| 'mathjax'
| 'katex'
| ( ( equation: string, element: HTMLElement, display: boolean ) => void );
public lazyLoad: undefined | ( () => Promise<void> );
constructor(
engine:
| 'mathjax'
| 'katex'
| ( (
equation: string,
element: HTMLElement,
display: boolean,
) => void ),
lazyLoad: undefined | ( () => Promise<void> ),
locale: Locale,
previewUid: string,
previewClassName: Array<string>,
katexRenderOptions: KatexOptions
) {
/**
* Configuration options passed during initialization.
*/
private options: MathViewOptions;
constructor( locale: Locale, options: MathViewOptions ) {
super( locale );
this.engine = engine;
this.lazyLoad = lazyLoad;
this.previewUid = previewUid;
this.katexRenderOptions = katexRenderOptions;
this.previewClassName = previewClassName;
this.options = options;
this.set( 'value', '' );
this.set( 'display', false );
// Update rendering when state changes.
// Checking isRendered prevents errors during initialization.
this.on( 'change', () => {
if ( this.isRendered ) {
this.updateMath();
@@ -55,19 +55,39 @@ export default class MathView extends View {
}
public updateMath(): void {
if ( this.element ) {
void renderEquation(
this.value,
this.element,
this.engine,
this.lazyLoad,
this.display,
true,
this.previewUid,
this.previewClassName,
this.katexRenderOptions
);
if ( !this.element ) {
return;
}
// Handle empty equations
if ( !this.value || !this.value.trim() ) {
this.element.textContent = '';
this.element.classList.remove( 'ck-math-render-error' );
return;
}
// Clear previous render
this.element.textContent = '';
this.element.classList.remove( 'ck-math-render-error' );
renderEquation(
this.value,
this.element,
this.options.engine,
this.options.lazyLoad,
this.display,
true, // isPreview
this.options.previewUid,
this.options.previewClassName,
this.options.katexRenderOptions
).catch( error => {
console.error( 'Math rendering failed:', error );
if ( this.element ) {
this.element.textContent = 'Error rendering equation';
this.element.classList.add( 'ck-math-render-error' );
}
} );
}
public override render(): void {

View File

@@ -3,6 +3,20 @@ import Math from '../src/math';
import AutoformatMath from '../src/autoformatmath';
import { describe, it, expect } from 'vitest';
// Suppress MathLive errors during async cleanup in tests
if (typeof window !== 'undefined') {
window.addEventListener('unhandledrejection', event => {
if (event.reason?.message?.includes('options') || event.reason?.message?.includes('mathlive')) {
event.preventDefault();
}
});
window.addEventListener('error', event => {
if (event.message?.includes('options') || event.message?.includes('mathlive')) {
event.preventDefault();
}
});
}
describe( 'CKEditor5 Math DLL', () => {
it( 'exports Math', () => {
expect( MathDll ).to.equal( Math );

View File

@@ -2,6 +2,20 @@ import { ClassicEditor, type EditorConfig } from 'ckeditor5';
import MathUI from '../src/mathui';
import { describe, beforeEach, it, afterEach, expect } from "vitest";
// Suppress MathLive errors during async cleanup
if (typeof window !== 'undefined') {
window.addEventListener('unhandledrejection', event => {
if (event.reason?.message?.includes('options') || event.reason?.message?.includes('mathlive')) {
event.preventDefault();
}
});
window.addEventListener('error', event => {
if (event.message?.includes('options') || event.message?.includes('mathlive')) {
event.preventDefault();
}
});
}
describe( 'Lazy load', () => {
let editorElement: HTMLDivElement;
let editor: ClassicEditor;
@@ -24,11 +38,14 @@ describe( 'Lazy load', () => {
beforeEach( () => {
editorElement = document.createElement( 'div' );
document.body.appendChild( editorElement );
lazyLoadInvoked = false;
} );
afterEach( () => {
afterEach( async () => {
if ( mathUIFeature?.formView ) {
mathUIFeature._hideUI();
}
await new Promise( resolve => setTimeout( resolve, 50 ) );
editorElement.remove();
return editor.destroy();
} );
@@ -37,6 +54,7 @@ describe( 'Lazy load', () => {
await buildEditor( {
math: {
engine: 'katex',
enablePreview: true,
lazyLoad: async () => {
lazyLoadInvoked = true;
}
@@ -44,6 +62,15 @@ describe( 'Lazy load', () => {
} );
mathUIFeature._showUI();
// Trigger render with a non-empty value to bypass empty check optimization
if ( mathUIFeature.formView ) {
mathUIFeature.formView.equation = 'x^2';
}
// Wait for async rendering and lazy loading
await new Promise( resolve => setTimeout( resolve, 100 ) );
expect( lazyLoadInvoked ).to.be.true;
} );
} );

View File

@@ -410,7 +410,7 @@ describe( 'MathUI', () => {
it( 'should bind mainFormView.mathInputView#value to math command value', () => {
const command = editor.commands.get( 'math' );
expect( formView!.mathInputView.value ).to.null;
expect( formView!.mathInputView.value ).to.be.null;
command!.value = 'x^2';
expect( formView!.mathInputView.value ).to.equal( 'x^2' );
@@ -419,10 +419,18 @@ describe( 'MathUI', () => {
it( 'should execute math command on mainFormView#submit event', () => {
const executeSpy = vi.spyOn( editor, 'execute' );
formView!.mathInputView.fieldView.element!.value = 'x^2';
formView!.mathInputView.value = 'x^2';
formView!.fire( 'submit' );
expect(executeSpy.mock.lastCall?.slice(0, 2)).toMatchObject(['math', 'x^2']);
expect( executeSpy.mock.lastCall?.slice( 0, 2 ) ).toMatchObject( [ 'math', 'x^2' ] );
} );
it( 'should update equation value when mathInputView changes', () => {
formView!.mathInputView.value = 'x^2';
expect( formView!.equation ).to.equal( 'x^2' );
formView!.mathInputView.value = '\\frac{1}{2}';
expect( formView!.equation ).to.equal( '\\frac{1}{2}' );
} );
it( 'should hide the balloon on mainFormView#cancel if math command does not have a value', () => {

View File

@@ -1,35 +1,220 @@
/**
* Math Equation Editor Dialog Styles - Compact & Readable
*/
/* === Z-INDEX: MathLive UI above CKEditor === */
.ML__keyboard, .ML__popover, .ML__menu, .ML__suggestions, .ML__autocomplete,
.ML__tooltip, .ML__sr-only, [data-ml-root], #mathlive-suggestion-popover,
.mathlive-suggestions-popover, [data-ml-tooltip], .ML__base {
z-index: calc(var(--ck-z-panel) + 1000) !important;
}
.ML__tooltip, [role="tooltip"], .ML__popover[role="tooltip"], .popover, [data-ml-tooltip] {
z-index: calc(var(--ck-z-panel) + 2000) !important;
position: fixed !important;
}
.ck.ck-balloon-panel, .ck.ck-balloon-panel .ck-balloon-panel__content {
overflow: visible !important;
}
/* === MAIN DIALOG === */
.ck.ck-math-form {
display: flex;
align-items: flex-start;
flex-direction: row;
flex-wrap: nowrap;
padding: var(--ck-spacing-standard);
@media screen and (max-width: 600px) {
flex-wrap: wrap;
& .ck-math-view {
flex-basis: 100%;
& .ck-labeled-view {
flex-basis: 100%;
}
& .ck-label {
flex-basis: 100%;
}
}
& .ck-button {
flex-basis: 50%;
}
}
display: flex;
flex-direction: column;
padding: var(--ck-spacing-standard);
box-sizing: border-box;
max-width: 80vw;
max-height: 80vh;
overflow: visible;
user-select: text;
}
.ck-math-tex.ck-placeholder::before {
display: none !important;
/* Scrollable content - vertical scroll, horizontal visible for tooltips */
.ck-math-view {
overflow-y: auto;
overflow-x: visible;
display: flex;
flex-direction: column;
flex: 1 1 auto;
gap: var(--ck-spacing-standard);
min-height: 0;
width: 100%;
}
.ck.ck-toolbar-container {
z-index: calc(var(--ck-z-panel) + 2);
/* === MATH INPUT === */
.ck.ck-math-input {
display: flex;
flex-direction: column;
gap: var(--ck-spacing-standard);
width: fit-content;
min-width: 100%;
max-width: 100%;
flex: 1 1 auto;
min-height: 0;
overflow: visible !important;
}
/* === MATHLIVE EDITOR === */
.ck.ck-math-input .ck-mathlive-container {
position: relative;
width: 100%;
min-height: 50px;
padding: var(--ck-spacing-small);
border: 1px solid var(--ck-color-input-border);
border-radius: var(--ck-border-radius);
background: var(--ck-color-input-background) !important;
transition: border-color 120ms ease;
overflow: visible !important;
clip-path: none !important;
}
.ck.ck-math-input .ck-mathlive-container:focus-within {
border-color: var(--ck-color-focus-border);
}
/* Position keyboard & menu buttons */
.ck-mathlive-container math-field::part(virtual-keyboard-toggle),
.ck-mathlive-container math-field::part(menu-toggle) {
position: absolute;
top: 8px;
}
.ck-mathlive-container math-field::part(virtual-keyboard-toggle) { right: 40px; }
.ck-mathlive-container math-field::part(menu-toggle) {
right: 8px;
display: flex !important;
visibility: visible !important;
}
/* Math field element */
.ck.ck-math-form math-field {
display: block !important;
width: 100%;
font-size: 1.5em;
background: transparent !important;
color: var(--ck-color-input-text);
border: none !important;
padding: 0;
outline: none !important;
--selection-background-color: rgba(33, 150, 243, 0.2);
--selection-color: inherit;
--contains-highlight-background-color: rgba(0, 0, 0, 0.05);
}
/* === LATEX TEXTAREA === */
.ck.ck-math-input .ck-latex-wrapper {
display: flex;
flex-direction: column;
width: fit-content;
min-width: 100%;
max-width: 100%;
padding: var(--ck-spacing-small);
border: 1px solid var(--ck-color-input-border);
border-radius: var(--ck-border-radius);
background: var(--ck-color-input-background) !important;
transition: border-color 120ms ease;
box-sizing: border-box;
}
.ck.ck-math-input .ck-latex-wrapper:focus-within {
border-color: var(--ck-color-focus-border);
}
.ck.ck-math-input .ck-latex-label {
font-size: 12px;
font-weight: 600;
color: var(--ck-color-text);
opacity: 0.8;
margin: 0 0 var(--ck-spacing-small) 0;
flex-shrink: 0;
}
.ck.ck-math-input .ck-latex-textarea {
width: fit-content;
min-width: 100%;
max-width: 100%;
min-height: 60px;
max-height: calc(80vh - 300px);
resize: both;
overflow: auto;
font-family: 'Courier New', monospace;
font-size: 0.95em;
background: transparent !important;
color: var(--ck-color-input-text);
border: none !important;
padding: 0;
outline: none !important;
box-sizing: border-box;
}
/* === DISPLAY TOGGLE === */
.ck-button-display-toggle {
align-self: flex-start;
padding: var(--ck-spacing-small) var(--ck-spacing-standard);
background: var(--ck-color-input-background);
color: var(--ck-color-text);
border: 1px solid var(--ck-color-input-border);
border-radius: var(--ck-border-radius);
cursor: pointer;
transition: all 0.2s ease;
}
.ck-button-display-toggle:hover { background: var(--ck-color-focus-border); }
/* === PREVIEW === */
.ck-math-preview,
.ck.ck-math-preview {
width: 100%;
min-height: 40px;
max-height: none !important;
height: auto !important;
padding: var(--ck-spacing-small);
background: transparent !important;
border: none !important;
display: block;
text-align: left;
overflow-x: auto !important;
overflow-y: visible !important;
flex-shrink: 0;
}
/* Center equation when in display mode */
.ck-math-preview[data-display="true"],
.ck.ck-math-preview[data-display="true"] {
text-align: center;
}
.ck-math-preview.ck-error, .ck-math-render-error {
border-color: var(--ck-color-error-text);
background: var(--ck-color-base-background);
color: var(--ck-color-error-text);
}
/* === BUTTONS === */
.ck-math-button-row {
display: flex;
gap: var(--ck-spacing-standard);
justify-content: flex-end;
margin-top: var(--ck-spacing-standard);
}
.ck-button-save, .ck-button-cancel {
padding: var(--ck-spacing-small) var(--ck-spacing-standard);
border: 1px solid var(--ck-color-input-border);
border-radius: var(--ck-border-radius);
cursor: pointer;
font-weight: 500;
}
.ck-button-save {
background: var(--ck-color-focus-border);
color: white;
}
.ck-button-cancel {
background: var(--ck-color-input-background);
color: var(--ck-color-text);
}
.ck-button-save:hover { opacity: 0.9; }
.ck-button-cancel:hover { background: var(--ck-color-base-background); }
/* === OVERFLOW FIX: Allow tooltips to escape === */
.ck.ck-balloon-panel,
.ck.ck-balloon-panel .ck-balloon-panel__content,
.ck.ck-math-form,
.ck-math-view,
.ck.ck-math-input,
.ck.ck-math-input .ck-mathlive-container {
overflow: visible !important;
clip-path: none !important;
}

View File

@@ -22,6 +22,9 @@ export default defineConfig( {
include: [
'tests/**/*.[jt]s'
],
exclude: [
'tests/setup.ts'
],
globals: true,
watch: false,
coverage: {

View File

@@ -50,6 +50,11 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, LocaleMapping | null> = {
coreTranslation: () => import("ckeditor5/translations/ja.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/ja.js"),
},
pl: {
languageCode: "pl",
coreTranslation: () => import("ckeditor5/translations/pl.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/pl.js"),
},
pt: {
languageCode: "pt",
coreTranslation: () => import("ckeditor5/translations/pt.js"),

90
pnpm-lock.yaml generated
View File

@@ -104,8 +104,8 @@ importers:
specifier: 0.18.0
version: 0.18.0
rollup-plugin-webpack-stats:
specifier: 2.1.8
version: 2.1.8(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
specifier: 2.1.9
version: 2.1.9(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
tslib:
specifier: 2.8.1
version: 2.8.1
@@ -1073,6 +1073,9 @@ importers:
'@ckeditor/ckeditor5-icons':
specifier: 47.3.0
version: 47.3.0
mathlive:
specifier: 0.108.2
version: 0.108.2
devDependencies:
'@ckeditor/ckeditor5-dev-build-tools':
specifier: 54.2.3
@@ -2129,6 +2132,10 @@ packages:
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
'@cortex-js/compute-engine@0.30.2':
resolution: {integrity: sha512-Zx+iisk9WWdbxjm8EYsneIBszvjfUs7BHNwf1jBtSINIgfWGpHrTTq9vW0J59iGCFt6bOFxbmWyxNMRSmksHMA==}
engines: {node: '>=21.7.3', npm: '>=10.5.0'}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
@@ -7076,6 +7083,10 @@ packages:
compare-versions@6.1.1:
resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
complex-esm@2.1.1-esm1:
resolution: {integrity: sha512-IShBEWHILB9s7MnfyevqNGxV0A1cfcSnewL/4uPFiSxkcQL4Mm3FxJ0pXMtCXuWLjYz3lRRyk6OfkeDZcjD6nw==}
engines: {node: '>=16.14.2', npm: '>=8.5.0'}
component-emitter@1.3.1:
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
@@ -10269,6 +10280,9 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mathlive@0.108.2:
resolution: {integrity: sha512-GIZkfprGTxrbHckOvwo92ZmOOxdD018BHDzlrEwYUU+pzR5KabhqI1s43lxe/vqXdF5RLiQKgDcuk5jxEjhkYg==}
mathml-tag-names@2.1.3:
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
@@ -12389,8 +12403,8 @@ packages:
resolution: {integrity: sha512-EsoOi8moHN6CAYyTZipxDDVTJn0j2nBCWor4wRU45RQ8ER2qREDykXLr3Ulz6hBh6oBKCFTQIjo21i0FXNo/IA==}
hasBin: true
rollup-plugin-stats@1.5.3:
resolution: {integrity: sha512-0IYVGhsFTjcddpqcElzU7Mi4vmDLihCCTH5QgCCgWpNY1VKMXVoEpxmCmGjivtJKLzI6t5QIicsPBC93UWWN2g==}
rollup-plugin-stats@1.5.4:
resolution: {integrity: sha512-b1hYagYLTyr8mCVUb7e1x9fjxOXFyeWmV9hIr7vYqq/agN+WDaGNzz+KmM3GAx0KGGI2qllOL+zAUi/l39s/Sg==}
engines: {node: '>=18'}
peerDependencies:
rolldown: ^1.0.0-beta.0
@@ -12416,8 +12430,8 @@ packages:
peerDependencies:
rollup: ^3.0.0||^4.0.0
rollup-plugin-webpack-stats@2.1.8:
resolution: {integrity: sha512-agc1OE+QwG3sGeTSdruh16DkxPb6QkgR7I3gntPDFHMXsK1bR2ADHUVod1eoE+epAOqiv3idx/hcSqZAI3a1yg==}
rollup-plugin-webpack-stats@2.1.9:
resolution: {integrity: sha512-ft1vdp3xPjE+zw8A22yCToo5cpymoWCjNDefWNO1awywsDrSDoRJhkoZTENkhJwmfh6oe5ztpGu7PfnJOMXc2g==}
engines: {node: '>=18'}
peerDependencies:
rolldown: ^1.0.0-beta.0
@@ -15285,8 +15299,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-block-quote@47.3.0':
dependencies:
@@ -15297,8 +15309,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-bookmark@47.3.0':
dependencies:
@@ -15361,8 +15371,6 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-code-block@47.3.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
dependencies:
@@ -15536,6 +15544,8 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-easy-image@47.3.0':
dependencies:
@@ -15573,6 +15583,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-inline@47.3.0':
dependencies:
@@ -15582,8 +15594,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-multi-root@47.3.0':
dependencies:
@@ -15606,8 +15616,6 @@ snapshots:
'@ckeditor/ckeditor5-table': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-emoji@47.3.0':
dependencies:
@@ -15633,6 +15641,8 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.3.0
'@ckeditor/ckeditor5-engine': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-essentials@47.3.0':
dependencies:
@@ -15664,8 +15674,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-export-word@47.3.0':
dependencies:
@@ -15690,6 +15698,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-font@47.3.0':
dependencies:
@@ -15699,8 +15709,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-footnotes@47.3.0':
dependencies:
@@ -15731,8 +15739,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-heading@47.3.0':
dependencies:
@@ -15764,6 +15770,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-html-embed@47.3.0':
dependencies:
@@ -15823,8 +15831,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-indent@47.3.0':
dependencies:
@@ -15859,6 +15865,8 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-link@47.3.0':
dependencies:
@@ -15885,6 +15893,8 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-list@47.3.0':
dependencies:
@@ -15937,8 +15947,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-mention@47.3.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)':
dependencies:
@@ -15948,8 +15956,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-merge-fields@47.3.0':
dependencies:
@@ -15962,8 +15968,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-minimap@47.3.0':
dependencies:
@@ -15972,8 +15976,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-operations-compressor@47.3.0':
dependencies:
@@ -16093,8 +16095,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-restricted-editing@47.3.0':
dependencies:
@@ -16216,8 +16216,6 @@ snapshots:
'@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-template@47.3.0':
dependencies:
@@ -16499,6 +16497,11 @@ snapshots:
'@colors/colors@1.5.0': {}
'@cortex-js/compute-engine@0.30.2':
dependencies:
complex-esm: 2.1.1-esm1
decimal.js: 10.6.0
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
@@ -22305,6 +22308,8 @@ snapshots:
compare-versions@6.1.1: {}
complex-esm@2.1.1-esm1: {}
component-emitter@1.3.1: {}
compress-commons@6.0.2:
@@ -22991,8 +22996,7 @@ snapshots:
decimal.js@10.5.0: {}
decimal.js@10.6.0:
optional: true
decimal.js@10.6.0: {}
decko@1.2.0: {}
@@ -26338,6 +26342,10 @@ snapshots:
math-intrinsics@1.1.0: {}
mathlive@0.108.2:
dependencies:
'@cortex-js/compute-engine': 0.30.2
mathml-tag-names@2.1.3: {}
mdast-util-find-and-replace@3.0.2:
@@ -28808,7 +28816,7 @@ snapshots:
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.29
optional: true
rollup-plugin-stats@1.5.3(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
rollup-plugin-stats@1.5.4(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
optionalDependencies:
rolldown: 1.0.0-beta.29
rollup: 4.52.0
@@ -28841,9 +28849,9 @@ snapshots:
'@rollup/pluginutils': 5.1.4(rollup@4.52.0)
rollup: 4.52.0
rollup-plugin-webpack-stats@2.1.8(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
rollup-plugin-webpack-stats@2.1.9(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
dependencies:
rollup-plugin-stats: 1.5.3(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
rollup-plugin-stats: 1.5.4(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
optionalDependencies:
rolldown: 1.0.0-beta.29
rollup: 4.52.0