Compare commits

..
7 Commits
Author SHA1 Message Date
amitwhandClaude d8e0748037 Release v1.9.2: Full-width background for code blocks
Fixed code block and ASCII art background to extend full paragraph width:

🔧 Background Fix:
- Moved shading (w:shd) from run level (<w:rPr>) to paragraph level (<w:pPr>)
- Background now extends full width like in the preview
- Applied to both createCodeBlockXml() and createAsciiArtXml() functions

📝 Technical Changes:
- src/wordTemplateExporter.js: Paragraph-level shading for full-width backgrounds
- Added w:ind with left="284" right="284" for consistent margins
- Removed redundant w:shd from run properties

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 09:10:18 +05:30
amitwhandClaude cb98549db8 Release v1.9.1: ASCII Art & Code Block Preservation
Fixed export of ASCII art, charts, tables, and flowcharts to match preview exactly:

🎨 ASCII Art Preservation:
- Code blocks now export exactly as shown in preview
- Each line rendered separately to preserve exact spacing and alignment
- Monospace font (Consolas) used consistently across all export formats
- Line breaks, spacing, and box-drawing characters preserved perfectly

📝 Word Template Exporter Improvements:
- createCodeBlockXml: Renders each line as separate paragraph with exact spacing
- Added <w:keepLines/> and <w:wordWrap w:val="0"/> for no-wrap behavior
- Enhanced isAsciiArt detection with more Unicode box-drawing characters
- Added support for rounded corners (╭╮╯╰), heavy box chars, more arrows

📄 PDF Export Enhancements:
- Added -V monofont="Consolas" for proper code block rendering
- Added --highlight-style=tango for syntax highlighting
- Applied to main export, fallback engines, and CLI conversion

🖨️ HTML/Electron PDF Improvements:
- Enhanced CSS: white-space: pre, word-wrap: normal
- font-family: Consolas, Monaco, 'Courier New', monospace
- Print media queries preserve code block formatting
- overflow: visible prevents content truncation

🔧 Technical Changes:
- src/wordTemplateExporter.js: Enhanced code block and ASCII art rendering
- src/main.js: Added monofont settings to all PDF export paths
- Updated version to 1.9.1

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 09:01:17 +05:30
amitwh ff651e9b21 Update Claude Code permissions settings 2025-11-08 17:39:14 +05:30
amitwhandClaude e013e5fd69 Fix v1.9.0 logo upload and add MiKTeX PATH support
This commit fixes the logo upload issue and adds MiKTeX to the app's PATH for LaTeX/PDF support.

**Logo Upload Fix:**
- Replaced file input with Browse button that uses dialog.showOpenDialog
- Added browse-header-footer-logo IPC handler in main process
- Now gets reliable file paths without depending on file.path property
- Added browse-btn CSS styling

**MiKTeX PATH Addition:**
- Automatically adds MiKTeX bin directory to process.env.PATH on Windows
- Enables XeLaTeX/PDFLaTeX for PDF exports with headers/footers
- Logs PATH addition for debugging

**Files Changed:**
- src/main.js: Added MiKTeX PATH setup, browse logo IPC handler
- src/renderer.js: Changed to browseForLogo() function, updated event listeners
- src/index.html: Replaced file inputs with browse buttons
- src/styles.css: Added .browse-btn styling

**Testing Notes:**
- Logo upload now works without "path undefined" errors
- DOCX exports with headers/footers confirmed working
- PDF exports should now use Pandoc+XeLaTeX with MiKTeX in PATH
- Requires MiKTeX installation and PC restart for full functionality

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 19:33:36 +05:30
amitwhandClaude f7362bec46 Fix v1.9.0 header/footer visibility issues
This commit fixes two critical issues in the v1.9.0 header/footer feature:

1. **Logo Upload Error Fix**:
   - Fixed "path undefined" error when uploading logos
   - Added validation in main.js to check filePath before processing
   - Added fallback in renderer.js to use webUtils.getPathForFile if file.path is undefined
   - Added file existence check before copying

