Merge master with comprehensive v1.2.0 editor enhancements

This commit is contained in:
2025-09-01 20:29:13 +05:30
5 changed files with 533 additions and 14 deletions
+30 -7
View File
@@ -6,15 +6,27 @@ A cross-platform Markdown editor and converter powered by Pandoc.
## Features ## Features
- 📝 **Rich Markdown Editor** - Full-featured editor with syntax highlighting and toolbar ### ✨ Advanced Markdown Editor
- 👁️ **Live Preview** - See your markdown rendered in real-time - 📝 **Rich Text Editor** - Full-featured editor with syntax highlighting and comprehensive toolbar
- 🔍 **Find & Replace** - Powerful search and replace with match highlighting and navigation
- 🔢 **Line Numbers** - Toggle line numbers for easier code editing and navigation
- ↩️ **Smart Auto-Indentation** - Automatic list continuation and intelligent indentation
-**Undo/Redo** - Full undo/redo support with keyboard shortcuts
- ⌨️ **Advanced Shortcuts** - Tab indentation, line selection, and smart text formatting
### 🎨 Themes & Interface
- 👁️ **Live Preview** - See your markdown rendered in real-time with synchronized scrolling
- 🎨 **Multiple Themes** - Choose from Light, Dark, Solarized, Monokai, or GitHub themes - 🎨 **Multiple Themes** - Choose from Light, Dark, Solarized, Monokai, or GitHub themes
- 📤 **Document Export** - Convert to HTML, PDF, DOCX, LaTeX, RTF, ODT, EPUB, PowerPoint (PPTX), and OpenDocument Presentation (ODP) - 💾 **Auto-Save** - Never lose your work with automatic saving every 30 seconds
### 📤 Export & Conversion
- 📄 **Document Export** - Convert to HTML, PDF, DOCX, LaTeX, RTF, ODT, EPUB, PowerPoint (PPTX), and OpenDocument Presentation (ODP)
- 📊 **Spreadsheet Export** - Export markdown tables to Excel (XLSX/XLS) and OpenDocument Spreadsheet (ODS) formats - 📊 **Spreadsheet Export** - Export markdown tables to Excel (XLSX/XLS) and OpenDocument Spreadsheet (ODS) formats
- 📥 **Document Import & Conversion** - Import various document formats and convert between formats via Pandoc - 📥 **Document Import & Conversion** - Import various document formats and convert between formats via Pandoc
- 📋 **Table Creation Helper** - Built-in table generator for easy markdown table creation - 📋 **Table Creation Helper** - Built-in table generator for easy markdown table creation
- 💾 **Auto-Save** - Never lose your work with automatic saving every 30 seconds
- 🖥️ **Cross-Platform** - Works on Windows, macOS, and Linux ### 🖥️ Platform Support
- **Cross-Platform** - Works seamlessly on Windows, macOS, and Linux
## Installation ## Installation
@@ -65,14 +77,25 @@ npm start
## Keyboard Shortcuts ## Keyboard Shortcuts
### File Operations
- `Ctrl/Cmd + N` - New file - `Ctrl/Cmd + N` - New file
- `Ctrl/Cmd + O` - Open file - `Ctrl/Cmd + O` - Open file
- `Ctrl/Cmd + S` - Save file - `Ctrl/Cmd + S` - Save file
- `Ctrl/Cmd + Shift + S` - Save as - `Ctrl/Cmd + Shift + S` - Save as
- `Ctrl/Cmd + I` - Import document - `Ctrl/Cmd + I` - Import document
### Editor Features
- `Ctrl/Cmd + F` - Find & Replace
- `Ctrl/Cmd + Z` - Undo
- `Ctrl/Cmd + Shift + Z` - Redo
- `Tab` - Indent lines or insert 4 spaces
- `Shift + Tab` - Outdent selected lines
- `Enter` - Auto-continue lists with proper indentation
### View & Navigation
- `Ctrl/Cmd + P` - Toggle preview - `Ctrl/Cmd + P` - Toggle preview
- `Ctrl/Cmd + Enter` - Toggle preview (alternative) - `Ctrl/Cmd + Enter` - Toggle preview (alternative)
- `Tab` - Insert 4 spaces (in editor) - `Escape` - Close find dialog
## Building ## Building
@@ -97,7 +120,7 @@ npm run dist:all
## Version History ## Version History
- **v1.2.0** - Added PowerPoint export, document conversion menu, table creation helper, spreadsheet export - **v1.2.0** - Major editor enhancements: Find & Replace, Line Numbers, Undo/Redo, Auto-indentation, PowerPoint export, document conversion menu, table creation helper, spreadsheet export
- **v1.1.0** - Added Excel/ODS spreadsheet export, updated author information, renamed to PanConverter - **v1.1.0** - Added Excel/ODS spreadsheet export, updated author information, renamed to PanConverter
- **v1.0.0** - Initial release with basic markdown editing, themes, and Pandoc export - **v1.0.0** - Initial release with basic markdown editing, themes, and Pandoc export
+34 -1
View File
@@ -69,6 +69,20 @@
</svg> </svg>
</button> </button>
<div class="toolbar-separator"></div> <div class="toolbar-separator"></div>
<button id="btn-find" title="Find & Replace (Ctrl+F)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</button>
<button id="btn-line-numbers" title="Toggle Line Numbers">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18"></path>
<path d="M3 12h18"></path>
<path d="M3 18h18"></path>
</svg>
</button>
<div class="toolbar-separator"></div>
<button id="btn-preview-toggle" title="Toggle Preview"> <button id="btn-preview-toggle" title="Toggle Preview">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path> <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
@@ -77,9 +91,28 @@
</button> </button>
</div> </div>
<!-- Find & Replace Dialog -->
<div id="find-dialog" class="find-dialog hidden">
<div class="find-controls">
<input type="text" id="find-input" placeholder="Find...">
<input type="text" id="replace-input" placeholder="Replace...">
<button id="btn-find-prev" title="Previous"></button>
<button id="btn-find-next" title="Next"></button>
<button id="btn-replace" title="Replace">Replace</button>
<button id="btn-replace-all" title="Replace All">All</button>
<button id="btn-find-close" title="Close">×</button>
</div>
<div class="find-info">
<span id="find-count">0 matches</span>
</div>
</div>
<div class="editor-container"> <div class="editor-container">
<div id="editor-pane" class="pane"> <div id="editor-pane" class="pane">
<textarea id="editor"></textarea> <div class="editor-wrapper">
<div id="line-numbers" class="line-numbers hidden"></div>
<textarea id="editor"></textarea>
</div>
</div> </div>
<div id="preview-pane" class="pane"> <div id="preview-pane" class="pane">
<div id="preview"></div> <div id="preview"></div>
+9 -3
View File
@@ -107,12 +107,18 @@ function createMenu() {
label: 'Edit', label: 'Edit',
submenu: [ submenu: [
{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, { label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
{ label: 'Redo', accelerator: 'CmdOrCtrl+Y', role: 'redo' }, { label: 'Redo', accelerator: 'CmdOrCtrl+Shift+Z', role: 'redo' },
{ type: 'separator' }, { type: 'separator' },
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }, { label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' },
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' }, { label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' },
{ label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectAll' } { label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectAll' },
{ type: 'separator' },
{
label: 'Find & Replace',
accelerator: 'CmdOrCtrl+F',
click: () => mainWindow.webContents.send('toggle-find')
}
] ]
}, },
{ {
@@ -178,7 +184,7 @@ function createMenu() {
type: 'info', type: 'info',
title: 'About PanConverter', title: 'About PanConverter',
message: 'PanConverter', message: 'PanConverter',
detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.2.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Markdown editing with live preview\n• Export to multiple formats via Pandoc\n• Export tables to Excel/ODS spreadsheets\n• PowerPoint & presentation export\n• Document import & conversion\n• Table creation helper\n• Multiple themes support', detail: 'A cross-platform Markdown editor and converter using Pandoc.\n\nVersion: 1.2.0\nAuthor: Amit Haridas\nEmail: amit.wh@gmail.com\nLicense: MIT\n\nFeatures:\n• Advanced markdown editing with live preview\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',
buttons: ['OK'] buttons: ['OK']
}); });
} }
+324 -3
View File
@@ -24,11 +24,22 @@ const previewPane = document.getElementById('preview-pane');
const editorPane = document.getElementById('editor-pane'); const editorPane = document.getElementById('editor-pane');
const statusText = document.getElementById('status-text'); const statusText = document.getElementById('status-text');
const wordCount = document.getElementById('word-count'); const wordCount = document.getElementById('word-count');
const lineNumbers = document.getElementById('line-numbers');
const findDialog = document.getElementById('find-dialog');
const findInput = document.getElementById('find-input');
const replaceInput = document.getElementById('replace-input');
const findCount = document.getElementById('find-count');
// State // State
let isPreviewVisible = true; let isPreviewVisible = true;
let currentContent = ''; let currentContent = '';
let isDirty = false; let isDirty = false;
let showLineNumbers = false;
let findMatches = [];
let currentMatchIndex = -1;
let undoStack = [];
let redoStack = [];
let maxUndoSize = 50;
// Initialize // Initialize
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
@@ -61,8 +72,21 @@ document.getElementById('btn-code').addEventListener('click', () => insertMarkdo
document.getElementById('btn-list').addEventListener('click', () => insertMarkdown('- ', '')); document.getElementById('btn-list').addEventListener('click', () => insertMarkdown('- ', ''));
document.getElementById('btn-quote').addEventListener('click', () => insertMarkdown('> ', '')); document.getElementById('btn-quote').addEventListener('click', () => insertMarkdown('> ', ''));
document.getElementById('btn-table').addEventListener('click', insertTable); document.getElementById('btn-table').addEventListener('click', insertTable);
document.getElementById('btn-find').addEventListener('click', toggleFindDialog);
document.getElementById('btn-line-numbers').addEventListener('click', toggleLineNumbers);
document.getElementById('btn-preview-toggle').addEventListener('click', togglePreview); document.getElementById('btn-preview-toggle').addEventListener('click', togglePreview);
// Find dialog handlers
document.getElementById('btn-find-close').addEventListener('click', closeFindDialog);
document.getElementById('btn-find-next').addEventListener('click', findNext);
document.getElementById('btn-find-prev').addEventListener('click', findPrev);
document.getElementById('btn-replace').addEventListener('click', replaceOne);
document.getElementById('btn-replace-all').addEventListener('click', replaceAll);
findInput.addEventListener('input', performFind);
replaceInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') replaceOne();
});
// Table insertion function // Table insertion function
function insertTable() { function insertTable() {
const rows = prompt('Number of rows:', '3'); const rows = prompt('Number of rows:', '3');
@@ -251,21 +275,318 @@ ipcRenderer.on('get-content-for-spreadsheet', (event, format) => {
ipcRenderer.send('export-spreadsheet', { content: editor.value, format }); ipcRenderer.send('export-spreadsheet', { content: editor.value, format });
}); });
ipcRenderer.on('toggle-find', () => {
toggleFindDialog();
});
// Enhanced editor input handler with undo/redo and auto-indentation
editor.addEventListener('input', (e) => {
// Save state for undo
if (e.inputType !== 'historyUndo' && e.inputType !== 'historyRedo') {
pushUndo();
}
updateLineNumbers();
});
// Push current state to undo stack
function pushUndo() {
if (undoStack.length >= maxUndoSize) {
undoStack.shift();
}
undoStack.push({
content: currentContent,
selectionStart: editor.selectionStart,
selectionEnd: editor.selectionEnd
});
redoStack = []; // Clear redo stack when new changes are made
}
// Undo function
function undo() {
if (undoStack.length > 1) {
redoStack.push(undoStack.pop());
const state = undoStack[undoStack.length - 1];
editor.value = state.content;
editor.selectionStart = state.selectionStart;
editor.selectionEnd = state.selectionEnd;
currentContent = state.content;
updatePreview();
updateWordCount();
updateLineNumbers();
}
}
// Redo function
function redo() {
if (redoStack.length > 0) {
const state = redoStack.pop();
undoStack.push(state);
editor.value = state.content;
editor.selectionStart = state.selectionStart;
editor.selectionEnd = state.selectionEnd;
currentContent = state.content;
updatePreview();
updateWordCount();
updateLineNumbers();
}
}
// Find & Replace functionality
function toggleFindDialog() {
if (findDialog.classList.contains('hidden')) {
findDialog.classList.remove('hidden');
findInput.focus();
if (editor.selectionStart !== editor.selectionEnd) {
findInput.value = editor.value.substring(editor.selectionStart, editor.selectionEnd);
performFind();
}
} else {
closeFindDialog();
}
}
function closeFindDialog() {
findDialog.classList.add('hidden');
clearHighlights();
editor.focus();
}
function performFind() {
const searchText = findInput.value;
clearHighlights();
findMatches = [];
currentMatchIndex = -1;
if (!searchText) {
findCount.textContent = '0 matches';
return;
}
const content = editor.value;
let index = 0;
while ((index = content.indexOf(searchText, index)) !== -1) {
findMatches.push(index);
index += searchText.length;
}
findCount.textContent = `${findMatches.length} matches`;
if (findMatches.length > 0) {
currentMatchIndex = 0;
highlightMatch();
}
}
function findNext() {
if (findMatches.length === 0) return;
currentMatchIndex = (currentMatchIndex + 1) % findMatches.length;
highlightMatch();
}
function findPrev() {
if (findMatches.length === 0) return;
currentMatchIndex = currentMatchIndex === 0 ? findMatches.length - 1 : currentMatchIndex - 1;
highlightMatch();
}
function highlightMatch() {
if (currentMatchIndex === -1 || !findMatches[currentMatchIndex]) return;
const matchStart = findMatches[currentMatchIndex];
const matchEnd = matchStart + findInput.value.length;
editor.selectionStart = matchStart;
editor.selectionEnd = matchEnd;
editor.focus();
// Scroll to match
const lineHeight = 20; // Approximate line height
const lineNumber = editor.value.substring(0, matchStart).split('\n').length;
editor.scrollTop = Math.max(0, (lineNumber - 10) * lineHeight);
}
function replaceOne() {
if (currentMatchIndex === -1 || !findMatches[currentMatchIndex]) return;
const searchText = findInput.value;
const replaceText = replaceInput.value;
const matchStart = findMatches[currentMatchIndex];
const matchEnd = matchStart + searchText.length;
editor.value = editor.value.substring(0, matchStart) +
replaceText +
editor.value.substring(matchEnd);
editor.selectionStart = matchStart;
editor.selectionEnd = matchStart + replaceText.length;
// Trigger input event
editor.dispatchEvent(new Event('input'));
// Refresh find
setTimeout(() => performFind(), 10);
}
function replaceAll() {
const searchText = findInput.value;
const replaceText = replaceInput.value;
if (!searchText) return;
let content = editor.value;
let replacements = 0;
while (content.includes(searchText)) {
content = content.replace(searchText, replaceText);
replacements++;
}
if (replacements > 0) {
editor.value = content;
editor.dispatchEvent(new Event('input'));
updateStatus(`Replaced ${replacements} occurrences`);
performFind();
}
}
function clearHighlights() {
// Clear any highlights (in a real implementation, you'd remove highlight spans)
}
// Line Numbers functionality
function toggleLineNumbers() {
showLineNumbers = !showLineNumbers;
if (showLineNumbers) {
lineNumbers.classList.remove('hidden');
updateLineNumbers();
} else {
lineNumbers.classList.add('hidden');
}
}
function updateLineNumbers() {
if (!showLineNumbers) return;
const lines = editor.value.split('\n');
const lineNumbersHtml = lines.map((_, index) =>
`<span class="line-number">${index + 1}</span>`
).join('');
lineNumbers.innerHTML = lineNumbersHtml;
// Sync scroll
lineNumbers.scrollTop = editor.scrollTop;
}
// Sync line numbers scroll with editor
editor.addEventListener('scroll', () => {
if (showLineNumbers) {
lineNumbers.scrollTop = editor.scrollTop;
}
});
// Auto-indentation for lists
function handleEnterKey(e) {
const cursorPos = editor.selectionStart;
const beforeCursor = editor.value.substring(0, cursorPos);
const lines = beforeCursor.split('\n');
const currentLine = lines[lines.length - 1];
// Check for list patterns
const listMatch = currentLine.match(/^(\s*)([-*+]|\d+\.)\s/);
if (listMatch) {
e.preventDefault();
const indent = listMatch[1];
const marker = listMatch[2];
// If current line is just the marker, remove it
if (currentLine.trim() === marker) {
const lineStart = beforeCursor.lastIndexOf('\n') + 1;
editor.value = editor.value.substring(0, lineStart) +
editor.value.substring(cursorPos);
editor.selectionStart = editor.selectionEnd = lineStart;
} else {
// Continue the list
let newMarker = marker;
if (/^\d+\./.test(marker)) {
const num = parseInt(marker) + 1;
newMarker = num + '.';
}
const insertion = '\n' + indent + newMarker + ' ';
editor.value = editor.value.substring(0, cursorPos) +
insertion +
editor.value.substring(cursorPos);
editor.selectionStart = editor.selectionEnd = cursorPos + insertion.length;
}
editor.dispatchEvent(new Event('input'));
}
}
// Keyboard shortcuts // Keyboard shortcuts
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + F for find
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
e.preventDefault();
toggleFindDialog();
}
// Ctrl/Cmd + Z for undo
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
e.preventDefault();
undo();
}
// Ctrl/Cmd + Shift + Z for redo
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && e.shiftKey) {
e.preventDefault();
redo();
}
// Ctrl/Cmd + Enter to toggle preview // Ctrl/Cmd + Enter to toggle preview
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
togglePreview(); togglePreview();
} }
// Tab key in editor // Enhanced Tab handling in editor
if (e.key === 'Tab' && e.target === editor) { if (e.key === 'Tab' && e.target === editor) {
e.preventDefault(); e.preventDefault();
const start = editor.selectionStart; const start = editor.selectionStart;
const end = editor.selectionEnd; const end = editor.selectionEnd;
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end); if (start === end) {
editor.selectionStart = editor.selectionEnd = start + 4; // Simple tab insertion
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
editor.selectionStart = editor.selectionEnd = start + 4;
} else {
// Indent/outdent selected lines
const beforeSelection = editor.value.substring(0, start);
const selection = editor.value.substring(start, end);
const afterSelection = editor.value.substring(end);
const lines = selection.split('\n');
const indentedLines = e.shiftKey ?
lines.map(line => line.replace(/^ /, '')) : // Outdent
lines.map(line => ' ' + line); // Indent
const newSelection = indentedLines.join('\n');
editor.value = beforeSelection + newSelection + afterSelection;
editor.selectionStart = start;
editor.selectionEnd = start + newSelection.length;
}
editor.dispatchEvent(new Event('input'));
}
// Enter key handling for auto-indentation
if (e.key === 'Enter' && e.target === editor) {
handleEnterKey(e);
}
// Escape to close find dialog
if (e.key === 'Escape' && !findDialog.classList.contains('hidden')) {
closeFindDialog();
} }
}); });
+136
View File
@@ -459,4 +459,140 @@ body.theme-github .status-bar {
#editor-pane.full-width { #editor-pane.full-width {
border-right: none; border-right: none;
}
/* Find & Replace Dialog */
.find-dialog {
position: absolute;
top: 60px;
right: 20px;
background: #fff;
border: 1px solid #ddd;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
min-width: 400px;
padding: 12px;
}
.find-dialog.hidden {
display: none;
}
.find-controls {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
}
.find-controls input {
flex: 1;
padding: 6px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
}
.find-controls button {
padding: 6px 10px;
border: 1px solid #ddd;
background: #f5f5f5;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
white-space: nowrap;
}
.find-controls button:hover {
background: #e5e5e5;
}
.find-info {
font-size: 12px;
color: #666;
text-align: right;
}
/* Editor Wrapper with Line Numbers */
.editor-wrapper {
position: relative;
display: flex;
height: 100%;
}
.line-numbers {
min-width: 40px;
padding: 8px 4px;
background: #f8f8f8;
border-right: 1px solid #ddd;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.4;
color: #666;
text-align: right;
user-select: none;
overflow: hidden;
}
.line-numbers.hidden {
display: none;
}
.line-numbers .line-number {
display: block;
white-space: nowrap;
}
/* Adjust editor when line numbers are shown */
.editor-wrapper #editor {
flex: 1;
}
/* Theme support for find dialog */
body.theme-dark .find-dialog {
background: #2d2d2d;
border-color: #555;
color: #f0f0f0;
}
body.theme-dark .find-controls input {
background: #333;
border-color: #555;
color: #f0f0f0;
}
body.theme-dark .find-controls button {
background: #404040;
border-color: #555;
color: #f0f0f0;
}
body.theme-dark .find-controls button:hover {
background: #4a4a4a;
}
body.theme-dark .line-numbers {
background: #2a2a2a;
border-right-color: #555;
color: #888;
}
/* Additional theme support for line numbers */
body.theme-solarized .line-numbers {
background: #fdf6e3;
border-right-color: #eee8d5;
color: #93a1a1;
}
body.theme-monokai .line-numbers {
background: #2f2f2f;
border-right-color: #555;
color: #75715e;
}
body.theme-github .line-numbers {
background: #fafbfc;
border-right-color: #e1e4e8;
color: #586069;
} }