diff --git a/package.json b/package.json index fbd562a..ec74add 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "markdown-converter", "version": "4.4.2", "description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting", - "main": "src/main.js", + "main": "src/main/index.js", "scripts": { "start": "electron .", "test": "jest", diff --git a/src/main.js b/src/main.js index 5164e72..c18b140 100644 --- a/src/main.js +++ b/src/main.js @@ -1,3422 +1,3 @@ -const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron'); -const path = require('path'); -const fs = require('fs'); -const { exec, execFile, spawn } = require('child_process'); -const WordTemplateExporter = require('./main/word-template'); -const PDFOperations = require('./main/PDFOperations'); -const GitOperations = require('./main/GitOperations'); -const { getAllowedDirectories, validatePath, resolveWritablePath, isPathAccessible } = require('./main/utils/paths'); -const fileOps = require('./main/files'); -const menu = require('./main/menu'); -const { createMainWindow } = require('./main/window'); - -// Add MiKTeX to PATH for LaTeX support -if (process.platform === 'win32') { - const miktexPath = 'C:\\Program Files\\MiKTeX\\miktex\\bin\\x64'; - if (fs.existsSync(miktexPath)) { - process.env.PATH = `${miktexPath};${process.env.PATH}`; - console.log('[MAIN] Added MiKTeX to PATH:', miktexPath); - } -} - -// Returns path to pandoc: bundled binary when packaged, dev bin or system fallback otherwise. -function getPandocPath() { - if (app.isPackaged) { - const ext = process.platform === 'win32' ? '.exe' : ''; - return path.join(process.resourcesPath, 'bin', `pandoc${ext}`); - } - // Development: prefer locally-downloaded binary in bin// - const devBin = path.join( - __dirname, - '..', - 'bin', - process.platform, - process.platform === 'win32' ? 'pandoc.exe' : 'pandoc', - ); - if (fs.existsSync(devBin)) return devBin; - return 'pandoc'; -} - -// Returns path to ffmpeg: asar-unpacked bundled binary when packaged, system fallback otherwise. -function getFFmpegPath() { - try { - let ffmpegPath = require('ffmpeg-static'); - if (app.isPackaged) { - // ffmpeg-static is in asarUnpack — rewrite the path to the unpacked location - ffmpegPath = ffmpegPath.replace('app.asar' + path.sep, 'app.asar.unpacked' + path.sep); - } - if (fs.existsSync(ffmpegPath)) return ffmpegPath; - } catch (_) { - /* ffmpeg-static not available */ - } - return process.platform === 'win32' ? 'ffmpeg' : 'ffmpeg'; -} - -// Check if Pandoc is available -function checkPandocAvailable() { - return new Promise((resolve) => { - const pandocPath = getPandocPath(); - execFile(pandocPath, ['--version'], (error) => { - resolve(!error); - }); - }); -} - -/** - * Safe command execution using execFile (no shell injection risk) - * @param {string} command - The command to execute - * @param {string[]} args - Array of arguments - * @param {object} options - Options for execFile - * @returns {Promise<{stdout: string, stderr: string}>} - */ -function safeExecFile(command, args, options = {}) { - return new Promise((resolve, reject) => { - execFile(command, args, { maxBuffer: 10 * 1024 * 1024, ...options }, (error, stdout, stderr) => { - if (error) { - reject({ error, stdout, stderr }); - } else { - resolve({ stdout, stderr }); - } - }); - }); -} - -// File size validation -const MAX_FILE_SIZE_MB = 50; -const MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024; - -// Sanitize error messages to strip absolute file paths -function sanitizeErrorMessage(message) { - if (typeof message !== 'string') return String(message); - // Strip absolute Windows paths, keeping only filename - return message - .replace(/[A-Z]:\\[^\s"']+\\([^\s"'\\]+)/gi, '$1') - .replace(/\/[^\s"']+\/([^\s"'/]+)/g, '$1'); -} - -// Rate limiter for conversions -function createRateLimiter(minIntervalMs = 2000) { - let lastCall = 0; - return function canProceed() { - const now = Date.now(); - if (now - lastCall < minIntervalMs) return false; - lastCall = now; - return true; - }; -} -const conversionLimiter = createRateLimiter(2000); - -// Convert structured data formats to markdown code blocks -function convertDataToMarkdown(content, format) { - switch (format) { - case 'json': - return '```json\n' + content + '\n```'; - case 'yaml': - case 'yml': - return '```yaml\n' + content + '\n```'; - case 'xml': - return '```xml\n' + content + '\n```'; - case 'toml': - return '```toml\n' + content + '\n```'; - default: - return '```\n' + content + '\n```'; - } -} - -/** - * Run Pandoc command safely with execFile - * @param {string[]} args - Pandoc arguments array - * @param {Function} callback - Callback function (error, stdout, stderr) - */ -function runPandoc(args, callback) { - const pandocPath = getPandocPath(); - execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback); -} - -/** - * Run a Pandoc command string safely using execFile - * Parses the command string and uses execFile to prevent shell injection - * @param {string} cmdString - Full Pandoc command string (e.g., 'pandoc "input.md" -o "output.pdf"') - * @param {Function} callback - Callback function (error, stdout, stderr) - */ -function runPandocCmd(cmdString, callback) { - const parsed = parseCommand(cmdString); - // Skip 'pandoc' if it's the first element (command itself) - // Use path.basename to handle full paths like bin/linux/pandoc - const cmdBase = path.basename(parsed.command).replace(/\.exe$/i, ''); - const args = cmdBase === 'pandoc' ? parsed.args : [parsed.command, ...parsed.args]; - const pandocPath = getPandocPath(); - execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback); -} - -/** - * Parse a command string into command and arguments array - * This helps transition from exec() to execFile() safely - * @param {string} cmdString - Full command string - * @returns {{command: string, args: string[]}} - */ -function parseCommand(cmdString) { - // Handle quoted strings properly - const parts = []; - let current = ''; - let inQuotes = false; - let quoteChar = ''; - - for (let i = 0; i < cmdString.length; i++) { - const char = cmdString[i]; - if ((char === '"' || char === "'") && !inQuotes) { - inQuotes = true; - quoteChar = char; - } else if (char === quoteChar && inQuotes) { - inQuotes = false; - quoteChar = ''; - } else if (char === ' ' && !inQuotes) { - if (current) { - parts.push(current); - current = ''; - } - } else { - current += char; - } - } - if (current) { - parts.push(current); - } - - return { - command: parts[0], - args: parts.slice(1) - }; -} - -// Simple storage implementation to replace electron-store -const settingsPath = path.join(app.getPath('userData'), 'settings.json'); -const store = { - get: (key, defaultValue) => { - try { - const data = fs.readFileSync(settingsPath, 'utf-8'); - const settings = JSON.parse(data); - return settings[key] || defaultValue; - } catch { - return defaultValue; - } - }, - set: (key, value) => { - let settings = {}; - try { - const data = fs.readFileSync(settingsPath, 'utf-8'); - settings = JSON.parse(data); - } catch {} - settings[key] = value; - fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); - } -}; - -// Plugin settings IPC handlers -ipcMain.handle('plugin-settings:get', (_event, key) => { - return store.get(key); -}); -ipcMain.handle('plugin-settings:set', (_event, { key, value }) => { - store.set(key, value); -}); -ipcMain.handle('get-app-version', () => app.getVersion()); - -// ============================================ -// SECURITY: Permission Request Handler -// ============================================ -// Restrict permissions to only those explicitly needed. -// Deny: camera, microphone, geolocation, notifications, etc. -// Allow: clipboard-read, clipboard-write (user expects these) -app.on('web-contents-created', (event, contents) => { - contents.session.setPermissionRequestHandler((webContents, permission, callback) => { - const ALLOWED_PERMISSIONS = [ - 'clipboard-read', - 'clipboard-write' - ]; - - if (ALLOWED_PERMISSIONS.includes(permission)) { - callback(true); - } else { - // Deny all other permissions (camera, microphone, geolocation, etc.) - console.warn(`[Security] Blocked permission request: ${permission}`); - callback(false); - } - }); - - // Disable file download dialogs for security - // contents.session.setDownloadPath(userDownloadsPath); -}); - -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 templateStartPage = 3; // Which page to start inserting content (default: page 3) -let rendererReady = false; // Track if renderer is ready to receive file data - -// Header & Footer Settings -let headerFooterSettings = { - enabled: true, - header: { - left: '', - center: '', - right: '', - logo: null // Will store image file path - }, - footer: { - left: '', - center: '$PAGE$ of $TOTAL$', - right: '', - logo: null - } -}; - -// Page Size Definitions (in twentieths of a point for Word, mm/inches for Pandoc) -const PAGE_SIZES = { - a4: { - name: 'A4', - pandoc: 'a4', - word: { width: 11906, height: 16838 }, // 210×297mm - dimensions: '210×297mm' - }, - a3: { - name: 'A3', - pandoc: 'a3', - word: { width: 16838, height: 23811 }, // 297×420mm - dimensions: '297×420mm' - }, - a5: { - name: 'A5', - pandoc: 'a5', - word: { width: 8391, height: 11906 }, // 148×210mm - dimensions: '148×210mm' - }, - b4: { - name: 'B4', - pandoc: 'b4', - word: { width: 14170, height: 20015 }, // 250×353mm - dimensions: '250×353mm' - }, - b5: { - name: 'B5', - pandoc: 'b5', - word: { width: 9979, height: 14170 }, // 176×250mm - dimensions: '176×250mm' - }, - letter: { - name: 'Letter', - pandoc: 'letter', - word: { width: 12240, height: 15840 }, // 8.5×11in - dimensions: '8.5×11in' - }, - legal: { - name: 'Legal', - pandoc: 'legal', - word: { width: 12240, height: 20160 }, // 8.5×14in - dimensions: '8.5×14in' - }, - tabloid: { - name: 'Tabloid', - pandoc: 'tabloid', - word: { width: 15840, height: 24480 }, // 11×17in - dimensions: '11×17in' - } -}; - -// Default page settings -let pageSettings = { - size: 'a4', - orientation: 'portrait', - customWidth: null, - customHeight: null -}; - -// Handle single instance lock for Windows file association -// When a file is double-clicked and the app is already running, -// Windows tries to start a second instance. We prevent this and -// pass the file to the existing instance instead. -const gotTheLock = app.requestSingleInstanceLock(); - -if (!gotTheLock) { - // Another instance is already running, quit this one - app.quit(); -} else { - // This is the first instance, handle second-instance events - app.on('second-instance', (event, commandLine, workingDirectory) => { - // Someone tried to run a second instance, focus our window instead - if (mainWindow) { - if (mainWindow.isMinimized()) mainWindow.restore(); - mainWindow.focus(); - } - - // Check if a file was passed to the second instance - // commandLine is an array like: ['PanConverter.exe', 'file.md'] - console.log('[MAIN] Second instance commandLine:', JSON.stringify(commandLine)); - const startIndex = app.isPackaged ? 1 : 2; - const fileArgs = commandLine.slice(startIndex); - console.log('[MAIN] Second instance file args:', fileArgs); - for (const arg of fileArgs) { - if ((arg.endsWith('.md') || arg.endsWith('.markdown'))) { - const resolvedPath = path.isAbsolute(arg) ? arg : path.resolve(workingDirectory, arg); - console.log('[MAIN] Second instance resolved path:', resolvedPath); - if (fs.existsSync(resolvedPath)) { - // Open the file in the existing instance - if (rendererReady) { - openFileFromPath(resolvedPath); - } else { - app.pendingFile = resolvedPath; - } - break; - } - } - } - }); -} - -// Check if pandoc is available (using execFile for consistency) -function checkPandocAvailability() { - return new Promise((resolve) => { - if (pandocAvailable !== null) { - resolve(pandocAvailable); - return; - } - - execFile('pandoc', ['--version'], (error, stdout, stderr) => { - pandocAvailable = !error; - resolve(pandocAvailable); - }); - }); -} - - - -// Show About Dialog with logo -function showAboutDialog() { - const aboutWindow = new BrowserWindow({ - width: 500, - height: 600, - parent: mainWindow, - modal: true, - resizable: false, - minimizable: false, - maximizable: false, - webPreferences: { - nodeIntegration: false, - contextIsolation: true - }, - icon: path.join(__dirname, '../assets/icon.png') - }); - - // Convert images to base64 for data URL compatibility - let logoBase64 = ''; - let iconBase64 = ''; - try { - const logoPath = path.join(__dirname, '../assets/logo.png'); - const iconPath = path.join(__dirname, '../assets/icon.png'); - if (fs.existsSync(logoPath)) { - logoBase64 = 'data:image/png;base64,' + fs.readFileSync(logoPath).toString('base64'); - } - if (fs.existsSync(iconPath)) { - iconBase64 = 'data:image/png;base64,' + fs.readFileSync(iconPath).toString('base64'); - } - } catch (e) { - console.error('Error loading about dialog images:', e); - } - - const aboutHTML = ` - - - - - About MarkdownConverter - - - - -

MarkdownConverter

-
Version ${app.getVersion()}
- -
- by - -
- -
-

Features

- -
- -
-

Contact

-

Email: amit.wh@gmail.com

-

Website: GitHub Repository

-
- - - -`; - - aboutWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(aboutHTML)); - aboutWindow.setMenuBarVisibility(false); -} - -// Show Dependencies Dialog -function showDependenciesDialog() { - const depsWindow = new BrowserWindow({ - width: 600, - height: 700, - parent: mainWindow, - modal: true, - resizable: true, - webPreferences: { - nodeIntegration: false, - contextIsolation: true - }, - icon: path.join(__dirname, '../assets/icon.png') - }); - - const depsHTML = ` - - - - - Dependencies & Requirements - - - -

Dependencies & Requirements

- -

Required Dependencies

- -
-
Pandoc Required
-
Universal document converter. Required for most export formats (PDF, DOCX, LaTeX, EPUB).
- https://pandoc.org/installing.html -
- -

Optional Dependencies (for extended features)

- -
-
FFmpeg Optional
-
Required for audio/video conversion and processing.
- https://ffmpeg.org/download.html -
- -
-
ImageMagick Optional
-
Required for image format conversion and processing.
- https://imagemagick.org/script/download.php -
- -
-
LibreOffice Optional
-
Required for enhanced document conversion (Office formats).
- https://www.libreoffice.org/download/download/ -
- -
-
MiKTeX / TeX Live Optional
-
Required for PDF export via LaTeX (higher quality PDFs).
- https://miktex.org/download -
- -

Bundled Libraries

