From 1f5e620b1506fdddd6eac0cdca61c57a35a940bc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 09:35:44 +0000 Subject: [PATCH 1/9] Add math (LaTeX) element rendered via MathJax to SVG Users can now insert equations through a toolbar button or by double-clicking an existing math element. A modal with a CodeMirror LaTeX editor and live preview lets them iterate on the source; the rendered SVG is stored in `src` and the LaTeX in a new `latex` column so reopening the file is instant and the presentation window never loads MathJax. `prepareElementUpsert` was split into two prepared statements: images keep the autosave-friendly behavior of preserving `src` on UPDATE, while math elements update both `src` and `latex` so edits actually take effect. The math element rides the existing image-render path (`FabricImage` of an SVG data URI) with `lockUniScaling` so the equation can't be distorted by a corner drag. https://claude.ai/code/session_01PUc5sMRoJbrgNbjaJXvsoj --- package-lock.json | 227 ++++++++++++++++ package.json | 3 + src/main/db.ts | 74 ++++-- src/renderer/src/App.svelte | 149 ++++++++++- src/renderer/src/Presentation.svelte | 4 +- .../src/components/MathEditorModal.svelte | 244 ++++++++++++++++++ .../src/components/PropertiesPanel.svelte | 13 + src/renderer/src/lib/elementUtils.ts | 4 + src/renderer/src/lib/i18n/en.json | 10 + src/renderer/src/lib/i18n/zh.json | 10 + src/renderer/src/lib/math.ts | 107 ++++++++ src/renderer/src/lib/types.ts | 9 +- tests/main/db.test.ts | 78 +++++- 13 files changed, 899 insertions(+), 33 deletions(-) create mode 100644 src/renderer/src/components/MathEditorModal.svelte create mode 100644 src/renderer/src/lib/math.ts diff --git a/package-lock.json b/package-lock.json index 3f712c0..a91b34c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,18 @@ "version": "1.2.0", "hasInstallScript": true, "dependencies": { + "@codemirror/legacy-modes": "^6.5.3", "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^4.0.0", "@types/adm-zip": "^0.5.7", "adm-zip": "^0.5.16", "better-sqlite3": "^12.6.2", + "codemirror": "^6.0.2", "electron-updater": "^6.8.3", "fabric": "^7.2.0", "fontkit": "^2.0.4", "gifuct-js": "^2.1.2", + "mathjax-full": "^3.2.1", "svelte-i18n": "^4.0.1", "uuid": "^11.1.0" }, @@ -11269,6 +11272,230 @@ "resolved": "https://registry.npmmirror.com/zimmerframe/-/zimmerframe-1.1.2.tgz", "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", "license": "MIT" + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.2", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz", + "integrity": "sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.3.tgz", + "integrity": "sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.6.tgz", + "integrity": "sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", + "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz", + "integrity": "sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mathjax-full": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.1.tgz", + "integrity": "sha512-aUz9o16MGZdeiIBwZjAfUBTiJb7LRqzZEl1YOZ8zQMGYIyh1/nxRebxKxjDe9L+xcZCr2OHdzoFBMcd6VnLv9Q==", + "license": "Apache-2.0", + "dependencies": { + "esm": "^3.2.25", + "mhchemparser": "^4.1.0", + "mj-context-menu": "^0.6.1", + "speech-rule-engine": "^4.0.6" + } + }, + "node_modules/mhchemparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", + "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", + "license": "Apache-2.0" + }, + "node_modules/mj-context-menu": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", + "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==", + "license": "Apache-2.0" + }, + "node_modules/speech-rule-engine": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.1.4.tgz", + "integrity": "sha512-i/VCLG1fvRc95pMHRqG4aQNscv+9aIsqA2oI7ZQS51sTdUcDHYX6cpT8/tqZ+enjs1tKVwbRBWgxut9SWn+f9g==", + "license": "Apache-2.0", + "dependencies": { + "@xmldom/xmldom": "0.9.10", + "commander": "13.1.0", + "wicked-good-xpath": "1.3.0" + }, + "bin": { + "sre": "bin/sre" + } + }, + "node_modules/speech-rule-engine/node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/speech-rule-engine/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/wicked-good-xpath": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", + "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==", + "license": "MIT" } } } diff --git a/package.json b/package.json index befea97..8ce49ce 100644 --- a/package.json +++ b/package.json @@ -30,15 +30,18 @@ "rebuild": "electron-rebuild" }, "dependencies": { + "@codemirror/legacy-modes": "^6.5.3", "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^4.0.0", "@types/adm-zip": "^0.5.7", "adm-zip": "^0.5.16", "better-sqlite3": "^12.6.2", + "codemirror": "^6.0.2", "electron-updater": "^6.8.3", "fabric": "^7.2.0", "fontkit": "^2.0.4", "gifuct-js": "^2.1.2", + "mathjax-full": "^3.2.1", "svelte-i18n": "^4.0.1", "uuid": "^11.1.0" }, diff --git a/src/main/db.ts b/src/main/db.ts index 70f623a..747e286 100644 --- a/src/main/db.ts +++ b/src/main/db.ts @@ -34,7 +34,7 @@ export const CURRENT_FORMAT_VERSION = 2 * `_default` -> `en` -> any-key fallback. */ export const CURRENT_COMPAT_NOTES = - 'This file uses twig format v2, which adds transparent shape fills and shape borders. Older versions may not render those shapes accurately.' + 'This file uses twig format v2, which adds transparent shape fills, shape borders, and math (LaTeX) elements. Older versions may not render those shapes or equations accurately.' /** * Settings keys reserved by the format metadata contract. The renderer cannot @@ -198,6 +198,9 @@ function ensureElementColumns(db: Database): void { if (!existing.has('stroke_width')) { db.exec('ALTER TABLE elements ADD COLUMN stroke_width REAL') } + if (!existing.has('latex')) { + db.exec('ALTER TABLE elements ADD COLUMN latex TEXT') + } } /** @@ -236,8 +239,8 @@ export type { SlideBackground } * Elements can be rectangles, text objects, or images with various styling properties. */ export interface TwigElement { - /** Type of element - rectangle shape, text, or image */ - type: 'rect' | 'ellipse' | 'triangle' | 'star' | 'arrow' | 'text' | 'image' + /** Type of element - rectangle shape, text, image, or math */ + type: 'rect' | 'ellipse' | 'triangle' | 'star' | 'arrow' | 'text' | 'image' | 'math' /** Unique identifier for this element */ id: string @@ -288,12 +291,15 @@ export interface TwigElement { // eslint-disable-next-line @typescript-eslint/no-explicit-any styles?: Record - /** Image data as base64 data URI (only for image elements) */ + /** Image data as base64 data URI (image and math elements; math stores rendered SVG) */ src?: string /** Original image filename (only for image elements) */ filename?: string + /** LaTeX source (only for math elements). The rendered SVG lives in `src`. */ + latex?: string + /** Z-index for layer ordering (higher = in front) */ zIndex: number @@ -469,6 +475,7 @@ export function initializeDatabase(db: Database, appVersion: string): void { styles TEXT, src TEXT, filename TEXT, + latex TEXT, z_index INTEGER DEFAULT 0, animations TEXT, shape_params TEXT, @@ -543,8 +550,9 @@ interface ElementRow { fontStyle?: string underline?: number | null styles?: string | null // Stored as JSON string in database - src?: string | null // Image data as base64 data URI + src?: string | null // Image data as base64 data URI (image or math elements) filename?: string | null // Original image filename + latex?: string | null // LaTeX source for math elements z_index: number animations?: string | null // Stored as JSON string in database shape_params?: string | null // Shape-specific geometry ratios (JSON); currently only used by 'arrow' @@ -572,7 +580,7 @@ export function getSlide(db: Database, slideId: string): Slide | null { const elementRows = db .prepare( - 'SELECT id, slide_id, type, x, y, width, height, angle, fill, stroke, stroke_width, text, fontSize, fontFamily, fontWeight, fontStyle, underline, styles, src, filename, z_index, animations, shape_params FROM elements WHERE slide_id = ? ORDER BY z_index ASC' + 'SELECT id, slide_id, type, x, y, width, height, angle, fill, stroke, stroke_width, text, fontSize, fontFamily, fontWeight, fontStyle, underline, styles, src, filename, latex, z_index, animations, shape_params FROM elements WHERE slide_id = ? ORDER BY z_index ASC' ) .all(slideId) as ElementRow[] @@ -649,9 +657,10 @@ export function getSlide(db: Database, slideId: string): Slide | null { } })() : undefined, - // Image-specific fields + // Image / math fields src: el.src || undefined, filename: el.filename || undefined, + latex: el.latex || undefined, zIndex: el.z_index ?? 0, animations: parsedAnimations, arrowShape @@ -761,17 +770,27 @@ function deleteOrphanElements(db: Database, slideId: string, keepIds: Set void { - const stmt = db.prepare(` - INSERT INTO elements (id, slide_id, type, x, y, width, height, angle, fill, stroke, stroke_width, text, fontSize, fontFamily, fontWeight, fontStyle, underline, styles, src, filename, z_index, animations, shape_params) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(id) DO UPDATE SET +function prepareElementUpsert(db: Database): (isMath: boolean, ...args: unknown[]) => void { + const columns = + 'id, slide_id, type, x, y, width, height, angle, fill, stroke, stroke_width, text, fontSize, fontFamily, fontWeight, fontStyle, underline, styles, src, filename, latex, z_index, animations, shape_params' + const placeholders = '?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?' + const baseUpdate = ` slide_id = excluded.slide_id, type = excluded.type, x = excluded.x, @@ -792,9 +811,20 @@ function prepareElementUpsert(db: Database): (...args: unknown[]) => void { filename = excluded.filename, z_index = excluded.z_index, animations = excluded.animations, - shape_params = excluded.shape_params + shape_params = excluded.shape_params` + const baseStmt = db.prepare(` + INSERT INTO elements (${columns}) + VALUES (${placeholders}) + ON CONFLICT(id) DO UPDATE SET${baseUpdate} + `) + const mathStmt = db.prepare(` + INSERT INTO elements (${columns}) + VALUES (${placeholders}) + ON CONFLICT(id) DO UPDATE SET${baseUpdate}, + src = excluded.src, + latex = excluded.latex `) - return (...args) => stmt.run(...args) + return (isMath, ...args) => (isMath ? mathStmt : baseStmt).run(...args) } /** Serialize an element's shape-specific params to a JSON string, or null. */ @@ -922,6 +952,7 @@ export function saveSlide(db: Database, slide: Slide): void { } elementUpsert( + el.type === 'math', el.id, s.id, el.type, @@ -940,8 +971,9 @@ export function saveSlide(db: Database, slide: Slide): void { el.fontStyle ?? null, el.underline === undefined ? null : Number(el.underline), stylesJson, - el.src ?? null, // Only written on initial INSERT; preserved on UPDATE + el.src ?? null, // Image elements: written only on INSERT. Math: also on UPDATE. el.filename ?? null, + el.latex ?? null, el.zIndex, animationsJson, serializeShapeParams(el) @@ -1065,6 +1097,7 @@ export function duplicateSlide(db: Database, sourceSlideId: string): Slide { } elementUpsert( + el.type === 'math', el.id, duplicatedSlide.id, el.type, @@ -1085,6 +1118,7 @@ export function duplicateSlide(db: Database, sourceSlideId: string): Slide { stylesJson, el.src ?? null, el.filename ?? null, + el.latex ?? null, el.zIndex, animationsJson, serializeShapeParams(el) @@ -1173,6 +1207,7 @@ export function saveAllSlides(db: Database, slides: Slide[]): void { } elementUpsert( + el.type === 'math', el.id, slide.id, el.type, @@ -1193,6 +1228,7 @@ export function saveAllSlides(db: Database, slides: Slide[]): void { stylesJson, el.src ?? null, el.filename ?? null, + el.latex ?? null, el.zIndex, animationsJson, serializeShapeParams(el) diff --git a/src/renderer/src/App.svelte b/src/renderer/src/App.svelte index 0f0485f..116f089 100644 --- a/src/renderer/src/App.svelte +++ b/src/renderer/src/App.svelte @@ -102,6 +102,8 @@ import StackPanel from './components/StackPanel.svelte' import AnimationOrderPanel from './components/AnimationOrderPanel.svelte' import SettingsModal from './components/SettingsModal.svelte' + import MathEditorModal from './components/MathEditorModal.svelte' + import type { RenderedMath } from './lib/math' import CloseFailureModal from './components/CloseFailureModal.svelte' import TempPresentationGuardModal from './components/TempPresentationGuardModal.svelte' import TooNewFileModal from './components/TooNewFileModal.svelte' @@ -244,6 +246,13 @@ // Settings modal let settingsOpen = $state(false) + + // Math editor modal state. When `mathModalOpen` is true the modal is shown, + // seeded with `mathModalLatex`. `mathModalElementId` is null on insert and + // the id of an existing element on edit. + let mathModalOpen = $state(false) + let mathModalLatex = $state('') + let mathModalElementId = $state(null) let tempPresentationGuardOpen = $state(false) let closeFailureGuardOpen = $state(false) let closeFailureGuardMessage = $state('') @@ -586,7 +595,7 @@ function registerImageAssetsFromSlide(slide: typeof appState.currentSlide): void { if (!slide) return for (const el of slide.elements) { - if (el.type === 'image' && el.src && el.id) { + if ((el.type === 'image' || el.type === 'math') && el.src && el.id) { imageAssets.set(el.id, el.src) } } @@ -606,7 +615,11 @@ if (!slide) return const loads: Promise[] = [] for (const el of slide.elements) { - if (el.type === 'image' && el.src && !imageElementCache.has(el.src)) { + if ( + (el.type === 'image' || el.type === 'math') && + el.src && + !imageElementCache.has(el.src) + ) { const src = el.src loads.push( new Promise((resolve) => { @@ -740,7 +753,7 @@ fabCanvas?.discardActiveObject() const restoredElements = snapshot.elements.map((el) => { - if (el.type === 'image') { + if (el.type === 'image' || el.type === 'math') { return { ...el, src: imageAssets.get(el.id) } as TwigElement } return { ...el } as TwigElement @@ -1105,7 +1118,10 @@ ) } - if (element.type === 'image' && sourceObject instanceof FabricImage) { + if ( + (element.type === 'image' || element.type === 'math') && + sourceObject instanceof FabricImage + ) { const ghost = new FabricImage(sourceObject.getElement()) ghost.set({ ...pos, @@ -1476,7 +1492,7 @@ ...getTextboxWrappingOptions(el.text) }) ) - } else if (el.type === 'image' && el.src) { + } else if ((el.type === 'image' || el.type === 'math') && el.src) { try { const img = await FabricImage.fromURL(el.src, { crossOrigin: 'anonymous' }) const scaleX = el.width / (img.width || 1) @@ -2093,6 +2109,7 @@ fabCanvas.off('selection:cleared', handleSelectionCleared) fabCanvas.off('contextmenu', handleContextMenu) fabCanvas.off('mouse:down:before', handleCanvasMouseDownBefore) + fabCanvas.off('mouse:dblclick', handleCanvasDblClick) // Read elements synchronously (before any await) so Svelte 5 tracks the // array within the $effect's reactive context. Accessing elements after an @@ -2111,9 +2128,11 @@ return } - // Add non-image elements synchronously in z-order + // Add non-image elements synchronously in z-order. Math elements render + // as FabricImages of their prerendered SVG, so they use the same async + // load path as images and are skipped here. sortedElements.forEach((element) => { - if (element.type === 'image') return + if (element.type === 'image' || element.type === 'math') return let fabObj: FabricObject | undefined if (element.type === 'rect') { @@ -2249,6 +2268,11 @@ id: element.id, ...ROTATION_SNAP }) + // Math elements lock to uniform scaling so resizing can't distort the + // equation's aspect ratio (the rendered SVG has intrinsic proportions). + if (element.type === 'math') { + img.set({ lockUniScaling: true }) + } applyControlLayout(img as ControlLayoutTarget, { widthPx: element.width, heightPx: element.height @@ -2261,7 +2285,7 @@ } sortedElements.forEach((element) => { - if (element.type !== 'image' || !element.src) return + if ((element.type !== 'image' && element.type !== 'math') || !element.src) return const imageZIndex = element.zIndex const cached = imageElementCache.get(element.src) @@ -2289,6 +2313,9 @@ id: element.id, ...ROTATION_SNAP }) + if (element.type === 'math') { + img.set({ lockUniScaling: true }) + } applyControlLayout(img as ControlLayoutTarget, { widthPx: element.width, heightPx: element.height @@ -2327,6 +2354,7 @@ fabCanvas.on('selection:cleared', handleSelectionCleared) fabCanvas.on('contextmenu', handleContextMenu) fabCanvas.on('mouse:down:before', handleCanvasMouseDownBefore) + fabCanvas.on('mouse:dblclick', handleCanvasDblClick) if (imageLoads.length === 0) { slideTransitionOverlaySrc = null @@ -2378,6 +2406,19 @@ * Handles the 'object:modified' event from fabric.js. * Syncs changes from the canvas back to the application state. */ + /** + * Reopens the math editor when the user double-clicks an existing math + * element. Other element types ignore this event — text already enters + * inline edit mode through fabric's built-in handler. + */ + function handleCanvasDblClick(event: { target?: TwigFabricObject }): void { + const id = event.target?.id + if (!id || appState.readOnly || !appState.currentSlide) return + const el = appState.currentSlide.elements.find((e) => e.id === id) + if (el?.type !== 'math') return + openMathEditor(id) + } + function handleObjectModified(event: { target?: TwigFabricObject | ActiveSelection }): void { if (!appState.currentSlide) return if (appState.readOnly) { @@ -4127,6 +4168,73 @@ * Opens an image file dialog and adds the selected image to the current slide. * The image is loaded as a base64 data URI and stored in the slide data. */ + /** + * Opens the math editor modal. With no element id the modal is in "insert" + * mode and seeds with a sample equation; with an id the modal seeds from + * that element's stored LaTeX (used by mouse:dblclick on math elements). + */ + function openMathEditor(elementId: string | null): void { + if (appState.readOnly || !appState.currentSlide) return + if (elementId) { + const el = appState.currentSlide.elements.find((e) => e.id === elementId) + if (!el || el.type !== 'math') return + mathModalElementId = elementId + mathModalLatex = el.latex ?? '' + } else { + mathModalElementId = null + mathModalLatex = 'x^2' + } + mathModalOpen = true + } + + function addMath(): void { + openMathEditor(null) + } + + /** + * Modal commit handler. Creates a new math element or updates an existing + * one, depending on whether `mathModalElementId` was set when the modal + * opened. Reuses the image-asset registry so undo/redo snapshots (which + * strip `src` for size) can re-attach the rendered SVG. + */ + function handleMathCommit({ latex, rendered }: { latex: string; rendered: RenderedMath }): void { + if (!appState.currentSlide || appState.readOnly) return + pushCheckpoint() + if (mathModalElementId) { + const el = appState.currentSlide.elements.find((e) => e.id === mathModalElementId) + if (el && el.type === 'math') { + el.latex = latex + el.src = rendered.src + el.width = rendered.width + el.height = rendered.height + imageAssets.set(el.id, rendered.src) + } + } else { + const id = `math_${uuid_v4()}` + const newMath: TwigElement = { + type: 'math', + id, + x: 480, + y: 270, + width: rendered.width, + height: rendered.height, + angle: 0, + src: rendered.src, + latex, + zIndex: nextZIndex() + } + imageAssets.set(id, rendered.src) + appState.currentSlide.elements.push(newMath) + } + scheduleSave() + } + + function handleMathClose(): void { + mathModalOpen = false + mathModalElementId = null + mathModalLatex = '' + } + async function addImage(): Promise { if (!appState.currentSlide || appState.readOnly) return @@ -4935,6 +5043,14 @@ /> + + resolveTempPresentationGuard('save')} @@ -5341,6 +5457,22 @@ {/if} + + + + +
+ +
+

+ {$_('math.editor.latex_label')} +

+
+
+ + +
+

+ {$_('math.editor.preview_label')} +

+
+ {#if previewSrc && !renderingError} + + {:else if renderingError} + + {$_('math.editor.error_prefix')}{renderingError} + + {:else} + + {/if} +
+
+
+ + +
+ + +
+ + +{/if} diff --git a/src/renderer/src/components/PropertiesPanel.svelte b/src/renderer/src/components/PropertiesPanel.svelte index 32475a7..d896d52 100644 --- a/src/renderer/src/components/PropertiesPanel.svelte +++ b/src/renderer/src/components/PropertiesPanel.svelte @@ -56,6 +56,7 @@ onApplyToAll, onAnimationChange, onSlideTransitionChange, + onEditMath, slideTransition, richText }: { @@ -66,6 +67,7 @@ onApplyToAll?: (bg: SlideBackground | null) => void onAnimationChange?: (elementId: string, animations: ElementAnimations) => void onSlideTransitionChange?: (t: SlideTransition | undefined) => void + onEditMath?: (elementId: string) => void slideTransition?: SlideTransition richText?: RichText } = $props() @@ -349,6 +351,17 @@ {#if selectedObject}
+ {#if selectedObject.type === 'math'} +
+ +
+ {/if} {#if selectedObject.type === 'text' && richText}
diff --git a/src/renderer/src/lib/elementUtils.ts b/src/renderer/src/lib/elementUtils.ts index eeb3bbe..4ac8e77 100644 --- a/src/renderer/src/lib/elementUtils.ts +++ b/src/renderer/src/lib/elementUtils.ts @@ -7,6 +7,10 @@ export function getElementLabel(el: TwigElement): string { return `Text: ${preview}${(el.text?.length ?? 0) > 20 ? '…' : ''}` } if (el.type === 'image') return `Image: ${el.filename ?? 'image'}` + if (el.type === 'math') { + const preview = el.latex?.slice(0, 20) ?? '' + return `Math: ${preview}${(el.latex?.length ?? 0) > 20 ? '…' : ''}` + } return 'Shape' } diff --git a/src/renderer/src/lib/i18n/en.json b/src/renderer/src/lib/i18n/en.json index d758280..6da006d 100644 --- a/src/renderer/src/lib/i18n/en.json +++ b/src/renderer/src/lib/i18n/en.json @@ -23,6 +23,8 @@ "toolbar.shape.title": "Add shape", "toolbar.media": "Media", "toolbar.media.title": "Add image", + "toolbar.math": "Math", + "toolbar.math.title": "Add equation", "toolbar.settings": "Settings", "toolbar.settings.title": "Settings", "panel.properties": "Properties", @@ -150,6 +152,14 @@ "close_failure.body": "twig couldn't finish preparing this presentation for close. You can cancel and keep the window open, or close anyway and risk losing recent changes.", "close_failure.close_anyway": "Close Anyway", "close_failure.cancel": "Cancel", + "math.editor.title": "Math", + "math.editor.latex_label": "LaTeX", + "math.editor.preview_label": "Preview", + "math.editor.insert": "Insert", + "math.editor.save": "Save", + "math.editor.cancel": "Cancel", + "math.editor.error_prefix": "Could not render: ", + "math.edit_button": "Edit LaTeX…", "settings.title": "Settings", "settings.language": "Language", "settings.language.en": "English", diff --git a/src/renderer/src/lib/i18n/zh.json b/src/renderer/src/lib/i18n/zh.json index aa1af2c..d6f59c3 100644 --- a/src/renderer/src/lib/i18n/zh.json +++ b/src/renderer/src/lib/i18n/zh.json @@ -23,6 +23,8 @@ "toolbar.shape.title": "添加形状", "toolbar.media": "媒体", "toolbar.media.title": "添加图片", + "toolbar.math": "公式", + "toolbar.math.title": "添加公式", "toolbar.settings": "设置", "toolbar.settings.title": "设置", "panel.properties": "属性", @@ -150,6 +152,14 @@ "close_failure.body": "twig 无法在关闭前完成当前演示文稿的收尾处理。你可以取消并保持窗口打开,或者仍然关闭,但最近的更改可能会丢失。", "close_failure.close_anyway": "仍然关闭", "close_failure.cancel": "取消", + "math.editor.title": "公式", + "math.editor.latex_label": "LaTeX", + "math.editor.preview_label": "预览", + "math.editor.insert": "插入", + "math.editor.save": "保存", + "math.editor.cancel": "取消", + "math.editor.error_prefix": "无法渲染:", + "math.edit_button": "编辑 LaTeX…", "settings.title": "设置", "settings.language": "语言", "settings.language.en": "English(英文)", diff --git a/src/renderer/src/lib/math.ts b/src/renderer/src/lib/math.ts new file mode 100644 index 0000000..755c0b6 --- /dev/null +++ b/src/renderer/src/lib/math.ts @@ -0,0 +1,107 @@ +/** + * MathJax (TeX → SVG) wrapper for math elements. + * + * Only the editor renderer should import this — the presentation window and + * file-load paths read the prerendered SVG out of `element.src` and never + * touch MathJax. The MathJax library itself is loaded via dynamic `import()` + * on the first call so the editor's initial bundle isn't bloated. + */ + +import { normalizeSvgDataUrl } from './svg' + +export type RenderedMath = { + /** SVG image data URI (base64-encoded). */ + src: string + /** Natural width in pixels of the rendered equation. */ + width: number + /** Natural height in pixels of the rendered equation. */ + height: number +} + +export type RenderResult = { ok: true; rendered: RenderedMath } | { ok: false; error: string } + +// MathJax's exposed types are awkward to import precisely — typing the +// document as `unknown` and casting at use site keeps this file framework-free. +type MathDocumentLike = { + convert: (latex: string, options?: { display?: boolean }) => unknown + adaptor: { firstChild: (node: unknown) => Element | null } +} + +let mathDocPromise: Promise | null = null + +async function getMathDoc(): Promise { + if (!mathDocPromise) { + mathDocPromise = (async () => { + const [ + { mathjax }, + { TeX }, + { AllPackages }, + { SVG }, + { browserAdaptor }, + { RegisterHTMLHandler } + ] = await Promise.all([ + import('mathjax-full/js/mathjax.js'), + import('mathjax-full/js/input/tex.js'), + import('mathjax-full/js/input/tex/AllPackages.js'), + import('mathjax-full/js/output/svg.js'), + import('mathjax-full/js/adaptors/browserAdaptor.js'), + import('mathjax-full/js/handlers/html.js') + ]) + const adaptor = browserAdaptor() + RegisterHTMLHandler(adaptor) + const doc = mathjax.document('', { + InputJax: new TeX({ packages: AllPackages }), + OutputJax: new SVG({ fontCache: 'local' }) + }) + return doc as unknown as MathDocumentLike + })() + } + return mathDocPromise +} + +// Tiny LRU keyed by latex source. Repeated previews (e.g. as the user types +// `x`, `x^`, `x^2`) shouldn't re-pay the conversion cost on every keystroke. +const CACHE_LIMIT = 50 +const renderCache = new Map() + +function rememberRender(key: string, value: RenderResult): RenderResult { + if (renderCache.has(key)) renderCache.delete(key) + renderCache.set(key, value) + if (renderCache.size > CACHE_LIMIT) { + const oldest = renderCache.keys().next().value + if (oldest !== undefined) renderCache.delete(oldest) + } + return value +} + +/** + * Render a LaTeX string to an SVG data URI with intrinsic pixel dimensions. + * + * Returns `{ ok: false }` (with the MathJax error message) for invalid input — + * the modal renders this inline instead of throwing. + */ +export async function renderLatexToSvgDataUrl(latex: string): Promise { + const trimmed = latex.trim() + if (!trimmed) return { ok: false, error: 'Empty equation' } + + const cached = renderCache.get(trimmed) + if (cached) return cached + + try { + const doc = await getMathDoc() + const node = doc.convert(trimmed, { display: true }) + const svgEl = doc.adaptor.firstChild(node) + if (!svgEl) return rememberRender(trimmed, { ok: false, error: 'No SVG produced' }) + + const serialized = new XMLSerializer().serializeToString(svgEl) + const dataUrl = `data:image/svg+xml;utf8,${encodeURIComponent(serialized)}` + const normalized = normalizeSvgDataUrl(dataUrl) + if (!normalized) { + return rememberRender(trimmed, { ok: false, error: 'Failed to parse rendered SVG' }) + } + return rememberRender(trimmed, { ok: true, rendered: normalized }) + } catch (e) { + const message = e instanceof Error ? e.message : 'Unknown error' + return rememberRender(trimmed, { ok: false, error: message }) + } +} diff --git a/src/renderer/src/lib/types.ts b/src/renderer/src/lib/types.ts index feff733..26c8295 100644 --- a/src/renderer/src/lib/types.ts +++ b/src/renderer/src/lib/types.ts @@ -10,8 +10,8 @@ * Represents a single element (shape, text, or image) on a slide. */ export interface TwigElement { - /** Type of element - rectangle shape, text, or image */ - type: 'rect' | 'ellipse' | 'triangle' | 'star' | 'arrow' | 'text' | 'image' + /** Type of element - rectangle shape, text, image, or math */ + type: 'rect' | 'ellipse' | 'triangle' | 'star' | 'arrow' | 'text' | 'image' | 'math' /** Unique identifier for this element */ id: string @@ -62,12 +62,15 @@ export interface TwigElement { // eslint-disable-next-line @typescript-eslint/no-explicit-any styles?: Record - /** Image data as base64 data URI (only for image elements) */ + /** Image data as base64 data URI (only for image and math elements; math stores rendered SVG) */ src?: string /** Original image filename (only for image elements) */ filename?: string + /** LaTeX source (only for math elements). The rendered SVG lives in `src`. */ + latex?: string + /** Z-index for layer ordering (higher = in front) */ zIndex: number diff --git a/tests/main/db.test.ts b/tests/main/db.test.ts index dbf90cc..db712c3 100644 --- a/tests/main/db.test.ts +++ b/tests/main/db.test.ts @@ -141,11 +141,12 @@ describe('src/main/db.ts', () => { expectStoredSlide(getSlide(db, slide.id), slide) }) - it('creates fresh databases with v2 stroke columns', () => { + it('creates fresh databases with v2 stroke and latex columns', () => { const colNames = getElementColumnNames(db) expect(colNames.has('stroke')).toBe(true) expect(colNames.has('stroke_width')).toBe(true) + expect(colNames.has('latex')).toBe(true) }) it('round-trips persisted slide fields through saveSlide and getSlide', () => { @@ -194,6 +195,80 @@ describe('src/main/db.ts', () => { expectStoredSlide(getSlide(db, slide.id), slide) }) + it('round-trips math element latex source and rendered SVG src', () => { + const slide = makeSlide({ + elements: [ + { + id: 'el-math', + type: 'math', + x: 480, + y: 270, + width: 120, + height: 60, + angle: 0, + src: 'data:image/svg+xml;base64,Zmlyc3Q=', + latex: 'x^2', + zIndex: 0 + } + ], + animationOrder: [] + }) + + saveSlide(db, slide) + expectStoredSlide(getSlide(db, slide.id), slide) + + // The image-row optimization in prepareElementUpsert skips src on UPDATE. + // Math must override that — without it, edited LaTeX would still render + // the original equation on the next file open. Verify both src and latex + // are refreshed when the same element is saved again. + const edited = JSON.parse(JSON.stringify(slide)) as Slide + edited.elements[0].latex = '\\int_0^1 x^2 \\, dx' + edited.elements[0].src = 'data:image/svg+xml;base64,c2Vjb25k' + edited.elements[0].width = 200 + edited.elements[0].height = 80 + saveSlide(db, edited) + + const reloaded = getSlide(db, slide.id) + expect(reloaded?.elements[0].latex).toBe('\\int_0^1 x^2 \\, dx') + expect(reloaded?.elements[0].src).toBe('data:image/svg+xml;base64,c2Vjb25k') + expect(reloaded?.elements[0].width).toBe(200) + expect(reloaded?.elements[0].height).toBe(80) + }) + + it('preserves image src on UPDATE (regression: math-element fix must not leak to images)', () => { + const slide = makeSlide({ + elements: [ + { + id: 'el-image-only', + type: 'image', + x: 100, + y: 100, + width: 50, + height: 50, + angle: 0, + src: 'data:image/png;base64,b3JpZ2luYWw=', + filename: 'orig.png', + zIndex: 0 + } + ], + animationOrder: [] + }) + + saveSlide(db, slide) + + // Simulate the canonical autosave shape: src is stripped from in-memory + // state for image elements (see App.svelte#takeSnapshot). Saving with null + // src must NOT overwrite the persisted blob. + const updated = JSON.parse(JSON.stringify(slide)) as Slide + updated.elements[0].src = undefined + updated.elements[0].x = 200 + saveSlide(db, updated) + + const reloaded = getSlide(db, slide.id) + expect(reloaded?.elements[0].src).toBe('data:image/png;base64,b3JpZ2luYWw=') + expect(reloaded?.elements[0].x).toBe(200) + }) + it('removes orphaned elements when a slide is updated', () => { const slide = makeSlide() saveSlide(db, slide) @@ -739,6 +814,7 @@ describe('format versioning', () => { const colNames = getElementColumnNames(instance) expect(colNames.has('stroke')).toBe(true) expect(colNames.has('stroke_width')).toBe(true) + expect(colNames.has('latex')).toBe(true) const legacySlide = getSlide(instance, 'slide-v1') expect(legacySlide?.elements[0].stroke).toBeUndefined() From 4576b6a831269d3cd4176dee236d545523311059 Mon Sep 17 00:00:00 2001 From: boomzero Date: Sun, 17 May 2026 17:33:06 +0800 Subject: [PATCH 2/9] Fix math element rendering in Electron renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch math.ts to MathJax's prebuilt es5/tex-svg.js bundle. The deep js/* CJS sources triggered "require is not defined" in the renderer because their transitive imports bypassed Vite's CJS pre-bundling. - Convert MathJax's ex-unit SVG dimensions to pixels before serializing so equations insert at a usable scale instead of viewBox-based ~10000px monsters. - Use fontCache: 'none' so glyph paths are inlined per SVG; with 'local' the refs broke once the SVG was lifted into a data URL. - Detect MathJax parse errors via the inner [data-mjx-error] element and surface the message as text — the error SVG has a full-viewBox black that otherwise renders as a solid block. - Add 'unsafe-eval' to the renderer CSP so MathJax's TeX parser and startup loader can run (they use new Function() internally). - Enable syntaxHighlighting(defaultHighlightStyle) on the CodeMirror editor so the stex StreamLanguage tokens actually get colored. Co-Authored-By: Claude Opus 4.7 --- package-lock.json | 448 +++++++++--------- src/renderer/index.html | 2 +- .../src/components/MathEditorModal.svelte | 5 + src/renderer/src/lib/math.ts | 101 ++-- 4 files changed, 295 insertions(+), 261 deletions(-) diff --git a/package-lock.json b/package-lock.json index a91b34c..dd14974 100644 --- a/package-lock.json +++ b/package-lock.json @@ -352,6 +352,96 @@ "node": ">=18" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.2", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz", + "integrity": "sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.3.tgz", + "integrity": "sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.6.tgz", + "integrity": "sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", + "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz", + "integrity": "sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -1945,6 +2035,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, "node_modules/@malept/cross-spawn-promise": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", @@ -2023,6 +2137,12 @@ "node": ">= 10.0.0" } }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@npmcli/agent": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/@npmcli/agent/-/agent-3.0.0.tgz", @@ -4607,6 +4727,21 @@ "node": ">=6" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", @@ -4693,6 +4828,12 @@ "buffer": "^5.1.0" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-dirname": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/cross-dirname/-/cross-dirname-0.1.0.tgz", @@ -5933,6 +6074,15 @@ "node": "*" } }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/esm-env": { "version": "1.2.2", "resolved": "https://registry.npmmirror.com/esm-env/-/esm-env-1.2.2.tgz", @@ -7801,6 +7951,18 @@ "node": ">= 0.4" } }, + "node_modules/mathjax-full": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.1.tgz", + "integrity": "sha512-aUz9o16MGZdeiIBwZjAfUBTiJb7LRqzZEl1YOZ8zQMGYIyh1/nxRebxKxjDe9L+xcZCr2OHdzoFBMcd6VnLv9Q==", + "license": "Apache-2.0", + "dependencies": { + "esm": "^3.2.25", + "mhchemparser": "^4.1.0", + "mj-context-menu": "^0.6.1", + "speech-rule-engine": "^4.0.6" + } + }, "node_modules/memoizee": { "version": "0.4.17", "resolved": "https://registry.npmmirror.com/memoizee/-/memoizee-0.4.17.tgz", @@ -7820,6 +7982,12 @@ "node": ">=0.12" } }, + "node_modules/mhchemparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", + "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", + "license": "Apache-2.0" + }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz", @@ -8046,6 +8214,12 @@ "dev": true, "license": "ISC" }, + "node_modules/mj-context-menu": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", + "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==", + "license": "Apache-2.0" + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -9527,6 +9701,38 @@ "source-map": "^0.6.0" } }, + "node_modules/speech-rule-engine": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.1.4.tgz", + "integrity": "sha512-i/VCLG1fvRc95pMHRqG4aQNscv+9aIsqA2oI7ZQS51sTdUcDHYX6cpT8/tqZ+enjs1tKVwbRBWgxut9SWn+f9g==", + "license": "Apache-2.0", + "dependencies": { + "@xmldom/xmldom": "0.9.10", + "commander": "13.1.0", + "wicked-good-xpath": "1.3.0" + }, + "bin": { + "sre": "bin/sre" + } + }, + "node_modules/speech-rule-engine/node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/speech-rule-engine/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -9661,6 +9867,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/sumchecker/-/sumchecker-3.0.1.tgz", @@ -10993,6 +11205,12 @@ } } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -11096,6 +11314,12 @@ "node": ">=8" } }, + "node_modules/wicked-good-xpath": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", + "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==", + "license": "MIT" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", @@ -11272,230 +11496,6 @@ "resolved": "https://registry.npmmirror.com/zimmerframe/-/zimmerframe-1.1.2.tgz", "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", "license": "MIT" - }, - "node_modules/@codemirror/autocomplete": { - "version": "6.20.2", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.2.tgz", - "integrity": "sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@codemirror/commands": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", - "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.6.0", - "@codemirror/view": "^6.27.0", - "@lezer/common": "^1.1.0" - } - }, - "node_modules/@codemirror/language": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", - "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.5.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "node_modules/@codemirror/legacy-modes": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.3.tgz", - "integrity": "sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0" - } - }, - "node_modules/@codemirror/lint": { - "version": "6.9.6", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.6.tgz", - "integrity": "sha512-6Kp7r6XfCi/D/5sdXieMfg9pJU1bUEx96WITuLU6ESaKizCz0QHFMjY/TaFSbigDdEAIgi93itLBIUETP4oK+A==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.42.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/search": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", - "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.37.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/state": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", - "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", - "license": "MIT", - "dependencies": { - "@marijn/find-cluster-break": "^1.0.0" - } - }, - "node_modules/@codemirror/view": { - "version": "6.43.0", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz", - "integrity": "sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.6.0", - "crelt": "^1.0.6", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, - "node_modules/@lezer/common": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", - "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", - "license": "MIT" - }, - "node_modules/@lezer/highlight": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", - "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.3.0" - } - }, - "node_modules/@lezer/lr": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", - "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", - "license": "MIT" - }, - "node_modules/codemirror": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", - "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } - }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "license": "MIT" - }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mathjax-full": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.1.tgz", - "integrity": "sha512-aUz9o16MGZdeiIBwZjAfUBTiJb7LRqzZEl1YOZ8zQMGYIyh1/nxRebxKxjDe9L+xcZCr2OHdzoFBMcd6VnLv9Q==", - "license": "Apache-2.0", - "dependencies": { - "esm": "^3.2.25", - "mhchemparser": "^4.1.0", - "mj-context-menu": "^0.6.1", - "speech-rule-engine": "^4.0.6" - } - }, - "node_modules/mhchemparser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", - "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", - "license": "Apache-2.0" - }, - "node_modules/mj-context-menu": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", - "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==", - "license": "Apache-2.0" - }, - "node_modules/speech-rule-engine": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.1.4.tgz", - "integrity": "sha512-i/VCLG1fvRc95pMHRqG4aQNscv+9aIsqA2oI7ZQS51sTdUcDHYX6cpT8/tqZ+enjs1tKVwbRBWgxut9SWn+f9g==", - "license": "Apache-2.0", - "dependencies": { - "@xmldom/xmldom": "0.9.10", - "commander": "13.1.0", - "wicked-good-xpath": "1.3.0" - }, - "bin": { - "sre": "bin/sre" - } - }, - "node_modules/speech-rule-engine/node_modules/@xmldom/xmldom": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", - "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", - "license": "MIT", - "engines": { - "node": ">=14.6" - } - }, - "node_modules/speech-rule-engine/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/style-mod": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", - "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", - "license": "MIT" - }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "license": "MIT" - }, - "node_modules/wicked-good-xpath": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", - "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==", - "license": "MIT" } } } diff --git a/src/renderer/index.html b/src/renderer/index.html index e9d0b14..a77d9e5 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -6,7 +6,7 @@