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
89 changes: 79 additions & 10 deletions desktop-app/resources/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,89 @@

<title>Markdown Viewer</title>
<link href="/assets/icon.jpg" rel="icon" type="image/jpg">
<!-- Updated libraries to latest versions with Subresource Integrity (SRI) -->
<link rel="stylesheet" href="/libs/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<link rel="stylesheet" href="/libs/github-markdown.min.css" integrity="sha384-hZuxRjC/Dsr4zEx1JlUhDQqkvqBPp2VLHsgXfnxPq1ULDy1eIdWCiux7nvO1RIZP" crossorigin="anonymous">

<!-- Preload core JS libraries for faster discoverability -->
<link rel="preload" href="/libs/marked.min.js" as="script" integrity="sha384-odPBjvtXVM/5hOYIr3A1dB+flh0c3wAT3bSesIOqEGmyUA4JoKf/YTWy0XKOYAY7" crossorigin="anonymous">
<link rel="preload" href="/libs/purify.min.js" as="script" integrity="sha384-3HPB1XT51W3gGRxAmZ+qbZwRpRlFQL632y8x+adAqCr4Wp3TaWwCLSTAJJKbyWEK" crossorigin="anonymous">
<link rel="preload" href="/libs/highlight.min.js" as="script" integrity="sha384-F/bZzf7p3Joyp5psL90p/p89AZJsndkSoGwRpXcZhleCWhd8SnRuoYo4d0yirjJp" crossorigin="anonymous">

