Compare commits

..

61 Commits

Author SHA1 Message Date
renovate[bot]
42f3968cc0 fix(deps): update dependency mind-elixir to v5.5.0 2026-01-06 01:51:40 +00: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
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
47 changed files with 846 additions and 587 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

@@ -56,7 +56,7 @@
"mark.js": "8.11.1",
"marked": "17.0.1",
"mermaid": "11.12.2",
"mind-elixir": "5.4.0",
"mind-elixir": "5.5.0",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.1",

View File

@@ -541,7 +541,6 @@ export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMapp
export class AppContext extends Component {
isMainWindow: boolean;
windowId: string;
components: Component[];
beforeUnloadListeners: (WeakRef<BeforeUploadListener> | (() => boolean))[];
tabManager!: TabManager;
@@ -550,11 +549,10 @@ export class AppContext extends Component {
lastSearchString?: string;
constructor(isMainWindow: boolean, windowId: string) {
constructor(isMainWindow: boolean) {
super();
this.isMainWindow = isMainWindow;
this.windowId = windowId;
// non-widget/layout components needed for the application
this.components = [];
this.beforeUnloadListeners = [];
@@ -684,7 +682,8 @@ export class AppContext extends Component {
this.beforeUnloadListeners = this.beforeUnloadListeners.filter(l => l !== listener);
}
}
const appContext = new AppContext(window.glob.isMainWindow, window.glob.windowId);
const appContext = new AppContext(window.glob.isMainWindow);
// we should save all outstanding changes before the page/app is closed
$(window).on("beforeunload", () => {

View File

@@ -142,15 +142,14 @@ export default class Entrypoints extends Component {
}
async openInWindowCommand({ notePath, hoistedNoteId, viewScope }: NoteCommandData) {
const extraWindowId = utils.randomString(4);
const extraWindowHash = linkService.calculateHash({ notePath, hoistedNoteId, viewScope });
if (utils.isElectron()) {
const { ipcRenderer } = utils.dynamicRequire("electron");
ipcRenderer.send("create-extra-window", { extraWindowId, extraWindowHash });
ipcRenderer.send("create-extra-window", { extraWindowHash });
} else {
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=${extraWindowId}${extraWindowHash}`;
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1${extraWindowHash}`;
window.open(url, "", "width=1000,height=800");
}

View File

@@ -11,8 +11,6 @@ import linkService from "../services/link.js";
import type { EventData } from "./app_context.js";
import type FNote from "../entities/fnote.js";
const MAX_SAVED_WINDOWS = 10;
interface TabState {
contexts: NoteContext[];
position: number;
@@ -27,13 +25,6 @@ interface NoteContextState {
viewScope: Record<string, any>;
}
interface WindowState {
windowId: string;
createdAt: number;
closedAt: number;
contexts: NoteContextState[];
}
export default class TabManager extends Component {
public children: NoteContext[];
public mutex: Mutex;
@@ -50,6 +41,9 @@ export default class TabManager extends Component {
this.recentlyClosedTabs = [];
this.tabsUpdate = new SpacedUpdate(async () => {
if (!appContext.isMainWindow) {
return;
}
if (options.is("databaseReadonly")) {
return;
}
@@ -58,21 +52,9 @@ export default class TabManager extends Component {
.map((nc) => nc.getPojoState())
.filter((t) => !!t);
// Update the current windows openNoteContexts in options
const savedWindows = options.getJson("openNoteContexts") || [];
const win = savedWindows.find((w: WindowState) => w.windowId === appContext.windowId);
if (win) {
win.contexts = openNoteContexts;
} else {
savedWindows.push({
windowId: appContext.windowId,
createdAt: Date.now(),
closedAt: 0,
contexts: openNoteContexts
} as WindowState);
}
await options.save("openNoteContexts", JSON.stringify(savedWindows));
await server.put("options", {
openNoteContexts: JSON.stringify(openNoteContexts)
});
});
appContext.addBeforeUnloadListener(this);
@@ -87,13 +69,8 @@ export default class TabManager extends Component {
}
async loadTabs() {
// Get the current windows openNoteContexts
const savedWindows = options.getJson("openNoteContexts") || [];
const currentWin = savedWindows.find(w => w.windowId === appContext.windowId);
const openNoteContexts = currentWin ? currentWin.contexts : undefined;
try {
const noteContextsToOpen = openNoteContexts || [];
const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || [];
// preload all notes at once
await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) =>
@@ -142,51 +119,6 @@ export default class TabManager extends Component {
}
});
// Save window contents
if (currentWin as WindowState) {
currentWin.createdAt = Date.now();
currentWin.closedAt = 0;
currentWin.contexts = filteredNoteContexts;
} else {
if (savedWindows?.length >= MAX_SAVED_WINDOWS) {
// Filter out the oldest entry
// 1) Never remove the "main" window
// 2) Prefer removing the oldest closed window (closedAt !== 0)
// 3) If no closed window exists, remove the window with the oldest created window
let oldestClosedIndex = -1;
let oldestClosedTime = Infinity;
let oldestCreatedIndex = -1;
let oldestCreatedTime = Infinity;
savedWindows.forEach((w: WindowState, i: number) => {
if (w.windowId === "main") return;
if (w.closedAt !== 0) {
if (w.closedAt < oldestClosedTime) {
oldestClosedTime = w.closedAt;
oldestClosedIndex = i;
}
} else {
if (w.createdAt < oldestCreatedTime) {
oldestCreatedTime = w.createdAt;
oldestCreatedIndex = i;
}
}
});
const indexToRemove = oldestClosedIndex !== -1 ? oldestClosedIndex : oldestCreatedIndex;
if (indexToRemove !== -1) {
savedWindows.splice(indexToRemove, 1);
}
}
savedWindows.push({
windowId: appContext.windowId,
createdAt: Date.now(),
closedAt: 0,
contexts: filteredNoteContexts
} as WindowState);
}
await options.save("openNoteContexts", JSON.stringify(savedWindows));
// if there's a notePath in the URL, make sure it's open and active
// (useful, for e.g., opening clipped notes from clipper or opening link in an extra window)
if (parsedFromUrl.notePath) {

View File

@@ -27,6 +27,10 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
loadResults.addRevision(ec.entityId, ec.noteId, ec.componentId);
} else if (ec.entityName === "options") {
const attributeEntity = ec.entity as FAttributeRow;
if (attributeEntity.name === "openNoteContexts") {
continue; // only noise
}
options.set(attributeEntity.name as OptionNames, attributeEntity.value);
loadResults.addOption(attributeEntity.name as OptionNames);
} else if (ec.entityName === "attachments") {

View File

@@ -13,8 +13,7 @@ function injectGlobals() {
uncheckedWindow.$ = $;
uncheckedWindow.WebSocket = () => {};
uncheckedWindow.glob = {
isMainWindow: true,
windowId: "main"
isMainWindow: true
};
}

View File

@@ -36,7 +36,6 @@ interface CustomGlobals {
isProtectedSessionAvailable: boolean;
isDev: boolean;
isMainWindow: boolean;
windowId: string;
maxEntityChangeIdAtLoad: number;
maxEntityChangeSyncIdAtLoad: number;
assetPath: 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 refreshCount = 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(refreshCount(note));
}, [ note, refreshCount ]);
// React to changes in count.
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
setCount(note.attributes.length);
setCount(refreshCount(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)) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 B

View File

@@ -6,7 +6,6 @@ import sqlInit from "@triliumnext/server/src/services/sql_init.js";
import windowService from "@triliumnext/server/src/services/window.js";
import tray from "@triliumnext/server/src/services/tray.js";
import options from "@triliumnext/server/src/services/options.js";
import electronDebug from "electron-debug";
import electronDl from "electron-dl";
import { PRODUCT_NAME } from "./app-info";
@@ -70,12 +69,10 @@ async function main() {
globalShortcut.unregisterAll();
});
app.on("second-instance", async (event, commandLine) => {
app.on("second-instance", (event, commandLine) => {
const lastFocusedWindow = windowService.getLastFocusedWindow();
if (commandLine.includes("--new-window")) {
const randomString = (await import("@triliumnext/server/src/services/utils.js")).randomString;
const extraWindowId = randomString(4);
windowService.createExtraWindow(extraWindowId, "");
windowService.createExtraWindow("");
} else if (lastFocusedWindow) {
if (lastFocusedWindow.isMinimized()) {
lastFocusedWindow.restore();
@@ -127,8 +124,7 @@ async function onReady() {
}
});
}
await normalizeOpenNoteContexts();
tray.createTray();
} else {
await windowService.createSetupWindow();
@@ -137,30 +133,6 @@ async function onReady() {
await windowService.registerGlobalShortcuts();
}
/**
* Some windows may have closed abnormally, leaving closedAt as 0 in openNoteContexts.
* This function normalizes those timestamps to the current time for correct sorting/filtering.
*/
async function normalizeOpenNoteContexts() {
const savedWindows = options.getOptionJson("openNoteContexts") || [];
const now = Date.now();
let changed = false;
for (const win of savedWindows) {
if (win.windowId !== "main" && win.closedAt === 0) {
win.closedAt = now;
changed = true;
}
}
if (changed) {
const { default: cls } = (await import("@triliumnext/server/src/services/cls.js"));
cls.wrap(() => {
options.setOption("openNoteContexts", JSON.stringify(savedWindows));
})();
}
}
function getElectronLocale() {
const uiLocale = options.getOptionOrNull("locale");
const formattingLocale = options.getOptionOrNull("formattingLocale");

View File

@@ -42,7 +42,7 @@ test("Highlights list is displayed", async ({ page, context }) => {
await app.closeAllTabs();
await app.goToNoteInNewTab("Highlights list");
await expect(app.sidebar).toContainText(/highlights/i);
await expect(app.sidebar).toContainText("Highlights List");
const rootList = app.sidebar.locator(".highlights-list ol");
let index = 0;
for (const highlightedEl of ["Bold 1", "Italic 1", "Underline 1", "Colored text 1", "Background text 1", "Bold 2", "Italic 2", "Underline 2", "Colored text 2", "Background text 2"]) {

View File

@@ -59,7 +59,7 @@ export default class App {
// Wait for the page to load.
if (url === "/") {
await expect(this.noteTree).toContainText("Trilium Integration Test");
await expect(this.page.locator(".tree")).toContainText("Trilium Integration Test");
if (!preserveTabs) {
await this.closeAllTabs();
}

View File

@@ -382,8 +382,6 @@
"tooltip": "Trilium Notes",
"close": "Quit Trilium",
"recents": "Recent notes",
"recently-closed-windows": "Recently closed windows",
"tabs-total": "{{number}} tabs total",
"bookmarks": "Bookmarks",
"today": "Open today's journal note",
"new-note": "New note",

View File

@@ -15,7 +15,7 @@
</head>
<body
id="trilium-app"
class="desktop heading-style-<%= headingStyle %> layout-<%= layoutOrientation %> platform-<%= platform %> <%= isElectron ? 'electron' : '' %> <%= hasNativeTitleBar ? 'native-titlebar' : '' %> <%= hasBackgroundEffects ? 'background-effects' : '' %> <%= isMainWindow ? '' : 'extra-window' %>"
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>

View File

@@ -12,7 +12,6 @@
isDev: <%= isDev %>,
appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>,
isMainWindow: <%= isMainWindow %>,
windowId: "<%= windowId %>",
isProtectedSessionAvailable: <%= isProtectedSessionAvailable %>,
triliumVersion: "<%= triliumVersion %>",
assetPath: "<%= assetPath %>",

View File

@@ -1,48 +0,0 @@
import cls from "../services/cls.js";
import sql from "../services/sql.js";
export default () => {
cls.init(() => {
const row = sql.getRow<{ value: string }>(
`SELECT value FROM options WHERE name = 'openNoteContexts'`
);
if (!row || !row.value) {
return;
}
let parsed: any;
try {
parsed = JSON.parse(row.value);
} catch {
return;
}
// Already in new format (array + windowId), skip
if (
Array.isArray(parsed) &&
parsed.length > 0 &&
parsed[0] &&
typeof parsed[0] === "object" &&
parsed[0].windowId
) {
return;
}
// Old format: just contexts
const migrated = [
{
windowId: "main",
createdAt: 0,
closedAt: 0,
contexts: parsed
}
];
sql.execute(
`UPDATE options SET value = ? WHERE name = 'openNoteContexts'`,
[JSON.stringify(migrated)]
);
});
};

View File

@@ -6,11 +6,6 @@
// Migrations should be kept in descending order, so the latest migration is first.
const MIGRATIONS: (SqlMigration | JsMigration)[] = [
// Migrate openNoteContexts option to the new structured format with window metadata
{
version: 234,
module: async () => import("./0234__migrate_open_note_contexts_format")
},
// Migrate geo map to collection
{
version: 233,

View File

@@ -56,7 +56,6 @@ function index(req: Request, res: Response) {
appCssNoteIds: getAppCssNoteIds(),
isDev,
isMainWindow: view === "mobile" ? true : !req.query.extraWindow,
windowId: view !== "mobile" && req.query.extraWindow ? req.query.extraWindow : "main",
isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(),
triliumVersion: packageJson.version,
assetPath,

View File

@@ -4,7 +4,7 @@ import packageJson from "../../package.json" with { type: "json" };
import dataDir from "./data_dir.js";
import { AppInfo } from "@triliumnext/commons";
const APP_DB_VERSION = 234;
const APP_DB_VERSION = 233;
const SYNC_VERSION = 36;
const CLIPPER_PROTOCOL_VERSION = "1.0";

View File

@@ -72,19 +72,6 @@ function getOptionBool(name: FilterOptionsByType<boolean>): boolean {
return val === "true";
}
function getOptionJson(name: OptionNames) {
const val = getOptionOrNull(name);
if (typeof val !== "string") {
return null;
}
try {
return JSON.parse(val);
} catch (e) {
return null;
}
}
function setOption<T extends OptionNames>(name: T, value: string | OptionDefinitions[T]) {
const option = becca.getOption(name);
@@ -150,7 +137,6 @@ export default {
getOption,
getOptionInt,
getOptionBool,
getOptionJson,
setOption,
createOption,
getOptions,

View File

@@ -45,15 +45,8 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts =
"openNoteContexts",
JSON.stringify([
{
windowId: "main",
createdAt: 0,
closedAt: 0,
contexts: [
{
notePath: "root",
active: true
}
]
notePath: "root",
active: true
}
]),
false
@@ -264,15 +257,8 @@ function initStartupOptions() {
"openNoteContexts",
JSON.stringify([
{
windowId: "main",
createdAt: 0,
closedAt: 0,
contexts: [
{
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
active: true
}
]
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
active: true
}
])
);

View File

@@ -147,15 +147,8 @@ async function createInitialDatabase(skipDemoDb?: boolean) {
"openNoteContexts",
JSON.stringify([
{
windowId: "main",
createdAt: 0,
closedAt: 0,
contexts: [
{
notePath: startNoteId,
active: true
}
]
notePath: startNoteId,
active: true
}
])
);

View File

@@ -196,39 +196,6 @@ function updateTrayMenu() {
return menuItems;
}
function buildClosedWindowsMenu() {
const savedWindows = optionService.getOptionJson("openNoteContexts") || [];
const openedWindowIds = windowService.getAllWindowIds();
const closedWindows = savedWindows
.filter(win => !openedWindowIds.includes(win.windowId))
.sort((a, b) => { return a.closedAt - b.closedAt; }); // sort by time in ascending order
const menuItems: Electron.MenuItemConstructorOptions[] = [];
for (let i = closedWindows.length - 1; i >= 0; i--) {
const win = closedWindows[i];
const activeCtx = win.contexts.find(c => c.active === true);
const activateNotePath = (activeCtx ?? win.contexts[0])?.notePath;
const activateNoteId = activateNotePath?.split("/").pop() ?? null;
if (!activateNoteId) continue;
// Get the title of the closed window
const winTitle = (() => {
const raw = becca_service.getNoteTitle(activateNoteId);
const truncated = raw.length > 20 ? `${raw.slice(0, 17)}` : raw;
const tabCount = win.contexts.filter(ctx => ctx.mainNtxId === null).length;
return tabCount > 1 ? `${truncated} (${t("tray.tabs-total", { number: tabCount })})` : truncated;
})();
menuItems.push({
label: winTitle,
type: "normal",
click: () => win.windowId !== "main" ? windowService.createExtraWindow(win.windowId, "") : windowService.createMainWindow()
});
}
return menuItems;
}
const windowVisibilityMenuItems: Electron.MenuItemConstructorOptions[] = [];
// Only call getWindowTitle if windowVisibilityMap has more than one window
@@ -291,12 +258,6 @@ function updateTrayMenu() {
icon: getIconPath("recents"),
submenu: buildRecentNotesMenu()
},
{
label: t("tray.recently-closed-windows"),
type: "submenu",
icon: getIconPath("closed-windows"),
submenu: buildClosedWindowsMenu()
},
{ type: "separator" },
{
label: t("tray.close"),

View File

@@ -16,45 +16,28 @@ import { formatDownloadTitle, isMac, isWindows } from "./utils.js";
// Prevent the window being garbage collected
let mainWindow: BrowserWindow | null;
let setupWindow: BrowserWindow | null;
let allWindows: BrowserWindow[] = []; // // Used to store all windows, sorted by the order of focus.
interface WindowEntry {
window: BrowserWindow;
windowId: string; // custom window ID
}
let allWindowEntries: WindowEntry[] = [];
function trackWindowFocus(win: BrowserWindow, windowId: string) {
function trackWindowFocus(win: BrowserWindow) {
// We need to get the last focused window from allWindows. If the last window is closed, we return the previous window.
// Therefore, we need to push the window into the allWindows array every time it gets focused.
win.on("focus", () => {
allWindowEntries = allWindowEntries.filter(w => !w.window.isDestroyed() && w.window !== win);
allWindowEntries.push({ window: win, windowId: windowId });
allWindows = allWindows.filter(w => !w.isDestroyed() && w !== win);
allWindows.push(win);
if (!optionService.getOptionBool("disableTray")) {
electron.ipcMain.emit("reload-tray");
}
});
win.on("closed", () => {
cls.wrap(() => {
const savedWindows = optionService.getOptionJson("openNoteContexts") || [];
const win = savedWindows.find(w => w.windowId === windowId);
if (win) {
win.closedAt = Date.now();
}
optionService.setOption("openNoteContexts", JSON.stringify(savedWindows));
})();
allWindowEntries = allWindowEntries.filter(w => !w.window.isDestroyed());
allWindows = allWindows.filter(w => !w.isDestroyed());
if (!optionService.getOptionBool("disableTray")) {
electron.ipcMain.emit("reload-tray");
}
});
}
async function createExtraWindow(extraWindowId: string, extraWindowHash: string) {
async function createExtraWindow(extraWindowHash: string) {
const spellcheckEnabled = optionService.getOptionBool("spellCheckEnabled");
const { BrowserWindow } = await import("electron");
@@ -73,15 +56,15 @@ async function createExtraWindow(extraWindowId: string, extraWindowHash: string)
});
win.setMenuBarVisibility(false);
win.loadURL(`http://127.0.0.1:${port}/?extraWindow=${extraWindowId}${extraWindowHash}`);
win.loadURL(`http://127.0.0.1:${port}/?extraWindow=1${extraWindowHash}`);
configureWebContents(win.webContents, spellcheckEnabled);
trackWindowFocus(win, extraWindowId);
trackWindowFocus(win);
}
electron.ipcMain.on("create-extra-window", (event, arg) => {
createExtraWindow(arg.extraWindowId, arg.extraWindowHash);
createExtraWindow(arg.extraWindowHash);
});
interface PrintOpts {
@@ -185,8 +168,8 @@ async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, ac
return { browserWindow, printReport };
}
async function createMainWindow(app?: App) {
if (app && "setUserTasks" in app) {
async function createMainWindow(app: App) {
if ("setUserTasks" in app) {
app.setUserTasks([
{
program: process.execPath,
@@ -236,7 +219,7 @@ async function createMainWindow(app?: App) {
mainWindow.on("closed", () => (mainWindow = null));
configureWebContents(mainWindow.webContents, spellcheckEnabled);
trackWindowFocus(mainWindow, "main");
trackWindowFocus(mainWindow);
}
function getWindowExtraOpts() {
@@ -398,15 +381,11 @@ function getMainWindow() {
}
function getLastFocusedWindow() {
return allWindowEntries.length > 0 ? allWindowEntries[allWindowEntries.length - 1]?.window : null;
return allWindows.length > 0 ? allWindows[allWindows.length - 1] : null;
}
function getAllWindows() {
return allWindowEntries.map(e => e.window);
}
function getAllWindowIds(): string[] {
return allWindowEntries.map(e => e.windowId);
return allWindows;
}
export default {
@@ -417,6 +396,5 @@ export default {
registerGlobalShortcuts,
getMainWindow,
getLastFocusedWindow,
getAllWindows,
getAllWindowIds
getAllWindows
};

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"),

68
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
@@ -186,7 +186,7 @@ importers:
version: 0.2.0(mermaid@11.12.2)
'@mind-elixir/node-menu':
specifier: 5.0.1
version: 5.0.1(mind-elixir@5.4.0)
version: 5.0.1(mind-elixir@5.5.0)
'@popperjs/core':
specifier: 2.11.8
version: 2.11.8
@@ -278,8 +278,8 @@ importers:
specifier: 11.12.2
version: 11.12.2
mind-elixir:
specifier: 5.4.0
version: 5.4.0
specifier: 5.5.0
version: 5.5.0
normalize.css:
specifier: 8.0.1
version: 8.0.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==}
@@ -10508,8 +10522,8 @@ packages:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'}
mind-elixir@5.4.0:
resolution: {integrity: sha512-yxXajDWoSF6id8b2LKxlhXidxH/v6mx4JV+isrtsZ62RGCMsRbjUMFO9xOfTVH8vyxWhsbCkiAP6/i5hqbyk6w==}
mind-elixir@5.5.0:
resolution: {integrity: sha512-a/bOTp3wJrK/vTm2/Vn5+9kYL0fNqxWvm8SsVojJO/tltLPPU8yMPzFCZHzGRz1Aoj6bpLxN+ExfIbc28nrNxQ==}
mini-css-extract-plugin@2.9.4:
resolution: {integrity: sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==}
@@ -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
@@ -15361,8 +15375,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:
@@ -16093,8 +16105,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:
@@ -16292,6 +16302,8 @@ snapshots:
'@ckeditor/ckeditor5-icons': 47.3.0
'@ckeditor/ckeditor5-ui': 47.3.0
'@ckeditor/ckeditor5-utils': 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-upload@47.3.0':
dependencies:
@@ -16499,6 +16511,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
@@ -18315,9 +18332,9 @@ snapshots:
'@microsoft/tsdoc@0.15.1': {}
'@mind-elixir/node-menu@5.0.1(mind-elixir@5.4.0)':
'@mind-elixir/node-menu@5.0.1(mind-elixir@5.5.0)':
dependencies:
mind-elixir: 5.4.0
mind-elixir: 5.5.0
'@mixmark-io/domino@2.2.0': {}
@@ -22305,6 +22322,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 +23010,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 +26356,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:
@@ -26765,7 +26787,7 @@ snapshots:
mimic-response@3.1.0: {}
mind-elixir@5.4.0: {}
mind-elixir@5.5.0: {}
mini-css-extract-plugin@2.9.4(webpack@5.101.3(@swc/core@1.11.29(@swc/helpers@0.5.17))(esbuild@0.27.2)):
dependencies:
@@ -28808,7 +28830,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 +28863,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