mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-03 02:11:07 +05:30
- Add comprehensive Find & Replace with match highlighting - Add toggleable line numbers with theme support - Implement full undo/redo functionality with keyboard shortcuts - Add smart auto-indentation for lists and code blocks - Enhanced keyboard shortcuts for productivity - Advanced tab handling for indentation/outdentation - Updated About dialog with v1.2.0 and complete feature list - Add Find & Replace to Edit menu - Updated README.md with comprehensive feature documentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
599 lines
18 KiB
JavaScript
599 lines
18 KiB
JavaScript
const { ipcRenderer } = require('electron');
|
|
const marked = require('marked');
|
|
const DOMPurify = require('dompurify');
|
|
const hljs = require('highlight.js');
|
|
|
|
// Configure marked
|
|
marked.setOptions({
|
|
highlight: function(code, lang) {
|
|
if (lang && hljs.getLanguage(lang)) {
|
|
try {
|
|
return hljs.highlight(code, { language: lang }).value;
|
|
} catch (err) {}
|
|
}
|
|
return hljs.highlightAuto(code).value;
|
|
},
|
|
breaks: true,
|
|
gfm: true
|
|
});
|
|
|
|
// Elements
|
|
const editor = document.getElementById('editor');
|
|
const preview = document.getElementById('preview');
|
|
const previewPane = document.getElementById('preview-pane');
|
|
const editorPane = document.getElementById('editor-pane');
|
|
const statusText = document.getElementById('status-text');
|
|
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
|
|
let isPreviewVisible = true;
|
|
let currentContent = '';
|
|
let isDirty = false;
|
|
let showLineNumbers = false;
|
|
let findMatches = [];
|
|
let currentMatchIndex = -1;
|
|
let undoStack = [];
|
|
let redoStack = [];
|
|
let maxUndoSize = 50;
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Request current theme
|
|
ipcRenderer.send('get-theme');
|
|
|
|
// Set up auto-save interval
|
|
setInterval(autoSave, 30000); // Auto-save every 30 seconds
|
|
|
|
// Initialize with empty content
|
|
updatePreview();
|
|
updateWordCount();
|
|
});
|
|
|
|
// Editor input handler
|
|
editor.addEventListener('input', () => {
|
|
currentContent = editor.value;
|
|
isDirty = true;
|
|
updatePreview();
|
|
updateWordCount();
|
|
updateStatus('Modified');
|
|
});
|
|
|
|
// Toolbar button handlers
|
|
document.getElementById('btn-bold').addEventListener('click', () => insertMarkdown('**', '**'));
|
|
document.getElementById('btn-italic').addEventListener('click', () => insertMarkdown('*', '*'));
|
|
document.getElementById('btn-heading').addEventListener('click', () => insertMarkdown('## ', ''));
|
|
document.getElementById('btn-link').addEventListener('click', () => insertMarkdown('[', '](url)'));
|
|
document.getElementById('btn-code').addEventListener('click', () => insertMarkdown('`', '`'));
|
|
document.getElementById('btn-list').addEventListener('click', () => insertMarkdown('- ', ''));
|
|
document.getElementById('btn-quote').addEventListener('click', () => insertMarkdown('> ', ''));
|
|
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);
|
|
|
|
// 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
|
|
function insertTable() {
|
|
const rows = prompt('Number of rows:', '3');
|
|
const cols = prompt('Number of columns:', '3');
|
|
|
|
if (!rows || !cols) return;
|
|
|
|
const numRows = parseInt(rows);
|
|
const numCols = parseInt(cols);
|
|
|
|
if (isNaN(numRows) || isNaN(numCols) || numRows < 1 || numCols < 1) {
|
|
alert('Please enter valid numbers for rows and columns');
|
|
return;
|
|
}
|
|
|
|
let table = '\n';
|
|
|
|
// Header row
|
|
table += '|';
|
|
for (let j = 0; j < numCols; j++) {
|
|
table += ` Header ${j + 1} |`;
|
|
}
|
|
table += '\n';
|
|
|
|
// Separator row
|
|
table += '|';
|
|
for (let j = 0; j < numCols; j++) {
|
|
table += ' --- |';
|
|
}
|
|
table += '\n';
|
|
|
|
// Data rows
|
|
for (let i = 0; i < numRows; i++) {
|
|
table += '|';
|
|
for (let j = 0; j < numCols; j++) {
|
|
table += ` Cell ${i + 1}-${j + 1} |`;
|
|
}
|
|
table += '\n';
|
|
}
|
|
table += '\n';
|
|
|
|
// Insert table at cursor position
|
|
const start = editor.selectionStart;
|
|
const end = editor.selectionEnd;
|
|
|
|
editor.value = editor.value.substring(0, start) + table + editor.value.substring(end);
|
|
|
|
// Set cursor after the table
|
|
editor.selectionStart = editor.selectionEnd = start + table.length;
|
|
editor.focus();
|
|
|
|
// Trigger input event
|
|
editor.dispatchEvent(new Event('input'));
|
|
}
|
|
|
|
// Markdown insertion helper
|
|
function insertMarkdown(before, after) {
|
|
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);
|
|
|
|
// Set cursor position
|
|
if (selectedText) {
|
|
editor.selectionStart = start;
|
|
editor.selectionEnd = start + replacement.length;
|
|
} else {
|
|
editor.selectionStart = start + before.length;
|
|
editor.selectionEnd = start + before.length + 4; // Select "text"
|
|
}
|
|
|
|
editor.focus();
|
|
|
|
// Trigger input event
|
|
editor.dispatchEvent(new Event('input'));
|
|
}
|
|
|
|
// Update preview
|
|
function updatePreview() {
|
|
const html = marked.parse(editor.value);
|
|
const clean = DOMPurify.sanitize(html);
|
|
preview.innerHTML = clean;
|
|
|
|
// Re-highlight code blocks
|
|
preview.querySelectorAll('pre code').forEach((block) => {
|
|
hljs.highlightElement(block);
|
|
});
|
|
}
|
|
|
|
// Toggle preview visibility
|
|
function togglePreview() {
|
|
isPreviewVisible = !isPreviewVisible;
|
|
|
|
if (isPreviewVisible) {
|
|
previewPane.classList.remove('hidden');
|
|
editorPane.classList.remove('full-width');
|
|
} else {
|
|
previewPane.classList.add('hidden');
|
|
editorPane.classList.add('full-width');
|
|
}
|
|
}
|
|
|
|
// Update word count
|
|
function updateWordCount() {
|
|
const text = editor.value;
|
|
const words = text.trim() ? text.trim().split(/\s+/).length : 0;
|
|
const chars = text.length;
|
|
wordCount.textContent = `Words: ${words} | Characters: ${chars}`;
|
|
}
|
|
|
|
// Update status
|
|
function updateStatus(text) {
|
|
statusText.textContent = text;
|
|
}
|
|
|
|
// Auto-save function
|
|
function autoSave() {
|
|
if (isDirty && currentContent) {
|
|
ipcRenderer.send('save-current-file', currentContent);
|
|
isDirty = false;
|
|
updateStatus('Auto-saved');
|
|
setTimeout(() => updateStatus('Ready'), 2000);
|
|
}
|
|
}
|
|
|
|
// IPC event handlers
|
|
ipcRenderer.on('file-new', () => {
|
|
if (isDirty) {
|
|
if (confirm('You have unsaved changes. Do you want to continue?')) {
|
|
editor.value = '';
|
|
currentContent = '';
|
|
isDirty = false;
|
|
updatePreview();
|
|
updateWordCount();
|
|
updateStatus('New file');
|
|
}
|
|
} else {
|
|
editor.value = '';
|
|
currentContent = '';
|
|
updatePreview();
|
|
updateWordCount();
|
|
updateStatus('New file');
|
|
}
|
|
});
|
|
|
|
ipcRenderer.on('file-opened', (event, { path, content }) => {
|
|
editor.value = content;
|
|
currentContent = content;
|
|
isDirty = false;
|
|
updatePreview();
|
|
updateWordCount();
|
|
updateStatus(`Opened: ${path}`);
|
|
});
|
|
|
|
ipcRenderer.on('file-save', () => {
|
|
ipcRenderer.send('save-current-file', editor.value);
|
|
isDirty = false;
|
|
updateStatus('Saved');
|
|
});
|
|
|
|
ipcRenderer.on('get-content-for-save', (event, path) => {
|
|
ipcRenderer.send('save-file', { path, content: editor.value });
|
|
isDirty = false;
|
|
updateStatus(`Saved: ${path}`);
|
|
});
|
|
|
|
ipcRenderer.on('toggle-preview', () => {
|
|
togglePreview();
|
|
});
|
|
|
|
ipcRenderer.on('theme-changed', (event, theme) => {
|
|
// Remove all theme classes
|
|
document.body.classList.remove('theme-light', 'theme-dark', 'theme-solarized', 'theme-monokai', 'theme-github');
|
|
|
|
// Add new theme class
|
|
if (theme !== 'light') {
|
|
document.body.classList.add(`theme-${theme}`);
|
|
}
|
|
|
|
updateStatus(`Theme: ${theme}`);
|
|
});
|
|
|
|
ipcRenderer.on('get-content-for-spreadsheet', (event, 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
|
|
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
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
togglePreview();
|
|
}
|
|
|
|
// Enhanced Tab handling in editor
|
|
if (e.key === 'Tab' && e.target === editor) {
|
|
e.preventDefault();
|
|
const start = editor.selectionStart;
|
|
const end = editor.selectionEnd;
|
|
|
|
if (start === end) {
|
|
// 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();
|
|
}
|
|
});
|
|
|
|
// Prevent accidental navigation
|
|
window.addEventListener('beforeunload', (e) => {
|
|
if (isDirty) {
|
|
e.preventDefault();
|
|
e.returnValue = '';
|
|
}
|
|
}); |