Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'self'; object-src 'none'; script-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net 'sha256-DgMFO4QE+qqf2xNgeNb5gMKG6BtiiQFniYj21c88yME='; worker-src 'self'; connect-src 'self' https://api.github.com https://raw.githubusercontent.com https://kroki.io https://www.plantuml.com https://mermaid.ink; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; font-src 'self' data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; media-src 'self' blob: data:; manifest-src 'self'; upgrade-insecure-requests">
<!-- DNS Prefetch & Preconnect CDN Origins to Warm Up Latency -->
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
Expand Down Expand Up @@ -36,26 +37,29 @@
<link rel="manifest" href="manifest.json">

<!-- Primary Meta Tags -->
<meta name="title" content="Markdown Viewer">
<meta name="title" content="Markdown Viewer - Secure Online Markdown Editor and Previewer">
<meta name="description" content="Markdown Viewer is a powerful GitHub-style Markdown rendering tool with live preview, LaTeX math, Mermaid diagrams, syntax highlighting, dark mode, and export options to PDF, HTML, and MD—all fully client-side and secure.">
<meta name="keywords" content="Markdown Viewer, GitHub-style markdown, live preview, markdown editor, LaTeX support, Mermaid diagrams, PDF export, syntax highlighting, markdown to HTML, secure markdown tool, client-side markdown viewer, ThisIs-Developer, advanced markdown parser, future markdown viewer, next-gen markdown tool">
<meta name="author" content="ThisIs-Developer">
<meta name="robots" content="index, follow">
<meta name="robots" content="index, follow, max-image-preview:large">
<meta name="language" content="English">
<meta name="distribution" content="global">
<meta name="rating" content="general">
<meta name="application-name" content="Markdown Viewer">
<meta name="theme-color" content="#0d1117">

<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://markdownviewer.pages.dev/">
<meta property="og:title" content="Markdown Viewer">
<meta property="og:title" content="Markdown Viewer - Secure Online Markdown Editor and Previewer">
<meta property="og:description" content="Markdown Viewer is a powerful GitHub-style Markdown rendering tool with live preview, LaTeX math, Mermaid diagrams, syntax highlighting, dark mode, and export options to PDF, HTML, and MD—all fully client-side and secure.">
<meta property="og:image" content="https://markdownviewer.pages.dev/assets/icon.jpg">
<meta property="og:site_name" content="Markdown Viewer">

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://markdownviewer.pages.dev/">
<meta property="twitter:title" content="Markdown Viewer">
<meta property="twitter:title" content="Markdown Viewer - Secure Online Markdown Editor and Previewer">
<meta property="twitter:description" content="Markdown Viewer is a powerful GitHub-style Markdown rendering tool with live preview, LaTeX math, Mermaid diagrams, syntax highlighting, dark mode, and export options to PDF, HTML, and MD—all fully client-side and secure.">
<meta property="twitter:image" content="https://markdownviewer.pages.dev/assets/icon.jpg">

Expand All @@ -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",
Expand All @@ -84,13 +96,12 @@
}
</script>

<title>Markdown Viewer</title>
<title>Markdown Viewer - Secure Online Markdown Editor and Previewer</title>
<link href="assets/icon.jpg" rel="icon" type="image/jpg">
<!-- Updated libraries to latest versions with Subresource Integrity (SRI) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown.min.css" integrity="sha384-hZuxRjC/Dsr4zEx1JlUhDQqkvqBPp2VLHsgXfnxPq1ULDy1eIdWCiux7nvO1RIZP" crossorigin="anonymous">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" as="style" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous"></noscript>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous">
<link rel="stylesheet" href="styles.css">

<!-- Loading order optimized - ensure libraries are loaded asynchronously using defer -->
Expand Down Expand Up @@ -1041,7 +1052,7 @@ <h3 class="modal-section-title">Open-source credits</h3>
</div>
</div>

<script type="text/markdown" id="default-markdown">---
<textarea id="default-markdown" hidden aria-hidden="true">---
title: Welcome to Markdown Viewer
description: A GitHub-style Markdown renderer with live preview, math, diagrams, and export support.
author: ThisIs-Developer
Expand Down Expand Up @@ -1208,10 +1219,10 @@ <h3 class="modal-section-title">Open-source credits</h3>
## 🛡️ Security Note

This is a fully client-side application. Your content never leaves your browser and stays secure on your device.
</script>
</textarea>

<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
<script src="script.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous" defer></script>
<script src="script.js" defer></script>
<!-- Screen reader dynamic accessibility announcer -->
<div id="app-accessibility-announcer" class="visually-hidden" aria-live="polite" aria-atomic="true"></div>
</body>
Expand Down
3 changes: 3 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
211 changes: 160 additions & 51 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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)~(?!~)/;
Expand Down Expand Up @@ -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 = '';
Expand Down Expand Up @@ -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,
});
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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();
Expand Down
Loading