diff --git a/CLAUDE.md b/CLAUDE.md
index e5ff76d..92398fe 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -113,62 +113,131 @@ gh release create v1.2.1 --title "Title" --notes "Release notes" \
## Feature Implementation Guide
-### v1.7.9 Enhanced Word Export (Latest)
+### v1.7.9 Enhanced Word Export with Template Support (Latest)
-#### 📝 Native Word Document Generation
-**Added Enhanced DOCX Export** (`src/wordTemplateExporter.js`, `src/main.js:537-573`, `src/main.js:241`)
+#### 📝 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`
-- **Native JavaScript Implementation**: Uses `docx` npm package for direct Word document generation
-- **No External Dependencies**: Works without Pandoc or Python installation
-- **Full Markdown Support**:
- - Headings (H1-H6) with proper Word styles
- - Text formatting (bold, italic, bold+italic)
- - Inline code with Consolas font
- - Code blocks with gray background
- - Ordered and unordered lists with proper numbering
- - Blockquotes with indentation
- - Tables with header styling
- - Horizontal rules
- - Links
+- **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 += `
+
+ // Disable wrapping
+ // Keep lines together
+ // Exact line height
+
+ // Arrows colored red (#FF0000)
+ // Text in Consolas 16pt with gray background
+`;
+```
+
+**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
-// Word Template Exporter class
+// 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 {
- async convert(markdownContent, outputPath, templatePath = null) {
- const children = this.parseMarkdown(markdownContent);
- const doc = new Document({
- sections: [{ children: children }]
- });
- const buffer = await Packer.toBuffer(doc);
- fs.writeFileSync(outputPath, buffer);
+ 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);
}
}
```
-**Features:**
-- Proper heading levels (H1-H6) using Word's built-in heading styles
-- Bold (**text**), italic (*text*), and combined (***text***) formatting
-- Inline code with `monospace font`
-- Multi-line code blocks with light gray background
-- Bullet and numbered lists with correct indentation
-- Blockquotes with italic styling and left indent
-- Tables with formatted headers
-- Preserves document structure and formatting
-
**Menu Integration:**
-- Location: File → Export → "DOCX (Enhanced)"
-- Keyboard shortcut: `Ctrl+Shift+W`
-- Saves with `.docx` extension
-- Shows success notification with output path
+- 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"
+ "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
@@ -940,12 +1009,14 @@ if (!gotTheLock) {
---
-**Last Updated**: October 26, 2025
-**Claude Assistant**: Development completed for v1.7.9 with Enhanced Word Export functionality:
-- **Native DOCX Generation**: Created `wordTemplateExporter.js` module using `docx` npm package for direct Word document creation
-- **Full Markdown Support**: Headings, formatting (bold/italic), code blocks, lists, blockquotes, tables, links
-- **Menu Integration**: Added "DOCX (Enhanced)" option in File → Export with Ctrl+Shift+W shortcut
-- **No External Dependencies**: Works without Pandoc or Python, pure JavaScript implementation
-- **Professional Output**: Proper Word styles, formatting, and document structure
+**Last Updated**: October 27, 2025
+**Claude Assistant**: Development completed for v1.7.9 with Template-Based Word Export:
+- **Template Support**: Word XML manipulation using PizZip to preserve template branding and styles
+- **Template Selection**: File menu option to choose custom Word templates, persisted across sessions
+- **Table Styling**: Orange headers (#F58220) with white bold text, white data rows, orange borders
+- **ASCII Art & Flowcharts**: Perfect monospace alignment with no-wrap properties, each line as separate paragraph
+- **Red Arrows**: Flowchart arrows (↓→←↑) rendered in red color for enhanced visibility
+- **Markdown Numbering Strip**: Removes markdown numbering from headings and lists to use template numbering
+- **Template Page Preservation**: Keeps first 2 pages (cover + TOC) intact, inserts content after 2nd section break
Previous releases: v1.7.8 (file association & print preview fixes), v1.7.7 (print menu with two options), v1.7.6 (table header cleanup), v1.7.5 (single-instance lock).
\ No newline at end of file
diff --git a/package.json b/package.json
index 2ee4be2..582578f 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"dependencies": {
"codemirror": "^6.0.2",
"docx": "^9.5.1",
+ "docx4js": "^3.3.0",
"dompurify": "^3.2.6",
"electron-store": "^10.1.0",
"highlight.js": "^11.11.1",
@@ -39,6 +40,7 @@
"marked": "^16.2.1",
"pdf-lib": "^1.17.1",
"pdfkit": "^0.14.0",
+ "pizzip": "^3.2.0",
"tslib": "^2.8.1",
"xlsx": "^0.18.5"
},
diff --git a/src/main.js b/src/main.js
index 28f87ae..ccee021 100644
--- a/src/main.js
+++ b/src/main.js
@@ -48,6 +48,7 @@ const store = {
let mainWindow;
let currentFile = null; // This will now represent the active tab's file
let pandocAvailable = null; // Cache pandoc availability check
+let wordTemplatePath = null; // Path to selected Word template
let rendererReady = false; // Track if renderer is ready to receive file data
// Handle single instance lock for Windows file association
@@ -251,6 +252,11 @@ function createMenu() {
]
},
{ type: 'separator' },
+ {
+ label: 'Select Word Template...',
+ click: selectWordTemplate
+ },
+ { type: 'separator' },
{
label: 'Quit',
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
@@ -535,6 +541,27 @@ function showBatchConversionDialog() {
mainWindow.webContents.send('show-batch-dialog');
}
+// Select Word Template
+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);
+
+ dialog.showMessageBox(mainWindow, {
+ type: 'info',
+ title: 'Template Selected',
+ message: 'Word template has been updated',
+ detail: `Template: ${path.basename(wordTemplatePath)}`
+ });
+ }
+}
+
// Enhanced Word Export with Template Support
async function exportWordWithTemplate() {
if (!currentFile) {
@@ -555,8 +582,8 @@ async function exportWordWithTemplate() {
if (result.canceled) return;
- // Create exporter instance
- const exporter = new WordTemplateExporter();
+ // Create exporter instance with selected template (if any)
+ const exporter = new WordTemplateExporter(wordTemplatePath);
// Convert markdown to DOCX
await exporter.convert(content, result.filePath);
@@ -1630,13 +1657,16 @@ function buildPandocCommand(content, format, outputPath) {
}
app.whenReady().then(() => {
+ // Load saved Word template path
+ wordTemplatePath = store.get('wordTemplatePath', null);
+
// Check for command line conversion requests
const args = process.argv.slice(2);
if (args.length >= 2 && (args[0] === '--convert' || args[0] === '--convert-to')) {
handleCLIConversion(args);
return; // Don't create window for CLI operations
}
-
+
createWindow();
// Handle file association on app startup
diff --git a/src/wordTemplateExporter.js b/src/wordTemplateExporter.js
index e5dfa9c..ddd443c 100644
--- a/src/wordTemplateExporter.js
+++ b/src/wordTemplateExporter.js
@@ -1,330 +1,617 @@
/**
* Word Template Exporter
- * Converts Markdown to DOCX using custom Word templates
- * Based on Template_exporter functionality
+ * Loads word_template.docx, preserves first 2 pages (cover + TOC),
+ * and adds markdown content starting from page 3 using template styles
*/
-const { Document, Packer, Paragraph, TextRun, AlignmentType, HeadingLevel, Table, TableRow, TableCell, WidthType, BorderStyle } = require('docx');
const fs = require('fs');
const path = require('path');
+const PizZip = require('pizzip');
+const Docx = require('docx4js').default;
class WordTemplateExporter {
- constructor() {
- this.inCodeBlock = false;
- this.inList = false;
- this.listType = null;
+ constructor(templatePath) {
+ this.templatePath = templatePath || path.join(__dirname, '../word_template.docx');
}
/**
- * Convert markdown to Word document with template
- * @param {string} markdownContent - The markdown content to convert
- * @param {string} outputPath - Where to save the DOCX file
- * @param {string} templatePath - Optional template path (for future implementation)
+ * Convert markdown to Word document using template
*/
- async convert(markdownContent, outputPath, templatePath = null) {
- // Parse markdown and create document elements
- const children = this.parseMarkdown(markdownContent);
+ async convert(markdownContent, outputPath) {
+ try {
+ // Load template
+ const templateBuffer = fs.readFileSync(this.templatePath);
+ const zip = new PizZip(templateBuffer);
- const doc = new Document({
- numbering: {
- config: [
- {
- reference: 'numbering',
- levels: [
- {
- level: 0,
- format: 'decimal',
- text: '%1.',
- alignment: AlignmentType.LEFT
- }
- ]
- },
- {
- reference: 'bullets',
- levels: [
- {
- level: 0,
- format: 'bullet',
- text: '\u2022',
- alignment: AlignmentType.LEFT
- }
- ]
- }
- ]
- },
- sections: [{
- properties: {},
- children: children
- }]
- });
+ // Extract document.xml
+ const documentXml = zip.file('word/document.xml').asText();
- // Write to file
- const buffer = await Packer.toBuffer(doc);
- fs.writeFileSync(outputPath, buffer);
+ // Parse markdown and generate Word XML
+ const newContentXml = this.markdownToWordXml(markdownContent);
- return outputPath;
+ // Insert new content after page 2 (after the section break)
+ const modifiedXml = this.insertContentAfterPage2(documentXml, newContentXml);
+
+ // 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;
+ }
}
- parseMarkdown(content) {
- const lines = content.split('\n');
- const elements = [];
- let i = 0;
+ /**
+ * Insert markdown content after page 2 in the document
+ */
+ insertContentAfterPage2(documentXml, newContentXml) {
+ // Find the last section break (after page 2)
+ // Look for the section properties tag that marks page breaks
+ const sectionBreakRegex = /]*>[\s\S]*?<\/w:sectPr>/g;
+ const matches = [...documentXml.matchAll(sectionBreakRegex)];
- while (i < lines.length) {
+ if (matches.length >= 2) {
+ // Insert after the 2nd section break
+ const insertPoint = matches[1].index + matches[1][0].length;
+ return documentXml.slice(0, insertPoint) + newContentXml + documentXml.slice(insertPoint);
+ } else {
+ // If no section breaks found, insert before closing body tag
+ return documentXml.replace('', newContentXml + '');
+ }
+ }
+
+ /**
+ * 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];
- // Skip empty lines
- if (!line.trim()) {
- elements.push(new Paragraph({ text: '' }));
- 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;
}
- // Code blocks
- if (line.trim().startsWith('```')) {
- const codeLines = [];
+ if (inCodeBlock) {
+ codeLines.push(line);
+ continue;
+ }
+
+ // Empty lines
+ if (!line.trim()) {
+ xml += '';
+ continue;
+ }
+
+ // Tables - detect table lines
+ if (line.includes('|') && line.trim().startsWith('|')) {
+ const tableLines = [line];
i++;
- while (i < lines.length && !lines[i].trim().startsWith('```')) {
- codeLines.push(lines[i]);
+ // Collect all consecutive table lines
+ while (i < lines.length && lines[i].includes('|')) {
+ tableLines.push(lines[i]);
i++;
}
- elements.push(this.createCodeBlock(codeLines.join('\n')));
- i++;
+ i--; // Back up one line
+ xml += this.createTableXml(tableLines);
continue;
}
- // Headers
- if (line.trim().startsWith('#')) {
- elements.push(this.createHeading(line));
+ // 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('>')) {
- elements.push(this.createQuote(line));
- i++;
+ const text = line.replace(/^>\s*/, '').trim();
+ xml += this.createQuoteXml(text);
continue;
}
- // Ordered lists
+ // Ordered lists - strip markdown numbering, use template numbering
if (/^\s*\d+\.\s+/.test(line)) {
- elements.push(this.createListItem(line, true));
- i++;
+ const text = line.replace(/^\s*\d+\.\s+/, '');
+ xml += this.createListItemXml(text, true);
continue;
}
// Unordered lists
if (/^\s*[-*+]\s+/.test(line)) {
- elements.push(this.createListItem(line, false));
- i++;
+ const text = line.replace(/^\s*[-*+]\s+/, '');
+ xml += this.createListItemXml(text, false);
continue;
}
// Horizontal rule
if (/^[-*_]{3,}$/.test(line.trim())) {
- elements.push(this.createHorizontalRule());
- i++;
- continue;
- }
-
- // Tables
- if (line.includes('|')) {
- const tableLines = [line];
- i++;
- while (i < lines.length && lines[i].includes('|')) {
- tableLines.push(lines[i]);
- i++;
- }
- const table = this.createTable(tableLines);
- if (table) elements.push(table);
+ xml += this.createHorizontalRuleXml();
continue;
}
// Normal paragraph
- elements.push(this.createParagraph(line));
- i++;
+ xml += this.createParagraphXml(line);
}
- return elements;
+ return xml;
}
- createHeading(line) {
- const level = line.match(/^#+/)[0].length;
- const text = line.replace(/^#+\s*/, '').trim();
-
- const headingLevels = {
- 1: HeadingLevel.HEADING_1,
- 2: HeadingLevel.HEADING_2,
- 3: HeadingLevel.HEADING_3,
- 4: HeadingLevel.HEADING_4,
- 5: HeadingLevel.HEADING_5,
- 6: HeadingLevel.HEADING_6
- };
-
- return new Paragraph({
- text: text,
- heading: headingLevels[Math.min(level, 6)]
- });
- }
-
- createParagraph(line) {
- const runs = this.parseInlineFormatting(line);
- return new Paragraph({ children: runs });
- }
-
- createQuote(line) {
- const text = line.replace(/^>\s*/, '').trim();
+ /**
+ * Create heading XML using template styles
+ */
+ createHeadingXml(text, level) {
+ const styleName = `Heading${level}`;
const runs = this.parseInlineFormatting(text);
- return new Paragraph({
- children: runs,
- indent: { left: 720 }, // 0.5 inch indent
- italics: true
- });
+ return `
+
+
+
+ ${runs}
+ `;
}
- createListItem(line, numbered) {
- const text = line.replace(/^\s*(\d+\.|-|\*|\+)\s+/, '').trim();
+ /**
+ * Create paragraph XML with Normal style
+ */
+ createParagraphXml(text) {
const runs = this.parseInlineFormatting(text);
- return new Paragraph({
- children: runs,
- numbering: {
- reference: numbered ? 'numbering' : 'bullets',
- level: 0
+ return `
+
+
+
+ ${runs}
+ `;
+ }
+
+ /**
+ * Create quote XML
+ */
+ createQuoteXml(text) {
+ const runs = this.parseInlineFormatting(text);
+
+ return `
+
+
+
+ ${runs}
+ `;
+ }
+
+ /**
+ * Create list item XML using template numbering
+ */
+ createListItemXml(text, numbered) {
+ const runs = this.parseInlineFormatting(text);
+ const numId = numbered ? '1' : '2'; // Template numbering IDs
+
+ return `
+
+
+
+
+
+
+
+ ${runs}
+ `;
+ }
+
+ /**
+ * Create code block XML
+ */
+ createCodeBlockXml(code) {
+ const escapedCode = this.escapeXml(code);
+
+ return `
+
+
+
+
+
+
+
+
+ ${escapedCode}
+
+ `;
+ }
+
+ /**
+ * Create horizontal rule XML
+ */
+ createHorizontalRuleXml() {
+ return `
+
+
+
+
+
+ `;
+ }
+
+ /**
+ * 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;
}
- });
- }
-
- createCodeBlock(code) {
- return new Paragraph({
- children: [
- new TextRun({
- text: code,
- font: 'Consolas',
- size: 18 // 9pt
- })
- ],
- shading: {
- fill: 'F5F5F5'
- }
- });
- }
-
- createHorizontalRule() {
- return new Paragraph({
- text: '_'.repeat(50),
- alignment: AlignmentType.CENTER
- });
- }
-
- createTable(tableLines) {
- // Skip separator lines
- const rows = tableLines.filter(line => !line.match(/^\s*\|[\s\-:]+\|\s*$/));
-
- if (rows.length === 0) return null;
-
- const tableRows = rows.map((line, index) => {
+ // Split by | and trim
const cells = line.split('|').filter(cell => cell.trim()).map(cell => cell.trim());
+ if (cells.length > 0) {
+ rows.push(cells);
+ }
+ }
- return new TableRow({
- children: cells.map(cellText => {
- const runs = this.parseInlineFormatting(cellText);
- return new TableCell({
- children: [new Paragraph({ children: runs })],
- shading: index === 0 ? { fill: 'E7E6E6' } : undefined,
- width: { size: 100 / cells.length, type: WidthType.PERCENTAGE }
- });
- })
+ if (rows.length === 0) return '';
+
+ // Calculate number of columns
+ const numCols = Math.max(...rows.map(row => row.length));
+
+ // Build table XML
+ let tableXml = '';
+
+ // Table properties - use template table style
+ tableXml += `
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ // Table grid
+ tableXml += '';
+ for (let i = 0; i < numCols; i++) {
+ tableXml += '';
+ }
+ tableXml += '';
+
+ // Table rows
+ rows.forEach((rowCells, rowIndex) => {
+ const isHeader = rowIndex === 0;
+ const isEvenRow = rowIndex % 2 === 0;
+
+ tableXml += '';
+
+ // Pad row to have same number of columns
+ while (rowCells.length < numCols) {
+ rowCells.push('');
+ }
+
+ rowCells.forEach((cellText, colIndex) => {
+ tableXml += '';
+ tableXml += '';
+
+ // Cell shading - orange for header, white for data rows
+ if (isHeader) {
+ tableXml += '';
+ }
+
+ // Cell borders
+ tableXml += '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '';
+ tableXml += '';
+
+ // Cell content
+ tableXml += '';
+ tableXml += '';
+ tableXml += '';
+ tableXml += '';
+
+ const runs = this.parseInlineFormatting(cellText);
+
+ if (isHeader) {
+ // Header: bold white text
+ tableXml += runs.replace(//g, '');
+ } else {
+ // Data rows: normal black text
+ tableXml += runs;
+ }
+
+ tableXml += '';
+ tableXml += '';
});
+
+ tableXml += '';
});
- return new Table({
- rows: tableRows,
- width: { size: 100, type: WidthType.PERCENTAGE }
- });
+ tableXml += '';
+
+ 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('|')) {
+ return false;
+ }
+
+ // Common ASCII art characters
+ const asciiArtChars = [
+ '─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼', // Box drawing
+ '═', '║', '╔', '╗', '╚', '╝', '╠', '╣', '╦', '╩', '╬', // Double box
+ '▲', '▼', '◄', '►', '♦', '●', '○', '■', '□', // Shapes
+ '↓', '→', '←', '↑', // Arrows
+ ];
+
+ // 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*\+[-=_]+\+/, // +-----+
+ /^\s*\|[-=_]{3,}\|/, // |-----|
+ /^\s*[-=_]{5,}$/, // -----
+ /^\s*\([A-Z][a-z]+\)\s*\|\|/, // (Deformable) ||
+ /^\s*\|\s+[A-Z\s]+[-:]\s+[A-Z]/, // | TILE ADHESIVE - TYPE
+ /^\s*\|\s*\[[^\]]+\]/, // | [BIS Mark / CE Mark]
+ /^\s*\|\s{2,}\w+.*\|\|/, // || text ||
+ /^\s*\[[^\]]+\]/, // [Step in brackets]
+ ];
+
+ return asciiPatterns.some(pattern => pattern.test(line));
+ }
+
+ /**
+ * Create ASCII art XML with monospace font
+ */
+ 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) => {
+ xml += `
+
+
+
+
+
+
+ `;
+
+ // 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;
+ let processedText = '';
+
+ 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 += `
+
+
+
+
+
+
+
+ ${beforeArrow}
+ `;
+ }
+
+ // Add arrow in red
+ const escapedArrow = this.escapeXml(foundArrowChar);
+ xml += `
+
+
+
+
+
+
+
+
+ ${escapedArrow}
+ `;
+
+ // Continue with remaining text
+ remainingLine = remainingLine.substring(arrowIndex + foundArrowChar.length);
+ } else {
+ // No more arrows, add remaining text
+ const escapedRemaining = this.escapeXml(remainingLine);
+ xml += `
+
+
+
+
+
+
+
+ ${escapedRemaining}
+ `;
+ remainingLine = '';
+ }
+ }
+ } else {
+ // No arrows, just add the line normally
+ const escapedLine = this.escapeXml(line);
+ xml += `
+
+
+
+
+
+
+
+ ${escapedLine}
+ `;
+ }
+
+ xml += ``;
+ });
+
+ return xml;
+ }
+
+ /**
+ * Parse inline formatting (bold, italic, code)
+ */
parseInlineFormatting(text) {
- const runs = [];
+ let xml = '';
let pos = 0;
// Patterns for inline formatting
const patterns = [
- { regex: /\*\*\*(.+?)\*\*\*/g, bold: true, italics: true },
+ { regex: /\*\*\*(.+?)\*\*\*/g, bold: true, italic: true },
{ regex: /\*\*(.+?)\*\*/g, bold: true },
- { regex: /\*(.+?)\*/g, italics: true },
- { regex: /`(.+?)`/g, code: true },
- { regex: /\[([^\]]+)\]\(([^\)]+)\)/g, link: true }
+ { regex: /\*(.+?)\*/g, italic: true },
+ { regex: /`(.+?)`/g, code: true }
];
- // Simple implementation: find all matches and create runs
- let segments = [{ text: text, format: {} }];
+ // Simple approach: process text sequentially
+ let remaining = text;
- for (const pattern of patterns) {
- const newSegments = [];
+ while (remaining.length > 0) {
+ let foundMatch = false;
+ let earliestPos = remaining.length;
+ let matchedPattern = null;
+ let match = null;
- for (const segment of segments) {
- if (segment.format.processed) {
- newSegments.push(segment);
- continue;
- }
-
- let lastIndex = 0;
- const matches = [...segment.text.matchAll(pattern.regex)];
-
- if (matches.length === 0) {
- newSegments.push(segment);
- continue;
- }
-
- matches.forEach(match => {
- // Add text before match
- if (match.index > lastIndex) {
- newSegments.push({
- text: segment.text.substring(lastIndex, match.index),
- format: segment.format
- });
- }
-
- // Add formatted text
- const format = { ...segment.format, processed: true };
- if (pattern.bold) format.bold = true;
- if (pattern.italics) format.italics = true;
- if (pattern.code) {
- format.font = 'Consolas';
- format.size = 20;
- }
-
- newSegments.push({
- text: match[1],
- format: format
- });
-
- lastIndex = match.index + match[0].length;
- });
-
- // Add remaining text
- if (lastIndex < segment.text.length) {
- newSegments.push({
- text: segment.text.substring(lastIndex),
- format: segment.format
- });
+ // 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;
}
}
- segments = newSegments;
+ 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;
+ }
}
- // Convert segments to TextRun objects
- return segments.map(seg => new TextRun({
- text: seg.text,
- bold: seg.format.bold,
- italics: seg.format.italics,
- font: seg.format.font,
- size: seg.format.size
- }));
+ 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 = '';
+
+ if (bold) propsXml += '';
+ if (italic) propsXml += '';
+ if (code) {
+ propsXml += '';
+ propsXml += '';
+ }
+
+ propsXml += '';
+
+ return `${propsXml}${escapedText}`;
+ }
+
+ /**
+ * Escape XML special characters
+ */
+ escapeXml(text) {
+ return text
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
}
}