diff --git a/desktop-app/resources/index.html b/desktop-app/resources/index.html
index cdff5203..da1acfb3 100644
--- a/desktop-app/resources/index.html
+++ b/desktop-app/resources/index.html
@@ -698,7 +698,8 @@
Choose how the PDF should be generated.
-
@@ -726,6 +740,70 @@
+
+
+
+
+
+
Choose how the exported HTML document should be styled.
+
Appearance
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Choose how the exported image should be styled.
+
Appearance
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop-app/resources/js/preview-worker.js b/desktop-app/resources/js/preview-worker.js
index 9a81e6d5..42ab838e 100644
--- a/desktop-app/resources/js/preview-worker.js
+++ b/desktop-app/resources/js/preview-worker.js
@@ -776,7 +776,7 @@ function ensureLibraries(urls) {
}
function isSegmentedPreviewSafe(markdown) {
- if (/^\s*---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/.test(markdown)) return false;
+ if (typeof markdown === 'string' && markdown.startsWith('---') && /^---\r?\n[a-zA-Z0-9_-]+\s*:[\s\S]*?\r?\n---(?:\r?\n|$)/.test(markdown)) return false;
if (/^\[[^\]\n]+\]:\s+\S+/m.test(markdown)) return false;
if (/\[\^[^\]\n]+\]/.test(markdown)) return false;
if (/\n:[ \t]+/.test(markdown)) return false;
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
index 8f8cfc19..e57b02ad 100644
--- a/desktop-app/resources/js/script.js
+++ b/desktop-app/resources/js/script.js
@@ -9,6 +9,9 @@ document.addEventListener("DOMContentLoaded", async function () {
const RELEASE_NOTES_SEEN_VERSION_KEY = 'markdownViewerReleaseNotesSeenVersion';
const DIRTY_DOCUMENT_JOURNAL_PREFIX = 'markdownViewerDirtyDocument:';
const SECRET_DIRTY_DOCUMENT_JOURNAL_PREFIX = 'markdownViewerSecretDirtyDocument:';
+ const PDF_EXPORT_THEME_KEY = 'markdownViewerExportPdfTheme';
+ const HTML_EXPORT_THEME_KEY = 'markdownViewerExportHtmlTheme';
+ const PNG_EXPORT_THEME_KEY = 'markdownViewerExportPngTheme';
const DOCUMENT_STORAGE_KEYS = new Set([
'markdownViewerGlobalState',
'markdownViewerTabs',
@@ -62,7 +65,10 @@ document.addEventListener("DOMContentLoaded", async function () {
RELEASE_NOTES_PENDING_MODE_KEY,
RELEASE_NOTES_SEEN_VERSION_KEY,
'find-replace-docked',
- 'app-lang'
+ 'app-lang',
+ PDF_EXPORT_THEME_KEY,
+ HTML_EXPORT_THEME_KEY,
+ PNG_EXPORT_THEME_KEY
];
for (const key of keys) {
try {
@@ -369,6 +375,26 @@ document.addEventListener("DOMContentLoaded", async function () {
const pdfExportCardRaster = document.getElementById("pdf-export-card-raster");
const pdfExportModeVector = document.getElementById("pdf-export-mode-vector");
const pdfExportModeRaster = document.getElementById("pdf-export-mode-raster");
+ const pdfExportThemeCardLight = document.getElementById("pdf-export-theme-card-light");
+ const pdfExportThemeCardDark = document.getElementById("pdf-export-theme-card-dark");
+ const pdfExportThemeLight = document.getElementById("pdf-export-theme-light");
+ const pdfExportThemeDark = document.getElementById("pdf-export-theme-dark");
+ const htmlExportModal = document.getElementById("html-export-modal");
+ const htmlExportClose = document.getElementById("html-export-close");
+ const htmlExportCancelBtn = document.getElementById("html-export-cancel");
+ const htmlExportConfirmBtn = document.getElementById("html-export-confirm");
+ const htmlExportThemeCardLight = document.getElementById("html-export-theme-card-light");
+ const htmlExportThemeCardDark = document.getElementById("html-export-theme-card-dark");
+ const htmlExportThemeLight = document.getElementById("html-export-theme-light");
+ const htmlExportThemeDark = document.getElementById("html-export-theme-dark");
+ const pngExportModal = document.getElementById("png-export-modal");
+ const pngExportClose = document.getElementById("png-export-close");
+ const pngExportCancelBtn = document.getElementById("png-export-cancel");
+ const pngExportConfirmBtn = document.getElementById("png-export-confirm");
+ const pngExportThemeCardLight = document.getElementById("png-export-theme-card-light");
+ const pngExportThemeCardDark = document.getElementById("png-export-theme-card-dark");
+ const pngExportThemeLight = document.getElementById("png-export-theme-light");
+ const pngExportThemeDark = document.getElementById("png-export-theme-dark");
const exportPng = document.getElementById("export-png");
const copyMarkdownButton = document.getElementById("copy-markdown-button");
const dragOverlay = document.getElementById("drag-overlay");
@@ -3208,6 +3234,7 @@ document.addEventListener("DOMContentLoaded", async function () {
const blockquotes = container.querySelectorAll("blockquote");
blockquotes.forEach((blockquote) => {
+ if (blockquote.classList.contains("markdown-alert")) return;
let firstParagraph = null;
for (const child of blockquote.children) {
if (child.tagName === "P") {
@@ -3259,10 +3286,32 @@ document.addEventListener("DOMContentLoaded", async function () {
}
function parseFrontmatter(markdown) {
- const match = markdown.match(/^\s*---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
+ if (typeof markdown !== 'string') return { frontmatter: null, body: '' };
+ if (!markdown.startsWith('---')) return { frontmatter: null, body: markdown };
+ const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
if (!match) return { frontmatter: null, body: markdown };
+
+ const content = match[1].trim();
+ if (!content || !/^[a-zA-Z0-9_-]+\s*:/m.test(content)) {
+ return { frontmatter: null, body: markdown };
+ }
+ if (/^#|^\s*\||^\s*```|^\s*[-*+]\s+\[/m.test(content)) {
+ return { frontmatter: null, body: markdown };
+ }
+
try {
- const data = jsyaml.load(match[1]) || {};
+ const data = jsyaml.load(match[1]);
+ if (!data || typeof data !== 'object' || Array.isArray(data) || Object.prototype.toString.call(data) !== '[object Object]') {
+ return { frontmatter: null, body: markdown };
+ }
+ const keys = Object.keys(data);
+ if (keys.length === 0) {
+ return { frontmatter: null, body: markdown };
+ }
+ const isValid = keys.every(k => typeof k === 'string' && k.length > 0 && k.length < 100 && !/[#|`\r\n]/.test(k));
+ if (!isValid) {
+ return { frontmatter: null, body: markdown };
+ }
return { frontmatter: data, body: markdown.slice(match[0].length) };
} catch (e) {
console.warn('Frontmatter YAML parse error:', e);
@@ -14288,18 +14337,23 @@ ${selector} .arrowheadPath {
console.warn('STL print theme preparation failed:', error);
}
+ try {
+ enhanceGitHubAlerts(markdownPreview);
+ } catch (_) {}
+
await waitForBrowserPrintFrame();
}
- async function prepareBrowserPrintExport() {
+ async function prepareBrowserPrintExport(exportTheme = 'light') {
const root = document.documentElement;
const previousTheme = root.getAttribute('data-theme') || initialTheme || 'light';
const previousPrintExport = root.getAttribute('data-browser-print-export');
- const shouldForceLightTheme = previousTheme === 'dark';
+ const targetTheme = exportTheme === 'dark' ? 'dark' : 'light';
+ const shouldChangeTheme = previousTheme !== targetTheme;
- root.setAttribute('data-browser-print-export', 'light');
- if (shouldForceLightTheme) {
- root.setAttribute('data-theme', 'light');
+ root.setAttribute('data-browser-print-export', targetTheme);
+ if (shouldChangeTheme) {
+ root.setAttribute('data-theme', targetTheme);
}
await renderThemeSensitivePreviewContentForPrint();
@@ -14317,7 +14371,7 @@ ${selector} .arrowheadPath {
root.setAttribute('data-browser-print-export', previousPrintExport);
}
- if (shouldForceLightTheme) {
+ if (shouldChangeTheme) {
root.setAttribute('data-theme', previousTheme);
renderThemeSensitivePreviewContentForPrint().catch(function(error) {
console.warn('Preview theme restoration after print failed:', error);
@@ -14326,12 +14380,12 @@ ${selector} .arrowheadPath {
};
}
- async function runBrowserPrintExport() {
+ async function runBrowserPrintExport(exportTheme = 'light') {
const previousDocumentTitle = document.title;
document.title = getBrowserPrintDocumentTitle();
let restoreBrowserPrintExport;
try {
- restoreBrowserPrintExport = await prepareBrowserPrintExport();
+ restoreBrowserPrintExport = await prepareBrowserPrintExport(exportTheme);
} catch (error) {
document.title = previousDocumentTitle;
throw error;
@@ -14508,6 +14562,7 @@ ${selector} .arrowheadPath {
roots.forEach(function(root) {
processEmojis(root);
enhancePreviewCodeBlocks(root);
+ enhanceGitHubAlerts(root);
});
enhanceReleaseNotesPreview();
@@ -23421,7 +23476,61 @@ ${selector} .arrowheadPath {
}
});
- exportHtml.addEventListener("click", function () {
+ htmlExportClose?.addEventListener("click", () => closeAppModal(htmlExportModal));
+ htmlExportCancelBtn?.addEventListener("click", () => closeAppModal(htmlExportModal));
+
+ function syncHtmlExportThemeCardStyles() {
+ const isDark = Boolean(htmlExportThemeDark?.checked);
+ if (isDark) {
+ htmlExportThemeCardDark?.classList.add('is-active', 'is-selected');
+ htmlExportThemeCardLight?.classList.remove('is-active', 'is-selected');
+ } else {
+ htmlExportThemeCardLight?.classList.add('is-active', 'is-selected');
+ htmlExportThemeCardDark?.classList.remove('is-active', 'is-selected');
+ }
+ }
+
+ function getStoredHtmlExportTheme() {
+ try {
+ const stored = localStorage.getItem(HTML_EXPORT_THEME_KEY);
+ if (stored === 'dark' || stored === 'light') return stored;
+ } catch (_) {}
+ return null;
+ }
+
+ htmlExportThemeLight?.addEventListener('change', () => {
+ syncHtmlExportThemeCardStyles();
+ saveStorageItem(HTML_EXPORT_THEME_KEY, 'light');
+ });
+ htmlExportThemeDark?.addEventListener('change', () => {
+ syncHtmlExportThemeCardStyles();
+ saveStorageItem(HTML_EXPORT_THEME_KEY, 'dark');
+ });
+
+ exportHtml.addEventListener("click", function (event) {
+ event.preventDefault();
+ if (!hasActiveOpenDocument()) return;
+ if (isReleaseNotesActive()) return;
+ const storedTheme = getStoredHtmlExportTheme();
+ const effectiveTheme = storedTheme || (document.documentElement.getAttribute("data-theme") === 'dark' ? 'dark' : 'light');
+ if (effectiveTheme === 'dark') {
+ if (htmlExportThemeDark) htmlExportThemeDark.checked = true;
+ } else {
+ if (htmlExportThemeLight) htmlExportThemeLight.checked = true;
+ }
+ syncHtmlExportThemeCardStyles();
+ openAppModal(htmlExportModal);
+ });
+
+ htmlExportConfirmBtn?.addEventListener("click", function (event) {
+ event.preventDefault();
+ closeAppModal(htmlExportModal);
+ const selectedTheme = document.querySelector('input[name="html-export-theme"]:checked')?.value || 'light';
+ saveStorageItem(HTML_EXPORT_THEME_KEY, selectedTheme);
+ generateHtmlExport(selectedTheme);
+ });
+
+ function generateHtmlExport(exportTheme = 'light') {
if (!hasActiveOpenDocument()) return;
if (isReleaseNotesActive()) return;
try {
@@ -23438,9 +23547,22 @@ ${selector} .arrowheadPath {
tempContainer.innerHTML = sanitizedHtml;
applyReferencePreviewLinks(tempContainer, referenceData.definitions);
enhanceGitHubAlerts(tempContainer);
+ tempContainer.querySelectorAll('.diagram-status, .diagram-toolbar, .mermaid-toolbar, .abc-toolbar').forEach(function(el) {
+ el.remove();
+ });
+ tempContainer.querySelectorAll('.diagram-viewer.is-loading, .mermaid-container.is-loading, .abc-container.is-loading').forEach(function(el) {
+ el.classList.remove('is-loading');
+ });
+ tempContainer.querySelectorAll('.mermaid').forEach(function(node) {
+ const rawCode = node.getAttribute('data-original-code');
+ if (rawCode) {
+ try {
+ node.textContent = decodeURIComponent(rawCode);
+ } catch (_) {}
+ }
+ });
const enhancedHtml = tempContainer.innerHTML;
- const isDarkTheme =
- document.documentElement.getAttribute("data-theme") === "dark";
+ const isDarkTheme = exportTheme === "dark";
const cssTheme = isDarkTheme
? "https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown-dark.min.css"
: "https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.3.0/github-markdown.min.css";
@@ -23448,7 +23570,7 @@ ${selector} .arrowheadPath {
? "sha384-tah+rtrY/RrDGiE3nwf/q5WWsyjjRO4iOPWuZi/NpDqjWEEsEU9fPpjX2f5lremS"
: "sha384-hZuxRjC/Dsr4zEx1JlUhDQqkvqBPp2VLHsgXfnxPq1ULDy1eIdWCiux7nvO1RIZP";
const fullHtml = `
-
+
@@ -23470,13 +23592,23 @@ ${selector} .arrowheadPath {