diff --git a/README.md b/README.md index a0306d8..8722a3b 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ Download the latest release for your platform from the [Releases](https://github #### Linux - **AppImage**: `PanConverter-1.5.0.AppImage` (universal, may require `--no-sandbox` flag) -- **Debian Package**: `pan-converter_1.5.0_amd64.deb` -- **Snap Package**: `pan-converter_1.5.0_amd64.snap` +- **Debian Package**: `pan-converter_1.5.6_amd64.deb` +- **Snap Package**: `pan-converter_1.5.6_amd64.snap` ### Install from Source ```bash @@ -138,6 +138,8 @@ npm run dist:all ## Version History +- **v1.5.6** - (Your release notes here) +- **v1.5.5** - Refactored PDF export, simplified Pandoc pathing, and removed XLSX dependency. - **v1.3.1** - Bug fixes: Fixed file associations for double-clicking .md files, corrected 50/50 layout alignment for editor/preview panes - **v1.3.0** - Major update: Tabbed interface for multiple files, enhanced PDF export with LaTeX engines, fixed file associations, removed redundant converter menu, improved UI architecture - **v1.2.1** - Comprehensive editor enhancements: Find & Replace, Line Numbers, Undo/Redo, Auto-indentation, PowerPoint export, document conversion menu, table creation helper, spreadsheet export diff --git a/package.json b/package.json index 4d63684..d6d244e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pan-converter", - "version": "1.5.4", + "version": "1.5.7", "description": "Cross-platform Markdown editor and converter using Pandoc", "main": "src/main.js", "scripts": { diff --git a/src/main.js b/src/main.js index 3a578a6..ded42fc 100644 --- a/src/main.js +++ b/src/main.js @@ -2,29 +2,11 @@ const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron') const path = require('path'); const fs = require('fs'); const { exec } = require('child_process'); -const XLSX = require('xlsx'); // Get the system Pandoc path function getPandocPath() { - // Check common Pandoc installation locations on Windows - const commonPaths = [ - 'pandoc', // System PATH - path.join(process.env.USERPROFILE || '', 'AppData', 'Local', 'Pandoc', 'pandoc.exe'), - path.join('C:', 'Program Files', 'Pandoc', 'pandoc.exe'), - path.join('C:', 'Program Files (x86)', 'Pandoc', 'pandoc.exe'), - ]; - - // Try each path and return the first one that exists - for (const pandocPath of commonPaths) { - if (pandocPath === 'pandoc') { - // For system PATH, we'll check in checkPandocAvailable() - return pandocPath; - } else if (fs.existsSync(pandocPath)) { - return `"${pandocPath}"`; - } - } - - // Fallback to system pandoc + // Pandoc is expected to be in the system's PATH. + // The command will be executed directly. Quoting is handled by exec. return 'pandoc'; } @@ -200,7 +182,6 @@ function createMenu() { { label: 'OpenDocument Presentation (ODP)', click: () => exportFile('odp') }, { type: 'separator' }, { label: 'CSV (Tables)', click: () => exportSpreadsheet('csv') }, - { label: 'XLSX (Tables)', click: () => exportSpreadsheet('xlsx') } ] }, { type: 'separator' }, @@ -296,7 +277,7 @@ function createMenu() { type: 'info', title: 'About PanConverter', message: 'PanConverter', - detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.5.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Windows Explorer context menu integration\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Enhanced PDF export with built-in Electron fallback\n• File association support for .md files\n• Command-line interface for batch conversion\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Improved preview typography and spacing\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• Complete theme support including Monokai fixes\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• Multiple themes support\n• Undo/redo functionality\n• Live word count and statistics', + detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.5.7\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Windows Explorer context menu integration\n• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Real-time preview updates while typing\n• Full toolbar markdown editing functions\n• Enhanced PDF export with built-in Electron fallback\n• File association support for .md files\n• Command-line interface for batch conversion\n• Advanced export options with templates and metadata\n• Batch file conversion with progress tracking\n• Improved preview typography and spacing\n• Adjustable font sizes via menu (Ctrl+Shift+Plus/Minus)\n• Complete theme support including Monokai fixes\n• Find & replace with match highlighting\n• Line numbers and auto-indentation\n• Export to multiple formats via Pandoc\n• PowerPoint & presentation export\n• Export tables to Excel/ODS spreadsheets\n• Document import & conversion\n• Table creation helper\n• Multiple themes support\n• Undo/redo functionality\n• Live word count and statistics', buttons: ['OK'] }); } @@ -435,12 +416,12 @@ function performExportWithOptions(format, options) { // Add specific options for PDF export to ensure proper generation if (format === 'pdf') { - const pdfEngine = options.pdfEngine || 'xelatex'; - const geometry = options.geometry || 'margin=1in'; - pandocCmd += ` --pdf-engine=${pdfEngine} -V geometry:${geometry}`; + const pdfEngine = options.pdfEngine || 'xelatex'; // Default to xelatex + pandocCmd += ` --pdf-engine="${pdfEngine}"`; + if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; // Try with specified PDF engine - exec(pandocCmd, (error, stdout, stderr) => { + exec(pandocCmd, (error) => { if (error) { // Try fallback engines if the specified one fails const fallbackEngines = ['pdflatex', 'lualatex']; @@ -450,13 +431,7 @@ function performExportWithOptions(format, options) { } }); } else if (format === 'docx') { - pandocCmd = `${getPandocPath()} "${currentFile}" -t docx -o "${outputFile}"`; - exportWithPandoc(pandocCmd, outputFile, format); - } else if (format === 'html') { - pandocCmd = `${getPandocPath()} "${currentFile}" -t html5 --standalone -o "${outputFile}"`; - exportWithPandoc(pandocCmd, outputFile, format); - } else if (format === 'latex') { - pandocCmd = `${getPandocPath()} "${currentFile}" -t latex -o "${outputFile}"`; + pandocCmd += ' -t docx'; exportWithPandoc(pandocCmd, outputFile, format); } else { // Generic export for other formats @@ -481,9 +456,10 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro } const engine = engines[index]; - const geometry = options.geometry || 'margin=1in'; let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -V geometry:${geometry} -o "${outputFile}"`; + if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; + // Add all other options if (options.template && options.template !== 'default') { pandocCmd += ` --template="${options.template}"`; @@ -497,7 +473,7 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, lastErro } } - exec(pandocCmd, (error, stdout, stderr) => { + exec(pandocCmd, (error) => { if (error) { tryPdfFallback(inputFile, outputFile, engines, index + 1, options, error); } else { @@ -916,20 +892,7 @@ ipcMain.on('export-spreadsheet', (event, { content, format }) => { }); }); - // Write CSV file fs.writeFileSync(outputFile, csvContent, 'utf-8'); - } else if (format === 'xlsx') { - // Convert tables to XLSX format - const workbook = XLSX.utils.book_new(); - - tables.forEach((table, index) => { - const sheetName = tables.length > 1 ? `Table${index + 1}` : 'Table'; - const worksheet = XLSX.utils.aoa_to_sheet(table); - XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); - }); - - // Write XLSX file - XLSX.writeFile(workbook, outputFile); } dialog.showMessageBox(mainWindow, { @@ -1092,8 +1055,8 @@ function performBatchConversion(inputFolder, outputFolder, format, options) { // Add PDF-specific options if (format === 'pdf') { const pdfEngine = options.pdfEngine || 'xelatex'; - const geometry = options.geometry || 'margin=1in'; - pandocCmd += ` --pdf-engine=${pdfEngine} -V geometry:${geometry}`; + pandocCmd += ` --pdf-engine="${pdfEngine}"`; + if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; } // Execute conversion diff --git a/src/renderer.js b/src/renderer.js index 4ee83c7..14f358f 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -125,7 +125,7 @@ class TabManager { tabContent.className = 'tab-content'; tabContent.id = `tab-content-${tab.id}`; tabContent.dataset.tabId = tab.id; - + tabContent.innerHTML = `
@@ -137,8 +137,16 @@ class TabManager {
`; - + document.querySelector('.editor-container').appendChild(tabContent); + + // Directly attach input listener to the new editor + const editor = document.getElementById(`editor-${tab.id}`); + if (editor) { + editor.addEventListener('input', () => { + this.handleEditorInput(tab.id); + }); + } } switchToTab(tabId) { @@ -262,9 +270,9 @@ class TabManager { restoreTabState(tabId) { const tab = this.tabs.get(tabId); if (!tab) return; - + const editor = document.getElementById(`editor-${tabId}`); - + if (editor) { editor.value = tab.content; this.updatePreview(tabId); @@ -282,9 +290,9 @@ class TabManager { updatePreview(tabId = this.activeTabId) { const tab = this.tabs.get(tabId); const preview = document.getElementById(`preview-${tabId}`); - + if (!tab || !preview) return; - + try { const html = marked.parse(tab.content); const sanitizedHtml = DOMPurify.sanitize(html); @@ -376,39 +384,39 @@ class TabManager { } setupEditorEvents() { - // Set up editor events using event delegation - document.addEventListener('input', (e) => { - if (e.target.classList.contains('editor-textarea')) { - const tabId = parseInt(e.target.id.split('-')[1]); - if (tabId === this.activeTabId) { + // Set up editor events using event delegation on the container + const editorContainer = document.querySelector('.editor-container'); + if (editorContainer) { + editorContainer.addEventListener('input', (e) => { + if (e.target.classList.contains('editor-textarea')) { + const tabId = parseInt(e.target.id.split('-')[1]); this.handleEditorInput(tabId); } - } - }); - - document.addEventListener('scroll', (e) => { - if (e.target.classList.contains('editor-textarea')) { - const tabId = parseInt(e.target.id.split('-')[1]); - if (tabId === this.activeTabId) { + }); + + editorContainer.addEventListener('scroll', (e) => { + if (e.target.classList.contains('editor-textarea')) { this.updateLineNumbers(); } - } - }); + }, true); + } } handleEditorInput(tabId) { const tab = this.tabs.get(tabId); if (!tab) return; - + const editor = document.getElementById(`editor-${tabId}`); + if (!editor) return; + tab.content = editor.value; tab.isDirty = true; - + this.updatePreview(tabId); this.updateWordCount(); this.updateLineNumbers(); this.updateTabBar(); - + // Add to undo stack this.pushUndoState(tabId); } @@ -517,7 +525,7 @@ class TabManager { // Save to localStorage and sync with main process localStorage.setItem('recentFiles', JSON.stringify(this.recentFiles)); - window.electronAPI.send('save-recent-files', this.recentFiles); + ipcRenderer.send('save-recent-files', this.recentFiles); } getRecentFiles() { @@ -532,18 +540,121 @@ class TabManager { } setupToolbarEvents() { - // Existing toolbar setup... + // Bold + document.getElementById('btn-bold').addEventListener('click', () => { + this.wrapSelection('**', '**'); + }); + + // Italic + document.getElementById('btn-italic').addEventListener('click', () => { + this.wrapSelection('*', '*'); + }); + + // Heading + document.getElementById('btn-heading').addEventListener('click', () => { + this.insertAtLineStart('## '); + }); + + // Link + document.getElementById('btn-link').addEventListener('click', () => { + this.wrapSelection('[', '](url)'); + }); + + // Code + document.getElementById('btn-code').addEventListener('click', () => { + this.wrapSelection('`', '`'); + }); + + // List + document.getElementById('btn-list').addEventListener('click', () => { + this.insertAtLineStart('- '); + }); + + // Quote + document.getElementById('btn-quote').addEventListener('click', () => { + this.insertAtLineStart('> '); + }); + + // Table + document.getElementById('btn-table').addEventListener('click', () => { + this.insertTable(); + }); + + // Preview toggle document.getElementById('btn-preview-toggle').addEventListener('click', () => { this.isPreviewVisible = !this.isPreviewVisible; this.updatePreviewVisibility(); }); - + + // Line numbers document.getElementById('btn-line-numbers').addEventListener('click', () => { this.showLineNumbers = !this.showLineNumbers; this.updateLineNumbers(); }); - - // Add other toolbar events... + } + + // Helper function to wrap selected text + wrapSelection(before, after) { + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (!editor) return; + + const start = editor.selectionStart; + const end = editor.selectionEnd; + const selectedText = editor.value.substring(start, end); + const replacement = before + (selectedText || 'text') + after; + + editor.value = editor.value.substring(0, start) + replacement + editor.value.substring(end); + + // Update cursor position + const newCursorPos = selectedText ? start + replacement.length : start + before.length; + editor.selectionStart = editor.selectionEnd = newCursorPos; + editor.focus(); + + // Trigger update + this.handleEditorInput(this.activeTabId); + } + + // Helper function to insert text at the start of current line + insertAtLineStart(prefix) { + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (!editor) return; + + const start = editor.selectionStart; + const text = editor.value; + + // Find the start of the current line + let lineStart = text.lastIndexOf('\n', start - 1) + 1; + + // Insert the prefix + editor.value = text.substring(0, lineStart) + prefix + text.substring(lineStart); + + // Update cursor position + editor.selectionStart = editor.selectionEnd = start + prefix.length; + editor.focus(); + + // Trigger update + this.handleEditorInput(this.activeTabId); + } + + // Insert a markdown table + insertTable() { + const editor = document.getElementById(`editor-${this.activeTabId}`); + if (!editor) return; + + const table = '\n| Column 1 | Column 2 | Column 3 |\n' + + '|----------|----------|----------|\n' + + '| Cell 1 | Cell 2 | Cell 3 |\n' + + '| Cell 4 | Cell 5 | Cell 6 |\n'; + + const start = editor.selectionStart; + editor.value = editor.value.substring(0, start) + table + editor.value.substring(start); + + // Update cursor position + editor.selectionStart = editor.selectionEnd = start + table.length; + editor.focus(); + + // Trigger update + this.handleEditorInput(this.activeTabId); } setupFindEvents() { @@ -561,11 +672,14 @@ class TabManager { // File operations openFile(filePath, content) { let tab = this.tabs.get(this.activeTabId); - + + // Handle both forward and back slashes for cross-platform compatibility + const fileName = filePath.split(/[\\/]/).pop(); + // If current tab is empty and untitled, reuse it if (!tab.filePath && !tab.isDirty && tab.content === '') { tab.filePath = filePath; - tab.title = filePath.split('/').pop(); + tab.title = fileName; tab.content = content; tab.originalContent = content; tab.isDirty = false; @@ -574,12 +688,12 @@ class TabManager { this.createNewTab(); tab = this.tabs.get(this.activeTabId); tab.filePath = filePath; - tab.title = filePath.split('/').pop(); + tab.title = fileName; tab.content = content; tab.originalContent = content; tab.isDirty = false; } - + this.restoreTabState(this.activeTabId); this.startAutoSave(); this.addToRecentFiles(filePath); @@ -603,12 +717,20 @@ let tabManager; document.addEventListener('DOMContentLoaded', () => { tabManager = new TabManager(); + // Attach input listener to the initial editor (tab 1) + const initialEditor = document.getElementById('editor-1'); + if (initialEditor) { + initialEditor.addEventListener('input', () => { + tabManager.handleEditorInput(1); + }); + } + // Request current theme ipcRenderer.send('get-theme'); // Signal that renderer is ready for file operations ipcRenderer.send('renderer-ready'); - + // Set up auto-save interval setInterval(() => { // Auto-save logic for all tabs @@ -626,7 +748,9 @@ ipcRenderer.on('file-new', () => { }); ipcRenderer.on('file-opened', (event, data) => { - tabManager.openFile(data.path, data.content); + if (tabManager) { + tabManager.openFile(data.path, data.content); + } }); ipcRenderer.on('file-save', () => { @@ -1110,13 +1234,13 @@ if (originalExportConfirm) { } // IPC event listeners for recent files functionality -if (window.electronAPI) { - window.electronAPI.on('recent-files-cleared', () => { +ipcRenderer.on('recent-files-cleared', () => { + if (tabManager) { tabManager.recentFiles = []; localStorage.setItem('recentFiles', JSON.stringify([])); console.log('Recent files cleared'); - }); -} + } +}); // Add math rendering support using KaTeX for enhanced preview function initMathSupport() { diff --git a/test.md b/test.md new file mode 100644 index 0000000..db1a7b9 --- /dev/null +++ b/test.md @@ -0,0 +1,23 @@ +# Test Document + +This is a **test** markdown document. + +## Features + +- Item 1 +- Item 2 +- Item 3 + +### Code Example + +```javascript +console.log("Hello World"); +``` + +> Tqweqweqweqweqweqweqeweqweqweqwe +dfe + +qwe +qwe +qwe +qwe \ No newline at end of file