feat(preview): add file preview page with metadata and styling
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m48s
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m48s
Implement a rich file preview interface to allow users to view file contents directly in the browser. Changes include: - Exposing raw file size (`SizeBytes`) in the download handler's file view. - Adding comprehensive CSS styling for the preview layout and cards. - Integrating Prism.js for syntax highlighting of code files. - Updating Content Security Policy (CSP) headers to permit inline styles and frame sources required by the preview components. - Adding unit tests to ensure preview metadata attributes are correctly rendered on the download page.
This commit is contained in:
717
backend/static/js/45-preview.js
Normal file
717
backend/static/js/45-preview.js
Normal file
@@ -0,0 +1,717 @@
|
||||
(function () {
|
||||
var preview = document.querySelector("[data-source-url][data-file-name][data-content-type]");
|
||||
if (!preview) {
|
||||
return;
|
||||
}
|
||||
|
||||
var SMALL_TEXT_BYTES = 50 * 1024;
|
||||
var LARGE_PREVIEW_BYTES = 500 * 1024;
|
||||
|
||||
var state = {
|
||||
fileName: preview.dataset.fileName || "",
|
||||
contentType: (preview.dataset.contentType || "").toLowerCase(),
|
||||
previewKind: preview.dataset.previewKind || "",
|
||||
sizeBytes: Number(preview.dataset.sizeBytes || 0),
|
||||
sizeLabel: preview.dataset.fileSize || "",
|
||||
sourceURL: preview.dataset.sourceUrl || "",
|
||||
downloadURL: preview.dataset.downloadUrl || "",
|
||||
iconURL: preview.dataset.iconUrl || "",
|
||||
activeMode: "",
|
||||
defaultMode: "default",
|
||||
pendingMode: "",
|
||||
textSource: "",
|
||||
textLoaded: false,
|
||||
rawLoaded: false,
|
||||
prismLoaded: false,
|
||||
renderLoaded: false,
|
||||
renderFullscreenFallback: false,
|
||||
confirmedLargeModes: {},
|
||||
tabs: []
|
||||
};
|
||||
|
||||
var els = {
|
||||
tabs: preview.querySelector("[data-preview-tabs]"),
|
||||
modeLabel: preview.querySelector("[data-preview-mode-label]"),
|
||||
defaultPane: preview.querySelector("[data-default-preview]"),
|
||||
imagePane: preview.querySelector("[data-image-preview]"),
|
||||
videoPane: preview.querySelector("[data-video-preview]"),
|
||||
browserAudioPane: preview.querySelector("[data-browser-audio-preview]"),
|
||||
rawPane: preview.querySelector("[data-raw-preview]"),
|
||||
rawOutput: preview.querySelector("[data-raw-output]"),
|
||||
codePane: preview.querySelector("[data-code-preview]"),
|
||||
codeOutput: preview.querySelector("[data-code-output]"),
|
||||
renderPane: preview.querySelector("[data-render-preview]"),
|
||||
fullscreenButton: preview.querySelector("[data-render-fullscreen]"),
|
||||
gatePane: preview.querySelector("[data-large-preview-gate]"),
|
||||
gateConfirm: preview.querySelector("[data-large-preview-confirm]"),
|
||||
gateCancel: preview.querySelector("[data-large-preview-cancel]"),
|
||||
placeholder: preview.querySelector("[data-preview-placeholder]")
|
||||
};
|
||||
|
||||
var fileType = detectFileType();
|
||||
state.tabs = buildTabs(fileType);
|
||||
state.defaultMode = chooseDefaultMode(fileType, state.tabs);
|
||||
|
||||
bindLargeGate();
|
||||
bindThemeChanges();
|
||||
bindRenderFullscreen();
|
||||
renderTabs();
|
||||
selectMode(state.defaultMode);
|
||||
|
||||
function detectFileType() {
|
||||
var extension = extensionFor(state.fileName);
|
||||
var baseType = state.contentType.split(";")[0].trim();
|
||||
var language = languageFor(extension, baseType);
|
||||
var isImage = state.previewKind === "image" || baseType.indexOf("image/") === 0 && baseType !== "image/svg+xml";
|
||||
var isVideo = state.previewKind === "video" || baseType.indexOf("video/") === 0;
|
||||
var isAudio = state.previewKind === "audio" || baseType.indexOf("audio/") === 0;
|
||||
|
||||
return {
|
||||
extension: extension,
|
||||
baseType: baseType,
|
||||
language: language,
|
||||
isTextLike: Boolean(language),
|
||||
isHTML: language === "html",
|
||||
isMarkdown: language === "markdown",
|
||||
isImage: isImage,
|
||||
isVideo: isVideo,
|
||||
isAudio: isAudio,
|
||||
isMobile: window.matchMedia && window.matchMedia("(max-width: 720px), (pointer: coarse)").matches
|
||||
};
|
||||
}
|
||||
|
||||
function buildTabs(type) {
|
||||
var tabs = [{ mode: "default", label: "Default" }];
|
||||
|
||||
if (type.isImage) {
|
||||
tabs.push({ mode: "image", label: "Image Preview" });
|
||||
return tabs;
|
||||
}
|
||||
|
||||
if (type.isVideo) {
|
||||
tabs.push({ mode: "video", label: "Video Preview" });
|
||||
return tabs;
|
||||
}
|
||||
|
||||
if (type.isAudio) {
|
||||
tabs.push({ mode: "browser-audio", label: "Browser Preview" });
|
||||
return tabs;
|
||||
}
|
||||
|
||||
if (type.isTextLike) {
|
||||
if (type.isHTML || type.isMarkdown) {
|
||||
tabs.push({ mode: "render", label: "Render Preview" });
|
||||
}
|
||||
tabs.push({ mode: "raw", label: "Raw Preview" });
|
||||
tabs.push({ mode: "code", label: "Code Preview" });
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
function chooseDefaultMode(type, tabs) {
|
||||
if (state.sizeBytes > LARGE_PREVIEW_BYTES) {
|
||||
if (type.isAudio && hasMode(tabs, "browser-audio")) {
|
||||
return "browser-audio";
|
||||
}
|
||||
return "default";
|
||||
}
|
||||
|
||||
if (type.isImage) {
|
||||
return "image";
|
||||
}
|
||||
if (type.isVideo) {
|
||||
return "video";
|
||||
}
|
||||
if (type.isAudio) {
|
||||
return "browser-audio";
|
||||
}
|
||||
if (type.isTextLike && state.sizeBytes > SMALL_TEXT_BYTES) {
|
||||
return "raw";
|
||||
}
|
||||
if (type.isMarkdown) {
|
||||
return "render";
|
||||
}
|
||||
if (type.isTextLike) {
|
||||
return "code";
|
||||
}
|
||||
return "default";
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
if (!els.tabs) {
|
||||
return;
|
||||
}
|
||||
els.tabs.innerHTML = "";
|
||||
state.tabs.forEach(function (tab) {
|
||||
var button = document.createElement("button");
|
||||
button.className = "preview-tab";
|
||||
button.type = "button";
|
||||
button.dataset.previewTab = tab.mode;
|
||||
button.textContent = tab.label;
|
||||
button.addEventListener("click", function () {
|
||||
selectMode(tab.mode);
|
||||
});
|
||||
els.tabs.appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
function selectMode(mode) {
|
||||
if (!hasMode(state.tabs, mode)) {
|
||||
mode = "default";
|
||||
}
|
||||
|
||||
if (mode !== "render") {
|
||||
exitRenderFullscreen();
|
||||
}
|
||||
|
||||
if (requiresLargeConfirmation(mode)) {
|
||||
showLargeGate(mode);
|
||||
return;
|
||||
}
|
||||
|
||||
state.activeMode = mode;
|
||||
updateTabs(mode);
|
||||
updateRenderFullscreenButton();
|
||||
hideAllPanes();
|
||||
setModeLabel(labelForMode(mode));
|
||||
|
||||
if (mode === "default") {
|
||||
show(els.defaultPane);
|
||||
} else if (mode === "image") {
|
||||
show(els.imagePane);
|
||||
} else if (mode === "video") {
|
||||
show(els.videoPane);
|
||||
} else if (mode === "browser-audio") {
|
||||
show(els.browserAudioPane);
|
||||
} else if (mode === "raw") {
|
||||
show(els.rawPane);
|
||||
ensureRawPreview();
|
||||
} else if (mode === "code") {
|
||||
show(els.codePane);
|
||||
ensurePrismPreview();
|
||||
} else if (mode === "render") {
|
||||
show(els.renderPane);
|
||||
if (fileType.isMarkdown) {
|
||||
ensureMarkdownRenderPreview();
|
||||
} else {
|
||||
ensureHTMLRenderPreview();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requiresLargeConfirmation(mode) {
|
||||
if (state.sizeBytes <= LARGE_PREVIEW_BYTES || state.confirmedLargeModes[mode]) {
|
||||
return false;
|
||||
}
|
||||
return mode === "raw" || mode === "code" || mode === "render";
|
||||
}
|
||||
|
||||
function showLargeGate(mode) {
|
||||
state.pendingMode = mode;
|
||||
updateTabs(state.activeMode || state.defaultMode);
|
||||
updateRenderFullscreenButton(false);
|
||||
hideAllPanes();
|
||||
show(els.gatePane);
|
||||
setModeLabel("Large preview");
|
||||
}
|
||||
|
||||
function bindLargeGate() {
|
||||
if (els.gateConfirm) {
|
||||
els.gateConfirm.addEventListener("click", function () {
|
||||
if (!state.pendingMode) {
|
||||
return;
|
||||
}
|
||||
state.confirmedLargeModes[state.pendingMode] = true;
|
||||
selectMode(state.pendingMode);
|
||||
});
|
||||
}
|
||||
if (els.gateCancel) {
|
||||
els.gateCancel.addEventListener("click", function () {
|
||||
state.pendingMode = "";
|
||||
selectMode(state.activeMode || state.defaultMode || "default");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function bindThemeChanges() {
|
||||
var themeSelect = document.querySelector("[data-theme-select]");
|
||||
if (!themeSelect) {
|
||||
return;
|
||||
}
|
||||
themeSelect.addEventListener("change", function () {
|
||||
window.setTimeout(function () {
|
||||
if (!fileType.isMarkdown || !state.renderLoaded) {
|
||||
return;
|
||||
}
|
||||
state.renderLoaded = false;
|
||||
if (state.activeMode === "render") {
|
||||
ensureMarkdownRenderPreview();
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
function bindRenderFullscreen() {
|
||||
if (!els.fullscreenButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
els.fullscreenButton.addEventListener("click", function () {
|
||||
if (isRenderFullscreen()) {
|
||||
exitRenderFullscreen();
|
||||
return;
|
||||
}
|
||||
enterRenderFullscreen();
|
||||
});
|
||||
|
||||
document.addEventListener("fullscreenchange", updateRenderFullscreenButton);
|
||||
}
|
||||
|
||||
function ensureTextLoaded() {
|
||||
if (state.textLoaded) {
|
||||
return Promise.resolve(state.textSource);
|
||||
}
|
||||
|
||||
showLoading("Loading preview...");
|
||||
return fetch(state.sourceURL, { credentials: "same-origin" })
|
||||
.then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error("Preview failed");
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(function (source) {
|
||||
state.textSource = source;
|
||||
state.textLoaded = true;
|
||||
hide(els.placeholder);
|
||||
return source;
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError("Preview unavailable");
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
function ensureRawPreview() {
|
||||
if (state.rawLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureTextLoaded().then(function (source) {
|
||||
els.rawOutput.textContent = source;
|
||||
state.rawLoaded = true;
|
||||
if (state.activeMode === "raw") {
|
||||
hide(els.placeholder);
|
||||
show(els.rawPane);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ensurePrismPreview() {
|
||||
if (state.prismLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading("Loading syntax preview...");
|
||||
Promise.all([ensureTextLoaded(), loadPrism()])
|
||||
.then(function (results) {
|
||||
var source = results[0];
|
||||
var language = fileType.language;
|
||||
if (language === "json") {
|
||||
source = formatJSON(source);
|
||||
}
|
||||
els.codeOutput.className = "language-" + language;
|
||||
els.codeOutput.textContent = source;
|
||||
if (window.Prism) {
|
||||
window.Prism.highlightElement(els.codeOutput);
|
||||
}
|
||||
state.prismLoaded = true;
|
||||
if (state.activeMode === "code") {
|
||||
hide(els.placeholder);
|
||||
show(els.codePane);
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
showError("Syntax preview unavailable");
|
||||
});
|
||||
}
|
||||
|
||||
function ensureHTMLRenderPreview() {
|
||||
if (state.renderLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading("Rendering preview...");
|
||||
ensureTextLoaded()
|
||||
.then(function (source) {
|
||||
els.renderPane.srcdoc = withBaseElement(source);
|
||||
state.renderLoaded = true;
|
||||
if (state.activeMode === "render") {
|
||||
hide(els.placeholder);
|
||||
show(els.renderPane);
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
showError("Render preview unavailable");
|
||||
});
|
||||
}
|
||||
|
||||
function ensureMarkdownRenderPreview() {
|
||||
if (state.renderLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading("Rendering Markdown...");
|
||||
Promise.all([ensureTextLoaded(), loadMarkdownLibs()])
|
||||
.then(function (results) {
|
||||
var markdown = results[0];
|
||||
var html = parseMarkdown(markdown);
|
||||
var clean = window.DOMPurify.sanitize(html);
|
||||
els.renderPane.srcdoc = markdownDocument(clean);
|
||||
state.renderLoaded = true;
|
||||
if (state.activeMode === "render") {
|
||||
hide(els.placeholder);
|
||||
show(els.renderPane);
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
showError("Markdown preview unavailable");
|
||||
});
|
||||
}
|
||||
|
||||
function showLoading(message) {
|
||||
if (!els.placeholder) {
|
||||
return;
|
||||
}
|
||||
var text = els.placeholder.querySelector("p");
|
||||
if (text) {
|
||||
text.textContent = message;
|
||||
}
|
||||
show(els.placeholder);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
hideAllPanes();
|
||||
var text = els.placeholder && els.placeholder.querySelector("p");
|
||||
if (text) {
|
||||
text.textContent = message;
|
||||
}
|
||||
show(els.placeholder);
|
||||
}
|
||||
|
||||
function hideAllPanes() {
|
||||
hide(els.defaultPane);
|
||||
hide(els.imagePane);
|
||||
hide(els.videoPane);
|
||||
hide(els.browserAudioPane);
|
||||
hide(els.rawPane);
|
||||
hide(els.codePane);
|
||||
hide(els.renderPane);
|
||||
hide(els.gatePane);
|
||||
hide(els.placeholder);
|
||||
}
|
||||
|
||||
function enterRenderFullscreen() {
|
||||
if (state.activeMode !== "render") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (preview.requestFullscreen) {
|
||||
var request = preview.requestFullscreen();
|
||||
if (request && typeof request.catch === "function") {
|
||||
request.catch(function () {
|
||||
state.renderFullscreenFallback = true;
|
||||
preview.classList.add("is-render-fullscreen");
|
||||
updateRenderFullscreenButton();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
state.renderFullscreenFallback = true;
|
||||
preview.classList.add("is-render-fullscreen");
|
||||
updateRenderFullscreenButton();
|
||||
}
|
||||
|
||||
function exitRenderFullscreen() {
|
||||
if (document.fullscreenElement === preview && document.exitFullscreen) {
|
||||
var exit = document.exitFullscreen();
|
||||
if (exit && typeof exit.catch === "function") {
|
||||
exit.catch(function () {});
|
||||
}
|
||||
}
|
||||
state.renderFullscreenFallback = false;
|
||||
preview.classList.remove("is-render-fullscreen");
|
||||
updateRenderFullscreenButton();
|
||||
}
|
||||
|
||||
function isRenderFullscreen() {
|
||||
return document.fullscreenElement === preview || state.renderFullscreenFallback;
|
||||
}
|
||||
|
||||
function updateRenderFullscreenButton(forceVisible) {
|
||||
if (!els.fullscreenButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
var visible = typeof forceVisible === "boolean" ? forceVisible : state.activeMode === "render";
|
||||
els.fullscreenButton.hidden = !visible;
|
||||
els.fullscreenButton.textContent = isRenderFullscreen() ? "Exit Full Screen" : "Full Screen";
|
||||
els.fullscreenButton.setAttribute("aria-pressed", isRenderFullscreen() ? "true" : "false");
|
||||
}
|
||||
|
||||
function updateTabs(mode) {
|
||||
if (!els.tabs) {
|
||||
return;
|
||||
}
|
||||
Array.prototype.forEach.call(els.tabs.querySelectorAll("[data-preview-tab]"), function (button) {
|
||||
button.classList.toggle("is-active", button.dataset.previewTab === mode);
|
||||
});
|
||||
}
|
||||
|
||||
function show(element) {
|
||||
if (element) {
|
||||
element.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
function hide(element) {
|
||||
if (element) {
|
||||
element.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function setModeLabel(label) {
|
||||
if (els.modeLabel) {
|
||||
els.modeLabel.textContent = label;
|
||||
}
|
||||
}
|
||||
|
||||
function hasMode(tabs, mode) {
|
||||
return tabs.some(function (tab) {
|
||||
return tab.mode === mode;
|
||||
});
|
||||
}
|
||||
|
||||
function labelForMode(mode) {
|
||||
var labels = {
|
||||
"default": "Default",
|
||||
"image": "Image preview",
|
||||
"video": "Video preview",
|
||||
"browser-audio": "Browser preview",
|
||||
"raw": "Raw preview",
|
||||
"code": "Code preview",
|
||||
"render": "Render preview"
|
||||
};
|
||||
return labels[mode] || "Preview";
|
||||
}
|
||||
|
||||
function loadPrism() {
|
||||
if (window.Prism) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
window.Prism = window.Prism || {};
|
||||
window.Prism.manual = true;
|
||||
return Promise.all([
|
||||
loadStyle("/static/lib/prismjs/prism.css"),
|
||||
loadScript("/static/lib/prismjs/prism.js")
|
||||
]);
|
||||
}
|
||||
|
||||
function loadMarkdownLibs() {
|
||||
if (window.marked && window.DOMPurify) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
loadScript("/static/lib/markdown/marked.umd.js"),
|
||||
loadScript("/static/lib/markdown/purify.min.js")
|
||||
]);
|
||||
}
|
||||
|
||||
function loadScript(src) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var existing = document.querySelector('script[src="' + src + '"]');
|
||||
if (existing) {
|
||||
if (existing.dataset.loaded === "true") {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
existing.addEventListener("load", resolve, { once: true });
|
||||
existing.addEventListener("error", reject, { once: true });
|
||||
return;
|
||||
}
|
||||
|
||||
var script = document.createElement("script");
|
||||
script.async = true;
|
||||
script.src = src;
|
||||
script.addEventListener("load", function () {
|
||||
script.dataset.loaded = "true";
|
||||
resolve();
|
||||
}, { once: true });
|
||||
script.addEventListener("error", reject, { once: true });
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
function loadStyle(href) {
|
||||
if (document.querySelector('link[href="' + href + '"]')) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(function (resolve, reject) {
|
||||
var link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
link.addEventListener("load", resolve, { once: true });
|
||||
link.addEventListener("error", reject, { once: true });
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
function parseMarkdown(source) {
|
||||
if (window.marked && typeof window.marked.parse === "function") {
|
||||
return window.marked.parse(source);
|
||||
}
|
||||
if (window.marked && window.marked.marked && typeof window.marked.marked.parse === "function") {
|
||||
return window.marked.marked.parse(source);
|
||||
}
|
||||
throw new Error("Marked unavailable");
|
||||
}
|
||||
|
||||
function markdownDocument(body) {
|
||||
var theme = currentTheme();
|
||||
var base = '<base href="' + escapeAttribute(new URL(state.sourceURL, window.location.href).href) + '">';
|
||||
return '<!doctype html><html data-theme="' + escapeAttribute(theme) + '"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">' +
|
||||
base +
|
||||
'<link rel="stylesheet" href="/static/css/80-markdown-preview.css">' +
|
||||
'<style>' + markdownThemeStyle(theme) + '</style>' +
|
||||
'</head><body><main>' + body + '</main></body></html>';
|
||||
}
|
||||
|
||||
function markdownThemeStyle(theme) {
|
||||
var themes = {
|
||||
revamp: ["dark", "#0b0b16", "#f5f3ff", "#aaa4d6", "#17142d", "#211b3e", "rgba(168,150,255,0.24)", "#67e8f9", "#a78bfa", "#1b1724", "#0f111a", "#f8fafc", "rgba(248,250,252,0.16)", "rgba(0,0,0,0.28)", "Inter,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif", "Consolas,Monaco,\"Andale Mono\",\"Ubuntu Mono\",monospace"],
|
||||
classic: ["dark", "#09090b", "#fafafa", "#a1a1aa", "#18181b", "#27272a", "rgba(255,255,255,0.13)", "#e4e4e7", "#d4d4d8", "#111113", "#09090b", "#fafafa", "rgba(250,250,250,0.15)", "rgba(0,0,0,0.3)", "Inter,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif", "Consolas,Monaco,\"Andale Mono\",\"Ubuntu Mono\",monospace"],
|
||||
retro: ["light", "#c0c0c0", "#000000", "#404040", "#ffffff", "#dfdfdf", "#000000", "#000078", "#000078", "#ffffff", "#000000", "#f5f5f5", "#808080", "transparent", "\"PixeloidSans\",\"PixelOperator\",\"Microsoft Sans Serif\",Tahoma,sans-serif", "\"PixelOperatorMono\",Consolas,monospace"],
|
||||
gruvbox: ["dark", "#1d2021", "#ebdbb2", "#bdae93", "#282828", "#32302f", "rgba(235,219,178,0.2)", "#fabd2f", "#d79921", "#1b1d1e", "#161819", "#fbf1c7", "rgba(251,241,199,0.18)", "rgba(0,0,0,0.26)", "Inter,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif", "Consolas,Monaco,\"Andale Mono\",\"Ubuntu Mono\",monospace"],
|
||||
cyberpunk: ["dark", "#08070d", "#fff36f", "#9bfaff", "#16131f", "#251d34", "rgba(255,242,0,0.34)", "#00f0ff", "#ff2a6d", "#100d18", "#07060b", "#f8fafc", "rgba(0,240,255,0.26)", "rgba(255,42,109,0.14)", "Inter,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif", "Consolas,Monaco,\"Andale Mono\",\"Ubuntu Mono\",monospace"]
|
||||
};
|
||||
var t = themes[theme] || themes.revamp;
|
||||
var vars = "color-scheme:" + t[0] + ";--md-bg:" + t[1] + ";--md-fg:" + t[2] + ";--md-muted:" + t[3] + ";--md-panel:" + t[4] + ";--md-panel-2:" + t[5] + ";--md-border:" + t[6] + ";--md-link:" + t[7] + ";--md-accent:" + t[8] + ";--md-code-bg:" + t[9] + ";--md-block-code-bg:" + t[10] + ";--md-block-code-fg:" + t[11] + ";--md-block-code-border:" + t[12] + ";--md-shadow:" + t[13] + ";--md-font:" + t[14] + ";--md-mono:" + t[15] + ";";
|
||||
return ":root{" + vars + "}*{box-sizing:border-box}html,body{min-height:100%;margin:0;background:var(--md-bg);color:var(--md-fg);font-family:var(--md-font)}body{padding:clamp(1rem,4vw,2.25rem);font-size:16px;line-height:1.65}main{max-width:54rem;margin:0 auto;padding:clamp(1rem,3vw,2rem);border:1px solid var(--md-border);border-radius:10px;background:var(--md-panel);box-shadow:0 20px 60px var(--md-shadow)}a{color:var(--md-link)}h1,h2,h3,h4,h5,h6{color:var(--md-fg);line-height:1.2}code,kbd,pre{font-family:var(--md-mono)}pre{overflow:auto;padding:1rem;border:1px solid var(--md-block-code-border)!important;background:var(--md-block-code-bg)!important;color:var(--md-block-code-fg)!important}code{background:var(--md-code-bg);border-radius:4px;padding:.12rem .3rem}pre code,pre>code,pre code[class*=\"language-\"]{padding:0!important;background:transparent!important;color:inherit!important;border:0!important}blockquote{margin:1rem 0;padding:.2rem 1rem;border-left:3px solid var(--md-accent);color:var(--md-muted);background:var(--md-panel-2)}table{width:100%;border-collapse:collapse;display:block;overflow:auto}th,td{border:1px solid var(--md-border);padding:.5rem .7rem}hr{border:0;border-top:1px solid var(--md-border)}img,video{max-width:100%;height:auto}";
|
||||
}
|
||||
|
||||
function currentTheme() {
|
||||
var theme = document.documentElement.dataset.theme || "revamp";
|
||||
return /^(revamp|classic|retro|gruvbox|cyberpunk)$/.test(theme) ? theme : "revamp";
|
||||
}
|
||||
|
||||
function withBaseElement(source) {
|
||||
var base = '<base href="' + escapeAttribute(new URL(state.sourceURL, window.location.href).href) + '">';
|
||||
if (/<head[\s>]/i.test(source)) {
|
||||
return source.replace(/<head([^>]*)>/i, "<head$1>" + base);
|
||||
}
|
||||
return base + source;
|
||||
}
|
||||
|
||||
function escapeAttribute(value) {
|
||||
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
||||
}
|
||||
|
||||
function formatJSON(source) {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(source), null, 2);
|
||||
} catch (error) {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
function extensionFor(name) {
|
||||
var parts = name.toLowerCase().split(".");
|
||||
return parts.length > 1 ? parts.pop() : "";
|
||||
}
|
||||
|
||||
function languageFor(extension, baseType) {
|
||||
var extensionMap = {
|
||||
"c": "c",
|
||||
"cc": "cpp",
|
||||
"conf": "nginx",
|
||||
"cpp": "cpp",
|
||||
"cs": "csharp",
|
||||
"css": "css",
|
||||
"csv": "csv",
|
||||
"diff": "diff",
|
||||
"dockerfile": "docker",
|
||||
"go": "go",
|
||||
"h": "c",
|
||||
"hpp": "cpp",
|
||||
"htm": "html",
|
||||
"html": "html",
|
||||
"ini": "ini",
|
||||
"java": "java",
|
||||
"js": "javascript",
|
||||
"json": "json",
|
||||
"jsx": "jsx",
|
||||
"kt": "kotlin",
|
||||
"log": "log",
|
||||
"lua": "lua",
|
||||
"md": "markdown",
|
||||
"mdown": "markdown",
|
||||
"markdown": "markdown",
|
||||
"php": "php",
|
||||
"pl": "perl",
|
||||
"properties": "properties",
|
||||
"py": "python",
|
||||
"rb": "ruby",
|
||||
"rs": "rust",
|
||||
"sh": "bash",
|
||||
"sql": "sql",
|
||||
"swift": "swift",
|
||||
"toml": "toml",
|
||||
"ts": "typescript",
|
||||
"tsx": "tsx",
|
||||
"txt": "text",
|
||||
"xml": "xml",
|
||||
"yaml": "yaml",
|
||||
"yml": "yaml",
|
||||
"zig": "zig"
|
||||
};
|
||||
var typeMap = {
|
||||
"application/javascript": "javascript",
|
||||
"application/json": "json",
|
||||
"application/ld+json": "json",
|
||||
"application/markdown": "markdown",
|
||||
"application/xml": "xml",
|
||||
"application/x-httpd-php": "php",
|
||||
"application/x-sh": "bash",
|
||||
"image/svg+xml": "xml",
|
||||
"text/css": "css",
|
||||
"text/csv": "csv",
|
||||
"text/html": "html",
|
||||
"text/javascript": "javascript",
|
||||
"text/markdown": "markdown",
|
||||
"text/plain": "text",
|
||||
"text/x-go": "go",
|
||||
"text/xml": "xml"
|
||||
};
|
||||
|
||||
if (extensionMap[extension]) {
|
||||
return extensionMap[extension];
|
||||
}
|
||||
if (typeMap[baseType]) {
|
||||
return typeMap[baseType];
|
||||
}
|
||||
if (baseType.indexOf("+json") !== -1) {
|
||||
return "json";
|
||||
}
|
||||
if (baseType.indexOf("+xml") !== -1) {
|
||||
return "xml";
|
||||
}
|
||||
if (baseType.indexOf("text/") === 0) {
|
||||
return "text";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user