mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-03 02:11:07 +05:30
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a11a9af75 | ||
|
|
8b52635032 | ||
|
|
7e11176290 | ||
|
|
4d5e552741 | ||
|
|
58314efc7a |
@@ -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.7.9
|
**Current Version**: v1.9.0
|
||||||
**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,105 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
|
|||||||
|
|
||||||
## Feature Implementation Guide
|
## Feature Implementation Guide
|
||||||
|
|
||||||
### v1.7.9 Enhanced Word Export with Template Support (Latest)
|
### v1.9.0 Custom Headers & Footers (Latest)
|
||||||
|
|
||||||
|
#### 📄 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
|
#### 📝 Template-Based Word Document Generation
|
||||||
**Added Enhanced DOCX Export** (`src/wordTemplateExporter.js`, `src/main.js:540-596`, `src/main.js:242`)
|
**Added Enhanced DOCX Export** (`src/wordTemplateExporter.js`, `src/main.js:540-596`, `src/main.js:242`)
|
||||||
@@ -1009,14 +1107,33 @@ if (!gotTheLock) {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated**: October 27, 2025
|
## Pending Tasks & Future Enhancements
|
||||||
**Claude Assistant**: Development completed for v1.7.9 with Template-Based Word Export:
|
|
||||||
- **Template Support**: Word XML manipulation using PizZip to preserve template branding and styles
|
|
||||||
- **Template Selection**: File menu option to choose custom Word templates, persisted across sessions
|
|
||||||
- **Table Styling**: Orange headers (#F58220) with white bold text, white data rows, orange borders
|
|
||||||
- **ASCII Art & Flowcharts**: Perfect monospace alignment with no-wrap properties, each line as separate paragraph
|
|
||||||
- **Red Arrows**: Flowchart arrows (↓→←↑) rendered in red color for enhanced visibility
|
|
||||||
- **Markdown Numbering Strip**: Removes markdown numbering from headings and lists to use template numbering
|
|
||||||
- **Template Page Preservation**: Keeps first 2 pages (cover + TOC) intact, inserts content after 2nd section break
|
|
||||||
|
|
||||||
Previous releases: v1.7.8 (file association & print preview fixes), v1.7.7 (print menu with two options), v1.7.6 (table header cleanup), v1.7.5 (single-instance lock).
|
No pending tasks at this time. All planned v1.9.0 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**: October 28, 2025
|
||||||
|
**Claude Assistant**: Development completed for v1.9.0 with Custom Headers & Footers!
|
||||||
|
|
||||||
|
### v1.9.0 - Latest Release Summary
|
||||||
|
- **Custom Headers & Footers**: Professional headers/footers for PDF, DOCX, ODT, and PowerPoint exports
|
||||||
|
- **Dynamic Field Support**: $PAGE$, $TOTAL$, $DATE$, $TIME$, $TITLE$, $AUTHOR$, $FILENAME$
|
||||||
|
- **Logo Upload UI**: Ready for image embedding in headers/footers
|
||||||
|
- **Three-Column Layout**: Left/Center/Right positioning for headers and footers
|
||||||
|
- **Batch Support**: Full header/footer integration in batch converter
|
||||||
|
- **Persistent Settings**: Configurations saved across sessions
|
||||||
|
- **Dark Theme Support**: Complete styling for all 22 themes
|
||||||
|
|
||||||
|
### Previous Releases
|
||||||
|
- 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
|
||||||
@@ -138,6 +138,8 @@ npm run dist:all
|
|||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
|
|
||||||
|
- **v1.8.0** - Added enhanced Word export with template support to batch converter
|
||||||
|
- **v1.7.9** - Enhanced Word Export with Template Support: Template-based DOCX generation with custom templates, table styling, ASCII art support, and red-colored flowchart arrows
|
||||||
- **v1.5.6** - (Your release notes here)
|
- **v1.5.6** - (Your release notes here)
|
||||||
- **v1.5.5** - Refactored PDF export, simplified Pandoc pathing, and removed XLSX dependency.
|
- **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.1** - Bug fixes: Fixed file associations for double-clicking .md files, corrected 50/50 layout alignment for editor/preview panes
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pan-converter",
|
"name": "pan-converter",
|
||||||
"version": "1.7.9",
|
"version": "1.9.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": {
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
|
"core-util-is": "^1.0.3",
|
||||||
"docx": "^9.5.1",
|
"docx": "^9.5.1",
|
||||||
"docx4js": "^3.3.0",
|
"docx4js": "^3.3.0",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
|
|||||||
+119
-2
@@ -515,7 +515,9 @@
|
|||||||
<select id="batch-format">
|
<select id="batch-format">
|
||||||
<option value="html">HTML</option>
|
<option value="html">HTML</option>
|
||||||
<option value="pdf">PDF</option>
|
<option value="pdf">PDF</option>
|
||||||
|
<option value="pdf-enhanced">PDF (Enhanced)</option>
|
||||||
<option value="docx">DOCX</option>
|
<option value="docx">DOCX</option>
|
||||||
|
<option value="docx-enhanced">DOCX (Enhanced)</option>
|
||||||
<option value="latex">LaTeX</option>
|
<option value="latex">LaTeX</option>
|
||||||
<option value="rtf">RTF</option>
|
<option value="rtf">RTF</option>
|
||||||
<option value="odt">ODT</option>
|
<option value="odt">ODT</option>
|
||||||
@@ -555,12 +557,12 @@
|
|||||||
|
|
||||||
<!-- PDF Editor Dialog -->
|
<!-- PDF Editor Dialog -->
|
||||||
<div id="pdf-editor-dialog" class="export-dialog hidden">
|
<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">
|
<div class="export-dialog-header">
|
||||||
<h3 id="pdf-editor-title">PDF Editor</h3>
|
<h3 id="pdf-editor-title">PDF Editor</h3>
|
||||||
<button id="pdf-editor-dialog-close" title="Close">×</button>
|
<button id="pdf-editor-dialog-close" title="Close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="export-dialog-body">
|
<div class="export-dialog-body pdf-editor-body">
|
||||||
<!-- Merge PDFs Section -->
|
<!-- Merge PDFs Section -->
|
||||||
<div id="pdf-merge-section" class="pdf-operation-section hidden">
|
<div id="pdf-merge-section" class="pdf-operation-section hidden">
|
||||||
<div class="export-section">
|
<div class="export-section">
|
||||||
@@ -924,6 +926,121 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
<input type="file" id="header-logo" accept="image/*">
|
||||||
|
<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>
|
||||||
|
<input type="file" id="footer-logo" accept="image/*">
|
||||||
|
<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="editor-container">
|
||||||
<div class="tab-content active" id="tab-content-1" data-tab-id="1">
|
<div class="tab-content active" id="tab-content-1" data-tab-id="1">
|
||||||
<div id="editor-pane-1" class="pane">
|
<div id="editor-pane-1" class="pane">
|
||||||
|
|||||||
+639
-10
@@ -49,8 +49,26 @@ let mainWindow;
|
|||||||
let currentFile = null; // This will now represent the active tab's file
|
let currentFile = null; // This will now represent the active tab's file
|
||||||
let pandocAvailable = null; // Cache pandoc availability check
|
let pandocAvailable = null; // Cache pandoc availability check
|
||||||
let wordTemplatePath = null; // Path to selected Word template
|
let wordTemplatePath = null; // Path to selected Word template
|
||||||
|
let templateStartPage = 3; // Which page to start inserting content (default: page 3)
|
||||||
let rendererReady = false; // Track if renderer is ready to receive file data
|
let rendererReady = false; // Track if renderer is ready to receive file data
|
||||||
|
|
||||||
|
// Header & Footer Settings
|
||||||
|
let headerFooterSettings = {
|
||||||
|
enabled: true,
|
||||||
|
header: {
|
||||||
|
left: '',
|
||||||
|
center: '',
|
||||||
|
right: '',
|
||||||
|
logo: null // Will store image file path
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
left: '',
|
||||||
|
center: '$PAGE$ of $TOTAL$',
|
||||||
|
right: '',
|
||||||
|
logo: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 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
|
||||||
@@ -238,6 +256,7 @@ function createMenu() {
|
|||||||
submenu: [
|
submenu: [
|
||||||
{ label: 'HTML', click: () => exportFile('html') },
|
{ label: 'HTML', click: () => exportFile('html') },
|
||||||
{ label: 'PDF', click: () => exportFile('pdf') },
|
{ label: 'PDF', click: () => exportFile('pdf') },
|
||||||
|
{ label: 'PDF (Enhanced)', click: () => exportPDFViaWordTemplate(), accelerator: 'Ctrl+Shift+P' },
|
||||||
{ label: 'DOCX', click: () => exportFile('docx') },
|
{ label: 'DOCX', click: () => exportFile('docx') },
|
||||||
{ label: 'DOCX (Enhanced)', click: () => exportWordWithTemplate(), accelerator: 'Ctrl+Shift+W' },
|
{ label: 'DOCX (Enhanced)', click: () => exportWordWithTemplate(), accelerator: 'Ctrl+Shift+W' },
|
||||||
{ label: 'LaTeX', click: () => exportFile('latex') },
|
{ label: 'LaTeX', click: () => exportFile('latex') },
|
||||||
@@ -256,6 +275,18 @@ function createMenu() {
|
|||||||
label: 'Select Word Template...',
|
label: 'Select Word Template...',
|
||||||
click: selectWordTemplate
|
click: selectWordTemplate
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Template Settings...',
|
||||||
|
click: showTemplateSettings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Header & Footer Settings...',
|
||||||
|
click: () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('open-header-footer-dialog');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Quit',
|
label: 'Quit',
|
||||||
@@ -475,7 +506,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.7.9\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Modern glassmorphism UI with gradient backgrounds\n• Comprehensive PDF Editor (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• 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: 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',
|
||||||
buttons: ['OK']
|
buttons: ['OK']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -562,6 +593,265 @@ async function selectWordTemplate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Template Settings Dialog
|
||||||
|
async function showTemplateSettings() {
|
||||||
|
const result = await dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'question',
|
||||||
|
title: 'Template Settings',
|
||||||
|
message: 'Configure Word Template Export',
|
||||||
|
detail: `Current template: ${wordTemplatePath ? path.basename(wordTemplatePath) : 'Default template'}\nContent starts from page: ${templateStartPage}\n\nWhich page should content start from?\n(Templates usually have cover pages, TOC, etc.)`,
|
||||||
|
buttons: ['Page 1', 'Page 2', 'Page 3', 'Page 4', 'Page 5', 'Custom...', 'Cancel'],
|
||||||
|
defaultId: templateStartPage - 1,
|
||||||
|
cancelId: 6
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.response === 6) return; // Cancel
|
||||||
|
|
||||||
|
let newStartPage;
|
||||||
|
if (result.response === 5) { // Custom
|
||||||
|
// Show input dialog for custom page number
|
||||||
|
mainWindow.webContents.send('show-custom-start-page-dialog', templateStartPage);
|
||||||
|
} else {
|
||||||
|
newStartPage = result.response + 1; // Convert button index to page number
|
||||||
|
templateStartPage = newStartPage;
|
||||||
|
store.set('templateStartPage', templateStartPage);
|
||||||
|
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'info',
|
||||||
|
title: 'Settings Updated',
|
||||||
|
message: 'Template settings have been updated',
|
||||||
|
detail: `Content will now start from page ${templateStartPage}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle custom start page input from renderer
|
||||||
|
ipcMain.on('set-custom-start-page', (event, pageNumber) => {
|
||||||
|
const page = parseInt(pageNumber);
|
||||||
|
if (page >= 1 && page <= 100) {
|
||||||
|
templateStartPage = page;
|
||||||
|
store.set('templateStartPage', templateStartPage);
|
||||||
|
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'info',
|
||||||
|
title: 'Settings Updated',
|
||||||
|
message: 'Template settings have been updated',
|
||||||
|
detail: `Content will now start from page ${templateStartPage}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dialog.showErrorBox('Invalid Page Number', 'Please enter a page number between 1 and 100');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Header & Footer Settings IPC Handlers
|
||||||
|
|
||||||
|
// Get current header/footer settings
|
||||||
|
ipcMain.on('get-header-footer-settings', (event) => {
|
||||||
|
event.reply('header-footer-settings-data', headerFooterSettings);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save header/footer settings
|
||||||
|
ipcMain.on('save-header-footer-settings', (event, settings) => {
|
||||||
|
headerFooterSettings = settings;
|
||||||
|
store.set('headerFooterSettings', headerFooterSettings);
|
||||||
|
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'info',
|
||||||
|
title: 'Settings Saved',
|
||||||
|
message: 'Header and footer settings have been saved successfully!',
|
||||||
|
buttons: ['OK']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save header/footer logo image
|
||||||
|
ipcMain.on('save-header-footer-logo', async (event, { position, filePath }) => {
|
||||||
|
try {
|
||||||
|
// 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) {
|
||||||
|
dialog.showErrorBox('Logo Error', `Failed to save logo: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear header/footer logo
|
||||||
|
ipcMain.on('clear-header-footer-logo', (event, position) => {
|
||||||
|
if (position === 'header') {
|
||||||
|
headerFooterSettings.header.logo = null;
|
||||||
|
} else if (position === 'footer') {
|
||||||
|
headerFooterSettings.footer.logo = null;
|
||||||
|
}
|
||||||
|
event.reply('header-footer-logo-cleared', position);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to process dynamic fields in header/footer text
|
||||||
|
function processDynamicFields(text, metadata = {}) {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const dateStr = now.toLocaleDateString();
|
||||||
|
const timeStr = now.toLocaleTimeString();
|
||||||
|
|
||||||
|
let result = text;
|
||||||
|
result = result.replace(/\$DATE\$/g, dateStr);
|
||||||
|
result = result.replace(/\$TIME\$/g, timeStr);
|
||||||
|
result = result.replace(/\$TITLE\$/g, metadata.title || 'Untitled');
|
||||||
|
result = result.replace(/\$AUTHOR\$/g, metadata.author || '');
|
||||||
|
result = result.replace(/\$FILENAME\$/g, metadata.filename || '');
|
||||||
|
|
||||||
|
// Note: $PAGE$ and $TOTAL$ are handled by Pandoc/export tools
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add headers/footers to DOCX file using PizZip and docx4js
|
||||||
|
async function addHeaderFooterToDocx(docxPath, metadata = {}) {
|
||||||
|
if (!headerFooterSettings.enabled) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const PizZip = require('pizzip');
|
||||||
|
|
||||||
|
// Read the DOCX file
|
||||||
|
const docxBuffer = fs.readFileSync(docxPath);
|
||||||
|
const zip = new PizZip(docxBuffer);
|
||||||
|
|
||||||
|
// Process dynamic fields
|
||||||
|
const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata);
|
||||||
|
const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata);
|
||||||
|
const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata);
|
||||||
|
const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata);
|
||||||
|
const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata);
|
||||||
|
const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata);
|
||||||
|
|
||||||
|
// Create header XML
|
||||||
|
if (headerLeft || headerCenter || headerRight) {
|
||||||
|
const headerXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||||||
|
<w:p>
|
||||||
|
<w:pPr><w:jc w:val="left"/></w:pPr>
|
||||||
|
<w:r><w:t>${headerLeft || ''}</w:t></w:r>
|
||||||
|
</w:p>
|
||||||
|
<w:p>
|
||||||
|
<w:pPr><w:jc w:val="center"/></w:pPr>
|
||||||
|
<w:r><w:t>${headerCenter || ''}</w:t></w:r>
|
||||||
|
</w:p>
|
||||||
|
<w:p>
|
||||||
|
<w:pPr><w:jc w:val="right"/></w:pPr>
|
||||||
|
<w:r><w:t>${headerRight || ''}</w:t></w:r>
|
||||||
|
</w:p>
|
||||||
|
</w:hdr>`;
|
||||||
|
zip.file('word/header1.xml', headerXml);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create footer XML with page numbers
|
||||||
|
if (footerLeft || footerCenter || footerRight) {
|
||||||
|
let footerCenterXml = '';
|
||||||
|
if (footerCenter) {
|
||||||
|
// Handle $PAGE$ and $TOTAL$ in footer
|
||||||
|
if (footerCenter.includes('$PAGE$') || footerCenter.includes('$TOTAL$')) {
|
||||||
|
const parts = footerCenter.split(/(\$PAGE\$|\$TOTAL\$)/);
|
||||||
|
footerCenterXml = parts.map(part => {
|
||||||
|
if (part === '$PAGE$') {
|
||||||
|
return '<w:fldSimple w:instr="PAGE"/>';
|
||||||
|
} else if (part === '$TOTAL$') {
|
||||||
|
return '<w:fldSimple w:instr="NUMPAGES"/>';
|
||||||
|
} else {
|
||||||
|
return `<w:r><w:t>${part}</w:t></w:r>`;
|
||||||
|
}
|
||||||
|
}).join('');
|
||||||
|
} else {
|
||||||
|
footerCenterXml = `<w:r><w:t>${footerCenter}</w:t></w:r>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const footerXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||||||
|
<w:p>
|
||||||
|
<w:pPr><w:jc w:val="left"/></w:pPr>
|
||||||
|
<w:r><w:t>${footerLeft || ''}</w:t></w:r>
|
||||||
|
</w:p>
|
||||||
|
<w:p>
|
||||||
|
<w:pPr><w:jc w:val="center"/></w:pPr>
|
||||||
|
${footerCenterXml}
|
||||||
|
</w:p>
|
||||||
|
<w:p>
|
||||||
|
<w:pPr><w:jc w:val="right"/></w:pPr>
|
||||||
|
<w:r><w:t>${footerRight || ''}</w:t></w:r>
|
||||||
|
</w:p>
|
||||||
|
</w:ftr>`;
|
||||||
|
zip.file('word/footer1.xml', footerXml);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update document.xml.rels to reference header/footer
|
||||||
|
let relsXml = zip.file('word/_rels/document.xml.rels').asText();
|
||||||
|
|
||||||
|
// Add header relationship if not exists
|
||||||
|
if ((headerLeft || headerCenter || headerRight) && !relsXml.includes('header1.xml')) {
|
||||||
|
const headerId = 'rId100';
|
||||||
|
const headerRel = `<Relationship Id="${headerId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="header1.xml"/>`;
|
||||||
|
relsXml = relsXml.replace('</Relationships>', headerRel + '</Relationships>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add footer relationship if not exists
|
||||||
|
if ((footerLeft || footerCenter || footerRight) && !relsXml.includes('footer1.xml')) {
|
||||||
|
const footerId = 'rId101';
|
||||||
|
const footerRel = `<Relationship Id="${footerId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer1.xml"/>`;
|
||||||
|
relsXml = relsXml.replace('</Relationships>', footerRel + '</Relationships>');
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.file('word/_rels/document.xml.rels', relsXml);
|
||||||
|
|
||||||
|
// Update document.xml to use header/footer in sections
|
||||||
|
let documentXml = zip.file('word/document.xml').asText();
|
||||||
|
if ((headerLeft || headerCenter || headerRight || footerLeft || footerCenter || footerRight)) {
|
||||||
|
// Find all section properties and add header/footer references
|
||||||
|
const sectPrRegex = /<w:sectPr[^>]*>[\s\S]*?<\/w:sectPr>/g;
|
||||||
|
documentXml = documentXml.replace(sectPrRegex, (match) => {
|
||||||
|
let updated = match;
|
||||||
|
if ((headerLeft || headerCenter || headerRight) && !match.includes('headerReference')) {
|
||||||
|
updated = updated.replace('</w:sectPr>', '<w:headerReference w:type="default" r:id="rId100"/></w:sectPr>');
|
||||||
|
}
|
||||||
|
if ((footerLeft || footerCenter || footerRight) && !match.includes('footerReference')) {
|
||||||
|
updated = updated.replace('</w:sectPr>', '<w:footerReference w:type="default" r:id="rId101"/></w:sectPr>');
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.file('word/document.xml', documentXml);
|
||||||
|
|
||||||
|
// Write modified DOCX
|
||||||
|
const newDocxBuffer = zip.generate({ type: 'nodebuffer' });
|
||||||
|
fs.writeFileSync(docxPath, newDocxBuffer);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add headers/footers to DOCX:', error);
|
||||||
|
// Don't fail the export, just log the error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Enhanced Word Export with Template Support
|
// Enhanced Word Export with Template Support
|
||||||
async function exportWordWithTemplate() {
|
async function exportWordWithTemplate() {
|
||||||
if (!currentFile) {
|
if (!currentFile) {
|
||||||
@@ -582,8 +872,8 @@ async function exportWordWithTemplate() {
|
|||||||
|
|
||||||
if (result.canceled) return;
|
if (result.canceled) return;
|
||||||
|
|
||||||
// Create exporter instance with selected template (if any)
|
// Create exporter instance with selected template and start page
|
||||||
const exporter = new WordTemplateExporter(wordTemplatePath);
|
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
|
||||||
|
|
||||||
// Convert markdown to DOCX
|
// Convert markdown to DOCX
|
||||||
await exporter.convert(content, result.filePath);
|
await exporter.convert(content, result.filePath);
|
||||||
@@ -600,6 +890,79 @@ async function exportWordWithTemplate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced PDF Export via Word Template
|
||||||
|
async function exportPDFViaWordTemplate() {
|
||||||
|
if (!currentFile) {
|
||||||
|
dialog.showErrorBox('Error', 'Please save the file first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get markdown content
|
||||||
|
const content = fs.readFileSync(currentFile, 'utf-8');
|
||||||
|
|
||||||
|
// Show dialog for output file
|
||||||
|
const result = await dialog.showSaveDialog(mainWindow, {
|
||||||
|
title: 'Export to PDF (Enhanced)',
|
||||||
|
defaultPath: currentFile.replace(/\.md$/, '.pdf'),
|
||||||
|
filters: [{ name: 'PDF Document', extensions: ['pdf'] }]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.canceled) return;
|
||||||
|
|
||||||
|
// Step 1: Create temporary DOCX file using Word template
|
||||||
|
const tempDocxPath = result.filePath.replace(/\.pdf$/, '_temp.docx');
|
||||||
|
|
||||||
|
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
|
||||||
|
await exporter.convert(content, tempDocxPath);
|
||||||
|
|
||||||
|
// Step 2: Convert DOCX to PDF using LibreOffice
|
||||||
|
const soffice = process.platform === 'win32'
|
||||||
|
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
|
||||||
|
: 'soffice';
|
||||||
|
|
||||||
|
const outputDir = path.dirname(result.filePath);
|
||||||
|
const convertCmd = `${soffice} --headless --convert-to pdf --outdir "${outputDir}" "${tempDocxPath}"`;
|
||||||
|
|
||||||
|
exec(convertCmd, (error, stdout, stderr) => {
|
||||||
|
// Clean up temporary DOCX file
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(tempDocxPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to delete temp file:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
dialog.showErrorBox('PDF Conversion Error',
|
||||||
|
`Failed to convert to PDF. Please ensure LibreOffice is installed.\n\nError: ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LibreOffice creates file with same base name as input
|
||||||
|
const generatedPdfPath = tempDocxPath.replace(/\.docx$/, '.pdf');
|
||||||
|
|
||||||
|
// Rename if needed
|
||||||
|
if (generatedPdfPath !== result.filePath) {
|
||||||
|
try {
|
||||||
|
fs.renameSync(generatedPdfPath, result.filePath);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to rename PDF:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'info',
|
||||||
|
title: 'Export Successful',
|
||||||
|
message: 'PDF exported successfully using Word template!',
|
||||||
|
detail: `Saved to: ${result.filePath}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
dialog.showErrorBox('Export Error', `Failed to export PDF: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Universal File Converter integration
|
// Universal File Converter integration
|
||||||
function showUniversalConverterDialog() {
|
function showUniversalConverterDialog() {
|
||||||
mainWindow.webContents.send('show-universal-converter-dialog');
|
mainWindow.webContents.send('show-universal-converter-dialog');
|
||||||
@@ -905,7 +1268,7 @@ function showExportSuccess(outputFile) {
|
|||||||
function exportWithPandoc(pandocCmd, outputFile, format) {
|
function exportWithPandoc(pandocCmd, outputFile, format) {
|
||||||
console.log(`Executing Pandoc command: ${pandocCmd}`);
|
console.log(`Executing Pandoc command: ${pandocCmd}`);
|
||||||
|
|
||||||
exec(pandocCmd, (error, stdout, stderr) => {
|
exec(pandocCmd, async (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(`Pandoc error for ${format}:`, error);
|
console.error(`Pandoc error for ${format}:`, error);
|
||||||
console.error(`Pandoc stderr:`, stderr);
|
console.error(`Pandoc stderr:`, stderr);
|
||||||
@@ -932,6 +1295,30 @@ function exportWithPandoc(pandocCmd, outputFile, format) {
|
|||||||
if (stderr) {
|
if (stderr) {
|
||||||
console.warn(`Pandoc stderr (non-fatal):`, stderr);
|
console.warn(`Pandoc stderr (non-fatal):`, stderr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add headers/footers to DOCX if enabled
|
||||||
|
if (format === 'docx' && headerFooterSettings.enabled) {
|
||||||
|
try {
|
||||||
|
const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document';
|
||||||
|
const metadata = {
|
||||||
|
filename: filename,
|
||||||
|
title: filename,
|
||||||
|
author: ''
|
||||||
|
};
|
||||||
|
await addHeaderFooterToDocx(outputFile, metadata);
|
||||||
|
console.log('Headers/footers added to DOCX');
|
||||||
|
} catch (hfError) {
|
||||||
|
console.error('Error adding headers/footers to DOCX:', hfError);
|
||||||
|
// Continue with success message even if header/footer fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add headers/footers to ODT if enabled
|
||||||
|
if (format === 'odt' && headerFooterSettings.enabled) {
|
||||||
|
// ODT format is similar to DOCX in structure, we could implement this
|
||||||
|
console.log('ODT header/footer support not yet implemented');
|
||||||
|
}
|
||||||
|
|
||||||
showExportSuccess(outputFile);
|
showExportSuccess(outputFile);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1432,7 +1819,7 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
|
|||||||
const totalCount = markdownFiles.length;
|
const totalCount = markdownFiles.length;
|
||||||
|
|
||||||
// Process each file
|
// Process each file
|
||||||
const processNextFile = (index) => {
|
const processNextFile = async (index) => {
|
||||||
if (index >= markdownFiles.length) {
|
if (index >= markdownFiles.length) {
|
||||||
// All files processed
|
// All files processed
|
||||||
dialog.showMessageBox(mainWindow, {
|
dialog.showMessageBox(mainWindow, {
|
||||||
@@ -1447,7 +1834,10 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
|
|||||||
const inputFile = markdownFiles[index];
|
const inputFile = markdownFiles[index];
|
||||||
const relativePath = path.relative(inputFolder, inputFile);
|
const relativePath = path.relative(inputFolder, inputFile);
|
||||||
const baseName = path.basename(relativePath, path.extname(relativePath));
|
const baseName = path.basename(relativePath, path.extname(relativePath));
|
||||||
const outputFile = path.join(outputFolder, relativePath.replace(/\.(md|markdown)$/i, `.${format}`));
|
let outputExtension = format;
|
||||||
|
if (format === 'docx-enhanced') outputExtension = 'docx';
|
||||||
|
if (format === 'pdf-enhanced') outputExtension = 'pdf';
|
||||||
|
const outputFile = path.join(outputFolder, relativePath.replace(/\.(md|markdown)$/i, `.${outputExtension}`));
|
||||||
|
|
||||||
// Create subdirectories in output folder if needed
|
// Create subdirectories in output folder if needed
|
||||||
const outputDir = path.dirname(outputFile);
|
const outputDir = path.dirname(outputFile);
|
||||||
@@ -1455,7 +1845,120 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
|
|||||||
fs.mkdirSync(outputDir, { recursive: true });
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build pandoc command
|
// Handle DOCX Enhanced format with WordTemplateExporter
|
||||||
|
if (format === 'docx-enhanced') {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(inputFile, 'utf-8');
|
||||||
|
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
|
||||||
|
await exporter.convert(content, outputFile);
|
||||||
|
|
||||||
|
completedCount++;
|
||||||
|
|
||||||
|
// Update progress
|
||||||
|
mainWindow.webContents.send('batch-progress', {
|
||||||
|
completed: index + 1,
|
||||||
|
total: totalCount,
|
||||||
|
currentFile: path.basename(inputFile),
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process next file
|
||||||
|
processNextFile(index + 1);
|
||||||
|
} catch (error) {
|
||||||
|
// Update progress with error
|
||||||
|
mainWindow.webContents.send('batch-progress', {
|
||||||
|
completed: index + 1,
|
||||||
|
total: totalCount,
|
||||||
|
currentFile: path.basename(inputFile),
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process next file even if this one failed
|
||||||
|
processNextFile(index + 1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle PDF Enhanced format with Word Template → PDF conversion
|
||||||
|
if (format === 'pdf-enhanced') {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(inputFile, 'utf-8');
|
||||||
|
|
||||||
|
// Step 1: Create temporary DOCX file using Word template
|
||||||
|
const tempDocxPath = outputFile.replace(/\.pdf$/, '_temp.docx');
|
||||||
|
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
|
||||||
|
await exporter.convert(content, tempDocxPath);
|
||||||
|
|
||||||
|
// Step 2: Convert DOCX to PDF using LibreOffice
|
||||||
|
const soffice = process.platform === 'win32'
|
||||||
|
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
|
||||||
|
: 'soffice';
|
||||||
|
|
||||||
|
const outputDir = path.dirname(outputFile);
|
||||||
|
const convertCmd = `${soffice} --headless --convert-to pdf --outdir "${outputDir}" "${tempDocxPath}"`;
|
||||||
|
|
||||||
|
exec(convertCmd, (error, stdout, stderr) => {
|
||||||
|
// Clean up temporary DOCX file
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(tempDocxPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to delete temp file:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// Update progress with error
|
||||||
|
mainWindow.webContents.send('batch-progress', {
|
||||||
|
completed: index + 1,
|
||||||
|
total: totalCount,
|
||||||
|
currentFile: path.basename(inputFile),
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
processNextFile(index + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LibreOffice creates file with same base name as input
|
||||||
|
const generatedPdfPath = tempDocxPath.replace(/\.docx$/, '.pdf');
|
||||||
|
|
||||||
|
// Rename if needed
|
||||||
|
if (generatedPdfPath !== outputFile) {
|
||||||
|
try {
|
||||||
|
fs.renameSync(generatedPdfPath, outputFile);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to rename PDF:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completedCount++;
|
||||||
|
|
||||||
|
// Update progress
|
||||||
|
mainWindow.webContents.send('batch-progress', {
|
||||||
|
completed: index + 1,
|
||||||
|
total: totalCount,
|
||||||
|
currentFile: path.basename(inputFile),
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process next file
|
||||||
|
processNextFile(index + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Update progress with error
|
||||||
|
mainWindow.webContents.send('batch-progress', {
|
||||||
|
completed: index + 1,
|
||||||
|
total: totalCount,
|
||||||
|
currentFile: path.basename(inputFile),
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process next file even if this one failed
|
||||||
|
processNextFile(index + 1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build pandoc command for other formats
|
||||||
let pandocCmd = `${getPandocPath()} "${inputFile}" -o "${outputFile}"`;
|
let pandocCmd = `${getPandocPath()} "${inputFile}" -o "${outputFile}"`;
|
||||||
|
|
||||||
// Add template if specified
|
// Add template if specified
|
||||||
@@ -1489,16 +1992,71 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
|
|||||||
if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`;
|
if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`;
|
||||||
if (options.csl) pandocCmd += ` --csl="${options.csl}"`;
|
if (options.csl) pandocCmd += ` --csl="${options.csl}"`;
|
||||||
|
|
||||||
// Add PDF-specific options
|
// 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 header/footer if enabled
|
||||||
|
if (headerFooterSettings.enabled) {
|
||||||
|
const filename = path.basename(inputFile, path.extname(inputFile));
|
||||||
|
const metadata = { filename, title: filename, author: '' };
|
||||||
|
|
||||||
|
const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata);
|
||||||
|
const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata);
|
||||||
|
const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata);
|
||||||
|
const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata);
|
||||||
|
const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata);
|
||||||
|
const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata);
|
||||||
|
|
||||||
|
// Create LaTeX header
|
||||||
|
const latexHeader = `
|
||||||
|
\\usepackage{fancyhdr}
|
||||||
|
\\pagestyle{fancy}
|
||||||
|
\\fancyhf{}
|
||||||
|
\\lhead{${headerLeft.replace(/\\/g, '\\\\')}}
|
||||||
|
\\chead{${headerCenter.replace(/\\/g, '\\\\')}}
|
||||||
|
\\rhead{${headerRight.replace(/\\/g, '\\\\')}}
|
||||||
|
\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}}
|
||||||
|
\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}}
|
||||||
|
\\rfoot{${footerRight.replace(/\\/g, '\\\\')}}
|
||||||
|
\\renewcommand{\\headrulewidth}{0.4pt}
|
||||||
|
\\renewcommand{\\footrulewidth}{0.4pt}
|
||||||
|
`;
|
||||||
|
const headerFile = path.join(require('os').tmpdir(), `header_batch_${Date.now()}.tex`);
|
||||||
|
fs.writeFileSync(headerFile, latexHeader, 'utf-8');
|
||||||
|
pandocCmd += ` --include-in-header="${headerFile}"`;
|
||||||
|
pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add DOCX-specific handling
|
||||||
|
if (format === 'docx') {
|
||||||
|
pandocCmd += ' -t docx';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add PowerPoint footer if enabled
|
||||||
|
if (format === 'pptx' && headerFooterSettings.enabled && headerFooterSettings.footer.center) {
|
||||||
|
const filename = path.basename(inputFile, path.extname(inputFile));
|
||||||
|
const metadata = { filename, title: filename, author: '' };
|
||||||
|
const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata);
|
||||||
|
pandocCmd += ` --variable footer="${footerText}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute conversion
|
// Execute conversion
|
||||||
exec(pandocCmd, (error, stdout, stderr) => {
|
exec(pandocCmd, async (error, stdout, stderr) => {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
|
// Add headers/footers to DOCX if enabled
|
||||||
|
if (format === 'docx' && headerFooterSettings.enabled) {
|
||||||
|
try {
|
||||||
|
const filename = path.basename(inputFile, path.extname(inputFile));
|
||||||
|
const metadata = { filename, title: filename, author: '' };
|
||||||
|
await addHeaderFooterToDocx(outputFile, metadata);
|
||||||
|
} catch (hfError) {
|
||||||
|
console.error('Batch: Error adding headers/footers to DOCX:', hfError);
|
||||||
|
}
|
||||||
|
}
|
||||||
completedCount++;
|
completedCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1635,21 +2193,85 @@ function buildPandocCommand(content, format, outputPath) {
|
|||||||
|
|
||||||
let command = `pandoc "${inputFile}" -o "${outputPath}"`;
|
let command = `pandoc "${inputFile}" -o "${outputPath}"`;
|
||||||
|
|
||||||
|
// Get metadata for dynamic fields
|
||||||
|
const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document';
|
||||||
|
const metadata = {
|
||||||
|
filename: filename,
|
||||||
|
title: filename,
|
||||||
|
author: '',
|
||||||
|
};
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case 'pdf':
|
case 'pdf':
|
||||||
command += ' --pdf-engine=xelatex --variable geometry:margin=1in';
|
command += ' --pdf-engine=xelatex --variable geometry:margin=1in';
|
||||||
|
|
||||||
|
// Add header/footer if enabled
|
||||||
|
if (headerFooterSettings.enabled) {
|
||||||
|
// Process dynamic fields
|
||||||
|
const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata);
|
||||||
|
const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata);
|
||||||
|
const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata);
|
||||||
|
const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata);
|
||||||
|
const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata);
|
||||||
|
const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata);
|
||||||
|
|
||||||
|
// Add Pandoc variables for fancyhdr package
|
||||||
|
if (headerLeft) command += ` --variable header-left="${headerLeft}"`;
|
||||||
|
if (headerCenter) command += ` --variable header-center="${headerCenter}"`;
|
||||||
|
if (headerRight) command += ` --variable header-right="${headerRight}"`;
|
||||||
|
if (footerLeft) command += ` --variable footer-left="${footerLeft}"`;
|
||||||
|
if (footerCenter) command += ` --variable footer-center="${footerCenter}"`;
|
||||||
|
if (footerRight) command += ` --variable footer-right="${footerRight}"`;
|
||||||
|
|
||||||
|
// Create custom LaTeX header with fancyhdr
|
||||||
|
const latexHeader = `
|
||||||
|
\\usepackage{fancyhdr}
|
||||||
|
\\pagestyle{fancy}
|
||||||
|
\\fancyhf{}
|
||||||
|
\\lhead{${headerLeft.replace(/\\/g, '\\\\')}}
|
||||||
|
\\chead{${headerCenter.replace(/\\/g, '\\\\')}}
|
||||||
|
\\rhead{${headerRight.replace(/\\/g, '\\\\')}}
|
||||||
|
\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}}
|
||||||
|
\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}}
|
||||||
|
\\rfoot{${footerRight.replace(/\\/g, '\\\\')}}
|
||||||
|
\\renewcommand{\\headrulewidth}{0.4pt}
|
||||||
|
\\renewcommand{\\footrulewidth}{0.4pt}
|
||||||
|
`;
|
||||||
|
const headerFile = path.join(require('os').tmpdir(), `header_${Date.now()}.tex`);
|
||||||
|
fs.writeFileSync(headerFile, latexHeader, 'utf-8');
|
||||||
|
command += ` --include-in-header="${headerFile}"`;
|
||||||
|
|
||||||
|
// Add lastpage package for $TOTAL$ support
|
||||||
|
command += ' --variable header-includes="\\\\usepackage{lastpage}"';
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'html':
|
case 'html':
|
||||||
command += ' --self-contained --css';
|
command += ' --self-contained --css';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'docx':
|
case 'docx':
|
||||||
command += ' --reference-doc';
|
command += ' --reference-doc';
|
||||||
|
|
||||||
|
// For DOCX, header/footer are handled via reference document or separate processing
|
||||||
|
// We'll add a note that DOCX headers/footers require reference doc or post-processing
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'odt':
|
||||||
|
// ODT headers/footers are handled via reference document
|
||||||
|
break;
|
||||||
|
|
||||||
case 'latex':
|
case 'latex':
|
||||||
command += ' --standalone';
|
command += ' --standalone';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'pptx':
|
case 'pptx':
|
||||||
command += ' --slide-level=2';
|
command += ' --slide-level=2';
|
||||||
|
// PowerPoint footer can be added with --variable
|
||||||
|
if (headerFooterSettings.enabled && headerFooterSettings.footer.center) {
|
||||||
|
const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata);
|
||||||
|
command += ` --variable footer="${footerText}"`;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1657,8 +2279,15 @@ function buildPandocCommand(content, format, outputPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
// Load saved Word template path
|
// Load saved Word template path and settings
|
||||||
wordTemplatePath = store.get('wordTemplatePath', null);
|
wordTemplatePath = store.get('wordTemplatePath', null);
|
||||||
|
templateStartPage = store.get('templateStartPage', 3);
|
||||||
|
|
||||||
|
// Load header/footer settings
|
||||||
|
const savedHFSettings = store.get('headerFooterSettings', null);
|
||||||
|
if (savedHFSettings) {
|
||||||
|
headerFooterSettings = savedHFSettings;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for command line conversion requests
|
// Check for command line conversion requests
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|||||||
+202
-82
@@ -1184,95 +1184,26 @@ function handlePrintPreview(withStyles) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store original display values for restoration
|
console.log('[RENDERER] Starting print with withStyles:', withStyles);
|
||||||
const elementsToHide = [];
|
console.log('[RENDERER] Preview content length:', previewContent.innerHTML.length);
|
||||||
|
|
||||||
// Hide toolbar
|
// Add body classes for print mode - let CSS handle everything
|
||||||
const toolbar = document.querySelector('.toolbar');
|
document.body.classList.add('printing');
|
||||||
if (toolbar) {
|
if (!withStyles) {
|
||||||
elementsToHide.push({ elem: toolbar, display: toolbar.style.display });
|
document.body.classList.add('printing-no-styles');
|
||||||
toolbar.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide tab bar
|
// Wait for CSS to apply then print
|
||||||
const tabBar = document.querySelector('.tab-bar');
|
|
||||||
if (tabBar) {
|
|
||||||
elementsToHide.push({ elem: tabBar, display: tabBar.style.display });
|
|
||||||
tabBar.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide status bar
|
|
||||||
const statusBar = document.querySelector('.status-bar');
|
|
||||||
if (statusBar) {
|
|
||||||
elementsToHide.push({ elem: statusBar, display: statusBar.style.display });
|
|
||||||
statusBar.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide editor pane
|
|
||||||
const editorPane = document.getElementById(`editor-pane-${activeTabId}`);
|
|
||||||
if (editorPane) {
|
|
||||||
elementsToHide.push({ elem: editorPane, display: editorPane.style.display });
|
|
||||||
editorPane.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide all dialogs
|
|
||||||
const dialogs = document.querySelectorAll('.dialog, .modal-overlay');
|
|
||||||
dialogs.forEach(dialog => {
|
|
||||||
elementsToHide.push({ elem: dialog, display: dialog.style.display });
|
|
||||||
dialog.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Make preview pane full width
|
|
||||||
const previewPane = document.getElementById(`preview-pane-${activeTabId}`);
|
|
||||||
let originalPreviewStyles = {};
|
|
||||||
if (previewPane) {
|
|
||||||
originalPreviewStyles = {
|
|
||||||
position: previewPane.style.position,
|
|
||||||
top: previewPane.style.top,
|
|
||||||
left: previewPane.style.left,
|
|
||||||
width: previewPane.style.width,
|
|
||||||
height: previewPane.style.height,
|
|
||||||
margin: previewPane.style.margin,
|
|
||||||
padding: previewPane.style.padding
|
|
||||||
};
|
|
||||||
previewPane.style.position = 'absolute';
|
|
||||||
previewPane.style.top = '0';
|
|
||||||
previewPane.style.left = '0';
|
|
||||||
previewPane.style.width = '100%';
|
|
||||||
previewPane.style.height = 'auto';
|
|
||||||
previewPane.style.margin = '0';
|
|
||||||
previewPane.style.padding = '20px';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply no-styles if needed
|
|
||||||
if (!withStyles && previewContent) {
|
|
||||||
previewContent.style.color = '#000';
|
|
||||||
previewContent.style.background = '#fff';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for DOM updates then print
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
console.log('[RENDERER] Sending do-print to main process');
|
||||||
ipcRenderer.send('do-print', { withStyles });
|
ipcRenderer.send('do-print', { withStyles });
|
||||||
|
|
||||||
// Restore everything after print dialog opens
|
// Restore classes after print dialog opens
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Restore hidden elements
|
document.body.classList.remove('printing', 'printing-no-styles');
|
||||||
elementsToHide.forEach(({ elem, display }) => {
|
console.log('[RENDERER] Print classes removed');
|
||||||
elem.style.display = display;
|
}, 1000);
|
||||||
});
|
}, 100);
|
||||||
|
|
||||||
// Restore preview pane styles
|
|
||||||
if (previewPane) {
|
|
||||||
Object.assign(previewPane.style, originalPreviewStyles);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore preview content styles
|
|
||||||
if (!withStyles && previewContent) {
|
|
||||||
previewContent.style.color = '';
|
|
||||||
previewContent.style.background = '';
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}, 200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export Dialog functionality
|
// Export Dialog functionality
|
||||||
@@ -2625,3 +2556,192 @@ function initMathSupport() {
|
|||||||
|
|
||||||
// Initialize math support on load
|
// Initialize math support on load
|
||||||
initMathSupport();
|
initMathSupport();
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// Header & Footer Dialog Management
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
let currentFieldTarget = null; // Track which input field is being edited
|
||||||
|
|
||||||
|
// Open header/footer settings dialog
|
||||||
|
function openHeaderFooterDialog() {
|
||||||
|
const dialog = document.getElementById('header-footer-dialog');
|
||||||
|
dialog.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Request current settings from main process
|
||||||
|
ipcRenderer.send('get-header-footer-settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close header/footer settings dialog
|
||||||
|
function closeHeaderFooterDialog() {
|
||||||
|
const dialog = document.getElementById('header-footer-dialog');
|
||||||
|
dialog.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open field picker dialog
|
||||||
|
function openFieldPickerDialog(targetInputId) {
|
||||||
|
currentFieldTarget = targetInputId;
|
||||||
|
const dialog = document.getElementById('field-picker-dialog');
|
||||||
|
dialog.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close field picker dialog
|
||||||
|
function closeFieldPickerDialog() {
|
||||||
|
const dialog = document.getElementById('field-picker-dialog');
|
||||||
|
dialog.classList.add('hidden');
|
||||||
|
currentFieldTarget = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load settings into dialog
|
||||||
|
ipcRenderer.on('header-footer-settings-data', (event, settings) => {
|
||||||
|
// Enable/disable checkbox
|
||||||
|
document.getElementById('hf-enabled').checked = settings.enabled;
|
||||||
|
|
||||||
|
// Header fields
|
||||||
|
document.getElementById('header-left').value = settings.header.left || '';
|
||||||
|
document.getElementById('header-center').value = settings.header.center || '';
|
||||||
|
document.getElementById('header-right').value = settings.header.right || '';
|
||||||
|
|
||||||
|
// Footer fields
|
||||||
|
document.getElementById('footer-left').value = settings.footer.left || '';
|
||||||
|
document.getElementById('footer-center').value = settings.footer.center || '';
|
||||||
|
document.getElementById('footer-right').value = settings.footer.right || '';
|
||||||
|
|
||||||
|
// Logo previews
|
||||||
|
if (settings.header.logo) {
|
||||||
|
document.getElementById('header-logo-preview').textContent = path.basename(settings.header.logo);
|
||||||
|
} else {
|
||||||
|
document.getElementById('header-logo-preview').textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.footer.logo) {
|
||||||
|
document.getElementById('footer-logo-preview').textContent = path.basename(settings.footer.logo);
|
||||||
|
} else {
|
||||||
|
document.getElementById('footer-logo-preview').textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update config content visibility
|
||||||
|
toggleConfigContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle config content based on enabled checkbox
|
||||||
|
function toggleConfigContent() {
|
||||||
|
const enabled = document.getElementById('hf-enabled').checked;
|
||||||
|
const configContent = document.getElementById('hf-config-content');
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
configContent.classList.remove('disabled');
|
||||||
|
} else {
|
||||||
|
configContent.classList.add('disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save header/footer settings
|
||||||
|
function saveHeaderFooterSettings() {
|
||||||
|
const settings = {
|
||||||
|
enabled: document.getElementById('hf-enabled').checked,
|
||||||
|
header: {
|
||||||
|
left: document.getElementById('header-left').value || '',
|
||||||
|
center: document.getElementById('header-center').value || '',
|
||||||
|
right: document.getElementById('header-right').value || '',
|
||||||
|
logo: null // Logo paths are managed separately
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
left: document.getElementById('footer-left').value || '',
|
||||||
|
center: document.getElementById('footer-center').value || '',
|
||||||
|
right: document.getElementById('footer-right').value || '',
|
||||||
|
logo: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ipcRenderer.send('save-header-footer-settings', settings);
|
||||||
|
closeHeaderFooterDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle logo file selection
|
||||||
|
function handleLogoSelection(position) {
|
||||||
|
const input = document.getElementById(`${position}-logo`);
|
||||||
|
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
const filePath = input.files[0].path;
|
||||||
|
|
||||||
|
// Send to main process to save
|
||||||
|
ipcRenderer.send('save-header-footer-logo', { position, filePath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle logo saved confirmation
|
||||||
|
ipcRenderer.on('header-footer-logo-saved', (event, { position, path }) => {
|
||||||
|
document.getElementById(`${position}-logo-preview`).textContent = path.split(/[\\/]/).pop();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear logo
|
||||||
|
function clearLogo(position) {
|
||||||
|
ipcRenderer.send('clear-header-footer-logo', position);
|
||||||
|
document.getElementById(`${position}-logo`).value = '';
|
||||||
|
document.getElementById(`${position}-logo-preview`).textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle logo cleared confirmation
|
||||||
|
ipcRenderer.on('header-footer-logo-cleared', (event, position) => {
|
||||||
|
console.log(`${position} logo cleared`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert dynamic field into input
|
||||||
|
function insertDynamicField(field) {
|
||||||
|
if (currentFieldTarget) {
|
||||||
|
const input = document.getElementById(currentFieldTarget);
|
||||||
|
const cursorPos = input.selectionStart;
|
||||||
|
const textBefore = input.value.substring(0, cursorPos);
|
||||||
|
const textAfter = input.value.substring(cursorPos);
|
||||||
|
|
||||||
|
input.value = textBefore + field + textAfter;
|
||||||
|
input.focus();
|
||||||
|
input.setSelectionRange(cursorPos + field.length, cursorPos + field.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeFieldPickerDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Listeners for Header/Footer Dialog
|
||||||
|
|
||||||
|
// Close buttons
|
||||||
|
document.getElementById('header-footer-close').addEventListener('click', closeHeaderFooterDialog);
|
||||||
|
document.getElementById('header-footer-cancel').addEventListener('click', closeHeaderFooterDialog);
|
||||||
|
document.getElementById('header-footer-save').addEventListener('click', saveHeaderFooterSettings);
|
||||||
|
|
||||||
|
// Enable/disable checkbox
|
||||||
|
document.getElementById('hf-enabled').addEventListener('change', toggleConfigContent);
|
||||||
|
|
||||||
|
// Field insert buttons
|
||||||
|
document.querySelectorAll('.field-insert-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const target = btn.getAttribute('data-target');
|
||||||
|
openFieldPickerDialog(target);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logo file inputs
|
||||||
|
document.getElementById('header-logo').addEventListener('change', () => handleLogoSelection('header'));
|
||||||
|
document.getElementById('footer-logo').addEventListener('change', () => handleLogoSelection('footer'));
|
||||||
|
|
||||||
|
// Logo clear buttons
|
||||||
|
document.getElementById('header-logo-clear').addEventListener('click', () => clearLogo('header'));
|
||||||
|
document.getElementById('footer-logo-clear').addEventListener('click', () => clearLogo('footer'));
|
||||||
|
|
||||||
|
// Field picker dialog
|
||||||
|
document.getElementById('field-picker-close').addEventListener('click', closeFieldPickerDialog);
|
||||||
|
document.querySelectorAll('.field-option').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const field = btn.getAttribute('data-field');
|
||||||
|
insertDynamicField(field);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export function to make openHeaderFooterDialog accessible globally
|
||||||
|
window.openHeaderFooterDialog = openHeaderFooterDialog;
|
||||||
|
|
||||||
|
// Listen for menu command to open dialog
|
||||||
|
ipcRenderer.on('open-header-footer-dialog', () => {
|
||||||
|
openHeaderFooterDialog();
|
||||||
|
});
|
||||||
+411
-2
@@ -1098,6 +1098,117 @@ body.theme-dark #add-metadata-field {
|
|||||||
color: #f0f0f0;
|
color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* PDF Editor Specific Styles */
|
||||||
|
.pdf-editor-content {
|
||||||
|
max-width: 600px !important;
|
||||||
|
max-height: 85vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-editor-body {
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: calc(85vh - 150px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-operation-section {
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-operation-section.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make PDF Editor sections more compact */
|
||||||
|
.pdf-operation-section .export-section {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-operation-section .export-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File list styling for merge operation */
|
||||||
|
.file-list {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
min-height: 100px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-entry {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-entry:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-file {
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-file:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme support for PDF Editor */
|
||||||
|
body.theme-dark .file-list {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .file-entry {
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-color: #444;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .pdf-editor-content {
|
||||||
|
background: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-dark .pdf-editor-body {
|
||||||
|
background: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
body.theme-dark .form-row button:hover,
|
body.theme-dark .form-row button:hover,
|
||||||
body.theme-dark #add-metadata-field:hover {
|
body.theme-dark #add-metadata-field:hover {
|
||||||
background: #333;
|
background: #333;
|
||||||
@@ -2834,18 +2945,33 @@ body.theme-concrete-warm .status-bar {
|
|||||||
.editor-container,
|
.editor-container,
|
||||||
#status-bar,
|
#status-bar,
|
||||||
.status-bar,
|
.status-bar,
|
||||||
|
.toolbar,
|
||||||
|
.tab-bar,
|
||||||
.editor-pane,
|
.editor-pane,
|
||||||
[id^="editor-pane-"] {
|
[id^="editor-pane-"],
|
||||||
|
.dialog,
|
||||||
|
.modal-overlay,
|
||||||
|
.export-dialog {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure body and html take full width */
|
||||||
|
html, body {
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Show preview in full width */
|
/* Show preview in full width */
|
||||||
#preview,
|
#preview,
|
||||||
.preview,
|
.preview,
|
||||||
.preview-content,
|
.preview-content,
|
||||||
[id^="preview-"],
|
[id^="preview-"],
|
||||||
[id^="preview-pane-"],
|
[id^="preview-pane-"],
|
||||||
.tab-content {
|
.tab-content,
|
||||||
|
.pane {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
@@ -2854,6 +2980,10 @@ body.theme-concrete-warm .status-bar {
|
|||||||
padding: 20px !important;
|
padding: 20px !important;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
position: static !important;
|
position: static !important;
|
||||||
|
left: auto !important;
|
||||||
|
right: auto !important;
|
||||||
|
top: auto !important;
|
||||||
|
bottom: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Optimize preview content for printing */
|
/* Optimize preview content for printing */
|
||||||
@@ -2865,6 +2995,13 @@ body.theme-concrete-warm .status-bar {
|
|||||||
background: white !important;
|
background: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* No-styles mode for printing without theme colors */
|
||||||
|
body.printing-no-styles .preview-content,
|
||||||
|
body.printing-no-styles [id^="preview-"] {
|
||||||
|
color: #000 !important;
|
||||||
|
background: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Text formatting */
|
/* Text formatting */
|
||||||
.preview-content h1,
|
.preview-content h1,
|
||||||
.preview-content h2,
|
.preview-content h2,
|
||||||
@@ -3021,3 +3158,275 @@ body.printing-no-styles .preview-content pre,
|
|||||||
color: #333 !important;
|
color: #333 !important;
|
||||||
border-color: #999 !important;
|
border-color: #999 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
Header & Footer Dialog Styles
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
.hf-enable-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: var(--bg-secondary, #f5f5f5);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-enable-section label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-enable-section input[type="checkbox"] {
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hf-config-content {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hf-config-content.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-section {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--bg-tertiary, #fafafa);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color, #e0e0e0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-section h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: var(--text-primary, #333);
|
||||||
|
font-size: 16px;
|
||||||
|
border-bottom: 2px solid var(--accent-color, #007bff);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-column label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: var(--text-secondary, #666);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-column input[type="text"] {
|
||||||
|
padding: 8px 35px 8px 10px;
|
||||||
|
border: 1px solid var(--border-color, #ccc);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: var(--input-bg, white);
|
||||||
|
color: var(--text-primary, #333);
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-column input[type="text"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color, #007bff);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-insert-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--accent-color, #007bff);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-insert-btn:hover {
|
||||||
|
background: var(--accent-hover, #0056b3);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-insert-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-logo-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
background: var(--bg-secondary, #f5f5f5);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-logo-row label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary, #666);
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-logo-row input[type="file"] {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid var(--border-color, #ccc);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--input-bg, white);
|
||||||
|
color: var(--text-primary, #333);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-logo-row input[type="file"]::-webkit-file-upload-button {
|
||||||
|
padding: 5px 12px;
|
||||||
|
background: var(--button-bg, #6c757d);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-logo-row input[type="file"]::-webkit-file-upload-button:hover {
|
||||||
|
background: var(--button-hover, #5a6268);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--danger-color, #dc3545);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn:hover {
|
||||||
|
background: var(--danger-hover, #c82333);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-preview {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary, #666);
|
||||||
|
max-width: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-help {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: var(--info-bg, #e7f3ff);
|
||||||
|
border-left: 4px solid var(--info-color, #007bff);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-help p {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary, #333);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-help ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-help li {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: var(--text-secondary, #555);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-help code {
|
||||||
|
background: rgba(0, 123, 255, 0.1);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: var(--accent-color, #007bff);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Field Picker Dialog */
|
||||||
|
.field-picker-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-option {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--bg-secondary, #f5f5f5);
|
||||||
|
border: 1px solid var(--border-color, #ddd);
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary, #333);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-option:hover {
|
||||||
|
background: var(--accent-color, #007bff);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent-color, #007bff);
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Theme Support for Header/Footer Dialog */
|
||||||
|
body[data-theme="dark"] .hf-enable-section,
|
||||||
|
body[data-theme="dark"] .hf-logo-row {
|
||||||
|
background: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="dark"] .hf-section {
|
||||||
|
background: #252525;
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="dark"] .hf-column input[type="text"],
|
||||||
|
body[data-theme="dark"] .hf-logo-row input[type="file"] {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="dark"] .hf-help {
|
||||||
|
background: rgba(0, 123, 255, 0.15);
|
||||||
|
border-left-color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="dark"] .field-option {
|
||||||
|
background: #2d2d2d;
|
||||||
|
border-color: #404040;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="dark"] .field-option:hover {
|
||||||
|
background: #0d6efd;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
+24
-10
@@ -10,8 +10,9 @@ const PizZip = require('pizzip');
|
|||||||
const Docx = require('docx4js').default;
|
const Docx = require('docx4js').default;
|
||||||
|
|
||||||
class WordTemplateExporter {
|
class WordTemplateExporter {
|
||||||
constructor(templatePath) {
|
constructor(templatePath, startPage = 3) {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,8 +30,8 @@ class WordTemplateExporter {
|
|||||||
// Parse markdown and generate Word XML
|
// Parse markdown and generate Word XML
|
||||||
const newContentXml = this.markdownToWordXml(markdownContent);
|
const newContentXml = this.markdownToWordXml(markdownContent);
|
||||||
|
|
||||||
// Insert new content after page 2 (after the section break)
|
// Insert new content after the specified start page
|
||||||
const modifiedXml = this.insertContentAfterPage2(documentXml, newContentXml);
|
const modifiedXml = this.insertContentAfterPage(documentXml, newContentXml, this.startPage);
|
||||||
|
|
||||||
// Update the zip with modified XML
|
// Update the zip with modified XML
|
||||||
zip.file('word/document.xml', modifiedXml);
|
zip.file('word/document.xml', modifiedXml);
|
||||||
@@ -47,20 +48,33 @@ class WordTemplateExporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert markdown content after page 2 in the document
|
* 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)
|
||||||
*/
|
*/
|
||||||
insertContentAfterPage2(documentXml, newContentXml) {
|
insertContentAfterPage(documentXml, newContentXml, afterPage) {
|
||||||
// Find the last section break (after page 2)
|
// Find section breaks that mark page boundaries
|
||||||
// Look for the section properties tag that marks page breaks
|
// Look for the section properties tag that marks page breaks
|
||||||
const sectionBreakRegex = /<w:sectPr[^>]*>[\s\S]*?<\/w:sectPr>/g;
|
const sectionBreakRegex = /<w:sectPr[^>]*>[\s\S]*?<\/w:sectPr>/g;
|
||||||
const matches = [...documentXml.matchAll(sectionBreakRegex)];
|
const matches = [...documentXml.matchAll(sectionBreakRegex)];
|
||||||
|
|
||||||
if (matches.length >= 2) {
|
// Calculate which section break to insert after
|
||||||
// Insert after the 2nd section break
|
// Page 1 = before 1st section break
|
||||||
const insertPoint = matches[1].index + matches[1][0].length;
|
// 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);
|
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 {
|
} else {
|
||||||
// If no section breaks found, insert before closing body tag
|
// If not enough section breaks, insert before closing body tag
|
||||||
return documentXml.replace('</w:body>', newContentXml + '</w:body>');
|
return documentXml.replace('</w:body>', newContentXml + '</w:body>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user