- -
-
pdf-lib
-
PDF manipulation library for merge, split, watermark, and encryption features.
-
- -
-
marked
-
Markdown parser for preview rendering.
-
- -
-
highlight.js
-
Syntax highlighting for code blocks.
-
- -
-
DOMPurify
-
HTML sanitization for security.
-
- -`; - - depsWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(depsHTML)); - depsWindow.setMenuBarVisibility(false); -} - -// Open PDF File for viewing/editing -function openPDFFile() { - const files = dialog.showOpenDialogSync(mainWindow, { - properties: ['openFile'], - filters: [ - { name: 'PDF Files', extensions: ['pdf'] } - ] - }); - - if (files && files[0]) { - const stats = fs.statSync(files[0]); - if (stats.size > MAX_FILE_SIZE) { - dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); - return; - } - mainWindow.webContents.send('open-pdf-viewer', files[0]); - } -} - -function openFile() { - const files = dialog.showOpenDialogSync(mainWindow, { - properties: ['openFile'], - filters: [ - { name: 'Markdown', extensions: ['md', 'markdown'] }, - { name: 'Developer Formats', extensions: ['json', 'yaml', 'yml', 'xml', 'toml'] }, - { name: 'All Files', extensions: ['*'] } - ] - }); - - if (files && files[0]) { - const stats = fs.statSync(files[0]); - if (stats.size > MAX_FILE_SIZE) { - dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); - return; - } - currentFile = files[0]; - const ext = path.extname(currentFile).toLowerCase().slice(1); - let content = fs.readFileSync(currentFile, 'utf-8'); - - // Wrap developer format files in code blocks for markdown display - const devFormats = ['json', 'yaml', 'yml', 'xml', 'toml']; - if (devFormats.includes(ext)) { - content = convertDataToMarkdown(content, ext); - } - - mainWindow.webContents.send('file-opened', { path: currentFile, content }); - } -} - -function openPdfFile() { - const files = dialog.showOpenDialogSync(mainWindow, { - properties: ['openFile'], - filters: [ - { name: 'PDF Files', extensions: ['pdf'] }, - { name: 'All Files', extensions: ['*'] } - ] - }); - - if (files && files[0]) { - const stats = fs.statSync(files[0]); - if (stats.size > MAX_FILE_SIZE) { - dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); - return; - } - mainWindow.webContents.send('open-pdf-file', files[0]); - } -} - -function saveAsFile() { - const file = dialog.showSaveDialogSync(mainWindow, { - defaultExt: '.md', - filters: [ - { name: 'Markdown', extensions: ['md', 'markdown'] }, - { name: 'All Files', extensions: ['*'] } - ] - }); - - if (file) { - currentFile = file; - mainWindow.webContents.send('get-content-for-save', file); - } -} - -function exportFile(format) { - if (!currentFile) { - dialog.showErrorBox('Error', 'Please save the file first'); - return; - } - - // Show export options dialog - showExportOptionsDialog(format); -} - -function showExportOptionsDialog(format) { - mainWindow.webContents.send('show-export-dialog', format); -} - -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)}` - }); - } -} - -// 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'); - } -}); - -// Header & Footer Settings IPC Handlers - -// Get current header/footer settings -ipcMain.on('get-header-footer-settings', (event) => { - event.reply('header-footer-settings-data', headerFooterSettings); -}); - -// Save header/footer settings -ipcMain.on('save-header-footer-settings', (event, settings) => { - headerFooterSettings = settings; - store.set('headerFooterSettings', headerFooterSettings); - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Settings Saved', - message: 'Header and footer settings have been saved successfully!', - buttons: ['OK'] - }); -}); - -// Get current page settings -ipcMain.on('get-page-settings', (event) => { - event.reply('page-settings-data', pageSettings); -}); - -// Update page settings from export dialog -ipcMain.on('update-page-settings', (event, settings) => { - pageSettings = settings; - store.set('pageSettings', pageSettings); - console.log('Page settings updated:', pageSettings); -}); - -// Save header/footer logo image -// Browse for header/footer logo -ipcMain.on('browse-header-footer-logo', async (event, position) => { - try { - const result = await dialog.showOpenDialog(mainWindow, { - title: `Select ${position.charAt(0).toUpperCase() + position.slice(1)} Logo/Image`, - filters: [ - { name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp'] } - ], - properties: ['openFile'] - }); - - if (!result.canceled && result.filePaths.length > 0) { - const filePath = result.filePaths[0]; - - // Copy image to userData directory for persistent storage - const userDataPath = app.getPath('userData'); - const logoDir = path.join(userDataPath, 'logos'); - - // Create logos directory if it doesn't exist - if (!fs.existsSync(logoDir)) { - fs.mkdirSync(logoDir, { recursive: true }); - } - - // Generate unique filename - const ext = path.extname(filePath); - const filename = `${position}_${Date.now()}${ext}`; - const destPath = path.join(logoDir, filename); - - // Copy file - fs.copyFileSync(filePath, destPath); - - // Update settings - if (position === 'header') { - headerFooterSettings.header.logo = destPath; - } else if (position === 'footer') { - headerFooterSettings.footer.logo = destPath; - } - - event.reply('header-footer-logo-saved', { position, path: destPath }); - } - } catch (error) { - console.error('Logo browse error:', error); - dialog.showErrorBox('Logo Error', sanitizeErrorMessage(`Failed to select logo: ${error.message}`)); - } -}); - -ipcMain.on('save-header-footer-logo', async (event, { position, filePath }) => { - try { - if (!filePath) { - dialog.showErrorBox('Logo Error', 'Failed to save logo: The "path" argument must be of type string. Received undefined'); - return; - } - - // Copy image to userData directory for persistent storage - const userDataPath = app.getPath('userData'); - const logoDir = path.join(userDataPath, 'logos'); - - // Create logos directory if it doesn't exist - if (!fs.existsSync(logoDir)) { - fs.mkdirSync(logoDir, { recursive: true }); - } - - // Verify source file exists - if (!fs.existsSync(filePath)) { - dialog.showErrorBox('Logo Error', sanitizeErrorMessage(`Source file not found: ${filePath}`)); - return; - } - - // Generate unique filename - const ext = path.extname(filePath); - const filename = `${position}_${Date.now()}${ext}`; - const destPath = path.join(logoDir, filename); - - // Copy file - fs.copyFileSync(filePath, destPath); - - // Update settings - if (position === 'header') { - headerFooterSettings.header.logo = destPath; - } else if (position === 'footer') { - headerFooterSettings.footer.logo = destPath; - } - - event.reply('header-footer-logo-saved', { position, path: destPath }); - } catch (error) { - console.error('Logo save error:', error); - dialog.showErrorBox('Logo Error', sanitizeErrorMessage(`Failed to save logo: ${error.message}`)); - } -}); - -// Clear header/footer logo -ipcMain.on('clear-header-footer-logo', (event, position) => { - if (position === 'header') { - headerFooterSettings.header.logo = null; - } else if (position === 'footer') { - headerFooterSettings.footer.logo = null; - } - event.reply('header-footer-logo-cleared', position); -}); - -// Helper function to process dynamic fields in header/footer text -function processDynamicFields(text, metadata = {}) { - if (!text) return ''; - - const now = new Date(); - const dateStr = now.toLocaleDateString(); - const timeStr = now.toLocaleTimeString(); - - let result = text; - result = result.replace(/\$DATE\$/g, dateStr); - result = result.replace(/\$TIME\$/g, timeStr); - result = result.replace(/\$TITLE\$/g, metadata.title || 'Untitled'); - result = result.replace(/\$AUTHOR\$/g, metadata.author || ''); - result = result.replace(/\$FILENAME\$/g, metadata.filename || ''); - - // Note: $PAGE$ and $TOTAL$ are handled by Pandoc/export tools - - return result; -} - -// Add headers/footers to DOCX file using PizZip and docx4js -// Function to set page size in DOCX files -async function setDocxPageSize(docxPath) { - try { - const PizZip = require('pizzip'); - - // Read the DOCX file - const docxBuffer = fs.readFileSync(docxPath); - const zip = new PizZip(docxBuffer); - - // Get document.xml - let documentXml = zip.file('word/document.xml').asText(); - - // Get page dimensions - let width, height; - const pageSize = PAGE_SIZES[pageSettings.size]; - - if (pageSize) { - width = pageSize.word.width; - height = pageSize.word.height; - } else if (pageSettings.customWidth && pageSettings.customHeight) { - // Parse custom dimensions (convert to twentieths of a point) - // Note: This is simplified - production code should handle various units - width = parseInt(pageSettings.customWidth) || 11906; - height = parseInt(pageSettings.customHeight) || 16838; - } else { - // Default to A4 - width = 11906; - height = 16838; - } - - // Swap dimensions for landscape - if (pageSettings.orientation === 'landscape') { - [width, height] = [height, width]; - } - - // Update all elements in section properties - const pgSzRegex = /]*\/>/g; - documentXml = documentXml.replace(pgSzRegex, () => { - return ``; - }); - - // If no pgSz found, add it to all sectPr elements - if (!pgSzRegex.test(documentXml)) { - const sectPrRegex = /]*>/g; - documentXml = documentXml.replace(sectPrRegex, (match) => { - return `${match}`; - }); - } - - // Save updated document.xml - zip.file('word/document.xml', documentXml); - - // Write modified DOCX - const newDocxBuffer = zip.generate({ type: 'nodebuffer' }); - fs.writeFileSync(docxPath, newDocxBuffer); - - } catch (error) { - console.error('Failed to set DOCX page size:', error); - } -} - -async function addHeaderFooterToDocx(docxPath, metadata = {}) { - if (!headerFooterSettings.enabled) return; - - try { - const PizZip = require('pizzip'); - - // Read the DOCX file - const docxBuffer = fs.readFileSync(docxPath); - const zip = new PizZip(docxBuffer); - - // Process dynamic fields - const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); - const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); - const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); - const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); - const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); - const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); - - // Create header XML - if (headerLeft || headerCenter || headerRight) { - const headerXml = ` - - - - ${headerLeft || ''} - - - - ${headerCenter || ''} - - - - ${headerRight || ''} - -`; - zip.file('word/header1.xml', headerXml); - } - - // Create footer XML with page numbers - if (footerLeft || footerCenter || footerRight) { - let footerCenterXml = ''; - if (footerCenter) { - // Handle $PAGE$ and $TOTAL$ in footer - if (footerCenter.includes('$PAGE$') || footerCenter.includes('$TOTAL$')) { - const parts = footerCenter.split(/(\$PAGE\$|\$TOTAL\$)/); - footerCenterXml = parts.map(part => { - if (part === '$PAGE$') { - return ''; - } else if (part === '$TOTAL$') { - return ''; - } else { - return `${part}`; - } - }).join(''); - } else { - footerCenterXml = `${footerCenter}`; - } - } - - const footerXml = ` - - - - ${footerLeft || ''} - - - - ${footerCenterXml} - - - - ${footerRight || ''} - -`; - zip.file('word/footer1.xml', footerXml); - } - - // Update document.xml.rels to reference header/footer - let relsXml = zip.file('word/_rels/document.xml.rels').asText(); - - // Add header relationship if not exists - if ((headerLeft || headerCenter || headerRight) && !relsXml.includes('header1.xml')) { - const headerId = 'rId100'; - const headerRel = ``; - relsXml = relsXml.replace('', headerRel + ''); - } - - // Add footer relationship if not exists - if ((footerLeft || footerCenter || footerRight) && !relsXml.includes('footer1.xml')) { - const footerId = 'rId101'; - const footerRel = ``; - relsXml = relsXml.replace('', footerRel + ''); - } - - zip.file('word/_rels/document.xml.rels', relsXml); - - // Update document.xml to use header/footer in sections - let documentXml = zip.file('word/document.xml').asText(); - if ((headerLeft || headerCenter || headerRight || footerLeft || footerCenter || footerRight)) { - // Find all section properties and add header/footer references - const sectPrRegex = /]*>[\s\S]*?<\/w:sectPr>/g; - documentXml = documentXml.replace(sectPrRegex, (match) => { - let updated = match; - if ((headerLeft || headerCenter || headerRight) && !match.includes('headerReference')) { - updated = updated.replace('', ''); - } - if ((footerLeft || footerCenter || footerRight) && !match.includes('footerReference')) { - updated = updated.replace('', ''); - } - return updated; - }); - } - - zip.file('word/document.xml', documentXml); - - // Write modified DOCX - const newDocxBuffer = zip.generate({ type: 'nodebuffer' }); - fs.writeFileSync(docxPath, newDocxBuffer); - - } catch (error) { - console.error('Failed to add headers/footers to DOCX:', error); - // Don't fail the export, just log the error - } -} - -// 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 with selected template, start page, and page settings - const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage, pageSettings); - - // 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', sanitizeErrorMessage(`Failed to export document: ${error.message}`)); - } -} - -// 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 (using execFile for safety) - const soffice = process.platform === 'win32' - ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' - : 'soffice'; - - const outputDir = path.dirname(result.filePath); - const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath]; - - execFile(soffice, sofficeArgs, (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', - sanitizeErrorMessage(`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', sanitizeErrorMessage(`Failed to export PDF: ${error.message}`)); - } -} - -// Universal File Converter integration -function showUniversalConverterDialog() { - mainWindow.webContents.send('show-universal-converter-dialog'); -} - -// PDF Editor dialog -function showPDFEditorDialog(operation) { - mainWindow.webContents.send('show-pdf-editor-dialog', operation); -} - -// Handle PDF editor from toolbar (with optional file path) -ipcMain.on('show-pdf-editor-from-toolbar', (event, { operation, filePath }) => { - mainWindow.webContents.send('show-pdf-editor-dialog', operation, filePath); -}); - -// Check if conversion tool is available (using execFile for safety) -function checkConverterAvailable(tool) { - return new Promise((resolve) => { - const isWin = process.platform === 'win32'; - const locateCmd = isWin ? 'where' : 'which'; - let toolName; - - switch (tool) { - case 'libreoffice': - toolName = 'soffice'; - break; - case 'imagemagick': - toolName = isWin ? 'magick' : 'convert'; - break; - case 'ffmpeg': { - // ffmpeg-static is always bundled — check it directly - const ffmpegBin = getFFmpegPath(); - resolve(fs.existsSync(ffmpegBin) || ffmpegBin === 'ffmpeg'); - return; - } - default: - resolve(false); - return; - } - - execFile(locateCmd, [toolName], (error) => { - resolve(!error); - }); - }); -} - -// Handle universal file conversion -ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, filePath }) => { - if (!conversionLimiter()) { - mainWindow.webContents.send('conversion-status', 'Please wait before converting again...'); - return; - } - try { - mainWindow.webContents.send('conversion-status', 'Checking converter availability...'); - - // Check if the required tool is available - const toolAvailable = await checkConverterAvailable(tool); - - if (!toolAvailable) { - throw new Error(`${tool} is not installed or not found in PATH. Please install it first.`); - } - - mainWindow.webContents.send('conversion-status', 'Converting file...'); - - const outputPath = filePath.replace(/\.[^/.]+$/, `.${toFormat}`); - let conversionInfo; - - switch (tool) { - case 'libreoffice': - conversionInfo = convertWithLibreOffice(filePath, toFormat, outputPath); - break; - case 'imagemagick': - conversionInfo = convertWithImageMagick(filePath, outputPath); - break; - case 'ffmpeg': - conversionInfo = convertWithFFmpeg(filePath, outputPath); - break; - case 'pandoc': - conversionInfo = convertWithPandoc(filePath, outputPath); - break; - default: - throw new Error(`Unknown conversion tool: ${tool}`); - } - - // Use execFile for safety (prevents command injection) - execFile(conversionInfo.command, conversionInfo.args, (error, stdout, stderr) => { - if (error) { - mainWindow.webContents.send('conversion-complete', { - success: false, - error: error.message - }); - - dialog.showMessageBox(mainWindow, { - type: 'error', - title: 'Conversion Failed', - message: `${tool} conversion failed`, - detail: stderr || error.message, - buttons: ['OK'] - }); - } else { - mainWindow.webContents.send('conversion-complete', { - success: true, - outputPath: outputPath - }); - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Conversion Complete', - message: 'File converted successfully!', - detail: `Saved to: ${outputPath}`, - buttons: ['OK'] - }); - } - }); - } catch (error) { - mainWindow.webContents.send('conversion-complete', { - success: false, - error: error.message - }); - - dialog.showMessageBox(mainWindow, { - type: 'error', - title: 'Conversion Failed', - message: 'Universal conversion failed', - detail: error.message, - buttons: ['OK'] - }); - } -}); - -// Handle universal batch file conversion -ipcMain.on('universal-convert-batch', async (event, { tool, fromFormat, toFormat, inputFolder, outputFolder, includeSubfolders, advancedOptions }) => { - if (!conversionLimiter()) { - mainWindow.webContents.send('conversion-status', 'Please wait before converting again...'); - return; - } - try { - const toolAvailable = await checkConverterAvailable(tool); - if (!toolAvailable) { - throw new Error(`${tool} is not installed or not found in PATH. Please install it first.`); - } - - // Collect matching files - const files = []; - function collectFiles(dir) { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory() && includeSubfolders) { - collectFiles(fullPath); - } else if (entry.isFile() && entry.name.toLowerCase().endsWith(`.${fromFormat}`)) { - files.push(fullPath); - } - } - } - collectFiles(inputFolder); - - if (files.length === 0) { - mainWindow.webContents.send('conversion-complete', { success: false, error: `No .${fromFormat} files found in the selected folder.` }); - return; - } - - let completed = 0; - let failed = 0; - - for (const filePath of files) { - const relativePath = path.relative(inputFolder, filePath); - const outputPath = path.join(outputFolder, relativePath.replace(/\.[^/.]+$/, `.${toFormat}`)); - - // Ensure output subdirectory exists - fs.mkdirSync(path.dirname(outputPath), { recursive: true }); - - mainWindow.webContents.send('conversion-status', `Converting ${completed + 1}/${files.length}: ${path.basename(filePath)}`); - - let conversionInfo; - switch (tool) { - case 'libreoffice': conversionInfo = convertWithLibreOffice(filePath, toFormat, outputPath); break; - case 'imagemagick': conversionInfo = convertWithImageMagick(filePath, outputPath); break; - case 'ffmpeg': conversionInfo = convertWithFFmpeg(filePath, outputPath); break; - case 'pandoc': conversionInfo = convertWithPandoc(filePath, outputPath); break; - default: throw new Error(`Unknown conversion tool: ${tool}`); - } - - await new Promise((resolve) => { - execFile(conversionInfo.command, conversionInfo.args, (error) => { - if (error) { failed++; } else { completed++; } - resolve(); - }); - }); - } - - mainWindow.webContents.send('conversion-complete', { - success: true, - outputPath: outputFolder - }); - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Batch Conversion Complete', - message: `Batch conversion finished!`, - detail: `Converted: ${completed}/${files.length} files${failed > 0 ? ` (${failed} failed)` : ''}\nOutput: ${outputFolder}`, - buttons: ['OK'] - }); - } catch (error) { - mainWindow.webContents.send('conversion-complete', { success: false, error: error.message }); - dialog.showMessageBox(mainWindow, { - type: 'error', - title: 'Batch Conversion Failed', - message: 'Batch conversion failed', - detail: error.message, - buttons: ['OK'] - }); - } -}); - -// LibreOffice conversion - returns {command, args} for execFile (safer than exec) -function convertWithLibreOffice(inputFile, outputFormat, outputPath) { - const outputDir = path.dirname(outputPath); - const soffice = process.platform === 'win32' - ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' - : 'soffice'; - - // LibreOffice conversion format mapping - const formatMap = { - 'pdf': 'pdf', - 'docx': 'docx', - 'doc': 'doc', - 'odt': 'odt', - 'rtf': 'rtf', - 'txt': 'txt', - 'html': 'html', - 'xlsx': 'xlsx', - 'xls': 'xls', - 'ods': 'ods', - 'csv': 'csv', - 'pptx': 'pptx', - 'ppt': 'ppt', - 'odp': 'odp' - }; - - const format = formatMap[outputFormat] || outputFormat; - - // Return command and args for execFile - return { - command: soffice, - args: ['--headless', '--convert-to', format, '--outdir', outputDir, inputFile] - }; -} - -// ImageMagick conversion - returns {command, args} for execFile -function convertWithImageMagick(inputFile, outputPath) { - const magick = process.platform === 'win32' ? 'magick' : 'convert'; - return { - command: magick, - args: [inputFile, outputPath] - }; -} - -// FFmpeg conversion - returns {command, args} for execFile -function convertWithFFmpeg(inputFile, outputPath) { - return { - command: getFFmpegPath(), - args: ['-i', inputFile, outputPath, '-y'], - }; -} - -// Pandoc conversion - returns {command, args} for execFile -function convertWithPandoc(inputFile, outputPath) { - return { - command: 'pandoc', - args: [inputFile, '-o', outputPath] - }; -} - -function performExportWithOptions(format, options) { - // Map format names to file extensions - const formatExtMap = { - revealjs: 'html', - beamer: 'pdf', - confluence: 'txt', - jira: 'txt', - }; - const fileExt = formatExtMap[format] || format; - - const outputFile = dialog.showSaveDialogSync(mainWindow, { - defaultPath: currentFile.replace(/\.[^/.]+$/, `.${fileExt}`), - filters: [ - { name: format.toUpperCase(), extensions: [fileExt] } - ] - }); - - if (!outputFile) return; // User cancelled - - console.log(`Attempting to export ${format} to:`, outputFile); - - // Check pandoc availability first - checkPandocAvailability().then((hasPandoc) => { - console.log('Pandoc available:', hasPandoc); - - if (!hasPandoc) { - // Handle formats that don't require pandoc - if (format === 'html') { - console.log('Using built-in HTML export'); - exportToHTML(outputFile); - return; - } else if (format === 'pdf') { - console.log('Using built-in PDF export'); - exportToPDFElectron(outputFile); - return; - } else { - dialog.showErrorBox('Export Error', - `Pandoc is required for ${format.toUpperCase()} export but is not installed or not found in PATH.\n\n` + - `Please install Pandoc from: https://pandoc.org/installing.html\n\n` + - `Alternatively, you can export to HTML or PDF using the built-in converters.` - ); - return; - } - } - - // Use pandoc for export with advanced options - console.log('Using Pandoc for export'); - let pandocCmd = `${getPandocPath()} "${currentFile}" -o "${outputFile}"`; - - // Add template if specified - if (options.template && options.template !== 'default') { - pandocCmd += ` --template="${options.template}"`; - } - - // Add metadata - if (options.metadata) { - for (const [key, value] of Object.entries(options.metadata)) { - if (value.trim()) { - pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; - } - } - } - - // Add variables - if (options.variables) { - for (const [key, value] of Object.entries(options.variables)) { - if (value.trim()) { - pandocCmd += ` -V ${key}="${value.replace(/"/g, '\\"')}"`; - } - } - } - - // Add other options - if (options.toc) pandocCmd += ' --toc'; - if (options.tocDepth) pandocCmd += ` --toc-depth=${options.tocDepth}`; - if (options.numberSections) pandocCmd += ' --number-sections'; - if (options.citeproc) pandocCmd += ' --citeproc'; - if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`; - if (options.csl) pandocCmd += ` --csl="${options.csl}"`; - - // Add specific options for PDF export to ensure proper generation - if (format === 'pdf') { - const pdfEngine = options.pdfEngine || 'xelatex'; // Default to xelatex - pandocCmd += ` --pdf-engine="${pdfEngine}"`; - if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; - - // Add monospace font settings for code blocks (ASCII art preservation) - pandocCmd += ' -V monofont="Consolas"'; - pandocCmd += ' --highlight-style=tango'; - - // Add header/footer if enabled - if (headerFooterSettings.enabled) { - const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; - const metadata = { filename, title: filename, author: '' }; - - const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); - const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); - const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); - const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); - const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); - const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); - - // Create LaTeX header - const latexHeader = ` -\\usepackage{fancyhdr} -\\pagestyle{fancy} -\\fancyhf{} -\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} -\\chead{${headerCenter.replace(/\\/g, '\\\\')}} -\\rhead{${headerRight.replace(/\\/g, '\\\\')}} -\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} -\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} -\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} -\\renewcommand{\\headrulewidth}{0.4pt} -\\renewcommand{\\footrulewidth}{0.4pt} -`; - const headerFile = path.join(require('os').tmpdir(), `header_export_${Date.now()}.tex`); - fs.writeFileSync(headerFile, latexHeader, 'utf-8'); - pandocCmd += ` --include-in-header="${headerFile}"`; - pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"'; - } - - // Try with specified PDF engine (using runPandocCmd for safety) - runPandocCmd(pandocCmd, (error) => { - if (error) { - // Try fallback engines if the specified one fails - const fallbackEngines = ['pdflatex', 'lualatex']; - tryPdfFallback(currentFile, outputFile, fallbackEngines, 0, options, error); - } else { - showExportSuccess(outputFile); - } - }); - } else if (format === 'docx') { - pandocCmd += ' -t docx'; - exportWithPandoc(pandocCmd, outputFile, format); - } else if (format === 'pptx') { - // Add PowerPoint footer if enabled - if (headerFooterSettings.enabled && headerFooterSettings.footer.center) { - const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; - const metadata = { filename, title: filename, author: '' }; - const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata); - pandocCmd += ` --variable footer="${footerText}"`; - } - exportWithPandoc(pandocCmd, outputFile, format); - } else if (format === 'json') { - pandocCmd = `${getPandocPath()} "${currentFile}" -t json -o "${outputFile}"`; - exportWithPandoc(pandocCmd, outputFile, format); - } else if (format === 'yaml' || format === 'xml' || format === 'toml') { - // For YAML/XML/TOML, save the raw markdown content with the new extension - try { - const content = fs.readFileSync(currentFile, 'utf-8'); - fs.writeFileSync(outputFile, content, 'utf-8'); - showExportSuccess(outputFile); - } catch (err) { - dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Failed to export: ${err.message}`)); - } - } else if (format === 'revealjs') { - let revealCmd = `${getPandocPath()} "${currentFile}" -t revealjs -s -o "${outputFile}" --slide-level=2`; - if (options) { - if (options.revealTheme) revealCmd += ` -V theme="${options.revealTheme}"`; - if (options.revealTransition) revealCmd += ` -V transition="${options.revealTransition}"`; - if (options.revealTransitionSpeed) revealCmd += ` -V transitionSpeed="${options.revealTransitionSpeed}"`; - if (options.revealControls !== undefined) revealCmd += ` -V controls="${options.revealControls}"`; - if (options.revealSlideNumber !== undefined) revealCmd += ` -V slideNumber="${options.revealSlideNumber}"`; - if (options.revealProgress !== undefined) revealCmd += ` -V progress="${options.revealProgress}"`; - if (options.revealHistory !== undefined) revealCmd += ` -V history="${options.revealHistory}"`; - if (options.revealCenter !== undefined) revealCmd += ` -V center="${options.revealCenter}"`; - - // Support for templates, metadata, bibliography - if (options.template && options.template !== 'default') { - revealCmd += ` --template="${options.template}"`; - } - if (options.metadata) { - for (const [key, value] of Object.entries(options.metadata)) { - if (value.trim()) { - revealCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; - } - } - } - if (options.bibliography) revealCmd += ` --bibliography="${options.bibliography}"`; - if (options.csl) revealCmd += ` --csl="${options.csl}"`; - } - exportWithPandoc(revealCmd, outputFile, format); - } else if (format === 'beamer') { - pandocCmd = `${getPandocPath()} "${currentFile}" -t beamer -o "${outputFile}"`; - exportWithPandoc(pandocCmd, outputFile, format); - } else if (format === 'confluence' || format === 'jira') { - pandocCmd = `${getPandocPath()} "${currentFile}" -t jira -o "${outputFile}"`; - exportWithPandoc(pandocCmd, outputFile, format); - } else if (format === 'mobi') { - // First export to EPUB, then try ebook-convert if available - const epubFile = outputFile.replace(/\.mobi$/i, '.epub'); - pandocCmd = `${getPandocPath()} "${currentFile}" -o "${epubFile}"`; - runPandocCmd(pandocCmd, (error) => { - if (error) { - dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Failed to export EPUB intermediate: ${error.message}`)); - return; - } - // Try ebook-convert (Calibre) for MOBI - execFile('ebook-convert', [epubFile, outputFile], (ebookError) => { - if (ebookError) { - dialog.showMessageBox(mainWindow, { - type: 'warning', - title: 'MOBI Export - Partial', - message: `Calibre's ebook-convert was not found. The file has been exported as EPUB instead.\n\nEPUB saved to: ${epubFile}\n\nTo get MOBI output, install Calibre from: https://calibre-ebook.com/`, - buttons: ['OK'] - }); - } else { - // Clean up intermediate EPUB - try { fs.unlinkSync(epubFile); } catch (e) { /* ignore */ } - showExportSuccess(outputFile); - } - }); - }); - } else { - // Generic export for other formats - exportWithPandoc(pandocCmd, outputFile, format); - } - }).catch((error) => { - console.error('Error checking pandoc availability:', error); - dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Error checking system requirements: ${error.message}`)); - }); -} - -function tryPdfFallback(inputFile, outputFile, engines, index, options, lastError) { - if (index >= engines.length) { - // All Pandoc PDF engines failed, fallback to Electron's built-in PDF export - console.log('All Pandoc PDF engines failed, falling back to Electron PDF export'); - exportToPDFElectron(outputFile); - return; - } - - const engine = engines[index]; - let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -o "${outputFile}"`; - - // Add monospace font settings for code blocks (ASCII art preservation) - pandocCmd += ' -V monofont="Consolas"'; - pandocCmd += ' --highlight-style=tango'; - - // Add geometry if specified - if (options.geometry) pandocCmd = pandocCmd.replace(` -o `, ` -V geometry:"${options.geometry}" -o `); - - // Add header/footer if enabled - if (headerFooterSettings.enabled) { - const filename = path.basename(inputFile, path.extname(inputFile)); - const metadata = { filename, title: filename, author: options.metadata?.author || '' }; - - const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); - const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); - const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); - const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); - const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); - const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); - - // Create LaTeX header for fancyhdr - const latexHeader = ` -\\usepackage{fancyhdr} -\\usepackage{lastpage} -\\pagestyle{fancy} -\\fancyhf{} -\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} -\\chead{${headerCenter.replace(/\\/g, '\\\\')}} -\\rhead{${headerRight.replace(/\\/g, '\\\\')}} -\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} -\\cfoot{${footerCenter.replace(/\$PAGE\$/g, '\\\\thepage').replace(/\$TOTAL\$/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} -\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} -\\renewcommand{\\headrulewidth}{0.4pt} -\\renewcommand{\\footrulewidth}{0.4pt} -`; - const headerFile = path.join(require('os').tmpdir(), `header_fallback_${Date.now()}.tex`); - fs.writeFileSync(headerFile, latexHeader, 'utf-8'); - pandocCmd += ` --include-in-header="${headerFile}"`; - } - - // Add all other options - if (options.template && options.template !== 'default') { - pandocCmd += ` --template="${options.template}"`; - } - - if (options.metadata) { - for (const [key, value] of Object.entries(options.metadata)) { - if (value.trim()) { - pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; - } - } - } - - // Use runPandocCmd for safety (prevents command injection) - runPandocCmd(pandocCmd, (error) => { - if (error) { - tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error); - } else { - showExportSuccess(outputFile); - } - }); -} - -function showExportSuccess(outputFile) { - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Export Complete', - message: `File exported successfully to ${outputFile}`, - buttons: ['OK'] - }); -} - -// Helper function to export with pandoc (general) - uses runPandocCmd for safety -function exportWithPandoc(pandocCmd, outputFile, format) { - console.log(`Executing Pandoc command: ${pandocCmd}`); - - runPandocCmd(pandocCmd, async (error, stdout, stderr) => { - if (error) { - console.error(`Pandoc error for ${format}:`, error); - console.error(`Pandoc stderr:`, stderr); - console.error(`Pandoc stdout:`, stdout); - - // Provide more specific error messages - let errorMessage = `Failed to export to ${format.toUpperCase()}`; - - if (error.message.includes('not found') || error.message.includes('not recognized')) { - errorMessage += '\n\nPandoc is not installed or not found in PATH.'; - errorMessage += '\nPlease install Pandoc from: https://pandoc.org/installing.html'; - } else if (stderr) { - errorMessage += `\n\nError details: ${stderr}`; - } else { - errorMessage += `\n\nError details: ${error.message}`; - } - - errorMessage += `\n\nCommand used: ${pandocCmd}`; - - dialog.showErrorBox('Export Error', sanitizeErrorMessage(errorMessage)); - } else { - console.log(`Successfully exported to ${format}:`, outputFile); - console.log(`Pandoc stdout:`, stdout); - if (stderr) { - console.warn(`Pandoc stderr (non-fatal):`, stderr); - } - - // Set page size for DOCX - if (format === 'docx') { - try { - await setDocxPageSize(outputFile); - console.log('Page size set for DOCX'); - } catch (pageSizeError) { - console.error('Error setting page size for DOCX:', pageSizeError); - } - } - - // Add headers/footers to DOCX if enabled - if (format === 'docx' && headerFooterSettings.enabled) { - try { - const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; - const metadata = { - filename: filename, - title: filename, - author: '' - }; - await addHeaderFooterToDocx(outputFile, metadata); - console.log('Headers/footers added to DOCX'); - } catch (hfError) { - console.error('Error adding headers/footers to DOCX:', hfError); - // Continue with success message even if header/footer fails - } - } - - // Set page size for ODT - if (format === 'odt') { - try { - await setDocxPageSize(outputFile); // ODT has similar structure - console.log('Page size set for ODT'); - } catch (pageSizeError) { - console.error('Error setting page size for ODT:', pageSizeError); - } - } - - // Add headers/footers to ODT if enabled - if (format === 'odt' && headerFooterSettings.enabled) { - // ODT format is similar to DOCX in structure, we could implement this - console.log('ODT header/footer support not yet implemented'); - } - - showExportSuccess(outputFile); - } - }); -} - -// Helper function to export PDF with pandoc (with fallbacks) - uses runPandocCmd for safety -function exportWithPandocPDF(pandocCmd, outputFile) { - runPandocCmd(pandocCmd, (error, stdout, stderr) => { - if (error) { - console.log('XeLaTeX failed, trying PDFLaTeX...'); - // Fallback to pdflatex - const fallbackCmd = pandocCmd.replace('--pdf-engine=xelatex', '--pdf-engine=pdflatex'); - runPandocCmd(fallbackCmd, (fallbackError, fallbackStdout, fallbackStderr) => { - if (fallbackError) { - console.log('PDFLaTeX failed, trying Electron PDF...'); - // Final fallback to Electron PDF - exportToPDFElectron(outputFile); - } else { - console.log('Successfully exported PDF with PDFLaTeX'); - showExportSuccess(outputFile); - } - }); - } else { - console.log('Successfully exported PDF with XeLaTeX'); - showExportSuccess(outputFile); - } - }); -} - -// Export to HTML using marked (no pandoc required) -function exportToHTML(outputFile) { - try { - const marked = require('marked'); - const markdownContent = fs.readFileSync(currentFile, 'utf8'); - const htmlContent = marked.parse(markdownContent); - - const fullHtml = ` - - - - - Exported Document - - - - ${htmlContent} - -`; - - fs.writeFileSync(outputFile, fullHtml, 'utf8'); - console.log('Successfully exported HTML'); - showExportSuccess(outputFile); - } catch (error) { - console.error('HTML export error:', error); - dialog.showErrorBox('HTML Export Error', sanitizeErrorMessage(`Failed to export HTML: ${error.message}`)); - } -} - -// Export to PDF using Electron (no pandoc required) -function exportToPDFElectron(outputFile) { - try { - const marked = require('marked'); - const markdownContent = fs.readFileSync(currentFile, 'utf8'); - const htmlContent = marked.parse(markdownContent); - - const fullHtml = ` - - - - - PDF Export - - - - ${htmlContent} - -`; - - // Create a hidden window to render and export PDF - const pdfWindow = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true, - contextIsolation: false - } - }); - - pdfWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(fullHtml)}`).then(() => { - return pdfWindow.webContents.printToPDF({ - marginsType: 1, // Use default margins - pageSize: 'A4', - printBackground: true, - printSelectionOnly: false, - landscape: false - }); - }).then((pdfData) => { - fs.writeFileSync(outputFile, pdfData); - pdfWindow.close(); - console.log('Successfully exported PDF with Electron'); - showExportSuccess(outputFile); - }).catch((error) => { - pdfWindow.close(); - console.error('Electron PDF export error:', error); - dialog.showErrorBox('PDF Export Error', - sanitizeErrorMessage(`Failed to export PDF using built-in engine: ${error.message}\n\n` + - `For better PDF export, please install Pandoc with LaTeX support.`) - ); - }); - } catch (error) { - console.error('PDF export setup error:', error); - dialog.showErrorBox('PDF Export Error', sanitizeErrorMessage(`Failed to setup PDF export: ${error.message}`)); - } -} - -function exportSpreadsheet(format) { - if (!currentFile) { - dialog.showErrorBox('Error', 'Please save the file first'); - return; - } - - // Request content from renderer - mainWindow.webContents.send('get-content-for-spreadsheet', format); -} - -function importDocument() { - const files = dialog.showOpenDialogSync(mainWindow, { - properties: ['openFile'], - filters: [ - { name: 'Documents', extensions: ['docx', 'odt', 'rtf', 'html', 'htm', 'tex', 'epub', 'pdf', 'txt'] }, - { name: 'Presentations', extensions: ['pptx', 'odp'] }, - { name: 'Markup Languages', extensions: ['rst', 'textile', 'mediawiki', 'org', 'asciidoc', 'twiki', 'opml'] }, - { name: 'E-book Formats', extensions: ['epub', 'fb2'] }, - { name: 'LaTeX Formats', extensions: ['tex', 'latex', 'ltx'] }, - { name: 'Web Formats', extensions: ['html', 'htm', 'xhtml'] }, - { name: 'Wiki Formats', extensions: ['mediawiki', 'dokuwiki', 'tikiwiki', 'twiki'] }, - { name: 'CSV/TSV', extensions: ['csv', 'tsv'] }, - { name: 'Developer Formats', extensions: ['json', 'yaml', 'yml', 'xml', 'toml'] }, - { name: 'All Files', extensions: ['*'] } - ] - }); - - if (files && files[0]) { - const inputFile = files[0]; - const stats = fs.statSync(inputFile); - if (stats.size > MAX_FILE_SIZE) { - dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); - return; - } - const ext = path.extname(inputFile).toLowerCase().slice(1); - const outputFile = inputFile.replace(/\.[^/.]+$/, '.md'); - - // Determine format-specific conversion options - let additionalOptions = ''; - - // For PDFs, extract text properly - if (ext === 'pdf') { - additionalOptions = '--pdf-engine=xelatex'; - } - - // For CSV/TSV, convert as tables - if (ext === 'csv' || ext === 'tsv') { - additionalOptions = '--from=csv -t markdown'; - } - - // For JSON, handle structure - if (ext === 'json') { - additionalOptions = '--from=json -t markdown'; - } - - // For YAML, XML, TOML - wrap content in code blocks directly - if (['yaml', 'yml', 'xml', 'toml'].includes(ext)) { - const rawContent = fs.readFileSync(inputFile, 'utf-8'); - const mdContent = convertDataToMarkdown(rawContent, ext); - const mdOutputFile = inputFile.replace(/\.[^/.]+$/, '.md'); - fs.writeFileSync(mdOutputFile, mdContent, 'utf-8'); - currentFile = mdOutputFile; - mainWindow.webContents.send('file-opened', { path: mdOutputFile, content: mdContent }); - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Import Complete', - message: `Document imported successfully as ${path.basename(mdOutputFile)}\n\nOriginal format: ${ext.toUpperCase()}\nConverted to: Markdown`, - buttons: ['OK'] - }); - return; - } - - // Convert to markdown using pandoc (using runPandocCmd for safety) - const pandocCmd = `${getPandocPath()} "${inputFile}" -t markdown ${additionalOptions} -o "${outputFile}"`; - - runPandocCmd(pandocCmd, (error, stdout, stderr) => { - if (error) { - dialog.showErrorBox('Import Error', sanitizeErrorMessage(`Failed to import: ${error.message}\n\nMake sure Pandoc is installed.\n\nSupported formats: DOCX, ODT, RTF, HTML, LaTeX, EPUB, PDF, PPTX, ODP, RST, Textile, MediaWiki, Org-mode, AsciiDoc, CSV, and more.`)); - } else { - // Open the converted markdown file - currentFile = outputFile; - const content = fs.readFileSync(outputFile, 'utf-8'); - mainWindow.webContents.send('file-opened', { path: outputFile, content }); - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Import Complete', - message: `Document imported successfully as ${path.basename(outputFile)}\n\nOriginal format: ${ext.toUpperCase()}\nConverted to: Markdown`, - buttons: ['OK'] - }); - } - }); - } -} - - -function setTheme(theme) { - store.set('theme', theme); - mainWindow.webContents.send('theme-changed', theme); -} - -// IPC handlers -ipcMain.on('save-file', (event, { path, content }) => { - fs.writeFileSync(path, content, 'utf-8'); - currentFile = path; -}); - -ipcMain.on('save-current-file', (event, payload) => { - const content = typeof payload === 'string' ? payload : payload?.content; - const targetFile = typeof payload === 'string' ? currentFile : payload?.filePath || currentFile; - - if (targetFile) { - fs.writeFileSync(targetFile, content, 'utf-8'); - currentFile = targetFile; - } else { - saveAsFile(); - } -}); - -ipcMain.on('get-theme', (event) => { - const theme = store.get('theme', 'atomonelight'); - event.reply('theme-changed', theme); -}); - -// Handle tab file tracking for exports -ipcMain.on('set-current-file', (event, filePath) => { - currentFile = filePath; -}); - -// Handle actual printing when renderer is ready -ipcMain.on('do-print', (event, { withStyles }) => { - if (mainWindow) { - // Renderer has already hidden UI, waited 300ms, and prepared the page - // Print immediately - DOM is fully rendered - mainWindow.webContents.print({ - silent: false, - printBackground: withStyles, - color: true, - margin: { marginType: 'default' } - }); - } -}); - -// Handle printing with custom options from print preview dialog -ipcMain.on('do-print-with-options', (event, options) => { - if (!mainWindow) return; - - const marginsMap = { - 'default': { marginType: 'default' }, - 'narrow': { top: 0.4, bottom: 0.4, left: 0.4, right: 0.4 }, - 'wide': { top: 1.0, bottom: 1.0, left: 1.0, right: 1.0 }, - 'none': { top: 0, bottom: 0, left: 0, right: 0 }, - }; - - const printOptions = { - silent: false, - printBackground: options.background, - landscape: options.orientation === 'landscape', - scaleFactor: options.scale / 100, - pageSize: options.paperSize, - }; - - if (options.margins && options.margins !== 'default') { - printOptions.margins = marginsMap[options.margins]; - } - - mainWindow.webContents.print(printOptions); -}); - -// Handle renderer ready for file association -ipcMain.on('renderer-ready', (event) => { - console.log('[MAIN] renderer-ready received, rendererReady was:', rendererReady); - if (mainWindow) { - mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] renderer-ready received, rendererReady was: ${rendererReady}')`); - } - rendererReady = true; - console.log('[MAIN] app.pendingFile:', app.pendingFile); - if (mainWindow) { - mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] app.pendingFile: ${app.pendingFile}')`); - } - if (app.pendingFile) { - console.log('[MAIN] Opening pending file:', app.pendingFile); - if (mainWindow) { - mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] Opening pending file: ${app.pendingFile}')`); - } - openFileFromPath(app.pendingFile); - app.pendingFile = null; - } -}); - -// Handle export with options -ipcMain.on('export-with-options', (event, { format, options }) => { - if (!conversionLimiter()) { - mainWindow.webContents.send('conversion-status', 'Please wait before converting again...'); - return; - } - performExportWithOptions(format, options); -}); - -// Handle batch conversion -ipcMain.on('batch-convert', (event, { inputFolder, outputFolder, format, options }) => { - if (!conversionLimiter()) { - mainWindow.webContents.send('conversion-status', 'Please wait before converting again...'); - return; - } - performBatchConversion(inputFolder, outputFolder, format, options); -}); - -// Handle folder selection for batch conversion -ipcMain.on('select-folder', (event, type) => { - const folder = dialog.showOpenDialogSync(mainWindow, { - properties: ['openDirectory'] - }); - - if (folder && folder[0]) { - event.reply('folder-selected', { type, path: folder[0] }); - } -}); - -ipcMain.on('export-spreadsheet', (event, { content, format }) => { - const outputFile = dialog.showSaveDialogSync(mainWindow, { - defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`), - filters: [ - { name: format.toUpperCase(), extensions: [format] } - ] - }); - - if (outputFile) { - try { - // Parse markdown content to extract tables - const tables = extractTablesFromMarkdown(content); - - if (tables.length === 0) { - dialog.showErrorBox('Export Error', 'No tables found in the markdown content'); - return; - } - - if (format === 'csv') { - // Convert tables to CSV format - let csvContent = ''; - tables.forEach((table, index) => { - if (index > 0) csvContent += '\n\n'; // Separate multiple tables - if (tables.length > 1) csvContent += `"Table ${index + 1}"\n`; - - table.forEach(row => { - const csvRow = row.map(cell => { - // Escape quotes and wrap in quotes if necessary - const cleanCell = cell.replace(/"/g, '""'); - return cleanCell.includes(',') || cleanCell.includes('"') || cleanCell.includes('\n') - ? `"${cleanCell}"` : cleanCell; - }).join(','); - csvContent += csvRow + '\n'; - }); - }); - - fs.writeFileSync(outputFile, csvContent, 'utf-8'); - } - - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Export Complete', - message: `${format.toUpperCase()} exported successfully to ${outputFile}`, - buttons: ['OK'] - }); - } catch (error) { - dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Failed to export: ${error.message}`)); - } - } -}); - -// Helper function to extract tables from markdown -function extractTablesFromMarkdown(markdown) { - const tables = []; - const lines = markdown.split('\n'); - let currentTable = []; - let inTable = false; - - for (const line of lines) { - if (line.includes('|')) { - if (!inTable) { - inTable = true; - currentTable = []; - } - - // Skip separator lines (|---|---|) - if (!line.match(/^\s*\|?\s*:?-+:?\s*\|/)) { - const cells = line.split('|') - .map(cell => cell.trim()) - .filter(cell => cell !== ''); - - if (cells.length > 0) { - currentTable.push(cells); - } - } - } else if (inTable && line.trim() === '') { - // End of table - if (currentTable.length > 0) { - tables.push(currentTable); - } - currentTable = []; - inTable = false; - } - } - - // Add last table if exists - if (currentTable.length > 0) { - tables.push(currentTable); - } - - return tables; -} - -function performBatchConversion(inputFolder, outputFolder, format, options) { - if (!fs.existsSync(inputFolder)) { - dialog.showErrorBox('Error', 'Input folder does not exist'); - return; - } - - // Create output folder if it doesn't exist - if (!fs.existsSync(outputFolder)) { - try { - fs.mkdirSync(outputFolder, { recursive: true }); - } catch (error) { - dialog.showErrorBox('Error', sanitizeErrorMessage(`Failed to create output folder: ${error.message}`)); - return; - } - } - - // Find all markdown files in input folder - const markdownFiles = []; - - function findMarkdownFiles(dir) { - const files = fs.readdirSync(dir); - - for (const file of files) { - const fullPath = path.join(dir, file); - const stat = fs.statSync(fullPath); - - if (stat.isDirectory()) { - findMarkdownFiles(fullPath); // Recursive search - } else if (file.match(/\.(md|markdown)$/i)) { - markdownFiles.push(fullPath); - } - } - } - - findMarkdownFiles(inputFolder); - - if (markdownFiles.length === 0) { - dialog.showErrorBox('No Files Found', 'No markdown files found in the selected folder'); - return; - } - - // Show progress dialog - let completedCount = 0; - const totalCount = markdownFiles.length; - - // Process each file - const processNextFile = async (index) => { - if (index >= markdownFiles.length) { - // All files processed - dialog.showMessageBox(mainWindow, { - type: 'info', - title: 'Batch Conversion Complete', - message: `Successfully converted ${completedCount} out of ${totalCount} files to ${format.toUpperCase()}.`, - buttons: ['OK'] - }); - return; - } - - const inputFile = markdownFiles[index]; - const relativePath = path.relative(inputFolder, inputFile); - const baseName = path.basename(relativePath, path.extname(relativePath)); - 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}`)); - - // Create subdirectories in output folder if needed - const outputDir = path.dirname(outputFile); - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } - - // Handle DOCX Enhanced format with WordTemplateExporter - if (format === 'docx-enhanced') { - try { - const content = fs.readFileSync(inputFile, 'utf-8'); - const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage); - await exporter.convert(content, outputFile); - - 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; - } - - // 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 (using execFile for safety) - const soffice = process.platform === 'win32' - ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' - : 'soffice'; - - const outputDir = path.dirname(outputFile); - const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath]; - - execFile(soffice, sofficeArgs, (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 - let pandocCmd = `${getPandocPath()} "${inputFile}" -o "${outputFile}"`; - - // Add template if specified - if (options.template && options.template !== 'default') { - pandocCmd += ` --template="${options.template}"`; - } - - // Add metadata - if (options.metadata) { - for (const [key, value] of Object.entries(options.metadata)) { - if (value.trim()) { - pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; - } - } - } - - // Add variables - if (options.variables) { - for (const [key, value] of Object.entries(options.variables)) { - if (value.trim()) { - pandocCmd += ` -V ${key}="${value.replace(/"/g, '\\"')}"`; - } - } - } - - // Add other options - if (options.toc) pandocCmd += ' --toc'; - if (options.tocDepth) pandocCmd += ` --toc-depth=${options.tocDepth}`; - if (options.numberSections) pandocCmd += ' --number-sections'; - if (options.citeproc) pandocCmd += ' --citeproc'; - if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`; - if (options.csl) pandocCmd += ` --csl="${options.csl}"`; - - // Add PDF-specific options with header/footer support - if (format === 'pdf') { - const pdfEngine = options.pdfEngine || 'xelatex'; - pandocCmd += ` --pdf-engine=${pdfEngine}`; - if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; - - // Add monospace font settings for code blocks (ASCII art preservation) - pandocCmd += ' -V monofont="Consolas"'; - pandocCmd += ' --highlight-style=tango'; - - // Add header/footer if enabled - if (headerFooterSettings.enabled) { - const filename = path.basename(inputFile, path.extname(inputFile)); - const metadata = { filename, title: filename, author: '' }; - - const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); - const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); - const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); - const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); - const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); - const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); - - // Create LaTeX header - const latexHeader = ` -\\usepackage{fancyhdr} -\\pagestyle{fancy} -\\fancyhf{} -\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} -\\chead{${headerCenter.replace(/\\/g, '\\\\')}} -\\rhead{${headerRight.replace(/\\/g, '\\\\')}} -\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} -\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} -\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} -\\renewcommand{\\headrulewidth}{0.4pt} -\\renewcommand{\\footrulewidth}{0.4pt} -`; - const headerFile = path.join(require('os').tmpdir(), `header_batch_${Date.now()}.tex`); - fs.writeFileSync(headerFile, latexHeader, 'utf-8'); - pandocCmd += ` --include-in-header="${headerFile}"`; - pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"'; - } - } - - // Add DOCX-specific handling - if (format === 'docx') { - pandocCmd += ' -t docx'; - } - - // Add PowerPoint footer if enabled - if (format === 'pptx' && headerFooterSettings.enabled && headerFooterSettings.footer.center) { - const filename = path.basename(inputFile, path.extname(inputFile)); - const metadata = { filename, title: filename, author: '' }; - const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata); - pandocCmd += ` --variable footer="${footerText}"`; - } - - // Execute conversion (using runPandocCmd for safety) - runPandocCmd(pandocCmd, async (error, stdout, stderr) => { - if (!error) { - // Add headers/footers to DOCX if enabled - if (format === 'docx' && headerFooterSettings.enabled) { - try { - const filename = path.basename(inputFile, path.extname(inputFile)); - const metadata = { filename, title: filename, author: '' }; - await addHeaderFooterToDocx(outputFile, metadata); - } catch (hfError) { - console.error('Batch: Error adding headers/footers to DOCX:', hfError); - } - } - completedCount++; - } - - // Update progress (you could send this to renderer for a progress bar) - mainWindow.webContents.send('batch-progress', { - completed: index + 1, - total: totalCount, - currentFile: path.basename(inputFile), - success: !error - }); - - // Process next file - processNextFile(index + 1); - }); - }; - - // Start processing - processNextFile(0); -} - -// Handle command line interface for file conversion -function handleCLIConversion(args) { - const command = args[0]; - const filePath = args[args.length - 1]; // File path is always last argument - - if (!fs.existsSync(filePath)) { - console.error(`Error: File not found: ${filePath}`); - app.quit(); - return; - } - - // Show conversion dialog for --convert command - if (command === '--convert') { - showConversionDialog(filePath); - return; - } - - // Direct conversion for --convert-to command - if (command === '--convert-to' && args.length >= 3) { - const format = args[1]; - performCLIConversion(filePath, format); - return; - } - - console.error('Usage: --convert OR --convert-to '); - app.quit(); -} - -// Show conversion dialog for CLI -function showConversionDialog(filePath) { - const { dialog } = require('electron'); - - // Create a hidden window for dialog operations - const hiddenWindow = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true, - contextIsolation: false - } - }); - - const formats = [ - { name: 'PDF', value: 'pdf' }, - { name: 'HTML', value: 'html' }, - { name: 'DOCX', value: 'docx' }, - { name: 'LaTeX', value: 'latex' }, - { name: 'RTF', value: 'rtf' }, - { name: 'ODT', value: 'odt' }, - { name: 'PowerPoint', value: 'pptx' } - ]; - - // Create format selection dialog using message box - const formatButtons = formats.map(f => f.name); - formatButtons.push('Cancel'); - - dialog.showMessageBox(hiddenWindow, { - type: 'question', - title: 'PanConverter - Choose Format', - message: `Convert "${path.basename(filePath)}" to:`, - detail: 'Select the output format for conversion', - buttons: formatButtons, - defaultId: 0, - cancelId: formatButtons.length - 1 - }).then(result => { - if (result.response < formats.length) { - const selectedFormat = formats[result.response].value; - performCLIConversion(filePath, selectedFormat); - } else { - console.log('Conversion cancelled'); - app.quit(); - } - hiddenWindow.destroy(); - }); -} - -// Perform CLI conversion -function performCLIConversion(inputPath, format) { - try { - const content = fs.readFileSync(inputPath, 'utf-8'); - const outputPath = inputPath.replace(/\.[^/.]+$/, `.${format}`); - - console.log(`Converting "${path.basename(inputPath)}" to ${format.toUpperCase()}...`); - - // Use existing export functions but with CLI output (using runPandocCmd for safety) - const pandocCommand = buildPandocCommand(content, format, outputPath); - - runPandocCmd(pandocCommand, (error, stdout, stderr) => { - if (error) { - console.error(`Conversion failed: ${error.message}`); - if (stderr) console.error(`Details: ${stderr}`); - app.quit(); - return; - } - - console.log(`Successfully converted to: ${outputPath}`); - - // Show Windows notification (using exec for PowerShell is acceptable here - hardcoded command) - if (process.platform === 'win32') { - const iconPath = path.join(__dirname, '../assets/icon.png'); - execFile('powershell', ['-Command', `New-BurntToastNotification -Text 'PanConverter', 'File converted to ${format.toUpperCase()}' -AppLogo '${iconPath}'`], () => {}); - } - - app.quit(); - }); - } catch (error) { - console.error(`Error reading file: ${error.message}`); - app.quit(); - } -} - -// Build Pandoc command for CLI conversion -function buildPandocCommand(content, format, outputPath) { - const inputFile = path.join(require('os').tmpdir(), `panconverter_temp_${Date.now()}.md`); - fs.writeFileSync(inputFile, content, 'utf-8'); - - let command = `pandoc "${inputFile}" -o "${outputPath}"`; - - // Get metadata for dynamic fields - const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; - const metadata = { - filename: filename, - title: filename, - author: '', - }; - - switch (format) { - case 'pdf': - command += ' --pdf-engine=xelatex --variable geometry:margin=1in'; - - // Add page size and orientation - const pageSize = PAGE_SIZES[pageSettings.size]; - if (pageSize) { - command += ` -V geometry:papersize=${pageSize.pandoc}`; - } else if (pageSettings.customWidth && pageSettings.customHeight) { - // Custom page size - command += ` -V geometry:paperwidth=${pageSettings.customWidth}`; - command += ` -V geometry:paperheight=${pageSettings.customHeight}`; - } - - // Add orientation - if (pageSettings.orientation === 'landscape') { - command += ' -V geometry:landscape'; - } - - // Add monospace font settings for code blocks (ASCII art preservation) - command += ' -V monofont="Consolas"'; - command += ' --highlight-style=tango'; - - // Add header/footer if enabled - if (headerFooterSettings.enabled) { - // Process dynamic fields - const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); - const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); - const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); - const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); - const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); - const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); - - // Add Pandoc variables for fancyhdr package - if (headerLeft) command += ` --variable header-left="${headerLeft}"`; - if (headerCenter) command += ` --variable header-center="${headerCenter}"`; - if (headerRight) command += ` --variable header-right="${headerRight}"`; - if (footerLeft) command += ` --variable footer-left="${footerLeft}"`; - if (footerCenter) command += ` --variable footer-center="${footerCenter}"`; - if (footerRight) command += ` --variable footer-right="${footerRight}"`; - - // Create custom LaTeX header with fancyhdr - const latexHeader = ` -\\usepackage{fancyhdr} -\\pagestyle{fancy} -\\fancyhf{} -\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} -\\chead{${headerCenter.replace(/\\/g, '\\\\')}} -\\rhead{${headerRight.replace(/\\/g, '\\\\')}} -\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} -\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} -\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} -\\renewcommand{\\headrulewidth}{0.4pt} -\\renewcommand{\\footrulewidth}{0.4pt} -`; - const headerFile = path.join(require('os').tmpdir(), `header_${Date.now()}.tex`); - fs.writeFileSync(headerFile, latexHeader, 'utf-8'); - command += ` --include-in-header="${headerFile}"`; - - // Add lastpage package for $TOTAL$ support - command += ' --variable header-includes="\\\\usepackage{lastpage}"'; - } - break; - - case 'html': - command += ' --self-contained --css'; - break; - - case 'docx': - command += ' --reference-doc'; - - // For DOCX, header/footer are handled via reference document or separate processing - // We'll add a note that DOCX headers/footers require reference doc or post-processing - break; - - case 'odt': - // ODT headers/footers are handled via reference document - break; - - case 'latex': - command += ' --standalone'; - break; - - case 'pptx': - command += ' --slide-level=2'; - // PowerPoint footer can be added with --variable - if (headerFooterSettings.enabled && headerFooterSettings.footer.center) { - const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata); - command += ` --variable footer="${footerText}"`; - } - break; - - case 'json': - command = `pandoc "${inputFile}" -t json -o "${outputPath}"`; - break; - - case 'yaml': - command = `pandoc "${inputFile}" -t markdown -o "${outputPath}"`; - break; - - case 'xml': - command = `pandoc "${inputFile}" -t jats -o "${outputPath}"`; - break; - - case 'toml': - // TOML: save raw markdown content with .toml extension - command = `pandoc "${inputFile}" -t markdown -o "${outputPath}"`; - break; - - case 'revealjs': - command = `pandoc "${inputFile}" -t revealjs -s -o "${outputPath}" --slide-level=2`; - break; - - case 'beamer': - command = `pandoc "${inputFile}" -t beamer -o "${outputPath}"`; - break; - - case 'confluence': - case 'jira': - command = `pandoc "${inputFile}" -t jira -o "${outputPath}"`; - break; - } - - return command; -} - -app.whenReady().then(() => { - // Load saved Word template path and settings - wordTemplatePath = store.get('wordTemplatePath', null); - templateStartPage = store.get('templateStartPage', 3); - - // Load header/footer settings - const savedHFSettings = store.get('headerFooterSettings', null); - if (savedHFSettings) { - headerFooterSettings = savedHFSettings; - } - - // Load page size settings - const savedPageSettings = store.get('pageSettings', null); - if (savedPageSettings) { - pageSettings = savedPageSettings; - } - - // 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 - } - - mainWindow = createMainWindow(); - mainWindow.on('closed', () => { mainWindow = null; }); - - // Register file ops IPC handlers - fileOps.register({ - validatePath, - resolveWritablePath, - isPathAccessible, - currentFileRef: { get current() { return currentFile; } }, - mainWindow, - }); - - // Handle file association on app startup - // In packaged apps, process.argv structure is different: - // Development: ['electron', 'app.js', 'file.md'] - need slice(2) - // Packaged: ['PanConverter.exe', 'file.md'] - need slice(1) - // We'll check all arguments after the executable - console.log('[MAIN] Full command line args:', JSON.stringify(process.argv)); - console.log('[MAIN] App is packaged:', app.isPackaged); - - // Start from index 1 (skip executable) and check each argument - const startIndex = app.isPackaged ? 1 : 2; - const fileArgs = process.argv.slice(startIndex); - console.log('[MAIN] File args to process (starting from index', startIndex + '):', fileArgs); - - for (const arg of fileArgs) { - console.log('[MAIN] Checking arg:', arg); - if ((arg.endsWith('.md') || arg.endsWith('.markdown'))) { - // Try to resolve the path (might be relative) - const resolvedPath = path.isAbsolute(arg) ? arg : path.resolve(process.cwd(), arg); - console.log('[MAIN] Resolved path:', resolvedPath); - if (fs.existsSync(resolvedPath)) { - // Store the file to open after window is ready - console.log('[MAIN] Setting pendingFile to:', resolvedPath); - app.pendingFile = resolvedPath; - break; - } else { - console.log('[MAIN] File does not exist:', resolvedPath); - } - } - } - console.log('[MAIN] Final app.pendingFile:', app.pendingFile); -}); - -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit(); - } -}); - -app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) { - mainWindow = createMainWindow(); - mainWindow.on('closed', () => { mainWindow = null; }); - } -}); - -// IPC handlers for recent files -ipcMain.on('save-recent-files', (event, recentFiles) => { - try { - const userDataPath = app.getPath('userData'); - const recentFilesPath = path.join(userDataPath, 'recent-files.json'); - fs.writeFileSync(recentFilesPath, JSON.stringify(recentFiles, null, 2)); - } catch (error) { - console.error('Error saving recent files:', error); - } -}); - -ipcMain.on('clear-recent-files', (event) => { - try { - const userDataPath = app.getPath('userData'); - const recentFilesPath = path.join(userDataPath, 'recent-files.json'); - fs.writeFileSync(recentFilesPath, JSON.stringify([], null, 2)); - // Rebuild menu to reflect changes - menu.register(mainWindow); - event.reply('recent-files-cleared'); - } catch (error) { - console.error('Error clearing recent files:', error); - } -}); - -// Handle file opening on macOS -app.on('open-file', (event, filePath) => { - event.preventDefault(); - if (mainWindow && rendererReady) { - openFileFromPath(filePath); - } else { - // Store the file path to open after window and renderer are ready - app.pendingFile = filePath; - } -}); - -// Handle file opening from command line or file association -function openFileFromPath(filePath) { - console.log('[MAIN] openFileFromPath called with:', filePath); - console.log('[MAIN] rendererReady:', rendererReady, 'mainWindow exists:', !!mainWindow, 'webContents exists:', !!(mainWindow?.webContents)); - if (fs.existsSync(filePath)) { - const stats = fs.statSync(filePath); - if (stats.size > MAX_FILE_SIZE) { - dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); - return; - } - currentFile = filePath; - const content = fs.readFileSync(filePath, 'utf-8'); - console.log('[MAIN] File read successfully, content length:', content.length); - console.log('[MAIN] About to send file-opened. mainWindow:', !!mainWindow, 'webContents:', !!(mainWindow?.webContents), 'rendererReady:', rendererReady); - if (mainWindow && mainWindow.webContents && rendererReady) { - // Send file immediately - renderer-ready means UI is initialized - console.log('[MAIN] Sending file-opened to renderer'); - mainWindow.webContents.send('file-opened', { path: filePath, content }); - } else { - // Store file to open after renderer is ready - console.log('[MAIN] Storing as pending file'); - app.pendingFile = filePath; - } - } else { - console.error('[MAIN] File does not exist:', filePath); - } -} - -// ======================================== -// PDF OPERATIONS — delegates to main/PDFOperations.js -// ======================================== - -ipcMain.on('process-pdf-operation', async (event, data) => { - try { - mainWindow.webContents.send('pdf-operation-progress', { - message: `Processing ${data.operation}...`, - progress: 10 - }); - - const result = await PDFOperations.executeOperation(data.operation, data); - mainWindow.webContents.send('pdf-operation-complete', result); - } catch (error) { - mainWindow.webContents.send('pdf-operation-complete', { - success: false, - error: error.message - }); - } -}); - -ipcMain.on('get-pdf-page-count', async (event, filePath) => { - try { - const count = await PDFOperations.getPageCount(filePath); - event.reply('pdf-page-count', { count }); - } catch (error) { - event.reply('pdf-page-count', { error: error.message }); - } -}); - -// IPC Handler for folder selection (for PDF operations) -ipcMain.on('select-pdf-folder', (event, inputId) => { - const folder = dialog.showOpenDialogSync(mainWindow, { - properties: ['openDirectory'] - }); - - if (folder && folder[0]) { - event.reply('pdf-folder-selected', { inputId, path: folder[0] }); - } -}); - -// IPC Handler for loading document templates -ipcMain.handle('load-template', async (event, filename) => { - try { - const templatePath = path.join(__dirname, 'templates', filename); - return fs.readFileSync(templatePath, 'utf-8'); - } catch (err) { - console.error('Failed to load template:', err); - return null; - } -}); - -// IPC Handler for saving pasted/dropped images -ipcMain.handle('save-pasted-image', async (event, { base64, ext }) => { - try { - let saveDir; - - if (currentFile) { - // Save relative to current file - saveDir = path.join(path.dirname(currentFile), 'assets'); - } else { - // Use temp directory - saveDir = path.join(app.getPath('temp'), 'markdown-converter-images'); - } - - if (!fs.existsSync(saveDir)) { - fs.mkdirSync(saveDir, { recursive: true }); - } - - const filename = `image-${Date.now()}.${ext}`; - const filePath = path.join(saveDir, filename); - const buffer = Buffer.from(base64, 'base64'); - fs.writeFileSync(filePath, buffer); - - return { relativePath: `assets/${filename}`, absolutePath: filePath }; - } catch (error) { - console.error('Failed to save pasted image:', error); - return null; - } -}); - -// IPC Handler to receive generated content from generator windows -ipcMain.on('insert-generated-content', (event, content) => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('insert-content', content); - } -}); - -// ============================================ -// File Explorer IPC Handlers -// ============================================ -ipcMain.handle('list-directory', async (event, dirPath) => { - try { - if (!dirPath) { - const result = await dialog.showOpenDialog(mainWindow, { - properties: ['openDirectory'] - }); - if (result.canceled || !result.filePaths[0]) return null; - dirPath = result.filePaths[0]; - } - - // Validate path to prevent traversal attacks - const validation = validatePath(dirPath); - if (!validation.valid) { - console.error('[SECURITY] Invalid directory path:', validation.error); - return null; - } - - if (!isPathAccessible(validation.resolved)) { - return null; - } - - const entries = fs.readdirSync(validation.resolved, { withFileTypes: true }) - .filter(e => !e.name.startsWith('.')) - .sort((a, b) => { - if (a.isDirectory() && !b.isDirectory()) return -1; - if (!a.isDirectory() && b.isDirectory()) return 1; - return a.name.localeCompare(b.name); - }) - .map(e => ({ - name: e.name, - isDirectory: e.isDirectory(), - size: e.isDirectory() ? 0 : fs.statSync(path.join(validation.resolved, e.name)).size, - modified: fs.statSync(path.join(validation.resolved, e.name)).mtimeMs, - path: path.join(validation.resolved, e.name) - })); - return { path: validation.resolved, entries }; - } catch (err) { - console.error('list-directory error:', err); - return null; - } -}); - -ipcMain.handle('select-custom-css', async (event) => { - const result = dialog.showOpenDialogSync(mainWindow, { - title: 'Select Custom Preview CSS', - properties: ['openFile'], - filters: [ - { name: 'CSS Stylesheets', extensions: ['css'] } - ] - }); - - if (result && result[0]) { - const filePath = result[0]; - const content = fs.readFileSync(filePath, 'utf-8'); - return { path: filePath, content }; - } - return null; -}); - -// ============================================ -// Snippets IPC Handlers -// ============================================ -const snippetsPath = path.join(app.getPath('userData'), 'snippets.json'); - -function loadSnippets() { - try { - if (fs.existsSync(snippetsPath)) { - return JSON.parse(fs.readFileSync(snippetsPath, 'utf-8')); - } - } catch (err) { console.error('Failed to load snippets:', err); } - return []; -} - -function saveSnippetsFile(snippets) { - fs.writeFileSync(snippetsPath, JSON.stringify(snippets, null, 2)); -} - -ipcMain.handle('get-snippets', async () => loadSnippets()); - -ipcMain.handle('save-snippet', async (event, snippet) => { - const snippets = loadSnippets(); - const existing = snippets.findIndex(s => s.id === snippet.id); - if (existing >= 0) snippets[existing] = snippet; - else snippets.push(snippet); - saveSnippetsFile(snippets); - return snippets; -}); - -ipcMain.handle('delete-snippet', async (event, id) => { - const snippets = loadSnippets().filter(s => s.id !== id); - saveSnippetsFile(snippets); - return snippets; -}); - -// ============================================ -// Code Execution (REPL) IPC Handler -// ============================================ -ipcMain.handle('execute-code', async (event, { code, language }) => { - const timeout = 10000; - - return new Promise((resolve) => { - let cmd, args; - if (language === 'javascript' || language === 'js') { - cmd = 'node'; - args = ['-e', code]; - } else if (language === 'python' || language === 'py') { - cmd = process.platform === 'win32' ? 'python' : 'python3'; - args = ['-c', code]; - } else if (language === 'bash' || language === 'sh') { - cmd = process.platform === 'win32' ? 'cmd' : 'bash'; - args = process.platform === 'win32' ? ['/c', code] : ['-c', code]; - } else { - resolve({ error: `Unsupported language: ${language}` }); - return; - } - - execFile(cmd, args, { timeout, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => { - resolve({ - stdout: stdout || '', - stderr: stderr || '', - error: err?.killed ? 'Execution timed out (10s limit)' : (err?.message || null) - }); - }); - }); -}); - -// Exports for src/main/menu/ items.js (lazy require inside click handlers) -module.exports = { - openFile, - openPdfFile, - openFileFromPath, - saveAsFile, - exportFile, - exportPDFViaWordTemplate, - exportWordWithTemplate, - exportSpreadsheet, - importDocument, - selectWordTemplate, - showTemplateSettings, - setTheme, - showBatchConversionDialog, - showUniversalConverterDialog, - showPDFEditorDialog, - showAboutDialog, - showDependenciesDialog, -}; +// src/main.js — DEPRECATED, will be removed in Task 8 +// See src/main/index.js for the new modular entrypoint +module.exports = require('./main/index.js'); diff --git a/src/main/index.js b/src/main/index.js new file mode 100644 index 0000000..be25d27 --- /dev/null +++ b/src/main/index.js @@ -0,0 +1,3401 @@ +const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron'); +const path = require('path'); +const fs = require('fs'); +const { exec, execFile, spawn } = require('child_process'); +const WordTemplateExporter = require('./word-template'); +const PDFOperations = require('./PDFOperations'); +const GitOperations = require('./GitOperations'); +const { getAllowedDirectories, validatePath, resolveWritablePath, isPathAccessible } = require('./utils/paths'); +const fileOps = require('./files'); +const menu = require('./menu'); +const { createMainWindow } = require('./window'); + +// Add MiKTeX to PATH for LaTeX support +if (process.platform === 'win32') { + const miktexPath = 'C:\\Program Files\\MiKTeX\\miktex\\bin\\x64'; + if (fs.existsSync(miktexPath)) { + process.env.PATH = `${miktexPath};${process.env.PATH}`; + console.log('[MAIN] Added MiKTeX to PATH:', miktexPath); + } +} + +// Returns path to pandoc: bundled binary when packaged, dev bin or system fallback otherwise. +function getPandocPath() { + if (app.isPackaged) { + const ext = process.platform === 'win32' ? '.exe' : ''; + return path.join(process.resourcesPath, 'bin', `pandoc${ext}`); + } + // Development: prefer locally-downloaded binary in bin// + const devBin = path.join( + __dirname, + '../..', + 'bin', + process.platform, + process.platform === 'win32' ? 'pandoc.exe' : 'pandoc', + ); + if (fs.existsSync(devBin)) return devBin; + return 'pandoc'; +} + +// Returns path to ffmpeg: asar-unpacked bundled binary when packaged, system fallback otherwise. +function getFFmpegPath() { + try { + let ffmpegPath = require('ffmpeg-static'); + if (app.isPackaged) { + // ffmpeg-static is in asarUnpack — rewrite the path to the unpacked location + ffmpegPath = ffmpegPath.replace('app.asar' + path.sep, 'app.asar.unpacked' + path.sep); + } + if (fs.existsSync(ffmpegPath)) return ffmpegPath; + } catch (_) { + /* ffmpeg-static not available */ + } + return process.platform === 'win32' ? 'ffmpeg' : 'ffmpeg'; +} + +// Check if Pandoc is available +function checkPandocAvailable() { + return new Promise((resolve) => { + const pandocPath = getPandocPath(); + execFile(pandocPath, ['--version'], (error) => { + resolve(!error); + }); + }); +} + +/** + * Safe command execution using execFile (no shell injection risk) + * @param {string} command - The command to execute + * @param {string[]} args - Array of arguments + * @param {object} options - Options for execFile + * @returns {Promise<{stdout: string, stderr: string}>} + */ +function safeExecFile(command, args, options = {}) { + return new Promise((resolve, reject) => { + execFile(command, args, { maxBuffer: 10 * 1024 * 1024, ...options }, (error, stdout, stderr) => { + if (error) { + reject({ error, stdout, stderr }); + } else { + resolve({ stdout, stderr }); + } + }); + }); +} + +// File size validation +const MAX_FILE_SIZE_MB = 50; +const MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024; + +// Sanitize error messages to strip absolute file paths +function sanitizeErrorMessage(message) { + if (typeof message !== 'string') return String(message); + // Strip absolute Windows paths, keeping only filename + return message + .replace(/[A-Z]:\\[^\s"']+\\([^\s"'\\]+)/gi, '$1') + .replace(/\/[^\s"']+\/([^\s"'/]+)/g, '$1'); +} + +// Rate limiter for conversions +function createRateLimiter(minIntervalMs = 2000) { + let lastCall = 0; + return function canProceed() { + const now = Date.now(); + if (now - lastCall < minIntervalMs) return false; + lastCall = now; + return true; + }; +} +const conversionLimiter = createRateLimiter(2000); + +// Convert structured data formats to markdown code blocks +function convertDataToMarkdown(content, format) { + switch (format) { + case 'json': + return '```json\n' + content + '\n```'; + case 'yaml': + case 'yml': + return '```yaml\n' + content + '\n```'; + case 'xml': + return '```xml\n' + content + '\n```'; + case 'toml': + return '```toml\n' + content + '\n```'; + default: + return '```\n' + content + '\n```'; + } +} + +/** + * Run Pandoc command safely with execFile + * @param {string[]} args - Pandoc arguments array + * @param {Function} callback - Callback function (error, stdout, stderr) + */ +function runPandoc(args, callback) { + const pandocPath = getPandocPath(); + execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback); +} + +/** + * Run a Pandoc command string safely using execFile + * Parses the command string and uses execFile to prevent shell injection + * @param {string} cmdString - Full Pandoc command string (e.g., 'pandoc "input.md" -o "output.pdf"') + * @param {Function} callback - Callback function (error, stdout, stderr) + */ +function runPandocCmd(cmdString, callback) { + const parsed = parseCommand(cmdString); + // Skip 'pandoc' if it's the first element (command itself) + // Use path.basename to handle full paths like bin/linux/pandoc + const cmdBase = path.basename(parsed.command).replace(/\.exe$/i, ''); + const args = cmdBase === 'pandoc' ? parsed.args : [parsed.command, ...parsed.args]; + const pandocPath = getPandocPath(); + execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback); +} + +/** + * Parse a command string into command and arguments array + * This helps transition from exec() to execFile() safely + * @param {string} cmdString - Full command string + * @returns {{command: string, args: string[]}} + */ +function parseCommand(cmdString) { + // Handle quoted strings properly + const parts = []; + let current = ''; + let inQuotes = false; + let quoteChar = ''; + + for (let i = 0; i < cmdString.length; i++) { + const char = cmdString[i]; + if ((char === '"' || char === "'") && !inQuotes) { + inQuotes = true; + quoteChar = char; + } else if (char === quoteChar && inQuotes) { + inQuotes = false; + quoteChar = ''; + } else if (char === ' ' && !inQuotes) { + if (current) { + parts.push(current); + current = ''; + } + } else { + current += char; + } + } + if (current) { + parts.push(current); + } + + return { + command: parts[0], + args: parts.slice(1) + }; +} + +const store = require('./store'); + +// Plugin settings IPC handlers +ipcMain.handle('plugin-settings:get', (_event, key) => { + return store.get(key); +}); +ipcMain.handle('plugin-settings:set', (_event, { key, value }) => { + store.set(key, value); +}); +ipcMain.handle('get-app-version', () => app.getVersion()); + +// ============================================ +// SECURITY: Permission Request Handler +// ============================================ +// Restrict permissions to only those explicitly needed. +// Deny: camera, microphone, geolocation, notifications, etc. +// Allow: clipboard-read, clipboard-write (user expects these) +app.on('web-contents-created', (event, contents) => { + contents.session.setPermissionRequestHandler((webContents, permission, callback) => { + const ALLOWED_PERMISSIONS = [ + 'clipboard-read', + 'clipboard-write' + ]; + + if (ALLOWED_PERMISSIONS.includes(permission)) { + callback(true); + } else { + // Deny all other permissions (camera, microphone, geolocation, etc.) + console.warn(`[Security] Blocked permission request: ${permission}`); + callback(false); + } + }); + + // Disable file download dialogs for security + // contents.session.setDownloadPath(userDownloadsPath); +}); + +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 templateStartPage = 3; // Which page to start inserting content (default: page 3) +let rendererReady = false; // Track if renderer is ready to receive file data + +// Header & Footer Settings +let headerFooterSettings = { + enabled: true, + header: { + left: '', + center: '', + right: '', + logo: null // Will store image file path + }, + footer: { + left: '', + center: '$PAGE$ of $TOTAL$', + right: '', + logo: null + } +}; + +// Page Size Definitions (in twentieths of a point for Word, mm/inches for Pandoc) +const PAGE_SIZES = { + a4: { + name: 'A4', + pandoc: 'a4', + word: { width: 11906, height: 16838 }, // 210×297mm + dimensions: '210×297mm' + }, + a3: { + name: 'A3', + pandoc: 'a3', + word: { width: 16838, height: 23811 }, // 297×420mm + dimensions: '297×420mm' + }, + a5: { + name: 'A5', + pandoc: 'a5', + word: { width: 8391, height: 11906 }, // 148×210mm + dimensions: '148×210mm' + }, + b4: { + name: 'B4', + pandoc: 'b4', + word: { width: 14170, height: 20015 }, // 250×353mm + dimensions: '250×353mm' + }, + b5: { + name: 'B5', + pandoc: 'b5', + word: { width: 9979, height: 14170 }, // 176×250mm + dimensions: '176×250mm' + }, + letter: { + name: 'Letter', + pandoc: 'letter', + word: { width: 12240, height: 15840 }, // 8.5×11in + dimensions: '8.5×11in' + }, + legal: { + name: 'Legal', + pandoc: 'legal', + word: { width: 12240, height: 20160 }, // 8.5×14in + dimensions: '8.5×14in' + }, + tabloid: { + name: 'Tabloid', + pandoc: 'tabloid', + word: { width: 15840, height: 24480 }, // 11×17in + dimensions: '11×17in' + } +}; + +// Default page settings +let pageSettings = { + size: 'a4', + orientation: 'portrait', + customWidth: null, + customHeight: null +}; + +// Handle single instance lock for Windows file association +// When a file is double-clicked and the app is already running, +// Windows tries to start a second instance. We prevent this and +// pass the file to the existing instance instead. +const gotTheLock = app.requestSingleInstanceLock(); + +if (!gotTheLock) { + // Another instance is already running, quit this one + app.quit(); +} else { + // This is the first instance, handle second-instance events + app.on('second-instance', (event, commandLine, workingDirectory) => { + // Someone tried to run a second instance, focus our window instead + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + } + + // Check if a file was passed to the second instance + // commandLine is an array like: ['PanConverter.exe', 'file.md'] + console.log('[MAIN] Second instance commandLine:', JSON.stringify(commandLine)); + const startIndex = app.isPackaged ? 1 : 2; + const fileArgs = commandLine.slice(startIndex); + console.log('[MAIN] Second instance file args:', fileArgs); + for (const arg of fileArgs) { + if ((arg.endsWith('.md') || arg.endsWith('.markdown'))) { + const resolvedPath = path.isAbsolute(arg) ? arg : path.resolve(workingDirectory, arg); + console.log('[MAIN] Second instance resolved path:', resolvedPath); + if (fs.existsSync(resolvedPath)) { + // Open the file in the existing instance + if (rendererReady) { + openFileFromPath(resolvedPath); + } else { + app.pendingFile = resolvedPath; + } + break; + } + } + } + }); +} + +// Check if pandoc is available (using execFile for consistency) +function checkPandocAvailability() { + return new Promise((resolve) => { + if (pandocAvailable !== null) { + resolve(pandocAvailable); + return; + } + + execFile('pandoc', ['--version'], (error, stdout, stderr) => { + pandocAvailable = !error; + resolve(pandocAvailable); + }); + }); +} + + + +// Show About Dialog with logo +function showAboutDialog() { + const aboutWindow = new BrowserWindow({ + width: 500, + height: 600, + parent: mainWindow, + modal: true, + resizable: false, + minimizable: false, + maximizable: false, + webPreferences: { + nodeIntegration: false, + contextIsolation: true + }, + icon: path.join(__dirname, '../assets/icon.png') + }); + + // Convert images to base64 for data URL compatibility + let logoBase64 = ''; + let iconBase64 = ''; + try { + const logoPath = path.join(__dirname, '../assets/logo.png'); + const iconPath = path.join(__dirname, '../assets/icon.png'); + if (fs.existsSync(logoPath)) { + logoBase64 = 'data:image/png;base64,' + fs.readFileSync(logoPath).toString('base64'); + } + if (fs.existsSync(iconPath)) { + iconBase64 = 'data:image/png;base64,' + fs.readFileSync(iconPath).toString('base64'); + } + } catch (e) { + console.error('Error loading about dialog images:', e); + } + + const aboutHTML = ` + + + + + About MarkdownConverter + + + + +

