From 16c0e002010adb0528df8aa2fd298bb327aa4fe1 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Mon, 12 Jan 2026 17:58:55 +0530 Subject: [PATCH] Normalize line endings and remove rpm from Linux build targets - Normalize line endings across all files - Remove rpm target from Linux build (requires rpmbuild) Co-Authored-By: Claude Opus 4.5 --- .gitignore | 70 +- .prettierrc | 22 +- EXPORT_FIX_SUMMARY.md | 204 +- GEMINI.md | 164 +- IMPROVEMENT_PLAN.md | 2956 ++++----- LICENSE | 40 +- PUSH_INSTRUCTIONS.md | 24 +- README.latex | 756 +-- README.md | 330 +- UPDATES.md | 332 +- eslint.config.js | 178 +- jest.config.js | 118 +- package.json | 363 +- push-to-github.sh | 164 +- scripts/README.md | 230 +- scripts/generate-icons.js | 164 +- scripts/install-context-menu.bat | 94 +- scripts/install-context-menu.ps1 | 132 +- scripts/install-context-menu.reg | 266 +- scripts/nsis-installer.nsh | 288 +- scripts/uninstall-context-menu.bat | 62 +- scripts/uninstall-context-menu.reg | 70 +- setup-upstream.sh | 74 +- src/ascii-generator.html | 1202 ++-- src/fonts.css | 150 +- src/index.html | 2826 ++++----- src/main.js | 7588 +++++++++++------------ src/preload.js | 752 +-- src/renderer.js | 9028 ++++++++++++++-------------- src/styles-concreteinfo.css | 1938 +++--- src/styles-modern.css | 4226 ++++++------- src/styles.css | 8160 ++++++++++++------------- src/table-generator.html | 1090 ++-- src/wordTemplateExporter.js | 1486 ++--- test-double-click.md | 24 +- test-export-functionality.js | 96 +- test-export.md | 50 +- test-file-open.md | 28 +- test.md | 44 +- tests/preload.test.js | 242 +- tests/setup.js | 200 +- tests/utils.test.js | 274 +- 42 files changed, 23252 insertions(+), 23253 deletions(-) diff --git a/.gitignore b/.gitignore index b47f6b3..7a31b98 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,36 @@ -node_modules/ -dist/ -*.log -.DS_Store -Thumbs.db -.env -.env.local -*.swp -*.swo -*~ -.vscode/ -.idea/ -*.iml -out/ -.cache/ -.npm/ -.electron/ -package-lock.json - -# Screenshots and temp files -*.png.bak -Screen.png -dark.png -light.png -pdf.png -uvmodal.png -nul -*.tmp - -# Development screenshots -pdf\ modal.png - -# Claude/AI development files -.claude/ -CLAUDE.md +node_modules/ +dist/ +*.log +.DS_Store +Thumbs.db +.env +.env.local +*.swp +*.swo +*~ +.vscode/ +.idea/ +*.iml +out/ +.cache/ +.npm/ +.electron/ +package-lock.json + +# Screenshots and temp files +*.png.bak +Screen.png +dark.png +light.png +pdf.png +uvmodal.png +nul +*.tmp + +# Development screenshots +pdf\ modal.png + +# Claude/AI development files +.claude/ +CLAUDE.md agents.md \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 0256aae..79ea2ab 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,11 +1,11 @@ -{ - "semi": true, - "singleQuote": true, - "tabWidth": 2, - "useTabs": false, - "trailingComma": "es5", - "bracketSpacing": true, - "arrowParens": "always", - "printWidth": 100, - "endOfLine": "auto" -} +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false, + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "always", + "printWidth": 100, + "endOfLine": "auto" +} diff --git a/EXPORT_FIX_SUMMARY.md b/EXPORT_FIX_SUMMARY.md index 955c319..d12f975 100644 --- a/EXPORT_FIX_SUMMARY.md +++ b/EXPORT_FIX_SUMMARY.md @@ -1,103 +1,103 @@ -# Export Functionality Fix - Summary - -## Issues Found and Fixed - -### 1. **Primary Issue: Pandoc Installation Problem** -- **Problem**: Pandoc is installed but has a system error (paging file too small) -- **Impact**: All pandoc-dependent exports (DOCX, LaTeX, etc.) were failing -- **Solution**: Added robust fallback mechanisms - -### 2. **Export Function Improvements** - -#### Before (Issues): -- ❌ No pandoc availability checking -- ❌ Poor error messages -- ❌ No fallback for missing pandoc -- ❌ Limited debugging information - -#### After (Fixed): -- ✅ **Pandoc Detection**: Automatically checks if pandoc is available -- ✅ **Built-in HTML Export**: Works without pandoc using marked library -- ✅ **Built-in PDF Export**: Works without pandoc using Electron's printToPDF -- ✅ **Better Error Messages**: Clear instructions for users -- ✅ **Comprehensive Logging**: Debug information in console -- ✅ **Graceful Fallbacks**: Falls back to built-in converters when pandoc fails - -## How It Works Now - -### Export Process Flow: -1. **User clicks export** → Check if file is saved -2. **Select output location** → Show save dialog -3. **Check pandoc availability** → Async pandoc detection -4. **Choose export method**: - - **If pandoc available**: Use pandoc with format-specific options - - **If pandoc not available**: - - HTML → Use built-in marked converter - - PDF → Use Electron's printToPDF - - Other formats → Show helpful error with installation guide - -### Supported Export Formats: - -#### ✅ **Always Work** (no pandoc required): -- **HTML**: Built-in converter using marked library -- **PDF**: Built-in converter using Electron - -#### ✅ **Work with Pandoc** (better quality): -- **DOCX**: Microsoft Word format -- **LaTeX**: LaTeX document -- **RTF**: Rich Text Format -- **ODT**: OpenDocument Text -- **EPUB**: E-book format -- **PPTX**: PowerPoint presentations -- **ODP**: OpenDocument Presentations - -## Testing the Fixes - -### Manual Test Procedure: -1. **Start the application**: `npm start` -2. **Open test file**: Load `test-export.md` -3. **Test HTML export**: File → Export → HTML (should work) -4. **Test PDF export**: File → Export → PDF (should work) -5. **Test DOCX export**: File → Export → DOCX (will show pandoc error) - -### Expected Behavior: -- **HTML/PDF exports**: Should work immediately and create files -- **Other format exports**: Should show informative error about pandoc -- **Console logs**: Should show debug information about export process - -## Fix Summary - -### Code Changes Made: -1. **Added `checkPandocAvailability()` function** - Detects pandoc -2. **Added `exportToHTML()` function** - Built-in HTML export -3. **Added `exportToPDFElectron()` function** - Built-in PDF export -4. **Added `exportWithPandoc()` helper** - Generic pandoc export -5. **Added `exportWithPandocPDF()` helper** - PDF with fallbacks -6. **Improved `exportFile()` function** - Main export logic with detection -7. **Enhanced error handling** - Better user messages -8. **Added comprehensive logging** - Debug information - -### Files Modified: -- `src/main.js` - Enhanced export functionality -- `test-export.md` - Created test file -- `test-export-functionality.js` - Created test script - -## User Instructions - -### For Users Without Pandoc: -- ✅ **HTML and PDF exports work perfectly** -- ✅ **No additional software needed** -- ✅ **Professional-looking output with proper styling** - -### For Users Who Want All Formats: -1. **Install Pandoc**: Visit https://pandoc.org/installing.html -2. **For PDF with LaTeX**: Also install MiKTeX or TeX Live -3. **Restart the application** after installation -4. **All export formats will then be available** - -## Result -🎉 **Export functionality is now working reliably!** -- Built-in exports (HTML, PDF) work without any dependencies -- Clear error messages guide users for advanced formats -- Robust error handling prevents crashes +# Export Functionality Fix - Summary + +## Issues Found and Fixed + +### 1. **Primary Issue: Pandoc Installation Problem** +- **Problem**: Pandoc is installed but has a system error (paging file too small) +- **Impact**: All pandoc-dependent exports (DOCX, LaTeX, etc.) were failing +- **Solution**: Added robust fallback mechanisms + +### 2. **Export Function Improvements** + +#### Before (Issues): +- ❌ No pandoc availability checking +- ❌ Poor error messages +- ❌ No fallback for missing pandoc +- ❌ Limited debugging information + +#### After (Fixed): +- ✅ **Pandoc Detection**: Automatically checks if pandoc is available +- ✅ **Built-in HTML Export**: Works without pandoc using marked library +- ✅ **Built-in PDF Export**: Works without pandoc using Electron's printToPDF +- ✅ **Better Error Messages**: Clear instructions for users +- ✅ **Comprehensive Logging**: Debug information in console +- ✅ **Graceful Fallbacks**: Falls back to built-in converters when pandoc fails + +## How It Works Now + +### Export Process Flow: +1. **User clicks export** → Check if file is saved +2. **Select output location** → Show save dialog +3. **Check pandoc availability** → Async pandoc detection +4. **Choose export method**: + - **If pandoc available**: Use pandoc with format-specific options + - **If pandoc not available**: + - HTML → Use built-in marked converter + - PDF → Use Electron's printToPDF + - Other formats → Show helpful error with installation guide + +### Supported Export Formats: + +#### ✅ **Always Work** (no pandoc required): +- **HTML**: Built-in converter using marked library +- **PDF**: Built-in converter using Electron + +#### ✅ **Work with Pandoc** (better quality): +- **DOCX**: Microsoft Word format +- **LaTeX**: LaTeX document +- **RTF**: Rich Text Format +- **ODT**: OpenDocument Text +- **EPUB**: E-book format +- **PPTX**: PowerPoint presentations +- **ODP**: OpenDocument Presentations + +## Testing the Fixes + +### Manual Test Procedure: +1. **Start the application**: `npm start` +2. **Open test file**: Load `test-export.md` +3. **Test HTML export**: File → Export → HTML (should work) +4. **Test PDF export**: File → Export → PDF (should work) +5. **Test DOCX export**: File → Export → DOCX (will show pandoc error) + +### Expected Behavior: +- **HTML/PDF exports**: Should work immediately and create files +- **Other format exports**: Should show informative error about pandoc +- **Console logs**: Should show debug information about export process + +## Fix Summary + +### Code Changes Made: +1. **Added `checkPandocAvailability()` function** - Detects pandoc +2. **Added `exportToHTML()` function** - Built-in HTML export +3. **Added `exportToPDFElectron()` function** - Built-in PDF export +4. **Added `exportWithPandoc()` helper** - Generic pandoc export +5. **Added `exportWithPandocPDF()` helper** - PDF with fallbacks +6. **Improved `exportFile()` function** - Main export logic with detection +7. **Enhanced error handling** - Better user messages +8. **Added comprehensive logging** - Debug information + +### Files Modified: +- `src/main.js` - Enhanced export functionality +- `test-export.md` - Created test file +- `test-export-functionality.js` - Created test script + +## User Instructions + +### For Users Without Pandoc: +- ✅ **HTML and PDF exports work perfectly** +- ✅ **No additional software needed** +- ✅ **Professional-looking output with proper styling** + +### For Users Who Want All Formats: +1. **Install Pandoc**: Visit https://pandoc.org/installing.html +2. **For PDF with LaTeX**: Also install MiKTeX or TeX Live +3. **Restart the application** after installation +4. **All export formats will then be available** + +## Result +🎉 **Export functionality is now working reliably!** +- Built-in exports (HTML, PDF) work without any dependencies +- Clear error messages guide users for advanced formats +- Robust error handling prevents crashes - Better user experience with informative dialogs \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md index db64e51..64db89c 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,82 +1,82 @@ -# GEMINI.md - -## Project Overview - -This project is a cross-platform Markdown editor and converter named **PanConverter**. It is built using the Electron framework, allowing it to run on Windows, macOS, and Linux. The application provides a rich text editor for writing Markdown, a live preview pane, and robust export capabilities powered by Pandoc. - -The core technologies used are: -- **Electron:** For creating the desktop application. -- **JavaScript:** The primary programming language. -- **Pandoc:** For converting Markdown to various formats like PDF, DOCX, HTML, etc. -- **Marked:** For parsing and rendering the Markdown preview in real-time. -- **CodeMirror:** As the underlying text editor component. -- **highlight.js:** For syntax highlighting in the editor. -- **DOMPurify:** To sanitize the HTML output in the preview pane for security. - -The application features a tabbed interface for working with multiple files, various themes, find and replace functionality, and detailed document statistics. - -## Building and Running - -To build and run this project locally, you will need to have Node.js and npm installed. - -### Installation - -1. **Clone the repository:** - ```bash - git clone https://github.com/amitwh/pan-converter.git - cd pan-converter - ``` - -2. **Install dependencies:** - ```bash - npm install - ``` - -### Running the Application - -To run the application in development mode, use the following command: - -```bash -npm start -``` - -### Building the Application - -You can build the application for different platforms using the scripts defined in `package.json`. - -- **Build for the current platform:** - ```bash - npm run build - ``` - -- **Build for a specific platform:** - ```bash - npm run build:win # For Windows - npm run build:mac # For macOS - npm run build:linux # For Linux - ``` - -- **Build for all platforms at once:** - ```bash - npm run dist:all - ``` - -The distributable files will be located in the `dist/` directory. - -### Testing - -The project does not have a dedicated test suite configured. The `test` script in `package.json` currently returns an error. - -```bash -npm test -``` - -## Development Conventions - -- **Code Style:** The codebase is written in JavaScript (ES6+). There is no linter or formatter configured, but the code generally follows standard JavaScript conventions. -- **Main vs. Renderer Process:** The application logic is split between the Electron main process (`src/main.js`) and the renderer process (`src/renderer.js`). - - `src/main.js` handles window management, application menus, file system operations, and communication with the operating system. - - `src/renderer.js` manages the user interface, editor functionality, and the Markdown preview. -- **Dependencies:** Project dependencies are managed through `package.json`. `devDependencies` are used for the build process, while `dependencies` are required for the application to run. -- **User Data:** The application stores settings and recent files in the user's application data directory. -- **Pandoc Integration:** The application relies on a system-installed version of Pandoc for its export functionality. It does not bundle Pandoc. +# GEMINI.md + +## Project Overview + +This project is a cross-platform Markdown editor and converter named **PanConverter**. It is built using the Electron framework, allowing it to run on Windows, macOS, and Linux. The application provides a rich text editor for writing Markdown, a live preview pane, and robust export capabilities powered by Pandoc. + +The core technologies used are: +- **Electron:** For creating the desktop application. +- **JavaScript:** The primary programming language. +- **Pandoc:** For converting Markdown to various formats like PDF, DOCX, HTML, etc. +- **Marked:** For parsing and rendering the Markdown preview in real-time. +- **CodeMirror:** As the underlying text editor component. +- **highlight.js:** For syntax highlighting in the editor. +- **DOMPurify:** To sanitize the HTML output in the preview pane for security. + +The application features a tabbed interface for working with multiple files, various themes, find and replace functionality, and detailed document statistics. + +## Building and Running + +To build and run this project locally, you will need to have Node.js and npm installed. + +### Installation + +1. **Clone the repository:** + ```bash + git clone https://github.com/amitwh/pan-converter.git + cd pan-converter + ``` + +2. **Install dependencies:** + ```bash + npm install + ``` + +### Running the Application + +To run the application in development mode, use the following command: + +```bash +npm start +``` + +### Building the Application + +You can build the application for different platforms using the scripts defined in `package.json`. + +- **Build for the current platform:** + ```bash + npm run build + ``` + +- **Build for a specific platform:** + ```bash + npm run build:win # For Windows + npm run build:mac # For macOS + npm run build:linux # For Linux + ``` + +- **Build for all platforms at once:** + ```bash + npm run dist:all + ``` + +The distributable files will be located in the `dist/` directory. + +### Testing + +The project does not have a dedicated test suite configured. The `test` script in `package.json` currently returns an error. + +```bash +npm test +``` + +## Development Conventions + +- **Code Style:** The codebase is written in JavaScript (ES6+). There is no linter or formatter configured, but the code generally follows standard JavaScript conventions. +- **Main vs. Renderer Process:** The application logic is split between the Electron main process (`src/main.js`) and the renderer process (`src/renderer.js`). + - `src/main.js` handles window management, application menus, file system operations, and communication with the operating system. + - `src/renderer.js` manages the user interface, editor functionality, and the Markdown preview. +- **Dependencies:** Project dependencies are managed through `package.json`. `devDependencies` are used for the build process, while `dependencies` are required for the application to run. +- **User Data:** The application stores settings and recent files in the user's application data directory. +- **Pandoc Integration:** The application relies on a system-installed version of Pandoc for its export functionality. It does not bundle Pandoc. diff --git a/IMPROVEMENT_PLAN.md b/IMPROVEMENT_PLAN.md index 39d2ec6..29b85a6 100644 --- a/IMPROVEMENT_PLAN.md +++ b/IMPROVEMENT_PLAN.md @@ -1,1478 +1,1478 @@ -# PanConverter World-Class Improvement Plan - -**Version**: 3.0 Roadmap -**Goal**: Transform PanConverter into a world-class, feature-rich, secure open source Markdown editor -**Current State**: v2.1.0 - Feature-rich but needs security hardening and architectural improvements - ---- - -## Executive Summary - -PanConverter has an impressive feature set (50+ features, 22 themes, PDF editor, batch processing). However, to become a world-class open source application, it needs: - -1. **Critical**: Security hardening (Electron best practices) -2. **Critical**: Testing infrastructure -3. **High**: Code architecture refactoring -4. **High**: Developer experience improvements -5. **Medium**: Performance optimization -6. **Medium**: New killer features -7. **Lower**: Community & ecosystem building - ---- - -## Phase 1: Security Hardening (Critical Priority) - -### 1.1 Electron Security Configuration - -**Current Issue**: `nodeIntegration: true` and `contextIsolation: false` in `src/main.js:203-204` - -**Risk Level**: CRITICAL - Allows XSS to become full system compromise - -**Implementation**: - -```javascript -// Before (INSECURE) -webPreferences: { - nodeIntegration: true, - contextIsolation: false -} - -// After (SECURE) -webPreferences: { - nodeIntegration: false, - contextIsolation: true, - preload: path.join(__dirname, 'preload.js'), - sandbox: true -} -``` - -**Tasks**: -- [ ] Create `src/preload.js` with secure IPC bridge -- [ ] Define explicit API surface in contextBridge -- [ ] Refactor all `require()` calls in renderer.js to use preload API -- [ ] Update all IPC communication to use new bridge -- [ ] Remove direct Node.js usage from renderer process -- [ ] Enable sandbox mode - -**Files to Create**: -``` -src/preload.js - Secure IPC bridge (~200 lines) -``` - -**Estimated Effort**: 2-3 days - ---- - -### 1.2 Command Injection Prevention - -**Current Issue**: `exec()` with template literals in `src/main.js:1493, 1542` - -**Implementation**: - -```javascript -// Before (VULNERABLE) -exec(`pandoc "${inputPath}" -o "${outputPath}"`, callback); - -// After (SAFE) -const { execFile } = require('child_process'); -execFile('pandoc', [inputPath, '-o', outputPath], callback); -``` - -**Tasks**: -- [ ] Replace all `exec()` calls with `execFile()` -- [ ] Implement argument array building instead of string concatenation -- [ ] Add path validation/sanitization helper function -- [ ] Audit all file path handling for traversal attacks -- [ ] Add input validation for export metadata fields - -**Estimated Effort**: 1-2 days - ---- - -### 1.3 Content Security Policy - -**Tasks**: -- [ ] Add CSP meta tag to index.html -- [ ] Configure CSP for inline styles (required for themes) -- [ ] Whitelist required external resources (fonts, KaTeX CDN) -- [ ] Add session.setPermissionRequestHandler for additional security - -**Implementation**: -```html - -``` - -**Estimated Effort**: 0.5 days - ---- - -## Phase 2: Testing Infrastructure (Critical Priority) - -### 2.1 Testing Framework Setup - -**Current Issue**: No tests exist (`npm test` returns error) - -**Implementation**: - -```json -// package.json additions -{ - "devDependencies": { - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "@testing-library/jest-dom": "^6.4.0", - "electron-test": "^1.0.0", - "spectron": "^19.0.0" - }, - "scripts": { - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "test:e2e": "jest --config jest.e2e.config.js" - } -} -``` - -**Directory Structure**: -``` -tests/ -├── unit/ -│ ├── tabManager.test.js -│ ├── markdownParser.test.js -│ ├── exportUtils.test.js -│ ├── pdfOperations.test.js -│ └── wordExporter.test.js -├── integration/ -│ ├── fileOperations.test.js -│ ├── ipcHandlers.test.js -│ └── exportPipeline.test.js -├── e2e/ -│ ├── editorWorkflow.test.js -│ ├── exportWorkflow.test.js -│ └── batchConversion.test.js -└── fixtures/ - ├── sample.md - ├── complex-tables.md - └── ascii-art.md -``` - -**Estimated Effort**: 3-4 days - ---- - -### 2.2 Unit Test Coverage Goals - -**Target**: 80% code coverage for critical paths - -**Priority Test Cases**: - -1. **TabManager Class** (src/renderer.js) - - Tab creation/switching/closing - - State persistence - - Undo/redo functionality - - Find & replace operations - -2. **Export Functions** (src/main.js) - - Pandoc command building - - Format-specific options - - Error handling paths - - Header/footer processing - -3. **WordTemplateExporter** (src/wordTemplateExporter.js) - - Markdown to XML conversion - - Template preservation - - ASCII art detection - - Table formatting - -4. **PDF Operations** (src/main.js) - - Merge/split operations - - Encryption/decryption - - Watermark application - - Page manipulation - -**Estimated Effort**: 5-7 days - ---- - -### 2.3 Code Quality Tools - -**Tasks**: -- [ ] Add ESLint configuration -- [ ] Add Prettier for code formatting -- [ ] Add Husky for pre-commit hooks -- [ ] Add lint-staged for incremental linting - -**Files to Create**: -``` -.eslintrc.js -.prettierrc -.husky/pre-commit -lint-staged.config.js -``` - -**ESLint Configuration**: -```javascript -// .eslintrc.js -module.exports = { - env: { - browser: true, - node: true, - es2022: true - }, - extends: [ - 'eslint:recommended', - 'plugin:security/recommended' - ], - parserOptions: { - ecmaVersion: 2022 - }, - rules: { - 'no-eval': 'error', - 'no-implied-eval': 'error', - 'security/detect-child-process': 'warn', - 'security/detect-non-literal-fs-filename': 'warn' - } -}; -``` - -**Estimated Effort**: 1 day - ---- - -## Phase 3: Code Architecture Refactoring (High Priority) - -### 3.1 Modularize Renderer.js - -**Current Issue**: 4,059 lines in single file - -**Proposed Structure**: -``` -src/ -├── renderer/ -│ ├── index.js # Entry point, initializes modules -│ ├── TabManager.js # Tab management (extracted class) -│ ├── EditorController.js # Editor events, formatting -│ ├── PreviewRenderer.js # Markdown rendering, KaTeX, Mermaid -│ ├── FindReplace.js # Find & replace functionality -│ ├── ExportDialog.js # Export options UI -│ ├── PDFEditorDialog.js # PDF editor interface -│ ├── BatchConverter.js # Batch conversion UI -│ ├── ThemeManager.js # Theme switching -│ ├── StatisticsTracker.js # Word/char counting -│ ├── AutoSave.js # Auto-save functionality -│ ├── KeyboardShortcuts.js # Shortcut handling -│ └── utils/ -│ ├── ipcBridge.js # IPC communication wrapper -│ ├── domHelpers.js # DOM manipulation utilities -│ └── validators.js # Input validation -``` - -**Benefits**: -- Easier testing (each module testable independently) -- Better maintainability -- Clearer separation of concerns -- Reduced merge conflicts - -**Estimated Effort**: 4-5 days - ---- - -### 3.2 Modularize Main.js - -**Current Issue**: 3,331 lines with 54 top-level functions - -**Proposed Structure**: -``` -src/ -├── main/ -│ ├── index.js # Entry point, app lifecycle -│ ├── WindowManager.js # Window creation and management -│ ├── MenuBuilder.js # Menu system -│ ├── IPCHandlers.js # IPC event handlers -│ ├── FileOperations.js # Open, save, import -│ ├── ExportEngine.js # All export functionality -│ ├── PandocWrapper.js # Pandoc command execution -│ ├── PDFOperations.js # PDF manipulation -│ ├── BatchProcessor.js # Batch conversion -│ ├── SettingsManager.js # Persistent settings -│ ├── RecentFiles.js # Recent files management -│ └── utils/ -│ ├── pathUtils.js # Path handling -│ ├── commandBuilder.js # Safe command building -│ └── tempFiles.js # Temporary file management -``` - -**Estimated Effort**: 3-4 days - ---- - -### 3.3 Create Unified IPC Interface - -**Current Issue**: 25+ scattered IPC handlers - -**Implementation**: -```javascript -// src/shared/ipcChannels.js -export const IPC_CHANNELS = { - // File Operations - FILE_OPEN: 'file:open', - FILE_SAVE: 'file:save', - FILE_IMPORT: 'file:import', - - // Export Operations - EXPORT_START: 'export:start', - EXPORT_PROGRESS: 'export:progress', - EXPORT_COMPLETE: 'export:complete', - EXPORT_ERROR: 'export:error', - - // PDF Operations - PDF_MERGE: 'pdf:merge', - PDF_SPLIT: 'pdf:split', - PDF_COMPRESS: 'pdf:compress', - // ... etc -}; - -// Type definitions (JSDoc or TypeScript) -/** - * @typedef {Object} ExportRequest - * @property {string} format - Output format - * @property {string} content - Markdown content - * @property {ExportOptions} options - Export options - */ -``` - -**Estimated Effort**: 2 days - ---- - -### 3.4 Implement Error Boundaries - -**Tasks**: -- [ ] Create ErrorBoundary wrapper for UI sections -- [ ] Implement global error handler in main process -- [ ] Add crash recovery mechanism -- [ ] Implement document auto-recovery on restart - -**Implementation**: -```javascript -// src/renderer/ErrorBoundary.js -class ErrorBoundary { - constructor(containerEl, fallbackFn) { - this.container = containerEl; - this.fallback = fallbackFn; - } - - wrap(fn) { - return (...args) => { - try { - return fn(...args); - } catch (error) { - console.error('Error caught by boundary:', error); - this.showFallback(error); - this.reportError(error); - } - }; - } - - showFallback(error) { - this.container.innerHTML = this.fallback(error); - } - - async reportError(error) { - await ipcBridge.send('error:report', { - message: error.message, - stack: error.stack, - timestamp: Date.now() - }); - } -} -``` - -**Estimated Effort**: 1-2 days - ---- - -## Phase 4: Developer Experience (High Priority) - -### 4.1 TypeScript Migration (Optional but Recommended) - -**Benefits**: -- Catch bugs at compile time -- Better IDE support -- Self-documenting code -- Easier refactoring - -**Migration Strategy**: -1. Add TypeScript configuration -2. Rename files incrementally (.js -> .ts) -3. Add type annotations gradually -4. Use strict mode for new code - -**Configuration**: -```json -// tsconfig.json -{ - "compilerOptions": { - "target": "ES2022", - "module": "commonjs", - "lib": ["ES2022", "DOM"], - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "allowJs": true, - "checkJs": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests"] -} -``` - -**Estimated Effort**: 5-7 days (incremental) - ---- - -### 4.2 Documentation Improvements - -**Tasks**: -- [ ] Add JSDoc comments to all functions -- [ ] Generate API documentation (TypeDoc or JSDoc) -- [ ] Create architecture diagram (Mermaid) -- [ ] Document IPC interface -- [ ] Create contributor guide - -**Files to Create**: -``` -docs/ -├── ARCHITECTURE.md # System architecture overview -├── API.md # IPC and internal APIs -├── CONTRIBUTING.md # Contribution guidelines -├── SECURITY.md # Security policy -├── TESTING.md # Testing guide -└── diagrams/ - ├── architecture.mermaid # System diagram - ├── ipc-flow.mermaid # IPC communication flow - └── export-pipeline.mermaid -``` - -**Example Architecture Diagram**: -```mermaid -graph TB - subgraph "Renderer Process" - UI[UI Components] - TM[TabManager] - ED[EditorController] - PR[PreviewRenderer] - EX[ExportDialog] - end - - subgraph "Main Process" - WM[WindowManager] - IPC[IPC Handlers] - FO[FileOperations] - EE[ExportEngine] - PW[PandocWrapper] - end - - subgraph "External" - FS[File System] - PD[Pandoc] - LO[LibreOffice] - end - - UI --> TM - TM --> ED - ED --> PR - UI --> EX - - EX -- IPC --> IPC - IPC --> FO - IPC --> EE - EE --> PW - PW --> PD - FO --> FS -``` - -**Estimated Effort**: 2-3 days - ---- - -### 4.3 Development Workflow Improvements - -**Tasks**: -- [ ] Add hot reload for development -- [ ] Add debugging configuration for VS Code -- [ ] Add npm scripts for common tasks -- [ ] Add GitHub Actions CI/CD pipeline - -**Package.json Scripts**: -```json -{ - "scripts": { - "start": "electron .", - "start:dev": "cross-env NODE_ENV=development electron .", - "start:debug": "electron --inspect=9229 .", - "build": "electron-builder", - "build:win": "electron-builder --win", - "build:mac": "electron-builder --mac", - "build:linux": "electron-builder --linux", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "test:e2e": "jest --config jest.e2e.config.js", - "lint": "eslint src/", - "lint:fix": "eslint src/ --fix", - "format": "prettier --write src/", - "typecheck": "tsc --noEmit", - "docs": "jsdoc -c jsdoc.config.js", - "clean": "rimraf dist/ coverage/", - "prepare": "husky install" - } -} -``` - -**VS Code Configuration**: -```json -// .vscode/launch.json -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Debug Main Process", - "type": "node", - "request": "launch", - "cwd": "${workspaceFolder}", - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", - "windows": { - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" - }, - "args": ["."], - "outputCapture": "std" - }, - { - "name": "Debug Renderer Process", - "type": "chrome", - "request": "attach", - "port": 9222, - "webRoot": "${workspaceFolder}/src" - } - ] -} -``` - -**GitHub Actions CI**: -```yaml -# .github/workflows/ci.yml -name: CI - -on: [push, pull_request] - -jobs: - test: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - node: [18, 20] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node }} - - run: npm ci - - run: npm run lint - - run: npm test - - run: npm run build -``` - -**Estimated Effort**: 1-2 days - ---- - -## Phase 5: Performance Optimization (Medium Priority) - -### 5.1 Preview Rendering Optimization - -**Current Issue**: Preview renders on every keystroke - -**Implementation**: -```javascript -// src/renderer/PreviewRenderer.js -class PreviewRenderer { - constructor(options = {}) { - this.debounceMs = options.debounceMs || 150; - this.cache = new Map(); - this.pendingRender = null; - } - - render(markdown) { - // Debounce rapid updates - clearTimeout(this.pendingRender); - this.pendingRender = setTimeout(() => { - this._doRender(markdown); - }, this.debounceMs); - } - - _doRender(markdown) { - // Check cache first - const cacheKey = this._hash(markdown); - if (this.cache.has(cacheKey)) { - this._applyRender(this.cache.get(cacheKey)); - return; - } - - // Render and cache - const html = marked.parse(markdown); - const sanitized = DOMPurify.sanitize(html); - this.cache.set(cacheKey, sanitized); - - // Limit cache size - if (this.cache.size > 100) { - const firstKey = this.cache.keys().next().value; - this.cache.delete(firstKey); - } - - this._applyRender(sanitized); - } - - _hash(str) { - // Fast hash for cache keys - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = ((hash << 5) - hash) + str.charCodeAt(i); - hash |= 0; - } - return hash; - } -} -``` - -**Estimated Effort**: 1 day - ---- - -### 5.2 Large File Handling - -**Tasks**: -- [ ] Implement virtual scrolling for editor -- [ ] Add lazy rendering for preview -- [ ] Chunk processing for files > 1MB -- [ ] Add file size warnings - -**Implementation**: -```javascript -// src/renderer/LargeFileHandler.js -class LargeFileHandler { - static CHUNK_SIZE = 50000; // 50KB chunks - static WARNING_SIZE = 1024 * 1024; // 1MB warning - - static async loadFile(path, onProgress) { - const stats = await fs.stat(path); - - if (stats.size > this.WARNING_SIZE) { - const proceed = await this.showWarning(stats.size); - if (!proceed) return null; - } - - // Stream large files - if (stats.size > this.CHUNK_SIZE * 2) { - return this.streamLoad(path, stats.size, onProgress); - } - - return fs.readFile(path, 'utf-8'); - } - - static async streamLoad(path, totalSize, onProgress) { - const chunks = []; - const stream = fs.createReadStream(path, { - encoding: 'utf-8', - highWaterMark: this.CHUNK_SIZE - }); - - let loaded = 0; - for await (const chunk of stream) { - chunks.push(chunk); - loaded += chunk.length; - onProgress?.(loaded / totalSize); - } - - return chunks.join(''); - } -} -``` - -**Estimated Effort**: 2 days - ---- - -### 5.3 Async File Operations - -**Current Issue**: Some synchronous fs operations block UI - -**Tasks**: -- [ ] Audit all `fs.readFileSync` / `fs.writeFileSync` calls -- [ ] Replace with async versions -- [ ] Add loading indicators for file operations -- [ ] Implement operation queuing - -**Estimated Effort**: 1 day - ---- - -### 5.4 Memory Optimization - -**Tasks**: -- [ ] Implement tab unloading for inactive tabs -- [ ] Add memory usage monitoring -- [ ] Optimize undo/redo stack size -- [ ] Clean up event listeners on tab close - -**Implementation**: -```javascript -// src/renderer/TabManager.js (enhancement) -class TabManager { - static MAX_ACTIVE_TABS = 5; - static UNDO_STACK_LIMIT = 50; - - unloadInactiveTabs() { - const activeTabs = this.getRecentlyActiveTabs(this.MAX_ACTIVE_TABS); - - for (const [id, tab] of this.tabs) { - if (!activeTabs.includes(id) && !tab.modified) { - this.unloadTab(id); - } - } - } - - unloadTab(id) { - const tab = this.tabs.get(id); - if (!tab || tab.modified) return; - - // Save state to disk cache - this.saveTabCache(id, tab); - - // Clear memory - tab.content = null; - tab.undoStack = []; - tab.redoStack = []; - tab.unloaded = true; - } - - reloadTab(id) { - const tab = this.tabs.get(id); - if (!tab?.unloaded) return; - - const cached = this.loadTabCache(id); - Object.assign(tab, cached); - tab.unloaded = false; - } -} -``` - -**Estimated Effort**: 1-2 days - ---- - -## Phase 6: New Killer Features (Medium Priority) - -### 6.1 Plugin/Extension System - -**Description**: Allow community to extend functionality - -**Architecture**: -``` -plugins/ -├── plugin-api.js # Plugin API definition -├── plugin-loader.js # Dynamic plugin loading -├── plugin-sandbox.js # Secure plugin execution -└── built-in/ - ├── spell-check/ - ├── grammar-check/ - └── git-integration/ -``` - -**Plugin API**: -```javascript -// Plugin manifest (plugin.json) -{ - "name": "spell-check", - "version": "1.0.0", - "description": "Spell checking for PanConverter", - "main": "index.js", - "permissions": ["editor:read", "editor:highlight"], - "activationEvents": ["onEditorChange"] -} - -// Plugin implementation -class SpellCheckPlugin { - constructor(api) { - this.api = api; - this.dictionary = null; - } - - async activate() { - this.dictionary = await this.loadDictionary(); - this.api.on('editor:change', this.checkSpelling.bind(this)); - } - - checkSpelling(content) { - const words = content.split(/\s+/); - const misspelled = words.filter(w => !this.dictionary.has(w.toLowerCase())); - this.api.highlightWords(misspelled, 'spelling-error'); - } - - deactivate() { - this.api.off('editor:change', this.checkSpelling); - } -} -``` - -**Estimated Effort**: 7-10 days - ---- - -### 6.2 Spell Check & Grammar - -**Implementation Options**: -1. **Built-in**: Use `nodehun` or `nspell` for spell checking -2. **LanguageTool**: Integrate with LanguageTool API for grammar -3. **Plugin**: Implement as first built-in plugin - -**Features**: -- Real-time spell checking with squiggly underlines -- Right-click suggestions -- Custom dictionary support -- Multiple language support -- Grammar checking (LanguageTool integration) - -**Dependencies**: -```json -{ - "nodehun": "^3.0.0", - "languagetool-api": "^1.0.0" -} -``` - -**Estimated Effort**: 3-4 days - ---- - -### 6.3 Version Control Integration - -**Features**: -- Git status in status bar -- Diff view for modified files -- Commit/push from within app -- Branch switching -- Conflict resolution UI - -**Implementation**: -```javascript -// src/main/GitIntegration.js -const simpleGit = require('simple-git'); - -class GitIntegration { - constructor(repoPath) { - this.git = simpleGit(repoPath); - } - - async getStatus() { - const status = await this.git.status(); - return { - branch: status.current, - modified: status.modified, - staged: status.staged, - ahead: status.ahead, - behind: status.behind - }; - } - - async showDiff(filePath) { - return await this.git.diff(['--', filePath]); - } - - async commit(message, files) { - await this.git.add(files); - return await this.git.commit(message); - } -} -``` - -**Dependencies**: -```json -{ - "simple-git": "^3.22.0" -} -``` - -**Estimated Effort**: 3-4 days - ---- - -### 6.4 Real-time Collaboration - -**Description**: Google Docs-like real-time editing - -**Architecture**: -- WebSocket server for real-time sync -- Operational Transformation (OT) or CRDT for conflict resolution -- Cursor presence indicators -- Chat/comments sidebar - -**Implementation Options**: -1. **Yjs**: CRDT-based collaboration library -2. **ShareDB**: OT-based real-time database -3. **Self-hosted**: Custom WebSocket + CRDT - -**Dependencies**: -```json -{ - "yjs": "^13.6.0", - "y-websocket": "^1.5.0", - "y-codemirror.next": "^0.3.0" -} -``` - -**Estimated Effort**: 10-15 days - ---- - -### 6.5 Cloud Sync & Backup - -**Features**: -- Sync documents across devices -- Automatic backup to cloud -- Support for multiple providers (Google Drive, Dropbox, OneDrive) -- Offline-first with sync when connected - -**Implementation**: -```javascript -// src/main/CloudSync.js -class CloudSync { - constructor(provider) { - this.provider = provider; // 'google', 'dropbox', 'onedrive' - this.syncQueue = []; - this.online = navigator.onLine; - } - - async sync(document) { - if (!this.online) { - this.queueForSync(document); - return; - } - - const remoteVersion = await this.provider.getVersion(document.id); - - if (remoteVersion > document.version) { - // Pull remote changes - return await this.pullChanges(document); - } else if (document.modified) { - // Push local changes - return await this.pushChanges(document); - } - } - - queueForSync(document) { - this.syncQueue.push({ - document, - timestamp: Date.now() - }); - this.persistQueue(); - } - - async processSyncQueue() { - while (this.syncQueue.length > 0) { - const item = this.syncQueue.shift(); - await this.sync(item.document); - } - } -} -``` - -**Estimated Effort**: 5-7 days - ---- - -### 6.6 AI-Powered Features - -**Features**: -- AI writing assistant (grammar, style suggestions) -- Auto-complete suggestions -- Document summarization -- Translation assistance -- Content generation from prompts - -**Implementation Options**: -1. **OpenAI API**: GPT-4 integration -2. **Anthropic API**: Claude integration -3. **Local LLM**: Ollama/llama.cpp for offline - -**Privacy-First Approach**: -```javascript -// src/main/AIAssistant.js -class AIAssistant { - constructor(config) { - this.mode = config.mode; // 'cloud' | 'local' | 'disabled' - this.localModel = null; - this.cloudClient = null; - } - - async initialize() { - if (this.mode === 'local') { - // Use Ollama or similar for local inference - this.localModel = await this.loadLocalModel(); - } else if (this.mode === 'cloud') { - // User provides their own API key - this.cloudClient = new OpenAI({ apiKey: config.apiKey }); - } - } - - async suggest(context, type) { - const prompt = this.buildPrompt(context, type); - - if (this.mode === 'local') { - return await this.localModel.generate(prompt); - } else { - return await this.cloudClient.chat.completions.create({ - model: 'gpt-4', - messages: [{ role: 'user', content: prompt }] - }); - } - } -} -``` - -**Estimated Effort**: 5-7 days - ---- - -### 6.7 Advanced Diagram Support - -**Current**: Basic Mermaid.js support - -**Enhancements**: -- PlantUML integration -- Draw.io/Excalidraw embedding -- Live diagram editing with visual editor -- Export diagrams as images - -**Implementation**: -```javascript -// src/renderer/DiagramRenderer.js -class DiagramRenderer { - static SUPPORTED = ['mermaid', 'plantuml', 'graphviz', 'excalidraw']; - - async render(type, code) { - switch (type) { - case 'mermaid': - return await mermaid.render('diagram', code); - case 'plantuml': - return await this.renderPlantUML(code); - case 'graphviz': - return await this.renderGraphviz(code); - case 'excalidraw': - return await this.renderExcalidraw(code); - } - } - - async renderPlantUML(code) { - // Use PlantUML server or local jar - const encoded = this.encodePlantUML(code); - const response = await fetch(`http://www.plantuml.com/plantuml/svg/${encoded}`); - return await response.text(); - } -} -``` - -**Dependencies**: -```json -{ - "@mermaid-js/mermaid": "^10.6.0", - "plantuml-encoder": "^1.4.0", - "@excalidraw/excalidraw": "^0.17.0" -} -``` - -**Estimated Effort**: 3-4 days - ---- - -### 6.8 Focus Mode & Zen Writing - -**Features**: -- Distraction-free writing mode -- Typewriter scrolling -- Ambient sounds/music -- Pomodoro timer integration -- Writing goals and statistics - -**Implementation**: -```javascript -// src/renderer/FocusMode.js -class FocusMode { - constructor(editor) { - this.editor = editor; - this.enabled = false; - this.typewriterMode = false; - this.ambientPlayer = null; - } - - enable(options = {}) { - this.enabled = true; - - // Hide UI elements - document.body.classList.add('focus-mode'); - - // Enable typewriter scrolling - if (options.typewriter) { - this.enableTypewriter(); - } - - // Start ambient sounds - if (options.ambient) { - this.startAmbient(options.ambientSound); - } - - // Start pomodoro - if (options.pomodoro) { - this.startPomodoro(options.pomodoroMinutes || 25); - } - } - - enableTypewriter() { - this.typewriterMode = true; - this.editor.on('change', () => { - const cursor = this.editor.getCursor(); - this.scrollToCenter(cursor.line); - }); - } - - scrollToCenter(line) { - const editorHeight = this.editor.element.clientHeight; - const lineHeight = this.editor.lineHeight; - const targetScroll = (line * lineHeight) - (editorHeight / 2); - this.editor.scrollTo(0, targetScroll); - } -} -``` - -**CSS**: -```css -.focus-mode { - --focus-bg: #1a1a2e; - --focus-text: #eee; -} - -.focus-mode #toolbar, -.focus-mode #tab-bar, -.focus-mode #status-bar, -.focus-mode .preview-pane { - display: none !important; -} - -.focus-mode .editor-pane { - width: 100% !important; - max-width: 800px; - margin: 0 auto; - padding: 100px 40px; -} - -.focus-mode .editor-content { - font-size: 18px; - line-height: 1.8; -} -``` - -**Estimated Effort**: 2-3 days - ---- - -### 6.9 Document Templates - -**Features**: -- Pre-built document templates -- Custom template creation -- Template marketplace/sharing -- Category-based organization - -**Template Categories**: -- **Academic**: Essay, Research Paper, Thesis, Lab Report -- **Business**: Report, Proposal, Meeting Notes, Invoice -- **Technical**: README, API Documentation, Tutorial, Changelog -- **Personal**: Journal, Blog Post, Recipe, Travel Log -- **Creative**: Story, Screenplay, Poetry, Song Lyrics - -**Implementation**: -```javascript -// src/renderer/TemplateManager.js -class TemplateManager { - static TEMPLATES_DIR = path.join(app.getPath('userData'), 'templates'); - - async getTemplates() { - const builtIn = await this.loadBuiltInTemplates(); - const custom = await this.loadCustomTemplates(); - return [...builtIn, ...custom]; - } - - async createFromTemplate(templateId) { - const template = await this.getTemplate(templateId); - const content = this.processVariables(template.content, { - date: new Date().toLocaleDateString(), - author: this.settings.get('author'), - title: 'Untitled' - }); - return content; - } - - processVariables(content, variables) { - return content.replace(/\{\{(\w+)\}\}/g, (match, key) => { - return variables[key] || match; - }); - } -} -``` - -**Estimated Effort**: 2-3 days - ---- - -### 6.10 Mobile Companion App - -**Platform**: React Native or Flutter - -**Features**: -- Sync with desktop app -- Basic editing capabilities -- Preview and share -- Offline support - -**Estimated Effort**: 15-20 days (separate project) - ---- - -## Phase 7: Community & Ecosystem (Lower Priority) - -### 7.1 Community Building - -**Tasks**: -- [ ] Create Discord/Slack community -- [ ] Set up GitHub Discussions -- [ ] Create Twitter/X account for updates -- [ ] Write blog posts about development -- [ ] Create video tutorials - -**Estimated Effort**: Ongoing - ---- - -### 7.2 Plugin Marketplace - -**Features**: -- Browse and install plugins -- Rating and reviews -- Automatic updates -- Revenue sharing for premium plugins - -**Estimated Effort**: 10-15 days - ---- - -### 7.3 Theme Marketplace - -**Features**: -- Community-created themes -- Theme preview -- Easy installation -- Theme editor tool - -**Estimated Effort**: 5-7 days - ---- - -### 7.4 Internationalization (i18n) - -**Supported Languages** (Priority): -1. English (default) -2. Spanish -3. French -4. German -5. Chinese (Simplified) -6. Japanese -7. Korean -8. Portuguese -9. Russian -10. Arabic - -**Implementation**: -```javascript -// src/i18n/index.js -const i18next = require('i18next'); - -i18next.init({ - lng: 'en', - fallbackLng: 'en', - resources: { - en: require('./locales/en.json'), - es: require('./locales/es.json'), - // ... more languages - } -}); - -// Usage -t('menu.file.new') // "New File" or "Nuevo Archivo" -``` - -**Estimated Effort**: 3-5 days (infrastructure) + ongoing translation - ---- - -### 7.5 Accessibility (a11y) - -**Tasks**: -- [ ] Add ARIA labels to all interactive elements -- [ ] Ensure keyboard navigation for all features -- [ ] Add screen reader support -- [ ] Ensure color contrast compliance (WCAG 2.1) -- [ ] Add focus indicators -- [ ] Support reduced motion preferences - -**Implementation**: -```html - - -``` - -**Estimated Effort**: 2-3 days - ---- - -## Implementation Timeline - -### Quarter 1: Foundation (Weeks 1-4) -| Week | Focus | Tasks | -|------|-------|-------| -| 1 | Security | Phase 1.1-1.3 (Security hardening) | -| 2 | Testing | Phase 2.1-2.2 (Test infrastructure) | -| 3 | Quality | Phase 2.3, 4.3 (Linting, CI/CD) | -| 4 | Architecture | Phase 3.1 (Modularize renderer.js) | - -### Quarter 2: Refinement (Weeks 5-8) -| Week | Focus | Tasks | -|------|-------|-------| -| 5 | Architecture | Phase 3.2-3.3 (Modularize main.js, IPC) | -| 6 | Performance | Phase 5.1-5.2 (Optimization) | -| 7 | Documentation | Phase 4.2 (Docs, architecture) | -| 8 | Polish | Phase 3.4, 5.3-5.4 (Error handling, memory) | - -### Quarter 3: Features (Weeks 9-16) -| Week | Focus | Tasks | -|------|-------|-------| -| 9-10 | Plugins | Phase 6.1 (Plugin system) | -| 11 | Spell Check | Phase 6.2 (Spell check & grammar) | -| 12 | Git | Phase 6.3 (Version control) | -| 13-14 | Focus Mode | Phase 6.8, 6.9 (Focus mode, templates) | -| 15-16 | Diagrams | Phase 6.7 (Advanced diagrams) | - -### Quarter 4: Ecosystem (Weeks 17-20) -| Week | Focus | Tasks | -|------|-------|-------| -| 17 | i18n | Phase 7.4 (Internationalization) | -| 18 | a11y | Phase 7.5 (Accessibility) | -| 19 | Community | Phase 7.1-7.2 (Community, marketplace) | -| 20 | Polish | Final testing, documentation, release | - ---- - -## Success Metrics - -### Quality Metrics -- [ ] 80%+ test coverage -- [ ] Zero critical security vulnerabilities -- [ ] < 100ms preview render time -- [ ] < 3s cold start time -- [ ] < 200MB memory usage (typical) - -### Community Metrics -- [ ] 1000+ GitHub stars -- [ ] 50+ contributors -- [ ] 20+ community plugins -- [ ] 50+ community themes -- [ ] Active Discord community - -### Feature Metrics -- [ ] 100+ export format combinations -- [ ] 10+ supported languages -- [ ] WCAG 2.1 AA compliance -- [ ] Plugin API stability (v1.0) - ---- - -## Risk Assessment - -| Risk | Probability | Impact | Mitigation | -|------|-------------|--------|------------| -| Breaking changes in refactor | High | Medium | Comprehensive testing, incremental changes | -| Security vulnerability discovered | Medium | High | Security audit, responsible disclosure policy | -| Community fragmentation | Low | Medium | Clear governance, contributor guidelines | -| Dependency deprecation | Medium | Medium | Regular dependency updates, abstraction layers | -| Scope creep | High | Medium | Strict prioritization, feature freeze periods | - ---- - -## Resource Requirements - -### Development -- 1-2 full-time developers (or equivalent open source contributions) -- Code review process for security-sensitive changes -- Automated CI/CD pipeline - -### Infrastructure -- GitHub repository (existing) -- CI/CD (GitHub Actions) -- Documentation hosting (GitHub Pages) -- Community platform (Discord/Discourse) - -### External Services (Optional) -- Code signing certificates (Windows/macOS) -- Translation services -- Security audit services - ---- - -## Conclusion - -PanConverter has a solid foundation with impressive features. By following this improvement plan, it can become a world-class open source Markdown editor that rivals commercial alternatives like Typora, Obsidian, and Bear. - -**Key Differentiators After Implementation**: -1. **Security-First**: Properly secured Electron app -2. **Extensible**: Plugin system for community extensions -3. **Feature-Rich**: PDF editor, batch processing, templates -4. **Cross-Platform**: Windows, macOS, Linux with consistent experience -5. **Open Source**: MIT licensed, community-driven -6. **Privacy-Focused**: Local-first with optional cloud sync - -**Next Steps**: -1. Review and prioritize this plan -2. Create GitHub issues/milestones -3. Begin Phase 1 (Security) immediately -4. Recruit contributors for parallel work - ---- - -*Plan Version: 1.0* -*Created: January 2026* -*Author: Claude Code Assistant* +# PanConverter World-Class Improvement Plan + +**Version**: 3.0 Roadmap +**Goal**: Transform PanConverter into a world-class, feature-rich, secure open source Markdown editor +**Current State**: v2.1.0 - Feature-rich but needs security hardening and architectural improvements + +--- + +## Executive Summary + +PanConverter has an impressive feature set (50+ features, 22 themes, PDF editor, batch processing). However, to become a world-class open source application, it needs: + +1. **Critical**: Security hardening (Electron best practices) +2. **Critical**: Testing infrastructure +3. **High**: Code architecture refactoring +4. **High**: Developer experience improvements +5. **Medium**: Performance optimization +6. **Medium**: New killer features +7. **Lower**: Community & ecosystem building + +--- + +## Phase 1: Security Hardening (Critical Priority) + +### 1.1 Electron Security Configuration + +**Current Issue**: `nodeIntegration: true` and `contextIsolation: false` in `src/main.js:203-204` + +**Risk Level**: CRITICAL - Allows XSS to become full system compromise + +**Implementation**: + +```javascript +// Before (INSECURE) +webPreferences: { + nodeIntegration: true, + contextIsolation: false +} + +// After (SECURE) +webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, 'preload.js'), + sandbox: true +} +``` + +**Tasks**: +- [ ] Create `src/preload.js` with secure IPC bridge +- [ ] Define explicit API surface in contextBridge +- [ ] Refactor all `require()` calls in renderer.js to use preload API +- [ ] Update all IPC communication to use new bridge +- [ ] Remove direct Node.js usage from renderer process +- [ ] Enable sandbox mode + +**Files to Create**: +``` +src/preload.js - Secure IPC bridge (~200 lines) +``` + +**Estimated Effort**: 2-3 days + +--- + +### 1.2 Command Injection Prevention + +**Current Issue**: `exec()` with template literals in `src/main.js:1493, 1542` + +**Implementation**: + +```javascript +// Before (VULNERABLE) +exec(`pandoc "${inputPath}" -o "${outputPath}"`, callback); + +// After (SAFE) +const { execFile } = require('child_process'); +execFile('pandoc', [inputPath, '-o', outputPath], callback); +``` + +**Tasks**: +- [ ] Replace all `exec()` calls with `execFile()` +- [ ] Implement argument array building instead of string concatenation +- [ ] Add path validation/sanitization helper function +- [ ] Audit all file path handling for traversal attacks +- [ ] Add input validation for export metadata fields + +**Estimated Effort**: 1-2 days + +--- + +### 1.3 Content Security Policy + +**Tasks**: +- [ ] Add CSP meta tag to index.html +- [ ] Configure CSP for inline styles (required for themes) +- [ ] Whitelist required external resources (fonts, KaTeX CDN) +- [ ] Add session.setPermissionRequestHandler for additional security + +**Implementation**: +```html + +``` + +**Estimated Effort**: 0.5 days + +--- + +## Phase 2: Testing Infrastructure (Critical Priority) + +### 2.1 Testing Framework Setup + +**Current Issue**: No tests exist (`npm test` returns error) + +**Implementation**: + +```json +// package.json additions +{ + "devDependencies": { + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "@testing-library/jest-dom": "^6.4.0", + "electron-test": "^1.0.0", + "spectron": "^19.0.0" + }, + "scripts": { + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "test:e2e": "jest --config jest.e2e.config.js" + } +} +``` + +**Directory Structure**: +``` +tests/ +├── unit/ +│ ├── tabManager.test.js +│ ├── markdownParser.test.js +│ ├── exportUtils.test.js +│ ├── pdfOperations.test.js +│ └── wordExporter.test.js +├── integration/ +│ ├── fileOperations.test.js +│ ├── ipcHandlers.test.js +│ └── exportPipeline.test.js +├── e2e/ +│ ├── editorWorkflow.test.js +│ ├── exportWorkflow.test.js +│ └── batchConversion.test.js +└── fixtures/ + ├── sample.md + ├── complex-tables.md + └── ascii-art.md +``` + +**Estimated Effort**: 3-4 days + +--- + +### 2.2 Unit Test Coverage Goals + +**Target**: 80% code coverage for critical paths + +**Priority Test Cases**: + +1. **TabManager Class** (src/renderer.js) + - Tab creation/switching/closing + - State persistence + - Undo/redo functionality + - Find & replace operations + +2. **Export Functions** (src/main.js) + - Pandoc command building + - Format-specific options + - Error handling paths + - Header/footer processing + +3. **WordTemplateExporter** (src/wordTemplateExporter.js) + - Markdown to XML conversion + - Template preservation + - ASCII art detection + - Table formatting + +4. **PDF Operations** (src/main.js) + - Merge/split operations + - Encryption/decryption + - Watermark application + - Page manipulation + +**Estimated Effort**: 5-7 days + +--- + +### 2.3 Code Quality Tools + +**Tasks**: +- [ ] Add ESLint configuration +- [ ] Add Prettier for code formatting +- [ ] Add Husky for pre-commit hooks +- [ ] Add lint-staged for incremental linting + +**Files to Create**: +``` +.eslintrc.js +.prettierrc +.husky/pre-commit +lint-staged.config.js +``` + +**ESLint Configuration**: +```javascript +// .eslintrc.js +module.exports = { + env: { + browser: true, + node: true, + es2022: true + }, + extends: [ + 'eslint:recommended', + 'plugin:security/recommended' + ], + parserOptions: { + ecmaVersion: 2022 + }, + rules: { + 'no-eval': 'error', + 'no-implied-eval': 'error', + 'security/detect-child-process': 'warn', + 'security/detect-non-literal-fs-filename': 'warn' + } +}; +``` + +**Estimated Effort**: 1 day + +--- + +## Phase 3: Code Architecture Refactoring (High Priority) + +### 3.1 Modularize Renderer.js + +**Current Issue**: 4,059 lines in single file + +**Proposed Structure**: +``` +src/ +├── renderer/ +│ ├── index.js # Entry point, initializes modules +│ ├── TabManager.js # Tab management (extracted class) +│ ├── EditorController.js # Editor events, formatting +│ ├── PreviewRenderer.js # Markdown rendering, KaTeX, Mermaid +│ ├── FindReplace.js # Find & replace functionality +│ ├── ExportDialog.js # Export options UI +│ ├── PDFEditorDialog.js # PDF editor interface +│ ├── BatchConverter.js # Batch conversion UI +│ ├── ThemeManager.js # Theme switching +│ ├── StatisticsTracker.js # Word/char counting +│ ├── AutoSave.js # Auto-save functionality +│ ├── KeyboardShortcuts.js # Shortcut handling +│ └── utils/ +│ ├── ipcBridge.js # IPC communication wrapper +│ ├── domHelpers.js # DOM manipulation utilities +│ └── validators.js # Input validation +``` + +**Benefits**: +- Easier testing (each module testable independently) +- Better maintainability +- Clearer separation of concerns +- Reduced merge conflicts + +**Estimated Effort**: 4-5 days + +--- + +### 3.2 Modularize Main.js + +**Current Issue**: 3,331 lines with 54 top-level functions + +**Proposed Structure**: +``` +src/ +├── main/ +│ ├── index.js # Entry point, app lifecycle +│ ├── WindowManager.js # Window creation and management +│ ├── MenuBuilder.js # Menu system +│ ├── IPCHandlers.js # IPC event handlers +│ ├── FileOperations.js # Open, save, import +│ ├── ExportEngine.js # All export functionality +│ ├── PandocWrapper.js # Pandoc command execution +│ ├── PDFOperations.js # PDF manipulation +│ ├── BatchProcessor.js # Batch conversion +│ ├── SettingsManager.js # Persistent settings +│ ├── RecentFiles.js # Recent files management +│ └── utils/ +│ ├── pathUtils.js # Path handling +│ ├── commandBuilder.js # Safe command building +│ └── tempFiles.js # Temporary file management +``` + +**Estimated Effort**: 3-4 days + +--- + +### 3.3 Create Unified IPC Interface + +**Current Issue**: 25+ scattered IPC handlers + +**Implementation**: +```javascript +// src/shared/ipcChannels.js +export const IPC_CHANNELS = { + // File Operations + FILE_OPEN: 'file:open', + FILE_SAVE: 'file:save', + FILE_IMPORT: 'file:import', + + // Export Operations + EXPORT_START: 'export:start', + EXPORT_PROGRESS: 'export:progress', + EXPORT_COMPLETE: 'export:complete', + EXPORT_ERROR: 'export:error', + + // PDF Operations + PDF_MERGE: 'pdf:merge', + PDF_SPLIT: 'pdf:split', + PDF_COMPRESS: 'pdf:compress', + // ... etc +}; + +// Type definitions (JSDoc or TypeScript) +/** + * @typedef {Object} ExportRequest + * @property {string} format - Output format + * @property {string} content - Markdown content + * @property {ExportOptions} options - Export options + */ +``` + +**Estimated Effort**: 2 days + +--- + +### 3.4 Implement Error Boundaries + +**Tasks**: +- [ ] Create ErrorBoundary wrapper for UI sections +- [ ] Implement global error handler in main process +- [ ] Add crash recovery mechanism +- [ ] Implement document auto-recovery on restart + +**Implementation**: +```javascript +// src/renderer/ErrorBoundary.js +class ErrorBoundary { + constructor(containerEl, fallbackFn) { + this.container = containerEl; + this.fallback = fallbackFn; + } + + wrap(fn) { + return (...args) => { + try { + return fn(...args); + } catch (error) { + console.error('Error caught by boundary:', error); + this.showFallback(error); + this.reportError(error); + } + }; + } + + showFallback(error) { + this.container.innerHTML = this.fallback(error); + } + + async reportError(error) { + await ipcBridge.send('error:report', { + message: error.message, + stack: error.stack, + timestamp: Date.now() + }); + } +} +``` + +**Estimated Effort**: 1-2 days + +--- + +## Phase 4: Developer Experience (High Priority) + +### 4.1 TypeScript Migration (Optional but Recommended) + +**Benefits**: +- Catch bugs at compile time +- Better IDE support +- Self-documenting code +- Easier refactoring + +**Migration Strategy**: +1. Add TypeScript configuration +2. Rename files incrementally (.js -> .ts) +3. Add type annotations gradually +4. Use strict mode for new code + +**Configuration**: +```json +// tsconfig.json +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "allowJs": true, + "checkJs": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} +``` + +**Estimated Effort**: 5-7 days (incremental) + +--- + +### 4.2 Documentation Improvements + +**Tasks**: +- [ ] Add JSDoc comments to all functions +- [ ] Generate API documentation (TypeDoc or JSDoc) +- [ ] Create architecture diagram (Mermaid) +- [ ] Document IPC interface +- [ ] Create contributor guide + +**Files to Create**: +``` +docs/ +├── ARCHITECTURE.md # System architecture overview +├── API.md # IPC and internal APIs +├── CONTRIBUTING.md # Contribution guidelines +├── SECURITY.md # Security policy +├── TESTING.md # Testing guide +└── diagrams/ + ├── architecture.mermaid # System diagram + ├── ipc-flow.mermaid # IPC communication flow + └── export-pipeline.mermaid +``` + +**Example Architecture Diagram**: +```mermaid +graph TB + subgraph "Renderer Process" + UI[UI Components] + TM[TabManager] + ED[EditorController] + PR[PreviewRenderer] + EX[ExportDialog] + end + + subgraph "Main Process" + WM[WindowManager] + IPC[IPC Handlers] + FO[FileOperations] + EE[ExportEngine] + PW[PandocWrapper] + end + + subgraph "External" + FS[File System] + PD[Pandoc] + LO[LibreOffice] + end + + UI --> TM + TM --> ED + ED --> PR + UI --> EX + + EX -- IPC --> IPC + IPC --> FO + IPC --> EE + EE --> PW + PW --> PD + FO --> FS +``` + +**Estimated Effort**: 2-3 days + +--- + +### 4.3 Development Workflow Improvements + +**Tasks**: +- [ ] Add hot reload for development +- [ ] Add debugging configuration for VS Code +- [ ] Add npm scripts for common tasks +- [ ] Add GitHub Actions CI/CD pipeline + +**Package.json Scripts**: +```json +{ + "scripts": { + "start": "electron .", + "start:dev": "cross-env NODE_ENV=development electron .", + "start:debug": "electron --inspect=9229 .", + "build": "electron-builder", + "build:win": "electron-builder --win", + "build:mac": "electron-builder --mac", + "build:linux": "electron-builder --linux", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "test:e2e": "jest --config jest.e2e.config.js", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "format": "prettier --write src/", + "typecheck": "tsc --noEmit", + "docs": "jsdoc -c jsdoc.config.js", + "clean": "rimraf dist/ coverage/", + "prepare": "husky install" + } +} +``` + +**VS Code Configuration**: +```json +// .vscode/launch.json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Main Process", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", + "windows": { + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" + }, + "args": ["."], + "outputCapture": "std" + }, + { + "name": "Debug Renderer Process", + "type": "chrome", + "request": "attach", + "port": 9222, + "webRoot": "${workspaceFolder}/src" + } + ] +} +``` + +**GitHub Actions CI**: +```yaml +# .github/workflows/ci.yml +name: CI + +on: [push, pull_request] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node: [18, 20] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - run: npm ci + - run: npm run lint + - run: npm test + - run: npm run build +``` + +**Estimated Effort**: 1-2 days + +--- + +## Phase 5: Performance Optimization (Medium Priority) + +### 5.1 Preview Rendering Optimization + +**Current Issue**: Preview renders on every keystroke + +**Implementation**: +```javascript +// src/renderer/PreviewRenderer.js +class PreviewRenderer { + constructor(options = {}) { + this.debounceMs = options.debounceMs || 150; + this.cache = new Map(); + this.pendingRender = null; + } + + render(markdown) { + // Debounce rapid updates + clearTimeout(this.pendingRender); + this.pendingRender = setTimeout(() => { + this._doRender(markdown); + }, this.debounceMs); + } + + _doRender(markdown) { + // Check cache first + const cacheKey = this._hash(markdown); + if (this.cache.has(cacheKey)) { + this._applyRender(this.cache.get(cacheKey)); + return; + } + + // Render and cache + const html = marked.parse(markdown); + const sanitized = DOMPurify.sanitize(html); + this.cache.set(cacheKey, sanitized); + + // Limit cache size + if (this.cache.size > 100) { + const firstKey = this.cache.keys().next().value; + this.cache.delete(firstKey); + } + + this._applyRender(sanitized); + } + + _hash(str) { + // Fast hash for cache keys + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = ((hash << 5) - hash) + str.charCodeAt(i); + hash |= 0; + } + return hash; + } +} +``` + +**Estimated Effort**: 1 day + +--- + +### 5.2 Large File Handling + +**Tasks**: +- [ ] Implement virtual scrolling for editor +- [ ] Add lazy rendering for preview +- [ ] Chunk processing for files > 1MB +- [ ] Add file size warnings + +**Implementation**: +```javascript +// src/renderer/LargeFileHandler.js +class LargeFileHandler { + static CHUNK_SIZE = 50000; // 50KB chunks + static WARNING_SIZE = 1024 * 1024; // 1MB warning + + static async loadFile(path, onProgress) { + const stats = await fs.stat(path); + + if (stats.size > this.WARNING_SIZE) { + const proceed = await this.showWarning(stats.size); + if (!proceed) return null; + } + + // Stream large files + if (stats.size > this.CHUNK_SIZE * 2) { + return this.streamLoad(path, stats.size, onProgress); + } + + return fs.readFile(path, 'utf-8'); + } + + static async streamLoad(path, totalSize, onProgress) { + const chunks = []; + const stream = fs.createReadStream(path, { + encoding: 'utf-8', + highWaterMark: this.CHUNK_SIZE + }); + + let loaded = 0; + for await (const chunk of stream) { + chunks.push(chunk); + loaded += chunk.length; + onProgress?.(loaded / totalSize); + } + + return chunks.join(''); + } +} +``` + +**Estimated Effort**: 2 days + +--- + +### 5.3 Async File Operations + +**Current Issue**: Some synchronous fs operations block UI + +**Tasks**: +- [ ] Audit all `fs.readFileSync` / `fs.writeFileSync` calls +- [ ] Replace with async versions +- [ ] Add loading indicators for file operations +- [ ] Implement operation queuing + +**Estimated Effort**: 1 day + +--- + +### 5.4 Memory Optimization + +**Tasks**: +- [ ] Implement tab unloading for inactive tabs +- [ ] Add memory usage monitoring +- [ ] Optimize undo/redo stack size +- [ ] Clean up event listeners on tab close + +**Implementation**: +```javascript +// src/renderer/TabManager.js (enhancement) +class TabManager { + static MAX_ACTIVE_TABS = 5; + static UNDO_STACK_LIMIT = 50; + + unloadInactiveTabs() { + const activeTabs = this.getRecentlyActiveTabs(this.MAX_ACTIVE_TABS); + + for (const [id, tab] of this.tabs) { + if (!activeTabs.includes(id) && !tab.modified) { + this.unloadTab(id); + } + } + } + + unloadTab(id) { + const tab = this.tabs.get(id); + if (!tab || tab.modified) return; + + // Save state to disk cache + this.saveTabCache(id, tab); + + // Clear memory + tab.content = null; + tab.undoStack = []; + tab.redoStack = []; + tab.unloaded = true; + } + + reloadTab(id) { + const tab = this.tabs.get(id); + if (!tab?.unloaded) return; + + const cached = this.loadTabCache(id); + Object.assign(tab, cached); + tab.unloaded = false; + } +} +``` + +**Estimated Effort**: 1-2 days + +--- + +## Phase 6: New Killer Features (Medium Priority) + +### 6.1 Plugin/Extension System + +**Description**: Allow community to extend functionality + +**Architecture**: +``` +plugins/ +├── plugin-api.js # Plugin API definition +├── plugin-loader.js # Dynamic plugin loading +├── plugin-sandbox.js # Secure plugin execution +└── built-in/ + ├── spell-check/ + ├── grammar-check/ + └── git-integration/ +``` + +**Plugin API**: +```javascript +// Plugin manifest (plugin.json) +{ + "name": "spell-check", + "version": "1.0.0", + "description": "Spell checking for PanConverter", + "main": "index.js", + "permissions": ["editor:read", "editor:highlight"], + "activationEvents": ["onEditorChange"] +} + +// Plugin implementation +class SpellCheckPlugin { + constructor(api) { + this.api = api; + this.dictionary = null; + } + + async activate() { + this.dictionary = await this.loadDictionary(); + this.api.on('editor:change', this.checkSpelling.bind(this)); + } + + checkSpelling(content) { + const words = content.split(/\s+/); + const misspelled = words.filter(w => !this.dictionary.has(w.toLowerCase())); + this.api.highlightWords(misspelled, 'spelling-error'); + } + + deactivate() { + this.api.off('editor:change', this.checkSpelling); + } +} +``` + +**Estimated Effort**: 7-10 days + +--- + +### 6.2 Spell Check & Grammar + +**Implementation Options**: +1. **Built-in**: Use `nodehun` or `nspell` for spell checking +2. **LanguageTool**: Integrate with LanguageTool API for grammar +3. **Plugin**: Implement as first built-in plugin + +**Features**: +- Real-time spell checking with squiggly underlines +- Right-click suggestions +- Custom dictionary support +- Multiple language support +- Grammar checking (LanguageTool integration) + +**Dependencies**: +```json +{ + "nodehun": "^3.0.0", + "languagetool-api": "^1.0.0" +} +``` + +**Estimated Effort**: 3-4 days + +--- + +### 6.3 Version Control Integration + +**Features**: +- Git status in status bar +- Diff view for modified files +- Commit/push from within app +- Branch switching +- Conflict resolution UI + +**Implementation**: +```javascript +// src/main/GitIntegration.js +const simpleGit = require('simple-git'); + +class GitIntegration { + constructor(repoPath) { + this.git = simpleGit(repoPath); + } + + async getStatus() { + const status = await this.git.status(); + return { + branch: status.current, + modified: status.modified, + staged: status.staged, + ahead: status.ahead, + behind: status.behind + }; + } + + async showDiff(filePath) { + return await this.git.diff(['--', filePath]); + } + + async commit(message, files) { + await this.git.add(files); + return await this.git.commit(message); + } +} +``` + +**Dependencies**: +```json +{ + "simple-git": "^3.22.0" +} +``` + +**Estimated Effort**: 3-4 days + +--- + +### 6.4 Real-time Collaboration + +**Description**: Google Docs-like real-time editing + +**Architecture**: +- WebSocket server for real-time sync +- Operational Transformation (OT) or CRDT for conflict resolution +- Cursor presence indicators +- Chat/comments sidebar + +**Implementation Options**: +1. **Yjs**: CRDT-based collaboration library +2. **ShareDB**: OT-based real-time database +3. **Self-hosted**: Custom WebSocket + CRDT + +**Dependencies**: +```json +{ + "yjs": "^13.6.0", + "y-websocket": "^1.5.0", + "y-codemirror.next": "^0.3.0" +} +``` + +**Estimated Effort**: 10-15 days + +--- + +### 6.5 Cloud Sync & Backup + +**Features**: +- Sync documents across devices +- Automatic backup to cloud +- Support for multiple providers (Google Drive, Dropbox, OneDrive) +- Offline-first with sync when connected + +**Implementation**: +```javascript +// src/main/CloudSync.js +class CloudSync { + constructor(provider) { + this.provider = provider; // 'google', 'dropbox', 'onedrive' + this.syncQueue = []; + this.online = navigator.onLine; + } + + async sync(document) { + if (!this.online) { + this.queueForSync(document); + return; + } + + const remoteVersion = await this.provider.getVersion(document.id); + + if (remoteVersion > document.version) { + // Pull remote changes + return await this.pullChanges(document); + } else if (document.modified) { + // Push local changes + return await this.pushChanges(document); + } + } + + queueForSync(document) { + this.syncQueue.push({ + document, + timestamp: Date.now() + }); + this.persistQueue(); + } + + async processSyncQueue() { + while (this.syncQueue.length > 0) { + const item = this.syncQueue.shift(); + await this.sync(item.document); + } + } +} +``` + +**Estimated Effort**: 5-7 days + +--- + +### 6.6 AI-Powered Features + +**Features**: +- AI writing assistant (grammar, style suggestions) +- Auto-complete suggestions +- Document summarization +- Translation assistance +- Content generation from prompts + +**Implementation Options**: +1. **OpenAI API**: GPT-4 integration +2. **Anthropic API**: Claude integration +3. **Local LLM**: Ollama/llama.cpp for offline + +**Privacy-First Approach**: +```javascript +// src/main/AIAssistant.js +class AIAssistant { + constructor(config) { + this.mode = config.mode; // 'cloud' | 'local' | 'disabled' + this.localModel = null; + this.cloudClient = null; + } + + async initialize() { + if (this.mode === 'local') { + // Use Ollama or similar for local inference + this.localModel = await this.loadLocalModel(); + } else if (this.mode === 'cloud') { + // User provides their own API key + this.cloudClient = new OpenAI({ apiKey: config.apiKey }); + } + } + + async suggest(context, type) { + const prompt = this.buildPrompt(context, type); + + if (this.mode === 'local') { + return await this.localModel.generate(prompt); + } else { + return await this.cloudClient.chat.completions.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: prompt }] + }); + } + } +} +``` + +**Estimated Effort**: 5-7 days + +--- + +### 6.7 Advanced Diagram Support + +**Current**: Basic Mermaid.js support + +**Enhancements**: +- PlantUML integration +- Draw.io/Excalidraw embedding +- Live diagram editing with visual editor +- Export diagrams as images + +**Implementation**: +```javascript +// src/renderer/DiagramRenderer.js +class DiagramRenderer { + static SUPPORTED = ['mermaid', 'plantuml', 'graphviz', 'excalidraw']; + + async render(type, code) { + switch (type) { + case 'mermaid': + return await mermaid.render('diagram', code); + case 'plantuml': + return await this.renderPlantUML(code); + case 'graphviz': + return await this.renderGraphviz(code); + case 'excalidraw': + return await this.renderExcalidraw(code); + } + } + + async renderPlantUML(code) { + // Use PlantUML server or local jar + const encoded = this.encodePlantUML(code); + const response = await fetch(`http://www.plantuml.com/plantuml/svg/${encoded}`); + return await response.text(); + } +} +``` + +**Dependencies**: +```json +{ + "@mermaid-js/mermaid": "^10.6.0", + "plantuml-encoder": "^1.4.0", + "@excalidraw/excalidraw": "^0.17.0" +} +``` + +**Estimated Effort**: 3-4 days + +--- + +### 6.8 Focus Mode & Zen Writing + +**Features**: +- Distraction-free writing mode +- Typewriter scrolling +- Ambient sounds/music +- Pomodoro timer integration +- Writing goals and statistics + +**Implementation**: +```javascript +// src/renderer/FocusMode.js +class FocusMode { + constructor(editor) { + this.editor = editor; + this.enabled = false; + this.typewriterMode = false; + this.ambientPlayer = null; + } + + enable(options = {}) { + this.enabled = true; + + // Hide UI elements + document.body.classList.add('focus-mode'); + + // Enable typewriter scrolling + if (options.typewriter) { + this.enableTypewriter(); + } + + // Start ambient sounds + if (options.ambient) { + this.startAmbient(options.ambientSound); + } + + // Start pomodoro + if (options.pomodoro) { + this.startPomodoro(options.pomodoroMinutes || 25); + } + } + + enableTypewriter() { + this.typewriterMode = true; + this.editor.on('change', () => { + const cursor = this.editor.getCursor(); + this.scrollToCenter(cursor.line); + }); + } + + scrollToCenter(line) { + const editorHeight = this.editor.element.clientHeight; + const lineHeight = this.editor.lineHeight; + const targetScroll = (line * lineHeight) - (editorHeight / 2); + this.editor.scrollTo(0, targetScroll); + } +} +``` + +**CSS**: +```css +.focus-mode { + --focus-bg: #1a1a2e; + --focus-text: #eee; +} + +.focus-mode #toolbar, +.focus-mode #tab-bar, +.focus-mode #status-bar, +.focus-mode .preview-pane { + display: none !important; +} + +.focus-mode .editor-pane { + width: 100% !important; + max-width: 800px; + margin: 0 auto; + padding: 100px 40px; +} + +.focus-mode .editor-content { + font-size: 18px; + line-height: 1.8; +} +``` + +**Estimated Effort**: 2-3 days + +--- + +### 6.9 Document Templates + +**Features**: +- Pre-built document templates +- Custom template creation +- Template marketplace/sharing +- Category-based organization + +**Template Categories**: +- **Academic**: Essay, Research Paper, Thesis, Lab Report +- **Business**: Report, Proposal, Meeting Notes, Invoice +- **Technical**: README, API Documentation, Tutorial, Changelog +- **Personal**: Journal, Blog Post, Recipe, Travel Log +- **Creative**: Story, Screenplay, Poetry, Song Lyrics + +**Implementation**: +```javascript +// src/renderer/TemplateManager.js +class TemplateManager { + static TEMPLATES_DIR = path.join(app.getPath('userData'), 'templates'); + + async getTemplates() { + const builtIn = await this.loadBuiltInTemplates(); + const custom = await this.loadCustomTemplates(); + return [...builtIn, ...custom]; + } + + async createFromTemplate(templateId) { + const template = await this.getTemplate(templateId); + const content = this.processVariables(template.content, { + date: new Date().toLocaleDateString(), + author: this.settings.get('author'), + title: 'Untitled' + }); + return content; + } + + processVariables(content, variables) { + return content.replace(/\{\{(\w+)\}\}/g, (match, key) => { + return variables[key] || match; + }); + } +} +``` + +**Estimated Effort**: 2-3 days + +--- + +### 6.10 Mobile Companion App + +**Platform**: React Native or Flutter + +**Features**: +- Sync with desktop app +- Basic editing capabilities +- Preview and share +- Offline support + +**Estimated Effort**: 15-20 days (separate project) + +--- + +## Phase 7: Community & Ecosystem (Lower Priority) + +### 7.1 Community Building + +**Tasks**: +- [ ] Create Discord/Slack community +- [ ] Set up GitHub Discussions +- [ ] Create Twitter/X account for updates +- [ ] Write blog posts about development +- [ ] Create video tutorials + +**Estimated Effort**: Ongoing + +--- + +### 7.2 Plugin Marketplace + +**Features**: +- Browse and install plugins +- Rating and reviews +- Automatic updates +- Revenue sharing for premium plugins + +**Estimated Effort**: 10-15 days + +--- + +### 7.3 Theme Marketplace + +**Features**: +- Community-created themes +- Theme preview +- Easy installation +- Theme editor tool + +**Estimated Effort**: 5-7 days + +--- + +### 7.4 Internationalization (i18n) + +**Supported Languages** (Priority): +1. English (default) +2. Spanish +3. French +4. German +5. Chinese (Simplified) +6. Japanese +7. Korean +8. Portuguese +9. Russian +10. Arabic + +**Implementation**: +```javascript +// src/i18n/index.js +const i18next = require('i18next'); + +i18next.init({ + lng: 'en', + fallbackLng: 'en', + resources: { + en: require('./locales/en.json'), + es: require('./locales/es.json'), + // ... more languages + } +}); + +// Usage +t('menu.file.new') // "New File" or "Nuevo Archivo" +``` + +**Estimated Effort**: 3-5 days (infrastructure) + ongoing translation + +--- + +### 7.5 Accessibility (a11y) + +**Tasks**: +- [ ] Add ARIA labels to all interactive elements +- [ ] Ensure keyboard navigation for all features +- [ ] Add screen reader support +- [ ] Ensure color contrast compliance (WCAG 2.1) +- [ ] Add focus indicators +- [ ] Support reduced motion preferences + +**Implementation**: +```html + + +``` + +**Estimated Effort**: 2-3 days + +--- + +## Implementation Timeline + +### Quarter 1: Foundation (Weeks 1-4) +| Week | Focus | Tasks | +|------|-------|-------| +| 1 | Security | Phase 1.1-1.3 (Security hardening) | +| 2 | Testing | Phase 2.1-2.2 (Test infrastructure) | +| 3 | Quality | Phase 2.3, 4.3 (Linting, CI/CD) | +| 4 | Architecture | Phase 3.1 (Modularize renderer.js) | + +### Quarter 2: Refinement (Weeks 5-8) +| Week | Focus | Tasks | +|------|-------|-------| +| 5 | Architecture | Phase 3.2-3.3 (Modularize main.js, IPC) | +| 6 | Performance | Phase 5.1-5.2 (Optimization) | +| 7 | Documentation | Phase 4.2 (Docs, architecture) | +| 8 | Polish | Phase 3.4, 5.3-5.4 (Error handling, memory) | + +### Quarter 3: Features (Weeks 9-16) +| Week | Focus | Tasks | +|------|-------|-------| +| 9-10 | Plugins | Phase 6.1 (Plugin system) | +| 11 | Spell Check | Phase 6.2 (Spell check & grammar) | +| 12 | Git | Phase 6.3 (Version control) | +| 13-14 | Focus Mode | Phase 6.8, 6.9 (Focus mode, templates) | +| 15-16 | Diagrams | Phase 6.7 (Advanced diagrams) | + +### Quarter 4: Ecosystem (Weeks 17-20) +| Week | Focus | Tasks | +|------|-------|-------| +| 17 | i18n | Phase 7.4 (Internationalization) | +| 18 | a11y | Phase 7.5 (Accessibility) | +| 19 | Community | Phase 7.1-7.2 (Community, marketplace) | +| 20 | Polish | Final testing, documentation, release | + +--- + +## Success Metrics + +### Quality Metrics +- [ ] 80%+ test coverage +- [ ] Zero critical security vulnerabilities +- [ ] < 100ms preview render time +- [ ] < 3s cold start time +- [ ] < 200MB memory usage (typical) + +### Community Metrics +- [ ] 1000+ GitHub stars +- [ ] 50+ contributors +- [ ] 20+ community plugins +- [ ] 50+ community themes +- [ ] Active Discord community + +### Feature Metrics +- [ ] 100+ export format combinations +- [ ] 10+ supported languages +- [ ] WCAG 2.1 AA compliance +- [ ] Plugin API stability (v1.0) + +--- + +## Risk Assessment + +| Risk | Probability | Impact | Mitigation | +|------|-------------|--------|------------| +| Breaking changes in refactor | High | Medium | Comprehensive testing, incremental changes | +| Security vulnerability discovered | Medium | High | Security audit, responsible disclosure policy | +| Community fragmentation | Low | Medium | Clear governance, contributor guidelines | +| Dependency deprecation | Medium | Medium | Regular dependency updates, abstraction layers | +| Scope creep | High | Medium | Strict prioritization, feature freeze periods | + +--- + +## Resource Requirements + +### Development +- 1-2 full-time developers (or equivalent open source contributions) +- Code review process for security-sensitive changes +- Automated CI/CD pipeline + +### Infrastructure +- GitHub repository (existing) +- CI/CD (GitHub Actions) +- Documentation hosting (GitHub Pages) +- Community platform (Discord/Discourse) + +### External Services (Optional) +- Code signing certificates (Windows/macOS) +- Translation services +- Security audit services + +--- + +## Conclusion + +PanConverter has a solid foundation with impressive features. By following this improvement plan, it can become a world-class open source Markdown editor that rivals commercial alternatives like Typora, Obsidian, and Bear. + +**Key Differentiators After Implementation**: +1. **Security-First**: Properly secured Electron app +2. **Extensible**: Plugin system for community extensions +3. **Feature-Rich**: PDF editor, batch processing, templates +4. **Cross-Platform**: Windows, macOS, Linux with consistent experience +5. **Open Source**: MIT licensed, community-driven +6. **Privacy-Focused**: Local-first with optional cloud sync + +**Next Steps**: +1. Review and prioritize this plan +2. Create GitHub issues/milestones +3. Begin Phase 1 (Security) immediately +4. Recruit contributors for parallel work + +--- + +*Plan Version: 1.0* +*Created: January 2026* +*Author: Claude Code Assistant* diff --git a/LICENSE b/LICENSE index 1d0db01..b7ce544 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2024 Amit Haridas - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +MIT License + +Copyright (c) 2024 Amit Haridas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/PUSH_INSTRUCTIONS.md b/PUSH_INSTRUCTIONS.md index 94a0220..b87e402 100644 --- a/PUSH_INSTRUCTIONS.md +++ b/PUSH_INSTRUCTIONS.md @@ -1,12 +1,12 @@ -# Test Double-Click File Opening - -This is a test file to verify double-click functionality. - -## Features to Test -- File should load automatically -- Content should display in editor -- Tab should show filename -- Preview should render markdown - -**This text should be bold** -*This text should be italic* +# Test Double-Click File Opening + +This is a test file to verify double-click functionality. + +## Features to Test +- File should load automatically +- Content should display in editor +- Tab should show filename +- Preview should render markdown + +**This text should be bold** +*This text should be italic* diff --git a/README.latex b/README.latex index 136030a..bba1579 100644 --- a/README.latex +++ b/README.latex @@ -1,378 +1,378 @@ -\hypertarget{panconverter}{% -\section{PanConverter}\label{panconverter}} - -A cross-platform Markdown editor and converter powered by Pandoc. - -\begin{figure} -\centering -\includegraphics{assets/icon.png} -\caption{PanConverter} -\end{figure} - -\hypertarget{features}{% -\subsection{Features}\label{features}} - -\hypertarget{advanced-markdown-editor}{% -\subsubsection{✨ Advanced Markdown -Editor}\label{advanced-markdown-editor}} - -\begin{itemize} -\tightlist -\item - 🗂️ \textbf{Tabbed Interface} - Work with multiple files simultaneously - in separate tabs -\item - 📝 \textbf{Rich Text Editor} - Full-featured editor with syntax - highlighting and comprehensive toolbar -\item - 🔍 \textbf{Find \& Replace} - Powerful search and replace with match - highlighting and navigation -\item - 🔢 \textbf{Line Numbers} - Toggle line numbers for easier code editing - and navigation -\item - ↩️ \textbf{Smart Auto-Indentation} - Automatic list continuation and - intelligent indentation -\item - ⏪ \textbf{Undo/Redo} - Full undo/redo support with keyboard shortcuts -\item - ⌨️ \textbf{Advanced Shortcuts} - Tab indentation, line selection, and - smart text formatting -\item - 📂 \textbf{File Association Support} - Open markdown files directly - from file manager -\end{itemize} - -\hypertarget{themes-interface}{% -\subsubsection{🎨 Themes \& Interface}\label{themes-interface}} - -\begin{itemize} -\tightlist -\item - 👁️ \textbf{Live Preview} - See your markdown rendered in real-time - with synchronized scrolling -\item - 🎨 \textbf{Multiple Themes} - Choose from Light, Dark, Solarized, - Monokai, or GitHub themes -\item - 💾 \textbf{Auto-Save} - Never lose your work with automatic saving - every 30 seconds -\end{itemize} - -\hypertarget{export-conversion}{% -\subsubsection{📤 Export \& Conversion}\label{export-conversion}} - -\begin{itemize} -\tightlist -\item - 📄 \textbf{Enhanced PDF Export} - Robust PDF generation with multiple - LaTeX engine fallbacks (XeLaTeX, PDFLaTeX, wkhtmltopdf) -\item - 📄 \textbf{Document Export} - Convert to HTML, DOCX, LaTeX, RTF, ODT, - EPUB, PowerPoint (PPTX), and OpenDocument Presentation (ODP) -\item - 📊 \textbf{Spreadsheet Export} - Export markdown tables to Excel - (XLSX/XLS) and OpenDocument Spreadsheet (ODS) formats -\item - 📥 \textbf{Document Import} - Import DOCX, ODT, RTF, HTML, PDF, and - presentation files to markdown -\item - 📋 \textbf{Table Creation Helper} - Built-in table generator for easy - markdown table creation -\end{itemize} - -\hypertarget{platform-support}{% -\subsubsection{🖥️ Platform Support}\label{platform-support}} - -\begin{itemize} -\tightlist -\item - \textbf{Cross-Platform} - Works seamlessly on Windows, macOS, and - Linux -\end{itemize} - -\hypertarget{installation}{% -\subsection{Installation}\label{installation}} - -\hypertarget{prerequisites}{% -\subsubsection{Prerequisites}\label{prerequisites}} - -\begin{itemize} -\tightlist -\item - \href{https://pandoc.org/installing.html}{Pandoc} must be installed - for export functionality - - \begin{itemize} - \tightlist - \item - \textbf{Ubuntu/Debian}: \texttt{sudo\ apt-get\ install\ pandoc} - \item - \textbf{macOS}: \texttt{brew\ install\ pandoc} - \item - \textbf{Windows}: Download installer from Pandoc website - \end{itemize} -\end{itemize} - -\hypertarget{pdf-export-requirements}{% -\subsubsection{PDF Export Requirements}\label{pdf-export-requirements}} - -For optimal PDF export, install a LaTeX engine (recommended): - -\textbf{Ubuntu/Debian}: -\texttt{sudo\ apt-get\ install\ texlive-xetex\ texlive-latex-base} - -\textbf{macOS}: \texttt{brew\ install\ -\/-cask\ mactex} - -\textbf{Windows}: Install MiKTeX or TeX Live - \textbf{Alternative}: -\texttt{sudo\ apt-get\ install\ wkhtmltopdf} (fallback option) - -\hypertarget{download}{% -\subsubsection{Download}\label{download}} - -Download the latest release for your platform from the -\href{https://github.com/amitwh/pan-converter/releases}{Releases} page. - -\hypertarget{linux}{% -\paragraph{Linux}\label{linux}} - -\begin{itemize} -\tightlist -\item - \textbf{AppImage}: \texttt{PanConverter-1.3.1.AppImage} (universal, - may require \texttt{-\/-no-sandbox} flag) -\item - \textbf{Debian Package}: \texttt{pan-converter\_1.3.1\_amd64.deb} -\item - \textbf{Snap Package}: \texttt{pan-converter\_1.3.1\_amd64.snap} -\end{itemize} - -\hypertarget{install-from-source}{% -\subsubsection{Install from Source}\label{install-from-source}} - -\begin{Shaded} -\begin{Highlighting}[] -\FunctionTok{git}\NormalTok{ clone https://github.com/amitwh/pan{-}converter.git} -\BuiltInTok{cd}\NormalTok{ pan{-}converter} -\ExtensionTok{npm}\NormalTok{ install} -\ExtensionTok{npm}\NormalTok{ start} -\end{Highlighting} -\end{Shaded} - -\hypertarget{usage}{% -\subsection{Usage}\label{usage}} - -\hypertarget{basic-workflow}{% -\subsubsection{Basic Workflow}\label{basic-workflow}} - -\begin{enumerate} -\def\labelenumi{\arabic{enumi}.} -\tightlist -\item - \textbf{Write} - Use the editor to write your Markdown content -\item - \textbf{Preview} - Toggle the preview pane to see rendered output -\item - \textbf{Theme} - Choose your preferred theme from the View menu -\item - \textbf{Export} - Export your document to various formats -\end{enumerate} - -\hypertarget{export-options}{% -\subsubsection{Export Options}\label{export-options}} - -\begin{itemize} -\tightlist -\item - \textbf{Documents}: HTML, PDF, DOCX, LaTeX, RTF, ODT, EPUB -\item - \textbf{Presentations}: PowerPoint (PPTX), OpenDocument Presentation - (ODP) -\item - \textbf{Spreadsheets}: Excel (XLSX/XLS), OpenDocument Spreadsheet - (ODS) -\end{itemize} - -\hypertarget{import-conversion}{% -\subsubsection{Import \& Conversion}\label{import-conversion}} - -\begin{itemize} -\tightlist -\item - \textbf{Import Documents}: Convert DOCX, ODT, RTF, HTML, PDF, and - presentation files to Markdown -\item - \textbf{Cross-Format Conversion}: Convert current file between - multiple formats -\item - \textbf{Smart Presentation Handling}: Automatic slide-level formatting - for PPTX/ODP exports -\end{itemize} - -\hypertarget{table-creation}{% -\subsubsection{Table Creation}\label{table-creation}} - -\begin{itemize} -\tightlist -\item - Click the table button in the toolbar -\item - Specify number of rows and columns -\item - Automatically generates properly formatted Markdown tables -\end{itemize} - -\hypertarget{keyboard-shortcuts}{% -\subsection{Keyboard Shortcuts}\label{keyboard-shortcuts}} - -\hypertarget{file-operations}{% -\subsubsection{File Operations}\label{file-operations}} - -\begin{itemize} -\tightlist -\item - \texttt{Ctrl/Cmd\ +\ N} - New file/tab -\item - \texttt{Ctrl/Cmd\ +\ T} - New tab -\item - \texttt{Ctrl/Cmd\ +\ W} - Close current tab -\item - \texttt{Ctrl/Cmd\ +\ Tab} - Switch to next tab -\item - \texttt{Ctrl/Cmd\ +\ O} - Open file -\item - \texttt{Ctrl/Cmd\ +\ S} - Save file -\item - \texttt{Ctrl/Cmd\ +\ Shift\ +\ S} - Save as -\item - \texttt{Ctrl/Cmd\ +\ I} - Import document -\end{itemize} - -\hypertarget{editor-features}{% -\subsubsection{Editor Features}\label{editor-features}} - -\begin{itemize} -\tightlist -\item - \texttt{Ctrl/Cmd\ +\ F} - Find \& Replace -\item - \texttt{Ctrl/Cmd\ +\ Z} - Undo -\item - \texttt{Ctrl/Cmd\ +\ Shift\ +\ Z} - Redo -\item - \texttt{Tab} - Indent lines or insert 4 spaces -\item - \texttt{Shift\ +\ Tab} - Outdent selected lines -\item - \texttt{Enter} - Auto-continue lists with proper indentation -\end{itemize} - -\hypertarget{view-navigation}{% -\subsubsection{View \& Navigation}\label{view-navigation}} - -\begin{itemize} -\tightlist -\item - \texttt{Ctrl/Cmd\ +\ P} - Toggle preview -\item - \texttt{Ctrl/Cmd\ +\ Enter} - Toggle preview (alternative) -\item - \texttt{Escape} - Close find dialog -\end{itemize} - -\hypertarget{building}{% -\subsection{Building}\label{building}} - -\begin{Shaded} -\begin{Highlighting}[] -\CommentTok{\# Install dependencies} -\ExtensionTok{npm}\NormalTok{ install} - -\CommentTok{\# Generate icons} -\ExtensionTok{npm}\NormalTok{ run generate{-}icons} - -\CommentTok{\# Build for current platform} -\ExtensionTok{npm}\NormalTok{ run build} - -\CommentTok{\# Build for specific platform} -\ExtensionTok{npm}\NormalTok{ run build:win }\CommentTok{\# Windows} -\ExtensionTok{npm}\NormalTok{ run build:mac }\CommentTok{\# macOS } -\ExtensionTok{npm}\NormalTok{ run build:linux }\CommentTok{\# Linux (generates .deb, .AppImage, and .snap)} - -\CommentTok{\# Build for all platforms} -\ExtensionTok{npm}\NormalTok{ run dist:all} -\end{Highlighting} -\end{Shaded} - -\hypertarget{version-history}{% -\subsection{Version History}\label{version-history}} - -\begin{itemize} -\tightlist -\item - \textbf{v1.3.1} - Bug fixes: Fixed file associations for - double-clicking .md files, corrected 50/50 layout alignment for - editor/preview panes -\item - \textbf{v1.3.0} - Major update: Tabbed interface for multiple files, - enhanced PDF export with LaTeX engines, fixed file associations, - removed redundant converter menu, improved UI architecture -\item - \textbf{v1.2.1} - Comprehensive editor enhancements: Find \& Replace, - Line Numbers, Undo/Redo, Auto-indentation, PowerPoint export, document - conversion menu, table creation helper, spreadsheet export -\item - \textbf{v1.1.0} - Added Excel/ODS spreadsheet export, updated author - information, renamed to PanConverter -\item - \textbf{v1.0.0} - Initial release with basic markdown editing, themes, - and Pandoc export -\end{itemize} - -\hypertarget{known-issues}{% -\subsection{Known Issues}\label{known-issues}} - -\begin{itemize} -\tightlist -\item - AppImage may require \texttt{-\/-no-sandbox} flag on some Linux - systems -\item - Windows/Mac builds require platform-specific build environments -\item - Large files may cause performance issues -\end{itemize} - -\hypertarget{contributing}{% -\subsection{Contributing}\label{contributing}} - -Contributions are welcome! Please feel free to submit a Pull Request to -the \href{https://github.com/amitwh/pan-converter}{GitHub repository}. - -\hypertarget{license}{% -\subsection{License}\label{license}} - -MIT License - see LICENSE file for details. - -\hypertarget{author}{% -\subsection{Author}\label{author}} - -\textbf{Amit Haridas} - -\href{mailto:amit.wh@gmail.com}{\nolinkurl{amit.wh@gmail.com}} - -\hypertarget{acknowledgments}{% -\subsection{Acknowledgments}\label{acknowledgments}} - -\begin{itemize} -\tightlist -\item - Built with \href{https://www.electronjs.org/}{Electron} -\item - Markdown parsing by \href{https://marked.js.org/}{marked} -\item - Export functionality powered by \href{https://pandoc.org/}{Pandoc} -\item - Syntax highlighting by \href{https://highlightjs.org/}{highlight.js} -\item - Spreadsheet export by \href{https://www.npmjs.com/package/xlsx}{XLSX} -\item - HTML sanitization by - \href{https://www.npmjs.com/package/dompurify}{DOMPurify} -\end{itemize} +\hypertarget{panconverter}{% +\section{PanConverter}\label{panconverter}} + +A cross-platform Markdown editor and converter powered by Pandoc. + +\begin{figure} +\centering +\includegraphics{assets/icon.png} +\caption{PanConverter} +\end{figure} + +\hypertarget{features}{% +\subsection{Features}\label{features}} + +\hypertarget{advanced-markdown-editor}{% +\subsubsection{✨ Advanced Markdown +Editor}\label{advanced-markdown-editor}} + +\begin{itemize} +\tightlist +\item + 🗂️ \textbf{Tabbed Interface} - Work with multiple files simultaneously + in separate tabs +\item + 📝 \textbf{Rich Text Editor} - Full-featured editor with syntax + highlighting and comprehensive toolbar +\item + 🔍 \textbf{Find \& Replace} - Powerful search and replace with match + highlighting and navigation +\item + 🔢 \textbf{Line Numbers} - Toggle line numbers for easier code editing + and navigation +\item + ↩️ \textbf{Smart Auto-Indentation} - Automatic list continuation and + intelligent indentation +\item + ⏪ \textbf{Undo/Redo} - Full undo/redo support with keyboard shortcuts +\item + ⌨️ \textbf{Advanced Shortcuts} - Tab indentation, line selection, and + smart text formatting +\item + 📂 \textbf{File Association Support} - Open markdown files directly + from file manager +\end{itemize} + +\hypertarget{themes-interface}{% +\subsubsection{🎨 Themes \& Interface}\label{themes-interface}} + +\begin{itemize} +\tightlist +\item + 👁️ \textbf{Live Preview} - See your markdown rendered in real-time + with synchronized scrolling +\item + 🎨 \textbf{Multiple Themes} - Choose from Light, Dark, Solarized, + Monokai, or GitHub themes +\item + 💾 \textbf{Auto-Save} - Never lose your work with automatic saving + every 30 seconds +\end{itemize} + +\hypertarget{export-conversion}{% +\subsubsection{📤 Export \& Conversion}\label{export-conversion}} + +\begin{itemize} +\tightlist +\item + 📄 \textbf{Enhanced PDF Export} - Robust PDF generation with multiple + LaTeX engine fallbacks (XeLaTeX, PDFLaTeX, wkhtmltopdf) +\item + 📄 \textbf{Document Export} - Convert to HTML, DOCX, LaTeX, RTF, ODT, + EPUB, PowerPoint (PPTX), and OpenDocument Presentation (ODP) +\item + 📊 \textbf{Spreadsheet Export} - Export markdown tables to Excel + (XLSX/XLS) and OpenDocument Spreadsheet (ODS) formats +\item + 📥 \textbf{Document Import} - Import DOCX, ODT, RTF, HTML, PDF, and + presentation files to markdown +\item + 📋 \textbf{Table Creation Helper} - Built-in table generator for easy + markdown table creation +\end{itemize} + +\hypertarget{platform-support}{% +\subsubsection{🖥️ Platform Support}\label{platform-support}} + +\begin{itemize} +\tightlist +\item + \textbf{Cross-Platform} - Works seamlessly on Windows, macOS, and + Linux +\end{itemize} + +\hypertarget{installation}{% +\subsection{Installation}\label{installation}} + +\hypertarget{prerequisites}{% +\subsubsection{Prerequisites}\label{prerequisites}} + +\begin{itemize} +\tightlist +\item + \href{https://pandoc.org/installing.html}{Pandoc} must be installed + for export functionality + + \begin{itemize} + \tightlist + \item + \textbf{Ubuntu/Debian}: \texttt{sudo\ apt-get\ install\ pandoc} + \item + \textbf{macOS}: \texttt{brew\ install\ pandoc} + \item + \textbf{Windows}: Download installer from Pandoc website + \end{itemize} +\end{itemize} + +\hypertarget{pdf-export-requirements}{% +\subsubsection{PDF Export Requirements}\label{pdf-export-requirements}} + +For optimal PDF export, install a LaTeX engine (recommended): - +\textbf{Ubuntu/Debian}: +\texttt{sudo\ apt-get\ install\ texlive-xetex\ texlive-latex-base} - +\textbf{macOS}: \texttt{brew\ install\ -\/-cask\ mactex} - +\textbf{Windows}: Install MiKTeX or TeX Live - \textbf{Alternative}: +\texttt{sudo\ apt-get\ install\ wkhtmltopdf} (fallback option) + +\hypertarget{download}{% +\subsubsection{Download}\label{download}} + +Download the latest release for your platform from the +\href{https://github.com/amitwh/pan-converter/releases}{Releases} page. + +\hypertarget{linux}{% +\paragraph{Linux}\label{linux}} + +\begin{itemize} +\tightlist +\item + \textbf{AppImage}: \texttt{PanConverter-1.3.1.AppImage} (universal, + may require \texttt{-\/-no-sandbox} flag) +\item + \textbf{Debian Package}: \texttt{pan-converter\_1.3.1\_amd64.deb} +\item + \textbf{Snap Package}: \texttt{pan-converter\_1.3.1\_amd64.snap} +\end{itemize} + +\hypertarget{install-from-source}{% +\subsubsection{Install from Source}\label{install-from-source}} + +\begin{Shaded} +\begin{Highlighting}[] +\FunctionTok{git}\NormalTok{ clone https://github.com/amitwh/pan{-}converter.git} +\BuiltInTok{cd}\NormalTok{ pan{-}converter} +\ExtensionTok{npm}\NormalTok{ install} +\ExtensionTok{npm}\NormalTok{ start} +\end{Highlighting} +\end{Shaded} + +\hypertarget{usage}{% +\subsection{Usage}\label{usage}} + +\hypertarget{basic-workflow}{% +\subsubsection{Basic Workflow}\label{basic-workflow}} + +\begin{enumerate} +\def\labelenumi{\arabic{enumi}.} +\tightlist +\item + \textbf{Write} - Use the editor to write your Markdown content +\item + \textbf{Preview} - Toggle the preview pane to see rendered output +\item + \textbf{Theme} - Choose your preferred theme from the View menu +\item + \textbf{Export} - Export your document to various formats +\end{enumerate} + +\hypertarget{export-options}{% +\subsubsection{Export Options}\label{export-options}} + +\begin{itemize} +\tightlist +\item + \textbf{Documents}: HTML, PDF, DOCX, LaTeX, RTF, ODT, EPUB +\item + \textbf{Presentations}: PowerPoint (PPTX), OpenDocument Presentation + (ODP) +\item + \textbf{Spreadsheets}: Excel (XLSX/XLS), OpenDocument Spreadsheet + (ODS) +\end{itemize} + +\hypertarget{import-conversion}{% +\subsubsection{Import \& Conversion}\label{import-conversion}} + +\begin{itemize} +\tightlist +\item + \textbf{Import Documents}: Convert DOCX, ODT, RTF, HTML, PDF, and + presentation files to Markdown +\item + \textbf{Cross-Format Conversion}: Convert current file between + multiple formats +\item + \textbf{Smart Presentation Handling}: Automatic slide-level formatting + for PPTX/ODP exports +\end{itemize} + +\hypertarget{table-creation}{% +\subsubsection{Table Creation}\label{table-creation}} + +\begin{itemize} +\tightlist +\item + Click the table button in the toolbar +\item + Specify number of rows and columns +\item + Automatically generates properly formatted Markdown tables +\end{itemize} + +\hypertarget{keyboard-shortcuts}{% +\subsection{Keyboard Shortcuts}\label{keyboard-shortcuts}} + +\hypertarget{file-operations}{% +\subsubsection{File Operations}\label{file-operations}} + +\begin{itemize} +\tightlist +\item + \texttt{Ctrl/Cmd\ +\ N} - New file/tab +\item + \texttt{Ctrl/Cmd\ +\ T} - New tab +\item + \texttt{Ctrl/Cmd\ +\ W} - Close current tab +\item + \texttt{Ctrl/Cmd\ +\ Tab} - Switch to next tab +\item + \texttt{Ctrl/Cmd\ +\ O} - Open file +\item + \texttt{Ctrl/Cmd\ +\ S} - Save file +\item + \texttt{Ctrl/Cmd\ +\ Shift\ +\ S} - Save as +\item + \texttt{Ctrl/Cmd\ +\ I} - Import document +\end{itemize} + +\hypertarget{editor-features}{% +\subsubsection{Editor Features}\label{editor-features}} + +\begin{itemize} +\tightlist +\item + \texttt{Ctrl/Cmd\ +\ F} - Find \& Replace +\item + \texttt{Ctrl/Cmd\ +\ Z} - Undo +\item + \texttt{Ctrl/Cmd\ +\ Shift\ +\ Z} - Redo +\item + \texttt{Tab} - Indent lines or insert 4 spaces +\item + \texttt{Shift\ +\ Tab} - Outdent selected lines +\item + \texttt{Enter} - Auto-continue lists with proper indentation +\end{itemize} + +\hypertarget{view-navigation}{% +\subsubsection{View \& Navigation}\label{view-navigation}} + +\begin{itemize} +\tightlist +\item + \texttt{Ctrl/Cmd\ +\ P} - Toggle preview +\item + \texttt{Ctrl/Cmd\ +\ Enter} - Toggle preview (alternative) +\item + \texttt{Escape} - Close find dialog +\end{itemize} + +\hypertarget{building}{% +\subsection{Building}\label{building}} + +\begin{Shaded} +\begin{Highlighting}[] +\CommentTok{\# Install dependencies} +\ExtensionTok{npm}\NormalTok{ install} + +\CommentTok{\# Generate icons} +\ExtensionTok{npm}\NormalTok{ run generate{-}icons} + +\CommentTok{\# Build for current platform} +\ExtensionTok{npm}\NormalTok{ run build} + +\CommentTok{\# Build for specific platform} +\ExtensionTok{npm}\NormalTok{ run build:win }\CommentTok{\# Windows} +\ExtensionTok{npm}\NormalTok{ run build:mac }\CommentTok{\# macOS } +\ExtensionTok{npm}\NormalTok{ run build:linux }\CommentTok{\# Linux (generates .deb, .AppImage, and .snap)} + +\CommentTok{\# Build for all platforms} +\ExtensionTok{npm}\NormalTok{ run dist:all} +\end{Highlighting} +\end{Shaded} + +\hypertarget{version-history}{% +\subsection{Version History}\label{version-history}} + +\begin{itemize} +\tightlist +\item + \textbf{v1.3.1} - Bug fixes: Fixed file associations for + double-clicking .md files, corrected 50/50 layout alignment for + editor/preview panes +\item + \textbf{v1.3.0} - Major update: Tabbed interface for multiple files, + enhanced PDF export with LaTeX engines, fixed file associations, + removed redundant converter menu, improved UI architecture +\item + \textbf{v1.2.1} - Comprehensive editor enhancements: Find \& Replace, + Line Numbers, Undo/Redo, Auto-indentation, PowerPoint export, document + conversion menu, table creation helper, spreadsheet export +\item + \textbf{v1.1.0} - Added Excel/ODS spreadsheet export, updated author + information, renamed to PanConverter +\item + \textbf{v1.0.0} - Initial release with basic markdown editing, themes, + and Pandoc export +\end{itemize} + +\hypertarget{known-issues}{% +\subsection{Known Issues}\label{known-issues}} + +\begin{itemize} +\tightlist +\item + AppImage may require \texttt{-\/-no-sandbox} flag on some Linux + systems +\item + Windows/Mac builds require platform-specific build environments +\item + Large files may cause performance issues +\end{itemize} + +\hypertarget{contributing}{% +\subsection{Contributing}\label{contributing}} + +Contributions are welcome! Please feel free to submit a Pull Request to +the \href{https://github.com/amitwh/pan-converter}{GitHub repository}. + +\hypertarget{license}{% +\subsection{License}\label{license}} + +MIT License - see LICENSE file for details. + +\hypertarget{author}{% +\subsection{Author}\label{author}} + +\textbf{Amit Haridas} - +\href{mailto:amit.wh@gmail.com}{\nolinkurl{amit.wh@gmail.com}} + +\hypertarget{acknowledgments}{% +\subsection{Acknowledgments}\label{acknowledgments}} + +\begin{itemize} +\tightlist +\item + Built with \href{https://www.electronjs.org/}{Electron} +\item + Markdown parsing by \href{https://marked.js.org/}{marked} +\item + Export functionality powered by \href{https://pandoc.org/}{Pandoc} +\item + Syntax highlighting by \href{https://highlightjs.org/}{highlight.js} +\item + Spreadsheet export by \href{https://www.npmjs.com/package/xlsx}{XLSX} +\item + HTML sanitization by + \href{https://www.npmjs.com/package/dompurify}{DOMPurify} +\end{itemize} diff --git a/README.md b/README.md index cca5841..d1bac52 100644 --- a/README.md +++ b/README.md @@ -1,165 +1,165 @@ -# MarkdownConverter - -A powerful cross-platform Markdown editor and document converter powered by Pandoc, built with Electron. 100% open-source with no proprietary dependencies. - -## Features - -### Markdown Editor -image -- **Multi-tab editing** - Work on multiple files simultaneously -- **Live preview** - Real-time markdown rendering with syntax highlighting -- **Dynamic splitter** - Drag to resize editor and preview panes -- **25+ themes** - Light and dark themes including Atom One Light, Dracula, Nord, Sepia, and more -- **Find & Replace** - Search and replace with regex support -- **Line numbers** - Toggle line numbers in the editor -- **Auto-save** - Automatic saving every 30 seconds -- **Math support** - KaTeX integration for mathematical expressions - -### PDF Viewer & Editor -image - -- **Built-in PDF viewer** - Open and view PDF files directly in the app -- **Page navigation** - Navigate pages with keyboard or buttons -- **Zoom controls** - Zoom in/out, fit to width, fit to page -- **Rotation** - Rotate pages left or right -- **PDF Editor tools**: - - Merge multiple PDFs - - Split PDFs by page range - - Compress PDFs - - Rotate pages - - Delete pages - - Reorder pages - - Add watermarks - - Password protection - - Remove passwords - - Set permissions - -### Export Options -- **PDF** - Export to PDF with customizable page sizes and orientation -- **DOCX** - Standard and Enhanced (template-based) Word export -- **ODT** - OpenDocument format -- **HTML** - Web-ready HTML export -- **PowerPoint** - PPTX presentation export -- **EPUB** - E-book format -- **LaTeX** - Academic document format -- **RTF** - Rich Text Format - -### Advanced Features -- **Custom headers & footers** - Add headers/footers to exports with dynamic fields -- **Page size configuration** - A3, A4, A5, B4, B5, Letter, Legal, Tabloid, or custom sizes -- **Batch conversion** - Convert entire folders of markdown files -- **ASCII Art Generator** - Create text banners and diagrams -- **Word templates** - Use custom Word templates for enhanced exports -- **Import documents** - Import from 30+ formats (DOCX, PDF, HTML, etc.) - -## Installation - -### Prerequisites -- [Node.js](https://nodejs.org/) (v16 or later) -- [Pandoc](https://pandoc.org/installing.html) (required for export functionality) - -### Install Dependencies -```bash -npm install -``` - -### Run the Application -```bash -npm start -``` - -### Build for Distribution -```bash -# Windows -npm run build:win - -# macOS -npm run build:mac - -# Linux -npm run build:linux -``` - -## Keyboard Shortcuts - -| Action | Shortcut | -|--------|----------| -| New File | Ctrl+N | -| Open File | Ctrl+O | -| Open PDF | Ctrl+Shift+O | -| Save | Ctrl+S | -| Save As | Ctrl+Shift+S | -| Export | Ctrl+E | -| Print | Ctrl+P | -| Find | Ctrl+F | -| Undo | Ctrl+Z | -| Redo | Ctrl+Shift+Z | -| New Tab | Ctrl+T | -| Close Tab | Ctrl+W | -| Toggle Preview | Ctrl+Shift+P | -| Zoom In | Ctrl+Shift++ | -| Zoom Out | Ctrl+Shift+- | - -## Themes - -### Light Themes -- Atom One Light (Default) -- GitHub Light -- Light -- Solarized Light -- Gruvbox Light -- Ayu Light -- Sepia -- Paper -- Rose Pine Dawn -- Concrete Light - -### Dark Themes -- Dark -- One Dark -- Dracula -- Nord -- Monokai -- Material -- Gruvbox Dark -- Tokyo Night -- Palenight -- Ayu Dark -- Ayu Mirage -- Oceanic Next -- Cobalt2 -- Concrete Dark -- Concrete Warm - -## PDF Viewer - -Open PDF files directly in MarkdownConverter: -- **File > Open PDF** or **Ctrl+Shift+O** -- Navigate pages with arrow buttons or page input -- Zoom controls: +/- buttons, Fit Width, Fit Page -- Rotate pages left or right -- Close PDF to return to editor - -## Open Source - -MarkdownConverter is 100% open-source. All dependencies are permissively licensed: -- **Electron** - MIT License -- **pdf-lib** - MIT License -- **pdfjs-dist** - Apache 2.0 License -- **marked** - MIT License -- **highlight.js** - BSD 3-Clause License -- **dompurify** - Apache 2.0/MIT License -- **docx** - MIT License -- **xlsx** - Apache 2.0 License (SheetJS Community Edition) - -## License - -MIT License - see LICENSE file for details. - -## Author - -Amit Haridas (amit.wh@gmail.com) - -## Version - -v3.0.0 +# MarkdownConverter + +A powerful cross-platform Markdown editor and document converter powered by Pandoc, built with Electron. 100% open-source with no proprietary dependencies. + +## Features + +### Markdown Editor +image +- **Multi-tab editing** - Work on multiple files simultaneously +- **Live preview** - Real-time markdown rendering with syntax highlighting +- **Dynamic splitter** - Drag to resize editor and preview panes +- **25+ themes** - Light and dark themes including Atom One Light, Dracula, Nord, Sepia, and more +- **Find & Replace** - Search and replace with regex support +- **Line numbers** - Toggle line numbers in the editor +- **Auto-save** - Automatic saving every 30 seconds +- **Math support** - KaTeX integration for mathematical expressions + +### PDF Viewer & Editor +image + +- **Built-in PDF viewer** - Open and view PDF files directly in the app +- **Page navigation** - Navigate pages with keyboard or buttons +- **Zoom controls** - Zoom in/out, fit to width, fit to page +- **Rotation** - Rotate pages left or right +- **PDF Editor tools**: + - Merge multiple PDFs + - Split PDFs by page range + - Compress PDFs + - Rotate pages + - Delete pages + - Reorder pages + - Add watermarks + - Password protection + - Remove passwords + - Set permissions + +### Export Options +- **PDF** - Export to PDF with customizable page sizes and orientation +- **DOCX** - Standard and Enhanced (template-based) Word export +- **ODT** - OpenDocument format +- **HTML** - Web-ready HTML export +- **PowerPoint** - PPTX presentation export +- **EPUB** - E-book format +- **LaTeX** - Academic document format +- **RTF** - Rich Text Format + +### Advanced Features +- **Custom headers & footers** - Add headers/footers to exports with dynamic fields +- **Page size configuration** - A3, A4, A5, B4, B5, Letter, Legal, Tabloid, or custom sizes +- **Batch conversion** - Convert entire folders of markdown files +- **ASCII Art Generator** - Create text banners and diagrams +- **Word templates** - Use custom Word templates for enhanced exports +- **Import documents** - Import from 30+ formats (DOCX, PDF, HTML, etc.) + +## Installation + +### Prerequisites +- [Node.js](https://nodejs.org/) (v16 or later) +- [Pandoc](https://pandoc.org/installing.html) (required for export functionality) + +### Install Dependencies +```bash +npm install +``` + +### Run the Application +```bash +npm start +``` + +### Build for Distribution +```bash +# Windows +npm run build:win + +# macOS +npm run build:mac + +# Linux +npm run build:linux +``` + +## Keyboard Shortcuts + +| Action | Shortcut | +|--------|----------| +| New File | Ctrl+N | +| Open File | Ctrl+O | +| Open PDF | Ctrl+Shift+O | +| Save | Ctrl+S | +| Save As | Ctrl+Shift+S | +| Export | Ctrl+E | +| Print | Ctrl+P | +| Find | Ctrl+F | +| Undo | Ctrl+Z | +| Redo | Ctrl+Shift+Z | +| New Tab | Ctrl+T | +| Close Tab | Ctrl+W | +| Toggle Preview | Ctrl+Shift+P | +| Zoom In | Ctrl+Shift++ | +| Zoom Out | Ctrl+Shift+- | + +## Themes + +### Light Themes +- Atom One Light (Default) +- GitHub Light +- Light +- Solarized Light +- Gruvbox Light +- Ayu Light +- Sepia +- Paper +- Rose Pine Dawn +- Concrete Light + +### Dark Themes +- Dark +- One Dark +- Dracula +- Nord +- Monokai +- Material +- Gruvbox Dark +- Tokyo Night +- Palenight +- Ayu Dark +- Ayu Mirage +- Oceanic Next +- Cobalt2 +- Concrete Dark +- Concrete Warm + +## PDF Viewer + +Open PDF files directly in MarkdownConverter: +- **File > Open PDF** or **Ctrl+Shift+O** +- Navigate pages with arrow buttons or page input +- Zoom controls: +/- buttons, Fit Width, Fit Page +- Rotate pages left or right +- Close PDF to return to editor + +## Open Source + +MarkdownConverter is 100% open-source. All dependencies are permissively licensed: +- **Electron** - MIT License +- **pdf-lib** - MIT License +- **pdfjs-dist** - Apache 2.0 License +- **marked** - MIT License +- **highlight.js** - BSD 3-Clause License +- **dompurify** - Apache 2.0/MIT License +- **docx** - MIT License +- **xlsx** - Apache 2.0 License (SheetJS Community Edition) + +## License + +MIT License - see LICENSE file for details. + +## Author + +Amit Haridas (amit.wh@gmail.com) + +## Version + +v3.0.0 diff --git a/UPDATES.md b/UPDATES.md index 891bd71..9cd0fdc 100644 --- a/UPDATES.md +++ b/UPDATES.md @@ -1,166 +1,166 @@ -# PanConverter - Updates & Changelog - -## Version 2.1.0 (December 14, 2025) - -### 🎨 UI/UX Improvements - -#### Subtle & Small Preview Popout Button -- Redesigned popout button with minimalist aesthetic -- Removed border for cleaner appearance -- Reduced size: 11px font, 2px×6px padding (previously 14px font, 4px×8px padding) -- Added opacity transition: 50% when idle, 100% on hover -- Subtle background effect on hover instead of heavy border styling -- **File**: `src/styles.css:195-211` - -#### Simplified Table Headers in Preview -- Removed gradient background from table headers in modern theme -- Changed from `var(--primary-gradient)` (purple gradient) to simple light gray (#f0f0f0) -- Updated text color to dark (#333333) for better readability -- Clean, professional appearance matching standard themes -- **File**: `src/styles-modern.css:445-449` - -### 📥 Enhanced Import Capabilities - -#### Comprehensive Format-to-Markdown Conversion -Dramatically expanded the "Import Document" feature to support 30+ file formats: - -**Supported Formats:** -- **Documents**: DOCX, ODT, RTF, HTML, HTM, TEX, EPUB, PDF, TXT -- **Presentations**: PPTX, ODP -- **Markup Languages**: RST, Textile, MediaWiki, Org-mode, AsciiDoc, TWiki, OPML -- **E-book Formats**: EPUB, FB2 -- **LaTeX Formats**: TEX, LATEX, LTX -- **Web Formats**: HTML, HTM, XHTML -- **Wiki Formats**: MediaWiki, DokuWiki, TikiWiki, TWiki -- **Data Formats**: CSV, TSV, JSON - -**Format-Specific Optimizations:** -- PDF text extraction with XeLaTeX engine -- CSV/TSV automatic table conversion -- JSON structure handling -- Improved error messages with format hints - -**Access**: File → Import Document (Ctrl+I) -**File**: `src/main.js:1933-1994` - -### 🎨 Exhaustive ASCII Art Generator - -#### 5 New Text Banner Styles -Complete alphabet (A-Z) and numbers (0-9) support for all styles: - -1. **Standard** - Classic ASCII art with slashes and underscores -2. **Banner** - Large format using # characters (7-line height) -3. **Block** - Modern Unicode block characters (█ ╔ ╗ ═ ║) -4. **Bubble** - Circular bubble letters (Ⓐ Ⓑ Ⓒ) -5. **Digital** - Digital display style (▄ ▀ ▐ ▌) - -**File**: `src/renderer.js:3397-3537` - -#### 19 Professional ASCII Templates -Organized into 4 categories with expanded options: - -**Arrows & Flow (4 templates):** -- Arrow Right - Horizontal flow indicators -- Arrow Down - Vertical flow indicators -- Decision - Binary decision diagrams -- Process Flow - Multi-step process visualization - -**Diagrams & Charts (6 templates):** -- Flowchart - Advanced flowchart with decision branches and loops -- Sequence - Sequence diagrams for User-System-Database interactions -- Network - Server-client network topology -- Hierarchy - Organizational tree structures -- Timeline - Milestone visualization with dates -- Table Simple - Basic table template with borders - -**Boxes & Containers (4 templates):** -- Header - Section header with decorative borders -- Note Box - Important notes with rounded corners (┏━━┓) -- Warning Box - Warning messages with bold borders (╔═══╗) -- Info Box - Information boxes with subtle styling (╭───╮) - -**Decorative Elements (6 templates):** -- Divider - Horizontal section separator (═══) -- Separator Fancy - Elegant rounded divider -- Brackets - Japanese-style brackets 【 】 -- Banner Stars - Star-bordered banners -- Checklist - Task lists with ✓ checkmarks -- Progress Bar - Visual progress indicators - -**Features:** -- All ASCII art automatically wrapped in code blocks for proper rendering -- Preserved formatting in markdown preview and all export formats -- Categorized template selection interface -- Real-time preview generation - -**Access**: Tools → ASCII Art Generator -**Files**: `src/renderer.js:3513-3671`, `src/index.html:427-466` - -### 📝 Technical Improvements - -- Enhanced ASCII art detection in Word template exporter -- Improved monospace font rendering across all export formats -- Better code block preservation in PDF and Word exports -- Optimized template categorization and organization - -### 🔧 Files Modified - -- `src/styles.css` - Preview popout button styling -- `src/styles-modern.css` - Table header simplification -- `src/main.js` - Enhanced import function, version update -- `src/renderer.js` - ASCII art generator enhancements -- `src/index.html` - ASCII template UI organization -- `package.json` - Version bump to 2.1.0 - ---- - -## Version 2.0.0 (Previous Release) - -### Major Features -- Export Profiles - Save and reuse export configurations -- Mermaid.js diagram support -- Command Palette (Ctrl+Shift+P) -- GitHub Light/Dark preview themes -- Table Generator -- ASCII Art Generator (basic) -- Resizable Preview Pane -- Pop-out Preview Window -- Configurable page sizes (A3-A5, B4-B5, Letter, Legal, Tabloid, Custom) -- Custom Headers & Footers for exports -- Enhanced PDF and Word export with templates -- 22 beautiful themes - -### Core Capabilities -- Cross-platform markdown editor with live preview -- Universal document conversion (30+ formats) -- PDF Editor (merge, split, compress, rotate, watermark, encrypt) -- Batch file conversion -- File association support -- Advanced export options -- Multi-tab interface - ---- - -## Installation & Usage - -### Prerequisites -- **Pandoc** - Required for document conversion -- **Optional**: LibreOffice, ImageMagick, FFmpeg for universal converter - -### Download -Get the latest release from: https://github.com/amitwh/pan-converter/releases - -### Supported Platforms -- Windows (x64) -- Linux (AppImage, .deb, .snap) -- macOS (planned) - ---- - -## Contributing - -Contributions are welcome! Please see [CLAUDE.md](CLAUDE.md) for development guidelines. - -**Author**: Amit Haridas (amit.wh@gmail.com) -**License**: MIT -**Repository**: https://github.com/amitwh/pan-converter +# PanConverter - Updates & Changelog + +## Version 2.1.0 (December 14, 2025) + +### 🎨 UI/UX Improvements + +#### Subtle & Small Preview Popout Button +- Redesigned popout button with minimalist aesthetic +- Removed border for cleaner appearance +- Reduced size: 11px font, 2px×6px padding (previously 14px font, 4px×8px padding) +- Added opacity transition: 50% when idle, 100% on hover +- Subtle background effect on hover instead of heavy border styling +- **File**: `src/styles.css:195-211` + +#### Simplified Table Headers in Preview +- Removed gradient background from table headers in modern theme +- Changed from `var(--primary-gradient)` (purple gradient) to simple light gray (#f0f0f0) +- Updated text color to dark (#333333) for better readability +- Clean, professional appearance matching standard themes +- **File**: `src/styles-modern.css:445-449` + +### 📥 Enhanced Import Capabilities + +#### Comprehensive Format-to-Markdown Conversion +Dramatically expanded the "Import Document" feature to support 30+ file formats: + +**Supported Formats:** +- **Documents**: DOCX, ODT, RTF, HTML, HTM, TEX, EPUB, PDF, TXT +- **Presentations**: PPTX, ODP +- **Markup Languages**: RST, Textile, MediaWiki, Org-mode, AsciiDoc, TWiki, OPML +- **E-book Formats**: EPUB, FB2 +- **LaTeX Formats**: TEX, LATEX, LTX +- **Web Formats**: HTML, HTM, XHTML +- **Wiki Formats**: MediaWiki, DokuWiki, TikiWiki, TWiki +- **Data Formats**: CSV, TSV, JSON + +**Format-Specific Optimizations:** +- PDF text extraction with XeLaTeX engine +- CSV/TSV automatic table conversion +- JSON structure handling +- Improved error messages with format hints + +**Access**: File → Import Document (Ctrl+I) +**File**: `src/main.js:1933-1994` + +### 🎨 Exhaustive ASCII Art Generator + +#### 5 New Text Banner Styles +Complete alphabet (A-Z) and numbers (0-9) support for all styles: + +1. **Standard** - Classic ASCII art with slashes and underscores +2. **Banner** - Large format using # characters (7-line height) +3. **Block** - Modern Unicode block characters (█ ╔ ╗ ═ ║) +4. **Bubble** - Circular bubble letters (Ⓐ Ⓑ Ⓒ) +5. **Digital** - Digital display style (▄ ▀ ▐ ▌) + +**File**: `src/renderer.js:3397-3537` + +#### 19 Professional ASCII Templates +Organized into 4 categories with expanded options: + +**Arrows & Flow (4 templates):** +- Arrow Right - Horizontal flow indicators +- Arrow Down - Vertical flow indicators +- Decision - Binary decision diagrams +- Process Flow - Multi-step process visualization + +**Diagrams & Charts (6 templates):** +- Flowchart - Advanced flowchart with decision branches and loops +- Sequence - Sequence diagrams for User-System-Database interactions +- Network - Server-client network topology +- Hierarchy - Organizational tree structures +- Timeline - Milestone visualization with dates +- Table Simple - Basic table template with borders + +**Boxes & Containers (4 templates):** +- Header - Section header with decorative borders +- Note Box - Important notes with rounded corners (┏━━┓) +- Warning Box - Warning messages with bold borders (╔═══╗) +- Info Box - Information boxes with subtle styling (╭───╮) + +**Decorative Elements (6 templates):** +- Divider - Horizontal section separator (═══) +- Separator Fancy - Elegant rounded divider +- Brackets - Japanese-style brackets 【 】 +- Banner Stars - Star-bordered banners +- Checklist - Task lists with ✓ checkmarks +- Progress Bar - Visual progress indicators + +**Features:** +- All ASCII art automatically wrapped in code blocks for proper rendering +- Preserved formatting in markdown preview and all export formats +- Categorized template selection interface +- Real-time preview generation + +**Access**: Tools → ASCII Art Generator +**Files**: `src/renderer.js:3513-3671`, `src/index.html:427-466` + +### 📝 Technical Improvements + +- Enhanced ASCII art detection in Word template exporter +- Improved monospace font rendering across all export formats +- Better code block preservation in PDF and Word exports +- Optimized template categorization and organization + +### 🔧 Files Modified + +- `src/styles.css` - Preview popout button styling +- `src/styles-modern.css` - Table header simplification +- `src/main.js` - Enhanced import function, version update +- `src/renderer.js` - ASCII art generator enhancements +- `src/index.html` - ASCII template UI organization +- `package.json` - Version bump to 2.1.0 + +--- + +## Version 2.0.0 (Previous Release) + +### Major Features +- Export Profiles - Save and reuse export configurations +- Mermaid.js diagram support +- Command Palette (Ctrl+Shift+P) +- GitHub Light/Dark preview themes +- Table Generator +- ASCII Art Generator (basic) +- Resizable Preview Pane +- Pop-out Preview Window +- Configurable page sizes (A3-A5, B4-B5, Letter, Legal, Tabloid, Custom) +- Custom Headers & Footers for exports +- Enhanced PDF and Word export with templates +- 22 beautiful themes + +### Core Capabilities +- Cross-platform markdown editor with live preview +- Universal document conversion (30+ formats) +- PDF Editor (merge, split, compress, rotate, watermark, encrypt) +- Batch file conversion +- File association support +- Advanced export options +- Multi-tab interface + +--- + +## Installation & Usage + +### Prerequisites +- **Pandoc** - Required for document conversion +- **Optional**: LibreOffice, ImageMagick, FFmpeg for universal converter + +### Download +Get the latest release from: https://github.com/amitwh/pan-converter/releases + +### Supported Platforms +- Windows (x64) +- Linux (AppImage, .deb, .snap) +- macOS (planned) + +--- + +## Contributing + +Contributions are welcome! Please see [CLAUDE.md](CLAUDE.md) for development guidelines. + +**Author**: Amit Haridas (amit.wh@gmail.com) +**License**: MIT +**Repository**: https://github.com/amitwh/pan-converter diff --git a/eslint.config.js b/eslint.config.js index fcb2039..94637ba 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,89 +1,89 @@ -/** - * ESLint Configuration for PanConverter - * Uses flat config format (ESLint 9+) - */ - -module.exports = [ - { - // Global ignores - ignores: [ - 'node_modules/**', - 'dist/**', - 'coverage/**', - '*.min.js' - ] - }, - { - // JavaScript files - files: ['**/*.js'], - languageOptions: { - ecmaVersion: 2022, - sourceType: 'module', - globals: { - // Node.js - require: 'readonly', - module: 'readonly', - exports: 'readonly', - __dirname: 'readonly', - __filename: 'readonly', - process: 'readonly', - Buffer: 'readonly', - console: 'readonly', - setTimeout: 'readonly', - setInterval: 'readonly', - clearTimeout: 'readonly', - clearInterval: 'readonly', - // Browser - window: 'readonly', - document: 'readonly', - localStorage: 'readonly', - alert: 'readonly', - Event: 'readonly', - CustomEvent: 'readonly', - HTMLElement: 'readonly', - MutationObserver: 'readonly', - // Electron - electronAPI: 'readonly', - // Libraries - marked: 'readonly', - DOMPurify: 'readonly', - hljs: 'readonly', - mermaid: 'readonly', - // Jest - jest: 'readonly', - describe: 'readonly', - test: 'readonly', - expect: 'readonly', - beforeEach: 'readonly', - afterEach: 'readonly', - beforeAll: 'readonly', - afterAll: 'readonly' - } - }, - rules: { - // Error prevention - 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], - 'no-undef': 'error', - 'no-console': 'off', // Allow console for Electron apps - - // Code quality - 'eqeqeq': ['warn', 'always'], - 'no-var': 'warn', - 'prefer-const': 'warn', - - // Style (handled by Prettier) - 'semi': 'off', - 'quotes': 'off', - 'indent': 'off', - - // Async handling - 'no-async-promise-executor': 'warn', - 'require-await': 'off', - - // Security - 'no-eval': 'error', - 'no-implied-eval': 'error', - 'no-new-func': 'error' - } - } -]; +/** + * ESLint Configuration for PanConverter + * Uses flat config format (ESLint 9+) + */ + +module.exports = [ + { + // Global ignores + ignores: [ + 'node_modules/**', + 'dist/**', + 'coverage/**', + '*.min.js' + ] + }, + { + // JavaScript files + files: ['**/*.js'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + // Node.js + require: 'readonly', + module: 'readonly', + exports: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + process: 'readonly', + Buffer: 'readonly', + console: 'readonly', + setTimeout: 'readonly', + setInterval: 'readonly', + clearTimeout: 'readonly', + clearInterval: 'readonly', + // Browser + window: 'readonly', + document: 'readonly', + localStorage: 'readonly', + alert: 'readonly', + Event: 'readonly', + CustomEvent: 'readonly', + HTMLElement: 'readonly', + MutationObserver: 'readonly', + // Electron + electronAPI: 'readonly', + // Libraries + marked: 'readonly', + DOMPurify: 'readonly', + hljs: 'readonly', + mermaid: 'readonly', + // Jest + jest: 'readonly', + describe: 'readonly', + test: 'readonly', + expect: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly', + beforeAll: 'readonly', + afterAll: 'readonly' + } + }, + rules: { + // Error prevention + 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + 'no-undef': 'error', + 'no-console': 'off', // Allow console for Electron apps + + // Code quality + 'eqeqeq': ['warn', 'always'], + 'no-var': 'warn', + 'prefer-const': 'warn', + + // Style (handled by Prettier) + 'semi': 'off', + 'quotes': 'off', + 'indent': 'off', + + // Async handling + 'no-async-promise-executor': 'warn', + 'require-await': 'off', + + // Security + 'no-eval': 'error', + 'no-implied-eval': 'error', + 'no-new-func': 'error' + } + } +]; diff --git a/jest.config.js b/jest.config.js index 2bf5a07..ad8413b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,59 +1,59 @@ -/** - * Jest Configuration for PanConverter - * @version 2.2.0 - */ - -module.exports = { - // Test environment - testEnvironment: 'jsdom', - - // Root directory - rootDir: '.', - - // Test file patterns - testMatch: [ - '**/tests/**/*.test.js', - '**/tests/**/*.spec.js' - ], - - // Coverage configuration - collectCoverageFrom: [ - 'src/**/*.js', - '!src/main.js', // Main process needs electron-mock - '!**/node_modules/**' - ], - - // Coverage thresholds (start low, increase over time) - coverageThreshold: { - global: { - branches: 10, - functions: 10, - lines: 10, - statements: 10 - } - }, - - // Transform settings (no transpilation needed for vanilla JS) - transform: {}, - - // Module paths - moduleDirectories: ['node_modules', 'src'], - - // Setup files - setupFilesAfterEnv: ['/tests/setup.js'], - - // Ignore patterns - testPathIgnorePatterns: [ - '/node_modules/', - '/dist/' - ], - - // Verbose output - verbose: true, - - // Clear mocks between tests - clearMocks: true, - - // Reset modules between tests - resetModules: true -}; +/** + * Jest Configuration for PanConverter + * @version 2.2.0 + */ + +module.exports = { + // Test environment + testEnvironment: 'jsdom', + + // Root directory + rootDir: '.', + + // Test file patterns + testMatch: [ + '**/tests/**/*.test.js', + '**/tests/**/*.spec.js' + ], + + // Coverage configuration + collectCoverageFrom: [ + 'src/**/*.js', + '!src/main.js', // Main process needs electron-mock + '!**/node_modules/**' + ], + + // Coverage thresholds (start low, increase over time) + coverageThreshold: { + global: { + branches: 10, + functions: 10, + lines: 10, + statements: 10 + } + }, + + // Transform settings (no transpilation needed for vanilla JS) + transform: {}, + + // Module paths + moduleDirectories: ['node_modules', 'src'], + + // Setup files + setupFilesAfterEnv: ['/tests/setup.js'], + + // Ignore patterns + testPathIgnorePatterns: [ + '/node_modules/', + '/dist/' + ], + + // Verbose output + verbose: true, + + // Clear mocks between tests + clearMocks: true, + + // Reset modules between tests + resetModules: true +}; diff --git a/package.json b/package.json index 976260c..853688f 100644 --- a/package.json +++ b/package.json @@ -1,182 +1,181 @@ -{ - "name": "markdown-converter", - "version": "3.0.0", - "description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting", - "main": "src/main.js", - "scripts": { - "start": "electron .", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "lint": "eslint src tests", - "lint:fix": "eslint src tests --fix", - "format": "prettier --write src tests", - "format:check": "prettier --check src tests", - "build": "electron-builder", - "build:win": "electron-builder --win", - "build:win-signed": "cross-env CSC_LINK=code-signing-cert.pfx CSC_KEY_PASSWORD=%CSC_KEY_PASSWORD% electron-builder --win", - "build:win-unsigned": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --win", - "build:mac": "electron-builder --mac", - "build:linux": "electron-builder --linux", - "dist": "electron-builder --publish=never", - "dist:all": "electron-builder -mwl", - "generate-icons": "node scripts/generate-icons.js" - }, - "keywords": [ - "markdown", - "pandoc", - "converter", - "editor", - "pdf", - "audio", - "video", - "image" - ], - "author": "ConcreteInfo ", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/amitwh/markdown-converter" - }, - "devDependencies": { - "@testing-library/dom": "^10.4.1", - "cross-env": "^10.0.0", - "electron": "^37.4.0", - "electron-builder": "^26.0.12", - "eslint": "^9.39.2", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.4", - "jest": "^30.2.0", - "jest-environment-jsdom": "^30.2.0", - "prettier": "^3.7.4", - "sharp": "^0.34.3" - }, - "dependencies": { - "codemirror": "^6.0.2", - "core-util-is": "^1.0.3", - "docx": "^9.5.1", - "docx4js": "^3.3.0", - "dompurify": "^3.2.6", - "electron-store": "^10.1.0", - "highlight.js": "^11.11.1", - "html2pdf.js": "^0.10.1", - "marked": "^16.2.1", - "pdf-lib": "^1.17.1", - "pdfjs-dist": "^3.11.174", - "pdfkit": "^0.14.0", - "pizzip": "^3.2.0", - "tslib": "^2.8.1", - "xlsx": "^0.18.5" - }, - "build": { - "appId": "com.concreteinfo.markdownconverter", - "productName": "MarkdownConverter", - "directories": { - "output": "dist" - }, - "icon": "assets/icon", - "files": [ - "src/**/*", - "assets/**/*", - "scripts/**/*", - "node_modules/**/*", - "package.json" - ], - "fileAssociations": [ - { - "ext": "md", - "name": "Markdown Document", - "description": "Markdown Document", - "mimeType": "text/markdown", - "role": "Editor" - }, - { - "ext": "markdown", - "name": "Markdown Document", - "description": "Markdown Document", - "mimeType": "text/markdown", - "role": "Editor" - }, - { - "ext": "pdf", - "name": "PDF Document", - "description": "PDF Document", - "mimeType": "application/pdf", - "role": "Editor" - } - ], - "mac": { - "category": "public.app-category.productivity", - "identity": null - }, - "win": { - "target": [ - { - "target": "nsis", - "arch": [ - "x64" - ] - }, - { - "target": "portable", - "arch": [ - "x64" - ] - }, - { - "target": "zip", - "arch": [ - "x64" - ] - } - ], - "artifactName": "${productName}-${version}-${arch}.${ext}", - "requestedExecutionLevel": "asInvoker", - "signAndEditExecutable": false - }, - "nsis": { - "oneClick": false, - "perMachine": false, - "allowToChangeInstallationDirectory": true, - "displayLanguageSelector": true, - "createDesktopShortcut": true, - "createStartMenuShortcut": true, - "shortcutName": "MarkdownConverter", - "runAfterFinish": true, - "menuCategory": "Productivity", - "license": "LICENSE", - "warningsAsErrors": false, - "artifactName": "${productName}-Setup-${version}.${ext}", - "deleteAppDataOnUninstall": false, - "differentialPackage": true - }, - "linux": { - "target": [ - "deb", - "AppImage", - "snap", - "rpm" - ], - "category": "Utility", - "maintainer": "ConcreteInfo " - }, - "deb": { - "depends": [ - "pandoc", - "ffmpeg", - "imagemagick", - "libreoffice-common" - ], - "description": "Professional Markdown editor and universal file converter", - "maintainer": "ConcreteInfo " - }, - "rpm": { - "depends": [ - "pandoc", - "ffmpeg", - "ImageMagick", - "libreoffice-core" - ] - } - } -} +{ + "name": "markdown-converter", + "version": "3.0.0", + "description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting", + "main": "src/main.js", + "scripts": { + "start": "electron .", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint src tests", + "lint:fix": "eslint src tests --fix", + "format": "prettier --write src tests", + "format:check": "prettier --check src tests", + "build": "electron-builder", + "build:win": "electron-builder --win", + "build:win-signed": "cross-env CSC_LINK=code-signing-cert.pfx CSC_KEY_PASSWORD=%CSC_KEY_PASSWORD% electron-builder --win", + "build:win-unsigned": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --win", + "build:mac": "electron-builder --mac", + "build:linux": "electron-builder --linux", + "dist": "electron-builder --publish=never", + "dist:all": "electron-builder -mwl", + "generate-icons": "node scripts/generate-icons.js" + }, + "keywords": [ + "markdown", + "pandoc", + "converter", + "editor", + "pdf", + "audio", + "video", + "image" + ], + "author": "ConcreteInfo ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/amitwh/markdown-converter" + }, + "devDependencies": { + "@testing-library/dom": "^10.4.1", + "cross-env": "^10.0.0", + "electron": "^37.4.0", + "electron-builder": "^26.0.12", + "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "jest": "^30.2.0", + "jest-environment-jsdom": "^30.2.0", + "prettier": "^3.7.4", + "sharp": "^0.34.3" + }, + "dependencies": { + "codemirror": "^6.0.2", + "core-util-is": "^1.0.3", + "docx": "^9.5.1", + "docx4js": "^3.3.0", + "dompurify": "^3.2.6", + "electron-store": "^10.1.0", + "highlight.js": "^11.11.1", + "html2pdf.js": "^0.10.1", + "marked": "^16.2.1", + "pdf-lib": "^1.17.1", + "pdfjs-dist": "^3.11.174", + "pdfkit": "^0.14.0", + "pizzip": "^3.2.0", + "tslib": "^2.8.1", + "xlsx": "^0.18.5" + }, + "build": { + "appId": "com.concreteinfo.markdownconverter", + "productName": "MarkdownConverter", + "directories": { + "output": "dist" + }, + "icon": "assets/icon", + "files": [ + "src/**/*", + "assets/**/*", + "scripts/**/*", + "node_modules/**/*", + "package.json" + ], + "fileAssociations": [ + { + "ext": "md", + "name": "Markdown Document", + "description": "Markdown Document", + "mimeType": "text/markdown", + "role": "Editor" + }, + { + "ext": "markdown", + "name": "Markdown Document", + "description": "Markdown Document", + "mimeType": "text/markdown", + "role": "Editor" + }, + { + "ext": "pdf", + "name": "PDF Document", + "description": "PDF Document", + "mimeType": "application/pdf", + "role": "Editor" + } + ], + "mac": { + "category": "public.app-category.productivity", + "identity": null + }, + "win": { + "target": [ + { + "target": "nsis", + "arch": [ + "x64" + ] + }, + { + "target": "portable", + "arch": [ + "x64" + ] + }, + { + "target": "zip", + "arch": [ + "x64" + ] + } + ], + "artifactName": "${productName}-${version}-${arch}.${ext}", + "requestedExecutionLevel": "asInvoker", + "signAndEditExecutable": false + }, + "nsis": { + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true, + "displayLanguageSelector": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "shortcutName": "MarkdownConverter", + "runAfterFinish": true, + "menuCategory": "Productivity", + "license": "LICENSE", + "warningsAsErrors": false, + "artifactName": "${productName}-Setup-${version}.${ext}", + "deleteAppDataOnUninstall": false, + "differentialPackage": true + }, + "linux": { + "target": [ + "deb", + "AppImage", + "snap" + ], + "category": "Utility", + "maintainer": "ConcreteInfo " + }, + "deb": { + "depends": [ + "pandoc", + "ffmpeg", + "imagemagick", + "libreoffice-common" + ], + "description": "Professional Markdown editor and universal file converter", + "maintainer": "ConcreteInfo " + }, + "rpm": { + "depends": [ + "pandoc", + "ffmpeg", + "ImageMagick", + "libreoffice-core" + ] + } + } +} diff --git a/push-to-github.sh b/push-to-github.sh index db22e4f..288de2e 100755 --- a/push-to-github.sh +++ b/push-to-github.sh @@ -1,83 +1,83 @@ -#!/bin/bash - -echo "==========================================" -echo "Pan Converter - GitHub Push Helper" -echo "==========================================" -echo "" -echo "Before running this script, please ensure:" -echo "1. You have created a repository named 'pan-converter' on GitHub" -echo "2. The repository is empty (no README, .gitignore, or license)" -echo "3. You have set up authentication (SSH key or Personal Access Token)" -echo "" -read -p "Have you completed the above steps? (y/n): " -n 1 -r -echo "" - -if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "Please complete the setup first:" - echo "1. Go to https://github.com/new" - echo "2. Create a new repository named 'pan-converter'" - echo "3. DO NOT initialize with README, .gitignore, or license" - echo "4. Set up SSH key or Personal Access Token for authentication" - exit 1 -fi - -echo "" -echo "Choose authentication method:" -echo "1. SSH (Recommended if you have SSH keys set up)" -echo "2. HTTPS (Requires Personal Access Token)" -read -p "Enter your choice (1 or 2): " AUTH_CHOICE - -if [ "$AUTH_CHOICE" = "1" ]; then - echo "Using SSH authentication..." - git remote set-url origin git@github.com:amitwh/pan-converter.git -elif [ "$AUTH_CHOICE" = "2" ]; then - echo "Using HTTPS authentication..." - echo "You'll need to enter your GitHub username and Personal Access Token" - git remote set-url origin https://github.com/amitwh/pan-converter.git -else - echo "Invalid choice. Exiting." - exit 1 -fi - -echo "" -echo "Pushing branches to GitHub..." -echo "==============================" - -# Push master branch with upstream tracking -echo "Pushing master branch..." -if git push -u origin master; then - echo "✓ Master branch pushed successfully" -else - echo "✗ Failed to push master branch" - echo "Please check your authentication and try again" - exit 1 -fi - -# Push other branches -echo "Pushing linux branch..." -if git push origin linux; then - echo "✓ Linux branch pushed successfully" -else - echo "✗ Failed to push linux branch" -fi - -echo "Pushing macos branch..." -if git push origin macos; then - echo "✓ macOS branch pushed successfully" -else - echo "✗ Failed to push macos branch" -fi - -echo "Pushing windows branch..." -if git push origin windows; then - echo "✓ Windows branch pushed successfully" -else - echo "✗ Failed to push windows branch" -fi - -echo "" -echo "==========================================" -echo "Push complete!" -echo "Your repository is available at:" -echo "https://github.com/amitwh/pan-converter" +#!/bin/bash + +echo "==========================================" +echo "Pan Converter - GitHub Push Helper" +echo "==========================================" +echo "" +echo "Before running this script, please ensure:" +echo "1. You have created a repository named 'pan-converter' on GitHub" +echo "2. The repository is empty (no README, .gitignore, or license)" +echo "3. You have set up authentication (SSH key or Personal Access Token)" +echo "" +read -p "Have you completed the above steps? (y/n): " -n 1 -r +echo "" + +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Please complete the setup first:" + echo "1. Go to https://github.com/new" + echo "2. Create a new repository named 'pan-converter'" + echo "3. DO NOT initialize with README, .gitignore, or license" + echo "4. Set up SSH key or Personal Access Token for authentication" + exit 1 +fi + +echo "" +echo "Choose authentication method:" +echo "1. SSH (Recommended if you have SSH keys set up)" +echo "2. HTTPS (Requires Personal Access Token)" +read -p "Enter your choice (1 or 2): " AUTH_CHOICE + +if [ "$AUTH_CHOICE" = "1" ]; then + echo "Using SSH authentication..." + git remote set-url origin git@github.com:amitwh/pan-converter.git +elif [ "$AUTH_CHOICE" = "2" ]; then + echo "Using HTTPS authentication..." + echo "You'll need to enter your GitHub username and Personal Access Token" + git remote set-url origin https://github.com/amitwh/pan-converter.git +else + echo "Invalid choice. Exiting." + exit 1 +fi + +echo "" +echo "Pushing branches to GitHub..." +echo "==============================" + +# Push master branch with upstream tracking +echo "Pushing master branch..." +if git push -u origin master; then + echo "✓ Master branch pushed successfully" +else + echo "✗ Failed to push master branch" + echo "Please check your authentication and try again" + exit 1 +fi + +# Push other branches +echo "Pushing linux branch..." +if git push origin linux; then + echo "✓ Linux branch pushed successfully" +else + echo "✗ Failed to push linux branch" +fi + +echo "Pushing macos branch..." +if git push origin macos; then + echo "✓ macOS branch pushed successfully" +else + echo "✗ Failed to push macos branch" +fi + +echo "Pushing windows branch..." +if git push origin windows; then + echo "✓ Windows branch pushed successfully" +else + echo "✗ Failed to push windows branch" +fi + +echo "" +echo "==========================================" +echo "Push complete!" +echo "Your repository is available at:" +echo "https://github.com/amitwh/pan-converter" echo "==========================================" \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md index b0c8c1c..4fbf357 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,116 +1,116 @@ -# PanConverter Windows Explorer Context Menu Integration - -This directory contains scripts to add PanConverter to the Windows Explorer context menu, allowing you to right-click on files and convert them directly without opening the full application. - -## Features - -### Context Menu Options -- **Convert with PanConverter**: Shows a format selection dialog for any supported file -- **PanConverter > Convert to...**: Direct conversion options (for Markdown files) - - PDF - - HTML - - DOCX - - LaTeX - - PowerPoint - -### Supported File Types -- **Markdown**: `.md`, `.markdown` -- **HTML**: `.html`, `.htm` -- **Documents**: `.docx`, `.odt`, `.rtf` -- **LaTeX**: `.tex` -- **PDF**: `.pdf` -- **Presentations**: `.pptx`, `.ppt`, `.odp` - -## Installation Methods - -### Method 1: Automatic (During PanConverter Installation) -When installing PanConverter using the Windows installer, you'll be prompted to install context menu integration automatically. - -### Method 2: Manual Installation - -#### Using PowerShell (Recommended) -1. Right-click on `install-context-menu.ps1` -2. Select "Run with PowerShell" -3. Follow the prompts (will request administrator privileges) - -#### Using Batch File -1. Right-click on `install-context-menu.bat` -2. Select "Run as administrator" -3. Follow the prompts - -#### Using Registry File Directly -1. Right-click on `install-context-menu.reg` -2. Select "Merge" -3. Confirm the registry modification - -## Uninstallation - -### Using PowerShell -```powershell -.\install-context-menu.ps1 -Uninstall -``` - -### Using Batch File -Run `uninstall-context-menu.bat` as administrator - -### Using Registry File -Run `uninstall-context-menu.reg` - -## How It Works - -### Command Line Interface -The context menu integration works by passing command line arguments to PanConverter: - -- `--convert `: Shows conversion dialog for the specified file -- `--convert-to `: Directly converts to the specified format - -### Examples -```batch -# Show conversion dialog -PanConverter.exe --convert "document.md" - -# Direct conversion to PDF -PanConverter.exe --convert-to pdf "document.md" -``` - -### Conversion Process -1. PanConverter reads the input file -2. Creates a temporary file if needed -3. Uses Pandoc to convert to the target format -4. Saves the output in the same directory as the input file -5. Shows a Windows notification when complete - -## Requirements - -- PanConverter must be installed in the default location: `%LOCALAPPDATA%\Programs\PanConverter\` -- Pandoc must be installed and accessible from the command line -- Administrator privileges required for registry modifications - -## Troubleshooting - -### Context Menu Not Appearing -1. Ensure you ran the installation script as administrator -2. Try logging out and back in, or restart Windows Explorer -3. Check if PanConverter is installed in the expected location - -### Conversion Failures -1. Verify Pandoc is installed: `pandoc --version` -2. Check if the input file is not corrupted or locked -3. Ensure you have write permissions in the output directory - -### Permission Errors -- The installation scripts require administrator privileges to modify the registry -- If you get permission errors, right-click and "Run as administrator" - -## File Descriptions - -- `install-context-menu.reg`: Registry entries for context menu -- `uninstall-context-menu.reg`: Registry entries removal -- `install-context-menu.ps1`: PowerShell installation script -- `install-context-menu.bat`: Batch installation script -- `uninstall-context-menu.bat`: Batch uninstallation script -- `nsis-installer.nsh`: NSIS installer integration script - -## Security Note - +# PanConverter Windows Explorer Context Menu Integration + +This directory contains scripts to add PanConverter to the Windows Explorer context menu, allowing you to right-click on files and convert them directly without opening the full application. + +## Features + +### Context Menu Options +- **Convert with PanConverter**: Shows a format selection dialog for any supported file +- **PanConverter > Convert to...**: Direct conversion options (for Markdown files) + - PDF + - HTML + - DOCX + - LaTeX + - PowerPoint + +### Supported File Types +- **Markdown**: `.md`, `.markdown` +- **HTML**: `.html`, `.htm` +- **Documents**: `.docx`, `.odt`, `.rtf` +- **LaTeX**: `.tex` +- **PDF**: `.pdf` +- **Presentations**: `.pptx`, `.ppt`, `.odp` + +## Installation Methods + +### Method 1: Automatic (During PanConverter Installation) +When installing PanConverter using the Windows installer, you'll be prompted to install context menu integration automatically. + +### Method 2: Manual Installation + +#### Using PowerShell (Recommended) +1. Right-click on `install-context-menu.ps1` +2. Select "Run with PowerShell" +3. Follow the prompts (will request administrator privileges) + +#### Using Batch File +1. Right-click on `install-context-menu.bat` +2. Select "Run as administrator" +3. Follow the prompts + +#### Using Registry File Directly +1. Right-click on `install-context-menu.reg` +2. Select "Merge" +3. Confirm the registry modification + +## Uninstallation + +### Using PowerShell +```powershell +.\install-context-menu.ps1 -Uninstall +``` + +### Using Batch File +Run `uninstall-context-menu.bat` as administrator + +### Using Registry File +Run `uninstall-context-menu.reg` + +## How It Works + +### Command Line Interface +The context menu integration works by passing command line arguments to PanConverter: + +- `--convert `: Shows conversion dialog for the specified file +- `--convert-to `: Directly converts to the specified format + +### Examples +```batch +# Show conversion dialog +PanConverter.exe --convert "document.md" + +# Direct conversion to PDF +PanConverter.exe --convert-to pdf "document.md" +``` + +### Conversion Process +1. PanConverter reads the input file +2. Creates a temporary file if needed +3. Uses Pandoc to convert to the target format +4. Saves the output in the same directory as the input file +5. Shows a Windows notification when complete + +## Requirements + +- PanConverter must be installed in the default location: `%LOCALAPPDATA%\Programs\PanConverter\` +- Pandoc must be installed and accessible from the command line +- Administrator privileges required for registry modifications + +## Troubleshooting + +### Context Menu Not Appearing +1. Ensure you ran the installation script as administrator +2. Try logging out and back in, or restart Windows Explorer +3. Check if PanConverter is installed in the expected location + +### Conversion Failures +1. Verify Pandoc is installed: `pandoc --version` +2. Check if the input file is not corrupted or locked +3. Ensure you have write permissions in the output directory + +### Permission Errors +- The installation scripts require administrator privileges to modify the registry +- If you get permission errors, right-click and "Run as administrator" + +## File Descriptions + +- `install-context-menu.reg`: Registry entries for context menu +- `uninstall-context-menu.reg`: Registry entries removal +- `install-context-menu.ps1`: PowerShell installation script +- `install-context-menu.bat`: Batch installation script +- `uninstall-context-menu.bat`: Batch uninstallation script +- `nsis-installer.nsh`: NSIS installer integration script + +## Security Note + The scripts modify the Windows registry to add context menu entries. Only run these scripts if you trust the source and understand the changes being made to your system. \ No newline at end of file diff --git a/scripts/generate-icons.js b/scripts/generate-icons.js index 2196f47..c8aa50e 100644 --- a/scripts/generate-icons.js +++ b/scripts/generate-icons.js @@ -1,82 +1,82 @@ -/** - * Icon Generator for MarkdownConverter - * Generates all required icon sizes from NewIcon.jpg - */ - -const sharp = require('sharp'); -const fs = require('fs'); -const path = require('path'); - -// Source image -const sourceImage = path.join(__dirname, '..', 'assets', 'docico1.png'); -const assetsDir = path.join(__dirname, '..', 'assets'); - -// Icon sizes needed for different platforms -const iconSizes = [16, 24, 32, 48, 64, 128, 256, 512, 1024]; - -async function generateIcons() { - console.log('Generating icons from docico1.png...'); - - // Check if source exists - if (!fs.existsSync(sourceImage)) { - console.error('Source image not found:', sourceImage); - process.exit(1); - } - - try { - // Generate main icon.png (512x512) - await sharp(sourceImage) - .resize(512, 512, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } }) - .png() - .toFile(path.join(assetsDir, 'icon.png')); - console.log('Generated icon.png (512x512)'); - - // Generate icon@2x.png (1024x1024) - await sharp(sourceImage) - .resize(1024, 1024, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } }) - .png() - .toFile(path.join(assetsDir, 'icon@2x.png')); - console.log('Generated icon@2x.png (1024x1024)'); - - // Create icons directory for multi-size icons - const iconsDir = path.join(assetsDir, 'icons'); - if (!fs.existsSync(iconsDir)) { - fs.mkdirSync(iconsDir, { recursive: true }); - } - - // Generate all sizes - for (const size of iconSizes) { - await sharp(sourceImage) - .resize(size, size, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } }) - .png() - .toFile(path.join(iconsDir, `${size}x${size}.png`)); - console.log(`Generated icons/${size}x${size}.png`); - } - - // Generate favicon - await sharp(sourceImage) - .resize(32, 32, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } }) - .png() - .toFile(path.join(assetsDir, 'favicon.png')); - console.log('Generated favicon.png (32x32)'); - - // Generate tray icon (smaller, for system tray) - await sharp(sourceImage) - .resize(24, 24, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } }) - .png() - .toFile(path.join(assetsDir, 'tray-icon.png')); - console.log('Generated tray-icon.png (24x24)'); - - console.log('\nIcon generation complete!'); - console.log('\nFor Windows .ico file, use an online converter or:'); - console.log(' magick convert assets/icons/*.png assets/icon.ico'); - console.log('\nFor macOS .icns file, use:'); - console.log(' iconutil -c icns assets/icon.iconset -o assets/icon.icns'); - - } catch (error) { - console.error('Error generating icons:', error); - process.exit(1); - } -} - -generateIcons(); +/** + * Icon Generator for MarkdownConverter + * Generates all required icon sizes from NewIcon.jpg + */ + +const sharp = require('sharp'); +const fs = require('fs'); +const path = require('path'); + +// Source image +const sourceImage = path.join(__dirname, '..', 'assets', 'docico1.png'); +const assetsDir = path.join(__dirname, '..', 'assets'); + +// Icon sizes needed for different platforms +const iconSizes = [16, 24, 32, 48, 64, 128, 256, 512, 1024]; + +async function generateIcons() { + console.log('Generating icons from docico1.png...'); + + // Check if source exists + if (!fs.existsSync(sourceImage)) { + console.error('Source image not found:', sourceImage); + process.exit(1); + } + + try { + // Generate main icon.png (512x512) + await sharp(sourceImage) + .resize(512, 512, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } }) + .png() + .toFile(path.join(assetsDir, 'icon.png')); + console.log('Generated icon.png (512x512)'); + + // Generate icon@2x.png (1024x1024) + await sharp(sourceImage) + .resize(1024, 1024, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } }) + .png() + .toFile(path.join(assetsDir, 'icon@2x.png')); + console.log('Generated icon@2x.png (1024x1024)'); + + // Create icons directory for multi-size icons + const iconsDir = path.join(assetsDir, 'icons'); + if (!fs.existsSync(iconsDir)) { + fs.mkdirSync(iconsDir, { recursive: true }); + } + + // Generate all sizes + for (const size of iconSizes) { + await sharp(sourceImage) + .resize(size, size, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } }) + .png() + .toFile(path.join(iconsDir, `${size}x${size}.png`)); + console.log(`Generated icons/${size}x${size}.png`); + } + + // Generate favicon + await sharp(sourceImage) + .resize(32, 32, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } }) + .png() + .toFile(path.join(assetsDir, 'favicon.png')); + console.log('Generated favicon.png (32x32)'); + + // Generate tray icon (smaller, for system tray) + await sharp(sourceImage) + .resize(24, 24, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } }) + .png() + .toFile(path.join(assetsDir, 'tray-icon.png')); + console.log('Generated tray-icon.png (24x24)'); + + console.log('\nIcon generation complete!'); + console.log('\nFor Windows .ico file, use an online converter or:'); + console.log(' magick convert assets/icons/*.png assets/icon.ico'); + console.log('\nFor macOS .icns file, use:'); + console.log(' iconutil -c icns assets/icon.iconset -o assets/icon.icns'); + + } catch (error) { + console.error('Error generating icons:', error); + process.exit(1); + } +} + +generateIcons(); diff --git a/scripts/install-context-menu.bat b/scripts/install-context-menu.bat index 3526c34..b767935 100644 --- a/scripts/install-context-menu.bat +++ b/scripts/install-context-menu.bat @@ -1,48 +1,48 @@ -@echo off -echo PanConverter Context Menu Installation -echo ===================================== -echo. - -REM Check for administrator privileges -net session >nul 2>&1 -if %errorLevel% == 0 ( - echo Running with administrator privileges... -) else ( - echo This script requires administrator privileges. - echo Please right-click and select "Run as administrator" - echo. - pause - exit /b 1 -) - -REM Check if PanConverter is installed -if not exist "%LOCALAPPDATA%\Programs\PanConverter\PanConverter.exe" ( - echo Warning: PanConverter not found at the expected location. - echo Please ensure PanConverter is installed before continuing. - echo Expected location: %LOCALAPPDATA%\Programs\PanConverter\PanConverter.exe - echo. - set /p continue="Continue anyway? (y/N): " - if /i not "%continue%"=="y" exit /b 1 -) - -echo Installing context menu entries... -reg import "%~dp0install-context-menu.reg" - -if %errorLevel% == 0 ( - echo. - echo Context menu integration installed successfully! - echo. - echo You can now right-click on supported files and select: - echo • "Convert with PanConverter" - Shows conversion dialog - echo • "PanConverter > Convert to..." - Direct conversion ^(for Markdown^) - echo. - echo Supported file types: - echo .md .markdown .html .htm .docx .odt .rtf .tex .pdf .pptx .ppt .odp -) else ( - echo. - echo Failed to install context menu entries. - echo Please check that the registry file exists and try again. -) - -echo. +@echo off +echo PanConverter Context Menu Installation +echo ===================================== +echo. + +REM Check for administrator privileges +net session >nul 2>&1 +if %errorLevel% == 0 ( + echo Running with administrator privileges... +) else ( + echo This script requires administrator privileges. + echo Please right-click and select "Run as administrator" + echo. + pause + exit /b 1 +) + +REM Check if PanConverter is installed +if not exist "%LOCALAPPDATA%\Programs\PanConverter\PanConverter.exe" ( + echo Warning: PanConverter not found at the expected location. + echo Please ensure PanConverter is installed before continuing. + echo Expected location: %LOCALAPPDATA%\Programs\PanConverter\PanConverter.exe + echo. + set /p continue="Continue anyway? (y/N): " + if /i not "%continue%"=="y" exit /b 1 +) + +echo Installing context menu entries... +reg import "%~dp0install-context-menu.reg" + +if %errorLevel% == 0 ( + echo. + echo Context menu integration installed successfully! + echo. + echo You can now right-click on supported files and select: + echo • "Convert with PanConverter" - Shows conversion dialog + echo • "PanConverter > Convert to..." - Direct conversion ^(for Markdown^) + echo. + echo Supported file types: + echo .md .markdown .html .htm .docx .odt .rtf .tex .pdf .pptx .ppt .odp +) else ( + echo. + echo Failed to install context menu entries. + echo Please check that the registry file exists and try again. +) + +echo. pause \ No newline at end of file diff --git a/scripts/install-context-menu.ps1 b/scripts/install-context-menu.ps1 index c067f3a..4a0a450 100644 --- a/scripts/install-context-menu.ps1 +++ b/scripts/install-context-menu.ps1 @@ -1,67 +1,67 @@ -# PanConverter Context Menu Installation Script -# This script installs PanConverter context menu integration for Windows Explorer - -param( - [switch]$Uninstall = $false -) - -# Check if running as administrator -if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { - Write-Host "This script requires Administrator privileges. Restarting with elevated permissions..." -ForegroundColor Yellow - Start-Process PowerShell -Verb RunAs "-File `"$PSCommandPath`" $(if ($Uninstall) { '-Uninstall' })" - exit -} - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$appPath = "${env:LOCALAPPDATA}\Programs\PanConverter\PanConverter.exe" - -if ($Uninstall) { - Write-Host "Uninstalling PanConverter context menu integration..." -ForegroundColor Yellow - $regFile = Join-Path $scriptDir "uninstall-context-menu.reg" - - if (Test-Path $regFile) { - reg import $regFile - if ($LASTEXITCODE -eq 0) { - Write-Host "PanConverter context menu has been successfully removed!" -ForegroundColor Green - } else { - Write-Host "Failed to remove context menu entries." -ForegroundColor Red - } - } else { - Write-Host "Uninstall registry file not found: $regFile" -ForegroundColor Red - } -} else { - Write-Host "Installing PanConverter context menu integration..." -ForegroundColor Yellow - - # Check if PanConverter is installed - if (-not (Test-Path $appPath)) { - Write-Host "Warning: PanConverter executable not found at: $appPath" -ForegroundColor Yellow - Write-Host "Please make sure PanConverter is installed before running this script." -ForegroundColor Yellow - $continue = Read-Host "Continue anyway? (y/N)" - if ($continue -ne 'y' -and $continue -ne 'Y') { - exit - } - } - - $regFile = Join-Path $scriptDir "install-context-menu.reg" - - if (Test-Path $regFile) { - reg import $regFile - if ($LASTEXITCODE -eq 0) { - Write-Host "PanConverter context menu has been successfully installed!" -ForegroundColor Green - Write-Host "" - Write-Host "You can now right-click on supported files (MD, HTML, DOCX, PDF, etc.) and select:" -ForegroundColor Cyan - Write-Host "• 'Convert with PanConverter' - Shows conversion dialog" -ForegroundColor Cyan - Write-Host "• 'PanConverter > Convert to...' - Direct conversion options (for Markdown files)" -ForegroundColor Cyan - Write-Host "" - Write-Host "Supported file types: .md, .markdown, .html, .htm, .docx, .odt, .rtf, .tex, .pdf, .pptx, .ppt, .odp" -ForegroundColor Gray - } else { - Write-Host "Failed to install context menu entries." -ForegroundColor Red - } - } else { - Write-Host "Install registry file not found: $regFile" -ForegroundColor Red - } -} - -Write-Host "" -Write-Host "Press any key to exit..." +# PanConverter Context Menu Installation Script +# This script installs PanConverter context menu integration for Windows Explorer + +param( + [switch]$Uninstall = $false +) + +# Check if running as administrator +if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { + Write-Host "This script requires Administrator privileges. Restarting with elevated permissions..." -ForegroundColor Yellow + Start-Process PowerShell -Verb RunAs "-File `"$PSCommandPath`" $(if ($Uninstall) { '-Uninstall' })" + exit +} + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$appPath = "${env:LOCALAPPDATA}\Programs\PanConverter\PanConverter.exe" + +if ($Uninstall) { + Write-Host "Uninstalling PanConverter context menu integration..." -ForegroundColor Yellow + $regFile = Join-Path $scriptDir "uninstall-context-menu.reg" + + if (Test-Path $regFile) { + reg import $regFile + if ($LASTEXITCODE -eq 0) { + Write-Host "PanConverter context menu has been successfully removed!" -ForegroundColor Green + } else { + Write-Host "Failed to remove context menu entries." -ForegroundColor Red + } + } else { + Write-Host "Uninstall registry file not found: $regFile" -ForegroundColor Red + } +} else { + Write-Host "Installing PanConverter context menu integration..." -ForegroundColor Yellow + + # Check if PanConverter is installed + if (-not (Test-Path $appPath)) { + Write-Host "Warning: PanConverter executable not found at: $appPath" -ForegroundColor Yellow + Write-Host "Please make sure PanConverter is installed before running this script." -ForegroundColor Yellow + $continue = Read-Host "Continue anyway? (y/N)" + if ($continue -ne 'y' -and $continue -ne 'Y') { + exit + } + } + + $regFile = Join-Path $scriptDir "install-context-menu.reg" + + if (Test-Path $regFile) { + reg import $regFile + if ($LASTEXITCODE -eq 0) { + Write-Host "PanConverter context menu has been successfully installed!" -ForegroundColor Green + Write-Host "" + Write-Host "You can now right-click on supported files (MD, HTML, DOCX, PDF, etc.) and select:" -ForegroundColor Cyan + Write-Host "• 'Convert with PanConverter' - Shows conversion dialog" -ForegroundColor Cyan + Write-Host "• 'PanConverter > Convert to...' - Direct conversion options (for Markdown files)" -ForegroundColor Cyan + Write-Host "" + Write-Host "Supported file types: .md, .markdown, .html, .htm, .docx, .odt, .rtf, .tex, .pdf, .pptx, .ppt, .odp" -ForegroundColor Gray + } else { + Write-Host "Failed to install context menu entries." -ForegroundColor Red + } + } else { + Write-Host "Install registry file not found: $regFile" -ForegroundColor Red + } +} + +Write-Host "" +Write-Host "Press any key to exit..." $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") \ No newline at end of file diff --git a/scripts/install-context-menu.reg b/scripts/install-context-menu.reg index 6da6c84..04f8280 100644 --- a/scripts/install-context-menu.reg +++ b/scripts/install-context-menu.reg @@ -1,134 +1,134 @@ -Windows Registry Editor Version 5.00 - -; PanConverter Context Menu Integration -; This registry file adds context menu options for converting files using PanConverter - -; Add context menu for Markdown files (.md, .markdown) -[HKEY_CLASSES_ROOT\.md\shell\PanConverter] -@="Convert with PanConverter" -"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" - -[HKEY_CLASSES_ROOT\.md\shell\PanConverter\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" - -[HKEY_CLASSES_ROOT\.markdown\shell\PanConverter] -@="Convert with PanConverter" -"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" - -[HKEY_CLASSES_ROOT\.markdown\shell\PanConverter\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" - -; Add context menu for HTML files -[HKEY_CLASSES_ROOT\.html\shell\PanConverter] -@="Convert with PanConverter" -"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" - -[HKEY_CLASSES_ROOT\.html\shell\PanConverter\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" - -[HKEY_CLASSES_ROOT\.htm\shell\PanConverter] -@="Convert with PanConverter" -"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" - -[HKEY_CLASSES_ROOT\.htm\shell\PanConverter\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" - -; Add context menu for DOCX files -[HKEY_CLASSES_ROOT\.docx\shell\PanConverter] -@="Convert with PanConverter" -"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" - -[HKEY_CLASSES_ROOT\.docx\shell\PanConverter\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" - -; Add context menu for ODT files -[HKEY_CLASSES_ROOT\.odt\shell\PanConverter] -@="Convert with PanConverter" -"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" - -[HKEY_CLASSES_ROOT\.odt\shell\PanConverter\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" - -; Add context menu for RTF files -[HKEY_CLASSES_ROOT\.rtf\shell\PanConverter] -@="Convert with PanConverter" -"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" - -[HKEY_CLASSES_ROOT\.rtf\shell\PanConverter\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" - -; Add context menu for LaTeX files -[HKEY_CLASSES_ROOT\.tex\shell\PanConverter] -@="Convert with PanConverter" -"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" - -[HKEY_CLASSES_ROOT\.tex\shell\PanConverter\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" - -; Add context menu for PDF files -[HKEY_CLASSES_ROOT\.pdf\shell\PanConverter] -@="Convert with PanConverter" -"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" - -[HKEY_CLASSES_ROOT\.pdf\shell\PanConverter\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" - -; Add context menu for PowerPoint files -[HKEY_CLASSES_ROOT\.pptx\shell\PanConverter] -@="Convert with PanConverter" -"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" - -[HKEY_CLASSES_ROOT\.pptx\shell\PanConverter\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" - -[HKEY_CLASSES_ROOT\.ppt\shell\PanConverter] -@="Convert with PanConverter" -"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" - -[HKEY_CLASSES_ROOT\.ppt\shell\PanConverter\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" - -; Add context menu for OpenDocument Presentation files -[HKEY_CLASSES_ROOT\.odp\shell\PanConverter] -@="Convert with PanConverter" -"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" - -[HKEY_CLASSES_ROOT\.odp\shell\PanConverter\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" - -; Add submenu with specific conversion options for Markdown files -[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu] -@="PanConverter" -"MUIVerb"="Convert to..." -"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" -"SubCommands"="" - -[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\PDF] -@="PDF" - -[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\PDF\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert-to pdf \"%1\"" - -[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\HTML] -@="HTML" - -[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\HTML\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert-to html \"%1\"" - -[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\DOCX] -@="DOCX" - -[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\DOCX\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert-to docx \"%1\"" - -[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\LaTeX] -@="LaTeX" - -[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\LaTeX\command] -@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert-to latex \"%1\"" - -[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\PPTX] -@="PowerPoint" - -[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\PPTX\command] +Windows Registry Editor Version 5.00 + +; PanConverter Context Menu Integration +; This registry file adds context menu options for converting files using PanConverter + +; Add context menu for Markdown files (.md, .markdown) +[HKEY_CLASSES_ROOT\.md\shell\PanConverter] +@="Convert with PanConverter" +"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" + +[HKEY_CLASSES_ROOT\.md\shell\PanConverter\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" + +[HKEY_CLASSES_ROOT\.markdown\shell\PanConverter] +@="Convert with PanConverter" +"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" + +[HKEY_CLASSES_ROOT\.markdown\shell\PanConverter\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" + +; Add context menu for HTML files +[HKEY_CLASSES_ROOT\.html\shell\PanConverter] +@="Convert with PanConverter" +"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" + +[HKEY_CLASSES_ROOT\.html\shell\PanConverter\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" + +[HKEY_CLASSES_ROOT\.htm\shell\PanConverter] +@="Convert with PanConverter" +"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" + +[HKEY_CLASSES_ROOT\.htm\shell\PanConverter\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" + +; Add context menu for DOCX files +[HKEY_CLASSES_ROOT\.docx\shell\PanConverter] +@="Convert with PanConverter" +"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" + +[HKEY_CLASSES_ROOT\.docx\shell\PanConverter\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" + +; Add context menu for ODT files +[HKEY_CLASSES_ROOT\.odt\shell\PanConverter] +@="Convert with PanConverter" +"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" + +[HKEY_CLASSES_ROOT\.odt\shell\PanConverter\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" + +; Add context menu for RTF files +[HKEY_CLASSES_ROOT\.rtf\shell\PanConverter] +@="Convert with PanConverter" +"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" + +[HKEY_CLASSES_ROOT\.rtf\shell\PanConverter\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" + +; Add context menu for LaTeX files +[HKEY_CLASSES_ROOT\.tex\shell\PanConverter] +@="Convert with PanConverter" +"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" + +[HKEY_CLASSES_ROOT\.tex\shell\PanConverter\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" + +; Add context menu for PDF files +[HKEY_CLASSES_ROOT\.pdf\shell\PanConverter] +@="Convert with PanConverter" +"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" + +[HKEY_CLASSES_ROOT\.pdf\shell\PanConverter\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" + +; Add context menu for PowerPoint files +[HKEY_CLASSES_ROOT\.pptx\shell\PanConverter] +@="Convert with PanConverter" +"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" + +[HKEY_CLASSES_ROOT\.pptx\shell\PanConverter\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" + +[HKEY_CLASSES_ROOT\.ppt\shell\PanConverter] +@="Convert with PanConverter" +"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" + +[HKEY_CLASSES_ROOT\.ppt\shell\PanConverter\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" + +; Add context menu for OpenDocument Presentation files +[HKEY_CLASSES_ROOT\.odp\shell\PanConverter] +@="Convert with PanConverter" +"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" + +[HKEY_CLASSES_ROOT\.odp\shell\PanConverter\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert \"%1\"" + +; Add submenu with specific conversion options for Markdown files +[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu] +@="PanConverter" +"MUIVerb"="Convert to..." +"Icon"="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\"" +"SubCommands"="" + +[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\PDF] +@="PDF" + +[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\PDF\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert-to pdf \"%1\"" + +[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\HTML] +@="HTML" + +[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\HTML\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert-to html \"%1\"" + +[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\DOCX] +@="DOCX" + +[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\DOCX\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert-to docx \"%1\"" + +[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\LaTeX] +@="LaTeX" + +[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\LaTeX\command] +@="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert-to latex \"%1\"" + +[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\PPTX] +@="PowerPoint" + +[HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu\shell\PPTX\command] @="\"%LOCALAPPDATA%\\Programs\\PanConverter\\PanConverter.exe\" --convert-to pptx \"%1\"" \ No newline at end of file diff --git a/scripts/nsis-installer.nsh b/scripts/nsis-installer.nsh index ea2769e..a730c4c 100644 --- a/scripts/nsis-installer.nsh +++ b/scripts/nsis-installer.nsh @@ -1,145 +1,145 @@ -; PanConverter NSIS Installer Include File -; Handles context menu installation and uninstallation - -!include "LogicLib.nsh" -!include "MUI2.nsh" - -; Custom installation page for context menu option -Var ContextMenuCheckbox -Var ContextMenuState - -Function ContextMenuPage - !insertmacro MUI_HEADER_TEXT "Additional Options" "Choose additional installation options" - - nsDialogs::Create 1018 - Pop $0 - - ${If} $0 == error - Abort - ${EndIf} - - ${NSD_CreateLabel} 0 0 100% 20u "Select additional features to install:" - Pop $0 - - ${NSD_CreateCheckbox} 20u 30u 280u 15u "Add PanConverter to Windows Explorer context menu" - Pop $ContextMenuCheckbox - ${NSD_SetState} $ContextMenuCheckbox ${BST_CHECKED} - - ${NSD_CreateLabel} 20u 50u 280u 30u "This will allow you to right-click on supported files and convert them directly using PanConverter." - Pop $0 - - nsDialogs::Show -FunctionEnd - -Function ContextMenuPageLeave - ${NSD_GetState} $ContextMenuCheckbox $ContextMenuState -FunctionEnd - -; Install context menu entries -Function InstallContextMenu - ${If} $ContextMenuState == ${BST_CHECKED} - DetailPrint "Installing context menu integration..." - - ; Create registry entries for context menu - WriteRegStr HKCR ".md\shell\PanConverter" "" "Convert with PanConverter" - WriteRegStr HKCR ".md\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" - WriteRegStr HKCR ".md\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' - - WriteRegStr HKCR ".markdown\shell\PanConverter" "" "Convert with PanConverter" - WriteRegStr HKCR ".markdown\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" - WriteRegStr HKCR ".markdown\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' - - ; Context menu for HTML files - WriteRegStr HKCR ".html\shell\PanConverter" "" "Convert with PanConverter" - WriteRegStr HKCR ".html\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" - WriteRegStr HKCR ".html\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' - - WriteRegStr HKCR ".htm\shell\PanConverter" "" "Convert with PanConverter" - WriteRegStr HKCR ".htm\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" - WriteRegStr HKCR ".htm\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' - - ; Context menu for DOCX files - WriteRegStr HKCR ".docx\shell\PanConverter" "" "Convert with PanConverter" - WriteRegStr HKCR ".docx\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" - WriteRegStr HKCR ".docx\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' - - ; Context menu for ODT files - WriteRegStr HKCR ".odt\shell\PanConverter" "" "Convert with PanConverter" - WriteRegStr HKCR ".odt\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" - WriteRegStr HKCR ".odt\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' - - ; Context menu for RTF files - WriteRegStr HKCR ".rtf\shell\PanConverter" "" "Convert with PanConverter" - WriteRegStr HKCR ".rtf\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" - WriteRegStr HKCR ".rtf\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' - - ; Context menu for LaTeX files - WriteRegStr HKCR ".tex\shell\PanConverter" "" "Convert with PanConverter" - WriteRegStr HKCR ".tex\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" - WriteRegStr HKCR ".tex\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' - - ; Context menu for PDF files - WriteRegStr HKCR ".pdf\shell\PanConverter" "" "Convert with PanConverter" - WriteRegStr HKCR ".pdf\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" - WriteRegStr HKCR ".pdf\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' - - ; Context menu for PowerPoint files - WriteRegStr HKCR ".pptx\shell\PanConverter" "" "Convert with PanConverter" - WriteRegStr HKCR ".pptx\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" - WriteRegStr HKCR ".pptx\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' - - WriteRegStr HKCR ".ppt\shell\PanConverter" "" "Convert with PanConverter" - WriteRegStr HKCR ".ppt\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" - WriteRegStr HKCR ".ppt\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' - - ; Context menu for ODP files - WriteRegStr HKCR ".odp\shell\PanConverter" "" "Convert with PanConverter" - WriteRegStr HKCR ".odp\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" - WriteRegStr HKCR ".odp\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' - - ; Submenu for Markdown files with direct conversion options - WriteRegStr HKCR ".md\shell\PanConverterMenu" "" "PanConverter" - WriteRegStr HKCR ".md\shell\PanConverterMenu" "MUIVerb" "Convert to..." - WriteRegStr HKCR ".md\shell\PanConverterMenu" "Icon" "$INSTDIR\PanConverter.exe" - WriteRegStr HKCR ".md\shell\PanConverterMenu" "SubCommands" "" - - WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\PDF" "" "PDF" - WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\PDF\command" "" '"$INSTDIR\PanConverter.exe" --convert-to pdf "%1"' - - WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\HTML" "" "HTML" - WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\HTML\command" "" '"$INSTDIR\PanConverter.exe" --convert-to html "%1"' - - WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\DOCX" "" "DOCX" - WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\DOCX\command" "" '"$INSTDIR\PanConverter.exe" --convert-to docx "%1"' - - WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\LaTeX" "" "LaTeX" - WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\LaTeX\command" "" '"$INSTDIR\PanConverter.exe" --convert-to latex "%1"' - - WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\PPTX" "" "PowerPoint" - WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\PPTX\command" "" '"$INSTDIR\PanConverter.exe" --convert-to pptx "%1"' - - DetailPrint "Context menu integration installed successfully!" - ${EndIf} -FunctionEnd - -; Uninstall context menu entries -Function un.RemoveContextMenu - DetailPrint "Removing context menu integration..." - - ; Remove context menu entries for all file types - DeleteRegKey HKCR ".md\shell\PanConverter" - DeleteRegKey HKCR ".md\shell\PanConverterMenu" - DeleteRegKey HKCR ".markdown\shell\PanConverter" - DeleteRegKey HKCR ".html\shell\PanConverter" - DeleteRegKey HKCR ".htm\shell\PanConverter" - DeleteRegKey HKCR ".docx\shell\PanConverter" - DeleteRegKey HKCR ".odt\shell\PanConverter" - DeleteRegKey HKCR ".rtf\shell\PanConverter" - DeleteRegKey HKCR ".tex\shell\PanConverter" - DeleteRegKey HKCR ".pdf\shell\PanConverter" - DeleteRegKey HKCR ".pptx\shell\PanConverter" - DeleteRegKey HKCR ".ppt\shell\PanConverter" - DeleteRegKey HKCR ".odp\shell\PanConverter" - - DetailPrint "Context menu integration removed successfully!" +; PanConverter NSIS Installer Include File +; Handles context menu installation and uninstallation + +!include "LogicLib.nsh" +!include "MUI2.nsh" + +; Custom installation page for context menu option +Var ContextMenuCheckbox +Var ContextMenuState + +Function ContextMenuPage + !insertmacro MUI_HEADER_TEXT "Additional Options" "Choose additional installation options" + + nsDialogs::Create 1018 + Pop $0 + + ${If} $0 == error + Abort + ${EndIf} + + ${NSD_CreateLabel} 0 0 100% 20u "Select additional features to install:" + Pop $0 + + ${NSD_CreateCheckbox} 20u 30u 280u 15u "Add PanConverter to Windows Explorer context menu" + Pop $ContextMenuCheckbox + ${NSD_SetState} $ContextMenuCheckbox ${BST_CHECKED} + + ${NSD_CreateLabel} 20u 50u 280u 30u "This will allow you to right-click on supported files and convert them directly using PanConverter." + Pop $0 + + nsDialogs::Show +FunctionEnd + +Function ContextMenuPageLeave + ${NSD_GetState} $ContextMenuCheckbox $ContextMenuState +FunctionEnd + +; Install context menu entries +Function InstallContextMenu + ${If} $ContextMenuState == ${BST_CHECKED} + DetailPrint "Installing context menu integration..." + + ; Create registry entries for context menu + WriteRegStr HKCR ".md\shell\PanConverter" "" "Convert with PanConverter" + WriteRegStr HKCR ".md\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" + WriteRegStr HKCR ".md\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' + + WriteRegStr HKCR ".markdown\shell\PanConverter" "" "Convert with PanConverter" + WriteRegStr HKCR ".markdown\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" + WriteRegStr HKCR ".markdown\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' + + ; Context menu for HTML files + WriteRegStr HKCR ".html\shell\PanConverter" "" "Convert with PanConverter" + WriteRegStr HKCR ".html\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" + WriteRegStr HKCR ".html\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' + + WriteRegStr HKCR ".htm\shell\PanConverter" "" "Convert with PanConverter" + WriteRegStr HKCR ".htm\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" + WriteRegStr HKCR ".htm\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' + + ; Context menu for DOCX files + WriteRegStr HKCR ".docx\shell\PanConverter" "" "Convert with PanConverter" + WriteRegStr HKCR ".docx\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" + WriteRegStr HKCR ".docx\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' + + ; Context menu for ODT files + WriteRegStr HKCR ".odt\shell\PanConverter" "" "Convert with PanConverter" + WriteRegStr HKCR ".odt\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" + WriteRegStr HKCR ".odt\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' + + ; Context menu for RTF files + WriteRegStr HKCR ".rtf\shell\PanConverter" "" "Convert with PanConverter" + WriteRegStr HKCR ".rtf\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" + WriteRegStr HKCR ".rtf\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' + + ; Context menu for LaTeX files + WriteRegStr HKCR ".tex\shell\PanConverter" "" "Convert with PanConverter" + WriteRegStr HKCR ".tex\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" + WriteRegStr HKCR ".tex\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' + + ; Context menu for PDF files + WriteRegStr HKCR ".pdf\shell\PanConverter" "" "Convert with PanConverter" + WriteRegStr HKCR ".pdf\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" + WriteRegStr HKCR ".pdf\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' + + ; Context menu for PowerPoint files + WriteRegStr HKCR ".pptx\shell\PanConverter" "" "Convert with PanConverter" + WriteRegStr HKCR ".pptx\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" + WriteRegStr HKCR ".pptx\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' + + WriteRegStr HKCR ".ppt\shell\PanConverter" "" "Convert with PanConverter" + WriteRegStr HKCR ".ppt\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" + WriteRegStr HKCR ".ppt\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' + + ; Context menu for ODP files + WriteRegStr HKCR ".odp\shell\PanConverter" "" "Convert with PanConverter" + WriteRegStr HKCR ".odp\shell\PanConverter" "Icon" "$INSTDIR\PanConverter.exe" + WriteRegStr HKCR ".odp\shell\PanConverter\command" "" '"$INSTDIR\PanConverter.exe" --convert "%1"' + + ; Submenu for Markdown files with direct conversion options + WriteRegStr HKCR ".md\shell\PanConverterMenu" "" "PanConverter" + WriteRegStr HKCR ".md\shell\PanConverterMenu" "MUIVerb" "Convert to..." + WriteRegStr HKCR ".md\shell\PanConverterMenu" "Icon" "$INSTDIR\PanConverter.exe" + WriteRegStr HKCR ".md\shell\PanConverterMenu" "SubCommands" "" + + WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\PDF" "" "PDF" + WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\PDF\command" "" '"$INSTDIR\PanConverter.exe" --convert-to pdf "%1"' + + WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\HTML" "" "HTML" + WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\HTML\command" "" '"$INSTDIR\PanConverter.exe" --convert-to html "%1"' + + WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\DOCX" "" "DOCX" + WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\DOCX\command" "" '"$INSTDIR\PanConverter.exe" --convert-to docx "%1"' + + WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\LaTeX" "" "LaTeX" + WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\LaTeX\command" "" '"$INSTDIR\PanConverter.exe" --convert-to latex "%1"' + + WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\PPTX" "" "PowerPoint" + WriteRegStr HKCR ".md\shell\PanConverterMenu\shell\PPTX\command" "" '"$INSTDIR\PanConverter.exe" --convert-to pptx "%1"' + + DetailPrint "Context menu integration installed successfully!" + ${EndIf} +FunctionEnd + +; Uninstall context menu entries +Function un.RemoveContextMenu + DetailPrint "Removing context menu integration..." + + ; Remove context menu entries for all file types + DeleteRegKey HKCR ".md\shell\PanConverter" + DeleteRegKey HKCR ".md\shell\PanConverterMenu" + DeleteRegKey HKCR ".markdown\shell\PanConverter" + DeleteRegKey HKCR ".html\shell\PanConverter" + DeleteRegKey HKCR ".htm\shell\PanConverter" + DeleteRegKey HKCR ".docx\shell\PanConverter" + DeleteRegKey HKCR ".odt\shell\PanConverter" + DeleteRegKey HKCR ".rtf\shell\PanConverter" + DeleteRegKey HKCR ".tex\shell\PanConverter" + DeleteRegKey HKCR ".pdf\shell\PanConverter" + DeleteRegKey HKCR ".pptx\shell\PanConverter" + DeleteRegKey HKCR ".ppt\shell\PanConverter" + DeleteRegKey HKCR ".odp\shell\PanConverter" + + DetailPrint "Context menu integration removed successfully!" FunctionEnd \ No newline at end of file diff --git a/scripts/uninstall-context-menu.bat b/scripts/uninstall-context-menu.bat index 8db8e09..8ec3346 100644 --- a/scripts/uninstall-context-menu.bat +++ b/scripts/uninstall-context-menu.bat @@ -1,32 +1,32 @@ -@echo off -echo PanConverter Context Menu Uninstallation -echo ======================================== -echo. - -REM Check for administrator privileges -net session >nul 2>&1 -if %errorLevel% == 0 ( - echo Running with administrator privileges... -) else ( - echo This script requires administrator privileges. - echo Please right-click and select "Run as administrator" - echo. - pause - exit /b 1 -) - -echo Removing context menu entries... -reg import "%~dp0uninstall-context-menu.reg" - -if %errorLevel% == 0 ( - echo. - echo Context menu integration removed successfully! - echo PanConverter entries have been removed from the Windows Explorer context menu. -) else ( - echo. - echo Failed to remove context menu entries. - echo Please check that the registry file exists and try again. -) - -echo. +@echo off +echo PanConverter Context Menu Uninstallation +echo ======================================== +echo. + +REM Check for administrator privileges +net session >nul 2>&1 +if %errorLevel% == 0 ( + echo Running with administrator privileges... +) else ( + echo This script requires administrator privileges. + echo Please right-click and select "Run as administrator" + echo. + pause + exit /b 1 +) + +echo Removing context menu entries... +reg import "%~dp0uninstall-context-menu.reg" + +if %errorLevel% == 0 ( + echo. + echo Context menu integration removed successfully! + echo PanConverter entries have been removed from the Windows Explorer context menu. +) else ( + echo. + echo Failed to remove context menu entries. + echo Please check that the registry file exists and try again. +) + +echo. pause \ No newline at end of file diff --git a/scripts/uninstall-context-menu.reg b/scripts/uninstall-context-menu.reg index 05b0470..f9b1291 100644 --- a/scripts/uninstall-context-menu.reg +++ b/scripts/uninstall-context-menu.reg @@ -1,36 +1,36 @@ -Windows Registry Editor Version 5.00 - -; PanConverter Context Menu Uninstall Script -; This registry file removes PanConverter context menu entries - -; Remove context menu for Markdown files (.md, .markdown) -[-HKEY_CLASSES_ROOT\.md\shell\PanConverter] -[-HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu] - -[-HKEY_CLASSES_ROOT\.markdown\shell\PanConverter] - -; Remove context menu for HTML files -[-HKEY_CLASSES_ROOT\.html\shell\PanConverter] -[-HKEY_CLASSES_ROOT\.htm\shell\PanConverter] - -; Remove context menu for DOCX files -[-HKEY_CLASSES_ROOT\.docx\shell\PanConverter] - -; Remove context menu for ODT files -[-HKEY_CLASSES_ROOT\.odt\shell\PanConverter] - -; Remove context menu for RTF files -[-HKEY_CLASSES_ROOT\.rtf\shell\PanConverter] - -; Remove context menu for LaTeX files -[-HKEY_CLASSES_ROOT\.tex\shell\PanConverter] - -; Remove context menu for PDF files -[-HKEY_CLASSES_ROOT\.pdf\shell\PanConverter] - -; Remove context menu for PowerPoint files -[-HKEY_CLASSES_ROOT\.pptx\shell\PanConverter] -[-HKEY_CLASSES_ROOT\.ppt\shell\PanConverter] - -; Remove context menu for OpenDocument Presentation files +Windows Registry Editor Version 5.00 + +; PanConverter Context Menu Uninstall Script +; This registry file removes PanConverter context menu entries + +; Remove context menu for Markdown files (.md, .markdown) +[-HKEY_CLASSES_ROOT\.md\shell\PanConverter] +[-HKEY_CLASSES_ROOT\.md\shell\PanConverterMenu] + +[-HKEY_CLASSES_ROOT\.markdown\shell\PanConverter] + +; Remove context menu for HTML files +[-HKEY_CLASSES_ROOT\.html\shell\PanConverter] +[-HKEY_CLASSES_ROOT\.htm\shell\PanConverter] + +; Remove context menu for DOCX files +[-HKEY_CLASSES_ROOT\.docx\shell\PanConverter] + +; Remove context menu for ODT files +[-HKEY_CLASSES_ROOT\.odt\shell\PanConverter] + +; Remove context menu for RTF files +[-HKEY_CLASSES_ROOT\.rtf\shell\PanConverter] + +; Remove context menu for LaTeX files +[-HKEY_CLASSES_ROOT\.tex\shell\PanConverter] + +; Remove context menu for PDF files +[-HKEY_CLASSES_ROOT\.pdf\shell\PanConverter] + +; Remove context menu for PowerPoint files +[-HKEY_CLASSES_ROOT\.pptx\shell\PanConverter] +[-HKEY_CLASSES_ROOT\.ppt\shell\PanConverter] + +; Remove context menu for OpenDocument Presentation files [-HKEY_CLASSES_ROOT\.odp\shell\PanConverter] \ No newline at end of file diff --git a/setup-upstream.sh b/setup-upstream.sh index 5fe3e5b..3ee4fd2 100755 --- a/setup-upstream.sh +++ b/setup-upstream.sh @@ -1,37 +1,37 @@ -#!/bin/bash - -# Pan Converter - Git Remote Setup Script -# -# Instructions: -# 1. Create a new repository on GitHub named "pan-converter" -# 2. Replace YOUR_GITHUB_USERNAME with your actual GitHub username -# 3. Run this script: bash setup-upstream.sh - -GITHUB_USERNAME="amitwh" -REPO_NAME="pan-converter" - -echo "Setting up remote repository..." - -# Add remote origin -git remote add origin "https://github.com/$GITHUB_USERNAME/$REPO_NAME.git" - -echo "Pushing all branches to remote..." - -# Push master branch -git push -u origin master - -# Push platform-specific branches -git push origin linux -git push origin macos -git push origin windows - -echo "Repository setup complete!" -echo "" -echo "Your repository is now available at:" -echo "https://github.com/$GITHUB_USERNAME/$REPO_NAME" -echo "" -echo "Branch structure:" -echo " - master (main development)" -echo " - linux (Linux-specific)" -echo " - macos (macOS-specific)" -echo " - windows (Windows-specific)" +#!/bin/bash + +# Pan Converter - Git Remote Setup Script +# +# Instructions: +# 1. Create a new repository on GitHub named "pan-converter" +# 2. Replace YOUR_GITHUB_USERNAME with your actual GitHub username +# 3. Run this script: bash setup-upstream.sh + +GITHUB_USERNAME="amitwh" +REPO_NAME="pan-converter" + +echo "Setting up remote repository..." + +# Add remote origin +git remote add origin "https://github.com/$GITHUB_USERNAME/$REPO_NAME.git" + +echo "Pushing all branches to remote..." + +# Push master branch +git push -u origin master + +# Push platform-specific branches +git push origin linux +git push origin macos +git push origin windows + +echo "Repository setup complete!" +echo "" +echo "Your repository is now available at:" +echo "https://github.com/$GITHUB_USERNAME/$REPO_NAME" +echo "" +echo "Branch structure:" +echo " - master (main development)" +echo " - linux (Linux-specific)" +echo " - macos (macOS-specific)" +echo " - windows (Windows-specific)" diff --git a/src/ascii-generator.html b/src/ascii-generator.html index 2543ae1..c6c9696 100644 --- a/src/ascii-generator.html +++ b/src/ascii-generator.html @@ -1,601 +1,601 @@ - - - - - - ASCII Art Generator - MarkdownConverter - - - - -
-

ASCII Art Generator

-
- -
-
-
- - - -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
Arrows & Flow
-
- - - - -
-
-
-
Diagrams
-
- - - - -
-
-
-
Boxes & Containers
-
- - - - -
-
-
-
Decorative
-
- - - - -
-
-
-
- -
-
Preview
-
-
-
-
-
- - - - - - + + + + + + ASCII Art Generator - MarkdownConverter + + + + +
+

ASCII Art Generator

+
+ +
+
+
+ + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
Arrows & Flow
+
+ + + + +
+
+
+
Diagrams
+
+ + + + +
+
+
+
Boxes & Containers
+
+ + + + +
+
+
+
Decorative
+
+ + + + +
+
+
+
+ +
+
Preview
+
+
+
+
+
+ + + + + + diff --git a/src/fonts.css b/src/fonts.css index c312c87..ca5866e 100644 --- a/src/fonts.css +++ b/src/fonts.css @@ -1,75 +1,75 @@ -/* Local Font Definitions for MarkdownConverter */ - -/* Inter Font Family */ -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 300; - font-display: swap; - src: url('../assets/fonts/Inter-Light.woff2') format('woff2'); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('../assets/fonts/Inter-Regular.woff2') format('woff2'); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url('../assets/fonts/Inter-Medium.woff2') format('woff2'); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url('../assets/fonts/Inter-SemiBold.woff2') format('woff2'); -} - -@font-face { - font-family: 'Inter'; - font-style: normal; - font-weight: 700; - font-display: swap; - src: url('../assets/fonts/Inter-Bold.woff2') format('woff2'); -} - -/* JetBrains Mono Font Family - For code, markdown editor, and ASCII art */ -@font-face { - font-family: 'JetBrains Mono'; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url('../assets/fonts/JetBrainsMono-Regular.woff2') format('woff2'); -} - -@font-face { - font-family: 'JetBrains Mono'; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url('../assets/fonts/JetBrainsMono-Medium.woff2') format('woff2'); -} - -@font-face { - font-family: 'JetBrains Mono'; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url('../assets/fonts/JetBrainsMono-SemiBold.woff2') format('woff2'); -} - -@font-face { - font-family: 'JetBrains Mono'; - font-style: normal; - font-weight: 700; - font-display: swap; - src: url('../assets/fonts/JetBrainsMono-Bold.woff2') format('woff2'); -} +/* Local Font Definitions for MarkdownConverter */ + +/* Inter Font Family */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url('../assets/fonts/Inter-Light.woff2') format('woff2'); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('../assets/fonts/Inter-Regular.woff2') format('woff2'); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('../assets/fonts/Inter-Medium.woff2') format('woff2'); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('../assets/fonts/Inter-SemiBold.woff2') format('woff2'); +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('../assets/fonts/Inter-Bold.woff2') format('woff2'); +} + +/* JetBrains Mono Font Family - For code, markdown editor, and ASCII art */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('../assets/fonts/JetBrainsMono-Regular.woff2') format('woff2'); +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('../assets/fonts/JetBrainsMono-Medium.woff2') format('woff2'); +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('../assets/fonts/JetBrainsMono-SemiBold.woff2') format('woff2'); +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('../assets/fonts/JetBrainsMono-Bold.woff2') format('woff2'); +} diff --git a/src/index.html b/src/index.html index 5fc5299..83ea896 100644 --- a/src/index.html +++ b/src/index.html @@ -1,1414 +1,1414 @@ - - - - - - MarkdownConverter - - - - - - - - - -
- -
-
- - MarkdownConverter -
-
- v3.0.0 -
-
-
-
- Untitled - -
- -
-
- - - - - - - - - - - -
- - -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- - -
-
-
-
-
-
-
- - - -
- -
- Ready - Words: 0 | Characters: 0 -
-
- - - + + + + + + MarkdownConverter + + + + + + + + + +
+ +
+
+ + MarkdownConverter +
+
+ v3.0.0 +
+
+
+
+ Untitled + +
+ +
+
+ + + + + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + +
+
+
+
+
+
+
+ + + +
+ +
+ Ready + Words: 0 | Characters: 0 +
+
+ + + \ No newline at end of file diff --git a/src/main.js b/src/main.js index e55ae1c..74a9c4b 100644 --- a/src/main.js +++ b/src/main.js @@ -1,3795 +1,3795 @@ -const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron'); -const path = require('path'); -const fs = require('fs'); -const { exec, execFile, spawn } = require('child_process'); -const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib'); -const WordTemplateExporter = require('./wordTemplateExporter'); - -// Add MiKTeX to PATH for LaTeX support -if (process.platform === 'win32') { - const miktexPath = 'C:\\Program Files\\MiKTeX\\miktex\\bin\\x64'; - if (fs.existsSync(miktexPath)) { - process.env.PATH = `${miktexPath};${process.env.PATH}`; - console.log('[MAIN] Added MiKTeX to PATH:', miktexPath); - } -} - -// Get the system Pandoc path -function getPandocPath() { - // Pandoc is expected to be in the system's PATH. - // The command will be executed directly. Quoting is handled by exec. - return 'pandoc'; -} - -// Check if Pandoc is available -function checkPandocAvailable() { - return new Promise((resolve) => { - const pandocPath = getPandocPath(); - execFile(pandocPath, ['--version'], (error) => { - resolve(!error); - }); - }); -} - -/** - * Safe command execution using execFile (no shell injection risk) - * @param {string} command - The command to execute - * @param {string[]} args - Array of arguments - * @param {object} options - Options for execFile - * @returns {Promise<{stdout: string, stderr: string}>} - */ -function safeExecFile(command, args, options = {}) { - return new Promise((resolve, reject) => { - execFile(command, args, { maxBuffer: 10 * 1024 * 1024, ...options }, (error, stdout, stderr) => { - if (error) { - reject({ error, stdout, stderr }); - } else { - resolve({ stdout, stderr }); - } - }); - }); -} - -/** - * Run Pandoc command safely with execFile - * @param {string[]} args - Pandoc arguments array - * @param {Function} callback - Callback function (error, stdout, stderr) - */ -function runPandoc(args, callback) { - const pandocPath = getPandocPath(); - execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback); -} - -/** - * Run a Pandoc command string safely using execFile - * Parses the command string and uses execFile to prevent shell injection - * @param {string} cmdString - Full Pandoc command string (e.g., 'pandoc "input.md" -o "output.pdf"') - * @param {Function} callback - Callback function (error, stdout, stderr) - */ -function runPandocCmd(cmdString, callback) { - const parsed = parseCommand(cmdString); - // Skip 'pandoc' if it's the first element (command itself) - const args = parsed.command === 'pandoc' ? parsed.args : [parsed.command, ...parsed.args]; - const pandocPath = getPandocPath(); - execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback); -} - -/** - * Parse a command string into command and arguments array - * This helps transition from exec() to execFile() safely - * @param {string} cmdString - Full command string - * @returns {{command: string, args: string[]}} - */ -function parseCommand(cmdString) { - // Handle quoted strings properly - const parts = []; - let current = ''; - let inQuotes = false; - let quoteChar = ''; - - for (let i = 0; i < cmdString.length; i++) { - const char = cmdString[i]; - if ((char === '"' || char === "'") && !inQuotes) { - inQuotes = true; - quoteChar = char; - } else if (char === quoteChar && inQuotes) { - inQuotes = false; - quoteChar = ''; - } else if (char === ' ' && !inQuotes) { - if (current) { - parts.push(current); - current = ''; - } - } else { - current += char; - } - } - if (current) { - parts.push(current); - } - - return { - command: parts[0], - args: parts.slice(1) - }; -} - -// Simple storage implementation to replace electron-store -const settingsPath = path.join(app.getPath('userData'), 'settings.json'); -const store = { - get: (key, defaultValue) => { - try { - const data = fs.readFileSync(settingsPath, 'utf-8'); - const settings = JSON.parse(data); - return settings[key] || defaultValue; - } catch { - return defaultValue; - } - }, - set: (key, value) => { - let settings = {}; - try { - const data = fs.readFileSync(settingsPath, 'utf-8'); - settings = JSON.parse(data); - } catch {} - settings[key] = value; - fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); - } -}; - -let mainWindow; -let currentFile = null; // This will now represent the active tab's file -let pandocAvailable = null; // Cache pandoc availability check -let wordTemplatePath = null; // Path to selected Word template -let templateStartPage = 3; // Which page to start inserting content (default: page 3) -let rendererReady = false; // Track if renderer is ready to receive file data - -// Header & Footer Settings -let headerFooterSettings = { - enabled: true, - header: { - left: '', - center: '', - right: '', - logo: null // Will store image file path - }, - footer: { - left: '', - center: '$PAGE$ of $TOTAL$', - right: '', - logo: null - } -}; - -// Page Size Definitions (in twentieths of a point for Word, mm/inches for Pandoc) -const PAGE_SIZES = { - a4: { - name: 'A4', - pandoc: 'a4', - word: { width: 11906, height: 16838 }, // 210×297mm - dimensions: '210×297mm' - }, - a3: { - name: 'A3', - pandoc: 'a3', - word: { width: 16838, height: 23811 }, // 297×420mm - dimensions: '297×420mm' - }, - a5: { - name: 'A5', - pandoc: 'a5', - word: { width: 8391, height: 11906 }, // 148×210mm - dimensions: '148×210mm' - }, - b4: { - name: 'B4', - pandoc: 'b4', - word: { width: 14170, height: 20015 }, // 250×353mm - dimensions: '250×353mm' - }, - b5: { - name: 'B5', - pandoc: 'b5', - word: { width: 9979, height: 14170 }, // 176×250mm - dimensions: '176×250mm' - }, - letter: { - name: 'Letter', - pandoc: 'letter', - word: { width: 12240, height: 15840 }, // 8.5×11in - dimensions: '8.5×11in' - }, - legal: { - name: 'Legal', - pandoc: 'legal', - word: { width: 12240, height: 20160 }, // 8.5×14in - dimensions: '8.5×14in' - }, - tabloid: { - name: 'Tabloid', - pandoc: 'tabloid', - word: { width: 15840, height: 24480 }, // 11×17in - dimensions: '11×17in' - } -}; - -// Default page settings -let pageSettings = { - size: 'a4', - orientation: 'portrait', - customWidth: null, - customHeight: null -}; - -// Handle single instance lock for Windows file association -// When a file is double-clicked and the app is already running, -// Windows tries to start a second instance. We prevent this and -// pass the file to the existing instance instead. -const gotTheLock = app.requestSingleInstanceLock(); - -if (!gotTheLock) { - // Another instance is already running, quit this one - app.quit(); -} else { - // This is the first instance, handle second-instance events - app.on('second-instance', (event, commandLine, workingDirectory) => { - // Someone tried to run a second instance, focus our window instead - if (mainWindow) { - if (mainWindow.isMinimized()) mainWindow.restore(); - mainWindow.focus(); - } - - // Check if a file was passed to the second instance - // commandLine is an array like: ['PanConverter.exe', 'file.md'] - console.log('[MAIN] Second instance commandLine:', JSON.stringify(commandLine)); - const startIndex = app.isPackaged ? 1 : 2; - const fileArgs = commandLine.slice(startIndex); - console.log('[MAIN] Second instance file args:', fileArgs); - for (const arg of fileArgs) { - if ((arg.endsWith('.md') || arg.endsWith('.markdown'))) { - const resolvedPath = path.isAbsolute(arg) ? arg : path.resolve(workingDirectory, arg); - console.log('[MAIN] Second instance resolved path:', resolvedPath); - if (fs.existsSync(resolvedPath)) { - // Open the file in the existing instance - if (rendererReady) { - openFileFromPath(resolvedPath); - } else { - app.pendingFile = resolvedPath; - } - break; - } - } - } - }); -} - -// Check if pandoc is available (using execFile for consistency) -function checkPandocAvailability() { - return new Promise((resolve) => { - if (pandocAvailable !== null) { - resolve(pandocAvailable); - return; - } - - execFile('pandoc', ['--version'], (error, stdout, stderr) => { - pandocAvailable = !error; - resolve(pandocAvailable); - }); - }); -} - -function createWindow() { - mainWindow = new BrowserWindow({ - width: 1200, - height: 800, - webPreferences: { - nodeIntegration: true, - contextIsolation: false - }, - icon: path.join(__dirname, '../assets/icon.png') - }); - - mainWindow.loadFile(path.join(__dirname, 'index.html')); - - createMenu(); - - mainWindow.on('closed', () => { - mainWindow = null; - }); - - // Wait for the page to fully load before sending file data - mainWindow.webContents.on('did-finish-load', () => { - console.log('Window finished loading'); - // Don't open file here - wait for renderer-ready signal - // The renderer will send renderer-ready when TabManager is initialized - }); -} - -function buildRecentFilesMenu() { - const recentFiles = getRecentFiles(); - - if (recentFiles.length === 0) { - return [ - { - label: 'No recent files', - enabled: false - } - ]; - } - - const recentFileItems = recentFiles.map(filePath => ({ - label: filePath.split(/[\\/]/).pop(), // Get filename only - click: () => { - if (fs.existsSync(filePath)) { - currentFile = filePath; - const content = fs.readFileSync(filePath, 'utf-8'); - mainWindow.webContents.send('file-opened', { path: filePath, content }); - } else { - dialog.showErrorBox('File Not Found', `The file "${filePath}" could not be found.`); - } - }, - toolTip: filePath // Show full path in tooltip - })); - - return [ - ...recentFileItems, - { type: 'separator' }, - { - label: 'Clear Recent Files', - click: () => { - mainWindow.webContents.send('clear-recent-files'); - } - } - ]; -} - -function getRecentFiles() { - try { - const recentFiles = JSON.parse(fs.readFileSync(path.join(app.getPath('userData'), 'recent-files.json'), 'utf-8')); - return recentFiles.filter(file => fs.existsSync(file)); - } catch (e) { - return []; - } -} - -function createMenu() { - const template = [ - { - label: 'File', - submenu: [ - { - label: 'New', - accelerator: 'CmdOrCtrl+N', - click: () => mainWindow.webContents.send('file-new') - }, - { - label: 'Open', - accelerator: 'CmdOrCtrl+O', - click: openFile - }, - { - label: 'Open PDF', - accelerator: 'CmdOrCtrl+Shift+O', - click: openPdfFile - }, - { - label: 'Save', - accelerator: 'CmdOrCtrl+S', - click: () => mainWindow.webContents.send('file-save') - }, - { - label: 'Save As', - accelerator: 'CmdOrCtrl+Shift+S', - click: saveAsFile - }, - { type: 'separator' }, - { - label: 'Print', - submenu: [ - { - label: 'Print Preview', - accelerator: 'CmdOrCtrl+P', - click: () => mainWindow.webContents.send('print-preview') - }, - { - label: 'Print Preview (With Styles)', - click: () => mainWindow.webContents.send('print-preview-styled') - } - ] - }, - { type: 'separator' }, - { - label: 'Recent Files', - submenu: buildRecentFilesMenu() - }, - { type: 'separator' }, - { - label: 'Import Document...', - accelerator: 'CmdOrCtrl+I', - click: importDocument - }, - { - label: 'Export', - submenu: [ - { label: 'HTML', click: () => exportFile('html') }, - { label: 'PDF', click: () => exportFile('pdf') }, - { label: 'PDF (Enhanced)', click: () => exportPDFViaWordTemplate(), accelerator: 'Ctrl+Shift+P' }, - { label: 'DOCX', click: () => exportFile('docx') }, - { label: 'DOCX (Enhanced)', click: () => exportWordWithTemplate(), accelerator: 'Ctrl+Shift+W' }, - { label: 'LaTeX', click: () => exportFile('latex') }, - { label: 'RTF', click: () => exportFile('rtf') }, - { label: 'ODT', click: () => exportFile('odt') }, - { label: 'EPUB', click: () => exportFile('epub') }, - { type: 'separator' }, - { label: 'PowerPoint (PPTX)', click: () => exportFile('pptx') }, - { label: 'OpenDocument Presentation (ODP)', click: () => exportFile('odp') }, - { type: 'separator' }, - { label: 'CSV (Tables)', click: () => exportSpreadsheet('csv') }, - ] - }, - { type: 'separator' }, - { - label: 'Select Word Template...', - click: selectWordTemplate - }, - { - label: 'Template Settings...', - click: showTemplateSettings - }, - { - label: 'Header & Footer Settings...', - click: () => { - if (mainWindow) { - mainWindow.webContents.send('open-header-footer-dialog'); - } - } - }, - { type: 'separator' }, - { - label: 'Quit', - accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q', - click: () => app.quit() - } - ] - }, - { - label: 'Edit', - submenu: [ - { - label: 'Undo', - accelerator: 'CmdOrCtrl+Z', - click: () => mainWindow.webContents.send('undo') - }, - { - label: 'Redo', - accelerator: 'CmdOrCtrl+Shift+Z', - click: () => mainWindow.webContents.send('redo') - }, - { type: 'separator' }, - { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, - { label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }, - { label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' }, - { label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectAll' }, - { type: 'separator' }, - { - label: 'Find & Replace', - accelerator: 'CmdOrCtrl+F', - click: () => mainWindow.webContents.send('toggle-find') - } - ] - }, - { - label: 'View', - submenu: [ - { - label: 'Toggle Preview', - accelerator: 'CmdOrCtrl+Shift+P', - click: () => mainWindow.webContents.send('toggle-preview') - }, - { - label: 'Theme', - submenu: [ - // Light Themes (grouped first) - { label: 'Atom One Light (Default)', click: () => setTheme('atomonelight') }, - { label: 'GitHub Light', click: () => setTheme('github') }, - { label: 'Light', click: () => setTheme('light') }, - { label: 'Solarized Light', click: () => setTheme('solarized') }, - { label: 'Gruvbox Light', click: () => setTheme('gruvbox-light') }, - { label: 'Ayu Light', click: () => setTheme('ayu-light') }, - { label: 'Sepia', click: () => setTheme('sepia') }, - { label: 'Paper', click: () => setTheme('paper') }, - { label: 'Rose Pine Dawn', click: () => setTheme('rosepine-dawn') }, - { label: 'Concrete Light', click: () => setTheme('concrete-light') }, - { type: 'separator' }, - // Dark Themes - { label: 'Dark', click: () => setTheme('dark') }, - { label: 'One Dark', click: () => setTheme('onedark') }, - { label: 'Dracula', click: () => setTheme('dracula') }, - { label: 'Nord', click: () => setTheme('nord') }, - { label: 'Monokai', click: () => setTheme('monokai') }, - { label: 'Material', click: () => setTheme('material') }, - { label: 'Gruvbox Dark', click: () => setTheme('gruvbox-dark') }, - { label: 'Tokyo Night', click: () => setTheme('tokyonight') }, - { label: 'Palenight', click: () => setTheme('palenight') }, - { label: 'Ayu Dark', click: () => setTheme('ayu-dark') }, - { label: 'Ayu Mirage', click: () => setTheme('ayu-mirage') }, - { label: 'Oceanic Next', click: () => setTheme('oceanic-next') }, - { label: 'Cobalt2', click: () => setTheme('cobalt2') }, - { label: 'Concrete Dark', click: () => setTheme('concrete-dark') }, - { label: 'Concrete Warm', click: () => setTheme('concrete-warm') } - ] - }, - { type: 'separator' }, - { - label: 'Font Size', - submenu: [ - { - label: 'Increase Font Size', - accelerator: 'CmdOrCtrl+Shift+Plus', - click: () => mainWindow.webContents.send('adjust-font-size', 'increase') - }, - { - label: 'Decrease Font Size', - accelerator: 'CmdOrCtrl+Shift+-', - click: () => mainWindow.webContents.send('adjust-font-size', 'decrease') - }, - { - label: 'Reset Font Size', - accelerator: 'CmdOrCtrl+Shift+0', - click: () => mainWindow.webContents.send('adjust-font-size', 'reset') - } - ] - }, - { type: 'separator' }, - { label: 'Reload', accelerator: 'CmdOrCtrl+R', role: 'reload' }, - { label: 'Toggle DevTools', accelerator: 'F12', role: 'toggleDevTools' }, - { type: 'separator' }, - { label: 'Zoom In', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' }, - { label: 'Zoom Out', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' }, - { label: 'Reset Zoom', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' } - ] - }, - { - label: 'Batch', - submenu: [ - { - label: 'Convert Markdown Folder...', - click: () => showBatchConversionDialog() - }, - { type: 'separator' }, - { - label: 'Batch Image Conversion...', - click: () => mainWindow.webContents.send('show-batch-converter', 'image') - }, - { - label: 'Batch Audio Conversion...', - click: () => mainWindow.webContents.send('show-batch-converter', 'audio') - }, - { - label: 'Batch Video Conversion...', - click: () => mainWindow.webContents.send('show-batch-converter', 'video') - }, - { - label: 'Batch PDF Conversion...', - click: () => mainWindow.webContents.send('show-batch-converter', 'pdf') - } - ] - }, - { - label: 'Convert', - submenu: [ - { - label: 'Universal File Converter...', - accelerator: 'CmdOrCtrl+Shift+C', - click: () => showUniversalConverterDialog() - } - ] - }, - { - label: 'PDF Editor', - submenu: [ - { - label: 'Open PDF File...', - accelerator: 'CmdOrCtrl+Shift+O', - click: () => openPDFFile() - }, - { type: 'separator' }, - { - label: 'Merge PDFs...', - click: () => showPDFEditorDialog('merge') - }, - { - label: 'Split PDF...', - click: () => showPDFEditorDialog('split') - }, - { - label: 'Compress PDF...', - click: () => showPDFEditorDialog('compress') - }, - { - type: 'separator' - }, - { - label: 'Rotate Pages...', - click: () => showPDFEditorDialog('rotate') - }, - { - label: 'Delete Pages...', - click: () => showPDFEditorDialog('delete') - }, - { - label: 'Reorder Pages...', - click: () => showPDFEditorDialog('reorder') - }, - { - type: 'separator' - }, - { - label: 'Add Watermark...', - click: () => showPDFEditorDialog('watermark') - }, - { - type: 'separator' - }, - { - label: 'Security', - submenu: [ - { - label: 'Add Password Protection...', - click: () => showPDFEditorDialog('encrypt') - }, - { - label: 'Remove Password...', - click: () => showPDFEditorDialog('decrypt') - }, - { - label: 'Set Permissions...', - click: () => showPDFEditorDialog('permissions') - } - ] - }, - { - type: 'separator' - }, - { - label: 'About PDF Editor', - click: () => { - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'About PDF Editor', - message: 'PDF Editor', - detail: 'Comprehensive PDF editing capabilities powered by pdf-lib.\n\nFeatures:\n• Merge multiple PDF files\n• Split PDF into separate files\n• Compress PDF to reduce file size\n• Rotate pages (90°, 180°, 270°)\n• Delete unwanted pages\n• Reorder pages\n• Add text watermarks\n\nSecurity Features:\n• Password protection (encryption)\n• Remove passwords (decryption)\n• Set document permissions\n\n100% offline and open-source.', - buttons: ['OK'] - }); - } - } - ] - }, - { - label: 'Tools', - submenu: [ - { - label: 'Table Generator', - accelerator: 'CmdOrCtrl+Shift+T', - click: () => openTableGenerator() - }, - { - label: 'ASCII Art Generator', - accelerator: 'CmdOrCtrl+Shift+A', - click: () => openAsciiGenerator() - }, - { type: 'separator' }, - { - label: 'Document Compare', - click: () => mainWindow.webContents.send('show-document-compare') - } - ] - }, - { - label: 'Help', - submenu: [ - { - label: 'About MarkdownConverter', - click: () => showAboutDialog() - }, - { type: 'separator' }, - { - label: 'Dependencies & Requirements', - click: () => showDependenciesDialog() - }, - { type: 'separator' }, - { - label: 'Documentation', - click: () => shell.openExternal('https://github.com/amitwh/markdown-converter') - }, - { - label: 'Report Issue', - click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/issues') - }, - { - label: 'Check for Updates', - click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/releases') - } - ] - } - ]; - - const menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(menu); -} - -// Show About Dialog with logo -function showAboutDialog() { - const aboutWindow = new BrowserWindow({ - width: 500, - height: 600, - parent: mainWindow, - modal: true, - resizable: false, - minimizable: false, - maximizable: false, - webPreferences: { - nodeIntegration: false, - contextIsolation: true - }, - icon: path.join(__dirname, '../assets/icon.png') - }); - - // Convert images to base64 for data URL compatibility - let logoBase64 = ''; - let iconBase64 = ''; - try { - const logoPath = path.join(__dirname, '../assets/logo.png'); - const iconPath = path.join(__dirname, '../assets/icon.png'); - if (fs.existsSync(logoPath)) { - logoBase64 = 'data:image/png;base64,' + fs.readFileSync(logoPath).toString('base64'); - } - if (fs.existsSync(iconPath)) { - iconBase64 = 'data:image/png;base64,' + fs.readFileSync(iconPath).toString('base64'); - } - } catch (e) { - console.error('Error loading about dialog images:', e); - } - - const aboutHTML = ` - - - - - About MarkdownConverter - - - - -

MarkdownConverter

-
Version 3.0.0
- -
- by - -
- -
-

Features

-
    -
  • Professional Markdown editing with syntax highlighting
  • -
  • Universal file converter (Image, Audio, Video, PDF)
  • -
  • Batch conversion for all media types
  • -
  • Advanced PDF Editor (merge, split, watermark, encrypt)
  • -
  • Export to PDF, DOCX, HTML, LaTeX, EPUB & more
  • -
  • 23+ beautiful themes including ConcreteInfo theme
  • -
  • ASCII Art & Table generators
  • -
  • Code file syntax highlighting
  • -
-
- -
-

Contact

-

Email: amit.wh@gmail.com

-

Website: GitHub Repository

-
- - - -`; - - aboutWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(aboutHTML)); - aboutWindow.setMenuBarVisibility(false); -} - -// Show Dependencies Dialog -function showDependenciesDialog() { - const depsWindow = new BrowserWindow({ - width: 600, - height: 700, - parent: mainWindow, - modal: true, - resizable: true, - webPreferences: { - nodeIntegration: false, - contextIsolation: true - }, - icon: path.join(__dirname, '../assets/icon.png') - }); - - const depsHTML = ` - - - - - Dependencies & Requirements - - - -

Dependencies & Requirements

- -

Required Dependencies

- -
-
Pandoc Required
-
Universal document converter. Required for most export formats (PDF, DOCX, LaTeX, EPUB).
- https://pandoc.org/installing.html -
- -

Optional Dependencies (for extended features)

- -
-
FFmpeg Optional
-
Required for audio/video conversion and processing.
- https://ffmpeg.org/download.html -
- -
-
ImageMagick Optional
-
Required for image format conversion and processing.
- https://imagemagick.org/script/download.php -
- -
-
LibreOffice Optional
-
Required for enhanced document conversion (Office formats).
- https://www.libreoffice.org/download/download/ -
- -
-
MiKTeX / TeX Live Optional
-
Required for PDF export via LaTeX (higher quality PDFs).
- https://miktex.org/download -
- -

Bundled Libraries

- -
-
pdf-lib
-
PDF manipulation library for merge, split, watermark, and encryption features.
-
- -
-
marked
-
Markdown parser for preview rendering.
-
- -
-
highlight.js
-
Syntax highlighting for code blocks.
-
- -
-
DOMPurify
-
HTML sanitization for security.
-
- -`; - - depsWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(depsHTML)); - depsWindow.setMenuBarVisibility(false); -} - -// Open PDF File for viewing/editing -function openPDFFile() { - const files = dialog.showOpenDialogSync(mainWindow, { - properties: ['openFile'], - filters: [ - { name: 'PDF Files', extensions: ['pdf'] } - ] - }); - - if (files && files[0]) { - mainWindow.webContents.send('open-pdf-viewer', files[0]); - } -} - -function openFile() { - const files = dialog.showOpenDialogSync(mainWindow, { - properties: ['openFile'], - filters: [ - { name: 'Markdown', extensions: ['md', 'markdown'] }, - { name: 'All Files', extensions: ['*'] } - ] - }); - - if (files && files[0]) { - currentFile = files[0]; - const content = fs.readFileSync(currentFile, 'utf-8'); - mainWindow.webContents.send('file-opened', { path: currentFile, content }); - } -} - -function openPdfFile() { - const files = dialog.showOpenDialogSync(mainWindow, { - properties: ['openFile'], - filters: [ - { name: 'PDF Files', extensions: ['pdf'] }, - { name: 'All Files', extensions: ['*'] } - ] - }); - - if (files && files[0]) { - mainWindow.webContents.send('open-pdf-file', files[0]); - } -} - -function saveAsFile() { - const file = dialog.showSaveDialogSync(mainWindow, { - defaultExt: '.md', - filters: [ - { name: 'Markdown', extensions: ['md', 'markdown'] }, - { name: 'All Files', extensions: ['*'] } - ] - }); - - if (file) { - currentFile = file; - mainWindow.webContents.send('get-content-for-save', file); - } -} - -function exportFile(format) { - if (!currentFile) { - dialog.showErrorBox('Error', 'Please save the file first'); - return; - } - - // Show export options dialog - showExportOptionsDialog(format); -} - -function showExportOptionsDialog(format) { - mainWindow.webContents.send('show-export-dialog', format); -} - -function showBatchConversionDialog() { - mainWindow.webContents.send('show-batch-dialog'); -} - -// Select Word Template -async function selectWordTemplate() { - const result = await dialog.showOpenDialog(mainWindow, { - title: 'Select Word Template', - filters: [{ name: 'Word Document', extensions: ['docx'] }], - properties: ['openFile'] - }); - - if (!result.canceled && result.filePaths.length > 0) { - wordTemplatePath = result.filePaths[0]; - store.set('wordTemplatePath', wordTemplatePath); - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Template Selected', - message: 'Word template has been updated', - detail: `Template: ${path.basename(wordTemplatePath)}` - }); - } -} - -// Template Settings Dialog -async function showTemplateSettings() { - const result = await dialog.showMessageBox(mainWindow, { - type: 'question', - title: 'Template Settings', - message: 'Configure Word Template Export', - detail: `Current template: ${wordTemplatePath ? path.basename(wordTemplatePath) : 'Default template'}\nContent starts from page: ${templateStartPage}\n\nWhich page should content start from?\n(Templates usually have cover pages, TOC, etc.)`, - buttons: ['Page 1', 'Page 2', 'Page 3', 'Page 4', 'Page 5', 'Custom...', 'Cancel'], - defaultId: templateStartPage - 1, - cancelId: 6 - }); - - if (result.response === 6) return; // Cancel - - let newStartPage; - if (result.response === 5) { // Custom - // Show input dialog for custom page number - mainWindow.webContents.send('show-custom-start-page-dialog', templateStartPage); - } else { - newStartPage = result.response + 1; // Convert button index to page number - templateStartPage = newStartPage; - store.set('templateStartPage', templateStartPage); - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Settings Updated', - message: 'Template settings have been updated', - detail: `Content will now start from page ${templateStartPage}` - }); - } -} - -// Handle custom start page input from renderer -ipcMain.on('set-custom-start-page', (event, pageNumber) => { - const page = parseInt(pageNumber); - if (page >= 1 && page <= 100) { - templateStartPage = page; - store.set('templateStartPage', templateStartPage); - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Settings Updated', - message: 'Template settings have been updated', - detail: `Content will now start from page ${templateStartPage}` - }); - } else { - dialog.showErrorBox('Invalid Page Number', 'Please enter a page number between 1 and 100'); - } -}); - -// Header & Footer Settings IPC Handlers - -// Get current header/footer settings -ipcMain.on('get-header-footer-settings', (event) => { - event.reply('header-footer-settings-data', headerFooterSettings); -}); - -// Save header/footer settings -ipcMain.on('save-header-footer-settings', (event, settings) => { - headerFooterSettings = settings; - store.set('headerFooterSettings', headerFooterSettings); - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Settings Saved', - message: 'Header and footer settings have been saved successfully!', - buttons: ['OK'] - }); -}); - -// Get current page settings -ipcMain.on('get-page-settings', (event) => { - event.reply('page-settings-data', pageSettings); -}); - -// Update page settings from export dialog -ipcMain.on('update-page-settings', (event, settings) => { - pageSettings = settings; - store.set('pageSettings', pageSettings); - console.log('Page settings updated:', pageSettings); -}); - -// Save header/footer logo image -// Browse for header/footer logo -ipcMain.on('browse-header-footer-logo', async (event, position) => { - try { - const result = await dialog.showOpenDialog(mainWindow, { - title: `Select ${position.charAt(0).toUpperCase() + position.slice(1)} Logo/Image`, - filters: [ - { name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp'] } - ], - properties: ['openFile'] - }); - - if (!result.canceled && result.filePaths.length > 0) { - const filePath = result.filePaths[0]; - - // Copy image to userData directory for persistent storage - const userDataPath = app.getPath('userData'); - const logoDir = path.join(userDataPath, 'logos'); - - // Create logos directory if it doesn't exist - if (!fs.existsSync(logoDir)) { - fs.mkdirSync(logoDir, { recursive: true }); - } - - // Generate unique filename - const ext = path.extname(filePath); - const filename = `${position}_${Date.now()}${ext}`; - const destPath = path.join(logoDir, filename); - - // Copy file - fs.copyFileSync(filePath, destPath); - - // Update settings - if (position === 'header') { - headerFooterSettings.header.logo = destPath; - } else if (position === 'footer') { - headerFooterSettings.footer.logo = destPath; - } - - event.reply('header-footer-logo-saved', { position, path: destPath }); - } - } catch (error) { - console.error('Logo browse error:', error); - dialog.showErrorBox('Logo Error', `Failed to select logo: ${error.message}`); - } -}); - -ipcMain.on('save-header-footer-logo', async (event, { position, filePath }) => { - try { - if (!filePath) { - dialog.showErrorBox('Logo Error', 'Failed to save logo: The "path" argument must be of type string. Received undefined'); - return; - } - - // Copy image to userData directory for persistent storage - const userDataPath = app.getPath('userData'); - const logoDir = path.join(userDataPath, 'logos'); - - // Create logos directory if it doesn't exist - if (!fs.existsSync(logoDir)) { - fs.mkdirSync(logoDir, { recursive: true }); - } - - // Verify source file exists - if (!fs.existsSync(filePath)) { - dialog.showErrorBox('Logo Error', `Source file not found: ${filePath}`); - return; - } - - // Generate unique filename - const ext = path.extname(filePath); - const filename = `${position}_${Date.now()}${ext}`; - const destPath = path.join(logoDir, filename); - - // Copy file - fs.copyFileSync(filePath, destPath); - - // Update settings - if (position === 'header') { - headerFooterSettings.header.logo = destPath; - } else if (position === 'footer') { - headerFooterSettings.footer.logo = destPath; - } - - event.reply('header-footer-logo-saved', { position, path: destPath }); - } catch (error) { - console.error('Logo save error:', error); - dialog.showErrorBox('Logo Error', `Failed to save logo: ${error.message}`); - } -}); - -// Clear header/footer logo -ipcMain.on('clear-header-footer-logo', (event, position) => { - if (position === 'header') { - headerFooterSettings.header.logo = null; - } else if (position === 'footer') { - headerFooterSettings.footer.logo = null; - } - event.reply('header-footer-logo-cleared', position); -}); - -// Helper function to process dynamic fields in header/footer text -function processDynamicFields(text, metadata = {}) { - if (!text) return ''; - - const now = new Date(); - const dateStr = now.toLocaleDateString(); - const timeStr = now.toLocaleTimeString(); - - let result = text; - result = result.replace(/\$DATE\$/g, dateStr); - result = result.replace(/\$TIME\$/g, timeStr); - result = result.replace(/\$TITLE\$/g, metadata.title || 'Untitled'); - result = result.replace(/\$AUTHOR\$/g, metadata.author || ''); - result = result.replace(/\$FILENAME\$/g, metadata.filename || ''); - - // Note: $PAGE$ and $TOTAL$ are handled by Pandoc/export tools - - return result; -} - -// Add headers/footers to DOCX file using PizZip and docx4js -// Function to set page size in DOCX files -async function setDocxPageSize(docxPath) { - try { - const PizZip = require('pizzip'); - - // Read the DOCX file - const docxBuffer = fs.readFileSync(docxPath); - const zip = new PizZip(docxBuffer); - - // Get document.xml - let documentXml = zip.file('word/document.xml').asText(); - - // Get page dimensions - let width, height; - const pageSize = PAGE_SIZES[pageSettings.size]; - - if (pageSize) { - width = pageSize.word.width; - height = pageSize.word.height; - } else if (pageSettings.customWidth && pageSettings.customHeight) { - // Parse custom dimensions (convert to twentieths of a point) - // Note: This is simplified - production code should handle various units - width = parseInt(pageSettings.customWidth) || 11906; - height = parseInt(pageSettings.customHeight) || 16838; - } else { - // Default to A4 - width = 11906; - height = 16838; - } - - // Swap dimensions for landscape - if (pageSettings.orientation === 'landscape') { - [width, height] = [height, width]; - } - - // Update all elements in section properties - const pgSzRegex = /]*\/>/g; - documentXml = documentXml.replace(pgSzRegex, () => { - return ``; - }); - - // If no pgSz found, add it to all sectPr elements - if (!pgSzRegex.test(documentXml)) { - const sectPrRegex = /]*>/g; - documentXml = documentXml.replace(sectPrRegex, (match) => { - return `${match}`; - }); - } - - // Save updated document.xml - zip.file('word/document.xml', documentXml); - - // Write modified DOCX - const newDocxBuffer = zip.generate({ type: 'nodebuffer' }); - fs.writeFileSync(docxPath, newDocxBuffer); - - } catch (error) { - console.error('Failed to set DOCX page size:', error); - } -} - -async function addHeaderFooterToDocx(docxPath, metadata = {}) { - if (!headerFooterSettings.enabled) return; - - try { - const PizZip = require('pizzip'); - - // Read the DOCX file - const docxBuffer = fs.readFileSync(docxPath); - const zip = new PizZip(docxBuffer); - - // Process dynamic fields - const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); - const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); - const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); - const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); - const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); - const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); - - // Create header XML - if (headerLeft || headerCenter || headerRight) { - const headerXml = ` - - - - ${headerLeft || ''} - - - - ${headerCenter || ''} - - - - ${headerRight || ''} - -`; - zip.file('word/header1.xml', headerXml); - } - - // Create footer XML with page numbers - if (footerLeft || footerCenter || footerRight) { - let footerCenterXml = ''; - if (footerCenter) { - // Handle $PAGE$ and $TOTAL$ in footer - if (footerCenter.includes('$PAGE$') || footerCenter.includes('$TOTAL$')) { - const parts = footerCenter.split(/(\$PAGE\$|\$TOTAL\$)/); - footerCenterXml = parts.map(part => { - if (part === '$PAGE$') { - return ''; - } else if (part === '$TOTAL$') { - return ''; - } else { - return `${part}`; - } - }).join(''); - } else { - footerCenterXml = `${footerCenter}`; - } - } - - const footerXml = ` - - - - ${footerLeft || ''} - - - - ${footerCenterXml} - - - - ${footerRight || ''} - -`; - zip.file('word/footer1.xml', footerXml); - } - - // Update document.xml.rels to reference header/footer - let relsXml = zip.file('word/_rels/document.xml.rels').asText(); - - // Add header relationship if not exists - if ((headerLeft || headerCenter || headerRight) && !relsXml.includes('header1.xml')) { - const headerId = 'rId100'; - const headerRel = ``; - relsXml = relsXml.replace('', headerRel + ''); - } - - // Add footer relationship if not exists - if ((footerLeft || footerCenter || footerRight) && !relsXml.includes('footer1.xml')) { - const footerId = 'rId101'; - const footerRel = ``; - relsXml = relsXml.replace('', footerRel + ''); - } - - zip.file('word/_rels/document.xml.rels', relsXml); - - // Update document.xml to use header/footer in sections - let documentXml = zip.file('word/document.xml').asText(); - if ((headerLeft || headerCenter || headerRight || footerLeft || footerCenter || footerRight)) { - // Find all section properties and add header/footer references - const sectPrRegex = /]*>[\s\S]*?<\/w:sectPr>/g; - documentXml = documentXml.replace(sectPrRegex, (match) => { - let updated = match; - if ((headerLeft || headerCenter || headerRight) && !match.includes('headerReference')) { - updated = updated.replace('', ''); - } - if ((footerLeft || footerCenter || footerRight) && !match.includes('footerReference')) { - updated = updated.replace('', ''); - } - return updated; - }); - } - - zip.file('word/document.xml', documentXml); - - // Write modified DOCX - const newDocxBuffer = zip.generate({ type: 'nodebuffer' }); - fs.writeFileSync(docxPath, newDocxBuffer); - - } catch (error) { - console.error('Failed to add headers/footers to DOCX:', error); - // Don't fail the export, just log the error - } -} - -// Enhanced Word Export with Template Support -async function exportWordWithTemplate() { - if (!currentFile) { - dialog.showErrorBox('Error', 'Please save the file first'); - return; - } - - try { - // Get markdown content - const content = fs.readFileSync(currentFile, 'utf-8'); - - // Show dialog for output file - const result = await dialog.showSaveDialog(mainWindow, { - title: 'Export to Word (Enhanced)', - defaultPath: currentFile.replace(/\.md$/, '.docx'), - filters: [{ name: 'Word Document', extensions: ['docx'] }] - }); - - if (result.canceled) return; - - // Create exporter instance with selected template, start page, and page settings - const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage, pageSettings); - - // Convert markdown to DOCX - await exporter.convert(content, result.filePath); - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Export Successful', - message: 'Document exported successfully!', - detail: `Saved to: ${result.filePath}` - }); - - } catch (error) { - dialog.showErrorBox('Export Error', `Failed to export document: ${error.message}`); - } -} - -// Enhanced PDF Export via Word Template -async function exportPDFViaWordTemplate() { - if (!currentFile) { - dialog.showErrorBox('Error', 'Please save the file first'); - return; - } - - try { - // Get markdown content - const content = fs.readFileSync(currentFile, 'utf-8'); - - // Show dialog for output file - const result = await dialog.showSaveDialog(mainWindow, { - title: 'Export to PDF (Enhanced)', - defaultPath: currentFile.replace(/\.md$/, '.pdf'), - filters: [{ name: 'PDF Document', extensions: ['pdf'] }] - }); - - if (result.canceled) return; - - // Step 1: Create temporary DOCX file using Word template - const tempDocxPath = result.filePath.replace(/\.pdf$/, '_temp.docx'); - - const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage); - await exporter.convert(content, tempDocxPath); - - // Step 2: Convert DOCX to PDF using LibreOffice (using execFile for safety) - const soffice = process.platform === 'win32' - ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' - : 'soffice'; - - const outputDir = path.dirname(result.filePath); - const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath]; - - execFile(soffice, sofficeArgs, (error, stdout, stderr) => { - // Clean up temporary DOCX file - try { - fs.unlinkSync(tempDocxPath); - } catch (e) { - console.error('Failed to delete temp file:', e); - } - - if (error) { - dialog.showErrorBox('PDF Conversion Error', - `Failed to convert to PDF. Please ensure LibreOffice is installed.\n\nError: ${error.message}`); - return; - } - - // LibreOffice creates file with same base name as input - const generatedPdfPath = tempDocxPath.replace(/\.docx$/, '.pdf'); - - // Rename if needed - if (generatedPdfPath !== result.filePath) { - try { - fs.renameSync(generatedPdfPath, result.filePath); - } catch (e) { - console.error('Failed to rename PDF:', e); - } - } - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Export Successful', - message: 'PDF exported successfully using Word template!', - detail: `Saved to: ${result.filePath}` - }); - }); - - } catch (error) { - dialog.showErrorBox('Export Error', `Failed to export PDF: ${error.message}`); - } -} - -// Universal File Converter integration -function showUniversalConverterDialog() { - mainWindow.webContents.send('show-universal-converter-dialog'); -} - -// PDF Editor dialog -function showPDFEditorDialog(operation) { - mainWindow.webContents.send('show-pdf-editor-dialog', operation); -} - -// Handle PDF editor from toolbar (with optional file path) -ipcMain.on('show-pdf-editor-from-toolbar', (event, { operation, filePath }) => { - mainWindow.webContents.send('show-pdf-editor-dialog', operation, filePath); -}); - -// Check if conversion tool is available (using execFile for safety) -function checkConverterAvailable(tool) { - return new Promise((resolve) => { - const isWin = process.platform === 'win32'; - const locateCmd = isWin ? 'where' : 'which'; - let toolName; - - switch (tool) { - case 'libreoffice': - toolName = 'soffice'; - break; - case 'imagemagick': - toolName = isWin ? 'magick' : 'convert'; - break; - case 'ffmpeg': - toolName = 'ffmpeg'; - break; - default: - resolve(false); - return; - } - - execFile(locateCmd, [toolName], (error) => { - resolve(!error); - }); - }); -} - -// Handle universal file conversion -ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, filePath }) => { - try { - mainWindow.webContents.send('conversion-status', 'Checking converter availability...'); - - // Check if the required tool is available - const toolAvailable = await checkConverterAvailable(tool); - - if (!toolAvailable) { - throw new Error(`${tool} is not installed or not found in PATH. Please install it first.`); - } - - mainWindow.webContents.send('conversion-status', 'Converting file...'); - - const outputPath = filePath.replace(/\.[^/.]+$/, `.${toFormat}`); - let conversionInfo; - - switch (tool) { - case 'libreoffice': - conversionInfo = convertWithLibreOffice(filePath, toFormat, outputPath); - break; - case 'imagemagick': - conversionInfo = convertWithImageMagick(filePath, outputPath); - break; - case 'ffmpeg': - conversionInfo = convertWithFFmpeg(filePath, outputPath); - break; - case 'pandoc': - conversionInfo = convertWithPandoc(filePath, outputPath); - break; - default: - throw new Error(`Unknown conversion tool: ${tool}`); - } - - // Use execFile for safety (prevents command injection) - execFile(conversionInfo.command, conversionInfo.args, (error, stdout, stderr) => { - if (error) { - mainWindow.webContents.send('conversion-complete', { - success: false, - error: error.message - }); - - dialog.showMessageBox(mainWindow, { - type: 'error', - title: 'Conversion Failed', - message: `${tool} conversion failed`, - detail: stderr || error.message, - buttons: ['OK'] - }); - } else { - mainWindow.webContents.send('conversion-complete', { - success: true, - outputPath: outputPath - }); - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Conversion Complete', - message: 'File converted successfully!', - detail: `Saved to: ${outputPath}`, - buttons: ['OK'] - }); - } - }); - } catch (error) { - mainWindow.webContents.send('conversion-complete', { - success: false, - error: error.message - }); - - dialog.showMessageBox(mainWindow, { - type: 'error', - title: 'Conversion Failed', - message: 'Universal conversion failed', - detail: error.message, - buttons: ['OK'] - }); - } -}); - -// LibreOffice conversion - returns {command, args} for execFile (safer than exec) -function convertWithLibreOffice(inputFile, outputFormat, outputPath) { - const outputDir = path.dirname(outputPath); - const soffice = process.platform === 'win32' - ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' - : 'soffice'; - - // LibreOffice conversion format mapping - const formatMap = { - 'pdf': 'pdf', - 'docx': 'docx', - 'doc': 'doc', - 'odt': 'odt', - 'rtf': 'rtf', - 'txt': 'txt', - 'html': 'html', - 'xlsx': 'xlsx', - 'xls': 'xls', - 'ods': 'ods', - 'csv': 'csv', - 'pptx': 'pptx', - 'ppt': 'ppt', - 'odp': 'odp' - }; - - const format = formatMap[outputFormat] || outputFormat; - - // Return command and args for execFile - return { - command: soffice, - args: ['--headless', '--convert-to', format, '--outdir', outputDir, inputFile] - }; -} - -// ImageMagick conversion - returns {command, args} for execFile -function convertWithImageMagick(inputFile, outputPath) { - const magick = process.platform === 'win32' ? 'magick' : 'convert'; - return { - command: magick, - args: [inputFile, outputPath] - }; -} - -// FFmpeg conversion - returns {command, args} for execFile -function convertWithFFmpeg(inputFile, outputPath) { - return { - command: 'ffmpeg', - args: ['-i', inputFile, outputPath, '-y'] - }; -} - -// Pandoc conversion - returns {command, args} for execFile -function convertWithPandoc(inputFile, outputPath) { - return { - command: 'pandoc', - args: [inputFile, '-o', outputPath] - }; -} - -function performExportWithOptions(format, options) { - const outputFile = dialog.showSaveDialogSync(mainWindow, { - defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`), - filters: [ - { name: format.toUpperCase(), extensions: [format] } - ] - }); - - if (!outputFile) return; // User cancelled - - console.log(`Attempting to export ${format} to:`, outputFile); - - // Check pandoc availability first - checkPandocAvailability().then((hasPandoc) => { - console.log('Pandoc available:', hasPandoc); - - if (!hasPandoc) { - // Handle formats that don't require pandoc - if (format === 'html') { - console.log('Using built-in HTML export'); - exportToHTML(outputFile); - return; - } else if (format === 'pdf') { - console.log('Using built-in PDF export'); - exportToPDFElectron(outputFile); - return; - } else { - dialog.showErrorBox('Export Error', - `Pandoc is required for ${format.toUpperCase()} export but is not installed or not found in PATH.\n\n` + - `Please install Pandoc from: https://pandoc.org/installing.html\n\n` + - `Alternatively, you can export to HTML or PDF using the built-in converters.` - ); - return; - } - } - - // Use pandoc for export with advanced options - console.log('Using Pandoc for export'); - let pandocCmd = `${getPandocPath()} "${currentFile}" -o "${outputFile}"`; - - // Add template if specified - if (options.template && options.template !== 'default') { - pandocCmd += ` --template="${options.template}"`; - } - - // Add metadata - if (options.metadata) { - for (const [key, value] of Object.entries(options.metadata)) { - if (value.trim()) { - pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; - } - } - } - - // Add variables - if (options.variables) { - for (const [key, value] of Object.entries(options.variables)) { - if (value.trim()) { - pandocCmd += ` -V ${key}="${value.replace(/"/g, '\\"')}"`; - } - } - } - - // Add other options - if (options.toc) pandocCmd += ' --toc'; - if (options.tocDepth) pandocCmd += ` --toc-depth=${options.tocDepth}`; - if (options.numberSections) pandocCmd += ' --number-sections'; - if (options.citeproc) pandocCmd += ' --citeproc'; - if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`; - if (options.csl) pandocCmd += ` --csl="${options.csl}"`; - - // Add specific options for PDF export to ensure proper generation - if (format === 'pdf') { - const pdfEngine = options.pdfEngine || 'xelatex'; // Default to xelatex - pandocCmd += ` --pdf-engine="${pdfEngine}"`; - if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; - - // Add monospace font settings for code blocks (ASCII art preservation) - pandocCmd += ' -V monofont="Consolas"'; - pandocCmd += ' --highlight-style=tango'; - - // Add header/footer if enabled - if (headerFooterSettings.enabled) { - const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; - const metadata = { filename, title: filename, author: '' }; - - const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); - const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); - const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); - const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); - const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); - const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); - - // Create LaTeX header - const latexHeader = ` -\\usepackage{fancyhdr} -\\pagestyle{fancy} -\\fancyhf{} -\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} -\\chead{${headerCenter.replace(/\\/g, '\\\\')}} -\\rhead{${headerRight.replace(/\\/g, '\\\\')}} -\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} -\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} -\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} -\\renewcommand{\\headrulewidth}{0.4pt} -\\renewcommand{\\footrulewidth}{0.4pt} -`; - const headerFile = path.join(require('os').tmpdir(), `header_export_${Date.now()}.tex`); - fs.writeFileSync(headerFile, latexHeader, 'utf-8'); - pandocCmd += ` --include-in-header="${headerFile}"`; - pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"'; - } - - // Try with specified PDF engine (using runPandocCmd for safety) - runPandocCmd(pandocCmd, (error) => { - if (error) { - // Try fallback engines if the specified one fails - const fallbackEngines = ['pdflatex', 'lualatex']; - tryPdfFallback(currentFile, outputFile, fallbackEngines, 0, options, error); - } else { - showExportSuccess(outputFile); - } - }); - } else if (format === 'docx') { - pandocCmd += ' -t docx'; - exportWithPandoc(pandocCmd, outputFile, format); - } else if (format === 'pptx') { - // Add PowerPoint footer if enabled - if (headerFooterSettings.enabled && headerFooterSettings.footer.center) { - const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; - const metadata = { filename, title: filename, author: '' }; - const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata); - pandocCmd += ` --variable footer="${footerText}"`; - } - exportWithPandoc(pandocCmd, outputFile, format); - } else { - // Generic export for other formats - exportWithPandoc(pandocCmd, outputFile, format); - } - }).catch((error) => { - console.error('Error checking pandoc availability:', error); - dialog.showErrorBox('Export Error', `Error checking system requirements: ${error.message}`); - }); -} - -function tryPdfFallback(inputFile, outputFile, engines, index, options, lastError) { - if (index >= engines.length) { - // All Pandoc PDF engines failed, fallback to Electron's built-in PDF export - console.log('All Pandoc PDF engines failed, falling back to Electron PDF export'); - exportToPDFElectron(outputFile); - return; - } - - const engine = engines[index]; - let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -o "${outputFile}"`; - - // Add monospace font settings for code blocks (ASCII art preservation) - pandocCmd += ' -V monofont="Consolas"'; - pandocCmd += ' --highlight-style=tango'; - - // Add geometry if specified - if (options.geometry) pandocCmd = pandocCmd.replace(` -o `, ` -V geometry:"${options.geometry}" -o `); - - // Add header/footer if enabled - if (headerFooterSettings.enabled) { - const filename = path.basename(inputFile, path.extname(inputFile)); - const metadata = { filename, title: filename, author: options.metadata?.author || '' }; - - const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); - const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); - const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); - const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); - const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); - const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); - - // Create LaTeX header for fancyhdr - const latexHeader = ` -\\usepackage{fancyhdr} -\\usepackage{lastpage} -\\pagestyle{fancy} -\\fancyhf{} -\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} -\\chead{${headerCenter.replace(/\\/g, '\\\\')}} -\\rhead{${headerRight.replace(/\\/g, '\\\\')}} -\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} -\\cfoot{${footerCenter.replace(/\$PAGE\$/g, '\\\\thepage').replace(/\$TOTAL\$/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} -\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} -\\renewcommand{\\headrulewidth}{0.4pt} -\\renewcommand{\\footrulewidth}{0.4pt} -`; - const headerFile = path.join(require('os').tmpdir(), `header_fallback_${Date.now()}.tex`); - fs.writeFileSync(headerFile, latexHeader, 'utf-8'); - pandocCmd += ` --include-in-header="${headerFile}"`; - } - - // Add all other options - if (options.template && options.template !== 'default') { - pandocCmd += ` --template="${options.template}"`; - } - - if (options.metadata) { - for (const [key, value] of Object.entries(options.metadata)) { - if (value.trim()) { - pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; - } - } - } - - // Use runPandocCmd for safety (prevents command injection) - runPandocCmd(pandocCmd, (error) => { - if (error) { - tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error); - } else { - showExportSuccess(outputFile); - } - }); -} - -function showExportSuccess(outputFile) { - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Export Complete', - message: `File exported successfully to ${outputFile}`, - buttons: ['OK'] - }); -} - -// Helper function to export with pandoc (general) - uses runPandocCmd for safety -function exportWithPandoc(pandocCmd, outputFile, format) { - console.log(`Executing Pandoc command: ${pandocCmd}`); - - runPandocCmd(pandocCmd, async (error, stdout, stderr) => { - if (error) { - console.error(`Pandoc error for ${format}:`, error); - console.error(`Pandoc stderr:`, stderr); - console.error(`Pandoc stdout:`, stdout); - - // Provide more specific error messages - let errorMessage = `Failed to export to ${format.toUpperCase()}`; - - if (error.message.includes('not found') || error.message.includes('not recognized')) { - errorMessage += '\n\nPandoc is not installed or not found in PATH.'; - errorMessage += '\nPlease install Pandoc from: https://pandoc.org/installing.html'; - } else if (stderr) { - errorMessage += `\n\nError details: ${stderr}`; - } else { - errorMessage += `\n\nError details: ${error.message}`; - } - - errorMessage += `\n\nCommand used: ${pandocCmd}`; - - dialog.showErrorBox('Export Error', errorMessage); - } else { - console.log(`Successfully exported to ${format}:`, outputFile); - console.log(`Pandoc stdout:`, stdout); - if (stderr) { - console.warn(`Pandoc stderr (non-fatal):`, stderr); - } - - // Set page size for DOCX - if (format === 'docx') { - try { - await setDocxPageSize(outputFile); - console.log('Page size set for DOCX'); - } catch (pageSizeError) { - console.error('Error setting page size for DOCX:', pageSizeError); - } - } - - // Add headers/footers to DOCX if enabled - if (format === 'docx' && headerFooterSettings.enabled) { - try { - const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; - const metadata = { - filename: filename, - title: filename, - author: '' - }; - await addHeaderFooterToDocx(outputFile, metadata); - console.log('Headers/footers added to DOCX'); - } catch (hfError) { - console.error('Error adding headers/footers to DOCX:', hfError); - // Continue with success message even if header/footer fails - } - } - - // Set page size for ODT - if (format === 'odt') { - try { - await setDocxPageSize(outputFile); // ODT has similar structure - console.log('Page size set for ODT'); - } catch (pageSizeError) { - console.error('Error setting page size for ODT:', pageSizeError); - } - } - - // Add headers/footers to ODT if enabled - if (format === 'odt' && headerFooterSettings.enabled) { - // ODT format is similar to DOCX in structure, we could implement this - console.log('ODT header/footer support not yet implemented'); - } - - showExportSuccess(outputFile); - } - }); -} - -// Helper function to export PDF with pandoc (with fallbacks) - uses runPandocCmd for safety -function exportWithPandocPDF(pandocCmd, outputFile) { - runPandocCmd(pandocCmd, (error, stdout, stderr) => { - if (error) { - console.log('XeLaTeX failed, trying PDFLaTeX...'); - // Fallback to pdflatex - const fallbackCmd = pandocCmd.replace('--pdf-engine=xelatex', '--pdf-engine=pdflatex'); - runPandocCmd(fallbackCmd, (fallbackError, fallbackStdout, fallbackStderr) => { - if (fallbackError) { - console.log('PDFLaTeX failed, trying Electron PDF...'); - // Final fallback to Electron PDF - exportToPDFElectron(outputFile); - } else { - console.log('Successfully exported PDF with PDFLaTeX'); - showExportSuccess(outputFile); - } - }); - } else { - console.log('Successfully exported PDF with XeLaTeX'); - showExportSuccess(outputFile); - } - }); -} - -// Export to HTML using marked (no pandoc required) -function exportToHTML(outputFile) { - try { - const marked = require('marked'); - const markdownContent = fs.readFileSync(currentFile, 'utf8'); - const htmlContent = marked.parse(markdownContent); - - const fullHtml = ` - - - - - Exported Document - - - - ${htmlContent} - -`; - - fs.writeFileSync(outputFile, fullHtml, 'utf8'); - console.log('Successfully exported HTML'); - showExportSuccess(outputFile); - } catch (error) { - console.error('HTML export error:', error); - dialog.showErrorBox('HTML Export Error', `Failed to export HTML: ${error.message}`); - } -} - -// Export to PDF using Electron (no pandoc required) -function exportToPDFElectron(outputFile) { - try { - const marked = require('marked'); - const markdownContent = fs.readFileSync(currentFile, 'utf8'); - const htmlContent = marked.parse(markdownContent); - - const fullHtml = ` - - - - - PDF Export - - - - ${htmlContent} - -`; - - // Create a hidden window to render and export PDF - const pdfWindow = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true, - contextIsolation: false - } - }); - - pdfWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(fullHtml)}`).then(() => { - return pdfWindow.webContents.printToPDF({ - marginsType: 1, // Use default margins - pageSize: 'A4', - printBackground: true, - printSelectionOnly: false, - landscape: false - }); - }).then((pdfData) => { - fs.writeFileSync(outputFile, pdfData); - pdfWindow.close(); - console.log('Successfully exported PDF with Electron'); - showExportSuccess(outputFile); - }).catch((error) => { - pdfWindow.close(); - console.error('Electron PDF export error:', error); - dialog.showErrorBox('PDF Export Error', - `Failed to export PDF using built-in engine: ${error.message}\n\n` + - `For better PDF export, please install Pandoc with LaTeX support.` - ); - }); - } catch (error) { - console.error('PDF export setup error:', error); - dialog.showErrorBox('PDF Export Error', `Failed to setup PDF export: ${error.message}`); - } -} - -function exportSpreadsheet(format) { - if (!currentFile) { - dialog.showErrorBox('Error', 'Please save the file first'); - return; - } - - // Request content from renderer - mainWindow.webContents.send('get-content-for-spreadsheet', format); -} - -function importDocument() { - const files = dialog.showOpenDialogSync(mainWindow, { - properties: ['openFile'], - filters: [ - { name: 'Documents', extensions: ['docx', 'odt', 'rtf', 'html', 'htm', 'tex', 'epub', 'pdf', 'txt'] }, - { name: 'Presentations', extensions: ['pptx', 'odp'] }, - { name: 'Markup Languages', extensions: ['rst', 'textile', 'mediawiki', 'org', 'asciidoc', 'twiki', 'opml'] }, - { name: 'E-book Formats', extensions: ['epub', 'fb2'] }, - { name: 'LaTeX Formats', extensions: ['tex', 'latex', 'ltx'] }, - { name: 'Web Formats', extensions: ['html', 'htm', 'xhtml'] }, - { name: 'Wiki Formats', extensions: ['mediawiki', 'dokuwiki', 'tikiwiki', 'twiki'] }, - { name: 'CSV/TSV', extensions: ['csv', 'tsv'] }, - { name: 'JSON', extensions: ['json'] }, - { name: 'All Files', extensions: ['*'] } - ] - }); - - if (files && files[0]) { - const inputFile = files[0]; - const ext = path.extname(inputFile).toLowerCase().slice(1); - const outputFile = inputFile.replace(/\.[^/.]+$/, '.md'); - - // Determine format-specific conversion options - let additionalOptions = ''; - - // For PDFs, extract text properly - if (ext === 'pdf') { - additionalOptions = '--pdf-engine=xelatex'; - } - - // For CSV/TSV, convert as tables - if (ext === 'csv' || ext === 'tsv') { - additionalOptions = '--from=csv -t markdown'; - } - - // For JSON, handle structure - if (ext === 'json') { - additionalOptions = '--from=json -t markdown'; - } - - // Convert to markdown using pandoc (using runPandocCmd for safety) - const pandocCmd = `${getPandocPath()} "${inputFile}" -t markdown ${additionalOptions} -o "${outputFile}"`; - - runPandocCmd(pandocCmd, (error, stdout, stderr) => { - if (error) { - dialog.showErrorBox('Import Error', `Failed to import: ${error.message}\n\nMake sure Pandoc is installed.\n\nSupported formats: DOCX, ODT, RTF, HTML, LaTeX, EPUB, PDF, PPTX, ODP, RST, Textile, MediaWiki, Org-mode, AsciiDoc, CSV, and more.`); - } else { - // Open the converted markdown file - currentFile = outputFile; - const content = fs.readFileSync(outputFile, 'utf-8'); - mainWindow.webContents.send('file-opened', { path: outputFile, content }); - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Import Complete', - message: `Document imported successfully as ${path.basename(outputFile)}\n\nOriginal format: ${ext.toUpperCase()}\nConverted to: Markdown`, - buttons: ['OK'] - }); - } - }); - } -} - - -function setTheme(theme) { - store.set('theme', theme); - mainWindow.webContents.send('theme-changed', theme); -} - -// IPC handlers -ipcMain.on('save-file', (event, { path, content }) => { - fs.writeFileSync(path, content, 'utf-8'); - currentFile = path; -}); - -ipcMain.on('save-current-file', (event, content) => { - if (currentFile) { - fs.writeFileSync(currentFile, content, 'utf-8'); - } else { - saveAsFile(); - } -}); - -ipcMain.on('get-theme', (event) => { - const theme = store.get('theme', 'atomonelight'); - event.reply('theme-changed', theme); -}); - -// Handle tab file tracking for exports -ipcMain.on('set-current-file', (event, filePath) => { - currentFile = filePath; -}); - -// Handle actual printing when renderer is ready -ipcMain.on('do-print', (event, { withStyles }) => { - if (mainWindow) { - // Renderer has already hidden UI, waited 300ms, and prepared the page - // Print immediately - DOM is fully rendered - mainWindow.webContents.print({ - silent: false, - printBackground: withStyles, - color: true, - margin: { marginType: 'default' } - }); - } -}); - -// Handle renderer ready for file association -ipcMain.on('renderer-ready', (event) => { - console.log('[MAIN] renderer-ready received, rendererReady was:', rendererReady); - if (mainWindow) { - mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] renderer-ready received, rendererReady was: ${rendererReady}')`); - } - rendererReady = true; - console.log('[MAIN] app.pendingFile:', app.pendingFile); - if (mainWindow) { - mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] app.pendingFile: ${app.pendingFile}')`); - } - if (app.pendingFile) { - console.log('[MAIN] Opening pending file:', app.pendingFile); - if (mainWindow) { - mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] Opening pending file: ${app.pendingFile}')`); - } - openFileFromPath(app.pendingFile); - app.pendingFile = null; - } -}); - -// Handle export with options -ipcMain.on('export-with-options', (event, { format, options }) => { - performExportWithOptions(format, options); -}); - -// Handle batch conversion -ipcMain.on('batch-convert', (event, { inputFolder, outputFolder, format, options }) => { - performBatchConversion(inputFolder, outputFolder, format, options); -}); - -// Handle folder selection for batch conversion -ipcMain.on('select-folder', (event, type) => { - const folder = dialog.showOpenDialogSync(mainWindow, { - properties: ['openDirectory'] - }); - - if (folder && folder[0]) { - event.reply('folder-selected', { type, path: folder[0] }); - } -}); - -ipcMain.on('export-spreadsheet', (event, { content, format }) => { - const outputFile = dialog.showSaveDialogSync(mainWindow, { - defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`), - filters: [ - { name: format.toUpperCase(), extensions: [format] } - ] - }); - - if (outputFile) { - try { - // Parse markdown content to extract tables - const tables = extractTablesFromMarkdown(content); - - if (tables.length === 0) { - dialog.showErrorBox('Export Error', 'No tables found in the markdown content'); - return; - } - - if (format === 'csv') { - // Convert tables to CSV format - let csvContent = ''; - tables.forEach((table, index) => { - if (index > 0) csvContent += '\n\n'; // Separate multiple tables - if (tables.length > 1) csvContent += `"Table ${index + 1}"\n`; - - table.forEach(row => { - const csvRow = row.map(cell => { - // Escape quotes and wrap in quotes if necessary - const cleanCell = cell.replace(/"/g, '""'); - return cleanCell.includes(',') || cleanCell.includes('"') || cleanCell.includes('\n') - ? `"${cleanCell}"` : cleanCell; - }).join(','); - csvContent += csvRow + '\n'; - }); - }); - - fs.writeFileSync(outputFile, csvContent, 'utf-8'); - } - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Export Complete', - message: `${format.toUpperCase()} exported successfully to ${outputFile}`, - buttons: ['OK'] - }); - } catch (error) { - dialog.showErrorBox('Export Error', `Failed to export: ${error.message}`); - } - } -}); - -// Helper function to extract tables from markdown -function extractTablesFromMarkdown(markdown) { - const tables = []; - const lines = markdown.split('\n'); - let currentTable = []; - let inTable = false; - - for (const line of lines) { - if (line.includes('|')) { - if (!inTable) { - inTable = true; - currentTable = []; - } - - // Skip separator lines (|---|---|) - if (!line.match(/^\s*\|?\s*:?-+:?\s*\|/)) { - const cells = line.split('|') - .map(cell => cell.trim()) - .filter(cell => cell !== ''); - - if (cells.length > 0) { - currentTable.push(cells); - } - } - } else if (inTable && line.trim() === '') { - // End of table - if (currentTable.length > 0) { - tables.push(currentTable); - } - currentTable = []; - inTable = false; - } - } - - // Add last table if exists - if (currentTable.length > 0) { - tables.push(currentTable); - } - - return tables; -} - -function performBatchConversion(inputFolder, outputFolder, format, options) { - if (!fs.existsSync(inputFolder)) { - dialog.showErrorBox('Error', 'Input folder does not exist'); - return; - } - - // Create output folder if it doesn't exist - if (!fs.existsSync(outputFolder)) { - try { - fs.mkdirSync(outputFolder, { recursive: true }); - } catch (error) { - dialog.showErrorBox('Error', `Failed to create output folder: ${error.message}`); - return; - } - } - - // Find all markdown files in input folder - const markdownFiles = []; - - function findMarkdownFiles(dir) { - const files = fs.readdirSync(dir); - - for (const file of files) { - const fullPath = path.join(dir, file); - const stat = fs.statSync(fullPath); - - if (stat.isDirectory()) { - findMarkdownFiles(fullPath); // Recursive search - } else if (file.match(/\.(md|markdown)$/i)) { - markdownFiles.push(fullPath); - } - } - } - - findMarkdownFiles(inputFolder); - - if (markdownFiles.length === 0) { - dialog.showErrorBox('No Files Found', 'No markdown files found in the selected folder'); - return; - } - - // Show progress dialog - let completedCount = 0; - const totalCount = markdownFiles.length; - - // Process each file - const processNextFile = async (index) => { - if (index >= markdownFiles.length) { - // All files processed - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Batch Conversion Complete', - message: `Successfully converted ${completedCount} out of ${totalCount} files to ${format.toUpperCase()}.`, - buttons: ['OK'] - }); - return; - } - - const inputFile = markdownFiles[index]; - const relativePath = path.relative(inputFolder, inputFile); - const baseName = path.basename(relativePath, path.extname(relativePath)); - let outputExtension = format; - if (format === 'docx-enhanced') outputExtension = 'docx'; - if (format === 'pdf-enhanced') outputExtension = 'pdf'; - const outputFile = path.join(outputFolder, relativePath.replace(/\.(md|markdown)$/i, `.${outputExtension}`)); - - // Create subdirectories in output folder if needed - const outputDir = path.dirname(outputFile); - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } - - // Handle DOCX Enhanced format with WordTemplateExporter - if (format === 'docx-enhanced') { - try { - const content = fs.readFileSync(inputFile, 'utf-8'); - const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage); - await exporter.convert(content, outputFile); - - completedCount++; - - // Update progress - mainWindow.webContents.send('batch-progress', { - completed: index + 1, - total: totalCount, - currentFile: path.basename(inputFile), - success: true - }); - - // Process next file - processNextFile(index + 1); - } catch (error) { - // Update progress with error - mainWindow.webContents.send('batch-progress', { - completed: index + 1, - total: totalCount, - currentFile: path.basename(inputFile), - success: false - }); - - // Process next file even if this one failed - processNextFile(index + 1); - } - return; - } - - // Handle PDF Enhanced format with Word Template → PDF conversion - if (format === 'pdf-enhanced') { - try { - const content = fs.readFileSync(inputFile, 'utf-8'); - - // Step 1: Create temporary DOCX file using Word template - const tempDocxPath = outputFile.replace(/\.pdf$/, '_temp.docx'); - const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage); - await exporter.convert(content, tempDocxPath); - - // Step 2: Convert DOCX to PDF using LibreOffice (using execFile for safety) - const soffice = process.platform === 'win32' - ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' - : 'soffice'; - - const outputDir = path.dirname(outputFile); - const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath]; - - execFile(soffice, sofficeArgs, (error, stdout, stderr) => { - // Clean up temporary DOCX file - try { - fs.unlinkSync(tempDocxPath); - } catch (e) { - console.error('Failed to delete temp file:', e); - } - - if (error) { - // Update progress with error - mainWindow.webContents.send('batch-progress', { - completed: index + 1, - total: totalCount, - currentFile: path.basename(inputFile), - success: false - }); - processNextFile(index + 1); - return; - } - - // LibreOffice creates file with same base name as input - const generatedPdfPath = tempDocxPath.replace(/\.docx$/, '.pdf'); - - // Rename if needed - if (generatedPdfPath !== outputFile) { - try { - fs.renameSync(generatedPdfPath, outputFile); - } catch (e) { - console.error('Failed to rename PDF:', e); - } - } - - completedCount++; - - // Update progress - mainWindow.webContents.send('batch-progress', { - completed: index + 1, - total: totalCount, - currentFile: path.basename(inputFile), - success: true - }); - - // Process next file - processNextFile(index + 1); - }); - - } catch (error) { - // Update progress with error - mainWindow.webContents.send('batch-progress', { - completed: index + 1, - total: totalCount, - currentFile: path.basename(inputFile), - success: false - }); - - // Process next file even if this one failed - processNextFile(index + 1); - } - return; - } - - // Build pandoc command for other formats - let pandocCmd = `${getPandocPath()} "${inputFile}" -o "${outputFile}"`; - - // Add template if specified - if (options.template && options.template !== 'default') { - pandocCmd += ` --template="${options.template}"`; - } - - // Add metadata - if (options.metadata) { - for (const [key, value] of Object.entries(options.metadata)) { - if (value.trim()) { - pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; - } - } - } - - // Add variables - if (options.variables) { - for (const [key, value] of Object.entries(options.variables)) { - if (value.trim()) { - pandocCmd += ` -V ${key}="${value.replace(/"/g, '\\"')}"`; - } - } - } - - // Add other options - if (options.toc) pandocCmd += ' --toc'; - if (options.tocDepth) pandocCmd += ` --toc-depth=${options.tocDepth}`; - if (options.numberSections) pandocCmd += ' --number-sections'; - if (options.citeproc) pandocCmd += ' --citeproc'; - if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`; - if (options.csl) pandocCmd += ` --csl="${options.csl}"`; - - // Add PDF-specific options with header/footer support - if (format === 'pdf') { - const pdfEngine = options.pdfEngine || 'xelatex'; - pandocCmd += ` --pdf-engine=${pdfEngine}`; - if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; - - // Add monospace font settings for code blocks (ASCII art preservation) - pandocCmd += ' -V monofont="Consolas"'; - pandocCmd += ' --highlight-style=tango'; - - // Add header/footer if enabled - if (headerFooterSettings.enabled) { - const filename = path.basename(inputFile, path.extname(inputFile)); - const metadata = { filename, title: filename, author: '' }; - - const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); - const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); - const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); - const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); - const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); - const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); - - // Create LaTeX header - const latexHeader = ` -\\usepackage{fancyhdr} -\\pagestyle{fancy} -\\fancyhf{} -\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} -\\chead{${headerCenter.replace(/\\/g, '\\\\')}} -\\rhead{${headerRight.replace(/\\/g, '\\\\')}} -\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} -\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} -\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} -\\renewcommand{\\headrulewidth}{0.4pt} -\\renewcommand{\\footrulewidth}{0.4pt} -`; - const headerFile = path.join(require('os').tmpdir(), `header_batch_${Date.now()}.tex`); - fs.writeFileSync(headerFile, latexHeader, 'utf-8'); - pandocCmd += ` --include-in-header="${headerFile}"`; - pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"'; - } - } - - // Add DOCX-specific handling - if (format === 'docx') { - pandocCmd += ' -t docx'; - } - - // Add PowerPoint footer if enabled - if (format === 'pptx' && headerFooterSettings.enabled && headerFooterSettings.footer.center) { - const filename = path.basename(inputFile, path.extname(inputFile)); - const metadata = { filename, title: filename, author: '' }; - const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata); - pandocCmd += ` --variable footer="${footerText}"`; - } - - // Execute conversion (using runPandocCmd for safety) - runPandocCmd(pandocCmd, async (error, stdout, stderr) => { - if (!error) { - // Add headers/footers to DOCX if enabled - if (format === 'docx' && headerFooterSettings.enabled) { - try { - const filename = path.basename(inputFile, path.extname(inputFile)); - const metadata = { filename, title: filename, author: '' }; - await addHeaderFooterToDocx(outputFile, metadata); - } catch (hfError) { - console.error('Batch: Error adding headers/footers to DOCX:', hfError); - } - } - completedCount++; - } - - // Update progress (you could send this to renderer for a progress bar) - mainWindow.webContents.send('batch-progress', { - completed: index + 1, - total: totalCount, - currentFile: path.basename(inputFile), - success: !error - }); - - // Process next file - processNextFile(index + 1); - }); - }; - - // Start processing - processNextFile(0); -} - -// Handle command line interface for file conversion -function handleCLIConversion(args) { - const command = args[0]; - const filePath = args[args.length - 1]; // File path is always last argument - - if (!fs.existsSync(filePath)) { - console.error(`Error: File not found: ${filePath}`); - app.quit(); - return; - } - - // Show conversion dialog for --convert command - if (command === '--convert') { - showConversionDialog(filePath); - return; - } - - // Direct conversion for --convert-to command - if (command === '--convert-to' && args.length >= 3) { - const format = args[1]; - performCLIConversion(filePath, format); - return; - } - - console.error('Usage: --convert OR --convert-to '); - app.quit(); -} - -// Show conversion dialog for CLI -function showConversionDialog(filePath) { - const { dialog } = require('electron'); - - // Create a hidden window for dialog operations - const hiddenWindow = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true, - contextIsolation: false - } - }); - - const formats = [ - { name: 'PDF', value: 'pdf' }, - { name: 'HTML', value: 'html' }, - { name: 'DOCX', value: 'docx' }, - { name: 'LaTeX', value: 'latex' }, - { name: 'RTF', value: 'rtf' }, - { name: 'ODT', value: 'odt' }, - { name: 'PowerPoint', value: 'pptx' } - ]; - - // Create format selection dialog using message box - const formatButtons = formats.map(f => f.name); - formatButtons.push('Cancel'); - - dialog.showMessageBox(hiddenWindow, { - type: 'question', - title: 'PanConverter - Choose Format', - message: `Convert "${path.basename(filePath)}" to:`, - detail: 'Select the output format for conversion', - buttons: formatButtons, - defaultId: 0, - cancelId: formatButtons.length - 1 - }).then(result => { - if (result.response < formats.length) { - const selectedFormat = formats[result.response].value; - performCLIConversion(filePath, selectedFormat); - } else { - console.log('Conversion cancelled'); - app.quit(); - } - hiddenWindow.destroy(); - }); -} - -// Perform CLI conversion -function performCLIConversion(inputPath, format) { - try { - const content = fs.readFileSync(inputPath, 'utf-8'); - const outputPath = inputPath.replace(/\.[^/.]+$/, `.${format}`); - - console.log(`Converting "${path.basename(inputPath)}" to ${format.toUpperCase()}...`); - - // Use existing export functions but with CLI output (using runPandocCmd for safety) - const pandocCommand = buildPandocCommand(content, format, outputPath); - - runPandocCmd(pandocCommand, (error, stdout, stderr) => { - if (error) { - console.error(`Conversion failed: ${error.message}`); - if (stderr) console.error(`Details: ${stderr}`); - app.quit(); - return; - } - - console.log(`Successfully converted to: ${outputPath}`); - - // Show Windows notification (using exec for PowerShell is acceptable here - hardcoded command) - if (process.platform === 'win32') { - const iconPath = path.join(__dirname, '../assets/icon.png'); - execFile('powershell', ['-Command', `New-BurntToastNotification -Text 'PanConverter', 'File converted to ${format.toUpperCase()}' -AppLogo '${iconPath}'`], () => {}); - } - - app.quit(); - }); - } catch (error) { - console.error(`Error reading file: ${error.message}`); - app.quit(); - } -} - -// Build Pandoc command for CLI conversion -function buildPandocCommand(content, format, outputPath) { - const inputFile = path.join(require('os').tmpdir(), `panconverter_temp_${Date.now()}.md`); - fs.writeFileSync(inputFile, content, 'utf-8'); - - let command = `pandoc "${inputFile}" -o "${outputPath}"`; - - // Get metadata for dynamic fields - const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; - const metadata = { - filename: filename, - title: filename, - author: '', - }; - - switch (format) { - case 'pdf': - command += ' --pdf-engine=xelatex --variable geometry:margin=1in'; - - // Add page size and orientation - const pageSize = PAGE_SIZES[pageSettings.size]; - if (pageSize) { - command += ` -V geometry:papersize=${pageSize.pandoc}`; - } else if (pageSettings.customWidth && pageSettings.customHeight) { - // Custom page size - command += ` -V geometry:paperwidth=${pageSettings.customWidth}`; - command += ` -V geometry:paperheight=${pageSettings.customHeight}`; - } - - // Add orientation - if (pageSettings.orientation === 'landscape') { - command += ' -V geometry:landscape'; - } - - // Add monospace font settings for code blocks (ASCII art preservation) - command += ' -V monofont="Consolas"'; - command += ' --highlight-style=tango'; - - // Add header/footer if enabled - if (headerFooterSettings.enabled) { - // Process dynamic fields - const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); - const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); - const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); - const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); - const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); - const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); - - // Add Pandoc variables for fancyhdr package - if (headerLeft) command += ` --variable header-left="${headerLeft}"`; - if (headerCenter) command += ` --variable header-center="${headerCenter}"`; - if (headerRight) command += ` --variable header-right="${headerRight}"`; - if (footerLeft) command += ` --variable footer-left="${footerLeft}"`; - if (footerCenter) command += ` --variable footer-center="${footerCenter}"`; - if (footerRight) command += ` --variable footer-right="${footerRight}"`; - - // Create custom LaTeX header with fancyhdr - const latexHeader = ` -\\usepackage{fancyhdr} -\\pagestyle{fancy} -\\fancyhf{} -\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} -\\chead{${headerCenter.replace(/\\/g, '\\\\')}} -\\rhead{${headerRight.replace(/\\/g, '\\\\')}} -\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} -\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} -\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} -\\renewcommand{\\headrulewidth}{0.4pt} -\\renewcommand{\\footrulewidth}{0.4pt} -`; - const headerFile = path.join(require('os').tmpdir(), `header_${Date.now()}.tex`); - fs.writeFileSync(headerFile, latexHeader, 'utf-8'); - command += ` --include-in-header="${headerFile}"`; - - // Add lastpage package for $TOTAL$ support - command += ' --variable header-includes="\\\\usepackage{lastpage}"'; - } - break; - - case 'html': - command += ' --self-contained --css'; - break; - - case 'docx': - command += ' --reference-doc'; - - // For DOCX, header/footer are handled via reference document or separate processing - // We'll add a note that DOCX headers/footers require reference doc or post-processing - break; - - case 'odt': - // ODT headers/footers are handled via reference document - break; - - case 'latex': - command += ' --standalone'; - break; - - case 'pptx': - command += ' --slide-level=2'; - // PowerPoint footer can be added with --variable - if (headerFooterSettings.enabled && headerFooterSettings.footer.center) { - const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata); - command += ` --variable footer="${footerText}"`; - } - break; - } - - return command; -} - -app.whenReady().then(() => { - // Load saved Word template path and settings - wordTemplatePath = store.get('wordTemplatePath', null); - templateStartPage = store.get('templateStartPage', 3); - - // Load header/footer settings - const savedHFSettings = store.get('headerFooterSettings', null); - if (savedHFSettings) { - headerFooterSettings = savedHFSettings; - } - - // Load page size settings - const savedPageSettings = store.get('pageSettings', null); - if (savedPageSettings) { - pageSettings = savedPageSettings; - } - - // Check for command line conversion requests - const args = process.argv.slice(2); - if (args.length >= 2 && (args[0] === '--convert' || args[0] === '--convert-to')) { - handleCLIConversion(args); - return; // Don't create window for CLI operations - } - - createWindow(); - - // Handle file association on app startup - // In packaged apps, process.argv structure is different: - // Development: ['electron', 'app.js', 'file.md'] - need slice(2) - // Packaged: ['PanConverter.exe', 'file.md'] - need slice(1) - // We'll check all arguments after the executable - console.log('[MAIN] Full command line args:', JSON.stringify(process.argv)); - console.log('[MAIN] App is packaged:', app.isPackaged); - - // Start from index 1 (skip executable) and check each argument - const startIndex = app.isPackaged ? 1 : 2; - const fileArgs = process.argv.slice(startIndex); - console.log('[MAIN] File args to process (starting from index', startIndex + '):', fileArgs); - - for (const arg of fileArgs) { - console.log('[MAIN] Checking arg:', arg); - if ((arg.endsWith('.md') || arg.endsWith('.markdown'))) { - // Try to resolve the path (might be relative) - const resolvedPath = path.isAbsolute(arg) ? arg : path.resolve(process.cwd(), arg); - console.log('[MAIN] Resolved path:', resolvedPath); - if (fs.existsSync(resolvedPath)) { - // Store the file to open after window is ready - console.log('[MAIN] Setting pendingFile to:', resolvedPath); - app.pendingFile = resolvedPath; - break; - } else { - console.log('[MAIN] File does not exist:', resolvedPath); - } - } - } - console.log('[MAIN] Final app.pendingFile:', app.pendingFile); -}); - -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit(); - } -}); - -app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); - } -}); - -// IPC handlers for recent files -ipcMain.on('save-recent-files', (event, recentFiles) => { - try { - const userDataPath = app.getPath('userData'); - const recentFilesPath = path.join(userDataPath, 'recent-files.json'); - fs.writeFileSync(recentFilesPath, JSON.stringify(recentFiles, null, 2)); - } catch (error) { - console.error('Error saving recent files:', error); - } -}); - -ipcMain.on('clear-recent-files', (event) => { - try { - const userDataPath = app.getPath('userData'); - const recentFilesPath = path.join(userDataPath, 'recent-files.json'); - fs.writeFileSync(recentFilesPath, JSON.stringify([], null, 2)); - // Rebuild menu to reflect changes - createMenu(); - event.reply('recent-files-cleared'); - } catch (error) { - console.error('Error clearing recent files:', error); - } -}); - -// Handle file opening on macOS -app.on('open-file', (event, filePath) => { - event.preventDefault(); - if (mainWindow && rendererReady) { - openFileFromPath(filePath); - } else { - // Store the file path to open after window and renderer are ready - app.pendingFile = filePath; - } -}); - -// Handle file opening from command line or file association -function openFileFromPath(filePath) { - console.log('[MAIN] openFileFromPath called with:', filePath); - console.log('[MAIN] rendererReady:', rendererReady, 'mainWindow exists:', !!mainWindow); - if (fs.existsSync(filePath)) { - currentFile = filePath; - const content = fs.readFileSync(filePath, 'utf-8'); - console.log('[MAIN] File read successfully, content length:', content.length); - if (mainWindow && mainWindow.webContents && rendererReady) { - // Send file immediately - renderer-ready means UI is initialized - console.log('[MAIN] Sending file-opened to renderer'); - mainWindow.webContents.send('file-opened', { path: filePath, content }); - } else { - // Store file to open after renderer is ready - console.log('[MAIN] Storing as pending file'); - app.pendingFile = filePath; - } - } else { - console.error('[MAIN] File does not exist:', filePath); - } -} - -// ======================================== -// PDF EDITOR OPERATIONS (using pdf-lib) -// ======================================== - -// Helper function to parse page ranges (e.g., "1-5, 7, 9-12") -function parsePageRanges(rangeString, totalPages) { - const pages = []; - const ranges = rangeString.split(',').map(r => r.trim()); - - for (const range of ranges) { - if (range.includes('-')) { - const [start, end] = range.split('-').map(n => parseInt(n.trim())); - for (let i = start; i <= end && i <= totalPages; i++) { - if (i > 0 && !pages.includes(i - 1)) { // Convert to 0-indexed - pages.push(i - 1); - } - } - } else { - const page = parseInt(range); - if (page > 0 && page <= totalPages && !pages.includes(page - 1)) { - pages.push(page - 1); - } - } - } - - return pages.sort((a, b) => a - b); -} - -// Helper function to convert hex color to RGB -function hexToRgb(hex) { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16) / 255, - g: parseInt(result[2], 16) / 255, - b: parseInt(result[3], 16) / 255 - } : { r: 0, g: 0, b: 0 }; -} - -// PDF Merge Operation -async function pdfMerge(data) { - try { - const mergedPdf = await PDFDocument.create(); - - for (const filePath of data.inputFiles) { - const pdfBytes = fs.readFileSync(filePath); - const pdf = await PDFDocument.load(pdfBytes); - const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices()); - copiedPages.forEach(page => mergedPdf.addPage(page)); - } - - const pdfBytes = await mergedPdf.save(); - fs.writeFileSync(data.outputPath, pdfBytes); - - return { success: true, message: `Successfully merged ${data.inputFiles.length} PDFs` }; - } catch (error) { - return { success: false, error: error.message }; - } -} - -// PDF Split Operation -async function pdfSplit(data) { - try { - const pdfBytes = fs.readFileSync(data.inputPath); - const pdf = await PDFDocument.load(pdfBytes); - const totalPages = pdf.getPageCount(); - - let splits = []; - - if (data.splitMode === 'pages') { - // Split by page ranges - const ranges = data.pageRanges.split(',').map(r => r.trim()); - for (let i = 0; i < ranges.length; i++) { - const range = ranges[i]; - let pages = []; - - if (range.includes('-')) { - const [start, end] = range.split('-').map(n => parseInt(n.trim())); - for (let p = start; p <= end && p <= totalPages; p++) { - pages.push(p - 1); - } - } else { - const page = parseInt(range); - if (page > 0 && page <= totalPages) { - pages.push(page - 1); - } - } - - if (pages.length > 0) { - splits.push({ pages, name: `part_${i + 1}` }); - } - } - } else if (data.splitMode === 'interval') { - // Split every N pages - const interval = data.interval; - for (let i = 0; i < totalPages; i += interval) { - const pages = []; - for (let j = i; j < i + interval && j < totalPages; j++) { - pages.push(j); - } - splits.push({ pages, name: `part_${Math.floor(i / interval) + 1}` }); - } - } else if (data.splitMode === 'size') { - // Split by size (approximate - we'll split evenly) - // This is complex, so we'll do a simple even split for now - const maxSize = data.maxSize * 1024 * 1024; // Convert MB to bytes - // For simplicity, split into fixed page chunks - const chunkSize = Math.max(1, Math.floor(totalPages / 5)); // Split into ~5 parts - for (let i = 0; i < totalPages; i += chunkSize) { - const pages = []; - for (let j = i; j < i + chunkSize && j < totalPages; j++) { - pages.push(j); - } - splits.push({ pages, name: `part_${Math.floor(i / chunkSize) + 1}` }); - } - } - - // Create split PDFs - const baseName = path.basename(data.inputPath, '.pdf'); - for (const split of splits) { - const newPdf = await PDFDocument.create(); - const copiedPages = await newPdf.copyPages(pdf, split.pages); - copiedPages.forEach(page => newPdf.addPage(page)); - - const outputPath = path.join(data.outputFolder, `${baseName}_${split.name}.pdf`); - const newPdfBytes = await newPdf.save(); - fs.writeFileSync(outputPath, newPdfBytes); - } - - return { success: true, message: `Successfully split PDF into ${splits.length} files` }; - } catch (error) { - return { success: false, error: error.message }; - } -} - -// PDF Compress Operation -async function pdfCompress(data) { - try { - const pdfBytes = fs.readFileSync(data.inputPath); - const pdf = await PDFDocument.load(pdfBytes); - - // pdf-lib doesn't have built-in compression, but we can save with default compression - // For actual compression, we would need additional libraries like Ghostscript - // For now, we'll save with standard options which provides some compression - const compressedPdfBytes = await pdf.save({ - useObjectStreams: true, - addDefaultPage: false, - objectsPerTick: 50 - }); - - fs.writeFileSync(data.outputPath, compressedPdfBytes); - - const originalSize = fs.statSync(data.inputPath).size; - const compressedSize = fs.statSync(data.outputPath).size; - const savings = ((originalSize - compressedSize) / originalSize * 100).toFixed(1); - - return { - success: true, - message: `PDF compressed. Size reduced by ${savings}% (${(originalSize / 1024).toFixed(1)}KB → ${(compressedSize / 1024).toFixed(1)}KB)` - }; - } catch (error) { - return { success: false, error: error.message }; - } -} - -// PDF Rotate Pages Operation -async function pdfRotate(data) { - try { - const pdfBytes = fs.readFileSync(data.inputPath); - const pdf = await PDFDocument.load(pdfBytes); - const totalPages = pdf.getPageCount(); - - let pagesToRotate = []; - if (data.pages && data.pages.trim()) { - pagesToRotate = parsePageRanges(data.pages, totalPages); - } else { - // Rotate all pages - pagesToRotate = Array.from({ length: totalPages }, (_, i) => i); - } - - pagesToRotate.forEach(pageIndex => { - const page = pdf.getPage(pageIndex); - page.setRotation(degrees(data.angle)); - }); - - const rotatedPdfBytes = await pdf.save(); - fs.writeFileSync(data.outputPath, rotatedPdfBytes); - - return { - success: true, - message: `Successfully rotated ${pagesToRotate.length} page(s) by ${data.angle}°` - }; - } catch (error) { - return { success: false, error: error.message }; - } -} - -// PDF Delete Pages Operation -async function pdfDeletePages(data) { - try { - const pdfBytes = fs.readFileSync(data.inputPath); - const pdf = await PDFDocument.load(pdfBytes); - const totalPages = pdf.getPageCount(); - - const pagesToDelete = parsePageRanges(data.pages, totalPages); - - // Remove pages in reverse order to maintain indices - pagesToDelete.sort((a, b) => b - a).forEach(pageIndex => { - pdf.removePage(pageIndex); - }); - - const newPdfBytes = await pdf.save(); - fs.writeFileSync(data.outputPath, newPdfBytes); - - return { - success: true, - message: `Successfully deleted ${pagesToDelete.length} page(s). New PDF has ${totalPages - pagesToDelete.length} pages` - }; - } catch (error) { - return { success: false, error: error.message }; - } -} - -// PDF Reorder Pages Operation -async function pdfReorder(data) { - try { - const pdfBytes = fs.readFileSync(data.inputPath); - const pdf = await PDFDocument.load(pdfBytes); - const totalPages = pdf.getPageCount(); - - // Parse new page order - const newOrder = data.newOrder.split(',').map(n => parseInt(n.trim()) - 1); // Convert to 0-indexed - - // Validate new order - if (newOrder.length !== totalPages) { - return { success: false, error: `New order must include all ${totalPages} pages` }; - } - - const newPdf = await PDFDocument.create(); - const copiedPages = await newPdf.copyPages(pdf, newOrder); - copiedPages.forEach(page => newPdf.addPage(page)); - - const reorderedPdfBytes = await newPdf.save(); - fs.writeFileSync(data.outputPath, reorderedPdfBytes); - - return { success: true, message: 'Successfully reordered PDF pages' }; - } catch (error) { - return { success: false, error: error.message }; - } -} - -// PDF Watermark Operation -async function pdfWatermark(data) { - try { - const pdfBytes = fs.readFileSync(data.inputPath); - const pdf = await PDFDocument.load(pdfBytes); - const totalPages = pdf.getPageCount(); - - // Determine which pages to watermark - let pagesToWatermark = []; - if (data.pages === 'all') { - pagesToWatermark = Array.from({ length: totalPages }, (_, i) => i); - } else if (data.pages === 'custom' && data.customPages) { - pagesToWatermark = parsePageRanges(data.customPages, totalPages); - } - - const font = await pdf.embedFont(StandardFonts.Helvetica); - const color = hexToRgb(data.color); - - for (const pageIndex of pagesToWatermark) { - const page = pdf.getPage(pageIndex); - const { width, height } = page.getSize(); - - let x, y, rotation = 0; - - // Calculate position based on selected position - switch (data.position) { - case 'center': - x = width / 2; - y = height / 2; - break; - case 'diagonal': - x = width / 2; - y = height / 2; - rotation = 45; - break; - case 'top-left': - x = 50; - y = height - 50; - break; - case 'top-center': - x = width / 2; - y = height - 50; - break; - case 'top-right': - x = width - 50; - y = height - 50; - break; - case 'bottom-left': - x = 50; - y = 50; - break; - case 'bottom-center': - x = width / 2; - y = 50; - break; - case 'bottom-right': - x = width - 50; - y = 50; - break; - default: - x = width / 2; - y = height / 2; - } - - page.drawText(data.text, { - x, - y, - size: data.fontSize, - font, - color: rgb(color.r, color.g, color.b), - opacity: data.opacity, - rotate: degrees(rotation) - }); - } - - const watermarkedPdfBytes = await pdf.save(); - fs.writeFileSync(data.outputPath, watermarkedPdfBytes); - - return { - success: true, - message: `Successfully added watermark to ${pagesToWatermark.length} page(s)` - }; - } catch (error) { - return { success: false, error: error.message }; - } -} - -// PDF Encrypt (Password Protection) Operation -async function pdfEncrypt(data) { - try { - const pdfBytes = fs.readFileSync(data.inputPath); - const pdf = await PDFDocument.load(pdfBytes); - - // pdf-lib has limited encryption support in v1.17.1 - // We'll save with password (basic encryption) - const encryptedPdfBytes = await pdf.save({ - userPassword: data.userPassword, - ownerPassword: data.ownerPassword || data.userPassword, - permissions: { - printing: data.permissions.printing ? 'highResolution' : 'lowResolution', - modifying: data.permissions.modifying, - copying: data.permissions.copying, - annotating: data.permissions.annotating, - fillingForms: data.permissions.fillingForms, - contentAccessibility: data.permissions.contentAccessibility, - documentAssembly: data.permissions.documentAssembly - } - }); - - fs.writeFileSync(data.outputPath, encryptedPdfBytes); - - return { success: true, message: 'Successfully added password protection to PDF' }; - } catch (error) { - // If pdf-lib doesn't support encryption in this version, provide a helpful error - if (error.message.includes('encrypt') || error.message.includes('password')) { - return { - success: false, - error: 'PDF encryption requires pdf-lib with encryption support. This feature may not be available in the current version.' - }; - } - return { success: false, error: error.message }; - } -} - -// PDF Decrypt (Remove Password) Operation -async function pdfDecrypt(data) { - try { - const pdfBytes = fs.readFileSync(data.inputPath); - const pdf = await PDFDocument.load(pdfBytes, { password: data.password }); - - // Save without password - const decryptedPdfBytes = await pdf.save(); - fs.writeFileSync(data.outputPath, decryptedPdfBytes); - - return { success: true, message: 'Successfully removed password protection from PDF' }; - } catch (error) { - if (error.message.includes('password') || error.message.includes('encrypted')) { - return { success: false, error: 'Incorrect password or PDF is not encrypted' }; - } - return { success: false, error: error.message }; - } -} - -// PDF Permissions Operation -async function pdfSetPermissions(data) { - try { - const pdfBytes = fs.readFileSync(data.inputPath); - const loadOptions = data.currentPassword ? { password: data.currentPassword } : {}; - const pdf = await PDFDocument.load(pdfBytes, loadOptions); - - const newPdfBytes = await pdf.save({ - ownerPassword: data.ownerPassword, - permissions: { - printing: data.permissions.printing ? 'highResolution' : 'lowResolution', - modifying: data.permissions.modifying, - copying: data.permissions.copying, - annotating: data.permissions.annotating, - fillingForms: data.permissions.fillingForms, - contentAccessibility: data.permissions.contentAccessibility, - documentAssembly: data.permissions.documentAssembly - } - }); - - fs.writeFileSync(data.outputPath, newPdfBytes); - - return { success: true, message: 'Successfully updated PDF permissions' }; - } catch (error) { - if (error.message.includes('encrypt') || error.message.includes('permission')) { - return { - success: false, - error: 'PDF permissions require pdf-lib with encryption support. This feature may not be available in the current version.' - }; - } - return { success: false, error: error.message }; - } -} - -// IPC Handler for PDF Operations -ipcMain.on('process-pdf-operation', async (event, data) => { - try { - mainWindow.webContents.send('pdf-operation-progress', { - message: `Processing ${data.operation}...`, - progress: 10 - }); - - let result; - - switch (data.operation) { - case 'merge': - result = await pdfMerge(data); - break; - case 'split': - result = await pdfSplit(data); - break; - case 'compress': - result = await pdfCompress(data); - break; - case 'rotate': - result = await pdfRotate(data); - break; - case 'delete': - result = await pdfDeletePages(data); - break; - case 'reorder': - result = await pdfReorder(data); - break; - case 'watermark': - result = await pdfWatermark(data); - break; - case 'encrypt': - result = await pdfEncrypt(data); - break; - case 'decrypt': - result = await pdfDecrypt(data); - break; - case 'permissions': - result = await pdfSetPermissions(data); - break; - default: - result = { success: false, error: `Unknown operation: ${data.operation}` }; - } - - mainWindow.webContents.send('pdf-operation-complete', result); - } catch (error) { - mainWindow.webContents.send('pdf-operation-complete', { - success: false, - error: error.message - }); - } -}); - -// IPC Handler for getting PDF page count -ipcMain.on('get-pdf-page-count', async (event, filePath) => { - try { - const pdfBytes = fs.readFileSync(filePath); - const pdf = await PDFDocument.load(pdfBytes); - const count = pdf.getPageCount(); - event.reply('pdf-page-count', { count }); - } catch (error) { - event.reply('pdf-page-count', { error: error.message }); - } -}); - -// IPC Handler for folder selection (for PDF operations) -ipcMain.on('select-pdf-folder', (event, inputId) => { - const folder = dialog.showOpenDialogSync(mainWindow, { - properties: ['openDirectory'] - }); - - if (folder && folder[0]) { - event.reply('pdf-folder-selected', { inputId, path: folder[0] }); - } -}); - -// ============================================ -// ASCII Art Generator Window -// ============================================ -let asciiGeneratorWindow = null; - -function openAsciiGenerator() { - if (asciiGeneratorWindow) { - asciiGeneratorWindow.focus(); - return; - } - - asciiGeneratorWindow = new BrowserWindow({ - width: 800, - height: 700, - parent: mainWindow, - modal: false, - title: 'ASCII Art Generator', - icon: path.join(__dirname, '../assets/icon.png'), - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - preload: path.join(__dirname, 'preload.js') - } - }); - - asciiGeneratorWindow.loadFile(path.join(__dirname, 'ascii-generator.html')); - asciiGeneratorWindow.setMenuBarVisibility(false); - - asciiGeneratorWindow.on('closed', () => { - asciiGeneratorWindow = null; - }); -} - -ipcMain.on('open-ascii-generator', () => { - openAsciiGenerator(); -}); - -// ============================================ -// Table Generator Window -// ============================================ -let tableGeneratorWindow = null; - -function openTableGenerator() { - if (tableGeneratorWindow) { - tableGeneratorWindow.focus(); - return; - } - - tableGeneratorWindow = new BrowserWindow({ - width: 900, - height: 700, - parent: mainWindow, - modal: false, - title: 'Table Generator', - icon: path.join(__dirname, '../assets/icon.png'), - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - preload: path.join(__dirname, 'preload.js') - } - }); - - tableGeneratorWindow.loadFile(path.join(__dirname, 'table-generator.html')); - tableGeneratorWindow.setMenuBarVisibility(false); - - tableGeneratorWindow.on('closed', () => { - tableGeneratorWindow = null; - }); -} - -ipcMain.on('open-table-generator', () => { - openTableGenerator(); -}); - -// IPC Handler to receive generated content from generator windows -ipcMain.on('insert-generated-content', (event, content) => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('insert-content', content); - } +const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron'); +const path = require('path'); +const fs = require('fs'); +const { exec, execFile, spawn } = require('child_process'); +const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib'); +const WordTemplateExporter = require('./wordTemplateExporter'); + +// Add MiKTeX to PATH for LaTeX support +if (process.platform === 'win32') { + const miktexPath = 'C:\\Program Files\\MiKTeX\\miktex\\bin\\x64'; + if (fs.existsSync(miktexPath)) { + process.env.PATH = `${miktexPath};${process.env.PATH}`; + console.log('[MAIN] Added MiKTeX to PATH:', miktexPath); + } +} + +// Get the system Pandoc path +function getPandocPath() { + // Pandoc is expected to be in the system's PATH. + // The command will be executed directly. Quoting is handled by exec. + return 'pandoc'; +} + +// Check if Pandoc is available +function checkPandocAvailable() { + return new Promise((resolve) => { + const pandocPath = getPandocPath(); + execFile(pandocPath, ['--version'], (error) => { + resolve(!error); + }); + }); +} + +/** + * Safe command execution using execFile (no shell injection risk) + * @param {string} command - The command to execute + * @param {string[]} args - Array of arguments + * @param {object} options - Options for execFile + * @returns {Promise<{stdout: string, stderr: string}>} + */ +function safeExecFile(command, args, options = {}) { + return new Promise((resolve, reject) => { + execFile(command, args, { maxBuffer: 10 * 1024 * 1024, ...options }, (error, stdout, stderr) => { + if (error) { + reject({ error, stdout, stderr }); + } else { + resolve({ stdout, stderr }); + } + }); + }); +} + +/** + * Run Pandoc command safely with execFile + * @param {string[]} args - Pandoc arguments array + * @param {Function} callback - Callback function (error, stdout, stderr) + */ +function runPandoc(args, callback) { + const pandocPath = getPandocPath(); + execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback); +} + +/** + * Run a Pandoc command string safely using execFile + * Parses the command string and uses execFile to prevent shell injection + * @param {string} cmdString - Full Pandoc command string (e.g., 'pandoc "input.md" -o "output.pdf"') + * @param {Function} callback - Callback function (error, stdout, stderr) + */ +function runPandocCmd(cmdString, callback) { + const parsed = parseCommand(cmdString); + // Skip 'pandoc' if it's the first element (command itself) + const args = parsed.command === 'pandoc' ? parsed.args : [parsed.command, ...parsed.args]; + const pandocPath = getPandocPath(); + execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback); +} + +/** + * Parse a command string into command and arguments array + * This helps transition from exec() to execFile() safely + * @param {string} cmdString - Full command string + * @returns {{command: string, args: string[]}} + */ +function parseCommand(cmdString) { + // Handle quoted strings properly + const parts = []; + let current = ''; + let inQuotes = false; + let quoteChar = ''; + + for (let i = 0; i < cmdString.length; i++) { + const char = cmdString[i]; + if ((char === '"' || char === "'") && !inQuotes) { + inQuotes = true; + quoteChar = char; + } else if (char === quoteChar && inQuotes) { + inQuotes = false; + quoteChar = ''; + } else if (char === ' ' && !inQuotes) { + if (current) { + parts.push(current); + current = ''; + } + } else { + current += char; + } + } + if (current) { + parts.push(current); + } + + return { + command: parts[0], + args: parts.slice(1) + }; +} + +// Simple storage implementation to replace electron-store +const settingsPath = path.join(app.getPath('userData'), 'settings.json'); +const store = { + get: (key, defaultValue) => { + try { + const data = fs.readFileSync(settingsPath, 'utf-8'); + const settings = JSON.parse(data); + return settings[key] || defaultValue; + } catch { + return defaultValue; + } + }, + set: (key, value) => { + let settings = {}; + try { + const data = fs.readFileSync(settingsPath, 'utf-8'); + settings = JSON.parse(data); + } catch {} + settings[key] = value; + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); + } +}; + +let mainWindow; +let currentFile = null; // This will now represent the active tab's file +let pandocAvailable = null; // Cache pandoc availability check +let wordTemplatePath = null; // Path to selected Word template +let templateStartPage = 3; // Which page to start inserting content (default: page 3) +let rendererReady = false; // Track if renderer is ready to receive file data + +// Header & Footer Settings +let headerFooterSettings = { + enabled: true, + header: { + left: '', + center: '', + right: '', + logo: null // Will store image file path + }, + footer: { + left: '', + center: '$PAGE$ of $TOTAL$', + right: '', + logo: null + } +}; + +// Page Size Definitions (in twentieths of a point for Word, mm/inches for Pandoc) +const PAGE_SIZES = { + a4: { + name: 'A4', + pandoc: 'a4', + word: { width: 11906, height: 16838 }, // 210×297mm + dimensions: '210×297mm' + }, + a3: { + name: 'A3', + pandoc: 'a3', + word: { width: 16838, height: 23811 }, // 297×420mm + dimensions: '297×420mm' + }, + a5: { + name: 'A5', + pandoc: 'a5', + word: { width: 8391, height: 11906 }, // 148×210mm + dimensions: '148×210mm' + }, + b4: { + name: 'B4', + pandoc: 'b4', + word: { width: 14170, height: 20015 }, // 250×353mm + dimensions: '250×353mm' + }, + b5: { + name: 'B5', + pandoc: 'b5', + word: { width: 9979, height: 14170 }, // 176×250mm + dimensions: '176×250mm' + }, + letter: { + name: 'Letter', + pandoc: 'letter', + word: { width: 12240, height: 15840 }, // 8.5×11in + dimensions: '8.5×11in' + }, + legal: { + name: 'Legal', + pandoc: 'legal', + word: { width: 12240, height: 20160 }, // 8.5×14in + dimensions: '8.5×14in' + }, + tabloid: { + name: 'Tabloid', + pandoc: 'tabloid', + word: { width: 15840, height: 24480 }, // 11×17in + dimensions: '11×17in' + } +}; + +// Default page settings +let pageSettings = { + size: 'a4', + orientation: 'portrait', + customWidth: null, + customHeight: null +}; + +// Handle single instance lock for Windows file association +// When a file is double-clicked and the app is already running, +// Windows tries to start a second instance. We prevent this and +// pass the file to the existing instance instead. +const gotTheLock = app.requestSingleInstanceLock(); + +if (!gotTheLock) { + // Another instance is already running, quit this one + app.quit(); +} else { + // This is the first instance, handle second-instance events + app.on('second-instance', (event, commandLine, workingDirectory) => { + // Someone tried to run a second instance, focus our window instead + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + } + + // Check if a file was passed to the second instance + // commandLine is an array like: ['PanConverter.exe', 'file.md'] + console.log('[MAIN] Second instance commandLine:', JSON.stringify(commandLine)); + const startIndex = app.isPackaged ? 1 : 2; + const fileArgs = commandLine.slice(startIndex); + console.log('[MAIN] Second instance file args:', fileArgs); + for (const arg of fileArgs) { + if ((arg.endsWith('.md') || arg.endsWith('.markdown'))) { + const resolvedPath = path.isAbsolute(arg) ? arg : path.resolve(workingDirectory, arg); + console.log('[MAIN] Second instance resolved path:', resolvedPath); + if (fs.existsSync(resolvedPath)) { + // Open the file in the existing instance + if (rendererReady) { + openFileFromPath(resolvedPath); + } else { + app.pendingFile = resolvedPath; + } + break; + } + } + } + }); +} + +// Check if pandoc is available (using execFile for consistency) +function checkPandocAvailability() { + return new Promise((resolve) => { + if (pandocAvailable !== null) { + resolve(pandocAvailable); + return; + } + + execFile('pandoc', ['--version'], (error, stdout, stderr) => { + pandocAvailable = !error; + resolve(pandocAvailable); + }); + }); +} + +function createWindow() { + mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + }, + icon: path.join(__dirname, '../assets/icon.png') + }); + + mainWindow.loadFile(path.join(__dirname, 'index.html')); + + createMenu(); + + mainWindow.on('closed', () => { + mainWindow = null; + }); + + // Wait for the page to fully load before sending file data + mainWindow.webContents.on('did-finish-load', () => { + console.log('Window finished loading'); + // Don't open file here - wait for renderer-ready signal + // The renderer will send renderer-ready when TabManager is initialized + }); +} + +function buildRecentFilesMenu() { + const recentFiles = getRecentFiles(); + + if (recentFiles.length === 0) { + return [ + { + label: 'No recent files', + enabled: false + } + ]; + } + + const recentFileItems = recentFiles.map(filePath => ({ + label: filePath.split(/[\\/]/).pop(), // Get filename only + click: () => { + if (fs.existsSync(filePath)) { + currentFile = filePath; + const content = fs.readFileSync(filePath, 'utf-8'); + mainWindow.webContents.send('file-opened', { path: filePath, content }); + } else { + dialog.showErrorBox('File Not Found', `The file "${filePath}" could not be found.`); + } + }, + toolTip: filePath // Show full path in tooltip + })); + + return [ + ...recentFileItems, + { type: 'separator' }, + { + label: 'Clear Recent Files', + click: () => { + mainWindow.webContents.send('clear-recent-files'); + } + } + ]; +} + +function getRecentFiles() { + try { + const recentFiles = JSON.parse(fs.readFileSync(path.join(app.getPath('userData'), 'recent-files.json'), 'utf-8')); + return recentFiles.filter(file => fs.existsSync(file)); + } catch (e) { + return []; + } +} + +function createMenu() { + const template = [ + { + label: 'File', + submenu: [ + { + label: 'New', + accelerator: 'CmdOrCtrl+N', + click: () => mainWindow.webContents.send('file-new') + }, + { + label: 'Open', + accelerator: 'CmdOrCtrl+O', + click: openFile + }, + { + label: 'Open PDF', + accelerator: 'CmdOrCtrl+Shift+O', + click: openPdfFile + }, + { + label: 'Save', + accelerator: 'CmdOrCtrl+S', + click: () => mainWindow.webContents.send('file-save') + }, + { + label: 'Save As', + accelerator: 'CmdOrCtrl+Shift+S', + click: saveAsFile + }, + { type: 'separator' }, + { + label: 'Print', + submenu: [ + { + label: 'Print Preview', + accelerator: 'CmdOrCtrl+P', + click: () => mainWindow.webContents.send('print-preview') + }, + { + label: 'Print Preview (With Styles)', + click: () => mainWindow.webContents.send('print-preview-styled') + } + ] + }, + { type: 'separator' }, + { + label: 'Recent Files', + submenu: buildRecentFilesMenu() + }, + { type: 'separator' }, + { + label: 'Import Document...', + accelerator: 'CmdOrCtrl+I', + click: importDocument + }, + { + label: 'Export', + submenu: [ + { label: 'HTML', click: () => exportFile('html') }, + { label: 'PDF', click: () => exportFile('pdf') }, + { label: 'PDF (Enhanced)', click: () => exportPDFViaWordTemplate(), accelerator: 'Ctrl+Shift+P' }, + { label: 'DOCX', click: () => exportFile('docx') }, + { label: 'DOCX (Enhanced)', click: () => exportWordWithTemplate(), accelerator: 'Ctrl+Shift+W' }, + { label: 'LaTeX', click: () => exportFile('latex') }, + { label: 'RTF', click: () => exportFile('rtf') }, + { label: 'ODT', click: () => exportFile('odt') }, + { label: 'EPUB', click: () => exportFile('epub') }, + { type: 'separator' }, + { label: 'PowerPoint (PPTX)', click: () => exportFile('pptx') }, + { label: 'OpenDocument Presentation (ODP)', click: () => exportFile('odp') }, + { type: 'separator' }, + { label: 'CSV (Tables)', click: () => exportSpreadsheet('csv') }, + ] + }, + { type: 'separator' }, + { + label: 'Select Word Template...', + click: selectWordTemplate + }, + { + label: 'Template Settings...', + click: showTemplateSettings + }, + { + label: 'Header & Footer Settings...', + click: () => { + if (mainWindow) { + mainWindow.webContents.send('open-header-footer-dialog'); + } + } + }, + { type: 'separator' }, + { + label: 'Quit', + accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q', + click: () => app.quit() + } + ] + }, + { + label: 'Edit', + submenu: [ + { + label: 'Undo', + accelerator: 'CmdOrCtrl+Z', + click: () => mainWindow.webContents.send('undo') + }, + { + label: 'Redo', + accelerator: 'CmdOrCtrl+Shift+Z', + click: () => mainWindow.webContents.send('redo') + }, + { type: 'separator' }, + { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, + { label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }, + { label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' }, + { label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectAll' }, + { type: 'separator' }, + { + label: 'Find & Replace', + accelerator: 'CmdOrCtrl+F', + click: () => mainWindow.webContents.send('toggle-find') + } + ] + }, + { + label: 'View', + submenu: [ + { + label: 'Toggle Preview', + accelerator: 'CmdOrCtrl+Shift+P', + click: () => mainWindow.webContents.send('toggle-preview') + }, + { + label: 'Theme', + submenu: [ + // Light Themes (grouped first) + { label: 'Atom One Light (Default)', click: () => setTheme('atomonelight') }, + { label: 'GitHub Light', click: () => setTheme('github') }, + { label: 'Light', click: () => setTheme('light') }, + { label: 'Solarized Light', click: () => setTheme('solarized') }, + { label: 'Gruvbox Light', click: () => setTheme('gruvbox-light') }, + { label: 'Ayu Light', click: () => setTheme('ayu-light') }, + { label: 'Sepia', click: () => setTheme('sepia') }, + { label: 'Paper', click: () => setTheme('paper') }, + { label: 'Rose Pine Dawn', click: () => setTheme('rosepine-dawn') }, + { label: 'Concrete Light', click: () => setTheme('concrete-light') }, + { type: 'separator' }, + // Dark Themes + { label: 'Dark', click: () => setTheme('dark') }, + { label: 'One Dark', click: () => setTheme('onedark') }, + { label: 'Dracula', click: () => setTheme('dracula') }, + { label: 'Nord', click: () => setTheme('nord') }, + { label: 'Monokai', click: () => setTheme('monokai') }, + { label: 'Material', click: () => setTheme('material') }, + { label: 'Gruvbox Dark', click: () => setTheme('gruvbox-dark') }, + { label: 'Tokyo Night', click: () => setTheme('tokyonight') }, + { label: 'Palenight', click: () => setTheme('palenight') }, + { label: 'Ayu Dark', click: () => setTheme('ayu-dark') }, + { label: 'Ayu Mirage', click: () => setTheme('ayu-mirage') }, + { label: 'Oceanic Next', click: () => setTheme('oceanic-next') }, + { label: 'Cobalt2', click: () => setTheme('cobalt2') }, + { label: 'Concrete Dark', click: () => setTheme('concrete-dark') }, + { label: 'Concrete Warm', click: () => setTheme('concrete-warm') } + ] + }, + { type: 'separator' }, + { + label: 'Font Size', + submenu: [ + { + label: 'Increase Font Size', + accelerator: 'CmdOrCtrl+Shift+Plus', + click: () => mainWindow.webContents.send('adjust-font-size', 'increase') + }, + { + label: 'Decrease Font Size', + accelerator: 'CmdOrCtrl+Shift+-', + click: () => mainWindow.webContents.send('adjust-font-size', 'decrease') + }, + { + label: 'Reset Font Size', + accelerator: 'CmdOrCtrl+Shift+0', + click: () => mainWindow.webContents.send('adjust-font-size', 'reset') + } + ] + }, + { type: 'separator' }, + { label: 'Reload', accelerator: 'CmdOrCtrl+R', role: 'reload' }, + { label: 'Toggle DevTools', accelerator: 'F12', role: 'toggleDevTools' }, + { type: 'separator' }, + { label: 'Zoom In', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' }, + { label: 'Zoom Out', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' }, + { label: 'Reset Zoom', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' } + ] + }, + { + label: 'Batch', + submenu: [ + { + label: 'Convert Markdown Folder...', + click: () => showBatchConversionDialog() + }, + { type: 'separator' }, + { + label: 'Batch Image Conversion...', + click: () => mainWindow.webContents.send('show-batch-converter', 'image') + }, + { + label: 'Batch Audio Conversion...', + click: () => mainWindow.webContents.send('show-batch-converter', 'audio') + }, + { + label: 'Batch Video Conversion...', + click: () => mainWindow.webContents.send('show-batch-converter', 'video') + }, + { + label: 'Batch PDF Conversion...', + click: () => mainWindow.webContents.send('show-batch-converter', 'pdf') + } + ] + }, + { + label: 'Convert', + submenu: [ + { + label: 'Universal File Converter...', + accelerator: 'CmdOrCtrl+Shift+C', + click: () => showUniversalConverterDialog() + } + ] + }, + { + label: 'PDF Editor', + submenu: [ + { + label: 'Open PDF File...', + accelerator: 'CmdOrCtrl+Shift+O', + click: () => openPDFFile() + }, + { type: 'separator' }, + { + label: 'Merge PDFs...', + click: () => showPDFEditorDialog('merge') + }, + { + label: 'Split PDF...', + click: () => showPDFEditorDialog('split') + }, + { + label: 'Compress PDF...', + click: () => showPDFEditorDialog('compress') + }, + { + type: 'separator' + }, + { + label: 'Rotate Pages...', + click: () => showPDFEditorDialog('rotate') + }, + { + label: 'Delete Pages...', + click: () => showPDFEditorDialog('delete') + }, + { + label: 'Reorder Pages...', + click: () => showPDFEditorDialog('reorder') + }, + { + type: 'separator' + }, + { + label: 'Add Watermark...', + click: () => showPDFEditorDialog('watermark') + }, + { + type: 'separator' + }, + { + label: 'Security', + submenu: [ + { + label: 'Add Password Protection...', + click: () => showPDFEditorDialog('encrypt') + }, + { + label: 'Remove Password...', + click: () => showPDFEditorDialog('decrypt') + }, + { + label: 'Set Permissions...', + click: () => showPDFEditorDialog('permissions') + } + ] + }, + { + type: 'separator' + }, + { + label: 'About PDF Editor', + click: () => { + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'About PDF Editor', + message: 'PDF Editor', + detail: 'Comprehensive PDF editing capabilities powered by pdf-lib.\n\nFeatures:\n• Merge multiple PDF files\n• Split PDF into separate files\n• Compress PDF to reduce file size\n• Rotate pages (90°, 180°, 270°)\n• Delete unwanted pages\n• Reorder pages\n• Add text watermarks\n\nSecurity Features:\n• Password protection (encryption)\n• Remove passwords (decryption)\n• Set document permissions\n\n100% offline and open-source.', + buttons: ['OK'] + }); + } + } + ] + }, + { + label: 'Tools', + submenu: [ + { + label: 'Table Generator', + accelerator: 'CmdOrCtrl+Shift+T', + click: () => openTableGenerator() + }, + { + label: 'ASCII Art Generator', + accelerator: 'CmdOrCtrl+Shift+A', + click: () => openAsciiGenerator() + }, + { type: 'separator' }, + { + label: 'Document Compare', + click: () => mainWindow.webContents.send('show-document-compare') + } + ] + }, + { + label: 'Help', + submenu: [ + { + label: 'About MarkdownConverter', + click: () => showAboutDialog() + }, + { type: 'separator' }, + { + label: 'Dependencies & Requirements', + click: () => showDependenciesDialog() + }, + { type: 'separator' }, + { + label: 'Documentation', + click: () => shell.openExternal('https://github.com/amitwh/markdown-converter') + }, + { + label: 'Report Issue', + click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/issues') + }, + { + label: 'Check for Updates', + click: () => shell.openExternal('https://github.com/amitwh/markdown-converter/releases') + } + ] + } + ]; + + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); +} + +// Show About Dialog with logo +function showAboutDialog() { + const aboutWindow = new BrowserWindow({ + width: 500, + height: 600, + parent: mainWindow, + modal: true, + resizable: false, + minimizable: false, + maximizable: false, + webPreferences: { + nodeIntegration: false, + contextIsolation: true + }, + icon: path.join(__dirname, '../assets/icon.png') + }); + + // Convert images to base64 for data URL compatibility + let logoBase64 = ''; + let iconBase64 = ''; + try { + const logoPath = path.join(__dirname, '../assets/logo.png'); + const iconPath = path.join(__dirname, '../assets/icon.png'); + if (fs.existsSync(logoPath)) { + logoBase64 = 'data:image/png;base64,' + fs.readFileSync(logoPath).toString('base64'); + } + if (fs.existsSync(iconPath)) { + iconBase64 = 'data:image/png;base64,' + fs.readFileSync(iconPath).toString('base64'); + } + } catch (e) { + console.error('Error loading about dialog images:', e); + } + + const aboutHTML = ` + + + + + About MarkdownConverter + + + + +

MarkdownConverter

+
Version 3.0.0
+ +
+ by + +
+ +
+

Features

+
    +
  • Professional Markdown editing with syntax highlighting
  • +
  • Universal file converter (Image, Audio, Video, PDF)
  • +
  • Batch conversion for all media types
  • +
  • Advanced PDF Editor (merge, split, watermark, encrypt)
  • +
  • Export to PDF, DOCX, HTML, LaTeX, EPUB & more
  • +
  • 23+ beautiful themes including ConcreteInfo theme
  • +
  • ASCII Art & Table generators
  • +
  • Code file syntax highlighting
  • +
+
+ +
+

Contact

+

Email: amit.wh@gmail.com

+

Website: GitHub Repository

+
+ + + +`; + + aboutWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(aboutHTML)); + aboutWindow.setMenuBarVisibility(false); +} + +// Show Dependencies Dialog +function showDependenciesDialog() { + const depsWindow = new BrowserWindow({ + width: 600, + height: 700, + parent: mainWindow, + modal: true, + resizable: true, + webPreferences: { + nodeIntegration: false, + contextIsolation: true + }, + icon: path.join(__dirname, '../assets/icon.png') + }); + + const depsHTML = ` + + + + + Dependencies & Requirements + + + +

Dependencies & Requirements

+ +

Required Dependencies

+ +
+
Pandoc Required
+
Universal document converter. Required for most export formats (PDF, DOCX, LaTeX, EPUB).
+ https://pandoc.org/installing.html +
+ +

Optional Dependencies (for extended features)

+ +
+
FFmpeg Optional
+
Required for audio/video conversion and processing.
+ https://ffmpeg.org/download.html +
+ +
+
ImageMagick Optional
+
Required for image format conversion and processing.
+ https://imagemagick.org/script/download.php +
+ +
+
LibreOffice Optional
+
Required for enhanced document conversion (Office formats).
+ https://www.libreoffice.org/download/download/ +
+ +
+
MiKTeX / TeX Live Optional
+
Required for PDF export via LaTeX (higher quality PDFs).
+ https://miktex.org/download +
+ +

Bundled Libraries

+ +
+
pdf-lib
+
PDF manipulation library for merge, split, watermark, and encryption features.
+
+ +
+
marked
+
Markdown parser for preview rendering.
+
+ +
+
highlight.js
+
Syntax highlighting for code blocks.
+
+ +
+
DOMPurify
+
HTML sanitization for security.
+
+ +`; + + depsWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(depsHTML)); + depsWindow.setMenuBarVisibility(false); +} + +// Open PDF File for viewing/editing +function openPDFFile() { + const files = dialog.showOpenDialogSync(mainWindow, { + properties: ['openFile'], + filters: [ + { name: 'PDF Files', extensions: ['pdf'] } + ] + }); + + if (files && files[0]) { + mainWindow.webContents.send('open-pdf-viewer', files[0]); + } +} + +function openFile() { + const files = dialog.showOpenDialogSync(mainWindow, { + properties: ['openFile'], + filters: [ + { name: 'Markdown', extensions: ['md', 'markdown'] }, + { name: 'All Files', extensions: ['*'] } + ] + }); + + if (files && files[0]) { + currentFile = files[0]; + const content = fs.readFileSync(currentFile, 'utf-8'); + mainWindow.webContents.send('file-opened', { path: currentFile, content }); + } +} + +function openPdfFile() { + const files = dialog.showOpenDialogSync(mainWindow, { + properties: ['openFile'], + filters: [ + { name: 'PDF Files', extensions: ['pdf'] }, + { name: 'All Files', extensions: ['*'] } + ] + }); + + if (files && files[0]) { + mainWindow.webContents.send('open-pdf-file', files[0]); + } +} + +function saveAsFile() { + const file = dialog.showSaveDialogSync(mainWindow, { + defaultExt: '.md', + filters: [ + { name: 'Markdown', extensions: ['md', 'markdown'] }, + { name: 'All Files', extensions: ['*'] } + ] + }); + + if (file) { + currentFile = file; + mainWindow.webContents.send('get-content-for-save', file); + } +} + +function exportFile(format) { + if (!currentFile) { + dialog.showErrorBox('Error', 'Please save the file first'); + return; + } + + // Show export options dialog + showExportOptionsDialog(format); +} + +function showExportOptionsDialog(format) { + mainWindow.webContents.send('show-export-dialog', format); +} + +function showBatchConversionDialog() { + mainWindow.webContents.send('show-batch-dialog'); +} + +// Select Word Template +async function selectWordTemplate() { + const result = await dialog.showOpenDialog(mainWindow, { + title: 'Select Word Template', + filters: [{ name: 'Word Document', extensions: ['docx'] }], + properties: ['openFile'] + }); + + if (!result.canceled && result.filePaths.length > 0) { + wordTemplatePath = result.filePaths[0]; + store.set('wordTemplatePath', wordTemplatePath); + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Template Selected', + message: 'Word template has been updated', + detail: `Template: ${path.basename(wordTemplatePath)}` + }); + } +} + +// Template Settings Dialog +async function showTemplateSettings() { + const result = await dialog.showMessageBox(mainWindow, { + type: 'question', + title: 'Template Settings', + message: 'Configure Word Template Export', + detail: `Current template: ${wordTemplatePath ? path.basename(wordTemplatePath) : 'Default template'}\nContent starts from page: ${templateStartPage}\n\nWhich page should content start from?\n(Templates usually have cover pages, TOC, etc.)`, + buttons: ['Page 1', 'Page 2', 'Page 3', 'Page 4', 'Page 5', 'Custom...', 'Cancel'], + defaultId: templateStartPage - 1, + cancelId: 6 + }); + + if (result.response === 6) return; // Cancel + + let newStartPage; + if (result.response === 5) { // Custom + // Show input dialog for custom page number + mainWindow.webContents.send('show-custom-start-page-dialog', templateStartPage); + } else { + newStartPage = result.response + 1; // Convert button index to page number + templateStartPage = newStartPage; + store.set('templateStartPage', templateStartPage); + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Settings Updated', + message: 'Template settings have been updated', + detail: `Content will now start from page ${templateStartPage}` + }); + } +} + +// Handle custom start page input from renderer +ipcMain.on('set-custom-start-page', (event, pageNumber) => { + const page = parseInt(pageNumber); + if (page >= 1 && page <= 100) { + templateStartPage = page; + store.set('templateStartPage', templateStartPage); + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Settings Updated', + message: 'Template settings have been updated', + detail: `Content will now start from page ${templateStartPage}` + }); + } else { + dialog.showErrorBox('Invalid Page Number', 'Please enter a page number between 1 and 100'); + } +}); + +// Header & Footer Settings IPC Handlers + +// Get current header/footer settings +ipcMain.on('get-header-footer-settings', (event) => { + event.reply('header-footer-settings-data', headerFooterSettings); +}); + +// Save header/footer settings +ipcMain.on('save-header-footer-settings', (event, settings) => { + headerFooterSettings = settings; + store.set('headerFooterSettings', headerFooterSettings); + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Settings Saved', + message: 'Header and footer settings have been saved successfully!', + buttons: ['OK'] + }); +}); + +// Get current page settings +ipcMain.on('get-page-settings', (event) => { + event.reply('page-settings-data', pageSettings); +}); + +// Update page settings from export dialog +ipcMain.on('update-page-settings', (event, settings) => { + pageSettings = settings; + store.set('pageSettings', pageSettings); + console.log('Page settings updated:', pageSettings); +}); + +// Save header/footer logo image +// Browse for header/footer logo +ipcMain.on('browse-header-footer-logo', async (event, position) => { + try { + const result = await dialog.showOpenDialog(mainWindow, { + title: `Select ${position.charAt(0).toUpperCase() + position.slice(1)} Logo/Image`, + filters: [ + { name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp'] } + ], + properties: ['openFile'] + }); + + if (!result.canceled && result.filePaths.length > 0) { + const filePath = result.filePaths[0]; + + // Copy image to userData directory for persistent storage + const userDataPath = app.getPath('userData'); + const logoDir = path.join(userDataPath, 'logos'); + + // Create logos directory if it doesn't exist + if (!fs.existsSync(logoDir)) { + fs.mkdirSync(logoDir, { recursive: true }); + } + + // Generate unique filename + const ext = path.extname(filePath); + const filename = `${position}_${Date.now()}${ext}`; + const destPath = path.join(logoDir, filename); + + // Copy file + fs.copyFileSync(filePath, destPath); + + // Update settings + if (position === 'header') { + headerFooterSettings.header.logo = destPath; + } else if (position === 'footer') { + headerFooterSettings.footer.logo = destPath; + } + + event.reply('header-footer-logo-saved', { position, path: destPath }); + } + } catch (error) { + console.error('Logo browse error:', error); + dialog.showErrorBox('Logo Error', `Failed to select logo: ${error.message}`); + } +}); + +ipcMain.on('save-header-footer-logo', async (event, { position, filePath }) => { + try { + if (!filePath) { + dialog.showErrorBox('Logo Error', 'Failed to save logo: The "path" argument must be of type string. Received undefined'); + return; + } + + // Copy image to userData directory for persistent storage + const userDataPath = app.getPath('userData'); + const logoDir = path.join(userDataPath, 'logos'); + + // Create logos directory if it doesn't exist + if (!fs.existsSync(logoDir)) { + fs.mkdirSync(logoDir, { recursive: true }); + } + + // Verify source file exists + if (!fs.existsSync(filePath)) { + dialog.showErrorBox('Logo Error', `Source file not found: ${filePath}`); + return; + } + + // Generate unique filename + const ext = path.extname(filePath); + const filename = `${position}_${Date.now()}${ext}`; + const destPath = path.join(logoDir, filename); + + // Copy file + fs.copyFileSync(filePath, destPath); + + // Update settings + if (position === 'header') { + headerFooterSettings.header.logo = destPath; + } else if (position === 'footer') { + headerFooterSettings.footer.logo = destPath; + } + + event.reply('header-footer-logo-saved', { position, path: destPath }); + } catch (error) { + console.error('Logo save error:', error); + dialog.showErrorBox('Logo Error', `Failed to save logo: ${error.message}`); + } +}); + +// Clear header/footer logo +ipcMain.on('clear-header-footer-logo', (event, position) => { + if (position === 'header') { + headerFooterSettings.header.logo = null; + } else if (position === 'footer') { + headerFooterSettings.footer.logo = null; + } + event.reply('header-footer-logo-cleared', position); +}); + +// Helper function to process dynamic fields in header/footer text +function processDynamicFields(text, metadata = {}) { + if (!text) return ''; + + const now = new Date(); + const dateStr = now.toLocaleDateString(); + const timeStr = now.toLocaleTimeString(); + + let result = text; + result = result.replace(/\$DATE\$/g, dateStr); + result = result.replace(/\$TIME\$/g, timeStr); + result = result.replace(/\$TITLE\$/g, metadata.title || 'Untitled'); + result = result.replace(/\$AUTHOR\$/g, metadata.author || ''); + result = result.replace(/\$FILENAME\$/g, metadata.filename || ''); + + // Note: $PAGE$ and $TOTAL$ are handled by Pandoc/export tools + + return result; +} + +// Add headers/footers to DOCX file using PizZip and docx4js +// Function to set page size in DOCX files +async function setDocxPageSize(docxPath) { + try { + const PizZip = require('pizzip'); + + // Read the DOCX file + const docxBuffer = fs.readFileSync(docxPath); + const zip = new PizZip(docxBuffer); + + // Get document.xml + let documentXml = zip.file('word/document.xml').asText(); + + // Get page dimensions + let width, height; + const pageSize = PAGE_SIZES[pageSettings.size]; + + if (pageSize) { + width = pageSize.word.width; + height = pageSize.word.height; + } else if (pageSettings.customWidth && pageSettings.customHeight) { + // Parse custom dimensions (convert to twentieths of a point) + // Note: This is simplified - production code should handle various units + width = parseInt(pageSettings.customWidth) || 11906; + height = parseInt(pageSettings.customHeight) || 16838; + } else { + // Default to A4 + width = 11906; + height = 16838; + } + + // Swap dimensions for landscape + if (pageSettings.orientation === 'landscape') { + [width, height] = [height, width]; + } + + // Update all elements in section properties + const pgSzRegex = /]*\/>/g; + documentXml = documentXml.replace(pgSzRegex, () => { + return ``; + }); + + // If no pgSz found, add it to all sectPr elements + if (!pgSzRegex.test(documentXml)) { + const sectPrRegex = /]*>/g; + documentXml = documentXml.replace(sectPrRegex, (match) => { + return `${match}`; + }); + } + + // Save updated document.xml + zip.file('word/document.xml', documentXml); + + // Write modified DOCX + const newDocxBuffer = zip.generate({ type: 'nodebuffer' }); + fs.writeFileSync(docxPath, newDocxBuffer); + + } catch (error) { + console.error('Failed to set DOCX page size:', error); + } +} + +async function addHeaderFooterToDocx(docxPath, metadata = {}) { + if (!headerFooterSettings.enabled) return; + + try { + const PizZip = require('pizzip'); + + // Read the DOCX file + const docxBuffer = fs.readFileSync(docxPath); + const zip = new PizZip(docxBuffer); + + // Process dynamic fields + const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); + const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); + const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); + const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); + const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); + const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); + + // Create header XML + if (headerLeft || headerCenter || headerRight) { + const headerXml = ` + + + + ${headerLeft || ''} + + + + ${headerCenter || ''} + + + + ${headerRight || ''} + +`; + zip.file('word/header1.xml', headerXml); + } + + // Create footer XML with page numbers + if (footerLeft || footerCenter || footerRight) { + let footerCenterXml = ''; + if (footerCenter) { + // Handle $PAGE$ and $TOTAL$ in footer + if (footerCenter.includes('$PAGE$') || footerCenter.includes('$TOTAL$')) { + const parts = footerCenter.split(/(\$PAGE\$|\$TOTAL\$)/); + footerCenterXml = parts.map(part => { + if (part === '$PAGE$') { + return ''; + } else if (part === '$TOTAL$') { + return ''; + } else { + return `${part}`; + } + }).join(''); + } else { + footerCenterXml = `${footerCenter}`; + } + } + + const footerXml = ` + + + + ${footerLeft || ''} + + + + ${footerCenterXml} + + + + ${footerRight || ''} + +`; + zip.file('word/footer1.xml', footerXml); + } + + // Update document.xml.rels to reference header/footer + let relsXml = zip.file('word/_rels/document.xml.rels').asText(); + + // Add header relationship if not exists + if ((headerLeft || headerCenter || headerRight) && !relsXml.includes('header1.xml')) { + const headerId = 'rId100'; + const headerRel = ``; + relsXml = relsXml.replace('', headerRel + ''); + } + + // Add footer relationship if not exists + if ((footerLeft || footerCenter || footerRight) && !relsXml.includes('footer1.xml')) { + const footerId = 'rId101'; + const footerRel = ``; + relsXml = relsXml.replace('', footerRel + ''); + } + + zip.file('word/_rels/document.xml.rels', relsXml); + + // Update document.xml to use header/footer in sections + let documentXml = zip.file('word/document.xml').asText(); + if ((headerLeft || headerCenter || headerRight || footerLeft || footerCenter || footerRight)) { + // Find all section properties and add header/footer references + const sectPrRegex = /]*>[\s\S]*?<\/w:sectPr>/g; + documentXml = documentXml.replace(sectPrRegex, (match) => { + let updated = match; + if ((headerLeft || headerCenter || headerRight) && !match.includes('headerReference')) { + updated = updated.replace('', ''); + } + if ((footerLeft || footerCenter || footerRight) && !match.includes('footerReference')) { + updated = updated.replace('', ''); + } + return updated; + }); + } + + zip.file('word/document.xml', documentXml); + + // Write modified DOCX + const newDocxBuffer = zip.generate({ type: 'nodebuffer' }); + fs.writeFileSync(docxPath, newDocxBuffer); + + } catch (error) { + console.error('Failed to add headers/footers to DOCX:', error); + // Don't fail the export, just log the error + } +} + +// Enhanced Word Export with Template Support +async function exportWordWithTemplate() { + if (!currentFile) { + dialog.showErrorBox('Error', 'Please save the file first'); + return; + } + + try { + // Get markdown content + const content = fs.readFileSync(currentFile, 'utf-8'); + + // Show dialog for output file + const result = await dialog.showSaveDialog(mainWindow, { + title: 'Export to Word (Enhanced)', + defaultPath: currentFile.replace(/\.md$/, '.docx'), + filters: [{ name: 'Word Document', extensions: ['docx'] }] + }); + + if (result.canceled) return; + + // Create exporter instance with selected template, start page, and page settings + const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage, pageSettings); + + // Convert markdown to DOCX + await exporter.convert(content, result.filePath); + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Export Successful', + message: 'Document exported successfully!', + detail: `Saved to: ${result.filePath}` + }); + + } catch (error) { + dialog.showErrorBox('Export Error', `Failed to export document: ${error.message}`); + } +} + +// Enhanced PDF Export via Word Template +async function exportPDFViaWordTemplate() { + if (!currentFile) { + dialog.showErrorBox('Error', 'Please save the file first'); + return; + } + + try { + // Get markdown content + const content = fs.readFileSync(currentFile, 'utf-8'); + + // Show dialog for output file + const result = await dialog.showSaveDialog(mainWindow, { + title: 'Export to PDF (Enhanced)', + defaultPath: currentFile.replace(/\.md$/, '.pdf'), + filters: [{ name: 'PDF Document', extensions: ['pdf'] }] + }); + + if (result.canceled) return; + + // Step 1: Create temporary DOCX file using Word template + const tempDocxPath = result.filePath.replace(/\.pdf$/, '_temp.docx'); + + const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage); + await exporter.convert(content, tempDocxPath); + + // Step 2: Convert DOCX to PDF using LibreOffice (using execFile for safety) + const soffice = process.platform === 'win32' + ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' + : 'soffice'; + + const outputDir = path.dirname(result.filePath); + const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath]; + + execFile(soffice, sofficeArgs, (error, stdout, stderr) => { + // Clean up temporary DOCX file + try { + fs.unlinkSync(tempDocxPath); + } catch (e) { + console.error('Failed to delete temp file:', e); + } + + if (error) { + dialog.showErrorBox('PDF Conversion Error', + `Failed to convert to PDF. Please ensure LibreOffice is installed.\n\nError: ${error.message}`); + return; + } + + // LibreOffice creates file with same base name as input + const generatedPdfPath = tempDocxPath.replace(/\.docx$/, '.pdf'); + + // Rename if needed + if (generatedPdfPath !== result.filePath) { + try { + fs.renameSync(generatedPdfPath, result.filePath); + } catch (e) { + console.error('Failed to rename PDF:', e); + } + } + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Export Successful', + message: 'PDF exported successfully using Word template!', + detail: `Saved to: ${result.filePath}` + }); + }); + + } catch (error) { + dialog.showErrorBox('Export Error', `Failed to export PDF: ${error.message}`); + } +} + +// Universal File Converter integration +function showUniversalConverterDialog() { + mainWindow.webContents.send('show-universal-converter-dialog'); +} + +// PDF Editor dialog +function showPDFEditorDialog(operation) { + mainWindow.webContents.send('show-pdf-editor-dialog', operation); +} + +// Handle PDF editor from toolbar (with optional file path) +ipcMain.on('show-pdf-editor-from-toolbar', (event, { operation, filePath }) => { + mainWindow.webContents.send('show-pdf-editor-dialog', operation, filePath); +}); + +// Check if conversion tool is available (using execFile for safety) +function checkConverterAvailable(tool) { + return new Promise((resolve) => { + const isWin = process.platform === 'win32'; + const locateCmd = isWin ? 'where' : 'which'; + let toolName; + + switch (tool) { + case 'libreoffice': + toolName = 'soffice'; + break; + case 'imagemagick': + toolName = isWin ? 'magick' : 'convert'; + break; + case 'ffmpeg': + toolName = 'ffmpeg'; + break; + default: + resolve(false); + return; + } + + execFile(locateCmd, [toolName], (error) => { + resolve(!error); + }); + }); +} + +// Handle universal file conversion +ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, filePath }) => { + try { + mainWindow.webContents.send('conversion-status', 'Checking converter availability...'); + + // Check if the required tool is available + const toolAvailable = await checkConverterAvailable(tool); + + if (!toolAvailable) { + throw new Error(`${tool} is not installed or not found in PATH. Please install it first.`); + } + + mainWindow.webContents.send('conversion-status', 'Converting file...'); + + const outputPath = filePath.replace(/\.[^/.]+$/, `.${toFormat}`); + let conversionInfo; + + switch (tool) { + case 'libreoffice': + conversionInfo = convertWithLibreOffice(filePath, toFormat, outputPath); + break; + case 'imagemagick': + conversionInfo = convertWithImageMagick(filePath, outputPath); + break; + case 'ffmpeg': + conversionInfo = convertWithFFmpeg(filePath, outputPath); + break; + case 'pandoc': + conversionInfo = convertWithPandoc(filePath, outputPath); + break; + default: + throw new Error(`Unknown conversion tool: ${tool}`); + } + + // Use execFile for safety (prevents command injection) + execFile(conversionInfo.command, conversionInfo.args, (error, stdout, stderr) => { + if (error) { + mainWindow.webContents.send('conversion-complete', { + success: false, + error: error.message + }); + + dialog.showMessageBox(mainWindow, { + type: 'error', + title: 'Conversion Failed', + message: `${tool} conversion failed`, + detail: stderr || error.message, + buttons: ['OK'] + }); + } else { + mainWindow.webContents.send('conversion-complete', { + success: true, + outputPath: outputPath + }); + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Conversion Complete', + message: 'File converted successfully!', + detail: `Saved to: ${outputPath}`, + buttons: ['OK'] + }); + } + }); + } catch (error) { + mainWindow.webContents.send('conversion-complete', { + success: false, + error: error.message + }); + + dialog.showMessageBox(mainWindow, { + type: 'error', + title: 'Conversion Failed', + message: 'Universal conversion failed', + detail: error.message, + buttons: ['OK'] + }); + } +}); + +// LibreOffice conversion - returns {command, args} for execFile (safer than exec) +function convertWithLibreOffice(inputFile, outputFormat, outputPath) { + const outputDir = path.dirname(outputPath); + const soffice = process.platform === 'win32' + ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' + : 'soffice'; + + // LibreOffice conversion format mapping + const formatMap = { + 'pdf': 'pdf', + 'docx': 'docx', + 'doc': 'doc', + 'odt': 'odt', + 'rtf': 'rtf', + 'txt': 'txt', + 'html': 'html', + 'xlsx': 'xlsx', + 'xls': 'xls', + 'ods': 'ods', + 'csv': 'csv', + 'pptx': 'pptx', + 'ppt': 'ppt', + 'odp': 'odp' + }; + + const format = formatMap[outputFormat] || outputFormat; + + // Return command and args for execFile + return { + command: soffice, + args: ['--headless', '--convert-to', format, '--outdir', outputDir, inputFile] + }; +} + +// ImageMagick conversion - returns {command, args} for execFile +function convertWithImageMagick(inputFile, outputPath) { + const magick = process.platform === 'win32' ? 'magick' : 'convert'; + return { + command: magick, + args: [inputFile, outputPath] + }; +} + +// FFmpeg conversion - returns {command, args} for execFile +function convertWithFFmpeg(inputFile, outputPath) { + return { + command: 'ffmpeg', + args: ['-i', inputFile, outputPath, '-y'] + }; +} + +// Pandoc conversion - returns {command, args} for execFile +function convertWithPandoc(inputFile, outputPath) { + return { + command: 'pandoc', + args: [inputFile, '-o', outputPath] + }; +} + +function performExportWithOptions(format, options) { + const outputFile = dialog.showSaveDialogSync(mainWindow, { + defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`), + filters: [ + { name: format.toUpperCase(), extensions: [format] } + ] + }); + + if (!outputFile) return; // User cancelled + + console.log(`Attempting to export ${format} to:`, outputFile); + + // Check pandoc availability first + checkPandocAvailability().then((hasPandoc) => { + console.log('Pandoc available:', hasPandoc); + + if (!hasPandoc) { + // Handle formats that don't require pandoc + if (format === 'html') { + console.log('Using built-in HTML export'); + exportToHTML(outputFile); + return; + } else if (format === 'pdf') { + console.log('Using built-in PDF export'); + exportToPDFElectron(outputFile); + return; + } else { + dialog.showErrorBox('Export Error', + `Pandoc is required for ${format.toUpperCase()} export but is not installed or not found in PATH.\n\n` + + `Please install Pandoc from: https://pandoc.org/installing.html\n\n` + + `Alternatively, you can export to HTML or PDF using the built-in converters.` + ); + return; + } + } + + // Use pandoc for export with advanced options + console.log('Using Pandoc for export'); + let pandocCmd = `${getPandocPath()} "${currentFile}" -o "${outputFile}"`; + + // Add template if specified + if (options.template && options.template !== 'default') { + pandocCmd += ` --template="${options.template}"`; + } + + // Add metadata + if (options.metadata) { + for (const [key, value] of Object.entries(options.metadata)) { + if (value.trim()) { + pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; + } + } + } + + // Add variables + if (options.variables) { + for (const [key, value] of Object.entries(options.variables)) { + if (value.trim()) { + pandocCmd += ` -V ${key}="${value.replace(/"/g, '\\"')}"`; + } + } + } + + // Add other options + if (options.toc) pandocCmd += ' --toc'; + if (options.tocDepth) pandocCmd += ` --toc-depth=${options.tocDepth}`; + if (options.numberSections) pandocCmd += ' --number-sections'; + if (options.citeproc) pandocCmd += ' --citeproc'; + if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`; + if (options.csl) pandocCmd += ` --csl="${options.csl}"`; + + // Add specific options for PDF export to ensure proper generation + if (format === 'pdf') { + const pdfEngine = options.pdfEngine || 'xelatex'; // Default to xelatex + pandocCmd += ` --pdf-engine="${pdfEngine}"`; + if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; + + // Add monospace font settings for code blocks (ASCII art preservation) + pandocCmd += ' -V monofont="Consolas"'; + pandocCmd += ' --highlight-style=tango'; + + // Add header/footer if enabled + if (headerFooterSettings.enabled) { + const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; + const metadata = { filename, title: filename, author: '' }; + + const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); + const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); + const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); + const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); + const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); + const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); + + // Create LaTeX header + const latexHeader = ` +\\usepackage{fancyhdr} +\\pagestyle{fancy} +\\fancyhf{} +\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} +\\chead{${headerCenter.replace(/\\/g, '\\\\')}} +\\rhead{${headerRight.replace(/\\/g, '\\\\')}} +\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} +\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} +\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} +\\renewcommand{\\headrulewidth}{0.4pt} +\\renewcommand{\\footrulewidth}{0.4pt} +`; + const headerFile = path.join(require('os').tmpdir(), `header_export_${Date.now()}.tex`); + fs.writeFileSync(headerFile, latexHeader, 'utf-8'); + pandocCmd += ` --include-in-header="${headerFile}"`; + pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"'; + } + + // Try with specified PDF engine (using runPandocCmd for safety) + runPandocCmd(pandocCmd, (error) => { + if (error) { + // Try fallback engines if the specified one fails + const fallbackEngines = ['pdflatex', 'lualatex']; + tryPdfFallback(currentFile, outputFile, fallbackEngines, 0, options, error); + } else { + showExportSuccess(outputFile); + } + }); + } else if (format === 'docx') { + pandocCmd += ' -t docx'; + exportWithPandoc(pandocCmd, outputFile, format); + } else if (format === 'pptx') { + // Add PowerPoint footer if enabled + if (headerFooterSettings.enabled && headerFooterSettings.footer.center) { + const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; + const metadata = { filename, title: filename, author: '' }; + const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata); + pandocCmd += ` --variable footer="${footerText}"`; + } + exportWithPandoc(pandocCmd, outputFile, format); + } else { + // Generic export for other formats + exportWithPandoc(pandocCmd, outputFile, format); + } + }).catch((error) => { + console.error('Error checking pandoc availability:', error); + dialog.showErrorBox('Export Error', `Error checking system requirements: ${error.message}`); + }); +} + +function tryPdfFallback(inputFile, outputFile, engines, index, options, lastError) { + if (index >= engines.length) { + // All Pandoc PDF engines failed, fallback to Electron's built-in PDF export + console.log('All Pandoc PDF engines failed, falling back to Electron PDF export'); + exportToPDFElectron(outputFile); + return; + } + + const engine = engines[index]; + let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -o "${outputFile}"`; + + // Add monospace font settings for code blocks (ASCII art preservation) + pandocCmd += ' -V monofont="Consolas"'; + pandocCmd += ' --highlight-style=tango'; + + // Add geometry if specified + if (options.geometry) pandocCmd = pandocCmd.replace(` -o `, ` -V geometry:"${options.geometry}" -o `); + + // Add header/footer if enabled + if (headerFooterSettings.enabled) { + const filename = path.basename(inputFile, path.extname(inputFile)); + const metadata = { filename, title: filename, author: options.metadata?.author || '' }; + + const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); + const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); + const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); + const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); + const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); + const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); + + // Create LaTeX header for fancyhdr + const latexHeader = ` +\\usepackage{fancyhdr} +\\usepackage{lastpage} +\\pagestyle{fancy} +\\fancyhf{} +\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} +\\chead{${headerCenter.replace(/\\/g, '\\\\')}} +\\rhead{${headerRight.replace(/\\/g, '\\\\')}} +\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} +\\cfoot{${footerCenter.replace(/\$PAGE\$/g, '\\\\thepage').replace(/\$TOTAL\$/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} +\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} +\\renewcommand{\\headrulewidth}{0.4pt} +\\renewcommand{\\footrulewidth}{0.4pt} +`; + const headerFile = path.join(require('os').tmpdir(), `header_fallback_${Date.now()}.tex`); + fs.writeFileSync(headerFile, latexHeader, 'utf-8'); + pandocCmd += ` --include-in-header="${headerFile}"`; + } + + // Add all other options + if (options.template && options.template !== 'default') { + pandocCmd += ` --template="${options.template}"`; + } + + if (options.metadata) { + for (const [key, value] of Object.entries(options.metadata)) { + if (value.trim()) { + pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; + } + } + } + + // Use runPandocCmd for safety (prevents command injection) + runPandocCmd(pandocCmd, (error) => { + if (error) { + tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error); + } else { + showExportSuccess(outputFile); + } + }); +} + +function showExportSuccess(outputFile) { + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Export Complete', + message: `File exported successfully to ${outputFile}`, + buttons: ['OK'] + }); +} + +// Helper function to export with pandoc (general) - uses runPandocCmd for safety +function exportWithPandoc(pandocCmd, outputFile, format) { + console.log(`Executing Pandoc command: ${pandocCmd}`); + + runPandocCmd(pandocCmd, async (error, stdout, stderr) => { + if (error) { + console.error(`Pandoc error for ${format}:`, error); + console.error(`Pandoc stderr:`, stderr); + console.error(`Pandoc stdout:`, stdout); + + // Provide more specific error messages + let errorMessage = `Failed to export to ${format.toUpperCase()}`; + + if (error.message.includes('not found') || error.message.includes('not recognized')) { + errorMessage += '\n\nPandoc is not installed or not found in PATH.'; + errorMessage += '\nPlease install Pandoc from: https://pandoc.org/installing.html'; + } else if (stderr) { + errorMessage += `\n\nError details: ${stderr}`; + } else { + errorMessage += `\n\nError details: ${error.message}`; + } + + errorMessage += `\n\nCommand used: ${pandocCmd}`; + + dialog.showErrorBox('Export Error', errorMessage); + } else { + console.log(`Successfully exported to ${format}:`, outputFile); + console.log(`Pandoc stdout:`, stdout); + if (stderr) { + console.warn(`Pandoc stderr (non-fatal):`, stderr); + } + + // Set page size for DOCX + if (format === 'docx') { + try { + await setDocxPageSize(outputFile); + console.log('Page size set for DOCX'); + } catch (pageSizeError) { + console.error('Error setting page size for DOCX:', pageSizeError); + } + } + + // Add headers/footers to DOCX if enabled + if (format === 'docx' && headerFooterSettings.enabled) { + try { + const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; + const metadata = { + filename: filename, + title: filename, + author: '' + }; + await addHeaderFooterToDocx(outputFile, metadata); + console.log('Headers/footers added to DOCX'); + } catch (hfError) { + console.error('Error adding headers/footers to DOCX:', hfError); + // Continue with success message even if header/footer fails + } + } + + // Set page size for ODT + if (format === 'odt') { + try { + await setDocxPageSize(outputFile); // ODT has similar structure + console.log('Page size set for ODT'); + } catch (pageSizeError) { + console.error('Error setting page size for ODT:', pageSizeError); + } + } + + // Add headers/footers to ODT if enabled + if (format === 'odt' && headerFooterSettings.enabled) { + // ODT format is similar to DOCX in structure, we could implement this + console.log('ODT header/footer support not yet implemented'); + } + + showExportSuccess(outputFile); + } + }); +} + +// Helper function to export PDF with pandoc (with fallbacks) - uses runPandocCmd for safety +function exportWithPandocPDF(pandocCmd, outputFile) { + runPandocCmd(pandocCmd, (error, stdout, stderr) => { + if (error) { + console.log('XeLaTeX failed, trying PDFLaTeX...'); + // Fallback to pdflatex + const fallbackCmd = pandocCmd.replace('--pdf-engine=xelatex', '--pdf-engine=pdflatex'); + runPandocCmd(fallbackCmd, (fallbackError, fallbackStdout, fallbackStderr) => { + if (fallbackError) { + console.log('PDFLaTeX failed, trying Electron PDF...'); + // Final fallback to Electron PDF + exportToPDFElectron(outputFile); + } else { + console.log('Successfully exported PDF with PDFLaTeX'); + showExportSuccess(outputFile); + } + }); + } else { + console.log('Successfully exported PDF with XeLaTeX'); + showExportSuccess(outputFile); + } + }); +} + +// Export to HTML using marked (no pandoc required) +function exportToHTML(outputFile) { + try { + const marked = require('marked'); + const markdownContent = fs.readFileSync(currentFile, 'utf8'); + const htmlContent = marked.parse(markdownContent); + + const fullHtml = ` + + + + + Exported Document + + + + ${htmlContent} + +`; + + fs.writeFileSync(outputFile, fullHtml, 'utf8'); + console.log('Successfully exported HTML'); + showExportSuccess(outputFile); + } catch (error) { + console.error('HTML export error:', error); + dialog.showErrorBox('HTML Export Error', `Failed to export HTML: ${error.message}`); + } +} + +// Export to PDF using Electron (no pandoc required) +function exportToPDFElectron(outputFile) { + try { + const marked = require('marked'); + const markdownContent = fs.readFileSync(currentFile, 'utf8'); + const htmlContent = marked.parse(markdownContent); + + const fullHtml = ` + + + + + PDF Export + + + + ${htmlContent} + +`; + + // Create a hidden window to render and export PDF + const pdfWindow = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + + pdfWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(fullHtml)}`).then(() => { + return pdfWindow.webContents.printToPDF({ + marginsType: 1, // Use default margins + pageSize: 'A4', + printBackground: true, + printSelectionOnly: false, + landscape: false + }); + }).then((pdfData) => { + fs.writeFileSync(outputFile, pdfData); + pdfWindow.close(); + console.log('Successfully exported PDF with Electron'); + showExportSuccess(outputFile); + }).catch((error) => { + pdfWindow.close(); + console.error('Electron PDF export error:', error); + dialog.showErrorBox('PDF Export Error', + `Failed to export PDF using built-in engine: ${error.message}\n\n` + + `For better PDF export, please install Pandoc with LaTeX support.` + ); + }); + } catch (error) { + console.error('PDF export setup error:', error); + dialog.showErrorBox('PDF Export Error', `Failed to setup PDF export: ${error.message}`); + } +} + +function exportSpreadsheet(format) { + if (!currentFile) { + dialog.showErrorBox('Error', 'Please save the file first'); + return; + } + + // Request content from renderer + mainWindow.webContents.send('get-content-for-spreadsheet', format); +} + +function importDocument() { + const files = dialog.showOpenDialogSync(mainWindow, { + properties: ['openFile'], + filters: [ + { name: 'Documents', extensions: ['docx', 'odt', 'rtf', 'html', 'htm', 'tex', 'epub', 'pdf', 'txt'] }, + { name: 'Presentations', extensions: ['pptx', 'odp'] }, + { name: 'Markup Languages', extensions: ['rst', 'textile', 'mediawiki', 'org', 'asciidoc', 'twiki', 'opml'] }, + { name: 'E-book Formats', extensions: ['epub', 'fb2'] }, + { name: 'LaTeX Formats', extensions: ['tex', 'latex', 'ltx'] }, + { name: 'Web Formats', extensions: ['html', 'htm', 'xhtml'] }, + { name: 'Wiki Formats', extensions: ['mediawiki', 'dokuwiki', 'tikiwiki', 'twiki'] }, + { name: 'CSV/TSV', extensions: ['csv', 'tsv'] }, + { name: 'JSON', extensions: ['json'] }, + { name: 'All Files', extensions: ['*'] } + ] + }); + + if (files && files[0]) { + const inputFile = files[0]; + const ext = path.extname(inputFile).toLowerCase().slice(1); + const outputFile = inputFile.replace(/\.[^/.]+$/, '.md'); + + // Determine format-specific conversion options + let additionalOptions = ''; + + // For PDFs, extract text properly + if (ext === 'pdf') { + additionalOptions = '--pdf-engine=xelatex'; + } + + // For CSV/TSV, convert as tables + if (ext === 'csv' || ext === 'tsv') { + additionalOptions = '--from=csv -t markdown'; + } + + // For JSON, handle structure + if (ext === 'json') { + additionalOptions = '--from=json -t markdown'; + } + + // Convert to markdown using pandoc (using runPandocCmd for safety) + const pandocCmd = `${getPandocPath()} "${inputFile}" -t markdown ${additionalOptions} -o "${outputFile}"`; + + runPandocCmd(pandocCmd, (error, stdout, stderr) => { + if (error) { + dialog.showErrorBox('Import Error', `Failed to import: ${error.message}\n\nMake sure Pandoc is installed.\n\nSupported formats: DOCX, ODT, RTF, HTML, LaTeX, EPUB, PDF, PPTX, ODP, RST, Textile, MediaWiki, Org-mode, AsciiDoc, CSV, and more.`); + } else { + // Open the converted markdown file + currentFile = outputFile; + const content = fs.readFileSync(outputFile, 'utf-8'); + mainWindow.webContents.send('file-opened', { path: outputFile, content }); + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Import Complete', + message: `Document imported successfully as ${path.basename(outputFile)}\n\nOriginal format: ${ext.toUpperCase()}\nConverted to: Markdown`, + buttons: ['OK'] + }); + } + }); + } +} + + +function setTheme(theme) { + store.set('theme', theme); + mainWindow.webContents.send('theme-changed', theme); +} + +// IPC handlers +ipcMain.on('save-file', (event, { path, content }) => { + fs.writeFileSync(path, content, 'utf-8'); + currentFile = path; +}); + +ipcMain.on('save-current-file', (event, content) => { + if (currentFile) { + fs.writeFileSync(currentFile, content, 'utf-8'); + } else { + saveAsFile(); + } +}); + +ipcMain.on('get-theme', (event) => { + const theme = store.get('theme', 'atomonelight'); + event.reply('theme-changed', theme); +}); + +// Handle tab file tracking for exports +ipcMain.on('set-current-file', (event, filePath) => { + currentFile = filePath; +}); + +// Handle actual printing when renderer is ready +ipcMain.on('do-print', (event, { withStyles }) => { + if (mainWindow) { + // Renderer has already hidden UI, waited 300ms, and prepared the page + // Print immediately - DOM is fully rendered + mainWindow.webContents.print({ + silent: false, + printBackground: withStyles, + color: true, + margin: { marginType: 'default' } + }); + } +}); + +// Handle renderer ready for file association +ipcMain.on('renderer-ready', (event) => { + console.log('[MAIN] renderer-ready received, rendererReady was:', rendererReady); + if (mainWindow) { + mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] renderer-ready received, rendererReady was: ${rendererReady}')`); + } + rendererReady = true; + console.log('[MAIN] app.pendingFile:', app.pendingFile); + if (mainWindow) { + mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] app.pendingFile: ${app.pendingFile}')`); + } + if (app.pendingFile) { + console.log('[MAIN] Opening pending file:', app.pendingFile); + if (mainWindow) { + mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] Opening pending file: ${app.pendingFile}')`); + } + openFileFromPath(app.pendingFile); + app.pendingFile = null; + } +}); + +// Handle export with options +ipcMain.on('export-with-options', (event, { format, options }) => { + performExportWithOptions(format, options); +}); + +// Handle batch conversion +ipcMain.on('batch-convert', (event, { inputFolder, outputFolder, format, options }) => { + performBatchConversion(inputFolder, outputFolder, format, options); +}); + +// Handle folder selection for batch conversion +ipcMain.on('select-folder', (event, type) => { + const folder = dialog.showOpenDialogSync(mainWindow, { + properties: ['openDirectory'] + }); + + if (folder && folder[0]) { + event.reply('folder-selected', { type, path: folder[0] }); + } +}); + +ipcMain.on('export-spreadsheet', (event, { content, format }) => { + const outputFile = dialog.showSaveDialogSync(mainWindow, { + defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`), + filters: [ + { name: format.toUpperCase(), extensions: [format] } + ] + }); + + if (outputFile) { + try { + // Parse markdown content to extract tables + const tables = extractTablesFromMarkdown(content); + + if (tables.length === 0) { + dialog.showErrorBox('Export Error', 'No tables found in the markdown content'); + return; + } + + if (format === 'csv') { + // Convert tables to CSV format + let csvContent = ''; + tables.forEach((table, index) => { + if (index > 0) csvContent += '\n\n'; // Separate multiple tables + if (tables.length > 1) csvContent += `"Table ${index + 1}"\n`; + + table.forEach(row => { + const csvRow = row.map(cell => { + // Escape quotes and wrap in quotes if necessary + const cleanCell = cell.replace(/"/g, '""'); + return cleanCell.includes(',') || cleanCell.includes('"') || cleanCell.includes('\n') + ? `"${cleanCell}"` : cleanCell; + }).join(','); + csvContent += csvRow + '\n'; + }); + }); + + fs.writeFileSync(outputFile, csvContent, 'utf-8'); + } + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Export Complete', + message: `${format.toUpperCase()} exported successfully to ${outputFile}`, + buttons: ['OK'] + }); + } catch (error) { + dialog.showErrorBox('Export Error', `Failed to export: ${error.message}`); + } + } +}); + +// Helper function to extract tables from markdown +function extractTablesFromMarkdown(markdown) { + const tables = []; + const lines = markdown.split('\n'); + let currentTable = []; + let inTable = false; + + for (const line of lines) { + if (line.includes('|')) { + if (!inTable) { + inTable = true; + currentTable = []; + } + + // Skip separator lines (|---|---|) + if (!line.match(/^\s*\|?\s*:?-+:?\s*\|/)) { + const cells = line.split('|') + .map(cell => cell.trim()) + .filter(cell => cell !== ''); + + if (cells.length > 0) { + currentTable.push(cells); + } + } + } else if (inTable && line.trim() === '') { + // End of table + if (currentTable.length > 0) { + tables.push(currentTable); + } + currentTable = []; + inTable = false; + } + } + + // Add last table if exists + if (currentTable.length > 0) { + tables.push(currentTable); + } + + return tables; +} + +function performBatchConversion(inputFolder, outputFolder, format, options) { + if (!fs.existsSync(inputFolder)) { + dialog.showErrorBox('Error', 'Input folder does not exist'); + return; + } + + // Create output folder if it doesn't exist + if (!fs.existsSync(outputFolder)) { + try { + fs.mkdirSync(outputFolder, { recursive: true }); + } catch (error) { + dialog.showErrorBox('Error', `Failed to create output folder: ${error.message}`); + return; + } + } + + // Find all markdown files in input folder + const markdownFiles = []; + + function findMarkdownFiles(dir) { + const files = fs.readdirSync(dir); + + for (const file of files) { + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + findMarkdownFiles(fullPath); // Recursive search + } else if (file.match(/\.(md|markdown)$/i)) { + markdownFiles.push(fullPath); + } + } + } + + findMarkdownFiles(inputFolder); + + if (markdownFiles.length === 0) { + dialog.showErrorBox('No Files Found', 'No markdown files found in the selected folder'); + return; + } + + // Show progress dialog + let completedCount = 0; + const totalCount = markdownFiles.length; + + // Process each file + const processNextFile = async (index) => { + if (index >= markdownFiles.length) { + // All files processed + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Batch Conversion Complete', + message: `Successfully converted ${completedCount} out of ${totalCount} files to ${format.toUpperCase()}.`, + buttons: ['OK'] + }); + return; + } + + const inputFile = markdownFiles[index]; + const relativePath = path.relative(inputFolder, inputFile); + const baseName = path.basename(relativePath, path.extname(relativePath)); + let outputExtension = format; + if (format === 'docx-enhanced') outputExtension = 'docx'; + if (format === 'pdf-enhanced') outputExtension = 'pdf'; + const outputFile = path.join(outputFolder, relativePath.replace(/\.(md|markdown)$/i, `.${outputExtension}`)); + + // Create subdirectories in output folder if needed + const outputDir = path.dirname(outputFile); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Handle DOCX Enhanced format with WordTemplateExporter + if (format === 'docx-enhanced') { + try { + const content = fs.readFileSync(inputFile, 'utf-8'); + const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage); + await exporter.convert(content, outputFile); + + completedCount++; + + // Update progress + mainWindow.webContents.send('batch-progress', { + completed: index + 1, + total: totalCount, + currentFile: path.basename(inputFile), + success: true + }); + + // Process next file + processNextFile(index + 1); + } catch (error) { + // Update progress with error + mainWindow.webContents.send('batch-progress', { + completed: index + 1, + total: totalCount, + currentFile: path.basename(inputFile), + success: false + }); + + // Process next file even if this one failed + processNextFile(index + 1); + } + return; + } + + // Handle PDF Enhanced format with Word Template → PDF conversion + if (format === 'pdf-enhanced') { + try { + const content = fs.readFileSync(inputFile, 'utf-8'); + + // Step 1: Create temporary DOCX file using Word template + const tempDocxPath = outputFile.replace(/\.pdf$/, '_temp.docx'); + const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage); + await exporter.convert(content, tempDocxPath); + + // Step 2: Convert DOCX to PDF using LibreOffice (using execFile for safety) + const soffice = process.platform === 'win32' + ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' + : 'soffice'; + + const outputDir = path.dirname(outputFile); + const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath]; + + execFile(soffice, sofficeArgs, (error, stdout, stderr) => { + // Clean up temporary DOCX file + try { + fs.unlinkSync(tempDocxPath); + } catch (e) { + console.error('Failed to delete temp file:', e); + } + + if (error) { + // Update progress with error + mainWindow.webContents.send('batch-progress', { + completed: index + 1, + total: totalCount, + currentFile: path.basename(inputFile), + success: false + }); + processNextFile(index + 1); + return; + } + + // LibreOffice creates file with same base name as input + const generatedPdfPath = tempDocxPath.replace(/\.docx$/, '.pdf'); + + // Rename if needed + if (generatedPdfPath !== outputFile) { + try { + fs.renameSync(generatedPdfPath, outputFile); + } catch (e) { + console.error('Failed to rename PDF:', e); + } + } + + completedCount++; + + // Update progress + mainWindow.webContents.send('batch-progress', { + completed: index + 1, + total: totalCount, + currentFile: path.basename(inputFile), + success: true + }); + + // Process next file + processNextFile(index + 1); + }); + + } catch (error) { + // Update progress with error + mainWindow.webContents.send('batch-progress', { + completed: index + 1, + total: totalCount, + currentFile: path.basename(inputFile), + success: false + }); + + // Process next file even if this one failed + processNextFile(index + 1); + } + return; + } + + // Build pandoc command for other formats + let pandocCmd = `${getPandocPath()} "${inputFile}" -o "${outputFile}"`; + + // Add template if specified + if (options.template && options.template !== 'default') { + pandocCmd += ` --template="${options.template}"`; + } + + // Add metadata + if (options.metadata) { + for (const [key, value] of Object.entries(options.metadata)) { + if (value.trim()) { + pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; + } + } + } + + // Add variables + if (options.variables) { + for (const [key, value] of Object.entries(options.variables)) { + if (value.trim()) { + pandocCmd += ` -V ${key}="${value.replace(/"/g, '\\"')}"`; + } + } + } + + // Add other options + if (options.toc) pandocCmd += ' --toc'; + if (options.tocDepth) pandocCmd += ` --toc-depth=${options.tocDepth}`; + if (options.numberSections) pandocCmd += ' --number-sections'; + if (options.citeproc) pandocCmd += ' --citeproc'; + if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`; + if (options.csl) pandocCmd += ` --csl="${options.csl}"`; + + // Add PDF-specific options with header/footer support + if (format === 'pdf') { + const pdfEngine = options.pdfEngine || 'xelatex'; + pandocCmd += ` --pdf-engine=${pdfEngine}`; + if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; + + // Add monospace font settings for code blocks (ASCII art preservation) + pandocCmd += ' -V monofont="Consolas"'; + pandocCmd += ' --highlight-style=tango'; + + // Add header/footer if enabled + if (headerFooterSettings.enabled) { + const filename = path.basename(inputFile, path.extname(inputFile)); + const metadata = { filename, title: filename, author: '' }; + + const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); + const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); + const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); + const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); + const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); + const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); + + // Create LaTeX header + const latexHeader = ` +\\usepackage{fancyhdr} +\\pagestyle{fancy} +\\fancyhf{} +\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} +\\chead{${headerCenter.replace(/\\/g, '\\\\')}} +\\rhead{${headerRight.replace(/\\/g, '\\\\')}} +\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} +\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} +\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} +\\renewcommand{\\headrulewidth}{0.4pt} +\\renewcommand{\\footrulewidth}{0.4pt} +`; + const headerFile = path.join(require('os').tmpdir(), `header_batch_${Date.now()}.tex`); + fs.writeFileSync(headerFile, latexHeader, 'utf-8'); + pandocCmd += ` --include-in-header="${headerFile}"`; + pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"'; + } + } + + // Add DOCX-specific handling + if (format === 'docx') { + pandocCmd += ' -t docx'; + } + + // Add PowerPoint footer if enabled + if (format === 'pptx' && headerFooterSettings.enabled && headerFooterSettings.footer.center) { + const filename = path.basename(inputFile, path.extname(inputFile)); + const metadata = { filename, title: filename, author: '' }; + const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata); + pandocCmd += ` --variable footer="${footerText}"`; + } + + // Execute conversion (using runPandocCmd for safety) + runPandocCmd(pandocCmd, async (error, stdout, stderr) => { + if (!error) { + // Add headers/footers to DOCX if enabled + if (format === 'docx' && headerFooterSettings.enabled) { + try { + const filename = path.basename(inputFile, path.extname(inputFile)); + const metadata = { filename, title: filename, author: '' }; + await addHeaderFooterToDocx(outputFile, metadata); + } catch (hfError) { + console.error('Batch: Error adding headers/footers to DOCX:', hfError); + } + } + completedCount++; + } + + // Update progress (you could send this to renderer for a progress bar) + mainWindow.webContents.send('batch-progress', { + completed: index + 1, + total: totalCount, + currentFile: path.basename(inputFile), + success: !error + }); + + // Process next file + processNextFile(index + 1); + }); + }; + + // Start processing + processNextFile(0); +} + +// Handle command line interface for file conversion +function handleCLIConversion(args) { + const command = args[0]; + const filePath = args[args.length - 1]; // File path is always last argument + + if (!fs.existsSync(filePath)) { + console.error(`Error: File not found: ${filePath}`); + app.quit(); + return; + } + + // Show conversion dialog for --convert command + if (command === '--convert') { + showConversionDialog(filePath); + return; + } + + // Direct conversion for --convert-to command + if (command === '--convert-to' && args.length >= 3) { + const format = args[1]; + performCLIConversion(filePath, format); + return; + } + + console.error('Usage: --convert OR --convert-to '); + app.quit(); +} + +// Show conversion dialog for CLI +function showConversionDialog(filePath) { + const { dialog } = require('electron'); + + // Create a hidden window for dialog operations + const hiddenWindow = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + + const formats = [ + { name: 'PDF', value: 'pdf' }, + { name: 'HTML', value: 'html' }, + { name: 'DOCX', value: 'docx' }, + { name: 'LaTeX', value: 'latex' }, + { name: 'RTF', value: 'rtf' }, + { name: 'ODT', value: 'odt' }, + { name: 'PowerPoint', value: 'pptx' } + ]; + + // Create format selection dialog using message box + const formatButtons = formats.map(f => f.name); + formatButtons.push('Cancel'); + + dialog.showMessageBox(hiddenWindow, { + type: 'question', + title: 'PanConverter - Choose Format', + message: `Convert "${path.basename(filePath)}" to:`, + detail: 'Select the output format for conversion', + buttons: formatButtons, + defaultId: 0, + cancelId: formatButtons.length - 1 + }).then(result => { + if (result.response < formats.length) { + const selectedFormat = formats[result.response].value; + performCLIConversion(filePath, selectedFormat); + } else { + console.log('Conversion cancelled'); + app.quit(); + } + hiddenWindow.destroy(); + }); +} + +// Perform CLI conversion +function performCLIConversion(inputPath, format) { + try { + const content = fs.readFileSync(inputPath, 'utf-8'); + const outputPath = inputPath.replace(/\.[^/.]+$/, `.${format}`); + + console.log(`Converting "${path.basename(inputPath)}" to ${format.toUpperCase()}...`); + + // Use existing export functions but with CLI output (using runPandocCmd for safety) + const pandocCommand = buildPandocCommand(content, format, outputPath); + + runPandocCmd(pandocCommand, (error, stdout, stderr) => { + if (error) { + console.error(`Conversion failed: ${error.message}`); + if (stderr) console.error(`Details: ${stderr}`); + app.quit(); + return; + } + + console.log(`Successfully converted to: ${outputPath}`); + + // Show Windows notification (using exec for PowerShell is acceptable here - hardcoded command) + if (process.platform === 'win32') { + const iconPath = path.join(__dirname, '../assets/icon.png'); + execFile('powershell', ['-Command', `New-BurntToastNotification -Text 'PanConverter', 'File converted to ${format.toUpperCase()}' -AppLogo '${iconPath}'`], () => {}); + } + + app.quit(); + }); + } catch (error) { + console.error(`Error reading file: ${error.message}`); + app.quit(); + } +} + +// Build Pandoc command for CLI conversion +function buildPandocCommand(content, format, outputPath) { + const inputFile = path.join(require('os').tmpdir(), `panconverter_temp_${Date.now()}.md`); + fs.writeFileSync(inputFile, content, 'utf-8'); + + let command = `pandoc "${inputFile}" -o "${outputPath}"`; + + // Get metadata for dynamic fields + const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; + const metadata = { + filename: filename, + title: filename, + author: '', + }; + + switch (format) { + case 'pdf': + command += ' --pdf-engine=xelatex --variable geometry:margin=1in'; + + // Add page size and orientation + const pageSize = PAGE_SIZES[pageSettings.size]; + if (pageSize) { + command += ` -V geometry:papersize=${pageSize.pandoc}`; + } else if (pageSettings.customWidth && pageSettings.customHeight) { + // Custom page size + command += ` -V geometry:paperwidth=${pageSettings.customWidth}`; + command += ` -V geometry:paperheight=${pageSettings.customHeight}`; + } + + // Add orientation + if (pageSettings.orientation === 'landscape') { + command += ' -V geometry:landscape'; + } + + // Add monospace font settings for code blocks (ASCII art preservation) + command += ' -V monofont="Consolas"'; + command += ' --highlight-style=tango'; + + // Add header/footer if enabled + if (headerFooterSettings.enabled) { + // Process dynamic fields + const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); + const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); + const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); + const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); + const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); + const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); + + // Add Pandoc variables for fancyhdr package + if (headerLeft) command += ` --variable header-left="${headerLeft}"`; + if (headerCenter) command += ` --variable header-center="${headerCenter}"`; + if (headerRight) command += ` --variable header-right="${headerRight}"`; + if (footerLeft) command += ` --variable footer-left="${footerLeft}"`; + if (footerCenter) command += ` --variable footer-center="${footerCenter}"`; + if (footerRight) command += ` --variable footer-right="${footerRight}"`; + + // Create custom LaTeX header with fancyhdr + const latexHeader = ` +\\usepackage{fancyhdr} +\\pagestyle{fancy} +\\fancyhf{} +\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} +\\chead{${headerCenter.replace(/\\/g, '\\\\')}} +\\rhead{${headerRight.replace(/\\/g, '\\\\')}} +\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} +\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} +\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} +\\renewcommand{\\headrulewidth}{0.4pt} +\\renewcommand{\\footrulewidth}{0.4pt} +`; + const headerFile = path.join(require('os').tmpdir(), `header_${Date.now()}.tex`); + fs.writeFileSync(headerFile, latexHeader, 'utf-8'); + command += ` --include-in-header="${headerFile}"`; + + // Add lastpage package for $TOTAL$ support + command += ' --variable header-includes="\\\\usepackage{lastpage}"'; + } + break; + + case 'html': + command += ' --self-contained --css'; + break; + + case 'docx': + command += ' --reference-doc'; + + // For DOCX, header/footer are handled via reference document or separate processing + // We'll add a note that DOCX headers/footers require reference doc or post-processing + break; + + case 'odt': + // ODT headers/footers are handled via reference document + break; + + case 'latex': + command += ' --standalone'; + break; + + case 'pptx': + command += ' --slide-level=2'; + // PowerPoint footer can be added with --variable + if (headerFooterSettings.enabled && headerFooterSettings.footer.center) { + const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata); + command += ` --variable footer="${footerText}"`; + } + break; + } + + return command; +} + +app.whenReady().then(() => { + // Load saved Word template path and settings + wordTemplatePath = store.get('wordTemplatePath', null); + templateStartPage = store.get('templateStartPage', 3); + + // Load header/footer settings + const savedHFSettings = store.get('headerFooterSettings', null); + if (savedHFSettings) { + headerFooterSettings = savedHFSettings; + } + + // Load page size settings + const savedPageSettings = store.get('pageSettings', null); + if (savedPageSettings) { + pageSettings = savedPageSettings; + } + + // Check for command line conversion requests + const args = process.argv.slice(2); + if (args.length >= 2 && (args[0] === '--convert' || args[0] === '--convert-to')) { + handleCLIConversion(args); + return; // Don't create window for CLI operations + } + + createWindow(); + + // Handle file association on app startup + // In packaged apps, process.argv structure is different: + // Development: ['electron', 'app.js', 'file.md'] - need slice(2) + // Packaged: ['PanConverter.exe', 'file.md'] - need slice(1) + // We'll check all arguments after the executable + console.log('[MAIN] Full command line args:', JSON.stringify(process.argv)); + console.log('[MAIN] App is packaged:', app.isPackaged); + + // Start from index 1 (skip executable) and check each argument + const startIndex = app.isPackaged ? 1 : 2; + const fileArgs = process.argv.slice(startIndex); + console.log('[MAIN] File args to process (starting from index', startIndex + '):', fileArgs); + + for (const arg of fileArgs) { + console.log('[MAIN] Checking arg:', arg); + if ((arg.endsWith('.md') || arg.endsWith('.markdown'))) { + // Try to resolve the path (might be relative) + const resolvedPath = path.isAbsolute(arg) ? arg : path.resolve(process.cwd(), arg); + console.log('[MAIN] Resolved path:', resolvedPath); + if (fs.existsSync(resolvedPath)) { + // Store the file to open after window is ready + console.log('[MAIN] Setting pendingFile to:', resolvedPath); + app.pendingFile = resolvedPath; + break; + } else { + console.log('[MAIN] File does not exist:', resolvedPath); + } + } + } + console.log('[MAIN] Final app.pendingFile:', app.pendingFile); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } +}); + +// IPC handlers for recent files +ipcMain.on('save-recent-files', (event, recentFiles) => { + try { + const userDataPath = app.getPath('userData'); + const recentFilesPath = path.join(userDataPath, 'recent-files.json'); + fs.writeFileSync(recentFilesPath, JSON.stringify(recentFiles, null, 2)); + } catch (error) { + console.error('Error saving recent files:', error); + } +}); + +ipcMain.on('clear-recent-files', (event) => { + try { + const userDataPath = app.getPath('userData'); + const recentFilesPath = path.join(userDataPath, 'recent-files.json'); + fs.writeFileSync(recentFilesPath, JSON.stringify([], null, 2)); + // Rebuild menu to reflect changes + createMenu(); + event.reply('recent-files-cleared'); + } catch (error) { + console.error('Error clearing recent files:', error); + } +}); + +// Handle file opening on macOS +app.on('open-file', (event, filePath) => { + event.preventDefault(); + if (mainWindow && rendererReady) { + openFileFromPath(filePath); + } else { + // Store the file path to open after window and renderer are ready + app.pendingFile = filePath; + } +}); + +// Handle file opening from command line or file association +function openFileFromPath(filePath) { + console.log('[MAIN] openFileFromPath called with:', filePath); + console.log('[MAIN] rendererReady:', rendererReady, 'mainWindow exists:', !!mainWindow); + if (fs.existsSync(filePath)) { + currentFile = filePath; + const content = fs.readFileSync(filePath, 'utf-8'); + console.log('[MAIN] File read successfully, content length:', content.length); + if (mainWindow && mainWindow.webContents && rendererReady) { + // Send file immediately - renderer-ready means UI is initialized + console.log('[MAIN] Sending file-opened to renderer'); + mainWindow.webContents.send('file-opened', { path: filePath, content }); + } else { + // Store file to open after renderer is ready + console.log('[MAIN] Storing as pending file'); + app.pendingFile = filePath; + } + } else { + console.error('[MAIN] File does not exist:', filePath); + } +} + +// ======================================== +// PDF EDITOR OPERATIONS (using pdf-lib) +// ======================================== + +// Helper function to parse page ranges (e.g., "1-5, 7, 9-12") +function parsePageRanges(rangeString, totalPages) { + const pages = []; + const ranges = rangeString.split(',').map(r => r.trim()); + + for (const range of ranges) { + if (range.includes('-')) { + const [start, end] = range.split('-').map(n => parseInt(n.trim())); + for (let i = start; i <= end && i <= totalPages; i++) { + if (i > 0 && !pages.includes(i - 1)) { // Convert to 0-indexed + pages.push(i - 1); + } + } + } else { + const page = parseInt(range); + if (page > 0 && page <= totalPages && !pages.includes(page - 1)) { + pages.push(page - 1); + } + } + } + + return pages.sort((a, b) => a - b); +} + +// Helper function to convert hex color to RGB +function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16) / 255, + g: parseInt(result[2], 16) / 255, + b: parseInt(result[3], 16) / 255 + } : { r: 0, g: 0, b: 0 }; +} + +// PDF Merge Operation +async function pdfMerge(data) { + try { + const mergedPdf = await PDFDocument.create(); + + for (const filePath of data.inputFiles) { + const pdfBytes = fs.readFileSync(filePath); + const pdf = await PDFDocument.load(pdfBytes); + const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices()); + copiedPages.forEach(page => mergedPdf.addPage(page)); + } + + const pdfBytes = await mergedPdf.save(); + fs.writeFileSync(data.outputPath, pdfBytes); + + return { success: true, message: `Successfully merged ${data.inputFiles.length} PDFs` }; + } catch (error) { + return { success: false, error: error.message }; + } +} + +// PDF Split Operation +async function pdfSplit(data) { + try { + const pdfBytes = fs.readFileSync(data.inputPath); + const pdf = await PDFDocument.load(pdfBytes); + const totalPages = pdf.getPageCount(); + + let splits = []; + + if (data.splitMode === 'pages') { + // Split by page ranges + const ranges = data.pageRanges.split(',').map(r => r.trim()); + for (let i = 0; i < ranges.length; i++) { + const range = ranges[i]; + let pages = []; + + if (range.includes('-')) { + const [start, end] = range.split('-').map(n => parseInt(n.trim())); + for (let p = start; p <= end && p <= totalPages; p++) { + pages.push(p - 1); + } + } else { + const page = parseInt(range); + if (page > 0 && page <= totalPages) { + pages.push(page - 1); + } + } + + if (pages.length > 0) { + splits.push({ pages, name: `part_${i + 1}` }); + } + } + } else if (data.splitMode === 'interval') { + // Split every N pages + const interval = data.interval; + for (let i = 0; i < totalPages; i += interval) { + const pages = []; + for (let j = i; j < i + interval && j < totalPages; j++) { + pages.push(j); + } + splits.push({ pages, name: `part_${Math.floor(i / interval) + 1}` }); + } + } else if (data.splitMode === 'size') { + // Split by size (approximate - we'll split evenly) + // This is complex, so we'll do a simple even split for now + const maxSize = data.maxSize * 1024 * 1024; // Convert MB to bytes + // For simplicity, split into fixed page chunks + const chunkSize = Math.max(1, Math.floor(totalPages / 5)); // Split into ~5 parts + for (let i = 0; i < totalPages; i += chunkSize) { + const pages = []; + for (let j = i; j < i + chunkSize && j < totalPages; j++) { + pages.push(j); + } + splits.push({ pages, name: `part_${Math.floor(i / chunkSize) + 1}` }); + } + } + + // Create split PDFs + const baseName = path.basename(data.inputPath, '.pdf'); + for (const split of splits) { + const newPdf = await PDFDocument.create(); + const copiedPages = await newPdf.copyPages(pdf, split.pages); + copiedPages.forEach(page => newPdf.addPage(page)); + + const outputPath = path.join(data.outputFolder, `${baseName}_${split.name}.pdf`); + const newPdfBytes = await newPdf.save(); + fs.writeFileSync(outputPath, newPdfBytes); + } + + return { success: true, message: `Successfully split PDF into ${splits.length} files` }; + } catch (error) { + return { success: false, error: error.message }; + } +} + +// PDF Compress Operation +async function pdfCompress(data) { + try { + const pdfBytes = fs.readFileSync(data.inputPath); + const pdf = await PDFDocument.load(pdfBytes); + + // pdf-lib doesn't have built-in compression, but we can save with default compression + // For actual compression, we would need additional libraries like Ghostscript + // For now, we'll save with standard options which provides some compression + const compressedPdfBytes = await pdf.save({ + useObjectStreams: true, + addDefaultPage: false, + objectsPerTick: 50 + }); + + fs.writeFileSync(data.outputPath, compressedPdfBytes); + + const originalSize = fs.statSync(data.inputPath).size; + const compressedSize = fs.statSync(data.outputPath).size; + const savings = ((originalSize - compressedSize) / originalSize * 100).toFixed(1); + + return { + success: true, + message: `PDF compressed. Size reduced by ${savings}% (${(originalSize / 1024).toFixed(1)}KB → ${(compressedSize / 1024).toFixed(1)}KB)` + }; + } catch (error) { + return { success: false, error: error.message }; + } +} + +// PDF Rotate Pages Operation +async function pdfRotate(data) { + try { + const pdfBytes = fs.readFileSync(data.inputPath); + const pdf = await PDFDocument.load(pdfBytes); + const totalPages = pdf.getPageCount(); + + let pagesToRotate = []; + if (data.pages && data.pages.trim()) { + pagesToRotate = parsePageRanges(data.pages, totalPages); + } else { + // Rotate all pages + pagesToRotate = Array.from({ length: totalPages }, (_, i) => i); + } + + pagesToRotate.forEach(pageIndex => { + const page = pdf.getPage(pageIndex); + page.setRotation(degrees(data.angle)); + }); + + const rotatedPdfBytes = await pdf.save(); + fs.writeFileSync(data.outputPath, rotatedPdfBytes); + + return { + success: true, + message: `Successfully rotated ${pagesToRotate.length} page(s) by ${data.angle}°` + }; + } catch (error) { + return { success: false, error: error.message }; + } +} + +// PDF Delete Pages Operation +async function pdfDeletePages(data) { + try { + const pdfBytes = fs.readFileSync(data.inputPath); + const pdf = await PDFDocument.load(pdfBytes); + const totalPages = pdf.getPageCount(); + + const pagesToDelete = parsePageRanges(data.pages, totalPages); + + // Remove pages in reverse order to maintain indices + pagesToDelete.sort((a, b) => b - a).forEach(pageIndex => { + pdf.removePage(pageIndex); + }); + + const newPdfBytes = await pdf.save(); + fs.writeFileSync(data.outputPath, newPdfBytes); + + return { + success: true, + message: `Successfully deleted ${pagesToDelete.length} page(s). New PDF has ${totalPages - pagesToDelete.length} pages` + }; + } catch (error) { + return { success: false, error: error.message }; + } +} + +// PDF Reorder Pages Operation +async function pdfReorder(data) { + try { + const pdfBytes = fs.readFileSync(data.inputPath); + const pdf = await PDFDocument.load(pdfBytes); + const totalPages = pdf.getPageCount(); + + // Parse new page order + const newOrder = data.newOrder.split(',').map(n => parseInt(n.trim()) - 1); // Convert to 0-indexed + + // Validate new order + if (newOrder.length !== totalPages) { + return { success: false, error: `New order must include all ${totalPages} pages` }; + } + + const newPdf = await PDFDocument.create(); + const copiedPages = await newPdf.copyPages(pdf, newOrder); + copiedPages.forEach(page => newPdf.addPage(page)); + + const reorderedPdfBytes = await newPdf.save(); + fs.writeFileSync(data.outputPath, reorderedPdfBytes); + + return { success: true, message: 'Successfully reordered PDF pages' }; + } catch (error) { + return { success: false, error: error.message }; + } +} + +// PDF Watermark Operation +async function pdfWatermark(data) { + try { + const pdfBytes = fs.readFileSync(data.inputPath); + const pdf = await PDFDocument.load(pdfBytes); + const totalPages = pdf.getPageCount(); + + // Determine which pages to watermark + let pagesToWatermark = []; + if (data.pages === 'all') { + pagesToWatermark = Array.from({ length: totalPages }, (_, i) => i); + } else if (data.pages === 'custom' && data.customPages) { + pagesToWatermark = parsePageRanges(data.customPages, totalPages); + } + + const font = await pdf.embedFont(StandardFonts.Helvetica); + const color = hexToRgb(data.color); + + for (const pageIndex of pagesToWatermark) { + const page = pdf.getPage(pageIndex); + const { width, height } = page.getSize(); + + let x, y, rotation = 0; + + // Calculate position based on selected position + switch (data.position) { + case 'center': + x = width / 2; + y = height / 2; + break; + case 'diagonal': + x = width / 2; + y = height / 2; + rotation = 45; + break; + case 'top-left': + x = 50; + y = height - 50; + break; + case 'top-center': + x = width / 2; + y = height - 50; + break; + case 'top-right': + x = width - 50; + y = height - 50; + break; + case 'bottom-left': + x = 50; + y = 50; + break; + case 'bottom-center': + x = width / 2; + y = 50; + break; + case 'bottom-right': + x = width - 50; + y = 50; + break; + default: + x = width / 2; + y = height / 2; + } + + page.drawText(data.text, { + x, + y, + size: data.fontSize, + font, + color: rgb(color.r, color.g, color.b), + opacity: data.opacity, + rotate: degrees(rotation) + }); + } + + const watermarkedPdfBytes = await pdf.save(); + fs.writeFileSync(data.outputPath, watermarkedPdfBytes); + + return { + success: true, + message: `Successfully added watermark to ${pagesToWatermark.length} page(s)` + }; + } catch (error) { + return { success: false, error: error.message }; + } +} + +// PDF Encrypt (Password Protection) Operation +async function pdfEncrypt(data) { + try { + const pdfBytes = fs.readFileSync(data.inputPath); + const pdf = await PDFDocument.load(pdfBytes); + + // pdf-lib has limited encryption support in v1.17.1 + // We'll save with password (basic encryption) + const encryptedPdfBytes = await pdf.save({ + userPassword: data.userPassword, + ownerPassword: data.ownerPassword || data.userPassword, + permissions: { + printing: data.permissions.printing ? 'highResolution' : 'lowResolution', + modifying: data.permissions.modifying, + copying: data.permissions.copying, + annotating: data.permissions.annotating, + fillingForms: data.permissions.fillingForms, + contentAccessibility: data.permissions.contentAccessibility, + documentAssembly: data.permissions.documentAssembly + } + }); + + fs.writeFileSync(data.outputPath, encryptedPdfBytes); + + return { success: true, message: 'Successfully added password protection to PDF' }; + } catch (error) { + // If pdf-lib doesn't support encryption in this version, provide a helpful error + if (error.message.includes('encrypt') || error.message.includes('password')) { + return { + success: false, + error: 'PDF encryption requires pdf-lib with encryption support. This feature may not be available in the current version.' + }; + } + return { success: false, error: error.message }; + } +} + +// PDF Decrypt (Remove Password) Operation +async function pdfDecrypt(data) { + try { + const pdfBytes = fs.readFileSync(data.inputPath); + const pdf = await PDFDocument.load(pdfBytes, { password: data.password }); + + // Save without password + const decryptedPdfBytes = await pdf.save(); + fs.writeFileSync(data.outputPath, decryptedPdfBytes); + + return { success: true, message: 'Successfully removed password protection from PDF' }; + } catch (error) { + if (error.message.includes('password') || error.message.includes('encrypted')) { + return { success: false, error: 'Incorrect password or PDF is not encrypted' }; + } + return { success: false, error: error.message }; + } +} + +// PDF Permissions Operation +async function pdfSetPermissions(data) { + try { + const pdfBytes = fs.readFileSync(data.inputPath); + const loadOptions = data.currentPassword ? { password: data.currentPassword } : {}; + const pdf = await PDFDocument.load(pdfBytes, loadOptions); + + const newPdfBytes = await pdf.save({ + ownerPassword: data.ownerPassword, + permissions: { + printing: data.permissions.printing ? 'highResolution' : 'lowResolution', + modifying: data.permissions.modifying, + copying: data.permissions.copying, + annotating: data.permissions.annotating, + fillingForms: data.permissions.fillingForms, + contentAccessibility: data.permissions.contentAccessibility, + documentAssembly: data.permissions.documentAssembly + } + }); + + fs.writeFileSync(data.outputPath, newPdfBytes); + + return { success: true, message: 'Successfully updated PDF permissions' }; + } catch (error) { + if (error.message.includes('encrypt') || error.message.includes('permission')) { + return { + success: false, + error: 'PDF permissions require pdf-lib with encryption support. This feature may not be available in the current version.' + }; + } + return { success: false, error: error.message }; + } +} + +// IPC Handler for PDF Operations +ipcMain.on('process-pdf-operation', async (event, data) => { + try { + mainWindow.webContents.send('pdf-operation-progress', { + message: `Processing ${data.operation}...`, + progress: 10 + }); + + let result; + + switch (data.operation) { + case 'merge': + result = await pdfMerge(data); + break; + case 'split': + result = await pdfSplit(data); + break; + case 'compress': + result = await pdfCompress(data); + break; + case 'rotate': + result = await pdfRotate(data); + break; + case 'delete': + result = await pdfDeletePages(data); + break; + case 'reorder': + result = await pdfReorder(data); + break; + case 'watermark': + result = await pdfWatermark(data); + break; + case 'encrypt': + result = await pdfEncrypt(data); + break; + case 'decrypt': + result = await pdfDecrypt(data); + break; + case 'permissions': + result = await pdfSetPermissions(data); + break; + default: + result = { success: false, error: `Unknown operation: ${data.operation}` }; + } + + mainWindow.webContents.send('pdf-operation-complete', result); + } catch (error) { + mainWindow.webContents.send('pdf-operation-complete', { + success: false, + error: error.message + }); + } +}); + +// IPC Handler for getting PDF page count +ipcMain.on('get-pdf-page-count', async (event, filePath) => { + try { + const pdfBytes = fs.readFileSync(filePath); + const pdf = await PDFDocument.load(pdfBytes); + const count = pdf.getPageCount(); + event.reply('pdf-page-count', { count }); + } catch (error) { + event.reply('pdf-page-count', { error: error.message }); + } +}); + +// IPC Handler for folder selection (for PDF operations) +ipcMain.on('select-pdf-folder', (event, inputId) => { + const folder = dialog.showOpenDialogSync(mainWindow, { + properties: ['openDirectory'] + }); + + if (folder && folder[0]) { + event.reply('pdf-folder-selected', { inputId, path: folder[0] }); + } +}); + +// ============================================ +// ASCII Art Generator Window +// ============================================ +let asciiGeneratorWindow = null; + +function openAsciiGenerator() { + if (asciiGeneratorWindow) { + asciiGeneratorWindow.focus(); + return; + } + + asciiGeneratorWindow = new BrowserWindow({ + width: 800, + height: 700, + parent: mainWindow, + modal: false, + title: 'ASCII Art Generator', + icon: path.join(__dirname, '../assets/icon.png'), + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, 'preload.js') + } + }); + + asciiGeneratorWindow.loadFile(path.join(__dirname, 'ascii-generator.html')); + asciiGeneratorWindow.setMenuBarVisibility(false); + + asciiGeneratorWindow.on('closed', () => { + asciiGeneratorWindow = null; + }); +} + +ipcMain.on('open-ascii-generator', () => { + openAsciiGenerator(); +}); + +// ============================================ +// Table Generator Window +// ============================================ +let tableGeneratorWindow = null; + +function openTableGenerator() { + if (tableGeneratorWindow) { + tableGeneratorWindow.focus(); + return; + } + + tableGeneratorWindow = new BrowserWindow({ + width: 900, + height: 700, + parent: mainWindow, + modal: false, + title: 'Table Generator', + icon: path.join(__dirname, '../assets/icon.png'), + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, 'preload.js') + } + }); + + tableGeneratorWindow.loadFile(path.join(__dirname, 'table-generator.html')); + tableGeneratorWindow.setMenuBarVisibility(false); + + tableGeneratorWindow.on('closed', () => { + tableGeneratorWindow = null; + }); +} + +ipcMain.on('open-table-generator', () => { + openTableGenerator(); +}); + +// IPC Handler to receive generated content from generator windows +ipcMain.on('insert-generated-content', (event, content) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('insert-content', content); + } }); \ No newline at end of file diff --git a/src/preload.js b/src/preload.js index bd04aa4..60d68b0 100644 --- a/src/preload.js +++ b/src/preload.js @@ -1,376 +1,376 @@ -/** - * Preload Script for PanConverter - * - * This script creates a secure bridge between the main process and renderer process. - * It exposes only specific IPC channels, preventing direct Node.js access in the renderer. - * - * Security Benefits: - * - No direct access to Node.js APIs (fs, path, child_process, etc.) - * - All IPC channels are explicitly whitelisted - * - Prevents XSS from escalating to full system access - * - * @version 2.2.0 - */ - -const { contextBridge, ipcRenderer } = require('electron'); - -// Define allowed IPC channels for security -const ALLOWED_SEND_CHANNELS = [ - // File operations - 'save-file', - 'save-current-file', - 'set-current-file', - 'save-recent-files', - 'clear-recent-files', - 'renderer-ready', - - // Theme - 'get-theme', - - // Print - 'do-print', - - // Export - 'export-with-options', - 'export-spreadsheet', - - // Batch conversion - 'batch-convert', - 'select-folder', - - // Universal converter - 'universal-convert', - 'universal-convert-batch', - - // Image converter - 'image-convert', - 'image-batch-convert', - 'image-resize', - 'image-compress', - 'image-rotate', - - // Audio converter - 'audio-convert', - 'audio-batch-convert', - 'audio-extract', - 'audio-trim', - 'audio-merge', - - // Video converter - 'video-convert', - 'video-batch-convert', - 'video-compress', - 'video-trim', - 'video-frames', - 'video-gif', - - // Header/Footer - 'get-header-footer-settings', - 'save-header-footer-settings', - 'browse-header-footer-logo', - 'save-header-footer-logo', - 'clear-header-footer-logo', - - // Page settings - 'get-page-settings', - 'update-page-settings', - - // Template settings - 'set-custom-start-page', - - // PDF operations - 'process-pdf-operation', - 'get-pdf-page-count', - 'select-pdf-folder', - - // ASCII generator (separate window) - 'open-ascii-generator', - - // Table generator (separate window) - 'open-table-generator', - - // Insert generated content - 'insert-generated-content' -]; - -const ALLOWED_RECEIVE_CHANNELS = [ - // File operations - 'file-new', - 'file-opened', - 'file-save', - 'get-content-for-save', - 'get-content-for-spreadsheet', - 'recent-files-cleared', - - // UI toggles - 'toggle-preview', - 'toggle-find', - - // Theme - 'theme-changed', - 'theme-data', - - // Edit operations - 'undo', - 'redo', - - // Font - 'adjust-font-size', - - // Print - 'print-preview', - 'print-preview-styled', - - // Export dialogs - 'show-export-dialog', - 'show-batch-dialog', - 'show-universal-converter-dialog', - 'show-table-generator', - 'show-pdf-editor-dialog', - - // Converter dialogs - 'show-image-converter', - 'show-audio-converter', - 'show-video-converter', - - // PDF viewer - 'open-pdf-viewer', - - // Conversion status - 'conversion-status', - 'conversion-complete', - 'batch-progress', - 'image-conversion-complete', - 'audio-conversion-complete', - 'video-conversion-complete', - - // Folder selection - 'folder-selected', - 'pdf-folder-selected', - - // Header/Footer - 'header-footer-settings-data', - 'header-footer-logo-selected', - 'header-footer-logo-saved', - - // Page settings - 'page-settings-data', - - // PDF operations - 'pdf-page-count', - 'pdf-operation-complete', - 'pdf-operation-error', - - // ASCII Art Generator - 'show-ascii-generator-window', - 'show-ascii-generator', - - // Table Generator - 'show-table-generator-window', - - // Header/Footer dialog - 'open-header-footer-dialog', - 'header-footer-logo-cleared', - - // PDF operation progress - 'pdf-operation-progress', - - // Insert content from generator windows - 'insert-content' -]; - -/** - * Secure API exposed to renderer process - * Access via window.electronAPI in renderer - */ -contextBridge.exposeInMainWorld('electronAPI', { - // ============================================ - // SEND METHODS (Renderer -> Main) - // ============================================ - - /** - * Send a message to the main process - * @param {string} channel - IPC channel name - * @param {any} data - Data to send - */ - send: (channel, data) => { - if (ALLOWED_SEND_CHANNELS.includes(channel)) { - ipcRenderer.send(channel, data); - } else { - console.warn(`[Preload] Blocked send to unauthorized channel: ${channel}`); - } - }, - - /** - * Invoke a main process handler and get a response - * @param {string} channel - IPC channel name - * @param {any} data - Data to send - * @returns {Promise} Response from main process - */ - invoke: async (channel, data) => { - if (ALLOWED_SEND_CHANNELS.includes(channel)) { - return await ipcRenderer.invoke(channel, data); - } else { - console.warn(`[Preload] Blocked invoke to unauthorized channel: ${channel}`); - return null; - } - }, - - // ============================================ - // RECEIVE METHODS (Main -> Renderer) - // ============================================ - - /** - * Register a listener for messages from main process - * @param {string} channel - IPC channel name - * @param {Function} callback - Function to call with received data - * @returns {Function} Cleanup function to remove listener - */ - on: (channel, callback) => { - if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) { - const subscription = (event, ...args) => callback(...args); - ipcRenderer.on(channel, subscription); - - // Return cleanup function - return () => { - ipcRenderer.removeListener(channel, subscription); - }; - } else { - console.warn(`[Preload] Blocked listener for unauthorized channel: ${channel}`); - return () => {}; // No-op cleanup - } - }, - - /** - * Register a one-time listener for messages from main process - * @param {string} channel - IPC channel name - * @param {Function} callback - Function to call with received data - */ - once: (channel, callback) => { - if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) { - ipcRenderer.once(channel, (event, ...args) => callback(...args)); - } else { - console.warn(`[Preload] Blocked once listener for unauthorized channel: ${channel}`); - } - }, - - /** - * Remove all listeners for a channel - * @param {string} channel - IPC channel name - */ - removeAllListeners: (channel) => { - if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) { - ipcRenderer.removeAllListeners(channel); - } - }, - - // ============================================ - // CONVENIENCE METHODS - // ============================================ - - // File Operations - file: { - save: (filePath, content) => ipcRenderer.send('save-file', { path: filePath, content }), - saveCurrent: (content) => ipcRenderer.send('save-current-file', content), - setCurrent: (filePath) => ipcRenderer.send('set-current-file', filePath), - saveRecent: (recentFiles) => ipcRenderer.send('save-recent-files', recentFiles), - clearRecent: () => ipcRenderer.send('clear-recent-files'), - rendererReady: () => ipcRenderer.send('renderer-ready') - }, - - // Theme Operations - theme: { - get: () => ipcRenderer.send('get-theme') - }, - - // Print Operations - print: { - doPrint: (options) => ipcRenderer.send('do-print', options) - }, - - // Export Operations - export: { - withOptions: (format, options) => ipcRenderer.send('export-with-options', { format, options }), - spreadsheet: (content, format) => ipcRenderer.send('export-spreadsheet', { content, format }) - }, - - // Batch Conversion - batch: { - convert: (inputFolder, outputFolder, format, options) => { - ipcRenderer.send('batch-convert', { inputFolder, outputFolder, format, options }); - }, - selectFolder: (type) => ipcRenderer.send('select-folder', type) - }, - - // Universal Converter - converter: { - convert: (tool, fromFormat, toFormat, filePath) => { - ipcRenderer.send('universal-convert', { tool, fromFormat, toFormat, filePath }); - }, - convertBatch: (tool, fromFormat, toFormat, inputFolder, outputFolder) => { - ipcRenderer.send('universal-convert-batch', { tool, fromFormat, toFormat, inputFolder, outputFolder }); - } - }, - - // Header/Footer Operations - headerFooter: { - getSettings: () => ipcRenderer.send('get-header-footer-settings'), - saveSettings: (settings) => ipcRenderer.send('save-header-footer-settings', settings), - browseLogo: (position) => ipcRenderer.send('browse-header-footer-logo', position), - saveLogo: (position, filePath) => ipcRenderer.send('save-header-footer-logo', { position, filePath }), - clearLogo: (position) => ipcRenderer.send('clear-header-footer-logo', position) - }, - - // Page Settings - page: { - getSettings: () => ipcRenderer.send('get-page-settings'), - updateSettings: (settings) => ipcRenderer.send('update-page-settings', settings), - setCustomStartPage: (pageNumber) => ipcRenderer.send('set-custom-start-page', pageNumber) - }, - - // PDF Operations - pdf: { - processOperation: (data) => ipcRenderer.send('process-pdf-operation', data), - getPageCount: (filePath) => ipcRenderer.send('get-pdf-page-count', filePath), - selectFolder: (inputId) => ipcRenderer.send('select-pdf-folder', inputId) - }, - - // Image Converter Operations - image: { - convert: (data) => ipcRenderer.send('image-convert', data), - batchConvert: (data) => ipcRenderer.send('image-batch-convert', data), - resize: (data) => ipcRenderer.send('image-resize', data), - compress: (data) => ipcRenderer.send('image-compress', data), - rotate: (data) => ipcRenderer.send('image-rotate', data) - }, - - // Audio Converter Operations - audio: { - convert: (data) => ipcRenderer.send('audio-convert', data), - batchConvert: (data) => ipcRenderer.send('audio-batch-convert', data), - extract: (data) => ipcRenderer.send('audio-extract', data), - trim: (data) => ipcRenderer.send('audio-trim', data), - merge: (data) => ipcRenderer.send('audio-merge', data) - }, - - // Video Converter Operations - video: { - convert: (data) => ipcRenderer.send('video-convert', data), - batchConvert: (data) => ipcRenderer.send('video-batch-convert', data), - compress: (data) => ipcRenderer.send('video-compress', data), - trim: (data) => ipcRenderer.send('video-trim', data), - extractFrames: (data) => ipcRenderer.send('video-frames', data), - toGif: (data) => ipcRenderer.send('video-gif', data) - }, - - // Generator Windows - generators: { - openAscii: () => ipcRenderer.send('open-ascii-generator'), - openTable: () => ipcRenderer.send('open-table-generator') - } -}); - -// Log successful preload initialization -console.log('[Preload] Secure IPC bridge initialized'); -console.log('[Preload] Allowed send channels:', ALLOWED_SEND_CHANNELS.length); -console.log('[Preload] Allowed receive channels:', ALLOWED_RECEIVE_CHANNELS.length); +/** + * Preload Script for PanConverter + * + * This script creates a secure bridge between the main process and renderer process. + * It exposes only specific IPC channels, preventing direct Node.js access in the renderer. + * + * Security Benefits: + * - No direct access to Node.js APIs (fs, path, child_process, etc.) + * - All IPC channels are explicitly whitelisted + * - Prevents XSS from escalating to full system access + * + * @version 2.2.0 + */ + +const { contextBridge, ipcRenderer } = require('electron'); + +// Define allowed IPC channels for security +const ALLOWED_SEND_CHANNELS = [ + // File operations + 'save-file', + 'save-current-file', + 'set-current-file', + 'save-recent-files', + 'clear-recent-files', + 'renderer-ready', + + // Theme + 'get-theme', + + // Print + 'do-print', + + // Export + 'export-with-options', + 'export-spreadsheet', + + // Batch conversion + 'batch-convert', + 'select-folder', + + // Universal converter + 'universal-convert', + 'universal-convert-batch', + + // Image converter + 'image-convert', + 'image-batch-convert', + 'image-resize', + 'image-compress', + 'image-rotate', + + // Audio converter + 'audio-convert', + 'audio-batch-convert', + 'audio-extract', + 'audio-trim', + 'audio-merge', + + // Video converter + 'video-convert', + 'video-batch-convert', + 'video-compress', + 'video-trim', + 'video-frames', + 'video-gif', + + // Header/Footer + 'get-header-footer-settings', + 'save-header-footer-settings', + 'browse-header-footer-logo', + 'save-header-footer-logo', + 'clear-header-footer-logo', + + // Page settings + 'get-page-settings', + 'update-page-settings', + + // Template settings + 'set-custom-start-page', + + // PDF operations + 'process-pdf-operation', + 'get-pdf-page-count', + 'select-pdf-folder', + + // ASCII generator (separate window) + 'open-ascii-generator', + + // Table generator (separate window) + 'open-table-generator', + + // Insert generated content + 'insert-generated-content' +]; + +const ALLOWED_RECEIVE_CHANNELS = [ + // File operations + 'file-new', + 'file-opened', + 'file-save', + 'get-content-for-save', + 'get-content-for-spreadsheet', + 'recent-files-cleared', + + // UI toggles + 'toggle-preview', + 'toggle-find', + + // Theme + 'theme-changed', + 'theme-data', + + // Edit operations + 'undo', + 'redo', + + // Font + 'adjust-font-size', + + // Print + 'print-preview', + 'print-preview-styled', + + // Export dialogs + 'show-export-dialog', + 'show-batch-dialog', + 'show-universal-converter-dialog', + 'show-table-generator', + 'show-pdf-editor-dialog', + + // Converter dialogs + 'show-image-converter', + 'show-audio-converter', + 'show-video-converter', + + // PDF viewer + 'open-pdf-viewer', + + // Conversion status + 'conversion-status', + 'conversion-complete', + 'batch-progress', + 'image-conversion-complete', + 'audio-conversion-complete', + 'video-conversion-complete', + + // Folder selection + 'folder-selected', + 'pdf-folder-selected', + + // Header/Footer + 'header-footer-settings-data', + 'header-footer-logo-selected', + 'header-footer-logo-saved', + + // Page settings + 'page-settings-data', + + // PDF operations + 'pdf-page-count', + 'pdf-operation-complete', + 'pdf-operation-error', + + // ASCII Art Generator + 'show-ascii-generator-window', + 'show-ascii-generator', + + // Table Generator + 'show-table-generator-window', + + // Header/Footer dialog + 'open-header-footer-dialog', + 'header-footer-logo-cleared', + + // PDF operation progress + 'pdf-operation-progress', + + // Insert content from generator windows + 'insert-content' +]; + +/** + * Secure API exposed to renderer process + * Access via window.electronAPI in renderer + */ +contextBridge.exposeInMainWorld('electronAPI', { + // ============================================ + // SEND METHODS (Renderer -> Main) + // ============================================ + + /** + * Send a message to the main process + * @param {string} channel - IPC channel name + * @param {any} data - Data to send + */ + send: (channel, data) => { + if (ALLOWED_SEND_CHANNELS.includes(channel)) { + ipcRenderer.send(channel, data); + } else { + console.warn(`[Preload] Blocked send to unauthorized channel: ${channel}`); + } + }, + + /** + * Invoke a main process handler and get a response + * @param {string} channel - IPC channel name + * @param {any} data - Data to send + * @returns {Promise} Response from main process + */ + invoke: async (channel, data) => { + if (ALLOWED_SEND_CHANNELS.includes(channel)) { + return await ipcRenderer.invoke(channel, data); + } else { + console.warn(`[Preload] Blocked invoke to unauthorized channel: ${channel}`); + return null; + } + }, + + // ============================================ + // RECEIVE METHODS (Main -> Renderer) + // ============================================ + + /** + * Register a listener for messages from main process + * @param {string} channel - IPC channel name + * @param {Function} callback - Function to call with received data + * @returns {Function} Cleanup function to remove listener + */ + on: (channel, callback) => { + if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) { + const subscription = (event, ...args) => callback(...args); + ipcRenderer.on(channel, subscription); + + // Return cleanup function + return () => { + ipcRenderer.removeListener(channel, subscription); + }; + } else { + console.warn(`[Preload] Blocked listener for unauthorized channel: ${channel}`); + return () => {}; // No-op cleanup + } + }, + + /** + * Register a one-time listener for messages from main process + * @param {string} channel - IPC channel name + * @param {Function} callback - Function to call with received data + */ + once: (channel, callback) => { + if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) { + ipcRenderer.once(channel, (event, ...args) => callback(...args)); + } else { + console.warn(`[Preload] Blocked once listener for unauthorized channel: ${channel}`); + } + }, + + /** + * Remove all listeners for a channel + * @param {string} channel - IPC channel name + */ + removeAllListeners: (channel) => { + if (ALLOWED_RECEIVE_CHANNELS.includes(channel)) { + ipcRenderer.removeAllListeners(channel); + } + }, + + // ============================================ + // CONVENIENCE METHODS + // ============================================ + + // File Operations + file: { + save: (filePath, content) => ipcRenderer.send('save-file', { path: filePath, content }), + saveCurrent: (content) => ipcRenderer.send('save-current-file', content), + setCurrent: (filePath) => ipcRenderer.send('set-current-file', filePath), + saveRecent: (recentFiles) => ipcRenderer.send('save-recent-files', recentFiles), + clearRecent: () => ipcRenderer.send('clear-recent-files'), + rendererReady: () => ipcRenderer.send('renderer-ready') + }, + + // Theme Operations + theme: { + get: () => ipcRenderer.send('get-theme') + }, + + // Print Operations + print: { + doPrint: (options) => ipcRenderer.send('do-print', options) + }, + + // Export Operations + export: { + withOptions: (format, options) => ipcRenderer.send('export-with-options', { format, options }), + spreadsheet: (content, format) => ipcRenderer.send('export-spreadsheet', { content, format }) + }, + + // Batch Conversion + batch: { + convert: (inputFolder, outputFolder, format, options) => { + ipcRenderer.send('batch-convert', { inputFolder, outputFolder, format, options }); + }, + selectFolder: (type) => ipcRenderer.send('select-folder', type) + }, + + // Universal Converter + converter: { + convert: (tool, fromFormat, toFormat, filePath) => { + ipcRenderer.send('universal-convert', { tool, fromFormat, toFormat, filePath }); + }, + convertBatch: (tool, fromFormat, toFormat, inputFolder, outputFolder) => { + ipcRenderer.send('universal-convert-batch', { tool, fromFormat, toFormat, inputFolder, outputFolder }); + } + }, + + // Header/Footer Operations + headerFooter: { + getSettings: () => ipcRenderer.send('get-header-footer-settings'), + saveSettings: (settings) => ipcRenderer.send('save-header-footer-settings', settings), + browseLogo: (position) => ipcRenderer.send('browse-header-footer-logo', position), + saveLogo: (position, filePath) => ipcRenderer.send('save-header-footer-logo', { position, filePath }), + clearLogo: (position) => ipcRenderer.send('clear-header-footer-logo', position) + }, + + // Page Settings + page: { + getSettings: () => ipcRenderer.send('get-page-settings'), + updateSettings: (settings) => ipcRenderer.send('update-page-settings', settings), + setCustomStartPage: (pageNumber) => ipcRenderer.send('set-custom-start-page', pageNumber) + }, + + // PDF Operations + pdf: { + processOperation: (data) => ipcRenderer.send('process-pdf-operation', data), + getPageCount: (filePath) => ipcRenderer.send('get-pdf-page-count', filePath), + selectFolder: (inputId) => ipcRenderer.send('select-pdf-folder', inputId) + }, + + // Image Converter Operations + image: { + convert: (data) => ipcRenderer.send('image-convert', data), + batchConvert: (data) => ipcRenderer.send('image-batch-convert', data), + resize: (data) => ipcRenderer.send('image-resize', data), + compress: (data) => ipcRenderer.send('image-compress', data), + rotate: (data) => ipcRenderer.send('image-rotate', data) + }, + + // Audio Converter Operations + audio: { + convert: (data) => ipcRenderer.send('audio-convert', data), + batchConvert: (data) => ipcRenderer.send('audio-batch-convert', data), + extract: (data) => ipcRenderer.send('audio-extract', data), + trim: (data) => ipcRenderer.send('audio-trim', data), + merge: (data) => ipcRenderer.send('audio-merge', data) + }, + + // Video Converter Operations + video: { + convert: (data) => ipcRenderer.send('video-convert', data), + batchConvert: (data) => ipcRenderer.send('video-batch-convert', data), + compress: (data) => ipcRenderer.send('video-compress', data), + trim: (data) => ipcRenderer.send('video-trim', data), + extractFrames: (data) => ipcRenderer.send('video-frames', data), + toGif: (data) => ipcRenderer.send('video-gif', data) + }, + + // Generator Windows + generators: { + openAscii: () => ipcRenderer.send('open-ascii-generator'), + openTable: () => ipcRenderer.send('open-table-generator') + } +}); + +// Log successful preload initialization +console.log('[Preload] Secure IPC bridge initialized'); +console.log('[Preload] Allowed send channels:', ALLOWED_SEND_CHANNELS.length); +console.log('[Preload] Allowed receive channels:', ALLOWED_RECEIVE_CHANNELS.length); diff --git a/src/renderer.js b/src/renderer.js index a128a64..d428b8d 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -1,4514 +1,4514 @@ -/** - * MarkdownConverter Renderer Process - * @version 3.0.0 - */ - -const { ipcRenderer } = require('electron'); -const marked = require('marked'); -const DOMPurify = require('dompurify'); -const hljs = require('highlight.js'); - -// Configure marked -marked.setOptions({ - highlight: function(code, lang) { - if (lang && hljs.getLanguage(lang)) { - try { - return hljs.highlight(code, { language: lang }).value; - } catch (err) { - console.warn('Syntax highlighting failed for language:', lang, err.message); - } - } - return hljs.highlightAuto(code).value; - }, - breaks: true, - gfm: true -}); - -// Tab Management -class TabManager { - constructor() { - this.tabs = new Map(); - this.activeTabId = 1; - this.nextTabId = 2; - this.isPreviewVisible = true; - this.showLineNumbers = false; - this.autoSaveInterval = null; - this.autoSaveDelay = 30000; // 30 seconds - this.recentFiles = JSON.parse(localStorage.getItem('recentFiles') || '[]'); - - // Initialize first tab - this.tabs.set(1, { - id: 1, - title: 'Untitled', - content: '', - filePath: null, - isDirty: false, - undoStack: [], - redoStack: [], - findMatches: [], - currentMatchIndex: -1 - }); - - this.setupEventListeners(); - this.updateUI(); - } - - setupEventListeners() { - // Tab bar events - document.getElementById('new-tab-btn').addEventListener('click', () => this.createNewTab()); - document.getElementById('tab-bar').addEventListener('click', (e) => { - if (e.target.classList.contains('tab-close')) { - e.stopPropagation(); - const tabId = parseInt(e.target.closest('.tab').dataset.tabId); - this.closeTab(tabId); - } else if (e.target.closest('.tab')) { - const tabId = parseInt(e.target.closest('.tab').dataset.tabId); - this.switchToTab(tabId); - } - }); - - // Editor events for active tab - this.setupEditorEvents(); - - // Toolbar events - this.setupToolbarEvents(); - - // Find dialog events - this.setupFindEvents(); - - // Keyboard shortcuts - document.addEventListener('keydown', (e) => { - if (e.ctrlKey || e.metaKey) { - switch (e.key) { - case 'n': - case 't': - e.preventDefault(); - this.createNewTab(); - break; - case 'w': - if (this.tabs.size > 1) { - e.preventDefault(); - this.closeTab(this.activeTabId); - } - break; - case 'Tab': - if (this.tabs.size > 1) { - e.preventDefault(); - this.switchToNextTab(); - } - break; - } - } - }); - } - - createNewTab() { - const newTabId = this.nextTabId++; - const tab = { - id: newTabId, - title: 'Untitled', - content: '', - filePath: null, - isDirty: false, - undoStack: [], - redoStack: [], - findMatches: [], - currentMatchIndex: -1 - }; - - this.tabs.set(newTabId, tab); - this.createTabElements(tab); - this.switchToTab(newTabId); - this.startAutoSave(); - this.updateTabBar(); - } - - createTabElements(tab) { - // Create tab content container - const tabContent = document.createElement('div'); - tabContent.className = 'tab-content'; - tabContent.id = `tab-content-${tab.id}`; - tabContent.dataset.tabId = tab.id; - - tabContent.innerHTML = ` -
-
- - -
-
-
-
-
- -
-
-
- `; - - document.querySelector('.editor-container').appendChild(tabContent); - - // Directly attach input listener to the new editor - const editor = document.getElementById(`editor-${tab.id}`); - if (editor) { - editor.addEventListener('input', () => { - this.handleEditorInput(tab.id); - }); - - // Add scroll listener for line number sync - editor.addEventListener('scroll', () => { - if (this.showLineNumbers && this.activeTabId === tab.id) { - const lineNumbers = document.getElementById(`line-numbers-${tab.id}`); - if (lineNumbers) { - lineNumbers.scrollTop = editor.scrollTop; - } - } - }); - } - } - - switchToTab(tabId) { - if (!this.tabs.has(tabId)) return; - - // Save current tab state before switching - if (this.activeTabId && this.tabs.has(this.activeTabId)) { - this.saveCurrentTabState(); - } - - this.activeTabId = tabId; - this.updateUI(); - this.restoreTabState(tabId); - this.focusActiveEditor(); - - // Notify main process about current file for exports - const tab = this.tabs.get(tabId); - if (tab?.filePath) { - ipcRenderer.send('set-current-file', tab.filePath); - } - } - - switchToNextTab() { - const tabIds = Array.from(this.tabs.keys()); - const currentIndex = tabIds.indexOf(this.activeTabId); - const nextIndex = (currentIndex + 1) % tabIds.length; - this.switchToTab(tabIds[nextIndex]); - } - - closeTab(tabId) { - if (this.tabs.size === 1) return; // Don't close the last tab - - const tab = this.tabs.get(tabId); - if (tab.isDirty) { - // Show confirmation dialog for unsaved changes - const result = confirm('You have unsaved changes. Do you want to close this tab without saving?'); - if (!result) return; - } - - // Remove tab elements - const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); - const tabContent = document.getElementById(`tab-content-${tabId}`); - - if (tabElement?.classList.contains('tab')) { - tabElement.remove(); - } - if (tabContent) { - tabContent.remove(); - } - - this.tabs.delete(tabId); - - // Switch to another tab if this was active - if (this.activeTabId === tabId) { - const remainingTabs = Array.from(this.tabs.keys()); - this.switchToTab(remainingTabs[0]); - } - - this.updateTabBar(); - } - - updateTabBar() { - const tabBar = document.getElementById('tab-bar'); - const existingTabs = tabBar.querySelectorAll('.tab'); - - // Remove all existing tab elements except the new tab button - existingTabs.forEach(tab => tab.remove()); - - // Add tabs in order - const sortedTabs = Array.from(this.tabs.values()).sort((a, b) => a.id - b.id); - const newTabBtn = document.getElementById('new-tab-btn'); - - sortedTabs.forEach(tab => { - const tabElement = document.createElement('div'); - tabElement.className = `tab ${tab.id === this.activeTabId ? 'active' : ''}`; - tabElement.dataset.tabId = tab.id; - - const title = tab.filePath ? - tab.filePath.split('/').pop() : - tab.title; - - const dirtyIndicator = tab.isDirty ? ' •' : ''; - - tabElement.innerHTML = ` - ${title}${dirtyIndicator} - - `; - - tabBar.insertBefore(tabElement, newTabBtn); - }); - } - - updateUI() { - // Show/hide tab contents - document.querySelectorAll('.tab-content').forEach(content => { - content.classList.remove('active'); - }); - - const activeContent = document.getElementById(`tab-content-${this.activeTabId}`); - if (activeContent) { - activeContent.classList.add('active'); - } - - // Update preview visibility - this.updatePreviewVisibility(); - this.updateLineNumbers(); - this.updateTabBar(); - } - - saveCurrentTabState() { - const tab = this.tabs.get(this.activeTabId); - if (!tab) return; - - const editor = document.getElementById(`editor-${this.activeTabId}`); - if (editor) { - tab.content = editor.value; - tab.isDirty = tab.content !== (tab.originalContent || ''); - } - } - - restoreTabState(tabId) { - const tab = this.tabs.get(tabId); - if (!tab) return; - - const editor = document.getElementById(`editor-${tabId}`); - - if (editor) { - editor.value = tab.content; - this.updatePreview(tabId); - this.updateWordCount(); - } - } - - focusActiveEditor() { - const editor = document.getElementById(`editor-${this.activeTabId}`); - if (editor) { - editor.focus(); - } - } - - updatePreview(tabId = this.activeTabId) { - const tab = this.tabs.get(tabId); - const preview = document.getElementById(`preview-${tabId}`); - - if (!tab || !preview) return; - - try { - // Check if libraries are available - if (!marked || !DOMPurify) { - preview.innerHTML = '

Error: Required libraries (marked/DOMPurify) not loaded. Check internet connection.

'; - return; - } - const html = marked.parse(tab.content); - const sanitizedHtml = DOMPurify.sanitize(html); - preview.innerHTML = sanitizedHtml; - - // Render math expressions if KaTeX is available - if (window.katex && window.renderMathInElement) { - try { - window.renderMathInElement(preview, { - delimiters: [ - {left: '$$', right: '$$', display: true}, - {left: '$', right: '$', display: false}, - {left: '\\[', right: '\\]', display: true}, - {left: '\\(', right: '\\)', display: false} - ] - }); - } catch (mathError) { - console.warn('Math rendering error:', mathError); - } - } - - // Render Mermaid diagrams if Mermaid is available - if (window.mermaid) { - try { - // Find all code blocks with language-mermaid class - const mermaidBlocks = preview.querySelectorAll('pre code.language-mermaid'); - mermaidBlocks.forEach((block, index) => { - const code = block.textContent; - const pre = block.parentElement; - - // Create a div for mermaid rendering - const mermaidDiv = document.createElement('div'); - mermaidDiv.className = 'mermaid'; - mermaidDiv.setAttribute('data-processed', 'true'); - mermaidDiv.textContent = code; - - // Replace the pre element with the mermaid div - pre.parentElement.replaceChild(mermaidDiv, pre); - }); - - // Initialize Mermaid with dark theme support - const theme = document.body.className.includes('theme-dark') ? 'dark' : 'default'; - mermaid.initialize({ - startOnLoad: false, - theme: theme, - securityLevel: 'loose' - }); - - // Render all mermaid diagrams - mermaid.run({ - querySelector: '.mermaid:not([data-rendered])' - }).then(() => { - // Mark as rendered - preview.querySelectorAll('.mermaid').forEach(el => { - el.setAttribute('data-rendered', 'true'); - }); - }); - } catch (mermaidError) { - console.warn('Mermaid rendering error:', mermaidError); - } - } - } catch (error) { - console.error('Error rendering preview:', error); - preview.innerHTML = '

Error rendering preview. Please check your markdown syntax.

'; - } - } - - updatePreviewVisibility() { - document.querySelectorAll('.tab-content').forEach(content => { - const previewPane = content.querySelector('.pane:last-child'); - const editorPane = content.querySelector('.pane:first-child'); - - if (this.isPreviewVisible) { - previewPane.classList.remove('hidden'); - editorPane.classList.remove('full-width'); - } else { - previewPane.classList.add('hidden'); - editorPane.classList.add('full-width'); - } - }); - } - - updateLineNumbers() { - const editor = document.getElementById(`editor-${this.activeTabId}`); - const lineNumbers = document.getElementById(`line-numbers-${this.activeTabId}`); - - if (!editor || !lineNumbers) return; - - if (this.showLineNumbers) { - const lines = editor.value.split('\n'); - lineNumbers.innerHTML = lines.map((_, i) => - `
${i + 1}
` - ).join(''); - lineNumbers.classList.remove('hidden'); - - // Sync scroll position - lineNumbers.scrollTop = editor.scrollTop; - } else { - lineNumbers.classList.add('hidden'); - } - } - - updateWordCount() { - const tab = this.tabs.get(this.activeTabId); - if (!tab) return; - - const content = tab.content; - const words = content.trim() ? content.trim().split(/\s+/).filter(word => word.length > 0).length : 0; - const chars = content.length; - const charsNoSpaces = content.replace(/\s/g, '').length; - - // Enhanced statistics - const lines = content.split('\n').length; - const paragraphs = content.split(/\n\s*\n/).filter(p => p.trim()).length; - const readingTime = Math.ceil(words / 200); // Average reading speed: 200 words/minute - const sentences = content.split(/[.!?]+/).filter(s => s.trim()).length; - - // Update the word count display with enhanced stats - const basicStats = `Words: ${words} | Characters: ${chars} (${charsNoSpaces} no spaces)`; - const enhancedStats = `Lines: ${lines} | Paragraphs: ${paragraphs} | Sentences: ${sentences} | Reading time: ${readingTime} min`; - - document.getElementById('word-count').textContent = basicStats; - - // Add enhanced stats to a separate element - let enhancedEl = document.getElementById('enhanced-stats'); - if (!enhancedEl) { - enhancedEl = document.createElement('div'); - enhancedEl.id = 'enhanced-stats'; - enhancedEl.className = 'enhanced-stats'; - document.querySelector('.status-bar').appendChild(enhancedEl); - } - enhancedEl.textContent = enhancedStats; - } - - setupEditorEvents() { - // Set up editor events using event delegation on the container - const editorContainer = document.querySelector('.editor-container'); - if (editorContainer) { - editorContainer.addEventListener('input', (e) => { - if (e.target.classList.contains('editor-textarea')) { - const tabId = parseInt(e.target.id.split('-')[1]); - this.handleEditorInput(tabId); - } - }); - - editorContainer.addEventListener('scroll', (e) => { - if (e.target.classList.contains('editor-textarea')) { - this.updateLineNumbers(); - } - }, true); - } - } - - handleEditorInput(tabId) { - const tab = this.tabs.get(tabId); - if (!tab) return; - - const editor = document.getElementById(`editor-${tabId}`); - if (!editor) return; - - tab.content = editor.value; - tab.isDirty = true; - - this.updatePreview(tabId); - this.updateWordCount(); - this.updateLineNumbers(); - this.updateTabBar(); - - // Add to undo stack - this.pushUndoState(tabId); - } - - pushUndoState(tabId) { - const tab = this.tabs.get(tabId); - if (!tab) return; - - tab.undoStack.push(tab.content); - if (tab.undoStack.length > 50) { - tab.undoStack.shift(); - } - tab.redoStack = []; - } - - undo() { - const tab = this.tabs.get(this.activeTabId); - if (!tab || tab.undoStack.length === 0) return; - - tab.redoStack.push(tab.content); - tab.content = tab.undoStack.pop(); - - const editor = document.getElementById(`editor-${this.activeTabId}`); - if (editor) { - editor.value = tab.content; - this.updatePreview(); - this.updateWordCount(); - } - } - - redo() { - const tab = this.tabs.get(this.activeTabId); - if (!tab || tab.redoStack.length === 0) return; - - tab.undoStack.push(tab.content); - tab.content = tab.redoStack.pop(); - - const editor = document.getElementById(`editor-${this.activeTabId}`); - if (editor) { - editor.value = tab.content; - this.updatePreview(); - this.updateWordCount(); - } - } - - // Auto-save functionality - startAutoSave() { - if (this.autoSaveInterval) { - clearInterval(this.autoSaveInterval); - } - - this.autoSaveInterval = setInterval(() => { - this.performAutoSave(); - }, this.autoSaveDelay); - } - - stopAutoSave() { - if (this.autoSaveInterval) { - clearInterval(this.autoSaveInterval); - this.autoSaveInterval = null; - } - } - - performAutoSave() { - const tab = this.tabs.get(this.activeTabId); - if (!tab || !tab.filePath || !tab.content) return; - - // Only auto-save if content has changed since last save - if (tab.lastSavedContent !== tab.content) { - ipcRenderer.send('save-file', { path: tab.filePath, content: tab.content }); - tab.lastSavedContent = tab.content; - - // Show brief auto-save indicator - this.showAutoSaveIndicator(); - } - } - - showAutoSaveIndicator() { - const indicator = document.createElement('div'); - indicator.textContent = 'Auto-saved'; - indicator.className = 'auto-save-indicator'; - document.body.appendChild(indicator); - - setTimeout(() => { - indicator.classList.add('fade-out'); - setTimeout(() => { - if (indicator.parentNode) { - indicator.parentNode.removeChild(indicator); - } - }, 300); - }, 1500); - } - - // Recent files functionality - addToRecentFiles(filePath) { - if (!filePath) return; - - // Remove if already exists - this.recentFiles = this.recentFiles.filter(f => f !== filePath); - - // Add to beginning - this.recentFiles.unshift(filePath); - - // Keep only last 10 files - this.recentFiles = this.recentFiles.slice(0, 10); - - // Save to localStorage and sync with main process - localStorage.setItem('recentFiles', JSON.stringify(this.recentFiles)); - ipcRenderer.send('save-recent-files', this.recentFiles); - } - - getRecentFiles() { - return this.recentFiles.filter(file => { - // Check if file still exists (basic check by trying to access it) - try { - return file && file.length > 0; - } catch (e) { - return false; - } - }); - } - - setupToolbarEvents() { - // Bold - document.getElementById('btn-bold').addEventListener('click', () => { - this.wrapSelection('**', '**'); - }); - - // Italic - document.getElementById('btn-italic').addEventListener('click', () => { - this.wrapSelection('*', '*'); - }); - - // Heading - document.getElementById('btn-heading').addEventListener('click', () => { - this.insertAtLineStart('## '); - }); - - // Link - document.getElementById('btn-link').addEventListener('click', () => { - this.wrapSelection('[', '](url)'); - }); - - // Code - document.getElementById('btn-code').addEventListener('click', () => { - this.wrapSelection('`', '`'); - }); - - // List - document.getElementById('btn-list').addEventListener('click', () => { - this.insertAtLineStart('- '); - }); - - // Quote - document.getElementById('btn-quote').addEventListener('click', () => { - this.insertAtLineStart('> '); - }); - - // Table - document.getElementById('btn-table').addEventListener('click', () => { - this.insertTable(); - }); - - // Strikethrough - document.getElementById('btn-strikethrough').addEventListener('click', () => { - this.wrapSelection('~~', '~~'); - }); - - // Code Block - document.getElementById('btn-code-block').addEventListener('click', () => { - this.insertCodeBlock(); - }); - - // Horizontal Rule - document.getElementById('btn-horizontal-rule').addEventListener('click', () => { - this.insertHorizontalRule(); - }); - - // Preview toggle - document.getElementById('btn-preview-toggle').addEventListener('click', () => { - this.isPreviewVisible = !this.isPreviewVisible; - this.updatePreviewVisibility(); - }); - - // Line numbers - document.getElementById('btn-line-numbers').addEventListener('click', () => { - this.showLineNumbers = !this.showLineNumbers; - this.updateLineNumbers(); - }); - } - - // Helper function to wrap selected text - wrapSelection(before, after) { - const editor = document.getElementById(`editor-${this.activeTabId}`); - if (!editor) return; - - const start = editor.selectionStart; - const end = editor.selectionEnd; - const selectedText = editor.value.substring(start, end); - const replacement = before + (selectedText || 'text') + after; - - editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end); - - // Update cursor position - const newCursorPos = selectedText ? start + replacement.length : start + before.length; - editor.selectionStart = editor.selectionEnd = newCursorPos; - editor.focus(); - - // Trigger update - this.handleEditorInput(this.activeTabId); - } - - // Helper function to insert text at the start of current line - insertAtLineStart(prefix) { - const editor = document.getElementById(`editor-${this.activeTabId}`); - if (!editor) return; - - const start = editor.selectionStart; - const text = editor.value; - - // Find the start of the current line - let lineStart = text.lastIndexOf('\n', start - 1) + 1; - - // Insert the prefix - editor.value = text.substring(0, lineStart) + prefix + text.substring(lineStart); - - // Update cursor position - editor.selectionStart = editor.selectionEnd = start + prefix.length; - editor.focus(); - - // Trigger update - this.handleEditorInput(this.activeTabId); - } - - // Insert a markdown table - insertTable() { - const editor = document.getElementById(`editor-${this.activeTabId}`); - if (!editor) return; - - const table = '\n| Column 1 | Column 2 | Column 3 |\n' + - '|----------|----------|----------|\n' + - '| Cell 1 | Cell 2 | Cell 3 |\n' + - '| Cell 4 | Cell 5 | Cell 6 |\n'; - - const start = editor.selectionStart; - editor.value = editor.value.substring(0, start) + table + editor.value.substring(start); - - // Update cursor position - editor.selectionStart = editor.selectionEnd = start + table.length; - editor.focus(); - - // Trigger update - this.handleEditorInput(this.activeTabId); - } - - // Insert a code block - insertCodeBlock() { - const editor = document.getElementById(`editor-${this.activeTabId}`); - if (!editor) return; - - const selectedText = editor.value.substring(editor.selectionStart, editor.selectionEnd); - const codeBlock = '\n```\n' + (selectedText || 'code here') + '\n```\n'; - - const start = editor.selectionStart; - editor.value = editor.value.substring(0, start) + codeBlock + editor.value.substring(editor.selectionEnd); - - // Update cursor position - editor.selectionStart = editor.selectionEnd = start + codeBlock.length; - editor.focus(); - - // Trigger update - this.handleEditorInput(this.activeTabId); - } - - // Insert a horizontal rule - insertHorizontalRule() { - const editor = document.getElementById(`editor-${this.activeTabId}`); - if (!editor) return; - - const hr = '\n\n---\n\n'; - const start = editor.selectionStart; - - editor.value = editor.value.substring(0, start) + hr + editor.value.substring(start); - - // Update cursor position - editor.selectionStart = editor.selectionEnd = start + hr.length; - editor.focus(); - - // Trigger update - this.handleEditorInput(this.activeTabId); - } - - setupFindEvents() { - const btnFind = document.getElementById('btn-find'); - const btnFindClose = document.getElementById('btn-find-close'); - const findInput = document.getElementById('find-input'); - const btnFindNext = document.getElementById('btn-find-next'); - const btnFindPrev = document.getElementById('btn-find-prev'); - const btnReplace = document.getElementById('btn-replace'); - const btnReplaceAll = document.getElementById('btn-replace-all'); - - if (!btnFind || !btnFindClose || !findInput || !btnFindNext || !btnFindPrev || !btnReplace || !btnReplaceAll) { - console.error('Find dialog elements not found'); - return; - } - - // Show find dialog - btnFind.addEventListener('click', () => { - document.getElementById('find-dialog').classList.remove('hidden'); - findInput.focus(); - }); - - // Close find dialog - btnFindClose.addEventListener('click', () => { - document.getElementById('find-dialog').classList.add('hidden'); - this.clearFindHighlights(); - }); - - // Find input change - update matches - findInput.addEventListener('input', () => { - this.performFind(); - }); - - // Find next - btnFindNext.addEventListener('click', () => { - this.findNext(); - }); - - // Find previous - btnFindPrev.addEventListener('click', () => { - this.findPrevious(); - }); - - // Replace - btnReplace.addEventListener('click', () => { - this.replaceOne(); - }); - - // Replace all - btnReplaceAll.addEventListener('click', () => { - this.replaceAll(); - }); - - // Enter key in find input - find next - findInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - if (e.shiftKey) { - this.findPrevious(); - } else { - this.findNext(); - } - } - }); - - } - - performFind() { - const findText = document.getElementById('find-input').value; - const tab = this.tabs.get(this.activeTabId); - const editor = document.getElementById(`editor-${this.activeTabId}`); - - if (!findText || !tab || !editor) { - this.clearFindHighlights(); - const findCount = document.getElementById('find-count'); - if (findCount) { - findCount.textContent = '0 matches'; - } - return; - } - - const content = editor.value; - const matches = []; - let index = 0; - - // Find all matches - while ((index = content.indexOf(findText, index)) !== -1) { - matches.push(index); - index += findText.length; - } - - tab.findMatches = matches; - tab.currentMatchIndex = matches.length > 0 ? 0 : -1; - - // Update match count - const findCount = document.getElementById('find-count'); - if (findCount) { - findCount.textContent = `${matches.length} match${matches.length !== 1 ? 'es' : ''}`; - } - - // Highlight first match - if (matches.length > 0) { - this.highlightMatch(0); - } - } - - findNext() { - const tab = this.tabs.get(this.activeTabId); - if (!tab || tab.findMatches.length === 0) return; - - tab.currentMatchIndex = (tab.currentMatchIndex + 1) % tab.findMatches.length; - this.highlightMatch(tab.currentMatchIndex); - } - - findPrevious() { - const tab = this.tabs.get(this.activeTabId); - if (!tab || tab.findMatches.length === 0) return; - - tab.currentMatchIndex = tab.currentMatchIndex - 1; - if (tab.currentMatchIndex < 0) { - tab.currentMatchIndex = tab.findMatches.length - 1; - } - this.highlightMatch(tab.currentMatchIndex); - } - - highlightMatch(matchIndex) { - const tab = this.tabs.get(this.activeTabId); - const editor = document.getElementById(`editor-${this.activeTabId}`); - const findText = document.getElementById('find-input').value; - - if (!tab || !editor || matchIndex < 0 || matchIndex >= tab.findMatches.length) return; - - const position = tab.findMatches[matchIndex]; - - // Select the match WITHOUT focusing (to keep focus on find input) - editor.setSelectionRange(position, position + findText.length); - - // Make the selection visible by briefly focusing and then restoring focus - const findInput = document.getElementById('find-input'); - const hadFocus = document.activeElement === findInput; - - // Temporarily focus editor to make selection visible - editor.focus(); - - // Restore focus to find input if it had focus - if (hadFocus) { - setTimeout(() => { - findInput.focus(); - // Restore cursor position in find input - findInput.setSelectionRange(findInput.value.length, findInput.value.length); - }, 10); - } - - // Scroll into view - const lineHeight = 20; // Approximate line height - const charPosition = position; - const numLines = editor.value.substring(0, charPosition).split('\n').length; - const scrollPosition = (numLines - 5) * lineHeight; // Show match 5 lines from top - - editor.scrollTop = Math.max(0, scrollPosition); - - // Update match counter - const findCount = document.getElementById('find-count'); - if (findCount) { - findCount.textContent = `Match ${matchIndex + 1} of ${tab.findMatches.length}`; - } - } - - replaceOne() { - const tab = this.tabs.get(this.activeTabId); - const editor = document.getElementById(`editor-${this.activeTabId}`); - const findText = document.getElementById('find-input').value; - const replaceText = document.getElementById('replace-input').value; - - if (!tab || !editor || tab.findMatches.length === 0 || tab.currentMatchIndex < 0) return; - - const position = tab.findMatches[tab.currentMatchIndex]; - const before = editor.value.substring(0, position); - const after = editor.value.substring(position + findText.length); - - editor.value = before + replaceText + after; - tab.content = editor.value; - tab.isDirty = true; - - this.updatePreview(this.activeTabId); - this.updateWordCount(); - this.updateTabBar(); - - // Re-perform find to update matches - this.performFind(); - } - - replaceAll() { - const tab = this.tabs.get(this.activeTabId); - const editor = document.getElementById(`editor-${this.activeTabId}`); - const findText = document.getElementById('find-input').value; - const replaceText = document.getElementById('replace-input').value; - - if (!tab || !editor || !findText) return; - - // Simple replace all - const newContent = editor.value.split(findText).join(replaceText); - const replacedCount = tab.findMatches.length; - - editor.value = newContent; - tab.content = newContent; - tab.isDirty = true; - - this.updatePreview(this.activeTabId); - this.updateWordCount(); - this.updateTabBar(); - - // Update match count - document.getElementById('find-count').textContent = `Replaced ${replacedCount} match${replacedCount !== 1 ? 'es' : ''}`; - - // Re-perform find - this.performFind(); - } - - clearFindHighlights() { - const tab = this.tabs.get(this.activeTabId); - if (tab) { - tab.findMatches = []; - tab.currentMatchIndex = -1; - } - } - - // File operations - openFile(filePath, content) { - console.log('openFile called with:', filePath, 'content length:', content.length); - let tab = this.tabs.get(this.activeTabId); - - // Handle both forward and back slashes for cross-platform compatibility - const fileName = filePath.split(/[\\/]/).pop(); - - // If current tab is empty and untitled, reuse it - if (!tab.filePath && !tab.isDirty && tab.content === '') { - console.log('Reusing current tab'); - tab.filePath = filePath; - tab.title = fileName; - tab.content = content; - tab.originalContent = content; - tab.isDirty = false; - - // Update the editor and preview - const editor = document.getElementById(`editor-${this.activeTabId}`); - if (editor) { - editor.value = content; - // Update preview after editor is updated - this.updatePreview(this.activeTabId); - this.updateWordCount(); - } - } else { - // Create new tab for the file - console.log('Creating new tab for file'); - this.createNewTab(); - tab = this.tabs.get(this.activeTabId); - tab.filePath = filePath; - tab.title = fileName; - tab.content = content; - tab.originalContent = content; - tab.isDirty = false; - - // Wait a moment for the DOM to update, then set content - setTimeout(() => { - const editor = document.getElementById(`editor-${this.activeTabId}`); - if (editor) { - editor.value = content; - this.updatePreview(this.activeTabId); - this.updateWordCount(); - } - }, 50); - } - this.startAutoSave(); - this.addToRecentFiles(filePath); - this.updateTabBar(); - - // Notify main process about current file for exports - ipcRenderer.send('set-current-file', filePath); - - console.log('File opened successfully'); - } - - getCurrentContent() { - const tab = this.tabs.get(this.activeTabId); - return tab ? tab.content : ''; - } - - getCurrentFilePath() { - const tab = this.tabs.get(this.activeTabId); - return tab ? tab.filePath : null; - } -} - -// Initialize tab manager -let tabManager; - -document.addEventListener('DOMContentLoaded', () => { - tabManager = new TabManager(); - - // Attach input listener to the initial editor (tab 1) - const initialEditor = document.getElementById('editor-1'); - if (initialEditor) { - initialEditor.addEventListener('input', () => { - tabManager.handleEditorInput(1); - }); - - // Add scroll listener for line number sync - initialEditor.addEventListener('scroll', () => { - if (tabManager.showLineNumbers && tabManager.activeTabId === 1) { - const lineNumbers = document.getElementById('line-numbers-1'); - if (lineNumbers) { - lineNumbers.scrollTop = initialEditor.scrollTop; - } - } - }); - } - - // Request current theme - ipcRenderer.send('get-theme'); - - // Also send renderer-ready immediately as backup - // This ensures we don't get stuck waiting for theme-changed - setTimeout(() => { - console.log('Backup renderer-ready timeout triggered'); - ipcRenderer.send('renderer-ready'); - }, 100); - - // Set up auto-save interval - setInterval(() => { - // Auto-save logic for all tabs - tabManager.tabs.forEach(tab => { - if (tab.isDirty && tab.filePath) { - ipcRenderer.send('save-current-file', tab.content); - } - }); - }, 30000); -}); - -// IPC event listeners -ipcRenderer.on('file-new', () => { - tabManager.createNewTab(); -}); - -ipcRenderer.on('file-opened', (event, data) => { - console.log('[RENDERER] file-opened received:', data.path, 'content length:', data.content.length); - if (tabManager) { - tabManager.openFile(data.path, data.content); - } else { - console.error('[RENDERER] tabManager not initialized!'); - } -}); - -ipcRenderer.on('file-save', () => { - const currentContent = tabManager.getCurrentContent(); - const currentFilePath = tabManager.getCurrentFilePath(); - if (currentFilePath) { - ipcRenderer.send('save-current-file', currentContent); - } -}); - -ipcRenderer.on('get-content-for-save', (event, filePath) => { - const currentContent = tabManager.getCurrentContent(); - ipcRenderer.send('save-file', { path: filePath, content: currentContent }); -}); - -ipcRenderer.on('get-content-for-spreadsheet', (event, format) => { - const currentContent = tabManager.getCurrentContent(); - ipcRenderer.send('export-spreadsheet', { content: currentContent, format }); -}); - -ipcRenderer.on('toggle-preview', () => { - tabManager.isPreviewVisible = !tabManager.isPreviewVisible; - tabManager.updatePreviewVisibility(); -}); - -ipcRenderer.on('toggle-find', () => { - const findDialog = document.getElementById('find-dialog'); - if (findDialog.classList.contains('hidden')) { - findDialog.classList.remove('hidden'); - document.getElementById('find-input').focus(); - } else { - findDialog.classList.add('hidden'); - } -}); - -ipcRenderer.on('theme-changed', (event, theme) => { - console.log('[RENDERER] Theme changed to:', theme); - document.body.className = `theme-${theme}`; - - // After theme is applied, wait for next frame then signal renderer is ready - // This ensures complete UI initialization before files are opened - requestAnimationFrame(() => { - console.log('[RENDERER] Sending renderer-ready from theme-changed'); - ipcRenderer.send('renderer-ready'); - }); -}); - -// Undo/Redo handlers -ipcRenderer.on('undo', () => { - if (tabManager) { - tabManager.undo(); - } -}); - -ipcRenderer.on('redo', () => { - if (tabManager) { - tabManager.redo(); - } -}); - -// Font size adjustment -let currentFontSize = parseInt(localStorage.getItem('fontSize')) || 15; - -function updateFontSizes(size) { - const editors = document.querySelectorAll('#editor, .editor-textarea'); - const previews = document.querySelectorAll('#preview, .preview-content'); - - editors.forEach(editor => { - editor.style.fontSize = `${size}px`; - }); - - previews.forEach(preview => { - preview.style.fontSize = `${size}px`; - }); - - localStorage.setItem('fontSize', size); -} - -// Apply saved font size on load -updateFontSizes(currentFontSize); - -ipcRenderer.on('adjust-font-size', (event, action) => { - if (action === 'increase' && currentFontSize < 24) { - currentFontSize++; - } else if (action === 'decrease' && currentFontSize > 10) { - currentFontSize--; - } else if (action === 'reset') { - currentFontSize = 15; - } - updateFontSizes(currentFontSize); -}); - -// Print preview request handlers - handle printing directly -ipcRenderer.on('print-preview', () => { - console.log('[RENDERER] print-preview received'); - handlePrintPreview(false); -}); - -ipcRenderer.on('print-preview-styled', () => { - console.log('[RENDERER] print-preview-styled received'); - handlePrintPreview(true); -}); - -function handlePrintPreview(withStyles) { - const activeTabId = tabManager ? tabManager.activeTabId : 1; - const previewContent = document.getElementById(`preview-${activeTabId}`); - - if (!previewContent || !previewContent.innerHTML.trim()) { - alert('Nothing to print. Please create or open a document and ensure the preview is visible.'); - return; - } - - console.log('[RENDERER] Starting print with withStyles:', withStyles); - console.log('[RENDERER] Preview content length:', previewContent.innerHTML.length); - - // Add body classes for print mode - let CSS handle everything - document.body.classList.add('printing'); - if (!withStyles) { - document.body.classList.add('printing-no-styles'); - } - - // Wait for CSS to apply then print - setTimeout(() => { - console.log('[RENDERER] Sending do-print to main process'); - ipcRenderer.send('do-print', { withStyles }); - - // Restore classes after print dialog opens - setTimeout(() => { - document.body.classList.remove('printing', 'printing-no-styles'); - console.log('[RENDERER] Print classes removed'); - }, 1000); - }, 100); -} - -// Export Dialog functionality -let currentExportFormat = null; - -ipcRenderer.on('show-export-dialog', (event, format) => { - currentExportFormat = format; - showExportDialog(format); -}); - -function showExportDialog(format) { - console.log('showExportDialog called with format:', format); - const dialog = document.getElementById('export-dialog'); - const title = document.getElementById('export-dialog-title'); - - if (!dialog) { - console.error('Export dialog element not found!'); - return; - } - - console.log('Dialog found, showing export options for:', format); - title.textContent = `Export as ${format.toUpperCase()}`; - dialog.setAttribute('data-format', format); - dialog.classList.remove('hidden'); - - // Initialize form values - initializeExportForm(format); - console.log('Export dialog should now be visible'); -} - -function hideExportDialog() { - const dialog = document.getElementById('export-dialog'); - dialog.classList.add('hidden'); - currentExportFormat = null; -} - -function initializeExportForm(format) { - // Reset advanced export toggle to unchecked - const advancedToggle = document.getElementById('advanced-export-toggle'); - const advancedOptions = document.getElementById('advanced-export-options'); - - advancedToggle.checked = false; - advancedOptions.classList.add('hidden'); - - // Reset form to defaults - document.getElementById('export-template').value = 'default'; - document.getElementById('custom-template-path').style.display = 'none'; - - // Clear metadata fields - const metadataFields = document.querySelectorAll('.metadata-field'); - metadataFields.forEach((field, index) => { - if (index < 4) { // Keep first 4 default fields - field.querySelector('.metadata-key').value = ['title', 'author', 'date', 'subject'][index] || ''; - field.querySelector('.metadata-value').value = ''; - } else { - field.remove(); // Remove additional fields - } - }); - - // Reset checkboxes and other fields - document.getElementById('export-toc').checked = false; - document.getElementById('export-number-sections').checked = false; - document.getElementById('export-citeproc').checked = false; - document.getElementById('export-toc-depth').value = 3; - - // PDF-specific fields - if (format === 'pdf') { - document.getElementById('pdf-engine').value = 'xelatex'; - document.getElementById('pdf-geometry').value = 'margin=1in'; - document.getElementById('custom-geometry').style.display = 'none'; - } - - // Clear bibliography fields - document.getElementById('bibliography-file').value = ''; - document.getElementById('csl-file').value = ''; - - // Request current page settings from main process and apply them - ipcRenderer.send('get-page-settings'); -} - -function collectExportOptions() { - const advancedMode = document.getElementById('advanced-export-toggle').checked; - const options = {}; - - if (advancedMode) { - // Collect advanced options - options.template = document.getElementById('export-template').value; - options.metadata = {}; - options.variables = {}; - options.toc = document.getElementById('export-toc').checked; - options.tocDepth = document.getElementById('export-toc-depth').value; - options.numberSections = document.getElementById('export-number-sections').checked; - options.citeproc = document.getElementById('export-citeproc').checked; - } else { - // Collect basic options only - options.template = 'default'; - options.metadata = {}; - options.variables = {}; - options.toc = document.getElementById('basic-toc').checked; - options.tocDepth = 3; - options.numberSections = document.getElementById('basic-number-sections').checked; - options.citeproc = false; - } - - if (advancedMode) { - // Collect custom template path - if (options.template === 'custom') { - options.template = document.getElementById('custom-template-path').value.trim(); - } - - // Collect metadata - const metadataFields = document.querySelectorAll('.metadata-field'); - metadataFields.forEach(field => { - const key = field.querySelector('.metadata-key').value.trim(); - const value = field.querySelector('.metadata-value').value.trim(); - if (key && value) { - options.metadata[key] = value; - } - }); - - // PDF-specific options - if (currentExportFormat === 'pdf') { - options.pdfEngine = document.getElementById('pdf-engine').value; - const geometrySelect = document.getElementById('pdf-geometry'); - if (geometrySelect.value === 'custom') { - options.geometry = document.getElementById('custom-geometry').value.trim() || 'margin=1in'; - } else { - options.geometry = geometrySelect.value; - } - } - - // Bibliography - const bibFile = document.getElementById('bibliography-file').value.trim(); - const cslFile = document.getElementById('csl-file').value.trim(); - if (bibFile) options.bibliography = bibFile; - if (cslFile) options.csl = cslFile; - } else { - // Basic mode - set default PDF options if needed - if (currentExportFormat === 'pdf') { - options.pdfEngine = 'xelatex'; - options.geometry = 'margin=1in'; - } - } - - // Collect page size and orientation (always collected, from basic options) - const pageSize = document.getElementById('page-size').value; - const pageOrientation = document.getElementById('page-orientation').value; - const customWidth = document.getElementById('custom-width').value.trim(); - const customHeight = document.getElementById('custom-height').value.trim(); - - // Send page settings to main process - ipcRenderer.send('update-page-settings', { - size: pageSize, - orientation: pageOrientation, - customWidth: customWidth || null, - customHeight: customHeight || null - }); - - return options; -} - -// Export Profiles Management -let exportProfiles = {}; - -function loadExportProfiles() { - const saved = localStorage.getItem('exportProfiles'); - if (saved) { - try { - exportProfiles = JSON.parse(saved); - populateProfileDropdown(); - } catch (e) { - console.error('Failed to load export profiles:', e); - exportProfiles = {}; - } - } -} - -function saveExportProfiles() { - localStorage.setItem('exportProfiles', JSON.stringify(exportProfiles)); -} - -function populateProfileDropdown() { - const select = document.getElementById('export-profile-select'); - if (!select) return; - - // Clear existing options except the first one - while (select.options.length > 1) { - select.remove(1); - } - - // Add saved profiles - Object.keys(exportProfiles).forEach(name => { - const option = document.createElement('option'); - option.value = name; - option.textContent = name; - select.appendChild(option); - }); -} - -function saveCurrentProfile() { - const name = prompt('Enter a name for this export profile:', 'My Profile'); - if (!name || name.trim() === '') return; - - const profileName = name.trim(); - - // Collect current settings - const profile = { - format: currentExportFormat, - advancedMode: document.getElementById('advanced-export-toggle').checked, - pageSize: document.getElementById('page-size').value, - pageOrientation: document.getElementById('page-orientation').value, - basicToc: document.getElementById('basic-toc').checked, - basicNumberSections: document.getElementById('basic-number-sections').checked - }; - - // Add advanced options if enabled - if (profile.advancedMode) { - profile.template = document.getElementById('export-template').value; - profile.toc = document.getElementById('export-toc').checked; - profile.tocDepth = document.getElementById('export-toc-depth').value; - profile.numberSections = document.getElementById('export-number-sections').checked; - profile.citeproc = document.getElementById('export-citeproc').checked; - - if (currentExportFormat === 'pdf') { - profile.pdfEngine = document.getElementById('pdf-engine').value; - profile.pdfGeometry = document.getElementById('pdf-geometry').value; - } - } - - exportProfiles[profileName] = profile; - saveExportProfiles(); - populateProfileDropdown(); - - // Select the newly created profile - document.getElementById('export-profile-select').value = profileName; - - alert(`Profile "${profileName}" saved successfully!`); -} - -function loadProfile(profileName) { - if (!profileName || !exportProfiles[profileName]) return; - - const profile = exportProfiles[profileName]; - - // Apply settings - if (profile.advancedMode !== undefined) { - document.getElementById('advanced-export-toggle').checked = profile.advancedMode; - const advancedOptions = document.getElementById('advanced-export-options'); - if (profile.advancedMode) { - advancedOptions.classList.remove('hidden'); - } else { - advancedOptions.classList.add('hidden'); - } - } - - if (profile.pageSize) document.getElementById('page-size').value = profile.pageSize; - if (profile.pageOrientation) document.getElementById('page-orientation').value = profile.pageOrientation; - if (profile.basicToc !== undefined) document.getElementById('basic-toc').checked = profile.basicToc; - if (profile.basicNumberSections !== undefined) document.getElementById('basic-number-sections').checked = profile.basicNumberSections; - - // Advanced options - if (profile.advancedMode && profile.template) document.getElementById('export-template').value = profile.template; - if (profile.toc !== undefined) document.getElementById('export-toc').checked = profile.toc; - if (profile.tocDepth) document.getElementById('export-toc-depth').value = profile.tocDepth; - if (profile.numberSections !== undefined) document.getElementById('export-number-sections').checked = profile.numberSections; - if (profile.citeproc !== undefined) document.getElementById('export-citeproc').checked = profile.citeproc; - - if (profile.pdfEngine) document.getElementById('pdf-engine').value = profile.pdfEngine; - if (profile.pdfGeometry) document.getElementById('pdf-geometry').value = profile.pdfGeometry; -} - -function deleteSelectedProfile() { - const select = document.getElementById('export-profile-select'); - const profileName = select.value; - - if (!profileName) { - alert('Please select a profile to delete.'); - return; - } - - if (confirm(`Are you sure you want to delete the profile "${profileName}"?`)) { - delete exportProfiles[profileName]; - saveExportProfiles(); - populateProfileDropdown(); - select.value = ''; - alert(`Profile "${profileName}" deleted successfully!`); - } -} - -// Event listeners for export dialog -document.addEventListener('DOMContentLoaded', () => { - // Load export profiles on startup - loadExportProfiles(); - // Template selection - document.getElementById('export-template').addEventListener('change', (e) => { - const customPath = document.getElementById('custom-template-path'); - const fileInput = document.getElementById('template-file-input'); - - if (e.target.value === 'custom') { - customPath.style.display = 'block'; - fileInput.style.display = 'block'; - } else { - customPath.style.display = 'none'; - fileInput.style.display = 'none'; - customPath.value = ''; - } - }); - - // Advanced export toggle - document.getElementById('advanced-export-toggle').addEventListener('change', (e) => { - const advancedOptions = document.getElementById('advanced-export-options'); - if (e.target.checked) { - advancedOptions.classList.remove('hidden'); - // Scroll the advanced options into view after they become visible - setTimeout(() => { - advancedOptions.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - }, 100); - } else { - advancedOptions.classList.add('hidden'); - } - }); - - // Template file input - document.getElementById('template-file-input').addEventListener('change', (e) => { - const file = e.target.files[0]; - if (file) { - document.getElementById('custom-template-path').value = file.path; - } - }); - - // PDF geometry selection - document.getElementById('pdf-geometry').addEventListener('change', (e) => { - const customGeometry = document.getElementById('custom-geometry'); - if (e.target.value === 'custom') { - customGeometry.style.display = 'block'; - } else { - customGeometry.style.display = 'none'; - } - }); - - // Page size selection - show/hide custom size inputs - document.getElementById('page-size').addEventListener('change', (e) => { - const customPageSize = document.getElementById('custom-page-size'); - if (e.target.value === 'custom') { - customPageSize.style.display = 'block'; - } else { - customPageSize.style.display = 'none'; - } - }); - - // Load saved page settings on startup - ipcRenderer.send('get-page-settings'); - - // Handle page settings data (can be called multiple times) - ipcRenderer.on('page-settings-data', (event, settings) => { - console.log('Received page settings:', settings); - if (settings) { - const pageSizeEl = document.getElementById('page-size'); - const pageOrientationEl = document.getElementById('page-orientation'); - const customPageSizeEl = document.getElementById('custom-page-size'); - const customWidthEl = document.getElementById('custom-width'); - const customHeightEl = document.getElementById('custom-height'); - - if (pageSizeEl) pageSizeEl.value = settings.size || 'a4'; - if (pageOrientationEl) pageOrientationEl.value = settings.orientation || 'portrait'; - - if (customWidthEl && settings.customWidth) { - customWidthEl.value = settings.customWidth; - } - if (customHeightEl && settings.customHeight) { - customHeightEl.value = settings.customHeight; - } - - // Show custom inputs if size is custom - if (customPageSizeEl) { - if (settings.size === 'custom') { - customPageSizeEl.style.display = 'block'; - } else { - customPageSizeEl.style.display = 'none'; - } - } - } - }); - - // Export Profile buttons - document.getElementById('save-profile-btn').addEventListener('click', saveCurrentProfile); - document.getElementById('delete-profile-btn').addEventListener('click', deleteSelectedProfile); - document.getElementById('export-profile-select').addEventListener('change', (e) => { - loadProfile(e.target.value); - }); - - // Add metadata field - document.getElementById('add-metadata-field').addEventListener('click', () => { - const container = document.querySelector('.metadata-container'); - const newField = document.createElement('div'); - newField.className = 'metadata-field'; - newField.innerHTML = ` - - - `; - container.appendChild(newField); - }); - - // Browse bibliography - document.getElementById('browse-bibliography').addEventListener('click', () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.bib,.yaml,.yml,.json'; - input.onchange = (e) => { - const file = e.target.files[0]; - if (file) { - document.getElementById('bibliography-file').value = file.path; - } - }; - input.click(); - }); - - // Browse CSL - document.getElementById('browse-csl').addEventListener('click', () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.csl'; - input.onchange = (e) => { - const file = e.target.files[0]; - if (file) { - document.getElementById('csl-file').value = file.path; - } - }; - input.click(); - }); - - // Dialog close buttons - document.getElementById('export-dialog-close').addEventListener('click', hideExportDialog); - document.getElementById('export-cancel').addEventListener('click', hideExportDialog); - - // Export confirm - document.getElementById('export-confirm').addEventListener('click', () => { - const options = collectExportOptions(); - ipcRenderer.send('export-with-options', { - format: currentExportFormat, - options: options - }); - hideExportDialog(); - }); - - // Close on backdrop click - document.getElementById('export-dialog').addEventListener('click', (e) => { - if (e.target === document.getElementById('export-dialog')) { - hideExportDialog(); - } - }); - - // Close on Escape key - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && !document.getElementById('export-dialog').classList.contains('hidden')) { - hideExportDialog(); - } - }); -}); - -// Batch Conversion Dialog functionality -let currentBatchOptions = {}; - -ipcRenderer.on('show-batch-dialog', () => { - showBatchDialog(); -}); - -// Universal Converter dialog handlers -ipcRenderer.on('show-universal-converter-dialog', () => { - showUniversalConverterDialog(); -}); - -ipcRenderer.on('conversion-status', (event, status) => { - document.getElementById('converter-status').textContent = status; -}); - -ipcRenderer.on('conversion-complete', (event, result) => { - document.getElementById('converter-progress').classList.add('hidden'); - if (result.success) { - document.getElementById('universal-converter-dialog').classList.add('hidden'); - } -}); - -ipcRenderer.on('batch-progress', (event, progress) => { - updateBatchProgress(progress); -}); - -ipcRenderer.on('folder-selected', (event, { type, path }) => { - if (type === 'input') { - document.getElementById('batch-input-folder').value = path; - validateBatchForm(); - } else if (type === 'output') { - document.getElementById('batch-output-folder').value = path; - validateBatchForm(); - } else if (type === 'converter-batch-input') { - document.getElementById('converter-batch-input-folder').value = path; - } else if (type === 'converter-batch-output') { - document.getElementById('converter-batch-output-folder').value = path; - } -}); - -function showBatchDialog() { - const dialog = document.getElementById('batch-dialog'); - dialog.classList.remove('hidden'); - - // Reset form - document.getElementById('batch-input-folder').value = ''; - document.getElementById('batch-output-folder').value = ''; - document.getElementById('batch-format').value = 'html'; - document.getElementById('batch-include-subfolders').checked = true; - document.getElementById('batch-progress').classList.add('hidden'); - document.getElementById('batch-start').disabled = true; - - currentBatchOptions = { - template: 'default', - metadata: {}, - variables: {}, - toc: false, - tocDepth: 3, - numberSections: false, - citeproc: false - }; -} - -function hideBatchDialog() { - const dialog = document.getElementById('batch-dialog'); - dialog.classList.add('hidden'); -} - -function updateBatchProgress(progress) { - const progressSection = document.getElementById('batch-progress'); - const progressFill = document.getElementById('batch-progress-fill'); - const progressText = document.getElementById('batch-progress-text'); - const progressCount = document.getElementById('batch-progress-count'); - - progressSection.classList.remove('hidden'); - - const percentage = Math.round((progress.completed / progress.total) * 100); - progressFill.style.width = `${percentage}%`; - - if (progress.completed === progress.total) { - progressText.textContent = 'Conversion complete!'; - } else { - progressText.textContent = `Processing: ${progress.currentFile}`; - } - - progressCount.textContent = `${progress.completed} / ${progress.total}`; -} - -function validateBatchForm() { - const inputFolder = document.getElementById('batch-input-folder').value.trim(); - const outputFolder = document.getElementById('batch-output-folder').value.trim(); - const startButton = document.getElementById('batch-start'); - - startButton.disabled = !inputFolder || !outputFolder; -} - -// Event listeners for batch dialog -document.addEventListener('DOMContentLoaded', () => { - // Browse input folder - document.getElementById('browse-input-folder').addEventListener('click', () => { - ipcRenderer.send('select-folder', 'input'); - }); - - // Browse output folder - document.getElementById('browse-output-folder').addEventListener('click', () => { - ipcRenderer.send('select-folder', 'output'); - }); - - // Show advanced options - document.getElementById('batch-show-options').addEventListener('click', () => { - const format = document.getElementById('batch-format').value; - currentExportFormat = format; - showExportDialog(format); - }); - - // Dialog close buttons - document.getElementById('batch-dialog-close').addEventListener('click', hideBatchDialog); - document.getElementById('batch-cancel').addEventListener('click', hideBatchDialog); - - // Start batch conversion - document.getElementById('batch-start').addEventListener('click', () => { - const inputFolder = document.getElementById('batch-input-folder').value.trim(); - const outputFolder = document.getElementById('batch-output-folder').value.trim(); - const format = document.getElementById('batch-format').value; - - if (!inputFolder || !outputFolder) { - return; - } - - // Use current export options from advanced dialog if they were set - const options = currentBatchOptions; - - // Start batch conversion - ipcRenderer.send('batch-convert', { - inputFolder, - outputFolder, - format, - options - }); - - // Show progress - document.getElementById('batch-progress').classList.remove('hidden'); - document.getElementById('batch-start').disabled = true; - }); - - // Close on backdrop click - document.getElementById('batch-dialog').addEventListener('click', (e) => { - if (e.target === document.getElementById('batch-dialog')) { - hideBatchDialog(); - } - }); - - // Close on Escape key (modified to handle both dialogs) - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { - if (!document.getElementById('export-dialog').classList.contains('hidden')) { - hideExportDialog(); - } else if (!document.getElementById('batch-dialog').classList.contains('hidden')) { - hideBatchDialog(); - } - } - }); - - // Input validation - document.getElementById('batch-input-folder').addEventListener('input', validateBatchForm); - document.getElementById('batch-output-folder').addEventListener('input', validateBatchForm); -}); - -// Override the export dialog confirm to also save batch options -const originalExportConfirm = document.getElementById('export-confirm'); -if (originalExportConfirm) { - originalExportConfirm.addEventListener('click', () => { - // If batch dialog is open, save options for batch conversion - if (!document.getElementById('batch-dialog').classList.contains('hidden')) { - currentBatchOptions = collectExportOptions(); - } - }); -} - -// Universal File Converter Dialog Functions -let converterFilePath = ''; - -// Format definitions for each converter -const converterFormats = { - libreoffice: { - input: [ - { value: 'docx', label: 'Word Document (DOCX)' }, - { value: 'doc', label: 'Word 97-2003 (DOC)' }, - { value: 'odt', label: 'OpenDocument Text (ODT)' }, - { value: 'rtf', label: 'Rich Text Format (RTF)' }, - { value: 'txt', label: 'Plain Text (TXT)' }, - { value: 'html', label: 'HTML Document' }, - { value: 'htm', label: 'HTM Document' }, - { value: 'xlsx', label: 'Excel Spreadsheet (XLSX)' }, - { value: 'xls', label: 'Excel 97-2003 (XLS)' }, - { value: 'ods', label: 'OpenDocument Spreadsheet (ODS)' }, - { value: 'csv', label: 'Comma Separated Values (CSV)' }, - { value: 'pptx', label: 'PowerPoint (PPTX)' }, - { value: 'ppt', label: 'PowerPoint 97-2003 (PPT)' }, - { value: 'odp', label: 'OpenDocument Presentation (ODP)' } - ], - output: [ - { value: 'pdf', label: 'PDF Document' }, - { value: 'docx', label: 'Word Document (DOCX)' }, - { value: 'doc', label: 'Word 97-2003 (DOC)' }, - { value: 'odt', label: 'OpenDocument Text (ODT)' }, - { value: 'rtf', label: 'Rich Text Format (RTF)' }, - { value: 'txt', label: 'Plain Text (TXT)' }, - { value: 'html', label: 'HTML Document' }, - { value: 'xlsx', label: 'Excel Spreadsheet (XLSX)' }, - { value: 'xls', label: 'Excel 97-2003 (XLS)' }, - { value: 'ods', label: 'OpenDocument Spreadsheet (ODS)' }, - { value: 'csv', label: 'CSV' }, - { value: 'pptx', label: 'PowerPoint (PPTX)' }, - { value: 'ppt', label: 'PowerPoint 97-2003 (PPT)' }, - { value: 'odp', label: 'OpenDocument Presentation (ODP)' } - ] - }, - imagemagick: { - input: [ - { value: 'jpg', label: 'JPEG Image (JPG)' }, - { value: 'jpeg', label: 'JPEG Image (JPEG)' }, - { value: 'png', label: 'PNG Image' }, - { value: 'gif', label: 'GIF Image' }, - { value: 'bmp', label: 'Bitmap Image (BMP)' }, - { value: 'tiff', label: 'TIFF Image' }, - { value: 'tif', label: 'TIF Image' }, - { value: 'webp', label: 'WebP Image' }, - { value: 'svg', label: 'SVG Vector Image' }, - { value: 'ico', label: 'Icon File (ICO)' }, - { value: 'psd', label: 'Photoshop (PSD)' }, - { value: 'raw', label: 'RAW Image' }, - { value: 'cr2', label: 'Canon RAW (CR2)' }, - { value: 'nef', label: 'Nikon RAW (NEF)' }, - { value: 'heic', label: 'HEIC Image' }, - { value: 'avif', label: 'AVIF Image' } - ], - output: [ - { value: 'jpg', label: 'JPEG Image (JPG)' }, - { value: 'png', label: 'PNG Image' }, - { value: 'gif', label: 'GIF Image' }, - { value: 'bmp', label: 'Bitmap Image (BMP)' }, - { value: 'tiff', label: 'TIFF Image' }, - { value: 'webp', label: 'WebP Image' }, - { value: 'svg', label: 'SVG Vector Image' }, - { value: 'ico', label: 'Icon File (ICO)' }, - { value: 'pdf', label: 'PDF Document' }, - { value: 'eps', label: 'EPS Vector' }, - { value: 'ps', label: 'PostScript' }, - { value: 'avif', label: 'AVIF Image' } - ] - }, - ffmpeg: { - input: [ - { value: 'mp4', label: 'MP4 Video' }, - { value: 'avi', label: 'AVI Video' }, - { value: 'mov', label: 'MOV Video (QuickTime)' }, - { value: 'mkv', label: 'MKV Video (Matroska)' }, - { value: 'wmv', label: 'WMV Video (Windows Media)' }, - { value: 'flv', label: 'FLV Video (Flash)' }, - { value: 'webm', label: 'WebM Video' }, - { value: 'mpeg', label: 'MPEG Video' }, - { value: 'mpg', label: 'MPG Video' }, - { value: 'm4v', label: 'M4V Video' }, - { value: 'mp3', label: 'MP3 Audio' }, - { value: 'wav', label: 'WAV Audio' }, - { value: 'ogg', label: 'OGG Audio' }, - { value: 'flac', label: 'FLAC Audio' }, - { value: 'aac', label: 'AAC Audio' }, - { value: 'm4a', label: 'M4A Audio' }, - { value: 'wma', label: 'WMA Audio' } - ], - output: [ - { value: 'mp4', label: 'MP4 Video' }, - { value: 'avi', label: 'AVI Video' }, - { value: 'mov', label: 'MOV Video (QuickTime)' }, - { value: 'mkv', label: 'MKV Video (Matroska)' }, - { value: 'webm', label: 'WebM Video' }, - { value: 'mpeg', label: 'MPEG Video' }, - { value: 'gif', label: 'Animated GIF' }, - { value: 'mp3', label: 'MP3 Audio' }, - { value: 'wav', label: 'WAV Audio' }, - { value: 'ogg', label: 'OGG Audio' }, - { value: 'flac', label: 'FLAC Audio' }, - { value: 'aac', label: 'AAC Audio' }, - { value: 'm4a', label: 'M4A Audio' } - ] - }, - pandoc: { - input: [ - { value: 'md', label: 'Markdown (MD)' }, - { value: 'markdown', label: 'Markdown' }, - { value: 'html', label: 'HTML Document' }, - { value: 'docx', label: 'Word Document (DOCX)' }, - { value: 'odt', label: 'OpenDocument Text (ODT)' }, - { value: 'rtf', label: 'Rich Text Format (RTF)' }, - { value: 'tex', label: 'LaTeX Document' }, - { value: 'latex', label: 'LaTeX' }, - { value: 'epub', label: 'EPUB eBook' }, - { value: 'rst', label: 'reStructuredText (RST)' }, - { value: 'textile', label: 'Textile' }, - { value: 'org', label: 'Org Mode' }, - { value: 'mediawiki', label: 'MediaWiki' }, - { value: 'docbook', label: 'DocBook XML' } - ], - output: [ - { value: 'html', label: 'HTML Document' }, - { value: 'pdf', label: 'PDF Document' }, - { value: 'docx', label: 'Word Document (DOCX)' }, - { value: 'odt', label: 'OpenDocument Text (ODT)' }, - { value: 'rtf', label: 'Rich Text Format (RTF)' }, - { value: 'epub', label: 'EPUB eBook' }, - { value: 'latex', label: 'LaTeX Document' }, - { value: 'md', label: 'Markdown (MD)' }, - { value: 'rst', label: 'reStructuredText (RST)' }, - { value: 'textile', label: 'Textile' }, - { value: 'org', label: 'Org Mode' }, - { value: 'mediawiki', label: 'MediaWiki' }, - { value: 'docbook', label: 'DocBook XML' }, - { value: 'pptx', label: 'PowerPoint (PPTX)' } - ] - } -}; - -function showUniversalConverterDialog() { - const dialog = document.getElementById('universal-converter-dialog'); - dialog.classList.remove('hidden'); - converterFilePath = ''; - document.getElementById('converter-file-path').value = ''; - document.getElementById('converter-tool').value = 'libreoffice'; - document.getElementById('converter-progress').classList.add('hidden'); - updateConverterFormats('libreoffice'); -} - -function updateConverterFormats(tool) { - const fromSelect = document.getElementById('converter-from'); - const toSelect = document.getElementById('converter-to'); - const helpText = document.getElementById('converter-tool-help'); - - // Clear existing options - fromSelect.innerHTML = ''; - toSelect.innerHTML = ''; - - // Get formats for selected tool - const formats = converterFormats[tool]; - - if (formats) { - // Populate input formats - formats.input.forEach(format => { - const option = document.createElement('option'); - option.value = format.value; - option.textContent = format.label; - fromSelect.appendChild(option); - }); - - // Populate output formats - formats.output.forEach(format => { - const option = document.createElement('option'); - option.value = format.value; - option.textContent = format.label; - toSelect.appendChild(option); - }); - - // Update help text - if (tool === 'libreoffice') { - helpText.textContent = 'Documents, Spreadsheets, Presentations - Office file conversions'; - } else if (tool === 'imagemagick') { - helpText.textContent = 'Image format conversions - JPG, PNG, GIF, TIFF, WebP, SVG, and more'; - } else if (tool === 'ffmpeg') { - helpText.textContent = 'Video and audio conversions - MP4, AVI, MOV, MP3, WAV, and more'; - } else if (tool === 'pandoc') { - helpText.textContent = 'Document markup conversions - Markdown, HTML, LaTeX, EPUB, and more'; - } - } -} - -function updateConverterAdvancedOptions(tool) { - // Hide all tool-specific options - const allOptions = document.querySelectorAll('.converter-options'); - allOptions.forEach(opt => opt.classList.add('hidden')); - - // Show options for selected tool - const toolOptions = document.querySelector(`.${tool}-options`); - if (toolOptions) { - toolOptions.classList.remove('hidden'); - } -} - -function collectConverterAdvancedOptions(tool) { - const options = {}; - const advancedMode = document.getElementById('converter-advanced-toggle').checked; - - if (!advancedMode) { - return options; - } - - // Tool-specific options - if (tool === 'imagemagick') { - options.quality = document.getElementById('imagemagick-quality').value; - options.dpi = document.getElementById('imagemagick-dpi').value || null; - options.resize = document.getElementById('imagemagick-resize').value || null; - options.compression = document.getElementById('imagemagick-compression').value || null; - } else if (tool === 'ffmpeg') { - options.videoCodec = document.getElementById('ffmpeg-video-codec').value || null; - options.audioCodec = document.getElementById('ffmpeg-audio-codec').value || null; - options.bitrate = document.getElementById('ffmpeg-bitrate').value || null; - options.preset = document.getElementById('ffmpeg-preset').value || null; - options.framerate = document.getElementById('ffmpeg-framerate').value || null; - } else if (tool === 'libreoffice') { - options.quality = document.getElementById('libreoffice-quality').value || null; - options.pageRange = document.getElementById('libreoffice-page-range').value || null; - options.exportBookmarks = document.getElementById('libreoffice-export-bookmarks').checked; - } - - return options; -} - -document.addEventListener('DOMContentLoaded', () => { - // Universal Converter tool change - const converterTool = document.getElementById('converter-tool'); - if (converterTool) { - converterTool.addEventListener('change', (e) => { - updateConverterFormats(e.target.value); - updateConverterAdvancedOptions(e.target.value); - }); - } - - // Batch mode toggle - const converterBatchMode = document.getElementById('converter-batch-mode'); - if (converterBatchMode) { - converterBatchMode.addEventListener('change', (e) => { - const batchOptions = document.getElementById('converter-batch-options'); - const singleFileSection = document.getElementById('converter-file-path').closest('.export-section'); - - if (e.target.checked) { - batchOptions.classList.remove('hidden'); - singleFileSection.style.display = 'none'; - } else { - batchOptions.classList.add('hidden'); - singleFileSection.style.display = 'block'; - } - }); - } - - // Advanced options toggle - const converterAdvancedToggle = document.getElementById('converter-advanced-toggle'); - if (converterAdvancedToggle) { - converterAdvancedToggle.addEventListener('change', (e) => { - const advancedOptions = document.getElementById('converter-advanced-options'); - if (e.target.checked) { - advancedOptions.classList.remove('hidden'); - // Update which tool-specific options to show - updateConverterAdvancedOptions(document.getElementById('converter-tool').value); - } else { - advancedOptions.classList.add('hidden'); - } - }); - } - - // ImageMagick quality slider - const imagemagickQuality = document.getElementById('imagemagick-quality'); - if (imagemagickQuality) { - imagemagickQuality.addEventListener('input', (e) => { - document.getElementById('imagemagick-quality-value').textContent = e.target.value; - }); - } - - // Browse batch input folder - const browseBatchInput = document.getElementById('browse-converter-batch-input'); - if (browseBatchInput) { - browseBatchInput.addEventListener('click', () => { - ipcRenderer.send('select-folder', 'converter-batch-input'); - }); - } - - // Browse batch output folder - const browseBatchOutput = document.getElementById('browse-converter-batch-output'); - if (browseBatchOutput) { - browseBatchOutput.addEventListener('click', () => { - ipcRenderer.send('select-folder', 'converter-batch-output'); - }); - } - - // Browse for file to convert - const browseConverterFile = document.getElementById('browse-converter-file'); - if (browseConverterFile) { - browseConverterFile.addEventListener('click', () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '*'; - input.onchange = (e) => { - const file = e.target.files[0]; - if (file) { - converterFilePath = file.path; - document.getElementById('converter-file-path').value = file.path; - } - }; - input.click(); - }); - } - - // Universal Converter dialog close - const converterDialogClose = document.getElementById('converter-dialog-close'); - if (converterDialogClose) { - converterDialogClose.addEventListener('click', () => { - document.getElementById('universal-converter-dialog').classList.add('hidden'); - }); - } - - // Universal Converter cancel - const converterCancel = document.getElementById('converter-cancel'); - if (converterCancel) { - converterCancel.addEventListener('click', () => { - document.getElementById('universal-converter-dialog').classList.add('hidden'); - }); - } - - // Universal Converter convert - const converterConvert = document.getElementById('converter-convert'); - if (converterConvert) { - converterConvert.addEventListener('click', () => { - const tool = document.getElementById('converter-tool').value; - const fromFormat = document.getElementById('converter-from').value; - const toFormat = document.getElementById('converter-to').value; - const batchMode = document.getElementById('converter-batch-mode').checked; - const advancedOptions = collectConverterAdvancedOptions(tool); - - if (batchMode) { - // Batch conversion - const inputFolder = document.getElementById('converter-batch-input-folder').value.trim(); - const outputFolder = document.getElementById('converter-batch-output-folder').value.trim(); - const includeSubfolders = document.getElementById('converter-batch-subfolders').checked; - - if (!inputFolder || !outputFolder) { - alert('Please select both input and output folders for batch conversion'); - return; - } - - // Show progress - document.getElementById('converter-progress').classList.remove('hidden'); - - // Send batch conversion request - ipcRenderer.send('universal-convert-batch', { - tool, - fromFormat, - toFormat, - inputFolder, - outputFolder, - includeSubfolders, - advancedOptions - }); - } else { - // Single file conversion - const filePath = converterFilePath; - - if (!filePath) { - alert('Please select a file to convert'); - return; - } - - // Show progress - document.getElementById('converter-progress').classList.remove('hidden'); - - // Send single file conversion request - ipcRenderer.send('universal-convert', { - tool, - fromFormat, - toFormat, - filePath, - advancedOptions - }); - } - }); - } -}); - -// IPC event listeners for recent files functionality -ipcRenderer.on('recent-files-cleared', () => { - if (tabManager) { - tabManager.recentFiles = []; - localStorage.setItem('recentFiles', JSON.stringify([])); - console.log('Recent files cleared'); - } -}); - -// ======================================== -// PDF Editor Dialog Functionality -// ======================================== - -let currentPDFOperation = null; -let mergeFilePaths = []; - -// Show Table Generator Dialog -ipcRenderer.on('show-table-generator', () => { - showTableGenerator(); -}); - -// Show PDF Editor Dialog -ipcRenderer.on('show-pdf-editor-dialog', (event, operation, openedFilePath) => { - currentPDFOperation = operation; - showPDFEditorDialog(operation, openedFilePath); -}); - -function showPDFEditorDialog(operation, openedFilePath = null) { - const dialog = document.getElementById('pdf-editor-dialog'); - const title = document.getElementById('pdf-editor-title'); - - // Hide all operation sections - document.querySelectorAll('.pdf-operation-section').forEach(section => { - section.classList.add('hidden'); - }); - - // Show the appropriate section and set title - let sectionId, titleText; - switch (operation) { - case 'merge': - sectionId = 'pdf-merge-section'; - titleText = 'Merge PDFs'; - mergeFilePaths = []; - // If we have an opened file, add it as the first file to merge - if (openedFilePath) { - mergeFilePaths.push(openedFilePath); - } - updateMergeFilesList(); - break; - case 'split': - sectionId = 'pdf-split-section'; - titleText = 'Split PDF'; - // Pre-fill input path if we have an opened file - if (openedFilePath) { - document.getElementById('split-input-path').value = openedFilePath; - } - break; - case 'compress': - sectionId = 'pdf-compress-section'; - titleText = 'Compress PDF'; - if (openedFilePath) { - document.getElementById('compress-input-path').value = openedFilePath; - } - break; - case 'rotate': - sectionId = 'pdf-rotate-section'; - titleText = 'Rotate Pages'; - if (openedFilePath) { - const rotateInput = document.getElementById('rotate-input-path'); - if (rotateInput) rotateInput.value = openedFilePath; - } - break; - case 'delete': - sectionId = 'pdf-delete-section'; - titleText = 'Delete Pages'; - if (openedFilePath) { - const deleteInput = document.getElementById('delete-input-path'); - if (deleteInput) deleteInput.value = openedFilePath; - } - break; - case 'reorder': - sectionId = 'pdf-reorder-section'; - titleText = 'Reorder Pages'; - if (openedFilePath) { - const reorderInput = document.getElementById('reorder-input-path'); - if (reorderInput) reorderInput.value = openedFilePath; - } - break; - case 'watermark': - sectionId = 'pdf-watermark-section'; - titleText = 'Add Watermark'; - if (openedFilePath) { - const watermarkInput = document.getElementById('watermark-input-path'); - if (watermarkInput) watermarkInput.value = openedFilePath; - } - break; - case 'encrypt': - sectionId = 'pdf-encrypt-section'; - titleText = 'Password Protection'; - if (openedFilePath) { - const encryptInput = document.getElementById('encrypt-input-path'); - if (encryptInput) encryptInput.value = openedFilePath; - } - break; - case 'decrypt': - sectionId = 'pdf-decrypt-section'; - titleText = 'Remove Password'; - if (openedFilePath) { - const decryptInput = document.getElementById('decrypt-input-path'); - if (decryptInput) decryptInput.value = openedFilePath; - } - break; - case 'permissions': - sectionId = 'pdf-permissions-section'; - titleText = 'Set Permissions'; - if (openedFilePath) { - const permInput = document.getElementById('permissions-input-path'); - if (permInput) permInput.value = openedFilePath; - } - break; - } - - title.textContent = titleText; - document.getElementById(sectionId).classList.remove('hidden'); - dialog.classList.remove('hidden'); -} - -function hidePDFEditorDialog() { - document.getElementById('pdf-editor-dialog').classList.add('hidden'); - document.getElementById('pdf-progress').classList.add('hidden'); - currentPDFOperation = null; -} - -function updateMergeFilesList() { - const listContainer = document.getElementById('merge-files-list'); - listContainer.innerHTML = ''; - - mergeFilePaths.forEach((filePath, index) => { - const fileEntry = document.createElement('div'); - fileEntry.className = 'file-entry'; - fileEntry.innerHTML = ` - ${filePath.split(/[\\/]/).pop()} - - `; - listContainer.appendChild(fileEntry); - }); -} - -// PDF Editor Event Listeners -document.addEventListener('DOMContentLoaded', () => { - // Close PDF Editor Dialog - const pdfEditorClose = document.getElementById('pdf-editor-dialog-close'); - if (pdfEditorClose) { - pdfEditorClose.addEventListener('click', hidePDFEditorDialog); - } - - const pdfEditorCancel = document.getElementById('pdf-editor-cancel'); - if (pdfEditorCancel) { - pdfEditorCancel.addEventListener('click', hidePDFEditorDialog); - } - - // Process button - const pdfEditorProcess = document.getElementById('pdf-editor-process'); - if (pdfEditorProcess) { - pdfEditorProcess.addEventListener('click', processPDFOperation); - } - - // Merge PDFs - Add file button - const addMergeFile = document.getElementById('add-merge-file'); - if (addMergeFile) { - addMergeFile.addEventListener('click', () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.pdf'; - input.multiple = true; - input.onchange = (e) => { - const files = Array.from(e.target.files); - files.forEach(file => { - if (!mergeFilePaths.includes(file.path)) { - mergeFilePaths.push(file.path); - } - }); - updateMergeFilesList(); - }; - input.click(); - }); - } - - // Remove file from merge list (using event delegation) - const mergeFilesList = document.getElementById('merge-files-list'); - if (mergeFilesList) { - mergeFilesList.addEventListener('click', (e) => { - if (e.target.classList.contains('remove-file')) { - const index = parseInt(e.target.dataset.index); - mergeFilePaths.splice(index, 1); - updateMergeFilesList(); - } - }); - } - - // Browse buttons for all operations - const browseButtons = [ - { id: 'browse-merge-output', inputId: 'merge-output-path', saveDialog: true }, - { id: 'browse-split-input', inputId: 'split-input-path', saveDialog: false }, - { id: 'browse-split-output', inputId: 'split-output-folder', folder: true }, - { id: 'browse-compress-input', inputId: 'compress-input-path', saveDialog: false }, - { id: 'browse-compress-output', inputId: 'compress-output-path', saveDialog: true }, - { id: 'browse-rotate-input', inputId: 'rotate-input-path', saveDialog: false }, - { id: 'browse-rotate-output', inputId: 'rotate-output-path', saveDialog: true }, - { id: 'browse-delete-input', inputId: 'delete-input-path', saveDialog: false }, - { id: 'browse-delete-output', inputId: 'delete-output-path', saveDialog: true }, - { id: 'browse-reorder-input', inputId: 'reorder-input-path', saveDialog: false }, - { id: 'browse-reorder-output', inputId: 'reorder-output-path', saveDialog: true }, - { id: 'browse-watermark-input', inputId: 'watermark-input-path', saveDialog: false }, - { id: 'browse-watermark-output', inputId: 'watermark-output-path', saveDialog: true }, - { id: 'browse-encrypt-input', inputId: 'encrypt-input-path', saveDialog: false }, - { id: 'browse-encrypt-output', inputId: 'encrypt-output-path', saveDialog: true }, - { id: 'browse-decrypt-input', inputId: 'decrypt-input-path', saveDialog: false }, - { id: 'browse-decrypt-output', inputId: 'decrypt-output-path', saveDialog: true }, - { id: 'browse-permissions-input', inputId: 'permissions-input-path', saveDialog: false }, - { id: 'browse-permissions-output', inputId: 'permissions-output-path', saveDialog: true } - ]; - - browseButtons.forEach(button => { - const btn = document.getElementById(button.id); - if (btn) { - btn.addEventListener('click', () => { - const input = document.createElement('input'); - input.type = 'file'; - - if (button.folder) { - // Request folder selection via IPC - ipcRenderer.send('select-pdf-folder', button.inputId); - } else if (button.saveDialog) { - input.nwsaveas = true; - input.accept = '.pdf'; - input.onchange = (e) => { - const file = e.target.files[0]; - if (file) { - document.getElementById(button.inputId).value = file.path; - } - }; - input.click(); - } else { - input.accept = '.pdf'; - input.onchange = (e) => { - const file = e.target.files[0]; - if (file) { - document.getElementById(button.inputId).value = file.path; - } - }; - input.click(); - } - }); - } - }); - - // Split mode change handler - const splitMode = document.getElementById('split-mode'); - if (splitMode) { - splitMode.addEventListener('change', (e) => { - // Hide all split options - document.getElementById('split-pages-options').classList.add('hidden'); - document.getElementById('split-interval-options').classList.add('hidden'); - document.getElementById('split-size-options').classList.add('hidden'); - - // Show selected split option - if (e.target.value === 'pages') { - document.getElementById('split-pages-options').classList.remove('hidden'); - } else if (e.target.value === 'interval') { - document.getElementById('split-interval-options').classList.remove('hidden'); - } else if (e.target.value === 'size') { - document.getElementById('split-size-options').classList.remove('hidden'); - } - }); - } - - // Watermark opacity slider - const watermarkOpacity = document.getElementById('watermark-opacity'); - if (watermarkOpacity) { - watermarkOpacity.addEventListener('input', (e) => { - document.getElementById('watermark-opacity-value').textContent = e.target.value; - }); - } - - // Watermark pages selection - const watermarkPages = document.getElementById('watermark-pages'); - if (watermarkPages) { - watermarkPages.addEventListener('change', (e) => { - const customPages = document.getElementById('watermark-custom-pages'); - if (e.target.value === 'custom') { - customPages.classList.remove('hidden'); - } else { - customPages.classList.add('hidden'); - } - }); - } - - // Overwrite checkbox handlers - toggle Save As section visibility - const overwriteCheckboxes = [ - { checkbox: 'compress-overwrite', section: 'compress-saveas-section' }, - { checkbox: 'rotate-overwrite', section: 'rotate-saveas-section' }, - { checkbox: 'delete-overwrite', section: 'delete-saveas-section' }, - { checkbox: 'reorder-overwrite', section: 'reorder-saveas-section' }, - { checkbox: 'watermark-overwrite', section: 'watermark-saveas-section' }, - { checkbox: 'encrypt-overwrite', section: 'encrypt-saveas-section' }, - { checkbox: 'decrypt-overwrite', section: 'decrypt-saveas-section' }, - { checkbox: 'permissions-overwrite', section: 'permissions-saveas-section' } - ]; - - overwriteCheckboxes.forEach(item => { - const checkbox = document.getElementById(item.checkbox); - const section = document.getElementById(item.section); - if (checkbox && section) { - checkbox.addEventListener('change', (e) => { - if (e.target.checked) { - section.classList.add('hidden'); - } else { - section.classList.remove('hidden'); - } - }); - } - }); - - // Load current page order button - const loadCurrentOrder = document.getElementById('load-current-order'); - if (loadCurrentOrder) { - loadCurrentOrder.addEventListener('click', () => { - const inputPath = document.getElementById('reorder-input-path').value; - if (!inputPath) { - alert('Please select a PDF file first'); - return; - } - // Request page count from main process - ipcRenderer.send('get-pdf-page-count', inputPath); - }); - } -}); - -// Handle folder selection response -ipcRenderer.on('pdf-folder-selected', (event, { inputId, path }) => { - document.getElementById(inputId).value = path; -}); - -// Handle PDF page count response -ipcRenderer.on('pdf-page-count', (event, { count, error }) => { - if (error) { - alert('Error reading PDF: ' + error); - return; - } - - const currentOrder = Array.from({ length: count }, (_, i) => i + 1).join(', '); - document.getElementById('current-order-display').textContent = currentOrder; - document.getElementById('current-page-order').classList.remove('hidden'); - document.getElementById('reorder-pages').value = currentOrder; -}); - -// Process PDF Operation -function processPDFOperation() { - const operation = currentPDFOperation; - let operationData = { operation }; - - try { - switch (operation) { - case 'merge': - if (mergeFilePaths.length < 2) { - alert('Please add at least 2 PDF files to merge'); - return; - } - operationData.inputFiles = mergeFilePaths; - operationData.outputPath = document.getElementById('merge-output-path').value.trim(); - if (!operationData.outputPath) { - alert('Please select an output file path'); - return; - } - break; - - case 'split': - operationData.inputPath = document.getElementById('split-input-path').value.trim(); - operationData.outputFolder = document.getElementById('split-output-folder').value.trim(); - operationData.splitMode = document.getElementById('split-mode').value; - - if (!operationData.inputPath || !operationData.outputFolder) { - alert('Please select input file and output folder'); - return; - } - - if (operationData.splitMode === 'pages') { - operationData.pageRanges = document.getElementById('split-page-ranges').value.trim(); - } else if (operationData.splitMode === 'interval') { - operationData.interval = parseInt(document.getElementById('split-interval').value); - } else if (operationData.splitMode === 'size') { - operationData.maxSize = parseInt(document.getElementById('split-size').value); - } - break; - - case 'compress': - operationData.inputPath = document.getElementById('compress-input-path').value.trim(); - operationData.overwrite = document.getElementById('compress-overwrite').checked; - operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('compress-output-path').value.trim(); - operationData.compressionLevel = document.getElementById('compress-level').value; - operationData.compressImages = document.getElementById('compress-images').checked; - operationData.removeDuplicates = document.getElementById('compress-remove-duplicates').checked; - operationData.optimizeFonts = document.getElementById('compress-optimize-fonts').checked; - - if (!operationData.inputPath || !operationData.outputPath) { - alert('Please select input file' + (operationData.overwrite ? '' : ' and output file paths')); - return; - } - break; - - case 'rotate': - operationData.inputPath = document.getElementById('rotate-input-path').value.trim(); - operationData.overwrite = document.getElementById('rotate-overwrite').checked; - operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('rotate-output-path').value.trim(); - operationData.pages = document.getElementById('rotate-pages').value.trim(); - operationData.angle = parseInt(document.getElementById('rotate-angle').value); - - if (!operationData.inputPath || !operationData.outputPath) { - alert('Please select input file' + (operationData.overwrite ? '' : ' and output file')); - return; - } - break; - - case 'delete': - operationData.inputPath = document.getElementById('delete-input-path').value.trim(); - operationData.overwrite = document.getElementById('delete-overwrite').checked; - operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('delete-output-path').value.trim(); - operationData.pages = document.getElementById('delete-pages').value.trim(); - - if (!operationData.inputPath || !operationData.outputPath || !operationData.pages) { - alert('Please fill in all required fields'); - return; - } - break; - - case 'reorder': - operationData.inputPath = document.getElementById('reorder-input-path').value.trim(); - operationData.overwrite = document.getElementById('reorder-overwrite').checked; - operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('reorder-output-path').value.trim(); - operationData.newOrder = document.getElementById('reorder-pages').value.trim(); - - if (!operationData.inputPath || !operationData.outputPath || !operationData.newOrder) { - alert('Please fill in all required fields'); - return; - } - break; - - case 'watermark': - operationData.inputPath = document.getElementById('watermark-input-path').value.trim(); - operationData.overwrite = document.getElementById('watermark-overwrite').checked; - operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('watermark-output-path').value.trim(); - operationData.text = document.getElementById('watermark-text').value.trim(); - operationData.fontSize = parseInt(document.getElementById('watermark-font-size').value); - operationData.opacity = parseInt(document.getElementById('watermark-opacity').value) / 100; - operationData.position = document.getElementById('watermark-position').value; - operationData.color = document.getElementById('watermark-color').value; - operationData.pages = document.getElementById('watermark-pages').value; - - if (operationData.pages === 'custom') { - operationData.customPages = document.getElementById('watermark-custom-pages').value.trim(); - } - - if (!operationData.inputPath || !operationData.outputPath || !operationData.text) { - alert('Please fill in all required fields'); - return; - } - break; - - case 'encrypt': - operationData.inputPath = document.getElementById('encrypt-input-path').value.trim(); - operationData.overwrite = document.getElementById('encrypt-overwrite').checked; - operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('encrypt-output-path').value.trim(); - operationData.userPassword = document.getElementById('encrypt-user-password').value; - operationData.ownerPassword = document.getElementById('encrypt-owner-password').value; - operationData.encryptionLevel = parseInt(document.getElementById('encrypt-level').value); - - operationData.permissions = { - printing: document.getElementById('encrypt-allow-printing').checked, - modifying: document.getElementById('encrypt-allow-modify').checked, - copying: document.getElementById('encrypt-allow-copy').checked, - annotating: document.getElementById('encrypt-allow-annotate').checked, - fillingForms: document.getElementById('encrypt-allow-forms').checked, - contentAccessibility: document.getElementById('encrypt-allow-extract').checked, - documentAssembly: document.getElementById('encrypt-allow-assemble').checked, - printingQuality: document.getElementById('encrypt-allow-print-high').checked - }; - - if (!operationData.inputPath || !operationData.outputPath || !operationData.userPassword) { - alert('Please select file and enter a user password'); - return; - } - break; - - case 'decrypt': - operationData.inputPath = document.getElementById('decrypt-input-path').value.trim(); - operationData.overwrite = document.getElementById('decrypt-overwrite').checked; - operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('decrypt-output-path').value.trim(); - operationData.password = document.getElementById('decrypt-password').value; - - if (!operationData.inputPath || !operationData.outputPath || !operationData.password) { - alert('Please fill in all required fields'); - return; - } - break; - - case 'permissions': - operationData.inputPath = document.getElementById('permissions-input-path').value.trim(); - operationData.overwrite = document.getElementById('permissions-overwrite').checked; - operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('permissions-output-path').value.trim(); - operationData.currentPassword = document.getElementById('permissions-current-password').value; - operationData.ownerPassword = document.getElementById('permissions-owner-password').value; - - operationData.permissions = { - printing: document.getElementById('permissions-allow-printing').checked, - modifying: document.getElementById('permissions-allow-modify').checked, - copying: document.getElementById('permissions-allow-copy').checked, - annotating: document.getElementById('permissions-allow-annotate').checked, - fillingForms: document.getElementById('permissions-allow-forms').checked, - contentAccessibility: document.getElementById('permissions-allow-extract').checked, - documentAssembly: document.getElementById('permissions-allow-assemble').checked, - printingQuality: document.getElementById('permissions-allow-print-high').checked - }; - - if (!operationData.inputPath || !operationData.outputPath || !operationData.ownerPassword) { - alert('Please fill in all required fields'); - return; - } - break; - } - - // Show progress - document.getElementById('pdf-progress').classList.remove('hidden'); - document.getElementById('pdf-progress-text').textContent = 'Processing PDF...'; - - // Send to main process - ipcRenderer.send('process-pdf-operation', operationData); - - } catch (error) { - alert('Error: ' + error.message); - console.error('PDF operation error:', error); - } -} - -// Handle PDF operation completion -ipcRenderer.on('pdf-operation-complete', (event, { success, error, message }) => { - document.getElementById('pdf-progress').classList.add('hidden'); - - if (success) { - alert(message || 'PDF operation completed successfully!'); - hidePDFEditorDialog(); - } else { - alert('Error: ' + (error || 'PDF operation failed')); - } -}); - -// Handle PDF operation progress -ipcRenderer.on('pdf-operation-progress', (event, { message, progress }) => { - document.getElementById('pdf-progress-text').textContent = message; - if (progress !== undefined) { - const progressFill = document.getElementById('pdf-progress-fill'); - if (progressFill) { - progressFill.style.width = `${progress}%`; - } - } -}); - -// Add math rendering support using KaTeX for enhanced preview -function initMathSupport() { - // Add KaTeX CSS - const katexCSS = document.createElement('link'); - katexCSS.rel = 'stylesheet'; - katexCSS.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css'; - katexCSS.crossOrigin = 'anonymous'; - document.head.appendChild(katexCSS); - - // Add KaTeX JS - const katexJS = document.createElement('script'); - katexJS.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js'; - katexJS.crossOrigin = 'anonymous'; - katexJS.onload = () => { - // Add auto-render extension - const autoRenderJS = document.createElement('script'); - autoRenderJS.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js'; - autoRenderJS.crossOrigin = 'anonymous'; - autoRenderJS.onload = () => { - console.log('Math support (KaTeX) initialized'); - // Re-render current preview to include math - if (tabManager) { - tabManager.updatePreview(); - } - }; - document.head.appendChild(autoRenderJS); - }; - document.head.appendChild(katexJS); -} - -// Initialize math support on load -initMathSupport(); - -// ================================ -// Header & Footer Dialog Management -// ================================ - -let currentFieldTarget = null; // Track which input field is being edited - -// Open header/footer settings dialog -function openHeaderFooterDialog() { - const dialog = document.getElementById('header-footer-dialog'); - dialog.classList.remove('hidden'); - - // Request current settings from main process - ipcRenderer.send('get-header-footer-settings'); -} - -// Close header/footer settings dialog -function closeHeaderFooterDialog() { - const dialog = document.getElementById('header-footer-dialog'); - dialog.classList.add('hidden'); -} - -// Open field picker dialog -function openFieldPickerDialog(targetInputId) { - currentFieldTarget = targetInputId; - const dialog = document.getElementById('field-picker-dialog'); - dialog.classList.remove('hidden'); -} - -// Close field picker dialog -function closeFieldPickerDialog() { - const dialog = document.getElementById('field-picker-dialog'); - dialog.classList.add('hidden'); - currentFieldTarget = null; -} - -// Load settings into dialog -ipcRenderer.on('header-footer-settings-data', (event, settings) => { - // Enable/disable checkbox - document.getElementById('hf-enabled').checked = settings.enabled; - - // Header fields - document.getElementById('header-left').value = settings.header.left || ''; - document.getElementById('header-center').value = settings.header.center || ''; - document.getElementById('header-right').value = settings.header.right || ''; - - // Footer fields - document.getElementById('footer-left').value = settings.footer.left || ''; - document.getElementById('footer-center').value = settings.footer.center || ''; - document.getElementById('footer-right').value = settings.footer.right || ''; - - // Logo previews - if (settings.header.logo) { - document.getElementById('header-logo-preview').textContent = path.basename(settings.header.logo); - } else { - document.getElementById('header-logo-preview').textContent = ''; - } - - if (settings.footer.logo) { - document.getElementById('footer-logo-preview').textContent = path.basename(settings.footer.logo); - } else { - document.getElementById('footer-logo-preview').textContent = ''; - } - - // Update config content visibility - toggleConfigContent(); -}); - -// Toggle config content based on enabled checkbox -function toggleConfigContent() { - const enabled = document.getElementById('hf-enabled').checked; - const configContent = document.getElementById('hf-config-content'); - - if (enabled) { - configContent.classList.remove('disabled'); - } else { - configContent.classList.add('disabled'); - } -} - -// Save header/footer settings -function saveHeaderFooterSettings() { - const settings = { - enabled: document.getElementById('hf-enabled').checked, - header: { - left: document.getElementById('header-left').value || '', - center: document.getElementById('header-center').value || '', - right: document.getElementById('header-right').value || '', - logo: null // Logo paths are managed separately - }, - footer: { - left: document.getElementById('footer-left').value || '', - center: document.getElementById('footer-center').value || '', - right: document.getElementById('footer-right').value || '', - logo: null - } - }; - - ipcRenderer.send('save-header-footer-settings', settings); - closeHeaderFooterDialog(); -} - -// Handle logo browsing - ask main process to show open dialog -function browseForLogo(position) { - ipcRenderer.send('browse-header-footer-logo', position); -} - -// Handle logo saved confirmation -ipcRenderer.on('header-footer-logo-saved', (event, { position, path }) => { - document.getElementById(`${position}-logo-preview`).textContent = path.split(/[\\/]/).pop(); -}); - -// Clear logo -function clearLogo(position) { - ipcRenderer.send('clear-header-footer-logo', position); - document.getElementById(`${position}-logo-preview`).textContent = ''; -} - -// Handle logo cleared confirmation -ipcRenderer.on('header-footer-logo-cleared', (event, position) => { - console.log(`${position} logo cleared`); -}); - -// Insert dynamic field into input -function insertDynamicField(field) { - if (currentFieldTarget) { - const input = document.getElementById(currentFieldTarget); - const cursorPos = input.selectionStart; - const textBefore = input.value.substring(0, cursorPos); - const textAfter = input.value.substring(cursorPos); - - input.value = textBefore + field + textAfter; - input.focus(); - input.setSelectionRange(cursorPos + field.length, cursorPos + field.length); - } - - closeFieldPickerDialog(); -} - -// Event Listeners for Header/Footer Dialog - -// Close buttons -document.getElementById('header-footer-close').addEventListener('click', closeHeaderFooterDialog); -document.getElementById('header-footer-cancel').addEventListener('click', closeHeaderFooterDialog); -document.getElementById('header-footer-save').addEventListener('click', saveHeaderFooterSettings); - -// Enable/disable checkbox -document.getElementById('hf-enabled').addEventListener('change', toggleConfigContent); - -// Field insert buttons -document.querySelectorAll('.field-insert-btn').forEach(btn => { - btn.addEventListener('click', () => { - const target = btn.getAttribute('data-target'); - openFieldPickerDialog(target); - }); -}); - -// Logo browse buttons -document.getElementById('header-logo-browse').addEventListener('click', () => browseForLogo('header')); -document.getElementById('footer-logo-browse').addEventListener('click', () => browseForLogo('footer')); - -// Logo clear buttons -document.getElementById('header-logo-clear').addEventListener('click', () => clearLogo('header')); -document.getElementById('footer-logo-clear').addEventListener('click', () => clearLogo('footer')); - -// Field picker dialog -document.getElementById('field-picker-close').addEventListener('click', closeFieldPickerDialog); -document.querySelectorAll('.field-option').forEach(btn => { - btn.addEventListener('click', () => { - const field = btn.getAttribute('data-field'); - insertDynamicField(field); - }); -}); - -// Export function to make openHeaderFooterDialog accessible globally -window.openHeaderFooterDialog = openHeaderFooterDialog; - -// Listen for menu command to open dialog -ipcRenderer.on('open-header-footer-dialog', () => { - openHeaderFooterDialog(); -}); -// Command Palette Implementation -const commands = [ - { name: 'New File', action: () => tabManager.createTab(), shortcut: 'Ctrl+N' }, - { name: 'Open File', action: () => ipcRenderer.send('open-file'), shortcut: 'Ctrl+O' }, - { name: 'Save File', action: () => ipcRenderer.send('save-file'), shortcut: 'Ctrl+S' }, - { name: 'Save As', action: () => ipcRenderer.send('save-file-as'), shortcut: 'Ctrl+Shift+S' }, - { name: 'Export to PDF', action: () => ipcRenderer.send('export', 'pdf'), shortcut: '' }, - { name: 'Export to DOCX', action: () => ipcRenderer.send('export', 'docx'), shortcut: '' }, - { name: 'Export to HTML', action: () => ipcRenderer.send('export', 'html'), shortcut: '' }, - { name: 'Toggle Preview', action: () => tabManager.togglePreview(), shortcut: 'Ctrl+Shift+P' }, - { name: 'Toggle Line Numbers', action: () => tabManager.toggleLineNumbers(), shortcut: '' }, - { name: 'Find & Replace', action: () => showFindDialog(), shortcut: 'Ctrl+F' }, - { name: 'New Tab', action: () => tabManager.createTab(), shortcut: 'Ctrl+T' }, - { name: 'Close Tab', action: () => tabManager.closeTab(tabManager.activeTabId), shortcut: 'Ctrl+W' }, - { name: 'Undo', action: () => tabManager.undo(), shortcut: 'Ctrl+Z' }, - { name: 'Redo', action: () => tabManager.redo(), shortcut: 'Ctrl+Shift+Z' }, - { name: 'Bold', action: () => insertMarkdown('**', '**'), shortcut: 'Ctrl+B' }, - { name: 'Italic', action: () => insertMarkdown('*', '*'), shortcut: 'Ctrl+I' }, - { name: 'Heading 1', action: () => insertMarkdown('# ', ''), shortcut: '' }, - { name: 'Heading 2', action: () => insertMarkdown('## ', ''), shortcut: '' }, - { name: 'Heading 3', action: () => insertMarkdown('### ', ''), shortcut: '' }, - { name: 'Insert Link', action: () => insertMarkdown('[', '](url)'), shortcut: '' }, - { name: 'Insert Image', action: () => insertMarkdown('![', '](image.jpg)'), shortcut: '' }, - { name: 'Insert Code Block', action: () => insertMarkdown('```\n', '\n```'), shortcut: '' }, - { name: 'Insert Table', action: () => insertMarkdown('\n| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |\n', ''), shortcut: '' }, - { name: 'Table Generator', action: () => showTableGenerator(), shortcut: '' }, - { name: 'ASCII Art Generator', action: () => showASCIIGenerator(), shortcut: '' }, - { name: 'Increase Font Size', action: () => ipcRenderer.send('adjust-font-size', 'increase'), shortcut: 'Ctrl+Shift++' }, - { name: 'Decrease Font Size', action: () => ipcRenderer.send('adjust-font-size', 'decrease'), shortcut: 'Ctrl+Shift+-' } -]; - -let commandPaletteSelectedIndex = 0; -let filteredCommands = [...commands]; - -function showCommandPalette() { - const palette = document.getElementById('command-palette'); - const searchInput = document.getElementById('command-search'); - - palette.classList.remove('hidden'); - searchInput.value = ''; - searchInput.focus(); - - filteredCommands = [...commands]; - commandPaletteSelectedIndex = 0; - renderCommandList(); -} - -function hideCommandPalette() { - const palette = document.getElementById('command-palette'); - palette.classList.add('hidden'); -} - -function renderCommandList() { - const list = document.getElementById('command-list'); - list.innerHTML = ''; - - filteredCommands.forEach((cmd, index) => { - const item = document.createElement('div'); - item.className = 'command-item'; - if (index === commandPaletteSelectedIndex) { - item.classList.add('selected'); - } - - const name = document.createElement('span'); - name.className = 'command-name'; - name.textContent = cmd.name; - - const shortcut = document.createElement('span'); - shortcut.className = 'command-shortcut'; - shortcut.textContent = cmd.shortcut || ''; - - item.appendChild(name); - if (cmd.shortcut) { - item.appendChild(shortcut); - } - - item.addEventListener('click', () => { - executeCommand(cmd); - }); - - list.appendChild(item); - }); -} - -function filterCommands(query) { - const lowerQuery = query.toLowerCase(); - filteredCommands = commands.filter(cmd => - cmd.name.toLowerCase().includes(lowerQuery) - ); - commandPaletteSelectedIndex = 0; - renderCommandList(); -} - -function executeCommand(cmd) { - hideCommandPalette(); - if (cmd && cmd.action) { - cmd.action(); - } -} - -function navigateCommandPalette(direction) { - commandPaletteSelectedIndex += direction; - if (commandPaletteSelectedIndex < 0) { - commandPaletteSelectedIndex = filteredCommands.length - 1; - } - if (commandPaletteSelectedIndex >= filteredCommands.length) { - commandPaletteSelectedIndex = 0; - } - renderCommandList(); -} - -// Command Palette Event Listeners -document.addEventListener('keydown', (e) => { - // Ctrl+Shift+P to open command palette - if (e.ctrlKey && e.shiftKey && e.key === 'P') { - e.preventDefault(); - showCommandPalette(); - return; - } - - // Handle command palette navigation when open - const palette = document.getElementById('command-palette'); - if (!palette.classList.contains('hidden')) { - if (e.key === 'Escape') { - e.preventDefault(); - hideCommandPalette(); - } else if (e.key === 'ArrowDown') { - e.preventDefault(); - navigateCommandPalette(1); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - navigateCommandPalette(-1); - } else if (e.key === 'Enter') { - e.preventDefault(); - if (filteredCommands[commandPaletteSelectedIndex]) { - executeCommand(filteredCommands[commandPaletteSelectedIndex]); - } - } - } -}); - -document.getElementById('command-search').addEventListener('input', (e) => { - filterCommands(e.target.value); -}); - -// Close on backdrop click -document.getElementById('command-palette').addEventListener('click', (e) => { - if (e.target === document.getElementById('command-palette')) { - hideCommandPalette(); - } -}); - -// ============================================================================ -// TABLE GENERATOR -// ============================================================================ - -function showTableGenerator() { - const dialog = document.getElementById('table-generator-dialog'); - dialog.classList.remove('hidden'); - - // Generate initial preview - generateTablePreview(); - - // Focus on rows input - setTimeout(() => { - document.getElementById('table-rows').focus(); - }, 100); -} - -function hideTableGenerator() { - const dialog = document.getElementById('table-generator-dialog'); - dialog.classList.add('hidden'); -} - -function generateTablePreview() { - const rows = parseInt(document.getElementById('table-rows').value) || 3; - const cols = parseInt(document.getElementById('table-cols').value) || 3; - const hasHeader = document.getElementById('table-has-header').checked; - const alignment = document.getElementById('table-alignment').value; - - const table = generateMarkdownTable(rows, cols, hasHeader, alignment); - document.getElementById('table-preview').textContent = table; -} - -function generateMarkdownTable(rows, cols, hasHeader, alignment) { - let table = ''; - - // Generate alignment string - let alignChar = '-'; - if (alignment === 'center') { - alignChar = ':' + '-'.repeat(Math.max(8, 10)) + ':'; - } else if (alignment === 'right') { - alignChar = '-'.repeat(Math.max(8, 10)) + ':'; - } else { - alignChar = '-'.repeat(Math.max(8, 10)); - } - - // Generate header row - if (hasHeader) { - table += '| '; - for (let c = 1; c <= cols; c++) { - table += `Header ${c}`; - if (c < cols) table += ' | '; - } - table += ' |\n'; - - // Separator row - table += '| '; - for (let c = 1; c <= cols; c++) { - table += alignChar; - if (c < cols) table += ' | '; - } - table += ' |\n'; - - // Data rows (excluding header) - for (let r = 1; r < rows; r++) { - table += '| '; - for (let c = 1; c <= cols; c++) { - table += `Cell ${r},${c}`; - if (c < cols) table += ' | '; - } - table += ' |\n'; - } - } else { - // No header - all rows are data rows - // First row (acts as separator position) - table += '| '; - for (let c = 1; c <= cols; c++) { - table += `Cell 1,${c}`; - if (c < cols) table += ' | '; - } - table += ' |\n'; - - // Separator row - table += '| '; - for (let c = 1; c <= cols; c++) { - table += alignChar; - if (c < cols) table += ' | '; - } - table += ' |\n'; - - // Remaining data rows - for (let r = 2; r <= rows; r++) { - table += '| '; - for (let c = 1; c <= cols; c++) { - table += `Cell ${r},${c}`; - if (c < cols) table += ' | '; - } - table += ' |\n'; - } - } - - return table; -} - -function insertGeneratedTable() { - const table = document.getElementById('table-preview').textContent; - - if (!table) { - alert('Please generate a table preview first'); - return; - } - - // Get active editor - const activeTabId = tabManager ? tabManager.activeTabId : 1; - const editor = document.getElementById(`editor-${activeTabId}`); - - if (!editor) { - alert('No active editor found'); - return; - } - - // Insert table at cursor position - const start = editor.selectionStart; - const before = editor.value.substring(0, start); - const after = editor.value.substring(editor.selectionEnd); - - // Add newlines for spacing - const tableWithSpacing = '\n' + table + '\n'; - editor.value = before + tableWithSpacing + after; - - // Update cursor position - editor.selectionStart = editor.selectionEnd = start + tableWithSpacing.length; - editor.focus(); - - // Trigger update - if (tabManager) { - tabManager.handleEditorInput(activeTabId); - } - - // Close dialog - hideTableGenerator(); -} - -// Table Generator Event Listeners -document.getElementById('table-dialog-close').addEventListener('click', hideTableGenerator); -document.getElementById('table-cancel').addEventListener('click', hideTableGenerator); -document.getElementById('table-generate-preview').addEventListener('click', generateTablePreview); -document.getElementById('table-insert').addEventListener('click', insertGeneratedTable); - -// Auto-update preview when inputs change -document.getElementById('table-rows').addEventListener('input', generateTablePreview); -document.getElementById('table-cols').addEventListener('input', generateTablePreview); -document.getElementById('table-has-header').addEventListener('change', generateTablePreview); -document.getElementById('table-alignment').addEventListener('change', generateTablePreview); - -// Close dialog on backdrop click -document.getElementById('table-generator-dialog').addEventListener('click', (e) => { - if (e.target === document.getElementById('table-generator-dialog')) { - hideTableGenerator(); - } -}); - -// Handle Enter key in inputs -document.getElementById('table-rows').addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - insertGeneratedTable(); - } -}); - -document.getElementById('table-cols').addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - insertGeneratedTable(); - } -}); - -// ============================================================================ -// ASCII ART GENERATOR -// ============================================================================ - -let currentASCIIMode = 'text'; - -function showASCIIGenerator() { - const dialog = document.getElementById('ascii-art-dialog'); - dialog.classList.remove('hidden'); - - // Initialize with text mode - switchASCIIMode('text'); - - // Generate initial preview - setTimeout(() => { - generateASCIIPreview(); - }, 100); -} - -function hideASCIIGenerator() { - const dialog = document.getElementById('ascii-art-dialog'); - dialog.classList.add('hidden'); -} - -function switchASCIIMode(mode) { - currentASCIIMode = mode; - - // Update button states - document.querySelectorAll('.ascii-mode-btn').forEach(btn => { - btn.classList.remove('active'); - }); - - // Hide all mode sections - document.querySelectorAll('.ascii-mode-section').forEach(section => { - section.classList.add('hidden'); - }); - - // Show selected mode - if (mode === 'text') { - document.getElementById('ascii-mode-text').classList.add('active'); - document.getElementById('ascii-text-mode').classList.remove('hidden'); - } else if (mode === 'box') { - document.getElementById('ascii-mode-box').classList.add('active'); - document.getElementById('ascii-box-mode').classList.remove('hidden'); - } else if (mode === 'templates') { - document.getElementById('ascii-mode-templates').classList.add('active'); - document.getElementById('ascii-templates-mode').classList.remove('hidden'); - } - - // Generate preview - generateASCIIPreview(); -} - -function generateASCIIPreview() { - let result = ''; - - if (currentASCIIMode === 'text') { - const text = document.getElementById('ascii-text-input').value || 'Sample'; - const style = document.getElementById('ascii-font-style').value; - result = textToASCII(text, style); - } else if (currentASCIIMode === 'box') { - const text = document.getElementById('ascii-box-text').value || 'Sample Text'; - const style = document.getElementById('ascii-box-style').value; - const padding = parseInt(document.getElementById('ascii-box-padding').value) || 2; - result = createASCIIBox(text, style, padding); - } else if (currentASCIIMode === 'templates') { - result = 'Select a template from the buttons above'; - } - - document.getElementById('ascii-preview').textContent = result; -} - -// Simple ASCII art text generator -function textToASCII(text, style) { - const fonts = { - standard: { - height: 5, - chars: { - 'A': [' _ ', ' / \\ ', '/___\\', '| |', '| |'], - 'B': ['____ ', '| \\', '|___/', '| \\', '|___/'], - 'C': [' ___ ', '/ _/', '| \\', '| ', '\\___/'], - 'D': ['____ ', '| \\', '| |', '| |', '|___/'], - 'E': ['_____', '| ', '|___ ', '| ', '|____'], - 'F': ['_____', '| ', '|___ ', '| ', '| '], - 'G': [' ___ ', '/ _', '| ||', '| |', '\\___/'], - 'H': ['| |', '| |', '|___|', '| |', '| |'], - 'I': ['_____', ' | ', ' | ', ' | ', '_____'], - 'J': ['_____', ' | ', ' | ', '| | ', '\\__/ '], - 'K': ['| |', '| / ', '|_/ ', '| \\ ', '| \\ '], - 'L': ['| ', '| ', '| ', '| ', '|____'], - 'M': ['| |', '|\\ /|', '| V |', '| |', '| |'], - 'N': ['| |', '|\\ |', '| \\ |', '| \\|', '| |'], - 'O': [' ___ ', '/ \\', '| |', '| |', '\\___/'], - 'P': ['____ ', '| \\', '|___/', '| ', '| '], - 'Q': [' ___ ', '/ \\', '| |', '| |\\ ', '\\__\\|'], - 'R': ['____ ', '| \\', '|___/', '| \\ ', '| \\ '], - 'S': [' ___ ', '/ _/', '\\_ \\', ' \\ ', '\\___/'], - 'T': ['_____', ' | ', ' | ', ' | ', ' | '], - 'U': ['| |', '| |', '| |', '| |', '\\___/'], - 'V': ['| |', '| |', '| |', ' \\ / ', ' V '], - 'W': ['| |', '| |', '| W |', '|/ \\|', '| |'], - 'X': ['| |', ' \\ / ', ' X ', ' / \\ ', '| |'], - 'Y': ['| |', ' \\ / ', ' Y ', ' | ', ' | '], - 'Z': ['_____', ' / ', ' / ', ' / ', '/___ '], - ' ': [' ', ' ', ' ', ' ', ' '], - '0': [' ___ ', '/ \\', '| | |', '| |', '\\___/'], - '1': [' | ', ' || ', ' | ', ' | ', ' _|_ '], - '2': [' ___ ', '\\ /', ' __/ ', '/ ', '\\____'], - '3': [' ___ ', '\\ /', ' __/ ', ' / ', '\\___/'], - '4': ['| |', '| |', '|___|', ' |', ' |'], - '5': [' ___ ', '| ', '|___ ', ' / ', '\\___/'], - '6': [' ___ ', '/ ', '|___ ', '| |', '\\___/'], - '7': ['_____', ' / ', ' / ', ' / ', '/ '], - '8': [' ___ ', '| |', ' ___ ', '| |', '\\___/'], - '9': [' ___ ', '| |', '\\___|', ' |', '\\___/'] - } - }, - banner: { - height: 7, - chars: { - 'A': [' ### ', ' ## ## ', ' ## ## ', ' ## ## ', ' ####### ', ' ## ## ', ' ## ## '], - 'B': [' ###### ', ' ## ## ', ' ## ## ', ' ###### ', ' ## ## ', ' ## ## ', ' ###### '], - 'C': [' ##### ', ' ## ## ', ' ## ', ' ## ', ' ## ', ' ## ## ', ' ##### '], - 'D': [' ###### ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ###### '], - 'E': [' ####### ', ' ## ', ' ## ', ' ##### ', ' ## ', ' ## ', ' ####### '], - 'F': [' ####### ', ' ## ', ' ## ', ' ##### ', ' ## ', ' ## ', ' ## '], - 'G': [' ##### ', ' ## ## ', ' ## ', ' ## ### ', ' ## ## ', ' ## ## ', ' ##### '], - 'H': [' ## ## ', ' ## ## ', ' ## ## ', ' ####### ', ' ## ## ', ' ## ## ', ' ## ## '], - 'I': [' ####### ', ' ### ', ' ### ', ' ### ', ' ### ', ' ### ', ' ####### '], - 'J': [' ####### ', ' ## ', ' ## ', ' ## ', ' ## ## ', ' ## ## ', ' #### '], - 'K': [' ## ## ', ' ## ## ', ' ## ## ', ' #### ', ' ## ## ', ' ## ## ', ' ## ## '], - 'L': [' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ####### '], - 'M': [' ## ## ', ' ### ### ', ' ####### ', ' ## # ## ', ' ## ## ', ' ## ## ', ' ## ## '], - 'N': [' ## ## ', ' ### ## ', ' #### ## ', ' ## #### ', ' ## ### ', ' ## ## ', ' ## ## '], - 'O': [' ##### ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ##### '], - 'P': [' ###### ', ' ## ## ', ' ## ## ', ' ###### ', ' ## ', ' ## ', ' ## '], - 'Q': [' ##### ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## # ## ', ' ## ## ', ' ### ## '], - 'R': [' ###### ', ' ## ## ', ' ## ## ', ' ###### ', ' ## ## ', ' ## ## ', ' ## ## '], - 'S': [' ##### ', ' ## ## ', ' ## ', ' ##### ', ' ## ', ' ## ## ', ' ##### '], - 'T': [' ####### ', ' ### ', ' ### ', ' ### ', ' ### ', ' ### ', ' ### '], - 'U': [' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ##### '], - 'V': [' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ### '], - 'W': [' ## ## ', ' ## ## ', ' ## ## ', ' ## # ## ', ' ####### ', ' ### ### ', ' ## ## '], - 'X': [' ## ## ', ' ## ## ', ' ### ', ' # ', ' ### ', ' ## ## ', ' ## ## '], - 'Y': [' ## ## ', ' ## ## ', ' ### ', ' # ', ' # ', ' # ', ' # '], - 'Z': [' ####### ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ####### '], - ' ': [' ', ' ', ' ', ' ', ' ', ' ', ' '], - '0': [' ##### ', ' ## ## ', ' ## ### ', ' ## #### ', ' ### ## ', ' ## ## ', ' ##### '], - '1': [' ## ', ' ### ', ' ## ', ' ## ', ' ## ', ' ## ', ' ####### '], - '2': [' ##### ', ' ## ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ####### '], - '3': [' ##### ', ' ## ## ', ' ## ', ' ### ', ' ## ', ' ## ## ', ' ##### '], - '4': [' ### ', ' #### ', ' ## ## ', ' ## ## ', ' ####### ', ' ## ', ' ## '], - '5': [' ####### ', ' ## ', ' ###### ', ' ## ', ' ## ', ' ## ## ', ' ##### '], - '6': [' ##### ', ' ## ## ', ' ## ', ' ###### ', ' ## ## ', ' ## ## ', ' ##### '], - '7': [' ####### ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## '], - '8': [' ##### ', ' ## ## ', ' ## ## ', ' ##### ', ' ## ## ', ' ## ## ', ' ##### '], - '9': [' ##### ', ' ## ## ', ' ## ## ', ' ###### ', ' ## ', ' ## ## ', ' ##### '] - } - }, - block: { - height: 6, - chars: { - 'A': ['█████╗ ', '██╔══██╗', '███████║', '██╔══██║', '██║ ██║', '╚═╝ ╚═╝'], - 'B': ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔══██╗', '██████╔╝', '╚═════╝ '], - 'C': ['██████╗', '██╔════╝', '██║ ', '██║ ', '╚██████╗', ' ╚═════╝'], - 'D': ['██████╗ ', '██╔══██╗', '██║ ██║', '██║ ██║', '██████╔╝', '╚═════╝ '], - 'E': ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '███████╗', '╚══════╝'], - 'F': ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '██║ ', '╚═╝ '], - 'G': ['██████╗ ', '██╔════╝', '██║ ███╗', '██║ ██║', '╚██████╔╝', ' ╚═════╝ '], - 'H': ['██╗ ██╗', '██║ ██║', '███████║', '██╔══██║', '██║ ██║', '╚═╝ ╚═╝'], - 'I': ['██╗', '██║', '██║', '██║', '██║', '╚═╝'], - ' ': [' ', ' ', ' ', ' ', ' ', ' '] - } - }, - bubble: { - height: 5, - chars: { - 'A': [' Ⓐ ', ' ⒜ ⒜ ', '⒜⒜⒜⒜⒜', '⒜ ⒜', '⒜ ⒜'], - 'B': ['ⒷⒷⒷⒷ ', 'Ⓑ Ⓑ', 'ⒷⒷⒷⒷ ', 'Ⓑ Ⓑ', 'ⒷⒷⒷⒷ '], - 'C': [' ⒸⒸⒸ ', 'Ⓒ ', 'Ⓒ ', 'Ⓒ ', ' ⒸⒸⒸ '], - ' ': [' ', ' ', ' ', ' ', ' '] - } - }, - digital: { - height: 7, - chars: { - 'A': [' ▄▀▀▀▄ ', '▐ ▌', '▐▄▄▄▄▌', '▐ ▌', '▐ ▌', '▐ ▌', ' '], - 'B': ['▐▀▀▀▄ ', '▐▄▄▄▀ ', '▐▄▄▄▄ ', '▐ ▌', '▐▄▄▄▀ ', ' ', ' '], - 'C': [' ▄▀▀▀▄', '▐ ', '▐ ', '▐ ', ' ▀▄▄▄▀', ' ', ' '], - ' ': [' ', ' ', ' ', ' ', ' ', ' ', ' '] - } - } - }; - - // Use selected font or fallback to standard - const font = fonts[style] || fonts.standard; - const lines = []; - const upperText = text.toUpperCase(); - - for (let i = 0; i < font.height; i++) { - lines[i] = ''; - } - - for (let char of upperText) { - const charLines = font.chars[char] || font.chars[' ']; - if (charLines) { - for (let i = 0; i < font.height; i++) { - lines[i] += (charLines[i] || ' '.repeat(font.chars[' '][i].length)); - } - } - } - - return lines.join('\n'); -} - -// Create ASCII box/frame -function createASCIIBox(text, style, padding) { - const styles = { - single: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' }, - double: { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' }, - rounded: { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' }, - bold: { tl: '┏', tr: '┓', bl: '┗', br: '┛', h: '━', v: '┃' }, - ascii: { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|' } - }; - - const chars = styles[style] || styles.single; - const lines = text.split('\n'); - const maxLength = Math.max(...lines.map(l => l.length)); - const width = maxLength + (padding * 2); - - let result = ''; - - // Top border - result += chars.tl + chars.h.repeat(width) + chars.tr + '\n'; - - // Padding rows above - for (let i = 0; i < padding - 1; i++) { - result += chars.v + ' '.repeat(width) + chars.v + '\n'; - } - - // Content lines - for (let line of lines) { - const paddedLine = line.padEnd(maxLength, ' '); - result += chars.v + ' '.repeat(padding) + paddedLine + ' '.repeat(padding) + chars.v + '\n'; - } - - // Padding rows below - for (let i = 0; i < padding - 1; i++) { - result += chars.v + ' '.repeat(width) + chars.v + '\n'; - } - - // Bottom border - result += chars.bl + chars.h.repeat(width) + chars.br; - - return result; -} - -// ASCII Art Templates -function getASCIITemplate(templateName) { - const templates = { - 'arrow-right': ` - ┌────────┐ - │ │ -────► NEXT │ - │ │ - └────────┘`, - 'arrow-down': ` - │ - │ - ▼ - ┌──────┐ - │ NEXT │ - └──────┘`, - 'check': ` - ✓ Task completed - ✓ All tests passed - ✓ Ready to deploy - ✓ Documentation updated`, - 'divider': ` -═══════════════════════════════════════════════════════════ -`, - 'header': ` -╔═══════════════════════════════════════════════════╗ -║ SECTION HEADER ║ -╚═══════════════════════════════════════════════════╝`, - 'flowchart': ` -┌─────────────┐ -│ START │ -└──────┬──────┘ - │ - ▼ -┌─────────────┐ -│ PROCESS │ -└──────┬──────┘ - │ - ┌───┴───┐ - │ Check │ - └───┬───┘ - │ - ┌───▼──┬──────┐ - │ Yes │ No │ - ▼ ▼ │ -┌────┐ ┌────┐ │ -│ A │ │ B │ │ -└─┬──┘ └─┬──┘ │ - │ │ │ - └──┬───┘ │ - │◄──────────┘ - ▼ -┌─────────────┐ -│ END │ -└─────────────┘`, - 'decision': ` - ┌─────────┐ - │ Check? │ - └────┬────┘ - │ - ┌─────┴─────┐ - │ │ - Yes│ │No - ▼ ▼ - ┌────────┐ ┌────────┐ - │ True │ │ False │ - └────────┘ └────────┘`, - 'sequence': ` - User System Database - │ │ │ - │ Request │ │ - ├──────────►│ │ - │ │ Query │ - │ ├──────────►│ - │ │ │ - │ │ Result │ - │ │◄──────────┤ - │ Response │ │ - │◄──────────┤ │ - │ │ │`, - 'table-simple': ` -┌──────────┬──────────┬──────────┐ -│ Header 1 │ Header 2 │ Header 3 │ -├──────────┼──────────┼──────────┤ -│ Data 1 │ Data 2 │ Data 3 │ -├──────────┼──────────┼──────────┤ -│ Data 4 │ Data 5 │ Data 6 │ -└──────────┴──────────┴──────────┘`, - 'timeline': ` - 2020 2021 2022 2023 - │ │ │ │ - ●───────────●───────────●───────────● - │ │ │ │ - Start Milestone Release Current`, - 'network': ` - ┌──────────┐ - │ Server │ - └────┬─────┘ - │ - ┌─────────┼─────────┐ - │ │ │ - ┌───▼───┐ ┌──▼────┐ ┌──▼────┐ - │Client1│ │Client2│ │Client3│ - └───────┘ └───────┘ └───────┘`, - 'hierarchy': ` - ┌─────────┐ - │ Root │ - └────┬────┘ - │ - ┌────────┴────────┐ - │ │ -┌───▼───┐ ┌───▼───┐ -│Child 1│ │Child 2│ -└───┬───┘ └───┬───┘ - │ │ -┌───▼───┐ ┌───▼───┐ -│Grand 1│ │Grand 2│ -└───────┘ └───────┘`, - 'process-flow': ` -┌─────────┐ ┌─────────┐ ┌─────────┐ -│ Input │────►│Process 1│────►│ Output │ -└─────────┘ └────┬────┘ └─────────┘ - │ - ┌────▼────┐ - │Process 2│ - └─────────┘`, - 'note-box': ` -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃ NOTE: ┃ -┃ This is an important note ┃ -┃ that requires attention! ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛`, - 'warning-box': ` -╔════════════════════════════════════╗ -║ ⚠ WARNING ║ -║ Proceed with caution! ║ -╚════════════════════════════════════╝`, - 'info-box': ` -╭────────────────────────────────────╮ -│ ℹ Information │ -│ Additional context here │ -╰────────────────────────────────────╯`, - 'separator-fancy': ` -╭──────────────────────────────────────────────────────╮ -│ │ -╰──────────────────────────────────────────────────────╯`, - 'brackets': ` -【 Important Text 】`, - 'banner-stars': ` -************************************************************************ -* IMPORTANT BANNER * -************************************************************************`, - 'progress-bar': ` -Progress: [████████████░░░░░░░░] 60% - 0% 100%` - }; - - return templates[templateName] || ''; -} - -function loadASCIITemplate(templateName) { - const template = getASCIITemplate(templateName); - document.getElementById('ascii-preview').textContent = template; -} - -function insertASCIIArt() { - const asciiArt = document.getElementById('ascii-preview').textContent; - - if (!asciiArt || asciiArt === 'Select a template from the buttons above') { - alert('Please generate ASCII art first'); - return; - } - - // Get active editor - const activeTabId = tabManager ? tabManager.activeTabId : 1; - const editor = document.getElementById(`editor-${activeTabId}`); - - if (!editor) { - alert('No active editor found'); - return; - } - - // Insert in code block for proper rendering - const start = editor.selectionStart; - const before = editor.value.substring(0, start); - const after = editor.value.substring(editor.selectionEnd); - - const codeBlock = '\n```\n' + asciiArt + '\n```\n'; - editor.value = before + codeBlock + after; - - // Update cursor position - editor.selectionStart = editor.selectionEnd = start + codeBlock.length; - editor.focus(); - - // Trigger update - if (tabManager) { - tabManager.handleEditorInput(activeTabId); - } - - // Close dialog - hideASCIIGenerator(); -} - -// ASCII Art Event Listeners -document.getElementById('ascii-dialog-close').addEventListener('click', hideASCIIGenerator); -document.getElementById('ascii-cancel').addEventListener('click', hideASCIIGenerator); -document.getElementById('ascii-generate').addEventListener('click', generateASCIIPreview); -document.getElementById('ascii-insert').addEventListener('click', insertASCIIArt); - -// Mode switching -document.getElementById('ascii-mode-text').addEventListener('click', () => switchASCIIMode('text')); -document.getElementById('ascii-mode-box').addEventListener('click', () => switchASCIIMode('box')); -document.getElementById('ascii-mode-templates').addEventListener('click', () => switchASCIIMode('templates')); - -// Auto-update preview for text mode -document.getElementById('ascii-text-input').addEventListener('input', generateASCIIPreview); -document.getElementById('ascii-font-style').addEventListener('change', generateASCIIPreview); - -// Auto-update preview for box mode -document.getElementById('ascii-box-text').addEventListener('input', generateASCIIPreview); -document.getElementById('ascii-box-style').addEventListener('change', generateASCIIPreview); -document.getElementById('ascii-box-padding').addEventListener('input', generateASCIIPreview); - -// Template buttons -document.querySelectorAll('.ascii-template-btn').forEach(btn => { - btn.addEventListener('click', function() { - const template = this.getAttribute('data-template'); - loadASCIITemplate(template); - }); -}); - -// Close dialog on backdrop click -document.getElementById('ascii-art-dialog').addEventListener('click', (e) => { - if (e.target === document.getElementById('ascii-art-dialog')) { - hideASCIIGenerator(); - } -}); - -// IPC listener for menu -ipcRenderer.on('show-ascii-generator', () => { - showASCIIGenerator(); -}); - -// ============================================================================ -// PANE RESIZING -// ============================================================================ - -let isResizing = false; -let currentResizer = null; - -document.addEventListener('mousedown', (e) => { - if (e.target.classList.contains('pane-resizer')) { - isResizing = true; - currentResizer = e.target; - document.body.style.cursor = 'col-resize'; - document.body.style.userSelect = 'none'; - } -}); - -document.addEventListener('mousemove', (e) => { - if (!isResizing) return; - - const container = currentResizer.parentElement; - const editorPane = currentResizer.previousElementSibling; - const previewPane = currentResizer.nextElementSibling; - - const containerRect = container.getBoundingClientRect(); - const containerWidth = containerRect.width; - const mouseX = e.clientX - containerRect.left; - - // Calculate percentages (minimum 20%, maximum 80% for each pane) - let editorPercentage = (mouseX / containerWidth) * 100; - editorPercentage = Math.max(20, Math.min(80, editorPercentage)); - - const previewPercentage = 100 - editorPercentage; - - editorPane.style.flex = `0 0 ${editorPercentage}%`; - previewPane.style.flex = `0 0 ${previewPercentage}%`; -}); - -document.addEventListener('mouseup', () => { - if (isResizing) { - isResizing = false; - currentResizer = null; - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - } -}); - -// ============================================================================ -// PREVIEW POP-OUT -// ============================================================================ - -let popoutWindow = null; - -function popoutPreview(tabId) { - const previewContent = document.getElementById(`preview-${tabId}`); - - if (!previewContent) { - alert('No preview content to pop out'); - return; - } - - // Close existing popout if open - if (popoutWindow && !popoutWindow.closed) { - popoutWindow.close(); - } - - // Create new window - popoutWindow = window.open('', 'Preview Window', 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no'); - - if (!popoutWindow) { - alert('Pop-up blocked! Please allow pop-ups for this application.'); - return; - } - - // Write content to popout window - const htmlContent = ` - - - - Preview - PanConverter - - - -
- ${previewContent.innerHTML} -
- - - - `; - - popoutWindow.document.write(htmlContent); - popoutWindow.document.close(); - - // Setup auto-update - setupPopoutAutoUpdate(tabId); -} - -function getPreviewStyles() { - // Extract relevant preview styles - return ` - h1, h2, h3, h4, h5, h6 { - margin-top: 24px; - margin-bottom: 16px; - font-weight: 600; - line-height: 1.25; - } - h1 { - font-size: 2em; - border-bottom: 1px solid #eaecef; - padding-bottom: 0.3em; - } - h2 { - font-size: 1.5em; - border-bottom: 1px solid #eaecef; - padding-bottom: 0.3em; - } - p { - margin-bottom: 16px; - } - code { - padding: 0.2em 0.4em; - margin: 0; - font-size: 85%; - background-color: #f6f8fa; - border: 1px solid #d0d7de; - border-radius: 6px; - font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; - } - pre { - padding: 16px; - overflow: auto; - font-size: 85%; - line-height: 1.45; - background-color: #f6f8fa; - border: 1px solid #d0d7de; - border-radius: 6px; - margin-bottom: 16px; - } - pre code { - background: transparent; - border: none; - padding: 0; - } - table { - border-collapse: collapse; - margin-bottom: 16px; - width: 100%; - } - table th, table td { - padding: 6px 13px; - border: 1px solid #dfe2e5; - } - table th { - font-weight: 600; - background-color: #f6f8fa; - } - a { - color: #0366d6; - text-decoration: none; - } - a:hover { - text-decoration: underline; - } - blockquote { - padding: 0 1em; - color: #6a737d; - border-left: 0.25em solid #dfe2e5; - margin-bottom: 16px; - } - img { - max-width: 100%; - height: auto; - } - ul, ol { - padding-left: 2em; - margin-bottom: 16px; - } - li { - margin-bottom: 4px; - } - `; -} - -function setupPopoutAutoUpdate(tabId) { - // Monitor for preview changes and update popout - const observer = new MutationObserver(() => { - if (popoutWindow && !popoutWindow.closed) { - const previewContent = document.getElementById(`preview-${tabId}`); - popoutWindow.postMessage({ - type: 'preview-update', - content: previewContent.innerHTML - }, '*'); - } else { - observer.disconnect(); - } - }); - - const previewElement = document.getElementById(`preview-${tabId}`); - if (previewElement) { - observer.observe(previewElement, { - childList: true, - subtree: true, - characterData: true - }); - } -} - -// Pop-out button event listener -document.addEventListener('click', (e) => { - if (e.target.classList.contains('preview-popout-btn')) { - const tabId = e.target.id.replace('preview-popout-', ''); - popoutPreview(tabId); - } -}); - -// ============================================ -// Insert Content from Generator Windows -// ============================================ -ipcRenderer.on('insert-content', (event, content) => { - if (tabManager) { - const activeTabId = tabManager.activeTabId; - const editor = document.getElementById(`editor-${activeTabId}`); - if (editor) { - const cursorPos = editor.selectionStart; - const textBefore = editor.value.substring(0, cursorPos); - const textAfter = editor.value.substring(cursorPos); - - editor.value = textBefore + content + textAfter; - editor.focus(); - - const newPos = cursorPos + content.length; - editor.setSelectionRange(newPos, newPos); - - // Update state and preview - tabManager.tabs.get(activeTabId).content = editor.value; - tabManager.tabs.get(activeTabId).modified = true; - tabManager.updatePreview(activeTabId); - tabManager.updateTabTitle(activeTabId); - } - } -}); - -// ============================================ -// PDF VIEWER FUNCTIONALITY -// ============================================ - -let pdfDoc = null; -let pdfCurrentPage = 1; -let pdfZoomLevel = 1.0; -let pdfRotation = 0; -let pdfFilePath = null; -let isPdfViewerActive = false; // Track if PDF viewer is currently shown - -// Initialize PDF.js -const pdfjsLib = require('pdfjs-dist'); -pdfjsLib.GlobalWorkerOptions.workerSrc = require.resolve('pdfjs-dist/build/pdf.worker.js'); - -// Open PDF file -async function openPdfFile(filePath) { - // Prevent multiple simultaneous PDF loads - if (isPdfViewerActive && pdfFilePath === filePath) { - console.log('PDF already open:', filePath); - return; - } - - // Close any existing PDF first - if (pdfDoc) { - try { - await pdfDoc.destroy(); - } catch (e) { - console.warn('Error destroying previous PDF:', e); - } - pdfDoc = null; - } - - try { - // Show loading state - document.getElementById('status-text').textContent = 'Loading PDF...'; - - const loadingTask = pdfjsLib.getDocument(filePath); - pdfDoc = await loadingTask.promise; - pdfFilePath = filePath; - pdfCurrentPage = 1; - pdfZoomLevel = 1.0; - pdfRotation = 0; - isPdfViewerActive = true; - - // Update UI - document.getElementById('pdf-total-pages').textContent = pdfDoc.numPages; - document.getElementById('pdf-page-input').value = 1; - document.getElementById('pdf-page-input').max = pdfDoc.numPages; - document.getElementById('pdf-filename').textContent = require('path').basename(filePath); - document.getElementById('pdf-zoom-level').textContent = '100%'; - - // Hide markdown toolbar, tabs, show PDF viewer - document.querySelector('.toolbar').classList.add('hidden'); - document.getElementById('tab-bar').classList.add('hidden'); - document.querySelectorAll('.tab-content').forEach(tc => tc.classList.add('hidden')); - document.getElementById('pdf-viewer-container').classList.remove('hidden'); - - // Render first page - await renderPdfPage(pdfCurrentPage); - - // Update status - document.getElementById('status-text').textContent = `PDF: ${require('path').basename(filePath)} (${pdfDoc.numPages} pages)`; - } catch (error) { - console.error('Error loading PDF:', error); - isPdfViewerActive = false; - pdfDoc = null; - pdfFilePath = null; - document.getElementById('status-text').textContent = 'Error loading PDF'; - alert('Error loading PDF: ' + error.message); - } -} - -// Render PDF page -async function renderPdfPage(pageNum) { - if (!pdfDoc) return; - - try { - const page = await pdfDoc.getPage(pageNum); - const canvas = document.getElementById('pdf-canvas'); - const ctx = canvas.getContext('2d'); - - // Calculate viewport with zoom and rotation - const viewport = page.getViewport({ scale: pdfZoomLevel, rotation: pdfRotation }); - - canvas.width = viewport.width; - canvas.height = viewport.height; - - const renderContext = { - canvasContext: ctx, - viewport: viewport - }; - - await page.render(renderContext).promise; - - // Update page input - document.getElementById('pdf-page-input').value = pageNum; - } catch (error) { - console.error('Error rendering PDF page:', error); - } -} - -// PDF navigation handlers -document.getElementById('pdf-prev-page')?.addEventListener('click', async () => { - if (pdfCurrentPage > 1) { - pdfCurrentPage--; - await renderPdfPage(pdfCurrentPage); - } -}); - -document.getElementById('pdf-next-page')?.addEventListener('click', async () => { - if (pdfDoc && pdfCurrentPage < pdfDoc.numPages) { - pdfCurrentPage++; - await renderPdfPage(pdfCurrentPage); - } -}); - -document.getElementById('pdf-page-input')?.addEventListener('change', async (e) => { - const pageNum = parseInt(e.target.value); - if (pdfDoc && pageNum >= 1 && pageNum <= pdfDoc.numPages) { - pdfCurrentPage = pageNum; - await renderPdfPage(pdfCurrentPage); - } -}); - -// PDF zoom handlers -document.getElementById('pdf-zoom-out')?.addEventListener('click', async () => { - if (pdfZoomLevel > 0.25) { - pdfZoomLevel -= 0.25; - document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%'; - await renderPdfPage(pdfCurrentPage); - } -}); - -document.getElementById('pdf-zoom-in')?.addEventListener('click', async () => { - if (pdfZoomLevel < 4.0) { - pdfZoomLevel += 0.25; - document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%'; - await renderPdfPage(pdfCurrentPage); - } -}); - -document.getElementById('pdf-fit-width')?.addEventListener('click', async () => { - if (!pdfDoc) return; - const page = await pdfDoc.getPage(pdfCurrentPage); - const viewport = page.getViewport({ scale: 1, rotation: pdfRotation }); - const containerWidth = document.getElementById('pdf-viewer').clientWidth - 40; - pdfZoomLevel = containerWidth / viewport.width; - document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%'; - await renderPdfPage(pdfCurrentPage); -}); - -document.getElementById('pdf-fit-page')?.addEventListener('click', async () => { - if (!pdfDoc) return; - const page = await pdfDoc.getPage(pdfCurrentPage); - const viewport = page.getViewport({ scale: 1, rotation: pdfRotation }); - const container = document.getElementById('pdf-viewer'); - const containerWidth = container.clientWidth - 40; - const containerHeight = container.clientHeight - 40; - const scaleX = containerWidth / viewport.width; - const scaleY = containerHeight / viewport.height; - pdfZoomLevel = Math.min(scaleX, scaleY); - document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%'; - await renderPdfPage(pdfCurrentPage); -}); - -// PDF rotation handlers -document.getElementById('pdf-rotate-left')?.addEventListener('click', async () => { - pdfRotation = (pdfRotation - 90 + 360) % 360; - await renderPdfPage(pdfCurrentPage); -}); - -document.getElementById('pdf-rotate-right')?.addEventListener('click', async () => { - pdfRotation = (pdfRotation + 90) % 360; - await renderPdfPage(pdfCurrentPage); -}); - -// Close PDF viewer -document.getElementById('pdf-close')?.addEventListener('click', () => { - closePdfViewer(); -}); - -async function closePdfViewer() { - // Destroy PDF document to free memory - if (pdfDoc) { - try { - await pdfDoc.destroy(); - } catch (e) { - console.warn('Error destroying PDF:', e); - } - } - - pdfDoc = null; - pdfFilePath = null; - isPdfViewerActive = false; - - // Hide PDF viewer - document.getElementById('pdf-viewer-container').classList.add('hidden'); - - // Show markdown tabs, tab bar, and toolbar - document.getElementById('tab-bar').classList.remove('hidden'); - document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('hidden')); - document.querySelector('.toolbar').classList.remove('hidden'); - - // Activate the correct markdown tab - if (tabManager) { - const activeTab = document.querySelector(`.tab-content[data-tab-id="${tabManager.activeTabId}"]`); - if (activeTab) activeTab.classList.add('active'); - // Refresh the active tab - tabManager.updatePreview(tabManager.activeTabId); - } - - document.getElementById('status-text').textContent = 'Ready'; -} - -// Handle PDF file open from main process -ipcRenderer.on('open-pdf-file', (event, filePath) => { - openPdfFile(filePath); -}); - -// ============================================ -// PDF EDITOR TOOLBAR BUTTONS -// ============================================ - -// Helper function to trigger PDF editor dialog -function openPdfEditorDialog(operation) { - // Use the opened PDF file if available, otherwise prompt - if (pdfFilePath) { - ipcRenderer.send('show-pdf-editor-from-toolbar', { operation, filePath: pdfFilePath }); - } else { - ipcRenderer.send('show-pdf-editor-from-toolbar', { operation, filePath: null }); - } -} - -// PDF Editor toolbar button handlers -document.getElementById('pdf-tb-merge')?.addEventListener('click', () => { - openPdfEditorDialog('merge'); -}); - -document.getElementById('pdf-tb-split')?.addEventListener('click', () => { - openPdfEditorDialog('split'); -}); - -document.getElementById('pdf-tb-compress')?.addEventListener('click', () => { - openPdfEditorDialog('compress'); -}); - -document.getElementById('pdf-tb-rotate')?.addEventListener('click', () => { - openPdfEditorDialog('rotate'); -}); - -document.getElementById('pdf-tb-delete')?.addEventListener('click', () => { - openPdfEditorDialog('delete'); -}); - -document.getElementById('pdf-tb-reorder')?.addEventListener('click', () => { - openPdfEditorDialog('reorder'); -}); - -document.getElementById('pdf-tb-watermark')?.addEventListener('click', () => { - openPdfEditorDialog('watermark'); -}); - -document.getElementById('pdf-tb-encrypt')?.addEventListener('click', () => { - openPdfEditorDialog('encrypt'); -}); - -document.getElementById('pdf-tb-decrypt')?.addEventListener('click', () => { - openPdfEditorDialog('decrypt'); -}); - -// ============================================ -// DYNAMIC PANE RESIZER -// ============================================ - -function initPaneResizer(tabId) { - const resizer = document.getElementById(`pane-resizer-${tabId}`); - const editorPane = document.getElementById(`editor-pane-${tabId}`); - const previewPane = document.getElementById(`preview-pane-${tabId}`); - const tabContent = document.getElementById(`tab-content-${tabId}`); - - if (!resizer || !editorPane || !previewPane) return; - - let isResizing = false; - let startX = 0; - let startEditorWidth = 0; - - resizer.addEventListener('mousedown', (e) => { - isResizing = true; - startX = e.clientX; - startEditorWidth = editorPane.offsetWidth; - document.body.style.cursor = 'col-resize'; - document.body.style.userSelect = 'none'; - e.preventDefault(); - }); - - document.addEventListener('mousemove', (e) => { - if (!isResizing) return; - - const containerWidth = tabContent.offsetWidth; - const deltaX = e.clientX - startX; - let newEditorWidth = startEditorWidth + deltaX; - - // Minimum widths - const minWidth = 150; - const maxWidth = containerWidth - minWidth - 6; // 6px for resizer - - newEditorWidth = Math.max(minWidth, Math.min(maxWidth, newEditorWidth)); - - const editorPercent = (newEditorWidth / containerWidth) * 100; - const previewPercent = 100 - editorPercent - 1; // 1% for resizer - - editorPane.style.flex = `0 0 ${editorPercent}%`; - previewPane.style.flex = `0 0 ${previewPercent}%`; - }); - - document.addEventListener('mouseup', () => { - if (isResizing) { - isResizing = false; - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - } - }); -} - -// Initialize resizer for first tab -document.addEventListener('DOMContentLoaded', () => { - initPaneResizer(1); -}); - -// ============================================ -// IMAGE/AUDIO/VIDEO CONVERTER HANDLERS -// These require external tools (ImageMagick, FFmpeg) -// ============================================ - -ipcRenderer.on('show-image-tool', (event, tool) => { - alert(`Image ${tool} tool requires ImageMagick to be installed.\n\nPlease install ImageMagick from: https://imagemagick.org/\n\nThis feature will be available in a future update with built-in support.`); -}); - -ipcRenderer.on('show-audio-tool', (event, tool) => { - alert(`Audio ${tool} tool requires FFmpeg to be installed.\n\nPlease install FFmpeg from: https://ffmpeg.org/\n\nThis feature will be available in a future update with built-in support.`); -}); - -ipcRenderer.on('show-video-tool', (event, tool) => { - alert(`Video ${tool} tool requires FFmpeg to be installed.\n\nPlease install FFmpeg from: https://ffmpeg.org/\n\nThis feature will be available in a future update with built-in support.`); -}); - +/** + * MarkdownConverter Renderer Process + * @version 3.0.0 + */ + +const { ipcRenderer } = require('electron'); +const marked = require('marked'); +const DOMPurify = require('dompurify'); +const hljs = require('highlight.js'); + +// Configure marked +marked.setOptions({ + highlight: function(code, lang) { + if (lang && hljs.getLanguage(lang)) { + try { + return hljs.highlight(code, { language: lang }).value; + } catch (err) { + console.warn('Syntax highlighting failed for language:', lang, err.message); + } + } + return hljs.highlightAuto(code).value; + }, + breaks: true, + gfm: true +}); + +// Tab Management +class TabManager { + constructor() { + this.tabs = new Map(); + this.activeTabId = 1; + this.nextTabId = 2; + this.isPreviewVisible = true; + this.showLineNumbers = false; + this.autoSaveInterval = null; + this.autoSaveDelay = 30000; // 30 seconds + this.recentFiles = JSON.parse(localStorage.getItem('recentFiles') || '[]'); + + // Initialize first tab + this.tabs.set(1, { + id: 1, + title: 'Untitled', + content: '', + filePath: null, + isDirty: false, + undoStack: [], + redoStack: [], + findMatches: [], + currentMatchIndex: -1 + }); + + this.setupEventListeners(); + this.updateUI(); + } + + setupEventListeners() { + // Tab bar events + document.getElementById('new-tab-btn').addEventListener('click', () => this.createNewTab()); + document.getElementById('tab-bar').addEventListener('click', (e) => { + if (e.target.classList.contains('tab-close')) { + e.stopPropagation(); + const tabId = parseInt(e.target.closest('.tab').dataset.tabId); + this.closeTab(tabId); + } else if (e.target.closest('.tab')) { + const tabId = parseInt(e.target.closest('.tab').dataset.tabId); + this.switchToTab(tabId); + } + }); + + // Editor events for active tab + this.setupEditorEvents(); + + // Toolbar events + this.setupToolbarEvents(); + + // Find dialog events + this.setupFindEvents(); + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + if (e.ctrlKey || e.metaKey) { + switch (e.key) { + case 'n': + case 't': + e.preventDefault(); + this.createNewTab(); + break; + case 'w': + if (this.tabs.size > 1) { + e.preventDefault(); + this.closeTab(this.activeTabId); + } + break; + case 'Tab': + if (this.tabs.size > 1) { + e.preventDefault(); + this.switchToNextTab(); + } + break; + } + } + }); + } + + createNewTab() { + const newTabId = this.nextTabId++; + const tab = { + id: newTabId, + title: 'Untitled', + content: '', + filePath: null, + isDirty: false, + undoStack: [], + redoStack: [], + findMatches: [], + currentMatchIndex: -1 + }; + + this.tabs.set(newTabId, tab); + this.createTabElements(tab); + this.switchToTab(newTabId); + this.startAutoSave(); + this.updateTabBar(); + } + + createTabElements(tab) { + // Create tab content container + const tabContent = document.createElement('div'); + tabContent.className = 'tab-content'; + tabContent.id = `tab-content-${tab.id}`; + tabContent.dataset.tabId = tab.id; + + tabContent.innerHTML = ` +
+
+ + +
+
+
+
+
+ +
+
+
+ `; + + document.querySelector('.editor-container').appendChild(tabContent); + + // Directly attach input listener to the new editor + const editor = document.getElementById(`editor-${tab.id}`); + if (editor) { + editor.addEventListener('input', () => { + this.handleEditorInput(tab.id); + }); + + // Add scroll listener for line number sync + editor.addEventListener('scroll', () => { + if (this.showLineNumbers && this.activeTabId === tab.id) { + const lineNumbers = document.getElementById(`line-numbers-${tab.id}`); + if (lineNumbers) { + lineNumbers.scrollTop = editor.scrollTop; + } + } + }); + } + } + + switchToTab(tabId) { + if (!this.tabs.has(tabId)) return; + + // Save current tab state before switching + if (this.activeTabId && this.tabs.has(this.activeTabId)) { + this.saveCurrentTabState(); + } + + this.activeTabId = tabId; + this.updateUI(); + this.restoreTabState(tabId); + this.focusActiveEditor(); + + // Notify main process about current file for exports + const tab = this.tabs.get(tabId); + if (tab?.filePath) { + ipcRenderer.send('set-current-file', tab.filePath); + } + } + + switchToNextTab() { + const tabIds = Array.from(this.tabs.keys()); + const currentIndex = tabIds.indexOf(this.activeTabId); + const nextIndex = (currentIndex + 1) % tabIds.length; + this.switchToTab(tabIds[nextIndex]); + } + + closeTab(tabId) { + if (this.tabs.size === 1) return; // Don't close the last tab + + const tab = this.tabs.get(tabId); + if (tab.isDirty) { + // Show confirmation dialog for unsaved changes + const result = confirm('You have unsaved changes. Do you want to close this tab without saving?'); + if (!result) return; + } + + // Remove tab elements + const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); + const tabContent = document.getElementById(`tab-content-${tabId}`); + + if (tabElement?.classList.contains('tab')) { + tabElement.remove(); + } + if (tabContent) { + tabContent.remove(); + } + + this.tabs.delete(tabId); + + // Switch to another tab if this was active + if (this.activeTabId === tabId) { + const remainingTabs = Array.from(this.tabs.keys()); + this.switchToTab(remainingTabs[0]); + } + + this.updateTabBar(); + } + + updateTabBar() { + const tabBar = document.getElementById('tab-bar'); + const existingTabs = tabBar.querySelectorAll('.tab'); + + // Remove all existing tab elements except the new tab button + existingTabs.forEach(tab => tab.remove()); + + // Add tabs in order + const sortedTabs = Array.from(this.tabs.values()).sort((a, b) => a.id - b.id); + const newTabBtn = document.getElementById('new-tab-btn'); + + sortedTabs.forEach(tab => { + const tabElement = document.createElement('div'); + tabElement.className = `tab ${tab.id === this.activeTabId ? 'active' : ''}`; + tabElement.dataset.tabId = tab.id; + + const title = tab.filePath ? + tab.filePath.split('/').pop() : + tab.title; + + const dirtyIndicator = tab.isDirty ? ' •' : ''; + + tabElement.innerHTML = ` + ${title}${dirtyIndicator} + + `; + + tabBar.insertBefore(tabElement, newTabBtn); + }); + } + + updateUI() { + // Show/hide tab contents + document.querySelectorAll('.tab-content').forEach(content => { + content.classList.remove('active'); + }); + + const activeContent = document.getElementById(`tab-content-${this.activeTabId}`); + if (activeContent) { + activeContent.classList.add('active'); + } + + // Update preview visibility + this.updatePreviewVisibility(); + this.updateLineNumbers(); + this.updateTabBar(); + } + + saveCurrentTabState() { + const tab = this.tabs.get(this.activeTabId); + if (!tab) return; + + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (editor) { + tab.content = editor.value; + tab.isDirty = tab.content !== (tab.originalContent || ''); + } + } + + restoreTabState(tabId) { + const tab = this.tabs.get(tabId); + if (!tab) return; + + const editor = document.getElementById(`editor-${tabId}`); + + if (editor) { + editor.value = tab.content; + this.updatePreview(tabId); + this.updateWordCount(); + } + } + + focusActiveEditor() { + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (editor) { + editor.focus(); + } + } + + updatePreview(tabId = this.activeTabId) { + const tab = this.tabs.get(tabId); + const preview = document.getElementById(`preview-${tabId}`); + + if (!tab || !preview) return; + + try { + // Check if libraries are available + if (!marked || !DOMPurify) { + preview.innerHTML = '

Error: Required libraries (marked/DOMPurify) not loaded. Check internet connection.

'; + return; + } + const html = marked.parse(tab.content); + const sanitizedHtml = DOMPurify.sanitize(html); + preview.innerHTML = sanitizedHtml; + + // Render math expressions if KaTeX is available + if (window.katex && window.renderMathInElement) { + try { + window.renderMathInElement(preview, { + delimiters: [ + {left: '$$', right: '$$', display: true}, + {left: '$', right: '$', display: false}, + {left: '\\[', right: '\\]', display: true}, + {left: '\\(', right: '\\)', display: false} + ] + }); + } catch (mathError) { + console.warn('Math rendering error:', mathError); + } + } + + // Render Mermaid diagrams if Mermaid is available + if (window.mermaid) { + try { + // Find all code blocks with language-mermaid class + const mermaidBlocks = preview.querySelectorAll('pre code.language-mermaid'); + mermaidBlocks.forEach((block, index) => { + const code = block.textContent; + const pre = block.parentElement; + + // Create a div for mermaid rendering + const mermaidDiv = document.createElement('div'); + mermaidDiv.className = 'mermaid'; + mermaidDiv.setAttribute('data-processed', 'true'); + mermaidDiv.textContent = code; + + // Replace the pre element with the mermaid div + pre.parentElement.replaceChild(mermaidDiv, pre); + }); + + // Initialize Mermaid with dark theme support + const theme = document.body.className.includes('theme-dark') ? 'dark' : 'default'; + mermaid.initialize({ + startOnLoad: false, + theme: theme, + securityLevel: 'loose' + }); + + // Render all mermaid diagrams + mermaid.run({ + querySelector: '.mermaid:not([data-rendered])' + }).then(() => { + // Mark as rendered + preview.querySelectorAll('.mermaid').forEach(el => { + el.setAttribute('data-rendered', 'true'); + }); + }); + } catch (mermaidError) { + console.warn('Mermaid rendering error:', mermaidError); + } + } + } catch (error) { + console.error('Error rendering preview:', error); + preview.innerHTML = '

Error rendering preview. Please check your markdown syntax.

'; + } + } + + updatePreviewVisibility() { + document.querySelectorAll('.tab-content').forEach(content => { + const previewPane = content.querySelector('.pane:last-child'); + const editorPane = content.querySelector('.pane:first-child'); + + if (this.isPreviewVisible) { + previewPane.classList.remove('hidden'); + editorPane.classList.remove('full-width'); + } else { + previewPane.classList.add('hidden'); + editorPane.classList.add('full-width'); + } + }); + } + + updateLineNumbers() { + const editor = document.getElementById(`editor-${this.activeTabId}`); + const lineNumbers = document.getElementById(`line-numbers-${this.activeTabId}`); + + if (!editor || !lineNumbers) return; + + if (this.showLineNumbers) { + const lines = editor.value.split('\n'); + lineNumbers.innerHTML = lines.map((_, i) => + `
${i + 1}
` + ).join(''); + lineNumbers.classList.remove('hidden'); + + // Sync scroll position + lineNumbers.scrollTop = editor.scrollTop; + } else { + lineNumbers.classList.add('hidden'); + } + } + + updateWordCount() { + const tab = this.tabs.get(this.activeTabId); + if (!tab) return; + + const content = tab.content; + const words = content.trim() ? content.trim().split(/\s+/).filter(word => word.length > 0).length : 0; + const chars = content.length; + const charsNoSpaces = content.replace(/\s/g, '').length; + + // Enhanced statistics + const lines = content.split('\n').length; + const paragraphs = content.split(/\n\s*\n/).filter(p => p.trim()).length; + const readingTime = Math.ceil(words / 200); // Average reading speed: 200 words/minute + const sentences = content.split(/[.!?]+/).filter(s => s.trim()).length; + + // Update the word count display with enhanced stats + const basicStats = `Words: ${words} | Characters: ${chars} (${charsNoSpaces} no spaces)`; + const enhancedStats = `Lines: ${lines} | Paragraphs: ${paragraphs} | Sentences: ${sentences} | Reading time: ${readingTime} min`; + + document.getElementById('word-count').textContent = basicStats; + + // Add enhanced stats to a separate element + let enhancedEl = document.getElementById('enhanced-stats'); + if (!enhancedEl) { + enhancedEl = document.createElement('div'); + enhancedEl.id = 'enhanced-stats'; + enhancedEl.className = 'enhanced-stats'; + document.querySelector('.status-bar').appendChild(enhancedEl); + } + enhancedEl.textContent = enhancedStats; + } + + setupEditorEvents() { + // Set up editor events using event delegation on the container + const editorContainer = document.querySelector('.editor-container'); + if (editorContainer) { + editorContainer.addEventListener('input', (e) => { + if (e.target.classList.contains('editor-textarea')) { + const tabId = parseInt(e.target.id.split('-')[1]); + this.handleEditorInput(tabId); + } + }); + + editorContainer.addEventListener('scroll', (e) => { + if (e.target.classList.contains('editor-textarea')) { + this.updateLineNumbers(); + } + }, true); + } + } + + handleEditorInput(tabId) { + const tab = this.tabs.get(tabId); + if (!tab) return; + + const editor = document.getElementById(`editor-${tabId}`); + if (!editor) return; + + tab.content = editor.value; + tab.isDirty = true; + + this.updatePreview(tabId); + this.updateWordCount(); + this.updateLineNumbers(); + this.updateTabBar(); + + // Add to undo stack + this.pushUndoState(tabId); + } + + pushUndoState(tabId) { + const tab = this.tabs.get(tabId); + if (!tab) return; + + tab.undoStack.push(tab.content); + if (tab.undoStack.length > 50) { + tab.undoStack.shift(); + } + tab.redoStack = []; + } + + undo() { + const tab = this.tabs.get(this.activeTabId); + if (!tab || tab.undoStack.length === 0) return; + + tab.redoStack.push(tab.content); + tab.content = tab.undoStack.pop(); + + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (editor) { + editor.value = tab.content; + this.updatePreview(); + this.updateWordCount(); + } + } + + redo() { + const tab = this.tabs.get(this.activeTabId); + if (!tab || tab.redoStack.length === 0) return; + + tab.undoStack.push(tab.content); + tab.content = tab.redoStack.pop(); + + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (editor) { + editor.value = tab.content; + this.updatePreview(); + this.updateWordCount(); + } + } + + // Auto-save functionality + startAutoSave() { + if (this.autoSaveInterval) { + clearInterval(this.autoSaveInterval); + } + + this.autoSaveInterval = setInterval(() => { + this.performAutoSave(); + }, this.autoSaveDelay); + } + + stopAutoSave() { + if (this.autoSaveInterval) { + clearInterval(this.autoSaveInterval); + this.autoSaveInterval = null; + } + } + + performAutoSave() { + const tab = this.tabs.get(this.activeTabId); + if (!tab || !tab.filePath || !tab.content) return; + + // Only auto-save if content has changed since last save + if (tab.lastSavedContent !== tab.content) { + ipcRenderer.send('save-file', { path: tab.filePath, content: tab.content }); + tab.lastSavedContent = tab.content; + + // Show brief auto-save indicator + this.showAutoSaveIndicator(); + } + } + + showAutoSaveIndicator() { + const indicator = document.createElement('div'); + indicator.textContent = 'Auto-saved'; + indicator.className = 'auto-save-indicator'; + document.body.appendChild(indicator); + + setTimeout(() => { + indicator.classList.add('fade-out'); + setTimeout(() => { + if (indicator.parentNode) { + indicator.parentNode.removeChild(indicator); + } + }, 300); + }, 1500); + } + + // Recent files functionality + addToRecentFiles(filePath) { + if (!filePath) return; + + // Remove if already exists + this.recentFiles = this.recentFiles.filter(f => f !== filePath); + + // Add to beginning + this.recentFiles.unshift(filePath); + + // Keep only last 10 files + this.recentFiles = this.recentFiles.slice(0, 10); + + // Save to localStorage and sync with main process + localStorage.setItem('recentFiles', JSON.stringify(this.recentFiles)); + ipcRenderer.send('save-recent-files', this.recentFiles); + } + + getRecentFiles() { + return this.recentFiles.filter(file => { + // Check if file still exists (basic check by trying to access it) + try { + return file && file.length > 0; + } catch (e) { + return false; + } + }); + } + + setupToolbarEvents() { + // Bold + document.getElementById('btn-bold').addEventListener('click', () => { + this.wrapSelection('**', '**'); + }); + + // Italic + document.getElementById('btn-italic').addEventListener('click', () => { + this.wrapSelection('*', '*'); + }); + + // Heading + document.getElementById('btn-heading').addEventListener('click', () => { + this.insertAtLineStart('## '); + }); + + // Link + document.getElementById('btn-link').addEventListener('click', () => { + this.wrapSelection('[', '](url)'); + }); + + // Code + document.getElementById('btn-code').addEventListener('click', () => { + this.wrapSelection('`', '`'); + }); + + // List + document.getElementById('btn-list').addEventListener('click', () => { + this.insertAtLineStart('- '); + }); + + // Quote + document.getElementById('btn-quote').addEventListener('click', () => { + this.insertAtLineStart('> '); + }); + + // Table + document.getElementById('btn-table').addEventListener('click', () => { + this.insertTable(); + }); + + // Strikethrough + document.getElementById('btn-strikethrough').addEventListener('click', () => { + this.wrapSelection('~~', '~~'); + }); + + // Code Block + document.getElementById('btn-code-block').addEventListener('click', () => { + this.insertCodeBlock(); + }); + + // Horizontal Rule + document.getElementById('btn-horizontal-rule').addEventListener('click', () => { + this.insertHorizontalRule(); + }); + + // Preview toggle + document.getElementById('btn-preview-toggle').addEventListener('click', () => { + this.isPreviewVisible = !this.isPreviewVisible; + this.updatePreviewVisibility(); + }); + + // Line numbers + document.getElementById('btn-line-numbers').addEventListener('click', () => { + this.showLineNumbers = !this.showLineNumbers; + this.updateLineNumbers(); + }); + } + + // Helper function to wrap selected text + wrapSelection(before, after) { + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (!editor) return; + + const start = editor.selectionStart; + const end = editor.selectionEnd; + const selectedText = editor.value.substring(start, end); + const replacement = before + (selectedText || 'text') + after; + + editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end); + + // Update cursor position + const newCursorPos = selectedText ? start + replacement.length : start + before.length; + editor.selectionStart = editor.selectionEnd = newCursorPos; + editor.focus(); + + // Trigger update + this.handleEditorInput(this.activeTabId); + } + + // Helper function to insert text at the start of current line + insertAtLineStart(prefix) { + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (!editor) return; + + const start = editor.selectionStart; + const text = editor.value; + + // Find the start of the current line + let lineStart = text.lastIndexOf('\n', start - 1) + 1; + + // Insert the prefix + editor.value = text.substring(0, lineStart) + prefix + text.substring(lineStart); + + // Update cursor position + editor.selectionStart = editor.selectionEnd = start + prefix.length; + editor.focus(); + + // Trigger update + this.handleEditorInput(this.activeTabId); + } + + // Insert a markdown table + insertTable() { + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (!editor) return; + + const table = '\n| Column 1 | Column 2 | Column 3 |\n' + + '|----------|----------|----------|\n' + + '| Cell 1 | Cell 2 | Cell 3 |\n' + + '| Cell 4 | Cell 5 | Cell 6 |\n'; + + const start = editor.selectionStart; + editor.value = editor.value.substring(0, start) + table + editor.value.substring(start); + + // Update cursor position + editor.selectionStart = editor.selectionEnd = start + table.length; + editor.focus(); + + // Trigger update + this.handleEditorInput(this.activeTabId); + } + + // Insert a code block + insertCodeBlock() { + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (!editor) return; + + const selectedText = editor.value.substring(editor.selectionStart, editor.selectionEnd); + const codeBlock = '\n```\n' + (selectedText || 'code here') + '\n```\n'; + + const start = editor.selectionStart; + editor.value = editor.value.substring(0, start) + codeBlock + editor.value.substring(editor.selectionEnd); + + // Update cursor position + editor.selectionStart = editor.selectionEnd = start + codeBlock.length; + editor.focus(); + + // Trigger update + this.handleEditorInput(this.activeTabId); + } + + // Insert a horizontal rule + insertHorizontalRule() { + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (!editor) return; + + const hr = '\n\n---\n\n'; + const start = editor.selectionStart; + + editor.value = editor.value.substring(0, start) + hr + editor.value.substring(start); + + // Update cursor position + editor.selectionStart = editor.selectionEnd = start + hr.length; + editor.focus(); + + // Trigger update + this.handleEditorInput(this.activeTabId); + } + + setupFindEvents() { + const btnFind = document.getElementById('btn-find'); + const btnFindClose = document.getElementById('btn-find-close'); + const findInput = document.getElementById('find-input'); + const btnFindNext = document.getElementById('btn-find-next'); + const btnFindPrev = document.getElementById('btn-find-prev'); + const btnReplace = document.getElementById('btn-replace'); + const btnReplaceAll = document.getElementById('btn-replace-all'); + + if (!btnFind || !btnFindClose || !findInput || !btnFindNext || !btnFindPrev || !btnReplace || !btnReplaceAll) { + console.error('Find dialog elements not found'); + return; + } + + // Show find dialog + btnFind.addEventListener('click', () => { + document.getElementById('find-dialog').classList.remove('hidden'); + findInput.focus(); + }); + + // Close find dialog + btnFindClose.addEventListener('click', () => { + document.getElementById('find-dialog').classList.add('hidden'); + this.clearFindHighlights(); + }); + + // Find input change - update matches + findInput.addEventListener('input', () => { + this.performFind(); + }); + + // Find next + btnFindNext.addEventListener('click', () => { + this.findNext(); + }); + + // Find previous + btnFindPrev.addEventListener('click', () => { + this.findPrevious(); + }); + + // Replace + btnReplace.addEventListener('click', () => { + this.replaceOne(); + }); + + // Replace all + btnReplaceAll.addEventListener('click', () => { + this.replaceAll(); + }); + + // Enter key in find input - find next + findInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + if (e.shiftKey) { + this.findPrevious(); + } else { + this.findNext(); + } + } + }); + + } + + performFind() { + const findText = document.getElementById('find-input').value; + const tab = this.tabs.get(this.activeTabId); + const editor = document.getElementById(`editor-${this.activeTabId}`); + + if (!findText || !tab || !editor) { + this.clearFindHighlights(); + const findCount = document.getElementById('find-count'); + if (findCount) { + findCount.textContent = '0 matches'; + } + return; + } + + const content = editor.value; + const matches = []; + let index = 0; + + // Find all matches + while ((index = content.indexOf(findText, index)) !== -1) { + matches.push(index); + index += findText.length; + } + + tab.findMatches = matches; + tab.currentMatchIndex = matches.length > 0 ? 0 : -1; + + // Update match count + const findCount = document.getElementById('find-count'); + if (findCount) { + findCount.textContent = `${matches.length} match${matches.length !== 1 ? 'es' : ''}`; + } + + // Highlight first match + if (matches.length > 0) { + this.highlightMatch(0); + } + } + + findNext() { + const tab = this.tabs.get(this.activeTabId); + if (!tab || tab.findMatches.length === 0) return; + + tab.currentMatchIndex = (tab.currentMatchIndex + 1) % tab.findMatches.length; + this.highlightMatch(tab.currentMatchIndex); + } + + findPrevious() { + const tab = this.tabs.get(this.activeTabId); + if (!tab || tab.findMatches.length === 0) return; + + tab.currentMatchIndex = tab.currentMatchIndex - 1; + if (tab.currentMatchIndex < 0) { + tab.currentMatchIndex = tab.findMatches.length - 1; + } + this.highlightMatch(tab.currentMatchIndex); + } + + highlightMatch(matchIndex) { + const tab = this.tabs.get(this.activeTabId); + const editor = document.getElementById(`editor-${this.activeTabId}`); + const findText = document.getElementById('find-input').value; + + if (!tab || !editor || matchIndex < 0 || matchIndex >= tab.findMatches.length) return; + + const position = tab.findMatches[matchIndex]; + + // Select the match WITHOUT focusing (to keep focus on find input) + editor.setSelectionRange(position, position + findText.length); + + // Make the selection visible by briefly focusing and then restoring focus + const findInput = document.getElementById('find-input'); + const hadFocus = document.activeElement === findInput; + + // Temporarily focus editor to make selection visible + editor.focus(); + + // Restore focus to find input if it had focus + if (hadFocus) { + setTimeout(() => { + findInput.focus(); + // Restore cursor position in find input + findInput.setSelectionRange(findInput.value.length, findInput.value.length); + }, 10); + } + + // Scroll into view + const lineHeight = 20; // Approximate line height + const charPosition = position; + const numLines = editor.value.substring(0, charPosition).split('\n').length; + const scrollPosition = (numLines - 5) * lineHeight; // Show match 5 lines from top + + editor.scrollTop = Math.max(0, scrollPosition); + + // Update match counter + const findCount = document.getElementById('find-count'); + if (findCount) { + findCount.textContent = `Match ${matchIndex + 1} of ${tab.findMatches.length}`; + } + } + + replaceOne() { + const tab = this.tabs.get(this.activeTabId); + const editor = document.getElementById(`editor-${this.activeTabId}`); + const findText = document.getElementById('find-input').value; + const replaceText = document.getElementById('replace-input').value; + + if (!tab || !editor || tab.findMatches.length === 0 || tab.currentMatchIndex < 0) return; + + const position = tab.findMatches[tab.currentMatchIndex]; + const before = editor.value.substring(0, position); + const after = editor.value.substring(position + findText.length); + + editor.value = before + replaceText + after; + tab.content = editor.value; + tab.isDirty = true; + + this.updatePreview(this.activeTabId); + this.updateWordCount(); + this.updateTabBar(); + + // Re-perform find to update matches + this.performFind(); + } + + replaceAll() { + const tab = this.tabs.get(this.activeTabId); + const editor = document.getElementById(`editor-${this.activeTabId}`); + const findText = document.getElementById('find-input').value; + const replaceText = document.getElementById('replace-input').value; + + if (!tab || !editor || !findText) return; + + // Simple replace all + const newContent = editor.value.split(findText).join(replaceText); + const replacedCount = tab.findMatches.length; + + editor.value = newContent; + tab.content = newContent; + tab.isDirty = true; + + this.updatePreview(this.activeTabId); + this.updateWordCount(); + this.updateTabBar(); + + // Update match count + document.getElementById('find-count').textContent = `Replaced ${replacedCount} match${replacedCount !== 1 ? 'es' : ''}`; + + // Re-perform find + this.performFind(); + } + + clearFindHighlights() { + const tab = this.tabs.get(this.activeTabId); + if (tab) { + tab.findMatches = []; + tab.currentMatchIndex = -1; + } + } + + // File operations + openFile(filePath, content) { + console.log('openFile called with:', filePath, 'content length:', content.length); + let tab = this.tabs.get(this.activeTabId); + + // Handle both forward and back slashes for cross-platform compatibility + const fileName = filePath.split(/[\\/]/).pop(); + + // If current tab is empty and untitled, reuse it + if (!tab.filePath && !tab.isDirty && tab.content === '') { + console.log('Reusing current tab'); + tab.filePath = filePath; + tab.title = fileName; + tab.content = content; + tab.originalContent = content; + tab.isDirty = false; + + // Update the editor and preview + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (editor) { + editor.value = content; + // Update preview after editor is updated + this.updatePreview(this.activeTabId); + this.updateWordCount(); + } + } else { + // Create new tab for the file + console.log('Creating new tab for file'); + this.createNewTab(); + tab = this.tabs.get(this.activeTabId); + tab.filePath = filePath; + tab.title = fileName; + tab.content = content; + tab.originalContent = content; + tab.isDirty = false; + + // Wait a moment for the DOM to update, then set content + setTimeout(() => { + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (editor) { + editor.value = content; + this.updatePreview(this.activeTabId); + this.updateWordCount(); + } + }, 50); + } + this.startAutoSave(); + this.addToRecentFiles(filePath); + this.updateTabBar(); + + // Notify main process about current file for exports + ipcRenderer.send('set-current-file', filePath); + + console.log('File opened successfully'); + } + + getCurrentContent() { + const tab = this.tabs.get(this.activeTabId); + return tab ? tab.content : ''; + } + + getCurrentFilePath() { + const tab = this.tabs.get(this.activeTabId); + return tab ? tab.filePath : null; + } +} + +// Initialize tab manager +let tabManager; + +document.addEventListener('DOMContentLoaded', () => { + tabManager = new TabManager(); + + // Attach input listener to the initial editor (tab 1) + const initialEditor = document.getElementById('editor-1'); + if (initialEditor) { + initialEditor.addEventListener('input', () => { + tabManager.handleEditorInput(1); + }); + + // Add scroll listener for line number sync + initialEditor.addEventListener('scroll', () => { + if (tabManager.showLineNumbers && tabManager.activeTabId === 1) { + const lineNumbers = document.getElementById('line-numbers-1'); + if (lineNumbers) { + lineNumbers.scrollTop = initialEditor.scrollTop; + } + } + }); + } + + // Request current theme + ipcRenderer.send('get-theme'); + + // Also send renderer-ready immediately as backup + // This ensures we don't get stuck waiting for theme-changed + setTimeout(() => { + console.log('Backup renderer-ready timeout triggered'); + ipcRenderer.send('renderer-ready'); + }, 100); + + // Set up auto-save interval + setInterval(() => { + // Auto-save logic for all tabs + tabManager.tabs.forEach(tab => { + if (tab.isDirty && tab.filePath) { + ipcRenderer.send('save-current-file', tab.content); + } + }); + }, 30000); +}); + +// IPC event listeners +ipcRenderer.on('file-new', () => { + tabManager.createNewTab(); +}); + +ipcRenderer.on('file-opened', (event, data) => { + console.log('[RENDERER] file-opened received:', data.path, 'content length:', data.content.length); + if (tabManager) { + tabManager.openFile(data.path, data.content); + } else { + console.error('[RENDERER] tabManager not initialized!'); + } +}); + +ipcRenderer.on('file-save', () => { + const currentContent = tabManager.getCurrentContent(); + const currentFilePath = tabManager.getCurrentFilePath(); + if (currentFilePath) { + ipcRenderer.send('save-current-file', currentContent); + } +}); + +ipcRenderer.on('get-content-for-save', (event, filePath) => { + const currentContent = tabManager.getCurrentContent(); + ipcRenderer.send('save-file', { path: filePath, content: currentContent }); +}); + +ipcRenderer.on('get-content-for-spreadsheet', (event, format) => { + const currentContent = tabManager.getCurrentContent(); + ipcRenderer.send('export-spreadsheet', { content: currentContent, format }); +}); + +ipcRenderer.on('toggle-preview', () => { + tabManager.isPreviewVisible = !tabManager.isPreviewVisible; + tabManager.updatePreviewVisibility(); +}); + +ipcRenderer.on('toggle-find', () => { + const findDialog = document.getElementById('find-dialog'); + if (findDialog.classList.contains('hidden')) { + findDialog.classList.remove('hidden'); + document.getElementById('find-input').focus(); + } else { + findDialog.classList.add('hidden'); + } +}); + +ipcRenderer.on('theme-changed', (event, theme) => { + console.log('[RENDERER] Theme changed to:', theme); + document.body.className = `theme-${theme}`; + + // After theme is applied, wait for next frame then signal renderer is ready + // This ensures complete UI initialization before files are opened + requestAnimationFrame(() => { + console.log('[RENDERER] Sending renderer-ready from theme-changed'); + ipcRenderer.send('renderer-ready'); + }); +}); + +// Undo/Redo handlers +ipcRenderer.on('undo', () => { + if (tabManager) { + tabManager.undo(); + } +}); + +ipcRenderer.on('redo', () => { + if (tabManager) { + tabManager.redo(); + } +}); + +// Font size adjustment +let currentFontSize = parseInt(localStorage.getItem('fontSize')) || 15; + +function updateFontSizes(size) { + const editors = document.querySelectorAll('#editor, .editor-textarea'); + const previews = document.querySelectorAll('#preview, .preview-content'); + + editors.forEach(editor => { + editor.style.fontSize = `${size}px`; + }); + + previews.forEach(preview => { + preview.style.fontSize = `${size}px`; + }); + + localStorage.setItem('fontSize', size); +} + +// Apply saved font size on load +updateFontSizes(currentFontSize); + +ipcRenderer.on('adjust-font-size', (event, action) => { + if (action === 'increase' && currentFontSize < 24) { + currentFontSize++; + } else if (action === 'decrease' && currentFontSize > 10) { + currentFontSize--; + } else if (action === 'reset') { + currentFontSize = 15; + } + updateFontSizes(currentFontSize); +}); + +// Print preview request handlers - handle printing directly +ipcRenderer.on('print-preview', () => { + console.log('[RENDERER] print-preview received'); + handlePrintPreview(false); +}); + +ipcRenderer.on('print-preview-styled', () => { + console.log('[RENDERER] print-preview-styled received'); + handlePrintPreview(true); +}); + +function handlePrintPreview(withStyles) { + const activeTabId = tabManager ? tabManager.activeTabId : 1; + const previewContent = document.getElementById(`preview-${activeTabId}`); + + if (!previewContent || !previewContent.innerHTML.trim()) { + alert('Nothing to print. Please create or open a document and ensure the preview is visible.'); + return; + } + + console.log('[RENDERER] Starting print with withStyles:', withStyles); + console.log('[RENDERER] Preview content length:', previewContent.innerHTML.length); + + // Add body classes for print mode - let CSS handle everything + document.body.classList.add('printing'); + if (!withStyles) { + document.body.classList.add('printing-no-styles'); + } + + // Wait for CSS to apply then print + setTimeout(() => { + console.log('[RENDERER] Sending do-print to main process'); + ipcRenderer.send('do-print', { withStyles }); + + // Restore classes after print dialog opens + setTimeout(() => { + document.body.classList.remove('printing', 'printing-no-styles'); + console.log('[RENDERER] Print classes removed'); + }, 1000); + }, 100); +} + +// Export Dialog functionality +let currentExportFormat = null; + +ipcRenderer.on('show-export-dialog', (event, format) => { + currentExportFormat = format; + showExportDialog(format); +}); + +function showExportDialog(format) { + console.log('showExportDialog called with format:', format); + const dialog = document.getElementById('export-dialog'); + const title = document.getElementById('export-dialog-title'); + + if (!dialog) { + console.error('Export dialog element not found!'); + return; + } + + console.log('Dialog found, showing export options for:', format); + title.textContent = `Export as ${format.toUpperCase()}`; + dialog.setAttribute('data-format', format); + dialog.classList.remove('hidden'); + + // Initialize form values + initializeExportForm(format); + console.log('Export dialog should now be visible'); +} + +function hideExportDialog() { + const dialog = document.getElementById('export-dialog'); + dialog.classList.add('hidden'); + currentExportFormat = null; +} + +function initializeExportForm(format) { + // Reset advanced export toggle to unchecked + const advancedToggle = document.getElementById('advanced-export-toggle'); + const advancedOptions = document.getElementById('advanced-export-options'); + + advancedToggle.checked = false; + advancedOptions.classList.add('hidden'); + + // Reset form to defaults + document.getElementById('export-template').value = 'default'; + document.getElementById('custom-template-path').style.display = 'none'; + + // Clear metadata fields + const metadataFields = document.querySelectorAll('.metadata-field'); + metadataFields.forEach((field, index) => { + if (index < 4) { // Keep first 4 default fields + field.querySelector('.metadata-key').value = ['title', 'author', 'date', 'subject'][index] || ''; + field.querySelector('.metadata-value').value = ''; + } else { + field.remove(); // Remove additional fields + } + }); + + // Reset checkboxes and other fields + document.getElementById('export-toc').checked = false; + document.getElementById('export-number-sections').checked = false; + document.getElementById('export-citeproc').checked = false; + document.getElementById('export-toc-depth').value = 3; + + // PDF-specific fields + if (format === 'pdf') { + document.getElementById('pdf-engine').value = 'xelatex'; + document.getElementById('pdf-geometry').value = 'margin=1in'; + document.getElementById('custom-geometry').style.display = 'none'; + } + + // Clear bibliography fields + document.getElementById('bibliography-file').value = ''; + document.getElementById('csl-file').value = ''; + + // Request current page settings from main process and apply them + ipcRenderer.send('get-page-settings'); +} + +function collectExportOptions() { + const advancedMode = document.getElementById('advanced-export-toggle').checked; + const options = {}; + + if (advancedMode) { + // Collect advanced options + options.template = document.getElementById('export-template').value; + options.metadata = {}; + options.variables = {}; + options.toc = document.getElementById('export-toc').checked; + options.tocDepth = document.getElementById('export-toc-depth').value; + options.numberSections = document.getElementById('export-number-sections').checked; + options.citeproc = document.getElementById('export-citeproc').checked; + } else { + // Collect basic options only + options.template = 'default'; + options.metadata = {}; + options.variables = {}; + options.toc = document.getElementById('basic-toc').checked; + options.tocDepth = 3; + options.numberSections = document.getElementById('basic-number-sections').checked; + options.citeproc = false; + } + + if (advancedMode) { + // Collect custom template path + if (options.template === 'custom') { + options.template = document.getElementById('custom-template-path').value.trim(); + } + + // Collect metadata + const metadataFields = document.querySelectorAll('.metadata-field'); + metadataFields.forEach(field => { + const key = field.querySelector('.metadata-key').value.trim(); + const value = field.querySelector('.metadata-value').value.trim(); + if (key && value) { + options.metadata[key] = value; + } + }); + + // PDF-specific options + if (currentExportFormat === 'pdf') { + options.pdfEngine = document.getElementById('pdf-engine').value; + const geometrySelect = document.getElementById('pdf-geometry'); + if (geometrySelect.value === 'custom') { + options.geometry = document.getElementById('custom-geometry').value.trim() || 'margin=1in'; + } else { + options.geometry = geometrySelect.value; + } + } + + // Bibliography + const bibFile = document.getElementById('bibliography-file').value.trim(); + const cslFile = document.getElementById('csl-file').value.trim(); + if (bibFile) options.bibliography = bibFile; + if (cslFile) options.csl = cslFile; + } else { + // Basic mode - set default PDF options if needed + if (currentExportFormat === 'pdf') { + options.pdfEngine = 'xelatex'; + options.geometry = 'margin=1in'; + } + } + + // Collect page size and orientation (always collected, from basic options) + const pageSize = document.getElementById('page-size').value; + const pageOrientation = document.getElementById('page-orientation').value; + const customWidth = document.getElementById('custom-width').value.trim(); + const customHeight = document.getElementById('custom-height').value.trim(); + + // Send page settings to main process + ipcRenderer.send('update-page-settings', { + size: pageSize, + orientation: pageOrientation, + customWidth: customWidth || null, + customHeight: customHeight || null + }); + + return options; +} + +// Export Profiles Management +let exportProfiles = {}; + +function loadExportProfiles() { + const saved = localStorage.getItem('exportProfiles'); + if (saved) { + try { + exportProfiles = JSON.parse(saved); + populateProfileDropdown(); + } catch (e) { + console.error('Failed to load export profiles:', e); + exportProfiles = {}; + } + } +} + +function saveExportProfiles() { + localStorage.setItem('exportProfiles', JSON.stringify(exportProfiles)); +} + +function populateProfileDropdown() { + const select = document.getElementById('export-profile-select'); + if (!select) return; + + // Clear existing options except the first one + while (select.options.length > 1) { + select.remove(1); + } + + // Add saved profiles + Object.keys(exportProfiles).forEach(name => { + const option = document.createElement('option'); + option.value = name; + option.textContent = name; + select.appendChild(option); + }); +} + +function saveCurrentProfile() { + const name = prompt('Enter a name for this export profile:', 'My Profile'); + if (!name || name.trim() === '') return; + + const profileName = name.trim(); + + // Collect current settings + const profile = { + format: currentExportFormat, + advancedMode: document.getElementById('advanced-export-toggle').checked, + pageSize: document.getElementById('page-size').value, + pageOrientation: document.getElementById('page-orientation').value, + basicToc: document.getElementById('basic-toc').checked, + basicNumberSections: document.getElementById('basic-number-sections').checked + }; + + // Add advanced options if enabled + if (profile.advancedMode) { + profile.template = document.getElementById('export-template').value; + profile.toc = document.getElementById('export-toc').checked; + profile.tocDepth = document.getElementById('export-toc-depth').value; + profile.numberSections = document.getElementById('export-number-sections').checked; + profile.citeproc = document.getElementById('export-citeproc').checked; + + if (currentExportFormat === 'pdf') { + profile.pdfEngine = document.getElementById('pdf-engine').value; + profile.pdfGeometry = document.getElementById('pdf-geometry').value; + } + } + + exportProfiles[profileName] = profile; + saveExportProfiles(); + populateProfileDropdown(); + + // Select the newly created profile + document.getElementById('export-profile-select').value = profileName; + + alert(`Profile "${profileName}" saved successfully!`); +} + +function loadProfile(profileName) { + if (!profileName || !exportProfiles[profileName]) return; + + const profile = exportProfiles[profileName]; + + // Apply settings + if (profile.advancedMode !== undefined) { + document.getElementById('advanced-export-toggle').checked = profile.advancedMode; + const advancedOptions = document.getElementById('advanced-export-options'); + if (profile.advancedMode) { + advancedOptions.classList.remove('hidden'); + } else { + advancedOptions.classList.add('hidden'); + } + } + + if (profile.pageSize) document.getElementById('page-size').value = profile.pageSize; + if (profile.pageOrientation) document.getElementById('page-orientation').value = profile.pageOrientation; + if (profile.basicToc !== undefined) document.getElementById('basic-toc').checked = profile.basicToc; + if (profile.basicNumberSections !== undefined) document.getElementById('basic-number-sections').checked = profile.basicNumberSections; + + // Advanced options + if (profile.advancedMode && profile.template) document.getElementById('export-template').value = profile.template; + if (profile.toc !== undefined) document.getElementById('export-toc').checked = profile.toc; + if (profile.tocDepth) document.getElementById('export-toc-depth').value = profile.tocDepth; + if (profile.numberSections !== undefined) document.getElementById('export-number-sections').checked = profile.numberSections; + if (profile.citeproc !== undefined) document.getElementById('export-citeproc').checked = profile.citeproc; + + if (profile.pdfEngine) document.getElementById('pdf-engine').value = profile.pdfEngine; + if (profile.pdfGeometry) document.getElementById('pdf-geometry').value = profile.pdfGeometry; +} + +function deleteSelectedProfile() { + const select = document.getElementById('export-profile-select'); + const profileName = select.value; + + if (!profileName) { + alert('Please select a profile to delete.'); + return; + } + + if (confirm(`Are you sure you want to delete the profile "${profileName}"?`)) { + delete exportProfiles[profileName]; + saveExportProfiles(); + populateProfileDropdown(); + select.value = ''; + alert(`Profile "${profileName}" deleted successfully!`); + } +} + +// Event listeners for export dialog +document.addEventListener('DOMContentLoaded', () => { + // Load export profiles on startup + loadExportProfiles(); + // Template selection + document.getElementById('export-template').addEventListener('change', (e) => { + const customPath = document.getElementById('custom-template-path'); + const fileInput = document.getElementById('template-file-input'); + + if (e.target.value === 'custom') { + customPath.style.display = 'block'; + fileInput.style.display = 'block'; + } else { + customPath.style.display = 'none'; + fileInput.style.display = 'none'; + customPath.value = ''; + } + }); + + // Advanced export toggle + document.getElementById('advanced-export-toggle').addEventListener('change', (e) => { + const advancedOptions = document.getElementById('advanced-export-options'); + if (e.target.checked) { + advancedOptions.classList.remove('hidden'); + // Scroll the advanced options into view after they become visible + setTimeout(() => { + advancedOptions.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + }, 100); + } else { + advancedOptions.classList.add('hidden'); + } + }); + + // Template file input + document.getElementById('template-file-input').addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + document.getElementById('custom-template-path').value = file.path; + } + }); + + // PDF geometry selection + document.getElementById('pdf-geometry').addEventListener('change', (e) => { + const customGeometry = document.getElementById('custom-geometry'); + if (e.target.value === 'custom') { + customGeometry.style.display = 'block'; + } else { + customGeometry.style.display = 'none'; + } + }); + + // Page size selection - show/hide custom size inputs + document.getElementById('page-size').addEventListener('change', (e) => { + const customPageSize = document.getElementById('custom-page-size'); + if (e.target.value === 'custom') { + customPageSize.style.display = 'block'; + } else { + customPageSize.style.display = 'none'; + } + }); + + // Load saved page settings on startup + ipcRenderer.send('get-page-settings'); + + // Handle page settings data (can be called multiple times) + ipcRenderer.on('page-settings-data', (event, settings) => { + console.log('Received page settings:', settings); + if (settings) { + const pageSizeEl = document.getElementById('page-size'); + const pageOrientationEl = document.getElementById('page-orientation'); + const customPageSizeEl = document.getElementById('custom-page-size'); + const customWidthEl = document.getElementById('custom-width'); + const customHeightEl = document.getElementById('custom-height'); + + if (pageSizeEl) pageSizeEl.value = settings.size || 'a4'; + if (pageOrientationEl) pageOrientationEl.value = settings.orientation || 'portrait'; + + if (customWidthEl && settings.customWidth) { + customWidthEl.value = settings.customWidth; + } + if (customHeightEl && settings.customHeight) { + customHeightEl.value = settings.customHeight; + } + + // Show custom inputs if size is custom + if (customPageSizeEl) { + if (settings.size === 'custom') { + customPageSizeEl.style.display = 'block'; + } else { + customPageSizeEl.style.display = 'none'; + } + } + } + }); + + // Export Profile buttons + document.getElementById('save-profile-btn').addEventListener('click', saveCurrentProfile); + document.getElementById('delete-profile-btn').addEventListener('click', deleteSelectedProfile); + document.getElementById('export-profile-select').addEventListener('change', (e) => { + loadProfile(e.target.value); + }); + + // Add metadata field + document.getElementById('add-metadata-field').addEventListener('click', () => { + const container = document.querySelector('.metadata-container'); + const newField = document.createElement('div'); + newField.className = 'metadata-field'; + newField.innerHTML = ` + + + `; + container.appendChild(newField); + }); + + // Browse bibliography + document.getElementById('browse-bibliography').addEventListener('click', () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.bib,.yaml,.yml,.json'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + document.getElementById('bibliography-file').value = file.path; + } + }; + input.click(); + }); + + // Browse CSL + document.getElementById('browse-csl').addEventListener('click', () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.csl'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + document.getElementById('csl-file').value = file.path; + } + }; + input.click(); + }); + + // Dialog close buttons + document.getElementById('export-dialog-close').addEventListener('click', hideExportDialog); + document.getElementById('export-cancel').addEventListener('click', hideExportDialog); + + // Export confirm + document.getElementById('export-confirm').addEventListener('click', () => { + const options = collectExportOptions(); + ipcRenderer.send('export-with-options', { + format: currentExportFormat, + options: options + }); + hideExportDialog(); + }); + + // Close on backdrop click + document.getElementById('export-dialog').addEventListener('click', (e) => { + if (e.target === document.getElementById('export-dialog')) { + hideExportDialog(); + } + }); + + // Close on Escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && !document.getElementById('export-dialog').classList.contains('hidden')) { + hideExportDialog(); + } + }); +}); + +// Batch Conversion Dialog functionality +let currentBatchOptions = {}; + +ipcRenderer.on('show-batch-dialog', () => { + showBatchDialog(); +}); + +// Universal Converter dialog handlers +ipcRenderer.on('show-universal-converter-dialog', () => { + showUniversalConverterDialog(); +}); + +ipcRenderer.on('conversion-status', (event, status) => { + document.getElementById('converter-status').textContent = status; +}); + +ipcRenderer.on('conversion-complete', (event, result) => { + document.getElementById('converter-progress').classList.add('hidden'); + if (result.success) { + document.getElementById('universal-converter-dialog').classList.add('hidden'); + } +}); + +ipcRenderer.on('batch-progress', (event, progress) => { + updateBatchProgress(progress); +}); + +ipcRenderer.on('folder-selected', (event, { type, path }) => { + if (type === 'input') { + document.getElementById('batch-input-folder').value = path; + validateBatchForm(); + } else if (type === 'output') { + document.getElementById('batch-output-folder').value = path; + validateBatchForm(); + } else if (type === 'converter-batch-input') { + document.getElementById('converter-batch-input-folder').value = path; + } else if (type === 'converter-batch-output') { + document.getElementById('converter-batch-output-folder').value = path; + } +}); + +function showBatchDialog() { + const dialog = document.getElementById('batch-dialog'); + dialog.classList.remove('hidden'); + + // Reset form + document.getElementById('batch-input-folder').value = ''; + document.getElementById('batch-output-folder').value = ''; + document.getElementById('batch-format').value = 'html'; + document.getElementById('batch-include-subfolders').checked = true; + document.getElementById('batch-progress').classList.add('hidden'); + document.getElementById('batch-start').disabled = true; + + currentBatchOptions = { + template: 'default', + metadata: {}, + variables: {}, + toc: false, + tocDepth: 3, + numberSections: false, + citeproc: false + }; +} + +function hideBatchDialog() { + const dialog = document.getElementById('batch-dialog'); + dialog.classList.add('hidden'); +} + +function updateBatchProgress(progress) { + const progressSection = document.getElementById('batch-progress'); + const progressFill = document.getElementById('batch-progress-fill'); + const progressText = document.getElementById('batch-progress-text'); + const progressCount = document.getElementById('batch-progress-count'); + + progressSection.classList.remove('hidden'); + + const percentage = Math.round((progress.completed / progress.total) * 100); + progressFill.style.width = `${percentage}%`; + + if (progress.completed === progress.total) { + progressText.textContent = 'Conversion complete!'; + } else { + progressText.textContent = `Processing: ${progress.currentFile}`; + } + + progressCount.textContent = `${progress.completed} / ${progress.total}`; +} + +function validateBatchForm() { + const inputFolder = document.getElementById('batch-input-folder').value.trim(); + const outputFolder = document.getElementById('batch-output-folder').value.trim(); + const startButton = document.getElementById('batch-start'); + + startButton.disabled = !inputFolder || !outputFolder; +} + +// Event listeners for batch dialog +document.addEventListener('DOMContentLoaded', () => { + // Browse input folder + document.getElementById('browse-input-folder').addEventListener('click', () => { + ipcRenderer.send('select-folder', 'input'); + }); + + // Browse output folder + document.getElementById('browse-output-folder').addEventListener('click', () => { + ipcRenderer.send('select-folder', 'output'); + }); + + // Show advanced options + document.getElementById('batch-show-options').addEventListener('click', () => { + const format = document.getElementById('batch-format').value; + currentExportFormat = format; + showExportDialog(format); + }); + + // Dialog close buttons + document.getElementById('batch-dialog-close').addEventListener('click', hideBatchDialog); + document.getElementById('batch-cancel').addEventListener('click', hideBatchDialog); + + // Start batch conversion + document.getElementById('batch-start').addEventListener('click', () => { + const inputFolder = document.getElementById('batch-input-folder').value.trim(); + const outputFolder = document.getElementById('batch-output-folder').value.trim(); + const format = document.getElementById('batch-format').value; + + if (!inputFolder || !outputFolder) { + return; + } + + // Use current export options from advanced dialog if they were set + const options = currentBatchOptions; + + // Start batch conversion + ipcRenderer.send('batch-convert', { + inputFolder, + outputFolder, + format, + options + }); + + // Show progress + document.getElementById('batch-progress').classList.remove('hidden'); + document.getElementById('batch-start').disabled = true; + }); + + // Close on backdrop click + document.getElementById('batch-dialog').addEventListener('click', (e) => { + if (e.target === document.getElementById('batch-dialog')) { + hideBatchDialog(); + } + }); + + // Close on Escape key (modified to handle both dialogs) + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + if (!document.getElementById('export-dialog').classList.contains('hidden')) { + hideExportDialog(); + } else if (!document.getElementById('batch-dialog').classList.contains('hidden')) { + hideBatchDialog(); + } + } + }); + + // Input validation + document.getElementById('batch-input-folder').addEventListener('input', validateBatchForm); + document.getElementById('batch-output-folder').addEventListener('input', validateBatchForm); +}); + +// Override the export dialog confirm to also save batch options +const originalExportConfirm = document.getElementById('export-confirm'); +if (originalExportConfirm) { + originalExportConfirm.addEventListener('click', () => { + // If batch dialog is open, save options for batch conversion + if (!document.getElementById('batch-dialog').classList.contains('hidden')) { + currentBatchOptions = collectExportOptions(); + } + }); +} + +// Universal File Converter Dialog Functions +let converterFilePath = ''; + +// Format definitions for each converter +const converterFormats = { + libreoffice: { + input: [ + { value: 'docx', label: 'Word Document (DOCX)' }, + { value: 'doc', label: 'Word 97-2003 (DOC)' }, + { value: 'odt', label: 'OpenDocument Text (ODT)' }, + { value: 'rtf', label: 'Rich Text Format (RTF)' }, + { value: 'txt', label: 'Plain Text (TXT)' }, + { value: 'html', label: 'HTML Document' }, + { value: 'htm', label: 'HTM Document' }, + { value: 'xlsx', label: 'Excel Spreadsheet (XLSX)' }, + { value: 'xls', label: 'Excel 97-2003 (XLS)' }, + { value: 'ods', label: 'OpenDocument Spreadsheet (ODS)' }, + { value: 'csv', label: 'Comma Separated Values (CSV)' }, + { value: 'pptx', label: 'PowerPoint (PPTX)' }, + { value: 'ppt', label: 'PowerPoint 97-2003 (PPT)' }, + { value: 'odp', label: 'OpenDocument Presentation (ODP)' } + ], + output: [ + { value: 'pdf', label: 'PDF Document' }, + { value: 'docx', label: 'Word Document (DOCX)' }, + { value: 'doc', label: 'Word 97-2003 (DOC)' }, + { value: 'odt', label: 'OpenDocument Text (ODT)' }, + { value: 'rtf', label: 'Rich Text Format (RTF)' }, + { value: 'txt', label: 'Plain Text (TXT)' }, + { value: 'html', label: 'HTML Document' }, + { value: 'xlsx', label: 'Excel Spreadsheet (XLSX)' }, + { value: 'xls', label: 'Excel 97-2003 (XLS)' }, + { value: 'ods', label: 'OpenDocument Spreadsheet (ODS)' }, + { value: 'csv', label: 'CSV' }, + { value: 'pptx', label: 'PowerPoint (PPTX)' }, + { value: 'ppt', label: 'PowerPoint 97-2003 (PPT)' }, + { value: 'odp', label: 'OpenDocument Presentation (ODP)' } + ] + }, + imagemagick: { + input: [ + { value: 'jpg', label: 'JPEG Image (JPG)' }, + { value: 'jpeg', label: 'JPEG Image (JPEG)' }, + { value: 'png', label: 'PNG Image' }, + { value: 'gif', label: 'GIF Image' }, + { value: 'bmp', label: 'Bitmap Image (BMP)' }, + { value: 'tiff', label: 'TIFF Image' }, + { value: 'tif', label: 'TIF Image' }, + { value: 'webp', label: 'WebP Image' }, + { value: 'svg', label: 'SVG Vector Image' }, + { value: 'ico', label: 'Icon File (ICO)' }, + { value: 'psd', label: 'Photoshop (PSD)' }, + { value: 'raw', label: 'RAW Image' }, + { value: 'cr2', label: 'Canon RAW (CR2)' }, + { value: 'nef', label: 'Nikon RAW (NEF)' }, + { value: 'heic', label: 'HEIC Image' }, + { value: 'avif', label: 'AVIF Image' } + ], + output: [ + { value: 'jpg', label: 'JPEG Image (JPG)' }, + { value: 'png', label: 'PNG Image' }, + { value: 'gif', label: 'GIF Image' }, + { value: 'bmp', label: 'Bitmap Image (BMP)' }, + { value: 'tiff', label: 'TIFF Image' }, + { value: 'webp', label: 'WebP Image' }, + { value: 'svg', label: 'SVG Vector Image' }, + { value: 'ico', label: 'Icon File (ICO)' }, + { value: 'pdf', label: 'PDF Document' }, + { value: 'eps', label: 'EPS Vector' }, + { value: 'ps', label: 'PostScript' }, + { value: 'avif', label: 'AVIF Image' } + ] + }, + ffmpeg: { + input: [ + { value: 'mp4', label: 'MP4 Video' }, + { value: 'avi', label: 'AVI Video' }, + { value: 'mov', label: 'MOV Video (QuickTime)' }, + { value: 'mkv', label: 'MKV Video (Matroska)' }, + { value: 'wmv', label: 'WMV Video (Windows Media)' }, + { value: 'flv', label: 'FLV Video (Flash)' }, + { value: 'webm', label: 'WebM Video' }, + { value: 'mpeg', label: 'MPEG Video' }, + { value: 'mpg', label: 'MPG Video' }, + { value: 'm4v', label: 'M4V Video' }, + { value: 'mp3', label: 'MP3 Audio' }, + { value: 'wav', label: 'WAV Audio' }, + { value: 'ogg', label: 'OGG Audio' }, + { value: 'flac', label: 'FLAC Audio' }, + { value: 'aac', label: 'AAC Audio' }, + { value: 'm4a', label: 'M4A Audio' }, + { value: 'wma', label: 'WMA Audio' } + ], + output: [ + { value: 'mp4', label: 'MP4 Video' }, + { value: 'avi', label: 'AVI Video' }, + { value: 'mov', label: 'MOV Video (QuickTime)' }, + { value: 'mkv', label: 'MKV Video (Matroska)' }, + { value: 'webm', label: 'WebM Video' }, + { value: 'mpeg', label: 'MPEG Video' }, + { value: 'gif', label: 'Animated GIF' }, + { value: 'mp3', label: 'MP3 Audio' }, + { value: 'wav', label: 'WAV Audio' }, + { value: 'ogg', label: 'OGG Audio' }, + { value: 'flac', label: 'FLAC Audio' }, + { value: 'aac', label: 'AAC Audio' }, + { value: 'm4a', label: 'M4A Audio' } + ] + }, + pandoc: { + input: [ + { value: 'md', label: 'Markdown (MD)' }, + { value: 'markdown', label: 'Markdown' }, + { value: 'html', label: 'HTML Document' }, + { value: 'docx', label: 'Word Document (DOCX)' }, + { value: 'odt', label: 'OpenDocument Text (ODT)' }, + { value: 'rtf', label: 'Rich Text Format (RTF)' }, + { value: 'tex', label: 'LaTeX Document' }, + { value: 'latex', label: 'LaTeX' }, + { value: 'epub', label: 'EPUB eBook' }, + { value: 'rst', label: 'reStructuredText (RST)' }, + { value: 'textile', label: 'Textile' }, + { value: 'org', label: 'Org Mode' }, + { value: 'mediawiki', label: 'MediaWiki' }, + { value: 'docbook', label: 'DocBook XML' } + ], + output: [ + { value: 'html', label: 'HTML Document' }, + { value: 'pdf', label: 'PDF Document' }, + { value: 'docx', label: 'Word Document (DOCX)' }, + { value: 'odt', label: 'OpenDocument Text (ODT)' }, + { value: 'rtf', label: 'Rich Text Format (RTF)' }, + { value: 'epub', label: 'EPUB eBook' }, + { value: 'latex', label: 'LaTeX Document' }, + { value: 'md', label: 'Markdown (MD)' }, + { value: 'rst', label: 'reStructuredText (RST)' }, + { value: 'textile', label: 'Textile' }, + { value: 'org', label: 'Org Mode' }, + { value: 'mediawiki', label: 'MediaWiki' }, + { value: 'docbook', label: 'DocBook XML' }, + { value: 'pptx', label: 'PowerPoint (PPTX)' } + ] + } +}; + +function showUniversalConverterDialog() { + const dialog = document.getElementById('universal-converter-dialog'); + dialog.classList.remove('hidden'); + converterFilePath = ''; + document.getElementById('converter-file-path').value = ''; + document.getElementById('converter-tool').value = 'libreoffice'; + document.getElementById('converter-progress').classList.add('hidden'); + updateConverterFormats('libreoffice'); +} + +function updateConverterFormats(tool) { + const fromSelect = document.getElementById('converter-from'); + const toSelect = document.getElementById('converter-to'); + const helpText = document.getElementById('converter-tool-help'); + + // Clear existing options + fromSelect.innerHTML = ''; + toSelect.innerHTML = ''; + + // Get formats for selected tool + const formats = converterFormats[tool]; + + if (formats) { + // Populate input formats + formats.input.forEach(format => { + const option = document.createElement('option'); + option.value = format.value; + option.textContent = format.label; + fromSelect.appendChild(option); + }); + + // Populate output formats + formats.output.forEach(format => { + const option = document.createElement('option'); + option.value = format.value; + option.textContent = format.label; + toSelect.appendChild(option); + }); + + // Update help text + if (tool === 'libreoffice') { + helpText.textContent = 'Documents, Spreadsheets, Presentations - Office file conversions'; + } else if (tool === 'imagemagick') { + helpText.textContent = 'Image format conversions - JPG, PNG, GIF, TIFF, WebP, SVG, and more'; + } else if (tool === 'ffmpeg') { + helpText.textContent = 'Video and audio conversions - MP4, AVI, MOV, MP3, WAV, and more'; + } else if (tool === 'pandoc') { + helpText.textContent = 'Document markup conversions - Markdown, HTML, LaTeX, EPUB, and more'; + } + } +} + +function updateConverterAdvancedOptions(tool) { + // Hide all tool-specific options + const allOptions = document.querySelectorAll('.converter-options'); + allOptions.forEach(opt => opt.classList.add('hidden')); + + // Show options for selected tool + const toolOptions = document.querySelector(`.${tool}-options`); + if (toolOptions) { + toolOptions.classList.remove('hidden'); + } +} + +function collectConverterAdvancedOptions(tool) { + const options = {}; + const advancedMode = document.getElementById('converter-advanced-toggle').checked; + + if (!advancedMode) { + return options; + } + + // Tool-specific options + if (tool === 'imagemagick') { + options.quality = document.getElementById('imagemagick-quality').value; + options.dpi = document.getElementById('imagemagick-dpi').value || null; + options.resize = document.getElementById('imagemagick-resize').value || null; + options.compression = document.getElementById('imagemagick-compression').value || null; + } else if (tool === 'ffmpeg') { + options.videoCodec = document.getElementById('ffmpeg-video-codec').value || null; + options.audioCodec = document.getElementById('ffmpeg-audio-codec').value || null; + options.bitrate = document.getElementById('ffmpeg-bitrate').value || null; + options.preset = document.getElementById('ffmpeg-preset').value || null; + options.framerate = document.getElementById('ffmpeg-framerate').value || null; + } else if (tool === 'libreoffice') { + options.quality = document.getElementById('libreoffice-quality').value || null; + options.pageRange = document.getElementById('libreoffice-page-range').value || null; + options.exportBookmarks = document.getElementById('libreoffice-export-bookmarks').checked; + } + + return options; +} + +document.addEventListener('DOMContentLoaded', () => { + // Universal Converter tool change + const converterTool = document.getElementById('converter-tool'); + if (converterTool) { + converterTool.addEventListener('change', (e) => { + updateConverterFormats(e.target.value); + updateConverterAdvancedOptions(e.target.value); + }); + } + + // Batch mode toggle + const converterBatchMode = document.getElementById('converter-batch-mode'); + if (converterBatchMode) { + converterBatchMode.addEventListener('change', (e) => { + const batchOptions = document.getElementById('converter-batch-options'); + const singleFileSection = document.getElementById('converter-file-path').closest('.export-section'); + + if (e.target.checked) { + batchOptions.classList.remove('hidden'); + singleFileSection.style.display = 'none'; + } else { + batchOptions.classList.add('hidden'); + singleFileSection.style.display = 'block'; + } + }); + } + + // Advanced options toggle + const converterAdvancedToggle = document.getElementById('converter-advanced-toggle'); + if (converterAdvancedToggle) { + converterAdvancedToggle.addEventListener('change', (e) => { + const advancedOptions = document.getElementById('converter-advanced-options'); + if (e.target.checked) { + advancedOptions.classList.remove('hidden'); + // Update which tool-specific options to show + updateConverterAdvancedOptions(document.getElementById('converter-tool').value); + } else { + advancedOptions.classList.add('hidden'); + } + }); + } + + // ImageMagick quality slider + const imagemagickQuality = document.getElementById('imagemagick-quality'); + if (imagemagickQuality) { + imagemagickQuality.addEventListener('input', (e) => { + document.getElementById('imagemagick-quality-value').textContent = e.target.value; + }); + } + + // Browse batch input folder + const browseBatchInput = document.getElementById('browse-converter-batch-input'); + if (browseBatchInput) { + browseBatchInput.addEventListener('click', () => { + ipcRenderer.send('select-folder', 'converter-batch-input'); + }); + } + + // Browse batch output folder + const browseBatchOutput = document.getElementById('browse-converter-batch-output'); + if (browseBatchOutput) { + browseBatchOutput.addEventListener('click', () => { + ipcRenderer.send('select-folder', 'converter-batch-output'); + }); + } + + // Browse for file to convert + const browseConverterFile = document.getElementById('browse-converter-file'); + if (browseConverterFile) { + browseConverterFile.addEventListener('click', () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '*'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + converterFilePath = file.path; + document.getElementById('converter-file-path').value = file.path; + } + }; + input.click(); + }); + } + + // Universal Converter dialog close + const converterDialogClose = document.getElementById('converter-dialog-close'); + if (converterDialogClose) { + converterDialogClose.addEventListener('click', () => { + document.getElementById('universal-converter-dialog').classList.add('hidden'); + }); + } + + // Universal Converter cancel + const converterCancel = document.getElementById('converter-cancel'); + if (converterCancel) { + converterCancel.addEventListener('click', () => { + document.getElementById('universal-converter-dialog').classList.add('hidden'); + }); + } + + // Universal Converter convert + const converterConvert = document.getElementById('converter-convert'); + if (converterConvert) { + converterConvert.addEventListener('click', () => { + const tool = document.getElementById('converter-tool').value; + const fromFormat = document.getElementById('converter-from').value; + const toFormat = document.getElementById('converter-to').value; + const batchMode = document.getElementById('converter-batch-mode').checked; + const advancedOptions = collectConverterAdvancedOptions(tool); + + if (batchMode) { + // Batch conversion + const inputFolder = document.getElementById('converter-batch-input-folder').value.trim(); + const outputFolder = document.getElementById('converter-batch-output-folder').value.trim(); + const includeSubfolders = document.getElementById('converter-batch-subfolders').checked; + + if (!inputFolder || !outputFolder) { + alert('Please select both input and output folders for batch conversion'); + return; + } + + // Show progress + document.getElementById('converter-progress').classList.remove('hidden'); + + // Send batch conversion request + ipcRenderer.send('universal-convert-batch', { + tool, + fromFormat, + toFormat, + inputFolder, + outputFolder, + includeSubfolders, + advancedOptions + }); + } else { + // Single file conversion + const filePath = converterFilePath; + + if (!filePath) { + alert('Please select a file to convert'); + return; + } + + // Show progress + document.getElementById('converter-progress').classList.remove('hidden'); + + // Send single file conversion request + ipcRenderer.send('universal-convert', { + tool, + fromFormat, + toFormat, + filePath, + advancedOptions + }); + } + }); + } +}); + +// IPC event listeners for recent files functionality +ipcRenderer.on('recent-files-cleared', () => { + if (tabManager) { + tabManager.recentFiles = []; + localStorage.setItem('recentFiles', JSON.stringify([])); + console.log('Recent files cleared'); + } +}); + +// ======================================== +// PDF Editor Dialog Functionality +// ======================================== + +let currentPDFOperation = null; +let mergeFilePaths = []; + +// Show Table Generator Dialog +ipcRenderer.on('show-table-generator', () => { + showTableGenerator(); +}); + +// Show PDF Editor Dialog +ipcRenderer.on('show-pdf-editor-dialog', (event, operation, openedFilePath) => { + currentPDFOperation = operation; + showPDFEditorDialog(operation, openedFilePath); +}); + +function showPDFEditorDialog(operation, openedFilePath = null) { + const dialog = document.getElementById('pdf-editor-dialog'); + const title = document.getElementById('pdf-editor-title'); + + // Hide all operation sections + document.querySelectorAll('.pdf-operation-section').forEach(section => { + section.classList.add('hidden'); + }); + + // Show the appropriate section and set title + let sectionId, titleText; + switch (operation) { + case 'merge': + sectionId = 'pdf-merge-section'; + titleText = 'Merge PDFs'; + mergeFilePaths = []; + // If we have an opened file, add it as the first file to merge + if (openedFilePath) { + mergeFilePaths.push(openedFilePath); + } + updateMergeFilesList(); + break; + case 'split': + sectionId = 'pdf-split-section'; + titleText = 'Split PDF'; + // Pre-fill input path if we have an opened file + if (openedFilePath) { + document.getElementById('split-input-path').value = openedFilePath; + } + break; + case 'compress': + sectionId = 'pdf-compress-section'; + titleText = 'Compress PDF'; + if (openedFilePath) { + document.getElementById('compress-input-path').value = openedFilePath; + } + break; + case 'rotate': + sectionId = 'pdf-rotate-section'; + titleText = 'Rotate Pages'; + if (openedFilePath) { + const rotateInput = document.getElementById('rotate-input-path'); + if (rotateInput) rotateInput.value = openedFilePath; + } + break; + case 'delete': + sectionId = 'pdf-delete-section'; + titleText = 'Delete Pages'; + if (openedFilePath) { + const deleteInput = document.getElementById('delete-input-path'); + if (deleteInput) deleteInput.value = openedFilePath; + } + break; + case 'reorder': + sectionId = 'pdf-reorder-section'; + titleText = 'Reorder Pages'; + if (openedFilePath) { + const reorderInput = document.getElementById('reorder-input-path'); + if (reorderInput) reorderInput.value = openedFilePath; + } + break; + case 'watermark': + sectionId = 'pdf-watermark-section'; + titleText = 'Add Watermark'; + if (openedFilePath) { + const watermarkInput = document.getElementById('watermark-input-path'); + if (watermarkInput) watermarkInput.value = openedFilePath; + } + break; + case 'encrypt': + sectionId = 'pdf-encrypt-section'; + titleText = 'Password Protection'; + if (openedFilePath) { + const encryptInput = document.getElementById('encrypt-input-path'); + if (encryptInput) encryptInput.value = openedFilePath; + } + break; + case 'decrypt': + sectionId = 'pdf-decrypt-section'; + titleText = 'Remove Password'; + if (openedFilePath) { + const decryptInput = document.getElementById('decrypt-input-path'); + if (decryptInput) decryptInput.value = openedFilePath; + } + break; + case 'permissions': + sectionId = 'pdf-permissions-section'; + titleText = 'Set Permissions'; + if (openedFilePath) { + const permInput = document.getElementById('permissions-input-path'); + if (permInput) permInput.value = openedFilePath; + } + break; + } + + title.textContent = titleText; + document.getElementById(sectionId).classList.remove('hidden'); + dialog.classList.remove('hidden'); +} + +function hidePDFEditorDialog() { + document.getElementById('pdf-editor-dialog').classList.add('hidden'); + document.getElementById('pdf-progress').classList.add('hidden'); + currentPDFOperation = null; +} + +function updateMergeFilesList() { + const listContainer = document.getElementById('merge-files-list'); + listContainer.innerHTML = ''; + + mergeFilePaths.forEach((filePath, index) => { + const fileEntry = document.createElement('div'); + fileEntry.className = 'file-entry'; + fileEntry.innerHTML = ` + ${filePath.split(/[\\/]/).pop()} + + `; + listContainer.appendChild(fileEntry); + }); +} + +// PDF Editor Event Listeners +document.addEventListener('DOMContentLoaded', () => { + // Close PDF Editor Dialog + const pdfEditorClose = document.getElementById('pdf-editor-dialog-close'); + if (pdfEditorClose) { + pdfEditorClose.addEventListener('click', hidePDFEditorDialog); + } + + const pdfEditorCancel = document.getElementById('pdf-editor-cancel'); + if (pdfEditorCancel) { + pdfEditorCancel.addEventListener('click', hidePDFEditorDialog); + } + + // Process button + const pdfEditorProcess = document.getElementById('pdf-editor-process'); + if (pdfEditorProcess) { + pdfEditorProcess.addEventListener('click', processPDFOperation); + } + + // Merge PDFs - Add file button + const addMergeFile = document.getElementById('add-merge-file'); + if (addMergeFile) { + addMergeFile.addEventListener('click', () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.pdf'; + input.multiple = true; + input.onchange = (e) => { + const files = Array.from(e.target.files); + files.forEach(file => { + if (!mergeFilePaths.includes(file.path)) { + mergeFilePaths.push(file.path); + } + }); + updateMergeFilesList(); + }; + input.click(); + }); + } + + // Remove file from merge list (using event delegation) + const mergeFilesList = document.getElementById('merge-files-list'); + if (mergeFilesList) { + mergeFilesList.addEventListener('click', (e) => { + if (e.target.classList.contains('remove-file')) { + const index = parseInt(e.target.dataset.index); + mergeFilePaths.splice(index, 1); + updateMergeFilesList(); + } + }); + } + + // Browse buttons for all operations + const browseButtons = [ + { id: 'browse-merge-output', inputId: 'merge-output-path', saveDialog: true }, + { id: 'browse-split-input', inputId: 'split-input-path', saveDialog: false }, + { id: 'browse-split-output', inputId: 'split-output-folder', folder: true }, + { id: 'browse-compress-input', inputId: 'compress-input-path', saveDialog: false }, + { id: 'browse-compress-output', inputId: 'compress-output-path', saveDialog: true }, + { id: 'browse-rotate-input', inputId: 'rotate-input-path', saveDialog: false }, + { id: 'browse-rotate-output', inputId: 'rotate-output-path', saveDialog: true }, + { id: 'browse-delete-input', inputId: 'delete-input-path', saveDialog: false }, + { id: 'browse-delete-output', inputId: 'delete-output-path', saveDialog: true }, + { id: 'browse-reorder-input', inputId: 'reorder-input-path', saveDialog: false }, + { id: 'browse-reorder-output', inputId: 'reorder-output-path', saveDialog: true }, + { id: 'browse-watermark-input', inputId: 'watermark-input-path', saveDialog: false }, + { id: 'browse-watermark-output', inputId: 'watermark-output-path', saveDialog: true }, + { id: 'browse-encrypt-input', inputId: 'encrypt-input-path', saveDialog: false }, + { id: 'browse-encrypt-output', inputId: 'encrypt-output-path', saveDialog: true }, + { id: 'browse-decrypt-input', inputId: 'decrypt-input-path', saveDialog: false }, + { id: 'browse-decrypt-output', inputId: 'decrypt-output-path', saveDialog: true }, + { id: 'browse-permissions-input', inputId: 'permissions-input-path', saveDialog: false }, + { id: 'browse-permissions-output', inputId: 'permissions-output-path', saveDialog: true } + ]; + + browseButtons.forEach(button => { + const btn = document.getElementById(button.id); + if (btn) { + btn.addEventListener('click', () => { + const input = document.createElement('input'); + input.type = 'file'; + + if (button.folder) { + // Request folder selection via IPC + ipcRenderer.send('select-pdf-folder', button.inputId); + } else if (button.saveDialog) { + input.nwsaveas = true; + input.accept = '.pdf'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + document.getElementById(button.inputId).value = file.path; + } + }; + input.click(); + } else { + input.accept = '.pdf'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) { + document.getElementById(button.inputId).value = file.path; + } + }; + input.click(); + } + }); + } + }); + + // Split mode change handler + const splitMode = document.getElementById('split-mode'); + if (splitMode) { + splitMode.addEventListener('change', (e) => { + // Hide all split options + document.getElementById('split-pages-options').classList.add('hidden'); + document.getElementById('split-interval-options').classList.add('hidden'); + document.getElementById('split-size-options').classList.add('hidden'); + + // Show selected split option + if (e.target.value === 'pages') { + document.getElementById('split-pages-options').classList.remove('hidden'); + } else if (e.target.value === 'interval') { + document.getElementById('split-interval-options').classList.remove('hidden'); + } else if (e.target.value === 'size') { + document.getElementById('split-size-options').classList.remove('hidden'); + } + }); + } + + // Watermark opacity slider + const watermarkOpacity = document.getElementById('watermark-opacity'); + if (watermarkOpacity) { + watermarkOpacity.addEventListener('input', (e) => { + document.getElementById('watermark-opacity-value').textContent = e.target.value; + }); + } + + // Watermark pages selection + const watermarkPages = document.getElementById('watermark-pages'); + if (watermarkPages) { + watermarkPages.addEventListener('change', (e) => { + const customPages = document.getElementById('watermark-custom-pages'); + if (e.target.value === 'custom') { + customPages.classList.remove('hidden'); + } else { + customPages.classList.add('hidden'); + } + }); + } + + // Overwrite checkbox handlers - toggle Save As section visibility + const overwriteCheckboxes = [ + { checkbox: 'compress-overwrite', section: 'compress-saveas-section' }, + { checkbox: 'rotate-overwrite', section: 'rotate-saveas-section' }, + { checkbox: 'delete-overwrite', section: 'delete-saveas-section' }, + { checkbox: 'reorder-overwrite', section: 'reorder-saveas-section' }, + { checkbox: 'watermark-overwrite', section: 'watermark-saveas-section' }, + { checkbox: 'encrypt-overwrite', section: 'encrypt-saveas-section' }, + { checkbox: 'decrypt-overwrite', section: 'decrypt-saveas-section' }, + { checkbox: 'permissions-overwrite', section: 'permissions-saveas-section' } + ]; + + overwriteCheckboxes.forEach(item => { + const checkbox = document.getElementById(item.checkbox); + const section = document.getElementById(item.section); + if (checkbox && section) { + checkbox.addEventListener('change', (e) => { + if (e.target.checked) { + section.classList.add('hidden'); + } else { + section.classList.remove('hidden'); + } + }); + } + }); + + // Load current page order button + const loadCurrentOrder = document.getElementById('load-current-order'); + if (loadCurrentOrder) { + loadCurrentOrder.addEventListener('click', () => { + const inputPath = document.getElementById('reorder-input-path').value; + if (!inputPath) { + alert('Please select a PDF file first'); + return; + } + // Request page count from main process + ipcRenderer.send('get-pdf-page-count', inputPath); + }); + } +}); + +// Handle folder selection response +ipcRenderer.on('pdf-folder-selected', (event, { inputId, path }) => { + document.getElementById(inputId).value = path; +}); + +// Handle PDF page count response +ipcRenderer.on('pdf-page-count', (event, { count, error }) => { + if (error) { + alert('Error reading PDF: ' + error); + return; + } + + const currentOrder = Array.from({ length: count }, (_, i) => i + 1).join(', '); + document.getElementById('current-order-display').textContent = currentOrder; + document.getElementById('current-page-order').classList.remove('hidden'); + document.getElementById('reorder-pages').value = currentOrder; +}); + +// Process PDF Operation +function processPDFOperation() { + const operation = currentPDFOperation; + let operationData = { operation }; + + try { + switch (operation) { + case 'merge': + if (mergeFilePaths.length < 2) { + alert('Please add at least 2 PDF files to merge'); + return; + } + operationData.inputFiles = mergeFilePaths; + operationData.outputPath = document.getElementById('merge-output-path').value.trim(); + if (!operationData.outputPath) { + alert('Please select an output file path'); + return; + } + break; + + case 'split': + operationData.inputPath = document.getElementById('split-input-path').value.trim(); + operationData.outputFolder = document.getElementById('split-output-folder').value.trim(); + operationData.splitMode = document.getElementById('split-mode').value; + + if (!operationData.inputPath || !operationData.outputFolder) { + alert('Please select input file and output folder'); + return; + } + + if (operationData.splitMode === 'pages') { + operationData.pageRanges = document.getElementById('split-page-ranges').value.trim(); + } else if (operationData.splitMode === 'interval') { + operationData.interval = parseInt(document.getElementById('split-interval').value); + } else if (operationData.splitMode === 'size') { + operationData.maxSize = parseInt(document.getElementById('split-size').value); + } + break; + + case 'compress': + operationData.inputPath = document.getElementById('compress-input-path').value.trim(); + operationData.overwrite = document.getElementById('compress-overwrite').checked; + operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('compress-output-path').value.trim(); + operationData.compressionLevel = document.getElementById('compress-level').value; + operationData.compressImages = document.getElementById('compress-images').checked; + operationData.removeDuplicates = document.getElementById('compress-remove-duplicates').checked; + operationData.optimizeFonts = document.getElementById('compress-optimize-fonts').checked; + + if (!operationData.inputPath || !operationData.outputPath) { + alert('Please select input file' + (operationData.overwrite ? '' : ' and output file paths')); + return; + } + break; + + case 'rotate': + operationData.inputPath = document.getElementById('rotate-input-path').value.trim(); + operationData.overwrite = document.getElementById('rotate-overwrite').checked; + operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('rotate-output-path').value.trim(); + operationData.pages = document.getElementById('rotate-pages').value.trim(); + operationData.angle = parseInt(document.getElementById('rotate-angle').value); + + if (!operationData.inputPath || !operationData.outputPath) { + alert('Please select input file' + (operationData.overwrite ? '' : ' and output file')); + return; + } + break; + + case 'delete': + operationData.inputPath = document.getElementById('delete-input-path').value.trim(); + operationData.overwrite = document.getElementById('delete-overwrite').checked; + operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('delete-output-path').value.trim(); + operationData.pages = document.getElementById('delete-pages').value.trim(); + + if (!operationData.inputPath || !operationData.outputPath || !operationData.pages) { + alert('Please fill in all required fields'); + return; + } + break; + + case 'reorder': + operationData.inputPath = document.getElementById('reorder-input-path').value.trim(); + operationData.overwrite = document.getElementById('reorder-overwrite').checked; + operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('reorder-output-path').value.trim(); + operationData.newOrder = document.getElementById('reorder-pages').value.trim(); + + if (!operationData.inputPath || !operationData.outputPath || !operationData.newOrder) { + alert('Please fill in all required fields'); + return; + } + break; + + case 'watermark': + operationData.inputPath = document.getElementById('watermark-input-path').value.trim(); + operationData.overwrite = document.getElementById('watermark-overwrite').checked; + operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('watermark-output-path').value.trim(); + operationData.text = document.getElementById('watermark-text').value.trim(); + operationData.fontSize = parseInt(document.getElementById('watermark-font-size').value); + operationData.opacity = parseInt(document.getElementById('watermark-opacity').value) / 100; + operationData.position = document.getElementById('watermark-position').value; + operationData.color = document.getElementById('watermark-color').value; + operationData.pages = document.getElementById('watermark-pages').value; + + if (operationData.pages === 'custom') { + operationData.customPages = document.getElementById('watermark-custom-pages').value.trim(); + } + + if (!operationData.inputPath || !operationData.outputPath || !operationData.text) { + alert('Please fill in all required fields'); + return; + } + break; + + case 'encrypt': + operationData.inputPath = document.getElementById('encrypt-input-path').value.trim(); + operationData.overwrite = document.getElementById('encrypt-overwrite').checked; + operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('encrypt-output-path').value.trim(); + operationData.userPassword = document.getElementById('encrypt-user-password').value; + operationData.ownerPassword = document.getElementById('encrypt-owner-password').value; + operationData.encryptionLevel = parseInt(document.getElementById('encrypt-level').value); + + operationData.permissions = { + printing: document.getElementById('encrypt-allow-printing').checked, + modifying: document.getElementById('encrypt-allow-modify').checked, + copying: document.getElementById('encrypt-allow-copy').checked, + annotating: document.getElementById('encrypt-allow-annotate').checked, + fillingForms: document.getElementById('encrypt-allow-forms').checked, + contentAccessibility: document.getElementById('encrypt-allow-extract').checked, + documentAssembly: document.getElementById('encrypt-allow-assemble').checked, + printingQuality: document.getElementById('encrypt-allow-print-high').checked + }; + + if (!operationData.inputPath || !operationData.outputPath || !operationData.userPassword) { + alert('Please select file and enter a user password'); + return; + } + break; + + case 'decrypt': + operationData.inputPath = document.getElementById('decrypt-input-path').value.trim(); + operationData.overwrite = document.getElementById('decrypt-overwrite').checked; + operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('decrypt-output-path').value.trim(); + operationData.password = document.getElementById('decrypt-password').value; + + if (!operationData.inputPath || !operationData.outputPath || !operationData.password) { + alert('Please fill in all required fields'); + return; + } + break; + + case 'permissions': + operationData.inputPath = document.getElementById('permissions-input-path').value.trim(); + operationData.overwrite = document.getElementById('permissions-overwrite').checked; + operationData.outputPath = operationData.overwrite ? operationData.inputPath : document.getElementById('permissions-output-path').value.trim(); + operationData.currentPassword = document.getElementById('permissions-current-password').value; + operationData.ownerPassword = document.getElementById('permissions-owner-password').value; + + operationData.permissions = { + printing: document.getElementById('permissions-allow-printing').checked, + modifying: document.getElementById('permissions-allow-modify').checked, + copying: document.getElementById('permissions-allow-copy').checked, + annotating: document.getElementById('permissions-allow-annotate').checked, + fillingForms: document.getElementById('permissions-allow-forms').checked, + contentAccessibility: document.getElementById('permissions-allow-extract').checked, + documentAssembly: document.getElementById('permissions-allow-assemble').checked, + printingQuality: document.getElementById('permissions-allow-print-high').checked + }; + + if (!operationData.inputPath || !operationData.outputPath || !operationData.ownerPassword) { + alert('Please fill in all required fields'); + return; + } + break; + } + + // Show progress + document.getElementById('pdf-progress').classList.remove('hidden'); + document.getElementById('pdf-progress-text').textContent = 'Processing PDF...'; + + // Send to main process + ipcRenderer.send('process-pdf-operation', operationData); + + } catch (error) { + alert('Error: ' + error.message); + console.error('PDF operation error:', error); + } +} + +// Handle PDF operation completion +ipcRenderer.on('pdf-operation-complete', (event, { success, error, message }) => { + document.getElementById('pdf-progress').classList.add('hidden'); + + if (success) { + alert(message || 'PDF operation completed successfully!'); + hidePDFEditorDialog(); + } else { + alert('Error: ' + (error || 'PDF operation failed')); + } +}); + +// Handle PDF operation progress +ipcRenderer.on('pdf-operation-progress', (event, { message, progress }) => { + document.getElementById('pdf-progress-text').textContent = message; + if (progress !== undefined) { + const progressFill = document.getElementById('pdf-progress-fill'); + if (progressFill) { + progressFill.style.width = `${progress}%`; + } + } +}); + +// Add math rendering support using KaTeX for enhanced preview +function initMathSupport() { + // Add KaTeX CSS + const katexCSS = document.createElement('link'); + katexCSS.rel = 'stylesheet'; + katexCSS.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css'; + katexCSS.crossOrigin = 'anonymous'; + document.head.appendChild(katexCSS); + + // Add KaTeX JS + const katexJS = document.createElement('script'); + katexJS.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js'; + katexJS.crossOrigin = 'anonymous'; + katexJS.onload = () => { + // Add auto-render extension + const autoRenderJS = document.createElement('script'); + autoRenderJS.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js'; + autoRenderJS.crossOrigin = 'anonymous'; + autoRenderJS.onload = () => { + console.log('Math support (KaTeX) initialized'); + // Re-render current preview to include math + if (tabManager) { + tabManager.updatePreview(); + } + }; + document.head.appendChild(autoRenderJS); + }; + document.head.appendChild(katexJS); +} + +// Initialize math support on load +initMathSupport(); + +// ================================ +// Header & Footer Dialog Management +// ================================ + +let currentFieldTarget = null; // Track which input field is being edited + +// Open header/footer settings dialog +function openHeaderFooterDialog() { + const dialog = document.getElementById('header-footer-dialog'); + dialog.classList.remove('hidden'); + + // Request current settings from main process + ipcRenderer.send('get-header-footer-settings'); +} + +// Close header/footer settings dialog +function closeHeaderFooterDialog() { + const dialog = document.getElementById('header-footer-dialog'); + dialog.classList.add('hidden'); +} + +// Open field picker dialog +function openFieldPickerDialog(targetInputId) { + currentFieldTarget = targetInputId; + const dialog = document.getElementById('field-picker-dialog'); + dialog.classList.remove('hidden'); +} + +// Close field picker dialog +function closeFieldPickerDialog() { + const dialog = document.getElementById('field-picker-dialog'); + dialog.classList.add('hidden'); + currentFieldTarget = null; +} + +// Load settings into dialog +ipcRenderer.on('header-footer-settings-data', (event, settings) => { + // Enable/disable checkbox + document.getElementById('hf-enabled').checked = settings.enabled; + + // Header fields + document.getElementById('header-left').value = settings.header.left || ''; + document.getElementById('header-center').value = settings.header.center || ''; + document.getElementById('header-right').value = settings.header.right || ''; + + // Footer fields + document.getElementById('footer-left').value = settings.footer.left || ''; + document.getElementById('footer-center').value = settings.footer.center || ''; + document.getElementById('footer-right').value = settings.footer.right || ''; + + // Logo previews + if (settings.header.logo) { + document.getElementById('header-logo-preview').textContent = path.basename(settings.header.logo); + } else { + document.getElementById('header-logo-preview').textContent = ''; + } + + if (settings.footer.logo) { + document.getElementById('footer-logo-preview').textContent = path.basename(settings.footer.logo); + } else { + document.getElementById('footer-logo-preview').textContent = ''; + } + + // Update config content visibility + toggleConfigContent(); +}); + +// Toggle config content based on enabled checkbox +function toggleConfigContent() { + const enabled = document.getElementById('hf-enabled').checked; + const configContent = document.getElementById('hf-config-content'); + + if (enabled) { + configContent.classList.remove('disabled'); + } else { + configContent.classList.add('disabled'); + } +} + +// Save header/footer settings +function saveHeaderFooterSettings() { + const settings = { + enabled: document.getElementById('hf-enabled').checked, + header: { + left: document.getElementById('header-left').value || '', + center: document.getElementById('header-center').value || '', + right: document.getElementById('header-right').value || '', + logo: null // Logo paths are managed separately + }, + footer: { + left: document.getElementById('footer-left').value || '', + center: document.getElementById('footer-center').value || '', + right: document.getElementById('footer-right').value || '', + logo: null + } + }; + + ipcRenderer.send('save-header-footer-settings', settings); + closeHeaderFooterDialog(); +} + +// Handle logo browsing - ask main process to show open dialog +function browseForLogo(position) { + ipcRenderer.send('browse-header-footer-logo', position); +} + +// Handle logo saved confirmation +ipcRenderer.on('header-footer-logo-saved', (event, { position, path }) => { + document.getElementById(`${position}-logo-preview`).textContent = path.split(/[\\/]/).pop(); +}); + +// Clear logo +function clearLogo(position) { + ipcRenderer.send('clear-header-footer-logo', position); + document.getElementById(`${position}-logo-preview`).textContent = ''; +} + +// Handle logo cleared confirmation +ipcRenderer.on('header-footer-logo-cleared', (event, position) => { + console.log(`${position} logo cleared`); +}); + +// Insert dynamic field into input +function insertDynamicField(field) { + if (currentFieldTarget) { + const input = document.getElementById(currentFieldTarget); + const cursorPos = input.selectionStart; + const textBefore = input.value.substring(0, cursorPos); + const textAfter = input.value.substring(cursorPos); + + input.value = textBefore + field + textAfter; + input.focus(); + input.setSelectionRange(cursorPos + field.length, cursorPos + field.length); + } + + closeFieldPickerDialog(); +} + +// Event Listeners for Header/Footer Dialog + +// Close buttons +document.getElementById('header-footer-close').addEventListener('click', closeHeaderFooterDialog); +document.getElementById('header-footer-cancel').addEventListener('click', closeHeaderFooterDialog); +document.getElementById('header-footer-save').addEventListener('click', saveHeaderFooterSettings); + +// Enable/disable checkbox +document.getElementById('hf-enabled').addEventListener('change', toggleConfigContent); + +// Field insert buttons +document.querySelectorAll('.field-insert-btn').forEach(btn => { + btn.addEventListener('click', () => { + const target = btn.getAttribute('data-target'); + openFieldPickerDialog(target); + }); +}); + +// Logo browse buttons +document.getElementById('header-logo-browse').addEventListener('click', () => browseForLogo('header')); +document.getElementById('footer-logo-browse').addEventListener('click', () => browseForLogo('footer')); + +// Logo clear buttons +document.getElementById('header-logo-clear').addEventListener('click', () => clearLogo('header')); +document.getElementById('footer-logo-clear').addEventListener('click', () => clearLogo('footer')); + +// Field picker dialog +document.getElementById('field-picker-close').addEventListener('click', closeFieldPickerDialog); +document.querySelectorAll('.field-option').forEach(btn => { + btn.addEventListener('click', () => { + const field = btn.getAttribute('data-field'); + insertDynamicField(field); + }); +}); + +// Export function to make openHeaderFooterDialog accessible globally +window.openHeaderFooterDialog = openHeaderFooterDialog; + +// Listen for menu command to open dialog +ipcRenderer.on('open-header-footer-dialog', () => { + openHeaderFooterDialog(); +}); +// Command Palette Implementation +const commands = [ + { name: 'New File', action: () => tabManager.createTab(), shortcut: 'Ctrl+N' }, + { name: 'Open File', action: () => ipcRenderer.send('open-file'), shortcut: 'Ctrl+O' }, + { name: 'Save File', action: () => ipcRenderer.send('save-file'), shortcut: 'Ctrl+S' }, + { name: 'Save As', action: () => ipcRenderer.send('save-file-as'), shortcut: 'Ctrl+Shift+S' }, + { name: 'Export to PDF', action: () => ipcRenderer.send('export', 'pdf'), shortcut: '' }, + { name: 'Export to DOCX', action: () => ipcRenderer.send('export', 'docx'), shortcut: '' }, + { name: 'Export to HTML', action: () => ipcRenderer.send('export', 'html'), shortcut: '' }, + { name: 'Toggle Preview', action: () => tabManager.togglePreview(), shortcut: 'Ctrl+Shift+P' }, + { name: 'Toggle Line Numbers', action: () => tabManager.toggleLineNumbers(), shortcut: '' }, + { name: 'Find & Replace', action: () => showFindDialog(), shortcut: 'Ctrl+F' }, + { name: 'New Tab', action: () => tabManager.createTab(), shortcut: 'Ctrl+T' }, + { name: 'Close Tab', action: () => tabManager.closeTab(tabManager.activeTabId), shortcut: 'Ctrl+W' }, + { name: 'Undo', action: () => tabManager.undo(), shortcut: 'Ctrl+Z' }, + { name: 'Redo', action: () => tabManager.redo(), shortcut: 'Ctrl+Shift+Z' }, + { name: 'Bold', action: () => insertMarkdown('**', '**'), shortcut: 'Ctrl+B' }, + { name: 'Italic', action: () => insertMarkdown('*', '*'), shortcut: 'Ctrl+I' }, + { name: 'Heading 1', action: () => insertMarkdown('# ', ''), shortcut: '' }, + { name: 'Heading 2', action: () => insertMarkdown('## ', ''), shortcut: '' }, + { name: 'Heading 3', action: () => insertMarkdown('### ', ''), shortcut: '' }, + { name: 'Insert Link', action: () => insertMarkdown('[', '](url)'), shortcut: '' }, + { name: 'Insert Image', action: () => insertMarkdown('![', '](image.jpg)'), shortcut: '' }, + { name: 'Insert Code Block', action: () => insertMarkdown('```\n', '\n```'), shortcut: '' }, + { name: 'Insert Table', action: () => insertMarkdown('\n| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |\n', ''), shortcut: '' }, + { name: 'Table Generator', action: () => showTableGenerator(), shortcut: '' }, + { name: 'ASCII Art Generator', action: () => showASCIIGenerator(), shortcut: '' }, + { name: 'Increase Font Size', action: () => ipcRenderer.send('adjust-font-size', 'increase'), shortcut: 'Ctrl+Shift++' }, + { name: 'Decrease Font Size', action: () => ipcRenderer.send('adjust-font-size', 'decrease'), shortcut: 'Ctrl+Shift+-' } +]; + +let commandPaletteSelectedIndex = 0; +let filteredCommands = [...commands]; + +function showCommandPalette() { + const palette = document.getElementById('command-palette'); + const searchInput = document.getElementById('command-search'); + + palette.classList.remove('hidden'); + searchInput.value = ''; + searchInput.focus(); + + filteredCommands = [...commands]; + commandPaletteSelectedIndex = 0; + renderCommandList(); +} + +function hideCommandPalette() { + const palette = document.getElementById('command-palette'); + palette.classList.add('hidden'); +} + +function renderCommandList() { + const list = document.getElementById('command-list'); + list.innerHTML = ''; + + filteredCommands.forEach((cmd, index) => { + const item = document.createElement('div'); + item.className = 'command-item'; + if (index === commandPaletteSelectedIndex) { + item.classList.add('selected'); + } + + const name = document.createElement('span'); + name.className = 'command-name'; + name.textContent = cmd.name; + + const shortcut = document.createElement('span'); + shortcut.className = 'command-shortcut'; + shortcut.textContent = cmd.shortcut || ''; + + item.appendChild(name); + if (cmd.shortcut) { + item.appendChild(shortcut); + } + + item.addEventListener('click', () => { + executeCommand(cmd); + }); + + list.appendChild(item); + }); +} + +function filterCommands(query) { + const lowerQuery = query.toLowerCase(); + filteredCommands = commands.filter(cmd => + cmd.name.toLowerCase().includes(lowerQuery) + ); + commandPaletteSelectedIndex = 0; + renderCommandList(); +} + +function executeCommand(cmd) { + hideCommandPalette(); + if (cmd && cmd.action) { + cmd.action(); + } +} + +function navigateCommandPalette(direction) { + commandPaletteSelectedIndex += direction; + if (commandPaletteSelectedIndex < 0) { + commandPaletteSelectedIndex = filteredCommands.length - 1; + } + if (commandPaletteSelectedIndex >= filteredCommands.length) { + commandPaletteSelectedIndex = 0; + } + renderCommandList(); +} + +// Command Palette Event Listeners +document.addEventListener('keydown', (e) => { + // Ctrl+Shift+P to open command palette + if (e.ctrlKey && e.shiftKey && e.key === 'P') { + e.preventDefault(); + showCommandPalette(); + return; + } + + // Handle command palette navigation when open + const palette = document.getElementById('command-palette'); + if (!palette.classList.contains('hidden')) { + if (e.key === 'Escape') { + e.preventDefault(); + hideCommandPalette(); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + navigateCommandPalette(1); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + navigateCommandPalette(-1); + } else if (e.key === 'Enter') { + e.preventDefault(); + if (filteredCommands[commandPaletteSelectedIndex]) { + executeCommand(filteredCommands[commandPaletteSelectedIndex]); + } + } + } +}); + +document.getElementById('command-search').addEventListener('input', (e) => { + filterCommands(e.target.value); +}); + +// Close on backdrop click +document.getElementById('command-palette').addEventListener('click', (e) => { + if (e.target === document.getElementById('command-palette')) { + hideCommandPalette(); + } +}); + +// ============================================================================ +// TABLE GENERATOR +// ============================================================================ + +function showTableGenerator() { + const dialog = document.getElementById('table-generator-dialog'); + dialog.classList.remove('hidden'); + + // Generate initial preview + generateTablePreview(); + + // Focus on rows input + setTimeout(() => { + document.getElementById('table-rows').focus(); + }, 100); +} + +function hideTableGenerator() { + const dialog = document.getElementById('table-generator-dialog'); + dialog.classList.add('hidden'); +} + +function generateTablePreview() { + const rows = parseInt(document.getElementById('table-rows').value) || 3; + const cols = parseInt(document.getElementById('table-cols').value) || 3; + const hasHeader = document.getElementById('table-has-header').checked; + const alignment = document.getElementById('table-alignment').value; + + const table = generateMarkdownTable(rows, cols, hasHeader, alignment); + document.getElementById('table-preview').textContent = table; +} + +function generateMarkdownTable(rows, cols, hasHeader, alignment) { + let table = ''; + + // Generate alignment string + let alignChar = '-'; + if (alignment === 'center') { + alignChar = ':' + '-'.repeat(Math.max(8, 10)) + ':'; + } else if (alignment === 'right') { + alignChar = '-'.repeat(Math.max(8, 10)) + ':'; + } else { + alignChar = '-'.repeat(Math.max(8, 10)); + } + + // Generate header row + if (hasHeader) { + table += '| '; + for (let c = 1; c <= cols; c++) { + table += `Header ${c}`; + if (c < cols) table += ' | '; + } + table += ' |\n'; + + // Separator row + table += '| '; + for (let c = 1; c <= cols; c++) { + table += alignChar; + if (c < cols) table += ' | '; + } + table += ' |\n'; + + // Data rows (excluding header) + for (let r = 1; r < rows; r++) { + table += '| '; + for (let c = 1; c <= cols; c++) { + table += `Cell ${r},${c}`; + if (c < cols) table += ' | '; + } + table += ' |\n'; + } + } else { + // No header - all rows are data rows + // First row (acts as separator position) + table += '| '; + for (let c = 1; c <= cols; c++) { + table += `Cell 1,${c}`; + if (c < cols) table += ' | '; + } + table += ' |\n'; + + // Separator row + table += '| '; + for (let c = 1; c <= cols; c++) { + table += alignChar; + if (c < cols) table += ' | '; + } + table += ' |\n'; + + // Remaining data rows + for (let r = 2; r <= rows; r++) { + table += '| '; + for (let c = 1; c <= cols; c++) { + table += `Cell ${r},${c}`; + if (c < cols) table += ' | '; + } + table += ' |\n'; + } + } + + return table; +} + +function insertGeneratedTable() { + const table = document.getElementById('table-preview').textContent; + + if (!table) { + alert('Please generate a table preview first'); + return; + } + + // Get active editor + const activeTabId = tabManager ? tabManager.activeTabId : 1; + const editor = document.getElementById(`editor-${activeTabId}`); + + if (!editor) { + alert('No active editor found'); + return; + } + + // Insert table at cursor position + const start = editor.selectionStart; + const before = editor.value.substring(0, start); + const after = editor.value.substring(editor.selectionEnd); + + // Add newlines for spacing + const tableWithSpacing = '\n' + table + '\n'; + editor.value = before + tableWithSpacing + after; + + // Update cursor position + editor.selectionStart = editor.selectionEnd = start + tableWithSpacing.length; + editor.focus(); + + // Trigger update + if (tabManager) { + tabManager.handleEditorInput(activeTabId); + } + + // Close dialog + hideTableGenerator(); +} + +// Table Generator Event Listeners +document.getElementById('table-dialog-close').addEventListener('click', hideTableGenerator); +document.getElementById('table-cancel').addEventListener('click', hideTableGenerator); +document.getElementById('table-generate-preview').addEventListener('click', generateTablePreview); +document.getElementById('table-insert').addEventListener('click', insertGeneratedTable); + +// Auto-update preview when inputs change +document.getElementById('table-rows').addEventListener('input', generateTablePreview); +document.getElementById('table-cols').addEventListener('input', generateTablePreview); +document.getElementById('table-has-header').addEventListener('change', generateTablePreview); +document.getElementById('table-alignment').addEventListener('change', generateTablePreview); + +// Close dialog on backdrop click +document.getElementById('table-generator-dialog').addEventListener('click', (e) => { + if (e.target === document.getElementById('table-generator-dialog')) { + hideTableGenerator(); + } +}); + +// Handle Enter key in inputs +document.getElementById('table-rows').addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + insertGeneratedTable(); + } +}); + +document.getElementById('table-cols').addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + insertGeneratedTable(); + } +}); + +// ============================================================================ +// ASCII ART GENERATOR +// ============================================================================ + +let currentASCIIMode = 'text'; + +function showASCIIGenerator() { + const dialog = document.getElementById('ascii-art-dialog'); + dialog.classList.remove('hidden'); + + // Initialize with text mode + switchASCIIMode('text'); + + // Generate initial preview + setTimeout(() => { + generateASCIIPreview(); + }, 100); +} + +function hideASCIIGenerator() { + const dialog = document.getElementById('ascii-art-dialog'); + dialog.classList.add('hidden'); +} + +function switchASCIIMode(mode) { + currentASCIIMode = mode; + + // Update button states + document.querySelectorAll('.ascii-mode-btn').forEach(btn => { + btn.classList.remove('active'); + }); + + // Hide all mode sections + document.querySelectorAll('.ascii-mode-section').forEach(section => { + section.classList.add('hidden'); + }); + + // Show selected mode + if (mode === 'text') { + document.getElementById('ascii-mode-text').classList.add('active'); + document.getElementById('ascii-text-mode').classList.remove('hidden'); + } else if (mode === 'box') { + document.getElementById('ascii-mode-box').classList.add('active'); + document.getElementById('ascii-box-mode').classList.remove('hidden'); + } else if (mode === 'templates') { + document.getElementById('ascii-mode-templates').classList.add('active'); + document.getElementById('ascii-templates-mode').classList.remove('hidden'); + } + + // Generate preview + generateASCIIPreview(); +} + +function generateASCIIPreview() { + let result = ''; + + if (currentASCIIMode === 'text') { + const text = document.getElementById('ascii-text-input').value || 'Sample'; + const style = document.getElementById('ascii-font-style').value; + result = textToASCII(text, style); + } else if (currentASCIIMode === 'box') { + const text = document.getElementById('ascii-box-text').value || 'Sample Text'; + const style = document.getElementById('ascii-box-style').value; + const padding = parseInt(document.getElementById('ascii-box-padding').value) || 2; + result = createASCIIBox(text, style, padding); + } else if (currentASCIIMode === 'templates') { + result = 'Select a template from the buttons above'; + } + + document.getElementById('ascii-preview').textContent = result; +} + +// Simple ASCII art text generator +function textToASCII(text, style) { + const fonts = { + standard: { + height: 5, + chars: { + 'A': [' _ ', ' / \\ ', '/___\\', '| |', '| |'], + 'B': ['____ ', '| \\', '|___/', '| \\', '|___/'], + 'C': [' ___ ', '/ _/', '| \\', '| ', '\\___/'], + 'D': ['____ ', '| \\', '| |', '| |', '|___/'], + 'E': ['_____', '| ', '|___ ', '| ', '|____'], + 'F': ['_____', '| ', '|___ ', '| ', '| '], + 'G': [' ___ ', '/ _', '| ||', '| |', '\\___/'], + 'H': ['| |', '| |', '|___|', '| |', '| |'], + 'I': ['_____', ' | ', ' | ', ' | ', '_____'], + 'J': ['_____', ' | ', ' | ', '| | ', '\\__/ '], + 'K': ['| |', '| / ', '|_/ ', '| \\ ', '| \\ '], + 'L': ['| ', '| ', '| ', '| ', '|____'], + 'M': ['| |', '|\\ /|', '| V |', '| |', '| |'], + 'N': ['| |', '|\\ |', '| \\ |', '| \\|', '| |'], + 'O': [' ___ ', '/ \\', '| |', '| |', '\\___/'], + 'P': ['____ ', '| \\', '|___/', '| ', '| '], + 'Q': [' ___ ', '/ \\', '| |', '| |\\ ', '\\__\\|'], + 'R': ['____ ', '| \\', '|___/', '| \\ ', '| \\ '], + 'S': [' ___ ', '/ _/', '\\_ \\', ' \\ ', '\\___/'], + 'T': ['_____', ' | ', ' | ', ' | ', ' | '], + 'U': ['| |', '| |', '| |', '| |', '\\___/'], + 'V': ['| |', '| |', '| |', ' \\ / ', ' V '], + 'W': ['| |', '| |', '| W |', '|/ \\|', '| |'], + 'X': ['| |', ' \\ / ', ' X ', ' / \\ ', '| |'], + 'Y': ['| |', ' \\ / ', ' Y ', ' | ', ' | '], + 'Z': ['_____', ' / ', ' / ', ' / ', '/___ '], + ' ': [' ', ' ', ' ', ' ', ' '], + '0': [' ___ ', '/ \\', '| | |', '| |', '\\___/'], + '1': [' | ', ' || ', ' | ', ' | ', ' _|_ '], + '2': [' ___ ', '\\ /', ' __/ ', '/ ', '\\____'], + '3': [' ___ ', '\\ /', ' __/ ', ' / ', '\\___/'], + '4': ['| |', '| |', '|___|', ' |', ' |'], + '5': [' ___ ', '| ', '|___ ', ' / ', '\\___/'], + '6': [' ___ ', '/ ', '|___ ', '| |', '\\___/'], + '7': ['_____', ' / ', ' / ', ' / ', '/ '], + '8': [' ___ ', '| |', ' ___ ', '| |', '\\___/'], + '9': [' ___ ', '| |', '\\___|', ' |', '\\___/'] + } + }, + banner: { + height: 7, + chars: { + 'A': [' ### ', ' ## ## ', ' ## ## ', ' ## ## ', ' ####### ', ' ## ## ', ' ## ## '], + 'B': [' ###### ', ' ## ## ', ' ## ## ', ' ###### ', ' ## ## ', ' ## ## ', ' ###### '], + 'C': [' ##### ', ' ## ## ', ' ## ', ' ## ', ' ## ', ' ## ## ', ' ##### '], + 'D': [' ###### ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ###### '], + 'E': [' ####### ', ' ## ', ' ## ', ' ##### ', ' ## ', ' ## ', ' ####### '], + 'F': [' ####### ', ' ## ', ' ## ', ' ##### ', ' ## ', ' ## ', ' ## '], + 'G': [' ##### ', ' ## ## ', ' ## ', ' ## ### ', ' ## ## ', ' ## ## ', ' ##### '], + 'H': [' ## ## ', ' ## ## ', ' ## ## ', ' ####### ', ' ## ## ', ' ## ## ', ' ## ## '], + 'I': [' ####### ', ' ### ', ' ### ', ' ### ', ' ### ', ' ### ', ' ####### '], + 'J': [' ####### ', ' ## ', ' ## ', ' ## ', ' ## ## ', ' ## ## ', ' #### '], + 'K': [' ## ## ', ' ## ## ', ' ## ## ', ' #### ', ' ## ## ', ' ## ## ', ' ## ## '], + 'L': [' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ####### '], + 'M': [' ## ## ', ' ### ### ', ' ####### ', ' ## # ## ', ' ## ## ', ' ## ## ', ' ## ## '], + 'N': [' ## ## ', ' ### ## ', ' #### ## ', ' ## #### ', ' ## ### ', ' ## ## ', ' ## ## '], + 'O': [' ##### ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ##### '], + 'P': [' ###### ', ' ## ## ', ' ## ## ', ' ###### ', ' ## ', ' ## ', ' ## '], + 'Q': [' ##### ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## # ## ', ' ## ## ', ' ### ## '], + 'R': [' ###### ', ' ## ## ', ' ## ## ', ' ###### ', ' ## ## ', ' ## ## ', ' ## ## '], + 'S': [' ##### ', ' ## ## ', ' ## ', ' ##### ', ' ## ', ' ## ## ', ' ##### '], + 'T': [' ####### ', ' ### ', ' ### ', ' ### ', ' ### ', ' ### ', ' ### '], + 'U': [' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ##### '], + 'V': [' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ## ## ', ' ### '], + 'W': [' ## ## ', ' ## ## ', ' ## ## ', ' ## # ## ', ' ####### ', ' ### ### ', ' ## ## '], + 'X': [' ## ## ', ' ## ## ', ' ### ', ' # ', ' ### ', ' ## ## ', ' ## ## '], + 'Y': [' ## ## ', ' ## ## ', ' ### ', ' # ', ' # ', ' # ', ' # '], + 'Z': [' ####### ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ####### '], + ' ': [' ', ' ', ' ', ' ', ' ', ' ', ' '], + '0': [' ##### ', ' ## ## ', ' ## ### ', ' ## #### ', ' ### ## ', ' ## ## ', ' ##### '], + '1': [' ## ', ' ### ', ' ## ', ' ## ', ' ## ', ' ## ', ' ####### '], + '2': [' ##### ', ' ## ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ####### '], + '3': [' ##### ', ' ## ## ', ' ## ', ' ### ', ' ## ', ' ## ## ', ' ##### '], + '4': [' ### ', ' #### ', ' ## ## ', ' ## ## ', ' ####### ', ' ## ', ' ## '], + '5': [' ####### ', ' ## ', ' ###### ', ' ## ', ' ## ', ' ## ## ', ' ##### '], + '6': [' ##### ', ' ## ## ', ' ## ', ' ###### ', ' ## ## ', ' ## ## ', ' ##### '], + '7': [' ####### ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## ', ' ## '], + '8': [' ##### ', ' ## ## ', ' ## ## ', ' ##### ', ' ## ## ', ' ## ## ', ' ##### '], + '9': [' ##### ', ' ## ## ', ' ## ## ', ' ###### ', ' ## ', ' ## ## ', ' ##### '] + } + }, + block: { + height: 6, + chars: { + 'A': ['█████╗ ', '██╔══██╗', '███████║', '██╔══██║', '██║ ██║', '╚═╝ ╚═╝'], + 'B': ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔══██╗', '██████╔╝', '╚═════╝ '], + 'C': ['██████╗', '██╔════╝', '██║ ', '██║ ', '╚██████╗', ' ╚═════╝'], + 'D': ['██████╗ ', '██╔══██╗', '██║ ██║', '██║ ██║', '██████╔╝', '╚═════╝ '], + 'E': ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '███████╗', '╚══════╝'], + 'F': ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '██║ ', '╚═╝ '], + 'G': ['██████╗ ', '██╔════╝', '██║ ███╗', '██║ ██║', '╚██████╔╝', ' ╚═════╝ '], + 'H': ['██╗ ██╗', '██║ ██║', '███████║', '██╔══██║', '██║ ██║', '╚═╝ ╚═╝'], + 'I': ['██╗', '██║', '██║', '██║', '██║', '╚═╝'], + ' ': [' ', ' ', ' ', ' ', ' ', ' '] + } + }, + bubble: { + height: 5, + chars: { + 'A': [' Ⓐ ', ' ⒜ ⒜ ', '⒜⒜⒜⒜⒜', '⒜ ⒜', '⒜ ⒜'], + 'B': ['ⒷⒷⒷⒷ ', 'Ⓑ Ⓑ', 'ⒷⒷⒷⒷ ', 'Ⓑ Ⓑ', 'ⒷⒷⒷⒷ '], + 'C': [' ⒸⒸⒸ ', 'Ⓒ ', 'Ⓒ ', 'Ⓒ ', ' ⒸⒸⒸ '], + ' ': [' ', ' ', ' ', ' ', ' '] + } + }, + digital: { + height: 7, + chars: { + 'A': [' ▄▀▀▀▄ ', '▐ ▌', '▐▄▄▄▄▌', '▐ ▌', '▐ ▌', '▐ ▌', ' '], + 'B': ['▐▀▀▀▄ ', '▐▄▄▄▀ ', '▐▄▄▄▄ ', '▐ ▌', '▐▄▄▄▀ ', ' ', ' '], + 'C': [' ▄▀▀▀▄', '▐ ', '▐ ', '▐ ', ' ▀▄▄▄▀', ' ', ' '], + ' ': [' ', ' ', ' ', ' ', ' ', ' ', ' '] + } + } + }; + + // Use selected font or fallback to standard + const font = fonts[style] || fonts.standard; + const lines = []; + const upperText = text.toUpperCase(); + + for (let i = 0; i < font.height; i++) { + lines[i] = ''; + } + + for (let char of upperText) { + const charLines = font.chars[char] || font.chars[' ']; + if (charLines) { + for (let i = 0; i < font.height; i++) { + lines[i] += (charLines[i] || ' '.repeat(font.chars[' '][i].length)); + } + } + } + + return lines.join('\n'); +} + +// Create ASCII box/frame +function createASCIIBox(text, style, padding) { + const styles = { + single: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' }, + double: { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' }, + rounded: { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' }, + bold: { tl: '┏', tr: '┓', bl: '┗', br: '┛', h: '━', v: '┃' }, + ascii: { tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|' } + }; + + const chars = styles[style] || styles.single; + const lines = text.split('\n'); + const maxLength = Math.max(...lines.map(l => l.length)); + const width = maxLength + (padding * 2); + + let result = ''; + + // Top border + result += chars.tl + chars.h.repeat(width) + chars.tr + '\n'; + + // Padding rows above + for (let i = 0; i < padding - 1; i++) { + result += chars.v + ' '.repeat(width) + chars.v + '\n'; + } + + // Content lines + for (let line of lines) { + const paddedLine = line.padEnd(maxLength, ' '); + result += chars.v + ' '.repeat(padding) + paddedLine + ' '.repeat(padding) + chars.v + '\n'; + } + + // Padding rows below + for (let i = 0; i < padding - 1; i++) { + result += chars.v + ' '.repeat(width) + chars.v + '\n'; + } + + // Bottom border + result += chars.bl + chars.h.repeat(width) + chars.br; + + return result; +} + +// ASCII Art Templates +function getASCIITemplate(templateName) { + const templates = { + 'arrow-right': ` + ┌────────┐ + │ │ +────► NEXT │ + │ │ + └────────┘`, + 'arrow-down': ` + │ + │ + ▼ + ┌──────┐ + │ NEXT │ + └──────┘`, + 'check': ` + ✓ Task completed + ✓ All tests passed + ✓ Ready to deploy + ✓ Documentation updated`, + 'divider': ` +═══════════════════════════════════════════════════════════ +`, + 'header': ` +╔═══════════════════════════════════════════════════╗ +║ SECTION HEADER ║ +╚═══════════════════════════════════════════════════╝`, + 'flowchart': ` +┌─────────────┐ +│ START │ +└──────┬──────┘ + │ + ▼ +┌─────────────┐ +│ PROCESS │ +└──────┬──────┘ + │ + ┌───┴───┐ + │ Check │ + └───┬───┘ + │ + ┌───▼──┬──────┐ + │ Yes │ No │ + ▼ ▼ │ +┌────┐ ┌────┐ │ +│ A │ │ B │ │ +└─┬──┘ └─┬──┘ │ + │ │ │ + └──┬───┘ │ + │◄──────────┘ + ▼ +┌─────────────┐ +│ END │ +└─────────────┘`, + 'decision': ` + ┌─────────┐ + │ Check? │ + └────┬────┘ + │ + ┌─────┴─────┐ + │ │ + Yes│ │No + ▼ ▼ + ┌────────┐ ┌────────┐ + │ True │ │ False │ + └────────┘ └────────┘`, + 'sequence': ` + User System Database + │ │ │ + │ Request │ │ + ├──────────►│ │ + │ │ Query │ + │ ├──────────►│ + │ │ │ + │ │ Result │ + │ │◄──────────┤ + │ Response │ │ + │◄──────────┤ │ + │ │ │`, + 'table-simple': ` +┌──────────┬──────────┬──────────┐ +│ Header 1 │ Header 2 │ Header 3 │ +├──────────┼──────────┼──────────┤ +│ Data 1 │ Data 2 │ Data 3 │ +├──────────┼──────────┼──────────┤ +│ Data 4 │ Data 5 │ Data 6 │ +└──────────┴──────────┴──────────┘`, + 'timeline': ` + 2020 2021 2022 2023 + │ │ │ │ + ●───────────●───────────●───────────● + │ │ │ │ + Start Milestone Release Current`, + 'network': ` + ┌──────────┐ + │ Server │ + └────┬─────┘ + │ + ┌─────────┼─────────┐ + │ │ │ + ┌───▼───┐ ┌──▼────┐ ┌──▼────┐ + │Client1│ │Client2│ │Client3│ + └───────┘ └───────┘ └───────┘`, + 'hierarchy': ` + ┌─────────┐ + │ Root │ + └────┬────┘ + │ + ┌────────┴────────┐ + │ │ +┌───▼───┐ ┌───▼───┐ +│Child 1│ │Child 2│ +└───┬───┘ └───┬───┘ + │ │ +┌───▼───┐ ┌───▼───┐ +│Grand 1│ │Grand 2│ +└───────┘ └───────┘`, + 'process-flow': ` +┌─────────┐ ┌─────────┐ ┌─────────┐ +│ Input │────►│Process 1│────►│ Output │ +└─────────┘ └────┬────┘ └─────────┘ + │ + ┌────▼────┐ + │Process 2│ + └─────────┘`, + 'note-box': ` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ NOTE: ┃ +┃ This is an important note ┃ +┃ that requires attention! ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛`, + 'warning-box': ` +╔════════════════════════════════════╗ +║ ⚠ WARNING ║ +║ Proceed with caution! ║ +╚════════════════════════════════════╝`, + 'info-box': ` +╭────────────────────────────────────╮ +│ ℹ Information │ +│ Additional context here │ +╰────────────────────────────────────╯`, + 'separator-fancy': ` +╭──────────────────────────────────────────────────────╮ +│ │ +╰──────────────────────────────────────────────────────╯`, + 'brackets': ` +【 Important Text 】`, + 'banner-stars': ` +************************************************************************ +* IMPORTANT BANNER * +************************************************************************`, + 'progress-bar': ` +Progress: [████████████░░░░░░░░] 60% + 0% 100%` + }; + + return templates[templateName] || ''; +} + +function loadASCIITemplate(templateName) { + const template = getASCIITemplate(templateName); + document.getElementById('ascii-preview').textContent = template; +} + +function insertASCIIArt() { + const asciiArt = document.getElementById('ascii-preview').textContent; + + if (!asciiArt || asciiArt === 'Select a template from the buttons above') { + alert('Please generate ASCII art first'); + return; + } + + // Get active editor + const activeTabId = tabManager ? tabManager.activeTabId : 1; + const editor = document.getElementById(`editor-${activeTabId}`); + + if (!editor) { + alert('No active editor found'); + return; + } + + // Insert in code block for proper rendering + const start = editor.selectionStart; + const before = editor.value.substring(0, start); + const after = editor.value.substring(editor.selectionEnd); + + const codeBlock = '\n```\n' + asciiArt + '\n```\n'; + editor.value = before + codeBlock + after; + + // Update cursor position + editor.selectionStart = editor.selectionEnd = start + codeBlock.length; + editor.focus(); + + // Trigger update + if (tabManager) { + tabManager.handleEditorInput(activeTabId); + } + + // Close dialog + hideASCIIGenerator(); +} + +// ASCII Art Event Listeners +document.getElementById('ascii-dialog-close').addEventListener('click', hideASCIIGenerator); +document.getElementById('ascii-cancel').addEventListener('click', hideASCIIGenerator); +document.getElementById('ascii-generate').addEventListener('click', generateASCIIPreview); +document.getElementById('ascii-insert').addEventListener('click', insertASCIIArt); + +// Mode switching +document.getElementById('ascii-mode-text').addEventListener('click', () => switchASCIIMode('text')); +document.getElementById('ascii-mode-box').addEventListener('click', () => switchASCIIMode('box')); +document.getElementById('ascii-mode-templates').addEventListener('click', () => switchASCIIMode('templates')); + +// Auto-update preview for text mode +document.getElementById('ascii-text-input').addEventListener('input', generateASCIIPreview); +document.getElementById('ascii-font-style').addEventListener('change', generateASCIIPreview); + +// Auto-update preview for box mode +document.getElementById('ascii-box-text').addEventListener('input', generateASCIIPreview); +document.getElementById('ascii-box-style').addEventListener('change', generateASCIIPreview); +document.getElementById('ascii-box-padding').addEventListener('input', generateASCIIPreview); + +// Template buttons +document.querySelectorAll('.ascii-template-btn').forEach(btn => { + btn.addEventListener('click', function() { + const template = this.getAttribute('data-template'); + loadASCIITemplate(template); + }); +}); + +// Close dialog on backdrop click +document.getElementById('ascii-art-dialog').addEventListener('click', (e) => { + if (e.target === document.getElementById('ascii-art-dialog')) { + hideASCIIGenerator(); + } +}); + +// IPC listener for menu +ipcRenderer.on('show-ascii-generator', () => { + showASCIIGenerator(); +}); + +// ============================================================================ +// PANE RESIZING +// ============================================================================ + +let isResizing = false; +let currentResizer = null; + +document.addEventListener('mousedown', (e) => { + if (e.target.classList.contains('pane-resizer')) { + isResizing = true; + currentResizer = e.target; + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + } +}); + +document.addEventListener('mousemove', (e) => { + if (!isResizing) return; + + const container = currentResizer.parentElement; + const editorPane = currentResizer.previousElementSibling; + const previewPane = currentResizer.nextElementSibling; + + const containerRect = container.getBoundingClientRect(); + const containerWidth = containerRect.width; + const mouseX = e.clientX - containerRect.left; + + // Calculate percentages (minimum 20%, maximum 80% for each pane) + let editorPercentage = (mouseX / containerWidth) * 100; + editorPercentage = Math.max(20, Math.min(80, editorPercentage)); + + const previewPercentage = 100 - editorPercentage; + + editorPane.style.flex = `0 0 ${editorPercentage}%`; + previewPane.style.flex = `0 0 ${previewPercentage}%`; +}); + +document.addEventListener('mouseup', () => { + if (isResizing) { + isResizing = false; + currentResizer = null; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + } +}); + +// ============================================================================ +// PREVIEW POP-OUT +// ============================================================================ + +let popoutWindow = null; + +function popoutPreview(tabId) { + const previewContent = document.getElementById(`preview-${tabId}`); + + if (!previewContent) { + alert('No preview content to pop out'); + return; + } + + // Close existing popout if open + if (popoutWindow && !popoutWindow.closed) { + popoutWindow.close(); + } + + // Create new window + popoutWindow = window.open('', 'Preview Window', 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no'); + + if (!popoutWindow) { + alert('Pop-up blocked! Please allow pop-ups for this application.'); + return; + } + + // Write content to popout window + const htmlContent = ` + + + + Preview - PanConverter + + + +
+ ${previewContent.innerHTML} +
+ + + + `; + + popoutWindow.document.write(htmlContent); + popoutWindow.document.close(); + + // Setup auto-update + setupPopoutAutoUpdate(tabId); +} + +function getPreviewStyles() { + // Extract relevant preview styles + return ` + h1, h2, h3, h4, h5, h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; + } + h1 { + font-size: 2em; + border-bottom: 1px solid #eaecef; + padding-bottom: 0.3em; + } + h2 { + font-size: 1.5em; + border-bottom: 1px solid #eaecef; + padding-bottom: 0.3em; + } + p { + margin-bottom: 16px; + } + code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: #f6f8fa; + border: 1px solid #d0d7de; + border-radius: 6px; + font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; + } + pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #f6f8fa; + border: 1px solid #d0d7de; + border-radius: 6px; + margin-bottom: 16px; + } + pre code { + background: transparent; + border: none; + padding: 0; + } + table { + border-collapse: collapse; + margin-bottom: 16px; + width: 100%; + } + table th, table td { + padding: 6px 13px; + border: 1px solid #dfe2e5; + } + table th { + font-weight: 600; + background-color: #f6f8fa; + } + a { + color: #0366d6; + text-decoration: none; + } + a:hover { + text-decoration: underline; + } + blockquote { + padding: 0 1em; + color: #6a737d; + border-left: 0.25em solid #dfe2e5; + margin-bottom: 16px; + } + img { + max-width: 100%; + height: auto; + } + ul, ol { + padding-left: 2em; + margin-bottom: 16px; + } + li { + margin-bottom: 4px; + } + `; +} + +function setupPopoutAutoUpdate(tabId) { + // Monitor for preview changes and update popout + const observer = new MutationObserver(() => { + if (popoutWindow && !popoutWindow.closed) { + const previewContent = document.getElementById(`preview-${tabId}`); + popoutWindow.postMessage({ + type: 'preview-update', + content: previewContent.innerHTML + }, '*'); + } else { + observer.disconnect(); + } + }); + + const previewElement = document.getElementById(`preview-${tabId}`); + if (previewElement) { + observer.observe(previewElement, { + childList: true, + subtree: true, + characterData: true + }); + } +} + +// Pop-out button event listener +document.addEventListener('click', (e) => { + if (e.target.classList.contains('preview-popout-btn')) { + const tabId = e.target.id.replace('preview-popout-', ''); + popoutPreview(tabId); + } +}); + +// ============================================ +// Insert Content from Generator Windows +// ============================================ +ipcRenderer.on('insert-content', (event, content) => { + if (tabManager) { + const activeTabId = tabManager.activeTabId; + const editor = document.getElementById(`editor-${activeTabId}`); + if (editor) { + const cursorPos = editor.selectionStart; + const textBefore = editor.value.substring(0, cursorPos); + const textAfter = editor.value.substring(cursorPos); + + editor.value = textBefore + content + textAfter; + editor.focus(); + + const newPos = cursorPos + content.length; + editor.setSelectionRange(newPos, newPos); + + // Update state and preview + tabManager.tabs.get(activeTabId).content = editor.value; + tabManager.tabs.get(activeTabId).modified = true; + tabManager.updatePreview(activeTabId); + tabManager.updateTabTitle(activeTabId); + } + } +}); + +// ============================================ +// PDF VIEWER FUNCTIONALITY +// ============================================ + +let pdfDoc = null; +let pdfCurrentPage = 1; +let pdfZoomLevel = 1.0; +let pdfRotation = 0; +let pdfFilePath = null; +let isPdfViewerActive = false; // Track if PDF viewer is currently shown + +// Initialize PDF.js +const pdfjsLib = require('pdfjs-dist'); +pdfjsLib.GlobalWorkerOptions.workerSrc = require.resolve('pdfjs-dist/build/pdf.worker.js'); + +// Open PDF file +async function openPdfFile(filePath) { + // Prevent multiple simultaneous PDF loads + if (isPdfViewerActive && pdfFilePath === filePath) { + console.log('PDF already open:', filePath); + return; + } + + // Close any existing PDF first + if (pdfDoc) { + try { + await pdfDoc.destroy(); + } catch (e) { + console.warn('Error destroying previous PDF:', e); + } + pdfDoc = null; + } + + try { + // Show loading state + document.getElementById('status-text').textContent = 'Loading PDF...'; + + const loadingTask = pdfjsLib.getDocument(filePath); + pdfDoc = await loadingTask.promise; + pdfFilePath = filePath; + pdfCurrentPage = 1; + pdfZoomLevel = 1.0; + pdfRotation = 0; + isPdfViewerActive = true; + + // Update UI + document.getElementById('pdf-total-pages').textContent = pdfDoc.numPages; + document.getElementById('pdf-page-input').value = 1; + document.getElementById('pdf-page-input').max = pdfDoc.numPages; + document.getElementById('pdf-filename').textContent = require('path').basename(filePath); + document.getElementById('pdf-zoom-level').textContent = '100%'; + + // Hide markdown toolbar, tabs, show PDF viewer + document.querySelector('.toolbar').classList.add('hidden'); + document.getElementById('tab-bar').classList.add('hidden'); + document.querySelectorAll('.tab-content').forEach(tc => tc.classList.add('hidden')); + document.getElementById('pdf-viewer-container').classList.remove('hidden'); + + // Render first page + await renderPdfPage(pdfCurrentPage); + + // Update status + document.getElementById('status-text').textContent = `PDF: ${require('path').basename(filePath)} (${pdfDoc.numPages} pages)`; + } catch (error) { + console.error('Error loading PDF:', error); + isPdfViewerActive = false; + pdfDoc = null; + pdfFilePath = null; + document.getElementById('status-text').textContent = 'Error loading PDF'; + alert('Error loading PDF: ' + error.message); + } +} + +// Render PDF page +async function renderPdfPage(pageNum) { + if (!pdfDoc) return; + + try { + const page = await pdfDoc.getPage(pageNum); + const canvas = document.getElementById('pdf-canvas'); + const ctx = canvas.getContext('2d'); + + // Calculate viewport with zoom and rotation + const viewport = page.getViewport({ scale: pdfZoomLevel, rotation: pdfRotation }); + + canvas.width = viewport.width; + canvas.height = viewport.height; + + const renderContext = { + canvasContext: ctx, + viewport: viewport + }; + + await page.render(renderContext).promise; + + // Update page input + document.getElementById('pdf-page-input').value = pageNum; + } catch (error) { + console.error('Error rendering PDF page:', error); + } +} + +// PDF navigation handlers +document.getElementById('pdf-prev-page')?.addEventListener('click', async () => { + if (pdfCurrentPage > 1) { + pdfCurrentPage--; + await renderPdfPage(pdfCurrentPage); + } +}); + +document.getElementById('pdf-next-page')?.addEventListener('click', async () => { + if (pdfDoc && pdfCurrentPage < pdfDoc.numPages) { + pdfCurrentPage++; + await renderPdfPage(pdfCurrentPage); + } +}); + +document.getElementById('pdf-page-input')?.addEventListener('change', async (e) => { + const pageNum = parseInt(e.target.value); + if (pdfDoc && pageNum >= 1 && pageNum <= pdfDoc.numPages) { + pdfCurrentPage = pageNum; + await renderPdfPage(pdfCurrentPage); + } +}); + +// PDF zoom handlers +document.getElementById('pdf-zoom-out')?.addEventListener('click', async () => { + if (pdfZoomLevel > 0.25) { + pdfZoomLevel -= 0.25; + document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%'; + await renderPdfPage(pdfCurrentPage); + } +}); + +document.getElementById('pdf-zoom-in')?.addEventListener('click', async () => { + if (pdfZoomLevel < 4.0) { + pdfZoomLevel += 0.25; + document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%'; + await renderPdfPage(pdfCurrentPage); + } +}); + +document.getElementById('pdf-fit-width')?.addEventListener('click', async () => { + if (!pdfDoc) return; + const page = await pdfDoc.getPage(pdfCurrentPage); + const viewport = page.getViewport({ scale: 1, rotation: pdfRotation }); + const containerWidth = document.getElementById('pdf-viewer').clientWidth - 40; + pdfZoomLevel = containerWidth / viewport.width; + document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%'; + await renderPdfPage(pdfCurrentPage); +}); + +document.getElementById('pdf-fit-page')?.addEventListener('click', async () => { + if (!pdfDoc) return; + const page = await pdfDoc.getPage(pdfCurrentPage); + const viewport = page.getViewport({ scale: 1, rotation: pdfRotation }); + const container = document.getElementById('pdf-viewer'); + const containerWidth = container.clientWidth - 40; + const containerHeight = container.clientHeight - 40; + const scaleX = containerWidth / viewport.width; + const scaleY = containerHeight / viewport.height; + pdfZoomLevel = Math.min(scaleX, scaleY); + document.getElementById('pdf-zoom-level').textContent = Math.round(pdfZoomLevel * 100) + '%'; + await renderPdfPage(pdfCurrentPage); +}); + +// PDF rotation handlers +document.getElementById('pdf-rotate-left')?.addEventListener('click', async () => { + pdfRotation = (pdfRotation - 90 + 360) % 360; + await renderPdfPage(pdfCurrentPage); +}); + +document.getElementById('pdf-rotate-right')?.addEventListener('click', async () => { + pdfRotation = (pdfRotation + 90) % 360; + await renderPdfPage(pdfCurrentPage); +}); + +// Close PDF viewer +document.getElementById('pdf-close')?.addEventListener('click', () => { + closePdfViewer(); +}); + +async function closePdfViewer() { + // Destroy PDF document to free memory + if (pdfDoc) { + try { + await pdfDoc.destroy(); + } catch (e) { + console.warn('Error destroying PDF:', e); + } + } + + pdfDoc = null; + pdfFilePath = null; + isPdfViewerActive = false; + + // Hide PDF viewer + document.getElementById('pdf-viewer-container').classList.add('hidden'); + + // Show markdown tabs, tab bar, and toolbar + document.getElementById('tab-bar').classList.remove('hidden'); + document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('hidden')); + document.querySelector('.toolbar').classList.remove('hidden'); + + // Activate the correct markdown tab + if (tabManager) { + const activeTab = document.querySelector(`.tab-content[data-tab-id="${tabManager.activeTabId}"]`); + if (activeTab) activeTab.classList.add('active'); + // Refresh the active tab + tabManager.updatePreview(tabManager.activeTabId); + } + + document.getElementById('status-text').textContent = 'Ready'; +} + +// Handle PDF file open from main process +ipcRenderer.on('open-pdf-file', (event, filePath) => { + openPdfFile(filePath); +}); + +// ============================================ +// PDF EDITOR TOOLBAR BUTTONS +// ============================================ + +// Helper function to trigger PDF editor dialog +function openPdfEditorDialog(operation) { + // Use the opened PDF file if available, otherwise prompt + if (pdfFilePath) { + ipcRenderer.send('show-pdf-editor-from-toolbar', { operation, filePath: pdfFilePath }); + } else { + ipcRenderer.send('show-pdf-editor-from-toolbar', { operation, filePath: null }); + } +} + +// PDF Editor toolbar button handlers +document.getElementById('pdf-tb-merge')?.addEventListener('click', () => { + openPdfEditorDialog('merge'); +}); + +document.getElementById('pdf-tb-split')?.addEventListener('click', () => { + openPdfEditorDialog('split'); +}); + +document.getElementById('pdf-tb-compress')?.addEventListener('click', () => { + openPdfEditorDialog('compress'); +}); + +document.getElementById('pdf-tb-rotate')?.addEventListener('click', () => { + openPdfEditorDialog('rotate'); +}); + +document.getElementById('pdf-tb-delete')?.addEventListener('click', () => { + openPdfEditorDialog('delete'); +}); + +document.getElementById('pdf-tb-reorder')?.addEventListener('click', () => { + openPdfEditorDialog('reorder'); +}); + +document.getElementById('pdf-tb-watermark')?.addEventListener('click', () => { + openPdfEditorDialog('watermark'); +}); + +document.getElementById('pdf-tb-encrypt')?.addEventListener('click', () => { + openPdfEditorDialog('encrypt'); +}); + +document.getElementById('pdf-tb-decrypt')?.addEventListener('click', () => { + openPdfEditorDialog('decrypt'); +}); + +// ============================================ +// DYNAMIC PANE RESIZER +// ============================================ + +function initPaneResizer(tabId) { + const resizer = document.getElementById(`pane-resizer-${tabId}`); + const editorPane = document.getElementById(`editor-pane-${tabId}`); + const previewPane = document.getElementById(`preview-pane-${tabId}`); + const tabContent = document.getElementById(`tab-content-${tabId}`); + + if (!resizer || !editorPane || !previewPane) return; + + let isResizing = false; + let startX = 0; + let startEditorWidth = 0; + + resizer.addEventListener('mousedown', (e) => { + isResizing = true; + startX = e.clientX; + startEditorWidth = editorPane.offsetWidth; + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + e.preventDefault(); + }); + + document.addEventListener('mousemove', (e) => { + if (!isResizing) return; + + const containerWidth = tabContent.offsetWidth; + const deltaX = e.clientX - startX; + let newEditorWidth = startEditorWidth + deltaX; + + // Minimum widths + const minWidth = 150; + const maxWidth = containerWidth - minWidth - 6; // 6px for resizer + + newEditorWidth = Math.max(minWidth, Math.min(maxWidth, newEditorWidth)); + + const editorPercent = (newEditorWidth / containerWidth) * 100; + const previewPercent = 100 - editorPercent - 1; // 1% for resizer + + editorPane.style.flex = `0 0 ${editorPercent}%`; + previewPane.style.flex = `0 0 ${previewPercent}%`; + }); + + document.addEventListener('mouseup', () => { + if (isResizing) { + isResizing = false; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + } + }); +} + +// Initialize resizer for first tab +document.addEventListener('DOMContentLoaded', () => { + initPaneResizer(1); +}); + +// ============================================ +// IMAGE/AUDIO/VIDEO CONVERTER HANDLERS +// These require external tools (ImageMagick, FFmpeg) +// ============================================ + +ipcRenderer.on('show-image-tool', (event, tool) => { + alert(`Image ${tool} tool requires ImageMagick to be installed.\n\nPlease install ImageMagick from: https://imagemagick.org/\n\nThis feature will be available in a future update with built-in support.`); +}); + +ipcRenderer.on('show-audio-tool', (event, tool) => { + alert(`Audio ${tool} tool requires FFmpeg to be installed.\n\nPlease install FFmpeg from: https://ffmpeg.org/\n\nThis feature will be available in a future update with built-in support.`); +}); + +ipcRenderer.on('show-video-tool', (event, tool) => { + alert(`Video ${tool} tool requires FFmpeg to be installed.\n\nPlease install FFmpeg from: https://ffmpeg.org/\n\nThis feature will be available in a future update with built-in support.`); +}); + diff --git a/src/styles-concreteinfo.css b/src/styles-concreteinfo.css index aa80553..cc5c2e2 100644 --- a/src/styles-concreteinfo.css +++ b/src/styles-concreteinfo.css @@ -1,969 +1,969 @@ -/** - * ConcreteInfo Theme for MarkdownConverter - * Based on logo palette: #464646, #9a9696, #e5461f, #e3e3e3, #0d0b09 - * Version: 3.0.0 - */ - -/* ============================================ - CSS Variables - ConcreteInfo Theme - ============================================ */ -:root { - /* Primary Colors from Logo Palette */ - --ci-dark-gray: #464646; - --ci-medium-gray: #9a9696; - --ci-accent: #e5461f; - --ci-light-gray: #e3e3e3; - --ci-black: #0d0b09; - - /* Extended Palette */ - --ci-accent-hover: #c93a18; - --ci-accent-light: rgba(229, 70, 31, 0.1); - --ci-white: #ffffff; - --ci-bg: #f5f5f5; - --ci-border: #d0d0d0; - - /* Semantic Colors */ - --ci-success: #28a745; - --ci-warning: #ffc107; - --ci-danger: #dc3545; - --ci-info: #17a2b8; - - /* Typography */ - --font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif; - --font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; - - /* Spacing */ - --spacing-xs: 4px; - --spacing-sm: 8px; - --spacing-md: 16px; - --spacing-lg: 24px; - --spacing-xl: 32px; - - /* Border Radius */ - --radius-sm: 4px; - --radius-md: 8px; - --radius-lg: 12px; - --radius-full: 9999px; - - /* Shadows */ - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); - --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); - --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); - --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15); - - /* Transitions */ - --transition-fast: 150ms ease; - --transition-normal: 250ms ease; - --transition-slow: 350ms ease; -} - -/* ============================================ - ConcreteInfo Theme Application - ============================================ */ -body.theme-concreteinfo { - background: var(--ci-bg); - color: var(--ci-dark-gray); -} - -body.theme-concreteinfo .toolbar { - background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%); - border-bottom: 3px solid var(--ci-accent); -} - -body.theme-concreteinfo .toolbar button { - color: var(--ci-white); - background: transparent; - border: 1px solid transparent; - transition: all var(--transition-fast); -} - -body.theme-concreteinfo .toolbar button:hover { - background: var(--ci-accent); - border-color: var(--ci-accent); - transform: translateY(-1px); -} - -body.theme-concreteinfo .tab-bar { - background: var(--ci-dark-gray); - border-bottom: 2px solid var(--ci-accent); -} - -body.theme-concreteinfo .tab { - background: var(--ci-medium-gray); - color: var(--ci-white); - border: none; - margin: 2px; - border-radius: var(--radius-sm) var(--radius-sm) 0 0; - transition: all var(--transition-fast); -} - -body.theme-concreteinfo .tab:hover { - background: var(--ci-accent-light); -} - -body.theme-concreteinfo .tab.active { - background: var(--ci-accent); - color: var(--ci-white); -} - -body.theme-concreteinfo .editor-textarea { - background: var(--ci-white); - color: var(--ci-black); - border: 1px solid var(--ci-border); - font-family: var(--font-mono); -} - -body.theme-concreteinfo .preview-content { - background: var(--ci-white); - color: var(--ci-dark-gray); - border-left: 3px solid var(--ci-accent); -} - -body.theme-concreteinfo .status-bar { - background: var(--ci-dark-gray); - color: var(--ci-light-gray); - border-top: 2px solid var(--ci-accent); -} - -/* ============================================ - Modern Modal Styles - ============================================ */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(13, 11, 9, 0.7); - backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - z-index: 10000; - opacity: 0; - visibility: hidden; - transition: all var(--transition-normal); -} - -.modal-overlay.active { - opacity: 1; - visibility: visible; -} - -.modal-content { - background: var(--ci-white); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-xl); - max-width: 600px; - width: 90%; - max-height: 85vh; - overflow: hidden; - transform: scale(0.9) translateY(20px); - transition: transform var(--transition-normal); -} - -.modal-overlay.active .modal-content { - transform: scale(1) translateY(0); -} - -.modal-header { - background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%); - color: var(--ci-white); - padding: var(--spacing-md) var(--spacing-lg); - display: flex; - align-items: center; - justify-content: space-between; - border-bottom: 3px solid var(--ci-accent); -} - -.modal-header h3 { - margin: 0; - font-size: 1.25rem; - font-weight: 600; - font-family: var(--font-sans); -} - -.modal-close { - background: transparent; - border: none; - color: var(--ci-white); - font-size: 1.5rem; - cursor: pointer; - width: 32px; - height: 32px; - border-radius: var(--radius-full); - display: flex; - align-items: center; - justify-content: center; - transition: all var(--transition-fast); -} - -.modal-close:hover { - background: var(--ci-accent); - transform: rotate(90deg); -} - -.modal-body { - padding: var(--spacing-lg); - overflow-y: auto; - max-height: 60vh; -} - -.modal-footer { - padding: var(--spacing-md) var(--spacing-lg); - background: var(--ci-light-gray); - display: flex; - justify-content: flex-end; - gap: var(--spacing-sm); - border-top: 1px solid var(--ci-border); -} - -/* ============================================ - Button Styles - ============================================ */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--spacing-xs); - padding: var(--spacing-sm) var(--spacing-md); - font-family: var(--font-sans); - font-size: 0.875rem; - font-weight: 500; - border-radius: var(--radius-md); - border: none; - cursor: pointer; - transition: all var(--transition-fast); -} - -.btn-primary { - background: var(--ci-accent); - color: var(--ci-white); -} - -.btn-primary:hover { - background: var(--ci-accent-hover); - transform: translateY(-1px); - box-shadow: var(--shadow-md); -} - -.btn-secondary { - background: var(--ci-medium-gray); - color: var(--ci-white); -} - -.btn-secondary:hover { - background: var(--ci-dark-gray); -} - -.btn-outline { - background: transparent; - border: 2px solid var(--ci-accent); - color: var(--ci-accent); -} - -.btn-outline:hover { - background: var(--ci-accent); - color: var(--ci-white); -} - -.btn-ghost { - background: transparent; - color: var(--ci-dark-gray); -} - -.btn-ghost:hover { - background: var(--ci-light-gray); -} - -/* ============================================ - Form Controls - ============================================ */ -.form-group { - margin-bottom: var(--spacing-md); -} - -.form-label { - display: block; - margin-bottom: var(--spacing-xs); - font-weight: 500; - color: var(--ci-dark-gray); - font-size: 0.875rem; -} - -.form-input, -.form-select { - width: 100%; - padding: var(--spacing-sm) var(--spacing-md); - font-family: var(--font-sans); - font-size: 0.875rem; - border: 2px solid var(--ci-border); - border-radius: var(--radius-md); - background: var(--ci-white); - color: var(--ci-dark-gray); - transition: all var(--transition-fast); -} - -.form-input:focus, -.form-select:focus { - outline: none; - border-color: var(--ci-accent); - box-shadow: 0 0 0 3px var(--ci-accent-light); -} - -.form-input::placeholder { - color: var(--ci-medium-gray); -} - -/* ============================================ - App Header with Logo - ============================================ */ -.app-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 6px 16px; - background: #ffffff; - border-bottom: 2px solid #e1e4e8; - min-height: 36px; -} - -.app-header-left { - display: flex; - align-items: center; - gap: 12px; -} - -.app-header-right { - display: flex; - align-items: center; - gap: 12px; -} - -.app-logo { - height: 22px; - width: auto; -} - -.app-title { - color: #24292e; - font-size: 0.95rem; - font-weight: 600; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; -} - -.app-version { - color: #6a737d; - font-size: 0.7rem; - font-family: 'SFMono-Regular', Consolas, monospace; -} - -/* Dark theme header adjustments */ -body.theme-dark .app-header, -body.theme-dracula .app-header, -body.theme-onedark .app-header, -body.theme-tokyonight .app-header, -body.theme-monokai .app-header, -body.theme-cobalt2 .app-header, -body.theme-gruvbox-dark .app-header, -body.theme-ayu-dark .app-header, -body.theme-ayu-mirage .app-header, -body.theme-palenight .app-header, -body.theme-oceanic-next .app-header, -body.theme-nord .app-header, -body.theme-concrete-dark .app-header { - background: #1f2937; - border-bottom-color: #374151; -} - -body.theme-dark .app-title, -body.theme-dracula .app-title, -body.theme-onedark .app-title, -body.theme-tokyonight .app-title, -body.theme-monokai .app-title, -body.theme-cobalt2 .app-title, -body.theme-gruvbox-dark .app-title, -body.theme-ayu-dark .app-title, -body.theme-ayu-mirage .app-title, -body.theme-palenight .app-title, -body.theme-oceanic-next .app-title, -body.theme-nord .app-title, -body.theme-concrete-dark .app-title { - color: #f3f4f6; -} - -body.theme-dark .app-version, -body.theme-dracula .app-version, -body.theme-onedark .app-version, -body.theme-tokyonight .app-version, -body.theme-monokai .app-version, -body.theme-cobalt2 .app-version, -body.theme-gruvbox-dark .app-version, -body.theme-ayu-dark .app-version, -body.theme-ayu-mirage .app-version, -body.theme-palenight .app-version, -body.theme-oceanic-next .app-version, -body.theme-nord .app-version, -body.theme-concrete-dark .app-version { - color: #9ca3af; -} - -body.theme-dark .app-logo, -body.theme-dracula .app-logo, -body.theme-onedark .app-logo, -body.theme-tokyonight .app-logo, -body.theme-monokai .app-logo, -body.theme-cobalt2 .app-logo, -body.theme-gruvbox-dark .app-logo, -body.theme-ayu-dark .app-logo, -body.theme-ayu-mirage .app-logo, -body.theme-palenight .app-logo, -body.theme-oceanic-next .app-logo, -body.theme-nord .app-logo, -body.theme-concrete-dark .app-logo { - filter: none; -} - -/* ============================================ - Converter Cards - ============================================ */ -.converter-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: var(--spacing-md); - padding: var(--spacing-md); -} - -.converter-card { - background: var(--ci-white); - border-radius: var(--radius-lg); - padding: var(--spacing-lg); - text-align: center; - border: 2px solid var(--ci-border); - transition: all var(--transition-fast); - cursor: pointer; -} - -.converter-card:hover { - border-color: var(--ci-accent); - transform: translateY(-4px); - box-shadow: var(--shadow-lg); -} - -.converter-card-icon { - font-size: 2.5rem; - margin-bottom: var(--spacing-md); -} - -.converter-card-title { - font-weight: 600; - color: var(--ci-dark-gray); - margin-bottom: var(--spacing-xs); -} - -.converter-card-desc { - font-size: 0.875rem; - color: var(--ci-medium-gray); -} - -/* ============================================ - Progress Bar - ============================================ */ -.progress-bar { - height: 8px; - background: var(--ci-light-gray); - border-radius: var(--radius-full); - overflow: hidden; -} - -.progress-fill { - height: 100%; - background: linear-gradient(90deg, var(--ci-accent) 0%, var(--ci-accent-hover) 100%); - border-radius: var(--radius-full); - transition: width var(--transition-normal); -} - -/* ============================================ - File Drop Zone - ============================================ */ -.drop-zone { - border: 2px dashed var(--ci-border); - border-radius: var(--radius-lg); - padding: var(--spacing-xl); - text-align: center; - transition: all var(--transition-fast); - cursor: pointer; -} - -.drop-zone:hover, -.drop-zone.drag-over { - border-color: var(--ci-accent); - background: var(--ci-accent-light); -} - -.drop-zone-icon { - font-size: 3rem; - color: var(--ci-medium-gray); - margin-bottom: var(--spacing-md); -} - -.drop-zone-text { - color: var(--ci-dark-gray); - font-weight: 500; -} - -.drop-zone-hint { - color: var(--ci-medium-gray); - font-size: 0.875rem; - margin-top: var(--spacing-xs); -} - -/* ============================================ - Syntax Highlighting for Editor - ============================================ */ -.editor-syntax .md-heading { - color: var(--ci-accent); - font-weight: bold; -} - -.editor-syntax .md-bold { - color: var(--ci-dark-gray); - font-weight: bold; -} - -.editor-syntax .md-italic { - color: var(--ci-dark-gray); - font-style: italic; -} - -.editor-syntax .md-code { - background: var(--ci-light-gray); - color: var(--ci-accent); - font-family: var(--font-mono); - padding: 2px 4px; - border-radius: var(--radius-sm); -} - -.editor-syntax .md-link { - color: var(--ci-info); - text-decoration: underline; -} - -.editor-syntax .md-list { - color: var(--ci-accent); -} - -.editor-syntax .md-blockquote { - color: var(--ci-medium-gray); - border-left: 3px solid var(--ci-accent); - padding-left: var(--spacing-md); -} - -/* ============================================ - ASCII Art Generator Styles - ============================================ */ -.ascii-preview { - font-family: var(--font-mono); - font-size: 12px; - line-height: 1.2; - background: var(--ci-black); - color: #00ff00; - padding: var(--spacing-md); - border-radius: var(--radius-md); - overflow-x: auto; - white-space: pre; -} - -.ascii-controls { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-sm); - margin-bottom: var(--spacing-md); -} - -/* ============================================ - Table Generator Styles - ============================================ */ -.table-generator-grid { - display: grid; - gap: 1px; - background: var(--ci-border); - border: 1px solid var(--ci-border); - border-radius: var(--radius-md); - overflow: hidden; -} - -.table-generator-cell { - background: var(--ci-white); - padding: var(--spacing-sm); - min-width: 80px; - min-height: 32px; -} - -.table-generator-cell input { - width: 100%; - border: none; - background: transparent; - text-align: center; - font-family: var(--font-mono); -} - -.table-generator-cell.header { - background: var(--ci-light-gray); - font-weight: 600; -} - -/* ============================================ - PDF Viewer/Editor Styles - ============================================ */ -.pdf-viewer { - background: var(--ci-dark-gray); - padding: var(--spacing-md); - border-radius: var(--radius-md); - min-height: 400px; - display: flex; - align-items: center; - justify-content: center; -} - -.pdf-page { - background: var(--ci-white); - box-shadow: var(--shadow-lg); - max-width: 100%; -} - -.pdf-toolbar { - display: flex; - align-items: center; - gap: var(--spacing-sm); - padding: var(--spacing-sm); - background: var(--ci-light-gray); - border-radius: var(--radius-md); - margin-bottom: var(--spacing-md); -} - -/* ============================================ - Dark Mode for ConcreteInfo Theme - ============================================ */ -body.theme-concreteinfo-dark { - --ci-bg: #1a1a1a; - --ci-white: #2a2a2a; - --ci-light-gray: #3a3a3a; - --ci-border: #4a4a4a; -} - -body.theme-concreteinfo-dark .editor-textarea, -body.theme-concreteinfo-dark .preview-content { - background: #2a2a2a; - color: #e0e0e0; -} - -body.theme-concreteinfo-dark .modal-content { - background: #2a2a2a; - color: #e0e0e0; -} - -body.theme-concreteinfo-dark .modal-body { - background: #2a2a2a; -} - -body.theme-concreteinfo-dark .modal-footer { - background: #1a1a1a; -} - -/* ============================================ - Animations - ============================================ */ -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes slideUp { - from { transform: translateY(20px); opacity: 0; } - to { transform: translateY(0); opacity: 1; } -} - -@keyframes pulse { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.05); } -} - -.animate-fade-in { - animation: fadeIn var(--transition-normal); -} - -.animate-slide-up { - animation: slideUp var(--transition-normal); -} - -.animate-pulse { - animation: pulse 2s infinite; -} - -/* ============================================ - Responsive Design - ============================================ */ -@media (max-width: 768px) { - .modal-content { - width: 95%; - max-height: 90vh; - } - - .converter-grid { - grid-template-columns: 1fr; - } - - .app-header { - flex-wrap: wrap; - } -} - -/* ============================================ - Concrete Dark Theme - ============================================ */ -body.theme-concrete-dark { - background: #1a1a1a; - color: #e3e3e3; -} - -body.theme-concrete-dark .toolbar { - background: linear-gradient(135deg, #0d0b09 0%, #1a1a1a 100%); - border-bottom: 3px solid #e5461f; -} - -body.theme-concrete-dark .toolbar button { - color: #e3e3e3; -} - -body.theme-concrete-dark .toolbar button:hover { - background: #e5461f; -} - -body.theme-concrete-dark .tab-bar { - background: #0d0b09; - border-bottom: 2px solid #e5461f; -} - -body.theme-concrete-dark .tab { - background: #2a2a2a; - color: #e3e3e3; - border-radius: 4px 4px 0 0; -} - -body.theme-concrete-dark .tab.active { - background: #464646; - border-bottom: 2px solid #e5461f; -} - -body.theme-concrete-dark .editor-textarea, -body.theme-concrete-dark .editor-pane { - background: #1a1a1a; - color: #e3e3e3; -} - -body.theme-concrete-dark .preview-content { - background: #1a1a1a; - color: #e3e3e3; -} - -body.theme-concrete-dark .status-bar { - background: #0d0b09; - color: #9a9696; - border-top: 1px solid #464646; -} - -body.theme-concrete-dark .export-dialog-content, -body.theme-concrete-dark .batch-dialog-content { - background: #2a2a2a; - color: #e3e3e3; -} - -body.theme-concrete-dark .export-dialog-header, -body.theme-concrete-dark .batch-dialog-header { - background: #1a1a1a; - border-bottom: 2px solid #e5461f; -} - -body.theme-concrete-dark .export-dialog-footer, -body.theme-concrete-dark .batch-dialog-footer { - background: #1a1a1a; -} - -body.theme-concrete-dark input, -body.theme-concrete-dark select, -body.theme-concrete-dark textarea { - background: #1a1a1a; - color: #e3e3e3; - border-color: #464646; -} - -body.theme-concrete-dark input:focus, -body.theme-concrete-dark select:focus, -body.theme-concrete-dark textarea:focus { - border-color: #e5461f; -} - -body.theme-concrete-dark .line-numbers { - background: #0d0b09; - color: #9a9696; -} - -/* ============================================ - Concrete Light Theme - ============================================ */ -body.theme-concrete-light { - background: #f5f5f5; - color: #464646; -} - -body.theme-concrete-light .toolbar { - background: #ffffff; - border-bottom: 3px solid #e5461f; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); -} - -body.theme-concrete-light .toolbar button { - color: #464646; -} - -body.theme-concrete-light .toolbar button:hover { - background: #e5461f; - color: #ffffff; -} - -body.theme-concrete-light .tab-bar { - background: #ffffff; - border-bottom: 2px solid #e3e3e3; -} - -body.theme-concrete-light .tab { - background: #f5f5f5; - color: #464646; - border-radius: 4px 4px 0 0; -} - -body.theme-concrete-light .tab.active { - background: #ffffff; - border-bottom: 2px solid #e5461f; - color: #e5461f; -} - -body.theme-concrete-light .editor-textarea, -body.theme-concrete-light .editor-pane { - background: #ffffff; - color: #464646; -} - -body.theme-concrete-light .preview-content { - background: #ffffff; - color: #464646; -} - -body.theme-concrete-light .status-bar { - background: #ffffff; - color: #9a9696; - border-top: 1px solid #e3e3e3; -} - -body.theme-concrete-light .export-dialog-content, -body.theme-concrete-light .batch-dialog-content { - background: #ffffff; - color: #464646; -} - -body.theme-concrete-light .export-dialog-header, -body.theme-concrete-light .batch-dialog-header { - background: #f5f5f5; - border-bottom: 2px solid #e5461f; -} - -body.theme-concrete-light .line-numbers { - background: #f5f5f5; - color: #9a9696; -} - -/* ============================================ - Concrete Warm Theme - ============================================ */ -body.theme-concrete-warm { - background: #faf8f5; - color: #464646; -} - -body.theme-concrete-warm .toolbar { - background: linear-gradient(135deg, #464646 0%, #5a5a5a 100%); - border-bottom: 3px solid #e5461f; -} - -body.theme-concrete-warm .toolbar button { - color: #faf8f5; -} - -body.theme-concrete-warm .toolbar button:hover { - background: #e5461f; -} - -body.theme-concrete-warm .tab-bar { - background: #464646; - border-bottom: 2px solid #e5461f; -} - -body.theme-concrete-warm .tab { - background: #6a6a6a; - color: #faf8f5; - border-radius: 4px 4px 0 0; -} - -body.theme-concrete-warm .tab.active { - background: #faf8f5; - color: #e5461f; - border-bottom: 2px solid #e5461f; -} - -body.theme-concrete-warm .editor-textarea, -body.theme-concrete-warm .editor-pane { - background: #faf8f5; - color: #464646; -} - -body.theme-concrete-warm .preview-content { - background: #faf8f5; - color: #464646; -} - -body.theme-concrete-warm .status-bar { - background: #f0ebe4; - color: #9a9696; - border-top: 1px solid #e3e3e3; -} - -body.theme-concrete-warm .export-dialog-content, -body.theme-concrete-warm .batch-dialog-content { - background: #faf8f5; - color: #464646; -} - -body.theme-concrete-warm .export-dialog-header, -body.theme-concrete-warm .batch-dialog-header { - background: #f0ebe4; - border-bottom: 2px solid #e5461f; -} - -body.theme-concrete-warm h1, -body.theme-concrete-warm h2, -body.theme-concrete-warm h3, -body.theme-concrete-warm h4, -body.theme-concrete-warm h5, -body.theme-concrete-warm h6 { - color: #e5461f; -} - -body.theme-concrete-warm a { - color: #e5461f; -} - -body.theme-concrete-warm .line-numbers { - background: #f0ebe4; - color: #9a9696; -} +/** + * ConcreteInfo Theme for MarkdownConverter + * Based on logo palette: #464646, #9a9696, #e5461f, #e3e3e3, #0d0b09 + * Version: 3.0.0 + */ + +/* ============================================ + CSS Variables - ConcreteInfo Theme + ============================================ */ +:root { + /* Primary Colors from Logo Palette */ + --ci-dark-gray: #464646; + --ci-medium-gray: #9a9696; + --ci-accent: #e5461f; + --ci-light-gray: #e3e3e3; + --ci-black: #0d0b09; + + /* Extended Palette */ + --ci-accent-hover: #c93a18; + --ci-accent-light: rgba(229, 70, 31, 0.1); + --ci-white: #ffffff; + --ci-bg: #f5f5f5; + --ci-border: #d0d0d0; + + /* Semantic Colors */ + --ci-success: #28a745; + --ci-warning: #ffc107; + --ci-danger: #dc3545; + --ci-info: #17a2b8; + + /* Typography */ + --font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; + + /* Spacing */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + + /* Border Radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-full: 9999px; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15); + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-normal: 250ms ease; + --transition-slow: 350ms ease; +} + +/* ============================================ + ConcreteInfo Theme Application + ============================================ */ +body.theme-concreteinfo { + background: var(--ci-bg); + color: var(--ci-dark-gray); +} + +body.theme-concreteinfo .toolbar { + background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%); + border-bottom: 3px solid var(--ci-accent); +} + +body.theme-concreteinfo .toolbar button { + color: var(--ci-white); + background: transparent; + border: 1px solid transparent; + transition: all var(--transition-fast); +} + +body.theme-concreteinfo .toolbar button:hover { + background: var(--ci-accent); + border-color: var(--ci-accent); + transform: translateY(-1px); +} + +body.theme-concreteinfo .tab-bar { + background: var(--ci-dark-gray); + border-bottom: 2px solid var(--ci-accent); +} + +body.theme-concreteinfo .tab { + background: var(--ci-medium-gray); + color: var(--ci-white); + border: none; + margin: 2px; + border-radius: var(--radius-sm) var(--radius-sm) 0 0; + transition: all var(--transition-fast); +} + +body.theme-concreteinfo .tab:hover { + background: var(--ci-accent-light); +} + +body.theme-concreteinfo .tab.active { + background: var(--ci-accent); + color: var(--ci-white); +} + +body.theme-concreteinfo .editor-textarea { + background: var(--ci-white); + color: var(--ci-black); + border: 1px solid var(--ci-border); + font-family: var(--font-mono); +} + +body.theme-concreteinfo .preview-content { + background: var(--ci-white); + color: var(--ci-dark-gray); + border-left: 3px solid var(--ci-accent); +} + +body.theme-concreteinfo .status-bar { + background: var(--ci-dark-gray); + color: var(--ci-light-gray); + border-top: 2px solid var(--ci-accent); +} + +/* ============================================ + Modern Modal Styles + ============================================ */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(13, 11, 9, 0.7); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + opacity: 0; + visibility: hidden; + transition: all var(--transition-normal); +} + +.modal-overlay.active { + opacity: 1; + visibility: visible; +} + +.modal-content { + background: var(--ci-white); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xl); + max-width: 600px; + width: 90%; + max-height: 85vh; + overflow: hidden; + transform: scale(0.9) translateY(20px); + transition: transform var(--transition-normal); +} + +.modal-overlay.active .modal-content { + transform: scale(1) translateY(0); +} + +.modal-header { + background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%); + color: var(--ci-white); + padding: var(--spacing-md) var(--spacing-lg); + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 3px solid var(--ci-accent); +} + +.modal-header h3 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + font-family: var(--font-sans); +} + +.modal-close { + background: transparent; + border: none; + color: var(--ci-white); + font-size: 1.5rem; + cursor: pointer; + width: 32px; + height: 32px; + border-radius: var(--radius-full); + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition-fast); +} + +.modal-close:hover { + background: var(--ci-accent); + transform: rotate(90deg); +} + +.modal-body { + padding: var(--spacing-lg); + overflow-y: auto; + max-height: 60vh; +} + +.modal-footer { + padding: var(--spacing-md) var(--spacing-lg); + background: var(--ci-light-gray); + display: flex; + justify-content: flex-end; + gap: var(--spacing-sm); + border-top: 1px solid var(--ci-border); +} + +/* ============================================ + Button Styles + ============================================ */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-xs); + padding: var(--spacing-sm) var(--spacing-md); + font-family: var(--font-sans); + font-size: 0.875rem; + font-weight: 500; + border-radius: var(--radius-md); + border: none; + cursor: pointer; + transition: all var(--transition-fast); +} + +.btn-primary { + background: var(--ci-accent); + color: var(--ci-white); +} + +.btn-primary:hover { + background: var(--ci-accent-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.btn-secondary { + background: var(--ci-medium-gray); + color: var(--ci-white); +} + +.btn-secondary:hover { + background: var(--ci-dark-gray); +} + +.btn-outline { + background: transparent; + border: 2px solid var(--ci-accent); + color: var(--ci-accent); +} + +.btn-outline:hover { + background: var(--ci-accent); + color: var(--ci-white); +} + +.btn-ghost { + background: transparent; + color: var(--ci-dark-gray); +} + +.btn-ghost:hover { + background: var(--ci-light-gray); +} + +/* ============================================ + Form Controls + ============================================ */ +.form-group { + margin-bottom: var(--spacing-md); +} + +.form-label { + display: block; + margin-bottom: var(--spacing-xs); + font-weight: 500; + color: var(--ci-dark-gray); + font-size: 0.875rem; +} + +.form-input, +.form-select { + width: 100%; + padding: var(--spacing-sm) var(--spacing-md); + font-family: var(--font-sans); + font-size: 0.875rem; + border: 2px solid var(--ci-border); + border-radius: var(--radius-md); + background: var(--ci-white); + color: var(--ci-dark-gray); + transition: all var(--transition-fast); +} + +.form-input:focus, +.form-select:focus { + outline: none; + border-color: var(--ci-accent); + box-shadow: 0 0 0 3px var(--ci-accent-light); +} + +.form-input::placeholder { + color: var(--ci-medium-gray); +} + +/* ============================================ + App Header with Logo + ============================================ */ +.app-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 16px; + background: #ffffff; + border-bottom: 2px solid #e1e4e8; + min-height: 36px; +} + +.app-header-left { + display: flex; + align-items: center; + gap: 12px; +} + +.app-header-right { + display: flex; + align-items: center; + gap: 12px; +} + +.app-logo { + height: 22px; + width: auto; +} + +.app-title { + color: #24292e; + font-size: 0.95rem; + font-weight: 600; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; +} + +.app-version { + color: #6a737d; + font-size: 0.7rem; + font-family: 'SFMono-Regular', Consolas, monospace; +} + +/* Dark theme header adjustments */ +body.theme-dark .app-header, +body.theme-dracula .app-header, +body.theme-onedark .app-header, +body.theme-tokyonight .app-header, +body.theme-monokai .app-header, +body.theme-cobalt2 .app-header, +body.theme-gruvbox-dark .app-header, +body.theme-ayu-dark .app-header, +body.theme-ayu-mirage .app-header, +body.theme-palenight .app-header, +body.theme-oceanic-next .app-header, +body.theme-nord .app-header, +body.theme-concrete-dark .app-header { + background: #1f2937; + border-bottom-color: #374151; +} + +body.theme-dark .app-title, +body.theme-dracula .app-title, +body.theme-onedark .app-title, +body.theme-tokyonight .app-title, +body.theme-monokai .app-title, +body.theme-cobalt2 .app-title, +body.theme-gruvbox-dark .app-title, +body.theme-ayu-dark .app-title, +body.theme-ayu-mirage .app-title, +body.theme-palenight .app-title, +body.theme-oceanic-next .app-title, +body.theme-nord .app-title, +body.theme-concrete-dark .app-title { + color: #f3f4f6; +} + +body.theme-dark .app-version, +body.theme-dracula .app-version, +body.theme-onedark .app-version, +body.theme-tokyonight .app-version, +body.theme-monokai .app-version, +body.theme-cobalt2 .app-version, +body.theme-gruvbox-dark .app-version, +body.theme-ayu-dark .app-version, +body.theme-ayu-mirage .app-version, +body.theme-palenight .app-version, +body.theme-oceanic-next .app-version, +body.theme-nord .app-version, +body.theme-concrete-dark .app-version { + color: #9ca3af; +} + +body.theme-dark .app-logo, +body.theme-dracula .app-logo, +body.theme-onedark .app-logo, +body.theme-tokyonight .app-logo, +body.theme-monokai .app-logo, +body.theme-cobalt2 .app-logo, +body.theme-gruvbox-dark .app-logo, +body.theme-ayu-dark .app-logo, +body.theme-ayu-mirage .app-logo, +body.theme-palenight .app-logo, +body.theme-oceanic-next .app-logo, +body.theme-nord .app-logo, +body.theme-concrete-dark .app-logo { + filter: none; +} + +/* ============================================ + Converter Cards + ============================================ */ +.converter-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--spacing-md); + padding: var(--spacing-md); +} + +.converter-card { + background: var(--ci-white); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + text-align: center; + border: 2px solid var(--ci-border); + transition: all var(--transition-fast); + cursor: pointer; +} + +.converter-card:hover { + border-color: var(--ci-accent); + transform: translateY(-4px); + box-shadow: var(--shadow-lg); +} + +.converter-card-icon { + font-size: 2.5rem; + margin-bottom: var(--spacing-md); +} + +.converter-card-title { + font-weight: 600; + color: var(--ci-dark-gray); + margin-bottom: var(--spacing-xs); +} + +.converter-card-desc { + font-size: 0.875rem; + color: var(--ci-medium-gray); +} + +/* ============================================ + Progress Bar + ============================================ */ +.progress-bar { + height: 8px; + background: var(--ci-light-gray); + border-radius: var(--radius-full); + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--ci-accent) 0%, var(--ci-accent-hover) 100%); + border-radius: var(--radius-full); + transition: width var(--transition-normal); +} + +/* ============================================ + File Drop Zone + ============================================ */ +.drop-zone { + border: 2px dashed var(--ci-border); + border-radius: var(--radius-lg); + padding: var(--spacing-xl); + text-align: center; + transition: all var(--transition-fast); + cursor: pointer; +} + +.drop-zone:hover, +.drop-zone.drag-over { + border-color: var(--ci-accent); + background: var(--ci-accent-light); +} + +.drop-zone-icon { + font-size: 3rem; + color: var(--ci-medium-gray); + margin-bottom: var(--spacing-md); +} + +.drop-zone-text { + color: var(--ci-dark-gray); + font-weight: 500; +} + +.drop-zone-hint { + color: var(--ci-medium-gray); + font-size: 0.875rem; + margin-top: var(--spacing-xs); +} + +/* ============================================ + Syntax Highlighting for Editor + ============================================ */ +.editor-syntax .md-heading { + color: var(--ci-accent); + font-weight: bold; +} + +.editor-syntax .md-bold { + color: var(--ci-dark-gray); + font-weight: bold; +} + +.editor-syntax .md-italic { + color: var(--ci-dark-gray); + font-style: italic; +} + +.editor-syntax .md-code { + background: var(--ci-light-gray); + color: var(--ci-accent); + font-family: var(--font-mono); + padding: 2px 4px; + border-radius: var(--radius-sm); +} + +.editor-syntax .md-link { + color: var(--ci-info); + text-decoration: underline; +} + +.editor-syntax .md-list { + color: var(--ci-accent); +} + +.editor-syntax .md-blockquote { + color: var(--ci-medium-gray); + border-left: 3px solid var(--ci-accent); + padding-left: var(--spacing-md); +} + +/* ============================================ + ASCII Art Generator Styles + ============================================ */ +.ascii-preview { + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.2; + background: var(--ci-black); + color: #00ff00; + padding: var(--spacing-md); + border-radius: var(--radius-md); + overflow-x: auto; + white-space: pre; +} + +.ascii-controls { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-md); +} + +/* ============================================ + Table Generator Styles + ============================================ */ +.table-generator-grid { + display: grid; + gap: 1px; + background: var(--ci-border); + border: 1px solid var(--ci-border); + border-radius: var(--radius-md); + overflow: hidden; +} + +.table-generator-cell { + background: var(--ci-white); + padding: var(--spacing-sm); + min-width: 80px; + min-height: 32px; +} + +.table-generator-cell input { + width: 100%; + border: none; + background: transparent; + text-align: center; + font-family: var(--font-mono); +} + +.table-generator-cell.header { + background: var(--ci-light-gray); + font-weight: 600; +} + +/* ============================================ + PDF Viewer/Editor Styles + ============================================ */ +.pdf-viewer { + background: var(--ci-dark-gray); + padding: var(--spacing-md); + border-radius: var(--radius-md); + min-height: 400px; + display: flex; + align-items: center; + justify-content: center; +} + +.pdf-page { + background: var(--ci-white); + box-shadow: var(--shadow-lg); + max-width: 100%; +} + +.pdf-toolbar { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-sm); + background: var(--ci-light-gray); + border-radius: var(--radius-md); + margin-bottom: var(--spacing-md); +} + +/* ============================================ + Dark Mode for ConcreteInfo Theme + ============================================ */ +body.theme-concreteinfo-dark { + --ci-bg: #1a1a1a; + --ci-white: #2a2a2a; + --ci-light-gray: #3a3a3a; + --ci-border: #4a4a4a; +} + +body.theme-concreteinfo-dark .editor-textarea, +body.theme-concreteinfo-dark .preview-content { + background: #2a2a2a; + color: #e0e0e0; +} + +body.theme-concreteinfo-dark .modal-content { + background: #2a2a2a; + color: #e0e0e0; +} + +body.theme-concreteinfo-dark .modal-body { + background: #2a2a2a; +} + +body.theme-concreteinfo-dark .modal-footer { + background: #1a1a1a; +} + +/* ============================================ + Animations + ============================================ */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +@keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +.animate-fade-in { + animation: fadeIn var(--transition-normal); +} + +.animate-slide-up { + animation: slideUp var(--transition-normal); +} + +.animate-pulse { + animation: pulse 2s infinite; +} + +/* ============================================ + Responsive Design + ============================================ */ +@media (max-width: 768px) { + .modal-content { + width: 95%; + max-height: 90vh; + } + + .converter-grid { + grid-template-columns: 1fr; + } + + .app-header { + flex-wrap: wrap; + } +} + +/* ============================================ + Concrete Dark Theme + ============================================ */ +body.theme-concrete-dark { + background: #1a1a1a; + color: #e3e3e3; +} + +body.theme-concrete-dark .toolbar { + background: linear-gradient(135deg, #0d0b09 0%, #1a1a1a 100%); + border-bottom: 3px solid #e5461f; +} + +body.theme-concrete-dark .toolbar button { + color: #e3e3e3; +} + +body.theme-concrete-dark .toolbar button:hover { + background: #e5461f; +} + +body.theme-concrete-dark .tab-bar { + background: #0d0b09; + border-bottom: 2px solid #e5461f; +} + +body.theme-concrete-dark .tab { + background: #2a2a2a; + color: #e3e3e3; + border-radius: 4px 4px 0 0; +} + +body.theme-concrete-dark .tab.active { + background: #464646; + border-bottom: 2px solid #e5461f; +} + +body.theme-concrete-dark .editor-textarea, +body.theme-concrete-dark .editor-pane { + background: #1a1a1a; + color: #e3e3e3; +} + +body.theme-concrete-dark .preview-content { + background: #1a1a1a; + color: #e3e3e3; +} + +body.theme-concrete-dark .status-bar { + background: #0d0b09; + color: #9a9696; + border-top: 1px solid #464646; +} + +body.theme-concrete-dark .export-dialog-content, +body.theme-concrete-dark .batch-dialog-content { + background: #2a2a2a; + color: #e3e3e3; +} + +body.theme-concrete-dark .export-dialog-header, +body.theme-concrete-dark .batch-dialog-header { + background: #1a1a1a; + border-bottom: 2px solid #e5461f; +} + +body.theme-concrete-dark .export-dialog-footer, +body.theme-concrete-dark .batch-dialog-footer { + background: #1a1a1a; +} + +body.theme-concrete-dark input, +body.theme-concrete-dark select, +body.theme-concrete-dark textarea { + background: #1a1a1a; + color: #e3e3e3; + border-color: #464646; +} + +body.theme-concrete-dark input:focus, +body.theme-concrete-dark select:focus, +body.theme-concrete-dark textarea:focus { + border-color: #e5461f; +} + +body.theme-concrete-dark .line-numbers { + background: #0d0b09; + color: #9a9696; +} + +/* ============================================ + Concrete Light Theme + ============================================ */ +body.theme-concrete-light { + background: #f5f5f5; + color: #464646; +} + +body.theme-concrete-light .toolbar { + background: #ffffff; + border-bottom: 3px solid #e5461f; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +body.theme-concrete-light .toolbar button { + color: #464646; +} + +body.theme-concrete-light .toolbar button:hover { + background: #e5461f; + color: #ffffff; +} + +body.theme-concrete-light .tab-bar { + background: #ffffff; + border-bottom: 2px solid #e3e3e3; +} + +body.theme-concrete-light .tab { + background: #f5f5f5; + color: #464646; + border-radius: 4px 4px 0 0; +} + +body.theme-concrete-light .tab.active { + background: #ffffff; + border-bottom: 2px solid #e5461f; + color: #e5461f; +} + +body.theme-concrete-light .editor-textarea, +body.theme-concrete-light .editor-pane { + background: #ffffff; + color: #464646; +} + +body.theme-concrete-light .preview-content { + background: #ffffff; + color: #464646; +} + +body.theme-concrete-light .status-bar { + background: #ffffff; + color: #9a9696; + border-top: 1px solid #e3e3e3; +} + +body.theme-concrete-light .export-dialog-content, +body.theme-concrete-light .batch-dialog-content { + background: #ffffff; + color: #464646; +} + +body.theme-concrete-light .export-dialog-header, +body.theme-concrete-light .batch-dialog-header { + background: #f5f5f5; + border-bottom: 2px solid #e5461f; +} + +body.theme-concrete-light .line-numbers { + background: #f5f5f5; + color: #9a9696; +} + +/* ============================================ + Concrete Warm Theme + ============================================ */ +body.theme-concrete-warm { + background: #faf8f5; + color: #464646; +} + +body.theme-concrete-warm .toolbar { + background: linear-gradient(135deg, #464646 0%, #5a5a5a 100%); + border-bottom: 3px solid #e5461f; +} + +body.theme-concrete-warm .toolbar button { + color: #faf8f5; +} + +body.theme-concrete-warm .toolbar button:hover { + background: #e5461f; +} + +body.theme-concrete-warm .tab-bar { + background: #464646; + border-bottom: 2px solid #e5461f; +} + +body.theme-concrete-warm .tab { + background: #6a6a6a; + color: #faf8f5; + border-radius: 4px 4px 0 0; +} + +body.theme-concrete-warm .tab.active { + background: #faf8f5; + color: #e5461f; + border-bottom: 2px solid #e5461f; +} + +body.theme-concrete-warm .editor-textarea, +body.theme-concrete-warm .editor-pane { + background: #faf8f5; + color: #464646; +} + +body.theme-concrete-warm .preview-content { + background: #faf8f5; + color: #464646; +} + +body.theme-concrete-warm .status-bar { + background: #f0ebe4; + color: #9a9696; + border-top: 1px solid #e3e3e3; +} + +body.theme-concrete-warm .export-dialog-content, +body.theme-concrete-warm .batch-dialog-content { + background: #faf8f5; + color: #464646; +} + +body.theme-concrete-warm .export-dialog-header, +body.theme-concrete-warm .batch-dialog-header { + background: #f0ebe4; + border-bottom: 2px solid #e5461f; +} + +body.theme-concrete-warm h1, +body.theme-concrete-warm h2, +body.theme-concrete-warm h3, +body.theme-concrete-warm h4, +body.theme-concrete-warm h5, +body.theme-concrete-warm h6 { + color: #e5461f; +} + +body.theme-concrete-warm a { + color: #e5461f; +} + +body.theme-concrete-warm .line-numbers { + background: #f0ebe4; + color: #9a9696; +} diff --git a/src/styles-modern.css b/src/styles-modern.css index 52445b2..1a57a53 100644 --- a/src/styles-modern.css +++ b/src/styles-modern.css @@ -1,2113 +1,2113 @@ -/* ======================================== - PanConverter v1.7.1 - Modern UI - Glassmorphism, Gradients & Animations - ======================================== */ - -/* Modern Color Palette */ -:root { - /* Primary Brand Colors - Purple-Blue Gradient */ - --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - --primary-light: #8b9aff; - --primary-dark: #5661b3; - - /* Accent Colors */ - --accent-green: #10b981; - --accent-blue: #3b82f6; - --accent-purple: #8b5cf6; - --accent-pink: #ec4899; - - /* Neutral Colors */ - --gray-50: #f9fafb; - --gray-100: #f3f4f6; - --gray-200: #e5e7eb; - --gray-300: #d1d5db; - --gray-400: #9ca3af; - --gray-500: #6b7280; - --gray-600: #4b5563; - --gray-700: #374151; - --gray-800: #1f2937; - --gray-900: #111827; - - /* Glassmorphism */ - --glass-bg: rgba(255, 255, 255, 0.7); - --glass-border: rgba(255, 255, 255, 0.18); - --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15); - - /* Effects */ - --blur-amount: 10px; - --transition-speed: 0.3s; - --transition-ease: cubic-bezier(0.4, 0, 0.2, 1); -} - -/* Reset & Base */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - overflow: hidden; - height: 100vh; - /* Background controlled by themes - do not set here */ - color: var(--gray-900); -} - -@keyframes gradientShift { - 0% { background-position: 0% 50%; } - 50% { background-position: 100% 50%; } - 100% { background-position: 0% 50%; } -} - -.container { - display: flex; - flex-direction: column; - height: 100vh; - padding: 4px 8px; - gap: 2px; -} - -/* Glass Container for main content */ -.main-content-glass { - background: var(--glass-bg); - backdrop-filter: blur(var(--blur-amount)); - -webkit-backdrop-filter: blur(var(--blur-amount)); - border: 1px solid var(--glass-border); - border-radius: 20px; - box-shadow: var(--glass-shadow); - overflow: hidden; - flex: 1; - display: flex; - flex-direction: column; -} - -/* Tab Bar - Modern Glassmorphism */ -.tab-bar { - display: flex; - align-items: center; - background: rgba(255, 255, 255, 0.5); - backdrop-filter: blur(10px); - border-bottom: 1px solid rgba(0, 0, 0, 0.08); - padding: 4px 8px; - min-height: 36px; - gap: 4px; - border-radius: 8px; -} - -.tab { - display: flex; - align-items: center; - padding: 8px 16px; - background: rgba(255, 255, 255, 0.3); - backdrop-filter: blur(8px); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 12px; - margin-right: 4px; - cursor: pointer; - user-select: none; - max-width: 200px; - min-width: 120px; - transition: all var(--transition-speed) var(--transition-ease); - transform: translateY(0); -} - -.tab:hover { - background: rgba(0, 0, 0, 0.05); -} - -.tab.active { - background: #ffffff; - color: #24292e; - border-bottom: 2px solid #0366d6; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); -} - -.tab-title { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 13px; - font-weight: 500; -} - -.tab-close { - background: rgba(255, 255, 255, 0.2); - border: none; - font-size: 14px; - font-weight: bold; - cursor: pointer; - padding: 4px; - margin-left: 8px; - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 6px; - color: currentColor; - transition: all var(--transition-speed) var(--transition-ease); -} - -.tab-close:hover { - background: rgba(255, 255, 255, 0.4); - transform: rotate(90deg); -} - -.new-tab-button { - background: rgba(255, 255, 255, 0.3); - backdrop-filter: blur(8px); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 10px; - padding: 8px 16px; - cursor: pointer; - font-size: 14px; - font-weight: 500; - color: var(--gray-700); - transition: all var(--transition-speed) var(--transition-ease); - display: flex; - align-items: center; - gap: 6px; -} - -.new-tab-button:hover { - background: rgba(255, 255, 255, 0.5); - transform: scale(1.05); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -/* Toolbar - Modern Gradient Buttons */ -.toolbar { - display: flex; - align-items: center; - padding: 4px 8px; - background: rgba(255, 255, 255, 0.4); - backdrop-filter: blur(10px); - border-bottom: 1px solid rgba(0, 0, 0, 0.06); - gap: 2px; - border-radius: 8px; -} - -.toolbar button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: 1px solid transparent; - background: transparent; - border-radius: 6px; - cursor: pointer; - transition: all 0.15s ease; - color: #586069; -} - -.toolbar button:hover { - background: rgba(0, 0, 0, 0.06); - color: #24292e; -} - -.toolbar button:active { - background: rgba(0, 0, 0, 0.1); - transform: scale(0.95); -} - -.toolbar-separator { - width: 1px; - height: 24px; - background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.4), transparent); - margin: 0 8px; -} - -/* Tab Content */ -.tab-content { - display: flex; - flex: 1; - overflow: hidden; -} - -.tab-content:not(.active) { - display: none; -} - -/* Editor Container */ -.editor-container { - display: flex; - flex: 1; - overflow: hidden; - background: rgba(255, 255, 255, 0.3); - backdrop-filter: blur(10px); - border-radius: 0 0 20px 20px; -} - -.pane { - flex: 1; - overflow: auto; - min-width: 0; -} - -.pane:first-child { - border-right: 1px solid rgba(255, 255, 255, 0.3); -} - -/* Custom Scrollbar */ -.pane::-webkit-scrollbar, -.editor-textarea::-webkit-scrollbar { - width: 10px; - height: 10px; -} - -.pane::-webkit-scrollbar-track, -.editor-textarea::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.1); - border-radius: 10px; -} - -.pane::-webkit-scrollbar-thumb, -.editor-textarea::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.25); - border-radius: 10px; - border: 2px solid rgba(255, 255, 255, 0.2); -} - -.pane::-webkit-scrollbar-thumb:hover, -.editor-textarea::-webkit-scrollbar-thumb:hover { - background: rgba(0, 0, 0, 0.4); -} - -.editor-textarea { - width: 100%; - height: 100%; - padding: 24px; - font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Courier New', monospace; - font-size: 15px; - line-height: 1.7; - border: none; - outline: none; - resize: none; - background: transparent; - /* Color controlled by theme */ -} - -.pane:last-child { - padding: 0; - /* Background controlled by theme */ -} - -.preview-content { - max-width: none; - margin: 0; - padding: 24px 32px; - line-height: 1.8; - font-size: 15px; - /* Color controlled by theme */ -} - -/* Status Bar */ -.status-bar { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 20px; - /* Background and color controlled by theme */ - font-size: 12px; - font-weight: 500; -} - -/* Preview Styles - Modern Typography */ -.preview-content h1, .preview-content h2, .preview-content h3 { - /* Color controlled by theme */ - font-weight: 700; - margin-top: 28px; - margin-bottom: 16px; - line-height: 1.3; -} - -.preview-content h1 { - font-size: 2.5em; - border-bottom: 3px solid #e1e4e8; - padding-bottom: 12px; -} - -.preview-content h2 { - font-size: 2em; - border-bottom: 2px solid #e1e4e8; - padding-bottom: 10px; -} - -.preview-content p { - margin-bottom: 18px; - line-height: 1.8; -} - -.preview-content code { - padding: 3px 8px; - margin: 0 2px; - font-size: 90%; - background: rgba(175, 184, 193, 0.2); - border: 1px solid #e1e4e8; - border-radius: 6px; - font-family: 'JetBrains Mono', 'Fira Code', monospace; - font-weight: 500; -} - -.preview-content pre { - padding: 20px; - overflow-x: auto; - font-size: 14px; - line-height: 1.6; - background: #f6f8fa; - border: 1px solid #e1e4e8; - border-radius: 12px; - margin-bottom: 20px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); -} - -.preview-content pre code { - background: transparent; - border: none; - padding: 0; - margin: 0; -} - -.preview-content blockquote { - padding: 16px 20px; - color: var(--gray-700); - border-left: 4px solid #e1e4e8; - margin-bottom: 20px; - background: #f6f8fa; - border-radius: 0 8px 8px 0; - font-style: italic; -} - -.preview-content a { - color: #0366d6; - text-decoration: none; - font-weight: 600; - position: relative; - transition: color var(--transition-speed); -} - -.preview-content a::after { - content: ''; - position: absolute; - left: 0; - bottom: -2px; - width: 0; - height: 2px; - background: #0366d6; - transition: width var(--transition-speed) var(--transition-ease); -} - -.preview-content a:hover { - color: #0366d6; -} - -.preview-content a:hover::after { - width: 100%; -} - -.preview-content table { - border-collapse: separate; - border-spacing: 0; - margin-bottom: 20px; - width: 100%; - border-radius: 12px; - overflow: hidden; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); -} - -.preview-content table th, -.preview-content table td { - padding: 12px 16px; - border: 1px solid #e1e4e8; -} - -.preview-content table th { - font-weight: 600; - background: #f0f0f0; - color: #333333; -} - -.preview-content table tr:nth-child(even) { - background: rgba(255, 255, 255, 0.5); -} - -/* Modern Dialogs */ -.export-dialog, -.batch-dialog, -.find-dialog-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.4); - backdrop-filter: blur(8px); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - animation: fadeIn 0.3s var(--transition-ease); -} - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -.export-dialog.hidden, -.batch-dialog.hidden { - display: none; -} - -.export-dialog-content, -.batch-dialog-content { - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); - width: 90%; - max-width: 650px; - max-height: 90vh; - overflow-y: auto; - animation: slideUpFade 0.4s var(--transition-ease); -} - -@keyframes slideUpFade { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.export-dialog-header, -.batch-dialog-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 20px 24px; - border-bottom: 1px solid #e1e4e8; - background: #24292e; - border-radius: 20px 20px 0 0; -} - -.export-dialog-header h3, -.batch-dialog-header h3 { - margin: 0; - font-size: 20px; - font-weight: 700; - color: white; -} - -.export-dialog-body, -.batch-dialog-body { - padding: 24px; -} - -.export-section, -.batch-section { - margin-bottom: 20px; -} - -.export-section label, -.batch-section label { - display: block; - font-weight: 600; - margin-bottom: 8px; - color: var(--gray-700); - font-size: 14px; -} - -.export-section select, -.export-section input, -.batch-section select, -.batch-section input { - width: 100%; - padding: 10px 14px; - border: 2px solid #e1e4e8; - border-radius: 10px; - font-size: 14px; - background: rgba(255, 255, 255, 0.8); - backdrop-filter: blur(5px); - transition: all var(--transition-speed) var(--transition-ease); -} - -.export-section select:focus, -.export-section input:focus, -.batch-section select:focus, -.batch-section input:focus { - outline: none; - border-color: #0366d6; - box-shadow: 0 0 0 4px rgba(3, 102, 214, 0.1); - background: white; -} - -/* Modern Buttons */ -.export-dialog-footer, -.batch-dialog-footer { - display: flex; - justify-content: flex-end; - gap: 12px; - padding: 20px 24px; - border-top: 1px solid #e1e4e8; - background: #f6f8fa; - border-radius: 0 0 20px 20px; -} - -.export-dialog-footer button, -.batch-dialog-footer button, -.folder-input-group button { - padding: 10px 24px; - border: none; - border-radius: 10px; - cursor: pointer; - font-size: 14px; - font-weight: 600; - transition: all var(--transition-speed) var(--transition-ease); - position: relative; - overflow: hidden; -} - -.export-dialog-footer button.primary, -.batch-dialog-footer button.primary { - background: #2ea44f; - color: white; - box-shadow: 0 4px 12px rgba(46, 164, 79, 0.3); -} - -.export-dialog-footer button.primary:hover, -.batch-dialog-footer button.primary:hover { - transform: translateY(-2px); - background: #22863a; - box-shadow: 0 6px 20px rgba(34, 134, 58, 0.4); -} - -.export-dialog-footer button:not(.primary), -.batch-dialog-footer button:not(.primary), -.folder-input-group button { - background: #fafbfc; - color: var(--gray-700); - border: 1px solid #e1e4e8; -} - -.export-dialog-footer button:not(.primary):hover, -.batch-dialog-footer button:not(.primary):hover, -.folder-input-group button:hover { - background: #f3f4f6; - border-color: #d0d7de; - transform: translateY(-2px); -} - -/* Find Dialog - Floating Modern */ -.find-dialog { - position: absolute; - top: 70px; - right: 24px; - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 16px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); - z-index: 1000; - min-width: 420px; - padding: 16px; - animation: slideInRight 0.3s var(--transition-ease); -} - -@keyframes slideInRight { - from { - opacity: 0; - transform: translateX(30px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -.find-dialog.hidden { - display: none; -} - -/* Progress Bars - Modern Gradient */ -.progress-bar { - width: 100%; - height: 8px; - background: #e1e4e8; - border-radius: 10px; - overflow: hidden; - margin-bottom: 12px; -} - -.progress-fill { - height: 100%; - background: #2ea44f; - transition: width 0.4s var(--transition-ease); - width: 0%; - position: relative; - overflow: hidden; -} - -.progress-fill::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(90deg, - transparent, - rgba(255, 255, 255, 0.3), - transparent - ); - animation: shimmer 2s infinite; -} - -@keyframes shimmer { - 0% { - transform: translateX(-100%); - } - 100% { - transform: translateX(100%); - } -} - -/* Auto-save Indicator - Modern */ -.auto-save-indicator { - position: fixed; - top: 24px; - right: 24px; - background: #2ea44f; - color: white; - padding: 10px 20px; - border-radius: 12px; - font-size: 13px; - font-weight: 600; - z-index: 10000; - box-shadow: 0 4px 20px rgba(46, 164, 79, 0.3); - animation: slideInRight 0.3s ease-out; - display: flex; - align-items: center; - gap: 8px; -} - -.auto-save-indicator::before { - content: '✓'; - display: inline-block; - width: 18px; - height: 18px; - background: white; - color: #2ea44f; - border-radius: 50%; - font-size: 12px; - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; -} - -/* Enhanced Statistics - Modern */ -.enhanced-stats { - font-size: 11px; - color: var(--gray-600); - padding: 2px 0; - margin-left: 16px; - font-family: 'JetBrains Mono', monospace; - font-weight: 500; -} - -/* Line Numbers - Modern */ -.line-numbers { - min-width: 50px; - padding: 12px 8px; - background: rgba(255, 255, 255, 0.3); - backdrop-filter: blur(8px); - border-right: 1px solid #e1e4e8; - font-family: 'JetBrains Mono', monospace; - font-size: 13px; - line-height: 1.7; - color: var(--gray-500); - text-align: right; - user-select: none; - overflow: hidden; -} - -.line-numbers.hidden { - display: none; -} - -/* Animations & Transitions */ -.toolbar button, -.tab, -.new-tab-button, -.export-dialog-footer button, -.batch-dialog-footer button { - position: relative; - overflow: hidden; -} - -.toolbar button::before, -.tab::before, -.new-tab-button::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - border-radius: 50%; - background: rgba(255, 255, 255, 0.4); - transform: translate(-50%, -50%); - transition: width 0.6s, height 0.6s; -} - -.toolbar button:hover::before, -.tab:hover::before, -.new-tab-button:hover::before { - width: 200px; - height: 200px; -} - -/* Responsive Design */ -@media (max-width: 768px) { - .container { - padding: 8px; - gap: 8px; - } - - .export-dialog-content, - .batch-dialog-content { - max-width: 95%; - } - - .find-dialog { - right: 12px; - min-width: 320px; - } -} - -/* Hide default elements */ -.pane.hidden { - display: none; -} - -.pane.full-width { - border-right: none; -} - -/* ======================================== - Dynamic Pane Resizer - Draggable splitter between editor/preview - ======================================== */ - -.pane-resizer { - width: 6px; - background: linear-gradient(to right, #e1e4e8, #d0d7de, #e1e4e8); - cursor: col-resize; - position: relative; - flex-shrink: 0; - transition: background 0.2s ease; - border-radius: 3px; -} - -.pane-resizer:hover { - background: linear-gradient(to right, #0366d6, #0366d6, #0366d6); -} - -.pane-resizer:active { - background: #0366d6; -} - -.pane-resizer::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 2px; - height: 30px; - background: rgba(0, 0, 0, 0.2); - border-radius: 1px; -} - -.pane-resizer:hover::before { - background: rgba(255, 255, 255, 0.8); -} - -/* Dark theme resizer */ -body.theme-dark .pane-resizer, -body.theme-one-dark .pane-resizer, -body.theme-dracula .pane-resizer, -body.theme-nord .pane-resizer, -body.theme-tokyo-night .pane-resizer, -body.theme-palenight .pane-resizer, -body.theme-ayu-dark .pane-resizer, -body.theme-ayu-mirage .pane-resizer, -body.theme-oceanic-next .pane-resizer, -body.theme-gruvbox-dark .pane-resizer, -body.theme-cobalt2 .pane-resizer { - background: linear-gradient(to right, #333, #444, #333); -} - -body.theme-dark .pane-resizer:hover, -body.theme-one-dark .pane-resizer:hover, -body.theme-dracula .pane-resizer:hover { - background: #61afef; -} - -/* ======================================== - PDF Viewer Styles - Full-featured PDF reader - ======================================== */ - -.pdf-viewer-container { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - background: #525659; -} - -.pdf-viewer-container.hidden { - display: none; -} - -.pdf-viewer-toolbar { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background: #323639; - border-bottom: 1px solid #1a1a1a; - flex-shrink: 0; -} - -.pdf-viewer-toolbar button { - background: #474a4d; - border: 1px solid #5a5d60; - border-radius: 4px; - color: #fff; - padding: 6px 10px; - cursor: pointer; - font-size: 14px; - transition: all 0.15s ease; -} - -.pdf-viewer-toolbar button:hover { - background: #5a5d60; -} - -.pdf-viewer-toolbar button:active { - background: #0366d6; -} - -.pdf-toolbar-separator { - width: 1px; - height: 24px; - background: #5a5d60; - margin: 0 4px; -} - -#pdf-page-info { - color: #fff; - font-size: 13px; - display: flex; - align-items: center; - gap: 4px; -} - -#pdf-page-input { - width: 50px; - padding: 4px 6px; - border: 1px solid #5a5d60; - border-radius: 4px; - background: #474a4d; - color: #fff; - text-align: center; - font-size: 13px; -} - -#pdf-zoom-level { - color: #fff; - font-size: 13px; - min-width: 50px; - text-align: center; -} - -.pdf-filename { - color: #aaa; - font-size: 12px; - flex: 1; - text-align: right; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - margin-right: 8px; -} - -#pdf-close { - background: #c0392b !important; - border-color: #e74c3c !important; -} - -#pdf-close:hover { - background: #e74c3c !important; -} - -/* PDF Editor Toolbar Buttons */ -.pdf-editor-btn { - display: flex; - align-items: center; - gap: 4px; - background: linear-gradient(135deg, #3498db, #2980b9) !important; - border: 1px solid #2980b9 !important; - color: #fff !important; - padding: 5px 8px !important; - font-size: 11px !important; - font-weight: 500; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; -} - -.pdf-editor-btn:hover { - background: linear-gradient(135deg, #2980b9, #1a5276) !important; - transform: translateY(-1px); -} - -.pdf-editor-btn:active { - transform: translateY(0); -} - -.pdf-editor-btn svg { - flex-shrink: 0; -} - -.pdf-editor-btn span { - white-space: nowrap; -} - -/* PDF toolbar wrapping for smaller screens */ -.pdf-toolbar-main { - flex-wrap: wrap; - min-height: auto; - padding: 6px 10px !important; -} - -/* Light theme PDF toolbar - refined styling */ -body.theme-light .pdf-viewer-toolbar, -body.theme-atomonelight .pdf-viewer-toolbar, -body.theme-github .pdf-viewer-toolbar, -body.theme-solarized .pdf-viewer-toolbar, -body.theme-gruvbox-light .pdf-viewer-toolbar, -body.theme-ayu-light .pdf-viewer-toolbar, -body.theme-sepia .pdf-viewer-toolbar, -body.theme-paper .pdf-viewer-toolbar, -body.theme-rosepine-dawn .pdf-viewer-toolbar { - background: linear-gradient(180deg, #fafafa 0%, #f0f0f0 100%) !important; - border-bottom: 1px solid #d0d0d0 !important; - box-shadow: 0 1px 3px rgba(0,0,0,0.08); -} - -body.theme-light .pdf-viewer-toolbar button, -body.theme-atomonelight .pdf-viewer-toolbar button, -body.theme-github .pdf-viewer-toolbar button, -body.theme-solarized .pdf-viewer-toolbar button, -body.theme-gruvbox-light .pdf-viewer-toolbar button, -body.theme-ayu-light .pdf-viewer-toolbar button, -body.theme-sepia .pdf-viewer-toolbar button, -body.theme-paper .pdf-viewer-toolbar button, -body.theme-rosepine-dawn .pdf-viewer-toolbar button { - background: #fff !important; - border: 1px solid #ccc !important; - color: #333 !important; - box-shadow: 0 1px 2px rgba(0,0,0,0.05); -} - -body.theme-light .pdf-viewer-toolbar button:hover, -body.theme-atomonelight .pdf-viewer-toolbar button:hover, -body.theme-github .pdf-viewer-toolbar button:hover, -body.theme-solarized .pdf-viewer-toolbar button:hover, -body.theme-gruvbox-light .pdf-viewer-toolbar button:hover, -body.theme-ayu-light .pdf-viewer-toolbar button:hover, -body.theme-sepia .pdf-viewer-toolbar button:hover, -body.theme-paper .pdf-viewer-toolbar button:hover, -body.theme-rosepine-dawn .pdf-viewer-toolbar button:hover { - background: #e8e8e8 !important; - border-color: #bbb !important; -} - -/* Light theme PDF editor buttons */ -body.theme-light .pdf-editor-btn, -body.theme-atomonelight .pdf-editor-btn, -body.theme-github .pdf-editor-btn, -body.theme-solarized .pdf-editor-btn, -body.theme-gruvbox-light .pdf-editor-btn, -body.theme-ayu-light .pdf-editor-btn, -body.theme-sepia .pdf-editor-btn, -body.theme-paper .pdf-editor-btn, -body.theme-rosepine-dawn .pdf-editor-btn { - background: linear-gradient(135deg, #4a90d9, #357abd) !important; - border: 1px solid #357abd !important; -} - -body.theme-light .pdf-editor-btn:hover, -body.theme-atomonelight .pdf-editor-btn:hover, -body.theme-github .pdf-editor-btn:hover, -body.theme-solarized .pdf-editor-btn:hover, -body.theme-gruvbox-light .pdf-editor-btn:hover, -body.theme-ayu-light .pdf-editor-btn:hover, -body.theme-sepia .pdf-editor-btn:hover, -body.theme-paper .pdf-editor-btn:hover, -body.theme-rosepine-dawn .pdf-editor-btn:hover { - background: linear-gradient(135deg, #357abd, #2868a0) !important; -} - -/* Light theme page info */ -body.theme-light #pdf-page-info, -body.theme-atomonelight #pdf-page-info, -body.theme-github #pdf-page-info, -body.theme-solarized #pdf-page-info, -body.theme-gruvbox-light #pdf-page-info, -body.theme-ayu-light #pdf-page-info, -body.theme-sepia #pdf-page-info, -body.theme-paper #pdf-page-info, -body.theme-rosepine-dawn #pdf-page-info, -body.theme-light #pdf-zoom-level, -body.theme-atomonelight #pdf-zoom-level, -body.theme-github #pdf-zoom-level, -body.theme-solarized #pdf-zoom-level, -body.theme-gruvbox-light #pdf-zoom-level, -body.theme-ayu-light #pdf-zoom-level, -body.theme-sepia #pdf-zoom-level, -body.theme-paper #pdf-zoom-level, -body.theme-rosepine-dawn #pdf-zoom-level { - color: #444 !important; - font-weight: 500; -} - -body.theme-light #pdf-page-input, -body.theme-atomonelight #pdf-page-input, -body.theme-github #pdf-page-input, -body.theme-solarized #pdf-page-input, -body.theme-gruvbox-light #pdf-page-input, -body.theme-ayu-light #pdf-page-input, -body.theme-sepia #pdf-page-input, -body.theme-paper #pdf-page-input, -body.theme-rosepine-dawn #pdf-page-input { - background: #fff !important; - border: 1px solid #ccc !important; - color: #333 !important; -} - -body.theme-light .pdf-toolbar-separator, -body.theme-atomonelight .pdf-toolbar-separator, -body.theme-github .pdf-toolbar-separator, -body.theme-solarized .pdf-toolbar-separator, -body.theme-gruvbox-light .pdf-toolbar-separator, -body.theme-ayu-light .pdf-toolbar-separator, -body.theme-sepia .pdf-toolbar-separator, -body.theme-paper .pdf-toolbar-separator, -body.theme-rosepine-dawn .pdf-toolbar-separator { - background: #d0d0d0 !important; -} - -body.theme-light .pdf-filename, -body.theme-atomonelight .pdf-filename, -body.theme-github .pdf-filename, -body.theme-solarized .pdf-filename, -body.theme-gruvbox-light .pdf-filename, -body.theme-ayu-light .pdf-filename, -body.theme-sepia .pdf-filename, -body.theme-paper .pdf-filename, -body.theme-rosepine-dawn .pdf-filename { - color: #555 !important; -} - -/* Sepia theme specific */ -body.theme-sepia .pdf-viewer-toolbar { - background: linear-gradient(180deg, #f4f0e8 0%, #e8e4dc 100%) !important; - border-bottom-color: #cdc8c0 !important; -} - -body.theme-sepia .pdf-viewer { - background: #d8d4cc !important; -} - -/* Rose Pine Dawn specific */ -body.theme-rosepine-dawn .pdf-viewer-toolbar { - background: linear-gradient(180deg, #faf4ed 0%, #f2e9e1 100%) !important; - border-bottom-color: #ddd8d1 !important; -} - -body.theme-rosepine-dawn .pdf-viewer { - background: #e6e0d8 !important; -} - -/* Dark theme PDF toolbar refinement */ -body.theme-dark .pdf-viewer-toolbar, -body.theme-onedark .pdf-viewer-toolbar, -body.theme-one-dark .pdf-viewer-toolbar, -body.theme-dracula .pdf-viewer-toolbar, -body.theme-nord .pdf-viewer-toolbar, -body.theme-tokyo-night .pdf-viewer-toolbar, -body.theme-tokyonight .pdf-viewer-toolbar, -body.theme-palenight .pdf-viewer-toolbar, -body.theme-monokai .pdf-viewer-toolbar, -body.theme-material .pdf-viewer-toolbar, -body.theme-gruvbox-dark .pdf-viewer-toolbar, -body.theme-ayu-dark .pdf-viewer-toolbar, -body.theme-ayu-mirage .pdf-viewer-toolbar, -body.theme-oceanic-next .pdf-viewer-toolbar, -body.theme-cobalt2 .pdf-viewer-toolbar, -body.theme-concrete-dark .pdf-viewer-toolbar, -body.theme-concrete-warm .pdf-viewer-toolbar { - background: linear-gradient(180deg, #2d2d2d 0%, #252525 100%) !important; - border-bottom: 1px solid #1a1a1a !important; -} - -body.theme-dark .pdf-viewer-toolbar button, -body.theme-onedark .pdf-viewer-toolbar button, -body.theme-one-dark .pdf-viewer-toolbar button, -body.theme-dracula .pdf-viewer-toolbar button, -body.theme-nord .pdf-viewer-toolbar button, -body.theme-tokyo-night .pdf-viewer-toolbar button, -body.theme-tokyonight .pdf-viewer-toolbar button, -body.theme-palenight .pdf-viewer-toolbar button, -body.theme-monokai .pdf-viewer-toolbar button, -body.theme-material .pdf-viewer-toolbar button, -body.theme-gruvbox-dark .pdf-viewer-toolbar button, -body.theme-ayu-dark .pdf-viewer-toolbar button, -body.theme-ayu-mirage .pdf-viewer-toolbar button, -body.theme-oceanic-next .pdf-viewer-toolbar button, -body.theme-cobalt2 .pdf-viewer-toolbar button, -body.theme-concrete-dark .pdf-viewer-toolbar button, -body.theme-concrete-warm .pdf-viewer-toolbar button { - background: #3d3d3d !important; - border: 1px solid #4d4d4d !important; - color: #e0e0e0 !important; -} - -body.theme-dark .pdf-viewer-toolbar button:hover, -body.theme-onedark .pdf-viewer-toolbar button:hover, -body.theme-one-dark .pdf-viewer-toolbar button:hover, -body.theme-dracula .pdf-viewer-toolbar button:hover, -body.theme-nord .pdf-viewer-toolbar button:hover, -body.theme-tokyo-night .pdf-viewer-toolbar button:hover, -body.theme-tokyonight .pdf-viewer-toolbar button:hover, -body.theme-palenight .pdf-viewer-toolbar button:hover, -body.theme-monokai .pdf-viewer-toolbar button:hover, -body.theme-material .pdf-viewer-toolbar button:hover, -body.theme-gruvbox-dark .pdf-viewer-toolbar button:hover, -body.theme-ayu-dark .pdf-viewer-toolbar button:hover, -body.theme-ayu-mirage .pdf-viewer-toolbar button:hover, -body.theme-oceanic-next .pdf-viewer-toolbar button:hover, -body.theme-cobalt2 .pdf-viewer-toolbar button:hover, -body.theme-concrete-dark .pdf-viewer-toolbar button:hover, -body.theme-concrete-warm .pdf-viewer-toolbar button:hover { - background: #4d4d4d !important; - border-color: #5d5d5d !important; -} - -body.theme-dark .pdf-viewer, -body.theme-onedark .pdf-viewer, -body.theme-one-dark .pdf-viewer, -body.theme-dracula .pdf-viewer, -body.theme-nord .pdf-viewer, -body.theme-tokyo-night .pdf-viewer, -body.theme-tokyonight .pdf-viewer, -body.theme-palenight .pdf-viewer, -body.theme-monokai .pdf-viewer, -body.theme-material .pdf-viewer, -body.theme-gruvbox-dark .pdf-viewer, -body.theme-ayu-dark .pdf-viewer, -body.theme-ayu-mirage .pdf-viewer, -body.theme-oceanic-next .pdf-viewer, -body.theme-cobalt2 .pdf-viewer, -body.theme-concrete-dark .pdf-viewer, -body.theme-concrete-warm .pdf-viewer { - background: #1a1a1a !important; -} - -body.theme-dark .pdf-viewer-container, -body.theme-onedark .pdf-viewer-container, -body.theme-one-dark .pdf-viewer-container, -body.theme-dracula .pdf-viewer-container, -body.theme-nord .pdf-viewer-container, -body.theme-tokyo-night .pdf-viewer-container, -body.theme-tokyonight .pdf-viewer-container, -body.theme-palenight .pdf-viewer-container, -body.theme-monokai .pdf-viewer-container, -body.theme-material .pdf-viewer-container, -body.theme-gruvbox-dark .pdf-viewer-container, -body.theme-ayu-dark .pdf-viewer-container, -body.theme-ayu-mirage .pdf-viewer-container, -body.theme-oceanic-next .pdf-viewer-container, -body.theme-cobalt2 .pdf-viewer-container, -body.theme-concrete-dark .pdf-viewer-container, -body.theme-concrete-warm .pdf-viewer-container { - background: #1a1a1a !important; -} - -body.theme-dark #pdf-page-input, -body.theme-onedark #pdf-page-input, -body.theme-one-dark #pdf-page-input, -body.theme-dracula #pdf-page-input, -body.theme-nord #pdf-page-input, -body.theme-tokyo-night #pdf-page-input, -body.theme-tokyonight #pdf-page-input, -body.theme-palenight #pdf-page-input, -body.theme-monokai #pdf-page-input, -body.theme-material #pdf-page-input, -body.theme-gruvbox-dark #pdf-page-input, -body.theme-ayu-dark #pdf-page-input, -body.theme-ayu-mirage #pdf-page-input, -body.theme-oceanic-next #pdf-page-input, -body.theme-cobalt2 #pdf-page-input, -body.theme-concrete-dark #pdf-page-input, -body.theme-concrete-warm #pdf-page-input { - background: #3d3d3d !important; - border: 1px solid #4d4d4d !important; - color: #e0e0e0 !important; -} - -body.theme-dark .pdf-toolbar-separator, -body.theme-onedark .pdf-toolbar-separator, -body.theme-one-dark .pdf-toolbar-separator, -body.theme-dracula .pdf-toolbar-separator, -body.theme-nord .pdf-toolbar-separator, -body.theme-tokyo-night .pdf-toolbar-separator, -body.theme-tokyonight .pdf-toolbar-separator, -body.theme-palenight .pdf-toolbar-separator, -body.theme-monokai .pdf-toolbar-separator, -body.theme-material .pdf-toolbar-separator, -body.theme-gruvbox-dark .pdf-toolbar-separator, -body.theme-ayu-dark .pdf-toolbar-separator, -body.theme-ayu-mirage .pdf-toolbar-separator, -body.theme-oceanic-next .pdf-toolbar-separator, -body.theme-cobalt2 .pdf-toolbar-separator, -body.theme-concrete-dark .pdf-toolbar-separator, -body.theme-concrete-warm .pdf-toolbar-separator { - background: #3d3d3d !important; -} - -.pdf-viewer { - flex: 1; - overflow: auto; - display: flex; - justify-content: center; - align-items: flex-start; - padding: 20px; - background: #525659; -} - -#pdf-canvas { - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); - background: white; -} - -/* Light theme PDF viewer */ -body.theme-light .pdf-viewer-container, -body.theme-atomonelight .pdf-viewer-container, -body.theme-github .pdf-viewer-container, -body.theme-solarized .pdf-viewer-container, -body.theme-gruvbox-light .pdf-viewer-container, -body.theme-ayu-light .pdf-viewer-container, -body.theme-sepia .pdf-viewer-container, -body.theme-paper .pdf-viewer-container, -body.theme-rosepine-dawn .pdf-viewer-container { - background: #e8e8e8; -} - -body.theme-light .pdf-viewer-toolbar, -body.theme-atomonelight .pdf-viewer-toolbar, -body.theme-github .pdf-viewer-toolbar, -body.theme-solarized .pdf-viewer-toolbar, -body.theme-gruvbox-light .pdf-viewer-toolbar, -body.theme-ayu-light .pdf-viewer-toolbar, -body.theme-sepia .pdf-viewer-toolbar, -body.theme-paper .pdf-viewer-toolbar, -body.theme-rosepine-dawn .pdf-viewer-toolbar { - background: #f5f5f5; - border-bottom-color: #ddd; -} - -body.theme-light .pdf-viewer-toolbar button, -body.theme-atomonelight .pdf-viewer-toolbar button, -body.theme-github .pdf-viewer-toolbar button { - background: #fff; - border-color: #ddd; - color: #333; -} - -body.theme-light .pdf-viewer-toolbar button:hover, -body.theme-atomonelight .pdf-viewer-toolbar button:hover, -body.theme-github .pdf-viewer-toolbar button:hover { - background: #e8e8e8; -} - -body.theme-light #pdf-page-info, -body.theme-atomonelight #pdf-page-info, -body.theme-github #pdf-page-info, -body.theme-light #pdf-zoom-level, -body.theme-atomonelight #pdf-zoom-level, -body.theme-github #pdf-zoom-level { - color: #333; -} - -body.theme-light #pdf-page-input, -body.theme-atomonelight #pdf-page-input, -body.theme-github #pdf-page-input { - background: #fff; - border-color: #ddd; - color: #333; -} - -body.theme-light .pdf-filename, -body.theme-atomonelight .pdf-filename, -body.theme-github .pdf-filename { - color: #666; -} - -body.theme-light .pdf-viewer, -body.theme-atomonelight .pdf-viewer, -body.theme-github .pdf-viewer, -body.theme-solarized .pdf-viewer, -body.theme-gruvbox-light .pdf-viewer, -body.theme-ayu-light .pdf-viewer, -body.theme-sepia .pdf-viewer, -body.theme-paper .pdf-viewer, -body.theme-rosepine-dawn .pdf-viewer { - background: #d0d0d0; -} - -/* ======================================== - Print Styles for styles-modern.css - Ensures proper print output - ======================================== */ - -@media print { - /* Reset container for print */ - .container { - padding: 0; - gap: 0; - display: block !important; - } - - /* Hide all UI elements */ - .tab-bar, - .toolbar, - .status-bar, - .pane-resizer, - .preview-header, - .find-dialog, - .export-dialog, - .batch-dialog, - .editor-pane, - [id^="editor-pane-"], - .new-tab-button, - .tab-close { - display: none !important; - } - - /* Show preview content */ - .preview-pane, - [id^="preview-pane-"], - .preview-content, - [id^="preview-"] { - display: block !important; - width: 100% !important; - padding: 20px !important; - position: static !important; - overflow: visible !important; - } - - /* Reset main content glass */ - .main-content-glass { - backdrop-filter: none !important; - border: none !important; - box-shadow: none !important; - } - - /* Reset editor container */ - .editor-container { - display: block !important; - } - - /* Reset tab content */ - .tab-content { - display: block !important; - } - - /* Reset pane */ - .pane { - display: block !important; - width: 100% !important; - } - - /* NO STYLES MODE - Black text on white (ink-saving) */ - body.printing-no-styles .container, - body.printing-no-styles .main-content-glass, - body.printing-no-styles .tab-content, - body.printing-no-styles .pane, - body.printing-no-styles .preview-pane, - body.printing-no-styles [id^="preview-pane-"], - body.printing-no-styles .preview-content, - body.printing-no-styles [id^="preview-"] { - background: white !important; - color: #000 !important; - } - - body.printing-no-styles .preview-content h1, - body.printing-no-styles .preview-content h2, - body.printing-no-styles .preview-content h3, - body.printing-no-styles .preview-content h4, - body.printing-no-styles .preview-content h5, - body.printing-no-styles .preview-content h6, - body.printing-no-styles .preview-content p, - body.printing-no-styles .preview-content li, - body.printing-no-styles .preview-content td, - body.printing-no-styles .preview-content th { - color: #000 !important; - } - - body.printing-no-styles .preview-content code, - body.printing-no-styles .preview-content pre { - background: #f5f5f5 !important; - color: #000 !important; - border-color: #ddd !important; - } - - body.printing-no-styles .preview-content a { - color: #000 !important; - } - - body.printing-no-styles .preview-content blockquote { - background: #f9f9f9 !important; - color: #333 !important; - border-left-color: #ccc !important; - } - - /* WITH STYLES MODE - Preserve theme colors */ - /* The theme colors are inherited from the body class */ -} - -/* ======================================== - Dark Theme Overrides for styles-modern.css - These ensure dark themes work properly - ======================================== */ - -/* Common dark theme styles for preview content */ -body.theme-dark .preview-content, -body.theme-dark [id^="preview-"], -body.theme-one-dark .preview-content, -body.theme-one-dark [id^="preview-"], -body.theme-dracula .preview-content, -body.theme-dracula [id^="preview-"], -body.theme-nord .preview-content, -body.theme-nord [id^="preview-"], -body.theme-tokyo-night .preview-content, -body.theme-tokyo-night [id^="preview-"], -body.theme-palenight .preview-content, -body.theme-palenight [id^="preview-"], -body.theme-ayu-dark .preview-content, -body.theme-ayu-dark [id^="preview-"], -body.theme-ayu-mirage .preview-content, -body.theme-ayu-mirage [id^="preview-"], -body.theme-oceanic-next .preview-content, -body.theme-oceanic-next [id^="preview-"], -body.theme-gruvbox-dark .preview-content, -body.theme-gruvbox-dark [id^="preview-"], -body.theme-cobalt2 .preview-content, -body.theme-cobalt2 [id^="preview-"] { - color: #e6e6e6; - background: transparent; -} - -/* Dark theme headings */ -body.theme-dark .preview-content h1, -body.theme-dark .preview-content h2, -body.theme-dark .preview-content h3, -body.theme-dark .preview-content h4, -body.theme-dark .preview-content h5, -body.theme-dark .preview-content h6, -body.theme-one-dark .preview-content h1, -body.theme-one-dark .preview-content h2, -body.theme-one-dark .preview-content h3, -body.theme-one-dark .preview-content h4, -body.theme-one-dark .preview-content h5, -body.theme-one-dark .preview-content h6, -body.theme-dracula .preview-content h1, -body.theme-dracula .preview-content h2, -body.theme-dracula .preview-content h3, -body.theme-dracula .preview-content h4, -body.theme-dracula .preview-content h5, -body.theme-dracula .preview-content h6, -body.theme-nord .preview-content h1, -body.theme-nord .preview-content h2, -body.theme-nord .preview-content h3, -body.theme-nord .preview-content h4, -body.theme-nord .preview-content h5, -body.theme-nord .preview-content h6, -body.theme-tokyo-night .preview-content h1, -body.theme-tokyo-night .preview-content h2, -body.theme-tokyo-night .preview-content h3, -body.theme-tokyo-night .preview-content h4, -body.theme-tokyo-night .preview-content h5, -body.theme-tokyo-night .preview-content h6, -body.theme-palenight .preview-content h1, -body.theme-palenight .preview-content h2, -body.theme-palenight .preview-content h3, -body.theme-palenight .preview-content h4, -body.theme-palenight .preview-content h5, -body.theme-palenight .preview-content h6, -body.theme-ayu-dark .preview-content h1, -body.theme-ayu-dark .preview-content h2, -body.theme-ayu-dark .preview-content h3, -body.theme-ayu-dark .preview-content h4, -body.theme-ayu-dark .preview-content h5, -body.theme-ayu-dark .preview-content h6, -body.theme-ayu-mirage .preview-content h1, -body.theme-ayu-mirage .preview-content h2, -body.theme-ayu-mirage .preview-content h3, -body.theme-ayu-mirage .preview-content h4, -body.theme-ayu-mirage .preview-content h5, -body.theme-ayu-mirage .preview-content h6, -body.theme-oceanic-next .preview-content h1, -body.theme-oceanic-next .preview-content h2, -body.theme-oceanic-next .preview-content h3, -body.theme-oceanic-next .preview-content h4, -body.theme-oceanic-next .preview-content h5, -body.theme-oceanic-next .preview-content h6, -body.theme-gruvbox-dark .preview-content h1, -body.theme-gruvbox-dark .preview-content h2, -body.theme-gruvbox-dark .preview-content h3, -body.theme-gruvbox-dark .preview-content h4, -body.theme-gruvbox-dark .preview-content h5, -body.theme-gruvbox-dark .preview-content h6, -body.theme-cobalt2 .preview-content h1, -body.theme-cobalt2 .preview-content h2, -body.theme-cobalt2 .preview-content h3, -body.theme-cobalt2 .preview-content h4, -body.theme-cobalt2 .preview-content h5, -body.theme-cobalt2 .preview-content h6 { - color: #f0f0f0; - border-bottom-color: #444; -} - -/* Dark theme paragraphs and text */ -body.theme-dark .preview-content p, -body.theme-dark .preview-content li, -body.theme-dark .preview-content td, -body.theme-one-dark .preview-content p, -body.theme-one-dark .preview-content li, -body.theme-one-dark .preview-content td, -body.theme-dracula .preview-content p, -body.theme-dracula .preview-content li, -body.theme-dracula .preview-content td, -body.theme-nord .preview-content p, -body.theme-nord .preview-content li, -body.theme-nord .preview-content td, -body.theme-tokyo-night .preview-content p, -body.theme-tokyo-night .preview-content li, -body.theme-tokyo-night .preview-content td, -body.theme-palenight .preview-content p, -body.theme-palenight .preview-content li, -body.theme-palenight .preview-content td, -body.theme-ayu-dark .preview-content p, -body.theme-ayu-dark .preview-content li, -body.theme-ayu-dark .preview-content td, -body.theme-ayu-mirage .preview-content p, -body.theme-ayu-mirage .preview-content li, -body.theme-ayu-mirage .preview-content td, -body.theme-oceanic-next .preview-content p, -body.theme-oceanic-next .preview-content li, -body.theme-oceanic-next .preview-content td, -body.theme-gruvbox-dark .preview-content p, -body.theme-gruvbox-dark .preview-content li, -body.theme-gruvbox-dark .preview-content td, -body.theme-cobalt2 .preview-content p, -body.theme-cobalt2 .preview-content li, -body.theme-cobalt2 .preview-content td { - color: #d4d4d4; -} - -/* Dark theme code blocks */ -body.theme-dark .preview-content pre, -body.theme-one-dark .preview-content pre, -body.theme-dracula .preview-content pre, -body.theme-nord .preview-content pre, -body.theme-tokyo-night .preview-content pre, -body.theme-palenight .preview-content pre, -body.theme-ayu-dark .preview-content pre, -body.theme-ayu-mirage .preview-content pre, -body.theme-oceanic-next .preview-content pre, -body.theme-gruvbox-dark .preview-content pre, -body.theme-cobalt2 .preview-content pre { - background: #1e1e1e; - border-color: #333; - color: #d4d4d4; -} - -/* Dark theme inline code */ -body.theme-dark .preview-content code, -body.theme-one-dark .preview-content code, -body.theme-dracula .preview-content code, -body.theme-nord .preview-content code, -body.theme-tokyo-night .preview-content code, -body.theme-palenight .preview-content code, -body.theme-ayu-dark .preview-content code, -body.theme-ayu-mirage .preview-content code, -body.theme-oceanic-next .preview-content code, -body.theme-gruvbox-dark .preview-content code, -body.theme-cobalt2 .preview-content code { - background: rgba(255, 255, 255, 0.1); - border-color: #444; - color: #e06c75; -} - -/* Dark theme blockquotes */ -body.theme-dark .preview-content blockquote, -body.theme-one-dark .preview-content blockquote, -body.theme-dracula .preview-content blockquote, -body.theme-nord .preview-content blockquote, -body.theme-tokyo-night .preview-content blockquote, -body.theme-palenight .preview-content blockquote, -body.theme-ayu-dark .preview-content blockquote, -body.theme-ayu-mirage .preview-content blockquote, -body.theme-oceanic-next .preview-content blockquote, -body.theme-gruvbox-dark .preview-content blockquote, -body.theme-cobalt2 .preview-content blockquote { - background: rgba(255, 255, 255, 0.05); - border-left-color: #555; - color: #aaa; -} - -/* Dark theme links */ -body.theme-dark .preview-content a, -body.theme-one-dark .preview-content a, -body.theme-dracula .preview-content a, -body.theme-nord .preview-content a, -body.theme-tokyo-night .preview-content a, -body.theme-palenight .preview-content a, -body.theme-ayu-dark .preview-content a, -body.theme-ayu-mirage .preview-content a, -body.theme-oceanic-next .preview-content a, -body.theme-gruvbox-dark .preview-content a, -body.theme-cobalt2 .preview-content a { - color: #61afef; -} - -/* Dark theme tables */ -body.theme-dark .preview-content table, -body.theme-one-dark .preview-content table, -body.theme-dracula .preview-content table, -body.theme-nord .preview-content table, -body.theme-tokyo-night .preview-content table, -body.theme-palenight .preview-content table, -body.theme-ayu-dark .preview-content table, -body.theme-ayu-mirage .preview-content table, -body.theme-oceanic-next .preview-content table, -body.theme-gruvbox-dark .preview-content table, -body.theme-cobalt2 .preview-content table { - border-color: #444; -} - -body.theme-dark .preview-content table th, -body.theme-one-dark .preview-content table th, -body.theme-dracula .preview-content table th, -body.theme-nord .preview-content table th, -body.theme-tokyo-night .preview-content table th, -body.theme-palenight .preview-content table th, -body.theme-ayu-dark .preview-content table th, -body.theme-ayu-mirage .preview-content table th, -body.theme-oceanic-next .preview-content table th, -body.theme-gruvbox-dark .preview-content table th, -body.theme-cobalt2 .preview-content table th { - background: #2d2d2d; - color: #e6e6e6; - border-color: #444; -} - -body.theme-dark .preview-content table td, -body.theme-one-dark .preview-content table td, -body.theme-dracula .preview-content table td, -body.theme-nord .preview-content table td, -body.theme-tokyo-night .preview-content table td, -body.theme-palenight .preview-content table td, -body.theme-ayu-dark .preview-content table td, -body.theme-ayu-mirage .preview-content table td, -body.theme-oceanic-next .preview-content table td, -body.theme-gruvbox-dark .preview-content table td, -body.theme-cobalt2 .preview-content table td { - border-color: #444; -} - -body.theme-dark .preview-content table tr:nth-child(even), -body.theme-one-dark .preview-content table tr:nth-child(even), -body.theme-dracula .preview-content table tr:nth-child(even), -body.theme-nord .preview-content table tr:nth-child(even), -body.theme-tokyo-night .preview-content table tr:nth-child(even), -body.theme-palenight .preview-content table tr:nth-child(even), -body.theme-ayu-dark .preview-content table tr:nth-child(even), -body.theme-ayu-mirage .preview-content table tr:nth-child(even), -body.theme-oceanic-next .preview-content table tr:nth-child(even), -body.theme-gruvbox-dark .preview-content table tr:nth-child(even), -body.theme-cobalt2 .preview-content table tr:nth-child(even) { - background: rgba(255, 255, 255, 0.03); -} - -/* Dark theme horizontal rules */ -body.theme-dark .preview-content hr, -body.theme-one-dark .preview-content hr, -body.theme-dracula .preview-content hr, -body.theme-nord .preview-content hr, -body.theme-tokyo-night .preview-content hr, -body.theme-palenight .preview-content hr, -body.theme-ayu-dark .preview-content hr, -body.theme-ayu-mirage .preview-content hr, -body.theme-oceanic-next .preview-content hr, -body.theme-gruvbox-dark .preview-content hr, -body.theme-cobalt2 .preview-content hr { - border-color: #444; - background: #444; -} - -/* ======================================== - Modal/Dialog Theming - Dialogs inherit app theme colors - ======================================== */ - -/* Dark theme modals */ -body.theme-dark .export-dialog-content, -body.theme-dark .batch-dialog-content, -body.theme-dark .pdf-editor-content, -body.theme-one-dark .export-dialog-content, -body.theme-one-dark .batch-dialog-content, -body.theme-one-dark .pdf-editor-content, -body.theme-onedark .export-dialog-content, -body.theme-onedark .batch-dialog-content, -body.theme-onedark .pdf-editor-content, -body.theme-dracula .export-dialog-content, -body.theme-dracula .batch-dialog-content, -body.theme-dracula .pdf-editor-content, -body.theme-nord .export-dialog-content, -body.theme-nord .batch-dialog-content, -body.theme-nord .pdf-editor-content, -body.theme-tokyo-night .export-dialog-content, -body.theme-tokyo-night .batch-dialog-content, -body.theme-tokyo-night .pdf-editor-content, -body.theme-tokyonight .export-dialog-content, -body.theme-tokyonight .batch-dialog-content, -body.theme-tokyonight .pdf-editor-content, -body.theme-palenight .export-dialog-content, -body.theme-palenight .batch-dialog-content, -body.theme-palenight .pdf-editor-content, -body.theme-ayu-dark .export-dialog-content, -body.theme-ayu-dark .batch-dialog-content, -body.theme-ayu-dark .pdf-editor-content, -body.theme-ayu-mirage .export-dialog-content, -body.theme-ayu-mirage .batch-dialog-content, -body.theme-ayu-mirage .pdf-editor-content, -body.theme-oceanic-next .export-dialog-content, -body.theme-oceanic-next .batch-dialog-content, -body.theme-oceanic-next .pdf-editor-content, -body.theme-gruvbox-dark .export-dialog-content, -body.theme-gruvbox-dark .batch-dialog-content, -body.theme-gruvbox-dark .pdf-editor-content, -body.theme-cobalt2 .export-dialog-content, -body.theme-cobalt2 .batch-dialog-content, -body.theme-cobalt2 .pdf-editor-content, -body.theme-monokai .export-dialog-content, -body.theme-monokai .batch-dialog-content, -body.theme-monokai .pdf-editor-content, -body.theme-material .export-dialog-content, -body.theme-material .batch-dialog-content, -body.theme-material .pdf-editor-content, -body.theme-concrete-dark .export-dialog-content, -body.theme-concrete-dark .batch-dialog-content, -body.theme-concrete-dark .pdf-editor-content, -body.theme-concrete-warm .export-dialog-content, -body.theme-concrete-warm .batch-dialog-content, -body.theme-concrete-warm .pdf-editor-content { - background: #2d2d2d !important; - border-color: #444 !important; - color: #e6e6e6 !important; -} - -/* Dark theme dialog headers */ -body.theme-dark .export-dialog-header, -body.theme-dark .batch-dialog-header, -body.theme-one-dark .export-dialog-header, -body.theme-one-dark .batch-dialog-header, -body.theme-onedark .export-dialog-header, -body.theme-onedark .batch-dialog-header, -body.theme-dracula .export-dialog-header, -body.theme-dracula .batch-dialog-header, -body.theme-nord .export-dialog-header, -body.theme-nord .batch-dialog-header, -body.theme-tokyo-night .export-dialog-header, -body.theme-tokyo-night .batch-dialog-header, -body.theme-tokyonight .export-dialog-header, -body.theme-tokyonight .batch-dialog-header, -body.theme-palenight .export-dialog-header, -body.theme-palenight .batch-dialog-header, -body.theme-ayu-dark .export-dialog-header, -body.theme-ayu-dark .batch-dialog-header, -body.theme-ayu-mirage .export-dialog-header, -body.theme-ayu-mirage .batch-dialog-header, -body.theme-oceanic-next .export-dialog-header, -body.theme-oceanic-next .batch-dialog-header, -body.theme-gruvbox-dark .export-dialog-header, -body.theme-gruvbox-dark .batch-dialog-header, -body.theme-cobalt2 .export-dialog-header, -body.theme-cobalt2 .batch-dialog-header, -body.theme-monokai .export-dialog-header, -body.theme-monokai .batch-dialog-header, -body.theme-material .export-dialog-header, -body.theme-material .batch-dialog-header, -body.theme-concrete-dark .export-dialog-header, -body.theme-concrete-dark .batch-dialog-header, -body.theme-concrete-warm .export-dialog-header, -body.theme-concrete-warm .batch-dialog-header { - background: #1a1a1a !important; - border-bottom-color: #333 !important; -} - -/* Dark theme dialog header text */ -body.theme-dark .export-dialog-header h3, -body.theme-dark .batch-dialog-header h3, -body.theme-onedark .export-dialog-header h3, -body.theme-onedark .batch-dialog-header h3, -body.theme-dracula .export-dialog-header h3, -body.theme-dracula .batch-dialog-header h3, -body.theme-nord .export-dialog-header h3, -body.theme-nord .batch-dialog-header h3, -body.theme-tokyonight .export-dialog-header h3, -body.theme-tokyonight .batch-dialog-header h3, -body.theme-monokai .export-dialog-header h3, -body.theme-monokai .batch-dialog-header h3, -body.theme-material .export-dialog-header h3, -body.theme-material .batch-dialog-header h3 { - color: #fff !important; -} - -/* Dark theme dialog body */ -body.theme-dark .export-dialog-body, -body.theme-dark .batch-dialog-body, -body.theme-one-dark .export-dialog-body, -body.theme-one-dark .batch-dialog-body, -body.theme-dracula .export-dialog-body, -body.theme-dracula .batch-dialog-body, -body.theme-nord .export-dialog-body, -body.theme-nord .batch-dialog-body, -body.theme-tokyo-night .export-dialog-body, -body.theme-tokyo-night .batch-dialog-body, -body.theme-palenight .export-dialog-body, -body.theme-palenight .batch-dialog-body, -body.theme-ayu-dark .export-dialog-body, -body.theme-ayu-dark .batch-dialog-body, -body.theme-ayu-mirage .export-dialog-body, -body.theme-ayu-mirage .batch-dialog-body, -body.theme-oceanic-next .export-dialog-body, -body.theme-oceanic-next .batch-dialog-body, -body.theme-gruvbox-dark .export-dialog-body, -body.theme-gruvbox-dark .batch-dialog-body, -body.theme-cobalt2 .export-dialog-body, -body.theme-cobalt2 .batch-dialog-body { - background: transparent; -} - -/* Dark theme dialog labels */ -body.theme-dark .export-section label, -body.theme-dark .batch-section label, -body.theme-one-dark .export-section label, -body.theme-one-dark .batch-section label, -body.theme-dracula .export-section label, -body.theme-dracula .batch-section label, -body.theme-nord .export-section label, -body.theme-nord .batch-section label, -body.theme-tokyo-night .export-section label, -body.theme-tokyo-night .batch-section label, -body.theme-palenight .export-section label, -body.theme-palenight .batch-section label, -body.theme-ayu-dark .export-section label, -body.theme-ayu-dark .batch-section label, -body.theme-ayu-mirage .export-section label, -body.theme-ayu-mirage .batch-section label, -body.theme-oceanic-next .export-section label, -body.theme-oceanic-next .batch-section label, -body.theme-gruvbox-dark .export-section label, -body.theme-gruvbox-dark .batch-section label, -body.theme-cobalt2 .export-section label, -body.theme-cobalt2 .batch-section label { - color: #ccc; -} - -/* Dark theme dialog inputs and selects */ -body.theme-dark .export-section select, -body.theme-dark .export-section input, -body.theme-dark .batch-section select, -body.theme-dark .batch-section input, -body.theme-one-dark .export-section select, -body.theme-one-dark .export-section input, -body.theme-one-dark .batch-section select, -body.theme-one-dark .batch-section input, -body.theme-dracula .export-section select, -body.theme-dracula .export-section input, -body.theme-dracula .batch-section select, -body.theme-dracula .batch-section input, -body.theme-nord .export-section select, -body.theme-nord .export-section input, -body.theme-nord .batch-section select, -body.theme-nord .batch-section input, -body.theme-tokyo-night .export-section select, -body.theme-tokyo-night .export-section input, -body.theme-tokyo-night .batch-section select, -body.theme-tokyo-night .batch-section input, -body.theme-palenight .export-section select, -body.theme-palenight .export-section input, -body.theme-palenight .batch-section select, -body.theme-palenight .batch-section input, -body.theme-ayu-dark .export-section select, -body.theme-ayu-dark .export-section input, -body.theme-ayu-dark .batch-section select, -body.theme-ayu-dark .batch-section input, -body.theme-ayu-mirage .export-section select, -body.theme-ayu-mirage .export-section input, -body.theme-ayu-mirage .batch-section select, -body.theme-ayu-mirage .batch-section input, -body.theme-oceanic-next .export-section select, -body.theme-oceanic-next .export-section input, -body.theme-oceanic-next .batch-section select, -body.theme-oceanic-next .batch-section input, -body.theme-gruvbox-dark .export-section select, -body.theme-gruvbox-dark .export-section input, -body.theme-gruvbox-dark .batch-section select, -body.theme-gruvbox-dark .batch-section input, -body.theme-cobalt2 .export-section select, -body.theme-cobalt2 .export-section input, -body.theme-cobalt2 .batch-section select, -body.theme-cobalt2 .batch-section input { - background: #2d2d2d; - border-color: #444; - color: #e6e6e6; -} - -/* Dark theme dialog footer */ -body.theme-dark .export-dialog-footer, -body.theme-dark .batch-dialog-footer, -body.theme-one-dark .export-dialog-footer, -body.theme-one-dark .batch-dialog-footer, -body.theme-dracula .export-dialog-footer, -body.theme-dracula .batch-dialog-footer, -body.theme-nord .export-dialog-footer, -body.theme-nord .batch-dialog-footer, -body.theme-tokyo-night .export-dialog-footer, -body.theme-tokyo-night .batch-dialog-footer, -body.theme-palenight .export-dialog-footer, -body.theme-palenight .batch-dialog-footer, -body.theme-ayu-dark .export-dialog-footer, -body.theme-ayu-dark .batch-dialog-footer, -body.theme-ayu-mirage .export-dialog-footer, -body.theme-ayu-mirage .batch-dialog-footer, -body.theme-oceanic-next .export-dialog-footer, -body.theme-oceanic-next .batch-dialog-footer, -body.theme-gruvbox-dark .export-dialog-footer, -body.theme-gruvbox-dark .batch-dialog-footer, -body.theme-cobalt2 .export-dialog-footer, -body.theme-cobalt2 .batch-dialog-footer { - background: #1a1a1a; - border-top-color: #333; -} - -/* Sepia theme modals */ -body.theme-sepia .export-dialog-content, -body.theme-sepia .batch-dialog-content, -body.theme-sepia .pdf-editor-content { - background: rgba(244, 236, 216, 0.98); - border-color: #d4c4a8; - color: #5b4636; -} - -body.theme-sepia .export-dialog-header, -body.theme-sepia .batch-dialog-header { - background: #e8dcc8; - border-bottom-color: #d4c4a8; -} - -body.theme-sepia .export-dialog-header h3, -body.theme-sepia .batch-dialog-header h3 { - color: #3d2914; -} - -body.theme-sepia .export-section label, -body.theme-sepia .batch-section label { - color: #5b4636; -} - -body.theme-sepia .export-section select, -body.theme-sepia .export-section input, -body.theme-sepia .batch-section select, -body.theme-sepia .batch-section input { - background: #f4ecd8; - border-color: #d4c4a8; - color: #5b4636; -} - -body.theme-sepia .export-dialog-footer, -body.theme-sepia .batch-dialog-footer { - background: #e8dcc8; - border-top-color: #d4c4a8; -} - -/* Rose Pine Dawn theme modals */ -body.theme-rosepine-dawn .export-dialog-content, -body.theme-rosepine-dawn .batch-dialog-content, -body.theme-rosepine-dawn .pdf-editor-content { - background: rgba(250, 244, 237, 0.98); - border-color: #dfdad9; - color: #575279; -} - -body.theme-rosepine-dawn .export-dialog-header, -body.theme-rosepine-dawn .batch-dialog-header { - background: #f2e9e1; - border-bottom-color: #dfdad9; -} - -body.theme-rosepine-dawn .export-dialog-header h3, -body.theme-rosepine-dawn .batch-dialog-header h3 { - color: #286983; -} - -body.theme-rosepine-dawn .export-section label, -body.theme-rosepine-dawn .batch-section label { - color: #575279; -} - -body.theme-rosepine-dawn .export-section select, -body.theme-rosepine-dawn .export-section input, -body.theme-rosepine-dawn .batch-section select, -body.theme-rosepine-dawn .batch-section input { - background: #faf4ed; - border-color: #dfdad9; - color: #575279; -} - -body.theme-rosepine-dawn .export-dialog-footer, -body.theme-rosepine-dawn .batch-dialog-footer { - background: #f2e9e1; - border-top-color: #dfdad9; -} +/* ======================================== + PanConverter v1.7.1 - Modern UI + Glassmorphism, Gradients & Animations + ======================================== */ + +/* Modern Color Palette */ +:root { + /* Primary Brand Colors - Purple-Blue Gradient */ + --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --primary-light: #8b9aff; + --primary-dark: #5661b3; + + /* Accent Colors */ + --accent-green: #10b981; + --accent-blue: #3b82f6; + --accent-purple: #8b5cf6; + --accent-pink: #ec4899; + + /* Neutral Colors */ + --gray-50: #f9fafb; + --gray-100: #f3f4f6; + --gray-200: #e5e7eb; + --gray-300: #d1d5db; + --gray-400: #9ca3af; + --gray-500: #6b7280; + --gray-600: #4b5563; + --gray-700: #374151; + --gray-800: #1f2937; + --gray-900: #111827; + + /* Glassmorphism */ + --glass-bg: rgba(255, 255, 255, 0.7); + --glass-border: rgba(255, 255, 255, 0.18); + --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15); + + /* Effects */ + --blur-amount: 10px; + --transition-speed: 0.3s; + --transition-ease: cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Reset & Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + overflow: hidden; + height: 100vh; + /* Background controlled by themes - do not set here */ + color: var(--gray-900); +} + +@keyframes gradientShift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +.container { + display: flex; + flex-direction: column; + height: 100vh; + padding: 4px 8px; + gap: 2px; +} + +/* Glass Container for main content */ +.main-content-glass { + background: var(--glass-bg); + backdrop-filter: blur(var(--blur-amount)); + -webkit-backdrop-filter: blur(var(--blur-amount)); + border: 1px solid var(--glass-border); + border-radius: 20px; + box-shadow: var(--glass-shadow); + overflow: hidden; + flex: 1; + display: flex; + flex-direction: column; +} + +/* Tab Bar - Modern Glassmorphism */ +.tab-bar { + display: flex; + align-items: center; + background: rgba(255, 255, 255, 0.5); + backdrop-filter: blur(10px); + border-bottom: 1px solid rgba(0, 0, 0, 0.08); + padding: 4px 8px; + min-height: 36px; + gap: 4px; + border-radius: 8px; +} + +.tab { + display: flex; + align-items: center; + padding: 8px 16px; + background: rgba(255, 255, 255, 0.3); + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 12px; + margin-right: 4px; + cursor: pointer; + user-select: none; + max-width: 200px; + min-width: 120px; + transition: all var(--transition-speed) var(--transition-ease); + transform: translateY(0); +} + +.tab:hover { + background: rgba(0, 0, 0, 0.05); +} + +.tab.active { + background: #ffffff; + color: #24292e; + border-bottom: 2px solid #0366d6; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.tab-title { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 13px; + font-weight: 500; +} + +.tab-close { + background: rgba(255, 255, 255, 0.2); + border: none; + font-size: 14px; + font-weight: bold; + cursor: pointer; + padding: 4px; + margin-left: 8px; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + color: currentColor; + transition: all var(--transition-speed) var(--transition-ease); +} + +.tab-close:hover { + background: rgba(255, 255, 255, 0.4); + transform: rotate(90deg); +} + +.new-tab-button { + background: rgba(255, 255, 255, 0.3); + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 10px; + padding: 8px 16px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + color: var(--gray-700); + transition: all var(--transition-speed) var(--transition-ease); + display: flex; + align-items: center; + gap: 6px; +} + +.new-tab-button:hover { + background: rgba(255, 255, 255, 0.5); + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +/* Toolbar - Modern Gradient Buttons */ +.toolbar { + display: flex; + align-items: center; + padding: 4px 8px; + background: rgba(255, 255, 255, 0.4); + backdrop-filter: blur(10px); + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + gap: 2px; + border-radius: 8px; +} + +.toolbar button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 1px solid transparent; + background: transparent; + border-radius: 6px; + cursor: pointer; + transition: all 0.15s ease; + color: #586069; +} + +.toolbar button:hover { + background: rgba(0, 0, 0, 0.06); + color: #24292e; +} + +.toolbar button:active { + background: rgba(0, 0, 0, 0.1); + transform: scale(0.95); +} + +.toolbar-separator { + width: 1px; + height: 24px; + background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.4), transparent); + margin: 0 8px; +} + +/* Tab Content */ +.tab-content { + display: flex; + flex: 1; + overflow: hidden; +} + +.tab-content:not(.active) { + display: none; +} + +/* Editor Container */ +.editor-container { + display: flex; + flex: 1; + overflow: hidden; + background: rgba(255, 255, 255, 0.3); + backdrop-filter: blur(10px); + border-radius: 0 0 20px 20px; +} + +.pane { + flex: 1; + overflow: auto; + min-width: 0; +} + +.pane:first-child { + border-right: 1px solid rgba(255, 255, 255, 0.3); +} + +/* Custom Scrollbar */ +.pane::-webkit-scrollbar, +.editor-textarea::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +.pane::-webkit-scrollbar-track, +.editor-textarea::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; +} + +.pane::-webkit-scrollbar-thumb, +.editor-textarea::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.25); + border-radius: 10px; + border: 2px solid rgba(255, 255, 255, 0.2); +} + +.pane::-webkit-scrollbar-thumb:hover, +.editor-textarea::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.4); +} + +.editor-textarea { + width: 100%; + height: 100%; + padding: 24px; + font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Courier New', monospace; + font-size: 15px; + line-height: 1.7; + border: none; + outline: none; + resize: none; + background: transparent; + /* Color controlled by theme */ +} + +.pane:last-child { + padding: 0; + /* Background controlled by theme */ +} + +.preview-content { + max-width: none; + margin: 0; + padding: 24px 32px; + line-height: 1.8; + font-size: 15px; + /* Color controlled by theme */ +} + +/* Status Bar */ +.status-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 20px; + /* Background and color controlled by theme */ + font-size: 12px; + font-weight: 500; +} + +/* Preview Styles - Modern Typography */ +.preview-content h1, .preview-content h2, .preview-content h3 { + /* Color controlled by theme */ + font-weight: 700; + margin-top: 28px; + margin-bottom: 16px; + line-height: 1.3; +} + +.preview-content h1 { + font-size: 2.5em; + border-bottom: 3px solid #e1e4e8; + padding-bottom: 12px; +} + +.preview-content h2 { + font-size: 2em; + border-bottom: 2px solid #e1e4e8; + padding-bottom: 10px; +} + +.preview-content p { + margin-bottom: 18px; + line-height: 1.8; +} + +.preview-content code { + padding: 3px 8px; + margin: 0 2px; + font-size: 90%; + background: rgba(175, 184, 193, 0.2); + border: 1px solid #e1e4e8; + border-radius: 6px; + font-family: 'JetBrains Mono', 'Fira Code', monospace; + font-weight: 500; +} + +.preview-content pre { + padding: 20px; + overflow-x: auto; + font-size: 14px; + line-height: 1.6; + background: #f6f8fa; + border: 1px solid #e1e4e8; + border-radius: 12px; + margin-bottom: 20px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); +} + +.preview-content pre code { + background: transparent; + border: none; + padding: 0; + margin: 0; +} + +.preview-content blockquote { + padding: 16px 20px; + color: var(--gray-700); + border-left: 4px solid #e1e4e8; + margin-bottom: 20px; + background: #f6f8fa; + border-radius: 0 8px 8px 0; + font-style: italic; +} + +.preview-content a { + color: #0366d6; + text-decoration: none; + font-weight: 600; + position: relative; + transition: color var(--transition-speed); +} + +.preview-content a::after { + content: ''; + position: absolute; + left: 0; + bottom: -2px; + width: 0; + height: 2px; + background: #0366d6; + transition: width var(--transition-speed) var(--transition-ease); +} + +.preview-content a:hover { + color: #0366d6; +} + +.preview-content a:hover::after { + width: 100%; +} + +.preview-content table { + border-collapse: separate; + border-spacing: 0; + margin-bottom: 20px; + width: 100%; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); +} + +.preview-content table th, +.preview-content table td { + padding: 12px 16px; + border: 1px solid #e1e4e8; +} + +.preview-content table th { + font-weight: 600; + background: #f0f0f0; + color: #333333; +} + +.preview-content table tr:nth-child(even) { + background: rgba(255, 255, 255, 0.5); +} + +/* Modern Dialogs */ +.export-dialog, +.batch-dialog, +.find-dialog-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(8px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn 0.3s var(--transition-ease); +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.export-dialog.hidden, +.batch-dialog.hidden { + display: none; +} + +.export-dialog-content, +.batch-dialog-content { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 20px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + width: 90%; + max-width: 650px; + max-height: 90vh; + overflow-y: auto; + animation: slideUpFade 0.4s var(--transition-ease); +} + +@keyframes slideUpFade { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.export-dialog-header, +.batch-dialog-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + border-bottom: 1px solid #e1e4e8; + background: #24292e; + border-radius: 20px 20px 0 0; +} + +.export-dialog-header h3, +.batch-dialog-header h3 { + margin: 0; + font-size: 20px; + font-weight: 700; + color: white; +} + +.export-dialog-body, +.batch-dialog-body { + padding: 24px; +} + +.export-section, +.batch-section { + margin-bottom: 20px; +} + +.export-section label, +.batch-section label { + display: block; + font-weight: 600; + margin-bottom: 8px; + color: var(--gray-700); + font-size: 14px; +} + +.export-section select, +.export-section input, +.batch-section select, +.batch-section input { + width: 100%; + padding: 10px 14px; + border: 2px solid #e1e4e8; + border-radius: 10px; + font-size: 14px; + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(5px); + transition: all var(--transition-speed) var(--transition-ease); +} + +.export-section select:focus, +.export-section input:focus, +.batch-section select:focus, +.batch-section input:focus { + outline: none; + border-color: #0366d6; + box-shadow: 0 0 0 4px rgba(3, 102, 214, 0.1); + background: white; +} + +/* Modern Buttons */ +.export-dialog-footer, +.batch-dialog-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 20px 24px; + border-top: 1px solid #e1e4e8; + background: #f6f8fa; + border-radius: 0 0 20px 20px; +} + +.export-dialog-footer button, +.batch-dialog-footer button, +.folder-input-group button { + padding: 10px 24px; + border: none; + border-radius: 10px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition: all var(--transition-speed) var(--transition-ease); + position: relative; + overflow: hidden; +} + +.export-dialog-footer button.primary, +.batch-dialog-footer button.primary { + background: #2ea44f; + color: white; + box-shadow: 0 4px 12px rgba(46, 164, 79, 0.3); +} + +.export-dialog-footer button.primary:hover, +.batch-dialog-footer button.primary:hover { + transform: translateY(-2px); + background: #22863a; + box-shadow: 0 6px 20px rgba(34, 134, 58, 0.4); +} + +.export-dialog-footer button:not(.primary), +.batch-dialog-footer button:not(.primary), +.folder-input-group button { + background: #fafbfc; + color: var(--gray-700); + border: 1px solid #e1e4e8; +} + +.export-dialog-footer button:not(.primary):hover, +.batch-dialog-footer button:not(.primary):hover, +.folder-input-group button:hover { + background: #f3f4f6; + border-color: #d0d7de; + transform: translateY(-2px); +} + +/* Find Dialog - Floating Modern */ +.find-dialog { + position: absolute; + top: 70px; + right: 24px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 16px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + z-index: 1000; + min-width: 420px; + padding: 16px; + animation: slideInRight 0.3s var(--transition-ease); +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(30px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.find-dialog.hidden { + display: none; +} + +/* Progress Bars - Modern Gradient */ +.progress-bar { + width: 100%; + height: 8px; + background: #e1e4e8; + border-radius: 10px; + overflow: hidden; + margin-bottom: 12px; +} + +.progress-fill { + height: 100%; + background: #2ea44f; + transition: width 0.4s var(--transition-ease); + width: 0%; + position: relative; + overflow: hidden; +} + +.progress-fill::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, + transparent, + rgba(255, 255, 255, 0.3), + transparent + ); + animation: shimmer 2s infinite; +} + +@keyframes shimmer { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } +} + +/* Auto-save Indicator - Modern */ +.auto-save-indicator { + position: fixed; + top: 24px; + right: 24px; + background: #2ea44f; + color: white; + padding: 10px 20px; + border-radius: 12px; + font-size: 13px; + font-weight: 600; + z-index: 10000; + box-shadow: 0 4px 20px rgba(46, 164, 79, 0.3); + animation: slideInRight 0.3s ease-out; + display: flex; + align-items: center; + gap: 8px; +} + +.auto-save-indicator::before { + content: '✓'; + display: inline-block; + width: 18px; + height: 18px; + background: white; + color: #2ea44f; + border-radius: 50%; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; +} + +/* Enhanced Statistics - Modern */ +.enhanced-stats { + font-size: 11px; + color: var(--gray-600); + padding: 2px 0; + margin-left: 16px; + font-family: 'JetBrains Mono', monospace; + font-weight: 500; +} + +/* Line Numbers - Modern */ +.line-numbers { + min-width: 50px; + padding: 12px 8px; + background: rgba(255, 255, 255, 0.3); + backdrop-filter: blur(8px); + border-right: 1px solid #e1e4e8; + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + line-height: 1.7; + color: var(--gray-500); + text-align: right; + user-select: none; + overflow: hidden; +} + +.line-numbers.hidden { + display: none; +} + +/* Animations & Transitions */ +.toolbar button, +.tab, +.new-tab-button, +.export-dialog-footer button, +.batch-dialog-footer button { + position: relative; + overflow: hidden; +} + +.toolbar button::before, +.tab::before, +.new-tab-button::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.4); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; +} + +.toolbar button:hover::before, +.tab:hover::before, +.new-tab-button:hover::before { + width: 200px; + height: 200px; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + padding: 8px; + gap: 8px; + } + + .export-dialog-content, + .batch-dialog-content { + max-width: 95%; + } + + .find-dialog { + right: 12px; + min-width: 320px; + } +} + +/* Hide default elements */ +.pane.hidden { + display: none; +} + +.pane.full-width { + border-right: none; +} + +/* ======================================== + Dynamic Pane Resizer + Draggable splitter between editor/preview + ======================================== */ + +.pane-resizer { + width: 6px; + background: linear-gradient(to right, #e1e4e8, #d0d7de, #e1e4e8); + cursor: col-resize; + position: relative; + flex-shrink: 0; + transition: background 0.2s ease; + border-radius: 3px; +} + +.pane-resizer:hover { + background: linear-gradient(to right, #0366d6, #0366d6, #0366d6); +} + +.pane-resizer:active { + background: #0366d6; +} + +.pane-resizer::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 2px; + height: 30px; + background: rgba(0, 0, 0, 0.2); + border-radius: 1px; +} + +.pane-resizer:hover::before { + background: rgba(255, 255, 255, 0.8); +} + +/* Dark theme resizer */ +body.theme-dark .pane-resizer, +body.theme-one-dark .pane-resizer, +body.theme-dracula .pane-resizer, +body.theme-nord .pane-resizer, +body.theme-tokyo-night .pane-resizer, +body.theme-palenight .pane-resizer, +body.theme-ayu-dark .pane-resizer, +body.theme-ayu-mirage .pane-resizer, +body.theme-oceanic-next .pane-resizer, +body.theme-gruvbox-dark .pane-resizer, +body.theme-cobalt2 .pane-resizer { + background: linear-gradient(to right, #333, #444, #333); +} + +body.theme-dark .pane-resizer:hover, +body.theme-one-dark .pane-resizer:hover, +body.theme-dracula .pane-resizer:hover { + background: #61afef; +} + +/* ======================================== + PDF Viewer Styles + Full-featured PDF reader + ======================================== */ + +.pdf-viewer-container { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + background: #525659; +} + +.pdf-viewer-container.hidden { + display: none; +} + +.pdf-viewer-toolbar { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: #323639; + border-bottom: 1px solid #1a1a1a; + flex-shrink: 0; +} + +.pdf-viewer-toolbar button { + background: #474a4d; + border: 1px solid #5a5d60; + border-radius: 4px; + color: #fff; + padding: 6px 10px; + cursor: pointer; + font-size: 14px; + transition: all 0.15s ease; +} + +.pdf-viewer-toolbar button:hover { + background: #5a5d60; +} + +.pdf-viewer-toolbar button:active { + background: #0366d6; +} + +.pdf-toolbar-separator { + width: 1px; + height: 24px; + background: #5a5d60; + margin: 0 4px; +} + +#pdf-page-info { + color: #fff; + font-size: 13px; + display: flex; + align-items: center; + gap: 4px; +} + +#pdf-page-input { + width: 50px; + padding: 4px 6px; + border: 1px solid #5a5d60; + border-radius: 4px; + background: #474a4d; + color: #fff; + text-align: center; + font-size: 13px; +} + +#pdf-zoom-level { + color: #fff; + font-size: 13px; + min-width: 50px; + text-align: center; +} + +.pdf-filename { + color: #aaa; + font-size: 12px; + flex: 1; + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-right: 8px; +} + +#pdf-close { + background: #c0392b !important; + border-color: #e74c3c !important; +} + +#pdf-close:hover { + background: #e74c3c !important; +} + +/* PDF Editor Toolbar Buttons */ +.pdf-editor-btn { + display: flex; + align-items: center; + gap: 4px; + background: linear-gradient(135deg, #3498db, #2980b9) !important; + border: 1px solid #2980b9 !important; + color: #fff !important; + padding: 5px 8px !important; + font-size: 11px !important; + font-weight: 500; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; +} + +.pdf-editor-btn:hover { + background: linear-gradient(135deg, #2980b9, #1a5276) !important; + transform: translateY(-1px); +} + +.pdf-editor-btn:active { + transform: translateY(0); +} + +.pdf-editor-btn svg { + flex-shrink: 0; +} + +.pdf-editor-btn span { + white-space: nowrap; +} + +/* PDF toolbar wrapping for smaller screens */ +.pdf-toolbar-main { + flex-wrap: wrap; + min-height: auto; + padding: 6px 10px !important; +} + +/* Light theme PDF toolbar - refined styling */ +body.theme-light .pdf-viewer-toolbar, +body.theme-atomonelight .pdf-viewer-toolbar, +body.theme-github .pdf-viewer-toolbar, +body.theme-solarized .pdf-viewer-toolbar, +body.theme-gruvbox-light .pdf-viewer-toolbar, +body.theme-ayu-light .pdf-viewer-toolbar, +body.theme-sepia .pdf-viewer-toolbar, +body.theme-paper .pdf-viewer-toolbar, +body.theme-rosepine-dawn .pdf-viewer-toolbar { + background: linear-gradient(180deg, #fafafa 0%, #f0f0f0 100%) !important; + border-bottom: 1px solid #d0d0d0 !important; + box-shadow: 0 1px 3px rgba(0,0,0,0.08); +} + +body.theme-light .pdf-viewer-toolbar button, +body.theme-atomonelight .pdf-viewer-toolbar button, +body.theme-github .pdf-viewer-toolbar button, +body.theme-solarized .pdf-viewer-toolbar button, +body.theme-gruvbox-light .pdf-viewer-toolbar button, +body.theme-ayu-light .pdf-viewer-toolbar button, +body.theme-sepia .pdf-viewer-toolbar button, +body.theme-paper .pdf-viewer-toolbar button, +body.theme-rosepine-dawn .pdf-viewer-toolbar button { + background: #fff !important; + border: 1px solid #ccc !important; + color: #333 !important; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); +} + +body.theme-light .pdf-viewer-toolbar button:hover, +body.theme-atomonelight .pdf-viewer-toolbar button:hover, +body.theme-github .pdf-viewer-toolbar button:hover, +body.theme-solarized .pdf-viewer-toolbar button:hover, +body.theme-gruvbox-light .pdf-viewer-toolbar button:hover, +body.theme-ayu-light .pdf-viewer-toolbar button:hover, +body.theme-sepia .pdf-viewer-toolbar button:hover, +body.theme-paper .pdf-viewer-toolbar button:hover, +body.theme-rosepine-dawn .pdf-viewer-toolbar button:hover { + background: #e8e8e8 !important; + border-color: #bbb !important; +} + +/* Light theme PDF editor buttons */ +body.theme-light .pdf-editor-btn, +body.theme-atomonelight .pdf-editor-btn, +body.theme-github .pdf-editor-btn, +body.theme-solarized .pdf-editor-btn, +body.theme-gruvbox-light .pdf-editor-btn, +body.theme-ayu-light .pdf-editor-btn, +body.theme-sepia .pdf-editor-btn, +body.theme-paper .pdf-editor-btn, +body.theme-rosepine-dawn .pdf-editor-btn { + background: linear-gradient(135deg, #4a90d9, #357abd) !important; + border: 1px solid #357abd !important; +} + +body.theme-light .pdf-editor-btn:hover, +body.theme-atomonelight .pdf-editor-btn:hover, +body.theme-github .pdf-editor-btn:hover, +body.theme-solarized .pdf-editor-btn:hover, +body.theme-gruvbox-light .pdf-editor-btn:hover, +body.theme-ayu-light .pdf-editor-btn:hover, +body.theme-sepia .pdf-editor-btn:hover, +body.theme-paper .pdf-editor-btn:hover, +body.theme-rosepine-dawn .pdf-editor-btn:hover { + background: linear-gradient(135deg, #357abd, #2868a0) !important; +} + +/* Light theme page info */ +body.theme-light #pdf-page-info, +body.theme-atomonelight #pdf-page-info, +body.theme-github #pdf-page-info, +body.theme-solarized #pdf-page-info, +body.theme-gruvbox-light #pdf-page-info, +body.theme-ayu-light #pdf-page-info, +body.theme-sepia #pdf-page-info, +body.theme-paper #pdf-page-info, +body.theme-rosepine-dawn #pdf-page-info, +body.theme-light #pdf-zoom-level, +body.theme-atomonelight #pdf-zoom-level, +body.theme-github #pdf-zoom-level, +body.theme-solarized #pdf-zoom-level, +body.theme-gruvbox-light #pdf-zoom-level, +body.theme-ayu-light #pdf-zoom-level, +body.theme-sepia #pdf-zoom-level, +body.theme-paper #pdf-zoom-level, +body.theme-rosepine-dawn #pdf-zoom-level { + color: #444 !important; + font-weight: 500; +} + +body.theme-light #pdf-page-input, +body.theme-atomonelight #pdf-page-input, +body.theme-github #pdf-page-input, +body.theme-solarized #pdf-page-input, +body.theme-gruvbox-light #pdf-page-input, +body.theme-ayu-light #pdf-page-input, +body.theme-sepia #pdf-page-input, +body.theme-paper #pdf-page-input, +body.theme-rosepine-dawn #pdf-page-input { + background: #fff !important; + border: 1px solid #ccc !important; + color: #333 !important; +} + +body.theme-light .pdf-toolbar-separator, +body.theme-atomonelight .pdf-toolbar-separator, +body.theme-github .pdf-toolbar-separator, +body.theme-solarized .pdf-toolbar-separator, +body.theme-gruvbox-light .pdf-toolbar-separator, +body.theme-ayu-light .pdf-toolbar-separator, +body.theme-sepia .pdf-toolbar-separator, +body.theme-paper .pdf-toolbar-separator, +body.theme-rosepine-dawn .pdf-toolbar-separator { + background: #d0d0d0 !important; +} + +body.theme-light .pdf-filename, +body.theme-atomonelight .pdf-filename, +body.theme-github .pdf-filename, +body.theme-solarized .pdf-filename, +body.theme-gruvbox-light .pdf-filename, +body.theme-ayu-light .pdf-filename, +body.theme-sepia .pdf-filename, +body.theme-paper .pdf-filename, +body.theme-rosepine-dawn .pdf-filename { + color: #555 !important; +} + +/* Sepia theme specific */ +body.theme-sepia .pdf-viewer-toolbar { + background: linear-gradient(180deg, #f4f0e8 0%, #e8e4dc 100%) !important; + border-bottom-color: #cdc8c0 !important; +} + +body.theme-sepia .pdf-viewer { + background: #d8d4cc !important; +} + +/* Rose Pine Dawn specific */ +body.theme-rosepine-dawn .pdf-viewer-toolbar { + background: linear-gradient(180deg, #faf4ed 0%, #f2e9e1 100%) !important; + border-bottom-color: #ddd8d1 !important; +} + +body.theme-rosepine-dawn .pdf-viewer { + background: #e6e0d8 !important; +} + +/* Dark theme PDF toolbar refinement */ +body.theme-dark .pdf-viewer-toolbar, +body.theme-onedark .pdf-viewer-toolbar, +body.theme-one-dark .pdf-viewer-toolbar, +body.theme-dracula .pdf-viewer-toolbar, +body.theme-nord .pdf-viewer-toolbar, +body.theme-tokyo-night .pdf-viewer-toolbar, +body.theme-tokyonight .pdf-viewer-toolbar, +body.theme-palenight .pdf-viewer-toolbar, +body.theme-monokai .pdf-viewer-toolbar, +body.theme-material .pdf-viewer-toolbar, +body.theme-gruvbox-dark .pdf-viewer-toolbar, +body.theme-ayu-dark .pdf-viewer-toolbar, +body.theme-ayu-mirage .pdf-viewer-toolbar, +body.theme-oceanic-next .pdf-viewer-toolbar, +body.theme-cobalt2 .pdf-viewer-toolbar, +body.theme-concrete-dark .pdf-viewer-toolbar, +body.theme-concrete-warm .pdf-viewer-toolbar { + background: linear-gradient(180deg, #2d2d2d 0%, #252525 100%) !important; + border-bottom: 1px solid #1a1a1a !important; +} + +body.theme-dark .pdf-viewer-toolbar button, +body.theme-onedark .pdf-viewer-toolbar button, +body.theme-one-dark .pdf-viewer-toolbar button, +body.theme-dracula .pdf-viewer-toolbar button, +body.theme-nord .pdf-viewer-toolbar button, +body.theme-tokyo-night .pdf-viewer-toolbar button, +body.theme-tokyonight .pdf-viewer-toolbar button, +body.theme-palenight .pdf-viewer-toolbar button, +body.theme-monokai .pdf-viewer-toolbar button, +body.theme-material .pdf-viewer-toolbar button, +body.theme-gruvbox-dark .pdf-viewer-toolbar button, +body.theme-ayu-dark .pdf-viewer-toolbar button, +body.theme-ayu-mirage .pdf-viewer-toolbar button, +body.theme-oceanic-next .pdf-viewer-toolbar button, +body.theme-cobalt2 .pdf-viewer-toolbar button, +body.theme-concrete-dark .pdf-viewer-toolbar button, +body.theme-concrete-warm .pdf-viewer-toolbar button { + background: #3d3d3d !important; + border: 1px solid #4d4d4d !important; + color: #e0e0e0 !important; +} + +body.theme-dark .pdf-viewer-toolbar button:hover, +body.theme-onedark .pdf-viewer-toolbar button:hover, +body.theme-one-dark .pdf-viewer-toolbar button:hover, +body.theme-dracula .pdf-viewer-toolbar button:hover, +body.theme-nord .pdf-viewer-toolbar button:hover, +body.theme-tokyo-night .pdf-viewer-toolbar button:hover, +body.theme-tokyonight .pdf-viewer-toolbar button:hover, +body.theme-palenight .pdf-viewer-toolbar button:hover, +body.theme-monokai .pdf-viewer-toolbar button:hover, +body.theme-material .pdf-viewer-toolbar button:hover, +body.theme-gruvbox-dark .pdf-viewer-toolbar button:hover, +body.theme-ayu-dark .pdf-viewer-toolbar button:hover, +body.theme-ayu-mirage .pdf-viewer-toolbar button:hover, +body.theme-oceanic-next .pdf-viewer-toolbar button:hover, +body.theme-cobalt2 .pdf-viewer-toolbar button:hover, +body.theme-concrete-dark .pdf-viewer-toolbar button:hover, +body.theme-concrete-warm .pdf-viewer-toolbar button:hover { + background: #4d4d4d !important; + border-color: #5d5d5d !important; +} + +body.theme-dark .pdf-viewer, +body.theme-onedark .pdf-viewer, +body.theme-one-dark .pdf-viewer, +body.theme-dracula .pdf-viewer, +body.theme-nord .pdf-viewer, +body.theme-tokyo-night .pdf-viewer, +body.theme-tokyonight .pdf-viewer, +body.theme-palenight .pdf-viewer, +body.theme-monokai .pdf-viewer, +body.theme-material .pdf-viewer, +body.theme-gruvbox-dark .pdf-viewer, +body.theme-ayu-dark .pdf-viewer, +body.theme-ayu-mirage .pdf-viewer, +body.theme-oceanic-next .pdf-viewer, +body.theme-cobalt2 .pdf-viewer, +body.theme-concrete-dark .pdf-viewer, +body.theme-concrete-warm .pdf-viewer { + background: #1a1a1a !important; +} + +body.theme-dark .pdf-viewer-container, +body.theme-onedark .pdf-viewer-container, +body.theme-one-dark .pdf-viewer-container, +body.theme-dracula .pdf-viewer-container, +body.theme-nord .pdf-viewer-container, +body.theme-tokyo-night .pdf-viewer-container, +body.theme-tokyonight .pdf-viewer-container, +body.theme-palenight .pdf-viewer-container, +body.theme-monokai .pdf-viewer-container, +body.theme-material .pdf-viewer-container, +body.theme-gruvbox-dark .pdf-viewer-container, +body.theme-ayu-dark .pdf-viewer-container, +body.theme-ayu-mirage .pdf-viewer-container, +body.theme-oceanic-next .pdf-viewer-container, +body.theme-cobalt2 .pdf-viewer-container, +body.theme-concrete-dark .pdf-viewer-container, +body.theme-concrete-warm .pdf-viewer-container { + background: #1a1a1a !important; +} + +body.theme-dark #pdf-page-input, +body.theme-onedark #pdf-page-input, +body.theme-one-dark #pdf-page-input, +body.theme-dracula #pdf-page-input, +body.theme-nord #pdf-page-input, +body.theme-tokyo-night #pdf-page-input, +body.theme-tokyonight #pdf-page-input, +body.theme-palenight #pdf-page-input, +body.theme-monokai #pdf-page-input, +body.theme-material #pdf-page-input, +body.theme-gruvbox-dark #pdf-page-input, +body.theme-ayu-dark #pdf-page-input, +body.theme-ayu-mirage #pdf-page-input, +body.theme-oceanic-next #pdf-page-input, +body.theme-cobalt2 #pdf-page-input, +body.theme-concrete-dark #pdf-page-input, +body.theme-concrete-warm #pdf-page-input { + background: #3d3d3d !important; + border: 1px solid #4d4d4d !important; + color: #e0e0e0 !important; +} + +body.theme-dark .pdf-toolbar-separator, +body.theme-onedark .pdf-toolbar-separator, +body.theme-one-dark .pdf-toolbar-separator, +body.theme-dracula .pdf-toolbar-separator, +body.theme-nord .pdf-toolbar-separator, +body.theme-tokyo-night .pdf-toolbar-separator, +body.theme-tokyonight .pdf-toolbar-separator, +body.theme-palenight .pdf-toolbar-separator, +body.theme-monokai .pdf-toolbar-separator, +body.theme-material .pdf-toolbar-separator, +body.theme-gruvbox-dark .pdf-toolbar-separator, +body.theme-ayu-dark .pdf-toolbar-separator, +body.theme-ayu-mirage .pdf-toolbar-separator, +body.theme-oceanic-next .pdf-toolbar-separator, +body.theme-cobalt2 .pdf-toolbar-separator, +body.theme-concrete-dark .pdf-toolbar-separator, +body.theme-concrete-warm .pdf-toolbar-separator { + background: #3d3d3d !important; +} + +.pdf-viewer { + flex: 1; + overflow: auto; + display: flex; + justify-content: center; + align-items: flex-start; + padding: 20px; + background: #525659; +} + +#pdf-canvas { + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); + background: white; +} + +/* Light theme PDF viewer */ +body.theme-light .pdf-viewer-container, +body.theme-atomonelight .pdf-viewer-container, +body.theme-github .pdf-viewer-container, +body.theme-solarized .pdf-viewer-container, +body.theme-gruvbox-light .pdf-viewer-container, +body.theme-ayu-light .pdf-viewer-container, +body.theme-sepia .pdf-viewer-container, +body.theme-paper .pdf-viewer-container, +body.theme-rosepine-dawn .pdf-viewer-container { + background: #e8e8e8; +} + +body.theme-light .pdf-viewer-toolbar, +body.theme-atomonelight .pdf-viewer-toolbar, +body.theme-github .pdf-viewer-toolbar, +body.theme-solarized .pdf-viewer-toolbar, +body.theme-gruvbox-light .pdf-viewer-toolbar, +body.theme-ayu-light .pdf-viewer-toolbar, +body.theme-sepia .pdf-viewer-toolbar, +body.theme-paper .pdf-viewer-toolbar, +body.theme-rosepine-dawn .pdf-viewer-toolbar { + background: #f5f5f5; + border-bottom-color: #ddd; +} + +body.theme-light .pdf-viewer-toolbar button, +body.theme-atomonelight .pdf-viewer-toolbar button, +body.theme-github .pdf-viewer-toolbar button { + background: #fff; + border-color: #ddd; + color: #333; +} + +body.theme-light .pdf-viewer-toolbar button:hover, +body.theme-atomonelight .pdf-viewer-toolbar button:hover, +body.theme-github .pdf-viewer-toolbar button:hover { + background: #e8e8e8; +} + +body.theme-light #pdf-page-info, +body.theme-atomonelight #pdf-page-info, +body.theme-github #pdf-page-info, +body.theme-light #pdf-zoom-level, +body.theme-atomonelight #pdf-zoom-level, +body.theme-github #pdf-zoom-level { + color: #333; +} + +body.theme-light #pdf-page-input, +body.theme-atomonelight #pdf-page-input, +body.theme-github #pdf-page-input { + background: #fff; + border-color: #ddd; + color: #333; +} + +body.theme-light .pdf-filename, +body.theme-atomonelight .pdf-filename, +body.theme-github .pdf-filename { + color: #666; +} + +body.theme-light .pdf-viewer, +body.theme-atomonelight .pdf-viewer, +body.theme-github .pdf-viewer, +body.theme-solarized .pdf-viewer, +body.theme-gruvbox-light .pdf-viewer, +body.theme-ayu-light .pdf-viewer, +body.theme-sepia .pdf-viewer, +body.theme-paper .pdf-viewer, +body.theme-rosepine-dawn .pdf-viewer { + background: #d0d0d0; +} + +/* ======================================== + Print Styles for styles-modern.css + Ensures proper print output + ======================================== */ + +@media print { + /* Reset container for print */ + .container { + padding: 0; + gap: 0; + display: block !important; + } + + /* Hide all UI elements */ + .tab-bar, + .toolbar, + .status-bar, + .pane-resizer, + .preview-header, + .find-dialog, + .export-dialog, + .batch-dialog, + .editor-pane, + [id^="editor-pane-"], + .new-tab-button, + .tab-close { + display: none !important; + } + + /* Show preview content */ + .preview-pane, + [id^="preview-pane-"], + .preview-content, + [id^="preview-"] { + display: block !important; + width: 100% !important; + padding: 20px !important; + position: static !important; + overflow: visible !important; + } + + /* Reset main content glass */ + .main-content-glass { + backdrop-filter: none !important; + border: none !important; + box-shadow: none !important; + } + + /* Reset editor container */ + .editor-container { + display: block !important; + } + + /* Reset tab content */ + .tab-content { + display: block !important; + } + + /* Reset pane */ + .pane { + display: block !important; + width: 100% !important; + } + + /* NO STYLES MODE - Black text on white (ink-saving) */ + body.printing-no-styles .container, + body.printing-no-styles .main-content-glass, + body.printing-no-styles .tab-content, + body.printing-no-styles .pane, + body.printing-no-styles .preview-pane, + body.printing-no-styles [id^="preview-pane-"], + body.printing-no-styles .preview-content, + body.printing-no-styles [id^="preview-"] { + background: white !important; + color: #000 !important; + } + + body.printing-no-styles .preview-content h1, + body.printing-no-styles .preview-content h2, + body.printing-no-styles .preview-content h3, + body.printing-no-styles .preview-content h4, + body.printing-no-styles .preview-content h5, + body.printing-no-styles .preview-content h6, + body.printing-no-styles .preview-content p, + body.printing-no-styles .preview-content li, + body.printing-no-styles .preview-content td, + body.printing-no-styles .preview-content th { + color: #000 !important; + } + + body.printing-no-styles .preview-content code, + body.printing-no-styles .preview-content pre { + background: #f5f5f5 !important; + color: #000 !important; + border-color: #ddd !important; + } + + body.printing-no-styles .preview-content a { + color: #000 !important; + } + + body.printing-no-styles .preview-content blockquote { + background: #f9f9f9 !important; + color: #333 !important; + border-left-color: #ccc !important; + } + + /* WITH STYLES MODE - Preserve theme colors */ + /* The theme colors are inherited from the body class */ +} + +/* ======================================== + Dark Theme Overrides for styles-modern.css + These ensure dark themes work properly + ======================================== */ + +/* Common dark theme styles for preview content */ +body.theme-dark .preview-content, +body.theme-dark [id^="preview-"], +body.theme-one-dark .preview-content, +body.theme-one-dark [id^="preview-"], +body.theme-dracula .preview-content, +body.theme-dracula [id^="preview-"], +body.theme-nord .preview-content, +body.theme-nord [id^="preview-"], +body.theme-tokyo-night .preview-content, +body.theme-tokyo-night [id^="preview-"], +body.theme-palenight .preview-content, +body.theme-palenight [id^="preview-"], +body.theme-ayu-dark .preview-content, +body.theme-ayu-dark [id^="preview-"], +body.theme-ayu-mirage .preview-content, +body.theme-ayu-mirage [id^="preview-"], +body.theme-oceanic-next .preview-content, +body.theme-oceanic-next [id^="preview-"], +body.theme-gruvbox-dark .preview-content, +body.theme-gruvbox-dark [id^="preview-"], +body.theme-cobalt2 .preview-content, +body.theme-cobalt2 [id^="preview-"] { + color: #e6e6e6; + background: transparent; +} + +/* Dark theme headings */ +body.theme-dark .preview-content h1, +body.theme-dark .preview-content h2, +body.theme-dark .preview-content h3, +body.theme-dark .preview-content h4, +body.theme-dark .preview-content h5, +body.theme-dark .preview-content h6, +body.theme-one-dark .preview-content h1, +body.theme-one-dark .preview-content h2, +body.theme-one-dark .preview-content h3, +body.theme-one-dark .preview-content h4, +body.theme-one-dark .preview-content h5, +body.theme-one-dark .preview-content h6, +body.theme-dracula .preview-content h1, +body.theme-dracula .preview-content h2, +body.theme-dracula .preview-content h3, +body.theme-dracula .preview-content h4, +body.theme-dracula .preview-content h5, +body.theme-dracula .preview-content h6, +body.theme-nord .preview-content h1, +body.theme-nord .preview-content h2, +body.theme-nord .preview-content h3, +body.theme-nord .preview-content h4, +body.theme-nord .preview-content h5, +body.theme-nord .preview-content h6, +body.theme-tokyo-night .preview-content h1, +body.theme-tokyo-night .preview-content h2, +body.theme-tokyo-night .preview-content h3, +body.theme-tokyo-night .preview-content h4, +body.theme-tokyo-night .preview-content h5, +body.theme-tokyo-night .preview-content h6, +body.theme-palenight .preview-content h1, +body.theme-palenight .preview-content h2, +body.theme-palenight .preview-content h3, +body.theme-palenight .preview-content h4, +body.theme-palenight .preview-content h5, +body.theme-palenight .preview-content h6, +body.theme-ayu-dark .preview-content h1, +body.theme-ayu-dark .preview-content h2, +body.theme-ayu-dark .preview-content h3, +body.theme-ayu-dark .preview-content h4, +body.theme-ayu-dark .preview-content h5, +body.theme-ayu-dark .preview-content h6, +body.theme-ayu-mirage .preview-content h1, +body.theme-ayu-mirage .preview-content h2, +body.theme-ayu-mirage .preview-content h3, +body.theme-ayu-mirage .preview-content h4, +body.theme-ayu-mirage .preview-content h5, +body.theme-ayu-mirage .preview-content h6, +body.theme-oceanic-next .preview-content h1, +body.theme-oceanic-next .preview-content h2, +body.theme-oceanic-next .preview-content h3, +body.theme-oceanic-next .preview-content h4, +body.theme-oceanic-next .preview-content h5, +body.theme-oceanic-next .preview-content h6, +body.theme-gruvbox-dark .preview-content h1, +body.theme-gruvbox-dark .preview-content h2, +body.theme-gruvbox-dark .preview-content h3, +body.theme-gruvbox-dark .preview-content h4, +body.theme-gruvbox-dark .preview-content h5, +body.theme-gruvbox-dark .preview-content h6, +body.theme-cobalt2 .preview-content h1, +body.theme-cobalt2 .preview-content h2, +body.theme-cobalt2 .preview-content h3, +body.theme-cobalt2 .preview-content h4, +body.theme-cobalt2 .preview-content h5, +body.theme-cobalt2 .preview-content h6 { + color: #f0f0f0; + border-bottom-color: #444; +} + +/* Dark theme paragraphs and text */ +body.theme-dark .preview-content p, +body.theme-dark .preview-content li, +body.theme-dark .preview-content td, +body.theme-one-dark .preview-content p, +body.theme-one-dark .preview-content li, +body.theme-one-dark .preview-content td, +body.theme-dracula .preview-content p, +body.theme-dracula .preview-content li, +body.theme-dracula .preview-content td, +body.theme-nord .preview-content p, +body.theme-nord .preview-content li, +body.theme-nord .preview-content td, +body.theme-tokyo-night .preview-content p, +body.theme-tokyo-night .preview-content li, +body.theme-tokyo-night .preview-content td, +body.theme-palenight .preview-content p, +body.theme-palenight .preview-content li, +body.theme-palenight .preview-content td, +body.theme-ayu-dark .preview-content p, +body.theme-ayu-dark .preview-content li, +body.theme-ayu-dark .preview-content td, +body.theme-ayu-mirage .preview-content p, +body.theme-ayu-mirage .preview-content li, +body.theme-ayu-mirage .preview-content td, +body.theme-oceanic-next .preview-content p, +body.theme-oceanic-next .preview-content li, +body.theme-oceanic-next .preview-content td, +body.theme-gruvbox-dark .preview-content p, +body.theme-gruvbox-dark .preview-content li, +body.theme-gruvbox-dark .preview-content td, +body.theme-cobalt2 .preview-content p, +body.theme-cobalt2 .preview-content li, +body.theme-cobalt2 .preview-content td { + color: #d4d4d4; +} + +/* Dark theme code blocks */ +body.theme-dark .preview-content pre, +body.theme-one-dark .preview-content pre, +body.theme-dracula .preview-content pre, +body.theme-nord .preview-content pre, +body.theme-tokyo-night .preview-content pre, +body.theme-palenight .preview-content pre, +body.theme-ayu-dark .preview-content pre, +body.theme-ayu-mirage .preview-content pre, +body.theme-oceanic-next .preview-content pre, +body.theme-gruvbox-dark .preview-content pre, +body.theme-cobalt2 .preview-content pre { + background: #1e1e1e; + border-color: #333; + color: #d4d4d4; +} + +/* Dark theme inline code */ +body.theme-dark .preview-content code, +body.theme-one-dark .preview-content code, +body.theme-dracula .preview-content code, +body.theme-nord .preview-content code, +body.theme-tokyo-night .preview-content code, +body.theme-palenight .preview-content code, +body.theme-ayu-dark .preview-content code, +body.theme-ayu-mirage .preview-content code, +body.theme-oceanic-next .preview-content code, +body.theme-gruvbox-dark .preview-content code, +body.theme-cobalt2 .preview-content code { + background: rgba(255, 255, 255, 0.1); + border-color: #444; + color: #e06c75; +} + +/* Dark theme blockquotes */ +body.theme-dark .preview-content blockquote, +body.theme-one-dark .preview-content blockquote, +body.theme-dracula .preview-content blockquote, +body.theme-nord .preview-content blockquote, +body.theme-tokyo-night .preview-content blockquote, +body.theme-palenight .preview-content blockquote, +body.theme-ayu-dark .preview-content blockquote, +body.theme-ayu-mirage .preview-content blockquote, +body.theme-oceanic-next .preview-content blockquote, +body.theme-gruvbox-dark .preview-content blockquote, +body.theme-cobalt2 .preview-content blockquote { + background: rgba(255, 255, 255, 0.05); + border-left-color: #555; + color: #aaa; +} + +/* Dark theme links */ +body.theme-dark .preview-content a, +body.theme-one-dark .preview-content a, +body.theme-dracula .preview-content a, +body.theme-nord .preview-content a, +body.theme-tokyo-night .preview-content a, +body.theme-palenight .preview-content a, +body.theme-ayu-dark .preview-content a, +body.theme-ayu-mirage .preview-content a, +body.theme-oceanic-next .preview-content a, +body.theme-gruvbox-dark .preview-content a, +body.theme-cobalt2 .preview-content a { + color: #61afef; +} + +/* Dark theme tables */ +body.theme-dark .preview-content table, +body.theme-one-dark .preview-content table, +body.theme-dracula .preview-content table, +body.theme-nord .preview-content table, +body.theme-tokyo-night .preview-content table, +body.theme-palenight .preview-content table, +body.theme-ayu-dark .preview-content table, +body.theme-ayu-mirage .preview-content table, +body.theme-oceanic-next .preview-content table, +body.theme-gruvbox-dark .preview-content table, +body.theme-cobalt2 .preview-content table { + border-color: #444; +} + +body.theme-dark .preview-content table th, +body.theme-one-dark .preview-content table th, +body.theme-dracula .preview-content table th, +body.theme-nord .preview-content table th, +body.theme-tokyo-night .preview-content table th, +body.theme-palenight .preview-content table th, +body.theme-ayu-dark .preview-content table th, +body.theme-ayu-mirage .preview-content table th, +body.theme-oceanic-next .preview-content table th, +body.theme-gruvbox-dark .preview-content table th, +body.theme-cobalt2 .preview-content table th { + background: #2d2d2d; + color: #e6e6e6; + border-color: #444; +} + +body.theme-dark .preview-content table td, +body.theme-one-dark .preview-content table td, +body.theme-dracula .preview-content table td, +body.theme-nord .preview-content table td, +body.theme-tokyo-night .preview-content table td, +body.theme-palenight .preview-content table td, +body.theme-ayu-dark .preview-content table td, +body.theme-ayu-mirage .preview-content table td, +body.theme-oceanic-next .preview-content table td, +body.theme-gruvbox-dark .preview-content table td, +body.theme-cobalt2 .preview-content table td { + border-color: #444; +} + +body.theme-dark .preview-content table tr:nth-child(even), +body.theme-one-dark .preview-content table tr:nth-child(even), +body.theme-dracula .preview-content table tr:nth-child(even), +body.theme-nord .preview-content table tr:nth-child(even), +body.theme-tokyo-night .preview-content table tr:nth-child(even), +body.theme-palenight .preview-content table tr:nth-child(even), +body.theme-ayu-dark .preview-content table tr:nth-child(even), +body.theme-ayu-mirage .preview-content table tr:nth-child(even), +body.theme-oceanic-next .preview-content table tr:nth-child(even), +body.theme-gruvbox-dark .preview-content table tr:nth-child(even), +body.theme-cobalt2 .preview-content table tr:nth-child(even) { + background: rgba(255, 255, 255, 0.03); +} + +/* Dark theme horizontal rules */ +body.theme-dark .preview-content hr, +body.theme-one-dark .preview-content hr, +body.theme-dracula .preview-content hr, +body.theme-nord .preview-content hr, +body.theme-tokyo-night .preview-content hr, +body.theme-palenight .preview-content hr, +body.theme-ayu-dark .preview-content hr, +body.theme-ayu-mirage .preview-content hr, +body.theme-oceanic-next .preview-content hr, +body.theme-gruvbox-dark .preview-content hr, +body.theme-cobalt2 .preview-content hr { + border-color: #444; + background: #444; +} + +/* ======================================== + Modal/Dialog Theming + Dialogs inherit app theme colors + ======================================== */ + +/* Dark theme modals */ +body.theme-dark .export-dialog-content, +body.theme-dark .batch-dialog-content, +body.theme-dark .pdf-editor-content, +body.theme-one-dark .export-dialog-content, +body.theme-one-dark .batch-dialog-content, +body.theme-one-dark .pdf-editor-content, +body.theme-onedark .export-dialog-content, +body.theme-onedark .batch-dialog-content, +body.theme-onedark .pdf-editor-content, +body.theme-dracula .export-dialog-content, +body.theme-dracula .batch-dialog-content, +body.theme-dracula .pdf-editor-content, +body.theme-nord .export-dialog-content, +body.theme-nord .batch-dialog-content, +body.theme-nord .pdf-editor-content, +body.theme-tokyo-night .export-dialog-content, +body.theme-tokyo-night .batch-dialog-content, +body.theme-tokyo-night .pdf-editor-content, +body.theme-tokyonight .export-dialog-content, +body.theme-tokyonight .batch-dialog-content, +body.theme-tokyonight .pdf-editor-content, +body.theme-palenight .export-dialog-content, +body.theme-palenight .batch-dialog-content, +body.theme-palenight .pdf-editor-content, +body.theme-ayu-dark .export-dialog-content, +body.theme-ayu-dark .batch-dialog-content, +body.theme-ayu-dark .pdf-editor-content, +body.theme-ayu-mirage .export-dialog-content, +body.theme-ayu-mirage .batch-dialog-content, +body.theme-ayu-mirage .pdf-editor-content, +body.theme-oceanic-next .export-dialog-content, +body.theme-oceanic-next .batch-dialog-content, +body.theme-oceanic-next .pdf-editor-content, +body.theme-gruvbox-dark .export-dialog-content, +body.theme-gruvbox-dark .batch-dialog-content, +body.theme-gruvbox-dark .pdf-editor-content, +body.theme-cobalt2 .export-dialog-content, +body.theme-cobalt2 .batch-dialog-content, +body.theme-cobalt2 .pdf-editor-content, +body.theme-monokai .export-dialog-content, +body.theme-monokai .batch-dialog-content, +body.theme-monokai .pdf-editor-content, +body.theme-material .export-dialog-content, +body.theme-material .batch-dialog-content, +body.theme-material .pdf-editor-content, +body.theme-concrete-dark .export-dialog-content, +body.theme-concrete-dark .batch-dialog-content, +body.theme-concrete-dark .pdf-editor-content, +body.theme-concrete-warm .export-dialog-content, +body.theme-concrete-warm .batch-dialog-content, +body.theme-concrete-warm .pdf-editor-content { + background: #2d2d2d !important; + border-color: #444 !important; + color: #e6e6e6 !important; +} + +/* Dark theme dialog headers */ +body.theme-dark .export-dialog-header, +body.theme-dark .batch-dialog-header, +body.theme-one-dark .export-dialog-header, +body.theme-one-dark .batch-dialog-header, +body.theme-onedark .export-dialog-header, +body.theme-onedark .batch-dialog-header, +body.theme-dracula .export-dialog-header, +body.theme-dracula .batch-dialog-header, +body.theme-nord .export-dialog-header, +body.theme-nord .batch-dialog-header, +body.theme-tokyo-night .export-dialog-header, +body.theme-tokyo-night .batch-dialog-header, +body.theme-tokyonight .export-dialog-header, +body.theme-tokyonight .batch-dialog-header, +body.theme-palenight .export-dialog-header, +body.theme-palenight .batch-dialog-header, +body.theme-ayu-dark .export-dialog-header, +body.theme-ayu-dark .batch-dialog-header, +body.theme-ayu-mirage .export-dialog-header, +body.theme-ayu-mirage .batch-dialog-header, +body.theme-oceanic-next .export-dialog-header, +body.theme-oceanic-next .batch-dialog-header, +body.theme-gruvbox-dark .export-dialog-header, +body.theme-gruvbox-dark .batch-dialog-header, +body.theme-cobalt2 .export-dialog-header, +body.theme-cobalt2 .batch-dialog-header, +body.theme-monokai .export-dialog-header, +body.theme-monokai .batch-dialog-header, +body.theme-material .export-dialog-header, +body.theme-material .batch-dialog-header, +body.theme-concrete-dark .export-dialog-header, +body.theme-concrete-dark .batch-dialog-header, +body.theme-concrete-warm .export-dialog-header, +body.theme-concrete-warm .batch-dialog-header { + background: #1a1a1a !important; + border-bottom-color: #333 !important; +} + +/* Dark theme dialog header text */ +body.theme-dark .export-dialog-header h3, +body.theme-dark .batch-dialog-header h3, +body.theme-onedark .export-dialog-header h3, +body.theme-onedark .batch-dialog-header h3, +body.theme-dracula .export-dialog-header h3, +body.theme-dracula .batch-dialog-header h3, +body.theme-nord .export-dialog-header h3, +body.theme-nord .batch-dialog-header h3, +body.theme-tokyonight .export-dialog-header h3, +body.theme-tokyonight .batch-dialog-header h3, +body.theme-monokai .export-dialog-header h3, +body.theme-monokai .batch-dialog-header h3, +body.theme-material .export-dialog-header h3, +body.theme-material .batch-dialog-header h3 { + color: #fff !important; +} + +/* Dark theme dialog body */ +body.theme-dark .export-dialog-body, +body.theme-dark .batch-dialog-body, +body.theme-one-dark .export-dialog-body, +body.theme-one-dark .batch-dialog-body, +body.theme-dracula .export-dialog-body, +body.theme-dracula .batch-dialog-body, +body.theme-nord .export-dialog-body, +body.theme-nord .batch-dialog-body, +body.theme-tokyo-night .export-dialog-body, +body.theme-tokyo-night .batch-dialog-body, +body.theme-palenight .export-dialog-body, +body.theme-palenight .batch-dialog-body, +body.theme-ayu-dark .export-dialog-body, +body.theme-ayu-dark .batch-dialog-body, +body.theme-ayu-mirage .export-dialog-body, +body.theme-ayu-mirage .batch-dialog-body, +body.theme-oceanic-next .export-dialog-body, +body.theme-oceanic-next .batch-dialog-body, +body.theme-gruvbox-dark .export-dialog-body, +body.theme-gruvbox-dark .batch-dialog-body, +body.theme-cobalt2 .export-dialog-body, +body.theme-cobalt2 .batch-dialog-body { + background: transparent; +} + +/* Dark theme dialog labels */ +body.theme-dark .export-section label, +body.theme-dark .batch-section label, +body.theme-one-dark .export-section label, +body.theme-one-dark .batch-section label, +body.theme-dracula .export-section label, +body.theme-dracula .batch-section label, +body.theme-nord .export-section label, +body.theme-nord .batch-section label, +body.theme-tokyo-night .export-section label, +body.theme-tokyo-night .batch-section label, +body.theme-palenight .export-section label, +body.theme-palenight .batch-section label, +body.theme-ayu-dark .export-section label, +body.theme-ayu-dark .batch-section label, +body.theme-ayu-mirage .export-section label, +body.theme-ayu-mirage .batch-section label, +body.theme-oceanic-next .export-section label, +body.theme-oceanic-next .batch-section label, +body.theme-gruvbox-dark .export-section label, +body.theme-gruvbox-dark .batch-section label, +body.theme-cobalt2 .export-section label, +body.theme-cobalt2 .batch-section label { + color: #ccc; +} + +/* Dark theme dialog inputs and selects */ +body.theme-dark .export-section select, +body.theme-dark .export-section input, +body.theme-dark .batch-section select, +body.theme-dark .batch-section input, +body.theme-one-dark .export-section select, +body.theme-one-dark .export-section input, +body.theme-one-dark .batch-section select, +body.theme-one-dark .batch-section input, +body.theme-dracula .export-section select, +body.theme-dracula .export-section input, +body.theme-dracula .batch-section select, +body.theme-dracula .batch-section input, +body.theme-nord .export-section select, +body.theme-nord .export-section input, +body.theme-nord .batch-section select, +body.theme-nord .batch-section input, +body.theme-tokyo-night .export-section select, +body.theme-tokyo-night .export-section input, +body.theme-tokyo-night .batch-section select, +body.theme-tokyo-night .batch-section input, +body.theme-palenight .export-section select, +body.theme-palenight .export-section input, +body.theme-palenight .batch-section select, +body.theme-palenight .batch-section input, +body.theme-ayu-dark .export-section select, +body.theme-ayu-dark .export-section input, +body.theme-ayu-dark .batch-section select, +body.theme-ayu-dark .batch-section input, +body.theme-ayu-mirage .export-section select, +body.theme-ayu-mirage .export-section input, +body.theme-ayu-mirage .batch-section select, +body.theme-ayu-mirage .batch-section input, +body.theme-oceanic-next .export-section select, +body.theme-oceanic-next .export-section input, +body.theme-oceanic-next .batch-section select, +body.theme-oceanic-next .batch-section input, +body.theme-gruvbox-dark .export-section select, +body.theme-gruvbox-dark .export-section input, +body.theme-gruvbox-dark .batch-section select, +body.theme-gruvbox-dark .batch-section input, +body.theme-cobalt2 .export-section select, +body.theme-cobalt2 .export-section input, +body.theme-cobalt2 .batch-section select, +body.theme-cobalt2 .batch-section input { + background: #2d2d2d; + border-color: #444; + color: #e6e6e6; +} + +/* Dark theme dialog footer */ +body.theme-dark .export-dialog-footer, +body.theme-dark .batch-dialog-footer, +body.theme-one-dark .export-dialog-footer, +body.theme-one-dark .batch-dialog-footer, +body.theme-dracula .export-dialog-footer, +body.theme-dracula .batch-dialog-footer, +body.theme-nord .export-dialog-footer, +body.theme-nord .batch-dialog-footer, +body.theme-tokyo-night .export-dialog-footer, +body.theme-tokyo-night .batch-dialog-footer, +body.theme-palenight .export-dialog-footer, +body.theme-palenight .batch-dialog-footer, +body.theme-ayu-dark .export-dialog-footer, +body.theme-ayu-dark .batch-dialog-footer, +body.theme-ayu-mirage .export-dialog-footer, +body.theme-ayu-mirage .batch-dialog-footer, +body.theme-oceanic-next .export-dialog-footer, +body.theme-oceanic-next .batch-dialog-footer, +body.theme-gruvbox-dark .export-dialog-footer, +body.theme-gruvbox-dark .batch-dialog-footer, +body.theme-cobalt2 .export-dialog-footer, +body.theme-cobalt2 .batch-dialog-footer { + background: #1a1a1a; + border-top-color: #333; +} + +/* Sepia theme modals */ +body.theme-sepia .export-dialog-content, +body.theme-sepia .batch-dialog-content, +body.theme-sepia .pdf-editor-content { + background: rgba(244, 236, 216, 0.98); + border-color: #d4c4a8; + color: #5b4636; +} + +body.theme-sepia .export-dialog-header, +body.theme-sepia .batch-dialog-header { + background: #e8dcc8; + border-bottom-color: #d4c4a8; +} + +body.theme-sepia .export-dialog-header h3, +body.theme-sepia .batch-dialog-header h3 { + color: #3d2914; +} + +body.theme-sepia .export-section label, +body.theme-sepia .batch-section label { + color: #5b4636; +} + +body.theme-sepia .export-section select, +body.theme-sepia .export-section input, +body.theme-sepia .batch-section select, +body.theme-sepia .batch-section input { + background: #f4ecd8; + border-color: #d4c4a8; + color: #5b4636; +} + +body.theme-sepia .export-dialog-footer, +body.theme-sepia .batch-dialog-footer { + background: #e8dcc8; + border-top-color: #d4c4a8; +} + +/* Rose Pine Dawn theme modals */ +body.theme-rosepine-dawn .export-dialog-content, +body.theme-rosepine-dawn .batch-dialog-content, +body.theme-rosepine-dawn .pdf-editor-content { + background: rgba(250, 244, 237, 0.98); + border-color: #dfdad9; + color: #575279; +} + +body.theme-rosepine-dawn .export-dialog-header, +body.theme-rosepine-dawn .batch-dialog-header { + background: #f2e9e1; + border-bottom-color: #dfdad9; +} + +body.theme-rosepine-dawn .export-dialog-header h3, +body.theme-rosepine-dawn .batch-dialog-header h3 { + color: #286983; +} + +body.theme-rosepine-dawn .export-section label, +body.theme-rosepine-dawn .batch-section label { + color: #575279; +} + +body.theme-rosepine-dawn .export-section select, +body.theme-rosepine-dawn .export-section input, +body.theme-rosepine-dawn .batch-section select, +body.theme-rosepine-dawn .batch-section input { + background: #faf4ed; + border-color: #dfdad9; + color: #575279; +} + +body.theme-rosepine-dawn .export-dialog-footer, +body.theme-rosepine-dawn .batch-dialog-footer { + background: #f2e9e1; + border-top-color: #dfdad9; +} diff --git a/src/styles.css b/src/styles.css index c377601..673fd4c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,4080 +1,4080 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - overflow: hidden; - height: 100vh; -} - -.hidden { - display: none !important; -} - -.container { - display: flex; - flex-direction: column; - height: 100vh; -} - -/* Tab Bar */ -.tab-bar { - display: flex; - align-items: center; - background: #f0f0f0; - border-bottom: 1px solid #ddd; - padding: 0 8px; - min-height: 36px; -} - -.tab { - display: flex; - align-items: center; - padding: 6px 12px; - background: #e8e8e8; - border: 1px solid #ccc; - border-bottom: none; - border-radius: 6px 6px 0 0; - margin-right: 2px; - cursor: pointer; - user-select: none; - max-width: 200px; - min-width: 120px; -} - -.tab.active { - background: #fff; - border-color: #999; - z-index: 1; -} - -.tab-title { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 13px; -} - -.tab-close { - background: none; - border: none; - font-size: 16px; - font-weight: bold; - cursor: pointer; - padding: 0; - margin-left: 8px; - width: 16px; - height: 16px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 3px; - color: #666; -} - -.tab-close:hover { - background: #ddd; - color: #000; -} - -.new-tab-button { - background: none; - border: 1px solid #ccc; - border-radius: 4px; - padding: 4px 8px; - cursor: pointer; - font-size: 14px; - margin-left: 8px; - color: #666; -} - -.new-tab-button:hover { - background: #e8e8e8; -} - -/* Tab Content */ -.tab-content { - display: flex; - flex: 1; - overflow: hidden; -} - -.tab-content:not(.active) { - display: none; -} - -/* Toolbar */ -.toolbar { - display: flex; - align-items: center; - padding: 8px 12px; - background: #f5f5f5; - border-bottom: 1px solid #ddd; - gap: 4px; -} - -.toolbar button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: 1px solid transparent; - background: transparent; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s; -} - -.toolbar button:hover { - background: #e0e0e0; - border-color: #ccc; -} - -.toolbar button:active { - background: #d0d0d0; -} - -.toolbar-separator { - width: 1px; - height: 24px; - background: #ccc; - margin: 0 8px; -} - -/* Editor Container */ -.editor-container { - display: flex; - flex: 1; - overflow: hidden; -} - -.pane { - flex: 1; - overflow: auto; - min-width: 0; -} - -.pane:first-child { - border-right: none; -} - -/* Pane Resizer */ -.pane-resizer { - width: 6px; - background: #e0e0e0; - cursor: col-resize; - position: relative; - flex-shrink: 0; - transition: background 0.2s; -} - -.pane-resizer:hover { - background: #999; -} - -.pane-resizer:active { - background: #666; -} - -/* Preview Header */ -.preview-header { - display: flex; - justify-content: flex-end; - align-items: center; - padding: 4px 8px; - background: #f6f8fa; - border-bottom: 1px solid #dfe2e5; - min-height: 32px; -} - -.preview-popout-btn { - background: transparent; - border: none; - border-radius: 3px; - padding: 2px 6px; - cursor: pointer; - font-size: 11px; - color: #8c959f; - opacity: 0.5; - transition: all 0.2s; -} - -.preview-popout-btn:hover { - background: rgba(0, 0, 0, 0.05); - opacity: 1; - color: #24292f; -} - -.editor-textarea { - width: 100%; - height: 100%; - padding: 20px; - font-family: 'SF Mono', Monaco, 'Courier New', monospace; - font-size: 15px; - line-height: 1.6; - border: none; - outline: none; - resize: none; -} - -.pane:last-child { - padding: 0; - background: #fff; -} - -.preview-content { - max-width: none; - margin: 0; - padding: 20px 24px 24px 24px; - line-height: 1.6; - font-size: 15px; - font-size: 14px; -} - -/* Legacy support for old selectors */ -#editor-pane { - border-right: 1px solid #ddd; -} - -#editor { - width: 100%; - height: 100%; - padding: 20px; - font-family: 'SF Mono', Monaco, 'Courier New', monospace; - font-size: 15px; - line-height: 1.6; - border: none; - outline: none; - resize: none; -} - -#preview-pane { - padding: 20px; - background: #fff; -} - -/* GitHub Light Theme for Preview (Default) */ -#preview, [id^="preview-"], .preview-content { - max-width: 800px; - margin: 0 auto; - font-size: 15px; - padding: 20px; - background: #ffffff; - color: #24292f; - line-height: 1.6; -} - -/* Status Bar */ -.status-bar { - display: flex; - justify-content: space-between; - padding: 4px 12px; - background: #f5f5f5; - border-top: 1px solid #ddd; - font-size: 12px; - color: #666; -} - -/* Preview Styles */ -#preview h1, #preview h2, #preview h3, #preview h4, #preview h5, #preview h6, -.preview-content h1, .preview-content h2, .preview-content h3, .preview-content h4, .preview-content h5, .preview-content h6 { - margin-top: 24px; - margin-bottom: 16px; - font-weight: 600; - line-height: 1.25; -} - -#preview h1, .preview-content h1 { - font-size: 2em; - border-bottom: 1px solid #eaecef; - padding-bottom: 0.3em; - margin-top: 0; -} - -#preview h2, .preview-content h2 { - font-size: 1.5em; - border-bottom: 1px solid #eaecef; - padding-bottom: 0.3em; -} - -#preview p, .preview-content p { - margin-bottom: 16px; - line-height: 1.6; -} - -#preview code, .preview-content code, [id^="preview-"] code { - padding: 0.2em 0.4em; - margin: 0; - font-size: 85%; - background-color: #f6f8fa; - border: 1px solid #d0d7de; - border-radius: 6px; - font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; - color: #24292f; -} - -#preview pre, .preview-content pre, [id^="preview-"] pre { - padding: 16px; - overflow: auto; - font-size: 85%; - line-height: 1.45; - background-color: #f6f8fa; - border: 1px solid #d0d7de; - border-radius: 6px; - margin-bottom: 16px; -} - -#preview pre code, .preview-content pre code { - display: inline; - max-width: auto; - padding: 0; - margin: 0; - overflow: visible; - line-height: inherit; - word-wrap: normal; - background-color: transparent; - border: 0; -} - -#preview blockquote, .preview-content blockquote { - padding: 0 1em; - color: #6a737d; - border-left: 0.25em solid #dfe2e5; - margin-bottom: 16px; -} - -/* GitHub Dark Theme for Preview */ -body.theme-dark #preview, -body.theme-dark [id^="preview-"], -body.theme-dark .preview-content { - background: #0d1117; - color: #c9d1d9; -} - -body.theme-dark #preview h1, -body.theme-dark [id^="preview-"] h1, -body.theme-dark .preview-content h1 { - color: #c9d1d9; - border-bottom-color: #21262d; -} - -body.theme-dark #preview h2, -body.theme-dark [id^="preview-"] h2, -body.theme-dark .preview-content h2 { - color: #c9d1d9; - border-bottom-color: #21262d; -} - -body.theme-dark #preview h3, -body.theme-dark #preview h4, -body.theme-dark #preview h5, -body.theme-dark #preview h6, -body.theme-dark [id^="preview-"] h3, -body.theme-dark [id^="preview-"] h4, -body.theme-dark [id^="preview-"] h5, -body.theme-dark [id^="preview-"] h6, -body.theme-dark .preview-content h3, -body.theme-dark .preview-content h4, -body.theme-dark .preview-content h5, -body.theme-dark .preview-content h6 { - color: #c9d1d9; -} - -body.theme-dark #preview p, -body.theme-dark [id^="preview-"] p, -body.theme-dark .preview-content p { - color: #c9d1d9; -} - -body.theme-dark #preview code, -body.theme-dark [id^="preview-"] code, -body.theme-dark .preview-content code { - background-color: #161b22; - border-color: #30363d; - color: #c9d1d9; -} - -body.theme-dark #preview pre, -body.theme-dark [id^="preview-"] pre, -body.theme-dark .preview-content pre { - background-color: #161b22; - border-color: #30363d; -} - -body.theme-dark #preview blockquote, -body.theme-dark [id^="preview-"] blockquote, -body.theme-dark .preview-content blockquote { - color: #8b949e; - border-left-color: #3b434b; -} - -body.theme-dark #preview a, -body.theme-dark [id^="preview-"] a, -body.theme-dark .preview-content a { - color: #58a6ff; -} - -body.theme-dark #preview a:hover, -body.theme-dark [id^="preview-"] a:hover, -body.theme-dark .preview-content a:hover { - color: #79c0ff; -} - -body.theme-dark #preview table, -body.theme-dark [id^="preview-"] table, -body.theme-dark .preview-content table { - border-color: #30363d; -} - -body.theme-dark #preview table th, -body.theme-dark [id^="preview-"] table th, -body.theme-dark .preview-content table th { - background-color: #161b22; - color: #c9d1d9; - border-color: #30363d; -} - -body.theme-dark #preview table td, -body.theme-dark [id^="preview-"] table td, -body.theme-dark .preview-content table td { - border-color: #30363d; - color: #c9d1d9; -} - -body.theme-dark #preview hr, -body.theme-dark [id^="preview-"] hr, -body.theme-dark .preview-content hr { - background-color: #21262d; - border-color: #21262d; -} - -body.theme-dark #preview ul, -body.theme-dark #preview ol, -body.theme-dark [id^="preview-"] ul, -body.theme-dark [id^="preview-"] ol, -body.theme-dark .preview-content ul, -body.theme-dark .preview-content ol { - color: #c9d1d9; -} - -body.theme-dark #preview li, -body.theme-dark [id^="preview-"] li, -body.theme-dark .preview-content li { - color: #c9d1d9; -} - -#preview ul, #preview ol, .preview-content ul, .preview-content ol { - padding-left: 2em; - margin-bottom: 16px; -} - -#preview li, .preview-content li { - margin-bottom: 4px; -} - -#preview a, .preview-content a { - color: #0366d6; - text-decoration: none; -} - -#preview a:hover, .preview-content a:hover { - text-decoration: underline; -} - -#preview img, .preview-content img { - max-width: 100%; - height: auto; -} - -#preview table, .preview-content table { - border-collapse: collapse; - margin-bottom: 16px; - width: 100%; -} - -#preview table th, #preview table td, -.preview-content table th, .preview-content table td { - padding: 6px 13px; - border: 1px solid #dfe2e5; -} - -#preview table th, .preview-content table th { - font-weight: 600; - background-color: #f6f8fa; -} - -#preview table tr:nth-child(2n), .preview-content table tr:nth-child(2n) { - background-color: #ffffff; -} - -/* Theme: Dark */ -body.theme-dark { - background: #1e1e1e; - color: #d4d4d4; -} - -body.theme-dark .tab-bar { - background: #2d2d30; - border-bottom-color: #3e3e42; -} - -body.theme-dark .tab { - background: #3c3c3c; - border-color: #555; - color: #d4d4d4; -} - -body.theme-dark .tab.active { - background: #1e1e1e; - border-color: #666; -} - -body.theme-dark .tab-close { - color: #ccc; -} - -body.theme-dark .tab-close:hover { - background: #555; - color: #fff; -} - -body.theme-dark .new-tab-button { - border-color: #555; - color: #ccc; -} - -body.theme-dark .new-tab-button:hover { - background: #3c3c3c; -} - -body.theme-dark .toolbar { - background: #2d2d30; - border-bottom-color: #3e3e42; -} - -body.theme-dark .toolbar button { - color: #cccccc; -} - -body.theme-dark .toolbar button:hover { - background: #3e3e42; - border-color: #464647; -} - -body.theme-dark .toolbar-separator { - background: #3e3e42; -} - -body.theme-dark .pane:first-child { - border-right-color: #3e3e42; -} - -body.theme-dark .editor-textarea { - background: #1e1e1e; - color: #d4d4d4; -} - -body.theme-dark .pane:last-child { - background: #1e1e1e; -} - -/* Legacy support */ -body.theme-dark #editor-pane { - border-right-color: #3e3e42; -} - -body.theme-dark #editor { - background: #1e1e1e; - color: #d4d4d4; -} - -body.theme-dark #preview-pane { - background: #252526; -} - -body.theme-dark #preview, body.theme-dark .preview-content { - color: #d4d4d4; -} - -body.theme-dark #preview h1, body.theme-dark #preview h2, -body.theme-dark .preview-content h1, body.theme-dark .preview-content h2 { - border-bottom-color: #3e3e42; -} - -body.theme-dark #preview code, body.theme-dark .preview-content code { - background-color: rgba(255,255,255,0.1); -} - -body.theme-dark #preview pre, body.theme-dark .preview-content pre { - background-color: #2d2d30; -} - -body.theme-dark #preview blockquote, body.theme-dark .preview-content blockquote { - color: #808080; - border-left-color: #3e3e42; -} - -body.theme-dark #preview a, body.theme-dark .preview-content a { - color: #569cd6; -} - -body.theme-dark #preview table th, body.theme-dark #preview table td, -body.theme-dark .preview-content table th, body.theme-dark .preview-content table td { - border-color: #3e3e42; -} - - -body.theme-dark #preview table tr:nth-child(2n), body.theme-dark .preview-content table tr:nth-child(2n) { - background-color: #2d2d30; -} - -body.theme-dark .status-bar { - background: #2d2d30; - border-top-color: #3e3e42; - color: #969696; -} - -/* Theme: Solarized */ -body.theme-solarized { - background: #fdf6e3; - color: #657b83; -} - -body.theme-solarized .tab-bar { - background: #eee8d5; - border-bottom-color: #93a1a1; -} - -body.theme-solarized .tab { - background: #fdf6e3; - border-color: #93a1a1; - color: #657b83; -} - -body.theme-solarized .tab.active { - background: #fdf6e3; - border-color: #859900; -} - -body.theme-solarized .tab-close { - color: #657b83; -} - -body.theme-solarized .tab-close:hover { - background: #93a1a1; - color: #dc322f; -} - -body.theme-solarized .new-tab-button { - border-color: #93a1a1; - color: #657b83; -} - -body.theme-solarized .new-tab-button:hover { - background: #eee8d5; -} - -body.theme-solarized .toolbar { - background: #eee8d5; - border-bottom-color: #93a1a1; -} - -body.theme-solarized .toolbar button { - color: #586e75; -} - -body.theme-solarized .toolbar button:hover { - background: #fdf6e3; - border-color: #93a1a1; -} - -body.theme-solarized #editor { - background: #fdf6e3; - color: #657b83; -} - -body.theme-solarized #preview, body.theme-solarized .preview-content { - color: #586e75; -} - -body.theme-solarized #preview code, body.theme-solarized .preview-content code { - background-color: #eee8d5; -} - -body.theme-solarized #preview pre, body.theme-solarized .preview-content pre { - background-color: #eee8d5; -} - -body.theme-solarized #preview a, body.theme-solarized .preview-content a { - color: #268bd2; -} - -/* Theme: Monokai */ -body.theme-monokai { - background: #272822; - color: #f8f8f2; -} - -body.theme-monokai .tab-bar { - background: #3e3d32; - border-bottom-color: #75715e; -} - -body.theme-monokai .tab { - background: #49483e; - border-color: #75715e; - color: #f8f8f2; -} - -body.theme-monokai .tab.active { - background: #272822; - border-color: #a6e22e; -} - -body.theme-monokai .tab-close { - color: #f8f8f2; -} - -body.theme-monokai .tab-close:hover { - background: #75715e; - color: #f92672; -} - -body.theme-monokai .new-tab-button { - border-color: #75715e; - color: #f8f8f2; -} - -body.theme-monokai .new-tab-button:hover { - background: #49483e; -} - -body.theme-monokai .toolbar { - background: #3e3d32; - border-bottom-color: #75715e; -} - -body.theme-monokai .toolbar button { - color: #f8f8f2; -} - -body.theme-monokai .toolbar button:hover { - background: #49483e; - border-color: #75715e; -} - -body.theme-monokai #editor { - background: #272822; - color: #f8f8f2; -} - -body.theme-monokai .editor-textarea { - background: #272822; - color: #f8f8f2; -} - -body.theme-monokai #preview-pane { - background: #272822; -} - -body.theme-monokai #preview, body.theme-monokai .preview-content { - color: #f8f8f2; -} - -body.theme-monokai #preview h1, body.theme-monokai #preview h2, -body.theme-monokai .preview-content h1, body.theme-monokai .preview-content h2 { - border-bottom-color: #75715e; -} - -body.theme-monokai #preview code, body.theme-monokai .preview-content code { - background-color: #3e3d32; -} - -body.theme-monokai #preview pre, body.theme-monokai .preview-content pre { - background-color: #3e3d32; -} - -body.theme-monokai #preview blockquote, body.theme-monokai .preview-content blockquote { - color: #75715e; - border-left-color: #75715e; -} - -body.theme-monokai #preview a, body.theme-monokai .preview-content a { - color: #66d9ef; -} - -body.theme-monokai .status-bar { - background: #3e3d32; - border-top-color: #75715e; - color: #75715e; -} - -/* Theme: GitHub */ -body.theme-github { - background: #fff; - color: #24292e; -} - -body.theme-github .tab-bar { - background: #fafbfc; - border-bottom-color: #e1e4e8; -} - -body.theme-github .tab { - background: #f6f8fa; - border-color: #e1e4e8; - color: #24292e; -} - -body.theme-github .tab.active { - background: #fff; - border-color: #0366d6; -} - -body.theme-github .tab-close { - color: #586069; -} - -body.theme-github .tab-close:hover { - background: #e1e4e8; - color: #cb2431; -} - -body.theme-github .new-tab-button { - border-color: #e1e4e8; - color: #586069; -} - -body.theme-github .new-tab-button:hover { - background: #f6f8fa; -} - -body.theme-github .toolbar { - background: #fafbfc; - border-bottom-color: #e1e4e8; -} - -body.theme-github .toolbar button { - color: #586069; -} - -body.theme-github .toolbar button:hover { - background: #f6f8fa; - border-color: #e1e4e8; -} - -body.theme-github #editor { - background: #fff; - color: #24292e; -} - -body.theme-github #preview, body.theme-github .preview-content { - color: #24292e; -} - -body.theme-github #preview h1, body.theme-github #preview h2, -body.theme-github .preview-content h1, body.theme-github .preview-content h2 { - border-bottom-color: #eaecef; -} - -body.theme-github #preview code, body.theme-github .preview-content code { - background-color: rgba(27,31,35,0.05); -} - -body.theme-github #preview pre, body.theme-github .preview-content pre { - background-color: #f6f8fa; -} - -body.theme-github #preview blockquote, body.theme-github .preview-content blockquote { - color: #6a737d; - border-left-color: #dfe2e5; -} - -body.theme-github #preview a, body.theme-github .preview-content a { - color: #0366d6; -} - -body.theme-github .status-bar { - background: #fafbfc; - border-top-color: #e1e4e8; - color: #586069; -} - -/* Hide preview pane by default */ -.pane.hidden { - display: none; -} - -.pane.full-width { - border-right: none; -} - -/* Legacy support */ -#preview-pane.hidden { - display: none; -} - -#editor-pane.full-width { - border-right: none; -} - -/* Find & Replace Dialog */ -.find-dialog { - position: absolute; - top: 60px; - right: 20px; - background: #fff; - border: 1px solid #ddd; - border-radius: 6px; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - z-index: 1000; - min-width: 400px; - padding: 12px; -} - -.find-dialog.hidden { - display: none; -} - -.find-controls { - display: flex; - align-items: center; - gap: 6px; - margin-bottom: 8px; -} - -.find-controls input { - flex: 1; - padding: 6px 8px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 13px; -} - -.find-controls button { - padding: 6px 10px; - border: 1px solid #ddd; - background: #f5f5f5; - border-radius: 4px; - cursor: pointer; - font-size: 12px; - white-space: nowrap; -} - -.find-controls button:hover { - background: #e5e5e5; -} - -.find-info { - font-size: 12px; - color: #666; - text-align: right; -} - -/* Editor Wrapper with Line Numbers */ -.editor-wrapper { - position: relative; - display: flex; - height: 100%; -} - -.line-numbers { - min-width: 40px; - padding: 8px 4px; - background: #f8f8f8; - border-right: 1px solid #ddd; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 13px; - line-height: 1.4; - color: #666; - text-align: right; - user-select: none; - overflow: hidden; -} - -.line-numbers.hidden { - display: none; -} - -.line-numbers .line-number { - display: block; - white-space: nowrap; -} - -/* Adjust editor when line numbers are shown */ -.editor-wrapper #editor { - flex: 1; -} - -/* Export Options Dialog */ -.export-dialog { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.export-dialog.hidden { - display: none; -} - -.export-dialog-content { - background: #fff; - border-radius: 6px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); - width: 85%; - max-width: 450px; - max-height: 80vh; - overflow-y: auto; - position: relative; - font-size: 12px; -} - -.export-dialog-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 16px; - border-bottom: 1px solid #eee; -} - -.export-dialog-header h3 { - margin: 0; - font-size: 14px; - font-weight: 600; -} - -.export-dialog-body { - padding: 14px 16px; -} - -.export-section { - margin-bottom: 14px; -} - -.export-section label { - display: block; - font-weight: 500; - margin-bottom: 4px; - color: #333; - font-size: 12px; -} - -.export-section label.checkbox-inline { - display: flex; - align-items: center; - gap: 6px; - cursor: pointer; - font-weight: 400; -} - -.export-section label.checkbox-inline input[type="checkbox"] { - width: auto; - margin: 0; -} - -/* PDF Editor buttons */ -#add-merge-file { - padding: 5px 10px; - font-size: 12px; - background: #4a90d9; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; -} - -#add-merge-file:hover { - background: #357abd; -} - -.export-section select, -.export-section input[type="text"], -.export-section input[type="number"] { - width: 100%; - padding: 5px 8px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 12px; -} - -.export-section select:focus, -.export-section input:focus { - outline: none; - border-color: #007acc; - box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.1); -} - -.metadata-container { - border: 1px solid #eee; - border-radius: 4px; - padding: 12px; - margin-bottom: 8px; -} - -.metadata-field { - display: flex; - gap: 8px; - margin-bottom: 8px; -} - -.metadata-field:last-child { - margin-bottom: 0; -} - -.metadata-key { - flex: 0 0 120px; - font-weight: 600; -} - -.metadata-value { - flex: 1; -} - -.checkbox-group { - display: flex; - flex-direction: column; - gap: 8px; - margin-bottom: 12px; -} - -.checkbox-group label { - display: flex; - align-items: center; - gap: 8px; - font-weight: normal; - margin-bottom: 0; -} - -.checkbox-group input[type="checkbox"] { - margin: 0; -} - -.form-row { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 8px; -} - -.form-row label { - flex: 0 0 120px; - margin-bottom: 0; -} - -.form-row input, -.form-row select { - flex: 1; -} - -.form-row button { - flex: 0 0 auto; - padding: 8px 16px; - background: #f5f5f5; - border: 1px solid #ddd; - border-radius: 4px; - cursor: pointer; - font-size: 14px; -} - -.form-row button:hover { - background: #e8e8e8; -} - -.pdf-only { - display: none; -} - -.export-dialog[data-format="pdf"] .pdf-only { - display: block; -} - -.export-dialog-footer { - display: flex; - justify-content: flex-end; - gap: 12px; - padding: 16px 20px; - border-top: 1px solid #eee; - background: #f8f9fa; - border-radius: 0 0 8px 8px; -} - -.export-dialog-footer button { - padding: 10px 20px; - border: 1px solid #ddd; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - font-weight: 500; -} - -.export-dialog-footer button.primary { - background: #007acc; - color: white; - border-color: #007acc; -} - -.export-dialog-footer button.primary:hover { - background: #005fa3; - border-color: #005fa3; -} - -.export-dialog-footer button:not(.primary) { - background: #f5f5f5; -} - -.export-dialog-footer button:not(.primary):hover { - background: #e8e8e8; -} - -#export-dialog-close { - background: none; - border: none; - font-size: 20px; - cursor: pointer; - padding: 0; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; -} - -#export-dialog-close:hover { - background: #f0f0f0; -} - -#add-metadata-field { - padding: 6px 12px; - background: #f8f9fa; - border: 1px solid #ddd; - border-radius: 4px; - cursor: pointer; - font-size: 13px; - color: #666; -} - -#add-metadata-field:hover { - background: #e9ecef; -} - -/* Theme support for export dialog */ -body.theme-dark .export-dialog-content { - background: #2d2d2d; - color: #f0f0f0; -} - -body.theme-dark .export-dialog-header { - border-color: #444; -} - -body.theme-dark .export-dialog-footer { - background: #333; - border-color: #444; -} - -body.theme-dark .export-section label { - color: #f0f0f0; -} - -body.theme-dark .export-section select, -body.theme-dark .export-section input { - background: #1a1a1a; - border-color: #555; - color: #f0f0f0; -} - -body.theme-dark .metadata-container { - border-color: #444; - background: #1a1a1a; -} - -body.theme-dark .form-row button, -body.theme-dark #add-metadata-field { - background: #1a1a1a; - border-color: #555; - color: #f0f0f0; -} - -/* PDF Editor Specific Styles */ -.pdf-editor-content { - max-width: 450px !important; - max-height: 75vh; - overflow-y: auto; - font-size: 12px; -} - -.pdf-editor-body { - min-height: 200px; - max-height: calc(85vh - 150px); - overflow-y: auto; -} - -.pdf-operation-section { - animation: fadeIn 0.3s ease-in; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.pdf-operation-section.hidden { - display: none !important; -} - -/* Make PDF Editor sections more compact */ -.pdf-operation-section .export-section { - margin-bottom: 18px; -} - -.pdf-operation-section .export-section:last-child { - margin-bottom: 0; -} - -/* File list styling for merge operation */ -.file-list { - border: 1px solid #ddd; - border-radius: 4px; - padding: 10px; - margin-bottom: 10px; - min-height: 100px; - max-height: 200px; - overflow-y: auto; - background: #fafafa; -} - -.file-entry { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; - margin-bottom: 6px; - background: white; - border: 1px solid #e0e0e0; - border-radius: 4px; -} - -.file-entry:last-child { - margin-bottom: 0; -} - -.file-name { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - margin-right: 10px; - font-size: 13px; -} - -.remove-file { - padding: 4px 10px; - background: #dc3545; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 12px; -} - -.remove-file:hover { - background: #c82333; -} - -/* Theme support for PDF Editor */ -body.theme-dark .file-list { - background: #1a1a1a; - border-color: #555; -} - -body.theme-dark .file-entry { - background: #2d2d2d; - border-color: #444; - color: #f0f0f0; -} - -body.theme-dark .pdf-editor-content { - background: #2d2d2d; -} - -body.theme-dark .pdf-editor-body { - background: #2d2d2d; -} - -body.theme-dark .form-row button:hover, -body.theme-dark #add-metadata-field:hover { - background: #333; -} - -body.theme-dark #export-dialog-close:hover { - background: #444; -} - -/* Batch Conversion Dialog */ -.batch-dialog { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.batch-dialog.hidden { - display: none; -} - -.batch-dialog-content { - background: #fff; - border-radius: 6px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); - width: 85%; - max-width: 450px; - max-height: 75vh; - overflow-y: auto; - font-size: 12px; -} - -.batch-dialog-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px 20px; - border-bottom: 1px solid #eee; -} - -.batch-dialog-header h3 { - margin: 0; - font-size: 18px; - font-weight: 600; -} - -.batch-dialog-body { - padding: 20px; -} - -.batch-section { - margin-bottom: 20px; -} - -.batch-section label { - display: block; - font-weight: 600; - margin-bottom: 8px; - color: #333; -} - -.batch-section select, -.batch-section input[type="text"] { - width: 100%; - padding: 8px 12px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 14px; -} - -.folder-input-group { - display: flex; - gap: 8px; -} - -.folder-input-group input { - flex: 1; -} - -.folder-input-group button { - padding: 8px 16px; - background: #f5f5f5; - border: 1px solid #ddd; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - white-space: nowrap; -} - -.folder-input-group button:hover { - background: #e8e8e8; -} - -.batch-section label input[type="checkbox"] { - margin-right: 8px; -} - -#batch-show-options { - padding: 8px 16px; - background: #f8f9fa; - border: 1px solid #ddd; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - color: #666; -} - -#batch-show-options:hover { - background: #e9ecef; -} - -.batch-progress { - border: 1px solid #ddd; - border-radius: 4px; - padding: 16px; - background: #f8f9fa; -} - -.progress-bar { - width: 100%; - height: 8px; - background: #e0e0e0; - border-radius: 4px; - overflow: hidden; - margin-bottom: 8px; -} - -.progress-fill { - height: 100%; - background: #007acc; - transition: width 0.3s ease; - width: 0%; -} - -.progress-text { - display: flex; - justify-content: space-between; - font-size: 14px; - color: #666; -} - -.batch-dialog-footer { - display: flex; - justify-content: flex-end; - gap: 12px; - padding: 16px 20px; - border-top: 1px solid #eee; - background: #f8f9fa; - border-radius: 0 0 8px 8px; -} - -.batch-dialog-footer button { - padding: 10px 20px; - border: 1px solid #ddd; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - font-weight: 500; -} - -.batch-dialog-footer button.primary { - background: #007acc; - color: white; - border-color: #007acc; -} - -.batch-dialog-footer button.primary:hover:not(:disabled) { - background: #005fa3; - border-color: #005fa3; -} - -.batch-dialog-footer button.primary:disabled { - background: #ccc; - border-color: #ccc; - cursor: not-allowed; -} - -.batch-dialog-footer button:not(.primary) { - background: #f5f5f5; -} - -.batch-dialog-footer button:not(.primary):hover { - background: #e8e8e8; -} - -#batch-dialog-close { - background: none; - border: none; - font-size: 20px; - cursor: pointer; - padding: 0; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; -} - -#batch-dialog-close:hover { - background: #f0f0f0; -} - -/* Theme support for batch dialog */ -body.theme-dark .batch-dialog-content { - background: #2d2d2d; - color: #f0f0f0; -} - -body.theme-dark .batch-dialog-header { - border-color: #444; -} - -body.theme-dark .batch-dialog-footer { - background: #333; - border-color: #444; -} - -body.theme-dark .batch-section label { - color: #f0f0f0; -} - -body.theme-dark .batch-section select, -body.theme-dark .batch-section input { - background: #1a1a1a; - border-color: #555; - color: #f0f0f0; -} - -body.theme-dark .folder-input-group button, -body.theme-dark #batch-show-options { - background: #1a1a1a; - border-color: #555; - color: #f0f0f0; -} - -body.theme-dark .folder-input-group button:hover, -body.theme-dark #batch-show-options:hover { - background: #333; -} - -body.theme-dark .batch-progress { - background: #1a1a1a; - border-color: #444; -} - -body.theme-dark .progress-text { - color: #ccc; -} - -body.theme-dark #batch-dialog-close:hover { - background: #444; -} - -/* Theme support for find dialog */ -body.theme-dark .find-dialog { - background: #2d2d2d; - border-color: #555; - color: #f0f0f0; -} - -body.theme-dark .find-controls input { - background: #333; - border-color: #555; - color: #f0f0f0; -} - -body.theme-dark .find-controls button { - background: #404040; - border-color: #555; - color: #f0f0f0; -} - -body.theme-dark .find-controls button:hover { - background: #4a4a4a; -} - -body.theme-dark .line-numbers { - background: #2a2a2a; - border-right-color: #555; - color: #888; -} - -/* Additional theme support for line numbers */ -body.theme-solarized .line-numbers { - background: #fdf6e3; - border-right-color: #eee8d5; - color: #93a1a1; -} - -body.theme-monokai .line-numbers { - background: #2f2f2f; - border-right-color: #555; - color: #75715e; -} - -body.theme-github .line-numbers { - background: #fafbfc; - border-right-color: #e1e4e8; - color: #586069; -} - -/* Export Profiles Styles */ -.export-profiles { - border-bottom: 1px solid #ddd; - padding-bottom: 15px; - margin-bottom: 15px; -} - -.profile-controls { - display: flex; - gap: 8px; - align-items: center; - margin-top: 8px; -} - -.profile-controls select { - flex: 1; - padding: 8px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 14px; -} - -.profile-controls button { - padding: 8px 12px; - border: 1px solid #ddd; - border-radius: 4px; - background: #f5f5f5; - cursor: pointer; - font-size: 13px; - transition: all 0.2s; -} - -.profile-controls button:hover { - background: #e8e8e8; -} - -/* Advanced Export Toggle Styles */ -.export-mode-toggle { - border-bottom: 1px solid #ddd; - padding-bottom: 15px; - margin-bottom: 20px; -} - -.checkbox-label { - display: flex; - align-items: center; - cursor: pointer; - font-weight: 500; - margin-bottom: 5px; -} - -.checkbox-label input[type="checkbox"] { - margin-right: 10px; - transform: scale(1.2); -} - -.export-help { - color: #666; - font-size: 12px; - font-style: italic; - margin-left: 25px; -} - -.advanced-options { - transition: all 0.3s ease; - overflow: visible; - position: relative; - z-index: 10; -} - -.advanced-options.hidden { - display: none; -} - -.basic-options { - background: #f8f9fa; - border: 1px solid #e9ecef; - border-radius: 6px; - padding: 15px; - margin-top: 10px; -} - -.basic-options label { - font-weight: normal; - margin-bottom: 8px; -} - -/* Theme support for advanced export toggle */ -body.theme-dark .export-mode-toggle { - border-color: #444; -} - -body.theme-dark .export-help { - color: #999; -} - -body.theme-dark .basic-options { - background: #1a1a1a; - border-color: #444; -} - -body.theme-dark .checkbox-label { - color: #f0f0f0; -} - -body.theme-solarized .export-mode-toggle { - border-color: #eee8d5; -} - -body.theme-solarized .export-help { - color: #586e75; -} - -body.theme-solarized .basic-options { - background: #fdf6e3; - border-color: #eee8d5; -} - -body.theme-monokai .export-mode-toggle { - border-color: #49483e; -} - -body.theme-monokai .export-help { - color: #75715e; -} - -body.theme-monokai .basic-options { - background: #2f2f2f; - border-color: #49483e; -} - -body.theme-github .export-mode-toggle { - border-color: #e1e4e8; -} - -body.theme-github .export-help { - color: #586069; -} - -body.theme-github .basic-options { - background: #f6f8fa; - border-color: #e1e4e8; -} - -/* Enhanced Statistics Styles */ -.enhanced-stats { - font-size: 11px; - color: #666; - padding: 2px 0; - border-top: 1px solid #ddd; - margin-top: 2px; - font-family: monospace; -} - -/* Theme support for enhanced stats */ -body.theme-dark .enhanced-stats { - color: #999; - border-color: #444; -} - -body.theme-solarized .enhanced-stats { - color: #586e75; - border-color: #eee8d5; -} - -body.theme-monokai .enhanced-stats { - color: #75715e; - border-color: #49483e; -} - -body.theme-github .enhanced-stats { - color: #586069; - border-color: #e1e4e8; -} - -/* Auto-save indicator */ -.auto-save-indicator { - position: fixed; - top: 20px; - right: 20px; - background: #28a745; - color: white; - padding: 8px 16px; - border-radius: 4px; - font-size: 12px; - font-weight: 500; - z-index: 10000; - box-shadow: 0 2px 8px rgba(0,0,0,0.2); - animation: slideInRight 0.3s ease-out; -} - -.auto-save-indicator.fade-out { - animation: fadeOut 0.3s ease-out forwards; -} - -@keyframes slideInRight { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -@keyframes fadeOut { - from { - opacity: 1; - } - to { - opacity: 0; - } -} - -/* Theme: Dracula */ -body.theme-dracula { - background: #282a36; - color: #f8f8f2; -} - -body.theme-dracula .tab-bar { - background: #343746; - border-bottom-color: #44475a; -} - -body.theme-dracula .tab { - background: #44475a; - border-color: #6272a4; - color: #f8f8f2; -} - -body.theme-dracula .tab.active { - background: #282a36; - border-color: #bd93f9; -} - -body.theme-dracula .toolbar { - background: #343746; - border-bottom-color: #44475a; -} - -body.theme-dracula .toolbar button { - color: #f8f8f2; -} - -body.theme-dracula .toolbar button:hover { - background: #44475a; - border-color: #6272a4; -} - -body.theme-dracula .editor-textarea { - background: #282a36; - color: #f8f8f2; -} - -body.theme-dracula .pane:last-child { - background: #282a36; -} - -body.theme-dracula .preview-content { - color: #f8f8f2; -} - -body.theme-dracula .preview-content h1, -body.theme-dracula .preview-content h2 { - border-bottom-color: #44475a; - color: #bd93f9; -} - -body.theme-dracula .preview-content code { - background-color: #44475a; -} - -body.theme-dracula .preview-content pre { - background-color: #44475a; -} - -body.theme-dracula .preview-content a { - color: #8be9fd; -} - -body.theme-dracula .status-bar { - background: #343746; - border-top-color: #44475a; - color: #f8f8f2; -} - -/* Theme: Nord */ -body.theme-nord { - background: #2e3440; - color: #d8dee9; -} - -body.theme-nord .tab-bar { - background: #3b4252; - border-bottom-color: #4c566a; -} - -body.theme-nord .tab { - background: #434c5e; - border-color: #4c566a; - color: #d8dee9; -} - -body.theme-nord .tab.active { - background: #2e3440; - border-color: #88c0d0; -} - -body.theme-nord .toolbar { - background: #3b4252; - border-bottom-color: #4c566a; -} - -body.theme-nord .toolbar button { - color: #d8dee9; -} - -body.theme-nord .toolbar button:hover { - background: #434c5e; - border-color: #4c566a; -} - -body.theme-nord .editor-textarea { - background: #2e3440; - color: #d8dee9; -} - -body.theme-nord .pane:last-child { - background: #2e3440; -} - -body.theme-nord .preview-content { - color: #d8dee9; -} - -body.theme-nord .preview-content h1, -body.theme-nord .preview-content h2 { - border-bottom-color: #4c566a; - color: #88c0d0; -} - -body.theme-nord .preview-content code { - background-color: #3b4252; -} - -body.theme-nord .preview-content pre { - background-color: #3b4252; -} - -body.theme-nord .preview-content a { - color: #81a1c1; -} - -body.theme-nord .status-bar { - background: #3b4252; - border-top-color: #4c566a; - color: #d8dee9; -} - -/* Theme: One Dark */ -body.theme-onedark { - background: #282c34; - color: #abb2bf; -} - -body.theme-onedark .tab-bar { - background: #21252b; - border-bottom-color: #181a1f; -} - -body.theme-onedark .tab { - background: #2c313a; - border-color: #3e4451; - color: #abb2bf; -} - -body.theme-onedark .tab.active { - background: #282c34; - border-color: #61afef; -} - -body.theme-onedark .toolbar { - background: #21252b; - border-bottom-color: #181a1f; -} - -body.theme-onedark .toolbar button { - color: #abb2bf; -} - -body.theme-onedark .toolbar button:hover { - background: #2c313a; - border-color: #3e4451; -} - -body.theme-onedark .editor-textarea { - background: #282c34; - color: #abb2bf; -} - -body.theme-onedark .pane:last-child { - background: #282c34; -} - -body.theme-onedark .preview-content { - color: #abb2bf; -} - -body.theme-onedark .preview-content h1, -body.theme-onedark .preview-content h2 { - border-bottom-color: #3e4451; - color: #61afef; -} - -body.theme-onedark .preview-content code { - background-color: #21252b; -} - -body.theme-onedark .preview-content pre { - background-color: #21252b; -} - -body.theme-onedark .preview-content a { - color: #56b6c2; -} - -body.theme-onedark .status-bar { - background: #21252b; - border-top-color: #181a1f; - color: #abb2bf; -} - -/* Theme: Atom One Light */ -body.theme-atomonelight { - background: #fafafa; - color: #383a42; -} - -body.theme-atomonelight .tab-bar { - background: #f0f0f0; - border-bottom-color: #e0e0e0; -} - -body.theme-atomonelight .tab { - background: #e5e5e6; - border-color: #d0d0d0; - color: #383a42; -} - -body.theme-atomonelight .tab.active { - background: #fafafa; - border-color: #4078f2; -} - -body.theme-atomonelight .toolbar { - background: #f0f0f0; - border-bottom-color: #e0e0e0; -} - -body.theme-atomonelight .toolbar button { - color: #383a42; -} - -body.theme-atomonelight .toolbar button:hover { - background: #e5e5e6; - border-color: #d0d0d0; -} - -body.theme-atomonelight .editor-textarea { - background: #fafafa; - color: #383a42; -} - -body.theme-atomonelight .pane:last-child { - background: #fafafa; -} - -body.theme-atomonelight .preview-content { - color: #383a42; -} - -body.theme-atomonelight .preview-content h1, -body.theme-atomonelight .preview-content h2 { - border-bottom-color: #e0e0e0; - color: #4078f2; -} - -body.theme-atomonelight .preview-content code { - background-color: #f0f0f0; -} - -body.theme-atomonelight .preview-content pre { - background-color: #f0f0f0; -} - -body.theme-atomonelight .preview-content a { - color: #0184bc; -} - -body.theme-atomonelight .status-bar { - background: #f0f0f0; - border-top-color: #e0e0e0; - color: #383a42; -} - -/* Theme: Material */ -body.theme-material { - background: #263238; - color: #eeffff; -} - -body.theme-material .tab-bar { - background: #2e3c43; - border-bottom-color: #37474f; -} - -body.theme-material .tab { - background: #37474f; - border-color: #546e7a; - color: #eeffff; -} - -body.theme-material .tab.active { - background: #263238; - border-color: #82aaff; -} - -body.theme-material .toolbar { - background: #2e3c43; - border-bottom-color: #37474f; -} - -body.theme-material .toolbar button { - color: #eeffff; -} - -body.theme-material .toolbar button:hover { - background: #37474f; - border-color: #546e7a; -} - -body.theme-material .editor-textarea { - background: #263238; - color: #eeffff; -} - -body.theme-material .pane:last-child { - background: #263238; -} - -body.theme-material .preview-content { - color: #eeffff; -} - -body.theme-material .preview-content h1, -body.theme-material .preview-content h2 { - border-bottom-color: #37474f; - color: #82aaff; -} - -body.theme-material .preview-content code { - background-color: #2e3c43; -} - -body.theme-material .preview-content pre { - background-color: #2e3c43; -} - -body.theme-material .preview-content a { - color: #80cbc4; -} - -body.theme-material .status-bar { - background: #2e3c43; - border-top-color: #37474f; - color: #eeffff; -} - -/* Theme: Gruvbox Dark */ -body.theme-gruvbox-dark { - background: #282828; - color: #ebdbb2; -} - -body.theme-gruvbox-dark .tab-bar { - background: #3c3836; - border-bottom-color: #504945; -} - -body.theme-gruvbox-dark .tab { - background: #504945; - border-color: #665c54; - color: #ebdbb2; -} - -body.theme-gruvbox-dark .tab.active { - background: #282828; - border-color: #fabd2f; -} - -body.theme-gruvbox-dark .toolbar { - background: #3c3836; - border-bottom-color: #504945; -} - -body.theme-gruvbox-dark .toolbar button { - color: #ebdbb2; -} - -body.theme-gruvbox-dark .toolbar button:hover { - background: #504945; - border-color: #665c54; -} - -body.theme-gruvbox-dark .editor-textarea { - background: #282828; - color: #ebdbb2; -} - -body.theme-gruvbox-dark .pane:last-child { - background: #282828; -} - -body.theme-gruvbox-dark .preview-content { - color: #ebdbb2; -} - -body.theme-gruvbox-dark .preview-content h1, -body.theme-gruvbox-dark .preview-content h2 { - border-bottom-color: #504945; - color: #fabd2f; -} - -body.theme-gruvbox-dark .preview-content code { - background-color: #3c3836; -} - -body.theme-gruvbox-dark .preview-content pre { - background-color: #3c3836; -} - -body.theme-gruvbox-dark .preview-content a { - color: #83a598; -} - -body.theme-gruvbox-dark .status-bar { - background: #3c3836; - border-top-color: #504945; - color: #ebdbb2; -} - -/* Theme: Gruvbox Light */ -body.theme-gruvbox-light { - background: #fbf1c7; - color: #3c3836; -} - -body.theme-gruvbox-light .tab-bar { - background: #ebdbb2; - border-bottom-color: #d5c4a1; -} - -body.theme-gruvbox-light .tab { - background: #d5c4a1; - border-color: #bdae93; - color: #3c3836; -} - -body.theme-gruvbox-light .tab.active { - background: #fbf1c7; - border-color: #b57614; -} - -body.theme-gruvbox-light .toolbar { - background: #ebdbb2; - border-bottom-color: #d5c4a1; -} - -body.theme-gruvbox-light .toolbar button { - color: #3c3836; -} - -body.theme-gruvbox-light .toolbar button:hover { - background: #d5c4a1; - border-color: #bdae93; -} - -body.theme-gruvbox-light .editor-textarea { - background: #fbf1c7; - color: #3c3836; -} - -body.theme-gruvbox-light .pane:last-child { - background: #fbf1c7; -} - -body.theme-gruvbox-light .preview-content { - color: #3c3836; -} - -body.theme-gruvbox-light .preview-content h1, -body.theme-gruvbox-light .preview-content h2 { - border-bottom-color: #d5c4a1; - color: #b57614; -} - -body.theme-gruvbox-light .preview-content code { - background-color: #ebdbb2; -} - -body.theme-gruvbox-light .preview-content pre { - background-color: #ebdbb2; -} - -body.theme-gruvbox-light .preview-content a { - color: #076678; -} - -body.theme-gruvbox-light .status-bar { - background: #ebdbb2; - border-top-color: #d5c4a1; - color: #3c3836; -} - -/* Theme: Tokyo Night */ -body.theme-tokyonight { - background: #1a1b26; - color: #c0caf5; -} - -body.theme-tokyonight .tab-bar { - background: #16161e; - border-bottom-color: #24283b; -} - -body.theme-tokyonight .tab { - background: #24283b; - border-color: #414868; - color: #c0caf5; -} - -body.theme-tokyonight .tab.active { - background: #1a1b26; - border-color: #7aa2f7; -} - -body.theme-tokyonight .toolbar { - background: #16161e; - border-bottom-color: #24283b; -} - -body.theme-tokyonight .toolbar button { - color: #c0caf5; -} - -body.theme-tokyonight .toolbar button:hover { - background: #24283b; - border-color: #414868; -} - -body.theme-tokyonight .editor-textarea { - background: #1a1b26; - color: #c0caf5; -} - -body.theme-tokyonight .pane:last-child { - background: #1a1b26; -} - -body.theme-tokyonight .preview-content { - color: #c0caf5; -} - -body.theme-tokyonight .preview-content h1, -body.theme-tokyonight .preview-content h2 { - border-bottom-color: #24283b; - color: #7aa2f7; -} - -body.theme-tokyonight .preview-content code { - background-color: #24283b; -} - -body.theme-tokyonight .preview-content pre { - background-color: #24283b; -} - -body.theme-tokyonight .preview-content a { - color: #7dcfff; -} - -body.theme-tokyonight .status-bar { - background: #16161e; - border-top-color: #24283b; - color: #c0caf5; -} - -/* Theme: Palenight */ -body.theme-palenight { - background: #292d3e; - color: #a6accd; -} - -body.theme-palenight .tab-bar { - background: #32374d; - border-bottom-color: #444267; -} - -body.theme-palenight .tab { - background: #444267; - border-color: #676e95; - color: #a6accd; -} - -body.theme-palenight .tab.active { - background: #292d3e; - border-color: #82aaff; -} - -body.theme-palenight .toolbar { - background: #32374d; - border-bottom-color: #444267; -} - -body.theme-palenight .toolbar button { - color: #a6accd; -} - -body.theme-palenight .toolbar button:hover { - background: #444267; - border-color: #676e95; -} - -body.theme-palenight .editor-textarea { - background: #292d3e; - color: #a6accd; -} - -body.theme-palenight .pane:last-child { - background: #292d3e; -} - -body.theme-palenight .preview-content { - color: #a6accd; -} - -body.theme-palenight .preview-content h1, -body.theme-palenight .preview-content h2 { - border-bottom-color: #444267; - color: #82aaff; -} - -body.theme-palenight .preview-content code { - background-color: #32374d; -} - -body.theme-palenight .preview-content pre { - background-color: #32374d; -} - -body.theme-palenight .preview-content a { - color: #89ddff; -} - -body.theme-palenight .status-bar { - background: #32374d; - border-top-color: #444267; - color: #a6accd; -} - -/* Theme: Ayu Dark */ -body.theme-ayu-dark { - background: #0a0e14; - color: #b3b1ad; -} - -body.theme-ayu-dark .tab-bar { - background: #0d1016; - border-bottom-color: #1f2430; -} - -body.theme-ayu-dark .tab { - background: #1f2430; - border-color: #3d424d; - color: #b3b1ad; -} - -body.theme-ayu-dark .tab.active { - background: #0a0e14; - border-color: #ffaa33; -} - -body.theme-ayu-dark .toolbar { - background: #0d1016; - border-bottom-color: #1f2430; -} - -body.theme-ayu-dark .toolbar button { - color: #b3b1ad; -} - -body.theme-ayu-dark .toolbar button:hover { - background: #1f2430; - border-color: #3d424d; -} - -body.theme-ayu-dark .editor-textarea { - background: #0a0e14; - color: #b3b1ad; -} - -body.theme-ayu-dark .pane:last-child { - background: #0a0e14; -} - -body.theme-ayu-dark .preview-content { - color: #b3b1ad; -} - -body.theme-ayu-dark .preview-content h1, -body.theme-ayu-dark .preview-content h2 { - border-bottom-color: #1f2430; - color: #ffaa33; -} - -body.theme-ayu-dark .preview-content code { - background-color: #0d1016; -} - -body.theme-ayu-dark .preview-content pre { - background-color: #0d1016; -} - -body.theme-ayu-dark .preview-content a { - color: #39bae6; -} - -body.theme-ayu-dark .status-bar { - background: #0d1016; - border-top-color: #1f2430; - color: #b3b1ad; -} - -/* Theme: Ayu Light */ -body.theme-ayu-light { - background: #fafafa; - color: #5c6166; -} - -body.theme-ayu-light .tab-bar { - background: #f3f4f5; - border-bottom-color: #e7e8e9; -} - -body.theme-ayu-light .tab { - background: #e7e8e9; - border-color: #d9dadb; - color: #5c6166; -} - -body.theme-ayu-light .tab.active { - background: #fafafa; - border-color: #ff6a00; -} - -body.theme-ayu-light .toolbar { - background: #f3f4f5; - border-bottom-color: #e7e8e9; -} - -body.theme-ayu-light .toolbar button { - color: #5c6166; -} - -body.theme-ayu-light .toolbar button:hover { - background: #e7e8e9; - border-color: #d9dadb; -} - -body.theme-ayu-light .editor-textarea { - background: #fafafa; - color: #5c6166; -} - -body.theme-ayu-light .pane:last-child { - background: #fafafa; -} - -body.theme-ayu-light .preview-content { - color: #5c6166; -} - -body.theme-ayu-light .preview-content h1, -body.theme-ayu-light .preview-content h2 { - border-bottom-color: #e7e8e9; - color: #ff6a00; -} - -body.theme-ayu-light .preview-content code { - background-color: #f3f4f5; -} - -body.theme-ayu-light .preview-content pre { - background-color: #f3f4f5; -} - -body.theme-ayu-light .preview-content a { - color: #399ee6; -} - -body.theme-ayu-light .status-bar { - background: #f3f4f5; - border-top-color: #e7e8e9; - color: #5c6166; -} - -/* Theme: Ayu Mirage */ -body.theme-ayu-mirage { - background: #1f2430; - color: #cbccc6; -} - -body.theme-ayu-mirage .tab-bar { - background: #232834; - border-bottom-color: #343d4b; -} - -body.theme-ayu-mirage .tab { - background: #343d4b; - border-color: #465268; - color: #cbccc6; -} - -body.theme-ayu-mirage .tab.active { - background: #1f2430; - border-color: #ffa759; -} - -body.theme-ayu-mirage .toolbar { - background: #232834; - border-bottom-color: #343d4b; -} - -body.theme-ayu-mirage .toolbar button { - color: #cbccc6; -} - -body.theme-ayu-mirage .toolbar button:hover { - background: #343d4b; - border-color: #465268; -} - -body.theme-ayu-mirage .editor-textarea { - background: #1f2430; - color: #cbccc6; -} - -body.theme-ayu-mirage .pane:last-child { - background: #1f2430; -} - -body.theme-ayu-mirage .preview-content { - color: #cbccc6; -} - -body.theme-ayu-mirage .preview-content h1, -body.theme-ayu-mirage .preview-content h2 { - border-bottom-color: #343d4b; - color: #ffa759; -} - -body.theme-ayu-mirage .preview-content code { - background-color: #232834; -} - -body.theme-ayu-mirage .preview-content pre { - background-color: #232834; -} - -body.theme-ayu-mirage .preview-content a { - color: #5ccfe6; -} - -body.theme-ayu-mirage .status-bar { - background: #232834; - border-top-color: #343d4b; - color: #cbccc6; -} - -/* Theme: Oceanic Next */ -body.theme-oceanic-next { - background: #1b2b34; - color: #cdd3de; -} - -body.theme-oceanic-next .tab-bar { - background: #20353f; - border-bottom-color: #343d46; -} - -body.theme-oceanic-next .tab { - background: #343d46; - border-color: #4f5b66; - color: #cdd3de; -} - -body.theme-oceanic-next .tab.active { - background: #1b2b34; - border-color: #6699cc; -} - -body.theme-oceanic-next .toolbar { - background: #20353f; - border-bottom-color: #343d46; -} - -body.theme-oceanic-next .toolbar button { - color: #cdd3de; -} - -body.theme-oceanic-next .toolbar button:hover { - background: #343d46; - border-color: #4f5b66; -} - -body.theme-oceanic-next .editor-textarea { - background: #1b2b34; - color: #cdd3de; -} - -body.theme-oceanic-next .pane:last-child { - background: #1b2b34; -} - -body.theme-oceanic-next .preview-content { - color: #cdd3de; -} - -body.theme-oceanic-next .preview-content h1, -body.theme-oceanic-next .preview-content h2 { - border-bottom-color: #343d46; - color: #6699cc; -} - -body.theme-oceanic-next .preview-content code { - background-color: #20353f; -} - -body.theme-oceanic-next .preview-content pre { - background-color: #20353f; -} - -body.theme-oceanic-next .preview-content a { - color: #5fb3b3; -} - -body.theme-oceanic-next .status-bar { - background: #20353f; - border-top-color: #343d46; - color: #cdd3de; -} - -/* Theme: Cobalt2 */ -body.theme-cobalt2 { - background: #193549; - color: #ffffff; -} - -body.theme-cobalt2 .tab-bar { - background: #15232d; - border-bottom-color: #2a4a5d; -} - -body.theme-cobalt2 .tab { - background: #0d3a58; - border-color: #2a4a5d; - color: #ffffff; -} - -body.theme-cobalt2 .tab.active { - background: #193549; - border-color: #ffc600; -} - -body.theme-cobalt2 .toolbar { - background: #15232d; - border-bottom-color: #2a4a5d; -} - -body.theme-cobalt2 .toolbar button { - color: #ffffff; -} - -body.theme-cobalt2 .toolbar button:hover { - background: #0d3a58; - border-color: #2a4a5d; -} - -body.theme-cobalt2 .editor-textarea { - background: #193549; - color: #ffffff; -} - -body.theme-cobalt2 .pane:last-child { - background: #193549; -} - -body.theme-cobalt2 .preview-content { - color: #ffffff; -} - -body.theme-cobalt2 .preview-content h1, -body.theme-cobalt2 .preview-content h2 { - border-bottom-color: #2a4a5d; - color: #ffc600; -} - -body.theme-cobalt2 .preview-content code { - background-color: #0d3a58; -} - -body.theme-cobalt2 .preview-content pre { - background-color: #0d3a58; -} - -body.theme-cobalt2 .preview-content a { - color: #3ad900; -} - -body.theme-cobalt2 .status-bar { - background: #15232d; - border-top-color: #2a4a5d; - color: #ffffff; -} - -/* Theme: Concrete Dark - ConcreteInfo branded dark theme */ -body.theme-concrete-dark { - background: #0d0b09; - color: #e3e3e3; -} - -body.theme-concrete-dark .tab-bar { - background: #1a1816; - border-bottom-color: #464646; -} - -body.theme-concrete-dark .tab { - background: #464646; - border-color: #9a9696; - color: #e3e3e3; -} - -body.theme-concrete-dark .tab.active { - background: #0d0b09; - border-color: #e5461f; -} - -body.theme-concrete-dark .toolbar { - background: #1a1816; - border-bottom-color: #464646; -} - -body.theme-concrete-dark .toolbar button { - color: #e3e3e3; -} - -body.theme-concrete-dark .toolbar button:hover { - background: #464646; - border-color: #e5461f; -} - -body.theme-concrete-dark .editor-textarea { - background: #0d0b09; - color: #e3e3e3; -} - -body.theme-concrete-dark .pane:last-child { - background: #0d0b09; -} - -body.theme-concrete-dark .preview-content { - color: #e3e3e3; -} - -body.theme-concrete-dark .preview-content h1, -body.theme-concrete-dark .preview-content h2 { - border-bottom-color: #464646; - color: #e5461f; -} - -body.theme-concrete-dark .preview-content code { - background-color: #464646; -} - -body.theme-concrete-dark .preview-content pre { - background-color: #464646; -} - -body.theme-concrete-dark .preview-content a { - color: #e5461f; -} - -body.theme-concrete-dark .status-bar { - background: #1a1816; - border-top-color: #464646; - color: #9a9696; -} - -/* Theme: Concrete Light - ConcreteInfo branded light theme */ -body.theme-concrete-light { - background: #fafafa; - color: #0d0b09; -} - -body.theme-concrete-light .tab-bar { - background: #e3e3e3; - border-bottom-color: #9a9696; -} - -body.theme-concrete-light .tab { - background: #e3e3e3; - border-color: #9a9696; - color: #0d0b09; -} - -body.theme-concrete-light .tab.active { - background: #fafafa; - border-color: #e5461f; -} - -body.theme-concrete-light .toolbar { - background: #e3e3e3; - border-bottom-color: #9a9696; -} - -body.theme-concrete-light .toolbar button { - color: #0d0b09; -} - -body.theme-concrete-light .toolbar button:hover { - background: #9a9696; - border-color: #e5461f; -} - -body.theme-concrete-light .editor-textarea { - background: #fafafa; - color: #0d0b09; -} - -body.theme-concrete-light .pane:last-child { - background: #fafafa; -} - -body.theme-concrete-light .preview-content { - color: #0d0b09; -} - -body.theme-concrete-light .preview-content h1, -body.theme-concrete-light .preview-content h2 { - border-bottom-color: #9a9696; - color: #e5461f; -} - -body.theme-concrete-light .preview-content code { - background-color: #e3e3e3; -} - -body.theme-concrete-light .preview-content pre { - background-color: #e3e3e3; -} - -body.theme-concrete-light .preview-content a { - color: #e5461f; -} - -body.theme-concrete-light .status-bar { - background: #e3e3e3; - border-top-color: #9a9696; - color: #464646; -} - -/* Theme: Concrete Warm - ConcreteInfo branded warm theme */ -body.theme-concrete-warm { - background: #464646; - color: #e3e3e3; -} - -body.theme-concrete-warm .tab-bar { - background: #3a3836; - border-bottom-color: #9a9696; -} - -body.theme-concrete-warm .tab { - background: #9a9696; - border-color: #e3e3e3; - color: #0d0b09; -} - -body.theme-concrete-warm .tab.active { - background: #464646; - border-color: #e5461f; -} - -body.theme-concrete-warm .toolbar { - background: #3a3836; - border-bottom-color: #9a9696; -} - -body.theme-concrete-warm .toolbar button { - color: #e3e3e3; -} - -body.theme-concrete-warm .toolbar button:hover { - background: #9a9696; - border-color: #e5461f; -} - -body.theme-concrete-warm .editor-textarea { - background: #464646; - color: #e3e3e3; -} - -body.theme-concrete-warm .pane:last-child { - background: #464646; -} - -body.theme-concrete-warm .preview-content { - color: #e3e3e3; -} - -body.theme-concrete-warm .preview-content h1, -body.theme-concrete-warm .preview-content h2 { - border-bottom-color: #9a9696; - color: #e5461f; -} - -body.theme-concrete-warm .preview-content code { - background-color: #3a3836; -} - -body.theme-concrete-warm .preview-content pre { - background-color: #3a3836; -} - -body.theme-concrete-warm .preview-content a { - color: #e5461f; -} - -body.theme-concrete-warm .status-bar { - background: #3a3836; - border-top-color: #9a9696; - color: #e3e3e3; -} - -/* ======================================== - Sepia Theme - Warm reading theme - ======================================== */ -body.theme-sepia { - background: #f4ecd8; - color: #5b4636; -} - -body.theme-sepia .tab-bar { - background: #e8dcc8; - border-bottom-color: #d4c4a8; -} - -body.theme-sepia .tab { - background: #e8dcc8; - border-color: #d4c4a8; - color: #5b4636; -} - -body.theme-sepia .tab.active { - background: #f4ecd8; - border-bottom-color: #8b7355; -} - -body.theme-sepia .toolbar { - background: #e8dcc8; - border-bottom-color: #d4c4a8; -} - -body.theme-sepia .toolbar button { - color: #5b4636; -} - -body.theme-sepia .toolbar button:hover { - background: #d4c4a8; -} - -body.theme-sepia .editor-textarea { - background: #f4ecd8; - color: #5b4636; -} - -body.theme-sepia .pane:last-child { - background: #f4ecd8; -} - -body.theme-sepia .preview-content { - color: #5b4636; -} - -body.theme-sepia .preview-content h1, -body.theme-sepia .preview-content h2 { - color: #3d2914; - border-bottom-color: #d4c4a8; -} - -body.theme-sepia .preview-content code { - background: #e8dcc8; - color: #8b4513; -} - -body.theme-sepia .preview-content pre { - background: #e8dcc8; - border-color: #d4c4a8; -} - -body.theme-sepia .preview-content a { - color: #8b4513; -} - -body.theme-sepia .preview-content blockquote { - border-left-color: #8b7355; - background: #e8dcc8; -} - -body.theme-sepia .status-bar { - background: #e8dcc8; - border-top-color: #d4c4a8; - color: #5b4636; -} - -/* ======================================== - Paper Theme - Clean white paper - ======================================== */ -body.theme-paper { - background: #ffffff; - color: #1a1a1a; -} - -body.theme-paper .tab-bar { - background: #f8f8f8; - border-bottom-color: #e0e0e0; -} - -body.theme-paper .tab { - background: #f0f0f0; - border-color: #e0e0e0; - color: #333; -} - -body.theme-paper .tab.active { - background: #ffffff; - border-bottom-color: #1a1a1a; -} - -body.theme-paper .toolbar { - background: #f8f8f8; - border-bottom-color: #e0e0e0; -} - -body.theme-paper .toolbar button { - color: #333; -} - -body.theme-paper .toolbar button:hover { - background: #e8e8e8; -} - -body.theme-paper .editor-textarea { - background: #ffffff; - color: #1a1a1a; -} - -body.theme-paper .pane:last-child { - background: #ffffff; -} - -body.theme-paper .preview-content { - color: #1a1a1a; -} - -body.theme-paper .preview-content h1, -body.theme-paper .preview-content h2 { - color: #000; - border-bottom-color: #e0e0e0; -} - -body.theme-paper .preview-content code { - background: #f5f5f5; - color: #c41a16; -} - -body.theme-paper .preview-content pre { - background: #f5f5f5; - border-color: #e0e0e0; -} - -body.theme-paper .preview-content a { - color: #0366d6; -} - -body.theme-paper .preview-content blockquote { - border-left-color: #ddd; - background: #f9f9f9; -} - -body.theme-paper .status-bar { - background: #f8f8f8; - border-top-color: #e0e0e0; - color: #333; -} - -/* ======================================== - Rose Pine Dawn - Warm rose-tinted light - ======================================== */ -body.theme-rosepine-dawn { - background: #faf4ed; - color: #575279; -} - -body.theme-rosepine-dawn .tab-bar { - background: #f2e9e1; - border-bottom-color: #dfdad9; -} - -body.theme-rosepine-dawn .tab { - background: #f2e9e1; - border-color: #dfdad9; - color: #575279; -} - -body.theme-rosepine-dawn .tab.active { - background: #faf4ed; - border-bottom-color: #907aa9; -} - -body.theme-rosepine-dawn .toolbar { - background: #f2e9e1; - border-bottom-color: #dfdad9; -} - -body.theme-rosepine-dawn .toolbar button { - color: #575279; -} - -body.theme-rosepine-dawn .toolbar button:hover { - background: #dfdad9; -} - -body.theme-rosepine-dawn .editor-textarea { - background: #faf4ed; - color: #575279; -} - -body.theme-rosepine-dawn .pane:last-child { - background: #faf4ed; -} - -body.theme-rosepine-dawn .preview-content { - color: #575279; -} - -body.theme-rosepine-dawn .preview-content h1, -body.theme-rosepine-dawn .preview-content h2 { - color: #286983; - border-bottom-color: #dfdad9; -} - -body.theme-rosepine-dawn .preview-content code { - background: #f2e9e1; - color: #b4637a; -} - -body.theme-rosepine-dawn .preview-content pre { - background: #f2e9e1; - border-color: #dfdad9; -} - -body.theme-rosepine-dawn .preview-content a { - color: #56949f; -} - -body.theme-rosepine-dawn .preview-content blockquote { - border-left-color: #907aa9; - background: #f2e9e1; - color: #797593; -} - -body.theme-rosepine-dawn .status-bar { - background: #f2e9e1; - border-top-color: #dfdad9; - color: #575279; -} - -/* Print Styles */ -@media print { - /* Hide UI elements for clean print output */ - #toolbar, - #tab-bar, - #editor-container, - .editor-container, - #status-bar, - .status-bar, - .toolbar, - .tab-bar, - .editor-pane, - [id^="editor-pane-"], - .dialog, - .modal-overlay, - .export-dialog { - display: none !important; - } - - /* Ensure body and html take full width */ - html, body { - width: 100% !important; - height: auto !important; - margin: 0 !important; - padding: 0 !important; - overflow: visible !important; - } - - /* Show preview in full width */ - #preview, - .preview, - .preview-content, - [id^="preview-"], - [id^="preview-pane-"], - .tab-content, - .pane { - display: block !important; - width: 100% !important; - max-width: 100% !important; - height: auto !important; - margin: 0 !important; - padding: 20px !important; - overflow: visible !important; - position: static !important; - left: auto !important; - right: auto !important; - top: auto !important; - bottom: auto !important; - } - - /* Optimize preview content for printing */ - .preview-content, - [id^="preview-"] { - line-height: 1.6; - font-size: 12pt; - color: #000 !important; - background: white !important; - } - - /* No-styles mode for printing without theme colors */ - body.printing-no-styles .preview-content, - body.printing-no-styles [id^="preview-"] { - color: #000 !important; - background: #fff !important; - } - - /* Text formatting */ - .preview-content h1, - .preview-content h2, - .preview-content h3, - .preview-content h4, - .preview-content h5, - .preview-content h6 { - page-break-after: avoid; - margin-top: 1em; - margin-bottom: 0.5em; - } - - .preview-content p { - margin-bottom: 0.5em; - page-break-inside: avoid; - } - - /* Code blocks */ - .preview-content pre { - background: #f5f5f5 !important; - border: 1px solid #ddd !important; - page-break-inside: avoid; - padding: 10px; - font-size: 10pt; - } - - .preview-content code { - background: #f5f5f5 !important; - color: #000 !important; - font-size: 10pt; - } - - /* Lists */ - .preview-content ul, - .preview-content ol { - margin-left: 20px; - page-break-inside: avoid; - } - - .preview-content li { - page-break-inside: avoid; - margin-bottom: 0.3em; - } - - /* Tables */ - .preview-content table { - width: 100%; - border-collapse: collapse; - page-break-inside: avoid; - margin-bottom: 1em; - } - - .preview-content th, - .preview-content td { - border: 1px solid #999 !important; - padding: 8px !important; - text-align: left; - } - - .preview-content th { - background: #f0f0f0 !important; - font-weight: bold; - } - - /* Links - remove underline for print */ - .preview-content a { - color: #000 !important; - text-decoration: none; - } - - /* Blockquotes */ - .preview-content blockquote { - border-left: 3px solid #ccc; - padding-left: 15px; - margin-left: 0; - color: #333 !important; - } - - /* Images */ - .preview-content img { - max-width: 100%; - height: auto; - page-break-inside: avoid; - } - - /* Horizontal rules */ - .preview-content hr { - border: none; - border-top: 1px solid #999; - margin: 1em 0; - } -} - -/* Print mode classes */ -.print-mode { - display: block !important; - width: 100% !important; - max-width: 100% !important; - position: fixed !important; - top: 0 !important; - left: 0 !important; - z-index: 9999 !important; - background: white !important; - padding: 20px !important; -} - -/* Ensure preview panes are visible in print mode */ -#preview-pane-1, #preview-pane-2, #preview-pane-3, #preview-pane-4, #preview-pane-5 { - display: block !important; -} - -/* No-styles printing mode */ -body.printing-no-styles .preview-content, -body.printing-no-styles [id^="preview-"], -.print-no-styles .preview-content { - color: #000 !important; - background: white !important; -} - -body.printing-no-styles .preview-content h1, -body.printing-no-styles .preview-content h2, -body.printing-no-styles .preview-content h3, -body.printing-no-styles .preview-content h4, -body.printing-no-styles .preview-content h5, -body.printing-no-styles .preview-content h6, -.print-no-styles .preview-content h1, -.print-no-styles .preview-content h2, -.print-no-styles .preview-content h3, -.print-no-styles .preview-content h4, -.print-no-styles .preview-content h5, -.print-no-styles .preview-content h6 { - color: #000 !important; - border-color: #999 !important; -} - -body.printing-no-styles .preview-content code, -.print-no-styles .preview-content code { - background: #f5f5f5 !important; - color: #000 !important; -} - -body.printing-no-styles .preview-content pre, -.print-no-styles .preview-content pre { - background: #f5f5f5 !important; - color: #000 !important; - border-color: #999 !important; -} - -.print-no-styles .preview-content a { - color: #000 !important; -} - -.print-no-styles .preview-content blockquote { - color: #333 !important; - border-color: #999 !important; -} - -/* ================================ - Header & Footer Dialog Styles - ================================ */ - -.hf-enable-section { - margin-bottom: 20px; - padding: 15px; - background: var(--bg-secondary, #f5f5f5); - border-radius: 5px; -} - -.hf-enable-section label { - display: flex; - align-items: center; - font-weight: 600; - cursor: pointer; -} - -.hf-enable-section input[type="checkbox"] { - margin-right: 10px; - width: 18px; - height: 18px; - cursor: pointer; -} - -#hf-config-content { - transition: opacity 0.3s ease; -} - -#hf-config-content.disabled { - opacity: 0.5; - pointer-events: none; -} - -.hf-section { - margin-bottom: 25px; - padding: 20px; - background: var(--bg-tertiary, #fafafa); - border-radius: 8px; - border: 1px solid var(--border-color, #e0e0e0); -} - -.hf-section h4 { - margin: 0 0 15px 0; - color: var(--text-primary, #333); - font-size: 16px; - border-bottom: 2px solid var(--accent-color, #007bff); - padding-bottom: 8px; -} - -.hf-row { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 15px; - margin-bottom: 15px; -} - -.hf-column { - display: flex; - flex-direction: column; - position: relative; -} - -.hf-column label { - font-weight: 600; - margin-bottom: 5px; - color: var(--text-secondary, #666); - font-size: 13px; -} - -.hf-column input[type="text"] { - padding: 8px 35px 8px 10px; - border: 1px solid var(--border-color, #ccc); - border-radius: 4px; - font-size: 14px; - background: var(--input-bg, white); - color: var(--text-primary, #333); - transition: border-color 0.2s ease; -} - -.hf-column input[type="text"]:focus { - outline: none; - border-color: var(--accent-color, #007bff); - box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.1); -} - -.field-insert-btn { - position: absolute; - right: 5px; - bottom: 5px; - width: 28px; - height: 28px; - padding: 0; - background: var(--accent-color, #007bff); - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 18px; - line-height: 1; - transition: all 0.2s ease; -} - -.field-insert-btn:hover { - background: var(--accent-hover, #0056b3); - transform: scale(1.05); -} - -.field-insert-btn:active { - transform: scale(0.95); -} - -.hf-logo-row { - display: flex; - align-items: center; - gap: 10px; - padding: 15px; - background: var(--bg-secondary, #f5f5f5); - border-radius: 6px; - margin-top: 10px; -} - -.hf-logo-row label { - font-weight: 600; - color: var(--text-secondary, #666); - min-width: 120px; -} - -.hf-logo-row input[type="file"] { - flex: 1; - padding: 6px 10px; - border: 1px solid var(--border-color, #ccc); - border-radius: 4px; - background: var(--input-bg, white); - color: var(--text-primary, #333); - font-size: 13px; - cursor: pointer; -} - -.hf-logo-row input[type="file"]::-webkit-file-upload-button { - padding: 5px 12px; - background: var(--button-bg, #6c757d); - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - margin-right: 10px; -} - -.hf-logo-row input[type="file"]::-webkit-file-upload-button:hover { - background: var(--button-hover, #5a6268); -} - -.browse-btn { - padding: 6px 12px; - background: var(--accent-color, #007bff); - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 13px; - transition: background 0.2s ease; - margin-right: 8px; -} - -.browse-btn:hover { - background: var(--accent-hover, #0056b3); -} - -.clear-btn { - padding: 6px 12px; - background: var(--danger-color, #dc3545); - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 13px; - transition: background 0.2s ease; -} - -.clear-btn:hover { - background: var(--danger-hover, #c82333); -} - -.logo-preview { - font-size: 12px; - color: var(--text-secondary, #666); - max-width: 150px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.hf-help { - margin-top: 20px; - padding: 15px; - background: var(--info-bg, #e7f3ff); - border-left: 4px solid var(--info-color, #007bff); - border-radius: 4px; -} - -.hf-help p { - margin: 0 0 10px 0; - font-weight: 600; - color: var(--text-primary, #333); -} - -.hf-help ul { - margin: 0; - padding-left: 20px; - list-style-type: none; -} - -.hf-help li { - margin-bottom: 6px; - color: var(--text-secondary, #555); - font-size: 13px; -} - -.hf-help code { - background: rgba(0, 123, 255, 0.1); - padding: 2px 6px; - border-radius: 3px; - font-family: 'Courier New', monospace; - color: var(--accent-color, #007bff); - font-weight: 600; -} - -/* Field Picker Dialog */ -.field-picker-list { - display: grid; - grid-template-columns: 1fr; - gap: 8px; -} - -.field-option { - padding: 12px 16px; - background: var(--bg-secondary, #f5f5f5); - border: 1px solid var(--border-color, #ddd); - border-radius: 6px; - text-align: left; - cursor: pointer; - font-size: 14px; - color: var(--text-primary, #333); - transition: all 0.2s ease; -} - -.field-option:hover { - background: var(--accent-color, #007bff); - color: white; - border-color: var(--accent-color, #007bff); - transform: translateX(5px); -} - -/* Dark Theme Support for Header/Footer Dialog */ -body[data-theme="dark"] .hf-enable-section, -body[data-theme="dark"] .hf-logo-row { - background: #2d2d2d; -} - -body[data-theme="dark"] .hf-section { - background: #252525; - border-color: #404040; -} - -body[data-theme="dark"] .hf-column input[type="text"], -body[data-theme="dark"] .hf-logo-row input[type="file"] { - background: #2d2d2d; - color: #e0e0e0; - border-color: #404040; -} - -body[data-theme="dark"] .hf-help { - background: rgba(0, 123, 255, 0.15); - border-left-color: #0d6efd; -} - -body[data-theme="dark"] .field-option { - background: #2d2d2d; - border-color: #404040; - color: #e0e0e0; -} - -body[data-theme="dark"] .field-option:hover { - background: #0d6efd; - color: white; -} -/* Mermaid Diagram Styles */ -.mermaid { - background: #f9f9f9; - padding: 20px; - border-radius: 8px; - margin: 20px 0; - text-align: center; - border: 1px solid #e0e0e0; -} - -.mermaid svg { - max-width: 100%; - height: auto; -} - -body.theme-dark .mermaid { - background: #2d2d2d; - border-color: #444; -} - -body.theme-solarized .mermaid { - background: #fdf6e3; - border-color: #eee8d5; -} - -body.theme-monokai .mermaid { - background: #2f2f2f; - border-color: #49483e; -} - -body.theme-github .mermaid { - background: #f6f8fa; - border-color: #e1e4e8; -} - -/* Command Palette Styles */ -.command-palette { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.7); - display: flex; - align-items: flex-start; - justify-content: center; - padding-top: 100px; - z-index: 10000; -} - -.command-palette.hidden { - display: none; -} - -.command-palette-content { - background: #fff; - border-radius: 12px; - width: 600px; - max-width: 90%; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); - overflow: hidden; -} - -#command-search { - width: 100%; - padding: 20px; - font-size: 18px; - border: none; - border-bottom: 1px solid #e0e0e0; - outline: none; -} - -.command-list { - max-height: 400px; - overflow-y: auto; -} - -.command-item { - padding: 12px 20px; - cursor: pointer; - display: flex; - justify-content: space-between; - align-items: center; - transition: background 0.15s; -} - -.command-item:hover, -.command-item.selected { - background: #f0f0f0; -} - -.command-name { - font-weight: 500; -} - -.command-shortcut { - font-size: 12px; - color: #666; - background: #f5f5f5; - padding: 3px 8px; - border-radius: 3px; -} - -.command-palette-footer { - padding: 12px 20px; - background: #f9f9f9; - border-top: 1px solid #e0e0e0; - display: flex; - gap: 20px; - font-size: 12px; - color: #666; -} - -/* Dark theme support for command palette */ -body.theme-dark .command-palette-content { - background: #2d2d2d; - color: #f0f0f0; -} - -body.theme-dark #command-search { - background: #2d2d2d; - color: #f0f0f0; - border-color: #444; -} - -body.theme-dark .command-item:hover, -body.theme-dark .command-item.selected { - background: #3d3d3d; -} - -body.theme-dark .command-shortcut { - background: #3d3d3d; - color: #ccc; -} - -body.theme-dark .command-palette-footer { - background: #252525; - border-color: #444; - color: #999; -} +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + overflow: hidden; + height: 100vh; +} + +.hidden { + display: none !important; +} + +.container { + display: flex; + flex-direction: column; + height: 100vh; +} + +/* Tab Bar */ +.tab-bar { + display: flex; + align-items: center; + background: #f0f0f0; + border-bottom: 1px solid #ddd; + padding: 0 8px; + min-height: 36px; +} + +.tab { + display: flex; + align-items: center; + padding: 6px 12px; + background: #e8e8e8; + border: 1px solid #ccc; + border-bottom: none; + border-radius: 6px 6px 0 0; + margin-right: 2px; + cursor: pointer; + user-select: none; + max-width: 200px; + min-width: 120px; +} + +.tab.active { + background: #fff; + border-color: #999; + z-index: 1; +} + +.tab-title { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 13px; +} + +.tab-close { + background: none; + border: none; + font-size: 16px; + font-weight: bold; + cursor: pointer; + padding: 0; + margin-left: 8px; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 3px; + color: #666; +} + +.tab-close:hover { + background: #ddd; + color: #000; +} + +.new-tab-button { + background: none; + border: 1px solid #ccc; + border-radius: 4px; + padding: 4px 8px; + cursor: pointer; + font-size: 14px; + margin-left: 8px; + color: #666; +} + +.new-tab-button:hover { + background: #e8e8e8; +} + +/* Tab Content */ +.tab-content { + display: flex; + flex: 1; + overflow: hidden; +} + +.tab-content:not(.active) { + display: none; +} + +/* Toolbar */ +.toolbar { + display: flex; + align-items: center; + padding: 8px 12px; + background: #f5f5f5; + border-bottom: 1px solid #ddd; + gap: 4px; +} + +.toolbar button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 1px solid transparent; + background: transparent; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; +} + +.toolbar button:hover { + background: #e0e0e0; + border-color: #ccc; +} + +.toolbar button:active { + background: #d0d0d0; +} + +.toolbar-separator { + width: 1px; + height: 24px; + background: #ccc; + margin: 0 8px; +} + +/* Editor Container */ +.editor-container { + display: flex; + flex: 1; + overflow: hidden; +} + +.pane { + flex: 1; + overflow: auto; + min-width: 0; +} + +.pane:first-child { + border-right: none; +} + +/* Pane Resizer */ +.pane-resizer { + width: 6px; + background: #e0e0e0; + cursor: col-resize; + position: relative; + flex-shrink: 0; + transition: background 0.2s; +} + +.pane-resizer:hover { + background: #999; +} + +.pane-resizer:active { + background: #666; +} + +/* Preview Header */ +.preview-header { + display: flex; + justify-content: flex-end; + align-items: center; + padding: 4px 8px; + background: #f6f8fa; + border-bottom: 1px solid #dfe2e5; + min-height: 32px; +} + +.preview-popout-btn { + background: transparent; + border: none; + border-radius: 3px; + padding: 2px 6px; + cursor: pointer; + font-size: 11px; + color: #8c959f; + opacity: 0.5; + transition: all 0.2s; +} + +.preview-popout-btn:hover { + background: rgba(0, 0, 0, 0.05); + opacity: 1; + color: #24292f; +} + +.editor-textarea { + width: 100%; + height: 100%; + padding: 20px; + font-family: 'SF Mono', Monaco, 'Courier New', monospace; + font-size: 15px; + line-height: 1.6; + border: none; + outline: none; + resize: none; +} + +.pane:last-child { + padding: 0; + background: #fff; +} + +.preview-content { + max-width: none; + margin: 0; + padding: 20px 24px 24px 24px; + line-height: 1.6; + font-size: 15px; + font-size: 14px; +} + +/* Legacy support for old selectors */ +#editor-pane { + border-right: 1px solid #ddd; +} + +#editor { + width: 100%; + height: 100%; + padding: 20px; + font-family: 'SF Mono', Monaco, 'Courier New', monospace; + font-size: 15px; + line-height: 1.6; + border: none; + outline: none; + resize: none; +} + +#preview-pane { + padding: 20px; + background: #fff; +} + +/* GitHub Light Theme for Preview (Default) */ +#preview, [id^="preview-"], .preview-content { + max-width: 800px; + margin: 0 auto; + font-size: 15px; + padding: 20px; + background: #ffffff; + color: #24292f; + line-height: 1.6; +} + +/* Status Bar */ +.status-bar { + display: flex; + justify-content: space-between; + padding: 4px 12px; + background: #f5f5f5; + border-top: 1px solid #ddd; + font-size: 12px; + color: #666; +} + +/* Preview Styles */ +#preview h1, #preview h2, #preview h3, #preview h4, #preview h5, #preview h6, +.preview-content h1, .preview-content h2, .preview-content h3, .preview-content h4, .preview-content h5, .preview-content h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; +} + +#preview h1, .preview-content h1 { + font-size: 2em; + border-bottom: 1px solid #eaecef; + padding-bottom: 0.3em; + margin-top: 0; +} + +#preview h2, .preview-content h2 { + font-size: 1.5em; + border-bottom: 1px solid #eaecef; + padding-bottom: 0.3em; +} + +#preview p, .preview-content p { + margin-bottom: 16px; + line-height: 1.6; +} + +#preview code, .preview-content code, [id^="preview-"] code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: #f6f8fa; + border: 1px solid #d0d7de; + border-radius: 6px; + font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; + color: #24292f; +} + +#preview pre, .preview-content pre, [id^="preview-"] pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #f6f8fa; + border: 1px solid #d0d7de; + border-radius: 6px; + margin-bottom: 16px; +} + +#preview pre code, .preview-content pre code { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; +} + +#preview blockquote, .preview-content blockquote { + padding: 0 1em; + color: #6a737d; + border-left: 0.25em solid #dfe2e5; + margin-bottom: 16px; +} + +/* GitHub Dark Theme for Preview */ +body.theme-dark #preview, +body.theme-dark [id^="preview-"], +body.theme-dark .preview-content { + background: #0d1117; + color: #c9d1d9; +} + +body.theme-dark #preview h1, +body.theme-dark [id^="preview-"] h1, +body.theme-dark .preview-content h1 { + color: #c9d1d9; + border-bottom-color: #21262d; +} + +body.theme-dark #preview h2, +body.theme-dark [id^="preview-"] h2, +body.theme-dark .preview-content h2 { + color: #c9d1d9; + border-bottom-color: #21262d; +} + +body.theme-dark #preview h3, +body.theme-dark #preview h4, +body.theme-dark #preview h5, +body.theme-dark #preview h6, +body.theme-dark [id^="preview-"] h3, +body.theme-dark [id^="preview-"] h4, +body.theme-dark [id^="preview-"] h5, +body.theme-dark [id^="preview-"] h6, +body.theme-dark .preview-content h3, +body.theme-dark .preview-content h4, +body.theme-dark .preview-content h5, +body.theme-dark .preview-content h6 { + color: #c9d1d9; +} + +body.theme-dark #preview p, +body.theme-dark [id^="preview-"] p, +body.theme-dark .preview-content p { + color: #c9d1d9; +} + +body.theme-dark #preview code, +body.theme-dark [id^="preview-"] code, +body.theme-dark .preview-content code { + background-color: #161b22; + border-color: #30363d; + color: #c9d1d9; +} + +body.theme-dark #preview pre, +body.theme-dark [id^="preview-"] pre, +body.theme-dark .preview-content pre { + background-color: #161b22; + border-color: #30363d; +} + +body.theme-dark #preview blockquote, +body.theme-dark [id^="preview-"] blockquote, +body.theme-dark .preview-content blockquote { + color: #8b949e; + border-left-color: #3b434b; +} + +body.theme-dark #preview a, +body.theme-dark [id^="preview-"] a, +body.theme-dark .preview-content a { + color: #58a6ff; +} + +body.theme-dark #preview a:hover, +body.theme-dark [id^="preview-"] a:hover, +body.theme-dark .preview-content a:hover { + color: #79c0ff; +} + +body.theme-dark #preview table, +body.theme-dark [id^="preview-"] table, +body.theme-dark .preview-content table { + border-color: #30363d; +} + +body.theme-dark #preview table th, +body.theme-dark [id^="preview-"] table th, +body.theme-dark .preview-content table th { + background-color: #161b22; + color: #c9d1d9; + border-color: #30363d; +} + +body.theme-dark #preview table td, +body.theme-dark [id^="preview-"] table td, +body.theme-dark .preview-content table td { + border-color: #30363d; + color: #c9d1d9; +} + +body.theme-dark #preview hr, +body.theme-dark [id^="preview-"] hr, +body.theme-dark .preview-content hr { + background-color: #21262d; + border-color: #21262d; +} + +body.theme-dark #preview ul, +body.theme-dark #preview ol, +body.theme-dark [id^="preview-"] ul, +body.theme-dark [id^="preview-"] ol, +body.theme-dark .preview-content ul, +body.theme-dark .preview-content ol { + color: #c9d1d9; +} + +body.theme-dark #preview li, +body.theme-dark [id^="preview-"] li, +body.theme-dark .preview-content li { + color: #c9d1d9; +} + +#preview ul, #preview ol, .preview-content ul, .preview-content ol { + padding-left: 2em; + margin-bottom: 16px; +} + +#preview li, .preview-content li { + margin-bottom: 4px; +} + +#preview a, .preview-content a { + color: #0366d6; + text-decoration: none; +} + +#preview a:hover, .preview-content a:hover { + text-decoration: underline; +} + +#preview img, .preview-content img { + max-width: 100%; + height: auto; +} + +#preview table, .preview-content table { + border-collapse: collapse; + margin-bottom: 16px; + width: 100%; +} + +#preview table th, #preview table td, +.preview-content table th, .preview-content table td { + padding: 6px 13px; + border: 1px solid #dfe2e5; +} + +#preview table th, .preview-content table th { + font-weight: 600; + background-color: #f6f8fa; +} + +#preview table tr:nth-child(2n), .preview-content table tr:nth-child(2n) { + background-color: #ffffff; +} + +/* Theme: Dark */ +body.theme-dark { + background: #1e1e1e; + color: #d4d4d4; +} + +body.theme-dark .tab-bar { + background: #2d2d30; + border-bottom-color: #3e3e42; +} + +body.theme-dark .tab { + background: #3c3c3c; + border-color: #555; + color: #d4d4d4; +} + +body.theme-dark .tab.active { + background: #1e1e1e; + border-color: #666; +} + +body.theme-dark .tab-close { + color: #ccc; +} + +body.theme-dark .tab-close:hover { + background: #555; + color: #fff; +} + +body.theme-dark .new-tab-button { + border-color: #555; + color: #ccc; +} + +body.theme-dark .new-tab-button:hover { + background: #3c3c3c; +} + +body.theme-dark .toolbar { + background: #2d2d30; + border-bottom-color: #3e3e42; +} + +body.theme-dark .toolbar button { + color: #cccccc; +} + +body.theme-dark .toolbar button:hover { + background: #3e3e42; + border-color: #464647; +} + +body.theme-dark .toolbar-separator { + background: #3e3e42; +} + +body.theme-dark .pane:first-child { + border-right-color: #3e3e42; +} + +body.theme-dark .editor-textarea { + background: #1e1e1e; + color: #d4d4d4; +} + +body.theme-dark .pane:last-child { + background: #1e1e1e; +} + +/* Legacy support */ +body.theme-dark #editor-pane { + border-right-color: #3e3e42; +} + +body.theme-dark #editor { + background: #1e1e1e; + color: #d4d4d4; +} + +body.theme-dark #preview-pane { + background: #252526; +} + +body.theme-dark #preview, body.theme-dark .preview-content { + color: #d4d4d4; +} + +body.theme-dark #preview h1, body.theme-dark #preview h2, +body.theme-dark .preview-content h1, body.theme-dark .preview-content h2 { + border-bottom-color: #3e3e42; +} + +body.theme-dark #preview code, body.theme-dark .preview-content code { + background-color: rgba(255,255,255,0.1); +} + +body.theme-dark #preview pre, body.theme-dark .preview-content pre { + background-color: #2d2d30; +} + +body.theme-dark #preview blockquote, body.theme-dark .preview-content blockquote { + color: #808080; + border-left-color: #3e3e42; +} + +body.theme-dark #preview a, body.theme-dark .preview-content a { + color: #569cd6; +} + +body.theme-dark #preview table th, body.theme-dark #preview table td, +body.theme-dark .preview-content table th, body.theme-dark .preview-content table td { + border-color: #3e3e42; +} + + +body.theme-dark #preview table tr:nth-child(2n), body.theme-dark .preview-content table tr:nth-child(2n) { + background-color: #2d2d30; +} + +body.theme-dark .status-bar { + background: #2d2d30; + border-top-color: #3e3e42; + color: #969696; +} + +/* Theme: Solarized */ +body.theme-solarized { + background: #fdf6e3; + color: #657b83; +} + +body.theme-solarized .tab-bar { + background: #eee8d5; + border-bottom-color: #93a1a1; +} + +body.theme-solarized .tab { + background: #fdf6e3; + border-color: #93a1a1; + color: #657b83; +} + +body.theme-solarized .tab.active { + background: #fdf6e3; + border-color: #859900; +} + +body.theme-solarized .tab-close { + color: #657b83; +} + +body.theme-solarized .tab-close:hover { + background: #93a1a1; + color: #dc322f; +} + +body.theme-solarized .new-tab-button { + border-color: #93a1a1; + color: #657b83; +} + +body.theme-solarized .new-tab-button:hover { + background: #eee8d5; +} + +body.theme-solarized .toolbar { + background: #eee8d5; + border-bottom-color: #93a1a1; +} + +body.theme-solarized .toolbar button { + color: #586e75; +} + +body.theme-solarized .toolbar button:hover { + background: #fdf6e3; + border-color: #93a1a1; +} + +body.theme-solarized #editor { + background: #fdf6e3; + color: #657b83; +} + +body.theme-solarized #preview, body.theme-solarized .preview-content { + color: #586e75; +} + +body.theme-solarized #preview code, body.theme-solarized .preview-content code { + background-color: #eee8d5; +} + +body.theme-solarized #preview pre, body.theme-solarized .preview-content pre { + background-color: #eee8d5; +} + +body.theme-solarized #preview a, body.theme-solarized .preview-content a { + color: #268bd2; +} + +/* Theme: Monokai */ +body.theme-monokai { + background: #272822; + color: #f8f8f2; +} + +body.theme-monokai .tab-bar { + background: #3e3d32; + border-bottom-color: #75715e; +} + +body.theme-monokai .tab { + background: #49483e; + border-color: #75715e; + color: #f8f8f2; +} + +body.theme-monokai .tab.active { + background: #272822; + border-color: #a6e22e; +} + +body.theme-monokai .tab-close { + color: #f8f8f2; +} + +body.theme-monokai .tab-close:hover { + background: #75715e; + color: #f92672; +} + +body.theme-monokai .new-tab-button { + border-color: #75715e; + color: #f8f8f2; +} + +body.theme-monokai .new-tab-button:hover { + background: #49483e; +} + +body.theme-monokai .toolbar { + background: #3e3d32; + border-bottom-color: #75715e; +} + +body.theme-monokai .toolbar button { + color: #f8f8f2; +} + +body.theme-monokai .toolbar button:hover { + background: #49483e; + border-color: #75715e; +} + +body.theme-monokai #editor { + background: #272822; + color: #f8f8f2; +} + +body.theme-monokai .editor-textarea { + background: #272822; + color: #f8f8f2; +} + +body.theme-monokai #preview-pane { + background: #272822; +} + +body.theme-monokai #preview, body.theme-monokai .preview-content { + color: #f8f8f2; +} + +body.theme-monokai #preview h1, body.theme-monokai #preview h2, +body.theme-monokai .preview-content h1, body.theme-monokai .preview-content h2 { + border-bottom-color: #75715e; +} + +body.theme-monokai #preview code, body.theme-monokai .preview-content code { + background-color: #3e3d32; +} + +body.theme-monokai #preview pre, body.theme-monokai .preview-content pre { + background-color: #3e3d32; +} + +body.theme-monokai #preview blockquote, body.theme-monokai .preview-content blockquote { + color: #75715e; + border-left-color: #75715e; +} + +body.theme-monokai #preview a, body.theme-monokai .preview-content a { + color: #66d9ef; +} + +body.theme-monokai .status-bar { + background: #3e3d32; + border-top-color: #75715e; + color: #75715e; +} + +/* Theme: GitHub */ +body.theme-github { + background: #fff; + color: #24292e; +} + +body.theme-github .tab-bar { + background: #fafbfc; + border-bottom-color: #e1e4e8; +} + +body.theme-github .tab { + background: #f6f8fa; + border-color: #e1e4e8; + color: #24292e; +} + +body.theme-github .tab.active { + background: #fff; + border-color: #0366d6; +} + +body.theme-github .tab-close { + color: #586069; +} + +body.theme-github .tab-close:hover { + background: #e1e4e8; + color: #cb2431; +} + +body.theme-github .new-tab-button { + border-color: #e1e4e8; + color: #586069; +} + +body.theme-github .new-tab-button:hover { + background: #f6f8fa; +} + +body.theme-github .toolbar { + background: #fafbfc; + border-bottom-color: #e1e4e8; +} + +body.theme-github .toolbar button { + color: #586069; +} + +body.theme-github .toolbar button:hover { + background: #f6f8fa; + border-color: #e1e4e8; +} + +body.theme-github #editor { + background: #fff; + color: #24292e; +} + +body.theme-github #preview, body.theme-github .preview-content { + color: #24292e; +} + +body.theme-github #preview h1, body.theme-github #preview h2, +body.theme-github .preview-content h1, body.theme-github .preview-content h2 { + border-bottom-color: #eaecef; +} + +body.theme-github #preview code, body.theme-github .preview-content code { + background-color: rgba(27,31,35,0.05); +} + +body.theme-github #preview pre, body.theme-github .preview-content pre { + background-color: #f6f8fa; +} + +body.theme-github #preview blockquote, body.theme-github .preview-content blockquote { + color: #6a737d; + border-left-color: #dfe2e5; +} + +body.theme-github #preview a, body.theme-github .preview-content a { + color: #0366d6; +} + +body.theme-github .status-bar { + background: #fafbfc; + border-top-color: #e1e4e8; + color: #586069; +} + +/* Hide preview pane by default */ +.pane.hidden { + display: none; +} + +.pane.full-width { + border-right: none; +} + +/* Legacy support */ +#preview-pane.hidden { + display: none; +} + +#editor-pane.full-width { + border-right: none; +} + +/* Find & Replace Dialog */ +.find-dialog { + position: absolute; + top: 60px; + right: 20px; + background: #fff; + border: 1px solid #ddd; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + z-index: 1000; + min-width: 400px; + padding: 12px; +} + +.find-dialog.hidden { + display: none; +} + +.find-controls { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 8px; +} + +.find-controls input { + flex: 1; + padding: 6px 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 13px; +} + +.find-controls button { + padding: 6px 10px; + border: 1px solid #ddd; + background: #f5f5f5; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + white-space: nowrap; +} + +.find-controls button:hover { + background: #e5e5e5; +} + +.find-info { + font-size: 12px; + color: #666; + text-align: right; +} + +/* Editor Wrapper with Line Numbers */ +.editor-wrapper { + position: relative; + display: flex; + height: 100%; +} + +.line-numbers { + min-width: 40px; + padding: 8px 4px; + background: #f8f8f8; + border-right: 1px solid #ddd; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; + line-height: 1.4; + color: #666; + text-align: right; + user-select: none; + overflow: hidden; +} + +.line-numbers.hidden { + display: none; +} + +.line-numbers .line-number { + display: block; + white-space: nowrap; +} + +/* Adjust editor when line numbers are shown */ +.editor-wrapper #editor { + flex: 1; +} + +/* Export Options Dialog */ +.export-dialog { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.export-dialog.hidden { + display: none; +} + +.export-dialog-content { + background: #fff; + border-radius: 6px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + width: 85%; + max-width: 450px; + max-height: 80vh; + overflow-y: auto; + position: relative; + font-size: 12px; +} + +.export-dialog-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid #eee; +} + +.export-dialog-header h3 { + margin: 0; + font-size: 14px; + font-weight: 600; +} + +.export-dialog-body { + padding: 14px 16px; +} + +.export-section { + margin-bottom: 14px; +} + +.export-section label { + display: block; + font-weight: 500; + margin-bottom: 4px; + color: #333; + font-size: 12px; +} + +.export-section label.checkbox-inline { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + font-weight: 400; +} + +.export-section label.checkbox-inline input[type="checkbox"] { + width: auto; + margin: 0; +} + +/* PDF Editor buttons */ +#add-merge-file { + padding: 5px 10px; + font-size: 12px; + background: #4a90d9; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; +} + +#add-merge-file:hover { + background: #357abd; +} + +.export-section select, +.export-section input[type="text"], +.export-section input[type="number"] { + width: 100%; + padding: 5px 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 12px; +} + +.export-section select:focus, +.export-section input:focus { + outline: none; + border-color: #007acc; + box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.1); +} + +.metadata-container { + border: 1px solid #eee; + border-radius: 4px; + padding: 12px; + margin-bottom: 8px; +} + +.metadata-field { + display: flex; + gap: 8px; + margin-bottom: 8px; +} + +.metadata-field:last-child { + margin-bottom: 0; +} + +.metadata-key { + flex: 0 0 120px; + font-weight: 600; +} + +.metadata-value { + flex: 1; +} + +.checkbox-group { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 12px; +} + +.checkbox-group label { + display: flex; + align-items: center; + gap: 8px; + font-weight: normal; + margin-bottom: 0; +} + +.checkbox-group input[type="checkbox"] { + margin: 0; +} + +.form-row { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} + +.form-row label { + flex: 0 0 120px; + margin-bottom: 0; +} + +.form-row input, +.form-row select { + flex: 1; +} + +.form-row button { + flex: 0 0 auto; + padding: 8px 16px; + background: #f5f5f5; + border: 1px solid #ddd; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +.form-row button:hover { + background: #e8e8e8; +} + +.pdf-only { + display: none; +} + +.export-dialog[data-format="pdf"] .pdf-only { + display: block; +} + +.export-dialog-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 16px 20px; + border-top: 1px solid #eee; + background: #f8f9fa; + border-radius: 0 0 8px 8px; +} + +.export-dialog-footer button { + padding: 10px 20px; + border: 1px solid #ddd; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: 500; +} + +.export-dialog-footer button.primary { + background: #007acc; + color: white; + border-color: #007acc; +} + +.export-dialog-footer button.primary:hover { + background: #005fa3; + border-color: #005fa3; +} + +.export-dialog-footer button:not(.primary) { + background: #f5f5f5; +} + +.export-dialog-footer button:not(.primary):hover { + background: #e8e8e8; +} + +#export-dialog-close { + background: none; + border: none; + font-size: 20px; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; +} + +#export-dialog-close:hover { + background: #f0f0f0; +} + +#add-metadata-field { + padding: 6px 12px; + background: #f8f9fa; + border: 1px solid #ddd; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + color: #666; +} + +#add-metadata-field:hover { + background: #e9ecef; +} + +/* Theme support for export dialog */ +body.theme-dark .export-dialog-content { + background: #2d2d2d; + color: #f0f0f0; +} + +body.theme-dark .export-dialog-header { + border-color: #444; +} + +body.theme-dark .export-dialog-footer { + background: #333; + border-color: #444; +} + +body.theme-dark .export-section label { + color: #f0f0f0; +} + +body.theme-dark .export-section select, +body.theme-dark .export-section input { + background: #1a1a1a; + border-color: #555; + color: #f0f0f0; +} + +body.theme-dark .metadata-container { + border-color: #444; + background: #1a1a1a; +} + +body.theme-dark .form-row button, +body.theme-dark #add-metadata-field { + background: #1a1a1a; + border-color: #555; + color: #f0f0f0; +} + +/* PDF Editor Specific Styles */ +.pdf-editor-content { + max-width: 450px !important; + max-height: 75vh; + overflow-y: auto; + font-size: 12px; +} + +.pdf-editor-body { + min-height: 200px; + max-height: calc(85vh - 150px); + overflow-y: auto; +} + +.pdf-operation-section { + animation: fadeIn 0.3s ease-in; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.pdf-operation-section.hidden { + display: none !important; +} + +/* Make PDF Editor sections more compact */ +.pdf-operation-section .export-section { + margin-bottom: 18px; +} + +.pdf-operation-section .export-section:last-child { + margin-bottom: 0; +} + +/* File list styling for merge operation */ +.file-list { + border: 1px solid #ddd; + border-radius: 4px; + padding: 10px; + margin-bottom: 10px; + min-height: 100px; + max-height: 200px; + overflow-y: auto; + background: #fafafa; +} + +.file-entry { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px; + margin-bottom: 6px; + background: white; + border: 1px solid #e0e0e0; + border-radius: 4px; +} + +.file-entry:last-child { + margin-bottom: 0; +} + +.file-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-right: 10px; + font-size: 13px; +} + +.remove-file { + padding: 4px 10px; + background: #dc3545; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; +} + +.remove-file:hover { + background: #c82333; +} + +/* Theme support for PDF Editor */ +body.theme-dark .file-list { + background: #1a1a1a; + border-color: #555; +} + +body.theme-dark .file-entry { + background: #2d2d2d; + border-color: #444; + color: #f0f0f0; +} + +body.theme-dark .pdf-editor-content { + background: #2d2d2d; +} + +body.theme-dark .pdf-editor-body { + background: #2d2d2d; +} + +body.theme-dark .form-row button:hover, +body.theme-dark #add-metadata-field:hover { + background: #333; +} + +body.theme-dark #export-dialog-close:hover { + background: #444; +} + +/* Batch Conversion Dialog */ +.batch-dialog { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.batch-dialog.hidden { + display: none; +} + +.batch-dialog-content { + background: #fff; + border-radius: 6px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + width: 85%; + max-width: 450px; + max-height: 75vh; + overflow-y: auto; + font-size: 12px; +} + +.batch-dialog-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid #eee; +} + +.batch-dialog-header h3 { + margin: 0; + font-size: 18px; + font-weight: 600; +} + +.batch-dialog-body { + padding: 20px; +} + +.batch-section { + margin-bottom: 20px; +} + +.batch-section label { + display: block; + font-weight: 600; + margin-bottom: 8px; + color: #333; +} + +.batch-section select, +.batch-section input[type="text"] { + width: 100%; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; +} + +.folder-input-group { + display: flex; + gap: 8px; +} + +.folder-input-group input { + flex: 1; +} + +.folder-input-group button { + padding: 8px 16px; + background: #f5f5f5; + border: 1px solid #ddd; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + white-space: nowrap; +} + +.folder-input-group button:hover { + background: #e8e8e8; +} + +.batch-section label input[type="checkbox"] { + margin-right: 8px; +} + +#batch-show-options { + padding: 8px 16px; + background: #f8f9fa; + border: 1px solid #ddd; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + color: #666; +} + +#batch-show-options:hover { + background: #e9ecef; +} + +.batch-progress { + border: 1px solid #ddd; + border-radius: 4px; + padding: 16px; + background: #f8f9fa; +} + +.progress-bar { + width: 100%; + height: 8px; + background: #e0e0e0; + border-radius: 4px; + overflow: hidden; + margin-bottom: 8px; +} + +.progress-fill { + height: 100%; + background: #007acc; + transition: width 0.3s ease; + width: 0%; +} + +.progress-text { + display: flex; + justify-content: space-between; + font-size: 14px; + color: #666; +} + +.batch-dialog-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 16px 20px; + border-top: 1px solid #eee; + background: #f8f9fa; + border-radius: 0 0 8px 8px; +} + +.batch-dialog-footer button { + padding: 10px 20px; + border: 1px solid #ddd; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: 500; +} + +.batch-dialog-footer button.primary { + background: #007acc; + color: white; + border-color: #007acc; +} + +.batch-dialog-footer button.primary:hover:not(:disabled) { + background: #005fa3; + border-color: #005fa3; +} + +.batch-dialog-footer button.primary:disabled { + background: #ccc; + border-color: #ccc; + cursor: not-allowed; +} + +.batch-dialog-footer button:not(.primary) { + background: #f5f5f5; +} + +.batch-dialog-footer button:not(.primary):hover { + background: #e8e8e8; +} + +#batch-dialog-close { + background: none; + border: none; + font-size: 20px; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; +} + +#batch-dialog-close:hover { + background: #f0f0f0; +} + +/* Theme support for batch dialog */ +body.theme-dark .batch-dialog-content { + background: #2d2d2d; + color: #f0f0f0; +} + +body.theme-dark .batch-dialog-header { + border-color: #444; +} + +body.theme-dark .batch-dialog-footer { + background: #333; + border-color: #444; +} + +body.theme-dark .batch-section label { + color: #f0f0f0; +} + +body.theme-dark .batch-section select, +body.theme-dark .batch-section input { + background: #1a1a1a; + border-color: #555; + color: #f0f0f0; +} + +body.theme-dark .folder-input-group button, +body.theme-dark #batch-show-options { + background: #1a1a1a; + border-color: #555; + color: #f0f0f0; +} + +body.theme-dark .folder-input-group button:hover, +body.theme-dark #batch-show-options:hover { + background: #333; +} + +body.theme-dark .batch-progress { + background: #1a1a1a; + border-color: #444; +} + +body.theme-dark .progress-text { + color: #ccc; +} + +body.theme-dark #batch-dialog-close:hover { + background: #444; +} + +/* Theme support for find dialog */ +body.theme-dark .find-dialog { + background: #2d2d2d; + border-color: #555; + color: #f0f0f0; +} + +body.theme-dark .find-controls input { + background: #333; + border-color: #555; + color: #f0f0f0; +} + +body.theme-dark .find-controls button { + background: #404040; + border-color: #555; + color: #f0f0f0; +} + +body.theme-dark .find-controls button:hover { + background: #4a4a4a; +} + +body.theme-dark .line-numbers { + background: #2a2a2a; + border-right-color: #555; + color: #888; +} + +/* Additional theme support for line numbers */ +body.theme-solarized .line-numbers { + background: #fdf6e3; + border-right-color: #eee8d5; + color: #93a1a1; +} + +body.theme-monokai .line-numbers { + background: #2f2f2f; + border-right-color: #555; + color: #75715e; +} + +body.theme-github .line-numbers { + background: #fafbfc; + border-right-color: #e1e4e8; + color: #586069; +} + +/* Export Profiles Styles */ +.export-profiles { + border-bottom: 1px solid #ddd; + padding-bottom: 15px; + margin-bottom: 15px; +} + +.profile-controls { + display: flex; + gap: 8px; + align-items: center; + margin-top: 8px; +} + +.profile-controls select { + flex: 1; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; +} + +.profile-controls button { + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + background: #f5f5f5; + cursor: pointer; + font-size: 13px; + transition: all 0.2s; +} + +.profile-controls button:hover { + background: #e8e8e8; +} + +/* Advanced Export Toggle Styles */ +.export-mode-toggle { + border-bottom: 1px solid #ddd; + padding-bottom: 15px; + margin-bottom: 20px; +} + +.checkbox-label { + display: flex; + align-items: center; + cursor: pointer; + font-weight: 500; + margin-bottom: 5px; +} + +.checkbox-label input[type="checkbox"] { + margin-right: 10px; + transform: scale(1.2); +} + +.export-help { + color: #666; + font-size: 12px; + font-style: italic; + margin-left: 25px; +} + +.advanced-options { + transition: all 0.3s ease; + overflow: visible; + position: relative; + z-index: 10; +} + +.advanced-options.hidden { + display: none; +} + +.basic-options { + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 6px; + padding: 15px; + margin-top: 10px; +} + +.basic-options label { + font-weight: normal; + margin-bottom: 8px; +} + +/* Theme support for advanced export toggle */ +body.theme-dark .export-mode-toggle { + border-color: #444; +} + +body.theme-dark .export-help { + color: #999; +} + +body.theme-dark .basic-options { + background: #1a1a1a; + border-color: #444; +} + +body.theme-dark .checkbox-label { + color: #f0f0f0; +} + +body.theme-solarized .export-mode-toggle { + border-color: #eee8d5; +} + +body.theme-solarized .export-help { + color: #586e75; +} + +body.theme-solarized .basic-options { + background: #fdf6e3; + border-color: #eee8d5; +} + +body.theme-monokai .export-mode-toggle { + border-color: #49483e; +} + +body.theme-monokai .export-help { + color: #75715e; +} + +body.theme-monokai .basic-options { + background: #2f2f2f; + border-color: #49483e; +} + +body.theme-github .export-mode-toggle { + border-color: #e1e4e8; +} + +body.theme-github .export-help { + color: #586069; +} + +body.theme-github .basic-options { + background: #f6f8fa; + border-color: #e1e4e8; +} + +/* Enhanced Statistics Styles */ +.enhanced-stats { + font-size: 11px; + color: #666; + padding: 2px 0; + border-top: 1px solid #ddd; + margin-top: 2px; + font-family: monospace; +} + +/* Theme support for enhanced stats */ +body.theme-dark .enhanced-stats { + color: #999; + border-color: #444; +} + +body.theme-solarized .enhanced-stats { + color: #586e75; + border-color: #eee8d5; +} + +body.theme-monokai .enhanced-stats { + color: #75715e; + border-color: #49483e; +} + +body.theme-github .enhanced-stats { + color: #586069; + border-color: #e1e4e8; +} + +/* Auto-save indicator */ +.auto-save-indicator { + position: fixed; + top: 20px; + right: 20px; + background: #28a745; + color: white; + padding: 8px 16px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + z-index: 10000; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); + animation: slideInRight 0.3s ease-out; +} + +.auto-save-indicator.fade-out { + animation: fadeOut 0.3s ease-out forwards; +} + +@keyframes slideInRight { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +/* Theme: Dracula */ +body.theme-dracula { + background: #282a36; + color: #f8f8f2; +} + +body.theme-dracula .tab-bar { + background: #343746; + border-bottom-color: #44475a; +} + +body.theme-dracula .tab { + background: #44475a; + border-color: #6272a4; + color: #f8f8f2; +} + +body.theme-dracula .tab.active { + background: #282a36; + border-color: #bd93f9; +} + +body.theme-dracula .toolbar { + background: #343746; + border-bottom-color: #44475a; +} + +body.theme-dracula .toolbar button { + color: #f8f8f2; +} + +body.theme-dracula .toolbar button:hover { + background: #44475a; + border-color: #6272a4; +} + +body.theme-dracula .editor-textarea { + background: #282a36; + color: #f8f8f2; +} + +body.theme-dracula .pane:last-child { + background: #282a36; +} + +body.theme-dracula .preview-content { + color: #f8f8f2; +} + +body.theme-dracula .preview-content h1, +body.theme-dracula .preview-content h2 { + border-bottom-color: #44475a; + color: #bd93f9; +} + +body.theme-dracula .preview-content code { + background-color: #44475a; +} + +body.theme-dracula .preview-content pre { + background-color: #44475a; +} + +body.theme-dracula .preview-content a { + color: #8be9fd; +} + +body.theme-dracula .status-bar { + background: #343746; + border-top-color: #44475a; + color: #f8f8f2; +} + +/* Theme: Nord */ +body.theme-nord { + background: #2e3440; + color: #d8dee9; +} + +body.theme-nord .tab-bar { + background: #3b4252; + border-bottom-color: #4c566a; +} + +body.theme-nord .tab { + background: #434c5e; + border-color: #4c566a; + color: #d8dee9; +} + +body.theme-nord .tab.active { + background: #2e3440; + border-color: #88c0d0; +} + +body.theme-nord .toolbar { + background: #3b4252; + border-bottom-color: #4c566a; +} + +body.theme-nord .toolbar button { + color: #d8dee9; +} + +body.theme-nord .toolbar button:hover { + background: #434c5e; + border-color: #4c566a; +} + +body.theme-nord .editor-textarea { + background: #2e3440; + color: #d8dee9; +} + +body.theme-nord .pane:last-child { + background: #2e3440; +} + +body.theme-nord .preview-content { + color: #d8dee9; +} + +body.theme-nord .preview-content h1, +body.theme-nord .preview-content h2 { + border-bottom-color: #4c566a; + color: #88c0d0; +} + +body.theme-nord .preview-content code { + background-color: #3b4252; +} + +body.theme-nord .preview-content pre { + background-color: #3b4252; +} + +body.theme-nord .preview-content a { + color: #81a1c1; +} + +body.theme-nord .status-bar { + background: #3b4252; + border-top-color: #4c566a; + color: #d8dee9; +} + +/* Theme: One Dark */ +body.theme-onedark { + background: #282c34; + color: #abb2bf; +} + +body.theme-onedark .tab-bar { + background: #21252b; + border-bottom-color: #181a1f; +} + +body.theme-onedark .tab { + background: #2c313a; + border-color: #3e4451; + color: #abb2bf; +} + +body.theme-onedark .tab.active { + background: #282c34; + border-color: #61afef; +} + +body.theme-onedark .toolbar { + background: #21252b; + border-bottom-color: #181a1f; +} + +body.theme-onedark .toolbar button { + color: #abb2bf; +} + +body.theme-onedark .toolbar button:hover { + background: #2c313a; + border-color: #3e4451; +} + +body.theme-onedark .editor-textarea { + background: #282c34; + color: #abb2bf; +} + +body.theme-onedark .pane:last-child { + background: #282c34; +} + +body.theme-onedark .preview-content { + color: #abb2bf; +} + +body.theme-onedark .preview-content h1, +body.theme-onedark .preview-content h2 { + border-bottom-color: #3e4451; + color: #61afef; +} + +body.theme-onedark .preview-content code { + background-color: #21252b; +} + +body.theme-onedark .preview-content pre { + background-color: #21252b; +} + +body.theme-onedark .preview-content a { + color: #56b6c2; +} + +body.theme-onedark .status-bar { + background: #21252b; + border-top-color: #181a1f; + color: #abb2bf; +} + +/* Theme: Atom One Light */ +body.theme-atomonelight { + background: #fafafa; + color: #383a42; +} + +body.theme-atomonelight .tab-bar { + background: #f0f0f0; + border-bottom-color: #e0e0e0; +} + +body.theme-atomonelight .tab { + background: #e5e5e6; + border-color: #d0d0d0; + color: #383a42; +} + +body.theme-atomonelight .tab.active { + background: #fafafa; + border-color: #4078f2; +} + +body.theme-atomonelight .toolbar { + background: #f0f0f0; + border-bottom-color: #e0e0e0; +} + +body.theme-atomonelight .toolbar button { + color: #383a42; +} + +body.theme-atomonelight .toolbar button:hover { + background: #e5e5e6; + border-color: #d0d0d0; +} + +body.theme-atomonelight .editor-textarea { + background: #fafafa; + color: #383a42; +} + +body.theme-atomonelight .pane:last-child { + background: #fafafa; +} + +body.theme-atomonelight .preview-content { + color: #383a42; +} + +body.theme-atomonelight .preview-content h1, +body.theme-atomonelight .preview-content h2 { + border-bottom-color: #e0e0e0; + color: #4078f2; +} + +body.theme-atomonelight .preview-content code { + background-color: #f0f0f0; +} + +body.theme-atomonelight .preview-content pre { + background-color: #f0f0f0; +} + +body.theme-atomonelight .preview-content a { + color: #0184bc; +} + +body.theme-atomonelight .status-bar { + background: #f0f0f0; + border-top-color: #e0e0e0; + color: #383a42; +} + +/* Theme: Material */ +body.theme-material { + background: #263238; + color: #eeffff; +} + +body.theme-material .tab-bar { + background: #2e3c43; + border-bottom-color: #37474f; +} + +body.theme-material .tab { + background: #37474f; + border-color: #546e7a; + color: #eeffff; +} + +body.theme-material .tab.active { + background: #263238; + border-color: #82aaff; +} + +body.theme-material .toolbar { + background: #2e3c43; + border-bottom-color: #37474f; +} + +body.theme-material .toolbar button { + color: #eeffff; +} + +body.theme-material .toolbar button:hover { + background: #37474f; + border-color: #546e7a; +} + +body.theme-material .editor-textarea { + background: #263238; + color: #eeffff; +} + +body.theme-material .pane:last-child { + background: #263238; +} + +body.theme-material .preview-content { + color: #eeffff; +} + +body.theme-material .preview-content h1, +body.theme-material .preview-content h2 { + border-bottom-color: #37474f; + color: #82aaff; +} + +body.theme-material .preview-content code { + background-color: #2e3c43; +} + +body.theme-material .preview-content pre { + background-color: #2e3c43; +} + +body.theme-material .preview-content a { + color: #80cbc4; +} + +body.theme-material .status-bar { + background: #2e3c43; + border-top-color: #37474f; + color: #eeffff; +} + +/* Theme: Gruvbox Dark */ +body.theme-gruvbox-dark { + background: #282828; + color: #ebdbb2; +} + +body.theme-gruvbox-dark .tab-bar { + background: #3c3836; + border-bottom-color: #504945; +} + +body.theme-gruvbox-dark .tab { + background: #504945; + border-color: #665c54; + color: #ebdbb2; +} + +body.theme-gruvbox-dark .tab.active { + background: #282828; + border-color: #fabd2f; +} + +body.theme-gruvbox-dark .toolbar { + background: #3c3836; + border-bottom-color: #504945; +} + +body.theme-gruvbox-dark .toolbar button { + color: #ebdbb2; +} + +body.theme-gruvbox-dark .toolbar button:hover { + background: #504945; + border-color: #665c54; +} + +body.theme-gruvbox-dark .editor-textarea { + background: #282828; + color: #ebdbb2; +} + +body.theme-gruvbox-dark .pane:last-child { + background: #282828; +} + +body.theme-gruvbox-dark .preview-content { + color: #ebdbb2; +} + +body.theme-gruvbox-dark .preview-content h1, +body.theme-gruvbox-dark .preview-content h2 { + border-bottom-color: #504945; + color: #fabd2f; +} + +body.theme-gruvbox-dark .preview-content code { + background-color: #3c3836; +} + +body.theme-gruvbox-dark .preview-content pre { + background-color: #3c3836; +} + +body.theme-gruvbox-dark .preview-content a { + color: #83a598; +} + +body.theme-gruvbox-dark .status-bar { + background: #3c3836; + border-top-color: #504945; + color: #ebdbb2; +} + +/* Theme: Gruvbox Light */ +body.theme-gruvbox-light { + background: #fbf1c7; + color: #3c3836; +} + +body.theme-gruvbox-light .tab-bar { + background: #ebdbb2; + border-bottom-color: #d5c4a1; +} + +body.theme-gruvbox-light .tab { + background: #d5c4a1; + border-color: #bdae93; + color: #3c3836; +} + +body.theme-gruvbox-light .tab.active { + background: #fbf1c7; + border-color: #b57614; +} + +body.theme-gruvbox-light .toolbar { + background: #ebdbb2; + border-bottom-color: #d5c4a1; +} + +body.theme-gruvbox-light .toolbar button { + color: #3c3836; +} + +body.theme-gruvbox-light .toolbar button:hover { + background: #d5c4a1; + border-color: #bdae93; +} + +body.theme-gruvbox-light .editor-textarea { + background: #fbf1c7; + color: #3c3836; +} + +body.theme-gruvbox-light .pane:last-child { + background: #fbf1c7; +} + +body.theme-gruvbox-light .preview-content { + color: #3c3836; +} + +body.theme-gruvbox-light .preview-content h1, +body.theme-gruvbox-light .preview-content h2 { + border-bottom-color: #d5c4a1; + color: #b57614; +} + +body.theme-gruvbox-light .preview-content code { + background-color: #ebdbb2; +} + +body.theme-gruvbox-light .preview-content pre { + background-color: #ebdbb2; +} + +body.theme-gruvbox-light .preview-content a { + color: #076678; +} + +body.theme-gruvbox-light .status-bar { + background: #ebdbb2; + border-top-color: #d5c4a1; + color: #3c3836; +} + +/* Theme: Tokyo Night */ +body.theme-tokyonight { + background: #1a1b26; + color: #c0caf5; +} + +body.theme-tokyonight .tab-bar { + background: #16161e; + border-bottom-color: #24283b; +} + +body.theme-tokyonight .tab { + background: #24283b; + border-color: #414868; + color: #c0caf5; +} + +body.theme-tokyonight .tab.active { + background: #1a1b26; + border-color: #7aa2f7; +} + +body.theme-tokyonight .toolbar { + background: #16161e; + border-bottom-color: #24283b; +} + +body.theme-tokyonight .toolbar button { + color: #c0caf5; +} + +body.theme-tokyonight .toolbar button:hover { + background: #24283b; + border-color: #414868; +} + +body.theme-tokyonight .editor-textarea { + background: #1a1b26; + color: #c0caf5; +} + +body.theme-tokyonight .pane:last-child { + background: #1a1b26; +} + +body.theme-tokyonight .preview-content { + color: #c0caf5; +} + +body.theme-tokyonight .preview-content h1, +body.theme-tokyonight .preview-content h2 { + border-bottom-color: #24283b; + color: #7aa2f7; +} + +body.theme-tokyonight .preview-content code { + background-color: #24283b; +} + +body.theme-tokyonight .preview-content pre { + background-color: #24283b; +} + +body.theme-tokyonight .preview-content a { + color: #7dcfff; +} + +body.theme-tokyonight .status-bar { + background: #16161e; + border-top-color: #24283b; + color: #c0caf5; +} + +/* Theme: Palenight */ +body.theme-palenight { + background: #292d3e; + color: #a6accd; +} + +body.theme-palenight .tab-bar { + background: #32374d; + border-bottom-color: #444267; +} + +body.theme-palenight .tab { + background: #444267; + border-color: #676e95; + color: #a6accd; +} + +body.theme-palenight .tab.active { + background: #292d3e; + border-color: #82aaff; +} + +body.theme-palenight .toolbar { + background: #32374d; + border-bottom-color: #444267; +} + +body.theme-palenight .toolbar button { + color: #a6accd; +} + +body.theme-palenight .toolbar button:hover { + background: #444267; + border-color: #676e95; +} + +body.theme-palenight .editor-textarea { + background: #292d3e; + color: #a6accd; +} + +body.theme-palenight .pane:last-child { + background: #292d3e; +} + +body.theme-palenight .preview-content { + color: #a6accd; +} + +body.theme-palenight .preview-content h1, +body.theme-palenight .preview-content h2 { + border-bottom-color: #444267; + color: #82aaff; +} + +body.theme-palenight .preview-content code { + background-color: #32374d; +} + +body.theme-palenight .preview-content pre { + background-color: #32374d; +} + +body.theme-palenight .preview-content a { + color: #89ddff; +} + +body.theme-palenight .status-bar { + background: #32374d; + border-top-color: #444267; + color: #a6accd; +} + +/* Theme: Ayu Dark */ +body.theme-ayu-dark { + background: #0a0e14; + color: #b3b1ad; +} + +body.theme-ayu-dark .tab-bar { + background: #0d1016; + border-bottom-color: #1f2430; +} + +body.theme-ayu-dark .tab { + background: #1f2430; + border-color: #3d424d; + color: #b3b1ad; +} + +body.theme-ayu-dark .tab.active { + background: #0a0e14; + border-color: #ffaa33; +} + +body.theme-ayu-dark .toolbar { + background: #0d1016; + border-bottom-color: #1f2430; +} + +body.theme-ayu-dark .toolbar button { + color: #b3b1ad; +} + +body.theme-ayu-dark .toolbar button:hover { + background: #1f2430; + border-color: #3d424d; +} + +body.theme-ayu-dark .editor-textarea { + background: #0a0e14; + color: #b3b1ad; +} + +body.theme-ayu-dark .pane:last-child { + background: #0a0e14; +} + +body.theme-ayu-dark .preview-content { + color: #b3b1ad; +} + +body.theme-ayu-dark .preview-content h1, +body.theme-ayu-dark .preview-content h2 { + border-bottom-color: #1f2430; + color: #ffaa33; +} + +body.theme-ayu-dark .preview-content code { + background-color: #0d1016; +} + +body.theme-ayu-dark .preview-content pre { + background-color: #0d1016; +} + +body.theme-ayu-dark .preview-content a { + color: #39bae6; +} + +body.theme-ayu-dark .status-bar { + background: #0d1016; + border-top-color: #1f2430; + color: #b3b1ad; +} + +/* Theme: Ayu Light */ +body.theme-ayu-light { + background: #fafafa; + color: #5c6166; +} + +body.theme-ayu-light .tab-bar { + background: #f3f4f5; + border-bottom-color: #e7e8e9; +} + +body.theme-ayu-light .tab { + background: #e7e8e9; + border-color: #d9dadb; + color: #5c6166; +} + +body.theme-ayu-light .tab.active { + background: #fafafa; + border-color: #ff6a00; +} + +body.theme-ayu-light .toolbar { + background: #f3f4f5; + border-bottom-color: #e7e8e9; +} + +body.theme-ayu-light .toolbar button { + color: #5c6166; +} + +body.theme-ayu-light .toolbar button:hover { + background: #e7e8e9; + border-color: #d9dadb; +} + +body.theme-ayu-light .editor-textarea { + background: #fafafa; + color: #5c6166; +} + +body.theme-ayu-light .pane:last-child { + background: #fafafa; +} + +body.theme-ayu-light .preview-content { + color: #5c6166; +} + +body.theme-ayu-light .preview-content h1, +body.theme-ayu-light .preview-content h2 { + border-bottom-color: #e7e8e9; + color: #ff6a00; +} + +body.theme-ayu-light .preview-content code { + background-color: #f3f4f5; +} + +body.theme-ayu-light .preview-content pre { + background-color: #f3f4f5; +} + +body.theme-ayu-light .preview-content a { + color: #399ee6; +} + +body.theme-ayu-light .status-bar { + background: #f3f4f5; + border-top-color: #e7e8e9; + color: #5c6166; +} + +/* Theme: Ayu Mirage */ +body.theme-ayu-mirage { + background: #1f2430; + color: #cbccc6; +} + +body.theme-ayu-mirage .tab-bar { + background: #232834; + border-bottom-color: #343d4b; +} + +body.theme-ayu-mirage .tab { + background: #343d4b; + border-color: #465268; + color: #cbccc6; +} + +body.theme-ayu-mirage .tab.active { + background: #1f2430; + border-color: #ffa759; +} + +body.theme-ayu-mirage .toolbar { + background: #232834; + border-bottom-color: #343d4b; +} + +body.theme-ayu-mirage .toolbar button { + color: #cbccc6; +} + +body.theme-ayu-mirage .toolbar button:hover { + background: #343d4b; + border-color: #465268; +} + +body.theme-ayu-mirage .editor-textarea { + background: #1f2430; + color: #cbccc6; +} + +body.theme-ayu-mirage .pane:last-child { + background: #1f2430; +} + +body.theme-ayu-mirage .preview-content { + color: #cbccc6; +} + +body.theme-ayu-mirage .preview-content h1, +body.theme-ayu-mirage .preview-content h2 { + border-bottom-color: #343d4b; + color: #ffa759; +} + +body.theme-ayu-mirage .preview-content code { + background-color: #232834; +} + +body.theme-ayu-mirage .preview-content pre { + background-color: #232834; +} + +body.theme-ayu-mirage .preview-content a { + color: #5ccfe6; +} + +body.theme-ayu-mirage .status-bar { + background: #232834; + border-top-color: #343d4b; + color: #cbccc6; +} + +/* Theme: Oceanic Next */ +body.theme-oceanic-next { + background: #1b2b34; + color: #cdd3de; +} + +body.theme-oceanic-next .tab-bar { + background: #20353f; + border-bottom-color: #343d46; +} + +body.theme-oceanic-next .tab { + background: #343d46; + border-color: #4f5b66; + color: #cdd3de; +} + +body.theme-oceanic-next .tab.active { + background: #1b2b34; + border-color: #6699cc; +} + +body.theme-oceanic-next .toolbar { + background: #20353f; + border-bottom-color: #343d46; +} + +body.theme-oceanic-next .toolbar button { + color: #cdd3de; +} + +body.theme-oceanic-next .toolbar button:hover { + background: #343d46; + border-color: #4f5b66; +} + +body.theme-oceanic-next .editor-textarea { + background: #1b2b34; + color: #cdd3de; +} + +body.theme-oceanic-next .pane:last-child { + background: #1b2b34; +} + +body.theme-oceanic-next .preview-content { + color: #cdd3de; +} + +body.theme-oceanic-next .preview-content h1, +body.theme-oceanic-next .preview-content h2 { + border-bottom-color: #343d46; + color: #6699cc; +} + +body.theme-oceanic-next .preview-content code { + background-color: #20353f; +} + +body.theme-oceanic-next .preview-content pre { + background-color: #20353f; +} + +body.theme-oceanic-next .preview-content a { + color: #5fb3b3; +} + +body.theme-oceanic-next .status-bar { + background: #20353f; + border-top-color: #343d46; + color: #cdd3de; +} + +/* Theme: Cobalt2 */ +body.theme-cobalt2 { + background: #193549; + color: #ffffff; +} + +body.theme-cobalt2 .tab-bar { + background: #15232d; + border-bottom-color: #2a4a5d; +} + +body.theme-cobalt2 .tab { + background: #0d3a58; + border-color: #2a4a5d; + color: #ffffff; +} + +body.theme-cobalt2 .tab.active { + background: #193549; + border-color: #ffc600; +} + +body.theme-cobalt2 .toolbar { + background: #15232d; + border-bottom-color: #2a4a5d; +} + +body.theme-cobalt2 .toolbar button { + color: #ffffff; +} + +body.theme-cobalt2 .toolbar button:hover { + background: #0d3a58; + border-color: #2a4a5d; +} + +body.theme-cobalt2 .editor-textarea { + background: #193549; + color: #ffffff; +} + +body.theme-cobalt2 .pane:last-child { + background: #193549; +} + +body.theme-cobalt2 .preview-content { + color: #ffffff; +} + +body.theme-cobalt2 .preview-content h1, +body.theme-cobalt2 .preview-content h2 { + border-bottom-color: #2a4a5d; + color: #ffc600; +} + +body.theme-cobalt2 .preview-content code { + background-color: #0d3a58; +} + +body.theme-cobalt2 .preview-content pre { + background-color: #0d3a58; +} + +body.theme-cobalt2 .preview-content a { + color: #3ad900; +} + +body.theme-cobalt2 .status-bar { + background: #15232d; + border-top-color: #2a4a5d; + color: #ffffff; +} + +/* Theme: Concrete Dark - ConcreteInfo branded dark theme */ +body.theme-concrete-dark { + background: #0d0b09; + color: #e3e3e3; +} + +body.theme-concrete-dark .tab-bar { + background: #1a1816; + border-bottom-color: #464646; +} + +body.theme-concrete-dark .tab { + background: #464646; + border-color: #9a9696; + color: #e3e3e3; +} + +body.theme-concrete-dark .tab.active { + background: #0d0b09; + border-color: #e5461f; +} + +body.theme-concrete-dark .toolbar { + background: #1a1816; + border-bottom-color: #464646; +} + +body.theme-concrete-dark .toolbar button { + color: #e3e3e3; +} + +body.theme-concrete-dark .toolbar button:hover { + background: #464646; + border-color: #e5461f; +} + +body.theme-concrete-dark .editor-textarea { + background: #0d0b09; + color: #e3e3e3; +} + +body.theme-concrete-dark .pane:last-child { + background: #0d0b09; +} + +body.theme-concrete-dark .preview-content { + color: #e3e3e3; +} + +body.theme-concrete-dark .preview-content h1, +body.theme-concrete-dark .preview-content h2 { + border-bottom-color: #464646; + color: #e5461f; +} + +body.theme-concrete-dark .preview-content code { + background-color: #464646; +} + +body.theme-concrete-dark .preview-content pre { + background-color: #464646; +} + +body.theme-concrete-dark .preview-content a { + color: #e5461f; +} + +body.theme-concrete-dark .status-bar { + background: #1a1816; + border-top-color: #464646; + color: #9a9696; +} + +/* Theme: Concrete Light - ConcreteInfo branded light theme */ +body.theme-concrete-light { + background: #fafafa; + color: #0d0b09; +} + +body.theme-concrete-light .tab-bar { + background: #e3e3e3; + border-bottom-color: #9a9696; +} + +body.theme-concrete-light .tab { + background: #e3e3e3; + border-color: #9a9696; + color: #0d0b09; +} + +body.theme-concrete-light .tab.active { + background: #fafafa; + border-color: #e5461f; +} + +body.theme-concrete-light .toolbar { + background: #e3e3e3; + border-bottom-color: #9a9696; +} + +body.theme-concrete-light .toolbar button { + color: #0d0b09; +} + +body.theme-concrete-light .toolbar button:hover { + background: #9a9696; + border-color: #e5461f; +} + +body.theme-concrete-light .editor-textarea { + background: #fafafa; + color: #0d0b09; +} + +body.theme-concrete-light .pane:last-child { + background: #fafafa; +} + +body.theme-concrete-light .preview-content { + color: #0d0b09; +} + +body.theme-concrete-light .preview-content h1, +body.theme-concrete-light .preview-content h2 { + border-bottom-color: #9a9696; + color: #e5461f; +} + +body.theme-concrete-light .preview-content code { + background-color: #e3e3e3; +} + +body.theme-concrete-light .preview-content pre { + background-color: #e3e3e3; +} + +body.theme-concrete-light .preview-content a { + color: #e5461f; +} + +body.theme-concrete-light .status-bar { + background: #e3e3e3; + border-top-color: #9a9696; + color: #464646; +} + +/* Theme: Concrete Warm - ConcreteInfo branded warm theme */ +body.theme-concrete-warm { + background: #464646; + color: #e3e3e3; +} + +body.theme-concrete-warm .tab-bar { + background: #3a3836; + border-bottom-color: #9a9696; +} + +body.theme-concrete-warm .tab { + background: #9a9696; + border-color: #e3e3e3; + color: #0d0b09; +} + +body.theme-concrete-warm .tab.active { + background: #464646; + border-color: #e5461f; +} + +body.theme-concrete-warm .toolbar { + background: #3a3836; + border-bottom-color: #9a9696; +} + +body.theme-concrete-warm .toolbar button { + color: #e3e3e3; +} + +body.theme-concrete-warm .toolbar button:hover { + background: #9a9696; + border-color: #e5461f; +} + +body.theme-concrete-warm .editor-textarea { + background: #464646; + color: #e3e3e3; +} + +body.theme-concrete-warm .pane:last-child { + background: #464646; +} + +body.theme-concrete-warm .preview-content { + color: #e3e3e3; +} + +body.theme-concrete-warm .preview-content h1, +body.theme-concrete-warm .preview-content h2 { + border-bottom-color: #9a9696; + color: #e5461f; +} + +body.theme-concrete-warm .preview-content code { + background-color: #3a3836; +} + +body.theme-concrete-warm .preview-content pre { + background-color: #3a3836; +} + +body.theme-concrete-warm .preview-content a { + color: #e5461f; +} + +body.theme-concrete-warm .status-bar { + background: #3a3836; + border-top-color: #9a9696; + color: #e3e3e3; +} + +/* ======================================== + Sepia Theme - Warm reading theme + ======================================== */ +body.theme-sepia { + background: #f4ecd8; + color: #5b4636; +} + +body.theme-sepia .tab-bar { + background: #e8dcc8; + border-bottom-color: #d4c4a8; +} + +body.theme-sepia .tab { + background: #e8dcc8; + border-color: #d4c4a8; + color: #5b4636; +} + +body.theme-sepia .tab.active { + background: #f4ecd8; + border-bottom-color: #8b7355; +} + +body.theme-sepia .toolbar { + background: #e8dcc8; + border-bottom-color: #d4c4a8; +} + +body.theme-sepia .toolbar button { + color: #5b4636; +} + +body.theme-sepia .toolbar button:hover { + background: #d4c4a8; +} + +body.theme-sepia .editor-textarea { + background: #f4ecd8; + color: #5b4636; +} + +body.theme-sepia .pane:last-child { + background: #f4ecd8; +} + +body.theme-sepia .preview-content { + color: #5b4636; +} + +body.theme-sepia .preview-content h1, +body.theme-sepia .preview-content h2 { + color: #3d2914; + border-bottom-color: #d4c4a8; +} + +body.theme-sepia .preview-content code { + background: #e8dcc8; + color: #8b4513; +} + +body.theme-sepia .preview-content pre { + background: #e8dcc8; + border-color: #d4c4a8; +} + +body.theme-sepia .preview-content a { + color: #8b4513; +} + +body.theme-sepia .preview-content blockquote { + border-left-color: #8b7355; + background: #e8dcc8; +} + +body.theme-sepia .status-bar { + background: #e8dcc8; + border-top-color: #d4c4a8; + color: #5b4636; +} + +/* ======================================== + Paper Theme - Clean white paper + ======================================== */ +body.theme-paper { + background: #ffffff; + color: #1a1a1a; +} + +body.theme-paper .tab-bar { + background: #f8f8f8; + border-bottom-color: #e0e0e0; +} + +body.theme-paper .tab { + background: #f0f0f0; + border-color: #e0e0e0; + color: #333; +} + +body.theme-paper .tab.active { + background: #ffffff; + border-bottom-color: #1a1a1a; +} + +body.theme-paper .toolbar { + background: #f8f8f8; + border-bottom-color: #e0e0e0; +} + +body.theme-paper .toolbar button { + color: #333; +} + +body.theme-paper .toolbar button:hover { + background: #e8e8e8; +} + +body.theme-paper .editor-textarea { + background: #ffffff; + color: #1a1a1a; +} + +body.theme-paper .pane:last-child { + background: #ffffff; +} + +body.theme-paper .preview-content { + color: #1a1a1a; +} + +body.theme-paper .preview-content h1, +body.theme-paper .preview-content h2 { + color: #000; + border-bottom-color: #e0e0e0; +} + +body.theme-paper .preview-content code { + background: #f5f5f5; + color: #c41a16; +} + +body.theme-paper .preview-content pre { + background: #f5f5f5; + border-color: #e0e0e0; +} + +body.theme-paper .preview-content a { + color: #0366d6; +} + +body.theme-paper .preview-content blockquote { + border-left-color: #ddd; + background: #f9f9f9; +} + +body.theme-paper .status-bar { + background: #f8f8f8; + border-top-color: #e0e0e0; + color: #333; +} + +/* ======================================== + Rose Pine Dawn - Warm rose-tinted light + ======================================== */ +body.theme-rosepine-dawn { + background: #faf4ed; + color: #575279; +} + +body.theme-rosepine-dawn .tab-bar { + background: #f2e9e1; + border-bottom-color: #dfdad9; +} + +body.theme-rosepine-dawn .tab { + background: #f2e9e1; + border-color: #dfdad9; + color: #575279; +} + +body.theme-rosepine-dawn .tab.active { + background: #faf4ed; + border-bottom-color: #907aa9; +} + +body.theme-rosepine-dawn .toolbar { + background: #f2e9e1; + border-bottom-color: #dfdad9; +} + +body.theme-rosepine-dawn .toolbar button { + color: #575279; +} + +body.theme-rosepine-dawn .toolbar button:hover { + background: #dfdad9; +} + +body.theme-rosepine-dawn .editor-textarea { + background: #faf4ed; + color: #575279; +} + +body.theme-rosepine-dawn .pane:last-child { + background: #faf4ed; +} + +body.theme-rosepine-dawn .preview-content { + color: #575279; +} + +body.theme-rosepine-dawn .preview-content h1, +body.theme-rosepine-dawn .preview-content h2 { + color: #286983; + border-bottom-color: #dfdad9; +} + +body.theme-rosepine-dawn .preview-content code { + background: #f2e9e1; + color: #b4637a; +} + +body.theme-rosepine-dawn .preview-content pre { + background: #f2e9e1; + border-color: #dfdad9; +} + +body.theme-rosepine-dawn .preview-content a { + color: #56949f; +} + +body.theme-rosepine-dawn .preview-content blockquote { + border-left-color: #907aa9; + background: #f2e9e1; + color: #797593; +} + +body.theme-rosepine-dawn .status-bar { + background: #f2e9e1; + border-top-color: #dfdad9; + color: #575279; +} + +/* Print Styles */ +@media print { + /* Hide UI elements for clean print output */ + #toolbar, + #tab-bar, + #editor-container, + .editor-container, + #status-bar, + .status-bar, + .toolbar, + .tab-bar, + .editor-pane, + [id^="editor-pane-"], + .dialog, + .modal-overlay, + .export-dialog { + display: none !important; + } + + /* Ensure body and html take full width */ + html, body { + width: 100% !important; + height: auto !important; + margin: 0 !important; + padding: 0 !important; + overflow: visible !important; + } + + /* Show preview in full width */ + #preview, + .preview, + .preview-content, + [id^="preview-"], + [id^="preview-pane-"], + .tab-content, + .pane { + display: block !important; + width: 100% !important; + max-width: 100% !important; + height: auto !important; + margin: 0 !important; + padding: 20px !important; + overflow: visible !important; + position: static !important; + left: auto !important; + right: auto !important; + top: auto !important; + bottom: auto !important; + } + + /* Optimize preview content for printing */ + .preview-content, + [id^="preview-"] { + line-height: 1.6; + font-size: 12pt; + color: #000 !important; + background: white !important; + } + + /* No-styles mode for printing without theme colors */ + body.printing-no-styles .preview-content, + body.printing-no-styles [id^="preview-"] { + color: #000 !important; + background: #fff !important; + } + + /* Text formatting */ + .preview-content h1, + .preview-content h2, + .preview-content h3, + .preview-content h4, + .preview-content h5, + .preview-content h6 { + page-break-after: avoid; + margin-top: 1em; + margin-bottom: 0.5em; + } + + .preview-content p { + margin-bottom: 0.5em; + page-break-inside: avoid; + } + + /* Code blocks */ + .preview-content pre { + background: #f5f5f5 !important; + border: 1px solid #ddd !important; + page-break-inside: avoid; + padding: 10px; + font-size: 10pt; + } + + .preview-content code { + background: #f5f5f5 !important; + color: #000 !important; + font-size: 10pt; + } + + /* Lists */ + .preview-content ul, + .preview-content ol { + margin-left: 20px; + page-break-inside: avoid; + } + + .preview-content li { + page-break-inside: avoid; + margin-bottom: 0.3em; + } + + /* Tables */ + .preview-content table { + width: 100%; + border-collapse: collapse; + page-break-inside: avoid; + margin-bottom: 1em; + } + + .preview-content th, + .preview-content td { + border: 1px solid #999 !important; + padding: 8px !important; + text-align: left; + } + + .preview-content th { + background: #f0f0f0 !important; + font-weight: bold; + } + + /* Links - remove underline for print */ + .preview-content a { + color: #000 !important; + text-decoration: none; + } + + /* Blockquotes */ + .preview-content blockquote { + border-left: 3px solid #ccc; + padding-left: 15px; + margin-left: 0; + color: #333 !important; + } + + /* Images */ + .preview-content img { + max-width: 100%; + height: auto; + page-break-inside: avoid; + } + + /* Horizontal rules */ + .preview-content hr { + border: none; + border-top: 1px solid #999; + margin: 1em 0; + } +} + +/* Print mode classes */ +.print-mode { + display: block !important; + width: 100% !important; + max-width: 100% !important; + position: fixed !important; + top: 0 !important; + left: 0 !important; + z-index: 9999 !important; + background: white !important; + padding: 20px !important; +} + +/* Ensure preview panes are visible in print mode */ +#preview-pane-1, #preview-pane-2, #preview-pane-3, #preview-pane-4, #preview-pane-5 { + display: block !important; +} + +/* No-styles printing mode */ +body.printing-no-styles .preview-content, +body.printing-no-styles [id^="preview-"], +.print-no-styles .preview-content { + color: #000 !important; + background: white !important; +} + +body.printing-no-styles .preview-content h1, +body.printing-no-styles .preview-content h2, +body.printing-no-styles .preview-content h3, +body.printing-no-styles .preview-content h4, +body.printing-no-styles .preview-content h5, +body.printing-no-styles .preview-content h6, +.print-no-styles .preview-content h1, +.print-no-styles .preview-content h2, +.print-no-styles .preview-content h3, +.print-no-styles .preview-content h4, +.print-no-styles .preview-content h5, +.print-no-styles .preview-content h6 { + color: #000 !important; + border-color: #999 !important; +} + +body.printing-no-styles .preview-content code, +.print-no-styles .preview-content code { + background: #f5f5f5 !important; + color: #000 !important; +} + +body.printing-no-styles .preview-content pre, +.print-no-styles .preview-content pre { + background: #f5f5f5 !important; + color: #000 !important; + border-color: #999 !important; +} + +.print-no-styles .preview-content a { + color: #000 !important; +} + +.print-no-styles .preview-content blockquote { + color: #333 !important; + border-color: #999 !important; +} + +/* ================================ + Header & Footer Dialog Styles + ================================ */ + +.hf-enable-section { + margin-bottom: 20px; + padding: 15px; + background: var(--bg-secondary, #f5f5f5); + border-radius: 5px; +} + +.hf-enable-section label { + display: flex; + align-items: center; + font-weight: 600; + cursor: pointer; +} + +.hf-enable-section input[type="checkbox"] { + margin-right: 10px; + width: 18px; + height: 18px; + cursor: pointer; +} + +#hf-config-content { + transition: opacity 0.3s ease; +} + +#hf-config-content.disabled { + opacity: 0.5; + pointer-events: none; +} + +.hf-section { + margin-bottom: 25px; + padding: 20px; + background: var(--bg-tertiary, #fafafa); + border-radius: 8px; + border: 1px solid var(--border-color, #e0e0e0); +} + +.hf-section h4 { + margin: 0 0 15px 0; + color: var(--text-primary, #333); + font-size: 16px; + border-bottom: 2px solid var(--accent-color, #007bff); + padding-bottom: 8px; +} + +.hf-row { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 15px; + margin-bottom: 15px; +} + +.hf-column { + display: flex; + flex-direction: column; + position: relative; +} + +.hf-column label { + font-weight: 600; + margin-bottom: 5px; + color: var(--text-secondary, #666); + font-size: 13px; +} + +.hf-column input[type="text"] { + padding: 8px 35px 8px 10px; + border: 1px solid var(--border-color, #ccc); + border-radius: 4px; + font-size: 14px; + background: var(--input-bg, white); + color: var(--text-primary, #333); + transition: border-color 0.2s ease; +} + +.hf-column input[type="text"]:focus { + outline: none; + border-color: var(--accent-color, #007bff); + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.1); +} + +.field-insert-btn { + position: absolute; + right: 5px; + bottom: 5px; + width: 28px; + height: 28px; + padding: 0; + background: var(--accent-color, #007bff); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 18px; + line-height: 1; + transition: all 0.2s ease; +} + +.field-insert-btn:hover { + background: var(--accent-hover, #0056b3); + transform: scale(1.05); +} + +.field-insert-btn:active { + transform: scale(0.95); +} + +.hf-logo-row { + display: flex; + align-items: center; + gap: 10px; + padding: 15px; + background: var(--bg-secondary, #f5f5f5); + border-radius: 6px; + margin-top: 10px; +} + +.hf-logo-row label { + font-weight: 600; + color: var(--text-secondary, #666); + min-width: 120px; +} + +.hf-logo-row input[type="file"] { + flex: 1; + padding: 6px 10px; + border: 1px solid var(--border-color, #ccc); + border-radius: 4px; + background: var(--input-bg, white); + color: var(--text-primary, #333); + font-size: 13px; + cursor: pointer; +} + +.hf-logo-row input[type="file"]::-webkit-file-upload-button { + padding: 5px 12px; + background: var(--button-bg, #6c757d); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + margin-right: 10px; +} + +.hf-logo-row input[type="file"]::-webkit-file-upload-button:hover { + background: var(--button-hover, #5a6268); +} + +.browse-btn { + padding: 6px 12px; + background: var(--accent-color, #007bff); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + transition: background 0.2s ease; + margin-right: 8px; +} + +.browse-btn:hover { + background: var(--accent-hover, #0056b3); +} + +.clear-btn { + padding: 6px 12px; + background: var(--danger-color, #dc3545); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + transition: background 0.2s ease; +} + +.clear-btn:hover { + background: var(--danger-hover, #c82333); +} + +.logo-preview { + font-size: 12px; + color: var(--text-secondary, #666); + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.hf-help { + margin-top: 20px; + padding: 15px; + background: var(--info-bg, #e7f3ff); + border-left: 4px solid var(--info-color, #007bff); + border-radius: 4px; +} + +.hf-help p { + margin: 0 0 10px 0; + font-weight: 600; + color: var(--text-primary, #333); +} + +.hf-help ul { + margin: 0; + padding-left: 20px; + list-style-type: none; +} + +.hf-help li { + margin-bottom: 6px; + color: var(--text-secondary, #555); + font-size: 13px; +} + +.hf-help code { + background: rgba(0, 123, 255, 0.1); + padding: 2px 6px; + border-radius: 3px; + font-family: 'Courier New', monospace; + color: var(--accent-color, #007bff); + font-weight: 600; +} + +/* Field Picker Dialog */ +.field-picker-list { + display: grid; + grid-template-columns: 1fr; + gap: 8px; +} + +.field-option { + padding: 12px 16px; + background: var(--bg-secondary, #f5f5f5); + border: 1px solid var(--border-color, #ddd); + border-radius: 6px; + text-align: left; + cursor: pointer; + font-size: 14px; + color: var(--text-primary, #333); + transition: all 0.2s ease; +} + +.field-option:hover { + background: var(--accent-color, #007bff); + color: white; + border-color: var(--accent-color, #007bff); + transform: translateX(5px); +} + +/* Dark Theme Support for Header/Footer Dialog */ +body[data-theme="dark"] .hf-enable-section, +body[data-theme="dark"] .hf-logo-row { + background: #2d2d2d; +} + +body[data-theme="dark"] .hf-section { + background: #252525; + border-color: #404040; +} + +body[data-theme="dark"] .hf-column input[type="text"], +body[data-theme="dark"] .hf-logo-row input[type="file"] { + background: #2d2d2d; + color: #e0e0e0; + border-color: #404040; +} + +body[data-theme="dark"] .hf-help { + background: rgba(0, 123, 255, 0.15); + border-left-color: #0d6efd; +} + +body[data-theme="dark"] .field-option { + background: #2d2d2d; + border-color: #404040; + color: #e0e0e0; +} + +body[data-theme="dark"] .field-option:hover { + background: #0d6efd; + color: white; +} +/* Mermaid Diagram Styles */ +.mermaid { + background: #f9f9f9; + padding: 20px; + border-radius: 8px; + margin: 20px 0; + text-align: center; + border: 1px solid #e0e0e0; +} + +.mermaid svg { + max-width: 100%; + height: auto; +} + +body.theme-dark .mermaid { + background: #2d2d2d; + border-color: #444; +} + +body.theme-solarized .mermaid { + background: #fdf6e3; + border-color: #eee8d5; +} + +body.theme-monokai .mermaid { + background: #2f2f2f; + border-color: #49483e; +} + +body.theme-github .mermaid { + background: #f6f8fa; + border-color: #e1e4e8; +} + +/* Command Palette Styles */ +.command-palette { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 100px; + z-index: 10000; +} + +.command-palette.hidden { + display: none; +} + +.command-palette-content { + background: #fff; + border-radius: 12px; + width: 600px; + max-width: 90%; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + overflow: hidden; +} + +#command-search { + width: 100%; + padding: 20px; + font-size: 18px; + border: none; + border-bottom: 1px solid #e0e0e0; + outline: none; +} + +.command-list { + max-height: 400px; + overflow-y: auto; +} + +.command-item { + padding: 12px 20px; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: background 0.15s; +} + +.command-item:hover, +.command-item.selected { + background: #f0f0f0; +} + +.command-name { + font-weight: 500; +} + +.command-shortcut { + font-size: 12px; + color: #666; + background: #f5f5f5; + padding: 3px 8px; + border-radius: 3px; +} + +.command-palette-footer { + padding: 12px 20px; + background: #f9f9f9; + border-top: 1px solid #e0e0e0; + display: flex; + gap: 20px; + font-size: 12px; + color: #666; +} + +/* Dark theme support for command palette */ +body.theme-dark .command-palette-content { + background: #2d2d2d; + color: #f0f0f0; +} + +body.theme-dark #command-search { + background: #2d2d2d; + color: #f0f0f0; + border-color: #444; +} + +body.theme-dark .command-item:hover, +body.theme-dark .command-item.selected { + background: #3d3d3d; +} + +body.theme-dark .command-shortcut { + background: #3d3d3d; + color: #ccc; +} + +body.theme-dark .command-palette-footer { + background: #252525; + border-color: #444; + color: #999; +} diff --git a/src/table-generator.html b/src/table-generator.html index 4ee64e6..e94a7d3 100644 --- a/src/table-generator.html +++ b/src/table-generator.html @@ -1,545 +1,545 @@ - - - - - - Table Generator - MarkdownConverter - - - - -
-

Table Generator

-
- -
-
-
Table Settings
-
-
- Rows - -
-
- Columns - -
-
- Alignment - -
-
- -
-
-
- - - - - -
-
- -
-
Edit Table Content
-
- - -
-
-
- -
-
Markdown Preview
-
-
-
-
-
- - - - - - + + + + + + Table Generator - MarkdownConverter + + + + +
+

Table Generator

+
+ +
+
+
Table Settings
+
+
+ Rows + +
+
+ Columns + +
+
+ Alignment + +
+
+ +
+
+
+ + + + + +
+
+ +
+
Edit Table Content
+
+ + +
+
+
+ +
+
Markdown Preview
+
+
+
+
+
+ + + + + + diff --git a/src/wordTemplateExporter.js b/src/wordTemplateExporter.js index 534d6e2..827c2f6 100644 --- a/src/wordTemplateExporter.js +++ b/src/wordTemplateExporter.js @@ -1,743 +1,743 @@ -/** - * Word Template Exporter - * Loads word_template.docx, preserves first 2 pages (cover + TOC), - * and adds markdown content starting from page 3 using template styles - */ - -const fs = require('fs'); -const path = require('path'); -const PizZip = require('pizzip'); -const Docx = require('docx4js').default; - -class WordTemplateExporter { - constructor(templatePath, startPage = 3, pageSettings = null) { - this.templatePath = templatePath || path.join(__dirname, '../word_template.docx'); - this.startPage = startPage; // Which page to start inserting content - this.pageSettings = pageSettings; // Page size and orientation settings - } - - /** - * Convert markdown to Word document using template - */ - async convert(markdownContent, outputPath) { - try { - // Load template - const templateBuffer = fs.readFileSync(this.templatePath); - const zip = new PizZip(templateBuffer); - - // Extract document.xml - let documentXml = zip.file('word/document.xml').asText(); - - // Set page size if settings provided - if (this.pageSettings) { - documentXml = this.setPageSize(documentXml); - } - - // Parse markdown and generate Word XML - const newContentXml = this.markdownToWordXml(markdownContent); - - // Insert new content after the specified start page - const modifiedXml = this.insertContentAfterPage(documentXml, newContentXml, this.startPage); - - // Update the zip with modified XML - zip.file('word/document.xml', modifiedXml); - - // Generate and save the new document - const newDocBuffer = zip.generate({ type: 'nodebuffer' }); - fs.writeFileSync(outputPath, newDocBuffer); - - return outputPath; - } catch (error) { - console.error('Error in Word export:', error); - throw error; - } - } - - /** - * Set page size in document XML - */ - setPageSize(documentXml) { - // Import PAGE_SIZES from main process (need to pass as parameter) - const PAGE_SIZES = { - a4: { width: 11906, height: 16838 }, - a3: { width: 16838, height: 23811 }, - a5: { width: 8391, height: 11906 }, - b4: { width: 14170, height: 20015 }, - b5: { width: 9979, height: 14170 }, - letter: { width: 12240, height: 15840 }, - legal: { width: 12240, height: 20160 }, - tabloid: { width: 15840, height: 24480 } - }; - - let width, height; - const pageSize = PAGE_SIZES[this.pageSettings.size]; - - if (pageSize) { - width = pageSize.width; - height = pageSize.height; - } else { - // Default to A4 - width = 11906; - height = 16838; - } - - // Swap dimensions for landscape - if (this.pageSettings.orientation === 'landscape') { - [width, height] = [height, width]; - } - - // Update all elements in section properties - const pgSzRegex = /]*\/>/g; - let modifiedXml = documentXml.replace(pgSzRegex, () => { - return ``; - }); - - // If no pgSz found, add it to all sectPr elements - if (!pgSzRegex.test(documentXml)) { - const sectPrRegex = /]*>/g; - modifiedXml = modifiedXml.replace(sectPrRegex, (match) => { - return `${match}`; - }); - } - - return modifiedXml; - } - - /** - * Insert markdown content after the specified page in the document - * @param {string} documentXml - The document XML - * @param {string} newContentXml - The new content to insert - * @param {number} afterPage - Insert content after this page number (1-based) - */ - insertContentAfterPage(documentXml, newContentXml, afterPage) { - // Find section breaks that mark page boundaries - // Look for the section properties tag that marks page breaks - const sectionBreakRegex = /]*>[\s\S]*?<\/w:sectPr>/g; - const matches = [...documentXml.matchAll(sectionBreakRegex)]; - - // Calculate which section break to insert after - // Page 1 = before 1st section break - // Page 2 = after 1st section break - // Page 3 = after 2nd section break, etc. - const sectionIndex = afterPage - 1; - - if (matches.length >= sectionIndex && sectionIndex > 0) { - // Insert after the specified section break - const insertPoint = matches[sectionIndex - 1].index + matches[sectionIndex - 1][0].length; - return documentXml.slice(0, insertPoint) + newContentXml + documentXml.slice(insertPoint); - } else if (afterPage === 1 || matches.length === 0) { - // Insert at the beginning (after ) or if no section breaks found - const bodyStart = documentXml.indexOf('') + 8; - return documentXml.slice(0, bodyStart) + newContentXml + documentXml.slice(bodyStart); - } else { - // If not enough section breaks, insert before closing body tag - return documentXml.replace('', newContentXml + ''); - } - } - - /** - * Convert markdown to Word XML format - */ - markdownToWordXml(markdown) { - const lines = markdown.split('\n'); - let xml = ''; - let inCodeBlock = false; - let codeLines = []; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - // Handle code blocks - if (line.trim().startsWith('```')) { - if (inCodeBlock) { - // End code block - xml += this.createCodeBlockXml(codeLines.join('\n')); - codeLines = []; - inCodeBlock = false; - } else { - inCodeBlock = true; - } - continue; - } - - if (inCodeBlock) { - codeLines.push(line); - continue; - } - - // Empty lines - if (!line.trim()) { - xml += ''; - continue; - } - - // Tables - detect table lines - if (line.includes('|') && line.trim().startsWith('|')) { - const tableLines = [line]; - i++; - // Collect all consecutive table lines - while (i < lines.length && lines[i].includes('|')) { - tableLines.push(lines[i]); - i++; - } - i--; // Back up one line - xml += this.createTableXml(tableLines); - continue; - } - - // ASCII flowcharts/diagrams - detect box drawing characters - if (this.isAsciiArt(line)) { - const asciiLines = [line]; - i++; - // Collect all consecutive ASCII art lines - while (i < lines.length && (this.isAsciiArt(lines[i]) || !lines[i].trim())) { - asciiLines.push(lines[i]); - i++; - if (i < lines.length && lines[i].trim() && !this.isAsciiArt(lines[i])) { - break; - } - } - i--; // Back up one line - xml += this.createAsciiArtXml(asciiLines.join('\n')); - continue; - } - - // Headings - strip markdown numbering - if (line.trim().startsWith('#')) { - const level = (line.match(/^#+/) || [''])[0].length; - let text = line.replace(/^#+\s*/, '').trim(); - // Remove markdown numbering like "1.1 Title" -> "Title" - text = text.replace(/^\d+(\.\d+)*\.?\s+/, ''); - xml += this.createHeadingXml(text, level); - continue; - } - - // Blockquotes - if (line.trim().startsWith('>')) { - const text = line.replace(/^>\s*/, '').trim(); - xml += this.createQuoteXml(text); - continue; - } - - // Ordered lists - strip markdown numbering, use template numbering - if (/^\s*\d+\.\s+/.test(line)) { - const text = line.replace(/^\s*\d+\.\s+/, ''); - xml += this.createListItemXml(text, true); - continue; - } - - // Unordered lists - if (/^\s*[-*+]\s+/.test(line)) { - const text = line.replace(/^\s*[-*+]\s+/, ''); - xml += this.createListItemXml(text, false); - continue; - } - - // Horizontal rule - if (/^[-*_]{3,}$/.test(line.trim())) { - xml += this.createHorizontalRuleXml(); - continue; - } - - // Normal paragraph - xml += this.createParagraphXml(line); - } - - return xml; - } - - /** - * Create heading XML using template styles - */ - createHeadingXml(text, level) { - const styleName = `Heading${level}`; - const runs = this.parseInlineFormatting(text); - - return ` - - - - ${runs} - `; - } - - /** - * Create paragraph XML with Normal style - */ - createParagraphXml(text) { - const runs = this.parseInlineFormatting(text); - - return ` - - - - ${runs} - `; - } - - /** - * Create quote XML - */ - createQuoteXml(text) { - const runs = this.parseInlineFormatting(text); - - return ` - - - - ${runs} - `; - } - - /** - * Create list item XML using template numbering - */ - createListItemXml(text, numbered) { - const runs = this.parseInlineFormatting(text); - const numId = numbered ? '1' : '2'; // Template numbering IDs - - return ` - - - - - - - - ${runs} - `; - } - - /** - * Create code block XML - renders each line as separate paragraph - * to preserve exact formatting like in preview (monospace, no wrapping) - * Background shading applied at paragraph level to extend full width - */ - createCodeBlockXml(code) { - const lines = code.split('\n'); - let xml = ''; - - lines.forEach((line, index) => { - const escapedLine = this.escapeXml(line); - - // Each line gets its own paragraph with exact spacing and no wrapping - // Shading (shd) is at paragraph level so background extends full width - xml += ` - - - - - - - - - - - - - - - - - ${escapedLine} - - `; - }); - - return xml; - } - - /** - * Create horizontal rule XML - */ - createHorizontalRuleXml() { - return ` - - - - - - `; - } - - /** - * Create table XML from markdown table lines with template styling - * Uses full-width tables with equal column distribution matching template style - */ - createTableXml(tableLines) { - // Parse table - const rows = []; - for (const line of tableLines) { - // Skip separator lines (e.g., |---|---|) - if (/^\s*\|[\s\-:]+\|\s*$/.test(line)) { - continue; - } - // Split by | and trim - const cells = line.split('|').filter(cell => cell.trim()).map(cell => cell.trim()); - if (cells.length > 0) { - rows.push(cells); - } - } - - if (rows.length === 0) return ''; - - // Calculate number of columns - const numCols = Math.max(...rows.map(row => row.length)); - - // Calculate column width in twips (1440 twips = 1 inch) - // Assume standard page width of 9360 twips (6.5 inches usable) - const totalTableWidth = 9360; - const colWidth = Math.floor(totalTableWidth / numCols); - - // Build table XML - let tableXml = ''; - - // Table properties - use template table style with full width - tableXml += ` - - - - - - - - - - - - - `; - - // Table grid with explicit column widths - tableXml += ''; - for (let i = 0; i < numCols; i++) { - tableXml += ``; - } - tableXml += ''; - - // Table rows - rows.forEach((rowCells, rowIndex) => { - const isHeader = rowIndex === 0; - - tableXml += ''; - - // Row properties for consistent height - tableXml += ''; - - // Pad row to have same number of columns - while (rowCells.length < numCols) { - rowCells.push(''); - } - - rowCells.forEach((cellText, colIndex) => { - tableXml += ''; - tableXml += ''; - - // Cell width - tableXml += ``; - - // Cell shading - orange for header, white for data rows - if (isHeader) { - tableXml += ''; - } else { - tableXml += ''; - } - - // Cell borders - tableXml += '' + - '' + - '' + - '' + - '' + - ''; - - // Cell margins for proper padding - tableXml += '' + - '' + - '' + - '' + - '' + - ''; - - tableXml += ''; - - // Cell content - tableXml += ''; - tableXml += ''; - tableXml += ''; - tableXml += ''; - - const runs = this.parseInlineFormatting(cellText); - - if (isHeader) { - // Header: bold white text - tableXml += runs.replace(//g, ''); - } else { - // Data rows: normal black text - tableXml += runs; - } - - tableXml += ''; - tableXml += ''; - }); - - tableXml += ''; - }); - - tableXml += ''; - - // Add spacing after table - tableXml += ''; - - return tableXml; - } - - /** - * Detect if line contains ASCII art/flowchart characters - */ - isAsciiArt(line) { - // Don't treat markdown tables as ASCII art - if (line.trim().startsWith('|') && line.trim().endsWith('|')) { - // Check if it's a proper markdown table (has multiple cells) - const cells = line.split('|').filter(c => c.trim()); - if (cells.length >= 2) { - return false; - } - } - - // Common ASCII art characters (Unicode box drawing and symbols) - const asciiArtChars = [ - '─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', // Box drawing - '═', '║', '╔', '╗', '╚', '╝', '╠', '╣', '╦', '╩', '╬', // Double box - '╭', '╮', '╯', '╰', // Rounded corners - '▲', '▼', '◄', '►', '♦', '●', '○', '■', '□', '◆', '◇', // Shapes - '↓', '→', '←', '↑', '↔', '↕', '⇒', '⇐', '⇓', '⇑', // Arrows - '┃', '━', '┏', '┓', '┗', '┛', '┣', '┫', '┳', '┻', '╋', // Heavy box - '░', '▒', '▓', '█', // Shading - ]; - - // Check for box drawing characters - if (asciiArtChars.some(char => line.includes(char))) { - return true; - } - - // Check for ASCII box patterns with regular characters - const asciiPatterns = [ - /^\s*\+[-=_+]+\+/, // +-----+ or +=====+ - /^\s*\|[-=_]{3,}\|/, // |-----| - /^\s*[-=_]{5,}$/, // ----- - /^\s*\+[-]+\+[-]+\+/, // +---+---+ - /^\s*\([A-Z][a-z]+\)\s*\|\|/, // (Deformable) || - /^\s*\|\s+[A-Z\s]+[-:]\s+[A-Z]/, // | TILE ADHESIVE - TYPE - /^\s*\|\s*\[[^\]]+\]/, // | [BIS Mark / CE Mark] - /^\s*\|\s{2,}\w+.*\|\|/, // || text || - /^\s*\[[^\]]+\]\s*$/, // [Step in brackets] - /^\s*<[-=]+>/, // <----> or <====> - /^\s*\/[-_\\\/]+\//, // /----/ - /^\s*\*[-=\*]+\*/, // *----* - ]; - - return asciiPatterns.some(pattern => pattern.test(line)); - } - - /** - * Create ASCII art XML with monospace font - * Background shading applied at paragraph level to extend full width - */ - createAsciiArtXml(asciiContent) { - // Split ASCII art into individual lines and create a paragraph for each - const lines = asciiContent.split('\n'); - let xml = ''; - - lines.forEach((line, index) => { - // Shading (shd) is at paragraph level so background extends full width - xml += ` - - - - - - - - - `; - - // Check if line contains arrow characters and color them red - const arrowChars = ['↓', '→', '←', '↑', '▼', '►', '◄', '▲']; - const hasArrow = arrowChars.some(arrow => line.includes(arrow)); - - if (hasArrow) { - // Split line into parts and color arrows red - let remainingLine = line; - - while (remainingLine.length > 0) { - let foundArrow = false; - let arrowIndex = -1; - let foundArrowChar = ''; - - // Find the first arrow in the remaining text - for (const arrow of arrowChars) { - const idx = remainingLine.indexOf(arrow); - if (idx !== -1 && (arrowIndex === -1 || idx < arrowIndex)) { - arrowIndex = idx; - foundArrowChar = arrow; - foundArrow = true; - } - } - - if (foundArrow) { - // Add text before arrow (if any) - if (arrowIndex > 0) { - const beforeArrow = this.escapeXml(remainingLine.substring(0, arrowIndex)); - xml += ` - - - - - - - ${beforeArrow} - `; - } - - // Add arrow in red - const escapedArrow = this.escapeXml(foundArrowChar); - xml += ` - - - - - - - - ${escapedArrow} - `; - - // Continue with remaining text - remainingLine = remainingLine.substring(arrowIndex + foundArrowChar.length); - } else { - // No more arrows, add remaining text - const escapedRemaining = this.escapeXml(remainingLine); - xml += ` - - - - - - - ${escapedRemaining} - `; - remainingLine = ''; - } - } - } else { - // No arrows, just add the line normally - const escapedLine = this.escapeXml(line); - xml += ` - - - - - - - ${escapedLine} - `; - } - - xml += ``; - }); - - return xml; - } - - /** - * Parse inline formatting (bold, italic, code) - */ - parseInlineFormatting(text) { - let xml = ''; - let pos = 0; - - // Patterns for inline formatting - const patterns = [ - { regex: /\*\*\*(.+?)\*\*\*/g, bold: true, italic: true }, - { regex: /\*\*(.+?)\*\*/g, bold: true }, - { regex: /\*(.+?)\*/g, italic: true }, - { regex: /`(.+?)`/g, code: true } - ]; - - // Simple approach: process text sequentially - let remaining = text; - - while (remaining.length > 0) { - let foundMatch = false; - let earliestPos = remaining.length; - let matchedPattern = null; - let match = null; - - // Find earliest match - for (const pattern of patterns) { - pattern.regex.lastIndex = 0; - const m = pattern.regex.exec(remaining); - if (m && m.index < earliestPos) { - earliestPos = m.index; - matchedPattern = pattern; - match = m; - foundMatch = true; - } - } - - if (foundMatch) { - // Add text before match - if (earliestPos > 0) { - xml += this.createRunXml(remaining.substring(0, earliestPos)); - } - - // Add formatted text - xml += this.createRunXml(match[1], matchedPattern.bold, matchedPattern.italic, matchedPattern.code); - - remaining = remaining.substring(earliestPos + match[0].length); - } else { - // No more matches, add remaining text - xml += this.createRunXml(remaining); - break; - } - } - - return xml; - } - - /** - * Create a run (text segment) XML - */ - createRunXml(text, bold = false, italic = false, code = false) { - if (!text) return ''; - - const escapedText = this.escapeXml(text); - let propsXml = ''; - - if (bold) propsXml += ''; - if (italic) propsXml += ''; - if (code) { - propsXml += ''; - propsXml += ''; - } - - propsXml += ''; - - return `${propsXml}${escapedText}`; - } - - /** - * Escape XML special characters - */ - escapeXml(text) { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } -} - -module.exports = WordTemplateExporter; +/** + * Word Template Exporter + * Loads word_template.docx, preserves first 2 pages (cover + TOC), + * and adds markdown content starting from page 3 using template styles + */ + +const fs = require('fs'); +const path = require('path'); +const PizZip = require('pizzip'); +const Docx = require('docx4js').default; + +class WordTemplateExporter { + constructor(templatePath, startPage = 3, pageSettings = null) { + this.templatePath = templatePath || path.join(__dirname, '../word_template.docx'); + this.startPage = startPage; // Which page to start inserting content + this.pageSettings = pageSettings; // Page size and orientation settings + } + + /** + * Convert markdown to Word document using template + */ + async convert(markdownContent, outputPath) { + try { + // Load template + const templateBuffer = fs.readFileSync(this.templatePath); + const zip = new PizZip(templateBuffer); + + // Extract document.xml + let documentXml = zip.file('word/document.xml').asText(); + + // Set page size if settings provided + if (this.pageSettings) { + documentXml = this.setPageSize(documentXml); + } + + // Parse markdown and generate Word XML + const newContentXml = this.markdownToWordXml(markdownContent); + + // Insert new content after the specified start page + const modifiedXml = this.insertContentAfterPage(documentXml, newContentXml, this.startPage); + + // Update the zip with modified XML + zip.file('word/document.xml', modifiedXml); + + // Generate and save the new document + const newDocBuffer = zip.generate({ type: 'nodebuffer' }); + fs.writeFileSync(outputPath, newDocBuffer); + + return outputPath; + } catch (error) { + console.error('Error in Word export:', error); + throw error; + } + } + + /** + * Set page size in document XML + */ + setPageSize(documentXml) { + // Import PAGE_SIZES from main process (need to pass as parameter) + const PAGE_SIZES = { + a4: { width: 11906, height: 16838 }, + a3: { width: 16838, height: 23811 }, + a5: { width: 8391, height: 11906 }, + b4: { width: 14170, height: 20015 }, + b5: { width: 9979, height: 14170 }, + letter: { width: 12240, height: 15840 }, + legal: { width: 12240, height: 20160 }, + tabloid: { width: 15840, height: 24480 } + }; + + let width, height; + const pageSize = PAGE_SIZES[this.pageSettings.size]; + + if (pageSize) { + width = pageSize.width; + height = pageSize.height; + } else { + // Default to A4 + width = 11906; + height = 16838; + } + + // Swap dimensions for landscape + if (this.pageSettings.orientation === 'landscape') { + [width, height] = [height, width]; + } + + // Update all elements in section properties + const pgSzRegex = /]*\/>/g; + let modifiedXml = documentXml.replace(pgSzRegex, () => { + return ``; + }); + + // If no pgSz found, add it to all sectPr elements + if (!pgSzRegex.test(documentXml)) { + const sectPrRegex = /]*>/g; + modifiedXml = modifiedXml.replace(sectPrRegex, (match) => { + return `${match}`; + }); + } + + return modifiedXml; + } + + /** + * Insert markdown content after the specified page in the document + * @param {string} documentXml - The document XML + * @param {string} newContentXml - The new content to insert + * @param {number} afterPage - Insert content after this page number (1-based) + */ + insertContentAfterPage(documentXml, newContentXml, afterPage) { + // Find section breaks that mark page boundaries + // Look for the section properties tag that marks page breaks + const sectionBreakRegex = /]*>[\s\S]*?<\/w:sectPr>/g; + const matches = [...documentXml.matchAll(sectionBreakRegex)]; + + // Calculate which section break to insert after + // Page 1 = before 1st section break + // Page 2 = after 1st section break + // Page 3 = after 2nd section break, etc. + const sectionIndex = afterPage - 1; + + if (matches.length >= sectionIndex && sectionIndex > 0) { + // Insert after the specified section break + const insertPoint = matches[sectionIndex - 1].index + matches[sectionIndex - 1][0].length; + return documentXml.slice(0, insertPoint) + newContentXml + documentXml.slice(insertPoint); + } else if (afterPage === 1 || matches.length === 0) { + // Insert at the beginning (after ) or if no section breaks found + const bodyStart = documentXml.indexOf('') + 8; + return documentXml.slice(0, bodyStart) + newContentXml + documentXml.slice(bodyStart); + } else { + // If not enough section breaks, insert before closing body tag + return documentXml.replace('', newContentXml + ''); + } + } + + /** + * Convert markdown to Word XML format + */ + markdownToWordXml(markdown) { + const lines = markdown.split('\n'); + let xml = ''; + let inCodeBlock = false; + let codeLines = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Handle code blocks + if (line.trim().startsWith('```')) { + if (inCodeBlock) { + // End code block + xml += this.createCodeBlockXml(codeLines.join('\n')); + codeLines = []; + inCodeBlock = false; + } else { + inCodeBlock = true; + } + continue; + } + + if (inCodeBlock) { + codeLines.push(line); + continue; + } + + // Empty lines + if (!line.trim()) { + xml += ''; + continue; + } + + // Tables - detect table lines + if (line.includes('|') && line.trim().startsWith('|')) { + const tableLines = [line]; + i++; + // Collect all consecutive table lines + while (i < lines.length && lines[i].includes('|')) { + tableLines.push(lines[i]); + i++; + } + i--; // Back up one line + xml += this.createTableXml(tableLines); + continue; + } + + // ASCII flowcharts/diagrams - detect box drawing characters + if (this.isAsciiArt(line)) { + const asciiLines = [line]; + i++; + // Collect all consecutive ASCII art lines + while (i < lines.length && (this.isAsciiArt(lines[i]) || !lines[i].trim())) { + asciiLines.push(lines[i]); + i++; + if (i < lines.length && lines[i].trim() && !this.isAsciiArt(lines[i])) { + break; + } + } + i--; // Back up one line + xml += this.createAsciiArtXml(asciiLines.join('\n')); + continue; + } + + // Headings - strip markdown numbering + if (line.trim().startsWith('#')) { + const level = (line.match(/^#+/) || [''])[0].length; + let text = line.replace(/^#+\s*/, '').trim(); + // Remove markdown numbering like "1.1 Title" -> "Title" + text = text.replace(/^\d+(\.\d+)*\.?\s+/, ''); + xml += this.createHeadingXml(text, level); + continue; + } + + // Blockquotes + if (line.trim().startsWith('>')) { + const text = line.replace(/^>\s*/, '').trim(); + xml += this.createQuoteXml(text); + continue; + } + + // Ordered lists - strip markdown numbering, use template numbering + if (/^\s*\d+\.\s+/.test(line)) { + const text = line.replace(/^\s*\d+\.\s+/, ''); + xml += this.createListItemXml(text, true); + continue; + } + + // Unordered lists + if (/^\s*[-*+]\s+/.test(line)) { + const text = line.replace(/^\s*[-*+]\s+/, ''); + xml += this.createListItemXml(text, false); + continue; + } + + // Horizontal rule + if (/^[-*_]{3,}$/.test(line.trim())) { + xml += this.createHorizontalRuleXml(); + continue; + } + + // Normal paragraph + xml += this.createParagraphXml(line); + } + + return xml; + } + + /** + * Create heading XML using template styles + */ + createHeadingXml(text, level) { + const styleName = `Heading${level}`; + const runs = this.parseInlineFormatting(text); + + return ` + + + + ${runs} + `; + } + + /** + * Create paragraph XML with Normal style + */ + createParagraphXml(text) { + const runs = this.parseInlineFormatting(text); + + return ` + + + + ${runs} + `; + } + + /** + * Create quote XML + */ + createQuoteXml(text) { + const runs = this.parseInlineFormatting(text); + + return ` + + + + ${runs} + `; + } + + /** + * Create list item XML using template numbering + */ + createListItemXml(text, numbered) { + const runs = this.parseInlineFormatting(text); + const numId = numbered ? '1' : '2'; // Template numbering IDs + + return ` + + + + + + + + ${runs} + `; + } + + /** + * Create code block XML - renders each line as separate paragraph + * to preserve exact formatting like in preview (monospace, no wrapping) + * Background shading applied at paragraph level to extend full width + */ + createCodeBlockXml(code) { + const lines = code.split('\n'); + let xml = ''; + + lines.forEach((line, index) => { + const escapedLine = this.escapeXml(line); + + // Each line gets its own paragraph with exact spacing and no wrapping + // Shading (shd) is at paragraph level so background extends full width + xml += ` + + + + + + + + + + + + + + + + + ${escapedLine} + + `; + }); + + return xml; + } + + /** + * Create horizontal rule XML + */ + createHorizontalRuleXml() { + return ` + + + + + + `; + } + + /** + * Create table XML from markdown table lines with template styling + * Uses full-width tables with equal column distribution matching template style + */ + createTableXml(tableLines) { + // Parse table + const rows = []; + for (const line of tableLines) { + // Skip separator lines (e.g., |---|---|) + if (/^\s*\|[\s\-:]+\|\s*$/.test(line)) { + continue; + } + // Split by | and trim + const cells = line.split('|').filter(cell => cell.trim()).map(cell => cell.trim()); + if (cells.length > 0) { + rows.push(cells); + } + } + + if (rows.length === 0) return ''; + + // Calculate number of columns + const numCols = Math.max(...rows.map(row => row.length)); + + // Calculate column width in twips (1440 twips = 1 inch) + // Assume standard page width of 9360 twips (6.5 inches usable) + const totalTableWidth = 9360; + const colWidth = Math.floor(totalTableWidth / numCols); + + // Build table XML + let tableXml = ''; + + // Table properties - use template table style with full width + tableXml += ` + + + + + + + + + + + + + `; + + // Table grid with explicit column widths + tableXml += ''; + for (let i = 0; i < numCols; i++) { + tableXml += ``; + } + tableXml += ''; + + // Table rows + rows.forEach((rowCells, rowIndex) => { + const isHeader = rowIndex === 0; + + tableXml += ''; + + // Row properties for consistent height + tableXml += ''; + + // Pad row to have same number of columns + while (rowCells.length < numCols) { + rowCells.push(''); + } + + rowCells.forEach((cellText, colIndex) => { + tableXml += ''; + tableXml += ''; + + // Cell width + tableXml += ``; + + // Cell shading - orange for header, white for data rows + if (isHeader) { + tableXml += ''; + } else { + tableXml += ''; + } + + // Cell borders + tableXml += '' + + '' + + '' + + '' + + '' + + ''; + + // Cell margins for proper padding + tableXml += '' + + '' + + '' + + '' + + '' + + ''; + + tableXml += ''; + + // Cell content + tableXml += ''; + tableXml += ''; + tableXml += ''; + tableXml += ''; + + const runs = this.parseInlineFormatting(cellText); + + if (isHeader) { + // Header: bold white text + tableXml += runs.replace(//g, ''); + } else { + // Data rows: normal black text + tableXml += runs; + } + + tableXml += ''; + tableXml += ''; + }); + + tableXml += ''; + }); + + tableXml += ''; + + // Add spacing after table + tableXml += ''; + + return tableXml; + } + + /** + * Detect if line contains ASCII art/flowchart characters + */ + isAsciiArt(line) { + // Don't treat markdown tables as ASCII art + if (line.trim().startsWith('|') && line.trim().endsWith('|')) { + // Check if it's a proper markdown table (has multiple cells) + const cells = line.split('|').filter(c => c.trim()); + if (cells.length >= 2) { + return false; + } + } + + // Common ASCII art characters (Unicode box drawing and symbols) + const asciiArtChars = [ + '─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', // Box drawing + '═', '║', '╔', '╗', '╚', '╝', '╠', '╣', '╦', '╩', '╬', // Double box + '╭', '╮', '╯', '╰', // Rounded corners + '▲', '▼', '◄', '►', '♦', '●', '○', '■', '□', '◆', '◇', // Shapes + '↓', '→', '←', '↑', '↔', '↕', '⇒', '⇐', '⇓', '⇑', // Arrows + '┃', '━', '┏', '┓', '┗', '┛', '┣', '┫', '┳', '┻', '╋', // Heavy box + '░', '▒', '▓', '█', // Shading + ]; + + // Check for box drawing characters + if (asciiArtChars.some(char => line.includes(char))) { + return true; + } + + // Check for ASCII box patterns with regular characters + const asciiPatterns = [ + /^\s*\+[-=_+]+\+/, // +-----+ or +=====+ + /^\s*\|[-=_]{3,}\|/, // |-----| + /^\s*[-=_]{5,}$/, // ----- + /^\s*\+[-]+\+[-]+\+/, // +---+---+ + /^\s*\([A-Z][a-z]+\)\s*\|\|/, // (Deformable) || + /^\s*\|\s+[A-Z\s]+[-:]\s+[A-Z]/, // | TILE ADHESIVE - TYPE + /^\s*\|\s*\[[^\]]+\]/, // | [BIS Mark / CE Mark] + /^\s*\|\s{2,}\w+.*\|\|/, // || text || + /^\s*\[[^\]]+\]\s*$/, // [Step in brackets] + /^\s*<[-=]+>/, // <----> or <====> + /^\s*\/[-_\\\/]+\//, // /----/ + /^\s*\*[-=\*]+\*/, // *----* + ]; + + return asciiPatterns.some(pattern => pattern.test(line)); + } + + /** + * Create ASCII art XML with monospace font + * Background shading applied at paragraph level to extend full width + */ + createAsciiArtXml(asciiContent) { + // Split ASCII art into individual lines and create a paragraph for each + const lines = asciiContent.split('\n'); + let xml = ''; + + lines.forEach((line, index) => { + // Shading (shd) is at paragraph level so background extends full width + xml += ` + + + + + + + + + `; + + // Check if line contains arrow characters and color them red + const arrowChars = ['↓', '→', '←', '↑', '▼', '►', '◄', '▲']; + const hasArrow = arrowChars.some(arrow => line.includes(arrow)); + + if (hasArrow) { + // Split line into parts and color arrows red + let remainingLine = line; + + while (remainingLine.length > 0) { + let foundArrow = false; + let arrowIndex = -1; + let foundArrowChar = ''; + + // Find the first arrow in the remaining text + for (const arrow of arrowChars) { + const idx = remainingLine.indexOf(arrow); + if (idx !== -1 && (arrowIndex === -1 || idx < arrowIndex)) { + arrowIndex = idx; + foundArrowChar = arrow; + foundArrow = true; + } + } + + if (foundArrow) { + // Add text before arrow (if any) + if (arrowIndex > 0) { + const beforeArrow = this.escapeXml(remainingLine.substring(0, arrowIndex)); + xml += ` + + + + + + + ${beforeArrow} + `; + } + + // Add arrow in red + const escapedArrow = this.escapeXml(foundArrowChar); + xml += ` + + + + + + + + ${escapedArrow} + `; + + // Continue with remaining text + remainingLine = remainingLine.substring(arrowIndex + foundArrowChar.length); + } else { + // No more arrows, add remaining text + const escapedRemaining = this.escapeXml(remainingLine); + xml += ` + + + + + + + ${escapedRemaining} + `; + remainingLine = ''; + } + } + } else { + // No arrows, just add the line normally + const escapedLine = this.escapeXml(line); + xml += ` + + + + + + + ${escapedLine} + `; + } + + xml += ``; + }); + + return xml; + } + + /** + * Parse inline formatting (bold, italic, code) + */ + parseInlineFormatting(text) { + let xml = ''; + let pos = 0; + + // Patterns for inline formatting + const patterns = [ + { regex: /\*\*\*(.+?)\*\*\*/g, bold: true, italic: true }, + { regex: /\*\*(.+?)\*\*/g, bold: true }, + { regex: /\*(.+?)\*/g, italic: true }, + { regex: /`(.+?)`/g, code: true } + ]; + + // Simple approach: process text sequentially + let remaining = text; + + while (remaining.length > 0) { + let foundMatch = false; + let earliestPos = remaining.length; + let matchedPattern = null; + let match = null; + + // Find earliest match + for (const pattern of patterns) { + pattern.regex.lastIndex = 0; + const m = pattern.regex.exec(remaining); + if (m && m.index < earliestPos) { + earliestPos = m.index; + matchedPattern = pattern; + match = m; + foundMatch = true; + } + } + + if (foundMatch) { + // Add text before match + if (earliestPos > 0) { + xml += this.createRunXml(remaining.substring(0, earliestPos)); + } + + // Add formatted text + xml += this.createRunXml(match[1], matchedPattern.bold, matchedPattern.italic, matchedPattern.code); + + remaining = remaining.substring(earliestPos + match[0].length); + } else { + // No more matches, add remaining text + xml += this.createRunXml(remaining); + break; + } + } + + return xml; + } + + /** + * Create a run (text segment) XML + */ + createRunXml(text, bold = false, italic = false, code = false) { + if (!text) return ''; + + const escapedText = this.escapeXml(text); + let propsXml = ''; + + if (bold) propsXml += ''; + if (italic) propsXml += ''; + if (code) { + propsXml += ''; + propsXml += ''; + } + + propsXml += ''; + + return `${propsXml}${escapedText}`; + } + + /** + * Escape XML special characters + */ + escapeXml(text) { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } +} + +module.exports = WordTemplateExporter; diff --git a/test-double-click.md b/test-double-click.md index 94a0220..b87e402 100644 --- a/test-double-click.md +++ b/test-double-click.md @@ -1,12 +1,12 @@ -# Test Double-Click File Opening - -This is a test file to verify double-click functionality. - -## Features to Test -- File should load automatically -- Content should display in editor -- Tab should show filename -- Preview should render markdown - -**This text should be bold** -*This text should be italic* +# Test Double-Click File Opening + +This is a test file to verify double-click functionality. + +## Features to Test +- File should load automatically +- Content should display in editor +- Tab should show filename +- Preview should render markdown + +**This text should be bold** +*This text should be italic* diff --git a/test-export-functionality.js b/test-export-functionality.js index 1dfaefe..aed8e72 100644 --- a/test-export-functionality.js +++ b/test-export-functionality.js @@ -1,49 +1,49 @@ -// Test script to verify export functionality -const { exec } = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -console.log('Testing export functionality...'); - -// Test 1: Check if pandoc is available -console.log('\n1. Checking Pandoc availability...'); -exec('pandoc --version', (error, stdout, stderr) => { - if (error) { - console.log('❌ Pandoc not available:', error.message); - console.log('✅ Built-in HTML and PDF export should work'); - } else { - console.log('✅ Pandoc is available'); - console.log(' Version info:', stdout.split('\n')[0]); - } -}); - -// Test 2: Check if test markdown file exists -console.log('\n2. Checking test file...'); -const testFile = path.join(__dirname, 'test-export.md'); -if (fs.existsSync(testFile)) { - console.log('✅ Test markdown file exists:', testFile); - const content = fs.readFileSync(testFile, 'utf8'); - console.log(' File size:', content.length, 'characters'); -} else { - console.log('❌ Test markdown file not found'); -} - -// Test 3: Check marked library -console.log('\n3. Testing marked library...'); -try { - const marked = require('marked'); - const testMarkdown = '# Test\nThis is a **test** markdown.'; - const html = marked.parse(testMarkdown); - console.log('✅ Marked library working'); - console.log(' Sample output:', html.substring(0, 50) + '...'); -} catch (error) { - console.log('❌ Marked library error:', error.message); -} - -console.log('\n✅ Export functionality test completed!'); -console.log('\nHow to test exports:'); -console.log('1. Open the application'); -console.log('2. Open test-export.md file'); -console.log('3. Try exporting to HTML (should work without Pandoc)'); -console.log('4. Try exporting to PDF (should work without Pandoc)'); +// Test script to verify export functionality +const { exec } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +console.log('Testing export functionality...'); + +// Test 1: Check if pandoc is available +console.log('\n1. Checking Pandoc availability...'); +exec('pandoc --version', (error, stdout, stderr) => { + if (error) { + console.log('❌ Pandoc not available:', error.message); + console.log('✅ Built-in HTML and PDF export should work'); + } else { + console.log('✅ Pandoc is available'); + console.log(' Version info:', stdout.split('\n')[0]); + } +}); + +// Test 2: Check if test markdown file exists +console.log('\n2. Checking test file...'); +const testFile = path.join(__dirname, 'test-export.md'); +if (fs.existsSync(testFile)) { + console.log('✅ Test markdown file exists:', testFile); + const content = fs.readFileSync(testFile, 'utf8'); + console.log(' File size:', content.length, 'characters'); +} else { + console.log('❌ Test markdown file not found'); +} + +// Test 3: Check marked library +console.log('\n3. Testing marked library...'); +try { + const marked = require('marked'); + const testMarkdown = '# Test\nThis is a **test** markdown.'; + const html = marked.parse(testMarkdown); + console.log('✅ Marked library working'); + console.log(' Sample output:', html.substring(0, 50) + '...'); +} catch (error) { + console.log('❌ Marked library error:', error.message); +} + +console.log('\n✅ Export functionality test completed!'); +console.log('\nHow to test exports:'); +console.log('1. Open the application'); +console.log('2. Open test-export.md file'); +console.log('3. Try exporting to HTML (should work without Pandoc)'); +console.log('4. Try exporting to PDF (should work without Pandoc)'); console.log('5. Try exporting to DOCX (requires Pandoc)'); \ No newline at end of file diff --git a/test-export.md b/test-export.md index b30a345..d335247 100644 --- a/test-export.md +++ b/test-export.md @@ -1,26 +1,26 @@ -# Test Document - -This is a test markdown document for export testing. - -## Features - -- **Bold text** -- *Italic text* -- [Links](https://example.com) - -## Code Example - -```javascript -function hello() { - console.log("Hello World!"); -} -``` - -## Table - -| Column 1 | Column 2 | Column 3 | -|----------|----------|----------| -| Value 1 | Value 2 | Value 3 | -| Value 4 | Value 5 | Value 6 | - +# Test Document + +This is a test markdown document for export testing. + +## Features + +- **Bold text** +- *Italic text* +- [Links](https://example.com) + +## Code Example + +```javascript +function hello() { + console.log("Hello World!"); +} +``` + +## Table + +| Column 1 | Column 2 | Column 3 | +|----------|----------|----------| +| Value 1 | Value 2 | Value 3 | +| Value 4 | Value 5 | Value 6 | + This is a test document to verify export functionality. \ No newline at end of file diff --git a/test-file-open.md b/test-file-open.md index 44b76f6..f8aba4c 100644 --- a/test-file-open.md +++ b/test-file-open.md @@ -1,14 +1,14 @@ -# Test File Opening - -This is a test file to verify that double-click file opening works correctly. - -## Features -- **Bold text** -- *Italic text* -- `Code` - -```javascript -console.log("Hello World"); -``` - -This should appear when you double-click to open the file! +# Test File Opening + +This is a test file to verify that double-click file opening works correctly. + +## Features +- **Bold text** +- *Italic text* +- `Code` + +```javascript +console.log("Hello World"); +``` + +This should appear when you double-click to open the file! diff --git a/test.md b/test.md index db1a7b9..0037d3c 100644 --- a/test.md +++ b/test.md @@ -1,23 +1,23 @@ -# Test Document - -This is a **test** markdown document. - -## Features - -- Item 1 -- Item 2 -- Item 3 - -### Code Example - -```javascript -console.log("Hello World"); -``` - -> Tqweqweqweqweqweqweqeweqweqweqwe -dfe - -qwe -qwe -qwe +# Test Document + +This is a **test** markdown document. + +## Features + +- Item 1 +- Item 2 +- Item 3 + +### Code Example + +```javascript +console.log("Hello World"); +``` + +> Tqweqweqweqweqweqweqeweqweqweqwe +dfe + +qwe +qwe +qwe qwe \ No newline at end of file diff --git a/tests/preload.test.js b/tests/preload.test.js index 60d75d9..e412b8c 100644 --- a/tests/preload.test.js +++ b/tests/preload.test.js @@ -1,121 +1,121 @@ -/** - * Tests for Preload Script Security - * Verifies that the IPC bridge is properly configured - */ - -describe('Preload Security', () => { - describe('Allowed Channels', () => { - const EXPECTED_SEND_CHANNELS = [ - 'save-file', - 'save-current-file', - 'set-current-file', - 'save-recent-files', - 'clear-recent-files', - 'renderer-ready', - 'get-theme', - 'do-print', - 'export-with-options', - 'export-spreadsheet', - 'batch-convert', - 'select-folder', - 'universal-convert', - 'universal-convert-batch', - 'get-header-footer-settings', - 'save-header-footer-settings', - 'browse-header-footer-logo', - 'save-header-footer-logo', - 'clear-header-footer-logo', - 'get-page-settings', - 'update-page-settings', - 'set-custom-start-page', - 'process-pdf-operation', - 'get-pdf-page-count', - 'select-pdf-folder' - ]; - - const EXPECTED_RECEIVE_CHANNELS = [ - 'file-new', - 'file-opened', - 'file-save', - 'get-content-for-save', - 'get-content-for-spreadsheet', - 'recent-files-cleared', - 'toggle-preview', - 'toggle-find', - 'theme-changed', - 'theme-data', - 'undo', - 'redo', - 'adjust-font-size', - 'print-preview', - 'print-preview-styled', - 'show-export-dialog', - 'show-batch-dialog', - 'show-universal-converter-dialog', - 'show-table-generator', - 'show-pdf-editor-dialog', - 'conversion-status', - 'conversion-complete', - 'batch-progress', - 'folder-selected', - 'pdf-folder-selected', - 'header-footer-settings-data', - 'header-footer-logo-selected', - 'header-footer-logo-saved', - 'page-settings-data', - 'pdf-page-count', - 'pdf-operation-complete', - 'pdf-operation-error' - ]; - - test('should define all expected send channels', () => { - // This test documents expected channels - expect(EXPECTED_SEND_CHANNELS.length).toBeGreaterThan(0); - }); - - test('should define all expected receive channels', () => { - // This test documents expected channels - expect(EXPECTED_RECEIVE_CHANNELS.length).toBeGreaterThan(0); - }); - }); - - describe('electronAPI Interface', () => { - test('should expose send method', () => { - expect(window.electronAPI.send).toBeDefined(); - expect(typeof window.electronAPI.send).toBe('function'); - }); - - test('should expose on method', () => { - expect(window.electronAPI.on).toBeDefined(); - expect(typeof window.electronAPI.on).toBe('function'); - }); - - test('should expose once method', () => { - expect(window.electronAPI.once).toBeDefined(); - expect(typeof window.electronAPI.once).toBe('function'); - }); - - test('should expose invoke method', () => { - expect(window.electronAPI.invoke).toBeDefined(); - expect(typeof window.electronAPI.invoke).toBe('function'); - }); - - test('should expose file convenience methods', () => { - expect(window.electronAPI.file).toBeDefined(); - expect(window.electronAPI.file.save).toBeDefined(); - expect(window.electronAPI.file.saveCurrent).toBeDefined(); - expect(window.electronAPI.file.setCurrent).toBeDefined(); - }); - - test('should expose theme convenience methods', () => { - expect(window.electronAPI.theme).toBeDefined(); - expect(window.electronAPI.theme.get).toBeDefined(); - }); - - test('should expose pdf convenience methods', () => { - expect(window.electronAPI.pdf).toBeDefined(); - expect(window.electronAPI.pdf.processOperation).toBeDefined(); - expect(window.electronAPI.pdf.getPageCount).toBeDefined(); - }); - }); -}); +/** + * Tests for Preload Script Security + * Verifies that the IPC bridge is properly configured + */ + +describe('Preload Security', () => { + describe('Allowed Channels', () => { + const EXPECTED_SEND_CHANNELS = [ + 'save-file', + 'save-current-file', + 'set-current-file', + 'save-recent-files', + 'clear-recent-files', + 'renderer-ready', + 'get-theme', + 'do-print', + 'export-with-options', + 'export-spreadsheet', + 'batch-convert', + 'select-folder', + 'universal-convert', + 'universal-convert-batch', + 'get-header-footer-settings', + 'save-header-footer-settings', + 'browse-header-footer-logo', + 'save-header-footer-logo', + 'clear-header-footer-logo', + 'get-page-settings', + 'update-page-settings', + 'set-custom-start-page', + 'process-pdf-operation', + 'get-pdf-page-count', + 'select-pdf-folder' + ]; + + const EXPECTED_RECEIVE_CHANNELS = [ + 'file-new', + 'file-opened', + 'file-save', + 'get-content-for-save', + 'get-content-for-spreadsheet', + 'recent-files-cleared', + 'toggle-preview', + 'toggle-find', + 'theme-changed', + 'theme-data', + 'undo', + 'redo', + 'adjust-font-size', + 'print-preview', + 'print-preview-styled', + 'show-export-dialog', + 'show-batch-dialog', + 'show-universal-converter-dialog', + 'show-table-generator', + 'show-pdf-editor-dialog', + 'conversion-status', + 'conversion-complete', + 'batch-progress', + 'folder-selected', + 'pdf-folder-selected', + 'header-footer-settings-data', + 'header-footer-logo-selected', + 'header-footer-logo-saved', + 'page-settings-data', + 'pdf-page-count', + 'pdf-operation-complete', + 'pdf-operation-error' + ]; + + test('should define all expected send channels', () => { + // This test documents expected channels + expect(EXPECTED_SEND_CHANNELS.length).toBeGreaterThan(0); + }); + + test('should define all expected receive channels', () => { + // This test documents expected channels + expect(EXPECTED_RECEIVE_CHANNELS.length).toBeGreaterThan(0); + }); + }); + + describe('electronAPI Interface', () => { + test('should expose send method', () => { + expect(window.electronAPI.send).toBeDefined(); + expect(typeof window.electronAPI.send).toBe('function'); + }); + + test('should expose on method', () => { + expect(window.electronAPI.on).toBeDefined(); + expect(typeof window.electronAPI.on).toBe('function'); + }); + + test('should expose once method', () => { + expect(window.electronAPI.once).toBeDefined(); + expect(typeof window.electronAPI.once).toBe('function'); + }); + + test('should expose invoke method', () => { + expect(window.electronAPI.invoke).toBeDefined(); + expect(typeof window.electronAPI.invoke).toBe('function'); + }); + + test('should expose file convenience methods', () => { + expect(window.electronAPI.file).toBeDefined(); + expect(window.electronAPI.file.save).toBeDefined(); + expect(window.electronAPI.file.saveCurrent).toBeDefined(); + expect(window.electronAPI.file.setCurrent).toBeDefined(); + }); + + test('should expose theme convenience methods', () => { + expect(window.electronAPI.theme).toBeDefined(); + expect(window.electronAPI.theme.get).toBeDefined(); + }); + + test('should expose pdf convenience methods', () => { + expect(window.electronAPI.pdf).toBeDefined(); + expect(window.electronAPI.pdf.processOperation).toBeDefined(); + expect(window.electronAPI.pdf.getPageCount).toBeDefined(); + }); + }); +}); diff --git a/tests/setup.js b/tests/setup.js index 7c6bdaa..c8461d0 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -1,100 +1,100 @@ -/** - * Jest Test Setup - * Provides mocks and utilities for PanConverter tests - */ - -// Mock window.electronAPI for renderer tests -global.window = global.window || {}; -global.window.electronAPI = { - send: jest.fn(), - on: jest.fn(() => jest.fn()), // Returns cleanup function - once: jest.fn(), - invoke: jest.fn(() => Promise.resolve(null)), - removeAllListeners: jest.fn(), - file: { - save: jest.fn(), - saveCurrent: jest.fn(), - setCurrent: jest.fn(), - saveRecent: jest.fn(), - clearRecent: jest.fn(), - rendererReady: jest.fn() - }, - theme: { - get: jest.fn() - }, - print: { - doPrint: jest.fn() - }, - export: { - withOptions: jest.fn(), - spreadsheet: jest.fn() - }, - batch: { - convert: jest.fn(), - selectFolder: jest.fn() - }, - converter: { - convert: jest.fn(), - convertBatch: jest.fn() - }, - headerFooter: { - getSettings: jest.fn(), - saveSettings: jest.fn(), - browseLogo: jest.fn(), - saveLogo: jest.fn(), - clearLogo: jest.fn() - }, - page: { - getSettings: jest.fn(), - updateSettings: jest.fn(), - setCustomStartPage: jest.fn() - }, - pdf: { - processOperation: jest.fn(), - getPageCount: jest.fn(), - selectFolder: jest.fn() - } -}; - -// Mock marked library -global.window.marked = { - parse: jest.fn((text) => `

${text}

`), - setOptions: jest.fn() -}; - -// Mock DOMPurify -global.window.DOMPurify = { - sanitize: jest.fn((html) => html) -}; - -// Mock highlight.js -global.window.hljs = { - highlight: jest.fn((code, options) => ({ value: code })), - highlightAuto: jest.fn((code) => ({ value: code })), - getLanguage: jest.fn(() => true) -}; - -// Mock localStorage -const localStorageMock = { - getItem: jest.fn(), - setItem: jest.fn(), - removeItem: jest.fn(), - clear: jest.fn() -}; -global.localStorage = localStorageMock; - -// Console spy to catch unintended console logs in tests -const originalConsoleError = console.error; -console.error = (...args) => { - // Allow certain expected errors - const message = args[0]?.toString() || ''; - if (message.includes('Warning:') || message.includes('React')) { - return; - } - originalConsoleError.apply(console, args); -}; - -// Cleanup after each test -afterEach(() => { - jest.clearAllMocks(); -}); +/** + * Jest Test Setup + * Provides mocks and utilities for PanConverter tests + */ + +// Mock window.electronAPI for renderer tests +global.window = global.window || {}; +global.window.electronAPI = { + send: jest.fn(), + on: jest.fn(() => jest.fn()), // Returns cleanup function + once: jest.fn(), + invoke: jest.fn(() => Promise.resolve(null)), + removeAllListeners: jest.fn(), + file: { + save: jest.fn(), + saveCurrent: jest.fn(), + setCurrent: jest.fn(), + saveRecent: jest.fn(), + clearRecent: jest.fn(), + rendererReady: jest.fn() + }, + theme: { + get: jest.fn() + }, + print: { + doPrint: jest.fn() + }, + export: { + withOptions: jest.fn(), + spreadsheet: jest.fn() + }, + batch: { + convert: jest.fn(), + selectFolder: jest.fn() + }, + converter: { + convert: jest.fn(), + convertBatch: jest.fn() + }, + headerFooter: { + getSettings: jest.fn(), + saveSettings: jest.fn(), + browseLogo: jest.fn(), + saveLogo: jest.fn(), + clearLogo: jest.fn() + }, + page: { + getSettings: jest.fn(), + updateSettings: jest.fn(), + setCustomStartPage: jest.fn() + }, + pdf: { + processOperation: jest.fn(), + getPageCount: jest.fn(), + selectFolder: jest.fn() + } +}; + +// Mock marked library +global.window.marked = { + parse: jest.fn((text) => `

${text}

`), + setOptions: jest.fn() +}; + +// Mock DOMPurify +global.window.DOMPurify = { + sanitize: jest.fn((html) => html) +}; + +// Mock highlight.js +global.window.hljs = { + highlight: jest.fn((code, options) => ({ value: code })), + highlightAuto: jest.fn((code) => ({ value: code })), + getLanguage: jest.fn(() => true) +}; + +// Mock localStorage +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + clear: jest.fn() +}; +global.localStorage = localStorageMock; + +// Console spy to catch unintended console logs in tests +const originalConsoleError = console.error; +console.error = (...args) => { + // Allow certain expected errors + const message = args[0]?.toString() || ''; + if (message.includes('Warning:') || message.includes('React')) { + return; + } + originalConsoleError.apply(console, args); +}; + +// Cleanup after each test +afterEach(() => { + jest.clearAllMocks(); +}); diff --git a/tests/utils.test.js b/tests/utils.test.js index 1bdc7df..c0c53a7 100644 --- a/tests/utils.test.js +++ b/tests/utils.test.js @@ -1,137 +1,137 @@ -/** - * Tests for Utility Functions - * Tests helper functions that can be extracted and tested - */ - -describe('Utility Functions', () => { - describe('parseCommand', () => { - // This function parses command strings into command and args - function parseCommand(cmdString) { - const parts = []; - let current = ''; - let inQuotes = false; - let quoteChar = ''; - - for (let i = 0; i < cmdString.length; i++) { - const char = cmdString[i]; - if ((char === '"' || char === "'") && !inQuotes) { - inQuotes = true; - quoteChar = char; - } else if (char === quoteChar && inQuotes) { - inQuotes = false; - quoteChar = ''; - } else if (char === ' ' && !inQuotes) { - if (current) { - parts.push(current); - current = ''; - } - } else { - current += char; - } - } - if (current) { - parts.push(current); - } - - return { - command: parts[0], - args: parts.slice(1) - }; - } - - test('should parse simple command', () => { - const result = parseCommand('pandoc input.md -o output.pdf'); - expect(result.command).toBe('pandoc'); - expect(result.args).toEqual(['input.md', '-o', 'output.pdf']); - }); - - test('should handle double-quoted paths', () => { - const result = parseCommand('pandoc "C:/path with spaces/file.md" -o output.pdf'); - expect(result.command).toBe('pandoc'); - expect(result.args).toEqual(['C:/path with spaces/file.md', '-o', 'output.pdf']); - }); - - test('should handle single-quoted paths', () => { - const result = parseCommand("pandoc 'file name.md' -o output.pdf"); - expect(result.command).toBe('pandoc'); - expect(result.args).toEqual(['file name.md', '-o', 'output.pdf']); - }); - - test('should handle multiple options', () => { - const result = parseCommand('pandoc input.md --pdf-engine=xelatex -V geometry:margin=1in -o output.pdf'); - expect(result.command).toBe('pandoc'); - expect(result.args).toContain('--pdf-engine=xelatex'); - expect(result.args).toContain('-V'); - }); - - test('should handle empty command', () => { - const result = parseCommand(''); - expect(result.command).toBeUndefined(); - expect(result.args).toEqual([]); - }); - }); - - describe('hexToRgb', () => { - // This function converts hex colors to RGB - function hexToRgb(hex) { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16) / 255, - g: parseInt(result[2], 16) / 255, - b: parseInt(result[3], 16) / 255 - } : null; - } - - test('should convert black hex to RGB', () => { - const result = hexToRgb('#000000'); - expect(result).toEqual({ r: 0, g: 0, b: 0 }); - }); - - test('should convert white hex to RGB', () => { - const result = hexToRgb('#ffffff'); - expect(result).toEqual({ r: 1, g: 1, b: 1 }); - }); - - test('should convert red hex to RGB', () => { - const result = hexToRgb('#ff0000'); - expect(result.r).toBeCloseTo(1); - expect(result.g).toBeCloseTo(0); - expect(result.b).toBeCloseTo(0); - }); - - test('should handle hex without hash', () => { - const result = hexToRgb('00ff00'); - expect(result.r).toBeCloseTo(0); - expect(result.g).toBeCloseTo(1); - expect(result.b).toBeCloseTo(0); - }); - - test('should return null for invalid hex', () => { - expect(hexToRgb('invalid')).toBeNull(); - expect(hexToRgb('#xyz')).toBeNull(); - }); - }); - - describe('File Path Utilities', () => { - test('should extract file extension', () => { - const getExtension = (filepath) => { - const match = filepath.match(/\.([^/.]+)$/); - return match ? match[1].toLowerCase() : ''; - }; - - expect(getExtension('file.md')).toBe('md'); - expect(getExtension('document.PDF')).toBe('pdf'); - expect(getExtension('path/to/file.docx')).toBe('docx'); - expect(getExtension('noextension')).toBe(''); - }); - - test('should replace file extension', () => { - const replaceExtension = (filepath, newExt) => { - return filepath.replace(/\.[^/.]+$/, `.${newExt}`); - }; - - expect(replaceExtension('file.md', 'pdf')).toBe('file.pdf'); - expect(replaceExtension('path/to/doc.docx', 'html')).toBe('path/to/doc.html'); - }); - }); -}); +/** + * Tests for Utility Functions + * Tests helper functions that can be extracted and tested + */ + +describe('Utility Functions', () => { + describe('parseCommand', () => { + // This function parses command strings into command and args + function parseCommand(cmdString) { + const parts = []; + let current = ''; + let inQuotes = false; + let quoteChar = ''; + + for (let i = 0; i < cmdString.length; i++) { + const char = cmdString[i]; + if ((char === '"' || char === "'") && !inQuotes) { + inQuotes = true; + quoteChar = char; + } else if (char === quoteChar && inQuotes) { + inQuotes = false; + quoteChar = ''; + } else if (char === ' ' && !inQuotes) { + if (current) { + parts.push(current); + current = ''; + } + } else { + current += char; + } + } + if (current) { + parts.push(current); + } + + return { + command: parts[0], + args: parts.slice(1) + }; + } + + test('should parse simple command', () => { + const result = parseCommand('pandoc input.md -o output.pdf'); + expect(result.command).toBe('pandoc'); + expect(result.args).toEqual(['input.md', '-o', 'output.pdf']); + }); + + test('should handle double-quoted paths', () => { + const result = parseCommand('pandoc "C:/path with spaces/file.md" -o output.pdf'); + expect(result.command).toBe('pandoc'); + expect(result.args).toEqual(['C:/path with spaces/file.md', '-o', 'output.pdf']); + }); + + test('should handle single-quoted paths', () => { + const result = parseCommand("pandoc 'file name.md' -o output.pdf"); + expect(result.command).toBe('pandoc'); + expect(result.args).toEqual(['file name.md', '-o', 'output.pdf']); + }); + + test('should handle multiple options', () => { + const result = parseCommand('pandoc input.md --pdf-engine=xelatex -V geometry:margin=1in -o output.pdf'); + expect(result.command).toBe('pandoc'); + expect(result.args).toContain('--pdf-engine=xelatex'); + expect(result.args).toContain('-V'); + }); + + test('should handle empty command', () => { + const result = parseCommand(''); + expect(result.command).toBeUndefined(); + expect(result.args).toEqual([]); + }); + }); + + describe('hexToRgb', () => { + // This function converts hex colors to RGB + function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16) / 255, + g: parseInt(result[2], 16) / 255, + b: parseInt(result[3], 16) / 255 + } : null; + } + + test('should convert black hex to RGB', () => { + const result = hexToRgb('#000000'); + expect(result).toEqual({ r: 0, g: 0, b: 0 }); + }); + + test('should convert white hex to RGB', () => { + const result = hexToRgb('#ffffff'); + expect(result).toEqual({ r: 1, g: 1, b: 1 }); + }); + + test('should convert red hex to RGB', () => { + const result = hexToRgb('#ff0000'); + expect(result.r).toBeCloseTo(1); + expect(result.g).toBeCloseTo(0); + expect(result.b).toBeCloseTo(0); + }); + + test('should handle hex without hash', () => { + const result = hexToRgb('00ff00'); + expect(result.r).toBeCloseTo(0); + expect(result.g).toBeCloseTo(1); + expect(result.b).toBeCloseTo(0); + }); + + test('should return null for invalid hex', () => { + expect(hexToRgb('invalid')).toBeNull(); + expect(hexToRgb('#xyz')).toBeNull(); + }); + }); + + describe('File Path Utilities', () => { + test('should extract file extension', () => { + const getExtension = (filepath) => { + const match = filepath.match(/\.([^/.]+)$/); + return match ? match[1].toLowerCase() : ''; + }; + + expect(getExtension('file.md')).toBe('md'); + expect(getExtension('document.PDF')).toBe('pdf'); + expect(getExtension('path/to/file.docx')).toBe('docx'); + expect(getExtension('noextension')).toBe(''); + }); + + test('should replace file extension', () => { + const replaceExtension = (filepath, newExt) => { + return filepath.replace(/\.[^/.]+$/, `.${newExt}`); + }; + + expect(replaceExtension('file.md', 'pdf')).toBe('file.pdf'); + expect(replaceExtension('path/to/doc.docx', 'html')).toBe('path/to/doc.html'); + }); + }); +});