function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } const Editor = { config: { debounceDelay: 300, mathJaxProcessing: false, localStorageKey: 'markdownEditorContent', autosaveInterval: 5000, }, state: { currentMathEngine: 'katex', currentMarkdownEngine: 'markdown-it', customCssVisible: false, lastText: '', lastRenderedHTML: '', mathJaxRunning: false, libsReady: { mathJax: false, katex: false, mermaid: false, hljs: false, markdownIt: false, marked: false }, isInitialized: false, mathPlaceholders: {}, isMobileView: false, currentMobilePane: 'editor', lastSavedTime: null, }, elements: { textarea: null, previewContent: null, previewPane: null, toolbar: null, markdownItBtn: null, markedBtn: null, mathJaxBtn: null, kaTeXBtn: null, downloadBtn: null, downloadPdfBtn: null, downloadMdBtn: null, downloadTxtBtn: null, toggleCssBtn: null, customCssContainer: null, customCssInput: null, applyCssBtn: null, closeCssBtn: null, customStyleTag: null, buffer: null, showEditorBtn: null, showPreviewBtn: null, autosaveIndicator: null, }, markdownItInstance: null, markedInstance: null, debouncedUpdate: null, autosaveTimer: null, Init: function () { this.getElements(); this.createBufferElement(); this.setupMarkdownRenderers(); this.InitializeMermaid(); this.debouncedUpdate = debounce(this.UpdatePreview.bind(this), this.config.debounceDelay); this.setupEventListeners(); this.initializeResponsiveUI(); this.setupAutosave(); this.LoadFromLocalStorage(); this.state.lastText = this.elements.textarea.value; // Immediate initial rendering (don't wait for library check) if (this.elements.textarea.value) { this.UpdatePreview(true); // Force update regardless of lastText comparison } this.CheckLibraries(); }, getElements: function () { this.elements.textarea = document.getElementById("markdown-input"); this.elements.previewContent = document.getElementById("preview-content"); this.elements.previewPane = document.getElementById("preview-pane"); this.elements.toolbar = document.querySelector(".toolbar"); this.elements.markdownItBtn = document.getElementById("btn-markdown-it"); this.elements.markedBtn = document.getElementById("btn-marked"); this.elements.mathJaxBtn = document.getElementById("btn-mathjax"); this.elements.kaTeXBtn = document.getElementById("btn-katex"); this.elements.downloadBtn = document.getElementById("btn-download"); this.elements.downloadPdfBtn = document.getElementById("btn-download-pdf"); this.elements.downloadMdBtn = document.getElementById("btn-download-md"); this.elements.downloadTxtBtn = document.getElementById("btn-download-txt"); this.elements.toggleCssBtn = document.getElementById("btn-toggle-css"); this.elements.customCssContainer = document.getElementById("custom-css-container"); this.elements.customCssInput = document.getElementById("custom-css-input"); this.elements.applyCssBtn = document.getElementById("btn-apply-css"); this.elements.closeCssBtn = document.getElementById("btn-close-css"); this.elements.customStyleTag = document.getElementById("custom-styles-output"); this.elements.showEditorBtn = document.getElementById("btn-show-editor"); this.elements.showPreviewBtn = document.getElementById("btn-show-preview"); this.elements.autosaveIndicator = document.getElementById("autosave-indicator"); if (!this.elements.textarea || !this.elements.previewContent || !this.elements.previewPane) { console.error("Critical elements not found. Aborting initialization."); alert("Error initializing editor: Required elements missing."); return false; } return true; }, createBufferElement: function () { this.elements.buffer = document.createElement('div'); this.elements.buffer.id = "mathjax-buffer"; this.elements.buffer.style.display = 'none'; document.body.appendChild(this.elements.buffer); }, setupMarkdownRenderers: function () { if (typeof markdownit !== 'function') { console.error("markdown-it library not loaded."); alert("Error initializing editor: markdown-it library failed to load."); return false; } else { this.state.libsReady.markdownIt = true; } this.markdownItInstance = window.markdownit({ html: true, linkify: true, typographer: true, highlight: (str, lang) => this.handleCodeHighlighting(str, lang) }); if (typeof markdownitFootnote === 'function') { this.markdownItInstance = this.markdownItInstance.use(markdownitFootnote); } if (typeof marked !== 'undefined') { this.state.libsReady.marked = true; marked.setOptions({ renderer: new marked.Renderer(), highlight: (code, lang) => this.handleCodeHighlighting(code, lang), pedantic: false, gfm: true, breaks: false, sanitize: false, smartLists: true, smartypants: false, xhtml: false }); this.markedInstance = marked; } return true; }, handleCodeHighlighting: function (code, lang) { if (lang && lang === 'mermaid') { return `
${this.EscapeHtml(code)}`;
}
if (lang && typeof hljs !== 'undefined' && hljs.getLanguage(lang)) {
try {
const highlightedCode = hljs.highlight(code, { language: lang, ignoreIllegals: true }).value;
return `${highlightedCode}`;
} catch (e) {
console.warn("Highlight.js error:", e);
}
}
return `${this.markdownItInstance?.utils.escapeHtml(code) || code}`;
},
// Detect lines with LaTeX even without brackets nd wrap them in $$...$$ 🤔
processLatexPaste: function(text) {
const lines = text.split('\n');
return lines.map(line => {
const trimmed = line.trim();
if (trimmed &&
!trimmed.startsWith('$$') &&
!trimmed.startsWith('\\[') &&
!trimmed.endsWith('$$') &&
!trimmed.endsWith('\\]') &&
!/\$.*\$/.test(trimmed) && // Check if line already has $ delimiters
!/\\[()\[\]]/.test(trimmed) && // Check if line already has \( \) or \[ \] delimiters
/\\[a-zA-Z]+\b/.test(trimmed)) { // Has LaTeX commands
return `$$${trimmed}$$`;
}
return line;
}).join('\n');
},
setupEventListeners: function () {
this.elements.textarea.addEventListener('input', () => {
this.SaveToLocalStorage();
this.debouncedUpdate();
});
// Replace the existing paste listener with this
this.elements.textarea.addEventListener('paste', (e) => {
const clipboardData = e.clipboardData || window.clipboardData;
const pastedText = clipboardData.getData('text/plain');
const processedText = this.processLatexPaste(pastedText);
// Prevent default paste and insert modified text
e.preventDefault();
document.execCommand('insertText', false, processedText);
// Trigger update
setTimeout(() => this.UpdatePreview(true), 0);
});
this.elements.markdownItBtn.addEventListener('click', () => this.SetMarkdownEngine('markdown-it'));
this.elements.markedBtn.addEventListener('click', () => this.SetMarkdownEngine('marked'));
this.elements.mathJaxBtn.addEventListener('click', () => this.SetMathEngine('mathjax'));
this.elements.kaTeXBtn.addEventListener('click', () => this.SetMathEngine('katex'));
this.elements.downloadPdfBtn.addEventListener('click', () => this.DownloadAs('pdf'));
this.elements.downloadMdBtn.addEventListener('click', () => this.DownloadAs('md'));
this.elements.downloadTxtBtn.addEventListener('click', () => this.DownloadAs('txt'));
this.elements.toggleCssBtn.addEventListener('click', this.ToggleCustomCSS.bind(this));
this.elements.applyCssBtn.addEventListener('click', this.ApplyCustomCSS.bind(this));
this.elements.closeCssBtn.addEventListener('click', this.ToggleCustomCSS.bind(this));
},
initializeResponsiveUI: function () {
this.CheckMobileView();
window.addEventListener('resize', this.CheckMobileView.bind(this));
if (this.elements.showEditorBtn && this.elements.showPreviewBtn) {
this.elements.showEditorBtn.addEventListener('click', () => this.SetMobilePane('editor'));
this.elements.showPreviewBtn.addEventListener('click', () => this.SetMobilePane('preview'));
}
this.ConnectMobileMenuButtons();
},
setupAutosave: function () {
this.autosaveTimer = setInterval(() => {
if (this.elements.textarea.value !== this.state.lastText) {
this.SaveToLocalStorage();
this.state.lastText = this.elements.textarea.value;
}
}, this.config.autosaveInterval);
if (this.elements.autosaveIndicator) {
this.updateAutosaveIndicator();
}
},
updateAutosaveIndicator: function () {
if (!this.elements.autosaveIndicator) return;
const now = new Date();
if (this.state.lastSavedTime) {
const secondsAgo = Math.floor((now - this.state.lastSavedTime) / 1000);
if (secondsAgo < 60) {
this.elements.autosaveIndicator.textContent = `Saved ${secondsAgo}s ago`;
} else {
const minutesAgo = Math.floor(secondsAgo / 60);
this.elements.autosaveIndicator.textContent = `Saved ${minutesAgo}m ago`;
}
} else {
this.elements.autosaveIndicator.textContent = "Auto-saved";
}
},
CheckLibraries: function () {
if (typeof MathJax !== 'undefined' && MathJax.Hub) {
this.state.libsReady.mathJax = true;
}
if (typeof katex !== 'undefined' && typeof renderMathInElement === 'function') {
this.state.libsReady.katex = true;
}
if (typeof mermaid !== 'undefined' && typeof mermaid.mermaidAPI !== 'undefined') {
this.state.libsReady.mermaid = true;
}
if (typeof hljs !== 'undefined') {
this.state.libsReady.hljs = true;
}
if (this.AllLibrariesReady() && !this.state.isInitialized) {
this.state.isInitialized = true;
this.UpdatePreview(true); // Force update to ensure preview reflects current content
} else if (!this.state.isInitialized) {
setTimeout(() => this.CheckLibraries(), 300);
}
},
AllLibrariesReady: function () {
return (this.state.libsReady.markdownIt || this.state.libsReady.marked) &&
(this.state.libsReady.mathJax || this.state.libsReady.katex);
},
InitializeMermaid: function () {
if (typeof mermaid !== 'undefined') {
try {
mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'loose',
fontFamily: 'sans-serif',
logLevel: 'fatal',
});
this.state.libsReady.mermaid = true;
} catch (e) {
console.error("Failed to initialize Mermaid:", e);
}
}
},
UpdatePreview: function (force = false) {
const text = this.elements.textarea.value;
if (!force && text === this.state.lastText && this.state.lastText !== '') return;
try {
const scrollPercent = this.elements.previewPane.scrollTop /
(this.elements.previewPane.scrollHeight - this.elements.previewPane.clientHeight);
if (this.state.currentMarkdownEngine === 'markdown-it' && this.state.libsReady.markdownIt) {
this.state.lastRenderedHTML = this.markdownItInstance.render(text);
this.elements.previewContent.innerHTML = this.state.lastRenderedHTML;
this.ProcessMath();
this.ProcessMermaid();
}
else if (this.state.currentMarkdownEngine === 'marked' && this.state.libsReady.marked) {
this.RenderWithMarked(text, scrollPercent);
return;
}
else {
// Try to use any available engine rather than showing error
if (this.state.libsReady.markdownIt) {
this.state.lastRenderedHTML = this.markdownItInstance.render(text);
this.elements.previewContent.innerHTML = this.state.lastRenderedHTML;
this.ProcessMath();
this.ProcessMermaid();
} else if (this.state.libsReady.marked) {
this.RenderWithMarked(text, scrollPercent);
return;
} else {
console.error("No valid markdown engine available");
this.elements.previewContent.innerHTML = 'Error: No valid markdown renderer available
'; return; } } this._restoreScrollPosition(scrollPercent); this.state.lastText = text; } catch (err) { console.error("Error during rendering:", err); this.elements.previewContent.innerHTML = `Error rendering preview. Check console for details.
${this.EscapeHtml(err.stack || err.message)}`;
}
},
RenderWithMarked: function (text, scrollPercent) {
if (!this.elements.buffer) {
this.createBufferElement();
}
if (this.state.currentMathEngine === 'mathjax') {
try {
if (!this.state.mathJaxRunning) {
this.state.mathJaxRunning = true;
const escapedText = this.EscapeHtml(text);
this.elements.buffer.innerHTML = escapedText;
MathJax.Hub.Queue(
["resetEquationNumbers", MathJax.InputJax.TeX],
["Typeset", MathJax.Hub, this.elements.buffer],
() => {
try {
const mathJaxProcessedHtml = this.elements.buffer.innerHTML;
const finalHtml = marked.parse(mathJaxProcessedHtml);
this.elements.previewContent.innerHTML = finalHtml;
this.ProcessMermaid();
this._restoreScrollPosition(scrollPercent);
this.state.lastText = text;
} catch (err) {
console.error("Error updating preview after MathJax:", err);
this.elements.previewContent.innerHTML = `Error updating preview with MathJax.
`; } finally { this.state.mathJaxRunning = false; } } ); } } catch (err) { console.error("Error during MathJax+marked rendering:", err); this.elements.previewContent.innerHTML = `Error rendering preview with MathJax.
`; this.state.mathJaxRunning = false; } } else { try { const html = marked.parse(text); this.elements.previewContent.innerHTML = html; if (this.state.currentMathEngine === 'katex') { this.ProcessMath(); } this.ProcessMermaid(); this._restoreScrollPosition(scrollPercent); this.state.lastText = text; } catch (err) { console.error("Error during standard marked rendering:", err); this.elements.previewContent.innerHTML = `Error rendering preview with marked.
`; } } }, _restoreScrollPosition: function (scrollPercent) { requestAnimationFrame(() => { const newScrollHeight = this.elements.previewPane.scrollHeight; const newScrollTop = scrollPercent * (newScrollHeight - this.elements.previewPane.clientHeight); if (isFinite(scrollPercent) && newScrollHeight > this.elements.previewPane.clientHeight) { this.elements.previewPane.scrollTop = newScrollTop; } else { this.elements.previewPane.scrollTop = 0; } }); }, ProcessMath: function () { if (!this.elements.previewContent) return; try { if (this.state.currentMathEngine === 'katex' && this.state.libsReady.katex) { if (typeof renderMathInElement === 'function') { renderMathInElement(this.elements.previewContent, { delimiters: [ { left: "$$", right: "$$", display: true }, { left: "\\[", right: "\\]", display: true }, { left: "$", right: "$", display: false }, { left: "\\(", right: "\\)", display: false } ], throwOnError: false }); } } else if (this.state.currentMathEngine === 'mathjax' && this.state.libsReady.mathJax) { if (typeof MathJax !== 'undefined' && MathJax.Hub) { if (this.config.mathJaxProcessing) return; this.config.mathJaxProcessing = true; MathJax.Hub.Queue( ["Typeset", MathJax.Hub, this.elements.previewContent], () => { this.config.mathJaxProcessing = false; } ); } } } catch (err) { console.error(`Error processing math:`, err); const errorDiv = document.createElement('div'); errorDiv.style.color = 'orange'; errorDiv.textContent = `Math processing error. Check console.`; this.elements.previewContent.prepend(errorDiv); } }, ProcessMermaid: function () { if (typeof mermaid === 'undefined' || !this.elements.previewContent) return; const mermaidBlocks = this.elements.previewContent.querySelectorAll('pre.mermaid'); if (mermaidBlocks.length === 0) return; try { mermaid.init(undefined, mermaidBlocks); } catch (err) { console.error("Error initializing mermaid diagrams:", err); mermaidBlocks.forEach((block, index) => { try { const container = document.createElement('div'); container.className = 'mermaid-diagram'; const code = this.UnescapeHtml(block.textContent || "").trim(); container.textContent = code; if (block.parentNode) { block.parentNode.replaceChild(container, block); mermaid.init(undefined, container); } } catch (blockErr) { console.error(`Error rendering mermaid block ${index}:`, blockErr); const errorDiv = document.createElement('div'); errorDiv.className = 'mermaid-error'; errorDiv.innerHTML = ` Mermaid Diagram ErrorThere was a problem rendering this diagram. Check your syntax.
${this.EscapeHtml(blockErr.message || String(blockErr))}
${this.EscapeHtml(block.textContent || "")}