MarkdownConverter

+
Version ${app.getVersion()}
+ +
+ by + +
+ +
+

Features

+
    +
  • Professional Markdown editing with syntax highlighting
  • +
  • Universal file converter (Image, Audio, Video, PDF)
  • +
  • Batch conversion for all media types
  • +
  • Advanced PDF Editor (merge, split, watermark, encrypt)
  • +
  • Export to PDF, DOCX, HTML, LaTeX, EPUB & more
  • +
  • 23+ beautiful themes including ConcreteInfo theme
  • +
  • ASCII Art & Table generators
  • +
  • Code file syntax highlighting
  • +
+
+ +
+

Contact

+

Email: amit.wh@gmail.com

+

Website: GitHub Repository

+
+ + + +`; + + aboutWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(aboutHTML)); + aboutWindow.setMenuBarVisibility(false); +} + +// Show Dependencies Dialog +function showDependenciesDialog() { + const depsWindow = new BrowserWindow({ + width: 600, + height: 700, + parent: mainWindow, + modal: true, + resizable: true, + webPreferences: { + nodeIntegration: false, + contextIsolation: true + }, + icon: path.join(__dirname, '../assets/icon.png') + }); + + const depsHTML = ` + + + + + Dependencies & Requirements + + + +

Dependencies & Requirements

