Release v3.0.0: PDF Viewer & Editor Integration

Major Features:
- Integrated PDF viewer with zoom, page navigation, and fit modes
- PDF editor toolbar with direct access to all PDF operations
- New app branding with docico1.png icon
- ConcreteInfo theme (light & dark variants)
- Comprehensive PDF editing: merge, split, compress, rotate, delete, reorder
- Watermark, encrypt, decrypt, and permissions management
- Overwrite original file option for all PDF operations

UI Improvements:
- PDF viewer takes full screen when active (hides tabs and toolbar)
- Compact modals with reduced font sizes
- Dark theme fixes for icons and dialogs
- Save/Save As options for PDF operations
- Improved PDF toolbar theming

Technical:
- Added isPdfViewerActive state tracking
- Proper PDF document lifecycle management
- Fixed hang issues when switching between PDF and markdown
- All dependencies are open-source (MIT, Apache 2.0, BSD)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-06 18:28:55 +05:30
co-authored by Claude
parent 3a3f0b9ac1
commit b96a6e5bd2
39 changed files with 6245 additions and 454 deletions
+7 -1
View File
@@ -21,7 +21,13 @@
"Bash(findstr:*)", "Bash(findstr:*)",
"Bash(cmd /c \"dir /b H:\\Coding\\pan-converter\\dist\\*.exe H:\\Coding\\pan-converter\\dist\\*.zip 2>nul | findstr 1.9.1\")", "Bash(cmd /c \"dir /b H:\\Coding\\pan-converter\\dist\\*.exe H:\\Coding\\pan-converter\\dist\\*.zip 2>nul | findstr 1.9.1\")",
"Bash(cmd /c \"dir H:\\Coding\\pan-converter\\dist\\*1.9.1*\")", "Bash(cmd /c \"dir H:\\Coding\\pan-converter\\dist\\*1.9.1*\")",
"Bash(gh release create v1.9.1 --title \"v1.9.1: ASCII Art & Code Block Preservation\" --notes \"$(cat <<''EOF''\n# Release v1.9.1: ASCII Art & Code Block Preservation\n\n## 🎯 Overview\nThis release fixes the export of ASCII art, charts, tables, and flowcharts to match the preview exactly. Code blocks now export with perfect monospace formatting, preserving all line breaks, spacing, and box-drawing characters.\n\n## 🎨 What''s Fixed\n\n### ASCII Art & Code Block Preservation\n- **Code blocks now export exactly as shown in preview**\n- Each line rendered separately to preserve exact spacing and alignment\n- Monospace font (Consolas) used consistently across all export formats\n- Box-drawing characters, arrows, and flowcharts render correctly\n- No more text wrapping or broken formatting in exports\n\n### Export Format Improvements\n\n#### DOCX (Enhanced) - Word Template Exporter\n- Each code line rendered as separate paragraph with exact spacing\n- Added `<w:keepLines/>` and `<w:wordWrap w:val=\"0\"/>` XML properties\n- Light gray background (#F5F5F5) for visual distinction\n- Perfect alignment preserved for ASCII art and flowcharts\n\n#### PDF Export (All Methods)\n- Added `-V monofont=\"Consolas\"` for proper code block rendering\n- Added `--highlight-style=tango` for syntax highlighting\n- Works with XeLaTeX, PDFLaTeX, and LuaLaTeX engines\n\n#### HTML & Electron PDF Fallback\n- Enhanced CSS: `white-space: pre`, `word-wrap: normal`\n- Print media queries preserve code block formatting\n- `overflow: visible` prevents content truncation\n\n### Improved ASCII Art Detection\n- Extended Unicode box-drawing character support\n- Added rounded corners: ╭╮╯╰\n- Added heavy box characters: ┃━┏┓┗┛┣┫┳┻╋\n- Added more arrow types: ↔↕⇒⇐⇓⇑\n- Additional ASCII patterns detected\n\n## 📦 Available Downloads\n\nChoose the package that suits your needs:\n\n1. **PanConverter-Setup-1.9.1.exe** (Recommended)\n - Full installer with uninstaller\n - File associations and Start Menu shortcuts\n - ~104 MB\n\n2. **PanConverter-1.9.1-x64.exe**\n - Portable executable, no installation required\n - Run directly from any folder\n - ~104 MB\n\n3. **PanConverter-1.9.1-x64.zip**\n - ZIP archive with unpacked application\n - For manual extraction and deployment\n - ~145 MB (uncompressed files)\n\n## 🔧 Technical Changes\n\n### Code Changes\n- `src/wordTemplateExporter.js`: Enhanced `createCodeBlockXml()` - renders each line separately\n- `src/wordTemplateExporter.js`: Improved `isAsciiArt()` - extended character detection\n- `src/main.js`: Added monofont settings to all PDF export paths\n- `src/main.js`: Enhanced HTML/Electron PDF export CSS\n\n### Key Properties for ASCII Preservation\n```css\npre {\n white-space: pre;\n word-wrap: normal;\n font-family: Consolas, Monaco, ''Courier New'', monospace;\n}\n```\n\n```xml\n<w:pPr>\n <w:keepLines/>\n <w:wordWrap w:val=\"0\"/>\n <w:spacing w:line=\"240\" w:lineRule=\"exact\"/>\n</w:pPr>\n```\n\n## 🔄 Upgrade Notes\n\nIf upgrading from v1.9.0 or earlier:\n- All your settings and preferences will be preserved\n- Recent files list will remain intact\n- Word template selections will be maintained\n- No breaking changes to existing workflows\n\n## 📝 Installation Instructions\n\n### Using the Installer (Recommended)\n1. Download `PanConverter-Setup-1.9.1.exe`\n2. Run the installer\n3. Follow the installation wizard\n4. Launch from Start Menu or Desktop shortcut\n\n### Using Portable Version\n1. Download `PanConverter-1.9.1-x64.exe`\n2. Save to any folder (e.g., Documents, Desktop)\n3. Double-click to run (no installation needed)\n4. Create your own shortcut if desired\n\n## 💬 Feedback\nHave suggestions or found a bug? Please [open an issue](https://github.com/amitwh/pan-converter/issues).\n\n---\n\n**Full Changelog**: https://github.com/amitwh/pan-converter/compare/v1.9.0...v1.9.1\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\" \"dist/PanConverter-1.9.1-x64.exe\" \"dist/PanConverter-1.9.1-x64.zip\" \"dist/PanConverter-Setup-1.9.1.exe\")" "Bash(gh release create v1.9.1 --title \"v1.9.1: ASCII Art & Code Block Preservation\" --notes \"$(cat <<''EOF''\n# Release v1.9.1: ASCII Art & Code Block Preservation\n\n## 🎯 Overview\nThis release fixes the export of ASCII art, charts, tables, and flowcharts to match the preview exactly. Code blocks now export with perfect monospace formatting, preserving all line breaks, spacing, and box-drawing characters.\n\n## 🎨 What''s Fixed\n\n### ASCII Art & Code Block Preservation\n- **Code blocks now export exactly as shown in preview**\n- Each line rendered separately to preserve exact spacing and alignment\n- Monospace font (Consolas) used consistently across all export formats\n- Box-drawing characters, arrows, and flowcharts render correctly\n- No more text wrapping or broken formatting in exports\n\n### Export Format Improvements\n\n#### DOCX (Enhanced) - Word Template Exporter\n- Each code line rendered as separate paragraph with exact spacing\n- Added `<w:keepLines/>` and `<w:wordWrap w:val=\"0\"/>` XML properties\n- Light gray background (#F5F5F5) for visual distinction\n- Perfect alignment preserved for ASCII art and flowcharts\n\n#### PDF Export (All Methods)\n- Added `-V monofont=\"Consolas\"` for proper code block rendering\n- Added `--highlight-style=tango` for syntax highlighting\n- Works with XeLaTeX, PDFLaTeX, and LuaLaTeX engines\n\n#### HTML & Electron PDF Fallback\n- Enhanced CSS: `white-space: pre`, `word-wrap: normal`\n- Print media queries preserve code block formatting\n- `overflow: visible` prevents content truncation\n\n### Improved ASCII Art Detection\n- Extended Unicode box-drawing character support\n- Added rounded corners: ╭╮╯╰\n- Added heavy box characters: ┃━┏┓┗┛┣┫┳┻╋\n- Added more arrow types: ↔↕⇒⇐⇓⇑\n- Additional ASCII patterns detected\n\n## 📦 Available Downloads\n\nChoose the package that suits your needs:\n\n1. **PanConverter-Setup-1.9.1.exe** (Recommended)\n - Full installer with uninstaller\n - File associations and Start Menu shortcuts\n - ~104 MB\n\n2. **PanConverter-1.9.1-x64.exe**\n - Portable executable, no installation required\n - Run directly from any folder\n - ~104 MB\n\n3. **PanConverter-1.9.1-x64.zip**\n - ZIP archive with unpacked application\n - For manual extraction and deployment\n - ~145 MB (uncompressed files)\n\n## 🔧 Technical Changes\n\n### Code Changes\n- `src/wordTemplateExporter.js`: Enhanced `createCodeBlockXml()` - renders each line separately\n- `src/wordTemplateExporter.js`: Improved `isAsciiArt()` - extended character detection\n- `src/main.js`: Added monofont settings to all PDF export paths\n- `src/main.js`: Enhanced HTML/Electron PDF export CSS\n\n### Key Properties for ASCII Preservation\n```css\npre {\n white-space: pre;\n word-wrap: normal;\n font-family: Consolas, Monaco, ''Courier New'', monospace;\n}\n```\n\n```xml\n<w:pPr>\n <w:keepLines/>\n <w:wordWrap w:val=\"0\"/>\n <w:spacing w:line=\"240\" w:lineRule=\"exact\"/>\n</w:pPr>\n```\n\n## 🔄 Upgrade Notes\n\nIf upgrading from v1.9.0 or earlier:\n- All your settings and preferences will be preserved\n- Recent files list will remain intact\n- Word template selections will be maintained\n- No breaking changes to existing workflows\n\n## 📝 Installation Instructions\n\n### Using the Installer (Recommended)\n1. Download `PanConverter-Setup-1.9.1.exe`\n2. Run the installer\n3. Follow the installation wizard\n4. Launch from Start Menu or Desktop shortcut\n\n### Using Portable Version\n1. Download `PanConverter-1.9.1-x64.exe`\n2. Save to any folder (e.g., Documents, Desktop)\n3. Double-click to run (no installation needed)\n4. Create your own shortcut if desired\n\n## 💬 Feedback\nHave suggestions or found a bug? Please [open an issue](https://github.com/amitwh/pan-converter/issues).\n\n---\n\n**Full Changelog**: https://github.com/amitwh/pan-converter/compare/v1.9.0...v1.9.1\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\" \"dist/PanConverter-1.9.1-x64.exe\" \"dist/PanConverter-1.9.1-x64.zip\" \"dist/PanConverter-Setup-1.9.1.exe\")",
"Bash(wc:*)",
"Bash(npm test)",
"Bash(npm run generate-icons:*)",
"Bash(node --check:*)",
"Bash(timeout /t 3)",
"Bash(timeout /t 8)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []
+11
View File
@@ -0,0 +1,11 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always",
"printWidth": 100,
"endOfLine": "auto"
}
+126 -7
View File
@@ -1,11 +1,11 @@
# PanConverter - Claude Development Guide # MarkdownConverter - Claude Development Guide
## Project Overview ## Project Overview
**PanConverter** is a cross-platform Markdown editor and converter powered by Pandoc, built with Electron. It provides professional-grade editing capabilities with comprehensive export options. **MarkdownConverter** (formerly PanConverter) is a cross-platform Markdown editor and universal file converter powered by Pandoc, built with Electron. It provides professional-grade editing capabilities with comprehensive export options, media conversion, and PDF editing tools.
**Current Version**: v1.9.2 **Current Version**: v3.0.0
**Author**: Amit Haridas (amit.wh@gmail.com) **Author**: Amit Haridas / ConcreteInfo (amit.wh@gmail.com)
**License**: MIT **License**: MIT
**Repository**: https://github.com/amitwh/pan-converter **Repository**: https://github.com/amitwh/pan-converter
@@ -26,13 +26,24 @@
``` ```
src/ src/
├── main.js # Electron main process, menu system, IPC handlers ├── main.js # Electron main process, menu system, IPC handlers
├── preload.js # Secure IPC bridge for context isolation (v2.2.0)
├── renderer.js # TabManager class, multi-file editing, event handling ├── renderer.js # TabManager class, multi-file editing, event handling
├── index.html # Application layout with tabbed interface ├── index.html # Application layout with tabbed interface
├── ascii-generator.html # Separate window for ASCII Art Generator (v3.0.0)
├── table-generator.html # Separate window for Table Generator (v3.0.0)
├── styles.css # Base styling with multi-theme support ├── styles.css # Base styling with multi-theme support
── styles-modern.css # Modern glassmorphism UI design system (v1.7.1) ── styles-modern.css # Modern glassmorphism UI design system (v1.7.1)
├── styles-concreteinfo.css # ConcreteInfo brand theme (v3.0.0)
└── wordTemplateExporter.js # Word template-based export (v1.7.9)
assets/ assets/
── icon.png # Application icon ── icon.png # Application icon (512x512)
├── logo.png # ConcreteInfo logo
├── NewIcon.jpg # Source image for icon generation
└── icons/ # Generated icons (16x16 to 1024x1024)
scripts/
└── generate-icons.js # Icon generation script
package.json # Dependencies and build configuration package.json # Dependencies and build configuration
CLAUDE.md # Development documentation for AI assistants CLAUDE.md # Development documentation for AI assistants
@@ -113,7 +124,115 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
## Feature Implementation Guide ## Feature Implementation Guide
### v1.9.1 ASCII Art & Code Block Preservation (Latest) ### v3.0.0 Major UI Overhaul & Media Converters (Latest)
#### Rebranding & UI Improvements
**Renamed to MarkdownConverter with ConcreteInfo Branding** (`src/index.html`, `package.json`)
**Changes:**
- Renamed application from "PanConverter" to "MarkdownConverter"
- Added ConcreteInfo logo in app header
- Removed preview popout button (per user feedback)
- Version updated to 3.0.0
#### ConcreteInfo Theme System
**New Theme Based on Logo Palette** (`src/styles-concreteinfo.css`)
**Color Palette:**
- Dark Gray: #464646
- Medium Gray: #9a9696
- Accent (Red): #e5461f
- Light Gray: #e3e3e3
- Black: #0d0b09
**Theme Variants:**
- Concrete Dark - Dark mode with red accents
- Concrete Light - Clean light mode
- Concrete Warm - Warm tones with gray toolbar
#### Separate Generator Windows
**ASCII Art & Table Generators in Dedicated Windows** (`src/ascii-generator.html`, `src/table-generator.html`, `src/main.js`)
**ASCII Art Generator Features:**
- Text Banner mode with 5 font styles (Standard, Banner, Block, Bubble, Digital)
- Box/Frame mode with 5 styles (Single, Double, Rounded, Bold, ASCII)
- 16 professional templates (arrows, flowcharts, diagrams, decorative)
- Real-time preview with terminal-style display
- One-click insert to editor
**Table Generator Features:**
- Adjustable rows and columns (1-50 rows, 1-20 columns)
- Quick templates (2-column, 3-column, 4x4 grid, etc.)
- Left/Center/Right alignment
- Optional header row
- Fill sample data option
- Live Markdown preview
- Copy or insert to editor
**Access:**
- Tools → ASCII Art Generator (Ctrl+Shift+A)
- Tools → Table Generator (Ctrl+Shift+T)
#### Media Converter Menus
**Separate Menus for Image, Audio, and Video Conversion** (`src/main.js`)
**Image Menu:**
- Convert Image... (single file)
- Batch Convert Images...
- Resize Image...
- Compress Image...
- Rotate Image...
**Audio Menu:**
- Convert Audio...
- Batch Convert Audio...
- Extract Audio from Video...
- Trim Audio...
- Merge Audio Files...
**Video Menu:**
- Convert Video...
- Batch Convert Videos...
- Compress Video...
- Trim Video...
- Extract Frames...
- Convert to GIF...
**Batch Menu:**
- Convert Markdown Folder...
- Batch Image Conversion...
- Batch Audio Conversion...
- Batch Video Conversion...
- Batch PDF Conversion...
#### Enhanced PDF Editor
**Open PDF Files Directly** (`src/main.js`)
- New "Open PDF File..." option in PDF Editor menu
- View and edit PDFs within the application
- All PDF operations available (merge, split, compress, rotate, etc.)
#### About Menu & Dependencies
**Updated Help Menu** (`src/main.js`)
- "About MarkdownConverter" shows app info with ConcreteInfo logo
- "Dependencies & Requirements" lists all required tools with download links:
- Pandoc (required)
- FFmpeg (optional, for media conversion)
- ImageMagick (optional, for image processing)
- LibreOffice (optional, for document conversion)
- MiKTeX/TeX Live (optional, for PDF via LaTeX)
#### Security Enhancements (v2.2.0 continued)
**Secure Preload Script** (`src/preload.js`)
- New IPC channels for media converters
- Secure generator window communication
- Insert content from generator windows
---
### v1.9.1 ASCII Art & Code Block Preservation
#### 🎨 Preserved ASCII Art in Exports #### 🎨 Preserved ASCII Art in Exports
**ASCII Art, Charts, Tables & Flowcharts Now Export Exactly as Previewed** (`src/wordTemplateExporter.js`, `src/main.js`) **ASCII Art, Charts, Tables & Flowcharts Now Export Exactly as Previewed** (`src/wordTemplateExporter.js`, `src/main.js`)
+113 -139
View File
@@ -1,162 +1,141 @@
# PanConverter # PanConverter
A cross-platform Markdown editor and converter powered by Pandoc. A powerful cross-platform Markdown editor and document converter powered by Pandoc, built with Electron.
![PanConverter](assets/icon.png)
## Features ## Features
### ✨ Advanced Markdown Editor ### Markdown Editor
- 🗂️ **Tabbed Interface** - Work with multiple files simultaneously in separate tabs - **Multi-tab editing** - Work on multiple files simultaneously
- 📝 **Rich Text Editor** - Full-featured editor with syntax highlighting and comprehensive toolbar - **Live preview** - Real-time markdown rendering with syntax highlighting
- 🔍 **Find & Replace** - Powerful search and replace with match highlighting and navigation - **Dynamic splitter** - Drag to resize editor and preview panes
- 🔢 **Line Numbers** - Toggle line numbers for easier code editing and navigation - **25+ themes** - Light and dark themes including Atom One Light, Dracula, Nord, Sepia, and more
- ↩️ **Smart Auto-Indentation** - Automatic list continuation and intelligent indentation - **Find & Replace** - Search and replace with regex support
- **Undo/Redo** - Full undo/redo support with keyboard shortcuts - **Line numbers** - Toggle line numbers in the editor
- ⌨️ **Advanced Shortcuts** - Tab indentation, line selection, and smart text formatting - **Auto-save** - Automatic saving every 30 seconds
- 📂 **File Association Support** - Open markdown files directly from file manager - **Math support** - KaTeX integration for mathematical expressions
### 🎨 Themes & Interface ### PDF Viewer & Editor
- 👁️ **Live Preview** - See your markdown rendered in real-time with synchronized scrolling - **Built-in PDF viewer** - Open and view PDF files directly in the app
- 🎨 **Multiple Themes** - Choose from Light, Dark, Solarized, Monokai, or GitHub themes - **Page navigation** - Navigate pages with keyboard or buttons
- 💾 **Auto-Save** - Never lose your work with automatic saving every 30 seconds - **Zoom controls** - Zoom in/out, fit to width, fit to page
- 📊 **Enhanced Statistics** - Detailed document statistics including word count, character count, lines, paragraphs, sentences, and estimated reading time - **Rotation** - Rotate pages left or right
- 🕒 **Recent Files** - Quick access to recently opened files via File menu - **PDF Editor tools**:
- 🧮 **Math Support** - Render mathematical expressions using KaTeX (supports $$, $, \\[\\], \\(\\) delimiters) - 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 & Conversion ### Export Options
- 📄 **Enhanced PDF Export** - Robust PDF generation with multiple LaTeX engine fallbacks (XeLaTeX, PDFLaTeX, wkhtmltopdf) - **PDF** - Export to PDF with customizable page sizes and orientation
- 📄 **Document Export** - Convert to HTML, DOCX, LaTeX, RTF, ODT, EPUB, PowerPoint (PPTX), and OpenDocument Presentation (ODP) - **DOCX** - Standard and Enhanced (template-based) Word export
- 📊 **Spreadsheet Export** - Export markdown tables to CSV format for compatibility with any spreadsheet application - **ODT** - OpenDocument format
- 📥 **Document Import** - Import DOCX, ODT, RTF, HTML, PDF, and presentation files to markdown - **HTML** - Web-ready HTML export
- 📋 **Table Creation Helper** - Built-in table generator for easy markdown table creation - **PowerPoint** - PPTX presentation export
- **EPUB** - E-book format
- **LaTeX** - Academic document format
- **RTF** - Rich Text Format
### 🖥️ Platform Support ### Advanced Features
- **Cross-Platform** - Works seamlessly on Windows, macOS, and Linux - **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 ## Installation
### Prerequisites ### Prerequisites
- [Pandoc](https://pandoc.org/installing.html) must be installed for export functionality - [Node.js](https://nodejs.org/) (v16 or later)
- **Ubuntu/Debian**: `sudo apt-get install pandoc` - [Pandoc](https://pandoc.org/installing.html) (required for export functionality)
- **macOS**: `brew install pandoc`
- **Windows**: Download installer from Pandoc website
### PDF Export Requirements ### Install Dependencies
For optimal PDF export, install a LaTeX engine (recommended):
- **Ubuntu/Debian**: `sudo apt-get install texlive-xetex texlive-latex-base`
- **macOS**: `brew install --cask mactex`
- **Windows**: Install MiKTeX or TeX Live
- **Alternative**: `sudo apt-get install wkhtmltopdf` (fallback option)
### Download
Download the latest release for your platform from the [Releases](https://github.com/amitwh/pan-converter/releases) page.
#### Linux
- **AppImage**: `PanConverter-1.5.0.AppImage` (universal, may require `--no-sandbox` flag)
- **Debian Package**: `pan-converter_1.5.6_amd64.deb`
- **Snap Package**: `pan-converter_1.5.6_amd64.snap`
### Install from Source
```bash ```bash
git clone https://github.com/amitwh/pan-converter.git
cd pan-converter
npm install npm install
```
### Run the Application
```bash
npm start npm start
``` ```
## Usage ### Build for Distribution
```bash
# Windows
npm run build:win
### Basic Workflow # macOS
1. **Write** - Use the editor to write your Markdown content npm run build:mac
2. **Preview** - Toggle the preview pane to see rendered output
3. **Theme** - Choose your preferred theme from the View menu
4. **Export** - Export your document to various formats
### Export Options # Linux
- **Documents**: HTML, PDF, DOCX, LaTeX, RTF, ODT, EPUB npm run build:linux
- **Presentations**: PowerPoint (PPTX), OpenDocument Presentation (ODP) ```
- **Spreadsheets**: CSV format for tables (compatible with Excel, LibreOffice, Google Sheets)
- **Advanced Export Options**: Optional template support, metadata insertion, table of contents, section numbering, and PDF-specific settings
- **Batch Conversion**: Convert entire folders of markdown files with progress tracking
### Import & Conversion
- **Import Documents**: Convert DOCX, ODT, RTF, HTML, PDF, and presentation files to Markdown
- **Cross-Format Conversion**: Convert current file between multiple formats
- **Smart Presentation Handling**: Automatic slide-level formatting for PPTX/ODP exports
### Table Creation
- Click the table button in the toolbar
- Specify number of rows and columns
- Automatically generates properly formatted Markdown tables
## Keyboard Shortcuts ## Keyboard Shortcuts
### File Operations | Action | Shortcut |
- `Ctrl/Cmd + N` - New file/tab |--------|----------|
- `Ctrl/Cmd + T` - New tab | New File | Ctrl+N |
- `Ctrl/Cmd + W` - Close current tab | Open File | Ctrl+O |
- `Ctrl/Cmd + Tab` - Switch to next tab | Open PDF | Ctrl+Shift+O |
- `Ctrl/Cmd + O` - Open file | Save | Ctrl+S |
- `Ctrl/Cmd + S` - Save file | Save As | Ctrl+Shift+S |
- `Ctrl/Cmd + Shift + S` - Save as | Export | Ctrl+E |
- `Ctrl/Cmd + I` - Import document | 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+- |
### Editor Features ## Themes
- `Ctrl/Cmd + F` - Find & Replace
- `Ctrl/Cmd + Z` - Undo
- `Ctrl/Cmd + Shift + Z` - Redo
- `Tab` - Indent lines or insert 4 spaces
- `Shift + Tab` - Outdent selected lines
- `Enter` - Auto-continue lists with proper indentation
### View & Navigation ### Light Themes
- `Ctrl/Cmd + P` - Toggle preview - Atom One Light (Default)
- `Ctrl/Cmd + Enter` - Toggle preview (alternative) - GitHub Light
- `Escape` - Close find dialog - Light
- Solarized Light
- Gruvbox Light
- Ayu Light
- Sepia
- Paper
- Rose Pine Dawn
- Concrete Light
## Building ### 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
```bash ## PDF Viewer
# Install dependencies
npm install
# Generate icons Open PDF files directly in PanConverter:
npm run generate-icons - **File > Open PDF** or **Ctrl+Shift+O**
- Navigate pages with arrow buttons or page input
# Build for current platform - Zoom controls: +/- buttons, Fit Width, Fit Page
npm run build - Rotate pages left or right
- Close PDF to return to editor
# Build for specific platform
npm run build:win # Windows
npm run build:mac # macOS
npm run build:linux # Linux (generates .deb, .AppImage, and .snap)
# Build for all platforms
npm run dist:all
```
## Version History
- **v1.8.0** - Added enhanced Word export with template support to batch converter
- **v1.7.9** - Enhanced Word Export with Template Support: Template-based DOCX generation with custom templates, table styling, ASCII art support, and red-colored flowchart arrows
- **v1.5.6** - (Your release notes here)
- **v1.5.5** - Refactored PDF export, simplified Pandoc pathing, and removed XLSX dependency.
- **v1.3.1** - Bug fixes: Fixed file associations for double-clicking .md files, corrected 50/50 layout alignment for editor/preview panes
- **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
- **v1.2.1** - Comprehensive editor enhancements: Find & Replace, Line Numbers, Undo/Redo, Auto-indentation, PowerPoint export, document conversion menu, table creation helper, spreadsheet export
- **v1.1.0** - Added Excel/ODS spreadsheet export, updated author information, renamed to PanConverter
- **v1.0.0** - Initial release with basic markdown editing, themes, and Pandoc export
## Known Issues
- AppImage may require `--no-sandbox` flag on some Linux systems
- Windows/Mac builds require platform-specific build environments
- Large files may cause performance issues
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request to the [GitHub repository](https://github.com/amitwh/pan-converter).
## License ## License
@@ -164,13 +143,8 @@ MIT License - see LICENSE file for details.
## Author ## Author
**Amit Haridas** - [amit.wh@gmail.com](mailto:amit.wh@gmail.com) Amit Haridas (amit.wh@gmail.com)
## Acknowledgments ## Version
- Built with [Electron](https://www.electronjs.org/) v3.0.0
- Markdown parsing by [marked](https://marked.js.org/)
- Export functionality powered by [Pandoc](https://pandoc.org/)
- Syntax highlighting by [highlight.js](https://highlightjs.org/)
- Spreadsheet export by [XLSX](https://www.npmjs.com/package/xlsx)
- HTML sanitization by [DOMPurify](https://www.npmjs.com/package/dompurify)
Binary file not shown.

After

Width:  |  Height:  |  Size: 869 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 181 KiB

-8
View File
@@ -1,8 +0,0 @@
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<rect width="256" height="256" rx="32" fill="#4A90E2"/>
<g transform="translate(40, 40)">
<path d="M20 40 L20 136 L80 136 L80 100 L56 100 L56 76 L80 76 L80 40 Z" fill="white" opacity="0.9"/>
<path d="M96 40 L96 136 L156 136 L156 100 L132 100 L132 88 L156 88 L156 52 L132 52 L132 40 Z" fill="white" opacity="0.9"/>
</g>
<text x="128" y="200" font-family="Arial, sans-serif" font-size="24" font-weight="bold" text-anchor="middle" fill="white">PAN</text>
</svg>

Before

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+89
View File
@@ -0,0 +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'
}
}
];
+59
View File
@@ -0,0 +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: ['<rootDir>/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
};
+60 -16
View File
@@ -1,13 +1,20 @@
{ {
"name": "pan-converter", "name": "markdown-converter",
"version": "2.1.0", "version": "3.0.0",
"description": "Cross-platform Markdown editor and converter using Pandoc", "description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {
"start": "electron .", "start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1", "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": "electron-builder",
"build:win": "cross-env CSC_LINK=code-signing-cert.pfx CSC_KEY_PASSWORD=PanConverter2024! electron-builder --win", "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:win-unsigned": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --win",
"build:mac": "electron-builder --mac", "build:mac": "electron-builder --mac",
"build:linux": "electron-builder --linux", "build:linux": "electron-builder --linux",
@@ -19,14 +26,29 @@
"markdown", "markdown",
"pandoc", "pandoc",
"converter", "converter",
"editor" "editor",
"pdf",
"audio",
"video",
"image"
], ],
"author": "Amit Haridas <amit.wh@gmail.com>", "author": "ConcreteInfo <amit.wh@gmail.com>",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/amitwh/markdown-converter"
},
"devDependencies": { "devDependencies": {
"@testing-library/dom": "^10.4.1",
"cross-env": "^10.0.0", "cross-env": "^10.0.0",
"electron": "^37.4.0", "electron": "^37.4.0",
"electron-builder": "^26.0.12", "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" "sharp": "^0.34.3"
}, },
"dependencies": { "dependencies": {
@@ -40,14 +62,15 @@
"html2pdf.js": "^0.10.1", "html2pdf.js": "^0.10.1",
"marked": "^16.2.1", "marked": "^16.2.1",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfjs-dist": "^3.11.174",
"pdfkit": "^0.14.0", "pdfkit": "^0.14.0",
"pizzip": "^3.2.0", "pizzip": "^3.2.0",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"build": { "build": {
"appId": "com.panconverter.app", "appId": "com.concreteinfo.markdownconverter",
"productName": "PanConverter", "productName": "MarkdownConverter",
"directories": { "directories": {
"output": "dist" "output": "dist"
}, },
@@ -73,10 +96,18 @@
"description": "Markdown Document", "description": "Markdown Document",
"mimeType": "text/markdown", "mimeType": "text/markdown",
"role": "Editor" "role": "Editor"
},
{
"ext": "pdf",
"name": "PDF Document",
"description": "PDF Document",
"mimeType": "application/pdf",
"role": "Editor"
} }
], ],
"mac": { "mac": {
"category": "public.app-category.productivity" "category": "public.app-category.productivity",
"identity": null
}, },
"win": { "win": {
"target": [ "target": [
@@ -110,7 +141,7 @@
"displayLanguageSelector": true, "displayLanguageSelector": true,
"createDesktopShortcut": true, "createDesktopShortcut": true,
"createStartMenuShortcut": true, "createStartMenuShortcut": true,
"shortcutName": "PanConverter", "shortcutName": "MarkdownConverter",
"runAfterFinish": true, "runAfterFinish": true,
"menuCategory": "Productivity", "menuCategory": "Productivity",
"license": "LICENSE", "license": "LICENSE",
@@ -123,16 +154,29 @@
"target": [ "target": [
"deb", "deb",
"AppImage", "AppImage",
"snap" "snap",
"rpm"
], ],
"category": "Utility" "category": "Utility",
"maintainer": "ConcreteInfo <amit.wh@gmail.com>"
}, },
"deb": { "deb": {
"depends": [ "depends": [
"pandoc" "pandoc",
"ffmpeg",
"imagemagick",
"libreoffice-common"
], ],
"description": "Markdown editor and converter using Pandoc", "description": "Professional Markdown editor and universal file converter",
"maintainer": "Amit Haridas <amit.wh@gmail.com>" "maintainer": "ConcreteInfo <amit.wh@gmail.com>"
},
"rpm": {
"depends": [
"pandoc",
"ffmpeg",
"ImageMagick",
"libreoffice-core"
]
} }
} }
} }
+70 -31
View File
@@ -1,43 +1,82 @@
/**
* Icon Generator for MarkdownConverter
* Generates all required icon sizes from NewIcon.jpg
*/
const sharp = require('sharp'); const sharp = require('sharp');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const sizes = { // Source image
'icon.png': 512, const sourceImage = path.join(__dirname, '..', 'assets', 'NewIcon.jpg');
'icon@2x.png': 1024, const assetsDir = path.join(__dirname, '..', 'assets');
'icon.ico': 256,
'icon.icns': 512
};
const svgPath = path.join(__dirname, '..', 'assets', 'icon.svg'); // Icon sizes needed for different platforms
const svgBuffer = fs.readFileSync(svgPath); const iconSizes = [16, 24, 32, 48, 64, 128, 256, 512, 1024];
async function generateIcons() { async function generateIcons() {
for (const [filename, size] of Object.entries(sizes)) { console.log('Generating icons from NewIcon.jpg...');
const outputPath = path.join(__dirname, '..', 'assets', filename);
if (filename.endsWith('.png')) { // Check if source exists
await sharp(svgBuffer) if (!fs.existsSync(sourceImage)) {
.resize(size, size) console.error('Source image not found:', sourceImage);
.png() process.exit(1);
.toFile(outputPath);
console.log(`Generated ${filename}`);
} else if (filename.endsWith('.ico')) {
// For ICO, we'll just use the PNG version
await sharp(svgBuffer)
.resize(size, size)
.png()
.toFile(outputPath.replace('.ico', '.png'));
console.log(`Generated ${filename.replace('.ico', '.png')} (use png2ico to convert)`);
} else if (filename.endsWith('.icns')) {
// For ICNS, we'll just use the PNG version
await sharp(svgBuffer)
.resize(size, size)
.png()
.toFile(outputPath.replace('.icns', '.png'));
console.log(`Generated ${filename.replace('.icns', '.png')} (use png2icns to convert)`);
} }
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().catch(console.error); generateIcons();
+601
View File
@@ -0,0 +1,601 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASCII Art Generator - MarkdownConverter</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--ci-dark-gray: #464646;
--ci-medium-gray: #9a9696;
--ci-accent: #e5461f;
--ci-light-gray: #e3e3e3;
--ci-black: #0d0b09;
--ci-white: #ffffff;
--ci-bg: #f5f5f5;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', system-ui, sans-serif;
background: var(--ci-bg);
color: var(--ci-dark-gray);
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%);
padding: 16px 24px;
border-bottom: 3px solid var(--ci-accent);
display: flex;
align-items: center;
justify-content: space-between;
}
.header h1 {
color: var(--ci-white);
font-size: 1.25rem;
font-weight: 600;
}
.container {
padding: 24px;
max-width: 100%;
}
.section {
background: var(--ci-white);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.section-title {
font-weight: 600;
margin-bottom: 16px;
color: var(--ci-dark-gray);
font-size: 0.95rem;
}
.mode-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.mode-tab {
padding: 10px 20px;
border: 2px solid var(--ci-light-gray);
background: var(--ci-white);
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.mode-tab:hover {
border-color: var(--ci-accent);
}
.mode-tab.active {
background: var(--ci-accent);
border-color: var(--ci-accent);
color: var(--ci-white);
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 8px;
color: var(--ci-dark-gray);
}
.form-input, .form-select, .form-textarea {
width: 100%;
padding: 10px 14px;
border: 2px solid var(--ci-light-gray);
border-radius: 8px;
font-size: 0.875rem;
font-family: inherit;
transition: border-color 0.2s;
}
.form-input:focus, .form-select:focus, .form-textarea:focus {
outline: none;
border-color: var(--ci-accent);
}
.form-textarea {
min-height: 80px;
resize: vertical;
}
.preview-container {
background: var(--ci-black);
border-radius: 8px;
padding: 16px;
min-height: 200px;
max-height: 350px;
overflow: auto;
}
.preview-content {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
line-height: 1.3;
color: #00ff00;
white-space: pre;
}
.template-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-bottom: 16px;
}
.template-btn {
padding: 8px 12px;
border: 2px solid var(--ci-light-gray);
background: var(--ci-white);
border-radius: 6px;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.template-btn:hover {
border-color: var(--ci-accent);
background: rgba(229, 70, 31, 0.05);
}
.template-btn.active {
border-color: var(--ci-accent);
background: rgba(229, 70, 31, 0.1);
}
.footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 24px;
background: var(--ci-white);
border-top: 1px solid var(--ci-light-gray);
position: sticky;
bottom: 0;
}
.btn {
padding: 10px 24px;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.btn-primary {
background: var(--ci-accent);
color: var(--ci-white);
}
.btn-primary:hover {
background: #c93a18;
transform: translateY(-1px);
}
.btn-secondary {
background: var(--ci-light-gray);
color: var(--ci-dark-gray);
}
.btn-secondary:hover {
background: #d0d0d0;
}
.mode-section {
display: none;
}
.mode-section.active {
display: block;
}
.template-category {
margin-bottom: 16px;
}
.template-category-title {
font-size: 0.8rem;
color: var(--ci-medium-gray);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
</style>
</head>
<body>
<div class="header">
<h1>ASCII Art Generator</h1>
</div>
<div class="container">
<div class="section">
<div class="mode-tabs">
<button class="mode-tab active" data-mode="text">Text Banner</button>
<button class="mode-tab" data-mode="box">Box/Frame</button>
<button class="mode-tab" data-mode="templates">Templates</button>
</div>
<!-- Text Banner Mode -->
<div id="text-mode" class="mode-section active">
<div class="form-group">
<label class="form-label">Text to Convert</label>
<input type="text" id="text-input" class="form-input" placeholder="Enter your text..." maxlength="30">
</div>
<div class="form-group">
<label class="form-label">Style</label>
<select id="font-style" class="form-select">
<option value="standard">Standard</option>
<option value="banner">Banner</option>
<option value="block">Block</option>
<option value="bubble">Bubble</option>
<option value="digital">Digital</option>
</select>
</div>
</div>
<!-- Box/Frame Mode -->
<div id="box-mode" class="mode-section">
<div class="form-group">
<label class="form-label">Text Content</label>
<textarea id="box-text" class="form-textarea" placeholder="Enter text for the box..."></textarea>
</div>
<div class="form-group">
<label class="form-label">Box Style</label>
<select id="box-style" class="form-select">
<option value="single">Single Line</option>
<option value="double">Double Line</option>
<option value="rounded">Rounded</option>
<option value="bold">Bold</option>
<option value="ascii">ASCII (+|-)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Padding</label>
<input type="number" id="box-padding" class="form-input" min="0" max="10" value="2" style="width: 100px;">
</div>
</div>
<!-- Templates Mode -->
<div id="templates-mode" class="mode-section">
<div class="template-category">
<div class="template-category-title">Arrows & Flow</div>
<div class="template-grid">
<button class="template-btn" data-template="arrow-right">Arrow Right</button>
<button class="template-btn" data-template="arrow-down">Arrow Down</button>
<button class="template-btn" data-template="decision">Decision</button>
<button class="template-btn" data-template="process">Process Flow</button>
</div>
</div>
<div class="template-category">
<div class="template-category-title">Diagrams</div>
<div class="template-grid">
<button class="template-btn" data-template="flowchart">Flowchart</button>
<button class="template-btn" data-template="sequence">Sequence</button>
<button class="template-btn" data-template="network">Network</button>
<button class="template-btn" data-template="hierarchy">Hierarchy</button>
</div>
</div>
<div class="template-category">
<div class="template-category-title">Boxes & Containers</div>
<div class="template-grid">
<button class="template-btn" data-template="header">Header</button>
<button class="template-btn" data-template="note">Note Box</button>
<button class="template-btn" data-template="warning">Warning</button>
<button class="template-btn" data-template="info">Info Box</button>
</div>
</div>
<div class="template-category">
<div class="template-category-title">Decorative</div>
<div class="template-grid">
<button class="template-btn" data-template="divider">Divider</button>
<button class="template-btn" data-template="separator">Separator</button>
<button class="template-btn" data-template="banner">Banner</button>
<button class="template-btn" data-template="checklist">Checklist</button>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Preview</div>
<div class="preview-container">
<div id="preview" class="preview-content"></div>
</div>
</div>
</div>
<div class="footer">
<button class="btn btn-secondary" id="btn-generate">Generate Preview</button>
<button class="btn btn-primary" id="btn-insert">Insert to Editor</button>
</div>
<script>
// ASCII Art Generator Logic
const FONTS = {
standard: {
height: 5,
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': ['/---\\', '| |', '\\----', ' |', '----/']
}
},
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': ['██╗', '██║', '██║', '██║', '██║', '╚═╝'],
'J': [' ██╗', ' ██║', ' ██║', '██ ██║', '╚████╔╝', ' ╚═══╝ '],
'K': ['██╗ ██╗', '██║ ██╔╝', '█████╔╝ ', '██╔═██╗ ', '██║ ██╗', '╚═╝ ╚═╝'],
'L': ['██╗ ', '██║ ', '██║ ', '██║ ', '███████╗', '╚══════╝'],
'M': ['███╗ ███╗', '████╗ ████║', '██╔████╔██║', '██║╚██╔╝██║', '██║ ╚═╝ ██║', '╚═╝ ╚═╝'],
'N': ['███╗ ██╗', '████╗ ██║', '██╔██╗ ██║', '██║╚██╗██║', '██║ ╚████║', '╚═╝ ╚═══╝'],
'O': ['█████╗ ', '██╔══██╗', '██║ ██║', '██║ ██║', '╚█████╔╝', ' ╚════╝ '],
'P': ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔═══╝ ', '██║ ', '╚═╝ '],
'Q': ['█████╗ ', '██╔══██╗', '██║ ██║', '██║ ██║', '╚██████╗', ' ╚═══██╝'],
'R': ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔══██╗', '██║ ██║', '╚═╝ ╚═╝'],
'S': ['█████╗ ', '██╔══╝ ', '█████╗ ', '╚══██║ ', '█████║ ', '╚════╝ '],
'T': ['████████╗', '╚══██╔══╝', ' ██║ ', ' ██║ ', ' ██║ ', ' ╚═╝ '],
'U': ['██╗ ██╗', '██║ ██║', '██║ ██║', '██║ ██║', '╚█████╔╝', ' ╚════╝ '],
'V': ['██╗ ██╗', '██║ ██║', '██║ ██║', '╚██╗ ██╔╝', ' ╚████╔╝ ', ' ╚═══╝ '],
'W': ['██╗ ██╗', '██║ ██║', '██║ █╗ ██║', '██║███╗██║', '╚███╔███╔╝', ' ╚══╝╚══╝ '],
'X': ['██╗ ██╗', '╚██╗██╔╝', ' ╚███╔╝ ', ' ██╔██╗ ', '██╔╝ ██╗', '╚═╝ ╚═╝'],
'Y': ['██╗ ██╗', '╚██╗ ██╔╝', ' ╚████╔╝ ', ' ╚██╔╝ ', ' ██║ ', ' ╚═╝ '],
'Z': ['███████╗', '╚════██║', ' ███╔═╝', ' ██╔══╝ ', '███████╗', '╚══════╝'],
' ': [' ', ' ', ' ', ' ', ' ', ' ']
}
}
};
const TEMPLATES = {
'arrow-right': ' ┌─────────────────────┐\n──▶│ Process or Action │──▶\n └─────────────────────┘',
'arrow-down': ' │\n ▼\n┌───────────────┐\n│ Process │\n└───────────────┘\n │\n ▼',
'decision': ' ╱╲\n ╲\n ╱ ? ╲\n ╱ ╲\n ╱────────╲\n ╱ ╲\n YES NO\n │ │\n ▼ ▼',
'process': '┌─────┐ ┌─────┐ ┌─────┐\n│ 1 │──▶│ 2 │──▶│ 3 │\n└─────┘ └─────┘ └─────┘',
'flowchart': '┌─────────────┐\n│ START │\n└──────┬──────┘\n │\n ▼\n┌─────────────┐\n│ Process A │\n└──────┬──────┘\n │\n ▼\n ╱────────╲\n ╱ Decision ╲\n ╲ ? ╱\n ╲────────╱\n │ │\n YES NO\n │ │\n ▼ ▼\n┌──────┐ ┌──────┐\n│ B │ │ C │\n└──────┘ └──────┘',
'sequence': ' User System Database\n │ │ │\n │ Request │ │\n ├──────────►│ │\n │ │ Query │\n │ ├──────────►│\n │ │ │\n │ │ Result │\n │ │◄──────────┤\n │ Response │ │\n │◄──────────┤ │\n │ │ │',
'network': ' ┌─────────┐\n │ Server │\n └────┬────┘\n │\n ┌─────────┼─────────┐\n │ │ │\n┌────┴────┐ ┌──┴──┐ ┌────┴────┐\n│ Client1 │ │ DB │ │ Client2 │\n└─────────┘ └─────┘ └─────────┘',
'hierarchy': ' ┌─────────┐\n │ CEO │\n └────┬────┘\n ┌─────────┼─────────┐\n │ │ │\n ┌───┴───┐ ┌───┴───┐ ┌───┴───┐\n │ VP1 │ │ VP2 │ │ VP3 │\n └───┬───┘ └───┬───┘ └───┬───┘\n │ │ │\n ┌───┴───┐ ┌───┴───┐ ┌───┴───┐\n │ Team1 │ │ Team2 │ │ Team3 │\n └───────┘ └───────┘ └───────┘',
'header': '╔════════════════════════════════════╗\n║ SECTION TITLE ║\n╚════════════════════════════════════╝',
'note': '┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n┃ NOTE: ┃\n┃ This is an important note ┃\n┃ that requires attention! ┃\n┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛',
'warning': '╔════════════════════════════════════╗\n║ ⚠️ WARNING ║\n║ ║\n║ Critical information here! ║\n╚════════════════════════════════════╝',
'info': '╭────────────────────────────────────╮\n│ ℹ️ INFO │\n│ │\n│ Helpful information here. │\n╰────────────────────────────────────╯',
'divider': '════════════════════════════════════════',
'separator': '╭──────────────────────────────────────╮\n│ │\n╰──────────────────────────────────────╯',
'banner': '★══════════════════════════════════════★\n║ YOUR TITLE HERE ║\n★══════════════════════════════════════★',
'checklist': '☐ Task 1 - Not completed\n☑ Task 2 - Completed \n☐ Task 3 - Not completed\n☐ Task 4 - Not completed'
};
const BOX_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: '|' }
};
let currentMode = 'text';
let currentTemplate = null;
// Mode switching
document.querySelectorAll('.mode-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.mode-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentMode = tab.dataset.mode;
document.querySelectorAll('.mode-section').forEach(s => s.classList.remove('active'));
document.getElementById(currentMode + '-mode').classList.add('active');
generatePreview();
});
});
// Template selection
document.querySelectorAll('.template-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.template-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentTemplate = btn.dataset.template;
generatePreview();
});
});
// Generate text banner
function generateTextBanner(text, style) {
const font = FONTS[style] || FONTS.standard;
const lines = Array(font.height).fill('');
for (const char of text.toUpperCase()) {
const charArt = font.chars[char] || font.chars[' '];
if (charArt) {
for (let i = 0; i < font.height; i++) {
lines[i] += charArt[i] + ' ';
}
}
}
return lines.join('\n');
}
// Generate box
function generateBox(text, style, padding) {
const box = BOX_STYLES[style] || BOX_STYLES.single;
const lines = text.split('\n');
const maxLen = Math.max(...lines.map(l => l.length)) + padding * 2;
let result = box.tl + box.h.repeat(maxLen + 2) + box.tr + '\n';
// Add padding lines at top
for (let i = 0; i < Math.floor(padding / 2); i++) {
result += box.v + ' '.repeat(maxLen + 2) + box.v + '\n';
}
// Add text lines
for (const line of lines) {
const paddedLine = ' '.repeat(padding) + line.padEnd(maxLen - padding) + ' ';
result += box.v + ' ' + paddedLine + box.v + '\n';
}
// Add padding lines at bottom
for (let i = 0; i < Math.floor(padding / 2); i++) {
result += box.v + ' '.repeat(maxLen + 2) + box.v + '\n';
}
result += box.bl + box.h.repeat(maxLen + 2) + box.br;
return result;
}
// Generate preview
function generatePreview() {
let result = '';
if (currentMode === 'text') {
const text = document.getElementById('text-input').value || 'HELLO';
const style = document.getElementById('font-style').value;
result = generateTextBanner(text, style);
} else if (currentMode === 'box') {
const text = document.getElementById('box-text').value || 'Your text here';
const style = document.getElementById('box-style').value;
const padding = parseInt(document.getElementById('box-padding').value) || 2;
result = generateBox(text, style, padding);
} else if (currentMode === 'templates') {
result = currentTemplate ? TEMPLATES[currentTemplate] : 'Select a template...';
}
document.getElementById('preview').textContent = result;
}
// Event listeners for live preview
document.getElementById('text-input').addEventListener('input', generatePreview);
document.getElementById('font-style').addEventListener('change', generatePreview);
document.getElementById('box-text').addEventListener('input', generatePreview);
document.getElementById('box-style').addEventListener('change', generatePreview);
document.getElementById('box-padding').addEventListener('input', generatePreview);
document.getElementById('btn-generate').addEventListener('click', generatePreview);
document.getElementById('btn-insert').addEventListener('click', () => {
const content = document.getElementById('preview').textContent;
if (content && window.electronAPI) {
// Wrap in code block for markdown
const wrapped = '```\n' + content + '\n```';
window.electronAPI.send('insert-generated-content', wrapped);
window.close();
}
});
// Initial preview
generatePreview();
</script>
</body>
</html>
+155 -28
View File
@@ -3,12 +3,13 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PanConverter - Markdown Editor</title> <title>MarkdownConverter</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="styles-modern.css"> <link rel="stylesheet" href="styles-modern.css">
<link rel="stylesheet" href="styles-concreteinfo.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
<!-- Mermaid.js for diagram rendering --> <!-- Mermaid.js for diagram rendering -->
@@ -16,6 +17,16 @@
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<!-- App Header with ConcreteInfo Logo -->
<div class="app-header" id="app-header">
<div class="app-header-left">
<img src="../assets/docico1.png" alt="MarkdownConverter" class="app-logo" id="app-logo">
<span class="app-title">MarkdownConverter</span>
</div>
<div class="app-header-right">
<span class="app-version">v3.0.0</span>
</div>
</div>
<div class="tab-bar" id="tab-bar"> <div class="tab-bar" id="tab-bar">
<div class="tab active" data-tab-id="1"> <div class="tab active" data-tab-id="1">
<span class="tab-title">Untitled</span> <span class="tab-title">Untitled</span>
@@ -870,10 +881,13 @@
</div> </div>
</div> </div>
<div class="export-section"> <div class="export-section">
<label>Output File:</label> <label class="checkbox-inline"><input type="checkbox" id="compress-overwrite"> Overwrite original file</label>
</div>
<div class="export-section" id="compress-saveas-section">
<label>Save As:</label>
<div class="folder-input-group"> <div class="folder-input-group">
<input type="text" id="compress-output-path" placeholder="Choose output location..." readonly> <input type="text" id="compress-output-path" placeholder="Select save location..." readonly>
<button id="browse-compress-output">Browse</button> <button id="browse-compress-output">Save As</button>
</div> </div>
</div> </div>
</div> </div>
@@ -901,10 +915,13 @@
</select> </select>
</div> </div>
<div class="export-section"> <div class="export-section">
<label>Output File:</label> <label class="checkbox-inline"><input type="checkbox" id="rotate-overwrite"> Overwrite original file</label>
</div>
<div class="export-section" id="rotate-saveas-section">
<label>Save As:</label>
<div class="folder-input-group"> <div class="folder-input-group">
<input type="text" id="rotate-output-path" placeholder="Choose output location..." readonly> <input type="text" id="rotate-output-path" placeholder="Select save location..." readonly>
<button id="browse-rotate-output">Browse</button> <button id="browse-rotate-output">Save As</button>
</div> </div>
</div> </div>
</div> </div>
@@ -924,10 +941,13 @@
<small>Separate page numbers/ranges with commas</small> <small>Separate page numbers/ranges with commas</small>
</div> </div>
<div class="export-section"> <div class="export-section">
<label>Output File:</label> <label class="checkbox-inline"><input type="checkbox" id="delete-overwrite"> Overwrite original file</label>
</div>
<div class="export-section" id="delete-saveas-section">
<label>Save As:</label>
<div class="folder-input-group"> <div class="folder-input-group">
<input type="text" id="delete-output-path" placeholder="Choose output location..." readonly> <input type="text" id="delete-output-path" placeholder="Select save location..." readonly>
<button id="browse-delete-output">Browse</button> <button id="browse-delete-output">Save As</button>
</div> </div>
</div> </div>
</div> </div>
@@ -953,10 +973,13 @@
</div> </div>
</div> </div>
<div class="export-section"> <div class="export-section">
<label>Output File:</label> <label class="checkbox-inline"><input type="checkbox" id="reorder-overwrite"> Overwrite original file</label>
</div>
<div class="export-section" id="reorder-saveas-section">
<label>Save As:</label>
<div class="folder-input-group"> <div class="folder-input-group">
<input type="text" id="reorder-output-path" placeholder="Choose output location..." readonly> <input type="text" id="reorder-output-path" placeholder="Select save location..." readonly>
<button id="browse-reorder-output">Browse</button> <button id="browse-reorder-output">Save As</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1009,10 +1032,13 @@
<input type="text" id="watermark-custom-pages" placeholder="e.g., 1-5, 7, 9-12" class="hidden"> <input type="text" id="watermark-custom-pages" placeholder="e.g., 1-5, 7, 9-12" class="hidden">
</div> </div>
<div class="export-section"> <div class="export-section">
<label>Output File:</label> <label class="checkbox-inline"><input type="checkbox" id="watermark-overwrite"> Overwrite original file</label>
</div>
<div class="export-section" id="watermark-saveas-section">
<label>Save As:</label>
<div class="folder-input-group"> <div class="folder-input-group">
<input type="text" id="watermark-output-path" placeholder="Choose output location..." readonly> <input type="text" id="watermark-output-path" placeholder="Select save location..." readonly>
<button id="browse-watermark-output">Browse</button> <button id="browse-watermark-output">Save As</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1058,10 +1084,13 @@
<small>These permissions apply when owner password is set</small> <small>These permissions apply when owner password is set</small>
</div> </div>
<div class="export-section"> <div class="export-section">
<label>Output File:</label> <label class="checkbox-inline"><input type="checkbox" id="encrypt-overwrite"> Overwrite original file</label>
</div>
<div class="export-section" id="encrypt-saveas-section">
<label>Save As:</label>
<div class="folder-input-group"> <div class="folder-input-group">
<input type="text" id="encrypt-output-path" placeholder="Choose output location..." readonly> <input type="text" id="encrypt-output-path" placeholder="Select save location..." readonly>
<button id="browse-encrypt-output">Browse</button> <button id="browse-encrypt-output">Save As</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1081,10 +1110,13 @@
<small>Enter the password to unlock and remove protection</small> <small>Enter the password to unlock and remove protection</small>
</div> </div>
<div class="export-section"> <div class="export-section">
<label>Output File:</label> <label class="checkbox-inline"><input type="checkbox" id="decrypt-overwrite"> Overwrite original file</label>
</div>
<div class="export-section" id="decrypt-saveas-section">
<label>Save As:</label>
<div class="folder-input-group"> <div class="folder-input-group">
<input type="text" id="decrypt-output-path" placeholder="Choose output location..." readonly> <input type="text" id="decrypt-output-path" placeholder="Select save location..." readonly>
<button id="browse-decrypt-output">Browse</button> <button id="browse-decrypt-output">Save As</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1121,10 +1153,13 @@
</div> </div>
</div> </div>
<div class="export-section"> <div class="export-section">
<label>Output File:</label> <label class="checkbox-inline"><input type="checkbox" id="permissions-overwrite"> Overwrite original file</label>
</div>
<div class="export-section" id="permissions-saveas-section">
<label>Save As:</label>
<div class="folder-input-group"> <div class="folder-input-group">
<input type="text" id="permissions-output-path" placeholder="Choose output location..." readonly> <input type="text" id="permissions-output-path" placeholder="Select save location..." readonly>
<button id="browse-permissions-output">Browse</button> <button id="browse-permissions-output">Save As</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1271,12 +1306,104 @@
</div> </div>
<div class="pane-resizer" id="pane-resizer-1" title="Drag to resize"></div> <div class="pane-resizer" id="pane-resizer-1" title="Drag to resize"></div>
<div id="preview-pane-1" class="pane"> <div id="preview-pane-1" class="pane">
<div class="preview-header">
<button class="preview-popout-btn" id="preview-popout-1" title="Pop out preview in new window"></button>
</div>
<div id="preview-1" class="preview-content"></div> <div id="preview-1" class="preview-content"></div>
</div> </div>
</div> </div>
<!-- PDF Viewer Container (hidden by default) -->
<div id="pdf-viewer-container" class="pdf-viewer-container hidden">
<div class="pdf-viewer-toolbar pdf-toolbar-main">
<!-- Navigation -->
<button id="pdf-prev-page" title="Previous Page"></button>
<span id="pdf-page-info">Page <input type="number" id="pdf-page-input" min="1" value="1"> of <span id="pdf-total-pages">0</span></span>
<button id="pdf-next-page" title="Next Page"></button>
<div class="pdf-toolbar-separator"></div>
<!-- Zoom -->
<button id="pdf-zoom-out" title="Zoom Out"></button>
<span id="pdf-zoom-level">100%</span>
<button id="pdf-zoom-in" title="Zoom In">+</button>
<button id="pdf-fit-width" title="Fit Width"></button>
<button id="pdf-fit-page" title="Fit Page"></button>
<div class="pdf-toolbar-separator"></div>
<!-- Rotation -->
<button id="pdf-rotate-left" title="Rotate Left"></button>
<button id="pdf-rotate-right" title="Rotate Right"></button>
<div class="pdf-toolbar-separator"></div>
<!-- PDF Editor Functions -->
<button id="pdf-tb-merge" class="pdf-editor-btn" title="Merge PDFs">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="9" rx="1"></rect>
<rect x="14" y="3" width="7" height="9" rx="1"></rect>
<path d="M12 15v6m-3-3h6"></path>
</svg>
<span>Merge</span>
</button>
<button id="pdf-tb-split" class="pdf-editor-btn" title="Split PDF">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="9" rx="1"></rect>
<path d="M12 12v9M9 18l3 3 3-3"></path>
</svg>
<span>Split</span>
</button>
<button id="pdf-tb-compress" class="pdf-editor-btn" title="Compress PDF">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 14h16M4 10h16M7 6l5 4 5-4M7 18l5-4 5 4"></path>
</svg>
<span>Compress</span>
</button>
<button id="pdf-tb-rotate" class="pdf-editor-btn" title="Rotate Pages">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 11-6.219-8.56"></path>
<polyline points="21 3 21 9 15 9"></polyline>
</svg>
<span>Rotate</span>
</button>
<button id="pdf-tb-delete" class="pdf-editor-btn" title="Delete Pages">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14z"></path>
</svg>
<span>Delete</span>
</button>
<button id="pdf-tb-reorder" class="pdf-editor-btn" title="Reorder Pages">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
<polyline points="7 9 4 12 7 15"></polyline>
<polyline points="17 9 20 12 17 15"></polyline>
</svg>
<span>Reorder</span>
</button>
<button id="pdf-tb-watermark" class="pdf-editor-btn" title="Add Watermark">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2L2 7l10 5 10-5-10-5z"></path>
<path d="M2 17l10 5 10-5M2 12l10 5 10-5"></path>
</svg>
<span>Watermark</span>
</button>
<button id="pdf-tb-encrypt" class="pdf-editor-btn" title="Encrypt PDF">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0110 0v4"></path>
</svg>
<span>Encrypt</span>
</button>
<button id="pdf-tb-decrypt" class="pdf-editor-btn" title="Decrypt PDF">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 019.9-1"></path>
</svg>
<span>Decrypt</span>
</button>
<div class="pdf-toolbar-separator"></div>
<!-- File Info and Close -->
<span id="pdf-filename" class="pdf-filename"></span>
<button id="pdf-close" title="Close PDF"></button>
</div>
<div id="pdf-viewer" class="pdf-viewer">
<canvas id="pdf-canvas"></canvas>
</div>
</div>
</div> </div>
<div class="status-bar"> <div class="status-bar">
+551 -88
View File
@@ -1,7 +1,7 @@
const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron'); const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const { exec } = require('child_process'); const { exec, execFile, spawn } = require('child_process');
const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib'); const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib');
const WordTemplateExporter = require('./wordTemplateExporter'); const WordTemplateExporter = require('./wordTemplateExporter');
@@ -25,12 +25,95 @@ function getPandocPath() {
function checkPandocAvailable() { function checkPandocAvailable() {
return new Promise((resolve) => { return new Promise((resolve) => {
const pandocPath = getPandocPath(); const pandocPath = getPandocPath();
exec(`${pandocPath} --version`, (error) => { execFile(pandocPath, ['--version'], (error) => {
resolve(!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 // Simple storage implementation to replace electron-store
const settingsPath = path.join(app.getPath('userData'), 'settings.json'); const settingsPath = path.join(app.getPath('userData'), 'settings.json');
const store = { const store = {
@@ -180,7 +263,7 @@ if (!gotTheLock) {
}); });
} }
// Check if pandoc is available // Check if pandoc is available (using execFile for consistency)
function checkPandocAvailability() { function checkPandocAvailability() {
return new Promise((resolve) => { return new Promise((resolve) => {
if (pandocAvailable !== null) { if (pandocAvailable !== null) {
@@ -188,7 +271,7 @@ function checkPandocAvailability() {
return; return;
} }
exec('pandoc --version', (error, stdout, stderr) => { execFile('pandoc', ['--version'], (error, stdout, stderr) => {
pandocAvailable = !error; pandocAvailable = !error;
resolve(pandocAvailable); resolve(pandocAvailable);
}); });
@@ -284,6 +367,11 @@ function createMenu() {
accelerator: 'CmdOrCtrl+O', accelerator: 'CmdOrCtrl+O',
click: openFile click: openFile
}, },
{
label: 'Open PDF',
accelerator: 'CmdOrCtrl+Shift+O',
click: openPdfFile
},
{ {
label: 'Save', label: 'Save',
accelerator: 'CmdOrCtrl+S', accelerator: 'CmdOrCtrl+S',
@@ -401,29 +489,33 @@ function createMenu() {
{ {
label: 'Theme', label: 'Theme',
submenu: [ 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: 'Light', click: () => setTheme('light') },
{ label: 'Dark', click: () => setTheme('dark') }, { label: 'Solarized Light', click: () => setTheme('solarized') },
{ label: 'Solarized', click: () => setTheme('solarized') }, { label: 'Gruvbox Light', click: () => setTheme('gruvbox-light') },
{ label: 'Monokai', click: () => setTheme('monokai') }, { label: 'Ayu Light', click: () => setTheme('ayu-light') },
{ label: 'GitHub', click: () => setTheme('github') }, { 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' }, { type: 'separator' },
// Dark Themes
{ label: 'Dark', click: () => setTheme('dark') },
{ label: 'One Dark', click: () => setTheme('onedark') },
{ label: 'Dracula', click: () => setTheme('dracula') }, { label: 'Dracula', click: () => setTheme('dracula') },
{ label: 'Nord', click: () => setTheme('nord') }, { label: 'Nord', click: () => setTheme('nord') },
{ label: 'One Dark', click: () => setTheme('onedark') }, { label: 'Monokai', click: () => setTheme('monokai') },
{ label: 'Atom One Light', click: () => setTheme('atomonelight') },
{ label: 'Material', click: () => setTheme('material') }, { label: 'Material', click: () => setTheme('material') },
{ label: 'Gruvbox Dark', click: () => setTheme('gruvbox-dark') }, { label: 'Gruvbox Dark', click: () => setTheme('gruvbox-dark') },
{ label: 'Gruvbox Light', click: () => setTheme('gruvbox-light') },
{ label: 'Tokyo Night', click: () => setTheme('tokyonight') }, { label: 'Tokyo Night', click: () => setTheme('tokyonight') },
{ label: 'Palenight', click: () => setTheme('palenight') }, { label: 'Palenight', click: () => setTheme('palenight') },
{ label: 'Ayu Dark', click: () => setTheme('ayu-dark') }, { label: 'Ayu Dark', click: () => setTheme('ayu-dark') },
{ label: 'Ayu Light', click: () => setTheme('ayu-light') },
{ label: 'Ayu Mirage', click: () => setTheme('ayu-mirage') }, { label: 'Ayu Mirage', click: () => setTheme('ayu-mirage') },
{ label: 'Oceanic Next', click: () => setTheme('oceanic-next') }, { label: 'Oceanic Next', click: () => setTheme('oceanic-next') },
{ label: 'Cobalt2', click: () => setTheme('cobalt2') }, { label: 'Cobalt2', click: () => setTheme('cobalt2') },
{ type: 'separator' },
{ label: 'Concrete Dark', click: () => setTheme('concrete-dark') }, { label: 'Concrete Dark', click: () => setTheme('concrete-dark') },
{ label: 'Concrete Light', click: () => setTheme('concrete-light') },
{ label: 'Concrete Warm', click: () => setTheme('concrete-warm') } { label: 'Concrete Warm', click: () => setTheme('concrete-warm') }
] ]
}, },
@@ -461,8 +553,25 @@ function createMenu() {
label: 'Batch', label: 'Batch',
submenu: [ submenu: [
{ {
label: 'Convert Folder...', label: 'Convert Markdown Folder...',
click: () => showBatchConversionDialog() 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')
} }
] ]
}, },
@@ -473,27 +582,18 @@ function createMenu() {
label: 'Universal File Converter...', label: 'Universal File Converter...',
accelerator: 'CmdOrCtrl+Shift+C', accelerator: 'CmdOrCtrl+Shift+C',
click: () => showUniversalConverterDialog() click: () => showUniversalConverterDialog()
},
{
type: 'separator'
},
{
label: 'About Converter',
click: () => {
dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'About Universal Converter',
message: 'Open-Source File Converter',
detail: 'PanConverter includes a powerful open-source file conversion system.\n\nSupported Converters:\n• LibreOffice - Document conversions (DOCX, PDF, ODT, etc.)\n• ImageMagick - Image format conversions\n• FFmpeg - Media file conversions\n• Pandoc - Document markup conversions\n\nFeatures:\n• 100% open-source and free\n• No API keys required\n• Runs completely offline\n• Supports 100+ file formats\n• High-quality professional conversions',
buttons: ['OK']
});
}
} }
] ]
}, },
{ {
label: 'PDF Editor', label: 'PDF Editor',
submenu: [ submenu: [
{
label: 'Open PDF File...',
accelerator: 'CmdOrCtrl+Shift+O',
click: () => openPDFFile()
},
{ type: 'separator' },
{ {
label: 'Merge PDFs...', label: 'Merge PDFs...',
click: () => showPDFEditorDialog('merge') click: () => showPDFEditorDialog('merge')
@@ -570,12 +670,15 @@ function createMenu() {
submenu: [ submenu: [
{ {
label: 'Table Generator', label: 'Table Generator',
click: () => mainWindow.webContents.send('show-table-generator') accelerator: 'CmdOrCtrl+Shift+T',
click: () => openTableGenerator()
}, },
{ {
label: 'ASCII Art Generator', label: 'ASCII Art Generator',
click: () => mainWindow.webContents.send('show-ascii-generator') accelerator: 'CmdOrCtrl+Shift+A',
click: () => openAsciiGenerator()
}, },
{ type: 'separator' },
{ {
label: 'Document Compare', label: 'Document Compare',
click: () => mainWindow.webContents.send('show-document-compare') click: () => mainWindow.webContents.send('show-document-compare')
@@ -586,20 +689,26 @@ function createMenu() {
label: 'Help', label: 'Help',
submenu: [ submenu: [
{ {
label: 'About', label: 'About MarkdownConverter',
click: () => { click: () => showAboutDialog()
dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'About PanConverter',
message: 'PanConverter',
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 2.1.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\n✨ New in v2.1.0:\n• Subtle & Small Preview Popout Button - Cleaner, minimalist design\n• Simplified Table Headers - Light gray background with better readability\n• Comprehensive Format-to-Markdown Conversion - Support for 30+ formats (RST, Textile, MediaWiki, Org-mode, AsciiDoc, CSV, JSON, and more)\n• Exhaustive ASCII Art Generator - 5 text banner styles (Standard, Banner, Block, Bubble, Digital) and 19 professional templates\n• Enhanced Templates - Flowcharts, sequence diagrams, network diagrams, hierarchy trees, timelines, note boxes, warning boxes, and decorative elements\n\n✨ Features from v2.0.0:\n• Export Profiles - Save and load export configurations\n• Mermaid.js diagram support - Render flowcharts, sequence diagrams, and more\n• Command Palette (Ctrl+Shift+P) - Quick access to all commands\n• GitHub Light/Dark preview themes - Beautiful code preview styling\n• Table Generator - Interactive table creation tool\n• Resizable Preview Pane - Drag divider to adjust editor/preview sizes\n• Pop-out Preview Window - Open preview in separate window with live sync\n\nCore Features:\n• Configurable page sizes (A3, A4, A5, B4, B5, Letter, Legal, Tabloid, Custom) with orientation support\n• Page size support for all export formats (PDF, DOCX, ODT, PowerPoint) and batch conversion\n• Preserved ASCII art, charts, tables, flowcharts in exports with full-width background\n• Custom Headers & Footers for PDF, DOCX, ODT, and PowerPoint exports\n• Dynamic field support: $PAGE$, $TOTAL$, $DATE$, $TIME$, $TITLE$, $AUTHOR$, $FILENAME$\n• Logo/image embedding in headers and footers\n• Modern glassmorphism UI with gradient backgrounds\n• Enhanced PDF export via Word template with configurable start page\n• Streamlined PDF Editor UI (merge, split, compress, rotate, watermark, encrypt)\n• Universal File Converter (LibreOffice, ImageMagick, FFmpeg, Pandoc)\n• Windows Explorer context menu integration\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Real-time preview updates while typing\n• Full toolbar markdown editing functions\n• Enhanced PDF export with built-in Electron fallback\n• Enhanced Word export with template support (single file & batch)\n• File association support for .md files\n• Command-line interface for batch conversion\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• 22 beautiful themes (including Dracula, Nord, Tokyo Night, Gruvbox, Ayu, Concrete, and more)\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Undo/redo functionality\n• Live word count and statistics',
buttons: ['OK']
});
}
}, },
{ type: 'separator' },
{
label: 'Dependencies & Requirements',
click: () => showDependenciesDialog()
},
{ type: 'separator' },
{ {
label: 'Documentation', label: 'Documentation',
click: () => shell.openExternal('https://github.com/amitwh/pan-converter') 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')
} }
] ]
} }
@@ -609,6 +718,237 @@ function createMenu() {
Menu.setApplicationMenu(menu); 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 = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>About MarkdownConverter</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: linear-gradient(135deg, #464646 0%, #0d0b09 100%);
color: #e3e3e3;
padding: 30px;
text-align: center;
}
.logo { width: 120px; margin-bottom: 20px; border-radius: 20px; }
h1 { font-size: 24px; margin-bottom: 5px; color: #fff; }
.version { color: #e5461f; font-size: 14px; margin-bottom: 20px; }
.company { display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 20px; }
.company-logo { height: 24px; }
.section { background: rgba(255,255,255,0.1); border-radius: 10px; padding: 15px; margin: 15px 0; text-align: left; }
.section h3 { color: #e5461f; font-size: 14px; margin-bottom: 10px; }
.section p, .section li { font-size: 12px; line-height: 1.6; }
ul { padding-left: 20px; }
.footer { margin-top: 20px; font-size: 11px; color: #9a9696; }
a { color: #e5461f; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<img src="${iconBase64}" class="logo" alt="MarkdownConverter">
<h1>MarkdownConverter</h1>
<div class="version">Version 3.0.0</div>
<div class="company">
<span>by</span>
<img src="${logoBase64}" class="company-logo" alt="ConcreteInfo">
</div>
<div class="section">
<h3>Features</h3>
<ul>
<li>Professional Markdown editing with syntax highlighting</li>
<li>Universal file converter (Image, Audio, Video, PDF)</li>
<li>Batch conversion for all media types</li>
<li>Advanced PDF Editor (merge, split, watermark, encrypt)</li>
<li>Export to PDF, DOCX, HTML, LaTeX, EPUB & more</li>
<li>23+ beautiful themes including ConcreteInfo theme</li>
<li>ASCII Art & Table generators</li>
<li>Code file syntax highlighting</li>
</ul>
</div>
<div class="section">
<h3>Contact</h3>
<p>Email: <a href="mailto:amit.wh@gmail.com">amit.wh@gmail.com</a></p>
<p>Website: <a href="https://github.com/amitwh/markdown-converter">GitHub Repository</a></p>
</div>
<div class="footer">
<p>License: MIT</p>
<p>© 2024-2025 ConcreteInfo. All rights reserved.</p>
</div>
</body>
</html>`;
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 = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Dependencies & Requirements</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: #f5f5f5;
color: #464646;
padding: 30px;
}
h1 { color: #e5461f; font-size: 20px; margin-bottom: 20px; border-bottom: 2px solid #e5461f; padding-bottom: 10px; }
h2 { color: #464646; font-size: 16px; margin: 20px 0 10px; }
.dep-card {
background: #fff;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.dep-name { font-weight: bold; color: #0d0b09; }
.dep-desc { font-size: 13px; color: #666; margin: 5px 0; }
.dep-link { color: #e5461f; font-size: 12px; text-decoration: none; }
.dep-link:hover { text-decoration: underline; }
.required { background: #fff3cd; border-left: 4px solid #ffc107; }
.optional { background: #d4edda; border-left: 4px solid #28a745; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; margin-left: 10px; }
.tag-required { background: #ffc107; color: #000; }
.tag-optional { background: #28a745; color: #fff; }
</style>
</head>
<body>
<h1>Dependencies & Requirements</h1>
<h2>Required Dependencies</h2>
<div class="dep-card required">
<div class="dep-name">Pandoc <span class="tag tag-required">Required</span></div>
<div class="dep-desc">Universal document converter. Required for most export formats (PDF, DOCX, LaTeX, EPUB).</div>
<a class="dep-link" href="https://pandoc.org/installing.html" target="_blank">https://pandoc.org/installing.html</a>
</div>
<h2>Optional Dependencies (for extended features)</h2>
<div class="dep-card optional">
<div class="dep-name">FFmpeg <span class="tag tag-optional">Optional</span></div>
<div class="dep-desc">Required for audio/video conversion and processing.</div>
<a class="dep-link" href="https://ffmpeg.org/download.html" target="_blank">https://ffmpeg.org/download.html</a>
</div>
<div class="dep-card optional">
<div class="dep-name">ImageMagick <span class="tag tag-optional">Optional</span></div>
<div class="dep-desc">Required for image format conversion and processing.</div>
<a class="dep-link" href="https://imagemagick.org/script/download.php" target="_blank">https://imagemagick.org/script/download.php</a>
</div>
<div class="dep-card optional">
<div class="dep-name">LibreOffice <span class="tag tag-optional">Optional</span></div>
<div class="dep-desc">Required for enhanced document conversion (Office formats).</div>
<a class="dep-link" href="https://www.libreoffice.org/download/download/" target="_blank">https://www.libreoffice.org/download/download/</a>
</div>
<div class="dep-card optional">
<div class="dep-name">MiKTeX / TeX Live <span class="tag tag-optional">Optional</span></div>
<div class="dep-desc">Required for PDF export via LaTeX (higher quality PDFs).</div>
<a class="dep-link" href="https://miktex.org/download" target="_blank">https://miktex.org/download</a>
</div>
<h2>Bundled Libraries</h2>
<div class="dep-card">
<div class="dep-name">pdf-lib</div>
<div class="dep-desc">PDF manipulation library for merge, split, watermark, and encryption features.</div>
</div>
<div class="dep-card">
<div class="dep-name">marked</div>
<div class="dep-desc">Markdown parser for preview rendering.</div>
</div>
<div class="dep-card">
<div class="dep-name">highlight.js</div>
<div class="dep-desc">Syntax highlighting for code blocks.</div>
</div>
<div class="dep-card">
<div class="dep-name">DOMPurify</div>
<div class="dep-desc">HTML sanitization for security.</div>
</div>
</body>
</html>`;
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() { function openFile() {
const files = dialog.showOpenDialogSync(mainWindow, { const files = dialog.showOpenDialogSync(mainWindow, {
properties: ['openFile'], properties: ['openFile'],
@@ -625,6 +965,20 @@ function openFile() {
} }
} }
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() { function saveAsFile() {
const file = dialog.showSaveDialogSync(mainWindow, { const file = dialog.showSaveDialogSync(mainWindow, {
defaultExt: '.md', defaultExt: '.md',
@@ -1133,15 +1487,15 @@ async function exportPDFViaWordTemplate() {
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage); const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
await exporter.convert(content, tempDocxPath); await exporter.convert(content, tempDocxPath);
// Step 2: Convert DOCX to PDF using LibreOffice // Step 2: Convert DOCX to PDF using LibreOffice (using execFile for safety)
const soffice = process.platform === 'win32' const soffice = process.platform === 'win32'
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"' ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe'
: 'soffice'; : 'soffice';
const outputDir = path.dirname(result.filePath); const outputDir = path.dirname(result.filePath);
const convertCmd = `${soffice} --headless --convert-to pdf --outdir "${outputDir}" "${tempDocxPath}"`; const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath];
exec(convertCmd, (error, stdout, stderr) => { execFile(soffice, sofficeArgs, (error, stdout, stderr) => {
// Clean up temporary DOCX file // Clean up temporary DOCX file
try { try {
fs.unlinkSync(tempDocxPath); fs.unlinkSync(tempDocxPath);
@@ -1190,26 +1544,34 @@ function showPDFEditorDialog(operation) {
mainWindow.webContents.send('show-pdf-editor-dialog', operation); mainWindow.webContents.send('show-pdf-editor-dialog', operation);
} }
// Check if conversion tool is available // 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) { function checkConverterAvailable(tool) {
return new Promise((resolve) => { return new Promise((resolve) => {
let command; const isWin = process.platform === 'win32';
const locateCmd = isWin ? 'where' : 'which';
let toolName;
switch (tool) { switch (tool) {
case 'libreoffice': case 'libreoffice':
command = process.platform === 'win32' ? 'where soffice' : 'which soffice'; toolName = 'soffice';
break; break;
case 'imagemagick': case 'imagemagick':
command = process.platform === 'win32' ? 'where magick' : 'which convert'; toolName = isWin ? 'magick' : 'convert';
break; break;
case 'ffmpeg': case 'ffmpeg':
command = process.platform === 'win32' ? 'where ffmpeg' : 'which ffmpeg'; toolName = 'ffmpeg';
break; break;
default: default:
resolve(false); resolve(false);
return; return;
} }
exec(command, (error) => { execFile(locateCmd, [toolName], (error) => {
resolve(!error); resolve(!error);
}); });
}); });
@@ -1230,26 +1592,27 @@ ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, file
mainWindow.webContents.send('conversion-status', 'Converting file...'); mainWindow.webContents.send('conversion-status', 'Converting file...');
const outputPath = filePath.replace(/\.[^/.]+$/, `.${toFormat}`); const outputPath = filePath.replace(/\.[^/.]+$/, `.${toFormat}`);
let conversionCmd; let conversionInfo;
switch (tool) { switch (tool) {
case 'libreoffice': case 'libreoffice':
conversionCmd = convertWithLibreOffice(filePath, toFormat, outputPath); conversionInfo = convertWithLibreOffice(filePath, toFormat, outputPath);
break; break;
case 'imagemagick': case 'imagemagick':
conversionCmd = convertWithImageMagick(filePath, outputPath); conversionInfo = convertWithImageMagick(filePath, outputPath);
break; break;
case 'ffmpeg': case 'ffmpeg':
conversionCmd = convertWithFFmpeg(filePath, outputPath); conversionInfo = convertWithFFmpeg(filePath, outputPath);
break; break;
case 'pandoc': case 'pandoc':
conversionCmd = `pandoc "${filePath}" -o "${outputPath}"`; conversionInfo = convertWithPandoc(filePath, outputPath);
break; break;
default: default:
throw new Error(`Unknown conversion tool: ${tool}`); throw new Error(`Unknown conversion tool: ${tool}`);
} }
exec(conversionCmd, (error, stdout, stderr) => { // Use execFile for safety (prevents command injection)
execFile(conversionInfo.command, conversionInfo.args, (error, stdout, stderr) => {
if (error) { if (error) {
mainWindow.webContents.send('conversion-complete', { mainWindow.webContents.send('conversion-complete', {
success: false, success: false,
@@ -1294,11 +1657,11 @@ ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, file
} }
}); });
// LibreOffice conversion command builder // LibreOffice conversion - returns {command, args} for execFile (safer than exec)
function convertWithLibreOffice(inputFile, outputFormat, outputPath) { function convertWithLibreOffice(inputFile, outputFormat, outputPath) {
const outputDir = path.dirname(outputPath); const outputDir = path.dirname(outputPath);
const soffice = process.platform === 'win32' const soffice = process.platform === 'win32'
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"' ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe'
: 'soffice'; : 'soffice';
// LibreOffice conversion format mapping // LibreOffice conversion format mapping
@@ -1321,19 +1684,36 @@ function convertWithLibreOffice(inputFile, outputFormat, outputPath) {
const format = formatMap[outputFormat] || outputFormat; const format = formatMap[outputFormat] || outputFormat;
// Use headless mode for conversion // Return command and args for execFile
return `${soffice} --headless --convert-to ${format} --outdir "${outputDir}" "${inputFile}"`; return {
command: soffice,
args: ['--headless', '--convert-to', format, '--outdir', outputDir, inputFile]
};
} }
// ImageMagick conversion command builder // ImageMagick conversion - returns {command, args} for execFile
function convertWithImageMagick(inputFile, outputPath) { function convertWithImageMagick(inputFile, outputPath) {
const magick = process.platform === 'win32' ? 'magick' : 'convert'; const magick = process.platform === 'win32' ? 'magick' : 'convert';
return `${magick} "${inputFile}" "${outputPath}"`; return {
command: magick,
args: [inputFile, outputPath]
};
} }
// FFmpeg conversion command builder // FFmpeg conversion - returns {command, args} for execFile
function convertWithFFmpeg(inputFile, outputPath) { function convertWithFFmpeg(inputFile, outputPath) {
return `ffmpeg -i "${inputFile}" "${outputPath}" -y`; 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) { function performExportWithOptions(format, options) {
@@ -1449,8 +1829,8 @@ function performExportWithOptions(format, options) {
pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"'; pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"';
} }
// Try with specified PDF engine // Try with specified PDF engine (using runPandocCmd for safety)
exec(pandocCmd, (error) => { runPandocCmd(pandocCmd, (error) => {
if (error) { if (error) {
// Try fallback engines if the specified one fails // Try fallback engines if the specified one fails
const fallbackEngines = ['pdflatex', 'lualatex']; const fallbackEngines = ['pdflatex', 'lualatex'];
@@ -1544,7 +1924,8 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro
} }
} }
exec(pandocCmd, (error) => { // Use runPandocCmd for safety (prevents command injection)
runPandocCmd(pandocCmd, (error) => {
if (error) { if (error) {
tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error); tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error);
} else { } else {
@@ -1562,11 +1943,11 @@ function showExportSuccess(outputFile) {
}); });
} }
// Helper function to export with pandoc (general) // Helper function to export with pandoc (general) - uses runPandocCmd for safety
function exportWithPandoc(pandocCmd, outputFile, format) { function exportWithPandoc(pandocCmd, outputFile, format) {
console.log(`Executing Pandoc command: ${pandocCmd}`); console.log(`Executing Pandoc command: ${pandocCmd}`);
exec(pandocCmd, async (error, stdout, stderr) => { runPandocCmd(pandocCmd, async (error, stdout, stderr) => {
if (error) { if (error) {
console.error(`Pandoc error for ${format}:`, error); console.error(`Pandoc error for ${format}:`, error);
console.error(`Pandoc stderr:`, stderr); console.error(`Pandoc stderr:`, stderr);
@@ -1642,14 +2023,14 @@ function exportWithPandoc(pandocCmd, outputFile, format) {
}); });
} }
// Helper function to export PDF with pandoc (with fallbacks) // Helper function to export PDF with pandoc (with fallbacks) - uses runPandocCmd for safety
function exportWithPandocPDF(pandocCmd, outputFile) { function exportWithPandocPDF(pandocCmd, outputFile) {
exec(pandocCmd, (error, stdout, stderr) => { runPandocCmd(pandocCmd, (error, stdout, stderr) => {
if (error) { if (error) {
console.log('XeLaTeX failed, trying PDFLaTeX...'); console.log('XeLaTeX failed, trying PDFLaTeX...');
// Fallback to pdflatex // Fallback to pdflatex
const fallbackCmd = pandocCmd.replace('--pdf-engine=xelatex', '--pdf-engine=pdflatex'); const fallbackCmd = pandocCmd.replace('--pdf-engine=xelatex', '--pdf-engine=pdflatex');
exec(fallbackCmd, (fallbackError, fallbackStdout, fallbackStderr) => { runPandocCmd(fallbackCmd, (fallbackError, fallbackStdout, fallbackStderr) => {
if (fallbackError) { if (fallbackError) {
console.log('PDFLaTeX failed, trying Electron PDF...'); console.log('PDFLaTeX failed, trying Electron PDF...');
// Final fallback to Electron PDF // Final fallback to Electron PDF
@@ -1970,10 +2351,10 @@ function importDocument() {
additionalOptions = '--from=json -t markdown'; additionalOptions = '--from=json -t markdown';
} }
// Convert to markdown using pandoc // Convert to markdown using pandoc (using runPandocCmd for safety)
const pandocCmd = `${getPandocPath()} "${inputFile}" -t markdown ${additionalOptions} -o "${outputFile}"`; const pandocCmd = `${getPandocPath()} "${inputFile}" -t markdown ${additionalOptions} -o "${outputFile}"`;
exec(pandocCmd, (error, stdout, stderr) => { runPandocCmd(pandocCmd, (error, stdout, stderr) => {
if (error) { 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.`); 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 { } else {
@@ -2014,7 +2395,7 @@ ipcMain.on('save-current-file', (event, content) => {
}); });
ipcMain.on('get-theme', (event) => { ipcMain.on('get-theme', (event) => {
const theme = store.get('theme', 'github'); const theme = store.get('theme', 'atomonelight');
event.reply('theme-changed', theme); event.reply('theme-changed', theme);
}); });
@@ -2288,15 +2669,15 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage); const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
await exporter.convert(content, tempDocxPath); await exporter.convert(content, tempDocxPath);
// Step 2: Convert DOCX to PDF using LibreOffice // Step 2: Convert DOCX to PDF using LibreOffice (using execFile for safety)
const soffice = process.platform === 'win32' const soffice = process.platform === 'win32'
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"' ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe'
: 'soffice'; : 'soffice';
const outputDir = path.dirname(outputFile); const outputDir = path.dirname(outputFile);
const convertCmd = `${soffice} --headless --convert-to pdf --outdir "${outputDir}" "${tempDocxPath}"`; const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath];
exec(convertCmd, (error, stdout, stderr) => { execFile(soffice, sofficeArgs, (error, stdout, stderr) => {
// Clean up temporary DOCX file // Clean up temporary DOCX file
try { try {
fs.unlinkSync(tempDocxPath); fs.unlinkSync(tempDocxPath);
@@ -2447,8 +2828,8 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
pandocCmd += ` --variable footer="${footerText}"`; pandocCmd += ` --variable footer="${footerText}"`;
} }
// Execute conversion // Execute conversion (using runPandocCmd for safety)
exec(pandocCmd, async (error, stdout, stderr) => { runPandocCmd(pandocCmd, async (error, stdout, stderr) => {
if (!error) { if (!error) {
// Add headers/footers to DOCX if enabled // Add headers/footers to DOCX if enabled
if (format === 'docx' && headerFooterSettings.enabled) { if (format === 'docx' && headerFooterSettings.enabled) {
@@ -2563,10 +2944,10 @@ function performCLIConversion(inputPath, format) {
console.log(`Converting "${path.basename(inputPath)}" to ${format.toUpperCase()}...`); console.log(`Converting "${path.basename(inputPath)}" to ${format.toUpperCase()}...`);
// Use existing export functions but with CLI output // Use existing export functions but with CLI output (using runPandocCmd for safety)
const pandocCommand = buildPandocCommand(content, format, outputPath); const pandocCommand = buildPandocCommand(content, format, outputPath);
exec(pandocCommand, (error, stdout, stderr) => { runPandocCmd(pandocCommand, (error, stdout, stderr) => {
if (error) { if (error) {
console.error(`Conversion failed: ${error.message}`); console.error(`Conversion failed: ${error.message}`);
if (stderr) console.error(`Details: ${stderr}`); if (stderr) console.error(`Details: ${stderr}`);
@@ -2576,9 +2957,10 @@ function performCLIConversion(inputPath, format) {
console.log(`Successfully converted to: ${outputPath}`); console.log(`Successfully converted to: ${outputPath}`);
// Show Windows notification // Show Windows notification (using exec for PowerShell is acceptable here - hardcoded command)
if (process.platform === 'win32') { if (process.platform === 'win32') {
exec(`powershell -Command "New-BurntToastNotification -Text 'PanConverter', 'File converted to ${format.toUpperCase()}' -AppLogo '${path.join(__dirname, '../assets/icon.png')}'"`, () => {}); const iconPath = path.join(__dirname, '../assets/icon.png');
execFile('powershell', ['-Command', `New-BurntToastNotification -Text 'PanConverter', 'File converted to ${format.toUpperCase()}' -AppLogo '${iconPath}'`], () => {});
} }
app.quit(); app.quit();
@@ -3330,3 +3712,84 @@ ipcMain.on('select-pdf-folder', (event, inputId) => {
event.reply('pdf-folder-selected', { inputId, path: 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);
}
});
+376
View File
@@ -0,0 +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<any>} 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);
+471 -16
View File
@@ -1,3 +1,8 @@
/**
* MarkdownConverter Renderer Process
* @version 3.0.0
*/
const { ipcRenderer } = require('electron'); const { ipcRenderer } = require('electron');
const marked = require('marked'); const marked = require('marked');
const DOMPurify = require('dompurify'); const DOMPurify = require('dompurify');
@@ -10,7 +15,6 @@ marked.setOptions({
try { try {
return hljs.highlight(code, { language: lang }).value; return hljs.highlight(code, { language: lang }).value;
} catch (err) { } catch (err) {
// Fallback to auto highlighting if language-specific highlighting fails
console.warn('Syntax highlighting failed for language:', lang, err.message); console.warn('Syntax highlighting failed for language:', lang, err.message);
} }
} }
@@ -308,6 +312,11 @@ class TabManager {
if (!tab || !preview) return; if (!tab || !preview) return;
try { try {
// Check if libraries are available
if (!marked || !DOMPurify) {
preview.innerHTML = '<p style="color: red; padding: 20px;">Error: Required libraries (marked/DOMPurify) not loaded. Check internet connection.</p>';
return;
}
const html = marked.parse(tab.content); const html = marked.parse(tab.content);
const sanitizedHtml = DOMPurify.sanitize(html); const sanitizedHtml = DOMPurify.sanitize(html);
preview.innerHTML = sanitizedHtml; preview.innerHTML = sanitizedHtml;
@@ -1043,7 +1052,6 @@ class TabManager {
this.updateTabBar(); this.updateTabBar();
// Notify main process about current file for exports // Notify main process about current file for exports
const { ipcRenderer } = require('electron');
ipcRenderer.send('set-current-file', filePath); ipcRenderer.send('set-current-file', filePath);
console.log('File opened successfully'); console.log('File opened successfully');
@@ -2308,12 +2316,12 @@ ipcRenderer.on('show-table-generator', () => {
}); });
// Show PDF Editor Dialog // Show PDF Editor Dialog
ipcRenderer.on('show-pdf-editor-dialog', (event, operation) => { ipcRenderer.on('show-pdf-editor-dialog', (event, operation, openedFilePath) => {
currentPDFOperation = operation; currentPDFOperation = operation;
showPDFEditorDialog(operation); showPDFEditorDialog(operation, openedFilePath);
}); });
function showPDFEditorDialog(operation) { function showPDFEditorDialog(operation, openedFilePath = null) {
const dialog = document.getElementById('pdf-editor-dialog'); const dialog = document.getElementById('pdf-editor-dialog');
const title = document.getElementById('pdf-editor-title'); const title = document.getElementById('pdf-editor-title');
@@ -2329,43 +2337,82 @@ function showPDFEditorDialog(operation) {
sectionId = 'pdf-merge-section'; sectionId = 'pdf-merge-section';
titleText = 'Merge PDFs'; titleText = 'Merge PDFs';
mergeFilePaths = []; mergeFilePaths = [];
// If we have an opened file, add it as the first file to merge
if (openedFilePath) {
mergeFilePaths.push(openedFilePath);
}
updateMergeFilesList(); updateMergeFilesList();
break; break;
case 'split': case 'split':
sectionId = 'pdf-split-section'; sectionId = 'pdf-split-section';
titleText = 'Split PDF'; titleText = 'Split PDF';
// Pre-fill input path if we have an opened file
if (openedFilePath) {
document.getElementById('split-input-path').value = openedFilePath;
}
break; break;
case 'compress': case 'compress':
sectionId = 'pdf-compress-section'; sectionId = 'pdf-compress-section';
titleText = 'Compress PDF'; titleText = 'Compress PDF';
if (openedFilePath) {
document.getElementById('compress-input-path').value = openedFilePath;
}
break; break;
case 'rotate': case 'rotate':
sectionId = 'pdf-rotate-section'; sectionId = 'pdf-rotate-section';
titleText = 'Rotate Pages'; titleText = 'Rotate Pages';
if (openedFilePath) {
const rotateInput = document.getElementById('rotate-input-path');
if (rotateInput) rotateInput.value = openedFilePath;
}
break; break;
case 'delete': case 'delete':
sectionId = 'pdf-delete-section'; sectionId = 'pdf-delete-section';
titleText = 'Delete Pages'; titleText = 'Delete Pages';
if (openedFilePath) {
const deleteInput = document.getElementById('delete-input-path');
if (deleteInput) deleteInput.value = openedFilePath;
}
break; break;
case 'reorder': case 'reorder':
sectionId = 'pdf-reorder-section'; sectionId = 'pdf-reorder-section';
titleText = 'Reorder Pages'; titleText = 'Reorder Pages';
if (openedFilePath) {
const reorderInput = document.getElementById('reorder-input-path');
if (reorderInput) reorderInput.value = openedFilePath;
}
break; break;
case 'watermark': case 'watermark':
sectionId = 'pdf-watermark-section'; sectionId = 'pdf-watermark-section';
titleText = 'Add Watermark'; titleText = 'Add Watermark';
if (openedFilePath) {
const watermarkInput = document.getElementById('watermark-input-path');
if (watermarkInput) watermarkInput.value = openedFilePath;
}
break; break;
case 'encrypt': case 'encrypt':
sectionId = 'pdf-encrypt-section'; sectionId = 'pdf-encrypt-section';
titleText = 'Password Protection'; titleText = 'Password Protection';
if (openedFilePath) {
const encryptInput = document.getElementById('encrypt-input-path');
if (encryptInput) encryptInput.value = openedFilePath;
}
break; break;
case 'decrypt': case 'decrypt':
sectionId = 'pdf-decrypt-section'; sectionId = 'pdf-decrypt-section';
titleText = 'Remove Password'; titleText = 'Remove Password';
if (openedFilePath) {
const decryptInput = document.getElementById('decrypt-input-path');
if (decryptInput) decryptInput.value = openedFilePath;
}
break; break;
case 'permissions': case 'permissions':
sectionId = 'pdf-permissions-section'; sectionId = 'pdf-permissions-section';
titleText = 'Set Permissions'; titleText = 'Set Permissions';
if (openedFilePath) {
const permInput = document.getElementById('permissions-input-path');
if (permInput) permInput.value = openedFilePath;
}
break; break;
} }
@@ -2545,6 +2592,32 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
// 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 // Load current page order button
const loadCurrentOrder = document.getElementById('load-current-order'); const loadCurrentOrder = document.getElementById('load-current-order');
if (loadCurrentOrder) { if (loadCurrentOrder) {
@@ -2619,33 +2692,36 @@ function processPDFOperation() {
case 'compress': case 'compress':
operationData.inputPath = document.getElementById('compress-input-path').value.trim(); operationData.inputPath = document.getElementById('compress-input-path').value.trim();
operationData.outputPath = document.getElementById('compress-output-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.compressionLevel = document.getElementById('compress-level').value;
operationData.compressImages = document.getElementById('compress-images').checked; operationData.compressImages = document.getElementById('compress-images').checked;
operationData.removeDuplicates = document.getElementById('compress-remove-duplicates').checked; operationData.removeDuplicates = document.getElementById('compress-remove-duplicates').checked;
operationData.optimizeFonts = document.getElementById('compress-optimize-fonts').checked; operationData.optimizeFonts = document.getElementById('compress-optimize-fonts').checked;
if (!operationData.inputPath || !operationData.outputPath) { if (!operationData.inputPath || !operationData.outputPath) {
alert('Please select input and output file paths'); alert('Please select input file' + (operationData.overwrite ? '' : ' and output file paths'));
return; return;
} }
break; break;
case 'rotate': case 'rotate':
operationData.inputPath = document.getElementById('rotate-input-path').value.trim(); operationData.inputPath = document.getElementById('rotate-input-path').value.trim();
operationData.outputPath = document.getElementById('rotate-output-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.pages = document.getElementById('rotate-pages').value.trim();
operationData.angle = parseInt(document.getElementById('rotate-angle').value); operationData.angle = parseInt(document.getElementById('rotate-angle').value);
if (!operationData.inputPath || !operationData.outputPath) { if (!operationData.inputPath || !operationData.outputPath) {
alert('Please select input and output file paths'); alert('Please select input file' + (operationData.overwrite ? '' : ' and output file'));
return; return;
} }
break; break;
case 'delete': case 'delete':
operationData.inputPath = document.getElementById('delete-input-path').value.trim(); operationData.inputPath = document.getElementById('delete-input-path').value.trim();
operationData.outputPath = document.getElementById('delete-output-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(); operationData.pages = document.getElementById('delete-pages').value.trim();
if (!operationData.inputPath || !operationData.outputPath || !operationData.pages) { if (!operationData.inputPath || !operationData.outputPath || !operationData.pages) {
@@ -2656,7 +2732,8 @@ function processPDFOperation() {
case 'reorder': case 'reorder':
operationData.inputPath = document.getElementById('reorder-input-path').value.trim(); operationData.inputPath = document.getElementById('reorder-input-path').value.trim();
operationData.outputPath = document.getElementById('reorder-output-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(); operationData.newOrder = document.getElementById('reorder-pages').value.trim();
if (!operationData.inputPath || !operationData.outputPath || !operationData.newOrder) { if (!operationData.inputPath || !operationData.outputPath || !operationData.newOrder) {
@@ -2667,7 +2744,8 @@ function processPDFOperation() {
case 'watermark': case 'watermark':
operationData.inputPath = document.getElementById('watermark-input-path').value.trim(); operationData.inputPath = document.getElementById('watermark-input-path').value.trim();
operationData.outputPath = document.getElementById('watermark-output-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.text = document.getElementById('watermark-text').value.trim();
operationData.fontSize = parseInt(document.getElementById('watermark-font-size').value); operationData.fontSize = parseInt(document.getElementById('watermark-font-size').value);
operationData.opacity = parseInt(document.getElementById('watermark-opacity').value) / 100; operationData.opacity = parseInt(document.getElementById('watermark-opacity').value) / 100;
@@ -2687,7 +2765,8 @@ function processPDFOperation() {
case 'encrypt': case 'encrypt':
operationData.inputPath = document.getElementById('encrypt-input-path').value.trim(); operationData.inputPath = document.getElementById('encrypt-input-path').value.trim();
operationData.outputPath = document.getElementById('encrypt-output-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.userPassword = document.getElementById('encrypt-user-password').value;
operationData.ownerPassword = document.getElementById('encrypt-owner-password').value; operationData.ownerPassword = document.getElementById('encrypt-owner-password').value;
operationData.encryptionLevel = parseInt(document.getElementById('encrypt-level').value); operationData.encryptionLevel = parseInt(document.getElementById('encrypt-level').value);
@@ -2704,14 +2783,15 @@ function processPDFOperation() {
}; };
if (!operationData.inputPath || !operationData.outputPath || !operationData.userPassword) { if (!operationData.inputPath || !operationData.outputPath || !operationData.userPassword) {
alert('Please select files and enter a user password'); alert('Please select file and enter a user password');
return; return;
} }
break; break;
case 'decrypt': case 'decrypt':
operationData.inputPath = document.getElementById('decrypt-input-path').value.trim(); operationData.inputPath = document.getElementById('decrypt-input-path').value.trim();
operationData.outputPath = document.getElementById('decrypt-output-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; operationData.password = document.getElementById('decrypt-password').value;
if (!operationData.inputPath || !operationData.outputPath || !operationData.password) { if (!operationData.inputPath || !operationData.outputPath || !operationData.password) {
@@ -2722,7 +2802,8 @@ function processPDFOperation() {
case 'permissions': case 'permissions':
operationData.inputPath = document.getElementById('permissions-input-path').value.trim(); operationData.inputPath = document.getElementById('permissions-input-path').value.trim();
operationData.outputPath = document.getElementById('permissions-output-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.currentPassword = document.getElementById('permissions-current-password').value;
operationData.ownerPassword = document.getElementById('permissions-owner-password').value; operationData.ownerPassword = document.getElementById('permissions-owner-password').value;
@@ -4057,3 +4138,377 @@ document.addEventListener('click', (e) => {
} }
}); });
// ============================================
// 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.`);
});
+969
View File
@@ -0,0 +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;
}
+1375 -87
View File
File diff suppressed because it is too large Load Diff
+296 -20
View File
@@ -1031,52 +1031,82 @@ body.theme-github .status-bar {
.export-dialog-content { .export-dialog-content {
background: #fff; background: #fff;
border-radius: 8px; border-radius: 6px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
width: 90%; width: 85%;
max-width: 600px; max-width: 450px;
max-height: 90vh; max-height: 80vh;
overflow-y: auto; overflow-y: auto;
position: relative; position: relative;
font-size: 12px;
} }
.export-dialog-header { .export-dialog-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 16px 20px; padding: 12px 16px;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
} }
.export-dialog-header h3 { .export-dialog-header h3 {
margin: 0; margin: 0;
font-size: 18px; font-size: 14px;
font-weight: 600; font-weight: 600;
} }
.export-dialog-body { .export-dialog-body {
padding: 20px; padding: 14px 16px;
} }
.export-section { .export-section {
margin-bottom: 20px; margin-bottom: 14px;
} }
.export-section label { .export-section label {
display: block; display: block;
font-weight: 600; font-weight: 500;
margin-bottom: 8px; margin-bottom: 4px;
color: #333; 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 select,
.export-section input[type="text"], .export-section input[type="text"],
.export-section input[type="number"] { .export-section input[type="number"] {
width: 100%; width: 100%;
padding: 8px 12px; padding: 5px 8px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
font-size: 14px; font-size: 12px;
} }
.export-section select:focus, .export-section select:focus,
@@ -1280,9 +1310,10 @@ body.theme-dark #add-metadata-field {
/* PDF Editor Specific Styles */ /* PDF Editor Specific Styles */
.pdf-editor-content { .pdf-editor-content {
max-width: 600px !important; max-width: 450px !important;
max-height: 85vh; max-height: 75vh;
overflow-y: auto; overflow-y: auto;
font-size: 12px;
} }
.pdf-editor-body { .pdf-editor-body {
@@ -1418,12 +1449,13 @@ body.theme-dark #export-dialog-close:hover {
.batch-dialog-content { .batch-dialog-content {
background: #fff; background: #fff;
border-radius: 8px; border-radius: 6px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
width: 90%; width: 85%;
max-width: 500px; max-width: 450px;
max-height: 80vh; max-height: 75vh;
overflow-y: auto; overflow-y: auto;
font-size: 12px;
} }
.batch-dialog-header { .batch-dialog-header {
@@ -3152,6 +3184,250 @@ body.theme-concrete-warm .status-bar {
color: #e3e3e3; 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 */ /* Print Styles */
@media print { @media print {
/* Hide UI elements for clean print output */ /* Hide UI elements for clean print output */
+545
View File
@@ -0,0 +1,545 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Table Generator - MarkdownConverter</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--ci-dark-gray: #464646;
--ci-medium-gray: #9a9696;
--ci-accent: #e5461f;
--ci-light-gray: #e3e3e3;
--ci-black: #0d0b09;
--ci-white: #ffffff;
--ci-bg: #f5f5f5;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', system-ui, sans-serif;
background: var(--ci-bg);
color: var(--ci-dark-gray);
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: linear-gradient(135deg, var(--ci-dark-gray) 0%, var(--ci-black) 100%);
padding: 16px 24px;
border-bottom: 3px solid var(--ci-accent);
display: flex;
align-items: center;
justify-content: space-between;
}
.header h1 {
color: var(--ci-white);
font-size: 1.25rem;
font-weight: 600;
}
.container {
padding: 24px;
flex: 1;
overflow: auto;
}
.section {
background: var(--ci-white);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.section-title {
font-weight: 600;
margin-bottom: 16px;
color: var(--ci-dark-gray);
font-size: 0.95rem;
}
.controls-row {
display: flex;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.control-label {
font-size: 0.8rem;
font-weight: 500;
color: var(--ci-medium-gray);
}
.form-input, .form-select {
padding: 8px 12px;
border: 2px solid var(--ci-light-gray);
border-radius: 6px;
font-size: 0.875rem;
font-family: inherit;
transition: border-color 0.2s;
min-width: 100px;
}
.form-input:focus, .form-select:focus {
outline: none;
border-color: var(--ci-accent);
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.875rem;
cursor: pointer;
}
.checkbox-label input {
width: 16px;
height: 16px;
accent-color: var(--ci-accent);
}
.table-container {
overflow-x: auto;
margin-bottom: 20px;
}
.editable-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
.editable-table th,
.editable-table td {
border: 1px solid var(--ci-light-gray);
padding: 0;
}
.editable-table th {
background: var(--ci-dark-gray);
color: var(--ci-white);
}
.editable-table input {
width: 100%;
padding: 10px 12px;
border: none;
font-family: inherit;
font-size: 0.875rem;
background: transparent;
}
.editable-table input:focus {
outline: 2px solid var(--ci-accent);
outline-offset: -2px;
background: rgba(229, 70, 31, 0.05);
}
.editable-table th input {
color: var(--ci-white);
font-weight: 600;
}
.editable-table th input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.preview-container {
background: var(--ci-black);
border-radius: 8px;
padding: 16px;
max-height: 250px;
overflow: auto;
}
.preview-content {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
line-height: 1.4;
color: #00ff00;
white-space: pre;
}
.footer {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 16px 24px;
background: var(--ci-white);
border-top: 1px solid var(--ci-light-gray);
}
.footer-left {
display: flex;
gap: 8px;
}
.footer-right {
display: flex;
gap: 12px;
}
.btn {
padding: 10px 20px;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.btn-primary {
background: var(--ci-accent);
color: var(--ci-white);
}
.btn-primary:hover {
background: #c93a18;
transform: translateY(-1px);
}
.btn-secondary {
background: var(--ci-light-gray);
color: var(--ci-dark-gray);
}
.btn-secondary:hover {
background: #d0d0d0;
}
.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-sm {
padding: 6px 12px;
font-size: 0.75rem;
}
.quick-templates {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.template-chip {
padding: 6px 12px;
background: var(--ci-light-gray);
border-radius: 20px;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.template-chip:hover {
background: var(--ci-accent);
color: var(--ci-white);
}
</style>
</head>
<body>
<div class="header">
<h1>Table Generator</h1>
</div>
<div class="container">
<div class="section">
<div class="section-title">Table Settings</div>
<div class="controls-row">
<div class="control-group">
<span class="control-label">Rows</span>
<input type="number" id="rows" class="form-input" min="1" max="50" value="4" style="width: 80px;">
</div>
<div class="control-group">
<span class="control-label">Columns</span>
<input type="number" id="cols" class="form-input" min="1" max="20" value="4" style="width: 80px;">
</div>
<div class="control-group">
<span class="control-label">Alignment</span>
<select id="alignment" class="form-select">
<option value="left">Left</option>
<option value="center">Center</option>
<option value="right">Right</option>
</select>
</div>
<div class="control-group" style="justify-content: flex-end;">
<label class="checkbox-label">
<input type="checkbox" id="has-header" checked>
Include Header Row
</label>
</div>
</div>
<div class="quick-templates">
<button class="template-chip" data-rows="3" data-cols="2">2-Column (3 rows)</button>
<button class="template-chip" data-rows="5" data-cols="3">3-Column (5 rows)</button>
<button class="template-chip" data-rows="4" data-cols="4">4x4 Grid</button>
<button class="template-chip" data-rows="10" data-cols="3">Data Table (10 rows)</button>
<button class="template-chip" data-rows="2" data-cols="5">Wide Table</button>
</div>
</div>
<div class="section">
<div class="section-title">Edit Table Content</div>
<div class="table-container">
<table class="editable-table" id="editable-table">
<!-- Generated dynamically -->
</table>
</div>
</div>
<div class="section">
<div class="section-title">Markdown Preview</div>
<div class="preview-container">
<div id="preview" class="preview-content"></div>
</div>
</div>
</div>
<div class="footer">
<div class="footer-left">
<button class="btn btn-secondary btn-sm" id="btn-clear">Clear All</button>
<button class="btn btn-outline btn-sm" id="btn-fill-sample">Fill Sample Data</button>
</div>
<div class="footer-right">
<button class="btn btn-secondary" id="btn-copy">Copy Markdown</button>
<button class="btn btn-primary" id="btn-insert">Insert to Editor</button>
</div>
</div>
<script>
let currentRows = 4;
let currentCols = 4;
let hasHeader = true;
let alignment = 'left';
// Generate the editable table
function generateEditableTable() {
const table = document.getElementById('editable-table');
hasHeader = document.getElementById('has-header').checked;
alignment = document.getElementById('alignment').value;
let html = '';
for (let i = 0; i < currentRows; i++) {
html += '<tr>';
for (let j = 0; j < currentCols; j++) {
const isHeader = hasHeader && i === 0;
const tag = isHeader ? 'th' : 'td';
const placeholder = isHeader ? `Header ${j + 1}` : `Cell ${i},${j + 1}`;
html += `<${tag}><input type="text" data-row="${i}" data-col="${j}" placeholder="${placeholder}"></${tag}>`;
}
html += '</tr>';
}
table.innerHTML = html;
// Add event listeners for live preview
table.querySelectorAll('input').forEach(input => {
input.addEventListener('input', updatePreview);
});
updatePreview();
}
// Get table data
function getTableData() {
const data = [];
for (let i = 0; i < currentRows; i++) {
const row = [];
for (let j = 0; j < currentCols; j++) {
const input = document.querySelector(`input[data-row="${i}"][data-col="${j}"]`);
row.push(input ? input.value || '' : '');
}
data.push(row);
}
return data;
}
// Generate markdown table
function generateMarkdown(data) {
if (data.length === 0 || data[0].length === 0) return '';
// Calculate column widths
const colWidths = [];
for (let j = 0; j < data[0].length; j++) {
let maxWidth = 3; // Minimum width for separator
for (let i = 0; i < data.length; i++) {
maxWidth = Math.max(maxWidth, (data[i][j] || '').length);
}
colWidths.push(maxWidth);
}
// Generate alignment function
const alignCell = (text, width) => {
text = text || '';
const padding = width - text.length;
if (alignment === 'center') {
const left = Math.floor(padding / 2);
const right = padding - left;
return ' '.repeat(left) + text + ' '.repeat(right);
} else if (alignment === 'right') {
return ' '.repeat(padding) + text;
}
return text + ' '.repeat(padding);
};
// Generate separator based on alignment
const getSeparator = (width) => {
if (alignment === 'center') {
return ':' + '-'.repeat(width - 2) + ':';
} else if (alignment === 'right') {
return '-'.repeat(width - 1) + ':';
}
return '-'.repeat(width);
};
let markdown = '';
const startRow = hasHeader ? 0 : -1;
for (let i = startRow; i < data.length; i++) {
if (i === -1) {
// Generate empty header for tables without header
markdown += '| ' + colWidths.map(w => ' '.repeat(w)).join(' | ') + ' |\n';
} else {
const row = data[i];
const cells = row.map((cell, j) => alignCell(cell, colWidths[j]));
markdown += '| ' + cells.join(' | ') + ' |\n';
}
// Add separator after first row
if ((hasHeader && i === 0) || (!hasHeader && i === -1)) {
const separators = colWidths.map(w => getSeparator(w));
markdown += '| ' + separators.join(' | ') + ' |\n';
}
}
return markdown;
}
// Update preview
function updatePreview() {
const data = getTableData();
const markdown = generateMarkdown(data);
document.getElementById('preview').textContent = markdown;
}
// Event listeners
document.getElementById('rows').addEventListener('change', (e) => {
currentRows = parseInt(e.target.value) || 4;
generateEditableTable();
});
document.getElementById('cols').addEventListener('change', (e) => {
currentCols = parseInt(e.target.value) || 4;
generateEditableTable();
});
document.getElementById('alignment').addEventListener('change', () => {
updatePreview();
});
document.getElementById('has-header').addEventListener('change', () => {
generateEditableTable();
});
// Quick templates
document.querySelectorAll('.template-chip').forEach(chip => {
chip.addEventListener('click', () => {
currentRows = parseInt(chip.dataset.rows);
currentCols = parseInt(chip.dataset.cols);
document.getElementById('rows').value = currentRows;
document.getElementById('cols').value = currentCols;
generateEditableTable();
});
});
// Clear all
document.getElementById('btn-clear').addEventListener('click', () => {
document.querySelectorAll('#editable-table input').forEach(input => {
input.value = '';
});
updatePreview();
});
// Fill sample data
document.getElementById('btn-fill-sample').addEventListener('click', () => {
const sampleHeaders = ['Name', 'Description', 'Status', 'Priority', 'Due Date'];
const sampleData = [
['Task 1', 'First task description', 'In Progress', 'High', '2024-01-15'],
['Task 2', 'Second task description', 'Completed', 'Medium', '2024-01-10'],
['Task 3', 'Third task description', 'Pending', 'Low', '2024-01-20'],
['Task 4', 'Fourth task description', 'In Review', 'High', '2024-01-18']
];
for (let i = 0; i < currentRows; i++) {
for (let j = 0; j < currentCols; j++) {
const input = document.querySelector(`input[data-row="${i}"][data-col="${j}"]`);
if (input) {
if (hasHeader && i === 0) {
input.value = sampleHeaders[j % sampleHeaders.length];
} else {
const dataRow = hasHeader ? i - 1 : i;
if (dataRow < sampleData.length && j < sampleData[dataRow].length) {
input.value = sampleData[dataRow][j];
} else {
input.value = `Data ${i}-${j + 1}`;
}
}
}
}
}
updatePreview();
});
// Copy markdown
document.getElementById('btn-copy').addEventListener('click', () => {
const markdown = document.getElementById('preview').textContent;
navigator.clipboard.writeText(markdown).then(() => {
const btn = document.getElementById('btn-copy');
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = 'Copy Markdown';
}, 2000);
});
});
// Insert to editor
document.getElementById('btn-insert').addEventListener('click', () => {
const content = document.getElementById('preview').textContent;
if (content && window.electronAPI) {
window.electronAPI.send('insert-generated-content', '\n' + content + '\n');
window.close();
}
});
// Initialize
generateEditableTable();
</script>
</body>
</html>
+121
View File
@@ -0,0 +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();
});
});
});
+100
View File
@@ -0,0 +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) => `<p>${text}</p>`),
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();
});
+137
View File
@@ -0,0 +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');
});
});
});