mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
Add Enhanced Word Export with native DOCX generation (v1.7.9)
- Created wordTemplateExporter.js module using docx npm package - Added "DOCX (Enhanced)" option in File → Export menu - Keyboard shortcut: Ctrl+Shift+W - Full markdown support: headings, formatting, code, lists, tables - No external dependencies (Pandoc/Python not required) - Proper numbering configuration for lists - Professional Word document output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+41
-1
@@ -3,6 +3,7 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { exec } = require('child_process');
|
||||
const { PDFDocument, rgb, degrees, StandardFonts } = require('pdf-lib');
|
||||
const WordTemplateExporter = require('./wordTemplateExporter');
|
||||
|
||||
// Get the system Pandoc path
|
||||
function getPandocPath() {
|
||||
@@ -237,6 +238,7 @@ function createMenu() {
|
||||
{ label: 'HTML', click: () => exportFile('html') },
|
||||
{ label: 'PDF', click: () => exportFile('pdf') },
|
||||
{ label: 'DOCX', click: () => exportFile('docx') },
|
||||
{ label: 'DOCX (Enhanced)', click: () => exportWordWithTemplate(), accelerator: 'Ctrl+Shift+W' },
|
||||
{ label: 'LaTeX', click: () => exportFile('latex') },
|
||||
{ label: 'RTF', click: () => exportFile('rtf') },
|
||||
{ label: 'ODT', click: () => exportFile('odt') },
|
||||
@@ -467,7 +469,7 @@ function createMenu() {
|
||||
type: 'info',
|
||||
title: 'About PanConverter',
|
||||
message: 'PanConverter',
|
||||
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.7.8\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Modern glassmorphism UI with gradient backgrounds\n• Comprehensive PDF Editor (merge, split, compress, rotate, watermark, encrypt)\n• Universal File Converter (LibreOffice, ImageMagick, FFmpeg, Pandoc)\n• Windows Explorer context menu integration\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Real-time preview updates while typing\n• Full toolbar markdown editing functions\n• Enhanced PDF export with built-in Electron fallback\n• File association support for .md files\n• Command-line interface for batch conversion\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Improved preview typography and spacing\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• Complete theme support including Monokai fixes\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• 22 beautiful themes (including Dracula, Nord, Tokyo Night, Gruvbox, Ayu, Concrete, and more)\n• Undo/redo functionality\n• Live word count and statistics',
|
||||
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.7.9\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Modern glassmorphism UI with gradient backgrounds\n• Comprehensive PDF Editor (merge, split, compress, rotate, watermark, encrypt)\n• Universal File Converter (LibreOffice, ImageMagick, FFmpeg, Pandoc)\n• Windows Explorer context menu integration\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Real-time preview updates while typing\n• Full toolbar markdown editing functions\n• Enhanced PDF export with built-in Electron fallback\n• File association support for .md files\n• Command-line interface for batch conversion\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Improved preview typography and spacing\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• Complete theme support including Monokai fixes\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• 22 beautiful themes (including Dracula, Nord, Tokyo Night, Gruvbox, Ayu, Concrete, and more)\n• Undo/redo functionality\n• Live word count and statistics',
|
||||
buttons: ['OK']
|
||||
});
|
||||
}
|
||||
@@ -533,6 +535,44 @@ function showBatchConversionDialog() {
|
||||
mainWindow.webContents.send('show-batch-dialog');
|
||||
}
|
||||
|
||||
// Enhanced Word Export with Template Support
|
||||
async function exportWordWithTemplate() {
|
||||
if (!currentFile) {
|
||||
dialog.showErrorBox('Error', 'Please save the file first');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get markdown content
|
||||
const content = fs.readFileSync(currentFile, 'utf-8');
|
||||
|
||||
// Show dialog for output file
|
||||
const result = await dialog.showSaveDialog(mainWindow, {
|
||||
title: 'Export to Word (Enhanced)',
|
||||
defaultPath: currentFile.replace(/\.md$/, '.docx'),
|
||||
filters: [{ name: 'Word Document', extensions: ['docx'] }]
|
||||
});
|
||||
|
||||
if (result.canceled) return;
|
||||
|
||||
// Create exporter instance
|
||||
const exporter = new WordTemplateExporter();
|
||||
|
||||
// Convert markdown to DOCX
|
||||
await exporter.convert(content, result.filePath);
|
||||
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'info',
|
||||
title: 'Export Successful',
|
||||
message: 'Document exported successfully!',
|
||||
detail: `Saved to: ${result.filePath}`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
dialog.showErrorBox('Export Error', `Failed to export document: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Universal File Converter integration
|
||||
function showUniversalConverterDialog() {
|
||||
mainWindow.webContents.send('show-universal-converter-dialog');
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* Word Template Exporter
|
||||
* Converts Markdown to DOCX using custom Word templates
|
||||
* Based on Template_exporter functionality
|
||||
*/
|
||||
|
||||
const { Document, Packer, Paragraph, TextRun, AlignmentType, HeadingLevel, Table, TableRow, TableCell, WidthType, BorderStyle } = require('docx');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class WordTemplateExporter {
|
||||
constructor() {
|
||||
this.inCodeBlock = false;
|
||||
this.inList = false;
|
||||
this.listType = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
async convert(markdownContent, outputPath, templatePath = null) {
|
||||
// Parse markdown and create document elements
|
||||
const children = this.parseMarkdown(markdownContent);
|
||||
|
||||
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
|
||||
}]
|
||||
});
|
||||
|
||||
// Write to file
|
||||
const buffer = await Packer.toBuffer(doc);
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
parseMarkdown(content) {
|
||||
const lines = content.split('\n');
|
||||
const elements = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
|
||||
// Skip empty lines
|
||||
if (!line.trim()) {
|
||||
elements.push(new Paragraph({ text: '' }));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Code blocks
|
||||
if (line.trim().startsWith('```')) {
|
||||
const codeLines = [];
|
||||
i++;
|
||||
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
||||
codeLines.push(lines[i]);
|
||||
i++;
|
||||
}
|
||||
elements.push(this.createCodeBlock(codeLines.join('\n')));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Headers
|
||||
if (line.trim().startsWith('#')) {
|
||||
elements.push(this.createHeading(line));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Blockquotes
|
||||
if (line.trim().startsWith('>')) {
|
||||
elements.push(this.createQuote(line));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ordered lists
|
||||
if (/^\s*\d+\.\s+/.test(line)) {
|
||||
elements.push(this.createListItem(line, true));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unordered lists
|
||||
if (/^\s*[-*+]\s+/.test(line)) {
|
||||
elements.push(this.createListItem(line, false));
|
||||
i++;
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normal paragraph
|
||||
elements.push(this.createParagraph(line));
|
||||
i++;
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
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();
|
||||
const runs = this.parseInlineFormatting(text);
|
||||
|
||||
return new Paragraph({
|
||||
children: runs,
|
||||
indent: { left: 720 }, // 0.5 inch indent
|
||||
italics: true
|
||||
});
|
||||
}
|
||||
|
||||
createListItem(line, numbered) {
|
||||
const text = line.replace(/^\s*(\d+\.|-|\*|\+)\s+/, '').trim();
|
||||
const runs = this.parseInlineFormatting(text);
|
||||
|
||||
return new Paragraph({
|
||||
children: runs,
|
||||
numbering: {
|
||||
reference: numbered ? 'numbering' : 'bullets',
|
||||
level: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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) => {
|
||||
const cells = line.split('|').filter(cell => cell.trim()).map(cell => cell.trim());
|
||||
|
||||
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 }
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
return new Table({
|
||||
rows: tableRows,
|
||||
width: { size: 100, type: WidthType.PERCENTAGE }
|
||||
});
|
||||
}
|
||||
|
||||
parseInlineFormatting(text) {
|
||||
const runs = [];
|
||||
let pos = 0;
|
||||
|
||||
// Patterns for inline formatting
|
||||
const patterns = [
|
||||
{ regex: /\*\*\*(.+?)\*\*\*/g, bold: true, italics: true },
|
||||
{ regex: /\*\*(.+?)\*\*/g, bold: true },
|
||||
{ regex: /\*(.+?)\*/g, italics: true },
|
||||
{ regex: /`(.+?)`/g, code: true },
|
||||
{ regex: /\[([^\]]+)\]\(([^\)]+)\)/g, link: true }
|
||||
];
|
||||
|
||||
// Simple implementation: find all matches and create runs
|
||||
let segments = [{ text: text, format: {} }];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const newSegments = [];
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
segments = newSegments;
|
||||
}
|
||||
|
||||
// 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
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WordTemplateExporter;
|
||||
Reference in New Issue
Block a user