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 @@ Open-source credits
-
+
-
-
+
+
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..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,9 @@ 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;
const markdownEditor = document.getElementById("markdown-editor");
const markdownPreview = document.getElementById("markdown-preview");
@@ -466,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)~(?!~)/;
@@ -1736,6 +1763,92 @@ 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 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 {
+ 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 +2789,8 @@ 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,
});
@@ -2729,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');
@@ -3566,52 +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 {
- 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);
- });
- } catch (e) {
- console.warn("MathJax rendering failed:", e);
- }
- } else {
- window.MathJax = {
- loader: { load: ['[tex]/ams', '[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 {
- 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);
- });
- } catch (e) {
- console.warn('MathJax rendering failed:', e);
- }
- }).catch(function(e) { console.warn('Failed to load MathJax:', e); });
- }
}
updateDocumentStats();