Compare commits

...
5 Commits
Author SHA1 Message Date
amitwh 88e9a5290d fix(renderer): add ensureEditor fallback and fix pandoc export path
1. Source editor blank: Added ensureEditor() method with try-catch that
   lazily creates the CodeMirror editor when setEditorContent is called
   if the initial DOMContentLoaded creation failed or was skipped.
   Replaced inline createEditor calls in createTabElements and
   DOMContentLoaded with ensureEditor().

2. Export via pandoc failing: runPandocCmd parsed the full path
   /bin/linux/pandoc as the command, then prepended it to args because
   parsed.command !== 'pandoc'. This caused pandoc to receive its own
   binary path as the first input file. Fix: use path.basename() to
   check if the command ends with 'pandoc' (or 'pandoc.exe').

Amit Haridas
2026-06-03 20:44:00 +05:30
amitwh fff15d8d3e fix(renderer): add window.electronAPI shim and update CSP for KaTeX
The main window uses nodeIntegration without preload, so window.electronAPI
was undefined. This caused the DOMContentLoaded handler to crash at:
  await window.electronAPI.getAppVersion()
which prevented the CodeMirror editor from being created (blank source
window) and stopped renderer-ready from being sent (broken menu/options).

Fixes:
1. Add window.electronAPI shim at top of renderer.js wrapping ipcRenderer.
2. Update CSP meta tag to allow KaTeX CDN (style-src, script-src, font-src).

Amit Haridas
2026-06-03 15:37:20 +05:30
amitwh c574d77c20 fix(renderer): remove let ModalManager to prevent SyntaxError on load
ModalManager.js is loaded via <script> tag in index.html, which
declares class ModalManager in the global script scope. renderer.js
was then doing 'let ModalManager;' which caused:
  SyntaxError: Identifier 'ModalManager' has already been declared

This prevented renderer.js from executing at all, breaking tabs,
editor, file open, and preview rendering.

Fix: conditionally require ModalManager only if undefined, without
re-declaring. In sloppy mode this safely assigns to the existing global.

Amit Haridas
2026-06-03 15:25:22 +05:30
amitwh 272215f9af fix(renderer): prevent welcome screen from overwriting opened markdown files
The welcome tab setup awaited getAppVersion() inside DOMContentLoaded.
While yielding, the renderer-ready timeout fired, file-opened was processed,
and openFile rendered the markdown. When the welcome setup resumed, it
overwrote tab.content and preview.innerHTML with the welcome screen,
leaving the preview blank.

Fix: only show the welcome screen if the tab is still empty (no filePath
and no content) when the async setup resumes.

Amit Haridas
2026-06-03 15:14:41 +05:30
amitwh 0192590567 fix(renderer): add missing closing brace for redo IPC handler
The ipcRenderer.on('redo') callback was missing its closing });
introduced in a9e05d2, causing SyntaxError: Unexpected end of input.

Amit Haridas
2026-06-03 14:36:59 +05:30
3 changed files with 65 additions and 63 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<!-- CSP: unsafe-inline/unsafe-eval required for marked.js extensions and Mermaid -->
<!-- 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'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https://www.plantuml.com;">
<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 name="viewport" content="width=device-width, initial-scale=1.0">
<title>MarkdownConverter</title>
<!-- Design tokens - loaded first for CSS variable availability -->
+3 -1
View File
@@ -242,7 +242,9 @@ function runPandoc(args, callback) {
function runPandocCmd(cmdString, callback) {
const parsed = parseCommand(cmdString);
// Skip 'pandoc' if it's the first element (command itself)
const args = parsed.command === 'pandoc' ? parsed.args : [parsed.command, ...parsed.args];
// Use path.basename to handle full paths like bin/linux/pandoc
const cmdBase = path.basename(parsed.command).replace(/\.exe$/i, '');
const args = cmdBase === 'pandoc' ? parsed.args : [parsed.command, ...parsed.args];
const pandocPath = getPandocPath();
execFile(pandocPath, args, { maxBuffer: 10 * 1024 * 1024 }, callback);
}
+61 -61
View File
@@ -4,6 +4,17 @@
*/
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 { markedHighlight } = require('marked-highlight');
const createDOMPurify = require('dompurify');
@@ -11,13 +22,10 @@ const DOMPurify = createDOMPurify(window);
const hljs = require('highlight.js');
const { createEditor } = require('./editor/codemirror-setup');
const { undo, redo } = require('@codemirror/commands');
// 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 {
// 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
// SyntaxError: "Identifier 'ModalManager' has already been declared".
if (typeof ModalManager === 'undefined') {
const result = require('./utils/ModalManager');
ModalManager = result.ModalManager || result;
}
@@ -482,38 +490,7 @@ class TabManager {
document.querySelector('.editor-container').appendChild(tabContent);
// Initialize CodeMirror editor
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,
});
}
this.ensureEditor(tab);
}
switchToTab(tabId) {
@@ -1484,9 +1461,47 @@ class TabManager {
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
setEditorContent(tabId, content) {
const tab = this.tabs.get(tabId);
if (!tab?.editorView) {
this.ensureEditor(tab);
}
if (tab?.editorView) {
tab.editorView.dispatch({
changes: { from: 0, to: tab.editorView.state.doc.length, insert: content }
@@ -1730,7 +1745,8 @@ document.addEventListener('DOMContentLoaded', async () => {
const welcomeHtml = getCreateWelcomeContent()(recentFiles, appVersion);
const tab = tabManager.tabs.get(tabManager.activeTabId);
if (tab) {
// Only show welcome if no file was opened while we were awaiting
if (tab && !tab.filePath && tab.content === '') {
tab.title = 'Welcome';
tab.content = '';
const preview = document.getElementById(`preview-${tab.id}`);
@@ -1899,27 +1915,9 @@ document.addEventListener('DOMContentLoaded', async () => {
});
// Initialize CodeMirror for the initial tab (tab 1)
const initialEditorContainer = document.getElementById('editor-cm-1');
if (initialEditorContainer) {
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,
});
const initialTab = tabManager.tabs.get(1);
if (initialTab) {
tabManager.ensureEditor(initialTab);
}
// Request current theme
@@ -2041,6 +2039,8 @@ ipcRenderer.on('redo', () => {
redo(tab.editorView);
}
}
});
// Custom Preview CSS event handlers and trigger helpers
function applyCustomPreviewCSS(cssContent) {
let styleTag = document.getElementById('custom-preview-style');