mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-03 02:11:07 +05:30
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e11176290 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pan-converter",
|
"name": "pan-converter",
|
||||||
"version": "1.8.1",
|
"version": "1.8.3",
|
||||||
"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": {
|
||||||
|
|||||||
@@ -515,6 +515,7 @@
|
|||||||
<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="docx-enhanced">DOCX (Enhanced)</option>
|
||||||
<option value="latex">LaTeX</option>
|
<option value="latex">LaTeX</option>
|
||||||
|
|||||||
+217
-6
@@ -49,6 +49,7 @@ let mainWindow;
|
|||||||
let currentFile = null; // This will now represent the active tab's file
|
let currentFile = null; // This will now represent the active tab's file
|
||||||
let pandocAvailable = null; // Cache pandoc availability check
|
let pandocAvailable = null; // Cache pandoc availability check
|
||||||
let wordTemplatePath = null; // Path to selected Word template
|
let wordTemplatePath = null; // Path to selected Word template
|
||||||
|
let templateStartPage = 3; // Which page to start inserting content (default: page 3)
|
||||||
let rendererReady = false; // Track if renderer is ready to receive file data
|
let rendererReady = false; // Track if renderer is ready to receive file data
|
||||||
|
|
||||||
// Handle single instance lock for Windows file association
|
// Handle single instance lock for Windows file association
|
||||||
@@ -238,6 +239,7 @@ function createMenu() {
|
|||||||
submenu: [
|
submenu: [
|
||||||
{ label: 'HTML', click: () => exportFile('html') },
|
{ label: 'HTML', click: () => exportFile('html') },
|
||||||
{ label: 'PDF', click: () => exportFile('pdf') },
|
{ label: 'PDF', click: () => exportFile('pdf') },
|
||||||
|
{ label: 'PDF (Enhanced)', click: () => exportPDFViaWordTemplate(), accelerator: 'Ctrl+Shift+P' },
|
||||||
{ label: 'DOCX', click: () => exportFile('docx') },
|
{ label: 'DOCX', click: () => exportFile('docx') },
|
||||||
{ label: 'DOCX (Enhanced)', click: () => exportWordWithTemplate(), accelerator: 'Ctrl+Shift+W' },
|
{ label: 'DOCX (Enhanced)', click: () => exportWordWithTemplate(), accelerator: 'Ctrl+Shift+W' },
|
||||||
{ label: 'LaTeX', click: () => exportFile('latex') },
|
{ label: 'LaTeX', click: () => exportFile('latex') },
|
||||||
@@ -256,6 +258,10 @@ function createMenu() {
|
|||||||
label: 'Select Word Template...',
|
label: 'Select Word Template...',
|
||||||
click: selectWordTemplate
|
click: selectWordTemplate
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Template Settings...',
|
||||||
|
click: showTemplateSettings
|
||||||
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Quit',
|
label: 'Quit',
|
||||||
@@ -475,7 +481,7 @@ function createMenu() {
|
|||||||
type: 'info',
|
type: 'info',
|
||||||
title: 'About PanConverter',
|
title: 'About PanConverter',
|
||||||
message: 'PanConverter',
|
message: 'PanConverter',
|
||||||
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.8.1\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Modern glassmorphism UI with gradient backgrounds\n• Streamlined PDF Editor UI (merge, split, compress, rotate, watermark, encrypt)\n• Universal File Converter (LibreOffice, ImageMagick, FFmpeg, Pandoc)\n• Windows Explorer context menu integration\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Real-time preview updates while typing\n• Full toolbar markdown editing functions\n• Enhanced PDF export with built-in Electron fallback\n• Enhanced Word export with template support (single file & batch)\n• File association support for .md files\n• Command-line interface for batch conversion\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Improved preview typography and spacing\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• Complete theme support including Monokai fixes\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• 22 beautiful themes (including Dracula, Nord, Tokyo Night, Gruvbox, Ayu, Concrete, and more)\n• Undo/redo functionality\n• Live word count and statistics',
|
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.8.3\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Modern glassmorphism UI with gradient backgrounds\n• Enhanced PDF export via Word template with configurable start page\n• Configurable template settings (start page selection)\n• Streamlined PDF Editor UI (merge, split, compress, rotate, watermark, encrypt)\n• Universal File Converter (LibreOffice, ImageMagick, FFmpeg, Pandoc)\n• Windows Explorer context menu integration\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Real-time preview updates while typing\n• Full toolbar markdown editing functions\n• Enhanced PDF export with built-in Electron fallback\n• Enhanced Word export with template support (single file & batch)\n• File association support for .md files\n• Command-line interface for batch conversion\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Improved preview typography and spacing\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• Complete theme support including Monokai fixes\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• 22 beautiful themes (including Dracula, Nord, Tokyo Night, Gruvbox, Ayu, Concrete, and more)\n• Undo/redo functionality\n• Live word count and statistics',
|
||||||
buttons: ['OK']
|
buttons: ['OK']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -562,6 +568,56 @@ async function selectWordTemplate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Template Settings Dialog
|
||||||
|
async function showTemplateSettings() {
|
||||||
|
const result = await dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'question',
|
||||||
|
title: 'Template Settings',
|
||||||
|
message: 'Configure Word Template Export',
|
||||||
|
detail: `Current template: ${wordTemplatePath ? path.basename(wordTemplatePath) : 'Default template'}\nContent starts from page: ${templateStartPage}\n\nWhich page should content start from?\n(Templates usually have cover pages, TOC, etc.)`,
|
||||||
|
buttons: ['Page 1', 'Page 2', 'Page 3', 'Page 4', 'Page 5', 'Custom...', 'Cancel'],
|
||||||
|
defaultId: templateStartPage - 1,
|
||||||
|
cancelId: 6
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.response === 6) return; // Cancel
|
||||||
|
|
||||||
|
let newStartPage;
|
||||||
|
if (result.response === 5) { // Custom
|
||||||
|
// Show input dialog for custom page number
|
||||||
|
mainWindow.webContents.send('show-custom-start-page-dialog', templateStartPage);
|
||||||
|
} else {
|
||||||
|
newStartPage = result.response + 1; // Convert button index to page number
|
||||||
|
templateStartPage = newStartPage;
|
||||||
|
store.set('templateStartPage', templateStartPage);
|
||||||
|
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'info',
|
||||||
|
title: 'Settings Updated',
|
||||||
|
message: 'Template settings have been updated',
|
||||||
|
detail: `Content will now start from page ${templateStartPage}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle custom start page input from renderer
|
||||||
|
ipcMain.on('set-custom-start-page', (event, pageNumber) => {
|
||||||
|
const page = parseInt(pageNumber);
|
||||||
|
if (page >= 1 && page <= 100) {
|
||||||
|
templateStartPage = page;
|
||||||
|
store.set('templateStartPage', templateStartPage);
|
||||||
|
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'info',
|
||||||
|
title: 'Settings Updated',
|
||||||
|
message: 'Template settings have been updated',
|
||||||
|
detail: `Content will now start from page ${templateStartPage}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dialog.showErrorBox('Invalid Page Number', 'Please enter a page number between 1 and 100');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Enhanced Word Export with Template Support
|
// Enhanced Word Export with Template Support
|
||||||
async function exportWordWithTemplate() {
|
async function exportWordWithTemplate() {
|
||||||
if (!currentFile) {
|
if (!currentFile) {
|
||||||
@@ -582,8 +638,8 @@ async function exportWordWithTemplate() {
|
|||||||
|
|
||||||
if (result.canceled) return;
|
if (result.canceled) return;
|
||||||
|
|
||||||
// Create exporter instance with selected template (if any)
|
// Create exporter instance with selected template and start page
|
||||||
const exporter = new WordTemplateExporter(wordTemplatePath);
|
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
|
||||||
|
|
||||||
// Convert markdown to DOCX
|
// Convert markdown to DOCX
|
||||||
await exporter.convert(content, result.filePath);
|
await exporter.convert(content, result.filePath);
|
||||||
@@ -600,6 +656,79 @@ async function exportWordWithTemplate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced PDF Export via Word Template
|
||||||
|
async function exportPDFViaWordTemplate() {
|
||||||
|
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 PDF (Enhanced)',
|
||||||
|
defaultPath: currentFile.replace(/\.md$/, '.pdf'),
|
||||||
|
filters: [{ name: 'PDF Document', extensions: ['pdf'] }]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.canceled) return;
|
||||||
|
|
||||||
|
// Step 1: Create temporary DOCX file using Word template
|
||||||
|
const tempDocxPath = result.filePath.replace(/\.pdf$/, '_temp.docx');
|
||||||
|
|
||||||
|
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
|
||||||
|
await exporter.convert(content, tempDocxPath);
|
||||||
|
|
||||||
|
// Step 2: Convert DOCX to PDF using LibreOffice
|
||||||
|
const soffice = process.platform === 'win32'
|
||||||
|
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
|
||||||
|
: 'soffice';
|
||||||
|
|
||||||
|
const outputDir = path.dirname(result.filePath);
|
||||||
|
const convertCmd = `${soffice} --headless --convert-to pdf --outdir "${outputDir}" "${tempDocxPath}"`;
|
||||||
|
|
||||||
|
exec(convertCmd, (error, stdout, stderr) => {
|
||||||
|
// Clean up temporary DOCX file
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(tempDocxPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to delete temp file:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
dialog.showErrorBox('PDF Conversion Error',
|
||||||
|
`Failed to convert to PDF. Please ensure LibreOffice is installed.\n\nError: ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LibreOffice creates file with same base name as input
|
||||||
|
const generatedPdfPath = tempDocxPath.replace(/\.docx$/, '.pdf');
|
||||||
|
|
||||||
|
// Rename if needed
|
||||||
|
if (generatedPdfPath !== result.filePath) {
|
||||||
|
try {
|
||||||
|
fs.renameSync(generatedPdfPath, result.filePath);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to rename PDF:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'info',
|
||||||
|
title: 'Export Successful',
|
||||||
|
message: 'PDF exported successfully using Word template!',
|
||||||
|
detail: `Saved to: ${result.filePath}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
dialog.showErrorBox('Export Error', `Failed to export PDF: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Universal File Converter integration
|
// Universal File Converter integration
|
||||||
function showUniversalConverterDialog() {
|
function showUniversalConverterDialog() {
|
||||||
mainWindow.webContents.send('show-universal-converter-dialog');
|
mainWindow.webContents.send('show-universal-converter-dialog');
|
||||||
@@ -1447,7 +1576,9 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
|
|||||||
const inputFile = markdownFiles[index];
|
const inputFile = markdownFiles[index];
|
||||||
const relativePath = path.relative(inputFolder, inputFile);
|
const relativePath = path.relative(inputFolder, inputFile);
|
||||||
const baseName = path.basename(relativePath, path.extname(relativePath));
|
const baseName = path.basename(relativePath, path.extname(relativePath));
|
||||||
const outputExtension = format === 'docx-enhanced' ? 'docx' : format;
|
let outputExtension = format;
|
||||||
|
if (format === 'docx-enhanced') outputExtension = 'docx';
|
||||||
|
if (format === 'pdf-enhanced') outputExtension = 'pdf';
|
||||||
const outputFile = path.join(outputFolder, relativePath.replace(/\.(md|markdown)$/i, `.${outputExtension}`));
|
const outputFile = path.join(outputFolder, relativePath.replace(/\.(md|markdown)$/i, `.${outputExtension}`));
|
||||||
|
|
||||||
// Create subdirectories in output folder if needed
|
// Create subdirectories in output folder if needed
|
||||||
@@ -1460,7 +1591,7 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
|
|||||||
if (format === 'docx-enhanced') {
|
if (format === 'docx-enhanced') {
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(inputFile, 'utf-8');
|
const content = fs.readFileSync(inputFile, 'utf-8');
|
||||||
const exporter = new WordTemplateExporter(wordTemplatePath);
|
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
|
||||||
await exporter.convert(content, outputFile);
|
await exporter.convert(content, outputFile);
|
||||||
|
|
||||||
completedCount++;
|
completedCount++;
|
||||||
@@ -1490,6 +1621,85 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle PDF Enhanced format with Word Template → PDF conversion
|
||||||
|
if (format === 'pdf-enhanced') {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(inputFile, 'utf-8');
|
||||||
|
|
||||||
|
// Step 1: Create temporary DOCX file using Word template
|
||||||
|
const tempDocxPath = outputFile.replace(/\.pdf$/, '_temp.docx');
|
||||||
|
const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage);
|
||||||
|
await exporter.convert(content, tempDocxPath);
|
||||||
|
|
||||||
|
// Step 2: Convert DOCX to PDF using LibreOffice
|
||||||
|
const soffice = process.platform === 'win32'
|
||||||
|
? '"C:\\Program Files\\LibreOffice\\program\\soffice.exe"'
|
||||||
|
: 'soffice';
|
||||||
|
|
||||||
|
const outputDir = path.dirname(outputFile);
|
||||||
|
const convertCmd = `${soffice} --headless --convert-to pdf --outdir "${outputDir}" "${tempDocxPath}"`;
|
||||||
|
|
||||||
|
exec(convertCmd, (error, stdout, stderr) => {
|
||||||
|
// Clean up temporary DOCX file
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(tempDocxPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to delete temp file:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// Update progress with error
|
||||||
|
mainWindow.webContents.send('batch-progress', {
|
||||||
|
completed: index + 1,
|
||||||
|
total: totalCount,
|
||||||
|
currentFile: path.basename(inputFile),
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
processNextFile(index + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LibreOffice creates file with same base name as input
|
||||||
|
const generatedPdfPath = tempDocxPath.replace(/\.docx$/, '.pdf');
|
||||||
|
|
||||||
|
// Rename if needed
|
||||||
|
if (generatedPdfPath !== outputFile) {
|
||||||
|
try {
|
||||||
|
fs.renameSync(generatedPdfPath, outputFile);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to rename PDF:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completedCount++;
|
||||||
|
|
||||||
|
// Update progress
|
||||||
|
mainWindow.webContents.send('batch-progress', {
|
||||||
|
completed: index + 1,
|
||||||
|
total: totalCount,
|
||||||
|
currentFile: path.basename(inputFile),
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process next file
|
||||||
|
processNextFile(index + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Update progress with error
|
||||||
|
mainWindow.webContents.send('batch-progress', {
|
||||||
|
completed: index + 1,
|
||||||
|
total: totalCount,
|
||||||
|
currentFile: path.basename(inputFile),
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process next file even if this one failed
|
||||||
|
processNextFile(index + 1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Build pandoc command for other formats
|
// Build pandoc command for other formats
|
||||||
let pandocCmd = `${getPandocPath()} "${inputFile}" -o "${outputFile}"`;
|
let pandocCmd = `${getPandocPath()} "${inputFile}" -o "${outputFile}"`;
|
||||||
|
|
||||||
@@ -1692,8 +1902,9 @@ function buildPandocCommand(content, format, outputPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
// Load saved Word template path
|
// Load saved Word template path and settings
|
||||||
wordTemplatePath = store.get('wordTemplatePath', null);
|
wordTemplatePath = store.get('wordTemplatePath', null);
|
||||||
|
templateStartPage = store.get('templateStartPage', 3);
|
||||||
|
|
||||||
// Check for command line conversion requests
|
// Check for command line conversion requests
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|||||||
+13
-82
@@ -1184,95 +1184,26 @@ function handlePrintPreview(withStyles) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store original display values for restoration
|
console.log('[RENDERER] Starting print with withStyles:', withStyles);
|
||||||
const elementsToHide = [];
|
console.log('[RENDERER] Preview content length:', previewContent.innerHTML.length);
|
||||||
|
|
||||||
// Hide toolbar
|
// Add body classes for print mode - let CSS handle everything
|
||||||
const toolbar = document.querySelector('.toolbar');
|
document.body.classList.add('printing');
|
||||||
if (toolbar) {
|
if (!withStyles) {
|
||||||
elementsToHide.push({ elem: toolbar, display: toolbar.style.display });
|
document.body.classList.add('printing-no-styles');
|
||||||
toolbar.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide tab bar
|
// Wait for CSS to apply then print
|
||||||
const tabBar = document.querySelector('.tab-bar');
|
|
||||||
if (tabBar) {
|
|
||||||
elementsToHide.push({ elem: tabBar, display: tabBar.style.display });
|
|
||||||
tabBar.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide status bar
|
|
||||||
const statusBar = document.querySelector('.status-bar');
|
|
||||||
if (statusBar) {
|
|
||||||
elementsToHide.push({ elem: statusBar, display: statusBar.style.display });
|
|
||||||
statusBar.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide editor pane
|
|
||||||
const editorPane = document.getElementById(`editor-pane-${activeTabId}`);
|
|
||||||
if (editorPane) {
|
|
||||||
elementsToHide.push({ elem: editorPane, display: editorPane.style.display });
|
|
||||||
editorPane.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide all dialogs
|
|
||||||
const dialogs = document.querySelectorAll('.dialog, .modal-overlay');
|
|
||||||
dialogs.forEach(dialog => {
|
|
||||||
elementsToHide.push({ elem: dialog, display: dialog.style.display });
|
|
||||||
dialog.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Make preview pane full width
|
|
||||||
const previewPane = document.getElementById(`preview-pane-${activeTabId}`);
|
|
||||||
let originalPreviewStyles = {};
|
|
||||||
if (previewPane) {
|
|
||||||
originalPreviewStyles = {
|
|
||||||
position: previewPane.style.position,
|
|
||||||
top: previewPane.style.top,
|
|
||||||
left: previewPane.style.left,
|
|
||||||
width: previewPane.style.width,
|
|
||||||
height: previewPane.style.height,
|
|
||||||
margin: previewPane.style.margin,
|
|
||||||
padding: previewPane.style.padding
|
|
||||||
};
|
|
||||||
previewPane.style.position = 'absolute';
|
|
||||||
previewPane.style.top = '0';
|
|
||||||
previewPane.style.left = '0';
|
|
||||||
previewPane.style.width = '100%';
|
|
||||||
previewPane.style.height = 'auto';
|
|
||||||
previewPane.style.margin = '0';
|
|
||||||
previewPane.style.padding = '20px';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply no-styles if needed
|
|
||||||
if (!withStyles && previewContent) {
|
|
||||||
previewContent.style.color = '#000';
|
|
||||||
previewContent.style.background = '#fff';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for DOM updates then print
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
console.log('[RENDERER] Sending do-print to main process');
|
||||||
ipcRenderer.send('do-print', { withStyles });
|
ipcRenderer.send('do-print', { withStyles });
|
||||||
|
|
||||||
// Restore everything after print dialog opens
|
// Restore classes after print dialog opens
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Restore hidden elements
|
document.body.classList.remove('printing', 'printing-no-styles');
|
||||||
elementsToHide.forEach(({ elem, display }) => {
|
console.log('[RENDERER] Print classes removed');
|
||||||
elem.style.display = display;
|
}, 1000);
|
||||||
});
|
}, 100);
|
||||||
|
|
||||||
// Restore preview pane styles
|
|
||||||
if (previewPane) {
|
|
||||||
Object.assign(previewPane.style, originalPreviewStyles);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore preview content styles
|
|
||||||
if (!withStyles && previewContent) {
|
|
||||||
previewContent.style.color = '';
|
|
||||||
previewContent.style.background = '';
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}, 200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export Dialog functionality
|
// Export Dialog functionality
|
||||||
|
|||||||
+28
-2
@@ -2945,18 +2945,33 @@ body.theme-concrete-warm .status-bar {
|
|||||||
.editor-container,
|
.editor-container,
|
||||||
#status-bar,
|
#status-bar,
|
||||||
.status-bar,
|
.status-bar,
|
||||||
|
.toolbar,
|
||||||
|
.tab-bar,
|
||||||
.editor-pane,
|
.editor-pane,
|
||||||
[id^="editor-pane-"] {
|
[id^="editor-pane-"],
|
||||||
|
.dialog,
|
||||||
|
.modal-overlay,
|
||||||
|
.export-dialog {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure body and html take full width */
|
||||||
|
html, body {
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Show preview in full width */
|
/* Show preview in full width */
|
||||||
#preview,
|
#preview,
|
||||||
.preview,
|
.preview,
|
||||||
.preview-content,
|
.preview-content,
|
||||||
[id^="preview-"],
|
[id^="preview-"],
|
||||||
[id^="preview-pane-"],
|
[id^="preview-pane-"],
|
||||||
.tab-content {
|
.tab-content,
|
||||||
|
.pane {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
@@ -2965,6 +2980,10 @@ body.theme-concrete-warm .status-bar {
|
|||||||
padding: 20px !important;
|
padding: 20px !important;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
position: static !important;
|
position: static !important;
|
||||||
|
left: auto !important;
|
||||||
|
right: auto !important;
|
||||||
|
top: auto !important;
|
||||||
|
bottom: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Optimize preview content for printing */
|
/* Optimize preview content for printing */
|
||||||
@@ -2976,6 +2995,13 @@ body.theme-concrete-warm .status-bar {
|
|||||||
background: white !important;
|
background: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* No-styles mode for printing without theme colors */
|
||||||
|
body.printing-no-styles .preview-content,
|
||||||
|
body.printing-no-styles [id^="preview-"] {
|
||||||
|
color: #000 !important;
|
||||||
|
background: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Text formatting */
|
/* Text formatting */
|
||||||
.preview-content h1,
|
.preview-content h1,
|
||||||
.preview-content h2,
|
.preview-content h2,
|
||||||
|
|||||||
+24
-10
@@ -10,8 +10,9 @@ const PizZip = require('pizzip');
|
|||||||
const Docx = require('docx4js').default;
|
const Docx = require('docx4js').default;
|
||||||
|
|
||||||
class WordTemplateExporter {
|
class WordTemplateExporter {
|
||||||
constructor(templatePath) {
|
constructor(templatePath, startPage = 3) {
|
||||||
this.templatePath = templatePath || path.join(__dirname, '../word_template.docx');
|
this.templatePath = templatePath || path.join(__dirname, '../word_template.docx');
|
||||||
|
this.startPage = startPage; // Which page to start inserting content
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,8 +30,8 @@ class WordTemplateExporter {
|
|||||||
// Parse markdown and generate Word XML
|
// Parse markdown and generate Word XML
|
||||||
const newContentXml = this.markdownToWordXml(markdownContent);
|
const newContentXml = this.markdownToWordXml(markdownContent);
|
||||||
|
|
||||||
// Insert new content after page 2 (after the section break)
|
// Insert new content after the specified start page
|
||||||
const modifiedXml = this.insertContentAfterPage2(documentXml, newContentXml);
|
const modifiedXml = this.insertContentAfterPage(documentXml, newContentXml, this.startPage);
|
||||||
|
|
||||||
// Update the zip with modified XML
|
// Update the zip with modified XML
|
||||||
zip.file('word/document.xml', modifiedXml);
|
zip.file('word/document.xml', modifiedXml);
|
||||||
@@ -47,20 +48,33 @@ class WordTemplateExporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert markdown content after page 2 in the document
|
* Insert markdown content after the specified page in the document
|
||||||
|
* @param {string} documentXml - The document XML
|
||||||
|
* @param {string} newContentXml - The new content to insert
|
||||||
|
* @param {number} afterPage - Insert content after this page number (1-based)
|
||||||
*/
|
*/
|
||||||
insertContentAfterPage2(documentXml, newContentXml) {
|
insertContentAfterPage(documentXml, newContentXml, afterPage) {
|
||||||
// Find the last section break (after page 2)
|
// Find section breaks that mark page boundaries
|
||||||
// Look for the section properties tag that marks page breaks
|
// Look for the section properties tag that marks page breaks
|
||||||
const sectionBreakRegex = /<w:sectPr[^>]*>[\s\S]*?<\/w:sectPr>/g;
|
const sectionBreakRegex = /<w:sectPr[^>]*>[\s\S]*?<\/w:sectPr>/g;
|
||||||
const matches = [...documentXml.matchAll(sectionBreakRegex)];
|
const matches = [...documentXml.matchAll(sectionBreakRegex)];
|
||||||
|
|
||||||
if (matches.length >= 2) {
|
// Calculate which section break to insert after
|
||||||
// Insert after the 2nd section break
|
// Page 1 = before 1st section break
|
||||||
const insertPoint = matches[1].index + matches[1][0].length;
|
// 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);
|
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 {
|
} else {
|
||||||
// If no section breaks found, insert before closing body tag
|
// If not enough section breaks, insert before closing body tag
|
||||||
return documentXml.replace('</w:body>', newContentXml + '</w:body>');
|
return documentXml.replace('</w:body>', newContentXml + '</w:body>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user