Merge windows into master for v3.0.0 release
@@ -21,7 +21,13 @@
|
||||
"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 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": [],
|
||||
"ask": []
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"printWidth": 100,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
# PanConverter - Claude Development Guide
|
||||
# MarkdownConverter - Claude Development Guide
|
||||
|
||||
## 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**: v2.1.0
|
||||
**Author**: Amit Haridas (amit.wh@gmail.com)
|
||||
**Current Version**: v3.0.0
|
||||
**Author**: Amit Haridas / ConcreteInfo (amit.wh@gmail.com)
|
||||
**License**: MIT
|
||||
**Repository**: https://github.com/amitwh/pan-converter
|
||||
|
||||
@@ -25,17 +25,28 @@
|
||||
### Application Structure
|
||||
```
|
||||
src/
|
||||
├── main.js # Electron main process, menu system, IPC handlers
|
||||
├── renderer.js # TabManager class, multi-file editing, event handling
|
||||
├── index.html # Application layout with tabbed interface
|
||||
├── styles.css # Base styling with multi-theme support
|
||||
└── styles-modern.css # Modern glassmorphism UI design system (v1.7.1)
|
||||
├── 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
|
||||
├── 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-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/
|
||||
└── 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)
|
||||
|
||||
package.json # Dependencies and build configuration
|
||||
CLAUDE.md # Development documentation for AI assistants
|
||||
scripts/
|
||||
└── generate-icons.js # Icon generation script
|
||||
|
||||
package.json # Dependencies and build configuration
|
||||
CLAUDE.md # Development documentation for AI assistants
|
||||
```
|
||||
|
||||
## Development Commands
|
||||
@@ -113,311 +124,111 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
|
||||
|
||||
## Feature Implementation Guide
|
||||
|
||||
### v2.1.0 UI Enhancements & Comprehensive Format Support (Latest)
|
||||
### v3.0.0 Major UI Overhaul & Media Converters (Latest)
|
||||
|
||||
#### 🎨 Subtle & Small Preview Popout Button
|
||||
**Minimalist Design for Preview Popout Button** (`src/styles.css:195-211`)
|
||||
#### Rebranding & UI Improvements
|
||||
**Renamed to MarkdownConverter with ConcreteInfo Branding** (`src/index.html`, `package.json`)
|
||||
|
||||
**Problem Solved:**
|
||||
- Previous design was too prominent and visually distracting
|
||||
- Border and larger size drew unnecessary attention
|
||||
- Didn't match the modern, minimalist aesthetic of the app
|
||||
**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
|
||||
|
||||
**Solution Implemented:**
|
||||
1. **Reduced Size** - Font size from 14px to 11px, padding from 4px×8px to 2px×6px
|
||||
2. **Removed Border** - Changed from `1px solid #d0d7de` to `border: none`
|
||||
3. **Opacity Transition** - Added 50% opacity when idle, 100% on hover
|
||||
4. **Subtle Hover Effect** - Light background `rgba(0, 0, 0, 0.05)` instead of `#f3f4f6` with border
|
||||
#### ConcreteInfo Theme System
|
||||
**New Theme Based on Logo Palette** (`src/styles-concreteinfo.css`)
|
||||
|
||||
**Key CSS Changes:**
|
||||
```css
|
||||
.preview-popout-btn {
|
||||
background: transparent;
|
||||
border: none; /* Previously: 1px solid #d0d7de */
|
||||
border-radius: 3px; /* Previously: 4px */
|
||||
padding: 2px 6px; /* Previously: 4px 8px */
|
||||
font-size: 11px; /* Previously: 14px */
|
||||
opacity: 0.5; /* New: makes it subtle */
|
||||
transition: all 0.2s;
|
||||
}
|
||||
**Color Palette:**
|
||||
- Dark Gray: #464646
|
||||
- Medium Gray: #9a9696
|
||||
- Accent (Red): #e5461f
|
||||
- Light Gray: #e3e3e3
|
||||
- Black: #0d0b09
|
||||
|
||||
.preview-popout-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.05); /* Previously: #f3f4f6 */
|
||||
opacity: 1; /* New: full opacity on hover */
|
||||
}
|
||||
```
|
||||
**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`)
|
||||
|
||||
#### 🎨 Simplified Table Headers
|
||||
**Clean Light Gray Table Headers** (`src/styles-modern.css:445-449`)
|
||||
**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
|
||||
|
||||
**Problem Solved:**
|
||||
- Gradient backgrounds in table headers looked too busy
|
||||
- Purple gradient (`var(--primary-gradient)`) didn't match professional document style
|
||||
- White text on gradient reduced readability in some contexts
|
||||
**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
|
||||
|
||||
**Solution Implemented:**
|
||||
1. **Simple Background** - Changed from gradient to solid light gray (#f0f0f0)
|
||||
2. **Dark Text** - Changed text color from white to dark (#333333)
|
||||
3. **Consistent Styling** - Matches standard theme table headers
|
||||
4. **Professional Appearance** - Clean, readable, print-friendly
|
||||
**Access:**
|
||||
- Tools → ASCII Art Generator (Ctrl+Shift+A)
|
||||
- Tools → Table Generator (Ctrl+Shift+T)
|
||||
|
||||
**Before:**
|
||||
```css
|
||||
.preview-content table th {
|
||||
background: var(--primary-gradient); /* Purple gradient */
|
||||
color: white;
|
||||
}
|
||||
```
|
||||
#### Media Converter Menus
|
||||
**Separate Menus for Image, Audio, and Video Conversion** (`src/main.js`)
|
||||
|
||||
**After:**
|
||||
```css
|
||||
.preview-content table th {
|
||||
background: #f0f0f0; /* Simple light gray */
|
||||
color: #333333; /* Dark text for readability */
|
||||
}
|
||||
```
|
||||
**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...
|
||||
|
||||
#### 📥 Comprehensive Format-to-Markdown Conversion
|
||||
**30+ Format Support for Import Feature** (`src/main.js:1933-1994`)
|
||||
**Video Menu:**
|
||||
- Convert Video...
|
||||
- Batch Convert Videos...
|
||||
- Compress Video...
|
||||
- Trim Video...
|
||||
- Extract Frames...
|
||||
- Convert to GIF...
|
||||
|
||||
**Enhanced Import Capabilities:**
|
||||
The "Import Document" feature now supports comprehensive format conversion to Markdown:
|
||||
**Batch Menu:**
|
||||
- Convert Markdown Folder...
|
||||
- Batch Image Conversion...
|
||||
- Batch Audio Conversion...
|
||||
- Batch Video Conversion...
|
||||
- Batch PDF Conversion...
|
||||
|
||||
**Supported Format Categories:**
|
||||
1. **Documents**: DOCX, ODT, RTF, HTML, HTM, TEX, EPUB, PDF, TXT
|
||||
2. **Presentations**: PPTX, ODP
|
||||
3. **Markup Languages**: RST, Textile, MediaWiki, Org-mode, AsciiDoc, TWiki, OPML
|
||||
4. **E-book Formats**: EPUB, FB2
|
||||
5. **LaTeX Formats**: TEX, LATEX, LTX
|
||||
6. **Web Formats**: HTML, HTM, XHTML
|
||||
7. **Wiki Formats**: MediaWiki, DokuWiki, TikiWiki, TWiki
|
||||
8. **Data Formats**: CSV, TSV, JSON
|
||||
#### Enhanced PDF Editor
|
||||
**Open PDF Files Directly** (`src/main.js`)
|
||||
|
||||
**Format-Specific Conversion Options:**
|
||||
```javascript
|
||||
// PDF text extraction
|
||||
if (ext === 'pdf') {
|
||||
additionalOptions = '--pdf-engine=xelatex';
|
||||
}
|
||||
- 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.)
|
||||
|
||||
// CSV/TSV table conversion
|
||||
if (ext === 'csv' || ext === 'tsv') {
|
||||
additionalOptions = '--from=csv -t markdown';
|
||||
}
|
||||
#### About Menu & Dependencies
|
||||
**Updated Help Menu** (`src/main.js`)
|
||||
|
||||
// JSON structure handling
|
||||
if (ext === 'json') {
|
||||
additionalOptions = '--from=json -t markdown';
|
||||
}
|
||||
```
|
||||
- "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)
|
||||
|
||||
**User Experience Improvements:**
|
||||
- Comprehensive file filter dialog with format categories
|
||||
- Format-specific error messages with helpful hints
|
||||
- Success dialog shows original and converted format
|
||||
- All conversions use Pandoc's native format detection
|
||||
#### Security Enhancements (v2.2.0 continued)
|
||||
**Secure Preload Script** (`src/preload.js`)
|
||||
|
||||
**Access**: File → Import Document (Ctrl+I)
|
||||
|
||||
---
|
||||
|
||||
#### 🎨 Exhaustive ASCII Art Generator
|
||||
**5 Text Banner Styles & 19 Professional Templates** (`src/renderer.js:3397-3671`, `src/index.html:427-466`)
|
||||
|
||||
**Enhanced Text Banner Styles:**
|
||||
|
||||
Complete alphabet (A-Z) and numbers (0-9) support for all styles:
|
||||
|
||||
1. **Standard** (5-line height)
|
||||
- Classic ASCII art using slashes, underscores, and pipes
|
||||
- Readable and compatible with all systems
|
||||
- Example: `/ \`, `\___/`, `| |`
|
||||
|
||||
2. **Banner** (7-line height)
|
||||
- Large format using # characters
|
||||
- Professional presentation style
|
||||
- Complete character set including punctuation
|
||||
- Example: ` ##### `, ` ## ## `, ` ####### `
|
||||
|
||||
3. **Block** (6-line height)
|
||||
- Modern Unicode block characters
|
||||
- Uses █ ╔ ╗ ╚ ╝ ═ ║
|
||||
- High visual impact
|
||||
- Example: `█████╗`, `██╔══██╗`, `███████║`
|
||||
|
||||
4. **Bubble** (5-line height)
|
||||
- Circular bubble letters
|
||||
- Playful and friendly appearance
|
||||
- Uses Ⓐ Ⓑ Ⓒ and ⒜ ⒝ ⒞
|
||||
- Example: ` Ⓐ `, ` ⒜ ⒜ `, `⒜⒜⒜⒜⒜`
|
||||
|
||||
5. **Digital** (7-line height)
|
||||
- Digital display style
|
||||
- Uses ▄ ▀ ▐ ▌ for segments
|
||||
- Retro calculator/LED aesthetic
|
||||
- Example: ` ▄▀▀▀▄ `, `▐ ▌`, `▐▄▄▄▄▌`
|
||||
|
||||
**19 Professional ASCII Templates:**
|
||||
|
||||
**Arrows & Flow (4 templates):**
|
||||
- Arrow Right - Horizontal process flow
|
||||
- Arrow Down - Vertical process flow
|
||||
- Decision - Binary decision with Yes/No branches
|
||||
- Process Flow - Multi-step process with parallel paths
|
||||
|
||||
**Diagrams & Charts (6 templates):**
|
||||
- Flowchart - Complex flowchart with decisions and loops
|
||||
- Sequence - User-System-Database interaction diagram
|
||||
- Network - Server-client network topology
|
||||
- Hierarchy - Organizational tree structure
|
||||
- Timeline - Milestone visualization with dates
|
||||
- Table Simple - Professional table template
|
||||
|
||||
**Boxes & Containers (4 templates):**
|
||||
- Header - Section header with borders
|
||||
- Note Box - Important notes (┏━━┓ style)
|
||||
- Warning Box - Warning messages (╔═══╗ style)
|
||||
- Info Box - Information boxes (╭───╮ style)
|
||||
|
||||
**Decorative Elements (6 templates):**
|
||||
- Divider - Horizontal separator (═══)
|
||||
- Separator Fancy - Elegant rounded divider
|
||||
- Brackets - Japanese-style brackets 【 】
|
||||
- Banner Stars - Star-bordered banners
|
||||
- Checklist - Task lists with ✓
|
||||
- Progress Bar - Visual progress indicators
|
||||
|
||||
**Template Examples:**
|
||||
|
||||
```
|
||||
Sequence Diagram:
|
||||
User System Database
|
||||
│ │ │
|
||||
│ Request │ │
|
||||
├──────────►│ │
|
||||
│ │ Query │
|
||||
│ ├──────────►│
|
||||
│ │ │
|
||||
│ │ Result │
|
||||
│ │◄──────────┤
|
||||
│ Response │ │
|
||||
│◄──────────┤ │
|
||||
│ │ │
|
||||
```
|
||||
|
||||
```
|
||||
Note Box:
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ NOTE: ┃
|
||||
┃ This is an important note ┃
|
||||
┃ that requires attention! ┃
|
||||
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
```
|
||||
|
||||
**Integration Features:**
|
||||
- Automatic code block wrapping for proper rendering
|
||||
- Preserved formatting in all export formats
|
||||
- Categorized template selection UI
|
||||
- Real-time preview generation
|
||||
- One-click insertion into editor
|
||||
|
||||
**Access**: Tools → ASCII Art Generator
|
||||
|
||||
**Key Improvements in v2.1.0:**
|
||||
1. ✅ Minimalist preview popout button design
|
||||
2. ✅ Professional light gray table headers
|
||||
3. ✅ Support for 30+ import formats
|
||||
4. ✅ 5 complete ASCII text banner font styles
|
||||
5. ✅ 19 professional ASCII art templates
|
||||
6. ✅ Organized template categories
|
||||
7. ✅ Enhanced user experience across all features
|
||||
|
||||
---
|
||||
|
||||
### v1.9.3 Configurable Page Sizes & Orientation
|
||||
|
||||
#### 📏 Universal Page Size Configuration
|
||||
**Comprehensive Page Size Support Across All Export Formats** (`src/main.js`, `src/index.html`, `src/renderer.js`, `src/wordTemplateExporter.js`)
|
||||
|
||||
**Feature Overview:**
|
||||
- Added configurable page sizes for all export formats (PDF, DOCX, DOCX Enhanced, ODT, PowerPoint)
|
||||
- Support for standard paper sizes: A3, A4, A5, B4, B5, Letter, Legal, Tabloid
|
||||
- Custom page size support with user-defined dimensions
|
||||
- Portrait and landscape orientation options
|
||||
- Persistent page size preferences across sessions
|
||||
- Batch conversion support for all page sizes
|
||||
|
||||
**Implementation Details:**
|
||||
|
||||
1. **Page Size Constants** (`src/main.js:81-139`)
|
||||
- Defined PAGE_SIZES with Pandoc identifiers and Word XML dimensions
|
||||
- Word dimensions in twentieths of a point (e.g., A4: 11906×16838)
|
||||
- Pandoc identifiers for LaTeX geometry package
|
||||
|
||||
2. **Export Dialog UI** (`src/index.html:246-275`)
|
||||
- Page size dropdown with 8 standard sizes plus custom option
|
||||
- Orientation selector (Portrait/Landscape)
|
||||
- Custom dimension inputs for width and height
|
||||
- Always visible in basic export options
|
||||
|
||||
3. **PDF Export** (`src/main.js:2475-2488`)
|
||||
- Uses `-V geometry:papersize=a4` Pandoc parameter
|
||||
- Custom dimensions via `-V geometry:paperwidth` and `paperheight`
|
||||
- Landscape orientation via `-V geometry:landscape`
|
||||
|
||||
4. **DOCX/ODT Export** (`src/main.js:858-916`, `src/main.js:1568-1603`)
|
||||
- `setDocxPageSize()` function modifies `<w:pgSz>` XML elements
|
||||
- Updates all section properties in document.xml
|
||||
- Swaps dimensions for landscape orientation
|
||||
- Applied to both standard and enhanced DOCX exports
|
||||
|
||||
5. **Word Template Exporter** (`src/wordTemplateExporter.js:13-104`)
|
||||
- Added `pageSettings` parameter to constructor
|
||||
- `setPageSize()` method updates template page dimensions
|
||||
- Integrated into all WordTemplateExporter instances
|
||||
- Supports batch conversion with page settings
|
||||
|
||||
6. **Renderer Integration** (`src/renderer.js:1340-1433`)
|
||||
- Collects page size and orientation from export dialog
|
||||
- Sends settings to main process via IPC
|
||||
- Loads saved settings on startup
|
||||
- Shows/hides custom size inputs based on selection
|
||||
|
||||
7. **Persistent Storage** (`src/main.js:2659-2663`)
|
||||
- Page settings loaded from user data on app startup
|
||||
- Automatically saved when changed
|
||||
- Default: A4 portrait
|
||||
|
||||
**Page Size Dimensions:**
|
||||
| Size | Pandoc ID | Word Dimensions (twips) | Metric/Imperial |
|
||||
|------|-----------|-------------------------|-----------------|
|
||||
| A3 | a3 | 16838×23811 | 297×420mm |
|
||||
| A4 | a4 | 11906×16838 | 210×297mm |
|
||||
| A5 | a5 | 8391×11906 | 148×210mm |
|
||||
| B4 | b4 | 14170×20015 | 250×353mm |
|
||||
| B5 | b5 | 9979×14170 | 176×250mm |
|
||||
| Letter | letter | 12240×15840 | 8.5×11in |
|
||||
| Legal | legal | 12240×20160 | 8.5×14in |
|
||||
| Tabloid | tabloid | 15840×24480 | 11×17in |
|
||||
|
||||
**ASCII Art Scaling:**
|
||||
- Code blocks and ASCII art automatically scale with page size
|
||||
- Monospace font (Consolas) maintains alignment across all sizes
|
||||
- Smaller pages may require font size adjustments for complex diagrams
|
||||
- Landscape orientation recommended for wide ASCII diagrams
|
||||
|
||||
**Key Improvements in v1.9.3:**
|
||||
1. ✅ Configurable page sizes for PDF, DOCX, ODT, PowerPoint exports
|
||||
2. ✅ Portrait and landscape orientation support
|
||||
3. ✅ Custom page dimensions for specialized requirements
|
||||
4. ✅ Persistent settings with automatic loading
|
||||
5. ✅ Batch conversion respects page size preferences
|
||||
6. ✅ Word template exporter integration
|
||||
7. ✅ Comprehensive UI with always-visible controls
|
||||
- New IPC channels for media converters
|
||||
- Secure generator window communication
|
||||
- Insert content from generator windows
|
||||
|
||||
---
|
||||
|
||||
@@ -1487,33 +1298,28 @@ if (!gotTheLock) {
|
||||
|
||||
## Pending Tasks & Future Enhancements
|
||||
|
||||
No pending tasks at this time. All planned v1.9.3 features have been completed.
|
||||
No pending tasks at this time. All planned v1.9.1 features have been completed.
|
||||
|
||||
### Potential Future Enhancements
|
||||
- Image embedding in headers/footers (partially implemented - logo upload UI ready)
|
||||
- Advanced page numbering formats (Roman numerals, custom prefixes)
|
||||
- First-page-different header/footer support
|
||||
- Per-section headers/footers within documents
|
||||
- Automatic font size scaling for different page sizes
|
||||
- Page size templates/presets for common document types
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: December 13, 2025
|
||||
**Claude Assistant**: Development completed for v1.9.3 with Configurable Page Sizes & Orientation!
|
||||
**Last Updated**: December 12, 2025
|
||||
**Claude Assistant**: Development completed for v1.9.1 with ASCII Art Preservation!
|
||||
|
||||
### v1.9.3 - Latest Release Summary
|
||||
- **Configurable Page Sizes**: Support for A3, A4, A5, B4, B5, Letter, Legal, Tabloid, and custom sizes
|
||||
- **Orientation Support**: Portrait and landscape options for all export formats
|
||||
- **Universal Export Support**: Page size configuration for PDF, DOCX (standard & enhanced), ODT, and PowerPoint
|
||||
- **Persistent Settings**: Page preferences saved and restored automatically
|
||||
- **Batch Conversion**: All page size settings apply to batch operations
|
||||
- **ASCII Art Scaling**: Code blocks and diagrams automatically adapt to different page sizes
|
||||
- **User-Friendly UI**: Always-visible page size controls in export dialog
|
||||
### v1.9.1 - Latest Release Summary
|
||||
- **ASCII Art Preservation**: Code blocks, flowcharts, tables, and ASCII art now export exactly as shown in preview
|
||||
- **Enhanced Code Block Rendering**: Each line rendered separately to preserve exact spacing and alignment
|
||||
- **Monospace Font Support**: Consolas font used consistently across all export formats
|
||||
- **Improved ASCII Detection**: Extended Unicode box-drawing and arrow character support
|
||||
- **PDF Export Enhancement**: Added monofont and highlight-style options for better code rendering
|
||||
- **HTML/Electron PDF Fix**: Enhanced CSS to prevent text wrapping in code blocks
|
||||
|
||||
### Previous Releases
|
||||
- v1.9.2: Full-width background for code blocks
|
||||
- v1.9.1: ASCII Art & Code Block Preservation
|
||||
- v1.9.0: Custom Headers & Footers for PDF, DOCX, ODT, PowerPoint exports
|
||||
- v1.8.3: Streamlined PDF Editor UI & Configurable Template Settings
|
||||
- v1.8.2: Enhanced PDF Export & Print Fix
|
||||
|
||||
@@ -1,162 +1,141 @@
|
||||
# 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.
|
||||
|
||||
## Features
|
||||
|
||||
### ✨ Advanced Markdown Editor
|
||||
- 🗂️ **Tabbed Interface** - Work with multiple files simultaneously in separate tabs
|
||||
- 📝 **Rich Text Editor** - Full-featured editor with syntax highlighting and comprehensive toolbar
|
||||
- 🔍 **Find & Replace** - Powerful search and replace with match highlighting and navigation
|
||||
- 🔢 **Line Numbers** - Toggle line numbers for easier code editing and navigation
|
||||
- ↩️ **Smart Auto-Indentation** - Automatic list continuation and intelligent indentation
|
||||
- ⏪ **Undo/Redo** - Full undo/redo support with keyboard shortcuts
|
||||
- ⌨️ **Advanced Shortcuts** - Tab indentation, line selection, and smart text formatting
|
||||
- 📂 **File Association Support** - Open markdown files directly from file manager
|
||||
### Markdown Editor
|
||||
- **Multi-tab editing** - Work on multiple files simultaneously
|
||||
- **Live preview** - Real-time markdown rendering with syntax highlighting
|
||||
- **Dynamic splitter** - Drag to resize editor and preview panes
|
||||
- **25+ themes** - Light and dark themes including Atom One Light, Dracula, Nord, Sepia, and more
|
||||
- **Find & Replace** - Search and replace with regex support
|
||||
- **Line numbers** - Toggle line numbers in the editor
|
||||
- **Auto-save** - Automatic saving every 30 seconds
|
||||
- **Math support** - KaTeX integration for mathematical expressions
|
||||
|
||||
### 🎨 Themes & Interface
|
||||
- 👁️ **Live Preview** - See your markdown rendered in real-time with synchronized scrolling
|
||||
- 🎨 **Multiple Themes** - Choose from Light, Dark, Solarized, Monokai, or GitHub themes
|
||||
- 💾 **Auto-Save** - Never lose your work with automatic saving every 30 seconds
|
||||
- 📊 **Enhanced Statistics** - Detailed document statistics including word count, character count, lines, paragraphs, sentences, and estimated reading time
|
||||
- 🕒 **Recent Files** - Quick access to recently opened files via File menu
|
||||
- 🧮 **Math Support** - Render mathematical expressions using KaTeX (supports $$, $, \\[\\], \\(\\) delimiters)
|
||||
### PDF Viewer & Editor
|
||||
- **Built-in PDF viewer** - Open and view PDF files directly in the app
|
||||
- **Page navigation** - Navigate pages with keyboard or buttons
|
||||
- **Zoom controls** - Zoom in/out, fit to width, fit to page
|
||||
- **Rotation** - Rotate pages left or right
|
||||
- **PDF Editor tools**:
|
||||
- Merge multiple PDFs
|
||||
- Split PDFs by page range
|
||||
- Compress PDFs
|
||||
- Rotate pages
|
||||
- Delete pages
|
||||
- Reorder pages
|
||||
- Add watermarks
|
||||
- Password protection
|
||||
- Remove passwords
|
||||
- Set permissions
|
||||
|
||||
### 📤 Export & Conversion
|
||||
- 📄 **Enhanced PDF Export** - Robust PDF generation with multiple LaTeX engine fallbacks (XeLaTeX, PDFLaTeX, wkhtmltopdf)
|
||||
- 📄 **Document Export** - Convert to HTML, DOCX, LaTeX, RTF, ODT, EPUB, PowerPoint (PPTX), and OpenDocument Presentation (ODP)
|
||||
- 📊 **Spreadsheet Export** - Export markdown tables to CSV format for compatibility with any spreadsheet application
|
||||
- 📥 **Document Import** - Import DOCX, ODT, RTF, HTML, PDF, and presentation files to markdown
|
||||
- 📋 **Table Creation Helper** - Built-in table generator for easy markdown table creation
|
||||
### Export Options
|
||||
- **PDF** - Export to PDF with customizable page sizes and orientation
|
||||
- **DOCX** - Standard and Enhanced (template-based) Word export
|
||||
- **ODT** - OpenDocument format
|
||||
- **HTML** - Web-ready HTML export
|
||||
- **PowerPoint** - PPTX presentation export
|
||||
- **EPUB** - E-book format
|
||||
- **LaTeX** - Academic document format
|
||||
- **RTF** - Rich Text Format
|
||||
|
||||
### 🖥️ Platform Support
|
||||
- **Cross-Platform** - Works seamlessly on Windows, macOS, and Linux
|
||||
### Advanced Features
|
||||
- **Custom headers & footers** - Add headers/footers to exports with dynamic fields
|
||||
- **Page size configuration** - A3, A4, A5, B4, B5, Letter, Legal, Tabloid, or custom sizes
|
||||
- **Batch conversion** - Convert entire folders of markdown files
|
||||
- **ASCII Art Generator** - Create text banners and diagrams
|
||||
- **Word templates** - Use custom Word templates for enhanced exports
|
||||
- **Import documents** - Import from 30+ formats (DOCX, PDF, HTML, etc.)
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
- [Pandoc](https://pandoc.org/installing.html) must be installed for export functionality
|
||||
- **Ubuntu/Debian**: `sudo apt-get install pandoc`
|
||||
- **macOS**: `brew install pandoc`
|
||||
- **Windows**: Download installer from Pandoc website
|
||||
- [Node.js](https://nodejs.org/) (v16 or later)
|
||||
- [Pandoc](https://pandoc.org/installing.html) (required for export functionality)
|
||||
|
||||
### PDF Export Requirements
|
||||
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
|
||||
### Install Dependencies
|
||||
```bash
|
||||
git clone https://github.com/amitwh/pan-converter.git
|
||||
cd pan-converter
|
||||
npm install
|
||||
```
|
||||
|
||||
### Run the Application
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
## Usage
|
||||
### Build for Distribution
|
||||
```bash
|
||||
# Windows
|
||||
npm run build:win
|
||||
|
||||
### Basic Workflow
|
||||
1. **Write** - Use the editor to write your Markdown content
|
||||
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
|
||||
# macOS
|
||||
npm run build:mac
|
||||
|
||||
### Export Options
|
||||
- **Documents**: HTML, PDF, DOCX, LaTeX, RTF, ODT, EPUB
|
||||
- **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
|
||||
# Linux
|
||||
npm run build:linux
|
||||
```
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
### File Operations
|
||||
- `Ctrl/Cmd + N` - New file/tab
|
||||
- `Ctrl/Cmd + T` - New tab
|
||||
- `Ctrl/Cmd + W` - Close current tab
|
||||
- `Ctrl/Cmd + Tab` - Switch to next tab
|
||||
- `Ctrl/Cmd + O` - Open file
|
||||
- `Ctrl/Cmd + S` - Save file
|
||||
- `Ctrl/Cmd + Shift + S` - Save as
|
||||
- `Ctrl/Cmd + I` - Import document
|
||||
| Action | Shortcut |
|
||||
|--------|----------|
|
||||
| New File | Ctrl+N |
|
||||
| Open File | Ctrl+O |
|
||||
| Open PDF | Ctrl+Shift+O |
|
||||
| Save | Ctrl+S |
|
||||
| Save As | Ctrl+Shift+S |
|
||||
| Export | Ctrl+E |
|
||||
| Print | Ctrl+P |
|
||||
| Find | Ctrl+F |
|
||||
| Undo | Ctrl+Z |
|
||||
| Redo | Ctrl+Shift+Z |
|
||||
| New Tab | Ctrl+T |
|
||||
| Close Tab | Ctrl+W |
|
||||
| Toggle Preview | Ctrl+Shift+P |
|
||||
| Zoom In | Ctrl+Shift++ |
|
||||
| Zoom Out | Ctrl+Shift+- |
|
||||
|
||||
### Editor Features
|
||||
- `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
|
||||
## Themes
|
||||
|
||||
### View & Navigation
|
||||
- `Ctrl/Cmd + P` - Toggle preview
|
||||
- `Ctrl/Cmd + Enter` - Toggle preview (alternative)
|
||||
- `Escape` - Close find dialog
|
||||
### Light Themes
|
||||
- Atom One Light (Default)
|
||||
- GitHub Light
|
||||
- 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
|
||||
# Install dependencies
|
||||
npm install
|
||||
## PDF Viewer
|
||||
|
||||
# Generate icons
|
||||
npm run generate-icons
|
||||
|
||||
# Build for current platform
|
||||
npm run build
|
||||
|
||||
# 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).
|
||||
Open PDF files directly in PanConverter:
|
||||
- **File > Open PDF** or **Ctrl+Shift+O**
|
||||
- Navigate pages with arrow buttons or page input
|
||||
- Zoom controls: +/- buttons, Fit Width, Fit Page
|
||||
- Rotate pages left or right
|
||||
- Close PDF to return to editor
|
||||
|
||||
## License
|
||||
|
||||
@@ -164,13 +143,8 @@ MIT License - see LICENSE file for details.
|
||||
|
||||
## 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/)
|
||||
- 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)
|
||||
v3.0.0
|
||||
|
||||
|
After Width: | Height: | Size: 869 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 181 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 532 KiB |
|
After Width: | Height: | Size: 532 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 783 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -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'
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -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
|
||||
};
|
||||
@@ -1,13 +1,20 @@
|
||||
{
|
||||
"name": "pan-converter",
|
||||
"version": "2.1.0",
|
||||
"description": "Cross-platform Markdown editor and converter using Pandoc",
|
||||
"name": "markdown-converter",
|
||||
"version": "3.0.0",
|
||||
"description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"test": "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: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:mac": "electron-builder --mac",
|
||||
"build:linux": "electron-builder --linux",
|
||||
@@ -19,14 +26,29 @@
|
||||
"markdown",
|
||||
"pandoc",
|
||||
"converter",
|
||||
"editor"
|
||||
"editor",
|
||||
"pdf",
|
||||
"audio",
|
||||
"video",
|
||||
"image"
|
||||
],
|
||||
"author": "Amit Haridas <amit.wh@gmail.com>",
|
||||
"author": "ConcreteInfo <amit.wh@gmail.com>",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/amitwh/markdown-converter"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"cross-env": "^10.0.0",
|
||||
"electron": "^37.4.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"jest": "^30.2.0",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"prettier": "^3.7.4",
|
||||
"sharp": "^0.34.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -40,14 +62,15 @@
|
||||
"html2pdf.js": "^0.10.1",
|
||||
"marked": "^16.2.1",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^3.11.174",
|
||||
"pdfkit": "^0.14.0",
|
||||
"pizzip": "^3.2.0",
|
||||
"tslib": "^2.8.1",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.panconverter.app",
|
||||
"productName": "PanConverter",
|
||||
"appId": "com.concreteinfo.markdownconverter",
|
||||
"productName": "MarkdownConverter",
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
@@ -73,10 +96,18 @@
|
||||
"description": "Markdown Document",
|
||||
"mimeType": "text/markdown",
|
||||
"role": "Editor"
|
||||
},
|
||||
{
|
||||
"ext": "pdf",
|
||||
"name": "PDF Document",
|
||||
"description": "PDF Document",
|
||||
"mimeType": "application/pdf",
|
||||
"role": "Editor"
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.productivity"
|
||||
"category": "public.app-category.productivity",
|
||||
"identity": null
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
@@ -110,7 +141,7 @@
|
||||
"displayLanguageSelector": true,
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "PanConverter",
|
||||
"shortcutName": "MarkdownConverter",
|
||||
"runAfterFinish": true,
|
||||
"menuCategory": "Productivity",
|
||||
"license": "LICENSE",
|
||||
@@ -123,16 +154,29 @@
|
||||
"target": [
|
||||
"deb",
|
||||
"AppImage",
|
||||
"snap"
|
||||
"snap",
|
||||
"rpm"
|
||||
],
|
||||
"category": "Utility"
|
||||
"category": "Utility",
|
||||
"maintainer": "ConcreteInfo <amit.wh@gmail.com>"
|
||||
},
|
||||
"deb": {
|
||||
"depends": [
|
||||
"pandoc"
|
||||
"pandoc",
|
||||
"ffmpeg",
|
||||
"imagemagick",
|
||||
"libreoffice-common"
|
||||
],
|
||||
"description": "Markdown editor and converter using Pandoc",
|
||||
"maintainer": "Amit Haridas <amit.wh@gmail.com>"
|
||||
"description": "Professional Markdown editor and universal file converter",
|
||||
"maintainer": "ConcreteInfo <amit.wh@gmail.com>"
|
||||
},
|
||||
"rpm": {
|
||||
"depends": [
|
||||
"pandoc",
|
||||
"ffmpeg",
|
||||
"ImageMagick",
|
||||
"libreoffice-core"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,82 @@
|
||||
/**
|
||||
* Icon Generator for MarkdownConverter
|
||||
* Generates all required icon sizes from NewIcon.jpg
|
||||
*/
|
||||
|
||||
const sharp = require('sharp');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const sizes = {
|
||||
'icon.png': 512,
|
||||
'icon@2x.png': 1024,
|
||||
'icon.ico': 256,
|
||||
'icon.icns': 512
|
||||
};
|
||||
// Source image
|
||||
const sourceImage = path.join(__dirname, '..', 'assets', 'NewIcon.jpg');
|
||||
const assetsDir = path.join(__dirname, '..', 'assets');
|
||||
|
||||
const svgPath = path.join(__dirname, '..', 'assets', 'icon.svg');
|
||||
const svgBuffer = fs.readFileSync(svgPath);
|
||||
// Icon sizes needed for different platforms
|
||||
const iconSizes = [16, 24, 32, 48, 64, 128, 256, 512, 1024];
|
||||
|
||||
async function generateIcons() {
|
||||
for (const [filename, size] of Object.entries(sizes)) {
|
||||
const outputPath = path.join(__dirname, '..', 'assets', filename);
|
||||
|
||||
if (filename.endsWith('.png')) {
|
||||
await sharp(svgBuffer)
|
||||
.resize(size, size)
|
||||
.png()
|
||||
.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)`);
|
||||
}
|
||||
console.log('Generating icons from NewIcon.jpg...');
|
||||
|
||||
// Check if source exists
|
||||
if (!fs.existsSync(sourceImage)) {
|
||||
console.error('Source image not found:', sourceImage);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate main icon.png (512x512)
|
||||
await sharp(sourceImage)
|
||||
.resize(512, 512, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } })
|
||||
.png()
|
||||
.toFile(path.join(assetsDir, 'icon.png'));
|
||||
console.log('Generated icon.png (512x512)');
|
||||
|
||||
// Generate icon@2x.png (1024x1024)
|
||||
await sharp(sourceImage)
|
||||
.resize(1024, 1024, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } })
|
||||
.png()
|
||||
.toFile(path.join(assetsDir, 'icon@2x.png'));
|
||||
console.log('Generated icon@2x.png (1024x1024)');
|
||||
|
||||
// Create icons directory for multi-size icons
|
||||
const iconsDir = path.join(assetsDir, 'icons');
|
||||
if (!fs.existsSync(iconsDir)) {
|
||||
fs.mkdirSync(iconsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate all sizes
|
||||
for (const size of iconSizes) {
|
||||
await sharp(sourceImage)
|
||||
.resize(size, size, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } })
|
||||
.png()
|
||||
.toFile(path.join(iconsDir, `${size}x${size}.png`));
|
||||
console.log(`Generated icons/${size}x${size}.png`);
|
||||
}
|
||||
|
||||
// Generate favicon
|
||||
await sharp(sourceImage)
|
||||
.resize(32, 32, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } })
|
||||
.png()
|
||||
.toFile(path.join(assetsDir, 'favicon.png'));
|
||||
console.log('Generated favicon.png (32x32)');
|
||||
|
||||
// Generate tray icon (smaller, for system tray)
|
||||
await sharp(sourceImage)
|
||||
.resize(24, 24, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } })
|
||||
.png()
|
||||
.toFile(path.join(assetsDir, 'tray-icon.png'));
|
||||
console.log('Generated tray-icon.png (24x24)');
|
||||
|
||||
console.log('\nIcon generation complete!');
|
||||
console.log('\nFor Windows .ico file, use an online converter or:');
|
||||
console.log(' magick convert assets/icons/*.png assets/icon.ico');
|
||||
console.log('\nFor macOS .icns file, use:');
|
||||
console.log(' iconutil -c icns assets/icon.iconset -o assets/icon.icns');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating icons:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
generateIcons().catch(console.error);
|
||||
generateIcons();
|
||||
|
||||
@@ -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>
|
||||
@@ -3,12 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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.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 rel="stylesheet" href="styles.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/highlight.js/11.9.0/styles/default.min.css">
|
||||
<!-- Mermaid.js for diagram rendering -->
|
||||
@@ -16,6 +17,16 @@
|
||||
</head>
|
||||
<body>
|
||||
<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 active" data-tab-id="1">
|
||||
<span class="tab-title">Untitled</span>
|
||||
@@ -870,10 +881,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<input type="text" id="compress-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-compress-output">Browse</button>
|
||||
<input type="text" id="compress-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-compress-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -901,10 +915,13 @@
|
||||
</select>
|
||||
</div>
|
||||
<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">
|
||||
<input type="text" id="rotate-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-rotate-output">Browse</button>
|
||||
<input type="text" id="rotate-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-rotate-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -924,10 +941,13 @@
|
||||
<small>Separate page numbers/ranges with commas</small>
|
||||
</div>
|
||||
<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">
|
||||
<input type="text" id="delete-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-delete-output">Browse</button>
|
||||
<input type="text" id="delete-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-delete-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -953,10 +973,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<input type="text" id="reorder-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-reorder-output">Browse</button>
|
||||
<input type="text" id="reorder-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-reorder-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1009,10 +1032,13 @@
|
||||
<input type="text" id="watermark-custom-pages" placeholder="e.g., 1-5, 7, 9-12" class="hidden">
|
||||
</div>
|
||||
<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">
|
||||
<input type="text" id="watermark-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-watermark-output">Browse</button>
|
||||
<input type="text" id="watermark-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-watermark-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1058,10 +1084,13 @@
|
||||
<small>These permissions apply when owner password is set</small>
|
||||
</div>
|
||||
<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">
|
||||
<input type="text" id="encrypt-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-encrypt-output">Browse</button>
|
||||
<input type="text" id="encrypt-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-encrypt-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1081,10 +1110,13 @@
|
||||
<small>Enter the password to unlock and remove protection</small>
|
||||
</div>
|
||||
<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">
|
||||
<input type="text" id="decrypt-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-decrypt-output">Browse</button>
|
||||
<input type="text" id="decrypt-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-decrypt-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1121,10 +1153,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<input type="text" id="permissions-output-path" placeholder="Choose output location..." readonly>
|
||||
<button id="browse-permissions-output">Browse</button>
|
||||
<input type="text" id="permissions-output-path" placeholder="Select save location..." readonly>
|
||||
<button id="browse-permissions-output">Save As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1271,12 +1306,104 @@
|
||||
</div>
|
||||
<div class="pane-resizer" id="pane-resizer-1" title="Drag to resize"></div>
|
||||
<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>
|
||||
</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 class="status-bar">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron');
|
||||
const path = require('path');
|
||||
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 WordTemplateExporter = require('./wordTemplateExporter');
|
||||
|
||||
@@ -25,12 +25,95 @@ function getPandocPath() {
|
||||
function checkPandocAvailable() {
|
||||
return new Promise((resolve) => {
|
||||
const pandocPath = getPandocPath();
|
||||
exec(`${pandocPath} --version`, (error) => {
|
||||
execFile(pandocPath, ['--version'], (error) => {
|
||||
resolve(!error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe command execution using execFile (no shell injection risk)
|
||||
* @param {string} command - The command to execute
|
||||
* @param {string[]} args - Array of arguments
|
||||
* @param {object} options - Options for execFile
|
||||
* @returns {Promise<{stdout: string, stderr: string}>}
|
||||
*/
|
||||
function safeExecFile(command, args, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(command, args, { maxBuffer: 10 * 1024 * 1024, ...options }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject({ error, stdout, stderr });
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run Pandoc command safely with execFile
|
||||
* @param {string[]} args - Pandoc arguments array
|
||||
* @param {Function} callback - Callback function (error, stdout, stderr)
|
||||
*/
|
||||
function runPandoc(args, callback) {
|
||||
const pandocPath = getPandocPath();
|
||||
execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a Pandoc command string safely using execFile
|
||||
* Parses the command string and uses execFile to prevent shell injection
|
||||
* @param {string} cmdString - Full Pandoc command string (e.g., 'pandoc "input.md" -o "output.pdf"')
|
||||
* @param {Function} callback - Callback function (error, stdout, stderr)
|
||||
*/
|
||||
function runPandocCmd(cmdString, callback) {
|
||||
const parsed = parseCommand(cmdString);
|
||||
// Skip 'pandoc' if it's the first element (command itself)
|
||||
const args = parsed.command === 'pandoc' ? parsed.args : [parsed.command, ...parsed.args];
|
||||
const pandocPath = getPandocPath();
|
||||
execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a command string into command and arguments array
|
||||
* This helps transition from exec() to execFile() safely
|
||||
* @param {string} cmdString - Full command string
|
||||
* @returns {{command: string, args: string[]}}
|
||||
*/
|
||||
function parseCommand(cmdString) {
|
||||
// Handle quoted strings properly
|
||||
const parts = [];
|
||||
let current = '';
|
||||
let inQuotes = false;
|
||||
let quoteChar = '';
|
||||
|
||||
for (let i = 0; i < cmdString.length; i++) {
|
||||
const char = cmdString[i];
|
||||
if ((char === '"' || char === "'") && !inQuotes) {
|
||||
inQuotes = true;
|
||||
quoteChar = char;
|
||||
} else if (char === quoteChar && inQuotes) {
|
||||
inQuotes = false;
|
||||
quoteChar = '';
|
||||
} else if (char === ' ' && !inQuotes) {
|
||||
if (current) {
|
||||
parts.push(current);
|
||||
current = '';
|
||||
}
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
if (current) {
|
||||
parts.push(current);
|
||||
}
|
||||
|
||||
return {
|
||||
command: parts[0],
|
||||
args: parts.slice(1)
|
||||
};
|
||||
}
|
||||
|
||||
// Simple storage implementation to replace electron-store
|
||||
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
|
||||
const store = {
|
||||
@@ -180,15 +263,15 @@ if (!gotTheLock) {
|
||||
});
|
||||
}
|
||||
|
||||
// Check if pandoc is available
|
||||
// Check if pandoc is available (using execFile for consistency)
|
||||
function checkPandocAvailability() {
|
||||
return new Promise((resolve) => {
|
||||
if (pandocAvailable !== null) {
|
||||
resolve(pandocAvailable);
|
||||
return;
|
||||
}
|
||||
|
||||
exec('pandoc --version', (error, stdout, stderr) => {
|
||||
|
||||
execFile('pandoc', ['--version'], (error, stdout, stderr) => {
|
||||
pandocAvailable = !error;
|
||||
resolve(pandocAvailable);
|
||||
});
|
||||
@@ -284,6 +367,11 @@ function createMenu() {
|
||||
accelerator: 'CmdOrCtrl+O',
|
||||
click: openFile
|
||||
},
|
||||
{
|
||||
label: 'Open PDF',
|
||||
accelerator: 'CmdOrCtrl+Shift+O',
|
||||
click: openPdfFile
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
accelerator: 'CmdOrCtrl+S',
|
||||
@@ -401,29 +489,33 @@ function createMenu() {
|
||||
{
|
||||
label: 'Theme',
|
||||
submenu: [
|
||||
// Light Themes (grouped first)
|
||||
{ label: 'Atom One Light (Default)', click: () => setTheme('atomonelight') },
|
||||
{ label: 'GitHub Light', click: () => setTheme('github') },
|
||||
{ label: 'Light', click: () => setTheme('light') },
|
||||
{ label: 'Dark', click: () => setTheme('dark') },
|
||||
{ label: 'Solarized', click: () => setTheme('solarized') },
|
||||
{ label: 'Monokai', click: () => setTheme('monokai') },
|
||||
{ label: 'GitHub', click: () => setTheme('github') },
|
||||
{ label: 'Solarized Light', click: () => setTheme('solarized') },
|
||||
{ label: 'Gruvbox Light', click: () => setTheme('gruvbox-light') },
|
||||
{ label: 'Ayu Light', click: () => setTheme('ayu-light') },
|
||||
{ label: 'Sepia', click: () => setTheme('sepia') },
|
||||
{ label: 'Paper', click: () => setTheme('paper') },
|
||||
{ label: 'Rose Pine Dawn', click: () => setTheme('rosepine-dawn') },
|
||||
{ label: 'Concrete Light', click: () => setTheme('concrete-light') },
|
||||
{ type: 'separator' },
|
||||
// Dark Themes
|
||||
{ label: 'Dark', click: () => setTheme('dark') },
|
||||
{ label: 'One Dark', click: () => setTheme('onedark') },
|
||||
{ label: 'Dracula', click: () => setTheme('dracula') },
|
||||
{ label: 'Nord', click: () => setTheme('nord') },
|
||||
{ label: 'One Dark', click: () => setTheme('onedark') },
|
||||
{ label: 'Atom One Light', click: () => setTheme('atomonelight') },
|
||||
{ label: 'Monokai', click: () => setTheme('monokai') },
|
||||
{ label: 'Material', click: () => setTheme('material') },
|
||||
{ label: 'Gruvbox Dark', click: () => setTheme('gruvbox-dark') },
|
||||
{ label: 'Gruvbox Light', click: () => setTheme('gruvbox-light') },
|
||||
{ label: 'Tokyo Night', click: () => setTheme('tokyonight') },
|
||||
{ label: 'Palenight', click: () => setTheme('palenight') },
|
||||
{ label: 'Ayu Dark', click: () => setTheme('ayu-dark') },
|
||||
{ label: 'Ayu Light', click: () => setTheme('ayu-light') },
|
||||
{ label: 'Ayu Mirage', click: () => setTheme('ayu-mirage') },
|
||||
{ label: 'Oceanic Next', click: () => setTheme('oceanic-next') },
|
||||
{ label: 'Cobalt2', click: () => setTheme('cobalt2') },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Concrete Dark', click: () => setTheme('concrete-dark') },
|
||||
{ label: 'Concrete Light', click: () => setTheme('concrete-light') },
|
||||
{ label: 'Concrete Warm', click: () => setTheme('concrete-warm') }
|
||||
]
|
||||
},
|
||||
@@ -461,8 +553,25 @@ function createMenu() {
|
||||
label: 'Batch',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Convert Folder...',
|
||||
label: 'Convert Markdown Folder...',
|
||||
click: () => showBatchConversionDialog()
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Batch Image Conversion...',
|
||||
click: () => mainWindow.webContents.send('show-batch-converter', 'image')
|
||||
},
|
||||
{
|
||||
label: 'Batch Audio Conversion...',
|
||||
click: () => mainWindow.webContents.send('show-batch-converter', 'audio')
|
||||
},
|
||||
{
|
||||
label: 'Batch Video Conversion...',
|
||||
click: () => mainWindow.webContents.send('show-batch-converter', 'video')
|
||||
},
|
||||
{
|
||||
label: 'Batch PDF Conversion...',
|
||||
click: () => mainWindow.webContents.send('show-batch-converter', 'pdf')
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -473,27 +582,18 @@ function createMenu() {
|
||||
label: 'Universal File Converter...',
|
||||
accelerator: 'CmdOrCtrl+Shift+C',
|
||||
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',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Open PDF File...',
|
||||
accelerator: 'CmdOrCtrl+Shift+O',
|
||||
click: () => openPDFFile()
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Merge PDFs...',
|
||||
click: () => showPDFEditorDialog('merge')
|
||||
@@ -570,12 +670,15 @@ function createMenu() {
|
||||
submenu: [
|
||||
{
|
||||
label: 'Table Generator',
|
||||
click: () => mainWindow.webContents.send('show-table-generator')
|
||||
accelerator: 'CmdOrCtrl+Shift+T',
|
||||
click: () => openTableGenerator()
|
||||
},
|
||||
{
|
||||
label: 'ASCII Art Generator',
|
||||
click: () => mainWindow.webContents.send('show-ascii-generator')
|
||||
accelerator: 'CmdOrCtrl+Shift+A',
|
||||
click: () => openAsciiGenerator()
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Document Compare',
|
||||
click: () => mainWindow.webContents.send('show-document-compare')
|
||||
@@ -586,20 +689,26 @@ function createMenu() {
|
||||
label: 'Help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'About',
|
||||
click: () => {
|
||||
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']
|
||||
});
|
||||
}
|
||||
label: 'About MarkdownConverter',
|
||||
click: () => showAboutDialog()
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Dependencies & Requirements',
|
||||
click: () => showDependenciesDialog()
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// 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() {
|
||||
const files = dialog.showOpenDialogSync(mainWindow, {
|
||||
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() {
|
||||
const file = dialog.showSaveDialogSync(mainWindow, {
|
||||
defaultExt: '.md',
|
||||
@@ -1133,15 +1487,15 @@ async function exportPDFViaWordTemplate() {
|
||||
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
|
||||
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'
|
||||
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
|
||||
? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe'
|
||||
: 'soffice';
|
||||
|
||||
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
|
||||
try {
|
||||
fs.unlinkSync(tempDocxPath);
|
||||
@@ -1190,26 +1544,34 @@ function showPDFEditorDialog(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) {
|
||||
return new Promise((resolve) => {
|
||||
let command;
|
||||
const isWin = process.platform === 'win32';
|
||||
const locateCmd = isWin ? 'where' : 'which';
|
||||
let toolName;
|
||||
|
||||
switch (tool) {
|
||||
case 'libreoffice':
|
||||
command = process.platform === 'win32' ? 'where soffice' : 'which soffice';
|
||||
toolName = 'soffice';
|
||||
break;
|
||||
case 'imagemagick':
|
||||
command = process.platform === 'win32' ? 'where magick' : 'which convert';
|
||||
toolName = isWin ? 'magick' : 'convert';
|
||||
break;
|
||||
case 'ffmpeg':
|
||||
command = process.platform === 'win32' ? 'where ffmpeg' : 'which ffmpeg';
|
||||
toolName = 'ffmpeg';
|
||||
break;
|
||||
default:
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
exec(command, (error) => {
|
||||
execFile(locateCmd, [toolName], (error) => {
|
||||
resolve(!error);
|
||||
});
|
||||
});
|
||||
@@ -1230,26 +1592,27 @@ ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, file
|
||||
mainWindow.webContents.send('conversion-status', 'Converting file...');
|
||||
|
||||
const outputPath = filePath.replace(/\.[^/.]+$/, `.${toFormat}`);
|
||||
let conversionCmd;
|
||||
let conversionInfo;
|
||||
|
||||
switch (tool) {
|
||||
case 'libreoffice':
|
||||
conversionCmd = convertWithLibreOffice(filePath, toFormat, outputPath);
|
||||
conversionInfo = convertWithLibreOffice(filePath, toFormat, outputPath);
|
||||
break;
|
||||
case 'imagemagick':
|
||||
conversionCmd = convertWithImageMagick(filePath, outputPath);
|
||||
conversionInfo = convertWithImageMagick(filePath, outputPath);
|
||||
break;
|
||||
case 'ffmpeg':
|
||||
conversionCmd = convertWithFFmpeg(filePath, outputPath);
|
||||
conversionInfo = convertWithFFmpeg(filePath, outputPath);
|
||||
break;
|
||||
case 'pandoc':
|
||||
conversionCmd = `pandoc "${filePath}" -o "${outputPath}"`;
|
||||
conversionInfo = convertWithPandoc(filePath, outputPath);
|
||||
break;
|
||||
default:
|
||||
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) {
|
||||
mainWindow.webContents.send('conversion-complete', {
|
||||
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) {
|
||||
const outputDir = path.dirname(outputPath);
|
||||
const soffice = process.platform === 'win32'
|
||||
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
|
||||
? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe'
|
||||
: 'soffice';
|
||||
|
||||
// LibreOffice conversion format mapping
|
||||
@@ -1321,19 +1684,36 @@ function convertWithLibreOffice(inputFile, outputFormat, outputPath) {
|
||||
|
||||
const format = formatMap[outputFormat] || outputFormat;
|
||||
|
||||
// Use headless mode for conversion
|
||||
return `${soffice} --headless --convert-to ${format} --outdir "${outputDir}" "${inputFile}"`;
|
||||
// Return command and args for execFile
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
@@ -1449,8 +1829,8 @@ function performExportWithOptions(format, options) {
|
||||
pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"';
|
||||
}
|
||||
|
||||
// Try with specified PDF engine
|
||||
exec(pandocCmd, (error) => {
|
||||
// Try with specified PDF engine (using runPandocCmd for safety)
|
||||
runPandocCmd(pandocCmd, (error) => {
|
||||
if (error) {
|
||||
// Try fallback engines if the specified one fails
|
||||
const fallbackEngines = ['pdflatex', 'lualatex'];
|
||||
@@ -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) {
|
||||
tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error);
|
||||
} 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) {
|
||||
console.log(`Executing Pandoc command: ${pandocCmd}`);
|
||||
|
||||
exec(pandocCmd, async (error, stdout, stderr) => {
|
||||
runPandocCmd(pandocCmd, async (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Pandoc error for ${format}:`, error);
|
||||
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) {
|
||||
exec(pandocCmd, (error, stdout, stderr) => {
|
||||
runPandocCmd(pandocCmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.log('XeLaTeX failed, trying PDFLaTeX...');
|
||||
// Fallback to pdflatex
|
||||
const fallbackCmd = pandocCmd.replace('--pdf-engine=xelatex', '--pdf-engine=pdflatex');
|
||||
exec(fallbackCmd, (fallbackError, fallbackStdout, fallbackStderr) => {
|
||||
runPandocCmd(fallbackCmd, (fallbackError, fallbackStdout, fallbackStderr) => {
|
||||
if (fallbackError) {
|
||||
console.log('PDFLaTeX failed, trying Electron PDF...');
|
||||
// Final fallback to Electron PDF
|
||||
@@ -1970,10 +2351,10 @@ function importDocument() {
|
||||
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}"`;
|
||||
|
||||
exec(pandocCmd, (error, stdout, stderr) => {
|
||||
runPandocCmd(pandocCmd, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
dialog.showErrorBox('Import Error', `Failed to import: ${error.message}\n\nMake sure Pandoc is installed.\n\nSupported formats: DOCX, ODT, RTF, HTML, LaTeX, EPUB, PDF, PPTX, ODP, RST, Textile, MediaWiki, Org-mode, AsciiDoc, CSV, and more.`);
|
||||
} else {
|
||||
@@ -2014,7 +2395,7 @@ ipcMain.on('save-current-file', (event, content) => {
|
||||
});
|
||||
|
||||
ipcMain.on('get-theme', (event) => {
|
||||
const theme = store.get('theme', 'github');
|
||||
const theme = store.get('theme', 'atomonelight');
|
||||
event.reply('theme-changed', theme);
|
||||
});
|
||||
|
||||
@@ -2288,15 +2669,15 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
|
||||
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
|
||||
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'
|
||||
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
|
||||
? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe'
|
||||
: 'soffice';
|
||||
|
||||
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
|
||||
try {
|
||||
fs.unlinkSync(tempDocxPath);
|
||||
@@ -2447,8 +2828,8 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
|
||||
pandocCmd += ` --variable footer="${footerText}"`;
|
||||
}
|
||||
|
||||
// Execute conversion
|
||||
exec(pandocCmd, async (error, stdout, stderr) => {
|
||||
// Execute conversion (using runPandocCmd for safety)
|
||||
runPandocCmd(pandocCmd, async (error, stdout, stderr) => {
|
||||
if (!error) {
|
||||
// Add headers/footers to DOCX if enabled
|
||||
if (format === 'docx' && headerFooterSettings.enabled) {
|
||||
@@ -2563,24 +2944,25 @@ function performCLIConversion(inputPath, format) {
|
||||
|
||||
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);
|
||||
|
||||
exec(pandocCommand, (error, stdout, stderr) => {
|
||||
|
||||
runPandocCmd(pandocCommand, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Conversion failed: ${error.message}`);
|
||||
if (stderr) console.error(`Details: ${stderr}`);
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.log(`Successfully converted to: ${outputPath}`);
|
||||
|
||||
// Show Windows notification
|
||||
|
||||
// Show Windows notification (using exec for PowerShell is acceptable here - hardcoded command)
|
||||
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();
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -3329,4 +3711,85 @@ ipcMain.on('select-pdf-folder', (event, inputId) => {
|
||||
if (folder && folder[0]) {
|
||||
event.reply('pdf-folder-selected', { inputId, path: folder[0] });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// ASCII Art Generator Window
|
||||
// ============================================
|
||||
let asciiGeneratorWindow = null;
|
||||
|
||||
function openAsciiGenerator() {
|
||||
if (asciiGeneratorWindow) {
|
||||
asciiGeneratorWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
asciiGeneratorWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 700,
|
||||
parent: mainWindow,
|
||||
modal: false,
|
||||
title: 'ASCII Art Generator',
|
||||
icon: path.join(__dirname, '../assets/icon.png'),
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
});
|
||||
|
||||
asciiGeneratorWindow.loadFile(path.join(__dirname, 'ascii-generator.html'));
|
||||
asciiGeneratorWindow.setMenuBarVisibility(false);
|
||||
|
||||
asciiGeneratorWindow.on('closed', () => {
|
||||
asciiGeneratorWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.on('open-ascii-generator', () => {
|
||||
openAsciiGenerator();
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Table Generator Window
|
||||
// ============================================
|
||||
let tableGeneratorWindow = null;
|
||||
|
||||
function openTableGenerator() {
|
||||
if (tableGeneratorWindow) {
|
||||
tableGeneratorWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
tableGeneratorWindow = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 700,
|
||||
parent: mainWindow,
|
||||
modal: false,
|
||||
title: 'Table Generator',
|
||||
icon: path.join(__dirname, '../assets/icon.png'),
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
});
|
||||
|
||||
tableGeneratorWindow.loadFile(path.join(__dirname, 'table-generator.html'));
|
||||
tableGeneratorWindow.setMenuBarVisibility(false);
|
||||
|
||||
tableGeneratorWindow.on('closed', () => {
|
||||
tableGeneratorWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.on('open-table-generator', () => {
|
||||
openTableGenerator();
|
||||
});
|
||||
|
||||
// IPC Handler to receive generated content from generator windows
|
||||
ipcMain.on('insert-generated-content', (event, content) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('insert-content', content);
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* MarkdownConverter Renderer Process
|
||||
* @version 3.0.0
|
||||
*/
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
const marked = require('marked');
|
||||
const DOMPurify = require('dompurify');
|
||||
@@ -10,7 +15,6 @@ marked.setOptions({
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
} catch (err) {
|
||||
// Fallback to auto highlighting if language-specific highlighting fails
|
||||
console.warn('Syntax highlighting failed for language:', lang, err.message);
|
||||
}
|
||||
}
|
||||
@@ -308,6 +312,11 @@ class TabManager {
|
||||
if (!tab || !preview) return;
|
||||
|
||||
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 sanitizedHtml = DOMPurify.sanitize(html);
|
||||
preview.innerHTML = sanitizedHtml;
|
||||
@@ -1043,7 +1052,6 @@ class TabManager {
|
||||
this.updateTabBar();
|
||||
|
||||
// Notify main process about current file for exports
|
||||
const { ipcRenderer } = require('electron');
|
||||
ipcRenderer.send('set-current-file', filePath);
|
||||
|
||||
console.log('File opened successfully');
|
||||
@@ -2308,12 +2316,12 @@ ipcRenderer.on('show-table-generator', () => {
|
||||
});
|
||||
|
||||
// Show PDF Editor Dialog
|
||||
ipcRenderer.on('show-pdf-editor-dialog', (event, operation) => {
|
||||
ipcRenderer.on('show-pdf-editor-dialog', (event, operation, openedFilePath) => {
|
||||
currentPDFOperation = operation;
|
||||
showPDFEditorDialog(operation);
|
||||
showPDFEditorDialog(operation, openedFilePath);
|
||||
});
|
||||
|
||||
function showPDFEditorDialog(operation) {
|
||||
function showPDFEditorDialog(operation, openedFilePath = null) {
|
||||
const dialog = document.getElementById('pdf-editor-dialog');
|
||||
const title = document.getElementById('pdf-editor-title');
|
||||
|
||||
@@ -2329,43 +2337,82 @@ function showPDFEditorDialog(operation) {
|
||||
sectionId = 'pdf-merge-section';
|
||||
titleText = 'Merge PDFs';
|
||||
mergeFilePaths = [];
|
||||
// If we have an opened file, add it as the first file to merge
|
||||
if (openedFilePath) {
|
||||
mergeFilePaths.push(openedFilePath);
|
||||
}
|
||||
updateMergeFilesList();
|
||||
break;
|
||||
case 'split':
|
||||
sectionId = 'pdf-split-section';
|
||||
titleText = 'Split PDF';
|
||||
// Pre-fill input path if we have an opened file
|
||||
if (openedFilePath) {
|
||||
document.getElementById('split-input-path').value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'compress':
|
||||
sectionId = 'pdf-compress-section';
|
||||
titleText = 'Compress PDF';
|
||||
if (openedFilePath) {
|
||||
document.getElementById('compress-input-path').value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'rotate':
|
||||
sectionId = 'pdf-rotate-section';
|
||||
titleText = 'Rotate Pages';
|
||||
if (openedFilePath) {
|
||||
const rotateInput = document.getElementById('rotate-input-path');
|
||||
if (rotateInput) rotateInput.value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
sectionId = 'pdf-delete-section';
|
||||
titleText = 'Delete Pages';
|
||||
if (openedFilePath) {
|
||||
const deleteInput = document.getElementById('delete-input-path');
|
||||
if (deleteInput) deleteInput.value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'reorder':
|
||||
sectionId = 'pdf-reorder-section';
|
||||
titleText = 'Reorder Pages';
|
||||
if (openedFilePath) {
|
||||
const reorderInput = document.getElementById('reorder-input-path');
|
||||
if (reorderInput) reorderInput.value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'watermark':
|
||||
sectionId = 'pdf-watermark-section';
|
||||
titleText = 'Add Watermark';
|
||||
if (openedFilePath) {
|
||||
const watermarkInput = document.getElementById('watermark-input-path');
|
||||
if (watermarkInput) watermarkInput.value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'encrypt':
|
||||
sectionId = 'pdf-encrypt-section';
|
||||
titleText = 'Password Protection';
|
||||
if (openedFilePath) {
|
||||
const encryptInput = document.getElementById('encrypt-input-path');
|
||||
if (encryptInput) encryptInput.value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'decrypt':
|
||||
sectionId = 'pdf-decrypt-section';
|
||||
titleText = 'Remove Password';
|
||||
if (openedFilePath) {
|
||||
const decryptInput = document.getElementById('decrypt-input-path');
|
||||
if (decryptInput) decryptInput.value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
case 'permissions':
|
||||
sectionId = 'pdf-permissions-section';
|
||||
titleText = 'Set Permissions';
|
||||
if (openedFilePath) {
|
||||
const permInput = document.getElementById('permissions-input-path');
|
||||
if (permInput) permInput.value = openedFilePath;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
const loadCurrentOrder = document.getElementById('load-current-order');
|
||||
if (loadCurrentOrder) {
|
||||
@@ -2619,33 +2692,36 @@ function processPDFOperation() {
|
||||
|
||||
case 'compress':
|
||||
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.compressImages = document.getElementById('compress-images').checked;
|
||||
operationData.removeDuplicates = document.getElementById('compress-remove-duplicates').checked;
|
||||
operationData.optimizeFonts = document.getElementById('compress-optimize-fonts').checked;
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath) {
|
||||
alert('Please select input and output file paths');
|
||||
alert('Please select input file' + (operationData.overwrite ? '' : ' and output file paths'));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'rotate':
|
||||
operationData.inputPath = document.getElementById('rotate-input-path').value.trim();
|
||||
operationData.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.angle = parseInt(document.getElementById('rotate-angle').value);
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath) {
|
||||
alert('Please select input and output file paths');
|
||||
alert('Please select input file' + (operationData.overwrite ? '' : ' and output file'));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
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();
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.pages) {
|
||||
@@ -2656,7 +2732,8 @@ function processPDFOperation() {
|
||||
|
||||
case 'reorder':
|
||||
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();
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.newOrder) {
|
||||
@@ -2667,7 +2744,8 @@ function processPDFOperation() {
|
||||
|
||||
case 'watermark':
|
||||
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.fontSize = parseInt(document.getElementById('watermark-font-size').value);
|
||||
operationData.opacity = parseInt(document.getElementById('watermark-opacity').value) / 100;
|
||||
@@ -2687,7 +2765,8 @@ function processPDFOperation() {
|
||||
|
||||
case 'encrypt':
|
||||
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.ownerPassword = document.getElementById('encrypt-owner-password').value;
|
||||
operationData.encryptionLevel = parseInt(document.getElementById('encrypt-level').value);
|
||||
@@ -2704,14 +2783,15 @@ function processPDFOperation() {
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'decrypt':
|
||||
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;
|
||||
|
||||
if (!operationData.inputPath || !operationData.outputPath || !operationData.password) {
|
||||
@@ -2722,7 +2802,8 @@ function processPDFOperation() {
|
||||
|
||||
case 'permissions':
|
||||
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.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.`);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1031,52 +1031,82 @@ body.theme-github .status-bar {
|
||||
|
||||
.export-dialog-content {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
width: 85%;
|
||||
max-width: 450px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.export-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.export-dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.export-dialog-body {
|
||||
padding: 20px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.export-section {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.export-section label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.export-section label.checkbox-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.export-section label.checkbox-inline input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* PDF Editor buttons */
|
||||
#add-merge-file {
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
background: #4a90d9;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#add-merge-file:hover {
|
||||
background: #357abd;
|
||||
}
|
||||
|
||||
.export-section select,
|
||||
.export-section input[type="text"],
|
||||
.export-section input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.export-section select:focus,
|
||||
@@ -1280,9 +1310,10 @@ body.theme-dark #add-metadata-field {
|
||||
|
||||
/* PDF Editor Specific Styles */
|
||||
.pdf-editor-content {
|
||||
max-width: 600px !important;
|
||||
max-height: 85vh;
|
||||
max-width: 450px !important;
|
||||
max-height: 75vh;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pdf-editor-body {
|
||||
@@ -1418,12 +1449,13 @@ body.theme-dark #export-dialog-close:hover {
|
||||
|
||||
.batch-dialog-content {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
width: 85%;
|
||||
max-width: 450px;
|
||||
max-height: 75vh;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.batch-dialog-header {
|
||||
@@ -3152,6 +3184,250 @@ body.theme-concrete-warm .status-bar {
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Sepia Theme - Warm reading theme
|
||||
======================================== */
|
||||
body.theme-sepia {
|
||||
background: #f4ecd8;
|
||||
color: #5b4636;
|
||||
}
|
||||
|
||||
body.theme-sepia .tab-bar {
|
||||
background: #e8dcc8;
|
||||
border-bottom-color: #d4c4a8;
|
||||
}
|
||||
|
||||
body.theme-sepia .tab {
|
||||
background: #e8dcc8;
|
||||
border-color: #d4c4a8;
|
||||
color: #5b4636;
|
||||
}
|
||||
|
||||
body.theme-sepia .tab.active {
|
||||
background: #f4ecd8;
|
||||
border-bottom-color: #8b7355;
|
||||
}
|
||||
|
||||
body.theme-sepia .toolbar {
|
||||
background: #e8dcc8;
|
||||
border-bottom-color: #d4c4a8;
|
||||
}
|
||||
|
||||
body.theme-sepia .toolbar button {
|
||||
color: #5b4636;
|
||||
}
|
||||
|
||||
body.theme-sepia .toolbar button:hover {
|
||||
background: #d4c4a8;
|
||||
}
|
||||
|
||||
body.theme-sepia .editor-textarea {
|
||||
background: #f4ecd8;
|
||||
color: #5b4636;
|
||||
}
|
||||
|
||||
body.theme-sepia .pane:last-child {
|
||||
background: #f4ecd8;
|
||||
}
|
||||
|
||||
body.theme-sepia .preview-content {
|
||||
color: #5b4636;
|
||||
}
|
||||
|
||||
body.theme-sepia .preview-content h1,
|
||||
body.theme-sepia .preview-content h2 {
|
||||
color: #3d2914;
|
||||
border-bottom-color: #d4c4a8;
|
||||
}
|
||||
|
||||
body.theme-sepia .preview-content code {
|
||||
background: #e8dcc8;
|
||||
color: #8b4513;
|
||||
}
|
||||
|
||||
body.theme-sepia .preview-content pre {
|
||||
background: #e8dcc8;
|
||||
border-color: #d4c4a8;
|
||||
}
|
||||
|
||||
body.theme-sepia .preview-content a {
|
||||
color: #8b4513;
|
||||
}
|
||||
|
||||
body.theme-sepia .preview-content blockquote {
|
||||
border-left-color: #8b7355;
|
||||
background: #e8dcc8;
|
||||
}
|
||||
|
||||
body.theme-sepia .status-bar {
|
||||
background: #e8dcc8;
|
||||
border-top-color: #d4c4a8;
|
||||
color: #5b4636;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Paper Theme - Clean white paper
|
||||
======================================== */
|
||||
body.theme-paper {
|
||||
background: #ffffff;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
body.theme-paper .tab-bar {
|
||||
background: #f8f8f8;
|
||||
border-bottom-color: #e0e0e0;
|
||||
}
|
||||
|
||||
body.theme-paper .tab {
|
||||
background: #f0f0f0;
|
||||
border-color: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
body.theme-paper .tab.active {
|
||||
background: #ffffff;
|
||||
border-bottom-color: #1a1a1a;
|
||||
}
|
||||
|
||||
body.theme-paper .toolbar {
|
||||
background: #f8f8f8;
|
||||
border-bottom-color: #e0e0e0;
|
||||
}
|
||||
|
||||
body.theme-paper .toolbar button {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
body.theme-paper .toolbar button:hover {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
body.theme-paper .editor-textarea {
|
||||
background: #ffffff;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
body.theme-paper .pane:last-child {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
body.theme-paper .preview-content {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
body.theme-paper .preview-content h1,
|
||||
body.theme-paper .preview-content h2 {
|
||||
color: #000;
|
||||
border-bottom-color: #e0e0e0;
|
||||
}
|
||||
|
||||
body.theme-paper .preview-content code {
|
||||
background: #f5f5f5;
|
||||
color: #c41a16;
|
||||
}
|
||||
|
||||
body.theme-paper .preview-content pre {
|
||||
background: #f5f5f5;
|
||||
border-color: #e0e0e0;
|
||||
}
|
||||
|
||||
body.theme-paper .preview-content a {
|
||||
color: #0366d6;
|
||||
}
|
||||
|
||||
body.theme-paper .preview-content blockquote {
|
||||
border-left-color: #ddd;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
body.theme-paper .status-bar {
|
||||
background: #f8f8f8;
|
||||
border-top-color: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Rose Pine Dawn - Warm rose-tinted light
|
||||
======================================== */
|
||||
body.theme-rosepine-dawn {
|
||||
background: #faf4ed;
|
||||
color: #575279;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .tab-bar {
|
||||
background: #f2e9e1;
|
||||
border-bottom-color: #dfdad9;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .tab {
|
||||
background: #f2e9e1;
|
||||
border-color: #dfdad9;
|
||||
color: #575279;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .tab.active {
|
||||
background: #faf4ed;
|
||||
border-bottom-color: #907aa9;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .toolbar {
|
||||
background: #f2e9e1;
|
||||
border-bottom-color: #dfdad9;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .toolbar button {
|
||||
color: #575279;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .toolbar button:hover {
|
||||
background: #dfdad9;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .editor-textarea {
|
||||
background: #faf4ed;
|
||||
color: #575279;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .pane:last-child {
|
||||
background: #faf4ed;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .preview-content {
|
||||
color: #575279;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .preview-content h1,
|
||||
body.theme-rosepine-dawn .preview-content h2 {
|
||||
color: #286983;
|
||||
border-bottom-color: #dfdad9;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .preview-content code {
|
||||
background: #f2e9e1;
|
||||
color: #b4637a;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .preview-content pre {
|
||||
background: #f2e9e1;
|
||||
border-color: #dfdad9;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .preview-content a {
|
||||
color: #56949f;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .preview-content blockquote {
|
||||
border-left-color: #907aa9;
|
||||
background: #f2e9e1;
|
||||
color: #797593;
|
||||
}
|
||||
|
||||
body.theme-rosepine-dawn .status-bar {
|
||||
background: #f2e9e1;
|
||||
border-top-color: #dfdad9;
|
||||
color: #575279;
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
/* Hide UI elements for clean print output */
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||