Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c57270599 | ||
|
|
9727a7fc2d | ||
|
|
da3301bdcf | ||
|
|
b96a6e5bd2 | ||
|
|
3a3f0b9ac1 | ||
|
|
c2b77e110f | ||
|
|
e63a0ec1bf | ||
|
|
f197acb960 | ||
|
|
4966c671de | ||
|
|
5e5fcf3172 | ||
|
|
bcbc82e36b | ||
|
|
53871d788b | ||
|
|
6db204796f | ||
|
|
8fbfe68823 | ||
|
|
c5d310b465 | ||
|
|
f6116d7008 | ||
|
|
1d89a91de0 | ||
|
|
ffbde12281 | ||
|
|
bbb51f4ca2 | ||
|
|
cd06909e2c | ||
|
|
fc6bfb3003 | ||
|
|
d114308fc4 | ||
|
|
2999882318 | ||
|
|
80bf533483 | ||
|
|
2d4d5802b6 | ||
|
|
7dc1cc048e | ||
|
|
bd94b9fa81 | ||
|
|
416afce88a | ||
|
|
3ccf8d102e | ||
|
|
b326772518 | ||
|
|
4514b2fab8 | ||
|
|
1a6ca50e03 | ||
|
|
55da44e0a8 | ||
|
|
ba23f2b702 | ||
|
|
8804b689b3 | ||
|
|
818a746a88 | ||
|
|
a02f96275e | ||
|
|
d2fab56be9 | ||
|
|
39645ecf8e | ||
|
|
a97c2f1537 | ||
|
|
5609086106 | ||
|
|
1ae4f2d41f | ||
|
|
51baae774d | ||
|
|
282af0836c | ||
|
|
0a1b623278 | ||
|
|
45b2063419 | ||
|
|
e46fd1d7e6 | ||
|
|
2436412ef1 | ||
|
|
c3f147d978 | ||
|
|
0e59375cad | ||
|
|
85bb1d7bb9 | ||
|
|
8f333fb810 | ||
|
|
11fc00160f | ||
|
|
f65cbdedb3 | ||
|
|
7c8e206764 | ||
|
|
2cad330e3d | ||
|
|
1815a3d078 | ||
|
|
64d53bd637 | ||
|
|
24576df3b0 | ||
|
|
96134f0aa8 | ||
|
|
afb88f0326 | ||
|
|
f5a6457734 | ||
|
|
d785628e45 | ||
|
|
856b12e7a1 | ||
|
|
0b49f849b4 | ||
|
|
df7c97ff84 | ||
|
|
fb04e9e32f | ||
|
|
f8eab068b0 | ||
|
|
6d0c3eca3d | ||
|
|
32fa6719cf | ||
|
|
61e6b4a299 | ||
|
|
4fed06a80d | ||
|
|
0fd20c1eb3 | ||
|
|
337b5afe5c | ||
|
|
b11fd95e94 | ||
|
|
49f494b429 | ||
|
|
d0910f14b3 | ||
|
|
9d8acd0171 | ||
|
|
c1d71e8dbf | ||
|
|
1e585afb7e | ||
|
|
20abd7a533 | ||
|
|
05a3910f0d | ||
|
|
72fbf73f05 | ||
|
|
be84864b5b | ||
|
|
547a9fdffd | ||
|
|
44a12f7805 | ||
|
|
4ea771b757 |
@@ -15,4 +15,17 @@ out/
|
||||
.cache/
|
||||
.npm/
|
||||
.electron/
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
|
||||
# Screenshots and temp files
|
||||
*.png.bak
|
||||
Screen.png
|
||||
dark.png
|
||||
light.png
|
||||
pdf.png
|
||||
uvmodal.png
|
||||
nul
|
||||
*.tmp
|
||||
|
||||
# Development screenshots
|
||||
pdf\ modal.png
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"printWidth": 100,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"CurrentProjectSetting": null
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"ExpandedNodes": [
|
||||
""
|
||||
],
|
||||
"SelectedNode": "\\package.json",
|
||||
"PreviewInSolutionExplorer": false
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"Version": 1,
|
||||
"WorkspaceRootPath": "H:\\Development\\pan-converter\\",
|
||||
"Documents": [
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|H:\\Development\\pan-converter\\package.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
|
||||
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:package.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
|
||||
}
|
||||
],
|
||||
"DocumentGroupContainers": [
|
||||
{
|
||||
"Orientation": 0,
|
||||
"VerticalTabListWidth": 256,
|
||||
"DocumentGroups": [
|
||||
{
|
||||
"DockedWidth": 200,
|
||||
"SelectedChildIndex": 0,
|
||||
"Children": [
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 0,
|
||||
"Title": "package.json",
|
||||
"DocumentMoniker": "H:\\Development\\pan-converter\\package.json",
|
||||
"RelativeDocumentMoniker": "package.json",
|
||||
"ToolTip": "H:\\Development\\pan-converter\\package.json",
|
||||
"RelativeToolTip": "package.json",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAqAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|",
|
||||
"WhenOpened": "2025-10-11T13:44:30.357Z",
|
||||
"EditorCaption": ""
|
||||
},
|
||||
{
|
||||
"$type": "Bookmark",
|
||||
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"Version": 1,
|
||||
"WorkspaceRootPath": "H:\\Development\\pan-converter\\",
|
||||
"Documents": [
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|H:\\Development\\pan-converter\\package.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
|
||||
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:package.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
|
||||
}
|
||||
],
|
||||
"DocumentGroupContainers": [
|
||||
{
|
||||
"Orientation": 0,
|
||||
"VerticalTabListWidth": 256,
|
||||
"DocumentGroups": [
|
||||
{
|
||||
"DockedWidth": 200,
|
||||
"SelectedChildIndex": 0,
|
||||
"Children": [
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 0,
|
||||
"Title": "package.json",
|
||||
"DocumentMoniker": "H:\\Development\\pan-converter\\package.json",
|
||||
"RelativeDocumentMoniker": "package.json",
|
||||
"ToolTip": "H:\\Development\\pan-converter\\package.json",
|
||||
"RelativeToolTip": "package.json",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAqAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|",
|
||||
"WhenOpened": "2025-10-11T13:44:30.357Z",
|
||||
"EditorCaption": ""
|
||||
},
|
||||
{
|
||||
"$type": "Bookmark",
|
||||
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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**: v1.7.1
|
||||
**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
|
||||
|
||||
@@ -19,21 +19,34 @@
|
||||
- **highlight.js** - Syntax highlighting
|
||||
- **KaTeX** - Mathematical expression rendering
|
||||
- **DOMPurify** - HTML sanitization
|
||||
- **PDFKit** - Native PDF generation without external dependencies (v1.7.7)
|
||||
- **html2pdf** - HTML to PDF conversion library (v1.7.7)
|
||||
|
||||
### 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
|
||||
@@ -111,7 +124,725 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
|
||||
|
||||
## Feature Implementation Guide
|
||||
|
||||
### v1.7.1 Modern UI Design (Latest)
|
||||
### v3.0.0 Major UI Overhaul & Media Converters (Latest)
|
||||
|
||||
#### Rebranding & UI Improvements
|
||||
**Renamed to MarkdownConverter with ConcreteInfo Branding** (`src/index.html`, `package.json`)
|
||||
|
||||
**Changes:**
|
||||
- Renamed application from "PanConverter" to "MarkdownConverter"
|
||||
- Added ConcreteInfo logo in app header
|
||||
- Removed preview popout button (per user feedback)
|
||||
- Version updated to 3.0.0
|
||||
|
||||
#### ConcreteInfo Theme System
|
||||
**New Theme Based on Logo Palette** (`src/styles-concreteinfo.css`)
|
||||
|
||||
**Color Palette:**
|
||||
- Dark Gray: #464646
|
||||
- Medium Gray: #9a9696
|
||||
- Accent (Red): #e5461f
|
||||
- Light Gray: #e3e3e3
|
||||
- Black: #0d0b09
|
||||
|
||||
**Theme Variants:**
|
||||
- Concrete Dark - Dark mode with red accents
|
||||
- Concrete Light - Clean light mode
|
||||
- Concrete Warm - Warm tones with gray toolbar
|
||||
|
||||
#### Separate Generator Windows
|
||||
**ASCII Art & Table Generators in Dedicated Windows** (`src/ascii-generator.html`, `src/table-generator.html`, `src/main.js`)
|
||||
|
||||
**ASCII Art Generator Features:**
|
||||
- Text Banner mode with 5 font styles (Standard, Banner, Block, Bubble, Digital)
|
||||
- Box/Frame mode with 5 styles (Single, Double, Rounded, Bold, ASCII)
|
||||
- 16 professional templates (arrows, flowcharts, diagrams, decorative)
|
||||
- Real-time preview with terminal-style display
|
||||
- One-click insert to editor
|
||||
|
||||
**Table Generator Features:**
|
||||
- Adjustable rows and columns (1-50 rows, 1-20 columns)
|
||||
- Quick templates (2-column, 3-column, 4x4 grid, etc.)
|
||||
- Left/Center/Right alignment
|
||||
- Optional header row
|
||||
- Fill sample data option
|
||||
- Live Markdown preview
|
||||
- Copy or insert to editor
|
||||
|
||||
**Access:**
|
||||
- Tools → ASCII Art Generator (Ctrl+Shift+A)
|
||||
- Tools → Table Generator (Ctrl+Shift+T)
|
||||
|
||||
#### Media Converter Menus
|
||||
**Separate Menus for Image, Audio, and Video Conversion** (`src/main.js`)
|
||||
|
||||
**Image Menu:**
|
||||
- Convert Image... (single file)
|
||||
- Batch Convert Images...
|
||||
- Resize Image...
|
||||
- Compress Image...
|
||||
- Rotate Image...
|
||||
|
||||
**Audio Menu:**
|
||||
- Convert Audio...
|
||||
- Batch Convert Audio...
|
||||
- Extract Audio from Video...
|
||||
- Trim Audio...
|
||||
- Merge Audio Files...
|
||||
|
||||
**Video Menu:**
|
||||
- Convert Video...
|
||||
- Batch Convert Videos...
|
||||
- Compress Video...
|
||||
- Trim Video...
|
||||
- Extract Frames...
|
||||
- Convert to GIF...
|
||||
|
||||
**Batch Menu:**
|
||||
- Convert Markdown Folder...
|
||||
- Batch Image Conversion...
|
||||
- Batch Audio Conversion...
|
||||
- Batch Video Conversion...
|
||||
- Batch PDF Conversion...
|
||||
|
||||
#### Enhanced PDF Editor
|
||||
**Open PDF Files Directly** (`src/main.js`)
|
||||
|
||||
- New "Open PDF File..." option in PDF Editor menu
|
||||
- View and edit PDFs within the application
|
||||
- All PDF operations available (merge, split, compress, rotate, etc.)
|
||||
|
||||
#### About Menu & Dependencies
|
||||
**Updated Help Menu** (`src/main.js`)
|
||||
|
||||
- "About MarkdownConverter" shows app info with ConcreteInfo logo
|
||||
- "Dependencies & Requirements" lists all required tools with download links:
|
||||
- Pandoc (required)
|
||||
- FFmpeg (optional, for media conversion)
|
||||
- ImageMagick (optional, for image processing)
|
||||
- LibreOffice (optional, for document conversion)
|
||||
- MiKTeX/TeX Live (optional, for PDF via LaTeX)
|
||||
|
||||
#### Security Enhancements (v2.2.0 continued)
|
||||
**Secure Preload Script** (`src/preload.js`)
|
||||
|
||||
- New IPC channels for media converters
|
||||
- Secure generator window communication
|
||||
- Insert content from generator windows
|
||||
|
||||
---
|
||||
|
||||
### v1.9.1 ASCII Art & Code Block Preservation
|
||||
|
||||
#### 🎨 Preserved ASCII Art in Exports
|
||||
**ASCII Art, Charts, Tables & Flowcharts Now Export Exactly as Previewed** (`src/wordTemplateExporter.js`, `src/main.js`)
|
||||
|
||||
**Problem Solved:**
|
||||
- Previously, ASCII art, flowcharts, and code blocks were being corrupted during export to Word and PDF
|
||||
- Line breaks, spacing, and monospace alignment were lost
|
||||
- Box-drawing characters and diagrams became unreadable
|
||||
|
||||
**Solution Implemented:**
|
||||
1. **Enhanced Word Template Exporter** (`src/wordTemplateExporter.js:259-289`)
|
||||
- Code blocks now render each line as a separate paragraph with exact spacing
|
||||
- `white-space: pre` and `word-wrap: normal` prevent text wrapping
|
||||
- Monospace font (Consolas) with consistent 18pt size
|
||||
- Light gray background (#F5F5F5) for visual distinction
|
||||
- `<w:keepLines/>` and `<w:wordWrap w:val="0"/>` preserve exact formatting
|
||||
|
||||
2. **Improved ASCII Art Detection** (`src/wordTemplateExporter.js:410-456`)
|
||||
- Extended Unicode box-drawing character support (╭╮╯╰ rounded corners, heavy box characters)
|
||||
- Additional arrow characters (↔↕⇒⇐⇓⇑)
|
||||
- More ASCII patterns detected (+---+---+, <---->, etc.)
|
||||
- Better markdown table vs ASCII art differentiation
|
||||
|
||||
3. **Enhanced PDF Export** (`src/main.js:1266-1268`, `src/main.js:1345-1347`, `src/main.js:2410-2412`)
|
||||
- Added `-V monofont="Consolas"` for monospace code blocks
|
||||
- Added `--highlight-style=tango` for syntax highlighting
|
||||
- Applied to main export, fallback export, and CLI conversion
|
||||
|
||||
4. **Electron PDF Fallback** (`src/main.js:1613-1637`)
|
||||
- Enhanced CSS for `<pre>` and `<code>` elements
|
||||
- `white-space: pre` prevents wrapping
|
||||
- `font-family: Consolas, Monaco, 'Courier New', monospace`
|
||||
- Print media query preserves formatting when printing
|
||||
|
||||
5. **HTML Export** (`src/main.js:1525-1548`)
|
||||
- Same CSS improvements as Electron PDF
|
||||
- Code blocks maintain exact preview appearance
|
||||
|
||||
**Key CSS Properties for ASCII Preservation:**
|
||||
```css
|
||||
pre {
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
font-family: Consolas, Monaco, 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
pre code {
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
display: block;
|
||||
}
|
||||
```
|
||||
|
||||
**Word XML Properties for ASCII Preservation:**
|
||||
```xml
|
||||
<w:pPr>
|
||||
<w:spacing w:line="240" w:lineRule="exact"/>
|
||||
<w:keepLines/>
|
||||
<w:wordWrap w:val="0"/>
|
||||
</w:pPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||
<w:noProof/>
|
||||
</w:rPr>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### v1.9.0 Custom Headers & Footers
|
||||
|
||||
#### 📄 Comprehensive Header & Footer System
|
||||
**Custom Headers & Footers for Standard Exports** (`src/main.js:701-853`, `src/index.html:929-1042`, `src/renderer.js:2560-2747`, `src/styles.css:3162-3432`)
|
||||
- **New Menu Option**: File → "Header & Footer Settings..." to configure headers/footers
|
||||
- **Three-Column Layout**: Left, Center, and Right positioning for both headers and footers
|
||||
- **Dynamic Field Support**: $PAGE$, $TOTAL$, $DATE$, $TIME$, $TITLE$, $AUTHOR$, $FILENAME$
|
||||
- **Logo/Image Embedding**: Upload logos to display in headers and footers
|
||||
- **Enable/Disable Toggle**: Quick on/off switch for all headers/footers
|
||||
- **Persistent Settings**: Configurations saved across sessions
|
||||
|
||||
**Format Support:**
|
||||
- **PDF Export** (Standard): LaTeX-based headers/footers using fancyhdr package with automatic page numbering
|
||||
- **DOCX Export** (Standard): Word XML manipulation with field support for PAGE and NUMPAGES
|
||||
- **ODT Export**: OpenDocument format support (similar to DOCX implementation)
|
||||
- **PowerPoint (PPTX)**: Footer text with dynamic fields on slides
|
||||
- **Batch Converter**: Full header/footer support for all batch conversions
|
||||
|
||||
**Dynamic Fields Processing:**
|
||||
- `$PAGE$` → Current page number (LaTeX: \thepage, Word: PAGE field)
|
||||
- `$TOTAL$` → Total pages (LaTeX: \pageref{LastPage}, Word: NUMPAGES field)
|
||||
- `$DATE$` → Current date (processed at export time)
|
||||
- `$TIME$` → Current time (processed at export time)
|
||||
- `$TITLE$` → Document title (from metadata or filename)
|
||||
- `$AUTHOR$` → Document author (from metadata)
|
||||
- `$FILENAME$` → File name without extension
|
||||
|
||||
**Technical Implementation:**
|
||||
```javascript
|
||||
// processDynamicFields function in main.js
|
||||
function processDynamicFields(text, metadata = {}) {
|
||||
const now = new Date();
|
||||
result = text.replace(/\$DATE\$/g, now.toLocaleDateString());
|
||||
result = result.replace(/\$TIME\$/g, now.toLocaleTimeString());
|
||||
result = result.replace(/\$TITLE\$/g, metadata.title || 'Untitled');
|
||||
// ... other replacements
|
||||
return result;
|
||||
}
|
||||
|
||||
// LaTeX header generation for PDF exports
|
||||
const latexHeader = `
|
||||
\\usepackage{fancyhdr}
|
||||
\\pagestyle{fancy}
|
||||
\\fancyhf{}
|
||||
\\lhead{${headerLeft}}
|
||||
\\chead{${headerCenter}}
|
||||
\\rhead{${headerRight}}
|
||||
\\lfoot{${footerLeft}}
|
||||
\\cfoot{${footerCenter}} // with \\thepage for page numbers
|
||||
\\rfoot{${footerRight}}
|
||||
`;
|
||||
|
||||
// Word XML generation for DOCX exports
|
||||
const footerXml = `<?xml version="1.0"?>
|
||||
<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||||
<w:p><w:r><w:t>${footerLeft}</w:t></w:r></w:p>
|
||||
<w:p>
|
||||
<w:fldSimple w:instr="PAGE"/> // Page number field
|
||||
<w:r><w:t> of </w:t></w:r>
|
||||
<w:fldSimple w:instr="NUMPAGES"/> // Total pages field
|
||||
</w:p>
|
||||
<w:p><w:r><w:t>${footerRight}</w:t></w:r></w:p>
|
||||
</w:ftr>`;
|
||||
```
|
||||
|
||||
**Dialog Features:**
|
||||
- **Field Insertion Buttons**: Quick-insert dynamic fields via popup dialog
|
||||
- **Logo Upload**: Browse and select images for headers/footers
|
||||
- **Logo Preview**: Show selected logo filename
|
||||
- **Clear Buttons**: Remove uploaded logos
|
||||
- **Enable Toggle**: Disable headers/footers without losing settings
|
||||
- **Responsive Layout**: Grid-based three-column design
|
||||
- **Dark Theme Support**: Complete styling for all 22 themes
|
||||
|
||||
**Integration Points:**
|
||||
- `buildPandocCommand()` - Adds LaTeX headers for PDF exports
|
||||
- `exportWithPandoc()` - Post-processes DOCX with header/footer XML
|
||||
- `performBatchConversion()` - Batch support for all formats
|
||||
- `addHeaderFooterToDocx()` - DOCX XML manipulation using PizZip
|
||||
|
||||
**Dependencies:**
|
||||
- **PizZip** (v3.2.0) - ZIP file manipulation for DOCX/ODT
|
||||
- **fancyhdr** (LaTeX package) - Advanced headers/footers for PDF
|
||||
- **lastpage** (LaTeX package) - Total page count support
|
||||
|
||||
**Key Improvements in v1.9.0:**
|
||||
1. ✅ Professional headers/footers for all standard export formats
|
||||
2. ✅ Dynamic field support with automatic value replacement
|
||||
3. ✅ Logo/image embedding in document headers/footers
|
||||
4. ✅ Batch converter integration with full header/footer support
|
||||
5. ✅ Persistent settings across application sessions
|
||||
6. ✅ User-friendly dialog with field insertion helper
|
||||
7. ✅ Complete dark theme support
|
||||
|
||||
**Note**: Enhanced exports (DOCX Enhanced, PDF Enhanced) use Word templates which already have their own headers/footers defined in the template itself, so this feature specifically targets standard (non-templated) exports.
|
||||
|
||||
---
|
||||
|
||||
### v1.7.9 Enhanced Word Export with Template Support
|
||||
|
||||
#### 📝 Template-Based Word Document Generation
|
||||
**Added Enhanced DOCX Export** (`src/wordTemplateExporter.js`, `src/main.js:540-596`, `src/main.js:242`)
|
||||
- **New Export Option**: "DOCX (Enhanced)" in File → Export menu with keyboard shortcut `Ctrl+Shift+W`
|
||||
- **Template Support**: Use custom Word templates to preserve branding, styles, and formatting
|
||||
- **Template Selection**: File → "Select Word Template..." menu option to choose custom templates
|
||||
- **Persistent Template**: Selected template is saved and reused across sessions
|
||||
- **Word XML Manipulation**: Direct manipulation of Word document XML using PizZip
|
||||
- **Template Page Preservation**: Preserves first 2 pages (cover + TOC) when using templates
|
||||
- **Smart Content Insertion**: Inserts markdown content after 2nd section break
|
||||
|
||||
**Full Markdown Support:**
|
||||
- Headings (H1-H6) with markdown numbering stripped (e.g., "1.1 Title" → "Title")
|
||||
- Text formatting (bold, italic, bold+italic, strikethrough)
|
||||
- Inline code with Consolas font and gray background
|
||||
- Code blocks with monospace font and light gray background
|
||||
- Ordered and unordered lists (uses template numbering, strips markdown numbering)
|
||||
- Blockquotes with indentation
|
||||
- **Tables with orange header styling**:
|
||||
- Orange header row (#F58220) with white bold text
|
||||
- White data rows (no alternating colors)
|
||||
- Orange borders (#F58220) on all cells
|
||||
- Proper column alignment and spacing
|
||||
- Horizontal rules
|
||||
- Links
|
||||
- **ASCII Art and Flowcharts**:
|
||||
- Detects box-drawing characters (┌, ─, │, └, etc.)
|
||||
- Detects flowchart patterns with square brackets
|
||||
- Preserves monospace alignment with Consolas font
|
||||
- Each line rendered separately to prevent wrapping
|
||||
- No-wrap paragraph properties for exact spacing
|
||||
- **Red-colored arrows** (↓, →, ←, ↑) for enhanced visibility
|
||||
- Gray background (#F5F5F5) for distinction from regular text
|
||||
|
||||
**ASCII Art Detection Patterns:**
|
||||
- Unicode box-drawing characters: ┌┐└┘├┤┬┴┼─│═║╔╗╚╝╠╣╦╩╬
|
||||
- Arrow characters: ↓→←↑▼►◄▲
|
||||
- ASCII box patterns: +-----+, |-----|, [Text in brackets]
|
||||
- Flowchart steps: START, [Step Description], END
|
||||
|
||||
**ASCII Art Rendering Features:**
|
||||
```javascript
|
||||
// Each line gets its own paragraph with no-wrap
|
||||
xml += `<w:p>
|
||||
<w:pPr>
|
||||
<w:wordWrap w:val="0"/> // Disable wrapping
|
||||
<w:keepLines/> // Keep lines together
|
||||
<w:line="240" w:lineRule="exact"/> // Exact line height
|
||||
</w:pPr>
|
||||
// Arrows colored red (#FF0000)
|
||||
// Text in Consolas 16pt with gray background
|
||||
</w:p>`;
|
||||
```
|
||||
|
||||
**Template Selection Workflow:**
|
||||
1. User clicks File → "Select Word Template..."
|
||||
2. Dialog opens to browse for .docx files
|
||||
3. Selected template path stored in global variable and persisted
|
||||
4. Template automatically loaded on app startup
|
||||
5. Enhanced export uses selected template (or defaults to `word_template.docx`)
|
||||
|
||||
**Technical Implementation:**
|
||||
```javascript
|
||||
// Template selection function
|
||||
async function selectWordTemplate() {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Select Word Template',
|
||||
filters: [{ name: 'Word Document', extensions: ['docx'] }],
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
if (!result.canceled && result.filePaths.length > 0) {
|
||||
wordTemplatePath = result.filePaths[0];
|
||||
store.set('wordTemplatePath', wordTemplatePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Word Template Exporter with template support
|
||||
class WordTemplateExporter {
|
||||
constructor(templatePath) {
|
||||
this.templatePath = templatePath || path.join(__dirname, '../word_template.docx');
|
||||
}
|
||||
|
||||
async convert(markdownContent, outputPath) {
|
||||
// Load template as ZIP
|
||||
const templateBuffer = fs.readFileSync(this.templatePath);
|
||||
const zip = new PizZip(templateBuffer);
|
||||
|
||||
// Extract and modify document.xml
|
||||
const documentXml = zip.file('word/document.xml').asText();
|
||||
const newContentXml = this.markdownToWordXml(markdownContent);
|
||||
const modifiedXml = this.insertContentAfterPage2(documentXml, newContentXml);
|
||||
|
||||
// Save modified document
|
||||
zip.file('word/document.xml', modifiedXml);
|
||||
const newDocBuffer = zip.generate({ type: 'nodebuffer' });
|
||||
fs.writeFileSync(outputPath, newDocBuffer);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Menu Integration:**
|
||||
- Export: File → Export → "DOCX (Enhanced)" (`Ctrl+Shift+W`)
|
||||
- Template Selection: File → "Select Word Template..."
|
||||
- Template path saved to: `userData/settings.json`
|
||||
|
||||
**Dependencies Added:**
|
||||
```json
|
||||
{
|
||||
"docx": "^9.5.1",
|
||||
"pizzip": "^3.2.0",
|
||||
"docx4js": "^3.3.0"
|
||||
}
|
||||
```
|
||||
|
||||
**Key Improvements in v1.7.9:**
|
||||
1. ✅ Template-based export preserving corporate branding
|
||||
2. ✅ Persistent template selection across sessions
|
||||
3. ✅ Tables with professional orange header styling and white data rows
|
||||
4. ✅ ASCII art and flowcharts with perfect alignment and no wrapping
|
||||
5. ✅ Red-colored arrows in flowcharts for enhanced readability
|
||||
6. ✅ Markdown numbering stripped from headings and lists
|
||||
7. ✅ Template's automatic numbering system used instead
|
||||
|
||||
### v1.7.8 Critical Bug Fixes
|
||||
|
||||
#### 🐛 File Association Fix for Packaged Apps
|
||||
**Fixed Command-Line Argument Parsing** (`src/main.js:1598-1627`, `src/main.js:70-90`)
|
||||
- **Root Cause**: `process.argv.slice(2)` works in development but fails in packaged apps
|
||||
- **Solution**: Detect if app is packaged using `app.isPackaged` and use correct slice index
|
||||
- Development: `process.argv.slice(2)` - ['electron', 'app.js', 'file.md']
|
||||
- Packaged: `process.argv.slice(1)` - ['PanConverter.exe', 'file.md']
|
||||
- **Path Resolution**: Added proper path resolution for both absolute and relative paths
|
||||
- **Comprehensive Logging**: Added detailed console logs to debug file loading issues
|
||||
- **Second Instance Handler**: Also fixed second-instance handler to use same logic
|
||||
|
||||
**Technical Implementation:**
|
||||
```javascript
|
||||
// Detect packaged vs development mode
|
||||
const startIndex = app.isPackaged ? 1 : 2;
|
||||
const fileArgs = process.argv.slice(startIndex);
|
||||
|
||||
// Resolve paths properly
|
||||
for (const arg of fileArgs) {
|
||||
if ((arg.endsWith('.md') || arg.endsWith('.markdown'))) {
|
||||
const resolvedPath = path.isAbsolute(arg) ? arg : path.resolve(process.cwd(), arg);
|
||||
if (fs.existsSync(resolvedPath)) {
|
||||
app.pendingFile = resolvedPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fixed Issues:**
|
||||
- ✅ Files now open correctly on first double-click in packaged app
|
||||
- ✅ Command-line arguments properly parsed in both dev and production
|
||||
- ✅ Relative and absolute paths handled correctly
|
||||
- ✅ Comprehensive logging for debugging file association issues
|
||||
|
||||
#### 🖨️ Print Preview Fix
|
||||
**Fixed Print Output to Show Preview Content** (`src/renderer.js:1178-1204`, `src/styles.css:2828-3014`)
|
||||
- **Root Cause**: Manual DOM manipulation conflicted with CSS `@media print` rules
|
||||
- **Solution**: Simplified handler to rely on CSS `@media print` for hiding UI elements
|
||||
- **CSS Enhancements**:
|
||||
- Added selectors for dynamic preview panes `[id^="preview-pane-"]` and `[id^="editor-pane-"]`
|
||||
- Added `.tab-content` to ensure full-width rendering
|
||||
- Added body class-based styling for no-styles mode
|
||||
- **Removed Manual Hiding**: Eliminated manual `display: none` manipulation in favor of CSS
|
||||
|
||||
**Technical Implementation:**
|
||||
```javascript
|
||||
// Simplified print handler - let CSS do the work
|
||||
function handlePrintPreview(withStyles) {
|
||||
const activeTabId = tabManager ? tabManager.activeTabId : 1;
|
||||
const previewContent = document.getElementById(`preview-${activeTabId}`);
|
||||
|
||||
if (!previewContent || !previewContent.innerHTML.trim()) {
|
||||
alert('Nothing to print...');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add body classes for CSS styling
|
||||
if (!withStyles) {
|
||||
document.body.classList.add('printing-no-styles');
|
||||
}
|
||||
document.body.classList.add('printing');
|
||||
|
||||
setTimeout(() => {
|
||||
ipcRenderer.send('do-print', { withStyles });
|
||||
setTimeout(() => {
|
||||
document.body.classList.remove('printing', 'printing-no-styles');
|
||||
}, 1000);
|
||||
}, 100);
|
||||
}
|
||||
```
|
||||
|
||||
**CSS Updates:**
|
||||
```css
|
||||
@media print {
|
||||
/* Hide all UI elements */
|
||||
#toolbar, #tab-bar, .editor-pane, [id^="editor-pane-"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Show preview in full width */
|
||||
[id^="preview-"], [id^="preview-pane-"], .tab-content {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
padding: 20px !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fixed Issues:**
|
||||
- ✅ Print now correctly renders preview content, not toolbar
|
||||
- ✅ Both print modes work (with styles and without styles)
|
||||
- ✅ Clean, professional print output with proper formatting
|
||||
- ✅ Proper page breaks, typography, and layout optimization
|
||||
|
||||
#### 🔧 Development Cleanup
|
||||
**Removed Debugging Code** (`src/main.js:120-123`)
|
||||
- Removed auto-opening of DevTools
|
||||
- Clean production-ready build
|
||||
- Debugging logs remain for troubleshooting but DevTools disabled by default
|
||||
|
||||
### v1.7.7 Print & Enhanced PDF Support
|
||||
|
||||
#### 🖨️ Native Print Functionality
|
||||
**Added Print Submenu to File Menu** (`src/main.js:202-215`, `src/renderer.js:1152-1188`, `src/styles.css:2828-2994`)
|
||||
- **Print Menu**: Submenu in File menu with two print options
|
||||
- **Print Preview** (`Ctrl+P`) - Prints preview in black text, no background colors (ink-saving)
|
||||
- **Print Preview (With Styles)** - Prints with full theme colors and styling
|
||||
- **Preview-Only Printing**: Only renders the markdown preview, hides all editor UI
|
||||
- **Native Print Dialog**: Uses Electron's webContents.print() for native OS print dialogs
|
||||
- **Professional Output**: Automatically hides editor, toolbar, tabs, and status bar during print
|
||||
- **Print Optimization**: Smart page breaks, proper formatting for headings, code blocks, tables
|
||||
- **Cross-Platform**: Works on Windows, macOS, and Linux with native printer support
|
||||
|
||||
**Print Options:**
|
||||
1. **Print Preview** (Ctrl+P)
|
||||
- Black text on white background for professional printing
|
||||
- No background colors to save ink
|
||||
- Perfect for documents and reports
|
||||
- Minimal ink consumption
|
||||
|
||||
2. **Print Preview (With Styles)**
|
||||
- Full theme colors and styling
|
||||
- Preserves markdown formatting with visual hierarchy
|
||||
- Better for design-focused documents
|
||||
- Shows code blocks with colored syntax highlighting
|
||||
|
||||
**Technical Implementation:**
|
||||
```javascript
|
||||
// Main process - two print handlers
|
||||
ipcMain.on('print-preview', (event) => {
|
||||
mainWindow.webContents.send('prepare-print-preview', false);
|
||||
setTimeout(() => {
|
||||
mainWindow.webContents.print({
|
||||
silent: false,
|
||||
printBackground: false,
|
||||
color: true,
|
||||
margin: { marginType: 'default' }
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
|
||||
ipcMain.on('print-preview-styled', (event) => {
|
||||
mainWindow.webContents.send('prepare-print-preview', true);
|
||||
setTimeout(() => {
|
||||
mainWindow.webContents.print({
|
||||
silent: false,
|
||||
printBackground: true,
|
||||
color: true,
|
||||
margin: { marginType: 'default' }
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Renderer process - prepares preview for printing
|
||||
ipcRenderer.on('prepare-print-preview', (event, withStyles) => {
|
||||
// Hide editor and UI elements
|
||||
document.getElementById('editor-container').style.display = 'none';
|
||||
document.getElementById('toolbar').style.display = 'none';
|
||||
document.getElementById('tab-bar').style.display = 'none';
|
||||
document.getElementById('status-bar').style.display = 'none';
|
||||
|
||||
// Add print mode classes
|
||||
const preview = document.getElementById('preview');
|
||||
preview.classList.add('print-mode');
|
||||
if (!withStyles) {
|
||||
preview.classList.add('print-no-styles');
|
||||
}
|
||||
|
||||
// Restore UI after print
|
||||
setTimeout(() => {
|
||||
document.getElementById('editor-container').style.display = '';
|
||||
document.getElementById('toolbar').style.display = '';
|
||||
document.getElementById('tab-bar').style.display = '';
|
||||
document.getElementById('status-bar').style.display = '';
|
||||
preview.classList.remove('print-mode', 'print-no-styles');
|
||||
}, 500);
|
||||
});
|
||||
```
|
||||
|
||||
**CSS Print Styles** (`src/styles.css:2828-2994`)
|
||||
- Comprehensive @media print stylesheet
|
||||
- Hides all UI elements in print view
|
||||
- Optimizes preview for paper output
|
||||
- Smart page breaks for headings and tables
|
||||
- Proper formatting for code blocks, lists, tables
|
||||
- Image optimization and link handling
|
||||
- Professional typography with 12pt font size
|
||||
|
||||
#### 📦 Enhanced PDF Export Dependencies
|
||||
**Added Native PDF Generation Libraries** (package.json)
|
||||
- **PDFKit** (v0.14.0) - Native PDF generation without Pandoc dependency
|
||||
- **html2pdf.js** (v0.10.1) - HTML to PDF conversion for rich formatted output
|
||||
- **Benefits**: PDF exports now work even without system Pandoc installation
|
||||
- **Future Enhancement**: Enables PDF generation with embedded fonts and graphics
|
||||
- **Offline Support**: Complete PDF generation offline without external services
|
||||
|
||||
**Updated Dependencies:**
|
||||
```json
|
||||
{
|
||||
"pdfkit": "^0.14.0",
|
||||
"html2pdf.js": "^0.10.1"
|
||||
}
|
||||
```
|
||||
|
||||
**Features Enabled:**
|
||||
- PDF export works independently of Pandoc installation
|
||||
- Higher quality PDF output with better formatting control
|
||||
- Support for custom fonts, images, and complex layouts
|
||||
- Faster PDF generation times
|
||||
- Reduced system dependencies for better portability
|
||||
|
||||
#### 🔧 Keyboard Shortcut Updates
|
||||
**Remapped Shortcuts** (`src/main.js`)
|
||||
- **Print**: `Ctrl+P` (new in v1.7.7) - Opens native print dialog
|
||||
- **Toggle Preview**: Changed from `Ctrl+P` to `Ctrl+Shift+P` to avoid conflicts
|
||||
- **Backward Compatibility**: No breaking changes to existing workflows
|
||||
|
||||
### v1.7.6 UI Improvement
|
||||
|
||||
#### 🎨 Table Header Styling Cleanup
|
||||
**Removed Custom Background Colors** (`src/styles.css:327-329`, `src/styles.css:451-453`)
|
||||
- Removed gray background color (#f6f8fa) from table headers in default theme
|
||||
- Removed dark theme table header background styling block entirely
|
||||
- Table headers now blend naturally with preview background for cleaner appearance
|
||||
- Headers maintain font-weight: 600 for visual distinction
|
||||
- All other table styling preserved (borders, alternating row colors)
|
||||
|
||||
**User Feedback Implementation:**
|
||||
- Based on user feedback: "remove the theming of table headers, it looks bad"
|
||||
- Result: Professional, clean table presentation matching overall preview styling
|
||||
- Consistent with normal preview theming across all themes
|
||||
|
||||
### v1.7.5 Critical File Association Fix
|
||||
|
||||
#### 🐛 Single-Instance Lock for Windows File Association
|
||||
**Fixed Installed App Double-Click Behavior** (`src/main.js:52-85`)
|
||||
- **Root Cause**: Windows launches second instance when double-clicking files with app already running
|
||||
- **Solution**: Implemented `app.requestSingleInstanceLock()` pattern
|
||||
- **Second Instance Handler**: Captures file path from new instance attempts and opens in existing instance
|
||||
- **Focus Management**: Automatically focuses and restores existing window
|
||||
- **Renderer Readiness**: Proper handling of file queue with `rendererReady` state
|
||||
|
||||
**Technical Implementation:**
|
||||
```javascript
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
||||
// Focus existing window
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.focus();
|
||||
}
|
||||
// Extract and open file from second instance
|
||||
const fileArgs = commandLine.slice(2);
|
||||
for (const arg of fileArgs) {
|
||||
if ((arg.endsWith('.md') || arg.endsWith('.markdown')) && fs.existsSync(arg)) {
|
||||
if (rendererReady) {
|
||||
openFileFromPath(arg);
|
||||
} else {
|
||||
app.pendingFile = arg;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Fixed Issues:**
|
||||
- ✅ Double-click `.md` files in Windows Explorer now opens correctly
|
||||
- ✅ Right-click "Open with PanConverter" now works in installed app
|
||||
- ✅ File association behavior now consistent between development and production
|
||||
- ✅ Single instance maintained across all file opening methods
|
||||
|
||||
**Why Previous Attempts Failed:**
|
||||
- v1.7.3-1.7.4: Only added `rendererReady` checks but missed second-instance handling
|
||||
- Development testing passed but production failed (different execution paths)
|
||||
- Second instance would start, receive file, then exit without passing to first instance
|
||||
|
||||
### v1.7.2 Enhanced Themes & Bug Fixes
|
||||
|
||||
#### 🎨 14 New Beautiful Themes
|
||||
**Expanded Theme Collection** (`src/main.js:242-266`, `src/styles.css:1590-2555`)
|
||||
- **Added 14 new professionally-designed themes** bringing the total to 19 themes
|
||||
- **New Themes Include**:
|
||||
- **Dracula** - Popular purple and pink dark theme
|
||||
- **Nord** - Arctic-inspired blue theme
|
||||
- **One Dark** - Atom's iconic dark theme
|
||||
- **Atom One Light** - Clean, bright light theme
|
||||
- **Material** - Google Material Design inspired
|
||||
- **Gruvbox Dark** - Warm retro groove colors (dark)
|
||||
- **Gruvbox Light** - Warm retro groove colors (light)
|
||||
- **Tokyo Night** - Modern Japanese-inspired dark theme
|
||||
- **Palenight** - Soft purple Material Design variant
|
||||
- **Ayu Dark** - Simple, elegant dark theme
|
||||
- **Ayu Light** - Minimalist light theme
|
||||
- **Ayu Mirage** - Balanced dark-light hybrid
|
||||
- **Oceanic Next** - Ocean-inspired teal and blue
|
||||
- **Cobalt2** - Deep blue with yellow accents
|
||||
|
||||
**Technical Implementation:**
|
||||
- Complete CSS styling for all UI elements per theme
|
||||
- Consistent theming across tabs, toolbar, editor, preview, and status bar
|
||||
- Theme-aware color schemes for syntax highlighting
|
||||
- All themes support glassmorphism effects from v1.7.1
|
||||
|
||||
#### 🐛 Undo/Redo Fix
|
||||
**Fixed Menu Integration** (`src/renderer.js:1107-1118`)
|
||||
- Fixed undo/redo menu items not connecting to custom undo/redo functionality
|
||||
- Added IPC event listeners in renderer to receive 'undo' and 'redo' messages
|
||||
- Connected Edit menu commands to TabManager's undo() and redo() methods
|
||||
- Keyboard shortcuts (Ctrl+Z, Ctrl+Shift+Z) now work correctly with custom undo stack
|
||||
|
||||
### v1.7.1 Modern UI Design
|
||||
|
||||
#### 🎨 Glassmorphism & Gradient UI
|
||||
**Modern Design System** (`src/styles-modern.css`, `src/index.html:7-11`)
|
||||
@@ -565,5 +1296,33 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: October 11, 2025
|
||||
**Claude Assistant**: Development completed for v1.7.1 with modern glassmorphism UI design featuring animated gradient backgrounds, translucent glass effects, smooth animations, enhanced typography with Inter and JetBrains Mono fonts, and comprehensive CSS custom properties for consistent theming.
|
||||
## Pending Tasks & Future Enhancements
|
||||
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: December 12, 2025
|
||||
**Claude Assistant**: Development completed for v1.9.1 with ASCII Art Preservation!
|
||||
|
||||
### 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.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
|
||||
- v1.8.1: Streamlined PDF Editor UI
|
||||
- v1.8.0: Enhanced Word Export with template support (batch)
|
||||
- v1.7.9: Template-Based Word Export with ASCII art support
|
||||
@@ -1,95 +1,12 @@
|
||||
# Push Instructions for Pan Converter
|
||||
# Test Double-Click File Opening
|
||||
|
||||
## Prerequisites
|
||||
1. Create a new repository on GitHub: https://github.com/new
|
||||
- Repository name: `pan-converter`
|
||||
- Keep it empty (no README, .gitignore, or license)
|
||||
- Make it public or private as desired
|
||||
This is a test file to verify double-click functionality.
|
||||
|
||||
2. Set up authentication:
|
||||
- **Option A - SSH Key** (Recommended):
|
||||
```bash
|
||||
# Check if you have SSH key
|
||||
ls -la ~/.ssh/id_rsa.pub
|
||||
|
||||
# If not, generate one:
|
||||
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
|
||||
|
||||
# Add to GitHub: Settings > SSH and GPG keys > New SSH key
|
||||
cat ~/.ssh/id_rsa.pub
|
||||
```
|
||||
|
||||
- **Option B - Personal Access Token**:
|
||||
- Go to GitHub Settings > Developer settings > Personal access tokens
|
||||
- Generate new token with 'repo' scope
|
||||
- Save the token securely
|
||||
## Features to Test
|
||||
- File should load automatically
|
||||
- Content should display in editor
|
||||
- Tab should show filename
|
||||
- Preview should render markdown
|
||||
|
||||
## Push Commands
|
||||
|
||||
### Using SSH (Recommended)
|
||||
```bash
|
||||
# Set SSH remote
|
||||
git remote set-url origin git@github.com:amitwh/pan-converter.git
|
||||
|
||||
# Push all branches
|
||||
git push -u origin master
|
||||
git push origin linux
|
||||
git push origin macos
|
||||
git push origin windows
|
||||
```
|
||||
|
||||
### Using HTTPS with Token
|
||||
```bash
|
||||
# Set HTTPS remote
|
||||
git remote set-url origin https://github.com/amitwh/pan-converter.git
|
||||
|
||||
# Push all branches (you'll be prompted for username and token)
|
||||
git push -u origin master
|
||||
git push origin linux
|
||||
git push origin macos
|
||||
git push origin windows
|
||||
|
||||
# When prompted:
|
||||
# Username: amitwh
|
||||
# Password: [paste your Personal Access Token]
|
||||
```
|
||||
|
||||
### Using GitHub CLI (if installed)
|
||||
```bash
|
||||
# Login to GitHub CLI
|
||||
gh auth login
|
||||
|
||||
# Create repo and push
|
||||
gh repo create pan-converter --public --source=. --remote=origin --push
|
||||
```
|
||||
|
||||
## Automated Script
|
||||
```bash
|
||||
# Run the provided script
|
||||
./push-to-github.sh
|
||||
```
|
||||
|
||||
## Verify Success
|
||||
After pushing, verify at: https://github.com/amitwh/pan-converter
|
||||
|
||||
You should see:
|
||||
- 4 branches (master, linux, macos, windows)
|
||||
- All project files
|
||||
- Complete commit history
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication Failed
|
||||
- For SSH: Ensure your SSH key is added to GitHub
|
||||
- For HTTPS: Use Personal Access Token, not password
|
||||
- Check token has 'repo' scope
|
||||
|
||||
### Repository Not Found
|
||||
- Ensure repository exists on GitHub
|
||||
- Check spelling: `pan-converter`
|
||||
- Verify you're logged into the correct GitHub account
|
||||
|
||||
### Permission Denied
|
||||
- Check repository ownership
|
||||
- Ensure you have write access
|
||||
- Verify authentication method is correct
|
||||
**This text should be bold**
|
||||
*This text should be italic*
|
||||
|
||||
@@ -1,160 +1,153 @@
|
||||
# PanConverter
|
||||
# MarkdownConverter
|
||||
|
||||
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. 100% open-source with no proprietary dependencies.
|
||||
|
||||
## 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
|
||||
Open PDF files directly in MarkdownConverter:
|
||||
- **File > Open PDF** or **Ctrl+Shift+O**
|
||||
- Navigate pages with arrow buttons or page input
|
||||
- Zoom controls: +/- buttons, Fit Width, Fit Page
|
||||
- Rotate pages left or right
|
||||
- Close PDF to return to editor
|
||||
|
||||
# Build for current platform
|
||||
npm run build
|
||||
## Open Source
|
||||
|
||||
# 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.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).
|
||||
MarkdownConverter is 100% open-source. All dependencies are permissively licensed:
|
||||
- **Electron** - MIT License
|
||||
- **pdf-lib** - MIT License
|
||||
- **pdfjs-dist** - Apache 2.0 License
|
||||
- **marked** - MIT License
|
||||
- **highlight.js** - BSD 3-Clause License
|
||||
- **dompurify** - Apache 2.0/MIT License
|
||||
- **docx** - MIT License
|
||||
- **xlsx** - Apache 2.0 License (SheetJS Community Edition)
|
||||
|
||||
## License
|
||||
|
||||
@@ -162,13 +155,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: 27 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 171 KiB |
@@ -0,0 +1,166 @@
|
||||
# PanConverter - Updates & Changelog
|
||||
|
||||
## Version 2.1.0 (December 14, 2025)
|
||||
|
||||
### 🎨 UI/UX Improvements
|
||||
|
||||
#### Subtle & Small Preview Popout Button
|
||||
- Redesigned popout button with minimalist aesthetic
|
||||
- Removed border for cleaner appearance
|
||||
- Reduced size: 11px font, 2px×6px padding (previously 14px font, 4px×8px padding)
|
||||
- Added opacity transition: 50% when idle, 100% on hover
|
||||
- Subtle background effect on hover instead of heavy border styling
|
||||
- **File**: `src/styles.css:195-211`
|
||||
|
||||
#### Simplified Table Headers in Preview
|
||||
- Removed gradient background from table headers in modern theme
|
||||
- Changed from `var(--primary-gradient)` (purple gradient) to simple light gray (#f0f0f0)
|
||||
- Updated text color to dark (#333333) for better readability
|
||||
- Clean, professional appearance matching standard themes
|
||||
- **File**: `src/styles-modern.css:445-449`
|
||||
|
||||
### 📥 Enhanced Import Capabilities
|
||||
|
||||
#### Comprehensive Format-to-Markdown Conversion
|
||||
Dramatically expanded the "Import Document" feature to support 30+ file formats:
|
||||
|
||||
**Supported Formats:**
|
||||
- **Documents**: DOCX, ODT, RTF, HTML, HTM, TEX, EPUB, PDF, TXT
|
||||
- **Presentations**: PPTX, ODP
|
||||
- **Markup Languages**: RST, Textile, MediaWiki, Org-mode, AsciiDoc, TWiki, OPML
|
||||
- **E-book Formats**: EPUB, FB2
|
||||
- **LaTeX Formats**: TEX, LATEX, LTX
|
||||
- **Web Formats**: HTML, HTM, XHTML
|
||||
- **Wiki Formats**: MediaWiki, DokuWiki, TikiWiki, TWiki
|
||||
- **Data Formats**: CSV, TSV, JSON
|
||||
|
||||
**Format-Specific Optimizations:**
|
||||
- PDF text extraction with XeLaTeX engine
|
||||
- CSV/TSV automatic table conversion
|
||||
- JSON structure handling
|
||||
- Improved error messages with format hints
|
||||
|
||||
**Access**: File → Import Document (Ctrl+I)
|
||||
**File**: `src/main.js:1933-1994`
|
||||
|
||||
### 🎨 Exhaustive ASCII Art Generator
|
||||
|
||||
#### 5 New Text Banner Styles
|
||||
Complete alphabet (A-Z) and numbers (0-9) support for all styles:
|
||||
|
||||
1. **Standard** - Classic ASCII art with slashes and underscores
|
||||
2. **Banner** - Large format using # characters (7-line height)
|
||||
3. **Block** - Modern Unicode block characters (█ ╔ ╗ ═ ║)
|
||||
4. **Bubble** - Circular bubble letters (Ⓐ Ⓑ Ⓒ)
|
||||
5. **Digital** - Digital display style (▄ ▀ ▐ ▌)
|
||||
|
||||
**File**: `src/renderer.js:3397-3537`
|
||||
|
||||
#### 19 Professional ASCII Templates
|
||||
Organized into 4 categories with expanded options:
|
||||
|
||||
**Arrows & Flow (4 templates):**
|
||||
- Arrow Right - Horizontal flow indicators
|
||||
- Arrow Down - Vertical flow indicators
|
||||
- Decision - Binary decision diagrams
|
||||
- Process Flow - Multi-step process visualization
|
||||
|
||||
**Diagrams & Charts (6 templates):**
|
||||
- Flowchart - Advanced flowchart with decision branches and loops
|
||||
- Sequence - Sequence diagrams for User-System-Database interactions
|
||||
- Network - Server-client network topology
|
||||
- Hierarchy - Organizational tree structures
|
||||
- Timeline - Milestone visualization with dates
|
||||
- Table Simple - Basic table template with borders
|
||||
|
||||
**Boxes & Containers (4 templates):**
|
||||
- Header - Section header with decorative borders
|
||||
- Note Box - Important notes with rounded corners (┏━━┓)
|
||||
- Warning Box - Warning messages with bold borders (╔═══╗)
|
||||
- Info Box - Information boxes with subtle styling (╭───╮)
|
||||
|
||||
**Decorative Elements (6 templates):**
|
||||
- Divider - Horizontal section separator (═══)
|
||||
- Separator Fancy - Elegant rounded divider
|
||||
- Brackets - Japanese-style brackets 【 】
|
||||
- Banner Stars - Star-bordered banners
|
||||
- Checklist - Task lists with ✓ checkmarks
|
||||
- Progress Bar - Visual progress indicators
|
||||
|
||||
**Features:**
|
||||
- All ASCII art automatically wrapped in code blocks for proper rendering
|
||||
- Preserved formatting in markdown preview and all export formats
|
||||
- Categorized template selection interface
|
||||
- Real-time preview generation
|
||||
|
||||
**Access**: Tools → ASCII Art Generator
|
||||
**Files**: `src/renderer.js:3513-3671`, `src/index.html:427-466`
|
||||
|
||||
### 📝 Technical Improvements
|
||||
|
||||
- Enhanced ASCII art detection in Word template exporter
|
||||
- Improved monospace font rendering across all export formats
|
||||
- Better code block preservation in PDF and Word exports
|
||||
- Optimized template categorization and organization
|
||||
|
||||
### 🔧 Files Modified
|
||||
|
||||
- `src/styles.css` - Preview popout button styling
|
||||
- `src/styles-modern.css` - Table header simplification
|
||||
- `src/main.js` - Enhanced import function, version update
|
||||
- `src/renderer.js` - ASCII art generator enhancements
|
||||
- `src/index.html` - ASCII template UI organization
|
||||
- `package.json` - Version bump to 2.1.0
|
||||
|
||||
---
|
||||
|
||||
## Version 2.0.0 (Previous Release)
|
||||
|
||||
### Major Features
|
||||
- Export Profiles - Save and reuse export configurations
|
||||
- Mermaid.js diagram support
|
||||
- Command Palette (Ctrl+Shift+P)
|
||||
- GitHub Light/Dark preview themes
|
||||
- Table Generator
|
||||
- ASCII Art Generator (basic)
|
||||
- Resizable Preview Pane
|
||||
- Pop-out Preview Window
|
||||
- Configurable page sizes (A3-A5, B4-B5, Letter, Legal, Tabloid, Custom)
|
||||
- Custom Headers & Footers for exports
|
||||
- Enhanced PDF and Word export with templates
|
||||
- 22 beautiful themes
|
||||
|
||||
### Core Capabilities
|
||||
- Cross-platform markdown editor with live preview
|
||||
- Universal document conversion (30+ formats)
|
||||
- PDF Editor (merge, split, compress, rotate, watermark, encrypt)
|
||||
- Batch file conversion
|
||||
- File association support
|
||||
- Advanced export options
|
||||
- Multi-tab interface
|
||||
|
||||
---
|
||||
|
||||
## Installation & Usage
|
||||
|
||||
### Prerequisites
|
||||
- **Pandoc** - Required for document conversion
|
||||
- **Optional**: LibreOffice, ImageMagick, FFmpeg for universal converter
|
||||
|
||||
### Download
|
||||
Get the latest release from: https://github.com/amitwh/pan-converter/releases
|
||||
|
||||
### Supported Platforms
|
||||
- Windows (x64)
|
||||
- Linux (AppImage, .deb, .snap)
|
||||
- macOS (planned)
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please see [CLAUDE.md](CLAUDE.md) for development guidelines.
|
||||
|
||||
**Author**: Amit Haridas (amit.wh@gmail.com)
|
||||
**License**: MIT
|
||||
**Repository**: https://github.com/amitwh/pan-converter
|
||||
|
After Width: | Height: | Size: 869 KiB |
|
After Width: | Height: | Size: 1.5 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: 206 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: 885 KiB |
|
After Width: | Height: | Size: 885 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 606 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.0 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
|
||||
};
|
||||
|
After Width: | Height: | Size: 24 KiB |
@@ -1,13 +1,20 @@
|
||||
{
|
||||
"name": "pan-converter",
|
||||
"version": "1.7.1",
|
||||
"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,29 +26,51 @@
|
||||
"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": {
|
||||
"codemirror": "^6.0.2",
|
||||
"core-util-is": "^1.0.3",
|
||||
"docx": "^9.5.1",
|
||||
"docx4js": "^3.3.0",
|
||||
"dompurify": "^3.2.6",
|
||||
"electron-store": "^10.1.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"html2pdf.js": "^0.10.1",
|
||||
"marked": "^16.2.1",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^3.11.174",
|
||||
"pdfkit": "^0.14.0",
|
||||
"pizzip": "^3.2.0",
|
||||
"tslib": "^2.8.1",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.panconverter.app",
|
||||
"productName": "PanConverter",
|
||||
"appId": "com.concreteinfo.markdownconverter",
|
||||
"productName": "MarkdownConverter",
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
@@ -67,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": [
|
||||
@@ -104,7 +141,7 @@
|
||||
"displayLanguageSelector": true,
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "PanConverter",
|
||||
"shortcutName": "MarkdownConverter",
|
||||
"runAfterFinish": true,
|
||||
"menuCategory": "Productivity",
|
||||
"license": "LICENSE",
|
||||
@@ -117,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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -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', 'docico1.png');
|
||||
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 docico1.png...');
|
||||
|
||||
// Check if source exists
|
||||
if (!fs.existsSync(sourceImage)) {
|
||||
console.error('Source image not found:', sourceImage);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate main icon.png (512x512)
|
||||
await sharp(sourceImage)
|
||||
.resize(512, 512, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } })
|
||||
.png()
|
||||
.toFile(path.join(assetsDir, 'icon.png'));
|
||||
console.log('Generated icon.png (512x512)');
|
||||
|
||||
// Generate icon@2x.png (1024x1024)
|
||||
await sharp(sourceImage)
|
||||
.resize(1024, 1024, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } })
|
||||
.png()
|
||||
.toFile(path.join(assetsDir, 'icon@2x.png'));
|
||||
console.log('Generated icon@2x.png (1024x1024)');
|
||||
|
||||
// Create icons directory for multi-size icons
|
||||
const iconsDir = path.join(assetsDir, 'icons');
|
||||
if (!fs.existsSync(iconsDir)) {
|
||||
fs.mkdirSync(iconsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate all sizes
|
||||
for (const size of iconSizes) {
|
||||
await sharp(sourceImage)
|
||||
.resize(size, size, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } })
|
||||
.png()
|
||||
.toFile(path.join(iconsDir, `${size}x${size}.png`));
|
||||
console.log(`Generated icons/${size}x${size}.png`);
|
||||
}
|
||||
|
||||
// Generate favicon
|
||||
await sharp(sourceImage)
|
||||
.resize(32, 32, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } })
|
||||
.png()
|
||||
.toFile(path.join(assetsDir, 'favicon.png'));
|
||||
console.log('Generated favicon.png (32x32)');
|
||||
|
||||
// Generate tray icon (smaller, for system tray)
|
||||
await sharp(sourceImage)
|
||||
.resize(24, 24, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } })
|
||||
.png()
|
||||
.toFile(path.join(assetsDir, 'tray-icon.png'));
|
||||
console.log('Generated tray-icon.png (24x24)');
|
||||
|
||||
console.log('\nIcon generation complete!');
|
||||
console.log('\nFor Windows .ico file, use an online converter or:');
|
||||
console.log(' magick convert assets/icons/*.png assets/icon.ico');
|
||||
console.log('\nFor macOS .icns file, use:');
|
||||
console.log(' iconutil -c icns assets/icon.iconset -o assets/icon.icns');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating icons:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
generateIcons().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,17 +3,26 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PanConverter - Markdown Editor</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">
|
||||
<title>MarkdownConverter</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="styles-modern.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/codemirror.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
|
||||
<link rel="stylesheet" href="styles-concreteinfo.css">
|
||||
<!-- Load external resources asynchronously -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css" media="print" onload="this.media='all'">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||
</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/logo.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>
|
||||
@@ -148,6 +157,19 @@
|
||||
</div>
|
||||
<div class="export-dialog-body">
|
||||
<!-- Simple/Advanced Export Toggle -->
|
||||
<!-- Export Profiles -->
|
||||
<div class="export-section export-profiles">
|
||||
<label>Export Profile:</label>
|
||||
<div class="profile-controls">
|
||||
<select id="export-profile-select">
|
||||
<option value="">Custom Settings</option>
|
||||
</select>
|
||||
<button id="save-profile-btn" type="button" title="Save current settings as profile">💾 Save</button>
|
||||
<button id="delete-profile-btn" type="button" title="Delete selected profile">🗑️ Delete</button>
|
||||
</div>
|
||||
<small class="export-help">Save and reuse your favorite export configurations</small>
|
||||
</div>
|
||||
|
||||
<div class="export-section export-mode-toggle">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="advanced-export-toggle">
|
||||
@@ -243,6 +265,37 @@
|
||||
</div> <!-- End advanced-export-options -->
|
||||
|
||||
<!-- Basic Export Options (always visible) -->
|
||||
<div class="export-section basic-options">
|
||||
<label>Page Size & Orientation:</label>
|
||||
<div class="form-row">
|
||||
<label for="page-size">Size:</label>
|
||||
<select id="page-size">
|
||||
<option value="a4">A4 (210×297mm)</option>
|
||||
<option value="a3">A3 (297×420mm)</option>
|
||||
<option value="a5">A5 (148×210mm)</option>
|
||||
<option value="b4">B4 (250×353mm)</option>
|
||||
<option value="b5">B5 (176×250mm)</option>
|
||||
<option value="letter">Letter (8.5×11in)</option>
|
||||
<option value="legal">Legal (8.5×14in)</option>
|
||||
<option value="tabloid">Tabloid (11×17in)</option>
|
||||
<option value="custom">Custom...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="page-orientation">Orientation:</label>
|
||||
<select id="page-orientation">
|
||||
<option value="portrait">Portrait</option>
|
||||
<option value="landscape">Landscape</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="custom-page-size" class="form-row" style="display: none;">
|
||||
<label>Custom Size:</label>
|
||||
<input type="text" id="custom-width" placeholder="Width (e.g., 210mm)" style="width: 45%; display: inline-block;">
|
||||
<span style="display: inline-block; width: 5%; text-align: center;">×</span>
|
||||
<input type="text" id="custom-height" placeholder="Height (e.g., 297mm)" style="width: 45%; display: inline-block;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="export-section basic-options">
|
||||
<label>Quick Export Settings:</label>
|
||||
<div class="checkbox-group">
|
||||
@@ -258,6 +311,180 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Command Palette -->
|
||||
<div id="command-palette" class="command-palette hidden">
|
||||
<div class="command-palette-content">
|
||||
<input type="text" id="command-search" placeholder="Type a command..." autofocus>
|
||||
<div id="command-list" class="command-list"></div>
|
||||
<div class="command-palette-footer">
|
||||
<span>↑↓ Navigate</span>
|
||||
<span>Enter Execute</span>
|
||||
<span>Esc Close</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Generator Dialog -->
|
||||
<div id="table-generator-dialog" class="export-dialog hidden">
|
||||
<div class="export-dialog-content" style="max-width: 500px;">
|
||||
<div class="export-dialog-header">
|
||||
<h3>📊 Table Generator</h3>
|
||||
<button id="table-dialog-close" title="Close">×</button>
|
||||
</div>
|
||||
<div class="export-dialog-body">
|
||||
<div class="export-section">
|
||||
<label for="table-rows">Number of Rows:</label>
|
||||
<input type="number" id="table-rows" min="1" max="50" value="3" style="width: 100px;">
|
||||
<small>Total rows including header (if enabled)</small>
|
||||
</div>
|
||||
|
||||
<div class="export-section">
|
||||
<label for="table-cols">Number of Columns:</label>
|
||||
<input type="number" id="table-cols" min="1" max="20" value="3" style="width: 100px;">
|
||||
</div>
|
||||
|
||||
<div class="export-section">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="table-has-header" checked>
|
||||
Include Header Row
|
||||
</label>
|
||||
<small>First row will be formatted as table headers</small>
|
||||
</div>
|
||||
|
||||
<div class="export-section">
|
||||
<label for="table-alignment">Column Alignment:</label>
|
||||
<select id="table-alignment">
|
||||
<option value="left">Left</option>
|
||||
<option value="center">Center</option>
|
||||
<option value="right">Right</option>
|
||||
</select>
|
||||
<small>Applies to all columns</small>
|
||||
</div>
|
||||
|
||||
<div class="export-section">
|
||||
<label for="table-preview">Preview:</label>
|
||||
<pre id="table-preview" style="background: #f5f5f5; padding: 10px; border-radius: 4px; font-size: 12px; max-height: 300px; overflow: auto;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="export-dialog-footer">
|
||||
<button id="table-generate-preview" class="btn-secondary">Update Preview</button>
|
||||
<button id="table-insert" class="btn-primary">Insert Table</button>
|
||||
<button id="table-cancel" class="btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ASCII Art Generator Dialog -->
|
||||
<div id="ascii-art-dialog" class="export-dialog hidden">
|
||||
<div class="export-dialog-content" style="max-width: 700px;">
|
||||
<div class="export-dialog-header">
|
||||
<h3>🎨 ASCII Art Generator</h3>
|
||||
<button id="ascii-dialog-close" title="Close">×</button>
|
||||
</div>
|
||||
<div class="export-dialog-body">
|
||||
<div class="export-section">
|
||||
<label>Mode:</label>
|
||||
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
|
||||
<button id="ascii-mode-text" class="btn-secondary ascii-mode-btn active">Text Banner</button>
|
||||
<button id="ascii-mode-box" class="btn-secondary ascii-mode-btn">Box/Frame</button>
|
||||
<button id="ascii-mode-templates" class="btn-secondary ascii-mode-btn">Templates</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text Banner Mode -->
|
||||
<div id="ascii-text-mode" class="ascii-mode-section">
|
||||
<div class="export-section">
|
||||
<label for="ascii-text-input">Text to Convert:</label>
|
||||
<input type="text" id="ascii-text-input" placeholder="Enter your text..." maxlength="50">
|
||||
</div>
|
||||
<div class="export-section">
|
||||
<label for="ascii-font-style">Style:</label>
|
||||
<select id="ascii-font-style">
|
||||
<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="ascii-box-mode" class="ascii-mode-section hidden">
|
||||
<div class="export-section">
|
||||
<label for="ascii-box-text">Text Content:</label>
|
||||
<textarea id="ascii-box-text" rows="3" placeholder="Enter text for the box..."></textarea>
|
||||
</div>
|
||||
<div class="export-section">
|
||||
<label for="ascii-box-style">Box Style:</label>
|
||||
<select id="ascii-box-style">
|
||||
<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="export-section">
|
||||
<label for="ascii-box-padding">Padding:</label>
|
||||
<input type="number" id="ascii-box-padding" min="1" max="10" value="2" style="width: 80px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Templates Mode -->
|
||||
<div id="ascii-templates-mode" class="ascii-mode-section hidden">
|
||||
<div class="export-section">
|
||||
<label>Arrows & Flow:</label>
|
||||
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 15px;">
|
||||
<button class="btn-secondary ascii-template-btn" data-template="arrow-right">Arrow →</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="arrow-down">Arrow ↓</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="decision">Decision</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="process-flow">Process Flow</button>
|
||||
</div>
|
||||
|
||||
<label>Diagrams & Charts:</label>
|
||||
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 15px;">
|
||||
<button class="btn-secondary ascii-template-btn" data-template="flowchart">Flowchart</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="sequence">Sequence</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="network">Network</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="hierarchy">Hierarchy</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="timeline">Timeline</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="table-simple">Table</button>
|
||||
</div>
|
||||
|
||||
<label>Boxes & Containers:</label>
|
||||
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 15px;">
|
||||
<button class="btn-secondary ascii-template-btn" data-template="header">Header</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="note-box">Note Box</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="warning-box">Warning</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="info-box">Info Box</button>
|
||||
</div>
|
||||
|
||||
<label>Decorative Elements:</label>
|
||||
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 15px;">
|
||||
<button class="btn-secondary ascii-template-btn" data-template="divider">Divider</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="separator-fancy">Separator</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="brackets">Brackets</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="banner-stars">Banner</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="check">Checklist ✓</button>
|
||||
<button class="btn-secondary ascii-template-btn" data-template="progress-bar">Progress Bar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="export-section">
|
||||
<label for="ascii-preview">Preview:</label>
|
||||
<pre id="ascii-preview" style="background: #f5f5f5; padding: 15px; border-radius: 4px; font-size: 12px; max-height: 400px; overflow: auto; font-family: 'Courier New', monospace; line-height: 1.2;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="export-dialog-footer">
|
||||
<button id="ascii-generate" class="btn-secondary">Generate Preview</button>
|
||||
<button id="ascii-insert" class="btn-primary">Insert ASCII Art</button>
|
||||
<button id="ascii-cancel" class="btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Universal File Converter Dialog -->
|
||||
<div id="universal-converter-dialog" class="export-dialog hidden">
|
||||
<div class="export-dialog-content">
|
||||
@@ -515,7 +742,9 @@
|
||||
<select id="batch-format">
|
||||
<option value="html">HTML</option>
|
||||
<option value="pdf">PDF</option>
|
||||
<option value="pdf-enhanced">PDF (Enhanced)</option>
|
||||
<option value="docx">DOCX</option>
|
||||
<option value="docx-enhanced">DOCX (Enhanced)</option>
|
||||
<option value="latex">LaTeX</option>
|
||||
<option value="rtf">RTF</option>
|
||||
<option value="odt">ODT</option>
|
||||
@@ -555,12 +784,12 @@
|
||||
|
||||
<!-- PDF Editor Dialog -->
|
||||
<div id="pdf-editor-dialog" class="export-dialog hidden">
|
||||
<div class="export-dialog-content">
|
||||
<div class="export-dialog-content pdf-editor-content">
|
||||
<div class="export-dialog-header">
|
||||
<h3 id="pdf-editor-title">PDF Editor</h3>
|
||||
<button id="pdf-editor-dialog-close" title="Close">×</button>
|
||||
</div>
|
||||
<div class="export-dialog-body">
|
||||
<div class="export-dialog-body pdf-editor-body">
|
||||
<!-- Merge PDFs Section -->
|
||||
<div id="pdf-merge-section" class="pdf-operation-section hidden">
|
||||
<div class="export-section">
|
||||
@@ -648,10 +877,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>
|
||||
@@ -679,10 +911,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>
|
||||
@@ -702,10 +937,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>
|
||||
@@ -731,10 +969,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>
|
||||
@@ -787,10 +1028,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>
|
||||
@@ -836,10 +1080,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>
|
||||
@@ -859,10 +1106,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>
|
||||
@@ -899,10 +1149,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>
|
||||
@@ -924,6 +1177,121 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header & Footer Configuration Dialog -->
|
||||
<div id="header-footer-dialog" class="export-dialog hidden">
|
||||
<div class="export-dialog-content" style="max-width: 700px;">
|
||||
<div class="export-dialog-header">
|
||||
<h3>Header & Footer Settings</h3>
|
||||
<button id="header-footer-close" class="dialog-close" title="Close">×</button>
|
||||
</div>
|
||||
<div class="export-dialog-body">
|
||||
<div class="hf-enable-section">
|
||||
<label>
|
||||
<input type="checkbox" id="hf-enabled" checked>
|
||||
Enable Headers and Footers
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="hf-config-content">
|
||||
<!-- Header Configuration -->
|
||||
<div class="hf-section">
|
||||
<h4>Header</h4>
|
||||
<div class="hf-row">
|
||||
<div class="hf-column">
|
||||
<label>Left</label>
|
||||
<input type="text" id="header-left" placeholder="e.g., Document Title">
|
||||
<button class="field-insert-btn" data-target="header-left" title="Insert dynamic field">+</button>
|
||||
</div>
|
||||
<div class="hf-column">
|
||||
<label>Center</label>
|
||||
<input type="text" id="header-center" placeholder="e.g., $TITLE$">
|
||||
<button class="field-insert-btn" data-target="header-center" title="Insert dynamic field">+</button>
|
||||
</div>
|
||||
<div class="hf-column">
|
||||
<label>Right</label>
|
||||
<input type="text" id="header-right" placeholder="e.g., $DATE$">
|
||||
<button class="field-insert-btn" data-target="header-right" title="Insert dynamic field">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hf-logo-row">
|
||||
<label>Header Logo/Image:</label>
|
||||
<button id="header-logo-browse" class="browse-btn">Browse...</button>
|
||||
<button id="header-logo-clear" class="clear-btn">Clear</button>
|
||||
<span id="header-logo-preview" class="logo-preview"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Configuration -->
|
||||
<div class="hf-section">
|
||||
<h4>Footer</h4>
|
||||
<div class="hf-row">
|
||||
<div class="hf-column">
|
||||
<label>Left</label>
|
||||
<input type="text" id="footer-left" placeholder="e.g., Company Name">
|
||||
<button class="field-insert-btn" data-target="footer-left" title="Insert dynamic field">+</button>
|
||||
</div>
|
||||
<div class="hf-column">
|
||||
<label>Center</label>
|
||||
<input type="text" id="footer-center" placeholder="e.g., $PAGE$ of $TOTAL$">
|
||||
<button class="field-insert-btn" data-target="footer-center" title="Insert dynamic field">+</button>
|
||||
</div>
|
||||
<div class="hf-column">
|
||||
<label>Right</label>
|
||||
<input type="text" id="footer-right" placeholder="e.g., $AUTHOR$">
|
||||
<button class="field-insert-btn" data-target="footer-right" title="Insert dynamic field">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hf-logo-row">
|
||||
<label>Footer Logo/Image:</label>
|
||||
<button id="footer-logo-browse" class="browse-btn">Browse...</button>
|
||||
<button id="footer-logo-clear" class="clear-btn">Clear</button>
|
||||
<span id="footer-logo-preview" class="logo-preview"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Fields Help -->
|
||||
<div class="hf-help">
|
||||
<p><strong>Available Dynamic Fields:</strong></p>
|
||||
<ul>
|
||||
<li><code>$PAGE$</code> - Current page number</li>
|
||||
<li><code>$TOTAL$</code> - Total page count</li>
|
||||
<li><code>$DATE$</code> - Current date</li>
|
||||
<li><code>$TIME$</code> - Current time</li>
|
||||
<li><code>$TITLE$</code> - Document title</li>
|
||||
<li><code>$AUTHOR$</code> - Document author</li>
|
||||
<li><code>$FILENAME$</code> - File name</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="export-dialog-footer">
|
||||
<button id="header-footer-cancel">Cancel</button>
|
||||
<button id="header-footer-save" class="primary">Save Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Field Picker Dialog -->
|
||||
<div id="field-picker-dialog" class="export-dialog hidden">
|
||||
<div class="export-dialog-content" style="max-width: 400px;">
|
||||
<div class="export-dialog-header">
|
||||
<h3>Insert Dynamic Field</h3>
|
||||
<button id="field-picker-close" class="dialog-close" title="Close">×</button>
|
||||
</div>
|
||||
<div class="export-dialog-body">
|
||||
<div class="field-picker-list">
|
||||
<button class="field-option" data-field="$PAGE$">Page Number</button>
|
||||
<button class="field-option" data-field="$TOTAL$">Total Pages</button>
|
||||
<button class="field-option" data-field="$DATE$">Current Date</button>
|
||||
<button class="field-option" data-field="$TIME$">Current Time</button>
|
||||
<button class="field-option" data-field="$TITLE$">Document Title</button>
|
||||
<button class="field-option" data-field="$AUTHOR$">Document Author</button>
|
||||
<button class="field-option" data-field="$FILENAME$">File Name</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-container">
|
||||
<div class="tab-content active" id="tab-content-1" data-tab-id="1">
|
||||
<div id="editor-pane-1" class="pane">
|
||||
@@ -932,10 +1300,106 @@
|
||||
<textarea id="editor-1" class="editor-textarea"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pane-resizer" id="pane-resizer-1" title="Drag to resize"></div>
|
||||
<div id="preview-pane-1" class="pane">
|
||||
<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">
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,743 @@
|
||||
/**
|
||||
* Word Template Exporter
|
||||
* Loads word_template.docx, preserves first 2 pages (cover + TOC),
|
||||
* and adds markdown content starting from page 3 using template styles
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const PizZip = require('pizzip');
|
||||
const Docx = require('docx4js').default;
|
||||
|
||||
class WordTemplateExporter {
|
||||
constructor(templatePath, startPage = 3, pageSettings = null) {
|
||||
this.templatePath = templatePath || path.join(__dirname, '../word_template.docx');
|
||||
this.startPage = startPage; // Which page to start inserting content
|
||||
this.pageSettings = pageSettings; // Page size and orientation settings
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert markdown to Word document using template
|
||||
*/
|
||||
async convert(markdownContent, outputPath) {
|
||||
try {
|
||||
// Load template
|
||||
const templateBuffer = fs.readFileSync(this.templatePath);
|
||||
const zip = new PizZip(templateBuffer);
|
||||
|
||||
// Extract document.xml
|
||||
let documentXml = zip.file('word/document.xml').asText();
|
||||
|
||||
// Set page size if settings provided
|
||||
if (this.pageSettings) {
|
||||
documentXml = this.setPageSize(documentXml);
|
||||
}
|
||||
|
||||
// Parse markdown and generate Word XML
|
||||
const newContentXml = this.markdownToWordXml(markdownContent);
|
||||
|
||||
// Insert new content after the specified start page
|
||||
const modifiedXml = this.insertContentAfterPage(documentXml, newContentXml, this.startPage);
|
||||
|
||||
// Update the zip with modified XML
|
||||
zip.file('word/document.xml', modifiedXml);
|
||||
|
||||
// Generate and save the new document
|
||||
const newDocBuffer = zip.generate({ type: 'nodebuffer' });
|
||||
fs.writeFileSync(outputPath, newDocBuffer);
|
||||
|
||||
return outputPath;
|
||||
} catch (error) {
|
||||
console.error('Error in Word export:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set page size in document XML
|
||||
*/
|
||||
setPageSize(documentXml) {
|
||||
// Import PAGE_SIZES from main process (need to pass as parameter)
|
||||
const PAGE_SIZES = {
|
||||
a4: { width: 11906, height: 16838 },
|
||||
a3: { width: 16838, height: 23811 },
|
||||
a5: { width: 8391, height: 11906 },
|
||||
b4: { width: 14170, height: 20015 },
|
||||
b5: { width: 9979, height: 14170 },
|
||||
letter: { width: 12240, height: 15840 },
|
||||
legal: { width: 12240, height: 20160 },
|
||||
tabloid: { width: 15840, height: 24480 }
|
||||
};
|
||||
|
||||
let width, height;
|
||||
const pageSize = PAGE_SIZES[this.pageSettings.size];
|
||||
|
||||
if (pageSize) {
|
||||
width = pageSize.width;
|
||||
height = pageSize.height;
|
||||
} else {
|
||||
// Default to A4
|
||||
width = 11906;
|
||||
height = 16838;
|
||||
}
|
||||
|
||||
// Swap dimensions for landscape
|
||||
if (this.pageSettings.orientation === 'landscape') {
|
||||
[width, height] = [height, width];
|
||||
}
|
||||
|
||||
// Update all <w:pgSz> elements in section properties
|
||||
const pgSzRegex = /<w:pgSz[^>]*\/>/g;
|
||||
let modifiedXml = documentXml.replace(pgSzRegex, () => {
|
||||
return `<w:pgSz w:w="${width}" w:h="${height}" w:orient="${this.pageSettings.orientation}"/>`;
|
||||
});
|
||||
|
||||
// If no pgSz found, add it to all sectPr elements
|
||||
if (!pgSzRegex.test(documentXml)) {
|
||||
const sectPrRegex = /<w:sectPr[^>]*>/g;
|
||||
modifiedXml = modifiedXml.replace(sectPrRegex, (match) => {
|
||||
return `${match}<w:pgSz w:w="${width}" w:h="${height}" w:orient="${this.pageSettings.orientation}"/>`;
|
||||
});
|
||||
}
|
||||
|
||||
return modifiedXml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert markdown content after the specified page in the document
|
||||
* @param {string} documentXml - The document XML
|
||||
* @param {string} newContentXml - The new content to insert
|
||||
* @param {number} afterPage - Insert content after this page number (1-based)
|
||||
*/
|
||||
insertContentAfterPage(documentXml, newContentXml, afterPage) {
|
||||
// Find section breaks that mark page boundaries
|
||||
// Look for the section properties tag that marks page breaks
|
||||
const sectionBreakRegex = /<w:sectPr[^>]*>[\s\S]*?<\/w:sectPr>/g;
|
||||
const matches = [...documentXml.matchAll(sectionBreakRegex)];
|
||||
|
||||
// Calculate which section break to insert after
|
||||
// Page 1 = before 1st section break
|
||||
// Page 2 = after 1st section break
|
||||
// Page 3 = after 2nd section break, etc.
|
||||
const sectionIndex = afterPage - 1;
|
||||
|
||||
if (matches.length >= sectionIndex && sectionIndex > 0) {
|
||||
// Insert after the specified section break
|
||||
const insertPoint = matches[sectionIndex - 1].index + matches[sectionIndex - 1][0].length;
|
||||
return documentXml.slice(0, insertPoint) + newContentXml + documentXml.slice(insertPoint);
|
||||
} else if (afterPage === 1 || matches.length === 0) {
|
||||
// Insert at the beginning (after <w:body>) or if no section breaks found
|
||||
const bodyStart = documentXml.indexOf('<w:body>') + 8;
|
||||
return documentXml.slice(0, bodyStart) + newContentXml + documentXml.slice(bodyStart);
|
||||
} else {
|
||||
// If not enough section breaks, insert before closing body tag
|
||||
return documentXml.replace('</w:body>', newContentXml + '</w:body>');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert markdown to Word XML format
|
||||
*/
|
||||
markdownToWordXml(markdown) {
|
||||
const lines = markdown.split('\n');
|
||||
let xml = '';
|
||||
let inCodeBlock = false;
|
||||
let codeLines = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Handle code blocks
|
||||
if (line.trim().startsWith('```')) {
|
||||
if (inCodeBlock) {
|
||||
// End code block
|
||||
xml += this.createCodeBlockXml(codeLines.join('\n'));
|
||||
codeLines = [];
|
||||
inCodeBlock = false;
|
||||
} else {
|
||||
inCodeBlock = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
codeLines.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Empty lines
|
||||
if (!line.trim()) {
|
||||
xml += '<w:p><w:pPr></w:pPr></w:p>';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tables - detect table lines
|
||||
if (line.includes('|') && line.trim().startsWith('|')) {
|
||||
const tableLines = [line];
|
||||
i++;
|
||||
// Collect all consecutive table lines
|
||||
while (i < lines.length && lines[i].includes('|')) {
|
||||
tableLines.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
i--; // Back up one line
|
||||
xml += this.createTableXml(tableLines);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ASCII flowcharts/diagrams - detect box drawing characters
|
||||
if (this.isAsciiArt(line)) {
|
||||
const asciiLines = [line];
|
||||
i++;
|
||||
// Collect all consecutive ASCII art lines
|
||||
while (i < lines.length && (this.isAsciiArt(lines[i]) || !lines[i].trim())) {
|
||||
asciiLines.push(lines[i]);
|
||||
i++;
|
||||
if (i < lines.length && lines[i].trim() && !this.isAsciiArt(lines[i])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
i--; // Back up one line
|
||||
xml += this.createAsciiArtXml(asciiLines.join('\n'));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Headings - strip markdown numbering
|
||||
if (line.trim().startsWith('#')) {
|
||||
const level = (line.match(/^#+/) || [''])[0].length;
|
||||
let text = line.replace(/^#+\s*/, '').trim();
|
||||
// Remove markdown numbering like "1.1 Title" -> "Title"
|
||||
text = text.replace(/^\d+(\.\d+)*\.?\s+/, '');
|
||||
xml += this.createHeadingXml(text, level);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Blockquotes
|
||||
if (line.trim().startsWith('>')) {
|
||||
const text = line.replace(/^>\s*/, '').trim();
|
||||
xml += this.createQuoteXml(text);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ordered lists - strip markdown numbering, use template numbering
|
||||
if (/^\s*\d+\.\s+/.test(line)) {
|
||||
const text = line.replace(/^\s*\d+\.\s+/, '');
|
||||
xml += this.createListItemXml(text, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unordered lists
|
||||
if (/^\s*[-*+]\s+/.test(line)) {
|
||||
const text = line.replace(/^\s*[-*+]\s+/, '');
|
||||
xml += this.createListItemXml(text, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Horizontal rule
|
||||
if (/^[-*_]{3,}$/.test(line.trim())) {
|
||||
xml += this.createHorizontalRuleXml();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normal paragraph
|
||||
xml += this.createParagraphXml(line);
|
||||
}
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create heading XML using template styles
|
||||
*/
|
||||
createHeadingXml(text, level) {
|
||||
const styleName = `Heading${level}`;
|
||||
const runs = this.parseInlineFormatting(text);
|
||||
|
||||
return `<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="${styleName}"/>
|
||||
</w:pPr>
|
||||
${runs}
|
||||
</w:p>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create paragraph XML with Normal style
|
||||
*/
|
||||
createParagraphXml(text) {
|
||||
const runs = this.parseInlineFormatting(text);
|
||||
|
||||
return `<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Normal"/>
|
||||
</w:pPr>
|
||||
${runs}
|
||||
</w:p>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create quote XML
|
||||
*/
|
||||
createQuoteXml(text) {
|
||||
const runs = this.parseInlineFormatting(text);
|
||||
|
||||
return `<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Quote"/>
|
||||
</w:pPr>
|
||||
${runs}
|
||||
</w:p>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create list item XML using template numbering
|
||||
*/
|
||||
createListItemXml(text, numbered) {
|
||||
const runs = this.parseInlineFormatting(text);
|
||||
const numId = numbered ? '1' : '2'; // Template numbering IDs
|
||||
|
||||
return `<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="${numbered ? 'ListNumber' : 'ListBullet'}"/>
|
||||
<w:numPr>
|
||||
<w:ilvl w:val="0"/>
|
||||
<w:numId w:val="${numId}"/>
|
||||
</w:numPr>
|
||||
</w:pPr>
|
||||
${runs}
|
||||
</w:p>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create code block XML - renders each line as separate paragraph
|
||||
* to preserve exact formatting like in preview (monospace, no wrapping)
|
||||
* Background shading applied at paragraph level to extend full width
|
||||
*/
|
||||
createCodeBlockXml(code) {
|
||||
const lines = code.split('\n');
|
||||
let xml = '';
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const escapedLine = this.escapeXml(line);
|
||||
|
||||
// Each line gets its own paragraph with exact spacing and no wrapping
|
||||
// Shading (shd) is at paragraph level so background extends full width
|
||||
xml += `<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Normal"/>
|
||||
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
|
||||
<w:spacing w:before="${index === 0 ? '120' : '0'}" w:after="${index === lines.length - 1 ? '120' : '0'}" w:line="240" w:lineRule="exact"/>
|
||||
<w:ind w:left="284" w:right="284"/>
|
||||
<w:jc w:val="left"/>
|
||||
<w:keepLines/>
|
||||
<w:wordWrap w:val="0"/>
|
||||
</w:pPr>
|
||||
<w:r>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||
<w:sz w:val="18"/>
|
||||
<w:szCs w:val="18"/>
|
||||
<w:noProof/>
|
||||
</w:rPr>
|
||||
<w:t xml:space="preserve">${escapedLine}</w:t>
|
||||
</w:r>
|
||||
</w:p>`;
|
||||
});
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create horizontal rule XML
|
||||
*/
|
||||
createHorizontalRuleXml() {
|
||||
return `<w:p>
|
||||
<w:pPr>
|
||||
<w:pBdr>
|
||||
<w:bottom w:val="single" w:sz="6" w:space="1" w:color="auto"/>
|
||||
</w:pBdr>
|
||||
</w:pPr>
|
||||
</w:p>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create table XML from markdown table lines with template styling
|
||||
* Uses full-width tables with equal column distribution matching template style
|
||||
*/
|
||||
createTableXml(tableLines) {
|
||||
// Parse table
|
||||
const rows = [];
|
||||
for (const line of tableLines) {
|
||||
// Skip separator lines (e.g., |---|---|)
|
||||
if (/^\s*\|[\s\-:]+\|\s*$/.test(line)) {
|
||||
continue;
|
||||
}
|
||||
// Split by | and trim
|
||||
const cells = line.split('|').filter(cell => cell.trim()).map(cell => cell.trim());
|
||||
if (cells.length > 0) {
|
||||
rows.push(cells);
|
||||
}
|
||||
}
|
||||
|
||||
if (rows.length === 0) return '';
|
||||
|
||||
// Calculate number of columns
|
||||
const numCols = Math.max(...rows.map(row => row.length));
|
||||
|
||||
// Calculate column width in twips (1440 twips = 1 inch)
|
||||
// Assume standard page width of 9360 twips (6.5 inches usable)
|
||||
const totalTableWidth = 9360;
|
||||
const colWidth = Math.floor(totalTableWidth / numCols);
|
||||
|
||||
// Build table XML
|
||||
let tableXml = '<w:tbl>';
|
||||
|
||||
// Table properties - use template table style with full width
|
||||
tableXml += `<w:tblPr>
|
||||
<w:tblStyle w:val="TableGrid"/>
|
||||
<w:tblW w:w="0" w:type="auto"/>
|
||||
<w:tblLayout w:type="fixed"/>
|
||||
<w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="0" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/>
|
||||
<w:tblBorders>
|
||||
<w:top w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||
<w:left w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||
<w:bottom w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||
<w:right w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||
<w:insideH w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||
<w:insideV w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||
</w:tblBorders>
|
||||
</w:tblPr>`;
|
||||
|
||||
// Table grid with explicit column widths
|
||||
tableXml += '<w:tblGrid>';
|
||||
for (let i = 0; i < numCols; i++) {
|
||||
tableXml += `<w:gridCol w:w="${colWidth}"/>`;
|
||||
}
|
||||
tableXml += '</w:tblGrid>';
|
||||
|
||||
// Table rows
|
||||
rows.forEach((rowCells, rowIndex) => {
|
||||
const isHeader = rowIndex === 0;
|
||||
|
||||
tableXml += '<w:tr>';
|
||||
|
||||
// Row properties for consistent height
|
||||
tableXml += '<w:trPr><w:trHeight w:val="0" w:hRule="atLeast"/></w:trPr>';
|
||||
|
||||
// Pad row to have same number of columns
|
||||
while (rowCells.length < numCols) {
|
||||
rowCells.push('');
|
||||
}
|
||||
|
||||
rowCells.forEach((cellText, colIndex) => {
|
||||
tableXml += '<w:tc>';
|
||||
tableXml += '<w:tcPr>';
|
||||
|
||||
// Cell width
|
||||
tableXml += `<w:tcW w:w="${colWidth}" w:type="dxa"/>`;
|
||||
|
||||
// Cell shading - orange for header, white for data rows
|
||||
if (isHeader) {
|
||||
tableXml += '<w:shd w:val="clear" w:color="auto" w:fill="F58220"/>';
|
||||
} else {
|
||||
tableXml += '<w:shd w:val="clear" w:color="auto" w:fill="FFFFFF"/>';
|
||||
}
|
||||
|
||||
// Cell borders
|
||||
tableXml += '<w:tcBorders>' +
|
||||
'<w:top w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
||||
'<w:left w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
||||
'<w:bottom w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
||||
'<w:right w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
||||
'</w:tcBorders>';
|
||||
|
||||
// Cell margins for proper padding
|
||||
tableXml += '<w:tcMar>' +
|
||||
'<w:top w:w="80" w:type="dxa"/>' +
|
||||
'<w:left w:w="120" w:type="dxa"/>' +
|
||||
'<w:bottom w:w="80" w:type="dxa"/>' +
|
||||
'<w:right w:w="120" w:type="dxa"/>' +
|
||||
'</w:tcMar>';
|
||||
|
||||
tableXml += '</w:tcPr>';
|
||||
|
||||
// Cell content
|
||||
tableXml += '<w:p>';
|
||||
tableXml += '<w:pPr>';
|
||||
tableXml += '<w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/>';
|
||||
tableXml += '</w:pPr>';
|
||||
|
||||
const runs = this.parseInlineFormatting(cellText);
|
||||
|
||||
if (isHeader) {
|
||||
// Header: bold white text
|
||||
tableXml += runs.replace(/<w:rPr>/g, '<w:rPr><w:b/><w:color w:val="FFFFFF"/>');
|
||||
} else {
|
||||
// Data rows: normal black text
|
||||
tableXml += runs;
|
||||
}
|
||||
|
||||
tableXml += '</w:p>';
|
||||
tableXml += '</w:tc>';
|
||||
});
|
||||
|
||||
tableXml += '</w:tr>';
|
||||
});
|
||||
|
||||
tableXml += '</w:tbl>';
|
||||
|
||||
// Add spacing after table
|
||||
tableXml += '<w:p><w:pPr><w:spacing w:before="120" w:after="0"/></w:pPr></w:p>';
|
||||
|
||||
return tableXml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if line contains ASCII art/flowchart characters
|
||||
*/
|
||||
isAsciiArt(line) {
|
||||
// Don't treat markdown tables as ASCII art
|
||||
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
|
||||
// Check if it's a proper markdown table (has multiple cells)
|
||||
const cells = line.split('|').filter(c => c.trim());
|
||||
if (cells.length >= 2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Common ASCII art characters (Unicode box drawing and symbols)
|
||||
const asciiArtChars = [
|
||||
'─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', // Box drawing
|
||||
'═', '║', '╔', '╗', '╚', '╝', '╠', '╣', '╦', '╩', '╬', // Double box
|
||||
'╭', '╮', '╯', '╰', // Rounded corners
|
||||
'▲', '▼', '◄', '►', '♦', '●', '○', '■', '□', '◆', '◇', // Shapes
|
||||
'↓', '→', '←', '↑', '↔', '↕', '⇒', '⇐', '⇓', '⇑', // Arrows
|
||||
'┃', '━', '┏', '┓', '┗', '┛', '┣', '┫', '┳', '┻', '╋', // Heavy box
|
||||
'░', '▒', '▓', '█', // Shading
|
||||
];
|
||||
|
||||
// Check for box drawing characters
|
||||
if (asciiArtChars.some(char => line.includes(char))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for ASCII box patterns with regular characters
|
||||
const asciiPatterns = [
|
||||
/^\s*\+[-=_+]+\+/, // +-----+ or +=====+
|
||||
/^\s*\|[-=_]{3,}\|/, // |-----|
|
||||
/^\s*[-=_]{5,}$/, // -----
|
||||
/^\s*\+[-]+\+[-]+\+/, // +---+---+
|
||||
/^\s*\([A-Z][a-z]+\)\s*\|\|/, // (Deformable) ||
|
||||
/^\s*\|\s+[A-Z\s]+[-:]\s+[A-Z]/, // | TILE ADHESIVE - TYPE
|
||||
/^\s*\|\s*\[[^\]]+\]/, // | [BIS Mark / CE Mark]
|
||||
/^\s*\|\s{2,}\w+.*\|\|/, // || text ||
|
||||
/^\s*\[[^\]]+\]\s*$/, // [Step in brackets]
|
||||
/^\s*<[-=]+>/, // <----> or <====>
|
||||
/^\s*\/[-_\\\/]+\//, // /----/
|
||||
/^\s*\*[-=\*]+\*/, // *----*
|
||||
];
|
||||
|
||||
return asciiPatterns.some(pattern => pattern.test(line));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ASCII art XML with monospace font
|
||||
* Background shading applied at paragraph level to extend full width
|
||||
*/
|
||||
createAsciiArtXml(asciiContent) {
|
||||
// Split ASCII art into individual lines and create a paragraph for each
|
||||
const lines = asciiContent.split('\n');
|
||||
let xml = '';
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
// Shading (shd) is at paragraph level so background extends full width
|
||||
xml += `<w:p>
|
||||
<w:pPr>
|
||||
<w:pStyle w:val="Normal"/>
|
||||
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
|
||||
<w:spacing w:before="${index === 0 ? '120' : '0'}" w:after="${index === lines.length - 1 ? '120' : '0'}" w:line="240" w:lineRule="exact"/>
|
||||
<w:ind w:left="284" w:right="284"/>
|
||||
<w:jc w:val="left"/>
|
||||
<w:keepLines/>
|
||||
<w:wordWrap w:val="0"/>
|
||||
</w:pPr>`;
|
||||
|
||||
// Check if line contains arrow characters and color them red
|
||||
const arrowChars = ['↓', '→', '←', '↑', '▼', '►', '◄', '▲'];
|
||||
const hasArrow = arrowChars.some(arrow => line.includes(arrow));
|
||||
|
||||
if (hasArrow) {
|
||||
// Split line into parts and color arrows red
|
||||
let remainingLine = line;
|
||||
|
||||
while (remainingLine.length > 0) {
|
||||
let foundArrow = false;
|
||||
let arrowIndex = -1;
|
||||
let foundArrowChar = '';
|
||||
|
||||
// Find the first arrow in the remaining text
|
||||
for (const arrow of arrowChars) {
|
||||
const idx = remainingLine.indexOf(arrow);
|
||||
if (idx !== -1 && (arrowIndex === -1 || idx < arrowIndex)) {
|
||||
arrowIndex = idx;
|
||||
foundArrowChar = arrow;
|
||||
foundArrow = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundArrow) {
|
||||
// Add text before arrow (if any)
|
||||
if (arrowIndex > 0) {
|
||||
const beforeArrow = this.escapeXml(remainingLine.substring(0, arrowIndex));
|
||||
xml += `<w:r>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||
<w:sz w:val="16"/>
|
||||
<w:szCs w:val="16"/>
|
||||
<w:noProof/>
|
||||
</w:rPr>
|
||||
<w:t xml:space="preserve">${beforeArrow}</w:t>
|
||||
</w:r>`;
|
||||
}
|
||||
|
||||
// Add arrow in red
|
||||
const escapedArrow = this.escapeXml(foundArrowChar);
|
||||
xml += `<w:r>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||
<w:sz w:val="16"/>
|
||||
<w:szCs w:val="16"/>
|
||||
<w:color w:val="FF0000"/>
|
||||
<w:noProof/>
|
||||
</w:rPr>
|
||||
<w:t xml:space="preserve">${escapedArrow}</w:t>
|
||||
</w:r>`;
|
||||
|
||||
// Continue with remaining text
|
||||
remainingLine = remainingLine.substring(arrowIndex + foundArrowChar.length);
|
||||
} else {
|
||||
// No more arrows, add remaining text
|
||||
const escapedRemaining = this.escapeXml(remainingLine);
|
||||
xml += `<w:r>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||
<w:sz w:val="16"/>
|
||||
<w:szCs w:val="16"/>
|
||||
<w:noProof/>
|
||||
</w:rPr>
|
||||
<w:t xml:space="preserve">${escapedRemaining}</w:t>
|
||||
</w:r>`;
|
||||
remainingLine = '';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No arrows, just add the line normally
|
||||
const escapedLine = this.escapeXml(line);
|
||||
xml += `<w:r>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||
<w:sz w:val="16"/>
|
||||
<w:szCs w:val="16"/>
|
||||
<w:noProof/>
|
||||
</w:rPr>
|
||||
<w:t xml:space="preserve">${escapedLine}</w:t>
|
||||
</w:r>`;
|
||||
}
|
||||
|
||||
xml += `</w:p>`;
|
||||
});
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse inline formatting (bold, italic, code)
|
||||
*/
|
||||
parseInlineFormatting(text) {
|
||||
let xml = '';
|
||||
let pos = 0;
|
||||
|
||||
// Patterns for inline formatting
|
||||
const patterns = [
|
||||
{ regex: /\*\*\*(.+?)\*\*\*/g, bold: true, italic: true },
|
||||
{ regex: /\*\*(.+?)\*\*/g, bold: true },
|
||||
{ regex: /\*(.+?)\*/g, italic: true },
|
||||
{ regex: /`(.+?)`/g, code: true }
|
||||
];
|
||||
|
||||
// Simple approach: process text sequentially
|
||||
let remaining = text;
|
||||
|
||||
while (remaining.length > 0) {
|
||||
let foundMatch = false;
|
||||
let earliestPos = remaining.length;
|
||||
let matchedPattern = null;
|
||||
let match = null;
|
||||
|
||||
// Find earliest match
|
||||
for (const pattern of patterns) {
|
||||
pattern.regex.lastIndex = 0;
|
||||
const m = pattern.regex.exec(remaining);
|
||||
if (m && m.index < earliestPos) {
|
||||
earliestPos = m.index;
|
||||
matchedPattern = pattern;
|
||||
match = m;
|
||||
foundMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundMatch) {
|
||||
// Add text before match
|
||||
if (earliestPos > 0) {
|
||||
xml += this.createRunXml(remaining.substring(0, earliestPos));
|
||||
}
|
||||
|
||||
// Add formatted text
|
||||
xml += this.createRunXml(match[1], matchedPattern.bold, matchedPattern.italic, matchedPattern.code);
|
||||
|
||||
remaining = remaining.substring(earliestPos + match[0].length);
|
||||
} else {
|
||||
// No more matches, add remaining text
|
||||
xml += this.createRunXml(remaining);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a run (text segment) XML
|
||||
*/
|
||||
createRunXml(text, bold = false, italic = false, code = false) {
|
||||
if (!text) return '';
|
||||
|
||||
const escapedText = this.escapeXml(text);
|
||||
let propsXml = '<w:rPr>';
|
||||
|
||||
if (bold) propsXml += '<w:b/>';
|
||||
if (italic) propsXml += '<w:i/>';
|
||||
if (code) {
|
||||
propsXml += '<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas"/>';
|
||||
propsXml += '<w:sz w:val="20"/>';
|
||||
}
|
||||
|
||||
propsXml += '</w:rPr>';
|
||||
|
||||
return `<w:r>${propsXml}<w:t xml:space="preserve">${escapedText}</w:t></w:r>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape XML special characters
|
||||
*/
|
||||
escapeXml(text) {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WordTemplateExporter;
|
||||
@@ -0,0 +1,12 @@
|
||||
# Test Double-Click File Opening
|
||||
|
||||
This is a test file to verify double-click functionality.
|
||||
|
||||
## Features to Test
|
||||
- File should load automatically
|
||||
- Content should display in editor
|
||||
- Tab should show filename
|
||||
- Preview should render markdown
|
||||
|
||||
**This text should be bold**
|
||||
*This text should be italic*
|
||||
@@ -0,0 +1,14 @@
|
||||
# Test File Opening
|
||||
|
||||
This is a test file to verify that double-click file opening works correctly.
|
||||
|
||||
## Features
|
||||
- **Bold text**
|
||||
- *Italic text*
|
||||
- `Code`
|
||||
|
||||
```javascript
|
||||
console.log("Hello World");
|
||||
```
|
||||
|
||||
This should appear when you double-click to open the file!
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||