From 12cceab14bb0766430485b644b06fc1567384b10 Mon Sep 17 00:00:00 2001 From: Baivab Sarkar Date: Thu, 25 Jun 2026 17:02:33 +0530 Subject: [PATCH 1/2] Optimize SEO performance and security hardening --- index.html | 33 +++++++++++++++++--------- manifest.json | 3 +++ script.js | 66 ++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 74 insertions(+), 28 deletions(-) diff --git a/index.html b/index.html index 77c49ec6..89be5ab2 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,7 @@ + @@ -36,26 +37,29 @@ - + - + + + - + + - + @@ -71,6 +75,14 @@ "applicationCategory": "DeveloperApplication", "operatingSystem": "All", "browserRequirements": "Requires HTML5 compatible browser", + "softwareVersion": "3.8.0", + "featureList": [ + "Client-side Markdown editing", + "GitHub-style live preview", + "LaTeX math rendering", + "Mermaid diagram rendering", + "PDF, HTML, PNG, and Markdown export" + ], "author": { "@type": "Organization", "name": "ThisIs-Developer", @@ -84,13 +96,12 @@ } - Markdown Viewer + Markdown Viewer - Secure Online Markdown Editor and Previewer - - + @@ -1041,7 +1052,7 @@ - + - - + +
diff --git a/manifest.json b/manifest.json index 9428f981..a77cbe0f 100644 --- a/manifest.json +++ b/manifest.json @@ -2,11 +2,14 @@ "name": "Markdown Viewer", "short_name": "Markdown Viewer", "description": "A premium client-side GitHub-style Markdown editor and live preview tool.", + "id": "/", "start_url": "./index.html?utm_source=pwa", + "scope": "./", "display": "standalone", "background_color": "#0d1117", "theme_color": "#0d1117", "orientation": "any", + "categories": ["developer", "productivity", "utilities"], "icons": [ { "src": "assets/icon.jpg", diff --git a/script.js b/script.js index 6ca6546b..50ed507f 100644 --- a/script.js +++ b/script.js @@ -193,6 +193,8 @@ document.addEventListener("DOMContentLoaded", async function () { const previewWorkerRequests = new Map(); const previewSegmentHtmlCache = new Map(); let previewSegmentCacheTabId = null; + let mathJaxTypesetPromise = Promise.resolve(); + let mathJaxTypesetRunId = 0; const markdownEditor = document.getElementById("markdown-editor"); const markdownPreview = document.getElementById("markdown-preview"); @@ -1736,6 +1738,49 @@ document.addEventListener("DOMContentLoaded", async function () { } } + function typesetMathJaxTargets(mathTargets, context) { + const runId = ++mathJaxTypesetRunId; + const mathJaxReady = MathJax.startup && MathJax.startup.promise + ? MathJax.startup.promise + : Promise.resolve(); + + mathJaxTypesetPromise = mathJaxTypesetPromise.catch(function() { + // Continue with the newest render after a previous MathJax run fails. + }).then(function() { + return mathJaxReady; + }).then(function() { + if (runId !== mathJaxTypesetRunId) return null; + if (context.renderId !== previewRenderGeneration) return; + if (typeof MathJax.typesetPromise !== 'function') { + throw new Error('MathJax typesetPromise API is unavailable.'); + } + const connectedTargets = mathTargets.filter(function(target) { + return target && target.isConnected; + }); + if (connectedTargets.length === 0) return null; + return MathJax.typesetPromise(connectedTargets); + }).then(function() { + if (runId !== mathJaxTypesetRunId) return; + if (context.renderId !== previewRenderGeneration) return; + queryPreviewRoots(mathTargets, 'mjx-container[tabindex="0"]').forEach(function(mjx) { + mjx.removeAttribute('tabindex'); + }); + }).catch(function(err) { + console.warn('MathJax typesetting failed:', err); + }); + + return mathJaxTypesetPromise; + } + + function clearMathJaxPreviewState(container) { + if (!window.MathJax || typeof MathJax.typesetClear !== 'function' || !container) return; + try { + MathJax.typesetClear([container]); + } catch (error) { + console.warn('MathJax cleanup failed:', error); + } + } + // PERF-012: Inlined default template to eliminate network request, FOUC, and layout shifts const defaultMarkdownTemplate = document.getElementById('default-markdown'); let templateText = ''; @@ -2676,6 +2721,7 @@ document.addEventListener("DOMContentLoaded", async function () { const shouldRestoreScroll = previewHasCommittedRender && !previewContainsSkeleton(); const scrollSnapshot = shouldRestoreScroll ? capturePreviewScroll() : null; + clearMathJaxPreviewState(markdownPreview); const patchResult = patchPreviewDom(markdownPreview, sanitizedHtml, { reusePreviewBlocks: context.previewEngineMode === 'segmented' && !context.force, }); @@ -3572,20 +3618,13 @@ document.addEventListener("DOMContentLoaded", async function () { const mathTargets = typesetTargets.length ? typesetTargets : roots; if (window.MathJax) { try { - MathJax.typesetPromise(mathTargets).then(function() { - if (context.renderId !== previewRenderGeneration) return; - queryPreviewRoots(mathTargets, 'mjx-container[tabindex="0"]').forEach(function(mjx) { - mjx.removeAttribute('tabindex'); - }); - }).catch(function(err) { - console.warn('MathJax typesetting failed:', err); - }); + typesetMathJaxTargets(mathTargets, context); } catch (e) { console.warn("MathJax rendering failed:", e); } } else { window.MathJax = { - loader: { load: ['[tex]/ams', '[tex]/boldsymbol'] }, + loader: { load: ['[tex]/boldsymbol'] }, options: { a11y: { inTabOrder: false } }, @@ -3599,14 +3638,7 @@ document.addEventListener("DOMContentLoaded", async function () { loadScript(CDN.mathjax).then(function() { if (context.renderId !== previewRenderGeneration) return; try { - MathJax.typesetPromise(mathTargets).then(function() { - if (context.renderId !== previewRenderGeneration) return; - queryPreviewRoots(mathTargets, 'mjx-container[tabindex="0"]').forEach(function(mjx) { - mjx.removeAttribute('tabindex'); - }); - }).catch(function(err) { - console.warn('MathJax typesetting failed:', err); - }); + typesetMathJaxTargets(mathTargets, context); } catch (e) { console.warn('MathJax rendering failed:', e); } From fd3b18d6a82946fc1ec7ecddadb803b5a16eba7d Mon Sep 17 00:00:00 2001 From: Baivab Sarkar Date: Thu, 25 Jun 2026 17:29:01 +0530 Subject: [PATCH 2/2] Fix duplicate inline MathJax render on paste --- script.js | 151 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 114 insertions(+), 37 deletions(-) diff --git a/script.js b/script.js index 50ed507f..5f01d2f1 100644 --- a/script.js +++ b/script.js @@ -40,28 +40,50 @@ document.addEventListener("DOMContentLoaded", async function () { // PERF-002: Lazy script loader for optional heavy libraries const _loadedScripts = new Set(); + const _loadingScripts = new Map(); function loadScript(url) { if (_loadedScripts.has(url)) return Promise.resolve(); - return new Promise(function(resolve, reject) { + if (_loadingScripts.has(url)) return _loadingScripts.get(url); + const loadPromise = new Promise(function(resolve, reject) { const script = document.createElement('script'); script.src = url; - script.onload = function() { _loadedScripts.add(url); resolve(); }; - script.onerror = function() { reject(new Error('Failed to load: ' + url)); }; + script.onload = function() { + _loadedScripts.add(url); + _loadingScripts.delete(url); + resolve(); + }; + script.onerror = function() { + _loadingScripts.delete(url); + reject(new Error('Failed to load: ' + url)); + }; document.head.appendChild(script); }); + _loadingScripts.set(url, loadPromise); + return loadPromise; } const _loadedStyles = new Set(); + const _loadingStyles = new Map(); function loadStyle(url) { if (_loadedStyles.has(url)) return Promise.resolve(); - return new Promise(function(resolve, reject) { + if (_loadingStyles.has(url)) return _loadingStyles.get(url); + const loadPromise = new Promise(function(resolve, reject) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = url; - link.onload = function() { _loadedStyles.add(url); resolve(); }; - link.onerror = function() { reject(new Error('Failed to load style: ' + url)); }; + link.onload = function() { + _loadedStyles.add(url); + _loadingStyles.delete(url); + resolve(); + }; + link.onerror = function() { + _loadingStyles.delete(url); + reject(new Error('Failed to load style: ' + url)); + }; document.head.appendChild(link); }); + _loadingStyles.set(url, loadPromise); + return loadPromise; } // CDN URLs for lazy-loaded libraries @@ -193,6 +215,7 @@ document.addEventListener("DOMContentLoaded", async function () { const previewWorkerRequests = new Map(); const previewSegmentHtmlCache = new Map(); let previewSegmentCacheTabId = null; + let mathJaxLoadPromise = null; let mathJaxTypesetPromise = Promise.resolve(); let mathJaxTypesetRunId = 0; @@ -468,6 +491,8 @@ document.addEventListener("DOMContentLoaded", async function () { const renderer = new marked.Renderer(); const BLOCK_MATH_MARKER_PATTERN = /^\$\$/m; const BLOCK_MATH_PATTERN = /^\$\$[ \t]*\n?([\s\S]*?)\n?\$\$[ \t]*(?:\n|$)/; + const RAW_MATH_TEXT_PATTERN = /\$\$|\$[^$]|\\\(|\\\[/; + const MATHJAX_TEXT_TARGET_SELECTOR = 'p, li, td, th, dd, dt, blockquote, figcaption, h1, h2, h3, h4, h5, h6, .math-block'; const DEFINITION_LIST_ITEM_PATTERN = /^:[ \t]+(.*)$/; const SUPERSCRIPT_PATTERN = /^\^(?!\s)([^^\n]*?\S)\^(?!\^)/; const SUBSCRIPT_PATTERN = /^~(?!~)(?!\s)([^~\n]*?\S)~(?!~)/; @@ -1772,6 +1797,49 @@ document.addEventListener("DOMContentLoaded", async function () { return mathJaxTypesetPromise; } + function configureMathJax() { + if (window.MathJax && typeof MathJax.typesetPromise === 'function') return; + if (window.MathJax && MathJax.loader && MathJax.tex) return; + window.MathJax = { + loader: { load: ['[tex]/boldsymbol'] }, + startup: { + typeset: false + }, + options: { + a11y: { inTabOrder: false } + }, + tex: { + inlineMath: [['$', '$'], ['\\(', '\\)']], + displayMath: [['$$', '$$'], ['\\[', '\\]']], + processEscapes: true, + packages: { '[+]': ['ams', 'boldsymbol'] } + } + }; + } + + function ensureMathJaxReady() { + if (window.MathJax && typeof MathJax.typesetPromise === 'function') { + return MathJax.startup && MathJax.startup.promise + ? MathJax.startup.promise + : Promise.resolve(); + } + + configureMathJax(); + if (!mathJaxLoadPromise) { + mathJaxLoadPromise = loadScript(CDN.mathjax) + .then(function() { + return MathJax.startup && MathJax.startup.promise + ? MathJax.startup.promise + : Promise.resolve(); + }) + .catch(function(error) { + mathJaxLoadPromise = null; + throw error; + }); + } + return mathJaxLoadPromise; + } + function clearMathJaxPreviewState(container) { if (!window.MathJax || typeof MathJax.typesetClear !== 'function' || !container) return; try { @@ -2721,6 +2789,7 @@ document.addEventListener("DOMContentLoaded", async function () { const shouldRestoreScroll = previewHasCommittedRender && !previewContainsSkeleton(); const scrollSnapshot = shouldRestoreScroll ? capturePreviewScroll() : null; + mathJaxTypesetRunId += 1; clearMathJaxPreviewState(markdownPreview); const patchResult = patchPreviewDom(markdownPreview, sanitizedHtml, { reusePreviewBlocks: context.previewEngineMode === 'segmented' && !context.force, @@ -2775,6 +2844,38 @@ document.addEventListener("DOMContentLoaded", async function () { return matches; } + function hasRawMathText(node) { + return Boolean(node && RAW_MATH_TEXT_PATTERN.test(node.textContent || '')); + } + + function addMathJaxTarget(targets, candidate) { + if (!candidate || !hasRawMathText(candidate)) return; + if (targets.some(function(target) { return target.contains(candidate); })) return; + for (let index = targets.length - 1; index >= 0; index -= 1) { + if (candidate.contains(targets[index])) { + targets.splice(index, 1); + } + } + targets.push(candidate); + } + + function getMathJaxTypesetTargets(roots) { + const targets = []; + roots.forEach(function(root) { + if (!root || root.nodeType !== Node.ELEMENT_NODE) return; + if (root.matches && root.matches(MATHJAX_TEXT_TARGET_SELECTOR)) { + addMathJaxTarget(targets, root); + } + root.querySelectorAll(MATHJAX_TEXT_TARGET_SELECTOR).forEach(function(candidate) { + addMathJaxTarget(targets, candidate); + }); + if (targets.length === 0 && hasRawMathText(root)) { + addMathJaxTarget(targets, root); + } + }); + return targets; + } + function markPreviewRootsReady(roots) { queryPreviewRoots(roots, '.mermaid-container.is-loading').forEach(function(container) { setDiagramRenderState(container, 'ready'); @@ -3612,38 +3713,14 @@ document.addEventListener("DOMContentLoaded", async function () { const hasMath = /\$\$|\$[^$]|\\\(|\\\[/.test(rawVal || '') || /```math\b/.test(rawVal || ''); if (hasMath) { - const typesetTargets = roots.filter(function(root) { - return root && root.nodeType === Node.ELEMENT_NODE && /\$\$|\$[^$]|\\\(|\\\[/.test(root.textContent || ''); + const mathTargets = getMathJaxTypesetTargets(roots); + if (mathTargets.length === 0) return; + ensureMathJaxReady().then(function() { + if (context.renderId !== previewRenderGeneration) return; + typesetMathJaxTargets(mathTargets, context); + }).catch(function(e) { + console.warn('Failed to load MathJax:', e); }); - const mathTargets = typesetTargets.length ? typesetTargets : roots; - if (window.MathJax) { - try { - typesetMathJaxTargets(mathTargets, context); - } catch (e) { - console.warn("MathJax rendering failed:", e); - } - } else { - window.MathJax = { - loader: { load: ['[tex]/boldsymbol'] }, - options: { - a11y: { inTabOrder: false } - }, - tex: { - inlineMath: [['$', '$'], ['\\(', '\\)']], - displayMath: [['$$', '$$'], ['\\[', '\\]']], - processEscapes: true, - packages: { '[+]': ['ams', 'boldsymbol'] } - } - }; - loadScript(CDN.mathjax).then(function() { - if (context.renderId !== previewRenderGeneration) return; - try { - typesetMathJaxTargets(mathTargets, context); - } catch (e) { - console.warn('MathJax rendering failed:', e); - } - }).catch(function(e) { console.warn('Failed to load MathJax:', e); }); - } } updateDocumentStats();