+ +

Required Dependencies

+ +
+
Pandoc Required
+
Universal document converter. Required for most export formats (PDF, DOCX, LaTeX, EPUB).
+ https://pandoc.org/installing.html +
+ +

Optional Dependencies (for extended features)

+ +
+
FFmpeg Optional
+
Required for audio/video conversion and processing.
+ https://ffmpeg.org/download.html +
+ +
+
ImageMagick Optional
+
Required for image format conversion and processing.
+ https://imagemagick.org/script/download.php +
+ +
+
LibreOffice Optional
+
Required for enhanced document conversion (Office formats).
+ https://www.libreoffice.org/download/download/ +
+ +
+
MiKTeX / TeX Live Optional
+
Required for PDF export via LaTeX (higher quality PDFs).
+ https://miktex.org/download +
+ +

Bundled Libraries

+ +
+
pdf-lib
+
PDF manipulation library for merge, split, watermark, and encryption features.
+
+ +
+
marked
+
Markdown parser for preview rendering.
+
+ +
+
highlight.js
+
Syntax highlighting for code blocks.
+
+ +
+
DOMPurify
+
HTML sanitization for security.
+
+ +`; + + depsWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(depsHTML)); + depsWindow.setMenuBarVisibility(false); +} + +// Open PDF File for viewing/editing +function openPDFFile() { + const files = dialog.showOpenDialogSync(mainWindow, { + properties: ['openFile'], + filters: [ + { name: 'PDF Files', extensions: ['pdf'] } + ] + }); + + if (files && files[0]) { + const stats = fs.statSync(files[0]); + if (stats.size > MAX_FILE_SIZE) { + dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); + return; + } + mainWindow.webContents.send('open-pdf-viewer', files[0]); + } +} + +function openFile() { + const files = dialog.showOpenDialogSync(mainWindow, { + properties: ['openFile'], + filters: [ + { name: 'Markdown', extensions: ['md', 'markdown'] }, + { name: 'Developer Formats', extensions: ['json', 'yaml', 'yml', 'xml', 'toml'] }, + { name: 'All Files', extensions: ['*'] } + ] + }); + + if (files && files[0]) { + const stats = fs.statSync(files[0]); + if (stats.size > MAX_FILE_SIZE) { + dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); + return; + } + currentFile = files[0]; + const ext = path.extname(currentFile).toLowerCase().slice(1); + let content = fs.readFileSync(currentFile, 'utf-8'); + + // Wrap developer format files in code blocks for markdown display + const devFormats = ['json', 'yaml', 'yml', 'xml', 'toml']; + if (devFormats.includes(ext)) { + content = convertDataToMarkdown(content, ext); + } + + mainWindow.webContents.send('file-opened', { path: currentFile, content }); + } +} + +function openPdfFile() { + const files = dialog.showOpenDialogSync(mainWindow, { + properties: ['openFile'], + filters: [ + { name: 'PDF Files', extensions: ['pdf'] }, + { name: 'All Files', extensions: ['*'] } + ] + }); + + if (files && files[0]) { + const stats = fs.statSync(files[0]); + if (stats.size > MAX_FILE_SIZE) { + dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); + return; + } + mainWindow.webContents.send('open-pdf-file', files[0]); + } +} + +function saveAsFile() { + const file = dialog.showSaveDialogSync(mainWindow, { + defaultExt: '.md', + filters: [ + { name: 'Markdown', extensions: ['md', 'markdown'] }, + { name: 'All Files', extensions: ['*'] } + ] + }); + + if (file) { + currentFile = file; + mainWindow.webContents.send('get-content-for-save', file); + } +} + +function exportFile(format) { + if (!currentFile) { + dialog.showErrorBox('Error', 'Please save the file first'); + return; + } + + // Show export options dialog + showExportOptionsDialog(format); +} + +function showExportOptionsDialog(format) { + mainWindow.webContents.send('show-export-dialog', format); +} + +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)}` + }); + } +} + +// 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'); + } +}); + +// Header & Footer Settings IPC Handlers + +// Get current header/footer settings +ipcMain.on('get-header-footer-settings', (event) => { + event.reply('header-footer-settings-data', headerFooterSettings); +}); + +// Save header/footer settings +ipcMain.on('save-header-footer-settings', (event, settings) => { + headerFooterSettings = settings; + store.set('headerFooterSettings', headerFooterSettings); + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Settings Saved', + message: 'Header and footer settings have been saved successfully!', + buttons: ['OK'] + }); +}); + +// Get current page settings +ipcMain.on('get-page-settings', (event) => { + event.reply('page-settings-data', pageSettings); +}); + +// Update page settings from export dialog +ipcMain.on('update-page-settings', (event, settings) => { + pageSettings = settings; + store.set('pageSettings', pageSettings); + console.log('Page settings updated:', pageSettings); +}); + +// Save header/footer logo image +// Browse for header/footer logo +ipcMain.on('browse-header-footer-logo', async (event, position) => { + try { + const result = await dialog.showOpenDialog(mainWindow, { + title: `Select ${position.charAt(0).toUpperCase() + position.slice(1)} Logo/Image`, + filters: [ + { name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp'] } + ], + properties: ['openFile'] + }); + + if (!result.canceled && result.filePaths.length > 0) { + const filePath = result.filePaths[0]; + + // Copy image to userData directory for persistent storage + const userDataPath = app.getPath('userData'); + const logoDir = path.join(userDataPath, 'logos'); + + // Create logos directory if it doesn't exist + if (!fs.existsSync(logoDir)) { + fs.mkdirSync(logoDir, { recursive: true }); + } + + // Generate unique filename + const ext = path.extname(filePath); + const filename = `${position}_${Date.now()}${ext}`; + const destPath = path.join(logoDir, filename); + + // Copy file + fs.copyFileSync(filePath, destPath); + + // Update settings + if (position === 'header') { + headerFooterSettings.header.logo = destPath; + } else if (position === 'footer') { + headerFooterSettings.footer.logo = destPath; + } + + event.reply('header-footer-logo-saved', { position, path: destPath }); + } + } catch (error) { + console.error('Logo browse error:', error); + dialog.showErrorBox('Logo Error', sanitizeErrorMessage(`Failed to select logo: ${error.message}`)); + } +}); + +ipcMain.on('save-header-footer-logo', async (event, { position, filePath }) => { + try { + if (!filePath) { + dialog.showErrorBox('Logo Error', 'Failed to save logo: The "path" argument must be of type string. Received undefined'); + return; + } + + // Copy image to userData directory for persistent storage + const userDataPath = app.getPath('userData'); + const logoDir = path.join(userDataPath, 'logos'); + + // Create logos directory if it doesn't exist + if (!fs.existsSync(logoDir)) { + fs.mkdirSync(logoDir, { recursive: true }); + } + + // Verify source file exists + if (!fs.existsSync(filePath)) { + dialog.showErrorBox('Logo Error', sanitizeErrorMessage(`Source file not found: ${filePath}`)); + return; + } + + // Generate unique filename + const ext = path.extname(filePath); + const filename = `${position}_${Date.now()}${ext}`; + const destPath = path.join(logoDir, filename); + + // Copy file + fs.copyFileSync(filePath, destPath); + + // Update settings + if (position === 'header') { + headerFooterSettings.header.logo = destPath; + } else if (position === 'footer') { + headerFooterSettings.footer.logo = destPath; + } + + event.reply('header-footer-logo-saved', { position, path: destPath }); + } catch (error) { + console.error('Logo save error:', error); + dialog.showErrorBox('Logo Error', sanitizeErrorMessage(`Failed to save logo: ${error.message}`)); + } +}); + +// Clear header/footer logo +ipcMain.on('clear-header-footer-logo', (event, position) => { + if (position === 'header') { + headerFooterSettings.header.logo = null; + } else if (position === 'footer') { + headerFooterSettings.footer.logo = null; + } + event.reply('header-footer-logo-cleared', position); +}); + +// Helper function to process dynamic fields in header/footer text +function processDynamicFields(text, metadata = {}) { + if (!text) return ''; + + const now = new Date(); + const dateStr = now.toLocaleDateString(); + const timeStr = now.toLocaleTimeString(); + + let result = text; + result = result.replace(/\$DATE\$/g, dateStr); + result = result.replace(/\$TIME\$/g, timeStr); + result = result.replace(/\$TITLE\$/g, metadata.title || 'Untitled'); + result = result.replace(/\$AUTHOR\$/g, metadata.author || ''); + result = result.replace(/\$FILENAME\$/g, metadata.filename || ''); + + // Note: $PAGE$ and $TOTAL$ are handled by Pandoc/export tools + + return result; +} + +// Add headers/footers to DOCX file using PizZip and docx4js +// Function to set page size in DOCX files +async function setDocxPageSize(docxPath) { + try { + const PizZip = require('pizzip'); + + // Read the DOCX file + const docxBuffer = fs.readFileSync(docxPath); + const zip = new PizZip(docxBuffer); + + // Get document.xml + let documentXml = zip.file('word/document.xml').asText(); + + // Get page dimensions + let width, height; + const pageSize = PAGE_SIZES[pageSettings.size]; + + if (pageSize) { + width = pageSize.word.width; + height = pageSize.word.height; + } else if (pageSettings.customWidth && pageSettings.customHeight) { + // Parse custom dimensions (convert to twentieths of a point) + // Note: This is simplified - production code should handle various units + width = parseInt(pageSettings.customWidth) || 11906; + height = parseInt(pageSettings.customHeight) || 16838; + } else { + // Default to A4 + width = 11906; + height = 16838; + } + + // Swap dimensions for landscape + if (pageSettings.orientation === 'landscape') { + [width, height] = [height, width]; + } + + // Update all elements in section properties + const pgSzRegex = /]*\/>/g; + documentXml = documentXml.replace(pgSzRegex, () => { + return ``; + }); + + // If no pgSz found, add it to all sectPr elements + if (!pgSzRegex.test(documentXml)) { + const sectPrRegex = /]*>/g; + documentXml = documentXml.replace(sectPrRegex, (match) => { + return `${match}`; + }); + } + + // Save updated document.xml + zip.file('word/document.xml', documentXml); + + // Write modified DOCX + const newDocxBuffer = zip.generate({ type: 'nodebuffer' }); + fs.writeFileSync(docxPath, newDocxBuffer); + + } catch (error) { + console.error('Failed to set DOCX page size:', error); + } +} + +async function addHeaderFooterToDocx(docxPath, metadata = {}) { + if (!headerFooterSettings.enabled) return; + + try { + const PizZip = require('pizzip'); + + // Read the DOCX file + const docxBuffer = fs.readFileSync(docxPath); + const zip = new PizZip(docxBuffer); + + // Process dynamic fields + const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); + const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); + const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); + const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); + const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); + const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); + + // Create header XML + if (headerLeft || headerCenter || headerRight) { + const headerXml = ` + + + + ${headerLeft || ''} + + + + ${headerCenter || ''} + + + + ${headerRight || ''} + +`; + zip.file('word/header1.xml', headerXml); + } + + // Create footer XML with page numbers + if (footerLeft || footerCenter || footerRight) { + let footerCenterXml = ''; + if (footerCenter) { + // Handle $PAGE$ and $TOTAL$ in footer + if (footerCenter.includes('$PAGE$') || footerCenter.includes('$TOTAL$')) { + const parts = footerCenter.split(/(\$PAGE\$|\$TOTAL\$)/); + footerCenterXml = parts.map(part => { + if (part === '$PAGE$') { + return ''; + } else if (part === '$TOTAL$') { + return ''; + } else { + return `${part}`; + } + }).join(''); + } else { + footerCenterXml = `${footerCenter}`; + } + } + + const footerXml = ` + + + + ${footerLeft || ''} + + + + ${footerCenterXml} + + + + ${footerRight || ''} + +`; + zip.file('word/footer1.xml', footerXml); + } + + // Update document.xml.rels to reference header/footer + let relsXml = zip.file('word/_rels/document.xml.rels').asText(); + + // Add header relationship if not exists + if ((headerLeft || headerCenter || headerRight) && !relsXml.includes('header1.xml')) { + const headerId = 'rId100'; + const headerRel = ``; + relsXml = relsXml.replace('', headerRel + ''); + } + + // Add footer relationship if not exists + if ((footerLeft || footerCenter || footerRight) && !relsXml.includes('footer1.xml')) { + const footerId = 'rId101'; + const footerRel = ``; + relsXml = relsXml.replace('', footerRel + ''); + } + + zip.file('word/_rels/document.xml.rels', relsXml); + + // Update document.xml to use header/footer in sections + let documentXml = zip.file('word/document.xml').asText(); + if ((headerLeft || headerCenter || headerRight || footerLeft || footerCenter || footerRight)) { + // Find all section properties and add header/footer references + const sectPrRegex = /]*>[\s\S]*?<\/w:sectPr>/g; + documentXml = documentXml.replace(sectPrRegex, (match) => { + let updated = match; + if ((headerLeft || headerCenter || headerRight) && !match.includes('headerReference')) { + updated = updated.replace('', ''); + } + if ((footerLeft || footerCenter || footerRight) && !match.includes('footerReference')) { + updated = updated.replace('', ''); + } + return updated; + }); + } + + zip.file('word/document.xml', documentXml); + + // Write modified DOCX + const newDocxBuffer = zip.generate({ type: 'nodebuffer' }); + fs.writeFileSync(docxPath, newDocxBuffer); + + } catch (error) { + console.error('Failed to add headers/footers to DOCX:', error); + // Don't fail the export, just log the error + } +} + +// 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 with selected template, start page, and page settings + const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage, pageSettings); + + // 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', sanitizeErrorMessage(`Failed to export document: ${error.message}`)); + } +} + +// 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 (using execFile for safety) + const soffice = process.platform === 'win32' + ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' + : 'soffice'; + + const outputDir = path.dirname(result.filePath); + const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath]; + + execFile(soffice, sofficeArgs, (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', + sanitizeErrorMessage(`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', sanitizeErrorMessage(`Failed to export PDF: ${error.message}`)); + } +} + +// Universal File Converter integration +function showUniversalConverterDialog() { + mainWindow.webContents.send('show-universal-converter-dialog'); +} + +// PDF Editor dialog +function showPDFEditorDialog(operation) { + mainWindow.webContents.send('show-pdf-editor-dialog', operation); +} + +// Handle PDF editor from toolbar (with optional file path) +ipcMain.on('show-pdf-editor-from-toolbar', (event, { operation, filePath }) => { + mainWindow.webContents.send('show-pdf-editor-dialog', operation, filePath); +}); + +// Check if conversion tool is available (using execFile for safety) +function checkConverterAvailable(tool) { + return new Promise((resolve) => { + const isWin = process.platform === 'win32'; + const locateCmd = isWin ? 'where' : 'which'; + let toolName; + + switch (tool) { + case 'libreoffice': + toolName = 'soffice'; + break; + case 'imagemagick': + toolName = isWin ? 'magick' : 'convert'; + break; + case 'ffmpeg': { + // ffmpeg-static is always bundled — check it directly + const ffmpegBin = getFFmpegPath(); + resolve(fs.existsSync(ffmpegBin) || ffmpegBin === 'ffmpeg'); + return; + } + default: + resolve(false); + return; + } + + execFile(locateCmd, [toolName], (error) => { + resolve(!error); + }); + }); +} + +// Handle universal file conversion +ipcMain.on('universal-convert', async (event, { tool, fromFormat, toFormat, filePath }) => { + if (!conversionLimiter()) { + mainWindow.webContents.send('conversion-status', 'Please wait before converting again...'); + return; + } + try { + mainWindow.webContents.send('conversion-status', 'Checking converter availability...'); + + // Check if the required tool is available + const toolAvailable = await checkConverterAvailable(tool); + + if (!toolAvailable) { + throw new Error(`${tool} is not installed or not found in PATH. Please install it first.`); + } + + mainWindow.webContents.send('conversion-status', 'Converting file...'); + + const outputPath = filePath.replace(/\.[^/.]+$/, `.${toFormat}`); + let conversionInfo; + + switch (tool) { + case 'libreoffice': + conversionInfo = convertWithLibreOffice(filePath, toFormat, outputPath); + break; + case 'imagemagick': + conversionInfo = convertWithImageMagick(filePath, outputPath); + break; + case 'ffmpeg': + conversionInfo = convertWithFFmpeg(filePath, outputPath); + break; + case 'pandoc': + conversionInfo = convertWithPandoc(filePath, outputPath); + break; + default: + throw new Error(`Unknown conversion tool: ${tool}`); + } + + // Use execFile for safety (prevents command injection) + execFile(conversionInfo.command, conversionInfo.args, (error, stdout, stderr) => { + if (error) { + mainWindow.webContents.send('conversion-complete', { + success: false, + error: error.message + }); + + dialog.showMessageBox(mainWindow, { + type: 'error', + title: 'Conversion Failed', + message: `${tool} conversion failed`, + detail: stderr || error.message, + buttons: ['OK'] + }); + } else { + mainWindow.webContents.send('conversion-complete', { + success: true, + outputPath: outputPath + }); + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Conversion Complete', + message: 'File converted successfully!', + detail: `Saved to: ${outputPath}`, + buttons: ['OK'] + }); + } + }); + } catch (error) { + mainWindow.webContents.send('conversion-complete', { + success: false, + error: error.message + }); + + dialog.showMessageBox(mainWindow, { + type: 'error', + title: 'Conversion Failed', + message: 'Universal conversion failed', + detail: error.message, + buttons: ['OK'] + }); + } +}); + +// Handle universal batch file conversion +ipcMain.on('universal-convert-batch', async (event, { tool, fromFormat, toFormat, inputFolder, outputFolder, includeSubfolders, advancedOptions }) => { + if (!conversionLimiter()) { + mainWindow.webContents.send('conversion-status', 'Please wait before converting again...'); + return; + } + try { + const toolAvailable = await checkConverterAvailable(tool); + if (!toolAvailable) { + throw new Error(`${tool} is not installed or not found in PATH. Please install it first.`); + } + + // Collect matching files + const files = []; + function collectFiles(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory() && includeSubfolders) { + collectFiles(fullPath); + } else if (entry.isFile() && entry.name.toLowerCase().endsWith(`.${fromFormat}`)) { + files.push(fullPath); + } + } + } + collectFiles(inputFolder); + + if (files.length === 0) { + mainWindow.webContents.send('conversion-complete', { success: false, error: `No .${fromFormat} files found in the selected folder.` }); + return; + } + + let completed = 0; + let failed = 0; + + for (const filePath of files) { + const relativePath = path.relative(inputFolder, filePath); + const outputPath = path.join(outputFolder, relativePath.replace(/\.[^/.]+$/, `.${toFormat}`)); + + // Ensure output subdirectory exists + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + + mainWindow.webContents.send('conversion-status', `Converting ${completed + 1}/${files.length}: ${path.basename(filePath)}`); + + let conversionInfo; + switch (tool) { + case 'libreoffice': conversionInfo = convertWithLibreOffice(filePath, toFormat, outputPath); break; + case 'imagemagick': conversionInfo = convertWithImageMagick(filePath, outputPath); break; + case 'ffmpeg': conversionInfo = convertWithFFmpeg(filePath, outputPath); break; + case 'pandoc': conversionInfo = convertWithPandoc(filePath, outputPath); break; + default: throw new Error(`Unknown conversion tool: ${tool}`); + } + + await new Promise((resolve) => { + execFile(conversionInfo.command, conversionInfo.args, (error) => { + if (error) { failed++; } else { completed++; } + resolve(); + }); + }); + } + + mainWindow.webContents.send('conversion-complete', { + success: true, + outputPath: outputFolder + }); + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Batch Conversion Complete', + message: `Batch conversion finished!`, + detail: `Converted: ${completed}/${files.length} files${failed > 0 ? ` (${failed} failed)` : ''}\nOutput: ${outputFolder}`, + buttons: ['OK'] + }); + } catch (error) { + mainWindow.webContents.send('conversion-complete', { success: false, error: error.message }); + dialog.showMessageBox(mainWindow, { + type: 'error', + title: 'Batch Conversion Failed', + message: 'Batch conversion failed', + detail: error.message, + buttons: ['OK'] + }); + } +}); + +// LibreOffice conversion - returns {command, args} for execFile (safer than exec) +function convertWithLibreOffice(inputFile, outputFormat, outputPath) { + const outputDir = path.dirname(outputPath); + const soffice = process.platform === 'win32' + ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' + : 'soffice'; + + // LibreOffice conversion format mapping + const formatMap = { + 'pdf': 'pdf', + 'docx': 'docx', + 'doc': 'doc', + 'odt': 'odt', + 'rtf': 'rtf', + 'txt': 'txt', + 'html': 'html', + 'xlsx': 'xlsx', + 'xls': 'xls', + 'ods': 'ods', + 'csv': 'csv', + 'pptx': 'pptx', + 'ppt': 'ppt', + 'odp': 'odp' + }; + + const format = formatMap[outputFormat] || outputFormat; + + // Return command and args for execFile + return { + command: soffice, + args: ['--headless', '--convert-to', format, '--outdir', outputDir, inputFile] + }; +} + +// ImageMagick conversion - returns {command, args} for execFile +function convertWithImageMagick(inputFile, outputPath) { + const magick = process.platform === 'win32' ? 'magick' : 'convert'; + return { + command: magick, + args: [inputFile, outputPath] + }; +} + +// FFmpeg conversion - returns {command, args} for execFile +function convertWithFFmpeg(inputFile, outputPath) { + return { + command: getFFmpegPath(), + args: ['-i', inputFile, outputPath, '-y'], + }; +} + +// Pandoc conversion - returns {command, args} for execFile +function convertWithPandoc(inputFile, outputPath) { + return { + command: 'pandoc', + args: [inputFile, '-o', outputPath] + }; +} + +function performExportWithOptions(format, options) { + // Map format names to file extensions + const formatExtMap = { + revealjs: 'html', + beamer: 'pdf', + confluence: 'txt', + jira: 'txt', + }; + const fileExt = formatExtMap[format] || format; + + const outputFile = dialog.showSaveDialogSync(mainWindow, { + defaultPath: currentFile.replace(/\.[^/.]+$/, `.${fileExt}`), + filters: [ + { name: format.toUpperCase(), extensions: [fileExt] } + ] + }); + + if (!outputFile) return; // User cancelled + + console.log(`Attempting to export ${format} to:`, outputFile); + + // Check pandoc availability first + checkPandocAvailability().then((hasPandoc) => { + console.log('Pandoc available:', hasPandoc); + + if (!hasPandoc) { + // Handle formats that don't require pandoc + if (format === 'html') { + console.log('Using built-in HTML export'); + exportToHTML(outputFile); + return; + } else if (format === 'pdf') { + console.log('Using built-in PDF export'); + exportToPDFElectron(outputFile); + return; + } else { + dialog.showErrorBox('Export Error', + `Pandoc is required for ${format.toUpperCase()} export but is not installed or not found in PATH.\n\n` + + `Please install Pandoc from: https://pandoc.org/installing.html\n\n` + + `Alternatively, you can export to HTML or PDF using the built-in converters.` + ); + return; + } + } + + // Use pandoc for export with advanced options + console.log('Using Pandoc for export'); + let pandocCmd = `${getPandocPath()} "${currentFile}" -o "${outputFile}"`; + + // Add template if specified + if (options.template && options.template !== 'default') { + pandocCmd += ` --template="${options.template}"`; + } + + // Add metadata + if (options.metadata) { + for (const [key, value] of Object.entries(options.metadata)) { + if (value.trim()) { + pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; + } + } + } + + // Add variables + if (options.variables) { + for (const [key, value] of Object.entries(options.variables)) { + if (value.trim()) { + pandocCmd += ` -V ${key}="${value.replace(/"/g, '\\"')}"`; + } + } + } + + // Add other options + if (options.toc) pandocCmd += ' --toc'; + if (options.tocDepth) pandocCmd += ` --toc-depth=${options.tocDepth}`; + if (options.numberSections) pandocCmd += ' --number-sections'; + if (options.citeproc) pandocCmd += ' --citeproc'; + if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`; + if (options.csl) pandocCmd += ` --csl="${options.csl}"`; + + // Add specific options for PDF export to ensure proper generation + if (format === 'pdf') { + const pdfEngine = options.pdfEngine || 'xelatex'; // Default to xelatex + pandocCmd += ` --pdf-engine="${pdfEngine}"`; + if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; + + // Add monospace font settings for code blocks (ASCII art preservation) + pandocCmd += ' -V monofont="Consolas"'; + pandocCmd += ' --highlight-style=tango'; + + // Add header/footer if enabled + if (headerFooterSettings.enabled) { + const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; + const metadata = { filename, title: filename, author: '' }; + + const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); + const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); + const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); + const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); + const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); + const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); + + // Create LaTeX header + const latexHeader = ` +\\usepackage{fancyhdr} +\\pagestyle{fancy} +\\fancyhf{} +\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} +\\chead{${headerCenter.replace(/\\/g, '\\\\')}} +\\rhead{${headerRight.replace(/\\/g, '\\\\')}} +\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} +\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} +\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} +\\renewcommand{\\headrulewidth}{0.4pt} +\\renewcommand{\\footrulewidth}{0.4pt} +`; + const headerFile = path.join(require('os').tmpdir(), `header_export_${Date.now()}.tex`); + fs.writeFileSync(headerFile, latexHeader, 'utf-8'); + pandocCmd += ` --include-in-header="${headerFile}"`; + pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"'; + } + + // Try with specified PDF engine (using runPandocCmd for safety) + runPandocCmd(pandocCmd, (error) => { + if (error) { + // Try fallback engines if the specified one fails + const fallbackEngines = ['pdflatex', 'lualatex']; + tryPdfFallback(currentFile, outputFile, fallbackEngines, 0, options, error); + } else { + showExportSuccess(outputFile); + } + }); + } else if (format === 'docx') { + pandocCmd += ' -t docx'; + exportWithPandoc(pandocCmd, outputFile, format); + } else if (format === 'pptx') { + // Add PowerPoint footer if enabled + if (headerFooterSettings.enabled && headerFooterSettings.footer.center) { + const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; + const metadata = { filename, title: filename, author: '' }; + const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata); + pandocCmd += ` --variable footer="${footerText}"`; + } + exportWithPandoc(pandocCmd, outputFile, format); + } else if (format === 'json') { + pandocCmd = `${getPandocPath()} "${currentFile}" -t json -o "${outputFile}"`; + exportWithPandoc(pandocCmd, outputFile, format); + } else if (format === 'yaml' || format === 'xml' || format === 'toml') { + // For YAML/XML/TOML, save the raw markdown content with the new extension + try { + const content = fs.readFileSync(currentFile, 'utf-8'); + fs.writeFileSync(outputFile, content, 'utf-8'); + showExportSuccess(outputFile); + } catch (err) { + dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Failed to export: ${err.message}`)); + } + } else if (format === 'revealjs') { + let revealCmd = `${getPandocPath()} "${currentFile}" -t revealjs -s -o "${outputFile}" --slide-level=2`; + if (options) { + if (options.revealTheme) revealCmd += ` -V theme="${options.revealTheme}"`; + if (options.revealTransition) revealCmd += ` -V transition="${options.revealTransition}"`; + if (options.revealTransitionSpeed) revealCmd += ` -V transitionSpeed="${options.revealTransitionSpeed}"`; + if (options.revealControls !== undefined) revealCmd += ` -V controls="${options.revealControls}"`; + if (options.revealSlideNumber !== undefined) revealCmd += ` -V slideNumber="${options.revealSlideNumber}"`; + if (options.revealProgress !== undefined) revealCmd += ` -V progress="${options.revealProgress}"`; + if (options.revealHistory !== undefined) revealCmd += ` -V history="${options.revealHistory}"`; + if (options.revealCenter !== undefined) revealCmd += ` -V center="${options.revealCenter}"`; + + // Support for templates, metadata, bibliography + if (options.template && options.template !== 'default') { + revealCmd += ` --template="${options.template}"`; + } + if (options.metadata) { + for (const [key, value] of Object.entries(options.metadata)) { + if (value.trim()) { + revealCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; + } + } + } + if (options.bibliography) revealCmd += ` --bibliography="${options.bibliography}"`; + if (options.csl) revealCmd += ` --csl="${options.csl}"`; + } + exportWithPandoc(revealCmd, outputFile, format); + } else if (format === 'beamer') { + pandocCmd = `${getPandocPath()} "${currentFile}" -t beamer -o "${outputFile}"`; + exportWithPandoc(pandocCmd, outputFile, format); + } else if (format === 'confluence' || format === 'jira') { + pandocCmd = `${getPandocPath()} "${currentFile}" -t jira -o "${outputFile}"`; + exportWithPandoc(pandocCmd, outputFile, format); + } else if (format === 'mobi') { + // First export to EPUB, then try ebook-convert if available + const epubFile = outputFile.replace(/\.mobi$/i, '.epub'); + pandocCmd = `${getPandocPath()} "${currentFile}" -o "${epubFile}"`; + runPandocCmd(pandocCmd, (error) => { + if (error) { + dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Failed to export EPUB intermediate: ${error.message}`)); + return; + } + // Try ebook-convert (Calibre) for MOBI + execFile('ebook-convert', [epubFile, outputFile], (ebookError) => { + if (ebookError) { + dialog.showMessageBox(mainWindow, { + type: 'warning', + title: 'MOBI Export - Partial', + message: `Calibre's ebook-convert was not found. The file has been exported as EPUB instead.\n\nEPUB saved to: ${epubFile}\n\nTo get MOBI output, install Calibre from: https://calibre-ebook.com/`, + buttons: ['OK'] + }); + } else { + // Clean up intermediate EPUB + try { fs.unlinkSync(epubFile); } catch (e) { /* ignore */ } + showExportSuccess(outputFile); + } + }); + }); + } else { + // Generic export for other formats + exportWithPandoc(pandocCmd, outputFile, format); + } + }).catch((error) => { + console.error('Error checking pandoc availability:', error); + dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Error checking system requirements: ${error.message}`)); + }); +} + +function tryPdfFallback(inputFile, outputFile, engines, index, options, lastError) { + if (index >= engines.length) { + // All Pandoc PDF engines failed, fallback to Electron's built-in PDF export + console.log('All Pandoc PDF engines failed, falling back to Electron PDF export'); + exportToPDFElectron(outputFile); + return; + } + + const engine = engines[index]; + let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -o "${outputFile}"`; + + // Add monospace font settings for code blocks (ASCII art preservation) + pandocCmd += ' -V monofont="Consolas"'; + pandocCmd += ' --highlight-style=tango'; + + // Add geometry if specified + if (options.geometry) pandocCmd = pandocCmd.replace(` -o `, ` -V geometry:"${options.geometry}" -o `); + + // Add header/footer if enabled + if (headerFooterSettings.enabled) { + const filename = path.basename(inputFile, path.extname(inputFile)); + const metadata = { filename, title: filename, author: options.metadata?.author || '' }; + + const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); + const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); + const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); + const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); + const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); + const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); + + // Create LaTeX header for fancyhdr + const latexHeader = ` +\\usepackage{fancyhdr} +\\usepackage{lastpage} +\\pagestyle{fancy} +\\fancyhf{} +\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} +\\chead{${headerCenter.replace(/\\/g, '\\\\')}} +\\rhead{${headerRight.replace(/\\/g, '\\\\')}} +\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} +\\cfoot{${footerCenter.replace(/\$PAGE\$/g, '\\\\thepage').replace(/\$TOTAL\$/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} +\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} +\\renewcommand{\\headrulewidth}{0.4pt} +\\renewcommand{\\footrulewidth}{0.4pt} +`; + const headerFile = path.join(require('os').tmpdir(), `header_fallback_${Date.now()}.tex`); + fs.writeFileSync(headerFile, latexHeader, 'utf-8'); + pandocCmd += ` --include-in-header="${headerFile}"`; + } + + // Add all other options + if (options.template && options.template !== 'default') { + pandocCmd += ` --template="${options.template}"`; + } + + if (options.metadata) { + for (const [key, value] of Object.entries(options.metadata)) { + if (value.trim()) { + pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; + } + } + } + + // Use runPandocCmd for safety (prevents command injection) + runPandocCmd(pandocCmd, (error) => { + if (error) { + tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error); + } else { + showExportSuccess(outputFile); + } + }); +} + +function showExportSuccess(outputFile) { + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Export Complete', + message: `File exported successfully to ${outputFile}`, + buttons: ['OK'] + }); +} + +// Helper function to export with pandoc (general) - uses runPandocCmd for safety +function exportWithPandoc(pandocCmd, outputFile, format) { + console.log(`Executing Pandoc command: ${pandocCmd}`); + + runPandocCmd(pandocCmd, async (error, stdout, stderr) => { + if (error) { + console.error(`Pandoc error for ${format}:`, error); + console.error(`Pandoc stderr:`, stderr); + console.error(`Pandoc stdout:`, stdout); + + // Provide more specific error messages + let errorMessage = `Failed to export to ${format.toUpperCase()}`; + + if (error.message.includes('not found') || error.message.includes('not recognized')) { + errorMessage += '\n\nPandoc is not installed or not found in PATH.'; + errorMessage += '\nPlease install Pandoc from: https://pandoc.org/installing.html'; + } else if (stderr) { + errorMessage += `\n\nError details: ${stderr}`; + } else { + errorMessage += `\n\nError details: ${error.message}`; + } + + errorMessage += `\n\nCommand used: ${pandocCmd}`; + + dialog.showErrorBox('Export Error', sanitizeErrorMessage(errorMessage)); + } else { + console.log(`Successfully exported to ${format}:`, outputFile); + console.log(`Pandoc stdout:`, stdout); + if (stderr) { + console.warn(`Pandoc stderr (non-fatal):`, stderr); + } + + // Set page size for DOCX + if (format === 'docx') { + try { + await setDocxPageSize(outputFile); + console.log('Page size set for DOCX'); + } catch (pageSizeError) { + console.error('Error setting page size for DOCX:', pageSizeError); + } + } + + // Add headers/footers to DOCX if enabled + if (format === 'docx' && headerFooterSettings.enabled) { + try { + const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; + const metadata = { + filename: filename, + title: filename, + author: '' + }; + await addHeaderFooterToDocx(outputFile, metadata); + console.log('Headers/footers added to DOCX'); + } catch (hfError) { + console.error('Error adding headers/footers to DOCX:', hfError); + // Continue with success message even if header/footer fails + } + } + + // Set page size for ODT + if (format === 'odt') { + try { + await setDocxPageSize(outputFile); // ODT has similar structure + console.log('Page size set for ODT'); + } catch (pageSizeError) { + console.error('Error setting page size for ODT:', pageSizeError); + } + } + + // Add headers/footers to ODT if enabled + if (format === 'odt' && headerFooterSettings.enabled) { + // ODT format is similar to DOCX in structure, we could implement this + console.log('ODT header/footer support not yet implemented'); + } + + showExportSuccess(outputFile); + } + }); +} + +// Helper function to export PDF with pandoc (with fallbacks) - uses runPandocCmd for safety +function exportWithPandocPDF(pandocCmd, outputFile) { + runPandocCmd(pandocCmd, (error, stdout, stderr) => { + if (error) { + console.log('XeLaTeX failed, trying PDFLaTeX...'); + // Fallback to pdflatex + const fallbackCmd = pandocCmd.replace('--pdf-engine=xelatex', '--pdf-engine=pdflatex'); + runPandocCmd(fallbackCmd, (fallbackError, fallbackStdout, fallbackStderr) => { + if (fallbackError) { + console.log('PDFLaTeX failed, trying Electron PDF...'); + // Final fallback to Electron PDF + exportToPDFElectron(outputFile); + } else { + console.log('Successfully exported PDF with PDFLaTeX'); + showExportSuccess(outputFile); + } + }); + } else { + console.log('Successfully exported PDF with XeLaTeX'); + showExportSuccess(outputFile); + } + }); +} + +// Export to HTML using marked (no pandoc required) +function exportToHTML(outputFile) { + try { + const marked = require('marked'); + const markdownContent = fs.readFileSync(currentFile, 'utf8'); + const htmlContent = marked.parse(markdownContent); + + const fullHtml = ` + + + + + Exported Document + + + + ${htmlContent} + +`; + + fs.writeFileSync(outputFile, fullHtml, 'utf8'); + console.log('Successfully exported HTML'); + showExportSuccess(outputFile); + } catch (error) { + console.error('HTML export error:', error); + dialog.showErrorBox('HTML Export Error', sanitizeErrorMessage(`Failed to export HTML: ${error.message}`)); + } +} + +// Export to PDF using Electron (no pandoc required) +function exportToPDFElectron(outputFile) { + try { + const marked = require('marked'); + const markdownContent = fs.readFileSync(currentFile, 'utf8'); + const htmlContent = marked.parse(markdownContent); + + const fullHtml = ` + + + + + PDF Export + + + + ${htmlContent} + +`; + + // Create a hidden window to render and export PDF + const pdfWindow = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + + pdfWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(fullHtml)}`).then(() => { + return pdfWindow.webContents.printToPDF({ + marginsType: 1, // Use default margins + pageSize: 'A4', + printBackground: true, + printSelectionOnly: false, + landscape: false + }); + }).then((pdfData) => { + fs.writeFileSync(outputFile, pdfData); + pdfWindow.close(); + console.log('Successfully exported PDF with Electron'); + showExportSuccess(outputFile); + }).catch((error) => { + pdfWindow.close(); + console.error('Electron PDF export error:', error); + dialog.showErrorBox('PDF Export Error', + sanitizeErrorMessage(`Failed to export PDF using built-in engine: ${error.message}\n\n` + + `For better PDF export, please install Pandoc with LaTeX support.`) + ); + }); + } catch (error) { + console.error('PDF export setup error:', error); + dialog.showErrorBox('PDF Export Error', sanitizeErrorMessage(`Failed to setup PDF export: ${error.message}`)); + } +} + +function exportSpreadsheet(format) { + if (!currentFile) { + dialog.showErrorBox('Error', 'Please save the file first'); + return; + } + + // Request content from renderer + mainWindow.webContents.send('get-content-for-spreadsheet', format); +} + +function importDocument() { + const files = dialog.showOpenDialogSync(mainWindow, { + properties: ['openFile'], + filters: [ + { name: 'Documents', extensions: ['docx', 'odt', 'rtf', 'html', 'htm', 'tex', 'epub', 'pdf', 'txt'] }, + { name: 'Presentations', extensions: ['pptx', 'odp'] }, + { name: 'Markup Languages', extensions: ['rst', 'textile', 'mediawiki', 'org', 'asciidoc', 'twiki', 'opml'] }, + { name: 'E-book Formats', extensions: ['epub', 'fb2'] }, + { name: 'LaTeX Formats', extensions: ['tex', 'latex', 'ltx'] }, + { name: 'Web Formats', extensions: ['html', 'htm', 'xhtml'] }, + { name: 'Wiki Formats', extensions: ['mediawiki', 'dokuwiki', 'tikiwiki', 'twiki'] }, + { name: 'CSV/TSV', extensions: ['csv', 'tsv'] }, + { name: 'Developer Formats', extensions: ['json', 'yaml', 'yml', 'xml', 'toml'] }, + { name: 'All Files', extensions: ['*'] } + ] + }); + + if (files && files[0]) { + const inputFile = files[0]; + const stats = fs.statSync(inputFile); + if (stats.size > MAX_FILE_SIZE) { + dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); + return; + } + const ext = path.extname(inputFile).toLowerCase().slice(1); + const outputFile = inputFile.replace(/\.[^/.]+$/, '.md'); + + // Determine format-specific conversion options + let additionalOptions = ''; + + // For PDFs, extract text properly + if (ext === 'pdf') { + additionalOptions = '--pdf-engine=xelatex'; + } + + // For CSV/TSV, convert as tables + if (ext === 'csv' || ext === 'tsv') { + additionalOptions = '--from=csv -t markdown'; + } + + // For JSON, handle structure + if (ext === 'json') { + additionalOptions = '--from=json -t markdown'; + } + + // For YAML, XML, TOML - wrap content in code blocks directly + if (['yaml', 'yml', 'xml', 'toml'].includes(ext)) { + const rawContent = fs.readFileSync(inputFile, 'utf-8'); + const mdContent = convertDataToMarkdown(rawContent, ext); + const mdOutputFile = inputFile.replace(/\.[^/.]+$/, '.md'); + fs.writeFileSync(mdOutputFile, mdContent, 'utf-8'); + currentFile = mdOutputFile; + mainWindow.webContents.send('file-opened', { path: mdOutputFile, content: mdContent }); + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Import Complete', + message: `Document imported successfully as ${path.basename(mdOutputFile)}\n\nOriginal format: ${ext.toUpperCase()}\nConverted to: Markdown`, + buttons: ['OK'] + }); + return; + } + + // Convert to markdown using pandoc (using runPandocCmd for safety) + const pandocCmd = `${getPandocPath()} "${inputFile}" -t markdown ${additionalOptions} -o "${outputFile}"`; + + runPandocCmd(pandocCmd, (error, stdout, stderr) => { + if (error) { + dialog.showErrorBox('Import Error', sanitizeErrorMessage(`Failed to import: ${error.message}\n\nMake sure Pandoc is installed.\n\nSupported formats: DOCX, ODT, RTF, HTML, LaTeX, EPUB, PDF, PPTX, ODP, RST, Textile, MediaWiki, Org-mode, AsciiDoc, CSV, and more.`)); + } else { + // Open the converted markdown file + currentFile = outputFile; + const content = fs.readFileSync(outputFile, 'utf-8'); + mainWindow.webContents.send('file-opened', { path: outputFile, content }); + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Import Complete', + message: `Document imported successfully as ${path.basename(outputFile)}\n\nOriginal format: ${ext.toUpperCase()}\nConverted to: Markdown`, + buttons: ['OK'] + }); + } + }); + } +} + + +function setTheme(theme) { + store.set('theme', theme); + mainWindow.webContents.send('theme-changed', theme); +} + +// IPC handlers +ipcMain.on('save-file', (event, { path, content }) => { + fs.writeFileSync(path, content, 'utf-8'); + currentFile = path; +}); + +ipcMain.on('save-current-file', (event, payload) => { + const content = typeof payload === 'string' ? payload : payload?.content; + const targetFile = typeof payload === 'string' ? currentFile : payload?.filePath || currentFile; + + if (targetFile) { + fs.writeFileSync(targetFile, content, 'utf-8'); + currentFile = targetFile; + } else { + saveAsFile(); + } +}); + +ipcMain.on('get-theme', (event) => { + const theme = store.get('theme', 'atomonelight'); + event.reply('theme-changed', theme); +}); + +// Handle tab file tracking for exports +ipcMain.on('set-current-file', (event, filePath) => { + currentFile = filePath; +}); + +// Handle actual printing when renderer is ready +ipcMain.on('do-print', (event, { withStyles }) => { + if (mainWindow) { + // Renderer has already hidden UI, waited 300ms, and prepared the page + // Print immediately - DOM is fully rendered + mainWindow.webContents.print({ + silent: false, + printBackground: withStyles, + color: true, + margin: { marginType: 'default' } + }); + } +}); + +// Handle printing with custom options from print preview dialog +ipcMain.on('do-print-with-options', (event, options) => { + if (!mainWindow) return; + + const marginsMap = { + 'default': { marginType: 'default' }, + 'narrow': { top: 0.4, bottom: 0.4, left: 0.4, right: 0.4 }, + 'wide': { top: 1.0, bottom: 1.0, left: 1.0, right: 1.0 }, + 'none': { top: 0, bottom: 0, left: 0, right: 0 }, + }; + + const printOptions = { + silent: false, + printBackground: options.background, + landscape: options.orientation === 'landscape', + scaleFactor: options.scale / 100, + pageSize: options.paperSize, + }; + + if (options.margins && options.margins !== 'default') { + printOptions.margins = marginsMap[options.margins]; + } + + mainWindow.webContents.print(printOptions); +}); + +// Handle renderer ready for file association +ipcMain.on('renderer-ready', (event) => { + console.log('[MAIN] renderer-ready received, rendererReady was:', rendererReady); + if (mainWindow) { + mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] renderer-ready received, rendererReady was: ${rendererReady}')`); + } + rendererReady = true; + console.log('[MAIN] app.pendingFile:', app.pendingFile); + if (mainWindow) { + mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] app.pendingFile: ${app.pendingFile}')`); + } + if (app.pendingFile) { + console.log('[MAIN] Opening pending file:', app.pendingFile); + if (mainWindow) { + mainWindow.webContents.executeJavaScript(`console.log('[MAIN->RENDERER] Opening pending file: ${app.pendingFile}')`); + } + openFileFromPath(app.pendingFile); + app.pendingFile = null; + } +}); + +// Handle export with options +ipcMain.on('export-with-options', (event, { format, options }) => { + if (!conversionLimiter()) { + mainWindow.webContents.send('conversion-status', 'Please wait before converting again...'); + return; + } + performExportWithOptions(format, options); +}); + +// Handle batch conversion +ipcMain.on('batch-convert', (event, { inputFolder, outputFolder, format, options }) => { + if (!conversionLimiter()) { + mainWindow.webContents.send('conversion-status', 'Please wait before converting again...'); + return; + } + performBatchConversion(inputFolder, outputFolder, format, options); +}); + +// Handle folder selection for batch conversion +ipcMain.on('select-folder', (event, type) => { + const folder = dialog.showOpenDialogSync(mainWindow, { + properties: ['openDirectory'] + }); + + if (folder && folder[0]) { + event.reply('folder-selected', { type, path: folder[0] }); + } +}); + +ipcMain.on('export-spreadsheet', (event, { content, format }) => { + const outputFile = dialog.showSaveDialogSync(mainWindow, { + defaultPath: currentFile.replace(/\.[^/.]+$/, `.${format}`), + filters: [ + { name: format.toUpperCase(), extensions: [format] } + ] + }); + + if (outputFile) { + try { + // Parse markdown content to extract tables + const tables = extractTablesFromMarkdown(content); + + if (tables.length === 0) { + dialog.showErrorBox('Export Error', 'No tables found in the markdown content'); + return; + } + + if (format === 'csv') { + // Convert tables to CSV format + let csvContent = ''; + tables.forEach((table, index) => { + if (index > 0) csvContent += '\n\n'; // Separate multiple tables + if (tables.length > 1) csvContent += `"Table ${index + 1}"\n`; + + table.forEach(row => { + const csvRow = row.map(cell => { + // Escape quotes and wrap in quotes if necessary + const cleanCell = cell.replace(/"/g, '""'); + return cleanCell.includes(',') || cleanCell.includes('"') || cleanCell.includes('\n') + ? `"${cleanCell}"` : cleanCell; + }).join(','); + csvContent += csvRow + '\n'; + }); + }); + + fs.writeFileSync(outputFile, csvContent, 'utf-8'); + } + + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Export Complete', + message: `${format.toUpperCase()} exported successfully to ${outputFile}`, + buttons: ['OK'] + }); + } catch (error) { + dialog.showErrorBox('Export Error', sanitizeErrorMessage(`Failed to export: ${error.message}`)); + } + } +}); + +// Helper function to extract tables from markdown +function extractTablesFromMarkdown(markdown) { + const tables = []; + const lines = markdown.split('\n'); + let currentTable = []; + let inTable = false; + + for (const line of lines) { + if (line.includes('|')) { + if (!inTable) { + inTable = true; + currentTable = []; + } + + // Skip separator lines (|---|---|) + if (!line.match(/^\s*\|?\s*:?-+:?\s*\|/)) { + const cells = line.split('|') + .map(cell => cell.trim()) + .filter(cell => cell !== ''); + + if (cells.length > 0) { + currentTable.push(cells); + } + } + } else if (inTable && line.trim() === '') { + // End of table + if (currentTable.length > 0) { + tables.push(currentTable); + } + currentTable = []; + inTable = false; + } + } + + // Add last table if exists + if (currentTable.length > 0) { + tables.push(currentTable); + } + + return tables; +} + +function performBatchConversion(inputFolder, outputFolder, format, options) { + if (!fs.existsSync(inputFolder)) { + dialog.showErrorBox('Error', 'Input folder does not exist'); + return; + } + + // Create output folder if it doesn't exist + if (!fs.existsSync(outputFolder)) { + try { + fs.mkdirSync(outputFolder, { recursive: true }); + } catch (error) { + dialog.showErrorBox('Error', sanitizeErrorMessage(`Failed to create output folder: ${error.message}`)); + return; + } + } + + // Find all markdown files in input folder + const markdownFiles = []; + + function findMarkdownFiles(dir) { + const files = fs.readdirSync(dir); + + for (const file of files) { + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + findMarkdownFiles(fullPath); // Recursive search + } else if (file.match(/\.(md|markdown)$/i)) { + markdownFiles.push(fullPath); + } + } + } + + findMarkdownFiles(inputFolder); + + if (markdownFiles.length === 0) { + dialog.showErrorBox('No Files Found', 'No markdown files found in the selected folder'); + return; + } + + // Show progress dialog + let completedCount = 0; + const totalCount = markdownFiles.length; + + // Process each file + const processNextFile = async (index) => { + if (index >= markdownFiles.length) { + // All files processed + dialog.showMessageBox(mainWindow, { + type: 'info', + title: 'Batch Conversion Complete', + message: `Successfully converted ${completedCount} out of ${totalCount} files to ${format.toUpperCase()}.`, + buttons: ['OK'] + }); + return; + } + + const inputFile = markdownFiles[index]; + const relativePath = path.relative(inputFolder, inputFile); + const baseName = path.basename(relativePath, path.extname(relativePath)); + 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}`)); + + // Create subdirectories in output folder if needed + const outputDir = path.dirname(outputFile); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Handle DOCX Enhanced format with WordTemplateExporter + if (format === 'docx-enhanced') { + try { + const content = fs.readFileSync(inputFile, 'utf-8'); + const exporter = new WordTemplateExporter(wordTemplatePath, templateStartPage); + await exporter.convert(content, outputFile); + + 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; + } + + // 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 (using execFile for safety) + const soffice = process.platform === 'win32' + ? 'C:\\Program Files\\LibreOffice\\program\\soffice.exe' + : 'soffice'; + + const outputDir = path.dirname(outputFile); + const sofficeArgs = ['--headless', '--convert-to', 'pdf', '--outdir', outputDir, tempDocxPath]; + + execFile(soffice, sofficeArgs, (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 + let pandocCmd = `${getPandocPath()} "${inputFile}" -o "${outputFile}"`; + + // Add template if specified + if (options.template && options.template !== 'default') { + pandocCmd += ` --template="${options.template}"`; + } + + // Add metadata + if (options.metadata) { + for (const [key, value] of Object.entries(options.metadata)) { + if (value.trim()) { + pandocCmd += ` -M ${key}="${value.replace(/"/g, '\\"')}"`; + } + } + } + + // Add variables + if (options.variables) { + for (const [key, value] of Object.entries(options.variables)) { + if (value.trim()) { + pandocCmd += ` -V ${key}="${value.replace(/"/g, '\\"')}"`; + } + } + } + + // Add other options + if (options.toc) pandocCmd += ' --toc'; + if (options.tocDepth) pandocCmd += ` --toc-depth=${options.tocDepth}`; + if (options.numberSections) pandocCmd += ' --number-sections'; + if (options.citeproc) pandocCmd += ' --citeproc'; + if (options.bibliography) pandocCmd += ` --bibliography="${options.bibliography}"`; + if (options.csl) pandocCmd += ` --csl="${options.csl}"`; + + // Add PDF-specific options with header/footer support + if (format === 'pdf') { + const pdfEngine = options.pdfEngine || 'xelatex'; + pandocCmd += ` --pdf-engine=${pdfEngine}`; + if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; + + // Add monospace font settings for code blocks (ASCII art preservation) + pandocCmd += ' -V monofont="Consolas"'; + pandocCmd += ' --highlight-style=tango'; + + // Add header/footer if enabled + if (headerFooterSettings.enabled) { + const filename = path.basename(inputFile, path.extname(inputFile)); + const metadata = { filename, title: filename, author: '' }; + + const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); + const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); + const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); + const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); + const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); + const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); + + // Create LaTeX header + const latexHeader = ` +\\usepackage{fancyhdr} +\\pagestyle{fancy} +\\fancyhf{} +\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} +\\chead{${headerCenter.replace(/\\/g, '\\\\')}} +\\rhead{${headerRight.replace(/\\/g, '\\\\')}} +\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} +\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} +\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} +\\renewcommand{\\headrulewidth}{0.4pt} +\\renewcommand{\\footrulewidth}{0.4pt} +`; + const headerFile = path.join(require('os').tmpdir(), `header_batch_${Date.now()}.tex`); + fs.writeFileSync(headerFile, latexHeader, 'utf-8'); + pandocCmd += ` --include-in-header="${headerFile}"`; + pandocCmd += ' --variable header-includes="\\\\usepackage{lastpage}"'; + } + } + + // Add DOCX-specific handling + if (format === 'docx') { + pandocCmd += ' -t docx'; + } + + // Add PowerPoint footer if enabled + if (format === 'pptx' && headerFooterSettings.enabled && headerFooterSettings.footer.center) { + const filename = path.basename(inputFile, path.extname(inputFile)); + const metadata = { filename, title: filename, author: '' }; + const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata); + pandocCmd += ` --variable footer="${footerText}"`; + } + + // Execute conversion (using runPandocCmd for safety) + runPandocCmd(pandocCmd, async (error, stdout, stderr) => { + if (!error) { + // Add headers/footers to DOCX if enabled + if (format === 'docx' && headerFooterSettings.enabled) { + try { + const filename = path.basename(inputFile, path.extname(inputFile)); + const metadata = { filename, title: filename, author: '' }; + await addHeaderFooterToDocx(outputFile, metadata); + } catch (hfError) { + console.error('Batch: Error adding headers/footers to DOCX:', hfError); + } + } + completedCount++; + } + + // Update progress (you could send this to renderer for a progress bar) + mainWindow.webContents.send('batch-progress', { + completed: index + 1, + total: totalCount, + currentFile: path.basename(inputFile), + success: !error + }); + + // Process next file + processNextFile(index + 1); + }); + }; + + // Start processing + processNextFile(0); +} + +// Handle command line interface for file conversion +function handleCLIConversion(args) { + const command = args[0]; + const filePath = args[args.length - 1]; // File path is always last argument + + if (!fs.existsSync(filePath)) { + console.error(`Error: File not found: ${filePath}`); + app.quit(); + return; + } + + // Show conversion dialog for --convert command + if (command === '--convert') { + showConversionDialog(filePath); + return; + } + + // Direct conversion for --convert-to command + if (command === '--convert-to' && args.length >= 3) { + const format = args[1]; + performCLIConversion(filePath, format); + return; + } + + console.error('Usage: --convert OR --convert-to '); + app.quit(); +} + +// Show conversion dialog for CLI +function showConversionDialog(filePath) { + const { dialog } = require('electron'); + + // Create a hidden window for dialog operations + const hiddenWindow = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + + const formats = [ + { name: 'PDF', value: 'pdf' }, + { name: 'HTML', value: 'html' }, + { name: 'DOCX', value: 'docx' }, + { name: 'LaTeX', value: 'latex' }, + { name: 'RTF', value: 'rtf' }, + { name: 'ODT', value: 'odt' }, + { name: 'PowerPoint', value: 'pptx' } + ]; + + // Create format selection dialog using message box + const formatButtons = formats.map(f => f.name); + formatButtons.push('Cancel'); + + dialog.showMessageBox(hiddenWindow, { + type: 'question', + title: 'PanConverter - Choose Format', + message: `Convert "${path.basename(filePath)}" to:`, + detail: 'Select the output format for conversion', + buttons: formatButtons, + defaultId: 0, + cancelId: formatButtons.length - 1 + }).then(result => { + if (result.response < formats.length) { + const selectedFormat = formats[result.response].value; + performCLIConversion(filePath, selectedFormat); + } else { + console.log('Conversion cancelled'); + app.quit(); + } + hiddenWindow.destroy(); + }); +} + +// Perform CLI conversion +function performCLIConversion(inputPath, format) { + try { + const content = fs.readFileSync(inputPath, 'utf-8'); + const outputPath = inputPath.replace(/\.[^/.]+$/, `.${format}`); + + console.log(`Converting "${path.basename(inputPath)}" to ${format.toUpperCase()}...`); + + // Use existing export functions but with CLI output (using runPandocCmd for safety) + const pandocCommand = buildPandocCommand(content, format, outputPath); + + runPandocCmd(pandocCommand, (error, stdout, stderr) => { + if (error) { + console.error(`Conversion failed: ${error.message}`); + if (stderr) console.error(`Details: ${stderr}`); + app.quit(); + return; + } + + console.log(`Successfully converted to: ${outputPath}`); + + // Show Windows notification (using exec for PowerShell is acceptable here - hardcoded command) + if (process.platform === 'win32') { + const iconPath = path.join(__dirname, '../assets/icon.png'); + execFile('powershell', ['-Command', `New-BurntToastNotification -Text 'PanConverter', 'File converted to ${format.toUpperCase()}' -AppLogo '${iconPath}'`], () => {}); + } + + app.quit(); + }); + } catch (error) { + console.error(`Error reading file: ${error.message}`); + app.quit(); + } +} + +// Build Pandoc command for CLI conversion +function buildPandocCommand(content, format, outputPath) { + const inputFile = path.join(require('os').tmpdir(), `panconverter_temp_${Date.now()}.md`); + fs.writeFileSync(inputFile, content, 'utf-8'); + + let command = `pandoc "${inputFile}" -o "${outputPath}"`; + + // Get metadata for dynamic fields + const filename = currentFile ? path.basename(currentFile, path.extname(currentFile)) : 'document'; + const metadata = { + filename: filename, + title: filename, + author: '', + }; + + switch (format) { + case 'pdf': + command += ' --pdf-engine=xelatex --variable geometry:margin=1in'; + + // Add page size and orientation + const pageSize = PAGE_SIZES[pageSettings.size]; + if (pageSize) { + command += ` -V geometry:papersize=${pageSize.pandoc}`; + } else if (pageSettings.customWidth && pageSettings.customHeight) { + // Custom page size + command += ` -V geometry:paperwidth=${pageSettings.customWidth}`; + command += ` -V geometry:paperheight=${pageSettings.customHeight}`; + } + + // Add orientation + if (pageSettings.orientation === 'landscape') { + command += ' -V geometry:landscape'; + } + + // Add monospace font settings for code blocks (ASCII art preservation) + command += ' -V monofont="Consolas"'; + command += ' --highlight-style=tango'; + + // Add header/footer if enabled + if (headerFooterSettings.enabled) { + // Process dynamic fields + const headerLeft = processDynamicFields(headerFooterSettings.header.left, metadata); + const headerCenter = processDynamicFields(headerFooterSettings.header.center, metadata); + const headerRight = processDynamicFields(headerFooterSettings.header.right, metadata); + const footerLeft = processDynamicFields(headerFooterSettings.footer.left, metadata); + const footerCenter = processDynamicFields(headerFooterSettings.footer.center, metadata); + const footerRight = processDynamicFields(headerFooterSettings.footer.right, metadata); + + // Add Pandoc variables for fancyhdr package + if (headerLeft) command += ` --variable header-left="${headerLeft}"`; + if (headerCenter) command += ` --variable header-center="${headerCenter}"`; + if (headerRight) command += ` --variable header-right="${headerRight}"`; + if (footerLeft) command += ` --variable footer-left="${footerLeft}"`; + if (footerCenter) command += ` --variable footer-center="${footerCenter}"`; + if (footerRight) command += ` --variable footer-right="${footerRight}"`; + + // Create custom LaTeX header with fancyhdr + const latexHeader = ` +\\usepackage{fancyhdr} +\\pagestyle{fancy} +\\fancyhf{} +\\lhead{${headerLeft.replace(/\\/g, '\\\\')}} +\\chead{${headerCenter.replace(/\\/g, '\\\\')}} +\\rhead{${headerRight.replace(/\\/g, '\\\\')}} +\\lfoot{${footerLeft.replace(/\\/g, '\\\\')}} +\\cfoot{${footerCenter.replace(/[$]PAGE[$]/g, '\\\\thepage').replace(/[$]TOTAL[$]/g, '\\\\pageref{LastPage}').replace(/\\/g, '\\\\')}} +\\rfoot{${footerRight.replace(/\\/g, '\\\\')}} +\\renewcommand{\\headrulewidth}{0.4pt} +\\renewcommand{\\footrulewidth}{0.4pt} +`; + const headerFile = path.join(require('os').tmpdir(), `header_${Date.now()}.tex`); + fs.writeFileSync(headerFile, latexHeader, 'utf-8'); + command += ` --include-in-header="${headerFile}"`; + + // Add lastpage package for $TOTAL$ support + command += ' --variable header-includes="\\\\usepackage{lastpage}"'; + } + break; + + case 'html': + command += ' --self-contained --css'; + break; + + case 'docx': + command += ' --reference-doc'; + + // For DOCX, header/footer are handled via reference document or separate processing + // We'll add a note that DOCX headers/footers require reference doc or post-processing + break; + + case 'odt': + // ODT headers/footers are handled via reference document + break; + + case 'latex': + command += ' --standalone'; + break; + + case 'pptx': + command += ' --slide-level=2'; + // PowerPoint footer can be added with --variable + if (headerFooterSettings.enabled && headerFooterSettings.footer.center) { + const footerText = processDynamicFields(headerFooterSettings.footer.center, metadata); + command += ` --variable footer="${footerText}"`; + } + break; + + case 'json': + command = `pandoc "${inputFile}" -t json -o "${outputPath}"`; + break; + + case 'yaml': + command = `pandoc "${inputFile}" -t markdown -o "${outputPath}"`; + break; + + case 'xml': + command = `pandoc "${inputFile}" -t jats -o "${outputPath}"`; + break; + + case 'toml': + // TOML: save raw markdown content with .toml extension + command = `pandoc "${inputFile}" -t markdown -o "${outputPath}"`; + break; + + case 'revealjs': + command = `pandoc "${inputFile}" -t revealjs -s -o "${outputPath}" --slide-level=2`; + break; + + case 'beamer': + command = `pandoc "${inputFile}" -t beamer -o "${outputPath}"`; + break; + + case 'confluence': + case 'jira': + command = `pandoc "${inputFile}" -t jira -o "${outputPath}"`; + break; + } + + return command; +} + +app.whenReady().then(() => { + // Load saved Word template path and settings + wordTemplatePath = store.get('wordTemplatePath', null); + templateStartPage = store.get('templateStartPage', 3); + + // Load header/footer settings + const savedHFSettings = store.get('headerFooterSettings', null); + if (savedHFSettings) { + headerFooterSettings = savedHFSettings; + } + + // Load page size settings + const savedPageSettings = store.get('pageSettings', null); + if (savedPageSettings) { + pageSettings = savedPageSettings; + } + + // 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 + } + + mainWindow = createMainWindow(); + mainWindow.on('closed', () => { mainWindow = null; }); + + // Register file ops IPC handlers + fileOps.register({ + validatePath, + resolveWritablePath, + isPathAccessible, + currentFileRef: { get current() { return currentFile; } }, + mainWindow, + }); + + // Handle file association on app startup + // In packaged apps, process.argv structure is different: + // Development: ['electron', 'app.js', 'file.md'] - need slice(2) + // Packaged: ['PanConverter.exe', 'file.md'] - need slice(1) + // We'll check all arguments after the executable + console.log('[MAIN] Full command line args:', JSON.stringify(process.argv)); + console.log('[MAIN] App is packaged:', app.isPackaged); + + // Start from index 1 (skip executable) and check each argument + const startIndex = app.isPackaged ? 1 : 2; + const fileArgs = process.argv.slice(startIndex); + console.log('[MAIN] File args to process (starting from index', startIndex + '):', fileArgs); + + for (const arg of fileArgs) { + console.log('[MAIN] Checking arg:', arg); + if ((arg.endsWith('.md') || arg.endsWith('.markdown'))) { + // Try to resolve the path (might be relative) + const resolvedPath = path.isAbsolute(arg) ? arg : path.resolve(process.cwd(), arg); + console.log('[MAIN] Resolved path:', resolvedPath); + if (fs.existsSync(resolvedPath)) { + // Store the file to open after window is ready + console.log('[MAIN] Setting pendingFile to:', resolvedPath); + app.pendingFile = resolvedPath; + break; + } else { + console.log('[MAIN] File does not exist:', resolvedPath); + } + } + } + console.log('[MAIN] Final app.pendingFile:', app.pendingFile); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + mainWindow = createMainWindow(); + mainWindow.on('closed', () => { mainWindow = null; }); + } +}); + +// IPC handlers for recent files +ipcMain.on('save-recent-files', (event, recentFiles) => { + try { + const userDataPath = app.getPath('userData'); + const recentFilesPath = path.join(userDataPath, 'recent-files.json'); + fs.writeFileSync(recentFilesPath, JSON.stringify(recentFiles, null, 2)); + } catch (error) { + console.error('Error saving recent files:', error); + } +}); + +ipcMain.on('clear-recent-files', (event) => { + try { + const userDataPath = app.getPath('userData'); + const recentFilesPath = path.join(userDataPath, 'recent-files.json'); + fs.writeFileSync(recentFilesPath, JSON.stringify([], null, 2)); + // Rebuild menu to reflect changes + menu.register(mainWindow); + event.reply('recent-files-cleared'); + } catch (error) { + console.error('Error clearing recent files:', error); + } +}); + +// Handle file opening on macOS +app.on('open-file', (event, filePath) => { + event.preventDefault(); + if (mainWindow && rendererReady) { + openFileFromPath(filePath); + } else { + // Store the file path to open after window and renderer are ready + app.pendingFile = filePath; + } +}); + +// Handle file opening from command line or file association +function openFileFromPath(filePath) { + console.log('[MAIN] openFileFromPath called with:', filePath); + console.log('[MAIN] rendererReady:', rendererReady, 'mainWindow exists:', !!mainWindow, 'webContents exists:', !!(mainWindow?.webContents)); + if (fs.existsSync(filePath)) { + const stats = fs.statSync(filePath); + if (stats.size > MAX_FILE_SIZE) { + dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`); + return; + } + currentFile = filePath; + const content = fs.readFileSync(filePath, 'utf-8'); + console.log('[MAIN] File read successfully, content length:', content.length); + console.log('[MAIN] About to send file-opened. mainWindow:', !!mainWindow, 'webContents:', !!(mainWindow?.webContents), 'rendererReady:', rendererReady); + if (mainWindow && mainWindow.webContents && rendererReady) { + // Send file immediately - renderer-ready means UI is initialized + console.log('[MAIN] Sending file-opened to renderer'); + mainWindow.webContents.send('file-opened', { path: filePath, content }); + } else { + // Store file to open after renderer is ready + console.log('[MAIN] Storing as pending file'); + app.pendingFile = filePath; + } + } else { + console.error('[MAIN] File does not exist:', filePath); + } +} + +// ======================================== +// PDF OPERATIONS — delegates to main/PDFOperations.js +// ======================================== + +ipcMain.on('process-pdf-operation', async (event, data) => { + try { + mainWindow.webContents.send('pdf-operation-progress', { + message: `Processing ${data.operation}...`, + progress: 10 + }); + + const result = await PDFOperations.executeOperation(data.operation, data); + mainWindow.webContents.send('pdf-operation-complete', result); + } catch (error) { + mainWindow.webContents.send('pdf-operation-complete', { + success: false, + error: error.message + }); + } +}); + +ipcMain.on('get-pdf-page-count', async (event, filePath) => { + try { + const count = await PDFOperations.getPageCount(filePath); + event.reply('pdf-page-count', { count }); + } catch (error) { + event.reply('pdf-page-count', { error: error.message }); + } +}); + +// IPC Handler for folder selection (for PDF operations) +ipcMain.on('select-pdf-folder', (event, inputId) => { + const folder = dialog.showOpenDialogSync(mainWindow, { + properties: ['openDirectory'] + }); + + if (folder && folder[0]) { + event.reply('pdf-folder-selected', { inputId, path: folder[0] }); + } +}); + +// IPC Handler for loading document templates +ipcMain.handle('load-template', async (event, filename) => { + try { + const templatePath = path.join(__dirname, '../templates', filename); + return fs.readFileSync(templatePath, 'utf-8'); + } catch (err) { + console.error('Failed to load template:', err); + return null; + } +}); + +// IPC Handler for saving pasted/dropped images +ipcMain.handle('save-pasted-image', async (event, { base64, ext }) => { + try { + let saveDir; + + if (currentFile) { + // Save relative to current file + saveDir = path.join(path.dirname(currentFile), 'assets'); + } else { + // Use temp directory + saveDir = path.join(app.getPath('temp'), 'markdown-converter-images'); + } + + if (!fs.existsSync(saveDir)) { + fs.mkdirSync(saveDir, { recursive: true }); + } + + const filename = `image-${Date.now()}.${ext}`; + const filePath = path.join(saveDir, filename); + const buffer = Buffer.from(base64, 'base64'); + fs.writeFileSync(filePath, buffer); + + return { relativePath: `assets/${filename}`, absolutePath: filePath }; + } catch (error) { + console.error('Failed to save pasted image:', error); + return null; + } +}); + +// IPC Handler to receive generated content from generator windows +ipcMain.on('insert-generated-content', (event, content) => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('insert-content', content); + } +}); + +// ============================================ +// File Explorer IPC Handlers +// ============================================ +ipcMain.handle('list-directory', async (event, dirPath) => { + try { + if (!dirPath) { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ['openDirectory'] + }); + if (result.canceled || !result.filePaths[0]) return null; + dirPath = result.filePaths[0]; + } + + // Validate path to prevent traversal attacks + const validation = validatePath(dirPath); + if (!validation.valid) { + console.error('[SECURITY] Invalid directory path:', validation.error); + return null; + } + + if (!isPathAccessible(validation.resolved)) { + return null; + } + + const entries = fs.readdirSync(validation.resolved, { withFileTypes: true }) + .filter(e => !e.name.startsWith('.')) + .sort((a, b) => { + if (a.isDirectory() && !b.isDirectory()) return -1; + if (!a.isDirectory() && b.isDirectory()) return 1; + return a.name.localeCompare(b.name); + }) + .map(e => ({ + name: e.name, + isDirectory: e.isDirectory(), + size: e.isDirectory() ? 0 : fs.statSync(path.join(validation.resolved, e.name)).size, + modified: fs.statSync(path.join(validation.resolved, e.name)).mtimeMs, + path: path.join(validation.resolved, e.name) + })); + return { path: validation.resolved, entries }; + } catch (err) { + console.error('list-directory error:', err); + return null; + } +}); + +ipcMain.handle('select-custom-css', async (event) => { + const result = dialog.showOpenDialogSync(mainWindow, { + title: 'Select Custom Preview CSS', + properties: ['openFile'], + filters: [ + { name: 'CSS Stylesheets', extensions: ['css'] } + ] + }); + + if (result && result[0]) { + const filePath = result[0]; + const content = fs.readFileSync(filePath, 'utf-8'); + return { path: filePath, content }; + } + return null; +}); + +// ============================================ +// Snippets IPC Handlers +// ============================================ +const snippetsPath = path.join(app.getPath('userData'), 'snippets.json'); + +function loadSnippets() { + try { + if (fs.existsSync(snippetsPath)) { + return JSON.parse(fs.readFileSync(snippetsPath, 'utf-8')); + } + } catch (err) { console.error('Failed to load snippets:', err); } + return []; +} + +function saveSnippetsFile(snippets) { + fs.writeFileSync(snippetsPath, JSON.stringify(snippets, null, 2)); +} + +ipcMain.handle('get-snippets', async () => loadSnippets()); + +ipcMain.handle('save-snippet', async (event, snippet) => { + const snippets = loadSnippets(); + const existing = snippets.findIndex(s => s.id === snippet.id); + if (existing >= 0) snippets[existing] = snippet; + else snippets.push(snippet); + saveSnippetsFile(snippets); + return snippets; +}); + +ipcMain.handle('delete-snippet', async (event, id) => { + const snippets = loadSnippets().filter(s => s.id !== id); + saveSnippetsFile(snippets); + return snippets; +}); + +// ============================================ +// Code Execution (REPL) IPC Handler +// ============================================ +ipcMain.handle('execute-code', async (event, { code, language }) => { + const timeout = 10000; + + return new Promise((resolve) => { + let cmd, args; + if (language === 'javascript' || language === 'js') { + cmd = 'node'; + args = ['-e', code]; + } else if (language === 'python' || language === 'py') { + cmd = process.platform === 'win32' ? 'python' : 'python3'; + args = ['-c', code]; + } else if (language === 'bash' || language === 'sh') { + cmd = process.platform === 'win32' ? 'cmd' : 'bash'; + args = process.platform === 'win32' ? ['/c', code] : ['-c', code]; + } else { + resolve({ error: `Unsupported language: ${language}` }); + return; + } + + execFile(cmd, args, { timeout, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => { + resolve({ + stdout: stdout || '', + stderr: stderr || '', + error: err?.killed ? 'Execution timed out (10s limit)' : (err?.message || null) + }); + }); + }); +}); + +// Exports for src/main/menu/ items.js (lazy require inside click handlers) +module.exports = { + openFile, + openPdfFile, + openFileFromPath, + saveAsFile, + exportFile, + exportPDFViaWordTemplate, + exportWordWithTemplate, + exportSpreadsheet, + importDocument, + selectWordTemplate, + showTemplateSettings, + setTheme, + showBatchConversionDialog, + showUniversalConverterDialog, + showPDFEditorDialog, + showAboutDialog, + showDependenciesDialog, +}; diff --git a/src/main/store.js b/src/main/store.js new file mode 100644 index 0000000..32664e6 --- /dev/null +++ b/src/main/store.js @@ -0,0 +1,31 @@ +// src/main/store.js +// Simple JSON-file preferences store (replaces electron-store) + +const { app } = require('electron'); +const path = require('path'); +const fs = require('fs'); + +const settingsPath = path.join(app.getPath('userData'), 'settings.json'); + +const store = { + get(key, defaultValue) { + try { + const data = fs.readFileSync(settingsPath, 'utf-8'); + const settings = JSON.parse(data); + return settings[key] !== undefined ? settings[key] : defaultValue; + } catch { + return defaultValue; + } + }, + set(key, value) { + let settings = {}; + try { + const data = fs.readFileSync(settingsPath, 'utf-8'); + settings = JSON.parse(data); + } catch {} + settings[key] = value; + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); + } +}; + +module.exports = store;