Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8e0748037 | ||
|
|
cb98549db8 | ||
|
|
ff651e9b21 | ||
|
|
e013e5fd69 | ||
|
|
f7362bec46 | ||
|
|
5a11a9af75 | ||
|
|
8b52635032 | ||
|
|
7e11176290 | ||
|
|
4d5e552741 | ||
|
|
58314efc7a | ||
|
|
478b404135 | ||
|
|
8be2651b0e | ||
|
|
922e74d671 | ||
|
|
7e67667035 | ||
|
|
7783fbf622 | ||
|
|
458a387c15 | ||
|
|
72677698d2 | ||
|
|
59c6513b49 | ||
|
|
c305789428 | ||
|
|
fefb299e6d | ||
|
|
3f3bdf5637 | ||
|
|
2ab8deffeb | ||
|
|
cc471d4101 | ||
|
|
0ac92b7060 | ||
|
|
f74f5f4221 | ||
|
|
e1c0bc387f | ||
|
|
ef3ce1647e | ||
|
|
e1e0072eea | ||
|
|
1b2afb5f15 | ||
|
|
c811af9d48 | ||
|
|
a6e91b7403 | ||
|
|
a44b13d2fb | ||
|
|
015446bff3 |
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"CurrentProjectSetting": null
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"ExpandedNodes": [
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"SelectedNode": "\\package.json",
|
||||||
|
"PreviewInSolutionExplorer": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"Version": 1,
|
||||||
|
"WorkspaceRootPath": "H:\\Development\\pan-converter\\",
|
||||||
|
"Documents": [
|
||||||
|
{
|
||||||
|
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|H:\\Development\\pan-converter\\package.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
|
||||||
|
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:package.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"DocumentGroupContainers": [
|
||||||
|
{
|
||||||
|
"Orientation": 0,
|
||||||
|
"VerticalTabListWidth": 256,
|
||||||
|
"DocumentGroups": [
|
||||||
|
{
|
||||||
|
"DockedWidth": 200,
|
||||||
|
"SelectedChildIndex": 0,
|
||||||
|
"Children": [
|
||||||
|
{
|
||||||
|
"$type": "Document",
|
||||||
|
"DocumentIndex": 0,
|
||||||
|
"Title": "package.json",
|
||||||
|
"DocumentMoniker": "H:\\Development\\pan-converter\\package.json",
|
||||||
|
"RelativeDocumentMoniker": "package.json",
|
||||||
|
"ToolTip": "H:\\Development\\pan-converter\\package.json",
|
||||||
|
"RelativeToolTip": "package.json",
|
||||||
|
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAqAAAAAAAAAA==",
|
||||||
|
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|",
|
||||||
|
"WhenOpened": "2025-10-11T13:44:30.357Z",
|
||||||
|
"EditorCaption": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"Version": 1,
|
||||||
|
"WorkspaceRootPath": "H:\\Development\\pan-converter\\",
|
||||||
|
"Documents": [
|
||||||
|
{
|
||||||
|
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|H:\\Development\\pan-converter\\package.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
|
||||||
|
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:package.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"DocumentGroupContainers": [
|
||||||
|
{
|
||||||
|
"Orientation": 0,
|
||||||
|
"VerticalTabListWidth": 256,
|
||||||
|
"DocumentGroups": [
|
||||||
|
{
|
||||||
|
"DockedWidth": 200,
|
||||||
|
"SelectedChildIndex": 0,
|
||||||
|
"Children": [
|
||||||
|
{
|
||||||
|
"$type": "Document",
|
||||||
|
"DocumentIndex": 0,
|
||||||
|
"Title": "package.json",
|
||||||
|
"DocumentMoniker": "H:\\Development\\pan-converter\\package.json",
|
||||||
|
"RelativeDocumentMoniker": "package.json",
|
||||||
|
"ToolTip": "H:\\Development\\pan-converter\\package.json",
|
||||||
|
"RelativeToolTip": "package.json",
|
||||||
|
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAqAAAAAAAAAA==",
|
||||||
|
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|",
|
||||||
|
"WhenOpened": "2025-10-11T13:44:30.357Z",
|
||||||
|
"EditorCaption": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**PanConverter** is a cross-platform Markdown editor and converter powered by Pandoc, built with Electron. It provides professional-grade editing capabilities with comprehensive export options.
|
**PanConverter** is a cross-platform Markdown editor and converter powered by Pandoc, built with Electron. It provides professional-grade editing capabilities with comprehensive export options.
|
||||||
|
|
||||||
**Current Version**: v1.7.1
|
**Current Version**: v1.9.2
|
||||||
**Author**: Amit Haridas (amit.wh@gmail.com)
|
**Author**: Amit Haridas (amit.wh@gmail.com)
|
||||||
**License**: MIT
|
**License**: MIT
|
||||||
**Repository**: https://github.com/amitwh/pan-converter
|
**Repository**: https://github.com/amitwh/pan-converter
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
- **highlight.js** - Syntax highlighting
|
- **highlight.js** - Syntax highlighting
|
||||||
- **KaTeX** - Mathematical expression rendering
|
- **KaTeX** - Mathematical expression rendering
|
||||||
- **DOMPurify** - HTML sanitization
|
- **DOMPurify** - HTML sanitization
|
||||||
|
- **PDFKit** - Native PDF generation without external dependencies (v1.7.7)
|
||||||
|
- **html2pdf** - HTML to PDF conversion library (v1.7.7)
|
||||||
|
|
||||||
### Application Structure
|
### Application Structure
|
||||||
```
|
```
|
||||||
@@ -111,7 +113,617 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
|
|||||||
|
|
||||||
## Feature Implementation Guide
|
## Feature Implementation Guide
|
||||||
|
|
||||||
### v1.7.1 Modern UI Design (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`)
|
||||||
|
- **New Export Option**: "DOCX (Enhanced)" in File → Export menu with keyboard shortcut `Ctrl+Shift+W`
|
||||||
|
- **Template Support**: Use custom Word templates to preserve branding, styles, and formatting
|
||||||
|
- **Template Selection**: File → "Select Word Template..." menu option to choose custom templates
|
||||||
|
- **Persistent Template**: Selected template is saved and reused across sessions
|
||||||
|
- **Word XML Manipulation**: Direct manipulation of Word document XML using PizZip
|
||||||
|
- **Template Page Preservation**: Preserves first 2 pages (cover + TOC) when using templates
|
||||||
|
- **Smart Content Insertion**: Inserts markdown content after 2nd section break
|
||||||
|
|
||||||
|
**Full Markdown Support:**
|
||||||
|
- Headings (H1-H6) with markdown numbering stripped (e.g., "1.1 Title" → "Title")
|
||||||
|
- Text formatting (bold, italic, bold+italic, strikethrough)
|
||||||
|
- Inline code with Consolas font and gray background
|
||||||
|
- Code blocks with monospace font and light gray background
|
||||||
|
- Ordered and unordered lists (uses template numbering, strips markdown numbering)
|
||||||
|
- Blockquotes with indentation
|
||||||
|
- **Tables with orange header styling**:
|
||||||
|
- Orange header row (#F58220) with white bold text
|
||||||
|
- White data rows (no alternating colors)
|
||||||
|
- Orange borders (#F58220) on all cells
|
||||||
|
- Proper column alignment and spacing
|
||||||
|
- Horizontal rules
|
||||||
|
- Links
|
||||||
|
- **ASCII Art and Flowcharts**:
|
||||||
|
- Detects box-drawing characters (┌, ─, │, └, etc.)
|
||||||
|
- Detects flowchart patterns with square brackets
|
||||||
|
- Preserves monospace alignment with Consolas font
|
||||||
|
- Each line rendered separately to prevent wrapping
|
||||||
|
- No-wrap paragraph properties for exact spacing
|
||||||
|
- **Red-colored arrows** (↓, →, ←, ↑) for enhanced visibility
|
||||||
|
- Gray background (#F5F5F5) for distinction from regular text
|
||||||
|
|
||||||
|
**ASCII Art Detection Patterns:**
|
||||||
|
- Unicode box-drawing characters: ┌┐└┘├┤┬┴┼─│═║╔╗╚╝╠╣╦╩╬
|
||||||
|
- Arrow characters: ↓→←↑▼►◄▲
|
||||||
|
- ASCII box patterns: +-----+, |-----|, [Text in brackets]
|
||||||
|
- Flowchart steps: START, [Step Description], END
|
||||||
|
|
||||||
|
**ASCII Art Rendering Features:**
|
||||||
|
```javascript
|
||||||
|
// Each line gets its own paragraph with no-wrap
|
||||||
|
xml += `<w:p>
|
||||||
|
<w:pPr>
|
||||||
|
<w:wordWrap w:val="0"/> // Disable wrapping
|
||||||
|
<w:keepLines/> // Keep lines together
|
||||||
|
<w:line="240" w:lineRule="exact"/> // Exact line height
|
||||||
|
</w:pPr>
|
||||||
|
// Arrows colored red (#FF0000)
|
||||||
|
// Text in Consolas 16pt with gray background
|
||||||
|
</w:p>`;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Template Selection Workflow:**
|
||||||
|
1. User clicks File → "Select Word Template..."
|
||||||
|
2. Dialog opens to browse for .docx files
|
||||||
|
3. Selected template path stored in global variable and persisted
|
||||||
|
4. Template automatically loaded on app startup
|
||||||
|
5. Enhanced export uses selected template (or defaults to `word_template.docx`)
|
||||||
|
|
||||||
|
**Technical Implementation:**
|
||||||
|
```javascript
|
||||||
|
// Template selection function
|
||||||
|
async function selectWordTemplate() {
|
||||||
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
title: 'Select Word Template',
|
||||||
|
filters: [{ name: 'Word Document', extensions: ['docx'] }],
|
||||||
|
properties: ['openFile']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
|
wordTemplatePath = result.filePaths[0];
|
||||||
|
store.set('wordTemplatePath', wordTemplatePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Word Template Exporter with template support
|
||||||
|
class WordTemplateExporter {
|
||||||
|
constructor(templatePath) {
|
||||||
|
this.templatePath = templatePath || path.join(__dirname, '../word_template.docx');
|
||||||
|
}
|
||||||
|
|
||||||
|
async convert(markdownContent, outputPath) {
|
||||||
|
// Load template as ZIP
|
||||||
|
const templateBuffer = fs.readFileSync(this.templatePath);
|
||||||
|
const zip = new PizZip(templateBuffer);
|
||||||
|
|
||||||
|
// Extract and modify document.xml
|
||||||
|
const documentXml = zip.file('word/document.xml').asText();
|
||||||
|
const newContentXml = this.markdownToWordXml(markdownContent);
|
||||||
|
const modifiedXml = this.insertContentAfterPage2(documentXml, newContentXml);
|
||||||
|
|
||||||
|
// Save modified document
|
||||||
|
zip.file('word/document.xml', modifiedXml);
|
||||||
|
const newDocBuffer = zip.generate({ type: 'nodebuffer' });
|
||||||
|
fs.writeFileSync(outputPath, newDocBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Menu Integration:**
|
||||||
|
- Export: File → Export → "DOCX (Enhanced)" (`Ctrl+Shift+W`)
|
||||||
|
- Template Selection: File → "Select Word Template..."
|
||||||
|
- Template path saved to: `userData/settings.json`
|
||||||
|
|
||||||
|
**Dependencies Added:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"docx": "^9.5.1",
|
||||||
|
"pizzip": "^3.2.0",
|
||||||
|
"docx4js": "^3.3.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Improvements in v1.7.9:**
|
||||||
|
1. ✅ Template-based export preserving corporate branding
|
||||||
|
2. ✅ Persistent template selection across sessions
|
||||||
|
3. ✅ Tables with professional orange header styling and white data rows
|
||||||
|
4. ✅ ASCII art and flowcharts with perfect alignment and no wrapping
|
||||||
|
5. ✅ Red-colored arrows in flowcharts for enhanced readability
|
||||||
|
6. ✅ Markdown numbering stripped from headings and lists
|
||||||
|
7. ✅ Template's automatic numbering system used instead
|
||||||
|
|
||||||
|
### v1.7.8 Critical Bug Fixes
|
||||||
|
|
||||||
|
#### 🐛 File Association Fix for Packaged Apps
|
||||||
|
**Fixed Command-Line Argument Parsing** (`src/main.js:1598-1627`, `src/main.js:70-90`)
|
||||||
|
- **Root Cause**: `process.argv.slice(2)` works in development but fails in packaged apps
|
||||||
|
- **Solution**: Detect if app is packaged using `app.isPackaged` and use correct slice index
|
||||||
|
- Development: `process.argv.slice(2)` - ['electron', 'app.js', 'file.md']
|
||||||
|
- Packaged: `process.argv.slice(1)` - ['PanConverter.exe', 'file.md']
|
||||||
|
- **Path Resolution**: Added proper path resolution for both absolute and relative paths
|
||||||
|
- **Comprehensive Logging**: Added detailed console logs to debug file loading issues
|
||||||
|
- **Second Instance Handler**: Also fixed second-instance handler to use same logic
|
||||||
|
|
||||||
|
**Technical Implementation:**
|
||||||
|
```javascript
|
||||||
|
// Detect packaged vs development mode
|
||||||
|
const startIndex = app.isPackaged ? 1 : 2;
|
||||||
|
const fileArgs = process.argv.slice(startIndex);
|
||||||
|
|
||||||
|
// Resolve paths properly
|
||||||
|
for (const arg of fileArgs) {
|
||||||
|
if ((arg.endsWith('.md') || arg.endsWith('.markdown'))) {
|
||||||
|
const resolvedPath = path.isAbsolute(arg) ? arg : path.resolve(process.cwd(), arg);
|
||||||
|
if (fs.existsSync(resolvedPath)) {
|
||||||
|
app.pendingFile = resolvedPath;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fixed Issues:**
|
||||||
|
- ✅ Files now open correctly on first double-click in packaged app
|
||||||
|
- ✅ Command-line arguments properly parsed in both dev and production
|
||||||
|
- ✅ Relative and absolute paths handled correctly
|
||||||
|
- ✅ Comprehensive logging for debugging file association issues
|
||||||
|
|
||||||
|
#### 🖨️ Print Preview Fix
|
||||||
|
**Fixed Print Output to Show Preview Content** (`src/renderer.js:1178-1204`, `src/styles.css:2828-3014`)
|
||||||
|
- **Root Cause**: Manual DOM manipulation conflicted with CSS `@media print` rules
|
||||||
|
- **Solution**: Simplified handler to rely on CSS `@media print` for hiding UI elements
|
||||||
|
- **CSS Enhancements**:
|
||||||
|
- Added selectors for dynamic preview panes `[id^="preview-pane-"]` and `[id^="editor-pane-"]`
|
||||||
|
- Added `.tab-content` to ensure full-width rendering
|
||||||
|
- Added body class-based styling for no-styles mode
|
||||||
|
- **Removed Manual Hiding**: Eliminated manual `display: none` manipulation in favor of CSS
|
||||||
|
|
||||||
|
**Technical Implementation:**
|
||||||
|
```javascript
|
||||||
|
// Simplified print handler - let CSS do the work
|
||||||
|
function handlePrintPreview(withStyles) {
|
||||||
|
const activeTabId = tabManager ? tabManager.activeTabId : 1;
|
||||||
|
const previewContent = document.getElementById(`preview-${activeTabId}`);
|
||||||
|
|
||||||
|
if (!previewContent || !previewContent.innerHTML.trim()) {
|
||||||
|
alert('Nothing to print...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add body classes for CSS styling
|
||||||
|
if (!withStyles) {
|
||||||
|
document.body.classList.add('printing-no-styles');
|
||||||
|
}
|
||||||
|
document.body.classList.add('printing');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
ipcRenderer.send('do-print', { withStyles });
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.classList.remove('printing', 'printing-no-styles');
|
||||||
|
}, 1000);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**CSS Updates:**
|
||||||
|
```css
|
||||||
|
@media print {
|
||||||
|
/* Hide all UI elements */
|
||||||
|
#toolbar, #tab-bar, .editor-pane, [id^="editor-pane-"] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show preview in full width */
|
||||||
|
[id^="preview-"], [id^="preview-pane-"], .tab-content {
|
||||||
|
display: block !important;
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 20px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fixed Issues:**
|
||||||
|
- ✅ Print now correctly renders preview content, not toolbar
|
||||||
|
- ✅ Both print modes work (with styles and without styles)
|
||||||
|
- ✅ Clean, professional print output with proper formatting
|
||||||
|
- ✅ Proper page breaks, typography, and layout optimization
|
||||||
|
|
||||||
|
#### 🔧 Development Cleanup
|
||||||
|
**Removed Debugging Code** (`src/main.js:120-123`)
|
||||||
|
- Removed auto-opening of DevTools
|
||||||
|
- Clean production-ready build
|
||||||
|
- Debugging logs remain for troubleshooting but DevTools disabled by default
|
||||||
|
|
||||||
|
### v1.7.7 Print & Enhanced PDF Support
|
||||||
|
|
||||||
|
#### 🖨️ Native Print Functionality
|
||||||
|
**Added Print Submenu to File Menu** (`src/main.js:202-215`, `src/renderer.js:1152-1188`, `src/styles.css:2828-2994`)
|
||||||
|
- **Print Menu**: Submenu in File menu with two print options
|
||||||
|
- **Print Preview** (`Ctrl+P`) - Prints preview in black text, no background colors (ink-saving)
|
||||||
|
- **Print Preview (With Styles)** - Prints with full theme colors and styling
|
||||||
|
- **Preview-Only Printing**: Only renders the markdown preview, hides all editor UI
|
||||||
|
- **Native Print Dialog**: Uses Electron's webContents.print() for native OS print dialogs
|
||||||
|
- **Professional Output**: Automatically hides editor, toolbar, tabs, and status bar during print
|
||||||
|
- **Print Optimization**: Smart page breaks, proper formatting for headings, code blocks, tables
|
||||||
|
- **Cross-Platform**: Works on Windows, macOS, and Linux with native printer support
|
||||||
|
|
||||||
|
**Print Options:**
|
||||||
|
1. **Print Preview** (Ctrl+P)
|
||||||
|
- Black text on white background for professional printing
|
||||||
|
- No background colors to save ink
|
||||||
|
- Perfect for documents and reports
|
||||||
|
- Minimal ink consumption
|
||||||
|
|
||||||
|
2. **Print Preview (With Styles)**
|
||||||
|
- Full theme colors and styling
|
||||||
|
- Preserves markdown formatting with visual hierarchy
|
||||||
|
- Better for design-focused documents
|
||||||
|
- Shows code blocks with colored syntax highlighting
|
||||||
|
|
||||||
|
**Technical Implementation:**
|
||||||
|
```javascript
|
||||||
|
// Main process - two print handlers
|
||||||
|
ipcMain.on('print-preview', (event) => {
|
||||||
|
mainWindow.webContents.send('prepare-print-preview', false);
|
||||||
|
setTimeout(() => {
|
||||||
|
mainWindow.webContents.print({
|
||||||
|
silent: false,
|
||||||
|
printBackground: false,
|
||||||
|
color: true,
|
||||||
|
margin: { marginType: 'default' }
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('print-preview-styled', (event) => {
|
||||||
|
mainWindow.webContents.send('prepare-print-preview', true);
|
||||||
|
setTimeout(() => {
|
||||||
|
mainWindow.webContents.print({
|
||||||
|
silent: false,
|
||||||
|
printBackground: true,
|
||||||
|
color: true,
|
||||||
|
margin: { marginType: 'default' }
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Renderer process - prepares preview for printing
|
||||||
|
ipcRenderer.on('prepare-print-preview', (event, withStyles) => {
|
||||||
|
// Hide editor and UI elements
|
||||||
|
document.getElementById('editor-container').style.display = 'none';
|
||||||
|
document.getElementById('toolbar').style.display = 'none';
|
||||||
|
document.getElementById('tab-bar').style.display = 'none';
|
||||||
|
document.getElementById('status-bar').style.display = 'none';
|
||||||
|
|
||||||
|
// Add print mode classes
|
||||||
|
const preview = document.getElementById('preview');
|
||||||
|
preview.classList.add('print-mode');
|
||||||
|
if (!withStyles) {
|
||||||
|
preview.classList.add('print-no-styles');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore UI after print
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('editor-container').style.display = '';
|
||||||
|
document.getElementById('toolbar').style.display = '';
|
||||||
|
document.getElementById('tab-bar').style.display = '';
|
||||||
|
document.getElementById('status-bar').style.display = '';
|
||||||
|
preview.classList.remove('print-mode', 'print-no-styles');
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**CSS Print Styles** (`src/styles.css:2828-2994`)
|
||||||
|
- Comprehensive @media print stylesheet
|
||||||
|
- Hides all UI elements in print view
|
||||||
|
- Optimizes preview for paper output
|
||||||
|
- Smart page breaks for headings and tables
|
||||||
|
- Proper formatting for code blocks, lists, tables
|
||||||
|
- Image optimization and link handling
|
||||||
|
- Professional typography with 12pt font size
|
||||||
|
|
||||||
|
#### 📦 Enhanced PDF Export Dependencies
|
||||||
|
**Added Native PDF Generation Libraries** (package.json)
|
||||||
|
- **PDFKit** (v0.14.0) - Native PDF generation without Pandoc dependency
|
||||||
|
- **html2pdf.js** (v0.10.1) - HTML to PDF conversion for rich formatted output
|
||||||
|
- **Benefits**: PDF exports now work even without system Pandoc installation
|
||||||
|
- **Future Enhancement**: Enables PDF generation with embedded fonts and graphics
|
||||||
|
- **Offline Support**: Complete PDF generation offline without external services
|
||||||
|
|
||||||
|
**Updated Dependencies:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pdfkit": "^0.14.0",
|
||||||
|
"html2pdf.js": "^0.10.1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features Enabled:**
|
||||||
|
- PDF export works independently of Pandoc installation
|
||||||
|
- Higher quality PDF output with better formatting control
|
||||||
|
- Support for custom fonts, images, and complex layouts
|
||||||
|
- Faster PDF generation times
|
||||||
|
- Reduced system dependencies for better portability
|
||||||
|
|
||||||
|
#### 🔧 Keyboard Shortcut Updates
|
||||||
|
**Remapped Shortcuts** (`src/main.js`)
|
||||||
|
- **Print**: `Ctrl+P` (new in v1.7.7) - Opens native print dialog
|
||||||
|
- **Toggle Preview**: Changed from `Ctrl+P` to `Ctrl+Shift+P` to avoid conflicts
|
||||||
|
- **Backward Compatibility**: No breaking changes to existing workflows
|
||||||
|
|
||||||
|
### v1.7.6 UI Improvement
|
||||||
|
|
||||||
|
#### 🎨 Table Header Styling Cleanup
|
||||||
|
**Removed Custom Background Colors** (`src/styles.css:327-329`, `src/styles.css:451-453`)
|
||||||
|
- Removed gray background color (#f6f8fa) from table headers in default theme
|
||||||
|
- Removed dark theme table header background styling block entirely
|
||||||
|
- Table headers now blend naturally with preview background for cleaner appearance
|
||||||
|
- Headers maintain font-weight: 600 for visual distinction
|
||||||
|
- All other table styling preserved (borders, alternating row colors)
|
||||||
|
|
||||||
|
**User Feedback Implementation:**
|
||||||
|
- Based on user feedback: "remove the theming of table headers, it looks bad"
|
||||||
|
- Result: Professional, clean table presentation matching overall preview styling
|
||||||
|
- Consistent with normal preview theming across all themes
|
||||||
|
|
||||||
|
### v1.7.5 Critical File Association Fix
|
||||||
|
|
||||||
|
#### 🐛 Single-Instance Lock for Windows File Association
|
||||||
|
**Fixed Installed App Double-Click Behavior** (`src/main.js:52-85`)
|
||||||
|
- **Root Cause**: Windows launches second instance when double-clicking files with app already running
|
||||||
|
- **Solution**: Implemented `app.requestSingleInstanceLock()` pattern
|
||||||
|
- **Second Instance Handler**: Captures file path from new instance attempts and opens in existing instance
|
||||||
|
- **Focus Management**: Automatically focuses and restores existing window
|
||||||
|
- **Renderer Readiness**: Proper handling of file queue with `rendererReady` state
|
||||||
|
|
||||||
|
**Technical Implementation:**
|
||||||
|
```javascript
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
if (!gotTheLock) {
|
||||||
|
app.quit();
|
||||||
|
} else {
|
||||||
|
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
||||||
|
// Focus existing window
|
||||||
|
if (mainWindow) {
|
||||||
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
|
mainWindow.focus();
|
||||||
|
}
|
||||||
|
// Extract and open file from second instance
|
||||||
|
const fileArgs = commandLine.slice(2);
|
||||||
|
for (const arg of fileArgs) {
|
||||||
|
if ((arg.endsWith('.md') || arg.endsWith('.markdown')) && fs.existsSync(arg)) {
|
||||||
|
if (rendererReady) {
|
||||||
|
openFileFromPath(arg);
|
||||||
|
} else {
|
||||||
|
app.pendingFile = arg;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fixed Issues:**
|
||||||
|
- ✅ Double-click `.md` files in Windows Explorer now opens correctly
|
||||||
|
- ✅ Right-click "Open with PanConverter" now works in installed app
|
||||||
|
- ✅ File association behavior now consistent between development and production
|
||||||
|
- ✅ Single instance maintained across all file opening methods
|
||||||
|
|
||||||
|
**Why Previous Attempts Failed:**
|
||||||
|
- v1.7.3-1.7.4: Only added `rendererReady` checks but missed second-instance handling
|
||||||
|
- Development testing passed but production failed (different execution paths)
|
||||||
|
- Second instance would start, receive file, then exit without passing to first instance
|
||||||
|
|
||||||
|
### v1.7.2 Enhanced Themes & Bug Fixes
|
||||||
|
|
||||||
|
#### 🎨 14 New Beautiful Themes
|
||||||
|
**Expanded Theme Collection** (`src/main.js:242-266`, `src/styles.css:1590-2555`)
|
||||||
|
- **Added 14 new professionally-designed themes** bringing the total to 19 themes
|
||||||
|
- **New Themes Include**:
|
||||||
|
- **Dracula** - Popular purple and pink dark theme
|
||||||
|
- **Nord** - Arctic-inspired blue theme
|
||||||
|
- **One Dark** - Atom's iconic dark theme
|
||||||
|
- **Atom One Light** - Clean, bright light theme
|
||||||
|
- **Material** - Google Material Design inspired
|
||||||
|
- **Gruvbox Dark** - Warm retro groove colors (dark)
|
||||||
|
- **Gruvbox Light** - Warm retro groove colors (light)
|
||||||
|
- **Tokyo Night** - Modern Japanese-inspired dark theme
|
||||||
|
- **Palenight** - Soft purple Material Design variant
|
||||||
|
- **Ayu Dark** - Simple, elegant dark theme
|
||||||
|
- **Ayu Light** - Minimalist light theme
|
||||||
|
- **Ayu Mirage** - Balanced dark-light hybrid
|
||||||
|
- **Oceanic Next** - Ocean-inspired teal and blue
|
||||||
|
- **Cobalt2** - Deep blue with yellow accents
|
||||||
|
|
||||||
|
**Technical Implementation:**
|
||||||
|
- Complete CSS styling for all UI elements per theme
|
||||||
|
- Consistent theming across tabs, toolbar, editor, preview, and status bar
|
||||||
|
- Theme-aware color schemes for syntax highlighting
|
||||||
|
- All themes support glassmorphism effects from v1.7.1
|
||||||
|
|
||||||
|
#### 🐛 Undo/Redo Fix
|
||||||
|
**Fixed Menu Integration** (`src/renderer.js:1107-1118`)
|
||||||
|
- Fixed undo/redo menu items not connecting to custom undo/redo functionality
|
||||||
|
- Added IPC event listeners in renderer to receive 'undo' and 'redo' messages
|
||||||
|
- Connected Edit menu commands to TabManager's undo() and redo() methods
|
||||||
|
- Keyboard shortcuts (Ctrl+Z, Ctrl+Shift+Z) now work correctly with custom undo stack
|
||||||
|
|
||||||
|
### v1.7.1 Modern UI Design
|
||||||
|
|
||||||
#### 🎨 Glassmorphism & Gradient UI
|
#### 🎨 Glassmorphism & Gradient UI
|
||||||
**Modern Design System** (`src/styles-modern.css`, `src/index.html:7-11`)
|
**Modern Design System** (`src/styles-modern.css`, `src/index.html:7-11`)
|
||||||
@@ -565,5 +1177,33 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated**: October 11, 2025
|
## Pending Tasks & Future Enhancements
|
||||||
**Claude Assistant**: Development completed for v1.7.1 with modern glassmorphism UI design featuring animated gradient backgrounds, translucent glass effects, smooth animations, enhanced typography with Inter and JetBrains Mono fonts, and comprehensive CSS custom properties for consistent theming.
|
|
||||||
|
No pending tasks at this time. All planned v1.9.1 features have been completed.
|
||||||
|
|
||||||
|
### Potential Future Enhancements
|
||||||
|
- Image embedding in headers/footers (partially implemented - logo upload UI ready)
|
||||||
|
- Advanced page numbering formats (Roman numerals, custom prefixes)
|
||||||
|
- First-page-different header/footer support
|
||||||
|
- Per-section headers/footers within documents
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: December 12, 2025
|
||||||
|
**Claude Assistant**: Development completed for v1.9.1 with ASCII Art Preservation!
|
||||||
|
|
||||||
|
### v1.9.1 - Latest Release Summary
|
||||||
|
- **ASCII Art Preservation**: Code blocks, flowcharts, tables, and ASCII art now export exactly as shown in preview
|
||||||
|
- **Enhanced Code Block Rendering**: Each line rendered separately to preserve exact spacing and alignment
|
||||||
|
- **Monospace Font Support**: Consolas font used consistently across all export formats
|
||||||
|
- **Improved ASCII Detection**: Extended Unicode box-drawing and arrow character support
|
||||||
|
- **PDF Export Enhancement**: Added monofont and highlight-style options for better code rendering
|
||||||
|
- **HTML/Electron PDF Fix**: Enhanced CSS to prevent text wrapping in code blocks
|
||||||
|
|
||||||
|
### Previous Releases
|
||||||
|
- v1.9.0: Custom Headers & Footers for PDF, DOCX, ODT, PowerPoint exports
|
||||||
|
- v1.8.3: Streamlined PDF Editor UI & Configurable Template Settings
|
||||||
|
- v1.8.2: Enhanced PDF Export & Print Fix
|
||||||
|
- v1.8.1: Streamlined PDF Editor UI
|
||||||
|
- v1.8.0: Enhanced Word Export with template support (batch)
|
||||||
|
- v1.7.9: Template-Based Word Export with ASCII art support
|
||||||
@@ -1,95 +1,12 @@
|
|||||||
# Push Instructions for Pan Converter
|
# Test Double-Click File Opening
|
||||||
|
|
||||||
## Prerequisites
|
This is a test file to verify double-click functionality.
|
||||||
1. Create a new repository on GitHub: https://github.com/new
|
|
||||||
- Repository name: `pan-converter`
|
|
||||||
- Keep it empty (no README, .gitignore, or license)
|
|
||||||
- Make it public or private as desired
|
|
||||||
|
|
||||||
2. Set up authentication:
|
## Features to Test
|
||||||
- **Option A - SSH Key** (Recommended):
|
- File should load automatically
|
||||||
```bash
|
- Content should display in editor
|
||||||
# Check if you have SSH key
|
- Tab should show filename
|
||||||
ls -la ~/.ssh/id_rsa.pub
|
- Preview should render markdown
|
||||||
|
|
||||||
# If not, generate one:
|
**This text should be bold**
|
||||||
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
|
*This text should be italic*
|
||||||
|
|
||||||
# Add to GitHub: Settings > SSH and GPG keys > New SSH key
|
|
||||||
cat ~/.ssh/id_rsa.pub
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Option B - Personal Access Token**:
|
|
||||||
- Go to GitHub Settings > Developer settings > Personal access tokens
|
|
||||||
- Generate new token with 'repo' scope
|
|
||||||
- Save the token securely
|
|
||||||
|
|
||||||
## Push Commands
|
|
||||||
|
|
||||||
### Using SSH (Recommended)
|
|
||||||
```bash
|
|
||||||
# Set SSH remote
|
|
||||||
git remote set-url origin git@github.com:amitwh/pan-converter.git
|
|
||||||
|
|
||||||
# Push all branches
|
|
||||||
git push -u origin master
|
|
||||||
git push origin linux
|
|
||||||
git push origin macos
|
|
||||||
git push origin windows
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using HTTPS with Token
|
|
||||||
```bash
|
|
||||||
# Set HTTPS remote
|
|
||||||
git remote set-url origin https://github.com/amitwh/pan-converter.git
|
|
||||||
|
|
||||||
# Push all branches (you'll be prompted for username and token)
|
|
||||||
git push -u origin master
|
|
||||||
git push origin linux
|
|
||||||
git push origin macos
|
|
||||||
git push origin windows
|
|
||||||
|
|
||||||
# When prompted:
|
|
||||||
# Username: amitwh
|
|
||||||
# Password: [paste your Personal Access Token]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using GitHub CLI (if installed)
|
|
||||||
```bash
|
|
||||||
# Login to GitHub CLI
|
|
||||||
gh auth login
|
|
||||||
|
|
||||||
# Create repo and push
|
|
||||||
gh repo create pan-converter --public --source=. --remote=origin --push
|
|
||||||
```
|
|
||||||
|
|
||||||
## Automated Script
|
|
||||||
```bash
|
|
||||||
# Run the provided script
|
|
||||||
./push-to-github.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verify Success
|
|
||||||
After pushing, verify at: https://github.com/amitwh/pan-converter
|
|
||||||
|
|
||||||
You should see:
|
|
||||||
- 4 branches (master, linux, macos, windows)
|
|
||||||
- All project files
|
|
||||||
- Complete commit history
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Authentication Failed
|
|
||||||
- For SSH: Ensure your SSH key is added to GitHub
|
|
||||||
- For HTTPS: Use Personal Access Token, not password
|
|
||||||
- Check token has 'repo' scope
|
|
||||||
|
|
||||||
### Repository Not Found
|
|
||||||
- Ensure repository exists on GitHub
|
|
||||||
- Check spelling: `pan-converter`
|
|
||||||
- Verify you're logged into the correct GitHub account
|
|
||||||
|
|
||||||
### Permission Denied
|
|
||||||
- Check repository ownership
|
|
||||||
- Ensure you have write access
|
|
||||||
- Verify authentication method is correct
|
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ npm run dist:all
|
|||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
|
|
||||||
|
- **v1.8.0** - Added enhanced Word export with template support to batch converter
|
||||||
|
- **v1.7.9** - Enhanced Word Export with Template Support: Template-based DOCX generation with custom templates, table styling, ASCII art support, and red-colored flowchart arrows
|
||||||
- **v1.5.6** - (Your release notes here)
|
- **v1.5.6** - (Your release notes here)
|
||||||
- **v1.5.5** - Refactored PDF export, simplified Pandoc pathing, and removed XLSX dependency.
|
- **v1.5.5** - Refactored PDF export, simplified Pandoc pathing, and removed XLSX dependency.
|
||||||
- **v1.3.1** - Bug fixes: Fixed file associations for double-clicking .md files, corrected 50/50 layout alignment for editor/preview panes
|
- **v1.3.1** - Bug fixes: Fixed file associations for double-clicking .md files, corrected 50/50 layout alignment for editor/preview panes
|
||||||
|
|||||||
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pan-converter",
|
"name": "pan-converter",
|
||||||
"version": "1.7.1",
|
"version": "1.9.2",
|
||||||
"description": "Cross-platform Markdown editor and converter using Pandoc",
|
"description": "Cross-platform Markdown editor and converter using Pandoc",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -31,11 +31,17 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
|
"core-util-is": "^1.0.3",
|
||||||
|
"docx": "^9.5.1",
|
||||||
|
"docx4js": "^3.3.0",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"electron-store": "^10.1.0",
|
"electron-store": "^10.1.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
"html2pdf.js": "^0.10.1",
|
||||||
"marked": "^16.2.1",
|
"marked": "^16.2.1",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
|
"pdfkit": "^0.14.0",
|
||||||
|
"pizzip": "^3.2.0",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
|
|||||||
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -515,7 +515,9 @@
|
|||||||
<select id="batch-format">
|
<select id="batch-format">
|
||||||
<option value="html">HTML</option>
|
<option value="html">HTML</option>
|
||||||
<option value="pdf">PDF</option>
|
<option value="pdf">PDF</option>
|
||||||
|
<option value="pdf-enhanced">PDF (Enhanced)</option>
|
||||||
<option value="docx">DOCX</option>
|
<option value="docx">DOCX</option>
|
||||||
|
<option value="docx-enhanced">DOCX (Enhanced)</option>
|
||||||
<option value="latex">LaTeX</option>
|
<option value="latex">LaTeX</option>
|
||||||
<option value="rtf">RTF</option>
|
<option value="rtf">RTF</option>
|
||||||
<option value="odt">ODT</option>
|
<option value="odt">ODT</option>
|
||||||
@@ -555,12 +557,12 @@
|
|||||||
|
|
||||||
<!-- PDF Editor Dialog -->
|
<!-- PDF Editor Dialog -->
|
||||||
<div id="pdf-editor-dialog" class="export-dialog hidden">
|
<div id="pdf-editor-dialog" class="export-dialog hidden">
|
||||||
<div class="export-dialog-content">
|
<div class="export-dialog-content pdf-editor-content">
|
||||||
<div class="export-dialog-header">
|
<div class="export-dialog-header">
|
||||||
<h3 id="pdf-editor-title">PDF Editor</h3>
|
<h3 id="pdf-editor-title">PDF Editor</h3>
|
||||||
<button id="pdf-editor-dialog-close" title="Close">×</button>
|
<button id="pdf-editor-dialog-close" title="Close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="export-dialog-body">
|
<div class="export-dialog-body pdf-editor-body">
|
||||||
<!-- Merge PDFs Section -->
|
<!-- Merge PDFs Section -->
|
||||||
<div id="pdf-merge-section" class="pdf-operation-section hidden">
|
<div id="pdf-merge-section" class="pdf-operation-section hidden">
|
||||||
<div class="export-section">
|
<div class="export-section">
|
||||||
@@ -924,6 +926,121 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Header & Footer Configuration Dialog -->
|
||||||
|
<div id="header-footer-dialog" class="export-dialog hidden">
|
||||||
|
<div class="export-dialog-content" style="max-width: 700px;">
|
||||||
|
<div class="export-dialog-header">
|
||||||
|
<h3>Header & Footer Settings</h3>
|
||||||
|
<button id="header-footer-close" class="dialog-close" title="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="export-dialog-body">
|
||||||
|
<div class="hf-enable-section">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="hf-enabled" checked>
|
||||||
|
Enable Headers and Footers
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="hf-config-content">
|
||||||
|
<!-- Header Configuration -->
|
||||||
|
<div class="hf-section">
|
||||||
|
<h4>Header</h4>
|
||||||
|
<div class="hf-row">
|
||||||
|
<div class="hf-column">
|
||||||
|
<label>Left</label>
|
||||||
|
<input type="text" id="header-left" placeholder="e.g., Document Title">
|
||||||
|
<button class="field-insert-btn" data-target="header-left" title="Insert dynamic field">+</button>
|
||||||
|
</div>
|
||||||
|
<div class="hf-column">
|
||||||
|
<label>Center</label>
|
||||||
|
<input type="text" id="header-center" placeholder="e.g., $TITLE$">
|
||||||
|
<button class="field-insert-btn" data-target="header-center" title="Insert dynamic field">+</button>
|
||||||
|
</div>
|
||||||
|
<div class="hf-column">
|
||||||
|
<label>Right</label>
|
||||||
|
<input type="text" id="header-right" placeholder="e.g., $DATE$">
|
||||||
|
<button class="field-insert-btn" data-target="header-right" title="Insert dynamic field">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hf-logo-row">
|
||||||
|
<label>Header Logo/Image:</label>
|
||||||
|
<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="editor-container">
|
||||||
<div class="tab-content active" id="tab-content-1" data-tab-id="1">
|
<div class="tab-content active" id="tab-content-1" data-tab-id="1">
|
||||||
<div id="editor-pane-1" class="pane">
|
<div id="editor-pane-1" class="pane">
|
||||||
|
|||||||
@@ -949,6 +949,7 @@ class TabManager {
|
|||||||
|
|
||||||
// File operations
|
// File operations
|
||||||
openFile(filePath, content) {
|
openFile(filePath, content) {
|
||||||
|
console.log('openFile called with:', filePath, 'content length:', content.length);
|
||||||
let tab = this.tabs.get(this.activeTabId);
|
let tab = this.tabs.get(this.activeTabId);
|
||||||
|
|
||||||
// Handle both forward and back slashes for cross-platform compatibility
|
// Handle both forward and back slashes for cross-platform compatibility
|
||||||
@@ -956,13 +957,24 @@ class TabManager {
|
|||||||
|
|
||||||
// If current tab is empty and untitled, reuse it
|
// If current tab is empty and untitled, reuse it
|
||||||
if (!tab.filePath && !tab.isDirty && tab.content === '') {
|
if (!tab.filePath && !tab.isDirty && tab.content === '') {
|
||||||
|
console.log('Reusing current tab');
|
||||||
tab.filePath = filePath;
|
tab.filePath = filePath;
|
||||||
tab.title = fileName;
|
tab.title = fileName;
|
||||||
tab.content = content;
|
tab.content = content;
|
||||||
tab.originalContent = content;
|
tab.originalContent = content;
|
||||||
tab.isDirty = false;
|
tab.isDirty = false;
|
||||||
|
|
||||||
|
// Update the editor and preview
|
||||||
|
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||||||
|
if (editor) {
|
||||||
|
editor.value = content;
|
||||||
|
// Update preview after editor is updated
|
||||||
|
this.updatePreview(this.activeTabId);
|
||||||
|
this.updateWordCount();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create new tab for the file
|
// Create new tab for the file
|
||||||
|
console.log('Creating new tab for file');
|
||||||
this.createNewTab();
|
this.createNewTab();
|
||||||
tab = this.tabs.get(this.activeTabId);
|
tab = this.tabs.get(this.activeTabId);
|
||||||
tab.filePath = filePath;
|
tab.filePath = filePath;
|
||||||
@@ -970,12 +982,26 @@ class TabManager {
|
|||||||
tab.content = content;
|
tab.content = content;
|
||||||
tab.originalContent = content;
|
tab.originalContent = content;
|
||||||
tab.isDirty = false;
|
tab.isDirty = false;
|
||||||
}
|
|
||||||
|
|
||||||
this.restoreTabState(this.activeTabId);
|
// Wait a moment for the DOM to update, then set content
|
||||||
|
setTimeout(() => {
|
||||||
|
const editor = document.getElementById(`editor-${this.activeTabId}`);
|
||||||
|
if (editor) {
|
||||||
|
editor.value = content;
|
||||||
|
this.updatePreview(this.activeTabId);
|
||||||
|
this.updateWordCount();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
this.startAutoSave();
|
this.startAutoSave();
|
||||||
this.addToRecentFiles(filePath);
|
this.addToRecentFiles(filePath);
|
||||||
this.updateTabBar();
|
this.updateTabBar();
|
||||||
|
|
||||||
|
// Notify main process about current file for exports
|
||||||
|
const { ipcRenderer } = require('electron');
|
||||||
|
ipcRenderer.send('set-current-file', filePath);
|
||||||
|
|
||||||
|
console.log('File opened successfully');
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentContent() {
|
getCurrentContent() {
|
||||||
@@ -1016,8 +1042,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Request current theme
|
// Request current theme
|
||||||
ipcRenderer.send('get-theme');
|
ipcRenderer.send('get-theme');
|
||||||
|
|
||||||
// Signal that renderer is ready for file operations
|
// Also send renderer-ready immediately as backup
|
||||||
ipcRenderer.send('renderer-ready');
|
// This ensures we don't get stuck waiting for theme-changed
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Backup renderer-ready timeout triggered');
|
||||||
|
ipcRenderer.send('renderer-ready');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
// Set up auto-save interval
|
// Set up auto-save interval
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
@@ -1036,8 +1066,11 @@ ipcRenderer.on('file-new', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcRenderer.on('file-opened', (event, data) => {
|
ipcRenderer.on('file-opened', (event, data) => {
|
||||||
|
console.log('[RENDERER] file-opened received:', data.path, 'content length:', data.content.length);
|
||||||
if (tabManager) {
|
if (tabManager) {
|
||||||
tabManager.openFile(data.path, data.content);
|
tabManager.openFile(data.path, data.content);
|
||||||
|
} else {
|
||||||
|
console.error('[RENDERER] tabManager not initialized!');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1075,7 +1108,28 @@ ipcRenderer.on('toggle-find', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcRenderer.on('theme-changed', (event, theme) => {
|
ipcRenderer.on('theme-changed', (event, theme) => {
|
||||||
|
console.log('[RENDERER] Theme changed to:', theme);
|
||||||
document.body.className = `theme-${theme}`;
|
document.body.className = `theme-${theme}`;
|
||||||
|
|
||||||
|
// After theme is applied, wait for next frame then signal renderer is ready
|
||||||
|
// This ensures complete UI initialization before files are opened
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
console.log('[RENDERER] Sending renderer-ready from theme-changed');
|
||||||
|
ipcRenderer.send('renderer-ready');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Undo/Redo handlers
|
||||||
|
ipcRenderer.on('undo', () => {
|
||||||
|
if (tabManager) {
|
||||||
|
tabManager.undo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on('redo', () => {
|
||||||
|
if (tabManager) {
|
||||||
|
tabManager.redo();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Font size adjustment
|
// Font size adjustment
|
||||||
@@ -1110,6 +1164,48 @@ ipcRenderer.on('adjust-font-size', (event, action) => {
|
|||||||
updateFontSizes(currentFontSize);
|
updateFontSizes(currentFontSize);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Print preview request handlers - handle printing directly
|
||||||
|
ipcRenderer.on('print-preview', () => {
|
||||||
|
console.log('[RENDERER] print-preview received');
|
||||||
|
handlePrintPreview(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on('print-preview-styled', () => {
|
||||||
|
console.log('[RENDERER] print-preview-styled received');
|
||||||
|
handlePrintPreview(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handlePrintPreview(withStyles) {
|
||||||
|
const activeTabId = tabManager ? tabManager.activeTabId : 1;
|
||||||
|
const previewContent = document.getElementById(`preview-${activeTabId}`);
|
||||||
|
|
||||||
|
if (!previewContent || !previewContent.innerHTML.trim()) {
|
||||||
|
alert('Nothing to print. Please create or open a document and ensure the preview is visible.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[RENDERER] Starting print with withStyles:', withStyles);
|
||||||
|
console.log('[RENDERER] Preview content length:', previewContent.innerHTML.length);
|
||||||
|
|
||||||
|
// Add body classes for print mode - let CSS handle everything
|
||||||
|
document.body.classList.add('printing');
|
||||||
|
if (!withStyles) {
|
||||||
|
document.body.classList.add('printing-no-styles');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for CSS to apply then print
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('[RENDERER] Sending do-print to main process');
|
||||||
|
ipcRenderer.send('do-print', { withStyles });
|
||||||
|
|
||||||
|
// Restore classes after print dialog opens
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.classList.remove('printing', 'printing-no-styles');
|
||||||
|
console.log('[RENDERER] Print classes removed');
|
||||||
|
}, 1000);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
// Export Dialog functionality
|
// Export Dialog functionality
|
||||||
let currentExportFormat = null;
|
let currentExportFormat = null;
|
||||||
|
|
||||||
@@ -2460,3 +2556,184 @@ function initMathSupport() {
|
|||||||
|
|
||||||
// Initialize math support on load
|
// Initialize math support on load
|
||||||
initMathSupport();
|
initMathSupport();
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// Header & Footer Dialog Management
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
let currentFieldTarget = null; // Track which input field is being edited
|
||||||
|
|
||||||
|
// Open header/footer settings dialog
|
||||||
|
function openHeaderFooterDialog() {
|
||||||
|
const dialog = document.getElementById('header-footer-dialog');
|
||||||
|
dialog.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Request current settings from main process
|
||||||
|
ipcRenderer.send('get-header-footer-settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close header/footer settings dialog
|
||||||
|
function closeHeaderFooterDialog() {
|
||||||
|
const dialog = document.getElementById('header-footer-dialog');
|
||||||
|
dialog.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open field picker dialog
|
||||||
|
function openFieldPickerDialog(targetInputId) {
|
||||||
|
currentFieldTarget = targetInputId;
|
||||||
|
const dialog = document.getElementById('field-picker-dialog');
|
||||||
|
dialog.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close field picker dialog
|
||||||
|
function closeFieldPickerDialog() {
|
||||||
|
const dialog = document.getElementById('field-picker-dialog');
|
||||||
|
dialog.classList.add('hidden');
|
||||||
|
currentFieldTarget = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load settings into dialog
|
||||||
|
ipcRenderer.on('header-footer-settings-data', (event, settings) => {
|
||||||
|
// Enable/disable checkbox
|
||||||
|
document.getElementById('hf-enabled').checked = settings.enabled;
|
||||||
|
|
||||||
|
// Header fields
|
||||||
|
document.getElementById('header-left').value = settings.header.left || '';
|
||||||
|
document.getElementById('header-center').value = settings.header.center || '';
|
||||||
|
document.getElementById('header-right').value = settings.header.right || '';
|
||||||
|
|
||||||
|
// Footer fields
|
||||||
|
document.getElementById('footer-left').value = settings.footer.left || '';
|
||||||
|
document.getElementById('footer-center').value = settings.footer.center || '';
|
||||||
|
document.getElementById('footer-right').value = settings.footer.right || '';
|
||||||
|
|
||||||
|
// Logo previews
|
||||||
|
if (settings.header.logo) {
|
||||||
|
document.getElementById('header-logo-preview').textContent = path.basename(settings.header.logo);
|
||||||
|
} else {
|
||||||
|
document.getElementById('header-logo-preview').textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.footer.logo) {
|
||||||
|
document.getElementById('footer-logo-preview').textContent = path.basename(settings.footer.logo);
|
||||||
|
} else {
|
||||||
|
document.getElementById('footer-logo-preview').textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update config content visibility
|
||||||
|
toggleConfigContent();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle config content based on enabled checkbox
|
||||||
|
function toggleConfigContent() {
|
||||||
|
const enabled = document.getElementById('hf-enabled').checked;
|
||||||
|
const configContent = document.getElementById('hf-config-content');
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
configContent.classList.remove('disabled');
|
||||||
|
} else {
|
||||||
|
configContent.classList.add('disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save header/footer settings
|
||||||
|
function saveHeaderFooterSettings() {
|
||||||
|
const settings = {
|
||||||
|
enabled: document.getElementById('hf-enabled').checked,
|
||||||
|
header: {
|
||||||
|
left: document.getElementById('header-left').value || '',
|
||||||
|
center: document.getElementById('header-center').value || '',
|
||||||
|
right: document.getElementById('header-right').value || '',
|
||||||
|
logo: null // Logo paths are managed separately
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
left: document.getElementById('footer-left').value || '',
|
||||||
|
center: document.getElementById('footer-center').value || '',
|
||||||
|
right: document.getElementById('footer-right').value || '',
|
||||||
|
logo: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ipcRenderer.send('save-header-footer-settings', settings);
|
||||||
|
closeHeaderFooterDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle logo 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();
|
||||||
|
});
|
||||||
@@ -0,0 +1,661 @@
|
|||||||
|
/**
|
||||||
|
* Word Template Exporter
|
||||||
|
* Loads word_template.docx, preserves first 2 pages (cover + TOC),
|
||||||
|
* and adds markdown content starting from page 3 using template styles
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const PizZip = require('pizzip');
|
||||||
|
const Docx = require('docx4js').default;
|
||||||
|
|
||||||
|
class WordTemplateExporter {
|
||||||
|
constructor(templatePath, startPage = 3) {
|
||||||
|
this.templatePath = templatePath || path.join(__dirname, '../word_template.docx');
|
||||||
|
this.startPage = startPage; // Which page to start inserting content
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert markdown to Word document using template
|
||||||
|
*/
|
||||||
|
async convert(markdownContent, outputPath) {
|
||||||
|
try {
|
||||||
|
// Load template
|
||||||
|
const templateBuffer = fs.readFileSync(this.templatePath);
|
||||||
|
const zip = new PizZip(templateBuffer);
|
||||||
|
|
||||||
|
// Extract document.xml
|
||||||
|
const documentXml = zip.file('word/document.xml').asText();
|
||||||
|
|
||||||
|
// Parse markdown and generate Word XML
|
||||||
|
const newContentXml = this.markdownToWordXml(markdownContent);
|
||||||
|
|
||||||
|
// Insert new content after the specified start page
|
||||||
|
const modifiedXml = this.insertContentAfterPage(documentXml, newContentXml, this.startPage);
|
||||||
|
|
||||||
|
// Update the zip with modified XML
|
||||||
|
zip.file('word/document.xml', modifiedXml);
|
||||||
|
|
||||||
|
// Generate and save the new document
|
||||||
|
const newDocBuffer = zip.generate({ type: 'nodebuffer' });
|
||||||
|
fs.writeFileSync(outputPath, newDocBuffer);
|
||||||
|
|
||||||
|
return outputPath;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in Word export:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert markdown content after the specified page in the document
|
||||||
|
* @param {string} documentXml - The document XML
|
||||||
|
* @param {string} newContentXml - The new content to insert
|
||||||
|
* @param {number} afterPage - Insert content after this page number (1-based)
|
||||||
|
*/
|
||||||
|
insertContentAfterPage(documentXml, newContentXml, afterPage) {
|
||||||
|
// Find section breaks that mark page boundaries
|
||||||
|
// Look for the section properties tag that marks page breaks
|
||||||
|
const sectionBreakRegex = /<w:sectPr[^>]*>[\s\S]*?<\/w:sectPr>/g;
|
||||||
|
const matches = [...documentXml.matchAll(sectionBreakRegex)];
|
||||||
|
|
||||||
|
// Calculate which section break to insert after
|
||||||
|
// Page 1 = before 1st section break
|
||||||
|
// Page 2 = after 1st section break
|
||||||
|
// Page 3 = after 2nd section break, etc.
|
||||||
|
const sectionIndex = afterPage - 1;
|
||||||
|
|
||||||
|
if (matches.length >= sectionIndex && sectionIndex > 0) {
|
||||||
|
// Insert after the specified section break
|
||||||
|
const insertPoint = matches[sectionIndex - 1].index + matches[sectionIndex - 1][0].length;
|
||||||
|
return documentXml.slice(0, insertPoint) + newContentXml + documentXml.slice(insertPoint);
|
||||||
|
} else if (afterPage === 1 || matches.length === 0) {
|
||||||
|
// Insert at the beginning (after <w:body>) or if no section breaks found
|
||||||
|
const bodyStart = documentXml.indexOf('<w:body>') + 8;
|
||||||
|
return documentXml.slice(0, bodyStart) + newContentXml + documentXml.slice(bodyStart);
|
||||||
|
} else {
|
||||||
|
// If not enough section breaks, insert before closing body tag
|
||||||
|
return documentXml.replace('</w:body>', newContentXml + '</w:body>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert markdown to Word XML format
|
||||||
|
*/
|
||||||
|
markdownToWordXml(markdown) {
|
||||||
|
const lines = markdown.split('\n');
|
||||||
|
let xml = '';
|
||||||
|
let inCodeBlock = false;
|
||||||
|
let codeLines = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
|
||||||
|
// Handle code blocks
|
||||||
|
if (line.trim().startsWith('```')) {
|
||||||
|
if (inCodeBlock) {
|
||||||
|
// End code block
|
||||||
|
xml += this.createCodeBlockXml(codeLines.join('\n'));
|
||||||
|
codeLines = [];
|
||||||
|
inCodeBlock = false;
|
||||||
|
} else {
|
||||||
|
inCodeBlock = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inCodeBlock) {
|
||||||
|
codeLines.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty lines
|
||||||
|
if (!line.trim()) {
|
||||||
|
xml += '<w:p><w:pPr></w:pPr></w:p>';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tables - detect table lines
|
||||||
|
if (line.includes('|') && line.trim().startsWith('|')) {
|
||||||
|
const tableLines = [line];
|
||||||
|
i++;
|
||||||
|
// Collect all consecutive table lines
|
||||||
|
while (i < lines.length && lines[i].includes('|')) {
|
||||||
|
tableLines.push(lines[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
i--; // Back up one line
|
||||||
|
xml += this.createTableXml(tableLines);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASCII flowcharts/diagrams - detect box drawing characters
|
||||||
|
if (this.isAsciiArt(line)) {
|
||||||
|
const asciiLines = [line];
|
||||||
|
i++;
|
||||||
|
// Collect all consecutive ASCII art lines
|
||||||
|
while (i < lines.length && (this.isAsciiArt(lines[i]) || !lines[i].trim())) {
|
||||||
|
asciiLines.push(lines[i]);
|
||||||
|
i++;
|
||||||
|
if (i < lines.length && lines[i].trim() && !this.isAsciiArt(lines[i])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i--; // Back up one line
|
||||||
|
xml += this.createAsciiArtXml(asciiLines.join('\n'));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headings - strip markdown numbering
|
||||||
|
if (line.trim().startsWith('#')) {
|
||||||
|
const level = (line.match(/^#+/) || [''])[0].length;
|
||||||
|
let text = line.replace(/^#+\s*/, '').trim();
|
||||||
|
// Remove markdown numbering like "1.1 Title" -> "Title"
|
||||||
|
text = text.replace(/^\d+(\.\d+)*\.?\s+/, '');
|
||||||
|
xml += this.createHeadingXml(text, level);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blockquotes
|
||||||
|
if (line.trim().startsWith('>')) {
|
||||||
|
const text = line.replace(/^>\s*/, '').trim();
|
||||||
|
xml += this.createQuoteXml(text);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordered lists - strip markdown numbering, use template numbering
|
||||||
|
if (/^\s*\d+\.\s+/.test(line)) {
|
||||||
|
const text = line.replace(/^\s*\d+\.\s+/, '');
|
||||||
|
xml += this.createListItemXml(text, true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unordered lists
|
||||||
|
if (/^\s*[-*+]\s+/.test(line)) {
|
||||||
|
const text = line.replace(/^\s*[-*+]\s+/, '');
|
||||||
|
xml += this.createListItemXml(text, false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal rule
|
||||||
|
if (/^[-*_]{3,}$/.test(line.trim())) {
|
||||||
|
xml += this.createHorizontalRuleXml();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal paragraph
|
||||||
|
xml += this.createParagraphXml(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create heading XML using template styles
|
||||||
|
*/
|
||||||
|
createHeadingXml(text, level) {
|
||||||
|
const styleName = `Heading${level}`;
|
||||||
|
const runs = this.parseInlineFormatting(text);
|
||||||
|
|
||||||
|
return `<w:p>
|
||||||
|
<w:pPr>
|
||||||
|
<w:pStyle w:val="${styleName}"/>
|
||||||
|
</w:pPr>
|
||||||
|
${runs}
|
||||||
|
</w:p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create paragraph XML with Normal style
|
||||||
|
*/
|
||||||
|
createParagraphXml(text) {
|
||||||
|
const runs = this.parseInlineFormatting(text);
|
||||||
|
|
||||||
|
return `<w:p>
|
||||||
|
<w:pPr>
|
||||||
|
<w:pStyle w:val="Normal"/>
|
||||||
|
</w:pPr>
|
||||||
|
${runs}
|
||||||
|
</w:p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create quote XML
|
||||||
|
*/
|
||||||
|
createQuoteXml(text) {
|
||||||
|
const runs = this.parseInlineFormatting(text);
|
||||||
|
|
||||||
|
return `<w:p>
|
||||||
|
<w:pPr>
|
||||||
|
<w:pStyle w:val="Quote"/>
|
||||||
|
</w:pPr>
|
||||||
|
${runs}
|
||||||
|
</w:p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create list item XML using template numbering
|
||||||
|
*/
|
||||||
|
createListItemXml(text, numbered) {
|
||||||
|
const runs = this.parseInlineFormatting(text);
|
||||||
|
const numId = numbered ? '1' : '2'; // Template numbering IDs
|
||||||
|
|
||||||
|
return `<w:p>
|
||||||
|
<w:pPr>
|
||||||
|
<w:pStyle w:val="${numbered ? 'ListNumber' : 'ListBullet'}"/>
|
||||||
|
<w:numPr>
|
||||||
|
<w:ilvl w:val="0"/>
|
||||||
|
<w:numId w:val="${numId}"/>
|
||||||
|
</w:numPr>
|
||||||
|
</w:pPr>
|
||||||
|
${runs}
|
||||||
|
</w:p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create code block XML - renders each line as separate paragraph
|
||||||
|
* to preserve exact formatting like in preview (monospace, no wrapping)
|
||||||
|
* Background shading applied at paragraph level to extend full width
|
||||||
|
*/
|
||||||
|
createCodeBlockXml(code) {
|
||||||
|
const lines = code.split('\n');
|
||||||
|
let xml = '';
|
||||||
|
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
const escapedLine = this.escapeXml(line);
|
||||||
|
|
||||||
|
// Each line gets its own paragraph with exact spacing and no wrapping
|
||||||
|
// Shading (shd) is at paragraph level so background extends full width
|
||||||
|
xml += `<w:p>
|
||||||
|
<w:pPr>
|
||||||
|
<w:pStyle w:val="Normal"/>
|
||||||
|
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
|
||||||
|
<w:spacing w:before="${index === 0 ? '120' : '0'}" w:after="${index === lines.length - 1 ? '120' : '0'}" w:line="240" w:lineRule="exact"/>
|
||||||
|
<w:ind w:left="284" w:right="284"/>
|
||||||
|
<w:jc w:val="left"/>
|
||||||
|
<w:keepLines/>
|
||||||
|
<w:wordWrap w:val="0"/>
|
||||||
|
</w:pPr>
|
||||||
|
<w:r>
|
||||||
|
<w:rPr>
|
||||||
|
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||||
|
<w:sz w:val="18"/>
|
||||||
|
<w:szCs w:val="18"/>
|
||||||
|
<w:noProof/>
|
||||||
|
</w:rPr>
|
||||||
|
<w:t xml:space="preserve">${escapedLine}</w:t>
|
||||||
|
</w:r>
|
||||||
|
</w:p>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create horizontal rule XML
|
||||||
|
*/
|
||||||
|
createHorizontalRuleXml() {
|
||||||
|
return `<w:p>
|
||||||
|
<w:pPr>
|
||||||
|
<w:pBdr>
|
||||||
|
<w:bottom w:val="single" w:sz="6" w:space="1" w:color="auto"/>
|
||||||
|
</w:pBdr>
|
||||||
|
</w:pPr>
|
||||||
|
</w:p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create table XML from markdown table lines with template styling
|
||||||
|
*/
|
||||||
|
createTableXml(tableLines) {
|
||||||
|
// Parse table
|
||||||
|
const rows = [];
|
||||||
|
for (const line of tableLines) {
|
||||||
|
// Skip separator lines (e.g., |---|---|)
|
||||||
|
if (/^\s*\|[\s\-:]+\|\s*$/.test(line)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Split by | and trim
|
||||||
|
const cells = line.split('|').filter(cell => cell.trim()).map(cell => cell.trim());
|
||||||
|
if (cells.length > 0) {
|
||||||
|
rows.push(cells);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.length === 0) return '';
|
||||||
|
|
||||||
|
// Calculate number of columns
|
||||||
|
const numCols = Math.max(...rows.map(row => row.length));
|
||||||
|
|
||||||
|
// Build table XML
|
||||||
|
let tableXml = '<w:tbl>';
|
||||||
|
|
||||||
|
// Table properties - use template table style
|
||||||
|
tableXml += `<w:tblPr>
|
||||||
|
<w:tblStyle w:val="TableGrid"/>
|
||||||
|
<w:tblW w:w="5000" w:type="pct"/>
|
||||||
|
<w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="0" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/>
|
||||||
|
<w:tblBorders>
|
||||||
|
<w:top w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||||
|
<w:left w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||||
|
<w:bottom w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||||
|
<w:right w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||||
|
<w:insideH w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||||
|
<w:insideV w:val="single" w:sz="8" w:space="0" w:color="F58220"/>
|
||||||
|
</w:tblBorders>
|
||||||
|
</w:tblPr>`;
|
||||||
|
|
||||||
|
// Table grid
|
||||||
|
tableXml += '<w:tblGrid>';
|
||||||
|
for (let i = 0; i < numCols; i++) {
|
||||||
|
tableXml += '<w:gridCol/>';
|
||||||
|
}
|
||||||
|
tableXml += '</w:tblGrid>';
|
||||||
|
|
||||||
|
// Table rows
|
||||||
|
rows.forEach((rowCells, rowIndex) => {
|
||||||
|
const isHeader = rowIndex === 0;
|
||||||
|
const isEvenRow = rowIndex % 2 === 0;
|
||||||
|
|
||||||
|
tableXml += '<w:tr>';
|
||||||
|
|
||||||
|
// Pad row to have same number of columns
|
||||||
|
while (rowCells.length < numCols) {
|
||||||
|
rowCells.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
rowCells.forEach((cellText, colIndex) => {
|
||||||
|
tableXml += '<w:tc>';
|
||||||
|
tableXml += '<w:tcPr>';
|
||||||
|
|
||||||
|
// Cell shading - orange for header, white for data rows
|
||||||
|
if (isHeader) {
|
||||||
|
tableXml += '<w:shd w:val="clear" w:color="FFFFFF" w:fill="F58220"/>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cell borders
|
||||||
|
tableXml += '<w:tcBorders>' +
|
||||||
|
'<w:top w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
||||||
|
'<w:left w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
||||||
|
'<w:bottom w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
||||||
|
'<w:right w:val="single" w:sz="8" w:space="0" w:color="F58220"/>' +
|
||||||
|
'</w:tcBorders>';
|
||||||
|
tableXml += '</w:tcPr>';
|
||||||
|
|
||||||
|
// Cell content
|
||||||
|
tableXml += '<w:p>';
|
||||||
|
tableXml += '<w:pPr>';
|
||||||
|
tableXml += '<w:spacing w:before="100" w:after="100"/>';
|
||||||
|
tableXml += '</w:pPr>';
|
||||||
|
|
||||||
|
const runs = this.parseInlineFormatting(cellText);
|
||||||
|
|
||||||
|
if (isHeader) {
|
||||||
|
// Header: bold white text
|
||||||
|
tableXml += runs.replace(/<w:rPr>/g, '<w:rPr><w:b/><w:color w:val="FFFFFF"/>');
|
||||||
|
} else {
|
||||||
|
// Data rows: normal black text
|
||||||
|
tableXml += runs;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableXml += '</w:p>';
|
||||||
|
tableXml += '</w:tc>';
|
||||||
|
});
|
||||||
|
|
||||||
|
tableXml += '</w:tr>';
|
||||||
|
});
|
||||||
|
|
||||||
|
tableXml += '</w:tbl>';
|
||||||
|
|
||||||
|
return tableXml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if line contains ASCII art/flowchart characters
|
||||||
|
*/
|
||||||
|
isAsciiArt(line) {
|
||||||
|
// Don't treat markdown tables as ASCII art
|
||||||
|
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
|
||||||
|
// Check if it's a proper markdown table (has multiple cells)
|
||||||
|
const cells = line.split('|').filter(c => c.trim());
|
||||||
|
if (cells.length >= 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common ASCII art characters (Unicode box drawing and symbols)
|
||||||
|
const asciiArtChars = [
|
||||||
|
'─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', // Box drawing
|
||||||
|
'═', '║', '╔', '╗', '╚', '╝', '╠', '╣', '╦', '╩', '╬', // Double box
|
||||||
|
'╭', '╮', '╯', '╰', // Rounded corners
|
||||||
|
'▲', '▼', '◄', '►', '♦', '●', '○', '■', '□', '◆', '◇', // Shapes
|
||||||
|
'↓', '→', '←', '↑', '↔', '↕', '⇒', '⇐', '⇓', '⇑', // Arrows
|
||||||
|
'┃', '━', '┏', '┓', '┗', '┛', '┣', '┫', '┳', '┻', '╋', // Heavy box
|
||||||
|
'░', '▒', '▓', '█', // Shading
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check for box drawing characters
|
||||||
|
if (asciiArtChars.some(char => line.includes(char))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for ASCII box patterns with regular characters
|
||||||
|
const asciiPatterns = [
|
||||||
|
/^\s*\+[-=_+]+\+/, // +-----+ or +=====+
|
||||||
|
/^\s*\|[-=_]{3,}\|/, // |-----|
|
||||||
|
/^\s*[-=_]{5,}$/, // -----
|
||||||
|
/^\s*\+[-]+\+[-]+\+/, // +---+---+
|
||||||
|
/^\s*\([A-Z][a-z]+\)\s*\|\|/, // (Deformable) ||
|
||||||
|
/^\s*\|\s+[A-Z\s]+[-:]\s+[A-Z]/, // | TILE ADHESIVE - TYPE
|
||||||
|
/^\s*\|\s*\[[^\]]+\]/, // | [BIS Mark / CE Mark]
|
||||||
|
/^\s*\|\s{2,}\w+.*\|\|/, // || text ||
|
||||||
|
/^\s*\[[^\]]+\]\s*$/, // [Step in brackets]
|
||||||
|
/^\s*<[-=]+>/, // <----> or <====>
|
||||||
|
/^\s*\/[-_\\\/]+\//, // /----/
|
||||||
|
/^\s*\*[-=\*]+\*/, // *----*
|
||||||
|
];
|
||||||
|
|
||||||
|
return asciiPatterns.some(pattern => pattern.test(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create ASCII art XML with monospace font
|
||||||
|
* Background shading applied at paragraph level to extend full width
|
||||||
|
*/
|
||||||
|
createAsciiArtXml(asciiContent) {
|
||||||
|
// Split ASCII art into individual lines and create a paragraph for each
|
||||||
|
const lines = asciiContent.split('\n');
|
||||||
|
let xml = '';
|
||||||
|
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
// Shading (shd) is at paragraph level so background extends full width
|
||||||
|
xml += `<w:p>
|
||||||
|
<w:pPr>
|
||||||
|
<w:pStyle w:val="Normal"/>
|
||||||
|
<w:shd w:val="clear" w:color="auto" w:fill="F5F5F5"/>
|
||||||
|
<w:spacing w:before="${index === 0 ? '120' : '0'}" w:after="${index === lines.length - 1 ? '120' : '0'}" w:line="240" w:lineRule="exact"/>
|
||||||
|
<w:ind w:left="284" w:right="284"/>
|
||||||
|
<w:jc w:val="left"/>
|
||||||
|
<w:keepLines/>
|
||||||
|
<w:wordWrap w:val="0"/>
|
||||||
|
</w:pPr>`;
|
||||||
|
|
||||||
|
// Check if line contains arrow characters and color them red
|
||||||
|
const arrowChars = ['↓', '→', '←', '↑', '▼', '►', '◄', '▲'];
|
||||||
|
const hasArrow = arrowChars.some(arrow => line.includes(arrow));
|
||||||
|
|
||||||
|
if (hasArrow) {
|
||||||
|
// Split line into parts and color arrows red
|
||||||
|
let remainingLine = line;
|
||||||
|
|
||||||
|
while (remainingLine.length > 0) {
|
||||||
|
let foundArrow = false;
|
||||||
|
let arrowIndex = -1;
|
||||||
|
let foundArrowChar = '';
|
||||||
|
|
||||||
|
// Find the first arrow in the remaining text
|
||||||
|
for (const arrow of arrowChars) {
|
||||||
|
const idx = remainingLine.indexOf(arrow);
|
||||||
|
if (idx !== -1 && (arrowIndex === -1 || idx < arrowIndex)) {
|
||||||
|
arrowIndex = idx;
|
||||||
|
foundArrowChar = arrow;
|
||||||
|
foundArrow = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundArrow) {
|
||||||
|
// Add text before arrow (if any)
|
||||||
|
if (arrowIndex > 0) {
|
||||||
|
const beforeArrow = this.escapeXml(remainingLine.substring(0, arrowIndex));
|
||||||
|
xml += `<w:r>
|
||||||
|
<w:rPr>
|
||||||
|
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||||
|
<w:sz w:val="16"/>
|
||||||
|
<w:szCs w:val="16"/>
|
||||||
|
<w:noProof/>
|
||||||
|
</w:rPr>
|
||||||
|
<w:t xml:space="preserve">${beforeArrow}</w:t>
|
||||||
|
</w:r>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add arrow in red
|
||||||
|
const escapedArrow = this.escapeXml(foundArrowChar);
|
||||||
|
xml += `<w:r>
|
||||||
|
<w:rPr>
|
||||||
|
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||||
|
<w:sz w:val="16"/>
|
||||||
|
<w:szCs w:val="16"/>
|
||||||
|
<w:color w:val="FF0000"/>
|
||||||
|
<w:noProof/>
|
||||||
|
</w:rPr>
|
||||||
|
<w:t xml:space="preserve">${escapedArrow}</w:t>
|
||||||
|
</w:r>`;
|
||||||
|
|
||||||
|
// Continue with remaining text
|
||||||
|
remainingLine = remainingLine.substring(arrowIndex + foundArrowChar.length);
|
||||||
|
} else {
|
||||||
|
// No more arrows, add remaining text
|
||||||
|
const escapedRemaining = this.escapeXml(remainingLine);
|
||||||
|
xml += `<w:r>
|
||||||
|
<w:rPr>
|
||||||
|
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||||
|
<w:sz w:val="16"/>
|
||||||
|
<w:szCs w:val="16"/>
|
||||||
|
<w:noProof/>
|
||||||
|
</w:rPr>
|
||||||
|
<w:t xml:space="preserve">${escapedRemaining}</w:t>
|
||||||
|
</w:r>`;
|
||||||
|
remainingLine = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No arrows, just add the line normally
|
||||||
|
const escapedLine = this.escapeXml(line);
|
||||||
|
xml += `<w:r>
|
||||||
|
<w:rPr>
|
||||||
|
<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>
|
||||||
|
<w:sz w:val="16"/>
|
||||||
|
<w:szCs w:val="16"/>
|
||||||
|
<w:noProof/>
|
||||||
|
</w:rPr>
|
||||||
|
<w:t xml:space="preserve">${escapedLine}</w:t>
|
||||||
|
</w:r>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
xml += `</w:p>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse inline formatting (bold, italic, code)
|
||||||
|
*/
|
||||||
|
parseInlineFormatting(text) {
|
||||||
|
let xml = '';
|
||||||
|
let pos = 0;
|
||||||
|
|
||||||
|
// Patterns for inline formatting
|
||||||
|
const patterns = [
|
||||||
|
{ regex: /\*\*\*(.+?)\*\*\*/g, bold: true, italic: true },
|
||||||
|
{ regex: /\*\*(.+?)\*\*/g, bold: true },
|
||||||
|
{ regex: /\*(.+?)\*/g, italic: true },
|
||||||
|
{ regex: /`(.+?)`/g, code: true }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Simple approach: process text sequentially
|
||||||
|
let remaining = text;
|
||||||
|
|
||||||
|
while (remaining.length > 0) {
|
||||||
|
let foundMatch = false;
|
||||||
|
let earliestPos = remaining.length;
|
||||||
|
let matchedPattern = null;
|
||||||
|
let match = null;
|
||||||
|
|
||||||
|
// Find earliest match
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
pattern.regex.lastIndex = 0;
|
||||||
|
const m = pattern.regex.exec(remaining);
|
||||||
|
if (m && m.index < earliestPos) {
|
||||||
|
earliestPos = m.index;
|
||||||
|
matchedPattern = pattern;
|
||||||
|
match = m;
|
||||||
|
foundMatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundMatch) {
|
||||||
|
// Add text before match
|
||||||
|
if (earliestPos > 0) {
|
||||||
|
xml += this.createRunXml(remaining.substring(0, earliestPos));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add formatted text
|
||||||
|
xml += this.createRunXml(match[1], matchedPattern.bold, matchedPattern.italic, matchedPattern.code);
|
||||||
|
|
||||||
|
remaining = remaining.substring(earliestPos + match[0].length);
|
||||||
|
} else {
|
||||||
|
// No more matches, add remaining text
|
||||||
|
xml += this.createRunXml(remaining);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a run (text segment) XML
|
||||||
|
*/
|
||||||
|
createRunXml(text, bold = false, italic = false, code = false) {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
const escapedText = this.escapeXml(text);
|
||||||
|
let propsXml = '<w:rPr>';
|
||||||
|
|
||||||
|
if (bold) propsXml += '<w:b/>';
|
||||||
|
if (italic) propsXml += '<w:i/>';
|
||||||
|
if (code) {
|
||||||
|
propsXml += '<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas"/>';
|
||||||
|
propsXml += '<w:sz w:val="20"/>';
|
||||||
|
}
|
||||||
|
|
||||||
|
propsXml += '</w:rPr>';
|
||||||
|
|
||||||
|
return `<w:r>${propsXml}<w:t xml:space="preserve">${escapedText}</w:t></w:r>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape XML special characters
|
||||||
|
*/
|
||||||
|
escapeXml(text) {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WordTemplateExporter;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Test Double-Click File Opening
|
||||||
|
|
||||||
|
This is a test file to verify double-click functionality.
|
||||||
|
|
||||||
|
## Features to Test
|
||||||
|
- File should load automatically
|
||||||
|
- Content should display in editor
|
||||||
|
- Tab should show filename
|
||||||
|
- Preview should render markdown
|
||||||
|
|
||||||
|
**This text should be bold**
|
||||||
|
*This text should be italic*
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Test File Opening
|
||||||
|
|
||||||
|
This is a test file to verify that double-click file opening works correctly.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- **Bold text**
|
||||||
|
- *Italic text*
|
||||||
|
- `Code`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
console.log("Hello World");
|
||||||
|
```
|
||||||
|
|
||||||
|
This should appear when you double-click to open the file!
|
||||||