Compare commits

..
2 Commits
5 changed files with 111 additions and 71 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "markdown-converter", "name": "markdown-converter",
"version": "4.4.2", "version": "4.4.3",
"description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting", "description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<!-- CSP: unsafe-inline/unsafe-eval required for marked.js extensions and Mermaid --> <!-- CSP: unsafe-inline/unsafe-eval required for marked.js extensions and Mermaid -->
<!-- TODO: Migrate to nonce-based CSP for better security --> <!-- TODO: Migrate to nonce-based CSP for better security -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data: blob:; font-src 'self' data: https://cdn.jsdelivr.net; connect-src 'self' https://www.plantuml.com;"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https://www.plantuml.com;">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MarkdownConverter</title> <title>MarkdownConverter</title>
<!-- Design tokens - loaded first for CSS variable availability --> <!-- Design tokens - loaded first for CSS variable availability -->
+1 -3
View File
@@ -242,9 +242,7 @@ function runPandoc(args, callback) {
function runPandocCmd(cmdString, callback) { function runPandocCmd(cmdString, callback) {
const parsed = parseCommand(cmdString); const parsed = parseCommand(cmdString);
// Skip 'pandoc' if it's the first element (command itself) // Skip 'pandoc' if it's the first element (command itself)
// Use path.basename to handle full paths like bin/linux/pandoc const args = parsed.command === 'pandoc' ? parsed.args : [parsed.command, ...parsed.args];
const cmdBase = path.basename(parsed.command).replace(/\.exe$/i, '');
const args = cmdBase === 'pandoc' ? parsed.args : [parsed.command, ...parsed.args];
const pandocPath = getPandocPath(); const pandocPath = getPandocPath();
execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback); execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback);
} }
+107 -65
View File
@@ -1,20 +1,9 @@
/** /**
* MarkdownConverter Renderer Process * MarkdownConverter Renderer Process
* @version 4.4.2 * @version 4.4.3
*/ */
const { ipcRenderer } = require('electron'); const { ipcRenderer } = require('electron');
// Shim window.electronAPI for main window which uses nodeIntegration
// without preload script (window.electronAPI is normally set by preload.js).
if (typeof window !== 'undefined' && !window.electronAPI) {
window.electronAPI = {
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
invoke: (channel, data) => ipcRenderer.invoke(channel, data),
on: (channel, callback) => ipcRenderer.on(channel, (event, ...args) => callback(...args))
};
}
const marked = require('marked'); const marked = require('marked');
const { markedHighlight } = require('marked-highlight'); const { markedHighlight } = require('marked-highlight');
const createDOMPurify = require('dompurify'); const createDOMPurify = require('dompurify');
@@ -22,12 +11,53 @@ const DOMPurify = createDOMPurify(window);
const hljs = require('highlight.js'); const hljs = require('highlight.js');
const { createEditor } = require('./editor/codemirror-setup'); const { createEditor } = require('./editor/codemirror-setup');
const { undo, redo } = require('@codemirror/commands'); const { undo, redo } = require('@codemirror/commands');
// ModalManager is loaded via <script src="utils/ModalManager.js"> in index.html.
// It already exists in the global scope — re-declaring with let/const causes // Define a fallback window.electronAPI object for when contextIsolation is disabled
// SyntaxError: "Identifier 'ModalManager' has already been declared". // and the preload script is not loaded in the main window context.
if (typeof ModalManager === 'undefined') { if (typeof window !== 'undefined' && !window.electronAPI) {
window.electronAPI = {
send: (channel, data) => {
ipcRenderer.send(channel, data);
},
invoke: async (channel, data) => {
return await ipcRenderer.invoke(channel, data);
},
on: (channel, callback) => {
const subscription = (event, ...args) => callback(...args);
ipcRenderer.on(channel, subscription);
return () => {
ipcRenderer.removeListener(channel, subscription);
};
},
once: (channel, callback) => {
ipcRenderer.once(channel, (event, ...args) => callback(...args));
},
removeAllListeners: (channel) => {
ipcRenderer.removeAllListeners(channel);
},
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
file: {
read: (filePath) => ipcRenderer.invoke('read-file', filePath),
write: (filePath, content) => ipcRenderer.invoke('write-file', { path: filePath, content }),
delete: (filePath) => ipcRenderer.invoke('delete-file', filePath),
ensureDir: (dirPath) => ipcRenderer.invoke('ensure-directory', dirPath),
exists: (filePath) => ipcRenderer.invoke('path-exists', filePath),
isDirectory: (filePath) => ipcRenderer.invoke('is-directory', filePath),
copy: (source, destination) => ipcRenderer.invoke('copy-path', { source, destination }),
move: (source, destination) => ipcRenderer.invoke('move-path', { source, destination })
}
};
}
// Use window.ModalManager if already set by script tag, otherwise require it.
// This prevents "Identifier 'ModalManager' has already been declared" when
// both the script tag in index.html and CommonJS require() declare it.
let _ModalManager;
if (typeof window !== 'undefined' && window.ModalManager) {
_ModalManager = window.ModalManager;
} else {
const result = require('./utils/ModalManager'); const result = require('./utils/ModalManager');
ModalManager = result.ModalManager || result; _ModalManager = result.ModalManager || result;
} }
// Lazy-loaded modules — defer heavy imports until first use // Lazy-loaded modules — defer heavy imports until first use
let _SidebarManager, _renderTemplatesPanel, _renderExplorerPanel, _renderGitPanel, _renderSnippetsPanel; let _SidebarManager, _renderTemplatesPanel, _renderExplorerPanel, _renderGitPanel, _renderSnippetsPanel;
@@ -490,7 +520,38 @@ class TabManager {
document.querySelector('.editor-container').appendChild(tabContent); document.querySelector('.editor-container').appendChild(tabContent);
// Initialize CodeMirror editor // Initialize CodeMirror editor
this.ensureEditor(tab); const editorContainer = document.getElementById(`editor-cm-${tab.id}`);
if (editorContainer) {
const isDark = document.body.className.includes('dark');
tab.editorView = createEditor(editorContainer, {
content: tab.content,
onChange: (newContent) => {
tab.content = newContent;
tab.isDirty = true;
// Dynamically enable/disable Large File Mode on edit
if (newContent.length > 1024 * 1024) {
if (!tab.largeFileMode) {
tab.largeFileMode = true;
this.isPreviewVisible = false;
this.updatePreviewVisibility();
notifyUser('Large content detected (>1MB). Large File Mode enabled to maintain peak responsiveness. Live preview auto-render is disabled.', 'warning');
}
} else {
tab.largeFileMode = false;
}
this.updatePreview(tab.id);
this.updateWordCount();
this.updateTabBar();
if (outlinePanelContainer?._refreshOutline) outlinePanelContainer._refreshOutline();
},
onUpdate: (view) => {
this.updateCursorPosition(view);
if (outlinePanelContainer?._setActiveHeading) outlinePanelContainer._setActiveHeading(view.state.doc.lineAt(view.state.selection.main.head).number);
},
isDark,
showLineNumbers: this.showLineNumbers,
});
}
} }
switchToTab(tabId) { switchToTab(tabId) {
@@ -1461,47 +1522,9 @@ class TabManager {
return tab?.content || ''; return tab?.content || '';
} }
// Ensure a tab has a CodeMirror editor (lazy creation fallback)
ensureEditor(tab) {
if (tab.editorView) return true;
const editorContainer = document.getElementById(`editor-cm-${tab.id}`);
if (!editorContainer) {
console.error(`[ensureEditor] editor-cm-${tab.id} not found in DOM`);
return false;
}
try {
const isDark = document.body.className.includes('dark');
tab.editorView = createEditor(editorContainer, {
content: tab.content,
onChange: (newContent) => {
tab.content = newContent;
tab.isDirty = true;
this.updatePreview(tab.id);
this.updateWordCount();
this.updateTabBar();
if (outlinePanelContainer?._refreshOutline) outlinePanelContainer._refreshOutline();
},
onUpdate: (view) => {
this.updateCursorPosition(view);
if (outlinePanelContainer?._setActiveHeading) outlinePanelContainer._setActiveHeading(view.state.doc.lineAt(view.state.selection.main.head).number);
},
isDark,
showLineNumbers: this.showLineNumbers,
});
console.log(`[ensureEditor] Created editor for tab ${tab.id}`);
return true;
} catch (err) {
console.error(`[ensureEditor] Failed to create editor for tab ${tab.id}:`, err);
return false;
}
}
// Set content in editor // Set content in editor
setEditorContent(tabId, content) { setEditorContent(tabId, content) {
const tab = this.tabs.get(tabId); const tab = this.tabs.get(tabId);
if (!tab?.editorView) {
this.ensureEditor(tab);
}
if (tab?.editorView) { if (tab?.editorView) {
tab.editorView.dispatch({ tab.editorView.dispatch({
changes: { from: 0, to: tab.editorView.state.doc.length, insert: content } changes: { from: 0, to: tab.editorView.state.doc.length, insert: content }
@@ -1558,6 +1581,7 @@ let tabManager;
let replPanel; let replPanel;
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
const ModalManager = _ModalManager;
tabManager = new TabManager(); tabManager = new TabManager();
// Load saved Custom Preview CSS if present // Load saved Custom Preview CSS if present
@@ -1690,6 +1714,11 @@ document.addEventListener('DOMContentLoaded', async () => {
}); });
const statusBarRight = document.querySelector('.status-bar-right'); const statusBarRight = document.querySelector('.status-bar-right');
// Initialize command palette
const CommandPalette = getCommandPalette();
const commandPalette = new CommandPalette();
const pluginRegistry = new PluginRegistry({ const pluginRegistry = new PluginRegistry({
sidebar: sidebarManager, sidebar: sidebarManager,
commands: commandPalette, commands: commandPalette,
@@ -1745,8 +1774,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const welcomeHtml = getCreateWelcomeContent()(recentFiles, appVersion); const welcomeHtml = getCreateWelcomeContent()(recentFiles, appVersion);
const tab = tabManager.tabs.get(tabManager.activeTabId); const tab = tabManager.tabs.get(tabManager.activeTabId);
// Only show welcome if no file was opened while we were awaiting if (tab) {
if (tab && !tab.filePath && tab.content === '') {
tab.title = 'Welcome'; tab.title = 'Welcome';
tab.content = ''; tab.content = '';
const preview = document.getElementById(`preview-${tab.id}`); const preview = document.getElementById(`preview-${tab.id}`);
@@ -1827,10 +1855,6 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
}); });
// Initialize command palette
const CommandPalette = getCommandPalette();
const commandPalette = new CommandPalette();
// Initialize print preview // Initialize print preview
const PrintPreview = getPrintPreview(); const PrintPreview = getPrintPreview();
const printPreview = new PrintPreview(); const printPreview = new PrintPreview();
@@ -1915,9 +1939,27 @@ document.addEventListener('DOMContentLoaded', async () => {
}); });
// Initialize CodeMirror for the initial tab (tab 1) // Initialize CodeMirror for the initial tab (tab 1)
const initialTab = tabManager.tabs.get(1); const initialEditorContainer = document.getElementById('editor-cm-1');
if (initialTab) { if (initialEditorContainer) {
tabManager.ensureEditor(initialTab); const tab = tabManager.tabs.get(1);
const isDark = document.body.className.includes('dark');
tab.editorView = createEditor(initialEditorContainer, {
content: tab.content,
onChange: (newContent) => {
tab.content = newContent;
tab.isDirty = true;
tabManager.updatePreview(tab.id);
tabManager.updateWordCount();
tabManager.updateTabBar();
if (outlinePanelContainer?._refreshOutline) outlinePanelContainer._refreshOutline();
},
onUpdate: (view) => {
tabManager.updateCursorPosition(view);
if (outlinePanelContainer?._setActiveHeading) outlinePanelContainer._setActiveHeading(view.state.doc.lineAt(view.state.selection.main.head).number);
},
isDark,
showLineNumbers: tabManager.showLineNumbers,
});
} }
// Request current theme // Request current theme
+1 -1
View File
@@ -1,6 +1,6 @@
/** /**
* ModalManager - Unified modal system with accessibility support * ModalManager - Unified modal system with accessibility support
* @version 4.4.2 * @version 4.4.3
*/ */
class ModalManager { class ModalManager {
#modal; #modal;