-
`,
-);
-
-fs.writeFileSync(path.join(RESOURCES_DIR, "index.html"), html, "utf-8");
-console.log(
- "✓ Generated resources/index.html (Neutralinojs injections applied)",
-);
-
-console.log("\nDone! Run `npm run dev` to start the desktop app.");
+#!/usr/bin/env node
+
+/**
+ * prepare.js — Build script for the Neutralinojs desktop app.
+ *
+ * Copies the Vite build output (dist/) into desktop-app/resources/ and
+ * generates a Neutralinojs-compatible index.html by injecting the required
+ * Neutralinojs script tags.
+ *
+ * Must be run after `npm run build` (vite build) from the repo root.
+ * The desktop:prepare npm script does both steps in sequence.
+ */
+
+const fs = require("fs");
+const path = require("path");
+
+const DIST_DIR = path.resolve(__dirname, "../dist");
+const RESOURCES_DIR = path.resolve(__dirname, "resources");
+
+if (!fs.existsSync(DIST_DIR)) {
+ console.error("✗ dist/ not found — run `npm run build` first");
+ process.exit(1);
+}
+
+/** Recursively copy a directory, creating target dirs as needed. */
+function copyDirSync(src, dest) {
+ fs.mkdirSync(dest, { recursive: true });
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
+ const srcPath = path.join(src, entry.name);
+ const destPath = path.join(dest, entry.name);
+ if (entry.isDirectory()) {
+ copyDirSync(srcPath, destPath);
+ } else {
+ fs.copyFileSync(srcPath, destPath);
+ }
+ }
+}
+
+/** Copy entire dist/ → resources/ */
+copyDirSync(DIST_DIR, RESOURCES_DIR);
+console.log("✓ Copied dist/ → resources/");
+
+/** @section Generate index.html with Neutralinojs injections */
+
+const indexPath = path.join(RESOURCES_DIR, "index.html");
+let html = fs.readFileSync(indexPath, "utf-8");
+const originalHtml = html;
+
+/**
+ * Vite emits:
+ * Inject neutralino.js + desktop-main.js before it.
+ */
+const moduleScriptRegex = /(\n \n $1',
+);
+
+if (html === originalHtml) {
+ console.error("✗ No prepare.js transformations were applied");
+ process.exit(1);
+}
+
+fs.writeFileSync(indexPath, html, "utf-8");
+console.log("✓ Generated resources/index.html (Neutralinojs injections applied)");
+
+/** @section Copy desktop-specific source files */
+const desktopSrcDir = path.resolve(__dirname, "src");
+const desktopMainSrc = path.join(desktopSrcDir, "desktop-main.js");
+const desktopMainDest = path.join(RESOURCES_DIR, "js", "desktop-main.js");
+
+if (fs.existsSync(desktopMainSrc)) {
+ fs.copyFileSync(desktopMainSrc, desktopMainDest);
+ console.log("✓ Copied desktop-main.js to resources/js/");
+}
+
+console.log("\nDone! Run `npm run desktop:dev` to start the desktop app.");
diff --git a/desktop-app/resources/assets/Black and Beige Simple Coming Soon Banner.png b/desktop-app/resources/assets/Black and Beige Simple Coming Soon Banner.png
deleted file mode 100644
index 8ec1fc1..0000000
Binary files a/desktop-app/resources/assets/Black and Beige Simple Coming Soon Banner.png and /dev/null differ
diff --git a/desktop-app/resources/assets/code.png b/desktop-app/resources/assets/code.png
deleted file mode 100644
index 9473cb1..0000000
Binary files a/desktop-app/resources/assets/code.png and /dev/null differ
diff --git a/desktop-app/resources/assets/github.png b/desktop-app/resources/assets/github.png
deleted file mode 100644
index 0c2ee50..0000000
Binary files a/desktop-app/resources/assets/github.png and /dev/null differ
diff --git a/desktop-app/resources/assets/icon.jpg b/desktop-app/resources/assets/icon.jpg
deleted file mode 100644
index cdb8b4a..0000000
Binary files a/desktop-app/resources/assets/icon.jpg and /dev/null differ
diff --git a/desktop-app/resources/assets/live-peview.gif b/desktop-app/resources/assets/live-peview.gif
deleted file mode 100644
index 56edb86..0000000
Binary files a/desktop-app/resources/assets/live-peview.gif and /dev/null differ
diff --git a/desktop-app/resources/assets/mathexp.png b/desktop-app/resources/assets/mathexp.png
deleted file mode 100644
index 4731f6f..0000000
Binary files a/desktop-app/resources/assets/mathexp.png and /dev/null differ
diff --git a/desktop-app/resources/assets/mermaid.png b/desktop-app/resources/assets/mermaid.png
deleted file mode 100644
index 16323a4..0000000
Binary files a/desktop-app/resources/assets/mermaid.png and /dev/null differ
diff --git a/desktop-app/resources/assets/table.png b/desktop-app/resources/assets/table.png
deleted file mode 100644
index e0cca14..0000000
Binary files a/desktop-app/resources/assets/table.png and /dev/null differ
diff --git a/desktop-app/resources/js/main.js b/desktop-app/resources/js/main.js
deleted file mode 100644
index 70773eb..0000000
--- a/desktop-app/resources/js/main.js
+++ /dev/null
@@ -1,127 +0,0 @@
-// This is just a sample app. You can structure your Neutralinojs app code as you wish.
-// This example app is written with vanilla JavaScript and HTML.
-// Feel free to use any frontend framework you like :)
-// See more details: https://neutralino.js.org/docs/how-to/use-a-frontend-library
-
-/*
- Function to display information about the Neutralino app.
- This function updates the content of the 'info' element in the HTML
- with details regarding the running Neutralino application, including
- its ID, port, operating system, and version information.
-*/
-function showInfo() {
- return `
- ${NL_APPID} is running on port ${NL_PORT} inside ${NL_OS}
-
-
server: v${NL_VERSION} . client: v${NL_CVERSION}
- `;
-}
-
-/*
- Function to open the official Neutralino documentation in the default web browser.
-*/
-function openDocs() {
- Neutralino.os.open("https://neutralino.js.org/docs");
-}
-
-/*
- Function to open a tutorial video on Neutralino's official YouTube channel in the default web browser.
-*/
-function openTutorial() {
- Neutralino.os.open("https://www.youtube.com/c/CodeZri");
-}
-
-/*
- Function to set up a system tray menu with options specific to the window mode.
- This function checks if the application is running in window mode, and if so,
- it defines the tray menu items and sets up the tray accordingly.
-*/
-function setTray() {
- // Tray menu is only available in window mode
- if (NL_MODE != "window") {
- console.log("INFO: Tray menu is only available in the window mode.");
- return;
- }
-
- // Define tray menu items
- let tray = {
- icon: "/resources/icons/trayIcon.png",
- menuItems: [
- { id: "VERSION", text: "Get version" },
- { id: "SEP", text: "-" },
- { id: "QUIT", text: "Quit" },
- ],
- };
-
- // Set the tray menu
- Neutralino.os.setTray(tray);
-}
-
-/*
- Function to handle click events on the tray menu items.
- This function performs different actions based on the clicked item's ID,
- such as displaying version information or exiting the application.
-*/
-function onTrayMenuItemClicked(event) {
- switch (event.detail.id) {
- case "VERSION":
- // Display version information
- Neutralino.os.showMessageBox(
- "Version information",
- `Neutralinojs server: v${NL_VERSION} | Neutralinojs client: v${NL_CVERSION}`,
- );
- break;
- case "QUIT":
- // Exit the application
- Neutralino.app.exit();
- break;
- }
-}
-
-/*
- Function to handle the window close event by gracefully exiting the Neutralino application.
-*/
-function onWindowClose() {
- Neutralino.app.exit();
-}
-
-// Initialize Neutralino
-Neutralino.init();
-
-// Register event listeners
-Neutralino.events.on("trayMenuItemClicked", onTrayMenuItemClicked);
-Neutralino.events.on("windowClose", onWindowClose);
-
-// Conditional initialization: Set up system tray if not running on macOS
-if (NL_OS != "Darwin") {
- // TODO: Fix https://github.com/neutralinojs/neutralinojs/issues/615
- setTray();
-}
-
-// Open file passed as command-line argument (e.g. when double-clicking a .md file)
-(async function loadInitialFile() {
- const args = Array.isArray(NL_ARGS) ? NL_ARGS : (() => { try { return JSON.parse(NL_ARGS); } catch(e) { return []; } })();
- const filePath = args.find(a => typeof a === 'string' && /\.(md|markdown)$/i.test(a));
- if (!filePath) return;
-
- try {
- const content = await Neutralino.filesystem.readFile(filePath);
-
- function applyContent() {
- const editor = document.getElementById('markdown-editor');
- const dropzone = document.getElementById('dropzone');
- if (!editor) return;
- editor.value = content;
- editor.dispatchEvent(new Event('input'));
- if (dropzone) dropzone.style.display = 'none';
- }
-
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', applyContent);
- } else {
- setTimeout(applyContent, 0);
- }
- } catch (e) {
- console.warn('Could not open initial file:', e);
- }
-})();
diff --git a/desktop-app/resources/js/neutralino.d.ts b/desktop-app/resources/js/neutralino.d.ts
deleted file mode 100644
index 09d69e6..0000000
--- a/desktop-app/resources/js/neutralino.d.ts
+++ /dev/null
@@ -1,531 +0,0 @@
-export declare enum LoggerType {
- WARNING = "WARNING",
- ERROR = "ERROR",
- INFO = "INFO"
-}
-export declare enum Icon {
- WARNING = "WARNING",
- ERROR = "ERROR",
- INFO = "INFO",
- QUESTION = "QUESTION"
-}
-export declare enum MessageBoxChoice {
- OK = "OK",
- OK_CANCEL = "OK_CANCEL",
- YES_NO = "YES_NO",
- YES_NO_CANCEL = "YES_NO_CANCEL",
- RETRY_CANCEL = "RETRY_CANCEL",
- ABORT_RETRY_IGNORE = "ABORT_RETRY_IGNORE"
-}
-export declare enum ClipboardFormat {
- unknown = "unknown",
- text = "text",
- image = "image"
-}
-export declare enum Mode {
- window = "window",
- browser = "browser",
- cloud = "cloud",
- chrome = "chrome"
-}
-export declare enum OperatingSystem {
- Linux = "Linux",
- Windows = "Windows",
- Darwin = "Darwin",
- FreeBSD = "FreeBSD",
- Unknown = "Unknown"
-}
-export declare enum Architecture {
- x64 = "x64",
- arm = "arm",
- itanium = "itanium",
- ia32 = "ia32",
- unknown = "unknown"
-}
-export interface DirectoryEntry {
- entry: string;
- path: string;
- type: string;
-}
-export interface FileReaderOptions {
- pos: number;
- size: number;
-}
-export interface DirectoryReaderOptions {
- recursive: boolean;
-}
-export interface OpenedFile {
- id: number;
- eof: boolean;
- pos: number;
- lastRead: number;
-}
-export interface Stats {
- size: number;
- isFile: boolean;
- isDirectory: boolean;
- createdAt: number;
- modifiedAt: number;
-}
-export interface Watcher {
- id: number;
- path: string;
-}
-export interface CopyOptions {
- recursive: boolean;
- overwrite: boolean;
- skip: boolean;
-}
-export interface PathParts {
- rootName: string;
- rootDirectory: string;
- rootPath: string;
- relativePath: string;
- parentPath: string;
- filename: string;
- stem: string;
- extension: string;
-}
-interface Permissions$1 {
- all: boolean;
- ownerAll: boolean;
- ownerRead: boolean;
- ownerWrite: boolean;
- ownerExec: boolean;
- groupAll: boolean;
- groupRead: boolean;
- groupWrite: boolean;
- groupExec: boolean;
- othersAll: boolean;
- othersRead: boolean;
- othersWrite: boolean;
- othersExec: boolean;
-}
-export type PermissionsMode = "ADD" | "REPLACE" | "REMOVE";
-declare function createDirectory(path: string): Promise
;
-declare function remove(path: string): Promise;
-declare function writeFile(path: string, data: string): Promise;
-declare function appendFile(path: string, data: string): Promise;
-declare function writeBinaryFile(path: string, data: ArrayBuffer): Promise;
-declare function appendBinaryFile(path: string, data: ArrayBuffer): Promise;
-declare function readFile(path: string, options?: FileReaderOptions): Promise;
-declare function readBinaryFile(path: string, options?: FileReaderOptions): Promise;
-declare function openFile(path: string): Promise;
-declare function createWatcher(path: string): Promise;
-declare function removeWatcher(id: number): Promise;
-declare function getWatchers(): Promise;
-declare function updateOpenedFile(id: number, event: string, data?: any): Promise;
-declare function getOpenedFileInfo(id: number): Promise;
-declare function readDirectory(path: string, options?: DirectoryReaderOptions): Promise;
-declare function copy(source: string, destination: string, options?: CopyOptions): Promise;
-declare function move(source: string, destination: string): Promise;
-declare function getStats(path: string): Promise;
-declare function getAbsolutePath(path: string): Promise;
-declare function getRelativePath(path: string, base?: string): Promise;
-declare function getPathParts(path: string): Promise;
-declare function getPermissions(path: string): Promise;
-declare function setPermissions(path: string, permissions: Permissions$1, mode: PermissionsMode): Promise;
-declare function getJoinedPath(...paths: string[]): Promise;
-declare function getNormalizedPath(path: string): Promise;
-declare function getUnnormalizedPath(path: string): Promise;
-export interface ExecCommandOptions {
- stdIn?: string;
- background?: boolean;
- cwd?: string;
-}
-export interface ExecCommandResult {
- pid: number;
- stdOut: string;
- stdErr: string;
- exitCode: number;
-}
-export interface SpawnedProcess {
- id: number;
- pid: number;
-}
-export interface SpawnedProcessOptions {
- cwd?: string;
- envs?: Record;
-}
-export interface Envs {
- [key: string]: string;
-}
-export interface OpenDialogOptions {
- multiSelections?: boolean;
- filters?: Filter[];
- defaultPath?: string;
-}
-export interface FolderDialogOptions {
- defaultPath?: string;
-}
-export interface SaveDialogOptions {
- forceOverwrite?: boolean;
- filters?: Filter[];
- defaultPath?: string;
-}
-export interface Filter {
- name: string;
- extensions: string[];
-}
-export interface TrayOptions {
- icon: string;
- menuItems: TrayMenuItem[];
-}
-export interface TrayMenuItem {
- id?: string;
- text: string;
- isDisabled?: boolean;
- isChecked?: boolean;
-}
-export type KnownPath = "config" | "data" | "cache" | "documents" | "pictures" | "music" | "video" | "downloads" | "savedGames1" | "savedGames2" | "temp";
-declare function execCommand(command: string, options?: ExecCommandOptions): Promise;
-declare function spawnProcess(command: string, options?: SpawnedProcessOptions): Promise;
-declare function updateSpawnedProcess(id: number, event: string, data?: any): Promise;
-declare function getSpawnedProcesses(): Promise;
-declare function getEnv(key: string): Promise;
-declare function getEnvs(): Promise;
-declare function showOpenDialog(title?: string, options?: OpenDialogOptions): Promise;
-declare function showFolderDialog(title?: string, options?: FolderDialogOptions): Promise;
-declare function showSaveDialog(title?: string, options?: SaveDialogOptions): Promise;
-declare function showNotification(title: string, content: string, icon?: Icon): Promise;
-declare function showMessageBox(title: string, content: string, choice?: MessageBoxChoice, icon?: Icon): Promise;
-declare function setTray(options: TrayOptions): Promise;
-declare function open$1(url: string): Promise;
-declare function getPath(name: KnownPath): Promise;
-export interface MemoryInfo {
- physical: {
- total: number;
- available: number;
- };
- virtual: {
- total: number;
- available: number;
- };
-}
-export interface KernelInfo {
- variant: string;
- version: string;
-}
-export interface OSInfo {
- name: string;
- description: string;
- version: string;
-}
-export interface CPUInfo {
- vendor: string;
- model: string;
- frequency: number;
- architecture: string;
- logicalThreads: number;
- physicalCores: number;
- physicalUnits: number;
-}
-export interface Display {
- id: number;
- resolution: Resolution;
- dpi: number;
- bpp: number;
- refreshRate: number;
-}
-export interface Resolution {
- width: number;
- height: number;
-}
-export interface MousePosition {
- x: number;
- y: number;
-}
-declare function getMemoryInfo(): Promise;
-declare function getArch(): Promise;
-declare function getKernelInfo(): Promise;
-declare function getOSInfo(): Promise;
-declare function getCPUInfo(): Promise;
-declare function getDisplays(): Promise;
-declare function getMousePosition(): Promise;
-declare function setData(key: string, data: string | null): Promise;
-declare function getData(key: string): Promise;
-declare function removeData(key: string): Promise;
-declare function getKeys(): Promise;
-declare function clear(): Promise;
-declare function log(message: string, type?: LoggerType): Promise;
-export interface OpenActionOptions {
- url: string;
-}
-export interface RestartOptions {
- args: string;
-}
-declare function exit(code?: number): Promise;
-declare function killProcess(): Promise;
-declare function restartProcess(options?: RestartOptions): Promise;
-declare function getConfig(): Promise;
-declare function broadcast(event: string, data?: any): Promise;
-declare function readProcessInput(readAll?: boolean): Promise;
-declare function writeProcessOutput(data: string): Promise;
-declare function writeProcessError(data: string): Promise;
-export interface WindowOptions extends WindowSizeOptions, WindowPosOptions {
- title?: string;
- icon?: string;
- fullScreen?: boolean;
- alwaysOnTop?: boolean;
- enableInspector?: boolean;
- borderless?: boolean;
- maximize?: boolean;
- hidden?: boolean;
- maximizable?: boolean;
- useSavedState?: boolean;
- exitProcessOnClose?: boolean;
- extendUserAgentWith?: string;
- injectGlobals?: boolean;
- injectClientLibrary?: boolean;
- injectScript?: string;
- processArgs?: string;
-}
-export interface WindowSizeOptions {
- width?: number;
- height?: number;
- minWidth?: number;
- minHeight?: number;
- maxWidth?: number;
- maxHeight?: number;
- resizable?: boolean;
-}
-export interface WindowPosOptions {
- x?: number;
- y?: number;
- center?: boolean;
-}
-export interface WindowMenu extends Array {
-}
-export interface WindowMenuItem {
- id?: string;
- text: string;
- action?: string;
- shortcut?: string;
- isDisabled?: boolean;
- isChecked?: boolean;
- menuItems?: WindowMenuItem[];
-}
-declare function setTitle(title: string): Promise;
-declare function getTitle(): Promise;
-declare function maximize(): Promise;
-declare function unmaximize(): Promise;
-declare function isMaximized(): Promise;
-declare function minimize(): Promise;
-declare function unminimize(): Promise;
-declare function isMinimized(): Promise;
-declare function setFullScreen(): Promise;
-declare function exitFullScreen(): Promise;
-declare function isFullScreen(): Promise;
-declare function show(): Promise;
-declare function hide(): Promise;
-declare function isVisible(): Promise;
-declare function focus$1(): Promise;
-declare function setIcon(icon: string): Promise;
-declare function move$1(x: number, y: number): Promise;
-declare function center(): Promise;
-declare function beginDrag(screenX?: number, screenY?: number): Promise;
-declare function setDraggableRegion(DOMElementOrId: string | HTMLElement, options?: {
- exclude?: Array;
-}): Promise<{
- success: true;
- message: string;
- exclusions: {
- add(elements: Array): void;
- remove(elements: Array): void;
- removeAll(): void;
- };
-}>;
-declare function unsetDraggableRegion(DOMElementOrId: string | HTMLElement): Promise<{
- success: true;
- message: string;
-}>;
-declare function setSize(options: WindowSizeOptions): Promise;
-declare function getSize(): Promise;
-declare function getPosition(): Promise;
-declare function setAlwaysOnTop(onTop: boolean): Promise;
-declare function setBorderless(borderless: boolean): Promise;
-declare function create(url: string, options?: WindowOptions): Promise;
-declare function snapshot(path: string): Promise;
-declare function setMainMenu(options: WindowMenu): Promise;
-declare function print$1(): Promise;
-interface Response$1 {
- success: boolean;
- message: string;
-}
-export type Builtin = "ready" | "trayMenuItemClicked" | "windowClose" | "serverOffline" | "clientConnect" | "clientDisconnect" | "appClientConnect" | "appClientDisconnect" | "extClientConnect" | "extClientDisconnect" | "extensionReady" | "neuDev_reloadApp";
-declare function on(event: string, handler: (ev: CustomEvent) => void): Promise;
-declare function off(event: string, handler: (ev: CustomEvent) => void): Promise;
-declare function dispatch(event: string, data?: any): Promise;
-declare function broadcast$1(event: string, data?: any): Promise;
-export interface ExtensionStats {
- loaded: string[];
- connected: string[];
-}
-declare function dispatch$1(extensionId: string, event: string, data?: any): Promise;
-declare function broadcast$2(event: string, data?: any): Promise;
-declare function getStats$1(): Promise;
-export interface Manifest {
- applicationId: string;
- version: string;
- resourcesURL: string;
-}
-declare function checkForUpdates(url: string): Promise;
-declare function install(): Promise;
-export interface ClipboardImage {
- width: number;
- height: number;
- bpp: number;
- bpr: number;
- redMask: number;
- greenMask: number;
- blueMask: number;
- redShift: number;
- greenShift: number;
- blueShift: number;
- data: ArrayBuffer;
-}
-declare function getFormat(): Promise;
-declare function readText(): Promise;
-declare function readImage(format?: string): Promise;
-declare function writeText(data: string): Promise;
-declare function writeImage(image: ClipboardImage): Promise;
-declare function readHTML(): Promise;
-declare function writeHTML(data: string): Promise;
-declare function clear$1(): Promise;
-interface Stats$1 {
- size: number;
- isFile: boolean;
- isDirectory: boolean;
-}
-declare function getFiles(): Promise;
-declare function getStats$2(path: string): Promise;
-declare function extractFile(path: string, destination: string): Promise;
-declare function extractDirectory(path: string, destination: string): Promise;
-declare function readFile$1(path: string): Promise;
-declare function readBinaryFile$1(path: string): Promise;
-declare function mount(path: string, target: string): Promise;
-declare function unmount(path: string): Promise;
-declare function getMounts(): Promise>;
-declare function getMethods(): Promise;
-export interface InitOptions {
- exportCustomMethods?: boolean;
-}
-export declare function init(options?: InitOptions): void;
-export type ErrorCode = "NE_FS_DIRCRER" | "NE_FS_RMDIRER" | "NE_FS_FILRDER" | "NE_FS_FILWRER" | "NE_FS_FILRMER" | "NE_FS_NOPATHE" | "NE_FS_COPYFER" | "NE_FS_MOVEFER" | "NE_OS_INVMSGA" | "NE_OS_INVKNPT" | "NE_ST_INVSTKY" | "NE_ST_STKEYWE" | "NE_RT_INVTOKN" | "NE_RT_NATPRME" | "NE_RT_APIPRME" | "NE_RT_NATRTER" | "NE_RT_NATNTIM" | "NE_CL_NSEROFF" | "NE_EX_EXTNOTC" | "NE_UP_CUPDMER" | "NE_UP_CUPDERR" | "NE_UP_UPDNOUF" | "NE_UP_UPDINER";
-interface Error$1 {
- code: ErrorCode;
- message: string;
-}
-declare global {
- interface Window {
- /** Mode of the application: window, browser, cloud, or chrome */
- NL_MODE: Mode;
- /** Application port */
- NL_PORT: number;
- /** Command-line arguments */
- NL_ARGS: string[];
- /** Basic authentication token */
- NL_TOKEN: string;
- /** Neutralinojs client version */
- NL_CVERSION: string;
- /** Application identifier */
- NL_APPID: string;
- /** Application version */
- NL_APPVERSION: string;
- /** Application path */
- NL_PATH: string;
- /** Application data path */
- NL_DATAPATH: string;
- /** Returns true if extensions are enabled */
- NL_EXTENABLED: boolean;
- /** Returns true if the client library is injected */
- NL_GINJECTED: boolean;
- /** Returns true if globals are injected */
- NL_CINJECTED: boolean;
- /** Operating system name: Linux, Windows, Darwin, FreeBSD, or Uknown */
- NL_OS: OperatingSystem;
- /** CPU architecture: x64, arm, itanium, ia32, or unknown */
- NL_ARCH: Architecture;
- /** Neutralinojs server version */
- NL_VERSION: string;
- /** Current working directory */
- NL_CWD: string;
- /** Identifier of the current process */
- NL_PID: string;
- /** Source of application resources: bundle or directory */
- NL_RESMODE: string;
- /** Release commit of the client library */
- NL_CCOMMIT: string;
- /** An array of custom methods */
- NL_CMETHODS: string[];
- }
- /** Neutralino global object for custom methods **/
- const Neutralino: any;
-}
-
-declare namespace custom {
- export { getMethods };
-}
-declare namespace filesystem {
- export { appendBinaryFile, appendFile, copy, createDirectory, createWatcher, getAbsolutePath, getJoinedPath, getNormalizedPath, getOpenedFileInfo, getPathParts, getPermissions, getRelativePath, getStats, getUnnormalizedPath, getWatchers, move, openFile, readBinaryFile, readDirectory, readFile, remove, removeWatcher, setPermissions, updateOpenedFile, writeBinaryFile, writeFile };
-}
-declare namespace os {
- export { execCommand, getEnv, getEnvs, getPath, getSpawnedProcesses, open$1 as open, setTray, showFolderDialog, showMessageBox, showNotification, showOpenDialog, showSaveDialog, spawnProcess, updateSpawnedProcess };
-}
-declare namespace computer {
- export { getArch, getCPUInfo, getDisplays, getKernelInfo, getMemoryInfo, getMousePosition, getOSInfo };
-}
-declare namespace storage {
- export { clear, getData, getKeys, removeData, setData };
-}
-declare namespace debug {
- export { log };
-}
-declare namespace app {
- export { broadcast, exit, getConfig, killProcess, readProcessInput, restartProcess, writeProcessError, writeProcessOutput };
-}
-declare namespace window$1 {
- export { beginDrag, center, create, exitFullScreen, focus$1 as focus, getPosition, getSize, getTitle, hide, isFullScreen, isMaximized, isMinimized, isVisible, maximize, minimize, move$1 as move, print$1 as print, setAlwaysOnTop, setBorderless, setDraggableRegion, setFullScreen, setIcon, setMainMenu, setSize, setTitle, show, snapshot, unmaximize, unminimize, unsetDraggableRegion };
-}
-declare namespace events {
- export { broadcast$1 as broadcast, dispatch, off, on };
-}
-declare namespace extensions {
- export { broadcast$2 as broadcast, dispatch$1 as dispatch, getStats$1 as getStats };
-}
-declare namespace updater {
- export { checkForUpdates, install };
-}
-declare namespace clipboard {
- export { clear$1 as clear, getFormat, readHTML, readImage, readText, writeHTML, writeImage, writeText };
-}
-declare namespace resources {
- export { extractDirectory, extractFile, getFiles, getStats$2 as getStats, readBinaryFile$1 as readBinaryFile, readFile$1 as readFile };
-}
-declare namespace server {
- export { getMounts, mount, unmount };
-}
-
-export {
- Error$1 as Error,
- Permissions$1 as Permissions,
- Response$1 as Response,
- app,
- clipboard,
- computer,
- custom,
- debug,
- events,
- extensions,
- filesystem,
- os,
- resources,
- server,
- storage,
- updater,
- window$1 as window,
-};
-
-export as namespace Neutralino;
-
-export {};
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
deleted file mode 100644
index 7ee9126..0000000
--- a/desktop-app/resources/js/script.js
+++ /dev/null
@@ -1,2835 +0,0 @@
-document.addEventListener("DOMContentLoaded", function () {
- let markdownRenderTimeout = null;
- const RENDER_DELAY = 100;
- let syncScrollingEnabled = true;
- let isEditorScrolling = false;
- let isPreviewScrolling = false;
- let scrollSyncTimeout = null;
- const SCROLL_SYNC_DELAY = 10;
-
- // View Mode State - Story 1.1
- let currentViewMode = 'split'; // 'editor', 'split', or 'preview'
-
- const markdownEditor = document.getElementById("markdown-editor");
- const markdownPreview = document.getElementById("markdown-preview");
- const themeToggle = document.getElementById("theme-toggle");
- const importFromFileButton = document.getElementById("import-from-file");
- const importFromGithubButton = document.getElementById("import-from-github");
- const fileInput = document.getElementById("file-input");
- const exportMd = document.getElementById("export-md");
- const exportHtml = document.getElementById("export-html");
- const exportPdf = document.getElementById("export-pdf");
- const copyMarkdownButton = document.getElementById("copy-markdown-button");
- const dropzone = document.getElementById("dropzone");
- const closeDropzoneBtn = document.getElementById("close-dropzone");
- const toggleSyncButton = document.getElementById("toggle-sync");
- const editorPane = document.getElementById("markdown-editor");
- const previewPane = document.querySelector(".preview-pane");
- const readingTimeElement = document.getElementById("reading-time");
- const wordCountElement = document.getElementById("word-count");
- const charCountElement = document.getElementById("char-count");
-
- // View Mode Elements - Story 1.1
- const contentContainer = document.querySelector(".content-container");
- const viewModeButtons = document.querySelectorAll(".view-mode-btn");
-
- // Mobile View Mode Elements - Story 1.4
- const mobileViewModeButtons = document.querySelectorAll(".mobile-view-mode-btn");
-
- // Resize Divider Elements - Story 1.3
- const resizeDivider = document.querySelector(".resize-divider");
- const editorPaneElement = document.querySelector(".editor-pane");
- const previewPaneElement = document.querySelector(".preview-pane");
- let isResizing = false;
- let editorWidthPercent = 50; // Default 50%
- const MIN_PANE_PERCENT = 20; // Minimum 20% width
-
- const mobileMenuToggle = document.getElementById("mobile-menu-toggle");
- const mobileMenuPanel = document.getElementById("mobile-menu-panel");
- const mobileMenuOverlay = document.getElementById("mobile-menu-overlay");
- const mobileCloseMenu = document.getElementById("close-mobile-menu");
- const mobileReadingTime = document.getElementById("mobile-reading-time");
- const mobileWordCount = document.getElementById("mobile-word-count");
- const mobileCharCount = document.getElementById("mobile-char-count");
- const mobileToggleSync = document.getElementById("mobile-toggle-sync");
- const mobileImportBtn = document.getElementById("mobile-import-button");
- const mobileImportGithubBtn = document.getElementById("mobile-import-github-button");
- const mobileExportMd = document.getElementById("mobile-export-md");
- const mobileExportHtml = document.getElementById("mobile-export-html");
- const mobileExportPdf = document.getElementById("mobile-export-pdf");
- const mobileCopyMarkdown = document.getElementById("mobile-copy-markdown");
- const mobileThemeToggle = document.getElementById("mobile-theme-toggle");
- const shareButton = document.getElementById("share-button");
- const mobileShareButton = document.getElementById("mobile-share-button");
- const githubImportModal = document.getElementById("github-import-modal");
- const githubImportTitle = document.getElementById("github-import-title");
- const githubImportUrlInput = document.getElementById("github-import-url");
- const githubImportFileSelect = document.getElementById("github-import-file-select");
- const githubImportError = document.getElementById("github-import-error");
- const githubImportCancelBtn = document.getElementById("github-import-cancel");
- const githubImportSubmitBtn = document.getElementById("github-import-submit");
-
- // Check dark mode preference first for proper initialization
- const prefersDarkMode =
- window.matchMedia &&
- window.matchMedia("(prefers-color-scheme: dark)").matches;
-
- document.documentElement.setAttribute(
- "data-theme",
- prefersDarkMode ? "dark" : "light"
- );
-
- themeToggle.innerHTML = prefersDarkMode
- ? ' '
- : ' ';
-
- const initMermaid = () => {
- const currentTheme = document.documentElement.getAttribute("data-theme");
- const mermaidTheme = currentTheme === "dark" ? "dark" : "default";
-
- mermaid.initialize({
- startOnLoad: false,
- theme: mermaidTheme,
- securityLevel: 'loose',
- flowchart: { useMaxWidth: true, htmlLabels: true },
- fontSize: 16
- });
- };
-
- try {
- initMermaid();
- } catch (e) {
- console.warn("Mermaid initialization failed:", e);
- }
-
- const markedOptions = {
- gfm: true,
- breaks: false,
- pedantic: false,
- sanitize: false,
- smartypants: false,
- xhtml: false,
- headerIds: true,
- mangle: false,
- };
-
- const renderer = new marked.Renderer();
- renderer.code = function (code, language) {
- if (language === 'mermaid') {
- const uniqueId = 'mermaid-diagram-' + Math.random().toString(36).substr(2, 9);
- return ``;
- }
-
- const validLanguage = hljs.getLanguage(language) ? language : "plaintext";
- const highlightedCode = hljs.highlight(code, {
- language: validLanguage,
- }).value;
- return `${highlightedCode} `;
- };
-
- marked.setOptions({
- ...markedOptions,
- renderer: renderer,
- });
-
- const sampleMarkdown = `# Welcome to Markdown Viewer
-
-## ✨ Key Features
-- **Live Preview** with GitHub styling
-- **Smart Import/Export** (MD, HTML, PDF)
-- **Mermaid Diagrams** for visual documentation
-- **LaTeX Math Support** for scientific notation
-- **Emoji Support** 😄 👍 🎉
-
-## 💻 Code with Syntax Highlighting
-\`\`\`javascript
- function renderMarkdown() {
- const markdown = markdownEditor.value;
- const html = marked.parse(markdown);
- const sanitizedHtml = DOMPurify.sanitize(html);
- markdownPreview.innerHTML = sanitizedHtml;
-
- // Syntax highlighting is handled automatically
- // during the parsing phase by the marked renderer.
- // Themes are applied instantly via CSS variables.
- }
-\`\`\`
-
-## 🧮 Mathematical Expressions
-Write complex formulas with LaTeX syntax:
-
-Inline equation: $$E = mc^2$$
-
-Display equations:
-$$\\frac{\\partial f}{\\partial x} = \\lim_{h \\to 0} \\frac{f(x+h) - f(x)}{h}$$
-
-$$\\sum_{i=1}^{n} i^2 = \\frac{n(n+1)(2n+1)}{6}$$
-
-## 📊 Mermaid Diagrams
-Create powerful visualizations directly in markdown:
-
-\`\`\`mermaid
-flowchart LR
- A[Start] --> B{Is it working?}
- B -->|Yes| C[Great!]
- B -->|No| D[Debug]
- C --> E[Deploy]
- D --> B
-\`\`\`
-
-### Sequence Diagram Example
-\`\`\`mermaid
-sequenceDiagram
- User->>Editor: Type markdown
- Editor->>Preview: Render content
- User->>Editor: Make changes
- Editor->>Preview: Update rendering
- User->>Export: Save as PDF
-\`\`\`
-
-## 📋 Task Management
-- [x] Create responsive layout
-- [x] Implement live preview with GitHub styling
-- [x] Add syntax highlighting for code blocks
-- [x] Support math expressions with LaTeX
-- [x] Enable mermaid diagrams
-
-## 🆚 Feature Comparison
-
-| Feature | Markdown Viewer (Ours) | Other Markdown Editors |
-|:-------------------------|:----------------------:|:-----------------------:|
-| Live Preview | ✅ GitHub-Styled | ✅ |
-| Sync Scrolling | ✅ Two-way | 🔄 Partial/None |
-| Mermaid Support | ✅ | ❌/Limited |
-| LaTeX Math Rendering | ✅ | ❌/Limited |
-
-### 📝 Multi-row Headers Support
-
-
-
-
- Document Type
- Support
-
-
- Markdown Viewer (Ours)
- Other Markdown Editors
-
-
-
-
- Technical Docs
- Full + Diagrams
- Limited/Basic
-
-
- Research Notes
- Full + Math
- Partial
-
-
- Developer Guides
- Full + Export Options
- Basic
-
-
-
-
-## 📝 Text Formatting Examples
-
-### Text Formatting
-
-Text can be formatted in various ways for ~~strikethrough~~, **bold**, *italic*, or ***bold italic***.
-
-For highlighting important information, use highlighted text or add underlines where appropriate.
-
-### Superscript and Subscript
-
-Chemical formulas: H2 O, CO2
-Mathematical notation: x2 , eiπ
-
-### Keyboard Keys
-
-Press Ctrl + B for bold text.
-
-### Abbreviations
-
-GUI
-API
-
-### Text Alignment
-
-
-Centered text for headings or important notices
-
-
-
-Right-aligned text (for dates, signatures, etc.)
-
-
-### **Lists**
-
-Create bullet points:
-* Item 1
-* Item 2
- * Nested item
- * Nested further
-
-### **Links and Images**
-
-Add a [link](https://github.com/ThisIs-Developer/Markdown-Viewer) to important resources.
-
-Embed an image:
-
-
-### **Blockquotes**
-
-Quote someone famous:
-> "The best way to predict the future is to invent it." - Alan Kay
-
----
-
-## 🛡️ Security Note
-
-This is a fully client-side application. Your content never leaves your browser and stays secure on your device.`;
-
- markdownEditor.value = sampleMarkdown;
-
- // ========================================
- // DOCUMENT TABS & SESSION MANAGEMENT
- // ========================================
-
- const STORAGE_KEY = 'markdownViewerTabs';
- const ACTIVE_TAB_KEY = 'markdownViewerActiveTab';
- const UNTITLED_COUNTER_KEY = 'markdownViewerUntitledCounter';
- let tabs = [];
- let activeTabId = null;
- let draggedTabId = null;
- let saveTabStateTimeout = null;
- let untitledCounter = 0;
-
- function loadTabsFromStorage() {
- try {
- return JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
- } catch (e) {
- return [];
- }
- }
-
- function saveTabsToStorage(tabsArr) {
- try {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(tabsArr));
- } catch (e) {
- console.warn('Failed to save tabs to localStorage:', e);
- }
- }
-
- function loadActiveTabId() {
- return localStorage.getItem(ACTIVE_TAB_KEY);
- }
-
- function saveActiveTabId(id) {
- localStorage.setItem(ACTIVE_TAB_KEY, id);
- }
-
- function loadUntitledCounter() {
- return parseInt(localStorage.getItem(UNTITLED_COUNTER_KEY) || '0', 10);
- }
-
- function saveUntitledCounter(val) {
- localStorage.setItem(UNTITLED_COUNTER_KEY, String(val));
- }
-
- function nextUntitledTitle() {
- untitledCounter += 1;
- saveUntitledCounter(untitledCounter);
- return 'Untitled ' + untitledCounter;
- }
-
- function createTab(content, title, viewMode) {
- if (content === undefined) content = '';
- if (title === undefined) title = null;
- if (viewMode === undefined) viewMode = 'split';
- return {
- id: 'tab_' + Date.now() + '_' + Math.random().toString(36).substring(2, 8),
- title: title || 'Untitled',
- content: content,
- scrollPos: 0,
- viewMode: viewMode,
- createdAt: Date.now()
- };
- }
-
- function renderTabBar(tabsArr, currentActiveTabId) {
- const tabList = document.getElementById('tab-list');
- if (!tabList) return;
- tabList.innerHTML = '';
- tabsArr.forEach(function(tab) {
- const item = document.createElement('div');
- item.className = 'tab-item' + (tab.id === currentActiveTabId ? ' active' : '');
- item.setAttribute('data-tab-id', tab.id);
- item.setAttribute('role', 'tab');
- item.setAttribute('aria-selected', tab.id === currentActiveTabId ? 'true' : 'false');
- item.setAttribute('draggable', 'true');
-
- const titleSpan = document.createElement('span');
- titleSpan.className = 'tab-title';
- titleSpan.textContent = tab.title || 'Untitled';
- titleSpan.title = tab.title || 'Untitled';
-
- // Three-dot menu button
- const menuBtn = document.createElement('button');
- menuBtn.className = 'tab-menu-btn';
- menuBtn.setAttribute('aria-label', 'File options');
- menuBtn.title = 'File options';
- menuBtn.innerHTML = '⋯';
-
- // Dropdown
- const dropdown = document.createElement('div');
- dropdown.className = 'tab-menu-dropdown';
- dropdown.innerHTML =
- '' +
- '' +
- '';
-
- menuBtn.appendChild(dropdown);
-
- menuBtn.addEventListener('click', function(e) {
- e.stopPropagation();
- // Close all other open dropdowns first
- document.querySelectorAll('.tab-menu-btn.open').forEach(function(btn) {
- if (btn !== menuBtn) btn.classList.remove('open');
- });
- menuBtn.classList.toggle('open');
- // Position the dropdown relative to the viewport so it escapes the
- // overflow scroll container on .tab-list
- if (menuBtn.classList.contains('open')) {
- var rect = menuBtn.getBoundingClientRect();
- dropdown.style.top = (rect.bottom + 4) + 'px';
- dropdown.style.right = (window.innerWidth - rect.right) + 'px';
- dropdown.style.left = 'auto';
- }
- });
-
- dropdown.querySelectorAll('.tab-menu-item').forEach(function(actionBtn) {
- actionBtn.addEventListener('click', function(e) {
- e.stopPropagation();
- menuBtn.classList.remove('open');
- const action = actionBtn.getAttribute('data-action');
- if (action === 'rename') renameTab(tab.id);
- else if (action === 'duplicate') duplicateTab(tab.id);
- else if (action === 'delete') deleteTab(tab.id);
- });
- });
-
- item.appendChild(titleSpan);
- item.appendChild(menuBtn);
-
- item.addEventListener('click', function() {
- switchTab(tab.id);
- });
-
- item.addEventListener('dragstart', function() {
- draggedTabId = tab.id;
- setTimeout(function() { item.classList.add('dragging'); }, 0);
- });
-
- item.addEventListener('dragend', function() {
- item.classList.remove('dragging');
- draggedTabId = null;
- });
-
- item.addEventListener('dragover', function(e) {
- e.preventDefault();
- item.classList.add('drag-over');
- });
-
- item.addEventListener('dragleave', function() {
- item.classList.remove('drag-over');
- });
-
- item.addEventListener('drop', function(e) {
- e.preventDefault();
- item.classList.remove('drag-over');
- if (!draggedTabId || draggedTabId === tab.id) return;
- const fromIdx = tabs.findIndex(function(t) { return t.id === draggedTabId; });
- const toIdx = tabs.findIndex(function(t) { return t.id === tab.id; });
- if (fromIdx === -1 || toIdx === -1) return;
- const moved = tabs.splice(fromIdx, 1)[0];
- tabs.splice(toIdx, 0, moved);
- saveTabsToStorage(tabs);
- renderTabBar(tabs, activeTabId);
- });
-
- tabList.appendChild(item);
- });
-
- // "+ Create" button at end of tab list
- const newBtn = document.createElement('button');
- newBtn.className = 'tab-new-btn';
- newBtn.title = 'New Tab (Ctrl+T)';
- newBtn.setAttribute('aria-label', 'Open new tab');
- newBtn.innerHTML = ' ';
- newBtn.addEventListener('click', function() { newTab(); });
- tabList.appendChild(newBtn);
-
- // Auto-scroll active tab into view
- const activeItem = tabList.querySelector('.tab-item.active');
- if (activeItem) {
- activeItem.scrollIntoView({ block: 'nearest', inline: 'nearest' });
- }
-
- renderMobileTabList(tabsArr, currentActiveTabId);
- }
-
- function renderMobileTabList(tabsArr, currentActiveTabId) {
- const mobileTabList = document.getElementById('mobile-tab-list');
- if (!mobileTabList) return;
- mobileTabList.innerHTML = '';
- tabsArr.forEach(function(tab) {
- const item = document.createElement('div');
- item.className = 'mobile-tab-item' + (tab.id === currentActiveTabId ? ' active' : '');
- item.setAttribute('role', 'tab');
- item.setAttribute('aria-selected', tab.id === currentActiveTabId ? 'true' : 'false');
- item.setAttribute('data-tab-id', tab.id);
-
- const titleSpan = document.createElement('span');
- titleSpan.className = 'mobile-tab-title';
- titleSpan.textContent = tab.title || 'Untitled';
- titleSpan.title = tab.title || 'Untitled';
-
- // Three-dot menu button (same as desktop)
- const menuBtn = document.createElement('button');
- menuBtn.className = 'tab-menu-btn';
- menuBtn.setAttribute('aria-label', 'File options');
- menuBtn.title = 'File options';
- menuBtn.innerHTML = '⋯';
-
- // Dropdown (same as desktop)
- const dropdown = document.createElement('div');
- dropdown.className = 'tab-menu-dropdown';
- dropdown.innerHTML =
- '' +
- '' +
- '';
-
- menuBtn.appendChild(dropdown);
-
- menuBtn.addEventListener('click', function(e) {
- e.stopPropagation();
- document.querySelectorAll('.tab-menu-btn.open').forEach(function(btn) {
- if (btn !== menuBtn) btn.classList.remove('open');
- });
- menuBtn.classList.toggle('open');
- if (menuBtn.classList.contains('open')) {
- const rect = menuBtn.getBoundingClientRect();
- dropdown.style.top = (rect.bottom + 4) + 'px';
- dropdown.style.right = (window.innerWidth - rect.right) + 'px';
- dropdown.style.left = 'auto';
- }
- });
-
- dropdown.querySelectorAll('.tab-menu-item').forEach(function(actionBtn) {
- actionBtn.addEventListener('click', function(e) {
- e.stopPropagation();
- menuBtn.classList.remove('open');
- const action = actionBtn.getAttribute('data-action');
- if (action === 'rename') {
- closeMobileMenu();
- renameTab(tab.id);
- } else if (action === 'duplicate') {
- duplicateTab(tab.id);
- closeMobileMenu();
- } else if (action === 'delete') {
- deleteTab(tab.id);
- }
- });
- });
-
- item.appendChild(titleSpan);
- item.appendChild(menuBtn);
-
- item.addEventListener('click', function() {
- switchTab(tab.id);
- closeMobileMenu();
- });
-
- mobileTabList.appendChild(item);
- });
- }
-
- // Close any open tab dropdown when clicking elsewhere in the document
- document.addEventListener('click', function() {
- document.querySelectorAll('.tab-menu-btn.open').forEach(function(btn) {
- btn.classList.remove('open');
- });
- });
-
- function saveCurrentTabState() {
- const tab = tabs.find(function(t) { return t.id === activeTabId; });
- if (!tab) return;
- tab.content = markdownEditor.value;
- tab.scrollPos = markdownEditor.scrollTop;
- tab.viewMode = currentViewMode || 'split';
- saveTabsToStorage(tabs);
- }
-
- function restoreViewMode(mode) {
- currentViewMode = null;
- setViewMode(mode || 'split');
- }
-
- function switchTab(tabId) {
- if (tabId === activeTabId) return;
- saveCurrentTabState();
- activeTabId = tabId;
- saveActiveTabId(activeTabId);
- const tab = tabs.find(function(t) { return t.id === tabId; });
- if (!tab) return;
- markdownEditor.value = tab.content;
- restoreViewMode(tab.viewMode);
- renderMarkdown();
- requestAnimationFrame(function() {
- markdownEditor.scrollTop = tab.scrollPos || 0;
- });
- renderTabBar(tabs, activeTabId);
- }
-
- function newTab(content, title) {
- if (content === undefined) content = '';
- if (tabs.length >= 20) {
- alert('Maximum of 20 tabs reached. Please close an existing tab to open a new one.');
- return;
- }
- if (!title) title = nextUntitledTitle();
- const tab = createTab(content, title);
- tabs.push(tab);
- switchTab(tab.id);
- markdownEditor.focus();
- }
-
- function closeTab(tabId) {
- const idx = tabs.findIndex(function(t) { return t.id === tabId; });
- if (idx === -1) return;
- tabs.splice(idx, 1);
- if (tabs.length === 0) {
- // Auto-create new "Untitled" when last tab is deleted
- const newT = createTab('', nextUntitledTitle());
- tabs.push(newT);
- activeTabId = newT.id;
- saveActiveTabId(activeTabId);
- markdownEditor.value = '';
- restoreViewMode('split');
- renderMarkdown();
- } else if (activeTabId === tabId) {
- const newIdx = Math.max(0, idx - 1);
- activeTabId = tabs[newIdx].id;
- saveActiveTabId(activeTabId);
- const newActiveTab = tabs[newIdx];
- markdownEditor.value = newActiveTab.content;
- restoreViewMode(newActiveTab.viewMode);
- renderMarkdown();
- requestAnimationFrame(function() {
- markdownEditor.scrollTop = newActiveTab.scrollPos || 0;
- });
- }
- saveTabsToStorage(tabs);
- renderTabBar(tabs, activeTabId);
- }
-
- function deleteTab(tabId) {
- closeTab(tabId);
- }
-
- function renameTab(tabId) {
- const tab = tabs.find(function(t) { return t.id === tabId; });
- if (!tab) return;
- const modal = document.getElementById('rename-modal');
- const input = document.getElementById('rename-modal-input');
- const confirmBtn = document.getElementById('rename-modal-confirm');
- const cancelBtn = document.getElementById('rename-modal-cancel');
- if (!modal || !input) return;
- input.value = tab.title;
- modal.style.display = 'flex';
- input.focus();
- input.select();
-
- function doRename() {
- const newName = input.value.trim();
- if (newName) {
- tab.title = newName;
- saveTabsToStorage(tabs);
- renderTabBar(tabs, activeTabId);
- }
- modal.style.display = 'none';
- cleanup();
- }
-
- function cleanup() {
- confirmBtn.removeEventListener('click', doRename);
- cancelBtn.removeEventListener('click', doCancel);
- input.removeEventListener('keydown', onKey);
- }
-
- function doCancel() {
- modal.style.display = 'none';
- cleanup();
- }
-
- function onKey(e) {
- if (e.key === 'Enter') doRename();
- else if (e.key === 'Escape') doCancel();
- }
-
- confirmBtn.addEventListener('click', doRename);
- cancelBtn.addEventListener('click', doCancel);
- input.addEventListener('keydown', onKey);
- }
-
- function duplicateTab(tabId) {
- const tab = tabs.find(function(t) { return t.id === tabId; });
- if (!tab) return;
- if (tabs.length >= 20) {
- alert('Maximum of 20 tabs reached. Please close an existing tab to open a new one.');
- return;
- }
- saveCurrentTabState();
- const dupTitle = tab.title + ' (copy)';
- const dup = createTab(tab.content, dupTitle, tab.viewMode);
- const idx = tabs.findIndex(function(t) { return t.id === tabId; });
- tabs.splice(idx + 1, 0, dup);
- switchTab(dup.id);
- }
-
- function resetAllTabs() {
- const modal = document.getElementById('reset-confirm-modal');
- const confirmBtn = document.getElementById('reset-modal-confirm');
- const cancelBtn = document.getElementById('reset-modal-cancel');
- if (!modal) return;
- modal.style.display = 'flex';
-
- function doReset() {
- modal.style.display = 'none';
- cleanup();
- tabs = [];
- untitledCounter = 0;
- saveUntitledCounter(0);
- const welcome = createTab(sampleMarkdown, 'Welcome to Markdown');
- tabs.push(welcome);
- activeTabId = welcome.id;
- saveActiveTabId(activeTabId);
- saveTabsToStorage(tabs);
- markdownEditor.value = sampleMarkdown;
- restoreViewMode('split');
- renderMarkdown();
- renderTabBar(tabs, activeTabId);
- }
-
- function doCancel() {
- modal.style.display = 'none';
- cleanup();
- }
-
- function cleanup() {
- confirmBtn.removeEventListener('click', doReset);
- cancelBtn.removeEventListener('click', doCancel);
- }
-
- confirmBtn.addEventListener('click', doReset);
- cancelBtn.addEventListener('click', doCancel);
- }
-
- function initTabs() {
- untitledCounter = loadUntitledCounter();
- tabs = loadTabsFromStorage();
- activeTabId = loadActiveTabId();
- if (tabs.length === 0) {
- const tab = createTab(sampleMarkdown, 'Welcome to Markdown');
- tabs.push(tab);
- activeTabId = tab.id;
- saveTabsToStorage(tabs);
- saveActiveTabId(activeTabId);
- } else if (!tabs.find(function(t) { return t.id === activeTabId; })) {
- activeTabId = tabs[0].id;
- saveActiveTabId(activeTabId);
- }
- const activeTab = tabs.find(function(t) { return t.id === activeTabId; });
- markdownEditor.value = activeTab.content;
- restoreViewMode(activeTab.viewMode);
- renderMarkdown();
- requestAnimationFrame(function() {
- markdownEditor.scrollTop = activeTab.scrollPos || 0;
- });
- renderTabBar(tabs, activeTabId);
- }
-
- function renderMarkdown() {
- try {
- const markdown = markdownEditor.value;
- const html = marked.parse(markdown);
- const sanitizedHtml = DOMPurify.sanitize(html, {
- ADD_TAGS: ['mjx-container'],
- ADD_ATTR: ['id', 'class', 'style']
- });
- markdownPreview.innerHTML = sanitizedHtml;
-
- processEmojis(markdownPreview);
-
- // Reinitialize mermaid with current theme before rendering diagrams
- initMermaid();
-
- try {
- const mermaidNodes = markdownPreview.querySelectorAll('.mermaid');
- if (mermaidNodes.length > 0) {
- Promise.resolve(mermaid.init(undefined, mermaidNodes))
- .then(() => addMermaidToolbars())
- .catch((e) => {
- console.warn("Mermaid rendering failed:", e);
- addMermaidToolbars();
- });
- }
- } catch (e) {
- console.warn("Mermaid rendering failed:", e);
- }
-
- if (window.MathJax) {
- try {
- MathJax.typesetPromise([markdownPreview]).catch((err) => {
- console.warn('MathJax typesetting failed:', err);
- });
- } catch (e) {
- console.warn("MathJax rendering failed:", e);
- }
- }
-
- updateDocumentStats();
- } catch (e) {
- console.error("Markdown rendering failed:", e);
- markdownPreview.innerHTML = `
- Error rendering markdown: ${e.message}
-
- ${markdownEditor.value} `;
- }
- }
-
- function importMarkdownFile(file) {
- const reader = new FileReader();
- reader.onload = function(e) {
- newTab(e.target.result, file.name.replace(/\.md$/i, ''));
- dropzone.style.display = "none";
- };
- reader.readAsText(file);
- }
-
- function isMarkdownPath(path) {
- return /\.(md|markdown)$/i.test(path || "");
- }
- const MAX_GITHUB_FILES_SHOWN = 30;
-
- function getFileName(path) {
- return (path || "").split("/").pop() || "document.md";
- }
-
- function buildRawGitHubUrl(owner, repo, ref, filePath) {
- const encodedPath = filePath
- .split("/")
- .map((part) => encodeURIComponent(part))
- .join("/");
- return `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(ref)}/${encodedPath}`;
- }
-
- async function fetchGitHubJson(url) {
- const response = await fetch(url, {
- headers: {
- Accept: "application/vnd.github+json"
- }
- });
- if (!response.ok) {
- throw new Error(`GitHub API request failed (${response.status})`);
- }
- return response.json();
- }
-
- async function fetchTextContent(url) {
- const response = await fetch(url);
- if (!response.ok) {
- throw new Error(`Failed to fetch file (${response.status})`);
- }
- return response.text();
- }
-
- function parseGitHubImportUrl(input) {
- let parsedUrl;
- try {
- parsedUrl = new URL((input || "").trim());
- } catch (_) {
- return null;
- }
-
- const host = parsedUrl.hostname.replace(/^www\./, "");
- const segments = parsedUrl.pathname.split("/").filter(Boolean);
-
- if (host === "raw.githubusercontent.com") {
- if (segments.length < 5) return null;
- const [owner, repo, ref, ...rest] = segments;
- const filePath = rest.join("/");
- return { owner, repo, ref, type: "file", filePath };
- }
-
- if (host !== "github.com" || segments.length < 2) return null;
-
- const owner = segments[0];
- const repo = segments[1].replace(/\.git$/i, "");
- if (segments.length === 2) {
- return { owner, repo, type: "repo" };
- }
-
- const mode = segments[2];
- if (mode === "blob" && segments.length >= 5) {
- return {
- owner,
- repo,
- type: "file",
- ref: segments[3],
- filePath: segments.slice(4).join("/")
- };
- }
-
- if (mode === "tree" && segments.length >= 4) {
- return {
- owner,
- repo,
- type: "tree",
- ref: segments[3],
- basePath: segments.slice(4).join("/")
- };
- }
-
- return { owner, repo, type: "repo" };
- }
-
- async function getDefaultBranch(owner, repo) {
- const repoInfo = await fetchGitHubJson(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`);
- return repoInfo.default_branch;
- }
-
- async function listMarkdownFiles(owner, repo, ref, basePath) {
- const treeResponse = await fetchGitHubJson(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/trees/${encodeURIComponent(ref)}?recursive=1`);
- const normalizedBasePath = (basePath || "").replace(/^\/+|\/+$/g, "");
-
- return (treeResponse.tree || [])
- .filter((entry) => entry.type === "blob" && isMarkdownPath(entry.path))
- .filter((entry) => !normalizedBasePath || entry.path === normalizedBasePath || entry.path.startsWith(normalizedBasePath + "/"))
- .map((entry) => entry.path)
- .sort((a, b) => a.localeCompare(b));
- }
-
- function setGitHubImportLoading(isLoading) {
- if (!githubImportSubmitBtn) return;
- if (isLoading) {
- githubImportSubmitBtn.dataset.loadingText = githubImportSubmitBtn.textContent;
- githubImportSubmitBtn.textContent = "Importing...";
- } else if (githubImportSubmitBtn.dataset.loadingText) {
- githubImportSubmitBtn.textContent = githubImportSubmitBtn.dataset.loadingText;
- delete githubImportSubmitBtn.dataset.loadingText;
- }
- }
-
- function setGitHubImportMessage(message, options = {}) {
- if (!githubImportError) return;
- const { isError = true } = options;
- githubImportError.classList.toggle("is-info", !isError);
- if (!message) {
- githubImportError.textContent = "";
- githubImportError.style.display = "none";
- return;
- }
- githubImportError.textContent = message;
- githubImportError.style.display = "block";
- }
-
- function resetGitHubImportModal() {
- if (!githubImportUrlInput || !githubImportFileSelect || !githubImportSubmitBtn) return;
- if (githubImportTitle) {
- githubImportTitle.textContent = "Import Markdown from GitHub";
- }
- githubImportUrlInput.value = "";
- githubImportUrlInput.style.display = "block";
- githubImportUrlInput.disabled = false;
- githubImportFileSelect.innerHTML = "";
- githubImportFileSelect.style.display = "none";
- githubImportFileSelect.disabled = false;
- githubImportSubmitBtn.dataset.step = "url";
- delete githubImportSubmitBtn.dataset.owner;
- delete githubImportSubmitBtn.dataset.repo;
- delete githubImportSubmitBtn.dataset.ref;
- githubImportSubmitBtn.textContent = "Import";
- setGitHubImportMessage("");
- }
-
- function openGitHubImportModal() {
- if (!githubImportModal || !githubImportUrlInput || !githubImportSubmitBtn) return;
- resetGitHubImportModal();
- githubImportModal.style.display = "flex";
- githubImportUrlInput.focus();
- }
-
- function closeGitHubImportModal() {
- if (!githubImportModal) return;
- githubImportModal.style.display = "none";
- resetGitHubImportModal();
- }
-
- async function handleGitHubImportSubmit() {
- if (!githubImportSubmitBtn || !githubImportUrlInput || !githubImportFileSelect) return;
- const setGitHubImportDialogDisabled = (disabled) => {
- githubImportSubmitBtn.disabled = disabled;
- if (githubImportCancelBtn) {
- githubImportCancelBtn.disabled = disabled;
- }
- };
- const step = githubImportSubmitBtn.dataset.step || "url";
- if (step === "select") {
- const selectedPath = githubImportFileSelect.value;
- const owner = githubImportSubmitBtn.dataset.owner;
- const repo = githubImportSubmitBtn.dataset.repo;
- const ref = githubImportSubmitBtn.dataset.ref;
- if (!owner || !repo || !ref || !selectedPath) {
- setGitHubImportMessage("Please select a file to import.");
- return;
- }
- setGitHubImportLoading(true);
- setGitHubImportDialogDisabled(true);
- try {
- const markdown = await fetchTextContent(buildRawGitHubUrl(owner, repo, ref, selectedPath));
- newTab(markdown, getFileName(selectedPath).replace(/\.(md|markdown)$/i, ""));
- closeGitHubImportModal();
- } catch (error) {
- console.error("GitHub import failed:", error);
- setGitHubImportMessage("GitHub import failed: " + error.message);
- } finally {
- setGitHubImportDialogDisabled(false);
- setGitHubImportLoading(false);
- }
- return;
- }
-
- const urlInput = githubImportUrlInput.value.trim();
- if (!urlInput) {
- setGitHubImportMessage("Please enter a GitHub URL.");
- return;
- }
-
- const parsed = parseGitHubImportUrl(urlInput);
- if (!parsed || !parsed.owner || !parsed.repo) {
- setGitHubImportMessage("Please enter a valid GitHub URL.");
- return;
- }
-
- setGitHubImportMessage("");
- setGitHubImportLoading(true);
- setGitHubImportDialogDisabled(true);
- try {
- if (parsed.type === "file") {
- if (!isMarkdownPath(parsed.filePath)) {
- throw new Error("The provided URL does not point to a Markdown file.");
- }
- const markdown = await fetchTextContent(buildRawGitHubUrl(parsed.owner, parsed.repo, parsed.ref, parsed.filePath));
- newTab(markdown, getFileName(parsed.filePath).replace(/\.(md|markdown)$/i, ""));
- closeGitHubImportModal();
- return;
- }
-
- const ref = parsed.ref || await getDefaultBranch(parsed.owner, parsed.repo);
- const files = await listMarkdownFiles(parsed.owner, parsed.repo, ref, parsed.basePath || "");
-
- if (!files.length) {
- setGitHubImportMessage("No Markdown files were found at that GitHub location.");
- return;
- }
-
- const shownFiles = files.slice(0, MAX_GITHUB_FILES_SHOWN);
- if (files.length === 1) {
- const targetPath = files[0];
- const markdown = await fetchTextContent(buildRawGitHubUrl(parsed.owner, parsed.repo, ref, targetPath));
- newTab(markdown, getFileName(targetPath).replace(/\.(md|markdown)$/i, ""));
- closeGitHubImportModal();
- return;
- }
-
- githubImportUrlInput.style.display = "none";
- githubImportFileSelect.style.display = "block";
- githubImportFileSelect.innerHTML = "";
- shownFiles.forEach((filePath) => {
- const option = document.createElement("option");
- option.value = filePath;
- option.textContent = filePath;
- githubImportFileSelect.appendChild(option);
- });
- if (files.length > MAX_GITHUB_FILES_SHOWN) {
- setGitHubImportMessage(`Showing first ${MAX_GITHUB_FILES_SHOWN} of ${files.length} Markdown files.`, { isError: false });
- } else {
- setGitHubImportMessage("");
- }
- if (githubImportTitle) {
- githubImportTitle.textContent = "Select a Markdown file to import";
- }
- githubImportSubmitBtn.dataset.step = "select";
- githubImportSubmitBtn.dataset.owner = parsed.owner;
- githubImportSubmitBtn.dataset.repo = parsed.repo;
- githubImportSubmitBtn.dataset.ref = ref;
- githubImportSubmitBtn.textContent = "Import Selected";
- } catch (error) {
- console.error("GitHub import failed:", error);
- setGitHubImportMessage("GitHub import failed: " + error.message);
- } finally {
- setGitHubImportDialogDisabled(false);
- setGitHubImportLoading(false);
- }
- }
-
- function processEmojis(element) {
- const walker = document.createTreeWalker(
- element,
- NodeFilter.SHOW_TEXT,
- null,
- false
- );
-
- const textNodes = [];
- let node;
- while ((node = walker.nextNode())) {
- let parent = node.parentNode;
- let isInCode = false;
- while (parent && parent !== element) {
- if (parent.tagName === 'PRE' || parent.tagName === 'CODE') {
- isInCode = true;
- break;
- }
- parent = parent.parentNode;
- }
-
- if (!isInCode && node.nodeValue.includes(':')) {
- textNodes.push(node);
- }
- }
-
- textNodes.forEach(textNode => {
- const text = textNode.nodeValue;
- const emojiRegex = /:([\w+-]+):/g;
-
- let match;
- let lastIndex = 0;
- let result = '';
- let hasEmoji = false;
-
- while ((match = emojiRegex.exec(text)) !== null) {
- const shortcode = match[1];
- const emoji = joypixels.shortnameToUnicode(`:${shortcode}:`);
-
- if (emoji !== `:${shortcode}:`) { // If conversion was successful
- hasEmoji = true;
- result += text.substring(lastIndex, match.index) + emoji;
- lastIndex = emojiRegex.lastIndex;
- } else {
- result += text.substring(lastIndex, emojiRegex.lastIndex);
- lastIndex = emojiRegex.lastIndex;
- }
- }
-
- if (hasEmoji) {
- result += text.substring(lastIndex);
- const span = document.createElement('span');
- span.innerHTML = result;
- textNode.parentNode.replaceChild(span, textNode);
- }
- });
- }
-
- function debouncedRender() {
- clearTimeout(markdownRenderTimeout);
- markdownRenderTimeout = setTimeout(renderMarkdown, RENDER_DELAY);
- }
-
- function updateDocumentStats() {
- const text = markdownEditor.value;
-
- const charCount = text.length;
- charCountElement.textContent = charCount.toLocaleString();
-
- const wordCount = text.trim() === "" ? 0 : text.trim().split(/\s+/).length;
- wordCountElement.textContent = wordCount.toLocaleString();
-
- const readingTimeMinutes = Math.ceil(wordCount / 200);
- readingTimeElement.textContent = readingTimeMinutes;
- }
-
- function syncEditorToPreview() {
- if (!syncScrollingEnabled || isPreviewScrolling) return;
-
- isEditorScrolling = true;
- clearTimeout(scrollSyncTimeout);
-
- scrollSyncTimeout = setTimeout(() => {
- const editorScrollRatio =
- editorPane.scrollTop /
- (editorPane.scrollHeight - editorPane.clientHeight);
- const previewScrollPosition =
- (previewPane.scrollHeight - previewPane.clientHeight) *
- editorScrollRatio;
-
- if (!isNaN(previewScrollPosition) && isFinite(previewScrollPosition)) {
- previewPane.scrollTop = previewScrollPosition;
- }
-
- setTimeout(() => {
- isEditorScrolling = false;
- }, 50);
- }, SCROLL_SYNC_DELAY);
- }
-
- function syncPreviewToEditor() {
- if (!syncScrollingEnabled || isEditorScrolling) return;
-
- isPreviewScrolling = true;
- clearTimeout(scrollSyncTimeout);
-
- scrollSyncTimeout = setTimeout(() => {
- const previewScrollRatio =
- previewPane.scrollTop /
- (previewPane.scrollHeight - previewPane.clientHeight);
- const editorScrollPosition =
- (editorPane.scrollHeight - editorPane.clientHeight) *
- previewScrollRatio;
-
- if (!isNaN(editorScrollPosition) && isFinite(editorScrollPosition)) {
- editorPane.scrollTop = editorScrollPosition;
- }
-
- setTimeout(() => {
- isPreviewScrolling = false;
- }, 50);
- }, SCROLL_SYNC_DELAY);
- }
-
- function toggleSyncScrolling() {
- syncScrollingEnabled = !syncScrollingEnabled;
- if (syncScrollingEnabled) {
- toggleSyncButton.innerHTML = ' Sync Off';
- toggleSyncButton.classList.add("sync-disabled");
- toggleSyncButton.classList.remove("sync-enabled");
- toggleSyncButton.classList.add("border-primary");
- } else {
- toggleSyncButton.innerHTML = ' Sync On';
- toggleSyncButton.classList.add("sync-enabled");
- toggleSyncButton.classList.remove("sync-disabled");
- toggleSyncButton.classList.remove("border-primary");
- }
- }
-
- // View Mode Functions - Story 1.1 & 1.2
- function setViewMode(mode) {
- if (mode === currentViewMode) return;
-
- const previousMode = currentViewMode;
- currentViewMode = mode;
-
- // Update content container class
- contentContainer.classList.remove('view-editor-only', 'view-preview-only', 'view-split');
- contentContainer.classList.add('view-' + (mode === 'editor' ? 'editor-only' : mode === 'preview' ? 'preview-only' : 'split'));
-
- // Update button active states (desktop)
- viewModeButtons.forEach(btn => {
- const btnMode = btn.getAttribute('data-mode');
- if (btnMode === mode) {
- btn.classList.add('active');
- btn.setAttribute('aria-pressed', 'true');
- } else {
- btn.classList.remove('active');
- btn.setAttribute('aria-pressed', 'false');
- }
- });
-
- // Story 1.4: Update mobile button active states
- mobileViewModeButtons.forEach(btn => {
- const btnMode = btn.getAttribute('data-mode');
- if (btnMode === mode) {
- btn.classList.add('active');
- btn.setAttribute('aria-pressed', 'true');
- } else {
- btn.classList.remove('active');
- btn.setAttribute('aria-pressed', 'false');
- }
- });
-
- // Story 1.2: Show/hide sync toggle based on view mode
- updateSyncToggleVisibility(mode);
-
- // Story 1.3: Handle pane widths when switching modes
- if (mode === 'split') {
- // Restore preserved pane widths when entering split mode
- applyPaneWidths();
- } else if (previousMode === 'split') {
- // Reset pane widths when leaving split mode
- resetPaneWidths();
- }
-
- // Re-render markdown when switching to a view that includes preview
- if (mode === 'split' || mode === 'preview') {
- renderMarkdown();
- }
- }
-
- // Story 1.2: Update sync toggle visibility
- function updateSyncToggleVisibility(mode) {
- const isSplitView = mode === 'split';
-
- // Desktop sync toggle
- if (toggleSyncButton) {
- toggleSyncButton.style.display = isSplitView ? '' : 'none';
- toggleSyncButton.setAttribute('aria-hidden', !isSplitView);
- }
-
- // Mobile sync toggle
- if (mobileToggleSync) {
- mobileToggleSync.style.display = isSplitView ? '' : 'none';
- mobileToggleSync.setAttribute('aria-hidden', !isSplitView);
- }
- }
-
- // Story 1.3: Resize Divider Functions
- function initResizer() {
- if (!resizeDivider) return;
-
- resizeDivider.addEventListener('mousedown', startResize);
- document.addEventListener('mousemove', handleResize);
- document.addEventListener('mouseup', stopResize);
-
- // Touch support for tablets (though disabled via CSS, keeping for future)
- resizeDivider.addEventListener('touchstart', startResizeTouch);
- document.addEventListener('touchmove', handleResizeTouch);
- document.addEventListener('touchend', stopResize);
- }
-
- function startResize(e) {
- if (currentViewMode !== 'split') return;
- e.preventDefault();
- isResizing = true;
- resizeDivider.classList.add('dragging');
- document.body.classList.add('resizing');
- }
-
- function startResizeTouch(e) {
- if (currentViewMode !== 'split') return;
- e.preventDefault();
- isResizing = true;
- resizeDivider.classList.add('dragging');
- document.body.classList.add('resizing');
- }
-
- function handleResize(e) {
- if (!isResizing) return;
-
- const containerRect = contentContainer.getBoundingClientRect();
- const containerWidth = containerRect.width;
- const mouseX = e.clientX - containerRect.left;
-
- // Calculate percentage
- let newEditorPercent = (mouseX / containerWidth) * 100;
-
- // Enforce minimum pane widths
- newEditorPercent = Math.max(MIN_PANE_PERCENT, Math.min(100 - MIN_PANE_PERCENT, newEditorPercent));
-
- editorWidthPercent = newEditorPercent;
- applyPaneWidths();
- }
-
- function handleResizeTouch(e) {
- if (!isResizing || !e.touches[0]) return;
-
- const containerRect = contentContainer.getBoundingClientRect();
- const containerWidth = containerRect.width;
- const touchX = e.touches[0].clientX - containerRect.left;
-
- let newEditorPercent = (touchX / containerWidth) * 100;
- newEditorPercent = Math.max(MIN_PANE_PERCENT, Math.min(100 - MIN_PANE_PERCENT, newEditorPercent));
-
- editorWidthPercent = newEditorPercent;
- applyPaneWidths();
- }
-
- function stopResize() {
- if (!isResizing) return;
- isResizing = false;
- resizeDivider.classList.remove('dragging');
- document.body.classList.remove('resizing');
- }
-
- function applyPaneWidths() {
- if (currentViewMode !== 'split') return;
-
- const previewPercent = 100 - editorWidthPercent;
- editorPaneElement.style.flex = `0 0 calc(${editorWidthPercent}% - 4px)`;
- previewPaneElement.style.flex = `0 0 calc(${previewPercent}% - 4px)`;
- }
-
- function resetPaneWidths() {
- editorPaneElement.style.flex = '';
- previewPaneElement.style.flex = '';
- }
-
- function openMobileMenu() {
- mobileMenuPanel.classList.add("active");
- mobileMenuOverlay.classList.add("active");
- }
- function closeMobileMenu() {
- mobileMenuPanel.classList.remove("active");
- mobileMenuOverlay.classList.remove("active");
- }
- mobileMenuToggle.addEventListener("click", openMobileMenu);
- mobileCloseMenu.addEventListener("click", closeMobileMenu);
- mobileMenuOverlay.addEventListener("click", closeMobileMenu);
-
- function updateMobileStats() {
- mobileCharCount.textContent = charCountElement.textContent;
- mobileWordCount.textContent = wordCountElement.textContent;
- mobileReadingTime.textContent = readingTimeElement.textContent;
- }
-
- const origUpdateStats = updateDocumentStats;
- updateDocumentStats = function() {
- origUpdateStats();
- updateMobileStats();
- };
-
- mobileToggleSync.addEventListener("click", () => {
- toggleSyncScrolling();
- if (syncScrollingEnabled) {
- mobileToggleSync.innerHTML = ' Sync Off';
- mobileToggleSync.classList.add("sync-disabled");
- mobileToggleSync.classList.remove("sync-enabled");
- mobileToggleSync.classList.add("border-primary");
- } else {
- mobileToggleSync.innerHTML = ' Sync On';
- mobileToggleSync.classList.add("sync-enabled");
- mobileToggleSync.classList.remove("sync-disabled");
- mobileToggleSync.classList.remove("border-primary");
- }
- });
- mobileImportBtn.addEventListener("click", () => fileInput.click());
- mobileImportGithubBtn.addEventListener("click", () => {
- closeMobileMenu();
- openGitHubImportModal();
- });
- mobileExportMd.addEventListener("click", () => exportMd.click());
- mobileExportHtml.addEventListener("click", () => exportHtml.click());
- mobileExportPdf.addEventListener("click", () => exportPdf.click());
- mobileCopyMarkdown.addEventListener("click", () => copyMarkdownButton.click());
- mobileThemeToggle.addEventListener("click", () => {
- themeToggle.click();
- mobileThemeToggle.innerHTML = themeToggle.innerHTML + " Toggle Dark Mode";
- });
-
- const mobileNewTabBtn = document.getElementById("mobile-new-tab-btn");
- if (mobileNewTabBtn) {
- mobileNewTabBtn.addEventListener("click", function() {
- newTab();
- closeMobileMenu();
- });
- }
-
- const mobileTabResetBtn = document.getElementById("mobile-tab-reset-btn");
- if (mobileTabResetBtn) {
- mobileTabResetBtn.addEventListener("click", function() {
- closeMobileMenu();
- resetAllTabs();
- });
- }
-
- initTabs();
- updateMobileStats();
-
- // Initialize resizer - Story 1.3
- initResizer();
-
- // View Mode Button Event Listeners - Story 1.1
- viewModeButtons.forEach(btn => {
- btn.addEventListener('click', function() {
- const mode = this.getAttribute('data-mode');
- setViewMode(mode);
- saveCurrentTabState();
- });
- });
-
- // Story 1.4: Mobile View Mode Button Event Listeners
- mobileViewModeButtons.forEach(btn => {
- btn.addEventListener('click', function() {
- const mode = this.getAttribute('data-mode');
- setViewMode(mode);
- saveCurrentTabState();
- closeMobileMenu();
- });
- });
-
- markdownEditor.addEventListener("input", function() {
- debouncedRender();
- clearTimeout(saveTabStateTimeout);
- saveTabStateTimeout = setTimeout(saveCurrentTabState, 500);
- });
-
- // Tab key handler to insert indentation instead of moving focus
- markdownEditor.addEventListener("keydown", function(e) {
- if (e.key === 'Tab') {
- e.preventDefault();
-
- const start = this.selectionStart;
- const end = this.selectionEnd;
- const value = this.value;
-
- // Insert 2 spaces
- const indent = ' '; // 2 spaces
-
- // Update textarea value
- this.value = value.substring(0, start) + indent + value.substring(end);
-
- // Update cursor position
- this.selectionStart = this.selectionEnd = start + indent.length;
-
- // Trigger input event to update preview
- this.dispatchEvent(new Event('input'));
- }
- });
-
- editorPane.addEventListener("scroll", syncEditorToPreview);
- previewPane.addEventListener("scroll", syncPreviewToEditor);
- toggleSyncButton.addEventListener("click", toggleSyncScrolling);
- themeToggle.addEventListener("click", function () {
- const theme =
- document.documentElement.getAttribute("data-theme") === "dark"
- ? "light"
- : "dark";
- document.documentElement.setAttribute("data-theme", theme);
-
- if (theme === "dark") {
- themeToggle.innerHTML = ' ';
- } else {
- themeToggle.innerHTML = ' ';
- }
-
- renderMarkdown();
- });
-
- if (importFromFileButton) {
- importFromFileButton.addEventListener("click", function (e) {
- e.preventDefault();
- fileInput.click();
- });
- }
-
- if (importFromGithubButton) {
- importFromGithubButton.addEventListener("click", function (e) {
- e.preventDefault();
- openGitHubImportModal();
- });
- }
-
- if (githubImportSubmitBtn) {
- githubImportSubmitBtn.addEventListener("click", handleGitHubImportSubmit);
- }
- if (githubImportCancelBtn) {
- githubImportCancelBtn.addEventListener("click", closeGitHubImportModal);
- }
- const handleGitHubImportInputKeydown = function(e) {
- if (e.key === "Enter") {
- e.preventDefault();
- handleGitHubImportSubmit();
- } else if (e.key === "Escape") {
- closeGitHubImportModal();
- }
- };
- if (githubImportUrlInput) {
- githubImportUrlInput.addEventListener("keydown", handleGitHubImportInputKeydown);
- }
- if (githubImportFileSelect) {
- githubImportFileSelect.addEventListener("keydown", handleGitHubImportInputKeydown);
- }
-
- fileInput.addEventListener("change", function (e) {
- const file = e.target.files[0];
- if (file) {
- importMarkdownFile(file);
- }
- this.value = "";
- });
-
- exportMd.addEventListener("click", function () {
- try {
- const blob = new Blob([markdownEditor.value], {
- type: "text/markdown;charset=utf-8",
- });
- saveAs(blob, "document.md");
- } catch (e) {
- console.error("Export failed:", e);
- alert("Export failed: " + e.message);
- }
- });
-
- exportHtml.addEventListener("click", function () {
- try {
- const markdown = markdownEditor.value;
- const html = marked.parse(markdown);
- const sanitizedHtml = DOMPurify.sanitize(html, {
- ADD_TAGS: ['mjx-container'],
- ADD_ATTR: ['id', 'class', 'style']
- });
- const isDarkTheme =
- document.documentElement.getAttribute("data-theme") === "dark";
- const cssTheme = isDarkTheme
- ? "https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown-dark.min.css"
- : "https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown.min.css";
- const fullHtml = `
-
-
-
-
- Markdown Export
-
-
-
-
-
- ${sanitizedHtml}
-
-
-`;
- const blob = new Blob([fullHtml], { type: "text/html;charset=utf-8" });
- saveAs(blob, "document.html");
- } catch (e) {
- console.error("HTML export failed:", e);
- alert("HTML export failed: " + e.message);
- }
- });
-
- // ============================================
- // Page-Break Detection Functions (Story 1.1)
- // ============================================
-
- // Page configuration constants for A4 PDF export
- const PAGE_CONFIG = {
- a4Width: 210, // mm
- a4Height: 297, // mm
- margin: 15, // mm each side
- contentWidth: 180, // 210 - 30 (margins)
- contentHeight: 267, // 297 - 30 (margins)
- windowWidth: 1000, // html2canvas config
- scale: 2 // html2canvas scale factor
- };
-
- /**
- * Task 1: Identifies all graphic elements that may need page-break handling
- * @param {HTMLElement} container - The container element to search within
- * @returns {Array} Array of {element, type} objects
- */
- function identifyGraphicElements(container) {
- const graphics = [];
-
- // Query for images
- container.querySelectorAll('img').forEach(el => {
- graphics.push({ element: el, type: 'img' });
- });
-
- // Query for SVGs (Mermaid diagrams)
- container.querySelectorAll('svg').forEach(el => {
- graphics.push({ element: el, type: 'svg' });
- });
-
- // Query for pre elements (code blocks)
- container.querySelectorAll('pre').forEach(el => {
- graphics.push({ element: el, type: 'pre' });
- });
-
- // Query for tables
- container.querySelectorAll('table').forEach(el => {
- graphics.push({ element: el, type: 'table' });
- });
-
- return graphics;
- }
-
- /**
- * Task 2: Calculates element positions relative to the container
- * @param {Array} elements - Array of {element, type} objects
- * @param {HTMLElement} container - The container element
- * @returns {Array} Array with position data added
- */
- function calculateElementPositions(elements, container) {
- const containerRect = container.getBoundingClientRect();
-
- return elements.map(item => {
- const rect = item.element.getBoundingClientRect();
- const top = rect.top - containerRect.top;
- const height = rect.height;
- const bottom = top + height;
-
- return {
- element: item.element,
- type: item.type,
- top: top,
- height: height,
- bottom: bottom
- };
- });
- }
-
- /**
- * Task 3: Calculates page boundary positions
- * @param {number} totalHeight - Total height of content in pixels
- * @param {number} elementWidth - Actual width of the rendered element in pixels
- * @param {Object} pageConfig - Page configuration object
- * @returns {Array} Array of y-coordinates where pages end
- */
- function calculatePageBoundaries(totalHeight, elementWidth, pageConfig) {
- // Calculate pixel height per page based on the element's actual width
- // This must match how PDF pagination will split the canvas
- // The aspect ratio of content area determines page height relative to width
- const aspectRatio = pageConfig.contentHeight / pageConfig.contentWidth;
- const pageHeightPx = elementWidth * aspectRatio;
-
- const boundaries = [];
- let y = pageHeightPx;
-
- while (y < totalHeight) {
- boundaries.push(y);
- y += pageHeightPx;
- }
-
- return { boundaries, pageHeightPx };
- }
-
- /**
- * Task 4: Detects which elements would be split across page boundaries
- * @param {Array} elements - Array of elements with position data
- * @param {Array} pageBoundaries - Array of page break y-coordinates
- * @returns {Array} Array of split elements with additional split info
- */
- function detectSplitElements(elements, pageBoundaries) {
- // Handle edge case: empty elements array
- if (!elements || elements.length === 0) {
- return [];
- }
-
- // Handle edge case: no page boundaries (single page)
- if (!pageBoundaries || pageBoundaries.length === 0) {
- return [];
- }
-
- const splitElements = [];
-
- for (const item of elements) {
- // Find which page the element starts on
- let startPage = 0;
- for (let i = 0; i < pageBoundaries.length; i++) {
- if (item.top >= pageBoundaries[i]) {
- startPage = i + 1;
- } else {
- break;
- }
- }
-
- // Find which page the element ends on
- let endPage = 0;
- for (let i = 0; i < pageBoundaries.length; i++) {
- if (item.bottom > pageBoundaries[i]) {
- endPage = i + 1;
- } else {
- break;
- }
- }
-
- // Element is split if it spans multiple pages
- if (endPage > startPage) {
- // Calculate overflow amount (how much crosses into next page)
- const boundaryY = pageBoundaries[startPage] || pageBoundaries[0];
- const overflowAmount = item.bottom - boundaryY;
-
- splitElements.push({
- element: item.element,
- type: item.type,
- top: item.top,
- height: item.height,
- splitPageIndex: startPage,
- overflowAmount: overflowAmount
- });
- }
- }
-
- return splitElements;
- }
-
- /**
- * Task 5: Main entry point for analyzing graphics for page breaks
- * @param {HTMLElement} tempElement - The rendered content container
- * @returns {Object} Analysis result with totalElements, splitElements, pageCount
- */
- function analyzeGraphicsForPageBreaks(tempElement) {
- try {
- // Step 1: Identify all graphic elements
- const graphics = identifyGraphicElements(tempElement);
- console.log('Step 1 - Graphics found:', graphics.length, graphics.map(g => g.type));
-
- // Step 2: Calculate positions for each element
- const elementsWithPositions = calculateElementPositions(graphics, tempElement);
- console.log('Step 2 - Element positions:', elementsWithPositions.map(e => ({
- type: e.type,
- top: Math.round(e.top),
- height: Math.round(e.height),
- bottom: Math.round(e.bottom)
- })));
-
- // Step 3: Calculate page boundaries using the element's ACTUAL width
- const totalHeight = tempElement.scrollHeight;
- const elementWidth = tempElement.offsetWidth;
- const { boundaries: pageBoundaries, pageHeightPx } = calculatePageBoundaries(
- totalHeight,
- elementWidth,
- PAGE_CONFIG
- );
-
- console.log('Step 3 - Page boundaries:', {
- elementWidth,
- totalHeight,
- pageHeightPx: Math.round(pageHeightPx),
- boundaries: pageBoundaries.map(b => Math.round(b))
- });
-
- // Step 4: Detect split elements
- const splitElements = detectSplitElements(elementsWithPositions, pageBoundaries);
- console.log('Step 4 - Split elements detected:', splitElements.length);
-
- // Calculate page count
- const pageCount = pageBoundaries.length + 1;
-
- return {
- totalElements: graphics.length,
- splitElements: splitElements,
- pageCount: pageCount,
- pageBoundaries: pageBoundaries,
- pageHeightPx: pageHeightPx
- };
- } catch (error) {
- console.error('Page-break analysis failed:', error);
- return {
- totalElements: 0,
- splitElements: [],
- pageCount: 1,
- pageBoundaries: [],
- pageHeightPx: 0
- };
- }
- }
-
- // ============================================
- // End Page-Break Detection Functions
- // ============================================
-
- // ============================================
- // Page-Break Insertion Functions (Story 1.2)
- // ============================================
-
- // Threshold for whitespace optimization (30% of page height)
- const PAGE_BREAK_THRESHOLD = 0.3;
-
- /**
- * Task 3: Categorizes split elements by whether they fit on a single page
- * @param {Array} splitElements - Array of split elements from detection
- * @param {number} pageHeightPx - Page height in pixels
- * @returns {Object} { fittingElements, oversizedElements }
- */
- function categorizeBySize(splitElements, pageHeightPx) {
- const fittingElements = [];
- const oversizedElements = [];
-
- for (const item of splitElements) {
- if (item.height <= pageHeightPx) {
- fittingElements.push(item);
- } else {
- oversizedElements.push(item);
- }
- }
-
- return { fittingElements, oversizedElements };
- }
-
- /**
- * Task 1: Inserts page breaks by adjusting margins for fitting elements
- * @param {Array} fittingElements - Elements that fit on a single page
- * @param {number} pageHeightPx - Page height in pixels
- */
- function insertPageBreaks(fittingElements, pageHeightPx) {
- for (const item of fittingElements) {
- // Calculate where the current page ends
- const currentPageBottom = (item.splitPageIndex + 1) * pageHeightPx;
-
- // Calculate remaining space on current page
- const remainingSpace = currentPageBottom - item.top;
- const remainingRatio = remainingSpace / pageHeightPx;
-
- console.log('Processing split element:', {
- type: item.type,
- top: Math.round(item.top),
- height: Math.round(item.height),
- splitPageIndex: item.splitPageIndex,
- currentPageBottom: Math.round(currentPageBottom),
- remainingSpace: Math.round(remainingSpace),
- remainingRatio: remainingRatio.toFixed(2)
- });
-
- // Task 4: Whitespace optimization
- // If remaining space is more than threshold and element almost fits, skip
- // (Will be handled by Story 1.3 scaling instead)
- if (remainingRatio > PAGE_BREAK_THRESHOLD) {
- const scaledHeight = item.height * 0.9; // 90% scale
- if (scaledHeight <= remainingSpace) {
- console.log(' -> Skipping (can fit with 90% scaling)');
- continue;
- }
- }
-
- // Calculate margin needed to push element to next page
- const marginNeeded = currentPageBottom - item.top + 5; // 5px buffer
-
- console.log(' -> Applying marginTop:', marginNeeded, 'px');
-
- // Determine which element to apply margin to
- // For SVG elements (Mermaid diagrams), apply to parent container for proper layout
- let targetElement = item.element;
- if (item.type === 'svg' && item.element.parentElement) {
- targetElement = item.element.parentElement;
- console.log(' -> Using parent element:', targetElement.tagName, targetElement.className);
- }
-
- // Apply margin to push element to next page
- const currentMargin = parseFloat(targetElement.style.marginTop) || 0;
- targetElement.style.marginTop = `${currentMargin + marginNeeded}px`;
-
- console.log(' -> Element after margin:', targetElement.tagName, 'marginTop =', targetElement.style.marginTop);
- }
- }
-
- /**
- * Task 2: Applies page breaks with cascading adjustment handling
- * @param {HTMLElement} tempElement - The rendered content container
- * @param {Object} pageConfig - Page configuration object (unused, kept for API compatibility)
- * @param {number} maxIterations - Maximum iterations to prevent infinite loops
- * @returns {Object} Final analysis result
- */
- function applyPageBreaksWithCascade(tempElement, pageConfig, maxIterations = 10) {
- let iteration = 0;
- let analysis;
- let previousSplitCount = -1;
-
- do {
- // Re-analyze after each adjustment
- analysis = analyzeGraphicsForPageBreaks(tempElement);
-
- // Use pageHeightPx from analysis (calculated from actual element width)
- const pageHeightPx = analysis.pageHeightPx;
-
- // Categorize elements by size
- const { fittingElements, oversizedElements } = categorizeBySize(
- analysis.splitElements,
- pageHeightPx
- );
-
- // Store oversized elements for Story 1.3
- analysis.oversizedElements = oversizedElements;
-
- // If no fitting elements need adjustment, we're done
- if (fittingElements.length === 0) {
- break;
- }
-
- // Check if we're making progress (prevent infinite loops)
- if (fittingElements.length === previousSplitCount) {
- console.warn('Page-break adjustment not making progress, stopping');
- break;
- }
- previousSplitCount = fittingElements.length;
-
- // Apply page breaks to fitting elements
- insertPageBreaks(fittingElements, pageHeightPx);
- iteration++;
-
- } while (iteration < maxIterations);
-
- if (iteration >= maxIterations) {
- console.warn('Page-break stabilization reached max iterations:', maxIterations);
- }
-
- console.log('Page-break cascade complete:', {
- iterations: iteration,
- finalSplitCount: analysis.splitElements.length,
- oversizedCount: analysis.oversizedElements ? analysis.oversizedElements.length : 0
- });
-
- return analysis;
- }
-
- // ============================================
- // End Page-Break Insertion Functions
- // ============================================
-
- // ============================================
- // Oversized Graphics Scaling Functions (Story 1.3)
- // ============================================
-
- // Minimum scale factor to maintain readability (50%)
- const MIN_SCALE_FACTOR = 0.5;
-
- /**
- * Task 1 & 2: Calculates scale factor with minimum enforcement
- * @param {number} elementHeight - Original height of element in pixels
- * @param {number} availableHeight - Available page height in pixels
- * @param {number} buffer - Small buffer to prevent edge overflow
- * @returns {Object} { scaleFactor, wasClampedToMin }
- */
- function calculateScaleFactor(elementHeight, availableHeight, buffer = 5) {
- const targetHeight = availableHeight - buffer;
- let scaleFactor = targetHeight / elementHeight;
- let wasClampedToMin = false;
-
- // Enforce minimum scale for readability
- if (scaleFactor < MIN_SCALE_FACTOR) {
- console.warn(
- `Warning: Large graphic requires ${(scaleFactor * 100).toFixed(0)}% scaling. ` +
- `Clamping to minimum ${MIN_SCALE_FACTOR * 100}%. Content may be cut off.`
- );
- scaleFactor = MIN_SCALE_FACTOR;
- wasClampedToMin = true;
- }
-
- return { scaleFactor, wasClampedToMin };
- }
-
- /**
- * Task 3: Applies CSS transform scaling to an element
- * @param {HTMLElement} element - The element to scale
- * @param {number} scaleFactor - Scale factor (0.5 = 50%)
- * @param {string} elementType - Type of element (svg, pre, img, table)
- */
- function applyGraphicScaling(element, scaleFactor, elementType) {
- // Get original dimensions before transform
- const originalHeight = element.offsetHeight;
-
- // Task 4: Handle SVG elements (Mermaid diagrams)
- if (elementType === 'svg') {
- // Remove max-width constraint that may interfere
- element.style.maxWidth = 'none';
- }
-
- // Apply CSS transform
- element.style.transform = `scale(${scaleFactor})`;
- element.style.transformOrigin = 'top left';
-
- // Calculate margin adjustment to collapse visual space
- const scaledHeight = originalHeight * scaleFactor;
- const marginAdjustment = originalHeight - scaledHeight;
-
- // Apply negative margin to pull subsequent content up
- element.style.marginBottom = `-${marginAdjustment}px`;
- }
-
- /**
- * Task 6: Handles all oversized elements by applying appropriate scaling
- * @param {Array} oversizedElements - Array of oversized element data
- * @param {number} pageHeightPx - Page height in pixels
- */
- function handleOversizedElements(oversizedElements, pageHeightPx) {
- if (!oversizedElements || oversizedElements.length === 0) {
- return;
- }
-
- let scaledCount = 0;
- let clampedCount = 0;
-
- for (const item of oversizedElements) {
- // Calculate required scale factor
- const { scaleFactor, wasClampedToMin } = calculateScaleFactor(
- item.height,
- pageHeightPx
- );
-
- // Apply scaling to the element
- applyGraphicScaling(item.element, scaleFactor, item.type);
-
- scaledCount++;
- if (wasClampedToMin) {
- clampedCount++;
- }
- }
-
- console.log('Oversized graphics scaling complete:', {
- totalScaled: scaledCount,
- clampedToMinimum: clampedCount
- });
- }
-
- // ============================================
- // End Oversized Graphics Scaling Functions
- // ============================================
-
- exportPdf.addEventListener("click", async function () {
- try {
- const originalText = exportPdf.innerHTML;
- exportPdf.innerHTML = ' Generating...';
- exportPdf.disabled = true;
-
- const progressContainer = document.createElement('div');
- progressContainer.style.position = 'fixed';
- progressContainer.style.top = '50%';
- progressContainer.style.left = '50%';
- progressContainer.style.transform = 'translate(-50%, -50%)';
- progressContainer.style.padding = '15px 20px';
- progressContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
- progressContainer.style.color = 'white';
- progressContainer.style.borderRadius = '5px';
- progressContainer.style.zIndex = '9999';
- progressContainer.style.textAlign = 'center';
-
- const statusText = document.createElement('div');
- statusText.textContent = 'Generating PDF...';
- progressContainer.appendChild(statusText);
- document.body.appendChild(progressContainer);
-
- const markdown = markdownEditor.value;
- const html = marked.parse(markdown);
- const sanitizedHtml = DOMPurify.sanitize(html, {
- ADD_TAGS: ['mjx-container', 'svg', 'path', 'g', 'marker', 'defs', 'pattern', 'clipPath'],
- ADD_ATTR: ['id', 'class', 'style', 'viewBox', 'd', 'fill', 'stroke', 'transform', 'marker-end', 'marker-start']
- });
-
- const tempElement = document.createElement("div");
- tempElement.className = "markdown-body pdf-export";
- tempElement.innerHTML = sanitizedHtml;
- tempElement.style.padding = "20px";
- tempElement.style.width = "210mm";
- tempElement.style.margin = "0 auto";
- tempElement.style.fontSize = "14px";
- tempElement.style.position = "fixed";
- tempElement.style.left = "-9999px";
- tempElement.style.top = "0";
-
- const currentTheme = document.documentElement.getAttribute("data-theme");
- tempElement.style.backgroundColor = currentTheme === "dark" ? "#0d1117" : "#ffffff";
- tempElement.style.color = currentTheme === "dark" ? "#c9d1d9" : "#24292e";
-
- document.body.appendChild(tempElement);
-
- await new Promise(resolve => setTimeout(resolve, 200));
-
- try {
- await mermaid.run({
- nodes: tempElement.querySelectorAll('.mermaid'),
- suppressErrors: true
- });
- } catch (mermaidError) {
- console.warn("Mermaid rendering issue:", mermaidError);
- }
-
- if (window.MathJax) {
- try {
- await MathJax.typesetPromise([tempElement]);
- } catch (mathJaxError) {
- console.warn("MathJax rendering issue:", mathJaxError);
- }
-
- // Hide MathJax assistive elements that cause duplicate text in PDF
- // These are screen reader elements that html2canvas captures as visible
- // Use multiple CSS properties to ensure html2canvas doesn't render them
- const assistiveElements = tempElement.querySelectorAll('mjx-assistive-mml');
- assistiveElements.forEach(el => {
- el.style.display = 'none';
- el.style.visibility = 'hidden';
- el.style.position = 'absolute';
- el.style.width = '0';
- el.style.height = '0';
- el.style.overflow = 'hidden';
- el.remove(); // Remove entirely from DOM
- });
-
- // Also hide any MathJax script elements that might contain source
- const mathScripts = tempElement.querySelectorAll('script[type*="math"], script[type*="tex"]');
- mathScripts.forEach(el => el.remove());
- }
-
- await new Promise(resolve => setTimeout(resolve, 500));
-
- // Analyze and apply page-breaks for graphics (Story 1.1 + 1.2)
- const pageBreakAnalysis = applyPageBreaksWithCascade(tempElement, PAGE_CONFIG);
-
- // Scale oversized graphics that can't fit on a single page (Story 1.3)
- if (pageBreakAnalysis.oversizedElements && pageBreakAnalysis.pageHeightPx) {
- handleOversizedElements(pageBreakAnalysis.oversizedElements, pageBreakAnalysis.pageHeightPx);
- }
-
- const pdfOptions = {
- orientation: 'portrait',
- unit: 'mm',
- format: 'a4',
- compress: true,
- hotfixes: ["px_scaling"]
- };
-
- const pdf = new jspdf.jsPDF(pdfOptions);
- const pageWidth = pdf.internal.pageSize.getWidth();
- const pageHeight = pdf.internal.pageSize.getHeight();
- const margin = 15;
- const contentWidth = pageWidth - (margin * 2);
-
- const canvas = await html2canvas(tempElement, {
- scale: 2,
- useCORS: true,
- allowTaint: true,
- logging: false,
- windowWidth: 1000,
- windowHeight: tempElement.scrollHeight
- });
-
- const scaleFactor = canvas.width / contentWidth;
- const imgHeight = canvas.height / scaleFactor;
- const pagesCount = Math.ceil(imgHeight / (pageHeight - margin * 2));
-
- for (let page = 0; page < pagesCount; page++) {
- if (page > 0) pdf.addPage();
-
- const sourceY = page * (pageHeight - margin * 2) * scaleFactor;
- const sourceHeight = Math.min(canvas.height - sourceY, (pageHeight - margin * 2) * scaleFactor);
- const destHeight = sourceHeight / scaleFactor;
-
- const pageCanvas = document.createElement('canvas');
- pageCanvas.width = canvas.width;
- pageCanvas.height = sourceHeight;
-
- const ctx = pageCanvas.getContext('2d');
- ctx.drawImage(canvas, 0, sourceY, canvas.width, sourceHeight, 0, 0, canvas.width, sourceHeight);
-
- const imgData = pageCanvas.toDataURL('image/png');
- pdf.addImage(imgData, 'PNG', margin, margin, contentWidth, destHeight);
- }
-
- pdf.save("document.pdf");
-
- statusText.textContent = 'Download successful!';
- setTimeout(() => {
- document.body.removeChild(progressContainer);
- }, 1500);
-
- document.body.removeChild(tempElement);
- exportPdf.innerHTML = originalText;
- exportPdf.disabled = false;
-
- } catch (error) {
- console.error("PDF export failed:", error);
- alert("PDF export failed: " + error.message);
- exportPdf.innerHTML = ' Export';
- exportPdf.disabled = false;
-
- const progressContainer = document.querySelector('div[style*="Preparing PDF"]');
- if (progressContainer) {
- document.body.removeChild(progressContainer);
- }
- }
- });
-
- copyMarkdownButton.addEventListener("click", function () {
- try {
- const markdownText = markdownEditor.value;
- copyToClipboard(markdownText);
- } catch (e) {
- console.error("Copy failed:", e);
- alert("Failed to copy Markdown: " + e.message);
- }
- });
-
- async function copyToClipboard(text) {
- try {
- if (navigator.clipboard && window.isSecureContext) {
- await navigator.clipboard.writeText(text);
- showCopiedMessage();
- } else {
- const textArea = document.createElement("textarea");
- textArea.value = text;
- textArea.style.position = "fixed";
- textArea.style.opacity = "0";
- document.body.appendChild(textArea);
- textArea.focus();
- textArea.select();
- const successful = document.execCommand("copy");
- document.body.removeChild(textArea);
- if (successful) {
- showCopiedMessage();
- } else {
- throw new Error("Copy command was unsuccessful");
- }
- }
- } catch (err) {
- console.error("Copy failed:", err);
- alert("Failed to copy HTML: " + err.message);
- }
- }
-
- function showCopiedMessage() {
- const originalText = copyMarkdownButton.innerHTML;
- copyMarkdownButton.innerHTML = ' Copied!';
-
- setTimeout(() => {
- copyMarkdownButton.innerHTML = originalText;
- }, 2000);
- }
-
- // ============================================
- // Share via URL (pako compression + base64url)
- // ============================================
-
- const MAX_SHARE_URL_LENGTH = 32000;
-
- function encodeMarkdownForShare(text) {
- const compressed = pako.deflate(new TextEncoder().encode(text));
- const chunkSize = 0x8000;
- let binary = '';
- for (let i = 0; i < compressed.length; i += chunkSize) {
- binary += String.fromCharCode.apply(null, compressed.subarray(i, i + chunkSize));
- }
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
- }
-
- function decodeMarkdownFromShare(encoded) {
- const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
- const binary = atob(base64);
- const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
- return new TextDecoder().decode(pako.inflate(bytes));
- }
-
- function copyShareUrl(btn) {
- const markdownText = markdownEditor.value;
- let encoded;
- try {
- encoded = encodeMarkdownForShare(markdownText);
- } catch (e) {
- console.error("Share encoding failed:", e);
- alert("Failed to encode content for sharing: " + e.message);
- return;
- }
-
- const shareUrl = window.location.origin + window.location.pathname + '#share=' + encoded;
- const tooLarge = shareUrl.length > MAX_SHARE_URL_LENGTH;
-
- const originalHTML = btn.innerHTML;
- const copiedHTML = ' Copied!';
-
- function onCopied() {
- if (!tooLarge) {
- window.location.hash = 'share=' + encoded;
- }
- btn.innerHTML = copiedHTML;
- setTimeout(() => { btn.innerHTML = originalHTML; }, 2000);
- }
-
- if (navigator.clipboard && window.isSecureContext) {
- navigator.clipboard.writeText(shareUrl).then(onCopied).catch(() => {
- // clipboard.writeText failed; nothing further to do in secure context
- });
- } else {
- try {
- const tempInput = document.createElement("textarea");
- tempInput.value = shareUrl;
- document.body.appendChild(tempInput);
- tempInput.select();
- document.execCommand("copy");
- document.body.removeChild(tempInput);
- onCopied();
- } catch (_) {
- // copy failed silently
- }
- }
- }
-
- shareButton.addEventListener("click", function () { copyShareUrl(shareButton); });
- mobileShareButton.addEventListener("click", function () { copyShareUrl(mobileShareButton); });
-
- function loadFromShareHash() {
- if (typeof pako === 'undefined') return;
- const hash = window.location.hash;
- if (!hash.startsWith('#share=')) return;
- const encoded = hash.slice('#share='.length);
- if (!encoded) return;
- try {
- const decoded = decodeMarkdownFromShare(encoded);
- markdownEditor.value = decoded;
- renderMarkdown();
- saveCurrentTabState();
- } catch (e) {
- console.error("Failed to load shared content:", e);
- alert("The shared URL could not be decoded. It may be corrupted or incomplete.");
- }
- }
-
- loadFromShareHash();
-
- const dropEvents = ["dragenter", "dragover", "dragleave", "drop"];
-
- dropEvents.forEach((eventName) => {
- dropzone.addEventListener(eventName, preventDefaults, false);
- document.body.addEventListener(eventName, preventDefaults, false);
- });
-
- function preventDefaults(e) {
- e.preventDefault();
- e.stopPropagation();
- }
-
- ["dragenter", "dragover"].forEach((eventName) => {
- dropzone.addEventListener(eventName, highlight, false);
- });
-
- ["dragleave", "drop"].forEach((eventName) => {
- dropzone.addEventListener(eventName, unhighlight, false);
- });
-
- function highlight() {
- dropzone.classList.add("active");
- }
-
- function unhighlight() {
- dropzone.classList.remove("active");
- }
-
- dropzone.addEventListener("drop", handleDrop, false);
- dropzone.addEventListener("click", function (e) {
- if (e.target !== closeDropzoneBtn && !closeDropzoneBtn.contains(e.target)) {
- fileInput.click();
- }
- });
- closeDropzoneBtn.addEventListener("click", function(e) {
- e.stopPropagation();
- dropzone.style.display = "none";
- });
-
- function handleDrop(e) {
- const dt = e.dataTransfer;
- const files = dt.files;
- if (files.length) {
- const file = files[0];
- const isMarkdownFile =
- file.type === "text/markdown" ||
- file.name.endsWith(".md") ||
- file.name.endsWith(".markdown");
- if (isMarkdownFile) {
- importMarkdownFile(file);
- } else {
- alert("Please upload a Markdown file (.md or .markdown)");
- }
- }
- }
-
- document.addEventListener("keydown", function (e) {
- if ((e.ctrlKey || e.metaKey) && e.key === "s") {
- e.preventDefault();
- exportMd.click();
- }
- if ((e.ctrlKey || e.metaKey) && e.key === "c") {
- const activeEl = document.activeElement;
- const isTextControl = activeEl && (activeEl.tagName === "TEXTAREA" || activeEl.tagName === "INPUT");
- const hasSelection = window.getSelection && window.getSelection().toString().trim().length > 0;
- if (!isTextControl && !hasSelection) {
- e.preventDefault();
- copyMarkdownButton.click();
- }
- }
- // Story 1.2: Only allow sync toggle shortcut when in split view
- if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "S") {
- e.preventDefault();
- if (currentViewMode === 'split') {
- toggleSyncScrolling();
- }
- }
- // New tab
- if ((e.ctrlKey || e.metaKey) && e.key === "t") {
- e.preventDefault();
- newTab();
- }
- // Close tab
- if ((e.ctrlKey || e.metaKey) && e.key === "w") {
- e.preventDefault();
- closeTab(activeTabId);
- }
- // Close Mermaid zoom modal with Escape
- if (e.key === "Escape") {
- closeMermaidModal();
- }
- });
-
- document.getElementById('tab-reset-btn').addEventListener('click', function() {
- resetAllTabs();
- });
-
- // ========================================
- // MERMAID DIAGRAM TOOLBAR
- // ========================================
-
- /**
- * Serialises an SVG element to a data URL suitable for use as an image source.
- * Inline styles and dimensions are preserved so the PNG matches the rendered diagram.
- */
- function svgToDataUrl(svgEl) {
- const clone = svgEl.cloneNode(true);
- // Ensure explicit width/height so the canvas has the right dimensions
- const bbox = svgEl.getBoundingClientRect();
- if (!clone.getAttribute('width')) clone.setAttribute('width', Math.round(bbox.width));
- if (!clone.getAttribute('height')) clone.setAttribute('height', Math.round(bbox.height));
- const serialized = new XMLSerializer().serializeToString(clone);
- return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(serialized);
- }
-
- /**
- * Renders an SVG element onto a canvas and resolves with the canvas.
- */
- function svgToCanvas(svgEl) {
- return new Promise((resolve, reject) => {
- const bbox = svgEl.getBoundingClientRect();
- const scale = window.devicePixelRatio || 1;
- const width = Math.max(Math.round(bbox.width), 1);
- const height = Math.max(Math.round(bbox.height), 1);
-
- const canvas = document.createElement('canvas');
- canvas.width = width * scale;
- canvas.height = height * scale;
- const ctx = canvas.getContext('2d');
- ctx.scale(scale, scale);
-
- // Fill background matching current theme using the CSS variable value
- const bgColor = getComputedStyle(document.documentElement)
- .getPropertyValue('--bg-color').trim() || '#ffffff';
- ctx.fillStyle = bgColor;
- ctx.fillRect(0, 0, width, height);
-
- const img = new Image();
- img.onload = () => { ctx.drawImage(img, 0, 0, width, height); resolve(canvas); };
- img.onerror = reject;
- img.src = svgToDataUrl(svgEl);
- });
- }
-
- /** Downloads the diagram in the given container as a PNG file. */
- async function downloadMermaidPng(container, btn) {
- const svgEl = container.querySelector('svg');
- if (!svgEl) return;
- const original = btn.innerHTML;
- btn.innerHTML = ' ';
- try {
- const canvas = await svgToCanvas(svgEl);
- canvas.toBlob(blob => {
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `diagram-${Date.now()}.png`;
- a.click();
- URL.revokeObjectURL(url);
- btn.innerHTML = ' ';
- setTimeout(() => { btn.innerHTML = original; }, 1500);
- }, 'image/png');
- } catch (e) {
- console.error('Mermaid PNG export failed:', e);
- btn.innerHTML = original;
- }
- }
-
- /** Copies the diagram in the given container as a PNG image to the clipboard. */
- async function copyMermaidImage(container, btn) {
- const svgEl = container.querySelector('svg');
- if (!svgEl) return;
- const original = btn.innerHTML;
- btn.innerHTML = ' ';
- try {
- const canvas = await svgToCanvas(svgEl);
- canvas.toBlob(async blob => {
- try {
- await navigator.clipboard.write([
- new ClipboardItem({ 'image/png': blob })
- ]);
- btn.innerHTML = ' Copied!';
- } catch (clipErr) {
- console.error('Clipboard write failed:', clipErr);
- btn.innerHTML = ' ';
- }
- setTimeout(() => { btn.innerHTML = original; }, 1800);
- }, 'image/png');
- } catch (e) {
- console.error('Mermaid copy failed:', e);
- btn.innerHTML = original;
- }
- }
-
- /** Downloads the SVG source of a diagram. */
- function downloadMermaidSvg(container, btn) {
- const svgEl = container.querySelector('svg');
- if (!svgEl) return;
- const clone = svgEl.cloneNode(true);
- const serialized = new XMLSerializer().serializeToString(clone);
- const blob = new Blob([serialized], { type: 'image/svg+xml' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `diagram-${Date.now()}.svg`;
- a.click();
- URL.revokeObjectURL(url);
- const original = btn.innerHTML;
- btn.innerHTML = ' ';
- setTimeout(() => { btn.innerHTML = original; }, 1500);
- }
-
- // ---- Zoom modal state ----
- let modalZoomScale = 1;
- let modalPanX = 0;
- let modalPanY = 0;
- let modalIsDragging = false;
- let modalDragStart = { x: 0, y: 0 };
- let modalCurrentSvgEl = null;
-
- const mermaidZoomModal = document.getElementById('mermaid-zoom-modal');
- const mermaidModalDiagram = document.getElementById('mermaid-modal-diagram');
-
- function applyModalTransform() {
- if (modalCurrentSvgEl) {
- modalCurrentSvgEl.style.transform =
- `translate(${modalPanX}px, ${modalPanY}px) scale(${modalZoomScale})`;
- }
- }
-
- function closeMermaidModal() {
- if (!mermaidZoomModal.classList.contains('active')) return;
- mermaidZoomModal.classList.remove('active');
- mermaidModalDiagram.innerHTML = '';
- modalCurrentSvgEl = null;
- modalZoomScale = 1;
- modalPanX = 0;
- modalPanY = 0;
- }
-
- /** Opens the zoom modal with the SVG from the given container. */
- function openMermaidZoomModal(container) {
- const svgEl = container.querySelector('svg');
- if (!svgEl) return;
-
- mermaidModalDiagram.innerHTML = '';
- modalZoomScale = 1;
- modalPanX = 0;
- modalPanY = 0;
-
- const svgClone = svgEl.cloneNode(true);
- // Remove fixed dimensions so it sizes naturally inside the modal
- svgClone.removeAttribute('width');
- svgClone.removeAttribute('height');
- svgClone.style.width = 'auto';
- svgClone.style.height = 'auto';
- svgClone.style.maxWidth = '80vw';
- svgClone.style.maxHeight = '60vh';
- svgClone.style.transformOrigin = 'center';
- mermaidModalDiagram.appendChild(svgClone);
- modalCurrentSvgEl = svgClone;
-
- mermaidZoomModal.classList.add('active');
- }
-
- // Modal close button
- document.getElementById('mermaid-modal-close').addEventListener('click', closeMermaidModal);
- // Click backdrop to close
- mermaidZoomModal.addEventListener('click', function(e) {
- if (e.target === mermaidZoomModal) closeMermaidModal();
- });
-
- // Zoom controls
- document.getElementById('mermaid-modal-zoom-in').addEventListener('click', () => {
- modalZoomScale = Math.min(modalZoomScale + 0.25, 10);
- applyModalTransform();
- });
- document.getElementById('mermaid-modal-zoom-out').addEventListener('click', () => {
- modalZoomScale = Math.max(modalZoomScale - 0.25, 0.1);
- applyModalTransform();
- });
- document.getElementById('mermaid-modal-zoom-reset').addEventListener('click', () => {
- modalZoomScale = 1; modalPanX = 0; modalPanY = 0;
- applyModalTransform();
- });
-
- // Mouse-wheel zoom inside modal
- mermaidModalDiagram.addEventListener('wheel', function(e) {
- e.preventDefault();
- const delta = e.deltaY < 0 ? 0.15 : -0.15;
- modalZoomScale = Math.min(Math.max(modalZoomScale + delta, 0.1), 10);
- applyModalTransform();
- }, { passive: false });
-
- // Drag to pan inside modal
- mermaidModalDiagram.addEventListener('mousedown', function(e) {
- modalIsDragging = true;
- modalDragStart = { x: e.clientX - modalPanX, y: e.clientY - modalPanY };
- mermaidModalDiagram.classList.add('dragging');
- });
- document.addEventListener('mousemove', function(e) {
- if (!modalIsDragging) return;
- modalPanX = e.clientX - modalDragStart.x;
- modalPanY = e.clientY - modalDragStart.y;
- applyModalTransform();
- });
- document.addEventListener('mouseup', function() {
- if (modalIsDragging) {
- modalIsDragging = false;
- mermaidModalDiagram.classList.remove('dragging');
- }
- });
-
- // Modal download buttons (operate on the currently displayed SVG)
- document.getElementById('mermaid-modal-download-png').addEventListener('click', async function() {
- if (!modalCurrentSvgEl) return;
- const btn = this;
- const original = btn.innerHTML;
- btn.innerHTML = ' ';
- try {
- // Use the original SVG (with dimensions) for proper PNG rendering
- const canvas = await svgToCanvas(modalCurrentSvgEl);
- canvas.toBlob(blob => {
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url; a.download = `diagram-${Date.now()}.png`; a.click();
- URL.revokeObjectURL(url);
- btn.innerHTML = ' ';
- setTimeout(() => { btn.innerHTML = original; }, 1500);
- }, 'image/png');
- } catch (e) {
- console.error('Modal PNG export failed:', e);
- btn.innerHTML = original;
- }
- });
-
- document.getElementById('mermaid-modal-copy').addEventListener('click', async function() {
- if (!modalCurrentSvgEl) return;
- const btn = this;
- const original = btn.innerHTML;
- btn.innerHTML = ' ';
- try {
- const canvas = await svgToCanvas(modalCurrentSvgEl);
- canvas.toBlob(async blob => {
- try {
- await navigator.clipboard.write([
- new ClipboardItem({ 'image/png': blob })
- ]);
- btn.innerHTML = ' Copied!';
- } catch (clipErr) {
- console.error('Clipboard write failed:', clipErr);
- btn.innerHTML = ' ';
- }
- setTimeout(() => { btn.innerHTML = original; }, 1800);
- }, 'image/png');
- } catch (e) {
- console.error('Modal copy failed:', e);
- btn.innerHTML = original;
- }
- });
-
- document.getElementById('mermaid-modal-download-svg').addEventListener('click', function() {
- if (!modalCurrentSvgEl) return;
- const serialized = new XMLSerializer().serializeToString(modalCurrentSvgEl);
- const blob = new Blob([serialized], { type: 'image/svg+xml' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url; a.download = `diagram-${Date.now()}.svg`; a.click();
- URL.revokeObjectURL(url);
- });
-
- /**
- * Adds the hover toolbar to every rendered Mermaid container.
- * Safe to call multiple times – existing toolbars are not duplicated.
- */
- function addMermaidToolbars() {
- markdownPreview.querySelectorAll('.mermaid-container').forEach(container => {
- if (container.querySelector('.mermaid-toolbar')) return; // already added
- const svgEl = container.querySelector('svg');
- if (!svgEl) return; // diagram not yet rendered
-
- const toolbar = document.createElement('div');
- toolbar.className = 'mermaid-toolbar';
- toolbar.setAttribute('aria-label', 'Diagram actions');
-
- const btnZoom = document.createElement('button');
- btnZoom.className = 'mermaid-toolbar-btn';
- btnZoom.title = 'Zoom diagram';
- btnZoom.setAttribute('aria-label', 'Zoom diagram');
- btnZoom.innerHTML = ' ';
- btnZoom.addEventListener('click', () => openMermaidZoomModal(container));
-
- const btnPng = document.createElement('button');
- btnPng.className = 'mermaid-toolbar-btn';
- btnPng.title = 'Download PNG';
- btnPng.setAttribute('aria-label', 'Download PNG');
- btnPng.innerHTML = ' PNG';
- btnPng.addEventListener('click', () => downloadMermaidPng(container, btnPng));
-
- const btnCopy = document.createElement('button');
- btnCopy.className = 'mermaid-toolbar-btn';
- btnCopy.title = 'Copy image to clipboard';
- btnCopy.setAttribute('aria-label', 'Copy image to clipboard');
- btnCopy.innerHTML = ' Copy';
- btnCopy.addEventListener('click', () => copyMermaidImage(container, btnCopy));
-
- const btnSvg = document.createElement('button');
- btnSvg.className = 'mermaid-toolbar-btn';
- btnSvg.title = 'Download SVG';
- btnSvg.setAttribute('aria-label', 'Download SVG');
- btnSvg.innerHTML = ' SVG';
- btnSvg.addEventListener('click', () => downloadMermaidSvg(container, btnSvg));
-
- toolbar.appendChild(btnZoom);
- toolbar.appendChild(btnCopy);
- toolbar.appendChild(btnPng);
- toolbar.appendChild(btnSvg);
- container.appendChild(toolbar);
- });
- }
-});
diff --git a/desktop-app/setup-binaries.js b/desktop-app/setup-binaries.js
index ac6816c..38ac8c5 100644
--- a/desktop-app/setup-binaries.js
+++ b/desktop-app/setup-binaries.js
@@ -23,7 +23,7 @@ const BIN_DIR = path.resolve(__dirname, "bin");
const VERSION_MARKER = path.join(BIN_DIR, ".version");
/** Neu CLI package — same version used across all npm scripts */
-const NEU_CLI = "@neutralinojs/neu@11.7.0";
+const NEU_CLI = "@neutralinojs/neu@11.7.1";
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
const expectedVersion = config.cli.binaryVersion;
diff --git a/desktop-app/src/desktop-main.js b/desktop-app/src/desktop-main.js
new file mode 100644
index 0000000..7e11bc7
--- /dev/null
+++ b/desktop-app/src/desktop-main.js
@@ -0,0 +1,66 @@
+/*
+ Function to set up a system tray menu with options specific to the window mode.
+ This function checks if the application is running in window mode, and if so,
+ it defines the tray menu items and sets up the tray accordingly.
+*/
+function setTray() {
+ // Tray menu is only available in window mode
+ if (NL_MODE != "window") {
+ console.log("INFO: Tray menu is only available in the window mode.");
+ return;
+ }
+
+ // Define tray menu items
+ let tray = {
+ icon: "/resources/assets/icon.jpg",
+ menuItems: [
+ { id: "VERSION", text: "Get version" },
+ { id: "SEP", text: "-" },
+ { id: "QUIT", text: "Quit" },
+ ],
+ };
+
+ // Set the tray menu
+ Neutralino.os.setTray(tray);
+}
+
+/*
+ Function to handle click events on the tray menu items.
+ This function performs different actions based on the clicked item's ID,
+ such as displaying version information or exiting the application.
+*/
+function onTrayMenuItemClicked(event) {
+ switch (event.detail.id) {
+ case "VERSION":
+ // Display version information
+ Neutralino.os.showMessageBox(
+ "Version information",
+ `Neutralinojs server: v${NL_VERSION}\nNeutralinojs client: v${NL_CVERSION}\nOS Name: ${NL_OS}\nArchitecture: ${NL_ARCH}\nApplication ID: ${NL_APPID}\nApplication Version: ${NL_APPVERSION}\nPort: ${NL_PORT}\nMode: ${NL_MODE}\nNeutralinojs server: v${NL_VERSION}\nNeutralinojs client: v${NL_CVERSION}\nCurrent working directory: ${NL_CWD}\nApplication path: ${NL_PATH}\nApplication data path: ${NL_DATAPATH}\nCommand-line arguments: ${NL_ARGS}\nProcess ID: ${NL_PID}\nResource mode: ${NL_RESMODE}\nExtensions enabled: ${NL_EXTENABLED}\nFramework binary's release commit hash: ${NL_COMMIT}\nClient library's release commit hash: ${NL_CCOMMIT}\nCustom method identifiers: ${NL_CMETHODS}\nInitial window state was loaded from the saved configuration: ${NL_WSAVSTLOADED}\nUser System Locale: ${NL_LOCALE}\nData passed during the framework binary compilation via the NEU_COMPILATION_DATA definition in the BuildZri configuration file: ${NL_COMPDATA}`,
+ );
+ break;
+ case "QUIT":
+ // Exit the application
+ Neutralino.app.exit();
+ break;
+ }
+}
+
+/*
+ Function to handle the window close event by gracefully exiting the Neutralino application.
+*/
+function onWindowClose() {
+ Neutralino.app.exit();
+}
+
+// Initialize Neutralino
+Neutralino.init();
+
+// Register event listeners
+Neutralino.events.on("trayMenuItemClicked", onTrayMenuItemClicked);
+Neutralino.events.on("windowClose", onWindowClose);
+
+// Conditional initialization: Set up system tray if not running on macOS
+if (NL_OS != "Darwin") {
+ // TODO: Fix https://github.com/neutralinojs/neutralinojs/issues/615
+ setTray();
+}
diff --git a/desktop-app/tag.sh b/desktop-app/tag.sh
new file mode 100644
index 0000000..c8ea173
--- /dev/null
+++ b/desktop-app/tag.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+set -euo pipefail
+
+echo ""
+echo "@tag.sh - Utility script to calculate the next tag for the desktop app"
+echo "---"
+
+DEFAULT_VERSION="$(date +"%Y.%-m.0")"
+DEFAULT_TAG_NAME="desktop-v$DEFAULT_VERSION"
+
+# Get the latest tag for the current branch and prune deleted tags
+TAG_NAME=$(git fetch --tags --prune --prune-tags && git tag -l --contains HEAD | tail -n1)
+
+# If no tag is found, create one using CalVer (Calendar Versioning)
+if [ -z "$TAG_NAME" ]; then
+ echo "[WARNING] No tag found, creating one using CalVer (Calendar Versioning)"
+ # Use CalVer (Calendar Versioning)
+ # format: YYYY.M.P
+ # YYYY = Year, M = Month, P = Patch (Defaults to 0 if not specified)
+ # Example: 2026.2.0
+ TAG_NAME="$DEFAULT_TAG_NAME"
+
+else # If a tag is found, determine the next tag
+ # Remove "desktop-v" prefix
+ TAG_NAME=${TAG_NAME#desktop-v}
+
+ # Check if not from current month or year
+ if [ "$(echo "$TAG_NAME" | awk -F. '{print $2}')" != "$(date +"%-m")" ] || [ "$(echo "$TAG_NAME" | awk -F. '{print $1}')" != "$(date +"%Y")" ]; then
+ # Reset patch to 0 and set YYYY.M to current date
+ TAG_NAME="$DEFAULT_VERSION"
+ else
+ # Same month & year => only increment the patch number
+ TAG_NAME=$(echo "$TAG_NAME" | awk -F. '{$NF = $NF + 1; OFS="."; print}')
+ fi
+ # Add "desktop-v" prefix back
+ TAG_NAME="desktop-v$TAG_NAME"
+fi
+
+# Get the current short commit-hash
+COMMIT_HASH=$(git show -s --format=%h)
+
+# Print the tag and commit-hash
+echo "TAG "$'\t'""$'\t '" | COMMIT"
+echo "----------------- | --------"
+echo "$TAG_NAME | $COMMIT_HASH"
+echo ""
+echo "To create and push the tag, run:"
+echo "git tag \"$TAG_NAME\" && git push origin \"$TAG_NAME\""
diff --git a/index.html b/index.html
deleted file mode 100644
index 550fee4..0000000
--- a/index.html
+++ /dev/null
@@ -1,346 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Markdown Viewer
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Are you sure you want to delete all files?
-
- Cancel
- Delete All
-
-
-
-
-
-
-
-
Rename file
-
-
- Cancel
- Rename
-
-
-
-
-
-
-
-
Import Markdown from GitHub
-
-
-
- 0 selected
- Select All
-
-
-
-
- Cancel
- Import
-
-
-
-
-
-
-
-
-
Drop your Markdown file here or click to browse
-
-
-
-
-
-
-
-
-
-
-
-
- Zoom In
-
-
- Zoom Out
-
-
- Reset
-
-
- Copy
-
-
- PNG
-
-
- SVG
-
-
-
-
-
-
-
-
-
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..6af3b8e
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2693 @@
+{
+ "name": "markdown-viewer",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "markdown-viewer",
+ "version": "1.0.0",
+ "dependencies": {
+ "@popperjs/core": "^2.11.8",
+ "bootstrap": "^5.3.8",
+ "bootstrap-icons": "^1.13.1",
+ "dompurify": "^3.3.3",
+ "emoji-toolkit": "^9.0.1",
+ "file-saver": "^2.0.5",
+ "github-markdown-css": "^5.9.0",
+ "highlight.js": "^11.11.1",
+ "html2canvas": "^1.4.1",
+ "jspdf": "^2.5.1",
+ "marked": "^16.0.0",
+ "mermaid": "^11.4.1"
+ },
+ "devDependencies": {
+ "vite": "^6.3.5"
+ }
+ },
+ "node_modules/@antfu/install-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
+ "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "package-manager-detector": "^1.3.0",
+ "tinyexec": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@braintree/sanitize-url": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz",
+ "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==",
+ "license": "MIT"
+ },
+ "node_modules/@chevrotain/cst-dts-gen": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz",
+ "integrity": "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/gast": "12.0.0",
+ "@chevrotain/types": "12.0.0"
+ }
+ },
+ "node_modules/@chevrotain/gast": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-12.0.0.tgz",
+ "integrity": "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/types": "12.0.0"
+ }
+ },
+ "node_modules/@chevrotain/regexp-to-ast": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz",
+ "integrity": "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@chevrotain/types": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-12.0.0.tgz",
+ "integrity": "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@chevrotain/utils": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-12.0.0.tgz",
+ "integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@iconify/types": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
+ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
+ "license": "MIT"
+ },
+ "node_modules/@iconify/utils": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.1.tgz",
+ "integrity": "sha512-MwzoDtw9rO1x+qfgLTV/IVXsHDBqeYZoMIQC8SfxfYSlaSUG+oWiAcoiB1yajAda6mqblm4/1/w2E8tRu7a7Tw==",
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/install-pkg": "^1.1.0",
+ "@iconify/types": "^2.0.0",
+ "mlly": "^1.8.2"
+ }
+ },
+ "node_modules/@mermaid-js/parser": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.0.tgz",
+ "integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==",
+ "license": "MIT",
+ "dependencies": {
+ "langium": "^4.0.0"
+ }
+ },
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
+ "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
+ "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
+ "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
+ "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
+ "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
+ "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
+ "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
+ "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
+ "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
+ "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
+ "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
+ "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
+ "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
+ "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
+ "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
+ "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
+ "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
+ "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
+ "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
+ "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
+ "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
+ "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
+ "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+ "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/raf": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
+ "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@upsetjs/venn.js": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz",
+ "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==",
+ "license": "MIT",
+ "optionalDependencies": {
+ "d3-selection": "^3.0.0",
+ "d3-transition": "^3.0.1"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/atob": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+ "license": "(MIT OR Apache-2.0)",
+ "bin": {
+ "atob": "bin/atob.js"
+ },
+ "engines": {
+ "node": ">= 4.5.0"
+ }
+ },
+ "node_modules/base64-arraybuffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/bootstrap": {
+ "version": "5.3.8",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
+ "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/twbs"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/bootstrap"
+ }
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "@popperjs/core": "^2.11.8"
+ }
+ },
+ "node_modules/bootstrap-icons": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz",
+ "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/twbs"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/bootstrap"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/btoa": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
+ "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
+ "license": "(MIT OR Apache-2.0)",
+ "bin": {
+ "btoa": "bin/btoa.js"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/canvg": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
+ "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "@types/raf": "^3.4.0",
+ "core-js": "^3.8.3",
+ "raf": "^3.4.1",
+ "regenerator-runtime": "^0.13.7",
+ "rgbcolor": "^1.0.1",
+ "stackblur-canvas": "^2.0.0",
+ "svg-pathdata": "^6.0.3"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/chevrotain": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz",
+ "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/cst-dts-gen": "12.0.0",
+ "@chevrotain/gast": "12.0.0",
+ "@chevrotain/regexp-to-ast": "12.0.0",
+ "@chevrotain/types": "12.0.0",
+ "@chevrotain/utils": "12.0.0"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/chevrotain-allstar": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.3.tgz",
+ "integrity": "sha512-2X4mkroolSMKqW+H22pyPMUVDqYZzPhephTmg/NODKb1IGYPHfxfhcW0EjS7wcPJNbze2i4vBWT7zT5FKF2lrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash-es": "^4.18.1"
+ },
+ "peerDependencies": {
+ "chevrotain": "^12.0.0"
+ }
+ },
+ "node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/confbox": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+ "license": "MIT"
+ },
+ "node_modules/core-js": {
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
+ "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/cose-base": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
+ "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
+ "license": "MIT",
+ "dependencies": {
+ "layout-base": "^1.0.0"
+ }
+ },
+ "node_modules/css-line-break": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+ "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+ "license": "MIT",
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
+ "node_modules/cytoscape": {
+ "version": "3.33.3",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.3.tgz",
+ "integrity": "sha512-Gej7U+OKR+LZ8kvX7rb2HhCYJ0IhvEFsnkud4SB1PR+BUY/TsSO0dmOW59WEVLu51b1Rm+gQRKoz4bLYxGSZ2g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/cytoscape-cose-bilkent": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
+ "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cose-base": "^1.0.0"
+ },
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
+ }
+ },
+ "node_modules/cytoscape-fcose": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
+ "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cose-base": "^2.2.0"
+ },
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
+ }
+ },
+ "node_modules/cytoscape-fcose/node_modules/cose-base": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
+ "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
+ "license": "MIT",
+ "dependencies": {
+ "layout-base": "^2.0.0"
+ }
+ },
+ "node_modules/cytoscape-fcose/node_modules/layout-base": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
+ "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==",
+ "license": "MIT"
+ },
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "license": "ISC",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-sankey": {
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
+ "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "1 - 2",
+ "d3-shape": "^1.2.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-array": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
+ "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "internmap": "^1.0.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-path": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+ "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/d3-sankey/node_modules/d3-shape": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-path": "1"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/internmap": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
+ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
+ "license": "ISC"
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dagre-d3-es": {
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz",
+ "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==",
+ "license": "MIT",
+ "dependencies": {
+ "d3": "^7.9.0",
+ "lodash-es": "^4.17.21"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.20",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
+ "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
+ "license": "MIT"
+ },
+ "node_modules/delaunator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
+ "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
+ "node_modules/dompurify": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz",
+ "integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
+ "node_modules/emoji-toolkit": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-9.0.1.tgz",
+ "integrity": "sha512-sMMNqKNLVHXJfIKoPbrRJwtYuysVNC9GlKetr72zE3SSVbHqoeDLWVrxP0uM0AE0qvdl3hbUk+tJhhwXZrDHaw==",
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
+ "node_modules/file-saver": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+ "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
+ "license": "MIT"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/github-markdown-css": {
+ "version": "5.9.0",
+ "resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.9.0.tgz",
+ "integrity": "sha512-tmT5sY+zvg2302XLYEfH2mtkViIM1SWf2nvYoF5N1ZsO0V6B2qZTiw3GOzw4vpjLygK/KG35qRlPFweHqfzz5w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/hachure-fill": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
+ "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==",
+ "license": "MIT"
+ },
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/html2canvas": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+ "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+ "license": "MIT",
+ "dependencies": {
+ "css-line-break": "^2.1.0",
+ "text-segmentation": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jspdf": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz",
+ "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2",
+ "atob": "^2.1.2",
+ "btoa": "^1.2.1",
+ "fflate": "^0.8.1"
+ },
+ "optionalDependencies": {
+ "canvg": "^3.0.6",
+ "core-js": "^3.6.0",
+ "dompurify": "^2.5.4",
+ "html2canvas": "^1.0.0-rc.5"
+ }
+ },
+ "node_modules/jspdf/node_modules/dompurify": {
+ "version": "2.5.9",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz",
+ "integrity": "sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optional": true
+ },
+ "node_modules/katex": {
+ "version": "0.16.45",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz",
+ "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^8.3.0"
+ },
+ "bin": {
+ "katex": "cli.js"
+ }
+ },
+ "node_modules/katex/node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/khroma": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
+ "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
+ },
+ "node_modules/langium": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.2.tgz",
+ "integrity": "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@chevrotain/regexp-to-ast": "~12.0.0",
+ "chevrotain": "~12.0.0",
+ "chevrotain-allstar": "~0.4.1",
+ "vscode-languageserver": "~9.0.1",
+ "vscode-languageserver-textdocument": "~1.0.11",
+ "vscode-uri": "~3.1.0"
+ },
+ "engines": {
+ "node": ">=20.10.0",
+ "npm": ">=10.2.3"
+ }
+ },
+ "node_modules/layout-base": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
+ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "license": "MIT"
+ },
+ "node_modules/marked": {
+ "version": "16.4.2",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
+ "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/mermaid": {
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.14.0.tgz",
+ "integrity": "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@braintree/sanitize-url": "^7.1.1",
+ "@iconify/utils": "^3.0.2",
+ "@mermaid-js/parser": "^1.1.0",
+ "@types/d3": "^7.4.3",
+ "@upsetjs/venn.js": "^2.0.0",
+ "cytoscape": "^3.33.1",
+ "cytoscape-cose-bilkent": "^4.1.0",
+ "cytoscape-fcose": "^2.2.0",
+ "d3": "^7.9.0",
+ "d3-sankey": "^0.12.3",
+ "dagre-d3-es": "7.0.14",
+ "dayjs": "^1.11.19",
+ "dompurify": "^3.3.1",
+ "katex": "^0.16.25",
+ "khroma": "^2.1.0",
+ "lodash-es": "^4.17.23",
+ "marked": "^16.3.0",
+ "roughjs": "^4.6.6",
+ "stylis": "^4.3.6",
+ "ts-dedent": "^2.2.0",
+ "uuid": "^11.1.0"
+ }
+ },
+ "node_modules/mlly": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
+ "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "pathe": "^2.0.3",
+ "pkg-types": "^1.3.1",
+ "ufo": "^1.6.3"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/package-manager-detector": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz",
+ "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==",
+ "license": "MIT"
+ },
+ "node_modules/path-data-parser": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
+ "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "license": "MIT"
+ },
+ "node_modules/performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pkg-types": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.1.8",
+ "mlly": "^1.7.4",
+ "pathe": "^2.0.1"
+ }
+ },
+ "node_modules/points-on-curve": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
+ "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==",
+ "license": "MIT"
+ },
+ "node_modules/points-on-path": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
+ "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
+ "license": "MIT",
+ "dependencies": {
+ "path-data-parser": "0.1.0",
+ "points-on-curve": "0.2.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.12",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
+ "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/raf": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
+ "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "performance-now": "^2.1.0"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/rgbcolor": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
+ "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
+ "license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.8.15"
+ }
+ },
+ "node_modules/robust-predicates": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
+ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
+ "license": "Unlicense"
+ },
+ "node_modules/rollup": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
+ "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.2",
+ "@rollup/rollup-android-arm64": "4.60.2",
+ "@rollup/rollup-darwin-arm64": "4.60.2",
+ "@rollup/rollup-darwin-x64": "4.60.2",
+ "@rollup/rollup-freebsd-arm64": "4.60.2",
+ "@rollup/rollup-freebsd-x64": "4.60.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.2",
+ "@rollup/rollup-linux-arm64-musl": "4.60.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.2",
+ "@rollup/rollup-linux-loong64-musl": "4.60.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-musl": "4.60.2",
+ "@rollup/rollup-openbsd-x64": "4.60.2",
+ "@rollup/rollup-openharmony-arm64": "4.60.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.2",
+ "@rollup/rollup-win32-x64-gnu": "4.60.2",
+ "@rollup/rollup-win32-x64-msvc": "4.60.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/roughjs": {
+ "version": "4.6.6",
+ "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
+ "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "hachure-fill": "^0.5.2",
+ "path-data-parser": "^0.1.0",
+ "points-on-curve": "^0.2.0",
+ "points-on-path": "^0.2.1"
+ }
+ },
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackblur-canvas": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
+ "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.1.14"
+ }
+ },
+ "node_modules/stylis": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
+ "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
+ "license": "MIT"
+ },
+ "node_modules/svg-pathdata": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
+ "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/text-segmentation": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+ "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+ "license": "MIT",
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
+ "node_modules/tinyexec": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
+ "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/ts-dedent": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
+ "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.10"
+ }
+ },
+ "node_modules/ufo": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
+ "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
+ "license": "MIT"
+ },
+ "node_modules/utrie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+ "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-arraybuffer": "^1.0.2"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "11.1.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
+ "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vscode-jsonrpc": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
+ "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/vscode-languageserver": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
+ "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
+ "license": "MIT",
+ "dependencies": {
+ "vscode-languageserver-protocol": "3.17.5"
+ },
+ "bin": {
+ "installServerIntoExtension": "bin/installServerIntoExtension"
+ }
+ },
+ "node_modules/vscode-languageserver-protocol": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
+ "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
+ "license": "MIT",
+ "dependencies": {
+ "vscode-jsonrpc": "8.2.0",
+ "vscode-languageserver-types": "3.17.5"
+ }
+ },
+ "node_modules/vscode-languageserver-textdocument": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
+ "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
+ "license": "MIT"
+ },
+ "node_modules/vscode-languageserver-types": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
+ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
+ "license": "MIT"
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..e78aeac
--- /dev/null
+++ b/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "markdown-viewer",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Markdown Viewer web app with root npm scripts",
+ "scripts": {
+ "build": "vite build",
+ "dev": "vite",
+ "preview": "vite preview",
+ "start": "npm run dev",
+ "desktop:install": "npm --prefix desktop-app install",
+ "desktop:prepare": "npm run build && node desktop-app/prepare.js",
+ "desktop:dev": "npm --prefix desktop-app run dev",
+ "desktop:build": "npm --prefix desktop-app run build",
+ "skills:install": "node packages/claude-skills/install.js",
+ "skills:list": "node packages/claude-skills/install.js --list"
+ },
+ "dependencies": {
+ "@popperjs/core": "^2.11.8",
+ "bootstrap": "^5.3.8",
+ "bootstrap-icons": "^1.13.1",
+ "dompurify": "^3.3.3",
+ "emoji-toolkit": "^9.0.1",
+ "file-saver": "^2.0.5",
+ "github-markdown-css": "^5.9.0",
+ "highlight.js": "^11.11.1",
+ "html2canvas": "^1.4.1",
+ "jspdf": "^2.5.1",
+ "marked": "^16.0.0",
+ "mermaid": "^11.4.1"
+ },
+ "devDependencies": {
+ "vite": "^6.3.5"
+ }
+}
diff --git a/packages/claude-skills/commands/add-feature.md b/packages/claude-skills/commands/add-feature.md
new file mode 100644
index 0000000..6a268f6
--- /dev/null
+++ b/packages/claude-skills/commands/add-feature.md
@@ -0,0 +1,19 @@
+Implement the following new feature in the Markdown Viewer: $ARGUMENTS
+
+## Rules to follow
+
+1. **All logic goes in `web/script.js`** inside the existing `DOMContentLoaded` closure. No new files, no modules, no global variables.
+2. **If a UI element is needed**, add it to both:
+ - The desktop/tablet toolbar in `web/index.html` (`.toolbar` div and/or `.view-mode-group`)
+ - The mobile menu panel (`#mobile-menu-panel`) so mobile users get the same feature
+3. **DOM references** belong at the top of the closure with the other `const` declarations (lines ~13–70 of script.js).
+4. **Do not npm-install libraries.** If a library is needed, load it from CDN via a `
-
-
-
-
+
+
-
-
-
-
-
-
-
-