diff --git a/index.html b/index.html index e6cdd05..d71f019 100644 --- a/index.html +++ b/index.html @@ -1,28 +1,34 @@
- - - - - + + + + + - - - + + + + +$$${token.text}$$
`; + }, }; - // Reset input text - let reset = () => { - let changed = editor.getValue() != defaultInput; - if (hasEdited || changed) { - var confirmed = window.confirm(confirmationMessage); - if (!confirmed) { - return; - } - } - presetValue(defaultInput); - document.querySelectorAll('.column').forEach((element) => { - element.scrollTo({ top: 0 }); - }); - }; + marked.use({ extensions: [mathExtension] }); - let presetValue = (value) => { - editor.setValue(value); - editor.revealPosition({ lineNumber: 1, column: 1 }); - editor.focus(); - hasEdited = false; + let options = { + headerIds: false, + mangle: false, }; - // ----- sync scroll position ----- - - let initScrollBarSync = (settings) => { - let checkbox = document.querySelector('#sync-scroll-checkbox'); - checkbox.checked = settings; - scrollBarSync = settings; - - checkbox.addEventListener('change', (event) => { - let checked = event.currentTarget.checked; - scrollBarSync = checked; - saveScrollBarSettings(checked); - }); - }; - - // ----- preview CSS loader (switch github-markdown css) ----- - const PREVIEW_CSS_LIGHT = 'css/github-markdown-light.css?v=1.11.0'; - const PREVIEW_CSS_DARK = 'css/github-markdown-dark_dimmed.css?v=1.11.0'; - - let setPreviewCss = (useDark) => { - const link = document.getElementById('gh-markdown-link'); - if (!link) { - // fallback: create link element - const newLink = document.createElement('link'); - newLink.id = 'gh-markdown-link'; - newLink.rel = 'stylesheet'; - newLink.href = useDark ? PREVIEW_CSS_DARK : PREVIEW_CSS_LIGHT; - document.head.appendChild(newLink); - return; - } - - // Only update if href differs to avoid unnecessary reload - const desired = useDark ? PREVIEW_CSS_DARK : PREVIEW_CSS_LIGHT; - if (link.getAttribute('href') !== desired) { - link.setAttribute('href', desired); - } - }; - - // ----- theme toggle (dark/light) ----- - let setTheme = (enabled) => { - document.documentElement.setAttribute('data-theme', enabled ? 'dark' : 'light'); - }; - - let initThemeToggle = (settings) => { - let checkbox = document.querySelector('#theme-checkbox'); - if (!checkbox) return; - checkbox.checked = settings; - setTheme(settings); - - // set Monaco editor theme to match page theme - if (monaco && monaco.editor && typeof monaco.editor.setTheme === 'function') { - monaco.editor.setTheme(settings ? 'vs-dark' : 'vs'); - } - // set preview css to match theme - setPreviewCss(settings); - - checkbox.addEventListener('change', (event) => { - let checked = event.currentTarget.checked; - setTheme(checked); - saveThemeSettings(checked); - setPreviewCss(checked); - if (monaco && monaco.editor && typeof monaco.editor.setTheme === 'function') { - monaco.editor.setTheme(checked ? 'vs-dark' : 'vs'); - } - }); - }; - - let enableScrollBarSync = () => { - scrollBarSync = true; - }; - - let disableScrollBarSync = () => { - scrollBarSync = false; - }; - - // ----- clipboard utils ----- - - let copyToClipboard = (text, successHandler, errorHandler) => { - navigator.clipboard.writeText(text).then( - () => { - successHandler(); - }, + // Parse and Sanitize + let html = marked.parse(markdown, options); + let sanitized = DOMPurify.sanitize(html); + + const outputEl = document.querySelector("#output"); + outputEl.innerHTML = sanitized; + + // Render with KaTeX + if (typeof renderMathInElement === "function") { + renderMathInElement(outputEl, { + delimiters: [ + { left: "$$", right: "$$", display: true }, + { left: "$", right: "$", display: false }, + ], + throwOnError: false, + }); + } + }; + + // Reset input text + let reset = () => { + let changed = editor.getValue() != defaultInput; + if (hasEdited || changed) { + var confirmed = window.confirm(confirmationMessage); + if (!confirmed) { + return; + } + } + presetValue(defaultInput); + document.querySelectorAll(".column").forEach((element) => { + element.scrollTo({ top: 0 }); + }); + }; + + let presetValue = (value) => { + editor.setValue(value); + editor.revealPosition({ lineNumber: 1, column: 1 }); + editor.focus(); + hasEdited = false; + }; + + // ----- sync scroll position ----- + + let initScrollBarSync = (settings) => { + let checkbox = document.querySelector("#sync-scroll-checkbox"); + checkbox.checked = settings; + scrollBarSync = settings; + + checkbox.addEventListener("change", (event) => { + let checked = event.currentTarget.checked; + scrollBarSync = checked; + saveScrollBarSettings(checked); + }); + }; + + // ----- preview CSS loader (switch github-markdown css) ----- + const PREVIEW_CSS_LIGHT = "css/github-markdown-light.css?v=1.11.0"; + const PREVIEW_CSS_DARK = "css/github-markdown-dark_dimmed.css?v=1.11.0"; + + let setPreviewCss = (useDark) => { + const link = document.getElementById("gh-markdown-link"); + if (!link) { + // fallback: create link element + const newLink = document.createElement("link"); + newLink.id = "gh-markdown-link"; + newLink.rel = "stylesheet"; + newLink.href = useDark ? PREVIEW_CSS_DARK : PREVIEW_CSS_LIGHT; + document.head.appendChild(newLink); + return; + } - () => { - errorHandler(); - } + // Only update if href differs to avoid unnecessary reload + const desired = useDark ? PREVIEW_CSS_DARK : PREVIEW_CSS_LIGHT; + if (link.getAttribute("href") !== desired) { + link.setAttribute("href", desired); + } + }; + + // ----- theme toggle (dark/light) ----- + let setTheme = (enabled) => { + document.documentElement.setAttribute( + "data-theme", + enabled ? "dark" : "light" + ); + }; + + let initThemeToggle = (settings) => { + let checkbox = document.querySelector("#theme-checkbox"); + if (!checkbox) return; + checkbox.checked = settings; + setTheme(settings); + + // set Monaco editor theme to match page theme + if ( + monaco && + monaco.editor && + typeof monaco.editor.setTheme === "function" + ) { + monaco.editor.setTheme(settings ? "vs-dark" : "vs"); + } + // set preview css to match theme + setPreviewCss(settings); + + checkbox.addEventListener("change", (event) => { + let checked = event.currentTarget.checked; + setTheme(checked); + saveThemeSettings(checked); + setPreviewCss(checked); + if ( + monaco && + monaco.editor && + typeof monaco.editor.setTheme === "function" + ) { + monaco.editor.setTheme(checked ? "vs-dark" : "vs"); + } + }); + }; + + let enableScrollBarSync = () => { + scrollBarSync = true; + }; + + let disableScrollBarSync = () => { + scrollBarSync = false; + }; + + // ----- clipboard utils ----- + + let copyToClipboard = (text, successHandler, errorHandler) => { + navigator.clipboard.writeText(text).then( + () => { + successHandler(); + }, + + () => { + errorHandler(); + } + ); + }; + + let notifyCopied = () => { + let labelElement = document.querySelector("#copy-button a"); + labelElement.innerHTML = "Copied!"; + setTimeout(() => { + labelElement.innerHTML = "Copy"; + }, 1000); + }; + + // ----- setup ----- + + // setup navigation actions + let setupResetButton = () => { + document + .querySelector("#reset-button") + .addEventListener("click", (event) => { + event.preventDefault(); + reset(); + }); + }; + + let setupCopyButton = (editor) => { + document + .querySelector("#copy-button") + .addEventListener("click", (event) => { + event.preventDefault(); + let value = editor.getValue(); + copyToClipboard( + value, + () => { + notifyCopied(); + }, + () => { + // nothing to do + } ); - }; - - let notifyCopied = () => { - let labelElement = document.querySelector("#copy-button a"); - labelElement.innerHTML = "Copied!"; - setTimeout(() => { - labelElement.innerHTML = "Copy"; - }, 1000) - }; - - // ----- export preview ----- - - let exportLightCssPromise = null; - - let getLightMarkdownCss = () => { - if (exportLightCssPromise) { - return exportLightCssPromise; - } - - exportLightCssPromise = fetch(PREVIEW_CSS_LIGHT) - .then((response) => { - if (!response.ok) { - throw new Error(`Failed to load export CSS: ${response.status}`); - } - return response.text(); - }) - .catch((error) => { - // eslint-disable-next-line no-console - console.error('Failed to load light markdown CSS', error); - return ''; - }); - - return exportLightCssPromise; - }; - - let exportPreviewToPdf = () => { - const previewElement = document.querySelector('#preview-wrapper'); - if (!previewElement) { - return; - } - - if (typeof window.html2pdf !== 'function') { - window.alert('PDF export is not available yet. Please try again in a moment.'); - return; - } - - getLightMarkdownCss().then((lightCss) => { - const options = { - margin: 10, - filename: 'markdown-preview.pdf', - image: { type: 'jpeg', quality: 0.98 }, - html2canvas: { - scale: 2, - useCORS: true, - onclone: (clonedDoc) => { - clonedDoc.documentElement.setAttribute('data-theme', 'light'); - - const markdownLink = clonedDoc.getElementById('gh-markdown-link'); - if (markdownLink) { - markdownLink.setAttribute('href', PREVIEW_CSS_LIGHT); - } - - if (lightCss) { - const style = clonedDoc.createElement('style'); - style.id = 'export-light-css'; - style.textContent = `${lightCss} -#preview-wrapper, #output, body { - background: #fff !important; - color: #24292f !important; -}`; - clonedDoc.head.appendChild(style); - } - - const clonedPreview = clonedDoc.getElementById('preview-wrapper'); - if (clonedPreview) { - clonedPreview.style.background = '#fff'; - clonedPreview.style.color = '#24292f'; - clonedPreview.style.width = '190mm'; - clonedPreview.style.maxWidth = '190mm'; - } - - const clonedOutput = clonedDoc.getElementById('output'); - if (clonedOutput) { - clonedOutput.style.background = '#fff'; - clonedOutput.style.color = '#24292f'; - clonedOutput.style.width = '190mm'; - clonedOutput.style.maxWidth = '190mm'; - } - } - }, - jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } - }; - - window.html2pdf() - .set(options) - .from(previewElement) - .save() - .catch((error) => { - // eslint-disable-next-line no-console - console.error('Failed to export PDF', error); - }); - }); - }; - - // ----- setup ----- - - // setup navigation actions - let setupResetButton = () => { - document.querySelector("#reset-button").addEventListener('click', (event) => { - event.preventDefault(); - reset(); - }); - }; - - let setupCopyButton = (editor) => { - document.querySelector("#copy-button").addEventListener('click', (event) => { - event.preventDefault(); - let value = editor.getValue(); - copyToClipboard(value, () => { - notifyCopied(); - }, - () => { - // nothing to do - }); - }); - }; - - let setupExportButton = () => { - const exportButton = document.querySelector('#export-button'); - if (!exportButton) { - return; - } - exportButton.addEventListener('click', (event) => { - event.preventDefault(); - exportPreviewToPdf(); - }); - }; - - // ----- local state ----- - - let loadLastContent = () => { - let lastContent = Storehouse.getItem(localStorageNamespace, localStorageKey); - return lastContent; - }; - - let saveLastContent = (content) => { - let expiredAt = new Date(2099, 1, 1); - Storehouse.setItem(localStorageNamespace, localStorageKey, content, expiredAt); - }; - - let loadScrollBarSettings = () => { - let lastContent = Storehouse.getItem(localStorageNamespace, localStorageScrollBarKey); - return lastContent; - }; - - let loadThemeSettings = () => { - let last = Storehouse.getItem(localStorageNamespace, localStorageThemeKey); - if (last === null || last === undefined) { - try { - // fallback to raw localStorage boot key used by inline script - const raw = localStorage.getItem('com.markdownlivepreview_theme'); - if (raw === 'dark') return true; - if (raw === 'light') return false; - } catch (e) { - // ignore - } - } - return last; - }; - - let saveScrollBarSettings = (settings) => { - let expiredAt = new Date(2099, 1, 1); - Storehouse.setItem(localStorageNamespace, localStorageScrollBarKey, settings, expiredAt); - }; - - let saveThemeSettings = (settings) => { - let expiredAt = new Date(2099, 1, 1); - Storehouse.setItem(localStorageNamespace, localStorageThemeKey, settings, expiredAt); - try { - localStorage.setItem('com.markdownlivepreview_theme', settings ? 'dark' : 'light'); - } catch (e) { - // ignore storage errors - } - }; - - let setupDivider = () => { - let lastLeftRatio = 0.5; - const divider = document.getElementById('split-divider'); - const leftPane = document.getElementById('edit'); - const rightPane = document.getElementById('preview'); - const container = document.getElementById('container'); - - let isDragging = false; - - divider.addEventListener('mouseenter', () => { - divider.classList.add('hover'); - }); - - divider.addEventListener('mouseleave', () => { - if (!isDragging) { - divider.classList.remove('hover'); - } - }); - - divider.addEventListener('mousedown', () => { - isDragging = true; - divider.classList.add('active'); - document.body.style.cursor = 'col-resize'; - }); - - divider.addEventListener('dblclick', () => { - const containerRect = container.getBoundingClientRect(); - const totalWidth = containerRect.width; - const dividerWidth = divider.offsetWidth; - const halfWidth = (totalWidth - dividerWidth) / 2; - - leftPane.style.width = halfWidth + 'px'; - rightPane.style.width = halfWidth + 'px'; - }); - - document.addEventListener('mousemove', (e) => { - if (!isDragging) return; - document.body.style.userSelect = 'none'; - const containerRect = container.getBoundingClientRect(); - const totalWidth = containerRect.width; - const offsetX = e.clientX - containerRect.left; - const dividerWidth = divider.offsetWidth; - - // Prevent overlap or out-of-bounds - const minWidth = 100; - const maxWidth = totalWidth - minWidth - dividerWidth; - const leftWidth = Math.max(minWidth, Math.min(offsetX, maxWidth)); - leftPane.style.width = leftWidth + 'px'; - rightPane.style.width = (totalWidth - leftWidth - dividerWidth) + 'px'; - lastLeftRatio = leftWidth / (totalWidth - dividerWidth); - }); - - document.addEventListener('mouseup', () => { - if (isDragging) { - isDragging = false; - divider.classList.remove('active'); - divider.classList.remove('hover'); - document.body.style.cursor = 'default'; - document.body.style.userSelect = ''; - } - }); - - window.addEventListener('resize', () => { - const containerRect = container.getBoundingClientRect(); - const totalWidth = containerRect.width; - const dividerWidth = divider.offsetWidth; - const availableWidth = totalWidth - dividerWidth; - - const newLeft = availableWidth * lastLeftRatio; - const newRight = availableWidth * (1 - lastLeftRatio); - - leftPane.style.width = newLeft + 'px'; - rightPane.style.width = newRight + 'px'; - }); - }; - - // ----- entry point ----- - let lastContent = loadLastContent(); - let editor = setupEditor(); - if (lastContent) { - presetValue(lastContent); - } else { - presetValue(defaultInput); + }); + }; + + // ----- local state ----- + + let loadLastContent = () => { + let lastContent = Storehouse.getItem( + localStorageNamespace, + localStorageKey + ); + return lastContent; + }; + + let saveLastContent = (content) => { + let expiredAt = new Date(2099, 1, 1); + Storehouse.setItem( + localStorageNamespace, + localStorageKey, + content, + expiredAt + ); + }; + + let loadScrollBarSettings = () => { + let lastContent = Storehouse.getItem( + localStorageNamespace, + localStorageScrollBarKey + ); + return lastContent; + }; + + let loadThemeSettings = () => { + let last = Storehouse.getItem(localStorageNamespace, localStorageThemeKey); + if (last === null || last === undefined) { + try { + // fallback to raw localStorage boot key used by inline script + const raw = localStorage.getItem("com.markdownlivepreview_theme"); + if (raw === "dark") return true; + if (raw === "light") return false; + } catch (e) { + // ignore + } } - setupResetButton(); - setupCopyButton(editor); - setupExportButton(); - - let scrollBarSettings = loadScrollBarSettings() || false; - initScrollBarSync(scrollBarSettings); - - // initialize theme (dark/light) - let themeSettings = loadThemeSettings(); - // normalize to boolean (Storehouse may return string or boolean) - if (themeSettings === 'true' || themeSettings === true) { - themeSettings = true; - } else { - themeSettings = false; + return last; + }; + + let saveScrollBarSettings = (settings) => { + let expiredAt = new Date(2099, 1, 1); + Storehouse.setItem( + localStorageNamespace, + localStorageScrollBarKey, + settings, + expiredAt + ); + }; + + let saveThemeSettings = (settings) => { + let expiredAt = new Date(2099, 1, 1); + Storehouse.setItem( + localStorageNamespace, + localStorageThemeKey, + settings, + expiredAt + ); + try { + localStorage.setItem( + "com.markdownlivepreview_theme", + settings ? "dark" : "light" + ); + } catch (e) { + // ignore storage errors } - initThemeToggle(themeSettings); - - setupDivider(); + }; + + let setupDivider = () => { + let lastLeftRatio = 0.5; + const divider = document.getElementById("split-divider"); + const leftPane = document.getElementById("edit"); + const rightPane = document.getElementById("preview"); + const container = document.getElementById("container"); + + let isDragging = false; + + divider.addEventListener("mouseenter", () => { + divider.classList.add("hover"); + }); + + divider.addEventListener("mouseleave", () => { + if (!isDragging) { + divider.classList.remove("hover"); + } + }); + + divider.addEventListener("mousedown", () => { + isDragging = true; + divider.classList.add("active"); + document.body.style.cursor = "col-resize"; + }); + + divider.addEventListener("dblclick", () => { + const containerRect = container.getBoundingClientRect(); + const totalWidth = containerRect.width; + const dividerWidth = divider.offsetWidth; + const halfWidth = (totalWidth - dividerWidth) / 2; + + leftPane.style.width = halfWidth + "px"; + rightPane.style.width = halfWidth + "px"; + }); + + document.addEventListener("mousemove", (e) => { + if (!isDragging) return; + document.body.style.userSelect = "none"; + const containerRect = container.getBoundingClientRect(); + const totalWidth = containerRect.width; + const offsetX = e.clientX - containerRect.left; + const dividerWidth = divider.offsetWidth; + + // Prevent overlap or out-of-bounds + const minWidth = 100; + const maxWidth = totalWidth - minWidth - dividerWidth; + const leftWidth = Math.max(minWidth, Math.min(offsetX, maxWidth)); + leftPane.style.width = leftWidth + "px"; + rightPane.style.width = totalWidth - leftWidth - dividerWidth + "px"; + lastLeftRatio = leftWidth / (totalWidth - dividerWidth); + }); + + document.addEventListener("mouseup", () => { + if (isDragging) { + isDragging = false; + divider.classList.remove("active"); + divider.classList.remove("hover"); + document.body.style.cursor = "default"; + document.body.style.userSelect = ""; + } + }); + + window.addEventListener("resize", () => { + const containerRect = container.getBoundingClientRect(); + const totalWidth = containerRect.width; + const dividerWidth = divider.offsetWidth; + const availableWidth = totalWidth - dividerWidth; + + const newLeft = availableWidth * lastLeftRatio; + const newRight = availableWidth * (1 - lastLeftRatio); + + leftPane.style.width = newLeft + "px"; + rightPane.style.width = newRight + "px"; + }); + }; + + // ----- entry point ----- + let lastContent = loadLastContent(); + let editor = setupEditor(); + if (lastContent) { + presetValue(lastContent); + } else { + presetValue(defaultInput); + } + setupResetButton(); + setupCopyButton(editor); + + let scrollBarSettings = loadScrollBarSettings() || false; + initScrollBarSync(scrollBarSettings); + + // initialize theme (dark/light) + let themeSettings = loadThemeSettings(); + // normalize to boolean (Storehouse may return string or boolean) + if (themeSettings === "true" || themeSettings === true) { + themeSettings = true; + } else { + themeSettings = false; + } + initThemeToggle(themeSettings); + + setupDivider(); }; window.addEventListener("load", () => { - init(); + init(); });