<script>
(function() {
const savedTheme = localStorage.getItem("markdown-viewer-theme") ||
(window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
document.documentElement.setAttribute("data-theme", savedTheme);
})();
</script>
<style id="critical-css">
:root {
--bg-color: #ffffff;
--editor-bg: #f6f8fa;
--preview-bg: #ffffff;
--text-color: #24292e;
--border-color: #e1e4e8;
--header-bg: #f6f8fa;
--skeleton-bg: #e2e8f0;
--skeleton-glow: rgba(255, 255, 255, 0.65);
}
[data-theme="dark"] {
--bg-color: #0d1117;
--editor-bg: #161b22;
--preview-bg: #0d1117;
--text-color: #c9d1d9;
--border-color: #30363d;
--header-bg: #161b22;
--skeleton-bg: #2d3139;
--skeleton-glow: rgba(255, 255, 255, 0.08);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background-color: var(--bg-color); color: var(--text-color); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; height: 100vh; overflow: hidden; }
.app-container { height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
.app-header { background-color: var(--header-bg); border-bottom: 1px solid var(--border-color); padding: 0.35rem 0.75rem; height: 45px; display: flex; align-items: center; z-index: 100; flex-shrink: 0; }
.content-container { display: flex; flex: 1; overflow: hidden; }
.editor-pane, .preview-pane { flex: 1; padding: 20px; overflow-y: auto; position: relative; }
.editor-pane { background-color: var(--editor-bg); border-right: 1px solid var(--border-color); }
.preview-pane { background-color: var(--preview-bg); }
.resize-divider { width: 8px; cursor: col-resize; background-color: var(--border-color); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
#markdown-editor { width: 100%; height: 100%; border: none; background: transparent; resize: none; outline: none; display: none; }
.editor-skeleton, .skeleton-preview-container { display: flex; flex-direction: column; gap: 12px; width: 100%; }
.skeleton-placeholder { background-color: var(--skeleton-bg); position: relative; overflow: hidden; border-radius: 4px; }
.skeleton-placeholder::after { position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient(90deg, transparent, var(--skeleton-glow), transparent); animation: skeleton-shimmer 1.6s infinite; content: ''; }
.skeleton-title { height: 28px; width: 40%; margin-bottom: 8px; }
.skeleton-subtitle { height: 20px; width: 25%; margin-top: 12px; margin-bottom: 4px; }
.skeleton-line { height: 16px; }
.skeleton-w90 { width: 90%; }
.skeleton-w92 { width: 92%; }
.skeleton-w88 { width: 88%; }
.skeleton-w85 { width: 85%; }
.skeleton-w60 { width: 60%; }
.skeleton-w45 { width: 45%; }
@keyframes skeleton-shimmer { 100% { transform: translateX(100%); } }
@media (prefers-color-scheme: dark) {
body:not([data-theme="light"]) {
background-color: #0d1117;
color: #c9d1d9;
}
}
</style>

<!-- Async CSS Loading (Preload + rel transition on load) -->
<link rel="preload" href="/libs/bootstrap.min.css" as="style" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" href="/libs/github-markdown.min.css" as="style" integrity="sha384-hZuxRjC/Dsr4zEx1JlUhDQqkvqBPp2VLHsgXfnxPq1ULDy1eIdWCiux7nvO1RIZP" crossorigin="anonymous" onload="this.onload=null;this.rel='stylesheet'">
<link rel="preload" href="/libs/bootstrap-icons.min.css" as="style" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/libs/bootstrap-icons.min.css" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous"></noscript>
<link rel="stylesheet" href="/styles.css">

<!-- Loading order optimized - ensure libraries are loaded asynchronously using defer -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

<!-- Noscript fallbacks for crawlers/users without javascript -->
<noscript>
<link rel="stylesheet" href="/libs/bootstrap.min.css" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<link rel="stylesheet" href="/libs/github-markdown.min.css" integrity="sha384-hZuxRjC/Dsr4zEx1JlUhDQqkvqBPp2VLHsgXfnxPq1ULDy1eIdWCiux7nvO1RIZP" crossorigin="anonymous">
<link rel="stylesheet" href="/libs/bootstrap-icons.min.css" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous">
<link rel="stylesheet" href="/styles.css">
</noscript>

<!-- Essential parsing/sanitization scripts loaded asynchronously via defer -->
<script src="/libs/marked.min.js" integrity="sha384-odPBjvtXVM/5hOYIr3A1dB+flh0c3wAT3bSesIOqEGmyUA4JoKf/YTWy0XKOYAY7" crossorigin="anonymous" defer></script>
<script src="/libs/highlight.min.js" integrity="sha384-F/bZzf7p3Joyp5psL90p/p89AZJsndkSoGwRpXcZhleCWhd8SnRuoYo4d0yirjJp" crossorigin="anonymous" defer></script>
<script src="/libs/purify.min.js" integrity="sha384-3HPB1XT51W3gGRxAmZ+qbZwRpRlFQL632y8x+adAqCr4Wp3TaWwCLSTAJJKbyWEK" crossorigin="anonymous" defer></script>
<script src="/libs/FileSaver.min.js" integrity="sha384-PlRSzpewlarQuj5alIadXwjNUX+2eNMKwr0f07ShWYLy8B6TjEbm7ZlcN/ScSbwy" crossorigin="anonymous" defer></script>
<!-- PERF-002: MathJax, Mermaid, JoyPixels, jsPDF, html2canvas, pako are now lazy-loaded by script.js on first use -->
<script src="/libs/js-yaml.min.js" integrity="sha384-+pxiN6T7yvpryuJmE1gM9PX7yQit15auDb+ZwwvJOd/4be2Cie5/IuVXgQb/S9du" crossorigin="anonymous" defer></script>
</head>
<body>
<div class="app-container">
Expand Down
130 changes: 109 additions & 21 deletions desktop-app/resources/js/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ document.addEventListener("DOMContentLoaded", function () {
html2canvas: 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js',
pako: 'https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js',
joypixels: 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/lib/js/joypixels.min.js',
joypixels_css: 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/extras/css/joypixels.min.css'
joypixels_css: 'https://cdn.jsdelivr.net/npm/emoji-toolkit@9.0.1/extras/css/joypixels.min.css',
filesaver: 'https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js',
jsyaml: 'https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js'
};

let markdownRenderTimeout = null;
Expand All @@ -43,8 +45,24 @@ document.addEventListener("DOMContentLoaded", function () {
let syncScrollingEnabled = true;
let isEditorScrolling = false;
let isPreviewScrolling = false;
let scrollSyncTimeout = null;
const SCROLL_SYNC_DELAY = 10;
// Performance caching variables to prevent forced reflows / layout thrashing
let cachedContainerLeft = 0;
let cachedContainerWidth = 0;
let cachedEditorPaneScrollHeight = 0;
let cachedEditorPaneClientHeight = 0;
let cachedPreviewPaneScrollHeight = 0;
let cachedPreviewPaneClientHeight = 0;

function updateCachedPaneHeights() {
if (editorPane) {
cachedEditorPaneScrollHeight = editorPane.scrollHeight;
cachedEditorPaneClientHeight = editorPane.clientHeight;
}
if (previewPane) {
cachedPreviewPaneScrollHeight = previewPane.scrollHeight;
cachedPreviewPaneClientHeight = previewPane.clientHeight;
}
}

// View Mode State - Story 1.1
let currentViewMode = 'split'; // 'editor', 'split', or 'preview'
Expand Down Expand Up @@ -814,9 +832,26 @@ document.addEventListener("DOMContentLoaded", function () {
});
}

let isJsYamlLoading = false;
function parseFrontmatter(markdown) {
const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
if (!match) return { frontmatter: null, body: markdown };

if (typeof jsyaml === 'undefined') {
if (!isJsYamlLoading) {
isJsYamlLoading = true;
loadScript(CDN.jsyaml).then(function() {
isJsYamlLoading = false;
_lastRenderedContent = null;
renderMarkdown();
}).catch(function(e) {
isJsYamlLoading = false;
console.warn('Failed to load js-yaml:', e);
});
}
return { frontmatter: null, body: markdown.slice(match[0].length) };
}

try {
const data = jsyaml.load(match[1]) || {};
return { frontmatter: data, body: markdown.slice(match[0].length) };
Expand Down Expand Up @@ -1567,13 +1602,15 @@ document.addEventListener("DOMContentLoaded", function () {
container.classList.remove('is-loading');
});
addMermaidToolbars();
updateCachedPaneHeights();
})
.catch((e) => {
console.warn("Mermaid rendering failed:", e);
markdownPreview.querySelectorAll('.mermaid-container.is-loading').forEach((container) => {
container.classList.remove('is-loading');
});
addMermaidToolbars();
updateCachedPaneHeights();
});
};
if (typeof mermaid === 'undefined') {
Expand All @@ -1599,6 +1636,7 @@ document.addEventListener("DOMContentLoaded", function () {
markdownPreview.querySelectorAll('mjx-container[tabindex="0"]').forEach(function(mjx) {
mjx.removeAttribute('tabindex');
});
updateCachedPaneHeights();
}).catch(function(err) {
console.warn('MathJax typesetting failed:', err);
});
Expand All @@ -1625,6 +1663,7 @@ document.addEventListener("DOMContentLoaded", function () {
markdownPreview.querySelectorAll('mjx-container[tabindex="0"]').forEach(function(mjx) {
mjx.removeAttribute('tabindex');
});
updateCachedPaneHeights();
}).catch(function(err) {
console.warn('MathJax typesetting failed:', err);
});
Expand All @@ -1639,6 +1678,7 @@ document.addEventListener("DOMContentLoaded", function () {
updateFindHighlights();
cleanupImageObjectUrls();
scheduleLineNumberUpdate();
updateCachedPaneHeights();
} catch (e) {
console.error("Markdown rendering failed:", e);
const safeMessage = escapeHtml(e && e.message ? e.message : 'Unknown error');
Expand Down Expand Up @@ -2251,11 +2291,14 @@ document.addEventListener("DOMContentLoaded", function () {

if (scrollSyncTimeout) cancelAnimationFrame(scrollSyncTimeout);
scrollSyncTimeout = requestAnimationFrame(function() {
if (cachedEditorPaneScrollHeight === 0) {
updateCachedPaneHeights();
}
const editorScrollRatio =
editorPane.scrollTop /
(editorPane.scrollHeight - editorPane.clientHeight);
(cachedEditorPaneScrollHeight - cachedEditorPaneClientHeight);
const previewScrollPosition =
(previewPane.scrollHeight - previewPane.clientHeight) *
(cachedPreviewPaneScrollHeight - cachedPreviewPaneClientHeight) *
editorScrollRatio;

if (!isNaN(previewScrollPosition) && isFinite(previewScrollPosition)) {
Expand All @@ -2274,11 +2317,14 @@ document.addEventListener("DOMContentLoaded", function () {

if (scrollSyncTimeout) cancelAnimationFrame(scrollSyncTimeout);
scrollSyncTimeout = requestAnimationFrame(function() {
if (cachedPreviewPaneScrollHeight === 0) {
updateCachedPaneHeights();
}
const previewScrollRatio =
previewPane.scrollTop /
(previewPane.scrollHeight - previewPane.clientHeight);
(cachedPreviewPaneScrollHeight - cachedPreviewPaneClientHeight);
const editorScrollPosition =
(editorPane.scrollHeight - editorPane.clientHeight) *
(cachedEditorPaneScrollHeight - cachedEditorPaneClientHeight) *
previewScrollRatio;

if (!isNaN(editorScrollPosition) && isFinite(editorScrollPosition)) {
Expand Down Expand Up @@ -4915,6 +4961,13 @@ document.addEventListener("DOMContentLoaded", function () {
isResizing = true;
resizeDivider.classList.add('dragging');
document.body.classList.add('resizing');

// Cache container coordinates on start to avoid getBoundingClientRect layout calls during drag
if (contentContainer) {
const containerRect = contentContainer.getBoundingClientRect();
cachedContainerLeft = containerRect.left;
cachedContainerWidth = containerRect.width;
}
}

function startResizeTouch(e) {
Expand All @@ -4924,17 +4977,22 @@ document.addEventListener("DOMContentLoaded", function () {
isResizing = true;
resizeDivider.classList.add('dragging');
document.body.classList.add('resizing');

// Cache container coordinates on start to avoid getBoundingClientRect layout calls during drag
if (contentContainer) {
const containerRect = contentContainer.getBoundingClientRect();
cachedContainerLeft = containerRect.left;
cachedContainerWidth = containerRect.width;
}
}

function handleResize(e) {
if (!isResizing) return;

const containerRect = contentContainer.getBoundingClientRect();
const containerWidth = containerRect.width;
const mouseX = e.clientX - containerRect.left;
const mouseX = e.clientX - cachedContainerLeft;

// Calculate percentage
let newEditorPercent = (mouseX / containerWidth) * 100;
// Calculate percentage using cached container width
let newEditorPercent = (mouseX / cachedContainerWidth) * 100;

// Enforce minimum pane widths
newEditorPercent = Math.max(MIN_PANE_PERCENT, Math.min(100 - MIN_PANE_PERCENT, newEditorPercent));
Expand All @@ -4946,11 +5004,9 @@ document.addEventListener("DOMContentLoaded", function () {
function handleResizeTouch(e) {
if (!isResizing || !e.touches[0]) return;

const containerRect = contentContainer.getBoundingClientRect();
const containerWidth = containerRect.width;
const touchX = e.touches[0].clientX - containerRect.left;
const touchX = e.touches[0].clientX - cachedContainerLeft;

let newEditorPercent = (touchX / containerWidth) * 100;
let newEditorPercent = (touchX / cachedContainerWidth) * 100;
newEditorPercent = Math.max(MIN_PANE_PERCENT, Math.min(100 - MIN_PANE_PERCENT, newEditorPercent));

editorWidthPercent = newEditorPercent;
Expand All @@ -4962,6 +5018,7 @@ document.addEventListener("DOMContentLoaded", function () {
isResizing = false;
resizeDivider.classList.remove('dragging');
document.body.classList.remove('resizing');
updateCachedPaneHeights();
}

function applyPaneWidths() {
Expand Down Expand Up @@ -5121,6 +5178,7 @@ document.addEventListener("DOMContentLoaded", function () {
toggleFrDockMode(true);
}
constrainFloatingPanelPosition();
updateCachedPaneHeights();
}, 100);
});

Expand Down Expand Up @@ -5155,9 +5213,12 @@ document.addEventListener("DOMContentLoaded", function () {
scheduleLineNumberUpdate();
});

initMarkdownFormatToolbar();
initFindReplaceModal();
initAppModals();
// Defer non-critical startup initializations to reduce startup time and TBT
setTimeout(function() {
initMarkdownFormatToolbar();
initFindReplaceModal();
initAppModals();
}, 50);

// Editor key handlers for list continuation and indentation
markdownEditor.addEventListener("keydown", function(e) {
Expand Down Expand Up @@ -5371,6 +5432,33 @@ document.addEventListener("DOMContentLoaded", function () {
this.value = "";
});

function triggerSaveAs(blob, filename) {
if (typeof saveAs === 'undefined') {
const exportDropdownBtn = document.getElementById("exportDropdown");
const originalHtml = exportDropdownBtn ? exportDropdownBtn.innerHTML : null;
if (exportDropdownBtn) {
exportDropdownBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span> Loading...';
exportDropdownBtn.disabled = true;
}
loadScript(CDN.filesaver).then(function() {
if (exportDropdownBtn) {
exportDropdownBtn.innerHTML = originalHtml;
exportDropdownBtn.disabled = false;
}
saveAs(blob, filename);
}).catch(function(e) {
if (exportDropdownBtn) {
exportDropdownBtn.innerHTML = originalHtml;
exportDropdownBtn.disabled = false;
}
console.error('Failed to load FileSaver:', e);
alert('Failed to load export library. Please check your internet connection.');
});
} else {
saveAs(blob, filename);
}
}

exportMd.addEventListener("click", function () {
if (typeof Neutralino !== 'undefined') {
nativeSaveMarkdown();
Expand All @@ -5380,7 +5468,7 @@ document.addEventListener("DOMContentLoaded", function () {
const blob = new Blob([markdownEditor.value], {
type: "text/markdown;charset=utf-8",
});
saveAs(blob, "document.md");
triggerSaveAs(blob, "document.md");
} catch (e) {
console.error("Export failed:", e);
alert("Export failed: " + e.message);
Expand Down Expand Up @@ -5587,7 +5675,7 @@ document.addEventListener("DOMContentLoaded", function () {
if (typeof Neutralino !== 'undefined') {
nativeSaveHtml(fullHtml);
} else {
saveAs(blob, "document.html");
triggerSaveAs(blob, "document.html");
}
} catch (e) {
console.error("HTML export failed:", e);
Expand Down
7 changes: 7 additions & 0 deletions desktop-app/resources/styles.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
@font-face {
font-family: "bootstrap-icons";
src: url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff2?dd670da4167998394e1cf6f26487e45e") format("woff2"),
url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff?dd670da4167998394e1cf6f26487e45e") format("woff");
font-display: block;
}

:root {
--bg-color: #ffffff;
--editor-bg: #f6f8fa;
Expand Down
Loading
Loading