mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-03 02:11:07 +05:30
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8e0748037 | ||
|
|
cb98549db8 | ||
|
|
ff651e9b21 | ||
|
|
e013e5fd69 | ||
|
|
f7362bec46 | ||
|
|
5a11a9af75 | ||
|
|
8b52635032 | ||
|
|
7e11176290 | ||
|
|
4d5e552741 | ||
|
|
58314efc7a |
+19
-33
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
||||
|
||||
**PanConverter** is a cross-platform Markdown editor and converter powered by Pandoc, built with Electron. It provides professional-grade editing capabilities with comprehensive export options.
|
||||
|
||||
**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
|
||||
@@ -138,6 +138,8 @@ npm run dist:all
|
||||
|
||||
## Version History
|
||||
|
||||
- **v1.8.0** - Added enhanced Word export with template support to batch converter
|
||||
- **v1.7.9** - Enhanced Word Export with Template Support: Template-based DOCX generation with custom templates, table styling, ASCII art support, and red-colored flowchart arrows
|
||||
- **v1.5.6** - (Your release notes here)
|
||||
- **v1.5.5** - Refactored PDF export, simplified Pandoc pathing, and removed XLSX dependency.
|
||||
- **v1.3.1** - Bug fixes: Fixed file associations for double-clicking .md files, corrected 50/50 layout alignment for editor/preview panes
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pan-converter",
|
||||
"version": "1.7.9",
|
||||
"version": "1.9.2",
|
||||
"description": "Cross-platform Markdown editor and converter using Pandoc",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
@@ -31,6 +31,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"codemirror": "^6.0.2",
|
||||
"core-util-is": "^1.0.3",
|
||||
"docx": "^9.5.1",
|
||||
"docx4js": "^3.3.0",
|
||||
"dompurify": "^3.2.6",
|
||||
|
||||
+119
-2
@@ -515,7 +515,9 @@
|
||||
<select id="batch-format">
|
||||
<option value="html">HTML</option>
|
||||
<option value="pdf">PDF</option>
|
||||
<option value="pdf-enhanced">PDF (Enhanced)</option>
|
||||
<option value="docx">DOCX</option>
|
||||
<option value="docx-enhanced">DOCX (Enhanced)</option>
|
||||
<option value="latex">LaTeX</option>
|
||||
<option value="rtf">RTF</option>
|
||||
<option value="odt">ODT</option>
|
||||
@@ -555,12 +557,12 @@
|
||||
|
||||
<!-- PDF Editor Dialog -->
|
||||
<div id="pdf-editor-dialog" class="export-dialog hidden">
|
||||
<div class="export-dialog-content">
|
||||
<div class="export-dialog-content pdf-editor-content">
|
||||
<div class="export-dialog-header">
|
||||
<h3 id="pdf-editor-title">PDF Editor</h3>
|
||||
<button id="pdf-editor-dialog-close" title="Close">×</button>
|
||||
</div>
|
||||
<div class="export-dialog-body">
|
||||
<div class="export-dialog-body pdf-editor-body">
|
||||
<!-- Merge PDFs Section -->
|
||||
<div id="pdf-merge-section" class="pdf-operation-section hidden">
|
||||
<div class="export-section">
|
||||
@@ -924,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">
|
||||
|
||||
+848
-13
File diff suppressed because it is too large
Load Diff
+194
-82
@@ -1184,95 +1184,26 @@ function handlePrintPreview(withStyles) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store original display values for restoration
|
||||
const elementsToHide = [];
|
||||
console.log('[RENDERER] Starting print with withStyles:', withStyles);
|
||||
console.log('[RENDERER] Preview content length:', previewContent.innerHTML.length);
|
||||
|
||||
// Hide toolbar
|
||||
const toolbar = document.querySelector('.toolbar');
|
||||
if (toolbar) {
|
||||
elementsToHide.push({ elem: toolbar, display: toolbar.style.display });
|
||||
toolbar.style.display = 'none';
|
||||
// Add body classes for print mode - let CSS handle everything
|
||||
document.body.classList.add('printing');
|
||||
if (!withStyles) {
|
||||
document.body.classList.add('printing-no-styles');
|
||||
}
|
||||
|
||||
// Hide tab bar
|
||||
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
|
||||
// Wait for CSS to apply then print
|
||||
setTimeout(() => {
|
||||
console.log('[RENDERER] Sending do-print to main process');
|
||||
ipcRenderer.send('do-print', { withStyles });
|
||||
|
||||
// Restore everything after print dialog opens
|
||||
// Restore classes after print dialog opens
|
||||
setTimeout(() => {
|
||||
// Restore hidden elements
|
||||
elementsToHide.forEach(({ elem, display }) => {
|
||||
elem.style.display = display;
|
||||
});
|
||||
|
||||
// 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);
|
||||
document.body.classList.remove('printing', 'printing-no-styles');
|
||||
console.log('[RENDERER] Print classes removed');
|
||||
}, 1000);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Export Dialog functionality
|
||||
@@ -2625,3 +2556,184 @@ function initMathSupport() {
|
||||
|
||||
// Initialize math support on load
|
||||
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();
|
||||
});
|
||||
+427
-2
@@ -1098,6 +1098,117 @@ body.theme-dark #add-metadata-field {
|
||||
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 #add-metadata-field:hover {
|
||||
background: #333;
|
||||
@@ -2834,18 +2945,33 @@ body.theme-concrete-warm .status-bar {
|
||||
.editor-container,
|
||||
#status-bar,
|
||||
.status-bar,
|
||||
.toolbar,
|
||||
.tab-bar,
|
||||
.editor-pane,
|
||||
[id^="editor-pane-"] {
|
||||
[id^="editor-pane-"],
|
||||
.dialog,
|
||||
.modal-overlay,
|
||||
.export-dialog {
|
||||
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 */
|
||||
#preview,
|
||||
.preview,
|
||||
.preview-content,
|
||||
[id^="preview-"],
|
||||
[id^="preview-pane-"],
|
||||
.tab-content {
|
||||
.tab-content,
|
||||
.pane {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
@@ -2854,6 +2980,10 @@ body.theme-concrete-warm .status-bar {
|
||||
padding: 20px !important;
|
||||
overflow: visible !important;
|
||||
position: static !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
}
|
||||
|
||||
/* Optimize preview content for printing */
|
||||
@@ -2865,6 +2995,13 @@ body.theme-concrete-warm .status-bar {
|
||||
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 */
|
||||
.preview-content h1,
|
||||
.preview-content h2,
|
||||
@@ -3021,3 +3158,291 @@ body.printing-no-styles .preview-content pre,
|
||||
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;
|
||||
}
|
||||
+79
-36
@@ -10,8 +10,9 @@ const PizZip = require('pizzip');
|
||||
const Docx = require('docx4js').default;
|
||||
|
||||
class WordTemplateExporter {
|
||||
constructor(templatePath) {
|
||||
constructor(templatePath, startPage = 3) {
|
||||
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
|
||||
const newContentXml = this.markdownToWordXml(markdownContent);
|
||||
|
||||
// Insert new content after page 2 (after the section break)
|
||||
const modifiedXml = this.insertContentAfterPage2(documentXml, newContentXml);
|
||||
// Insert new content after the specified start page
|
||||
const modifiedXml = this.insertContentAfterPage(documentXml, newContentXml, this.startPage);
|
||||
|
||||
// Update the zip with modified XML
|
||||
zip.file('word/document.xml', modifiedXml);
|
||||
@@ -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) {
|
||||
// Find the last section break (after page 2)
|
||||
insertContentAfterPage(documentXml, newContentXml, afterPage) {
|
||||
// Find section breaks that mark page boundaries
|
||||
// Look for the section properties tag that marks page breaks
|
||||
const sectionBreakRegex = /<w:sectPr[^>]*>[\s\S]*?<\/w:sectPr>/g;
|
||||
const matches = [...documentXml.matchAll(sectionBreakRegex)];
|
||||
|
||||
if (matches.length >= 2) {
|
||||
// Insert after the 2nd section break
|
||||
const insertPoint = matches[1].index + matches[1][0].length;
|
||||
// Calculate which section break to insert after
|
||||
// Page 1 = before 1st section break
|
||||
// Page 2 = after 1st section break
|
||||
// Page 3 = after 2nd section break, etc.
|
||||
const sectionIndex = afterPage - 1;
|
||||
|
||||
if (matches.length >= sectionIndex && sectionIndex > 0) {
|
||||
// Insert after the specified section break
|
||||
const insertPoint = matches[sectionIndex - 1].index + matches[sectionIndex - 1][0].length;
|
||||
return documentXml.slice(0, insertPoint) + newContentXml + documentXml.slice(insertPoint);
|
||||
} else if (afterPage === 1 || matches.length === 0) {
|
||||
// Insert at the beginning (after <w:body>) or if no section breaks found
|
||||
const bodyStart = documentXml.indexOf('<w:body>') + 8;
|
||||
return documentXml.slice(0, bodyStart) + newContentXml + documentXml.slice(bodyStart);
|
||||
} else {
|
||||
// If 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>');
|
||||
}
|
||||
}
|
||||
@@ -239,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -383,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
|
||||
@@ -401,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));
|
||||
@@ -416,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
|
||||
@@ -423,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"/>
|
||||
@@ -439,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;
|
||||
@@ -465,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>
|
||||
@@ -480,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>
|
||||
@@ -496,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>
|
||||
@@ -512,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>
|
||||
|
||||
Reference in New Issue
Block a user