mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b281255e90 | ||
|
|
d8e0748037 | ||
|
|
cb98549db8 | ||
|
|
ff651e9b21 | ||
|
|
e013e5fd69 | ||
|
|
f7362bec46 |
+19
-33
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**Current Version**: v1.9.0
|
**Current Version**: v1.9.2
|
||||||
**Author**: Amit Haridas (amit.wh@gmail.com)
|
**Author**: Amit Haridas (amit.wh@gmail.com)
|
||||||
**License**: MIT
|
**License**: MIT
|
||||||
**Repository**: https://github.com/amitwh/pan-converter
|
**Repository**: https://github.com/amitwh/pan-converter
|
||||||
@@ -113,7 +113,77 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
|
|||||||
|
|
||||||
## Feature Implementation Guide
|
## Feature Implementation Guide
|
||||||
|
|
||||||
### v1.9.0 Custom Headers & Footers (Latest)
|
### v1.9.1 ASCII Art & Code Block Preservation (Latest)
|
||||||
|
|
||||||
|
#### 🎨 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
|
#### 📄 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`)
|
**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`)
|
||||||
@@ -1109,7 +1179,7 @@ if (!gotTheLock) {
|
|||||||
|
|
||||||
## Pending Tasks & Future Enhancements
|
## Pending Tasks & Future Enhancements
|
||||||
|
|
||||||
No pending tasks at this time. All planned v1.9.0 features have been completed.
|
No pending tasks at this time. All planned v1.9.1 features have been completed.
|
||||||
|
|
||||||
### Potential Future Enhancements
|
### Potential Future Enhancements
|
||||||
- Image embedding in headers/footers (partially implemented - logo upload UI ready)
|
- Image embedding in headers/footers (partially implemented - logo upload UI ready)
|
||||||
@@ -1119,19 +1189,19 @@ No pending tasks at this time. All planned v1.9.0 features have been completed.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated**: October 28, 2025
|
**Last Updated**: December 12, 2025
|
||||||
**Claude Assistant**: Development completed for v1.9.0 with Custom Headers & Footers!
|
**Claude Assistant**: Development completed for v1.9.1 with ASCII Art Preservation!
|
||||||
|
|
||||||
### v1.9.0 - Latest Release Summary
|
### v1.9.1 - Latest Release Summary
|
||||||
- **Custom Headers & Footers**: Professional headers/footers for PDF, DOCX, ODT, and PowerPoint exports
|
- **ASCII Art Preservation**: Code blocks, flowcharts, tables, and ASCII art now export exactly as shown in preview
|
||||||
- **Dynamic Field Support**: $PAGE$, $TOTAL$, $DATE$, $TIME$, $TITLE$, $AUTHOR$, $FILENAME$
|
- **Enhanced Code Block Rendering**: Each line rendered separately to preserve exact spacing and alignment
|
||||||
- **Logo Upload UI**: Ready for image embedding in headers/footers
|
- **Monospace Font Support**: Consolas font used consistently across all export formats
|
||||||
- **Three-Column Layout**: Left/Center/Right positioning for headers and footers
|
- **Improved ASCII Detection**: Extended Unicode box-drawing and arrow character support
|
||||||
- **Batch Support**: Full header/footer integration in batch converter
|
- **PDF Export Enhancement**: Added monofont and highlight-style options for better code rendering
|
||||||
- **Persistent Settings**: Configurations saved across sessions
|
- **HTML/Electron PDF Fix**: Enhanced CSS to prevent text wrapping in code blocks
|
||||||
- **Dark Theme Support**: Complete styling for all 22 themes
|
|
||||||
|
|
||||||
### Previous Releases
|
### 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.3: Streamlined PDF Editor UI & Configurable Template Settings
|
||||||
- v1.8.2: Enhanced PDF Export & Print Fix
|
- v1.8.2: Enhanced PDF Export & Print Fix
|
||||||
- v1.8.1: Streamlined PDF Editor UI
|
- v1.8.1: Streamlined PDF Editor UI
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pan-converter",
|
"name": "pan-converter",
|
||||||
"version": "1.9.0",
|
"version": "2.0.0",
|
||||||
"description": "Cross-platform Markdown editor and converter using Pandoc",
|
"description": "Cross-platform Markdown editor and converter using Pandoc",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+200
-2
@@ -11,6 +11,8 @@
|
|||||||
<link rel="stylesheet" href="styles-modern.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/codemirror/6.65.7/codemirror.min.css">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
|
||||||
|
<!-- Mermaid.js for diagram rendering -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -148,6 +150,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="export-dialog-body">
|
<div class="export-dialog-body">
|
||||||
<!-- Simple/Advanced Export Toggle -->
|
<!-- 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">
|
<div class="export-section export-mode-toggle">
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" id="advanced-export-toggle">
|
<input type="checkbox" id="advanced-export-toggle">
|
||||||
@@ -243,6 +258,37 @@
|
|||||||
</div> <!-- End advanced-export-options -->
|
</div> <!-- End advanced-export-options -->
|
||||||
|
|
||||||
<!-- Basic Export Options (always visible) -->
|
<!-- 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">
|
<div class="export-section basic-options">
|
||||||
<label>Quick Export Settings:</label>
|
<label>Quick Export Settings:</label>
|
||||||
<div class="checkbox-group">
|
<div class="checkbox-group">
|
||||||
@@ -258,6 +304,154 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>Select Template:</label>
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;">
|
||||||
|
<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="check">Checkmark ✓</button>
|
||||||
|
<button class="btn-secondary ascii-template-btn" data-template="divider">Divider</button>
|
||||||
|
<button class="btn-secondary ascii-template-btn" data-template="header">Header</button>
|
||||||
|
<button class="btn-secondary ascii-template-btn" data-template="flowchart">Flowchart</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 -->
|
<!-- Universal File Converter Dialog -->
|
||||||
<div id="universal-converter-dialog" class="export-dialog hidden">
|
<div id="universal-converter-dialog" class="export-dialog hidden">
|
||||||
<div class="export-dialog-content">
|
<div class="export-dialog-content">
|
||||||
@@ -964,7 +1158,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="hf-logo-row">
|
<div class="hf-logo-row">
|
||||||
<label>Header Logo/Image:</label>
|
<label>Header Logo/Image:</label>
|
||||||
<input type="file" id="header-logo" accept="image/*">
|
<button id="header-logo-browse" class="browse-btn">Browse...</button>
|
||||||
<button id="header-logo-clear" class="clear-btn">Clear</button>
|
<button id="header-logo-clear" class="clear-btn">Clear</button>
|
||||||
<span id="header-logo-preview" class="logo-preview"></span>
|
<span id="header-logo-preview" class="logo-preview"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -992,7 +1186,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="hf-logo-row">
|
<div class="hf-logo-row">
|
||||||
<label>Footer Logo/Image:</label>
|
<label>Footer Logo/Image:</label>
|
||||||
<input type="file" id="footer-logo" accept="image/*">
|
<button id="footer-logo-browse" class="browse-btn">Browse...</button>
|
||||||
<button id="footer-logo-clear" class="clear-btn">Clear</button>
|
<button id="footer-logo-clear" class="clear-btn">Clear</button>
|
||||||
<span id="footer-logo-preview" class="logo-preview"></span>
|
<span id="footer-logo-preview" class="logo-preview"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1049,7 +1243,11 @@
|
|||||||
<textarea id="editor-1" class="editor-textarea"></textarea>
|
<textarea id="editor-1" class="editor-textarea"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</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-pane-1" class="pane">
|
||||||
|
<div class="preview-header">
|
||||||
|
<button class="preview-popout-btn" id="preview-popout-1" title="Pop out preview in new window">⬜</button>
|
||||||
|
</div>
|
||||||
<div id="preview-1" class="preview-content"></div>
|
<div id="preview-1" class="preview-content"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+409
-7
@@ -5,6 +5,15 @@ const { exec } = require('child_process');
|
|||||||
const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib');
|
const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib');
|
||||||
const WordTemplateExporter = require('./wordTemplateExporter');
|
const WordTemplateExporter = require('./wordTemplateExporter');
|
||||||
|
|
||||||
|
// Add MiKTeX to PATH for LaTeX support
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const miktexPath = 'C:\\Program Files\\MiKTeX\\miktex\\bin\\x64';
|
||||||
|
if (fs.existsSync(miktexPath)) {
|
||||||
|
process.env.PATH = `${miktexPath};${process.env.PATH}`;
|
||||||
|
console.log('[MAIN] Added MiKTeX to PATH:', miktexPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get the system Pandoc path
|
// Get the system Pandoc path
|
||||||
function getPandocPath() {
|
function getPandocPath() {
|
||||||
// Pandoc is expected to be in the system's PATH.
|
// Pandoc is expected to be in the system's PATH.
|
||||||
@@ -69,6 +78,66 @@ let headerFooterSettings = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Page Size Definitions (in twentieths of a point for Word, mm/inches for Pandoc)
|
||||||
|
const PAGE_SIZES = {
|
||||||
|
a4: {
|
||||||
|
name: 'A4',
|
||||||
|
pandoc: 'a4',
|
||||||
|
word: { width: 11906, height: 16838 }, // 210×297mm
|
||||||
|
dimensions: '210×297mm'
|
||||||
|
},
|
||||||
|
a3: {
|
||||||
|
name: 'A3',
|
||||||
|
pandoc: 'a3',
|
||||||
|
word: { width: 16838, height: 23811 }, // 297×420mm
|
||||||
|
dimensions: '297×420mm'
|
||||||
|
},
|
||||||
|
a5: {
|
||||||
|
name: 'A5',
|
||||||
|
pandoc: 'a5',
|
||||||
|
word: { width: 8391, height: 11906 }, // 148×210mm
|
||||||
|
dimensions: '148×210mm'
|
||||||
|
},
|
||||||
|
b4: {
|
||||||
|
name: 'B4',
|
||||||
|
pandoc: 'b4',
|
||||||
|
word: { width: 14170, height: 20015 }, // 250×353mm
|
||||||
|
dimensions: '250×353mm'
|
||||||
|
},
|
||||||
|
b5: {
|
||||||
|
name: 'B5',
|
||||||
|
pandoc: 'b5',
|
||||||
|
word: { width: 9979, height: 14170 }, // 176×250mm
|
||||||
|
dimensions: '176×250mm'
|
||||||
|
},
|
||||||
|
letter: {
|
||||||
|
name: 'Letter',
|
||||||
|
pandoc: 'letter',
|
||||||
|
word: { width: 12240, height: 15840 }, // 8.5×11in
|
||||||
|
dimensions: '8.5×11in'
|
||||||
|
},
|
||||||
|
legal: {
|
||||||
|
name: 'Legal',
|
||||||
|
pandoc: 'legal',
|
||||||
|
word: { width: 12240, height: 20160 }, // 8.5×14in
|
||||||
|
dimensions: '8.5×14in'
|
||||||
|
},
|
||||||
|
tabloid: {
|
||||||
|
name: 'Tabloid',
|
||||||
|
pandoc: 'tabloid',
|
||||||
|
word: { width: 15840, height: 24480 }, // 11×17in
|
||||||
|
dimensions: '11×17in'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default page settings
|
||||||
|
let pageSettings = {
|
||||||
|
size: 'a4',
|
||||||
|
orientation: 'portrait',
|
||||||
|
customWidth: null,
|
||||||
|
customHeight: null
|
||||||
|
};
|
||||||
|
|
||||||
// Handle single instance lock for Windows file association
|
// Handle single instance lock for Windows file association
|
||||||
// When a file is double-clicked and the app is already running,
|
// When a file is double-clicked and the app is already running,
|
||||||
// Windows tries to start a second instance. We prevent this and
|
// Windows tries to start a second instance. We prevent this and
|
||||||
@@ -496,6 +565,23 @@ function createMenu() {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Tools',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Table Generator',
|
||||||
|
click: () => mainWindow.webContents.send('show-table-generator')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'ASCII Art Generator',
|
||||||
|
click: () => mainWindow.webContents.send('show-ascii-generator')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Document Compare',
|
||||||
|
click: () => mainWindow.webContents.send('show-document-compare')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Help',
|
label: 'Help',
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -506,7 +592,7 @@ function createMenu() {
|
|||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'About PanConverter',
|
title: 'About PanConverter',
|
||||||
message: 'PanConverter',
|
message: 'PanConverter',
|
||||||
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.9.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Custom Headers & Footers for PDF, DOCX, ODT, and PowerPoint exports\n• Dynamic field support: $PAGE$, $TOTAL$, $DATE$, $TIME$, $TITLE$, $AUTHOR$, $FILENAME$\n• Logo/image embedding in headers and footers\n• Modern glassmorphism UI with gradient backgrounds\n• Enhanced PDF export via Word template with configurable start page\n• Configurable template settings (start page selection)\n• Streamlined PDF Editor UI (merge, split, compress, rotate, watermark, encrypt)\n• Universal File Converter (LibreOffice, ImageMagick, FFmpeg, Pandoc)\n• Windows Explorer context menu integration\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Real-time preview updates while typing\n• Full toolbar markdown editing functions\n• Enhanced PDF export with built-in Electron fallback\n• Enhanced Word export with template support (single file & batch)\n• File association support for .md files\n• Command-line interface for batch conversion\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Improved preview typography and spacing\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• Complete theme support including Monokai fixes\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• 22 beautiful themes (including Dracula, Nord, Tokyo Night, Gruvbox, Ayu, Concrete, and more)\n• Undo/redo functionality\n• Live word count and statistics',
|
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 2.0.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\n✨ New in v2.0.0:\n• Export Profiles - Save and load export configurations\n• Mermaid.js diagram support - Render flowcharts, sequence diagrams, and more\n• Command Palette (Ctrl+Shift+P) - Quick access to all commands\n• GitHub Light/Dark preview themes - Beautiful code preview styling\n• Table Generator - Interactive table creation tool\n• ASCII Art Generator - Create text banners, boxes, and templates\n• Resizable Preview Pane - Drag divider to adjust editor/preview sizes\n• Pop-out Preview Window - Open preview in separate window with live sync\n\nCore Features:\n• Configurable page sizes (A3, A4, A5, B4, B5, Letter, Legal, Tabloid, Custom) with orientation support\n• Page size support for all export formats (PDF, DOCX, ODT, PowerPoint) and batch conversion\n• Preserved ASCII art, charts, tables, flowcharts in exports with full-width background\n• Custom Headers & Footers for PDF, DOCX, ODT, and PowerPoint exports\n• Dynamic field support: $PAGE$, $TOTAL$, $DATE$, $TIME$, $TITLE$, $AUTHOR$, $FILENAME$\n• Logo/image embedding in headers and footers\n• Modern glassmorphism UI with gradient backgrounds\n• Enhanced PDF export via Word template with configurable start page\n• Streamlined PDF Editor UI (merge, split, compress, rotate, watermark, encrypt)\n• Universal File Converter (LibreOffice, ImageMagick, FFmpeg, Pandoc)\n• Windows Explorer context menu integration\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Real-time preview updates while typing\n• Full toolbar markdown editing functions\n• Enhanced PDF export with built-in Electron fallback\n• Enhanced Word export with template support (single file & batch)\n• File association support for .md files\n• Command-line interface for batch conversion\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• 22 beautiful themes (including Dracula, Nord, Tokyo Night, Gruvbox, Ayu, Concrete, and more)\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Undo/redo functionality\n• Live word count and statistics',
|
||||||
buttons: ['OK']
|
buttons: ['OK']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -663,9 +749,72 @@ ipcMain.on('save-header-footer-settings', (event, settings) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get current page settings
|
||||||
|
ipcMain.on('get-page-settings', (event) => {
|
||||||
|
event.reply('page-settings-data', pageSettings);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update page settings from export dialog
|
||||||
|
ipcMain.on('update-page-settings', (event, settings) => {
|
||||||
|
pageSettings = settings;
|
||||||
|
store.set('pageSettings', pageSettings);
|
||||||
|
console.log('Page settings updated:', pageSettings);
|
||||||
|
});
|
||||||
|
|
||||||
// Save header/footer logo image
|
// Save header/footer logo image
|
||||||
|
// Browse for header/footer logo
|
||||||
|
ipcMain.on('browse-header-footer-logo', async (event, position) => {
|
||||||
|
try {
|
||||||
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
title: `Select ${position.charAt(0).toUpperCase() + position.slice(1)} Logo/Image`,
|
||||||
|
filters: [
|
||||||
|
{ name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp'] }
|
||||||
|
],
|
||||||
|
properties: ['openFile']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
|
const filePath = result.filePaths[0];
|
||||||
|
|
||||||
|
// Copy image to userData directory for persistent storage
|
||||||
|
const userDataPath = app.getPath('userData');
|
||||||
|
const logoDir = path.join(userDataPath, 'logos');
|
||||||
|
|
||||||
|
// Create logos directory if it doesn't exist
|
||||||
|
if (!fs.existsSync(logoDir)) {
|
||||||
|
fs.mkdirSync(logoDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique filename
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
const filename = `${position}_${Date.now()}${ext}`;
|
||||||
|
const destPath = path.join(logoDir, filename);
|
||||||
|
|
||||||
|
// Copy file
|
||||||
|
fs.copyFileSync(filePath, destPath);
|
||||||
|
|
||||||
|
// Update settings
|
||||||
|
if (position === 'header') {
|
||||||
|
headerFooterSettings.header.logo = destPath;
|
||||||
|
} else if (position === 'footer') {
|
||||||
|
headerFooterSettings.footer.logo = destPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.reply('header-footer-logo-saved', { position, path: destPath });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logo browse error:', error);
|
||||||
|
dialog.showErrorBox('Logo Error', `Failed to select logo: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.on('save-header-footer-logo', async (event, { position, filePath }) => {
|
ipcMain.on('save-header-footer-logo', async (event, { position, filePath }) => {
|
||||||
try {
|
try {
|
||||||
|
if (!filePath) {
|
||||||
|
dialog.showErrorBox('Logo Error', 'Failed to save logo: The "path" argument must be of type string. Received undefined');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Copy image to userData directory for persistent storage
|
// Copy image to userData directory for persistent storage
|
||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = app.getPath('userData');
|
||||||
const logoDir = path.join(userDataPath, 'logos');
|
const logoDir = path.join(userDataPath, 'logos');
|
||||||
@@ -675,6 +824,12 @@ ipcMain.on('save-header-footer-logo', async (event, { position, filePath }) => {
|
|||||||
fs.mkdirSync(logoDir, { recursive: true });
|
fs.mkdirSync(logoDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify source file exists
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
dialog.showErrorBox('Logo Error', `Source file not found: ${filePath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Generate unique filename
|
// Generate unique filename
|
||||||
const ext = path.extname(filePath);
|
const ext = path.extname(filePath);
|
||||||
const filename = `${position}_${Date.now()}${ext}`;
|
const filename = `${position}_${Date.now()}${ext}`;
|
||||||
@@ -692,6 +847,7 @@ ipcMain.on('save-header-footer-logo', async (event, { position, filePath }) => {
|
|||||||
|
|
||||||
event.reply('header-footer-logo-saved', { position, path: destPath });
|
event.reply('header-footer-logo-saved', { position, path: destPath });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Logo save error:', error);
|
||||||
dialog.showErrorBox('Logo Error', `Failed to save logo: ${error.message}`);
|
dialog.showErrorBox('Logo Error', `Failed to save logo: ${error.message}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -727,6 +883,67 @@ function processDynamicFields(text, metadata = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add headers/footers to DOCX file using PizZip and docx4js
|
// Add headers/footers to DOCX file using PizZip and docx4js
|
||||||
|
// Function to set page size in DOCX files
|
||||||
|
async function setDocxPageSize(docxPath) {
|
||||||
|
try {
|
||||||
|
const PizZip = require('pizzip');
|
||||||
|
|
||||||
|
// Read the DOCX file
|
||||||
|
const docxBuffer = fs.readFileSync(docxPath);
|
||||||
|
const zip = new PizZip(docxBuffer);
|
||||||
|
|
||||||
|
// Get document.xml
|
||||||
|
let documentXml = zip.file('word/document.xml').asText();
|
||||||
|
|
||||||
|
// Get page dimensions
|
||||||
|
let width, height;
|
||||||
|
const pageSize = PAGE_SIZES[pageSettings.size];
|
||||||
|
|
||||||
|
if (pageSize) {
|
||||||
|
width = pageSize.word.width;
|
||||||
|
height = pageSize.word.height;
|
||||||
|
} else if (pageSettings.customWidth && pageSettings.customHeight) {
|
||||||
|
// Parse custom dimensions (convert to twentieths of a point)
|
||||||
|
// Note: This is simplified - production code should handle various units
|
||||||
|
width = parseInt(pageSettings.customWidth) || 11906;
|
||||||
|
height = parseInt(pageSettings.customHeight) || 16838;
|
||||||
|
} else {
|
||||||
|
// Default to A4
|
||||||
|
width = 11906;
|
||||||
|
height = 16838;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap dimensions for landscape
|
||||||
|
if (pageSettings.orientation === 'landscape') {
|
||||||
|
[width, height] = [height, width];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all <w:pgSz> elements in section properties
|
||||||
|
const pgSzRegex = /<w:pgSz[^>]*\/>/g;
|
||||||
|
documentXml = documentXml.replace(pgSzRegex, () => {
|
||||||
|
return `<w:pgSz w:w="${width}" w:h="${height}" w:orient="${pageSettings.orientation}"/>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no pgSz found, add it to all sectPr elements
|
||||||
|
if (!pgSzRegex.test(documentXml)) {
|
||||||
|
const sectPrRegex = /<w:sectPr[^>]*>/g;
|
||||||
|
documentXml = documentXml.replace(sectPrRegex, (match) => {
|
||||||
|
return `${match}<w:pgSz w:w="${width}" w:h="${height}" w:orient="${pageSettings.orientation}"/>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save updated document.xml
|
||||||
|
zip.file('word/document.xml', documentXml);
|
||||||
|
|
||||||
|
// Write modified DOCX
|
||||||
|
const newDocxBuffer = zip.generate({ type: 'nodebuffer' });
|
||||||
|
fs.writeFileSync(docxPath, newDocxBuffer);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set DOCX page size:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function addHeaderFooterToDocx(docxPath, metadata = {}) {
|
async function addHeaderFooterToDocx(docxPath, metadata = {}) {
|
||||||
if (!headerFooterSettings.enabled) return;
|
if (!headerFooterSettings.enabled) return;
|
||||||
|
|
||||||
@@ -872,8 +1089,8 @@ async function exportWordWithTemplate() {
|
|||||||
|
|
||||||
if (result.canceled) return;
|
if (result.canceled) return;
|
||||||
|
|
||||||
// Create exporter instance with selected template and start page
|
// Create exporter instance with selected template, start page, and page settings
|
||||||
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
|
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage, pageSettings);
|
||||||
|
|
||||||
// Convert markdown to DOCX
|
// Convert markdown to DOCX
|
||||||
await exporter.convert(content, result.filePath);
|
await exporter.convert(content, result.filePath);
|
||||||
@@ -1196,6 +1413,42 @@ function performExportWithOptions(format, options) {
|
|||||||
pandocCmd += ` --pdf-engine="${pdfEngine}"`;
|
pandocCmd += ` --pdf-engine="${pdfEngine}"`;
|
||||||
if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`;
|
if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`;
|
||||||
|
|
||||||
|
// Add monospace font settings for code blocks (ASCII art preservation)
|
||||||
|
pandocCmd += ' -V monofont="Consolas"';
|
||||||
|
pandocCmd += ' --highlight-style=tango';
|
||||||
|
|
||||||
|
// Add header/footer if enabled
|
||||||
|
if (headerFooterSettings.enabled) {
|
||||||
|
const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document';
|
||||||
|
const metadata = { filename, title: filename, author: '' };
|
||||||
|
|
||||||
|
const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata);
|
||||||
|
const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata);
|
||||||
|
const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata);
|
||||||
|
const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata);
|
||||||
|
const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata);
|
||||||
|
const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata);
|
||||||
|
|
||||||
|
// Create LaTeX header
|
||||||
|
const latexHeader = `
|
||||||
|
\\usepackage{fancyhdr}
|
||||||
|
\\pagestyle{fancy}
|
||||||
|
\\fancyhf{}
|
||||||
|
\\lhead{${headerLeft.replace(/\\/g, '\\\\')}}
|
||||||
|
\\chead{${headerCenter.replace(/\\/g, '\\\\')}}
|
||||||
|
\\rhead{${headerRight.replace(/\\/g, '\\\\')}}
|
||||||
|
\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}}
|
||||||
|
\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}}
|
||||||
|
\\rfoot{${footerRight.replace(/\\/g, '\\\\')}}
|
||||||
|
\\renewcommand{\\headrulewidth}{0.4pt}
|
||||||
|
\\renewcommand{\\footrulewidth}{0.4pt}
|
||||||
|
`;
|
||||||
|
const headerFile = path.join(require('os').tmpdir(), `header_export_${Date.now()}.tex`);
|
||||||
|
fs.writeFileSync(headerFile, latexHeader, 'utf-8');
|
||||||
|
pandocCmd += ` --include-in-header="${headerFile}"`;
|
||||||
|
pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"';
|
||||||
|
}
|
||||||
|
|
||||||
// Try with specified PDF engine
|
// Try with specified PDF engine
|
||||||
exec(pandocCmd, (error) => {
|
exec(pandocCmd, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -1209,6 +1462,15 @@ function performExportWithOptions(format, options) {
|
|||||||
} else if (format === 'docx') {
|
} else if (format === 'docx') {
|
||||||
pandocCmd += ' -t docx';
|
pandocCmd += ' -t docx';
|
||||||
exportWithPandoc(pandocCmd, outputFile, format);
|
exportWithPandoc(pandocCmd, outputFile, format);
|
||||||
|
} else if (format === 'pptx') {
|
||||||
|
// Add PowerPoint footer if enabled
|
||||||
|
if (headerFooterSettings.enabled && headerFooterSettings.footer.center) {
|
||||||
|
const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document';
|
||||||
|
const metadata = { filename, title: filename, author: '' };
|
||||||
|
const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata);
|
||||||
|
pandocCmd += ` --variable footer="${footerText}"`;
|
||||||
|
}
|
||||||
|
exportWithPandoc(pandocCmd, outputFile, format);
|
||||||
} else {
|
} else {
|
||||||
// Generic export for other formats
|
// Generic export for other formats
|
||||||
exportWithPandoc(pandocCmd, outputFile, format);
|
exportWithPandoc(pandocCmd, outputFile, format);
|
||||||
@@ -1230,9 +1492,45 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro
|
|||||||
const engine = engines[index];
|
const engine = engines[index];
|
||||||
let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -o "${outputFile}"`;
|
let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -o "${outputFile}"`;
|
||||||
|
|
||||||
|
// Add monospace font settings for code blocks (ASCII art preservation)
|
||||||
|
pandocCmd += ' -V monofont="Consolas"';
|
||||||
|
pandocCmd += ' --highlight-style=tango';
|
||||||
|
|
||||||
// Add geometry if specified
|
// Add geometry if specified
|
||||||
if (options.geometry) pandocCmd = pandocCmd.replace(` -o `, ` -V geometry:"${options.geometry}" -o `);
|
if (options.geometry) pandocCmd = pandocCmd.replace(` -o `, ` -V geometry:"${options.geometry}" -o `);
|
||||||
|
|
||||||
|
// Add header/footer if enabled
|
||||||
|
if (headerFooterSettings.enabled) {
|
||||||
|
const filename = path.basename(inputFile, path.extname(inputFile));
|
||||||
|
const metadata = { filename, title: filename, author: options.metadata?.author || '' };
|
||||||
|
|
||||||
|
const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata);
|
||||||
|
const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata);
|
||||||
|
const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata);
|
||||||
|
const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata);
|
||||||
|
const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata);
|
||||||
|
const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata);
|
||||||
|
|
||||||
|
// Create LaTeX header for fancyhdr
|
||||||
|
const latexHeader = `
|
||||||
|
\\usepackage{fancyhdr}
|
||||||
|
\\usepackage{lastpage}
|
||||||
|
\\pagestyle{fancy}
|
||||||
|
\\fancyhf{}
|
||||||
|
\\lhead{${headerLeft.replace(/\\/g, '\\\\')}}
|
||||||
|
\\chead{${headerCenter.replace(/\\/g, '\\\\')}}
|
||||||
|
\\rhead{${headerRight.replace(/\\/g, '\\\\')}}
|
||||||
|
\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}}
|
||||||
|
\\cfoot{${footerCenter.replace(/\$PAGE\$/g, '\\\\thepage').replace(/\$TOTAL\$/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}}
|
||||||
|
\\rfoot{${footerRight.replace(/\\/g, '\\\\')}}
|
||||||
|
\\renewcommand{\\headrulewidth}{0.4pt}
|
||||||
|
\\renewcommand{\\footrulewidth}{0.4pt}
|
||||||
|
`;
|
||||||
|
const headerFile = path.join(require('os').tmpdir(), `header_fallback_${Date.now()}.tex`);
|
||||||
|
fs.writeFileSync(headerFile, latexHeader, 'utf-8');
|
||||||
|
pandocCmd += ` --include-in-header="${headerFile}"`;
|
||||||
|
}
|
||||||
|
|
||||||
// Add all other options
|
// Add all other options
|
||||||
if (options.template && options.template !== 'default') {
|
if (options.template && options.template !== 'default') {
|
||||||
pandocCmd += ` --template="${options.template}"`;
|
pandocCmd += ` --template="${options.template}"`;
|
||||||
@@ -1296,6 +1594,16 @@ function exportWithPandoc(pandocCmd, outputFile, format) {
|
|||||||
console.warn(`Pandoc stderr (non-fatal):`, stderr);
|
console.warn(`Pandoc stderr (non-fatal):`, stderr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set page size for DOCX
|
||||||
|
if (format === 'docx') {
|
||||||
|
try {
|
||||||
|
await setDocxPageSize(outputFile);
|
||||||
|
console.log('Page size set for DOCX');
|
||||||
|
} catch (pageSizeError) {
|
||||||
|
console.error('Error setting page size for DOCX:', pageSizeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add headers/footers to DOCX if enabled
|
// Add headers/footers to DOCX if enabled
|
||||||
if (format === 'docx' && headerFooterSettings.enabled) {
|
if (format === 'docx' && headerFooterSettings.enabled) {
|
||||||
try {
|
try {
|
||||||
@@ -1313,6 +1621,16 @@ function exportWithPandoc(pandocCmd, outputFile, format) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set page size for ODT
|
||||||
|
if (format === 'odt') {
|
||||||
|
try {
|
||||||
|
await setDocxPageSize(outputFile); // ODT has similar structure
|
||||||
|
console.log('Page size set for ODT');
|
||||||
|
} catch (pageSizeError) {
|
||||||
|
console.error('Error setting page size for ODT:', pageSizeError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add headers/footers to ODT if enabled
|
// Add headers/footers to ODT if enabled
|
||||||
if (format === 'odt' && headerFooterSettings.enabled) {
|
if (format === 'odt' && headerFooterSettings.enabled) {
|
||||||
// ODT format is similar to DOCX in structure, we could implement this
|
// ODT format is similar to DOCX in structure, we could implement this
|
||||||
@@ -1374,21 +1692,37 @@ function exportToHTML(outputFile) {
|
|||||||
margin-top: 1.5em;
|
margin-top: 1.5em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
/* Inline code */
|
||||||
code {
|
code {
|
||||||
background: #f4f4f4;
|
background: #f4f4f4;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-family: Consolas, Monaco, 'Courier New', monospace;
|
font-family: Consolas, Monaco, 'Courier New', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
/* Code blocks - critical for ASCII art preservation */
|
||||||
pre {
|
pre {
|
||||||
background: #f4f4f4;
|
background: #f5f5f5;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
font-family: Consolas, Monaco, 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
pre code {
|
pre code {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
blockquote {
|
blockquote {
|
||||||
border-left: 4px solid #ddd;
|
border-left: 4px solid #ddd;
|
||||||
@@ -1420,6 +1754,18 @@ function exportToHTML(outputFile) {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
@media print {
|
||||||
|
pre {
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
overflow: visible;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -1462,21 +1808,38 @@ function exportToPDFElectron(outputFile) {
|
|||||||
margin-top: 1.5em;
|
margin-top: 1.5em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
/* Inline code */
|
||||||
code {
|
code {
|
||||||
background: #f4f4f4;
|
background: #f4f4f4;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-family: Consolas, Monaco, 'Courier New', monospace;
|
font-family: Consolas, Monaco, 'Courier New', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
/* Code blocks - critical for ASCII art preservation */
|
||||||
pre {
|
pre {
|
||||||
background: #f4f4f4;
|
background: #f5f5f5;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow-x: auto;
|
overflow-x: visible;
|
||||||
|
overflow-y: visible;
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
font-family: Consolas, Monaco, 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
pre code {
|
pre code {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
blockquote {
|
blockquote {
|
||||||
border-left: 4px solid #ddd;
|
border-left: 4px solid #ddd;
|
||||||
@@ -1503,6 +1866,16 @@ function exportToPDFElectron(outputFile) {
|
|||||||
}
|
}
|
||||||
@media print {
|
@media print {
|
||||||
body { padding: 20px; }
|
body { padding: 20px; }
|
||||||
|
pre {
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
overflow: visible;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -1995,9 +2368,13 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
|
|||||||
// Add PDF-specific options with header/footer support
|
// Add PDF-specific options with header/footer support
|
||||||
if (format === 'pdf') {
|
if (format === 'pdf') {
|
||||||
const pdfEngine = options.pdfEngine || 'xelatex';
|
const pdfEngine = options.pdfEngine || 'xelatex';
|
||||||
pandocCmd += ` --pdf-engine="${pdfEngine}"`;
|
pandocCmd += ` --pdf-engine=${pdfEngine}`;
|
||||||
if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`;
|
if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`;
|
||||||
|
|
||||||
|
// Add monospace font settings for code blocks (ASCII art preservation)
|
||||||
|
pandocCmd += ' -V monofont="Consolas"';
|
||||||
|
pandocCmd += ' --highlight-style=tango';
|
||||||
|
|
||||||
// Add header/footer if enabled
|
// Add header/footer if enabled
|
||||||
if (headerFooterSettings.enabled) {
|
if (headerFooterSettings.enabled) {
|
||||||
const filename = path.basename(inputFile, path.extname(inputFile));
|
const filename = path.basename(inputFile, path.extname(inputFile));
|
||||||
@@ -2205,6 +2582,25 @@ function buildPandocCommand(content, format, outputPath) {
|
|||||||
case 'pdf':
|
case 'pdf':
|
||||||
command += ' --pdf-engine=xelatex --variable geometry:margin=1in';
|
command += ' --pdf-engine=xelatex --variable geometry:margin=1in';
|
||||||
|
|
||||||
|
// Add page size and orientation
|
||||||
|
const pageSize = PAGE_SIZES[pageSettings.size];
|
||||||
|
if (pageSize) {
|
||||||
|
command += ` -V geometry:papersize=${pageSize.pandoc}`;
|
||||||
|
} else if (pageSettings.customWidth && pageSettings.customHeight) {
|
||||||
|
// Custom page size
|
||||||
|
command += ` -V geometry:paperwidth=${pageSettings.customWidth}`;
|
||||||
|
command += ` -V geometry:paperheight=${pageSettings.customHeight}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add orientation
|
||||||
|
if (pageSettings.orientation === 'landscape') {
|
||||||
|
command += ' -V geometry:landscape';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add monospace font settings for code blocks (ASCII art preservation)
|
||||||
|
command += ' -V monofont="Consolas"';
|
||||||
|
command += ' --highlight-style=tango';
|
||||||
|
|
||||||
// Add header/footer if enabled
|
// Add header/footer if enabled
|
||||||
if (headerFooterSettings.enabled) {
|
if (headerFooterSettings.enabled) {
|
||||||
// Process dynamic fields
|
// Process dynamic fields
|
||||||
@@ -2289,6 +2685,12 @@ app.whenReady().then(() => {
|
|||||||
headerFooterSettings = savedHFSettings;
|
headerFooterSettings = savedHFSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load page size settings
|
||||||
|
const savedPageSettings = store.get('pageSettings', null);
|
||||||
|
if (savedPageSettings) {
|
||||||
|
pageSettings = savedPageSettings;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for command line conversion requests
|
// Check for command line conversion requests
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
if (args.length >= 2 && (args[0] === '--convert' || args[0] === '--convert-to')) {
|
if (args.length >= 2 && (args[0] === '--convert' || args[0] === '--convert-to')) {
|
||||||
|
|||||||
+1144
-14
File diff suppressed because it is too large
Load Diff
+380
-9
@@ -10,6 +10,10 @@ body {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -156,7 +160,53 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pane:first-child {
|
.pane:first-child {
|
||||||
border-right: 1px solid #ddd;
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pane Resizer */
|
||||||
|
.pane-resizer {
|
||||||
|
width: 6px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
cursor: col-resize;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-resizer:hover {
|
||||||
|
background: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-resizer:active {
|
||||||
|
background: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preview Header */
|
||||||
|
.preview-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #f6f8fa;
|
||||||
|
border-bottom: 1px solid #dfe2e5;
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-popout-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #d0d7de;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #57606a;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-popout-btn:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-color: #8c959f;
|
||||||
|
color: #24292f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-textarea {
|
.editor-textarea {
|
||||||
@@ -207,10 +257,15 @@ body {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#preview {
|
/* GitHub Light Theme for Preview (Default) */
|
||||||
|
#preview, [id^="preview-"], .preview-content {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #24292f;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Status Bar */
|
/* Status Bar */
|
||||||
@@ -251,22 +306,25 @@ body {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
#preview code, .preview-content code {
|
#preview code, .preview-content code, [id^="preview-"] code {
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
background-color: rgba(27,31,35,0.05);
|
background-color: #f6f8fa;
|
||||||
border-radius: 3px;
|
border: 1px solid #d0d7de;
|
||||||
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
border-radius: 6px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
||||||
|
color: #24292f;
|
||||||
}
|
}
|
||||||
|
|
||||||
#preview pre, .preview-content pre {
|
#preview pre, .preview-content pre, [id^="preview-"] pre {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
background-color: #f6f8fa;
|
background-color: #f6f8fa;
|
||||||
border-radius: 3px;
|
border: 1px solid #d0d7de;
|
||||||
|
border-radius: 6px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,6 +347,126 @@ body {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* GitHub Dark Theme for Preview */
|
||||||
|
body.theme-dark #preview,
|
||||||
|
body.theme-dark [id^="preview-"],
|
||||||
|
body.theme-dark .preview-content {
|
||||||
|
background: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview h1,
|
||||||
|
body.theme-dark [id^="preview-"] h1,
|
||||||
|
body.theme-dark .preview-content h1 {
|
||||||
|
color: #c9d1d9;
|
||||||
|
border-bottom-color: #21262d;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview h2,
|
||||||
|
body.theme-dark [id^="preview-"] h2,
|
||||||
|
body.theme-dark .preview-content h2 {
|
||||||
|
color: #c9d1d9;
|
||||||
|
border-bottom-color: #21262d;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview h3,
|
||||||
|
body.theme-dark #preview h4,
|
||||||
|
body.theme-dark #preview h5,
|
||||||
|
body.theme-dark #preview h6,
|
||||||
|
body.theme-dark [id^="preview-"] h3,
|
||||||
|
body.theme-dark [id^="preview-"] h4,
|
||||||
|
body.theme-dark [id^="preview-"] h5,
|
||||||
|
body.theme-dark [id^="preview-"] h6,
|
||||||
|
body.theme-dark .preview-content h3,
|
||||||
|
body.theme-dark .preview-content h4,
|
||||||
|
body.theme-dark .preview-content h5,
|
||||||
|
body.theme-dark .preview-content h6 {
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview p,
|
||||||
|
body.theme-dark [id^="preview-"] p,
|
||||||
|
body.theme-dark .preview-content p {
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview code,
|
||||||
|
body.theme-dark [id^="preview-"] code,
|
||||||
|
body.theme-dark .preview-content code {
|
||||||
|
background-color: #161b22;
|
||||||
|
border-color: #30363d;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview pre,
|
||||||
|
body.theme-dark [id^="preview-"] pre,
|
||||||
|
body.theme-dark .preview-content pre {
|
||||||
|
background-color: #161b22;
|
||||||
|
border-color: #30363d;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview blockquote,
|
||||||
|
body.theme-dark [id^="preview-"] blockquote,
|
||||||
|
body.theme-dark .preview-content blockquote {
|
||||||
|
color: #8b949e;
|
||||||
|
border-left-color: #3b434b;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview a,
|
||||||
|
body.theme-dark [id^="preview-"] a,
|
||||||
|
body.theme-dark .preview-content a {
|
||||||
|
color: #58a6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview a:hover,
|
||||||
|
body.theme-dark [id^="preview-"] a:hover,
|
||||||
|
body.theme-dark .preview-content a:hover {
|
||||||
|
color: #79c0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview table,
|
||||||
|
body.theme-dark [id^="preview-"] table,
|
||||||
|
body.theme-dark .preview-content table {
|
||||||
|
border-color: #30363d;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview table th,
|
||||||
|
body.theme-dark [id^="preview-"] table th,
|
||||||
|
body.theme-dark .preview-content table th {
|
||||||
|
background-color: #161b22;
|
||||||
|
color: #c9d1d9;
|
||||||
|
border-color: #30363d;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview table td,
|
||||||
|
body.theme-dark [id^="preview-"] table td,
|
||||||
|
body.theme-dark .preview-content table td {
|
||||||
|
border-color: #30363d;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview hr,
|
||||||
|
body.theme-dark [id^="preview-"] hr,
|
||||||
|
body.theme-dark .preview-content hr {
|
||||||
|
background-color: #21262d;
|
||||||
|
border-color: #21262d;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview ul,
|
||||||
|
body.theme-dark #preview ol,
|
||||||
|
body.theme-dark [id^="preview-"] ul,
|
||||||
|
body.theme-dark [id^="preview-"] ol,
|
||||||
|
body.theme-dark .preview-content ul,
|
||||||
|
body.theme-dark .preview-content ol {
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #preview li,
|
||||||
|
body.theme-dark [id^="preview-"] li,
|
||||||
|
body.theme-dark .preview-content li {
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
#preview ul, #preview ol, .preview-content ul, .preview-content ol {
|
#preview ul, #preview ol, .preview-content ul, .preview-content ol {
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
@@ -326,10 +504,11 @@ body {
|
|||||||
|
|
||||||
#preview table th, .preview-content table th {
|
#preview table th, .preview-content table th {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
background-color: #f6f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
#preview table tr:nth-child(2n), .preview-content table tr:nth-child(2n) {
|
#preview table tr:nth-child(2n), .preview-content table tr:nth-child(2n) {
|
||||||
background-color: #f6f8fa;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Theme: Dark */
|
/* Theme: Dark */
|
||||||
@@ -1516,6 +1695,42 @@ body.theme-github .line-numbers {
|
|||||||
color: #586069;
|
color: #586069;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Export Profiles Styles */
|
||||||
|
.export-profiles {
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-controls select {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-controls button {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-controls button:hover {
|
||||||
|
background: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
/* Advanced Export Toggle Styles */
|
/* Advanced Export Toggle Styles */
|
||||||
.export-mode-toggle {
|
.export-mode-toggle {
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
@@ -3312,6 +3527,22 @@ body.printing-no-styles .preview-content pre,
|
|||||||
background: var(--button-hover, #5a6268);
|
background: var(--button-hover, #5a6268);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.browse-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--accent-color, #007bff);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browse-btn:hover {
|
||||||
|
background: var(--accent-hover, #0056b3);
|
||||||
|
}
|
||||||
|
|
||||||
.clear-btn {
|
.clear-btn {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: var(--danger-color, #dc3545);
|
background: var(--danger-color, #dc3545);
|
||||||
@@ -3430,3 +3661,143 @@ body[data-theme="dark"] .field-option:hover {
|
|||||||
background: #0d6efd;
|
background: #0d6efd;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
/* Mermaid Diagram Styles */
|
||||||
|
.mermaid {
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid svg {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .mermaid {
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-solarized .mermaid {
|
||||||
|
background: #fdf6e3;
|
||||||
|
border-color: #eee8d5;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-monokai .mermaid {
|
||||||
|
background: #2f2f2f;
|
||||||
|
border-color: #49483e;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-github .mermaid {
|
||||||
|
background: #f6f8fa;
|
||||||
|
border-color: #e1e4e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Command Palette Styles */
|
||||||
|
.command-palette {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 100px;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-content {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 600px;
|
||||||
|
max-width: 90%;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#command-search {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-item {
|
||||||
|
padding: 12px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-item:hover,
|
||||||
|
.command-item.selected {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-name {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-shortcut {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-footer {
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme support for command palette */
|
||||||
|
body.theme-dark .command-palette-content {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark #command-search {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #f0f0f0;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .command-item:hover,
|
||||||
|
body.theme-dark .command-item.selected {
|
||||||
|
background: #3d3d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .command-shortcut {
|
||||||
|
background: #3d3d3d;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .command-palette-footer {
|
||||||
|
background: #252525;
|
||||||
|
border-color: #444;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|||||||
+146
-35
@@ -10,9 +10,10 @@ const PizZip = require('pizzip');
|
|||||||
const Docx = require('docx4js').default;
|
const Docx = require('docx4js').default;
|
||||||
|
|
||||||
class WordTemplateExporter {
|
class WordTemplateExporter {
|
||||||
constructor(templatePath, startPage = 3) {
|
constructor(templatePath, startPage = 3, pageSettings = null) {
|
||||||
this.templatePath = templatePath || path.join(__dirname, '../word_template.docx');
|
this.templatePath = templatePath || path.join(__dirname, '../word_template.docx');
|
||||||
this.startPage = startPage; // Which page to start inserting content
|
this.startPage = startPage; // Which page to start inserting content
|
||||||
|
this.pageSettings = pageSettings; // Page size and orientation settings
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,7 +26,12 @@ class WordTemplateExporter {
|
|||||||
const zip = new PizZip(templateBuffer);
|
const zip = new PizZip(templateBuffer);
|
||||||
|
|
||||||
// Extract document.xml
|
// Extract document.xml
|
||||||
const documentXml = zip.file('word/document.xml').asText();
|
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
|
// Parse markdown and generate Word XML
|
||||||
const newContentXml = this.markdownToWordXml(markdownContent);
|
const newContentXml = this.markdownToWordXml(markdownContent);
|
||||||
@@ -47,6 +53,56 @@ class WordTemplateExporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* Insert markdown content after the specified page in the document
|
||||||
* @param {string} documentXml - The document XML
|
* @param {string} documentXml - The document XML
|
||||||
@@ -253,23 +309,42 @@ class WordTemplateExporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create code block XML
|
* 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) {
|
createCodeBlockXml(code) {
|
||||||
const escapedCode = this.escapeXml(code);
|
const lines = code.split('\n');
|
||||||
|
let xml = '';
|
||||||
|
|
||||||
return `<w:p>
|
lines.forEach((line, index) => {
|
||||||
<w:pPr>
|
const escapedLine = this.escapeXml(line);
|
||||||
<w:pStyle w:val="Code"/>
|
|
||||||
</w:pPr>
|
// Each line gets its own paragraph with exact spacing and no wrapping
|
||||||
<w:r>
|
// Shading (shd) is at paragraph level so background extends full width
|
||||||
<w:rPr>
|
xml += `<w:p>
|
||||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas"/>
|
<w:pPr>
|
||||||
<w:sz w:val="18"/>
|
<w:pStyle w:val="Normal"/>
|
||||||
</w:rPr>
|
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
|
||||||
<w:t xml:space="preserve">${escapedCode}</w:t>
|
<w:spacing w:before="${index === 0 ? '120' : '0'}" w:after="${index === lines.length - 1 ? '120' : '0'}" w:line="240" w:lineRule="exact"/>
|
||||||
</w:r>
|
<w:ind w:left="284" w:right="284"/>
|
||||||
</w:p>`;
|
<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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -287,6 +362,7 @@ class WordTemplateExporter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create table XML from markdown table lines with template styling
|
* Create table XML from markdown table lines with template styling
|
||||||
|
* Uses full-width tables with equal column distribution matching template style
|
||||||
*/
|
*/
|
||||||
createTableXml(tableLines) {
|
createTableXml(tableLines) {
|
||||||
// Parse table
|
// Parse table
|
||||||
@@ -308,13 +384,19 @@ class WordTemplateExporter {
|
|||||||
// Calculate number of columns
|
// Calculate number of columns
|
||||||
const numCols = Math.max(...rows.map(row => row.length));
|
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
|
// Build table XML
|
||||||
let tableXml = '<w:tbl>';
|
let tableXml = '<w:tbl>';
|
||||||
|
|
||||||
// Table properties - use template table style
|
// Table properties - use template table style with full width
|
||||||
tableXml += `<w:tblPr>
|
tableXml += `<w:tblPr>
|
||||||
<w:tblStyle w:val="TableGrid"/>
|
<w:tblStyle w:val="TableGrid"/>
|
||||||
<w:tblW w:w="5000" w:type="pct"/>
|
<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: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:tblBorders>
|
||||||
<w:top w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
<w:top w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||||
@@ -326,20 +408,22 @@ class WordTemplateExporter {
|
|||||||
</w:tblBorders>
|
</w:tblBorders>
|
||||||
</w:tblPr>`;
|
</w:tblPr>`;
|
||||||
|
|
||||||
// Table grid
|
// Table grid with explicit column widths
|
||||||
tableXml += '<w:tblGrid>';
|
tableXml += '<w:tblGrid>';
|
||||||
for (let i = 0; i < numCols; i++) {
|
for (let i = 0; i < numCols; i++) {
|
||||||
tableXml += '<w:gridCol/>';
|
tableXml += `<w:gridCol w:w="${colWidth}"/>`;
|
||||||
}
|
}
|
||||||
tableXml += '</w:tblGrid>';
|
tableXml += '</w:tblGrid>';
|
||||||
|
|
||||||
// Table rows
|
// Table rows
|
||||||
rows.forEach((rowCells, rowIndex) => {
|
rows.forEach((rowCells, rowIndex) => {
|
||||||
const isHeader = rowIndex === 0;
|
const isHeader = rowIndex === 0;
|
||||||
const isEvenRow = rowIndex % 2 === 0;
|
|
||||||
|
|
||||||
tableXml += '<w:tr>';
|
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
|
// Pad row to have same number of columns
|
||||||
while (rowCells.length < numCols) {
|
while (rowCells.length < numCols) {
|
||||||
rowCells.push('');
|
rowCells.push('');
|
||||||
@@ -349,9 +433,14 @@ class WordTemplateExporter {
|
|||||||
tableXml += '<w:tc>';
|
tableXml += '<w:tc>';
|
||||||
tableXml += '<w:tcPr>';
|
tableXml += '<w:tcPr>';
|
||||||
|
|
||||||
|
// Cell width
|
||||||
|
tableXml += `<w:tcW w:w="${colWidth}" w:type="dxa"/>`;
|
||||||
|
|
||||||
// Cell shading - orange for header, white for data rows
|
// Cell shading - orange for header, white for data rows
|
||||||
if (isHeader) {
|
if (isHeader) {
|
||||||
tableXml += '<w:shd w:val="clear" w:color="FFFFFF" w:fill="F58220"/>';
|
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
|
// Cell borders
|
||||||
@@ -361,12 +450,21 @@ class WordTemplateExporter {
|
|||||||
'<w:bottom 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:right w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
||||||
'</w:tcBorders>';
|
'</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>';
|
tableXml += '</w:tcPr>';
|
||||||
|
|
||||||
// Cell content
|
// Cell content
|
||||||
tableXml += '<w:p>';
|
tableXml += '<w:p>';
|
||||||
tableXml += '<w:pPr>';
|
tableXml += '<w:pPr>';
|
||||||
tableXml += '<w:spacing w:before="100" w:after="100"/>';
|
tableXml += '<w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/>';
|
||||||
tableXml += '</w:pPr>';
|
tableXml += '</w:pPr>';
|
||||||
|
|
||||||
const runs = this.parseInlineFormatting(cellText);
|
const runs = this.parseInlineFormatting(cellText);
|
||||||
@@ -388,6 +486,9 @@ class WordTemplateExporter {
|
|||||||
|
|
||||||
tableXml += '</w:tbl>';
|
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;
|
return tableXml;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,15 +498,22 @@ class WordTemplateExporter {
|
|||||||
isAsciiArt(line) {
|
isAsciiArt(line) {
|
||||||
// Don't treat markdown tables as ASCII art
|
// Don't treat markdown tables as ASCII art
|
||||||
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
|
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
|
||||||
return false;
|
// 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
|
// Common ASCII art characters (Unicode box drawing and symbols)
|
||||||
const asciiArtChars = [
|
const asciiArtChars = [
|
||||||
'─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', // Box drawing
|
'─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', // Box drawing
|
||||||
'═', '║', '╔', '╗', '╚', '╝', '╠', '╣', '╦', '╩', '╬', // Double box
|
'═', '║', '╔', '╗', '╚', '╝', '╠', '╣', '╦', '╩', '╬', // Double box
|
||||||
'▲', '▼', '◄', '►', '♦', '●', '○', '■', '□', // Shapes
|
'╭', '╮', '╯', '╰', // Rounded corners
|
||||||
'↓', '→', '←', '↑', // Arrows
|
'▲', '▼', '◄', '►', '♦', '●', '○', '■', '□', '◆', '◇', // Shapes
|
||||||
|
'↓', '→', '←', '↑', '↔', '↕', '⇒', '⇐', '⇓', '⇑', // Arrows
|
||||||
|
'┃', '━', '┏', '┓', '┗', '┛', '┣', '┫', '┳', '┻', '╋', // Heavy box
|
||||||
|
'░', '▒', '▓', '█', // Shading
|
||||||
];
|
];
|
||||||
|
|
||||||
// Check for box drawing characters
|
// Check for box drawing characters
|
||||||
@@ -415,14 +523,18 @@ class WordTemplateExporter {
|
|||||||
|
|
||||||
// Check for ASCII box patterns with regular characters
|
// Check for ASCII box patterns with regular characters
|
||||||
const asciiPatterns = [
|
const asciiPatterns = [
|
||||||
/^\s*\+[-=_]+\+/, // +-----+
|
/^\s*\+[-=_+]+\+/, // +-----+ or +=====+
|
||||||
/^\s*\|[-=_]{3,}\|/, // |-----|
|
/^\s*\|[-=_]{3,}\|/, // |-----|
|
||||||
/^\s*[-=_]{5,}$/, // -----
|
/^\s*[-=_]{5,}$/, // -----
|
||||||
|
/^\s*\+[-]+\+[-]+\+/, // +---+---+
|
||||||
/^\s*\([A-Z][a-z]+\)\s*\|\|/, // (Deformable) ||
|
/^\s*\([A-Z][a-z]+\)\s*\|\|/, // (Deformable) ||
|
||||||
/^\s*\|\s+[A-Z\s]+[-:]\s+[A-Z]/, // | TILE ADHESIVE - TYPE
|
/^\s*\|\s+[A-Z\s]+[-:]\s+[A-Z]/, // | TILE ADHESIVE - TYPE
|
||||||
/^\s*\|\s*\[[^\]]+\]/, // | [BIS Mark / CE Mark]
|
/^\s*\|\s*\[[^\]]+\]/, // | [BIS Mark / CE Mark]
|
||||||
/^\s*\|\s{2,}\w+.*\|\|/, // || text ||
|
/^\s*\|\s{2,}\w+.*\|\|/, // || text ||
|
||||||
/^\s*\[[^\]]+\]/, // [Step in brackets]
|
/^\s*\[[^\]]+\]\s*$/, // [Step in brackets]
|
||||||
|
/^\s*<[-=]+>/, // <----> or <====>
|
||||||
|
/^\s*\/[-_\\\/]+\//, // /----/
|
||||||
|
/^\s*\*[-=\*]+\*/, // *----*
|
||||||
];
|
];
|
||||||
|
|
||||||
return asciiPatterns.some(pattern => pattern.test(line));
|
return asciiPatterns.some(pattern => pattern.test(line));
|
||||||
@@ -430,6 +542,7 @@ class WordTemplateExporter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create ASCII art XML with monospace font
|
* Create ASCII art XML with monospace font
|
||||||
|
* Background shading applied at paragraph level to extend full width
|
||||||
*/
|
*/
|
||||||
createAsciiArtXml(asciiContent) {
|
createAsciiArtXml(asciiContent) {
|
||||||
// Split ASCII art into individual lines and create a paragraph for each
|
// Split ASCII art into individual lines and create a paragraph for each
|
||||||
@@ -437,10 +550,13 @@ class WordTemplateExporter {
|
|||||||
let xml = '';
|
let xml = '';
|
||||||
|
|
||||||
lines.forEach((line, index) => {
|
lines.forEach((line, index) => {
|
||||||
|
// Shading (shd) is at paragraph level so background extends full width
|
||||||
xml += `<w:p>
|
xml += `<w:p>
|
||||||
<w:pPr>
|
<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: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="0" w:right="0"/>
|
<w:ind w:left="284" w:right="284"/>
|
||||||
<w:jc w:val="left"/>
|
<w:jc w:val="left"/>
|
||||||
<w:keepLines/>
|
<w:keepLines/>
|
||||||
<w:wordWrap w:val="0"/>
|
<w:wordWrap w:val="0"/>
|
||||||
@@ -453,7 +569,6 @@ class WordTemplateExporter {
|
|||||||
if (hasArrow) {
|
if (hasArrow) {
|
||||||
// Split line into parts and color arrows red
|
// Split line into parts and color arrows red
|
||||||
let remainingLine = line;
|
let remainingLine = line;
|
||||||
let processedText = '';
|
|
||||||
|
|
||||||
while (remainingLine.length > 0) {
|
while (remainingLine.length > 0) {
|
||||||
let foundArrow = false;
|
let foundArrow = false;
|
||||||
@@ -479,7 +594,6 @@ class WordTemplateExporter {
|
|||||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||||
<w:sz w:val="16"/>
|
<w:sz w:val="16"/>
|
||||||
<w:szCs w:val="16"/>
|
<w:szCs w:val="16"/>
|
||||||
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
|
|
||||||
<w:noProof/>
|
<w:noProof/>
|
||||||
</w:rPr>
|
</w:rPr>
|
||||||
<w:t xml:space="preserve">${beforeArrow}</w:t>
|
<w:t xml:space="preserve">${beforeArrow}</w:t>
|
||||||
@@ -494,7 +608,6 @@ class WordTemplateExporter {
|
|||||||
<w:sz w:val="16"/>
|
<w:sz w:val="16"/>
|
||||||
<w:szCs w:val="16"/>
|
<w:szCs w:val="16"/>
|
||||||
<w:color w:val="FF0000"/>
|
<w:color w:val="FF0000"/>
|
||||||
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
|
|
||||||
<w:noProof/>
|
<w:noProof/>
|
||||||
</w:rPr>
|
</w:rPr>
|
||||||
<w:t xml:space="preserve">${escapedArrow}</w:t>
|
<w:t xml:space="preserve">${escapedArrow}</w:t>
|
||||||
@@ -510,7 +623,6 @@ class WordTemplateExporter {
|
|||||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||||
<w:sz w:val="16"/>
|
<w:sz w:val="16"/>
|
||||||
<w:szCs w:val="16"/>
|
<w:szCs w:val="16"/>
|
||||||
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
|
|
||||||
<w:noProof/>
|
<w:noProof/>
|
||||||
</w:rPr>
|
</w:rPr>
|
||||||
<w:t xml:space="preserve">${escapedRemaining}</w:t>
|
<w:t xml:space="preserve">${escapedRemaining}</w:t>
|
||||||
@@ -526,7 +638,6 @@ class WordTemplateExporter {
|
|||||||
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||||
<w:sz w:val="16"/>
|
<w:sz w:val="16"/>
|
||||||
<w:szCs w:val="16"/>
|
<w:szCs w:val="16"/>
|
||||||
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
|
|
||||||
<w:noProof/>
|
<w:noProof/>
|
||||||
</w:rPr>
|
</w:rPr>
|
||||||
<w:t xml:space="preserve">${escapedLine}</w:t>
|
<w:t xml:space="preserve">${escapedLine}</w:t>
|
||||||
|
|||||||
Reference in New Issue
Block a user