diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 0a3a28e..ab31660 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -4,13 +4,14 @@ on: push: tags: - "desktop-v*" + workflow_dispatch: permissions: contents: write jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout repository @@ -21,13 +22,21 @@ jobs: with: node-version: "lts/*" + - name: Inject version from tag + working-directory: desktop-app + run: | + VERSION="${GITHUB_REF_NAME#desktop-v}" + jq --arg v "$VERSION" '.version = $v' neutralino.config.json > tmp.json \ + && mv tmp.json neutralino.config.json + echo "::notice::Building version $VERSION" + - name: Setup Neutralinojs binaries working-directory: desktop-app run: npm run setup - name: Build all binaries (embedded + portable) working-directory: desktop-app - run: npm run build:all + run: npm run build - name: Stage release assets working-directory: desktop-app @@ -63,6 +72,7 @@ jobs: --exclude='desktop-app/bin' \ --exclude='desktop-app/node_modules' \ --exclude='desktop-app/output' \ + --exclude="desktop-app/$STAGING" \ --exclude='.git' \ desktop-app/ cd desktop-app @@ -74,6 +84,6 @@ jobs: - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: - name: "Markdown Viewer Desktop ${{ github.ref_name }}" + name: "${{ github.ref_name }}" generate_release_notes: true files: desktop-app/release-assets/* diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 6a5177a..a6d8f22 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -2,9 +2,13 @@ name: Build and Push Docker Image on: push: - branches: [ main ] + branches: [main] + paths-ignore: + - "desktop-app/**" pull_request: - branches: [ main ] + branches: [main] + paths-ignore: + - "desktop-app/**" env: REGISTRY: ghcr.io @@ -18,37 +22,36 @@ jobs: packages: write steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=pr - type=sha,prefix=sha- - type=raw,value=latest,enable={{is_default_branch}} - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3047a5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Salesforce +.sfdx/ + +# Node +node_modules/ +*.log +npm-debug.log* + +# OS generated files +.DS_Store +Thumbs.db + +# Editor +.vscode/ +.idea/ diff --git a/README.md b/README.md index 8008159..beae75c 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,12 @@ License + ## šŸš€ Overview Markdown Viewer is a professional, full-featured Markdown editor and preview application that runs entirely in your browser. It provides a GitHub-style rendering experience with a clean split-screen interface, allowing you to write Markdown on one side and instantly preview the formatted output on the other. + ## ✨ Features - **GitHub-style Markdown rendering** - See your Markdown exactly as it would appear on GitHub @@ -56,6 +58,24 @@ Markdown Viewer is a professional, full-featured Markdown editor and preview app 5. **Toggle Dark Mode** - Click the moon icon to switch between light and dark themes 6. **Toggle Sync Scrolling** - Enable/disable synchronized scrolling between panels +## šŸ’» Run With npm + +From the project root: + +```bash +npm install +npm run dev +``` + +This starts a local server at `http://localhost:5173`. + +Desktop app shortcuts are also available from the root: + +```bash +npm run desktop:install +npm run desktop:dev +``` + ### Mermaid Diagram Toolbar When a Mermaid diagram is rendered, hover over it to reveal a small toolbar with the following actions: diff --git a/desktop-app/.dockerignore b/desktop-app/.dockerignore index 28f2c2a..6be7bc7 100644 --- a/desktop-app/.dockerignore +++ b/desktop-app/.dockerignore @@ -1,5 +1,6 @@ # Build-generated resources resources/js/script.js +resources/js/neutralino* resources/styles.css resources/assets/ resources/index.html diff --git a/desktop-app/.gitignore b/desktop-app/.gitignore index 4c77b9d..9fc6d7b 100644 --- a/desktop-app/.gitignore +++ b/desktop-app/.gitignore @@ -5,17 +5,16 @@ node_modules/ .lite_workspace.lua # Neutralinojs binaries and builds -/bin -/dist - -# Neutralinojs client (minified) -neutralino.js +bin/ +dist/ # Build-generated resources (copied from root by prepare.js) -/resources/js/script.js -/resources/styles.css -/resources/assets/ -/resources/index.html +resources/js/script.js +resources/js/neutralino* +resources/styles.css +resources/assets/ +resources/index.html + # Neutralinojs related files .storage @@ -25,4 +24,4 @@ neutralino.js .tmp # Docker build output -/output \ No newline at end of file +output/ \ No newline at end of file diff --git a/desktop-app/Dockerfile b/desktop-app/Dockerfile index 9c5f12d..1f613b1 100644 --- a/desktop-app/Dockerfile +++ b/desktop-app/Dockerfile @@ -13,7 +13,7 @@ COPY . . WORKDIR /app/desktop-app # Setup (download binaries + prepare resources) and build all variants -RUN npm run build:all +RUN npm run build # Final stage: Export the dist artifacts FROM alpine:latest diff --git a/desktop-app/README.md b/desktop-app/README.md index 210d881..a925842 100644 --- a/desktop-app/README.md +++ b/desktop-app/README.md @@ -11,9 +11,8 @@ Neutralinojs platform binaries are managed by `setup-binaries.js`, which downloa Desktop-only files (not generated): - `resources/js/main.js` — Neutralinojs lifecycle, tray menu, window events -- `resources/js/neutralino.js` — Neutralinojs client library - `neutralino.config.json` — App configuration -- `setup-binaries.js` — Idempotent binary setup (downloads on first use) +- `setup-binaries.js` — Idempotent binary setup (downloads on first use or updates if `cli.binaryVersion` changes) ## Development @@ -45,7 +44,7 @@ For more information, see the [Neutralinojs documentation](https://neutralino.js ### Building the app -**Default** — Single-file executables with embedded resources: +**Default** — Single-file executables with embedded resources + release ZIP bundle with separate `resources.neu` file: ```bash npm run build @@ -57,10 +56,10 @@ npm run build npm run build:portable ``` -**Both** — Build embedded + portable in one step: +**Embedded** — Single-file executables with embedded resources: ```bash -npm run build:all +npm run build:embedded ``` Build output is placed in `dist/`. @@ -79,7 +78,31 @@ Build artifacts will be output to `desktop-app/output/`. ## Releases -Prebuilt binaries are automatically built and published as GitHub Releases when a tag matching `desktop-v*` is pushed (e.g., `desktop-v1.0.0`). See [`.github/workflows/desktop-build.yml`](../.github/workflows/desktop-build.yml). +Prebuilt binaries are automatically built and published as GitHub Releases when a tag matching `desktop-v*` is pushed (e.g., `desktop-v2026.2.0`). See [`.github/workflows/desktop-build.yml`](../.github/workflows/desktop-build.yml). + +### Versioning + +The Git tag is the **single source of truth** for the release version, using CalVer (Calendar Versioning) format `desktop-vYYYY.M.P`; + +- `YYYY` = Year +- `M` = Month +- `P` = Patch (Defaults to 0, bumped if new release occurs same month) + +The CI workflow extracts the version from the tag (e.g., `desktop-v2026.2.0` → `2026.2.0`) and injects it into `neutralino.config.json` at build time. `package.json` carries a placeholder version (`0.0.0-dev`) since this is *not* an npm package. + +To create a release, you can use the utility script `tag.sh` to calculate the next [lightweight tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging): + +```bash +./tag.sh # Calculates the next tag based on the current date, latest tag, and commit SHA +``` + +or run the following commands, replacing `` with the desired version (e.g., `2026.2.1`): + +```bash +git tag desktop-v && git push origin desktop-v +``` + +### Release assets Each release includes: diff --git a/desktop-app/neutralino.config.json b/desktop-app/neutralino.config.json index cc55a4e..a2c7fca 100644 --- a/desktop-app/neutralino.config.json +++ b/desktop-app/neutralino.config.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json", - "applicationId": "js.neutralino.sample", - "version": "1.0.0", + "applicationId": "js.markdownviewer.desktop", + "version": "2026.2.0", "defaultMode": "window", "port": 0, "documentRoot": "/resources/", @@ -13,7 +13,7 @@ "enabled": true, "writeToLogFile": true }, - "nativeAllowList": ["app.*", "os.*", "filesystem.readFile", "debug.log"], + "nativeAllowList": ["app.*", "os.*", "debug.log"], "globalVariables": {}, "modes": { "window": { diff --git a/desktop-app/package-lock.json b/desktop-app/package-lock.json index c95ab6d..e9d5d81 100644 --- a/desktop-app/package-lock.json +++ b/desktop-app/package-lock.json @@ -1,12 +1,13 @@ { "name": "markdown-viewer-desktop", - "version": "1.0.0", + "version": "0.0.0-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "markdown-viewer-desktop", - "version": "1.0.0" + "version": "0.0.0-dev", + "license": "MIT" } } } diff --git a/desktop-app/package.json b/desktop-app/package.json index b02da6d..5ba9ba3 100644 --- a/desktop-app/package.json +++ b/desktop-app/package.json @@ -1,17 +1,29 @@ { "name": "markdown-viewer-desktop", - "version": "1.0.0", + "author": "ramezio", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/ramezio/markdown-viewer-fork.git" + }, + "contributors": [ + "ramezio", + "JBroeren", + "ThisIs-Developer" + ], + "version": "0.0.0-dev", "private": true, - "description": "Neutralinojs desktop port of Markdown Viewer", + "description": "Neutralinojs desktop port of Markdown Viewer (https://github.com/ThisIs-Developer/markdown-viewer)", "scripts": { "setup": "node setup-binaries.js", "postsetup": "node prepare.js", + "clean": "npx -y rimraf bin dist node_modules .tmp .neutralinojs.log resources/js/script.js resources/styles.css resources/assets resources/index.html resources/js/neutralino.js resources/js/neutralino.d.ts", "predev": "npm run setup", - "dev": "npx -y @neutralinojs/neu@11.7.0 run", + "dev": "npx -y @neutralinojs/neu@11.7.1 run", "prebuild": "npm run setup", - "build": "npx -y @neutralinojs/neu@11.7.0 build --embed-resources", - "build:portable": "npx -y @neutralinojs/neu@11.7.0 build --release", - "build:all": "npm run build && npm run build:portable" + "build": "npx -y @neutralinojs/neu@11.7.1 build --embed-resources --release", + "build:portable": "npm run setup && npx -y @neutralinojs/neu@11.7.1 build --release", + "build:embedded": "npm run setup && npx -y @neutralinojs/neu@11.7.1 build --embed-resources" }, "dependencies": {} } diff --git a/desktop-app/prepare.js b/desktop-app/prepare.js index 2e03333..bbb1df0 100644 --- a/desktop-app/prepare.js +++ b/desktop-app/prepare.js @@ -15,7 +15,7 @@ const fs = require("fs"); const path = require("path"); -const ROOT_DIR = path.resolve(__dirname, ".."); +const ROOT_DIR = path.resolve(__dirname, "../web"); const RESOURCES_DIR = path.resolve(__dirname, "resources"); /** @section Copy shared files */ @@ -36,25 +36,25 @@ function copyDirSync(src, dest) { } } -/** script.js → resources/js/script.js */ +/** web/script.js → resources/js/script.js */ const jsDest = path.join(RESOURCES_DIR, "js"); fs.mkdirSync(jsDest, { recursive: true }); fs.copyFileSync( path.join(ROOT_DIR, "script.js"), path.join(jsDest, "script.js"), ); -console.log("āœ“ Copied script.js → resources/js/script.js"); +console.log("āœ“ Copied web/script.js → resources/js/script.js"); -/** styles.css → resources/styles.css */ +/** web/styles.css → resources/styles.css */ fs.copyFileSync( path.join(ROOT_DIR, "styles.css"), path.join(RESOURCES_DIR, "styles.css"), ); -console.log("āœ“ Copied styles.css → resources/styles.css"); +console.log("āœ“ Copied web/styles.css → resources/styles.css"); -/** assets/ → resources/assets/ */ +/** web/assets/ → resources/assets/ */ copyDirSync(path.join(ROOT_DIR, "assets"), path.join(RESOURCES_DIR, "assets")); -console.log("āœ“ Copied assets/ → resources/assets/"); +console.log("āœ“ Copied web/assets/ → resources/assets/"); /** @section Generate index.html with Neutralinojs injections */ @@ -64,20 +64,39 @@ let html = fs.readFileSync(path.join(ROOT_DIR, "index.html"), "utf-8"); html = html.replace(/href="assets\//g, 'href="/assets/'); html = html.replace(/href="styles\.css"/g, 'href="/styles.css"'); /** Replace root script.js tag with neutralino.js + main.js + script.js under /js/ */ +const originalHtml = html; +const scriptTagRegex = /<\/script>/; + +if (!scriptTagRegex.test(html)) { + console.error("āœ— Could not find root script.js tag in index.html"); + process.exit(1); +} + html = html.replace( - /\n \n ', ); /** Inject Neutralinojs app-info element after .app-container */ +const appContainerMarker = '
'; +if (!html.includes(appContainerMarker)) { + console.error("āœ— Could not find app container marker in index.html"); + process.exit(1); +} + html = html.replace( - '
', + appContainerMarker, `
`, ); +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 `
${code}
`; - } - - 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 TypeSupport
Markdown Viewer (Ours)Other Markdown Editors
Technical DocsFull + DiagramsLimited/Basic
Research NotesFull + MathPartial
Developer GuidesFull + Export OptionsBasic
- -## šŸ“ 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: H2O, 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: -![Markdown Logo](https://markdownviewer.pages.dev/assets/icon.jpg) - -### **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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- -
-

Markdown Viewer

- - - -
-
- 0 Min Read -
-
- 0 Words -
-
- 0 Chars -
-
-
- - -
- - - -
- - -
- - - - - - - - - - - -
- - -
- - -
-
-
Menu
- -
- - -
-
- Documents - -
-
- -
- -
- - -
- - - -
- -
-
- 0 Min Read -
-
- 0 Words -
-
- 0 Chars -
-
- -
- - - - - - - - - - - - - - - - - -
-
- -
-
-
-
- - -
-
- -
- - - - - - - - - - -
- -

Drop your Markdown file here or click to browse

-
- -
-
- -
- - -
-
-
-
-
- - - - - - - - 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 `
${code}
`; - } - - 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 TypeSupport
Markdown Viewer (Ours)Other Markdown Editors
Technical DocsFull + DiagramsLimited/Basic
Research NotesFull + MathPartial
Developer GuidesFull + Export OptionsBasic
- -## šŸ“ 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: H2O, 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: -![Markdown Logo](https://markdownviewer.pages.dev/assets/icon.jpg) - -### **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 - + - - - + + + + - + - + - - + + - - + - - - -
-
-
-
-
+

Markdown Viewer

@@ -100,17 +93,24 @@

Markdown Viewer

+ + - -
-
- Documents - -
-
- -
- -
-
- - - - + + + + + + + - - @@ -239,51 +229,6 @@
Menu
- -
-
- -
- - - - - - - - - -
- -