`,
);
+if (html === originalHtml) {
+ console.error("ā No prepare.js transformations were applied");
+ process.exit(1);
+}
+
fs.writeFileSync(path.join(RESOURCES_DIR, "index.html"), html, "utf-8");
console.log(
"ā Generated resources/index.html (Neutralinojs injections applied)",
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
index 70773eb..7e11bc7 100644
--- a/desktop-app/resources/js/main.js
+++ b/desktop-app/resources/js/main.js
@@ -1,36 +1,3 @@
-// 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,
@@ -45,7 +12,7 @@ function setTray() {
// Define tray menu items
let tray = {
- icon: "/resources/icons/trayIcon.png",
+ icon: "/resources/assets/icon.jpg",
menuItems: [
{ id: "VERSION", text: "Get version" },
{ id: "SEP", text: "-" },
@@ -68,7 +35,7 @@ function onTrayMenuItemClicked(event) {
// Display version information
Neutralino.os.showMessageBox(
"Version information",
- `Neutralinojs server: v${NL_VERSION} | Neutralinojs client: v${NL_CVERSION}`,
+ `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":
@@ -97,31 +64,3 @@ 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/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..5deae0d
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,643 @@
+{
+ "name": "markdown-viewer",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "markdown-viewer",
+ "version": "1.0.0",
+ "devDependencies": {
+ "http-server": "^14.1.1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/corser": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
+ "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/http-server": {
+ "version": "14.1.1",
+ "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
+ "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "basic-auth": "^2.0.1",
+ "chalk": "^4.1.2",
+ "corser": "^2.0.1",
+ "he": "^1.2.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy": "^1.18.1",
+ "mime": "^1.6.0",
+ "minimist": "^1.2.6",
+ "opener": "^1.5.1",
+ "portfinder": "^1.0.28",
+ "secure-compare": "3.0.1",
+ "union": "~0.5.0",
+ "url-join": "^4.0.1"
+ },
+ "bin": {
+ "http-server": "bin/http-server"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/opener": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
+ "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
+ "dev": true,
+ "license": "(WTFPL OR MIT)",
+ "bin": {
+ "opener": "bin/opener-bin.js"
+ }
+ },
+ "node_modules/portfinder": {
+ "version": "1.0.38",
+ "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz",
+ "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async": "^3.2.6",
+ "debug": "^4.3.6"
+ },
+ "engines": {
+ "node": ">= 10.12"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
+ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/secure-compare": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
+ "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/union": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
+ "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
+ "dev": true,
+ "dependencies": {
+ "qs": "^6.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/url-join": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..b64da8e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "markdown-viewer",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Markdown Viewer web app with root npm scripts",
+ "scripts": {
+ "dev": "http-server web -p 5173 -c-1 -o",
+ "start": "npm run dev",
+ "desktop:install": "npm --prefix desktop-app install",
+ "desktop:dev": "npm --prefix desktop-app run dev",
+ "desktop:build": "npm --prefix desktop-app run build"
+ },
+ "devDependencies": {
+ "http-server": "^14.1.1"
+ }
+}
diff --git a/script.js b/script.js
deleted file mode 100644
index 97ad3d4..0000000
--- a/script.js
+++ /dev/null
@@ -1,2982 +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 githubImportSelectionToolbar = document.getElementById("github-import-selection-toolbar");
- const githubImportSelectedCount = document.getElementById("github-import-selected-count");
- const githubImportSelectAllBtn = document.getElementById("github-import-select-all");
- const githubImportTree = document.getElementById("github-import-tree");
- 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;
- const GITHUB_IMPORT_MIN_REQUEST_INTERVAL_MS = 800;
- let lastGitHubImportRequestAt = 0;
- const selectedGitHubImportPaths = new Set();
- let availableGitHubImportPaths = [];
-
- 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 now = Date.now();
- const waitTime = GITHUB_IMPORT_MIN_REQUEST_INTERVAL_MS - (now - lastGitHubImportRequestAt);
- if (waitTime > 0) {
- await new Promise((resolve) => setTimeout(resolve, waitTime));
- }
- lastGitHubImportRequestAt = Date.now();
- 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 buildMarkdownFileTree(paths) {
- const root = { folders: {}, files: [] };
- (paths || []).forEach((path) => {
- const segments = (path || "").split("/").filter(Boolean);
- if (!segments.length) return;
- const fileName = segments.pop();
- let node = root;
- segments.forEach((segment) => {
- if (!node.folders[segment]) {
- node.folders[segment] = { folders: {}, files: [] };
- }
- node = node.folders[segment];
- });
- node.files.push({ name: fileName, path });
- });
- return root;
- }
-
- function updateGitHubImportSelectedCount() {
- if (!githubImportSelectedCount) return;
- const count = selectedGitHubImportPaths.size;
- githubImportSelectedCount.textContent = `${count} selected`;
- }
-
- function updateGitHubSelectAllButtonLabel() {
- if (!githubImportSelectAllBtn) return;
- const total = availableGitHubImportPaths.length;
- const allSelected = total > 0 && selectedGitHubImportPaths.size === total;
- githubImportSelectAllBtn.textContent = allSelected ? "Clear All" : "Select All";
- }
-
- function syncGitHubSelectionToButtons() {
- if (!githubImportTree) return;
- Array.from(githubImportTree.querySelectorAll(".github-tree-file-btn")).forEach((btn) => {
- const isSelected = selectedGitHubImportPaths.has(btn.dataset.path);
- btn.classList.toggle("is-selected", isSelected);
- btn.setAttribute("aria-pressed", isSelected ? "true" : "false");
- });
- }
-
- function setGitHubSelectedPaths(paths) {
- selectedGitHubImportPaths.clear();
- (paths || []).forEach((path) => selectedGitHubImportPaths.add(path));
- updateGitHubImportSelectedCount();
- syncGitHubSelectionToButtons();
- updateGitHubSelectAllButtonLabel();
- }
-
- function toggleGitHubSelectedPath(path) {
- if (!path) return;
- if (selectedGitHubImportPaths.has(path)) {
- selectedGitHubImportPaths.delete(path);
- } else {
- selectedGitHubImportPaths.add(path);
- }
- updateGitHubImportSelectedCount();
- syncGitHubSelectionToButtons();
- updateGitHubSelectAllButtonLabel();
- }
-
- function renderGitHubImportTree(paths) {
- if (!githubImportTree || !githubImportFileSelect) return;
- githubImportTree.innerHTML = "";
- const tree = buildMarkdownFileTree(paths);
-
- const createTreeBranch = function(node, parentPath) {
- const list = document.createElement("ul");
- const folderNames = Object.keys(node.folders).sort((a, b) => a.localeCompare(b));
- folderNames.forEach((folderName) => {
- const folderPath = parentPath ? `${parentPath}/${folderName}` : folderName;
- const item = document.createElement("li");
- const folderLabel = document.createElement("span");
- folderLabel.className = "github-tree-folder-label";
- folderLabel.textContent = `š ${folderName}`;
- item.appendChild(folderLabel);
- item.appendChild(createTreeBranch(node.folders[folderName], folderPath));
- list.appendChild(item);
- });
-
- node.files
- .sort((a, b) => a.path.localeCompare(b.path))
- .forEach((file) => {
- const fileItem = document.createElement("li");
- const fileButton = document.createElement("button");
- fileButton.type = "button";
- fileButton.className = "github-tree-file-btn";
- fileButton.dataset.path = file.path;
- fileButton.setAttribute("aria-pressed", "false");
- fileButton.textContent = `š ${file.name}`;
- fileButton.addEventListener("click", function() {
- toggleGitHubSelectedPath(file.path);
- });
- fileItem.appendChild(fileButton);
- list.appendChild(fileItem);
- });
-
- return list;
- };
-
- githubImportTree.appendChild(createTreeBranch(tree, ""));
- syncGitHubSelectionToButtons();
- }
-
- 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;
- if (githubImportSelectionToolbar) {
- githubImportSelectionToolbar.style.display = "none";
- }
- availableGitHubImportPaths = [];
- setGitHubSelectedPaths([]);
- if (githubImportTree) {
- githubImportTree.innerHTML = "";
- githubImportTree.style.display = "none";
- }
- 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;
- }
- if (githubImportSelectAllBtn) {
- githubImportSelectAllBtn.disabled = disabled;
- }
- };
- const step = githubImportSubmitBtn.dataset.step || "url";
- if (step === "select") {
- const selectedPaths = Array.from(selectedGitHubImportPaths);
- const owner = githubImportSubmitBtn.dataset.owner;
- const repo = githubImportSubmitBtn.dataset.repo;
- const ref = githubImportSubmitBtn.dataset.ref;
- if (!owner || !repo || !ref || !selectedPaths.length) {
- setGitHubImportMessage("Please select at least one file to import.");
- return;
- }
- setGitHubImportLoading(true);
- setGitHubImportDialogDisabled(true);
- try {
- for (const selectedPath of selectedPaths) {
- 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;
- }
-
- githubImportFileSelect.innerHTML = "";
- githubImportUrlInput.style.display = "none";
- githubImportFileSelect.style.display = "none";
- if (githubImportSelectionToolbar) {
- githubImportSelectionToolbar.style.display = "flex";
- }
- if (githubImportTree) {
- githubImportTree.style.display = "block";
- }
- shownFiles.forEach((filePath) => {
- const option = document.createElement("option");
- option.value = filePath;
- option.textContent = filePath;
- githubImportFileSelect.appendChild(option);
- });
- availableGitHubImportPaths = shownFiles.slice();
- setGitHubSelectedPaths(shownFiles[0] ? [shownFiles[0]] : []);
- renderGitHubImportTree(shownFiles);
- 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 Markdown file(s) 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);
- }
- if (githubImportSelectAllBtn) {
- githubImportSelectAllBtn.addEventListener("click", function() {
- const allPaths = availableGitHubImportPaths.slice();
- const shouldSelectAll = selectedGitHubImportPaths.size !== allPaths.length;
- setGitHubSelectedPaths(shouldSelectAll ? allPaths : []);
- });
- }
-
- 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/styles.css b/styles.css
deleted file mode 100644
index 363c234..0000000
--- a/styles.css
+++ /dev/null
@@ -1,1789 +0,0 @@
-:root {
- --bg-color: #ffffff;
- --editor-bg: #f6f8fa;
- --preview-bg: #ffffff; /* Preview background for light mode */
- --text-color: #24292e;
- --preview-text-color: #24292e; /* Text color for preview in light mode */
- --border-color: #e1e4e8;
- --header-bg: #f6f8fa;
- --button-bg: #f6f8fa;
- --button-hover: #e1e4e8;
- --button-active: #d1d5da;
- --dropzone-bg: rgba(255, 255, 255, 0.8);
- --scrollbar-thumb: #c1c1c1;
- --scrollbar-track: #f1f1f1;
- --accent-color: #0366d6;
- --table-bg: #ffffff; /* Table background for light mode */
- --code-bg: #f6f8fa; /* Code block background for light mode */
-}
-
-[data-theme="dark"] {
- --bg-color: #0d1117;
- --editor-bg: #161b22;
- --preview-bg: #0d1117; /* Preview background for dark mode */
- --text-color: #c9d1d9;
- --preview-text-color: #c9d1d9; /* Text color for preview in dark mode */
- --border-color: #30363d;
- --header-bg: #161b22;
- --button-bg: #21262d;
- --button-hover: #30363d;
- --button-active: #3b434b;
- --dropzone-bg: rgba(13, 17, 23, 0.8);
- --scrollbar-thumb: #484f58;
- --scrollbar-track: #21262d;
- --accent-color: #58a6ff;
- --table-bg: #161b22; /* Table background for dark mode */
- --code-bg: #161b22; /* Code block background for dark mode */
-}
-
-* {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
-}
-
-body {
- background-color: var(--bg-color);
- color: var(--text-color);
- transition: background-color 0.3s ease, color 0.3s ease;
- min-height: 100vh;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
-}
-
-.app-header {
- background-color: var(--header-bg);
- border-bottom: 1px solid var(--border-color);
- padding: 1rem;
- transition: background-color 0.3s ease;
- position: relative;
- z-index: 100;
- flex-shrink: 0;
-}
-
-.app-container {
- height: 100vh;
- display: flex;
- flex-direction: column;
- overflow: hidden;
-}
-
-.content-container {
- display: flex;
- flex: 1;
- overflow: hidden;
-}
-
-.editor-pane, .preview-pane {
- flex: 1;
- padding: 20px;
- overflow-y: auto;
- position: relative;
- transition: background-color 0.3s ease;
-}
-
-.editor-pane {
- background-color: var(--editor-bg);
- border-right: 1px solid var(--border-color);
- padding-right: 0px;
-}
-
-.preview-pane {
- background-color: var(--preview-bg); /* Using the new variable for preview background */
-}
-
-/* Custom scrollbar */
-.editor-pane::-webkit-scrollbar,
-.preview-pane::-webkit-scrollbar,
-#markdown-editor::-webkit-scrollbar {
- width: 8px;
- height: 8px;
-}
-
-.editor-pane::-webkit-scrollbar-track,
-.preview-pane::-webkit-scrollbar-track,
-#markdown-editor::-webkit-scrollbar-track {
- background: var(--scrollbar-track);
-}
-
-.editor-pane::-webkit-scrollbar-thumb,
-.preview-pane::-webkit-scrollbar-thumb,
-#markdown-editor::-webkit-scrollbar-thumb {
- background: var(--scrollbar-thumb);
- border-radius: 4px;
-}
-
-.editor-pane::-webkit-scrollbar-thumb:hover,
-.preview-pane::-webkit-scrollbar-thumb:hover,
-#markdown-editor::-webkit-scrollbar-thumb:hover {
- background: var(--button-active);
-}
-
-#markdown-editor {
- width: 100%;
- height: 100%;
- border: none;
- background-color: var(--editor-bg);
- color: var(--text-color);
- resize: none;
- font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
- font-size: 14px;
- line-height: 1.5;
- padding: 10px;
- transition: background-color 0.3s ease, color 0.3s ease;
- overflow-y: auto;
-}
-
-#markdown-editor:focus {
- outline: none;
-}
-
-.preview-pane {
- padding: 20px;
-}
-
-.markdown-body {
- padding: 20px;
- width: 100%;
- background-color: var(--preview-bg); /* Ensuring the markdown content matches preview background */
- color: var(--preview-text-color); /* Using specific text color for preview content */
-}
-
-/* Style tables in light mode */
-.markdown-body table {
- background-color: var(--table-bg);
- border-color: var(--border-color);
-}
-
-.markdown-body table tr {
- background-color: var(--table-bg);
- border-top: 1px solid var(--border-color);
-}
-
-.markdown-body table tr:nth-child(2n) {
- background-color: var(--bg-color);
-}
-
-/* Style code blocks in light mode */
-.markdown-body pre {
- background-color: var(--code-bg);
- border-radius: 6px;
-}
-
-.markdown-body code {
- background-color: var(--code-bg);
- border-radius: 3px;
- padding: 0.2em 0.4em;
-}
-
-.toolbar {
- display: flex;
- gap: 8px;
-}
-
-.tool-button {
- background-color: var(--button-bg);
- border: 1px solid var(--border-color);
- color: var(--text-color);
- border-radius: 6px;
- padding: 6px 12px;
- font-size: 14px;
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 4px;
- transition: all 0.2s ease;
-}
-
-.tool-button:hover {
- background-color: var(--button-hover);
-}
-
-.tool-button:active {
- background-color: var(--button-active);
-}
-
-.tool-button i {
- font-size: 16px;
-}
-
-.file-input {
- display: none;
-}
-
-.dropzone {
- border: 2px dashed var(--border-color);
- border-radius: 6px;
- padding: 20px;
- text-align: center;
- margin-bottom: 20px;
- cursor: pointer;
- transition: all 0.3s ease;
- background-color: var(--dropzone-bg);
-}
-
-.dropzone.active {
- border-color: var(--accent-color);
- background-color: rgba(var(--accent-color), 0.05);
-}
-
-.dropzone p {
- transition: transform 0.2s ease;
-}
-
-.dropzone:hover p {
- transform: scale(1.02);
-}
-
-/* Dropdown improvements */
-.dropdown-menu {
- background-color: var(--bg-color);
- border-color: var(--border-color);
-}
-
-.dropdown-item {
- color: var(--text-color);
-}
-
-.dropdown-item:hover, .dropdown-item:focus {
- background-color: var(--button-hover);
- color: var(--text-color);
-}
-
-/* Responsive design for mobile */
-@media (max-width: 1080px) {
- .content-container {
- flex-direction: column;
- }
-
- .editor-pane, .preview-pane {
- flex: none;
- height: 50%;
- border-right: none;
- }
-
- .editor-pane {
- border-bottom: 1px solid var(--border-color);
- }
-
- .toolbar {
- flex-wrap: wrap;
- justify-content: center;
- gap: 1rem;
- }
-}
-
-/* Loading indicators */
-.loading {
- opacity: 0.6;
- pointer-events: none;
-}
-
-/* Focus outline for accessibility */
-button:focus,
-a:focus {
- outline: 2px solid var(--accent-color);
- outline-offset: 2px;
-}
-
-/* Animation for copied message */
-@keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
-}
-
-/* Tooltip styles */
-.tooltip {
- position: absolute;
- background: var(--button-bg);
- border: 1px solid var(--border-color);
- padding: 5px 8px;
- border-radius: 4px;
- font-size: 12px;
- z-index: 1000;
- animation: fadeIn 0.2s ease;
-}
-
-/* Styles for GitHub markdown preview light mode */
-.markdown-body {
- color-scheme: light;
- --color-prettylights-syntax-comment: #6a737d;
- --color-prettylights-syntax-constant: #005cc5;
- --color-prettylights-syntax-entity: #6f42c1;
- --color-prettylights-syntax-storage-modifier-import: #24292e;
- --color-prettylights-syntax-entity-tag: #22863a;
- --color-prettylights-syntax-keyword: #d73a49;
- --color-prettylights-syntax-string: #032f62;
- --color-prettylights-syntax-variable: #e36209;
- --color-prettylights-syntax-brackethighlighter-unmatched: #b31d28;
- --color-prettylights-syntax-invalid-illegal-text: #fafbfc;
- --color-prettylights-syntax-invalid-illegal-bg: #b31d28;
- --color-prettylights-syntax-carriage-return-text: #fafbfc;
- --color-prettylights-syntax-carriage-return-bg: #d73a49;
- --color-prettylights-syntax-string-regexp: #22863a;
- --color-prettylights-syntax-markup-list: #735c0f;
- --color-prettylights-syntax-markup-heading: #005cc5;
- --color-prettylights-syntax-markup-italic: #24292e;
- --color-prettylights-syntax-markup-bold: #24292e;
- --color-prettylights-syntax-markup-deleted-text: #b31d28;
- --color-prettylights-syntax-markup-deleted-bg: #ffeef0;
- --color-prettylights-syntax-markup-inserted-text: #22863a;
- --color-prettylights-syntax-markup-inserted-bg: #f0fff4;
- --color-prettylights-syntax-markup-changed-text: #e36209;
- --color-prettylights-syntax-markup-changed-bg: #ffebda;
- --color-prettylights-syntax-markup-ignored-text: #f6f8fa;
- --color-prettylights-syntax-markup-ignored-bg: #005cc5;
- --color-prettylights-syntax-meta-diff-range: #6f42c1;
- --color-prettylights-syntax-brackethighlighter-angle: #586069;
- --color-prettylights-syntax-sublimelinter-gutter-mark: #e1e4e8;
- --color-prettylights-syntax-constant-other-reference-link: #032f62;
- --color-fg-default: #24292e;
- --color-fg-muted: #586069;
- --color-fg-subtle: #6a737d;
- --color-canvas-default: #ffffff;
- --color-canvas-subtle: #f6f8fa;
- --color-border-default: #e1e4e8;
- --color-border-muted: #eaecef;
- --color-neutral-muted: rgba(175,184,193,0.2);
- --color-accent-fg: #0366d6;
- --color-accent-emphasis: #0366d6;
- --color-attention-subtle: #fff5b1;
- --color-danger-fg: #d73a49;
-}
-
-/* Styles for GitHub markdown preview dark mode */
-[data-theme="dark"] .markdown-body {
- color-scheme: dark;
- --color-prettylights-syntax-comment: #8b949e;
- --color-prettylights-syntax-constant: #79c0ff;
- --color-prettylights-syntax-entity: #d2a8ff;
- --color-prettylights-syntax-storage-modifier-import: #c9d1d9;
- --color-prettylights-syntax-entity-tag: #7ee787;
- --color-prettylights-syntax-keyword: #ff7b72;
- --color-prettylights-syntax-string: #a5d6ff;
- --color-prettylights-syntax-variable: #ffa657;
- --color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
- --color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
- --color-prettylights-syntax-invalid-illegal-bg: #8e1519;
- --color-prettylights-syntax-carriage-return-text: #f0f6fc;
- --color-prettylights-syntax-carriage-return-bg: #b62324;
- --color-prettylights-syntax-string-regexp: #7ee787;
- --color-prettylights-syntax-markup-list: #f2cc60;
- --color-prettylights-syntax-markup-heading: #1f6feb;
- --color-prettylights-syntax-markup-italic: #c9d1d9;
- --color-prettylights-syntax-markup-bold: #c9d1d9;
- --color-prettylights-syntax-markup-deleted-text: #ffdcd7;
- --color-prettylights-syntax-markup-deleted-bg: #67060c;
- --color-prettylights-syntax-markup-inserted-text: #aff5b4;
- --color-prettylights-syntax-markup-inserted-bg: #033a16;
- --color-prettylights-syntax-markup-changed-text: #ffdfb6;
- --color-prettylights-syntax-markup-changed-bg: #5a1e02;
- --color-prettylights-syntax-markup-ignored-text: #c9d1d9;
- --color-prettylights-syntax-markup-ignored-bg: #1158c7;
- --color-prettylights-syntax-meta-diff-range: #d2a8ff;
- --color-prettylights-syntax-brackethighlighter-angle: #8b949e;
- --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
- --color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
- --color-fg-default: #c9d1d9;
- --color-fg-muted: #8b949e;
- --color-fg-subtle: #484f58;
- --color-canvas-default: #0d1117;
- --color-canvas-subtle: #161b22;
- --color-border-default: #30363d;
- --color-border-muted: #21262d;
- --color-neutral-muted: rgba(110,118,129,0.4);
- --color-accent-fg: #58a6ff;
- --color-accent-emphasis: #1f6feb;
- --color-attention-subtle: rgba(187,128,9,0.15);
- --color-danger-fg: #f85149;
-}
-
-/* Override specific styles for dark mode tables and code */
-[data-theme="dark"] .markdown-body table tr {
- background-color: var(--table-bg);
-}
-
-[data-theme="dark"] .markdown-body table tr:nth-child(2n) {
- background-color: #1c2128; /* Slightly lighter than base dark background */
-}
-
-[data-theme="dark"] .markdown-body pre {
- background-color: var(--code-bg);
-}
-
-[data-theme="dark"] .markdown-body code {
- background-color: var(--code-bg);
-}
-
-/* Syntax Highlighting Mapping to GitHub Variables */
-.hljs {
- color: var(--color-fg-default);
-}
-.hljs-doctag,
-.hljs-keyword,
-.hljs-meta .hljs-keyword,
-.hljs-template-tag,
-.hljs-template-variable,
-.hljs-type,
-.hljs-variable.language_ {
- color: var(--color-prettylights-syntax-keyword);
-}
-.hljs-title,
-.hljs-title.class_,
-.hljs-title.class_.inherited__,
-.hljs-title.function_ {
- color: var(--color-prettylights-syntax-entity);
-}
-.hljs-attr,
-.hljs-attribute,
-.hljs-literal,
-.hljs-meta,
-.hljs-number,
-.hljs-operator,
-.hljs-variable,
-.hljs-selector-attr,
-.hljs-selector-class,
-.hljs-selector-id {
- color: var(--color-prettylights-syntax-constant);
-}
-.hljs-regexp,
-.hljs-string,
-.hljs-meta .hljs-string {
- color: var(--color-prettylights-syntax-string);
-}
-.hljs-built_in,
-.hljs-symbol {
- color: var(--color-prettylights-syntax-variable);
-}
-.hljs-comment,
-.hljs-code,
-.hljs-formula {
- color: var(--color-prettylights-syntax-comment);
-}
-.hljs-name,
-.hljs-quote,
-.hljs-selector-tag,
-.hljs-selector-pseudo {
- color: var(--color-prettylights-syntax-entity-tag);
-}
-.hljs-subst {
- color: var(--color-fg-default);
-}
-.hljs-section {
- color: var(--color-prettylights-syntax-markup-heading);
- font-weight: bold;
-}
-.hljs-bullet {
- color: var(--color-prettylights-syntax-constant);
-}
-.hljs-emphasis {
- color: var(--color-fg-default);
- font-style: italic;
-}
-.hljs-strong {
- color: var(--color-fg-default);
- font-weight: bold;
-}
-.hljs-addition {
- color: var(--color-prettylights-syntax-markup-inserted-text);
- background-color: var(--color-prettylights-syntax-markup-inserted-bg);
-}
-.hljs-deletion {
- color: var(--color-prettylights-syntax-markup-deleted-text);
- background-color: var(--color-prettylights-syntax-markup-deleted-bg);
-}
-
-.stats-container {
- font-size: 0.9rem;
- color: var(--text-color);
-}
-
-.stat-item {
- align-items: center;
-}
-
-.stat-item i {
- font-size: 1rem;
- opacity: 0.8;
-}
-
-.dropzone {
- border: 2px dashed var(--border-color);
- border-radius: 6px;
- padding: 20px;
- text-align: center;
- margin-bottom: 10px;
- cursor: pointer;
- transition: all 0.3s ease;
- background-color: var(--dropzone-bg);
- position: relative;
-}
-
-.dropzone.active {
- border-color: var(--accent-color);
- background-color: rgba(var(--accent-color), 0.05);
-}
-
-.dropzone p {
- transition: transform 0.2s ease;
-}
-
-.dropzone:hover {
- border: var(--accent-color) 2px dashed;
-}
-
-.dropzone:hover p {
- transform: scale(1.02);
-}
-
-.close-btn {
- position: absolute;
- top: 5px;
- right: 5px;
- background: none;
- border: none;
- color: var(--text-color);
- font-size: 1rem;
- cursor: pointer;
- padding: 5px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 50%;
- width: 28px;
- height: 28px;
- opacity: 0.6;
- transition: all 0.2s ease;
- background-color: var(--button-bg);
- border: 1px solid var(--border-color);
-}
-
-.close-btn:hover {
- opacity: 1;
- background-color: var(--color-danger-fg);
-}
-
-.editor-pane {
- overflow: hidden;
-}
-
-/* Mobile Menu Styles */
-.mobile-menu {
- display: none;
- position: relative;
- z-index: 1001;
-}
-
-@media (max-width: 1080px) {
- .mobile-menu {
- display: block;
- }
-}
-
-/* slideāin panel */
-.mobile-menu-panel {
- position: fixed;
- top: 0;
- right: -300px;
- width: 280px;
- height: 100vh;
- background-color: var(--bg-color);
- box-shadow: -2px 0 10px rgba(0, 0, 0, 0.2);
- transition: right 0.3s ease;
- overflow-y: auto;
- padding: 1rem;
- display: flex;
- flex-direction: column;
- z-index: 1002;
-}
-
-.mobile-menu-panel.active {
- right: 0;
-}
-
-/* translucent overlay behind panel */
-.mobile-menu-overlay {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100vh;
- background-color: rgba(0, 0, 0, 0.5);
- opacity: 0;
- visibility: hidden;
- transition: opacity 0.3s ease, visibility 0.3s ease;
- z-index: 1000;
-}
-
-.mobile-menu-overlay.active {
- display: block;
- opacity: 1;
- visibility: visible;
-}
-
-/* header inside mobile menu */
-.mobile-menu-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1rem;
-}
-
-.mobile-menu-header h5 {
- margin: 0;
- font-size: 1.25rem;
- color: var(--text-color);
-}
-
-/* stats section in mobile menu */
-.mobile-stats-container {
- border-bottom: 1px solid var(--border-color);
- padding-bottom: 0.75rem;
- margin-bottom: 1rem;
-}
-
-.mobile-stats-container .stat-item {
- font-size: 0.9rem;
- color: var(--text-color);
- display: flex;
- align-items: center;
-}
-
-.mobile-stats-container .stat-item i {
- margin-right: 0.5em;
- opacity: 0.8;
-}
-
-/* menu buttons list */
-.mobile-menu-items {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
- flex-grow: 1;
-}
-
-/* each menu item */
-.mobile-menu-item {
- background-color: var(--button-bg);
- border: 1px solid var(--border-color);
- color: var(--text-color);
- border-radius: 6px;
- padding: 0.6rem 1rem;
- font-size: 1rem;
- text-align: left;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- transition: background-color 0.2s ease;
- cursor: pointer;
-}
-
-.mobile-menu-item:hover {
- background-color: var(--button-hover);
-}
-
-.mobile-menu-item:active {
- background-color: var(--button-active);
-}
-
-/* close button override */
-#close-mobile-menu.tool-button {
- padding: 0.25rem 0.5rem;
- font-size: 1rem;
-}
-
-/* ensure dropzone doesnāt cover menu */
-.mobile-menu-panel .dropzone {
- margin-bottom: 0;
-}
-
-/* Mobile document tabs section */
-.mobile-tabs-section {
- border-bottom: 1px solid var(--border-color);
- padding-bottom: 0.75rem;
-}
-
-.mobile-tabs-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 0.5rem;
-}
-
-.mobile-tabs-label {
- font-size: 0.85rem;
- font-weight: 600;
- color: var(--text-color);
- opacity: 0.8;
- text-transform: uppercase;
- letter-spacing: 0.04em;
-}
-
-.mobile-new-tab-btn {
- background: none;
- border: 1px solid var(--border-color);
- border-radius: 4px;
- color: var(--text-color);
- padding: 2px 7px;
- font-size: 0.9rem;
- cursor: pointer;
- display: flex;
- align-items: center;
- transition: background-color 0.15s ease;
-}
-
-.mobile-new-tab-btn:hover {
- background-color: var(--button-hover);
-}
-
-.mobile-tab-list {
- display: flex;
- flex-direction: column;
- gap: 4px;
- max-height: 180px;
- overflow-y: auto;
-}
-
-.mobile-tab-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- background-color: var(--button-bg);
- border: 1px solid var(--border-color);
- border-radius: 6px;
- padding: 0.45rem 0.75rem;
- font-size: 0.9rem;
- color: var(--text-color);
- cursor: pointer;
- transition: background-color 0.15s ease;
- gap: 0.5rem;
-}
-
-.mobile-tab-item:hover {
- background-color: var(--button-hover);
-}
-
-.mobile-tab-item.active {
- border-color: var(--accent-color);
- color: var(--accent-color);
- background-color: var(--bg-color);
-}
-
-.mobile-tab-title {
- flex: 1;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0;
-}
-
-.mobile-tab-item .tab-menu-btn {
- opacity: 0.6;
-}
-
-.mobile-tab-item:hover .tab-menu-btn,
-.mobile-tab-item.active .tab-menu-btn {
- opacity: 0.8;
-}
-
-#mobile-tab-reset-btn {
- margin-left: 0;
- height: auto;
- padding: 0.45rem 0.75rem;
- justify-content: center;
- font-size: 0.9rem;
-}
-
-/* hide desktop-only stats and toolbar on mobile */
-@media (max-width: 767px) {
- .stats-container.d-none.d-md-flex,
- .toolbar.d-none.d-md-flex {
- display: none !important;
- }
-}
-
-.github-link {
- color: var(--text-color);
- text-decoration: none;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: transform 0.2s ease, color 0.2s ease;
- margin-right: 2rem;
-}
-
-.github-link:hover {
- color: var(--accent-color);
- transform: scale(1.1);
-}
-
-.github-link i {
- font-size: 1.5rem;
-}
-
-/* ========================================
- VIEW MODE CONTROLS - Story 1.1
- ======================================== */
-
-/* Header layout for three sections */
-.header-container {
- position: relative;
-}
-
-.header-left {
- flex: 1;
- justify-content: flex-start;
-}
-
-.header-right {
- flex: 1;
- justify-content: flex-end;
-}
-
-/* View Mode Button Group */
-.view-mode-group {
- display: flex;
- gap: 0;
- position: absolute;
- left: 50%;
- transform: translateX(-50%);
-}
-
-.view-mode-btn {
- background-color: var(--button-bg);
- border: 1px solid var(--border-color);
- color: var(--text-color);
- padding: 6px 12px;
- font-size: 14px;
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 4px;
- transition: all 0.2s ease;
-}
-
-.view-mode-btn:first-child {
- border-radius: 6px 0 0 6px;
-}
-
-.view-mode-btn:last-child {
- border-radius: 0 6px 6px 0;
-}
-
-.view-mode-btn:not(:last-child) {
- border-right: none;
-}
-
-.view-mode-btn:hover {
- background-color: var(--button-hover);
-}
-
-.view-mode-btn.active {
- background-color: var(--button-bg);
- border-color: var(--accent-color);
- color: var(--accent-color);
- border-width: 2px;
- padding: 5px 11px; /* Adjust for thicker border */
-}
-
-.view-mode-btn.active:not(:last-child) {
- border-right: 2px solid var(--accent-color);
-}
-
-.view-mode-btn i {
- font-size: 16px;
-}
-
-/* Pane View States */
-.content-container.view-editor-only .preview-pane {
- display: none;
-}
-
-.content-container.view-editor-only .editor-pane {
- flex: 1;
- border-right: none;
-}
-
-.content-container.view-preview-only .editor-pane {
- display: none;
-}
-
-.content-container.view-preview-only .preview-pane {
- flex: 1;
-}
-
-.content-container.view-split .editor-pane,
-.content-container.view-split .preview-pane {
- flex: 1;
-}
-
-/* Responsive adjustments for view mode buttons */
-@media (max-width: 1079px) {
- .view-mode-group {
- position: static;
- transform: none;
- }
-}
-
-@media (max-width: 767px) {
- .view-mode-group {
- display: none;
- }
-}
-
-/* ========================================
- RESIZE DIVIDER - Story 1.3
- ======================================== */
-
-.resize-divider {
- width: 8px;
- background-color: transparent;
- cursor: col-resize;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- position: relative;
- z-index: 10;
- transition: background-color 0.2s ease;
-}
-
-.resize-divider:hover {
- background-color: var(--button-hover);
-}
-
-.resize-divider.dragging {
- background-color: var(--accent-color);
-}
-
-.resize-divider-handle {
- width: 2px;
- height: 40px;
- background-color: var(--border-color);
- border-radius: 2px;
- transition: background-color 0.2s ease, width 0.2s ease;
-}
-
-.resize-divider:hover .resize-divider-handle,
-.resize-divider.dragging .resize-divider-handle {
- background-color: var(--accent-color);
- width: 3px;
-}
-
-/* Hide divider in single-pane modes */
-.content-container.view-editor-only .resize-divider,
-.content-container.view-preview-only .resize-divider {
- display: none;
-}
-
-/* Hide divider on tablet and mobile (no drag resize) */
-@media (max-width: 1079px) {
- .resize-divider {
- display: none;
- }
-}
-
-/* Prevent text selection during drag */
-.resizing {
- user-select: none;
- cursor: col-resize !important;
-}
-
-.resizing * {
- cursor: col-resize !important;
-}
-
-/* ========================================
- MOBILE VIEW MODE CONTROLS - Story 1.4
- ======================================== */
-
-.mobile-view-mode-group {
- display: flex;
- gap: 0;
- border-bottom: 1px solid var(--border-color);
- padding-bottom: 0.75rem;
-}
-
-.mobile-view-mode-btn {
- flex: 1;
- background-color: var(--button-bg);
- border: 1px solid var(--border-color);
- color: var(--text-color);
- padding: 8px 12px;
- font-size: 14px;
- cursor: pointer;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 4px;
- transition: all 0.2s ease;
-}
-
-.mobile-view-mode-btn:first-child {
- border-radius: 6px 0 0 6px;
-}
-
-.mobile-view-mode-btn:last-child {
- border-radius: 0 6px 6px 0;
-}
-
-.mobile-view-mode-btn:not(:last-child) {
- border-right: none;
-}
-
-.mobile-view-mode-btn:hover,
-.mobile-view-mode-btn:active {
- background-color: var(--button-hover);
-}
-
-.mobile-view-mode-btn.active {
- background-color: var(--button-bg);
- border-color: var(--accent-color);
- color: var(--accent-color);
- border-width: 2px;
- padding: 7px 11px;
-}
-
-.mobile-view-mode-btn.active:not(:last-child) {
- border-right: 2px solid var(--accent-color);
-}
-
-.mobile-view-mode-btn i {
- font-size: 18px;
-}
-
-.mobile-view-mode-btn span {
- font-size: 12px;
-}
-
-/* ========================================
- RESPONSIVE VIEW MODE FIXES - Story 1.5
- ======================================== */
-
-/* On tablet/mobile, ensure single-pane modes take full height */
-@media (max-width: 1079px) {
- .content-container.view-editor-only .editor-pane,
- .content-container.view-preview-only .preview-pane {
- height: 100%;
- }
-
- .content-container.view-split .editor-pane,
- .content-container.view-split .preview-pane {
- height: 50%;
- }
-}
-
-/* ========================================
- PDF EXPORT TABLE FIX - Rowspan/Colspan
- ======================================== */
-
-/* Fix for html2canvas not properly rendering rowspan/colspan cells.
- Apply backgrounds to cells instead of rows to prevent row backgrounds
- from painting over rowspan cells during canvas capture. */
-.pdf-export table tr {
- background-color: transparent !important;
-}
-
-.pdf-export table th,
-.pdf-export table td {
- background-color: var(--table-bg, #ffffff);
- position: relative;
-}
-
-.pdf-export table tr:nth-child(2n) th,
-.pdf-export table tr:nth-child(2n) td {
- background-color: var(--bg-color, #f6f8fa);
-}
-
-/* Ensure rowspan cells render correctly */
-.pdf-export table th[rowspan],
-.pdf-export table td[rowspan] {
- vertical-align: middle;
- background-color: var(--table-bg, #ffffff) !important;
-}
-
-/* Ensure colspan cells render correctly */
-.pdf-export table th[colspan],
-.pdf-export table td[colspan] {
- text-align: center;
-}
-
-/* Dark mode PDF export table fix */
-[data-theme="dark"] .pdf-export table th,
-[data-theme="dark"] .pdf-export table td {
- background-color: var(--table-bg, #161b22);
-}
-
-[data-theme="dark"] .pdf-export table tr:nth-child(2n) th,
-[data-theme="dark"] .pdf-export table tr:nth-child(2n) td {
- background-color: #1c2128;
-}
-
-[data-theme="dark"] .pdf-export table th[rowspan],
-[data-theme="dark"] .pdf-export table td[rowspan] {
- background-color: var(--table-bg, #161b22) !important;
-}
-
-/* ========================================
- MERMAID DIAGRAM TOOLBAR
- ======================================== */
-
-.mermaid-container {
- position: relative;
-}
-
-.mermaid-toolbar {
- position: absolute;
- top: 8px;
- right: 8px;
- display: flex;
- gap: 4px;
- opacity: 0;
- transition: opacity 0.2s ease;
- z-index: 10;
-}
-
-.mermaid-container:hover .mermaid-toolbar {
- opacity: 1;
-}
-
-.mermaid-toolbar-btn {
- background-color: var(--button-bg);
- border: 1px solid var(--border-color);
- color: var(--text-color);
- border-radius: 4px;
- padding: 4px 7px;
- font-size: 13px;
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 3px;
- transition: background-color 0.2s ease, color 0.2s ease;
- white-space: nowrap;
-}
-
-.mermaid-toolbar-btn:hover {
- background-color: var(--button-hover);
- color: var(--accent-color);
-}
-
-.mermaid-toolbar-btn:active {
- background-color: var(--button-active);
-}
-
-.mermaid-toolbar-btn i {
- font-size: 14px;
-}
-
-/* ========================================
- MERMAID ZOOM MODAL
- ======================================== */
-
-#mermaid-zoom-modal {
- display: none;
- position: fixed;
- inset: 0;
- z-index: 2000;
- background-color: rgba(0, 0, 0, 0.75);
- align-items: center;
- justify-content: center;
-}
-
-#mermaid-zoom-modal.active {
- display: flex;
-}
-
-.mermaid-modal-content {
- background-color: var(--bg-color);
- border: 1px solid var(--border-color);
- border-radius: 8px;
- padding: 16px;
- width: 85vw;
- height: 85vh;
- max-width: 85vw;
- max-height: 85vh;
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-@media (max-width: 576px) {
- .mermaid-modal-content {
- width: 95vw;
- height: 90vh;
- max-width: 95vw;
- max-height: 90vh;
- padding: 10px;
- }
-}
-
-.mermaid-modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.mermaid-modal-header span {
- font-weight: 600;
- font-size: 15px;
- color: var(--text-color);
-}
-
-.mermaid-modal-close {
- background: none;
- border: none;
- color: var(--text-color);
- font-size: 1.2rem;
- cursor: pointer;
- padding: 2px 6px;
- border-radius: 4px;
- display: flex;
- align-items: center;
- transition: background-color 0.2s ease;
-}
-
-.mermaid-modal-close:hover {
- background-color: var(--button-hover);
-}
-
-.mermaid-modal-diagram {
- overflow: auto;
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 200px;
- cursor: grab;
-}
-
-.mermaid-modal-diagram.dragging {
- cursor: grabbing;
-}
-
-.mermaid-modal-diagram svg {
- transform-origin: center;
- transition: transform 0.1s ease;
- max-width: none;
-}
-
-.mermaid-modal-controls {
- display: flex;
- justify-content: center;
- gap: 8px;
- flex-wrap: wrap;
-}
-
-.mermaid-modal-controls .mermaid-toolbar-btn {
- opacity: 1;
-}
-
-/* ========================================
- DOCUMENT TABS & SESSION MANAGEMENT
- ======================================== */
-
-.tab-bar {
- display: flex;
- align-items: center;
- background-color: var(--header-bg);
- border-bottom: 1px solid var(--border-color);
- height: 36px;
- overflow: visible; /* ā was: overflow: hidden */
- flex-shrink: 0;
- padding: 0 4px;
- gap: 0;
- user-select: none;
- position: relative;
- z-index: 10;
-}
-
-.tab-list {
- display: flex;
- align-items: flex-end;
- overflow-x: auto;
- overflow-y: visible; /* ā was: overflow-y: hidden */
- flex: 1;
- height: 100%;
- scrollbar-width: none;
- -ms-overflow-style: none;
-}
-
-.tab-list::-webkit-scrollbar {
- display: none;
-}
-
-.tab-item {
- display: flex;
- align-items: center;
- gap: 6px;
- height: 30px;
- padding: 0 10px 0 12px;
- min-width: 100px;
- max-width: 180px;
- background-color: var(--button-bg);
- border: 1px solid var(--border-color);
- border-bottom: none;
- border-radius: 6px 6px 0 0;
- cursor: pointer;
- font-size: 13px;
- color: var(--text-color);
- white-space: nowrap;
- /* overflow: hidden; <-- REMOVE THIS */
- position: relative;
- transition: background-color 0.15s ease, color 0.15s ease;
- flex-shrink: 0;
- margin-right: 2px;
- opacity: 0.7;
-}
-
-.tab-item:hover {
- background-color: var(--button-hover);
- opacity: 0.9;
-}
-
-.tab-item.active {
- background-color: var(--bg-color);
- border-color: var(--border-color);
- color: var(--accent-color);
- border-bottom: 1px solid var(--bg-color);
- opacity: 1;
- z-index: 2;
-}
-
-.tab-item.unsaved::after {
- content: '';
- display: inline-block;
- width: 6px;
- height: 6px;
- background-color: var(--accent-color);
- border-radius: 50%;
- flex-shrink: 0;
- margin-left: 2px;
-}
-
-.tab-title {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- flex: 1;
- min-width: 0;
-}
-
-.tab-close-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 16px;
- height: 16px;
- border-radius: 3px;
- background: none;
- border: none;
- color: var(--text-color);
- cursor: pointer;
- padding: 0;
- font-size: 11px;
- opacity: 0;
- flex-shrink: 0;
- transition: background-color 0.15s ease, opacity 0.15s ease;
-}
-
-.tab-item:hover .tab-close-btn,
-.tab-item.active .tab-close-btn {
- opacity: 0.6;
-}
-
-.tab-close-btn:hover {
- background-color: var(--button-active);
- opacity: 1 !important;
- color: var(--color-danger-fg, #d73a49);
-}
-
-.tab-new-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- border-radius: 5px;
- background: none;
- border: 1px solid transparent;
- color: var(--text-color);
- cursor: pointer;
- font-size: 16px;
- flex-shrink: 0;
- margin-left: 4px;
- transition: background-color 0.15s ease, border-color 0.15s ease;
-}
-
-.tab-new-btn:hover {
- background-color: var(--button-hover);
- border-color: var(--border-color);
-}
-
-/* Drag-and-drop visual feedback */
-.tab-item.dragging {
- opacity: 0.4;
-}
-
-.tab-item.drag-over {
- border-left: 2px solid var(--accent-color);
-}
-
-/* Tab enter animation */
-@keyframes tabSlideIn {
- from { opacity: 0; transform: translateY(4px); }
- to { opacity: 0.7; transform: translateY(0); }
-}
-
-.tab-item {
- animation: tabSlideIn 0.12s ease forwards;
-}
-
-.tab-item.active {
- animation: none;
-}
-
-/* Hide tab bar on very small screens ā single-file use */
-@media (max-width: 480px) {
- .tab-bar {
- display: none;
- }
-}
-
-/* ========================================
- THREE-DOT TAB MENU
- ======================================== */
-
-.tab-menu-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 18px;
- height: 18px;
- border-radius: 3px;
- background: none;
- border: none;
- color: var(--text-color);
- cursor: pointer;
- padding: 0;
- font-size: 14px;
- font-weight: bold;
- letter-spacing: 1px;
- opacity: 0.65;
- flex-shrink: 0;
- transition: background-color 0.15s ease, opacity 0.15s ease;
- position: relative;
-}
-
-.tab-item:hover .tab-menu-btn,
-.tab-item.active .tab-menu-btn {
- opacity: 0.65;
-}
-
-.tab-menu-btn:hover {
- background-color: var(--button-active);
- opacity: 1 !important;
-}
-
-.tab-menu-dropdown {
- display: none;
- position: fixed;
- min-width: 130px;
- background-color: var(--header-bg);
- border: 1px solid var(--border-color);
- border-radius: 6px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
- z-index: 99999;
- overflow: hidden;
- flex-direction: column;
-}
-
-.tab-menu-btn.open .tab-menu-dropdown {
- display: flex;
-}
-
-.tab-menu-item {
- display: flex;
- align-items: center;
- gap: 7px;
- padding: 7px 12px;
- background: none;
- border: none;
- color: var(--text-color);
- font-size: 12px;
- cursor: pointer;
- text-align: left;
- transition: background-color 0.12s ease;
- white-space: nowrap;
-}
-
-.tab-menu-item:hover {
- background-color: var(--button-hover);
-}
-
-.tab-menu-item-danger {
- color: var(--color-danger-fg, #d73a49);
-}
-
-.tab-menu-item-danger:hover {
- background-color: rgba(215, 58, 73, 0.1);
-}
-
-/* ========================================
- RESET BUTTON
- ======================================== */
-
-.tab-reset-btn {
- display: flex;
- align-items: center;
- gap: 4px;
- height: 26px;
- padding: 0 10px;
- border-radius: 5px;
- background: none;
- border: 1px solid var(--border-color);
- color: var(--text-color);
- cursor: pointer;
- font-size: 12px;
- flex-shrink: 0;
- margin-left: 6px;
- transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
-}
-
-.tab-reset-btn:hover {
- background-color: rgba(215, 58, 73, 0.1);
- border-color: var(--color-danger-fg, #d73a49);
- color: var(--color-danger-fg, #d73a49);
-}
-
-/* ========================================
- RESET & RENAME CONFIRMATION MODALS
- ======================================== */
-
-.reset-modal-overlay {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.45);
- z-index: 2000;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.reset-modal-box {
- background: var(--header-bg);
- border: 1px solid var(--border-color);
- border-radius: 10px;
- padding: 24px 28px;
- min-width: 280px;
- max-width: 360px;
- box-shadow: 0 8px 32px rgba(0,0,0,0.25);
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
-
-.reset-modal-message {
- margin: 0;
- font-size: 14px;
- color: var(--text-color);
- font-weight: 500;
- text-align: center;
-}
-
-.reset-modal-actions {
- display: flex;
- gap: 10px;
- justify-content: flex-end;
-}
-
-.reset-modal-btn {
- padding: 6px 16px;
- border-radius: 6px;
- border: 1px solid var(--border-color);
- background: var(--button-bg);
- color: var(--text-color);
- font-size: 13px;
- cursor: pointer;
- transition: background-color 0.15s ease;
-}
-
-.reset-modal-btn:hover {
- background-color: var(--button-hover);
-}
-
-.reset-modal-confirm {
- background-color: var(--color-danger-fg, #d73a49);
- border-color: var(--color-danger-fg, #d73a49);
- color: #fff;
-}
-
-.reset-modal-confirm:hover {
- background-color: #b02a37;
- border-color: #b02a37;
-}
-
-/* ========================================
- RENAME MODAL INPUT
- ======================================== */
-
-.rename-modal-input {
- width: 100%;
- padding: 7px 10px;
- border-radius: 6px;
- border: 1px solid var(--border-color);
- background: var(--bg-color);
- color: var(--text-color);
- font-size: 13px;
- outline: none;
- box-sizing: border-box;
-}
-
-.rename-modal-input:focus {
- border-color: var(--accent-color);
-}
-
-.github-import-error {
- margin: 0;
- font-size: 12px;
- color: var(--color-danger-fg, #d73a49);
- text-align: left;
- line-height: 1.5;
-}
-
-.github-import-error.is-info {
- color: var(--text-secondary, #57606a);
-}
-
-#github-import-modal .reset-modal-box {
- width: 60vw;
- max-width: 60vw;
- min-width: 340px;
- padding: 30px 34px;
- gap: 16px;
- box-shadow: 0 20px 48px rgba(0, 0, 0, 0.22);
-}
-
-#github-import-modal .reset-modal-message {
- font-size: 18px;
- line-height: 1.35;
- text-align: left;
-}
-
-#github-import-url,
-#github-import-file-select {
- min-height: 46px;
- padding: 10px 12px;
- font-size: 15px;
-}
-
-#github-import-file-select {
- min-height: 180px;
-}
-
-.github-import-tree {
- max-height: 420px;
- overflow: auto;
- border: 1px solid var(--border-color);
- border-radius: 10px;
- padding: 12px;
- background: var(--bg-color);
-}
-
-.github-import-selection-toolbar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- padding: 10px 12px;
- border: 1px solid var(--border-color);
- border-radius: 8px;
- background: var(--button-bg);
-}
-
-.github-import-selected-count {
- font-size: 14px;
- font-weight: 600;
- color: var(--text-color);
-}
-
-.github-import-tree ul {
- list-style: none;
- margin: 0;
- padding-left: 18px;
-}
-
-.github-import-tree > ul {
- padding-left: 4px;
-}
-
-.github-import-tree li {
- margin: 2px 0;
-}
-
-.github-tree-folder-label {
- display: inline-block;
- font-size: 14px;
- color: var(--text-secondary, #57606a);
- margin-bottom: 4px;
-}
-
-.github-tree-file-btn {
- border: 0;
- background: transparent;
- color: var(--text-color);
- cursor: pointer;
- padding: 6px 8px;
- border-radius: 6px;
- text-align: left;
- width: 100%;
- font-size: 14px;
-}
-
-.github-tree-file-btn:hover,
-.github-tree-file-btn:focus-visible {
- background: var(--button-hover);
- outline: none;
-}
-
-.github-tree-file-btn.is-selected {
- background: rgba(56, 139, 253, 0.14);
- color: var(--accent-color);
-}
-
-#github-import-modal .reset-modal-actions {
- gap: 12px;
-}
-
-#github-import-modal .reset-modal-btn {
- min-height: 42px;
- padding: 9px 18px;
- font-size: 14px;
-}
-
-@media (max-width: 576px) {
- #github-import-modal .reset-modal-box {
- width: 95vw;
- max-width: 95vw;
- min-width: 0;
- padding: 20px;
- gap: 14px;
- }
-
- .github-import-selection-toolbar {
- flex-direction: column;
- align-items: stretch;
- }
-
- #github-import-modal .reset-modal-message {
- font-size: 16px;
- }
-
- #github-import-modal .reset-modal-actions {
- flex-direction: column-reverse;
- }
-
- #github-import-modal .reset-modal-btn {
- width: 100%;
- }
-}
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000..25e000b
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,30 @@
+{
+ "version": 2,
+ "buildCommand": "echo 'Static files ready'",
+ "outputDirectory": "web",
+ "cleanUrls": true,
+ "trailingSlash": false,
+ "headers": [
+ {
+ "source": "/(.*)",
+ "headers": [
+ { "key": "X-Frame-Options", "value": "SAMEORIGIN" },
+ { "key": "X-Content-Type-Options", "value": "nosniff" },
+ { "key": "X-XSS-Protection", "value": "1; mode=block" },
+ { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
+ ]
+ },
+ {
+ "source": "/assets/(.*)",
+ "headers": [
+ { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
+ ]
+ }
+ ],
+ "rewrites": [
+ {
+ "source": "/(.*)",
+ "destination": "/index.html"
+ }
+ ]
+}
diff --git a/assets/Black and Beige Simple Coming Soon Banner.png b/web/assets/Black and Beige Simple Coming Soon Banner.png
similarity index 100%
rename from assets/Black and Beige Simple Coming Soon Banner.png
rename to web/assets/Black and Beige Simple Coming Soon Banner.png
diff --git a/assets/code.png b/web/assets/code.png
similarity index 100%
rename from assets/code.png
rename to web/assets/code.png
diff --git a/assets/github.png b/web/assets/github.png
similarity index 100%
rename from assets/github.png
rename to web/assets/github.png
diff --git a/assets/icon.jpg b/web/assets/icon.jpg
similarity index 100%
rename from assets/icon.jpg
rename to web/assets/icon.jpg
diff --git a/assets/live-peview.gif b/web/assets/live-peview.gif
similarity index 100%
rename from assets/live-peview.gif
rename to web/assets/live-peview.gif
diff --git a/assets/mathexp.png b/web/assets/mathexp.png
similarity index 100%
rename from assets/mathexp.png
rename to web/assets/mathexp.png
diff --git a/assets/mermaid.png b/web/assets/mermaid.png
similarity index 100%
rename from assets/mermaid.png
rename to web/assets/mermaid.png
diff --git a/assets/table.png b/web/assets/table.png
similarity index 100%
rename from assets/table.png
rename to web/assets/table.png
diff --git a/desktop-app/resources/index.html b/web/index.html
similarity index 61%
rename from desktop-app/resources/index.html
rename to web/index.html
index 87ae86c..7f556a5 100644
--- a/desktop-app/resources/index.html
+++ b/web/index.html
@@ -28,40 +28,33 @@
Markdown Viewer
-
+
-
-
-
+
+
+
+
-
+
-
+
-
-
+
+
-
-
+
-
-
-
-