2. **Header/Footer Visibility Fix**:
   - Added header/footer support to tryPdfFallback function (PDF fallback engines)
   - Previously, only performExportWithOptions had header/footer logic
   - Now all PDF export paths include headers/footers when enabled
   - Fallback engines now generate fancyhdr LaTeX code for headers/footers

Technical details:
- src/main.js:667-709: Enhanced logo upload handler with error checking
- src/main.js:1289-1319: Added header/footer to tryPdfFallback function
- src/renderer.js:2661-2678: Fixed logo file path handling

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 17:41:37 +05:30
amitwhandClaude 5a11a9af75 Release v1.9.0: Custom Headers & Footers for Professional Document Exports
Comprehensive header and footer customization system for all standard export formats with dynamic field support and persistent settings.

 Major Features:
- Custom Headers & Footers for PDF, DOCX, ODT, and PowerPoint exports
- Three-column layout (Left/Center/Right) for both headers and footers
- Dynamic field support: $PAGE$, $TOTAL$, $DATE$, $TIME$, $TITLE$, $AUTHOR$, $FILENAME$
- Logo/image upload UI for header and footer branding
- Enable/disable toggle with persistent settings across sessions
- Field insertion helper dialog for quick dynamic field selection
- Complete dark theme support for all 22 themes

🎯 Export Format Support:
- PDF (Standard): LaTeX fancyhdr package with automatic page numbering
- DOCX (Standard): Word XML manipulation with PAGE and NUMPAGES fields
- ODT: OpenDocument format with Pandoc integration
- PowerPoint (PPTX): Footer text with dynamic field replacement
- Batch Converter: Full header/footer support for all formats

💡 Technical Implementation:
- processDynamicFields() function for field replacement
- LaTeX header generation with fancyhdr and lastpage packages
- Word XML manipulation using PizZip for DOCX headers/footers
- addHeaderFooterToDocx() function for post-processing DOCX exports
- buildPandocCommand() enhanced with header/footer parameters
- IPC handlers for dialog communication and settings management

🎨 User Interface:
- New "Header & Footer Settings..." menu option in File menu
- Professional configuration dialog with field previews
- Logo upload with file path display and clear buttons
- Dynamic field picker popup dialog
- Responsive grid layout for three-column design
- Smooth transitions and animations

📦 Integration Points:
- Menu system (File → Header & Footer Settings...)
- Standard export functions (PDF, DOCX, ODT, PPTX)
- Batch converter with full format support
- Settings persistence via electron-store
- Logo file management in userData directory

🔧 Code Changes:
- src/main.js: Added header/footer IPC handlers, state management, processDynamicFields(), addHeaderFooterToDocx(), enhanced buildPandocCommand() and performBatchConversion()
- src/index.html: Added header/footer dialog and field picker dialog
- src/renderer.js: Added dialog management, logo handling, field insertion
- src/styles.css: Added comprehensive dialog styling with dark theme support
- package.json: Updated version to 1.9.0
- CLAUDE.md: Documented v1.9.0 features and implementation details

📝 Note: Enhanced exports (DOCX Enhanced, PDF Enhanced) use Word templates with pre-configured headers/footers. This feature specifically targets standard (non-templated) exports.

