Merge windows branch with enhanced export functionality and CLI support

- Combined advanced export options with built-in fallbacks
- Integrated Windows context menu installation scripts
- Added CLI conversion functionality
- Enhanced error handling for pandoc availability
- Updated to version 1.5.0 with comprehensive feature set
- Resolved merge conflicts preserving all functionality
This commit is contained in:
2025-09-21 23:01:52 +05:30
14 changed files with 1243 additions and 21 deletions
+446 -10
View File
@@ -43,6 +43,22 @@ const store = {
let mainWindow;
let currentFile = null; // This will now represent the active tab's file
let pandocAvailable = null; // Cache pandoc availability check
// Check if pandoc is available
function checkPandocAvailability() {
return new Promise((resolve) => {
if (pandocAvailable !== null) {
resolve(pandocAvailable);
return;
}
exec('pandoc --version', (error, stdout, stderr) => {
pandocAvailable = !error;
resolve(pandocAvailable);
});
});
}
function createWindow() {
mainWindow = new BrowserWindow({
@@ -259,7 +275,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• Tabbed interface for multiple files\n• Advanced markdown editing with live preview\n• Enhanced PDF export with LaTeX engines\n• Optional advanced export options\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 CSV format\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.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',
buttons: ['OK']
});
}
@@ -333,7 +349,36 @@ function performExportWithOptions(format, options) {
]
});
if (outputFile) {
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
@@ -383,16 +428,23 @@ function performExportWithOptions(format, options) {
showExportSuccess(outputFile);
}
});
} 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}"`;
exportWithPandoc(pandocCmd, outputFile, format);
} else {
exec(pandocCmd, (error, stdout, stderr) => {
if (error) {
dialog.showErrorBox('Export Error', `Failed to export: ${error.message}\n\nMake sure Pandoc is installed.`);
} else {
showExportSuccess(outputFile);
}
});
// Generic export for other formats
exportWithPandoc(pandocCmd, outputFile, format);
}
}
}).catch((error) => {
console.error('Error checking pandoc availability:', error);
dialog.showErrorBox('Export Error', `Error checking system requirements: ${error.message}`);
});
}
function tryPdfFallback(inputFile, outputFile, engines, index, options, lastError) {
@@ -442,6 +494,246 @@ function showExportSuccess(outputFile) {
});
}
// Helper function to export with pandoc (general)
function exportWithPandoc(pandocCmd, outputFile, format) {
exec(pandocCmd, (error, stdout, stderr) => {
if (error) {
console.error(`Pandoc error for ${format}:`, error);
dialog.showErrorBox('Export Error',
`Failed to export to ${format.toUpperCase()}:\n${error.message}\n\n` +
`Command used: ${pandocCmd}\n\n` +
`Please ensure Pandoc is properly installed and accessible.`
);
} else {
console.log(`Successfully exported to ${format}:`, outputFile);
showExportSuccess(outputFile);
}
});
}
// Helper function to export PDF with pandoc (with fallbacks)
function exportWithPandocPDF(pandocCmd, outputFile) {
exec(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');
exec(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 = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Exported Document</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 40px;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 1.5em;
margin-bottom: 0.5em;
}
code {
background: #f4f4f4;
padding: 2px 4px;
border-radius: 3px;
font-family: Consolas, Monaco, 'Courier New', monospace;
}
pre {
background: #f4f4f4;
padding: 1em;
border-radius: 5px;
overflow-x: auto;
}
pre code {
background: transparent;
padding: 0;
}
blockquote {
border-left: 4px solid #ddd;
margin-left: 0;
padding-left: 1em;
color: #666;
}
table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f4f4f4;
}
a {
color: #0066cc;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
img {
max-width: 100%;
height: auto;
}
</style>
</head>
<body>
${htmlContent}
</body>
</html>`;
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', `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 = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PDF Export</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 40px;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 1.5em;
margin-bottom: 0.5em;
}
code {
background: #f4f4f4;
padding: 2px 4px;
border-radius: 3px;
font-family: Consolas, Monaco, 'Courier New', monospace;
}
pre {
background: #f4f4f4;
padding: 1em;
border-radius: 5px;
overflow-x: auto;
}
pre code {
background: transparent;
padding: 0;
}
blockquote {
border-left: 4px solid #ddd;
margin-left: 0;
padding-left: 1em;
color: #666;
}
table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f4f4f4;
}
img {
max-width: 100%;
height: auto;
}
@media print {
body { padding: 20px; }
}
</style>
</head>
<body>
${htmlContent}
</body>
</html>`;
// 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',
`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', `Failed to setup PDF export: ${error.message}`);
}
}
function exportSpreadsheet(format) {
if (!currentFile) {
dialog.showErrorBox('Error', 'Please save the file first');
@@ -773,7 +1065,151 @@ function performBatchConversion(inputFolder, outputFolder, format, options) {
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 <file> OR --convert-to <format> <file>');
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
const pandocCommand = buildPandocCommand(content, format, outputPath);
exec(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
if (process.platform === 'win32') {
exec(`powershell -Command "New-BurntToastNotification -Text 'PanConverter', 'File converted to ${format.toUpperCase()}' -AppLogo '${path.join(__dirname, '../assets/icon.png')}'"`, () => {});
}
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}"`;
switch (format) {
case 'pdf':
command += ' --pdf-engine=xelatex --variable geometry:margin=1in';
break;
case 'html':
command += ' --self-contained --css';
break;
case 'docx':
command += ' --reference-doc';
break;
case 'latex':
command += ' --standalone';
break;
case 'pptx':
command += ' --slide-level=2';
break;
}
return command;
}
app.whenReady().then(() => {
// Check for command line conversion requests
const args = process.argv.slice(2);
if (args.length >= 2 && (args[0] === '--convert' || args[0] === '--convert-to')) {
handleCLIConversion(args);
return; // Don't create window for CLI operations
}
createWindow();
// Handle file association on app startup
+12 -10
View File
@@ -9,7 +9,10 @@ marked.setOptions({
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(code, { language: lang }).value;
} catch (err) {}
} catch (err) {
// Fallback to auto highlighting if language-specific highlighting fails
console.warn('Syntax highlighting failed for language:', lang, err.message);
}
}
return hljs.highlightAuto(code).value;
},
@@ -74,6 +77,7 @@ class TabManager {
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case 'n':
case 't':
e.preventDefault();
this.createNewTab();
break;
@@ -83,10 +87,6 @@ class TabManager {
this.closeTab(this.activeTabId);
}
break;
case 't':
e.preventDefault();
this.createNewTab();
break;
case 'Tab':
if (this.tabs.size > 1) {
e.preventDefault();
@@ -156,7 +156,7 @@ class TabManager {
// Notify main process about current file for exports
const tab = this.tabs.get(tabId);
if (tab && tab.filePath) {
if (tab?.filePath) {
ipcRenderer.send('set-current-file', tab.filePath);
}
}
@@ -173,14 +173,16 @@ class TabManager {
const tab = this.tabs.get(tabId);
if (tab.isDirty) {
// TODO: Show confirmation dialog
// Show confirmation dialog for unsaved changes
const result = confirm('You have unsaved changes. Do you want to close this tab without saving?');
if (!result) return;
}
// Remove tab elements
const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`);
const tabContent = document.getElementById(`tab-content-${tabId}`);
if (tabElement && tabElement.classList.contains('tab')) {
if (tabElement?.classList.contains('tab')) {
tabElement.remove();
}
if (tabContent) {
@@ -262,7 +264,6 @@ class TabManager {
if (!tab) return;
const editor = document.getElementById(`editor-${tabId}`);
const preview = document.getElementById(`preview-${tabId}`);
if (editor) {
editor.value = tab.content;
@@ -305,7 +306,8 @@ class TabManager {
}
}
} catch (error) {
preview.innerHTML = '<p>Error rendering preview</p>';
console.error('Error rendering preview:', error);
preview.innerHTML = '<p class="error">Error rendering preview. Please check your markdown syntax.</p>';
}
}