🚀 Impact:
Users can now add professional headers and footers to all document exports with custom branding, dynamic page numbers, dates, and titles - significantly improving document presentation for business and academic use cases.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 17:34:32 +05:30
amitwh 8b52635032 Update CLAUDE.md: Add v1.9.0 header/footer feature planning
- Added comprehensive pending tasks section for v1.9.0
- Documented header & footer customization feature requirements
- Detailed implementation plan with technical specifications
- Updated current version to v1.8.3
- Added recent release summary and previous releases section
2025-10-28 17:12:40 +05:30
8 changed files with 1465 additions and 90 deletions
File diff suppressed because one or more lines are too long
+199 -12
View File
@@ -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.
**Current Version**: v1.7.9
**Current Version**: v1.9.2
**Author**: Amit Haridas (amit.wh@gmail.com)
**License**: MIT
**Repository**: https://github.com/amitwh/pan-converter
@@ -113,7 +113,175 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
## Feature Implementation Guide
### v1.7.9 Enhanced Word Export with Template Support (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
**Custom Headers & Footers for Standard Exports** (`src/main.js:701-853`, `src/index.html:929-1042`, `src/renderer.js:2560-2747`, `src/styles.css:3162-3432`)
- **New Menu Option**: File → "Header & Footer Settings..." to configure headers/footers
- **Three-Column Layout**: Left, Center, and Right positioning for both headers and footers
- **Dynamic Field Support**: $PAGE$, $TOTAL$, $DATE$, $TIME$, $TITLE$, $AUTHOR$, $FILENAME$
- **Logo/Image Embedding**: Upload logos to display in headers and footers
- **Enable/Disable Toggle**: Quick on/off switch for all headers/footers
- **Persistent Settings**: Configurations saved across sessions
**Format Support:**
- **PDF Export** (Standard): LaTeX-based headers/footers using fancyhdr package with automatic page numbering
- **DOCX Export** (Standard): Word XML manipulation with field support for PAGE and NUMPAGES
- **ODT Export**: OpenDocument format support (similar to DOCX implementation)
- **PowerPoint (PPTX)**: Footer text with dynamic fields on slides
- **Batch Converter**: Full header/footer support for all batch conversions
**Dynamic Fields Processing:**
- `$PAGE$` → Current page number (LaTeX: \thepage, Word: PAGE field)
- `$TOTAL$` → Total pages (LaTeX: \pageref{LastPage}, Word: NUMPAGES field)
- `$DATE$` → Current date (processed at export time)
- `$TIME$` → Current time (processed at export time)
- `$TITLE$` → Document title (from metadata or filename)
- `$AUTHOR$` → Document author (from metadata)
- `$FILENAME$` → File name without extension
**Technical Implementation:**
```javascript
// processDynamicFields function in main.js
function processDynamicFields(text, metadata = {}) {
const now = new Date();
result = text.replace(/\$DATE\$/g, now.toLocaleDateString());
result = result.replace(/\$TIME\$/g, now.toLocaleTimeString());
result = result.replace(/\$TITLE\$/g, metadata.title || 'Untitled');
// ... other replacements
return result;
}
// LaTeX header generation for PDF exports
const latexHeader = `
\\usepackage{fancyhdr}
\\pagestyle{fancy}
\\fancyhf{}
\\lhead{${headerLeft}}
\\chead{${headerCenter}}
\\rhead{${headerRight}}
\\lfoot{${footerLeft}}
\\cfoot{${footerCenter}} // with \\thepage for page numbers
\\rfoot{${footerRight}}
`;
// Word XML generation for DOCX exports
const footerXml = `<?xml version="1.0"?>
<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:p><w:r><w:t>${footerLeft}</w:t></w:r></w:p>
<w:p>
<w:fldSimple w:instr="PAGE"/> // Page number field
<w:r><w:t> of </w:t></w:r>
<w:fldSimple w:instr="NUMPAGES"/> // Total pages field
</w:p>
<w:p><w:r><w:t>${footerRight}</w:t></w:r></w:p>
</w:ftr>`;
```
**Dialog Features:**
- **Field Insertion Buttons**: Quick-insert dynamic fields via popup dialog
- **Logo Upload**: Browse and select images for headers/footers
- **Logo Preview**: Show selected logo filename
- **Clear Buttons**: Remove uploaded logos
- **Enable Toggle**: Disable headers/footers without losing settings
- **Responsive Layout**: Grid-based three-column design
- **Dark Theme Support**: Complete styling for all 22 themes
**Integration Points:**
- `buildPandocCommand()` - Adds LaTeX headers for PDF exports
- `exportWithPandoc()` - Post-processes DOCX with header/footer XML
- `performBatchConversion()` - Batch support for all formats
- `addHeaderFooterToDocx()` - DOCX XML manipulation using PizZip
**Dependencies:**
- **PizZip** (v3.2.0) - ZIP file manipulation for DOCX/ODT
- **fancyhdr** (LaTeX package) - Advanced headers/footers for PDF
- **lastpage** (LaTeX package) - Total page count support
**Key Improvements in v1.9.0:**
1. ✅ Professional headers/footers for all standard export formats
2. ✅ Dynamic field support with automatic value replacement
3. ✅ Logo/image embedding in document headers/footers
4. ✅ Batch converter integration with full header/footer support
5. ✅ Persistent settings across application sessions
6. ✅ User-friendly dialog with field insertion helper
7. ✅ Complete dark theme support
**Note**: Enhanced exports (DOCX Enhanced, PDF Enhanced) use Word templates which already have their own headers/footers defined in the template itself, so this feature specifically targets standard (non-templated) exports.
---
### v1.7.9 Enhanced Word Export with Template Support
#### 📝 Template-Based Word Document Generation
**Added Enhanced DOCX Export** (`src/wordTemplateExporter.js`, `src/main.js:540-596`, `src/main.js:242`)
@@ -1009,14 +1177,33 @@ if (!gotTheLock) {
---
**Last Updated**: October 27, 2025
**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
## Pending Tasks & Future Enhancements
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.1 features have been completed.
### Potential Future Enhancements
- Image embedding in headers/footers (partially implemented - logo upload UI ready)
- Advanced page numbering formats (Roman numerals, custom prefixes)
- First-page-different header/footer support
- Per-section headers/footers within documents
---
**Last Updated**: December 12, 2025
**Claude Assistant**: Development completed for v1.9.1 with ASCII Art Preservation!
### v1.9.1 - Latest Release Summary
- **ASCII Art Preservation**: Code blocks, flowcharts, tables, and ASCII art now export exactly as shown in preview
- **Enhanced Code Block Rendering**: Each line rendered separately to preserve exact spacing and alignment
- **Monospace Font Support**: Consolas font used consistently across all export formats
- **Improved ASCII Detection**: Extended Unicode box-drawing and arrow character support
- **PDF Export Enhancement**: Added monofont and highlight-style options for better code rendering
- **HTML/Electron PDF Fix**: Enhanced CSS to prevent text wrapping in code blocks
### Previous Releases
- v1.9.0: Custom Headers & Footers for PDF, DOCX, ODT, PowerPoint exports
- v1.8.3: Streamlined PDF Editor UI & Configurable Template Settings
- v1.8.2: Enhanced PDF Export & Print Fix
- v1.8.1: Streamlined PDF Editor UI
- v1.8.0: Enhanced Word Export with template support (batch)
- v1.7.9: Template-Based Word Export with ASCII art support
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pan-converter",
"version": "1.8.3",
"version": "1.9.2",
"description": "Cross-platform Markdown editor and converter using Pandoc",
"main": "src/main.js",
"scripts": {
+115
View File
@@ -926,6 +926,121 @@
</div>
</div>
<!-- Header & Footer Configuration Dialog -->
<div id="header-footer-dialog" class="export-dialog hidden">
<div class="export-dialog-content" style="max-width: 700px;">
<div class="export-dialog-header">
<h3>Header & Footer Settings</h3>
<button id="header-footer-close" class="dialog-close" title="Close">×</button>
</div>
<div class="export-dialog-body">
<div class="hf-enable-section">
<label>
<input type="checkbox" id="hf-enabled" checked>
Enable Headers and Footers
</label>
</div>
<div id="hf-config-content">
<!-- Header Configuration -->
<div class="hf-section">
<h4>Header</h4>
<div class="hf-row">
<div class="hf-column">
<label>Left</label>
<input type="text" id="header-left" placeholder="e.g., Document Title">
<button class="field-insert-btn" data-target="header-left" title="Insert dynamic field">+</button>
</div>
<div class="hf-column">
<label>Center</label>
<input type="text" id="header-center" placeholder="e.g., $TITLE$">
<button class="field-insert-btn" data-target="header-center" title="Insert dynamic field">+</button>
</div>
<div class="hf-column">
<label>Right</label>
<input type="text" id="header-right" placeholder="e.g., $DATE$">
<button class="field-insert-btn" data-target="header-right" title="Insert dynamic field">+</button>
</div>
</div>
<div class="hf-logo-row">
<label>Header Logo/Image:</label>
<button id="header-logo-browse" class="browse-btn">Browse...</button>
<button id="header-logo-clear" class="clear-btn">Clear</button>
<span id="header-logo-preview" class="logo-preview"></span>
</div>
</div>
<!-- Footer Configuration -->
<div class="hf-section">
<h4>Footer</h4>
<div class="hf-row">
<div class="hf-column">
<label>Left</label>
<input type="text" id="footer-left" placeholder="e.g., Company Name">
<button class="field-insert-btn" data-target="footer-left" title="Insert dynamic field">+</button>
</div>
<div class="hf-column">
<label>Center</label>
<input type="text" id="footer-center" placeholder="e.g., $PAGE$ of $TOTAL$">
<button class="field-insert-btn" data-target="footer-center" title="Insert dynamic field">+</button>
</div>
<div class="hf-column">
<label>Right</label>
<input type="text" id="footer-right" placeholder="e.g., $AUTHOR$">
<button class="field-insert-btn" data-target="footer-right" title="Insert dynamic field">+</button>
</div>
</div>
<div class="hf-logo-row">
<label>Footer Logo/Image:</label>
<button id="footer-logo-browse" class="browse-btn">Browse...</button>
<button id="footer-logo-clear" class="clear-btn">Clear</button>
<span id="footer-logo-preview" class="logo-preview"></span>
</div>
</div>
<!-- Dynamic Fields Help -->
<div class="hf-help">
<p><strong>Available Dynamic Fields:</strong></p>
<ul>
<li><code>$PAGE$</code> - Current page number</li>
<li><code>$TOTAL$</code> - Total page count</li>
<li><code>$DATE$</code> - Current date</li>
<li><code>$TIME$</code> - Current time</li>
<li><code>$TITLE$</code> - Document title</li>
<li><code>$AUTHOR$</code> - Document author</li>
<li><code>$FILENAME$</code> - File name</li>
</ul>
</div>
</div>
</div>
<div class="export-dialog-footer">
<button id="header-footer-cancel">Cancel</button>
<button id="header-footer-save" class="primary">Save Settings</button>
</div>
</div>
</div>
<!-- Dynamic Field Picker Dialog -->
<div id="field-picker-dialog" class="export-dialog hidden">
<div class="export-dialog-content" style="max-width: 400px;">
<div class="export-dialog-header">
<h3>Insert Dynamic Field</h3>
<button id="field-picker-close" class="dialog-close" title="Close">×</button>
</div>
<div class="export-dialog-body">
<div class="field-picker-list">
<button class="field-option" data-field="$PAGE$">Page Number</button>
<button class="field-option" data-field="$TOTAL$">Total Pages</button>
<button class="field-option" data-field="$DATE$">Current Date</button>
<button class="field-option" data-field="$TIME$">Current Time</button>
<button class="field-option" data-field="$TITLE$">Document Title</button>
<button class="field-option" data-field="$AUTHOR$">Document Author</button>
<button class="field-option" data-field="$FILENAME$">File Name</button>
</div>
</div>
</div>
</div>
<div class="editor-container">
<div class="tab-content active" id="tab-content-1" data-tab-id="1">
<div id="editor-pane-1" class="pane">
+606 -17
View File
@@ -5,6 +5,15 @@ const { exec } = require('child_process');
const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib');
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
function getPandocPath() {
// Pandoc is expected to be in the system's PATH.
@@ -52,6 +61,23 @@ 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
// 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
// When a file is double-clicked and the app is already running,
// Windows tries to start a second instance. We prevent this and
@@ -262,6 +288,14 @@ function createMenu() {
label: 'Template Settings...',
click: showTemplateSettings
},
{
label: 'Header & Footer Settings...',
click: () => {
if (mainWindow) {
mainWindow.webContents.send('open-header-footer-dialog');
}
}
},
{ type: 'separator' },
{
label: 'Quit',
@@ -481,7 +515,7 @@ function createMenu() {
type: 'info',
title: 'About PanConverter',
message: 'PanConverter',
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.8.3\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\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: 1.9.2\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\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• 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']
});
}
@@ -618,6 +652,273 @@ ipcMain.on('set-custom-start-page', (event, pageNumber) => {
}
});
// 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
// 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 }) => {
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
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 });
}
// Verify source file exists
if (!fs.existsSync(filePath)) {
dialog.showErrorBox('Logo Error', `Source file not found: ${filePath}`);
return;
}
// 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 save error:', 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
async function exportWordWithTemplate() {
if (!currentFile) {
@@ -962,6 +1263,42 @@ function performExportWithOptions(format, options) {
pandocCmd += ` --pdf-engine="${pdfEngine}"`;
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
exec(pandocCmd, (error) => {
if (error) {
@@ -975,6 +1312,15 @@ function performExportWithOptions(format, options) {
} else if (format === 'docx') {
pandocCmd += ' -t docx';
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 {
// Generic export for other formats
exportWithPandoc(pandocCmd, outputFile, format);
@@ -996,9 +1342,45 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro
const engine = engines[index];
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
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
if (options.template && options.template !== 'default') {
pandocCmd += ` --template="${options.template}"`;
@@ -1033,16 +1415,16 @@ function showExportSuccess(outputFile) {
// Helper function to export with pandoc (general)
function exportWithPandoc(pandocCmd, outputFile, format) {
console.log(`Executing Pandoc command: ${pandocCmd}`);
exec(pandocCmd, (error, stdout, stderr) => {
exec(pandocCmd, async (error, stdout, stderr) => {
if (error) {
console.error(`Pandoc error for ${format}:`, error);
console.error(`Pandoc stderr:`, stderr);
console.error(`Pandoc stdout:`, stdout);
// Provide more specific error messages
let errorMessage = `Failed to export to ${format.toUpperCase()}`;
if (error.message.includes('not found') || error.message.includes('not recognized')) {
errorMessage += '\n\nPandoc is not installed or not found in PATH.';
errorMessage += '\nPlease install Pandoc from: https://pandoc.org/installing.html';
@@ -1051,9 +1433,9 @@ function exportWithPandoc(pandocCmd, outputFile, format) {
} else {
errorMessage += `\n\nError details: ${error.message}`;
}
errorMessage += `\n\nCommand used: ${pandocCmd}`;
dialog.showErrorBox('Export Error', errorMessage);
} else {
console.log(`Successfully exported to ${format}:`, outputFile);
@@ -1061,6 +1443,30 @@ function exportWithPandoc(pandocCmd, outputFile, format) {
if (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);
}
});
@@ -1096,7 +1502,7 @@ function exportToHTML(outputFile) {
const marked = require('marked');
const markdownContent = fs.readFileSync(currentFile, 'utf8');
const htmlContent = marked.parse(markdownContent);
const fullHtml = `<!DOCTYPE html>
<html lang="en">
<head>
@@ -1116,21 +1522,37 @@ function exportToHTML(outputFile) {
margin-top: 1.5em;
margin-bottom: 0.5em;
}
/* Inline code */
code {
background: #f4f4f4;
padding: 2px 4px;
border-radius: 3px;
font-family: Consolas, Monaco, 'Courier New', monospace;
font-size: 0.9em;
}
/* Code blocks - critical for ASCII art preservation */
pre {
background: #f4f4f4;
background: #f5f5f5;
padding: 1em;
border-radius: 5px;
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 {
background: transparent;
padding: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
white-space: pre;
word-wrap: normal;
display: block;
}
blockquote {
border-left: 4px solid #ddd;
@@ -1162,6 +1584,18 @@ function exportToHTML(outputFile) {
max-width: 100%;
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>
</head>
<body>
@@ -1184,7 +1618,7 @@ function exportToPDFElectron(outputFile) {
const marked = require('marked');
const markdownContent = fs.readFileSync(currentFile, 'utf8');
const htmlContent = marked.parse(markdownContent);
const fullHtml = `<!DOCTYPE html>
<html lang="en">
<head>
@@ -1204,21 +1638,38 @@ function exportToPDFElectron(outputFile) {
margin-top: 1.5em;
margin-bottom: 0.5em;
}
/* Inline code */
code {
background: #f4f4f4;
padding: 2px 4px;
border-radius: 3px;
font-family: Consolas, Monaco, 'Courier New', monospace;
font-size: 0.9em;
}
/* Code blocks - critical for ASCII art preservation */
pre {
background: #f4f4f4;
background: #f5f5f5;
padding: 1em;
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 {
background: transparent;
padding: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
white-space: pre;
word-wrap: normal;
display: block;
}
blockquote {
border-left: 4px solid #ddd;
@@ -1245,6 +1696,16 @@ function exportToPDFElectron(outputFile) {
}
@media print {
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>
</head>
@@ -1734,16 +2195,71 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`;
if (options.csl) pandocCmd += ` --csl="${options.csl}"`;
// Add PDF-specific options
// Add PDF-specific options with header/footer support
if (format === 'pdf') {
const pdfEngine = options.pdfEngine || 'xelatex';
pandocCmd += ` --pdf-engine="${pdfEngine}"`;
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
exec(pandocCmd, (error, stdout, stderr) => {
exec(pandocCmd, async (error, stdout, stderr) => {
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++;
}
@@ -1877,27 +2393,94 @@ function performCLIConversion(inputPath, format) {
function buildPandocCommand(content, format, outputPath) {
const inputFile = path.join(require('os').tmpdir(), `panconverter_temp_${Date.now()}.md`);
fs.writeFileSync(inputFile, content, 'utf-8');
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) {
case 'pdf':
command += ' --pdf-engine=xelatex --variable geometry:margin=1in';
// Add monospace font settings for code blocks (ASCII art preservation)
command += ' -V monofont="Consolas"';
command += ' --highlight-style=tango';
// 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;
case 'html':
command += ' --self-contained --css';
break;
case 'docx':
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;
case 'odt':
// ODT headers/footers are handled via reference document
break;
case 'latex':
command += ' --standalone';
break;
case 'pptx':
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;
}
return command;
}
@@ -1906,6 +2489,12 @@ app.whenReady().then(() => {
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
const args = process.argv.slice(2);
if (args.length >= 2 && (args[0] === '--convert' || args[0] === '--convert-to')) {
+182 -1
View File
@@ -2555,4 +2555,185 @@ function initMathSupport() {
}
// 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 browsing - ask main process to show open dialog
function browseForLogo(position) {
ipcRenderer.send('browse-header-footer-logo', position);
}
// 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-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 browse buttons
document.getElementById('header-logo-browse').addEventListener('click', () => browseForLogo('header'));
document.getElementById('footer-logo-browse').addEventListener('click', () => browseForLogo('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();
});
+288
View File
@@ -3157,4 +3157,292 @@ body.printing-no-styles .preview-content pre,
.print-no-styles .preview-content blockquote {
color: #333 !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);
}
.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 {
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;
}
+55 -26
View File
@@ -253,23 +253,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) {
const escapedCode = this.escapeXml(code);
const lines = code.split('\n');
let xml = '';
return `<w:p>
<w:pPr>
<w:pStyle w:val="Code"/>
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas"/>
<w:sz w:val="18"/>
</w:rPr>
<w:t xml:space="preserve">${escapedCode}</w:t>
</w:r>
</w:p>`;
lines.forEach((line, index) => {
const escapedLine = this.escapeXml(line);
// Each line gets its own paragraph with exact spacing and no wrapping
// Shading (shd) is at paragraph level so background extends full width
xml += `<w:p>
<w:pPr>
<w:pStyle w:val="Normal"/>
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
<w:spacing w:before="${index === 0 ? '120' : '0'}" w:after="${index === lines.length - 1 ? '120' : '0'}" w:line="240" w:lineRule="exact"/>
<w:ind w:left="284" w:right="284"/>
<w:jc w:val="left"/>
<w:keepLines/>
<w:wordWrap w:val="0"/>
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
<w:sz w:val="18"/>
<w:szCs w:val="18"/>
<w:noProof/>
</w:rPr>
<w:t xml:space="preserve">${escapedLine}</w:t>
</w:r>
</w:p>`;
});
return xml;
}
/**
@@ -397,15 +416,22 @@ class WordTemplateExporter {
isAsciiArt(line) {
// Don't treat markdown tables as ASCII art
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 = [
'─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', // Box drawing
'═', '║', '╔', '╗', '╚', '╝', '╠', '╣', '╦', '╩', '╬', // Double box
'', '', '', '', '♦', '●', '○', '■', '□', // Shapes
'', '', '', '', // Arrows
'', '', '', '', // Rounded corners
'', '', '', '', '♦', '●', '○', '■', '□', '◆', '◇', // Shapes
'↓', '→', '←', '↑', '↔', '↕', '⇒', '⇐', '⇓', '⇑', // Arrows
'┃', '━', '┏', '┓', '┗', '┛', '┣', '┫', '┳', '┻', '╋', // Heavy box
'░', '▒', '▓', '█', // Shading
];
// Check for box drawing characters
@@ -415,14 +441,18 @@ class WordTemplateExporter {
// Check for ASCII box patterns with regular characters
const asciiPatterns = [
/^\s*\+[-=_]+\+/, // +-----+
/^\s*\+[-=_+]+\+/, // +-----+ or +=====+
/^\s*\|[-=_]{3,}\|/, // |-----|
/^\s*[-=_]{5,}$/, // -----
/^\s*\+[-]+\+[-]+\+/, // +---+---+
/^\s*\([A-Z][a-z]+\)\s*\|\|/, // (Deformable) ||
/^\s*\|\s+[A-Z\s]+[-:]\s+[A-Z]/, // | TILE ADHESIVE - TYPE
/^\s*\|\s*\[[^\]]+\]/, // | [BIS Mark / CE Mark]
/^\s*\|\s{2,}\w+.*\|\|/, // || text ||
/^\s*\[[^\]]+\]/, // [Step in brackets]
/^\s*\[[^\]]+\]\s*$/, // [Step in brackets]
/^\s*<[-=]+>/, // <----> or <====>
/^\s*\/[-_\\\/]+\//, // /----/
/^\s*\*[-=\*]+\*/, // *----*
];
return asciiPatterns.some(pattern => pattern.test(line));
@@ -430,6 +460,7 @@ class WordTemplateExporter {
/**
* Create ASCII art XML with monospace font
* Background shading applied at paragraph level to extend full width
*/
createAsciiArtXml(asciiContent) {
// Split ASCII art into individual lines and create a paragraph for each
@@ -437,10 +468,13 @@ class WordTemplateExporter {
let xml = '';
lines.forEach((line, index) => {
// Shading (shd) is at paragraph level so background extends full width
xml += `<w:p>
<w:pPr>
<w:pStyle w:val="Normal"/>
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
<w:spacing w:before="${index === 0 ? '120' : '0'}" w:after="${index === lines.length - 1 ? '120' : '0'}" w:line="240" w:lineRule="exact"/>
<w:ind w:left="0" w:right="0"/>
<w:ind w:left="284" w:right="284"/>
<w:jc w:val="left"/>
<w:keepLines/>
<w:wordWrap w:val="0"/>
@@ -453,7 +487,6 @@ class WordTemplateExporter {
if (hasArrow) {
// Split line into parts and color arrows red
let remainingLine = line;
let processedText = '';
while (remainingLine.length > 0) {
let foundArrow = false;
@@ -479,7 +512,6 @@ class WordTemplateExporter {
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
<w:sz w:val="16"/>
<w:szCs w:val="16"/>
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
<w:noProof/>
</w:rPr>
<w:t xml:space="preserve">${beforeArrow}</w:t>
@@ -494,7 +526,6 @@ class WordTemplateExporter {
<w:sz w:val="16"/>
<w:szCs w:val="16"/>
<w:color w:val="FF0000"/>
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
<w:noProof/>
</w:rPr>
<w:t xml:space="preserve">${escapedArrow}</w:t>
@@ -510,7 +541,6 @@ class WordTemplateExporter {
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
<w:sz w:val="16"/>
<w:szCs w:val="16"/>
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
<w:noProof/>
</w:rPr>
<w:t xml:space="preserve">${escapedRemaining}</w:t>
@@ -526,7 +556,6 @@ class WordTemplateExporter {
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
<w:sz w:val="16"/>
<w:szCs w:val="16"/>
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
<w:noProof/>
</w:rPr>
<w:t xml:space="preserve">${escapedLine}</w:t>