Improve Markdown; Add Mermaid/LaTeX
This commit is contained in:
+432
-20
@@ -14,6 +14,10 @@ const STATE = {
|
||||
cleanupListenersAttached: false,
|
||||
lastScale: 1,
|
||||
prismScripts: null,
|
||||
mermaidScript: null,
|
||||
katexCore: null,
|
||||
katexAutoRender: null,
|
||||
katexCss: null,
|
||||
};
|
||||
|
||||
const PRISM_FILES = [
|
||||
@@ -26,6 +30,10 @@ const PRISM_FILES = [
|
||||
"prism-bash.min.txt",
|
||||
];
|
||||
|
||||
const MERMAID_FILE = "mermaid.min.txt";
|
||||
const KATEX_FILES = ["katex.min.txt", "katex-auto-render.min.txt"];
|
||||
const KATEX_CSS_FILE = "katex-with-fonts.min.css.txt";
|
||||
|
||||
async function loadPrismScripts() {
|
||||
if (STATE.prismScripts) return STATE.prismScripts;
|
||||
|
||||
@@ -47,6 +55,61 @@ async function loadPrismScripts() {
|
||||
|
||||
loadPrismScripts();
|
||||
|
||||
async function loadMermaidScript() {
|
||||
if (STATE.mermaidScript) return STATE.mermaidScript;
|
||||
|
||||
try {
|
||||
const basePath = import.meta.url.substring(0, import.meta.url.lastIndexOf("/"));
|
||||
const res = await fetch(`${basePath}/${MERMAID_FILE}`);
|
||||
STATE.mermaidScript = res.ok ? await res.text() : "";
|
||||
return STATE.mermaidScript;
|
||||
} catch (e) {
|
||||
console.error("[WAS Viewer] Failed to load Mermaid script:", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
async function loadKatexScripts() {
|
||||
if (STATE.katexCore && STATE.katexAutoRender && STATE.katexCss) return true;
|
||||
|
||||
try {
|
||||
const basePath = import.meta.url.substring(0, import.meta.url.lastIndexOf("/"));
|
||||
const [katexCore, autoRender, katexCss] = await Promise.all([
|
||||
fetch(`${basePath}/${KATEX_FILES[0]}`).then(r => r.ok ? r.text() : ""),
|
||||
fetch(`${basePath}/${KATEX_FILES[1]}`).then(r => r.ok ? r.text() : ""),
|
||||
fetch(`${basePath}/${KATEX_CSS_FILE}`).then(r => r.ok ? r.text() : "")
|
||||
]);
|
||||
STATE.katexCore = katexCore;
|
||||
STATE.katexAutoRender = autoRender;
|
||||
STATE.katexCss = katexCss;
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("[WAS Viewer] Failed to load KaTeX scripts:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshAllViewers() {
|
||||
for (const [nodeId, elements] of STATE.nodeIdToElements.entries()) {
|
||||
if (elements && elements.lastContentHash) {
|
||||
elements.lastContentHash = "";
|
||||
const node = app?.graph?.getNodeById(parseInt(nodeId));
|
||||
if (node) {
|
||||
try {
|
||||
updateIframeContent(node, elements);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadMermaidScript().then(() => {
|
||||
if (STATE.mermaidScript) refreshAllViewers();
|
||||
});
|
||||
loadKatexScripts().then(() => {
|
||||
if (STATE.katexCore && STATE.katexCss) refreshAllViewers();
|
||||
});
|
||||
|
||||
function simpleHash(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
@@ -106,7 +169,43 @@ function detectContentType(content) {
|
||||
return "html";
|
||||
}
|
||||
|
||||
const scores = { html: 0, markdown: 0, python: 0, javascript: 0, css: 0 };
|
||||
const scores = { html: 0, markdown: 0, python: 0, javascript: 0, css: 0, json: 0, yaml: 0, csv: 0, ansi: 0 };
|
||||
|
||||
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
||||
try {
|
||||
JSON.parse(trimmed);
|
||||
return "json";
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (/\x1b\[[\d;]*m/.test(trimmed) || /\033\[[\d;]*m/.test(trimmed)) {
|
||||
return "ansi";
|
||||
}
|
||||
|
||||
const lines = trimmed.split("\n");
|
||||
const csvLines = lines.filter(l => l.includes(",") && !l.includes("{") && !l.includes("<"));
|
||||
if (csvLines.length >= 2) {
|
||||
const firstLineCommas = (lines[0].match(/,/g) || []).length;
|
||||
if (firstLineCommas >= 1) {
|
||||
const consistentCommas = csvLines.filter(l => (l.match(/,/g) || []).length === firstLineCommas).length;
|
||||
if (consistentCommas >= csvLines.length * 0.8) {
|
||||
scores.csv += 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const yamlPatterns = [
|
||||
[/^[\w-]+:\s*.+$/m, 2],
|
||||
[/^[\w-]+:\s*$/m, 2],
|
||||
[/^\s+-\s+.+$/m, 1],
|
||||
[/^---\s*$/m, 3],
|
||||
];
|
||||
for (const [pattern, weight] of yamlPatterns) {
|
||||
if (pattern.test(trimmed)) scores.yaml += weight;
|
||||
}
|
||||
if (trimmed.includes(": ") && !trimmed.includes("{") && !trimmed.includes("<")) {
|
||||
scores.yaml += 1;
|
||||
}
|
||||
|
||||
const htmlTags = ["<div", "<span", "<p>", "<h1", "<h2", "<h3", "<table", "<ul", "<ol",
|
||||
"<img", "<a ", "<br", "<hr", "<em>", "<strong>", "<b>", "<i>", "<code>", "<pre>"];
|
||||
@@ -198,21 +297,75 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function parseNestedList(match, listTag, itemTag) {
|
||||
const regex = new RegExp(`<${itemTag}[^>]*data-level="(\\d+)"[^>]*(?:class="([^"]*)")?[^>]*>(.*?)<\\/${itemTag}>`, 'g');
|
||||
const items = [];
|
||||
let m;
|
||||
while ((m = regex.exec(match)) !== null) {
|
||||
items.push({ level: parseInt(m[1]), className: m[2] || '', content: m[3] });
|
||||
}
|
||||
|
||||
if (items.length === 0) return match;
|
||||
|
||||
let result = '';
|
||||
let stack = [];
|
||||
|
||||
for (const item of items) {
|
||||
while (stack.length > 0 && stack[stack.length - 1] > item.level) {
|
||||
result += `</li></${listTag}>`;
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
if (stack.length === 0 || stack[stack.length - 1] < item.level) {
|
||||
result += `<${listTag}>`;
|
||||
stack.push(item.level);
|
||||
} else if (stack[stack.length - 1] === item.level && result.length > 0) {
|
||||
result += '</li>';
|
||||
}
|
||||
|
||||
const classAttr = item.className ? ` class="${item.className}"` : '';
|
||||
result += `<li${classAttr}>${item.content}`;
|
||||
}
|
||||
|
||||
while (stack.length > 0) {
|
||||
result += `</li></${listTag}>`;
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseMarkdown(md) {
|
||||
let html = md;
|
||||
|
||||
// Normalize mixed LaTeX delimiters: $$\(...\)$$ -> $$...$$ and $$\[...\]$$ -> $$...$$
|
||||
html = html.replace(/\$\$\s*\\\(([^]*?)\\\)\s*\$\$/g, '$$$$$1$$$$');
|
||||
html = html.replace(/\$\$\s*\\\[([^]*?)\\\]\s*\$\$/g, '$$$$$1$$$$');
|
||||
|
||||
// Convert inline $$...$$ to $...$ when surrounded by text on same line (not at line start/end)
|
||||
html = html.replace(/([^\n$])\$\$([^$\n]+)\$\$([^\n$])/g, '$1$$$2$$$3');
|
||||
|
||||
html = html.replace(/^```(?:mermaid|flow|flex)(?:\[([^\]]*)\])?\n([\s\S]*?)```$/gm, (_, options, code) => {
|
||||
const centered = options && options.toLowerCase().includes('center');
|
||||
const style = centered ? 'text-align:center;' : 'text-align:left;';
|
||||
return `<div class="mermaid" style="${style}">${code.trim()}</div>`;
|
||||
});
|
||||
|
||||
html = html.replace(/^```(\w*)\n([\s\S]*?)```$/gm, (_, lang, code) => {
|
||||
return `<pre><code class="language-${lang || "text"}">${escapeHtml(code.trim())}</code></pre>`;
|
||||
});
|
||||
|
||||
html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
|
||||
|
||||
html = html.replace(/^######\s+(.*)$/gm, "<h6>$1</h6>");
|
||||
html = html.replace(/^#####\s+(.*)$/gm, "<h5>$1</h5>");
|
||||
html = html.replace(/^####\s+(.*)$/gm, "<h4>$1</h4>");
|
||||
html = html.replace(/^###\s+(.*)$/gm, "<h3>$1</h3>");
|
||||
html = html.replace(/^##\s+(.*)$/gm, "<h2>$1</h2>");
|
||||
html = html.replace(/^#\s+(.*)$/gm, "<h1>$1</h1>");
|
||||
html = html.replace(/^(.+)\n=+\s*$/gm, "<h1>$1</h1>");
|
||||
html = html.replace(/^(.+)\n-+\s*$/gm, "<h2>$1</h2>");
|
||||
|
||||
html = html.replace(/^######\s*(.+)$/gm, "<h6>$1</h6>");
|
||||
html = html.replace(/^#####\s*(.+)$/gm, "<h5>$1</h5>");
|
||||
html = html.replace(/^####\s*(.+)$/gm, "<h4>$1</h4>");
|
||||
html = html.replace(/^###\s*(.+)$/gm, "<h3>$1</h3>");
|
||||
html = html.replace(/^##\s*(.+)$/gm, "<h2>$1</h2>");
|
||||
html = html.replace(/^#\s*(.+)$/gm, "<h1>$1</h1>");
|
||||
|
||||
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width:100%;">');
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
@@ -225,14 +378,31 @@ function parseMarkdown(md) {
|
||||
|
||||
html = html.replace(/^>\s+(.*)$/gm, "<blockquote>$1</blockquote>");
|
||||
|
||||
html = html.replace(/^\d+\.\s+(.*)$/gm, "<oli>$1</oli>");
|
||||
html = html.replace(/(<oli>.*<\/oli>\n?)+/g, (match) => {
|
||||
return "<ol>" + match.replace(/<\/?oli>/g, (tag) => tag.replace("oli", "li")) + "</ol>";
|
||||
html = html.replace(/^(\s*)- \[x\]\s+(.*)$/gim, (_, indent, text) => {
|
||||
const level = Math.floor(indent.length / 2);
|
||||
return `<uli data-level="${level}" class="task-item checked"><input type="checkbox" checked disabled> ${text}</uli>`;
|
||||
});
|
||||
html = html.replace(/^(\s*)- \[ \]\s+(.*)$/gm, (_, indent, text) => {
|
||||
const level = Math.floor(indent.length / 2);
|
||||
return `<uli data-level="${level}" class="task-item"><input type="checkbox" disabled> ${text}</uli>`;
|
||||
});
|
||||
|
||||
html = html.replace(/^[-*+]\s+(.*)$/gm, "<uli>$1</uli>");
|
||||
html = html.replace(/(<uli>.*<\/uli>\n?)+/g, (match) => {
|
||||
return "<ul>" + match.replace(/<\/?uli>/g, (tag) => tag.replace("uli", "li")) + "</ul>";
|
||||
html = html.replace(/^(\s*)\d+\.\s+(.*)$/gm, (_, indent, text) => {
|
||||
const level = Math.floor(indent.length / 2);
|
||||
return `<oli data-level="${level}">${text}</oli>`;
|
||||
});
|
||||
|
||||
html = html.replace(/^(\s*)[-*+]\s+(.*)$/gm, (_, indent, text) => {
|
||||
const level = Math.floor(indent.length / 2);
|
||||
return `<uli data-level="${level}">${text}</uli>`;
|
||||
});
|
||||
|
||||
html = html.replace(/(<oli[^>]*>.*?<\/oli>\s*)+/g, (match) => {
|
||||
return parseNestedList(match, 'ol', 'oli');
|
||||
});
|
||||
|
||||
html = html.replace(/(<uli[^>]*>.*?<\/uli>\s*)+/g, (match) => {
|
||||
return parseNestedList(match, 'ul', 'uli');
|
||||
});
|
||||
|
||||
html = html.replace(/^---$/gm, "<hr>");
|
||||
@@ -273,12 +443,108 @@ function renderCodeContent(content, language) {
|
||||
python: "python",
|
||||
javascript: "javascript",
|
||||
css: "css",
|
||||
yaml: "yaml",
|
||||
text: "plaintext",
|
||||
};
|
||||
const lang = langMap[language] || "plaintext";
|
||||
return `<pre><code class="language-${lang}">${escapeHtml(content)}</code></pre>`;
|
||||
}
|
||||
|
||||
function renderJsonTree(data, depth = 0) {
|
||||
const indent = " ".repeat(depth);
|
||||
if (data === null) return `<span class="json-null">null</span>`;
|
||||
if (typeof data === "boolean") return `<span class="json-boolean">${data}</span>`;
|
||||
if (typeof data === "number") return `<span class="json-number">${data}</span>`;
|
||||
if (typeof data === "string") return `<span class="json-string">"${escapeHtml(data)}"</span>`;
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
if (data.length === 0) return `<span class="json-bracket">[]</span>`;
|
||||
const id = `json-${Math.random().toString(36).slice(2, 9)}`;
|
||||
const items = data.map((item, i) => {
|
||||
const comma = i < data.length - 1 ? "," : "";
|
||||
return `${indent} ${renderJsonTree(item, depth + 1)}${comma}`;
|
||||
}).join("\n");
|
||||
return `<span class="json-toggle" onclick="toggleJson('${id}')">[<span class="json-count">${data.length} items</span>]</span><div id="${id}" class="json-collapsible">\n${items}\n${indent}<span class="json-bracket">]</span></div>`;
|
||||
}
|
||||
|
||||
if (typeof data === "object") {
|
||||
const keys = Object.keys(data);
|
||||
if (keys.length === 0) return `<span class="json-bracket">{}</span>`;
|
||||
const id = `json-${Math.random().toString(36).slice(2, 9)}`;
|
||||
const items = keys.map((key, i) => {
|
||||
const comma = i < keys.length - 1 ? "," : "";
|
||||
return `${indent} <span class="json-key">"${escapeHtml(key)}"</span>: ${renderJsonTree(data[key], depth + 1)}${comma}`;
|
||||
}).join("\n");
|
||||
return `<span class="json-toggle" onclick="toggleJson('${id}')">{<span class="json-count">${keys.length} keys</span>}</span><div id="${id}" class="json-collapsible">\n${items}\n${indent}<span class="json-bracket">}</span></div>`;
|
||||
}
|
||||
|
||||
return escapeHtml(String(data));
|
||||
}
|
||||
|
||||
function renderCsvTable(content) {
|
||||
const lines = content.trim().split("\n");
|
||||
if (lines.length === 0) return `<pre>${escapeHtml(content)}</pre>`;
|
||||
|
||||
const parseRow = (line) => {
|
||||
const result = [];
|
||||
let current = "";
|
||||
let inQuotes = false;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
if (char === '"') {
|
||||
inQuotes = !inQuotes;
|
||||
} else if (char === "," && !inQuotes) {
|
||||
result.push(current.trim());
|
||||
current = "";
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
result.push(current.trim());
|
||||
return result;
|
||||
};
|
||||
|
||||
const headers = parseRow(lines[0]);
|
||||
const rows = lines.slice(1).map(parseRow);
|
||||
|
||||
const thead = `<thead><tr>${headers.map(h => `<th>${escapeHtml(h)}</th>`).join("")}</tr></thead>`;
|
||||
const tbody = `<tbody>${rows.map(row =>
|
||||
`<tr>${row.map(cell => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`
|
||||
).join("")}</tbody>`;
|
||||
|
||||
return `<div class="csv-container"><table class="csv-table">${thead}${tbody}</table></div>`;
|
||||
}
|
||||
|
||||
function renderAnsiContent(content) {
|
||||
const ansiColors = {
|
||||
"30": "#000", "31": "#e74c3c", "32": "#2ecc71", "33": "#f39c12",
|
||||
"34": "#3498db", "35": "#9b59b6", "36": "#1abc9c", "37": "#ecf0f1",
|
||||
"90": "#7f8c8d", "91": "#ff6b6b", "92": "#69db7c", "93": "#ffd43b",
|
||||
"94": "#74c0fc", "95": "#da77f2", "96": "#63e6be", "97": "#fff",
|
||||
};
|
||||
const bgColors = {
|
||||
"40": "#000", "41": "#e74c3c", "42": "#2ecc71", "43": "#f39c12",
|
||||
"44": "#3498db", "45": "#9b59b6", "46": "#1abc9c", "47": "#ecf0f1",
|
||||
};
|
||||
|
||||
let html = escapeHtml(content);
|
||||
html = html.replace(/(?:\x1b|\033)\[([\d;]*)m/g, (match, codes) => {
|
||||
if (!codes || codes === "0") return "</span>";
|
||||
const parts = codes.split(";");
|
||||
let style = "";
|
||||
for (const code of parts) {
|
||||
if (ansiColors[code]) style += `color:${ansiColors[code]};`;
|
||||
if (bgColors[code]) style += `background:${bgColors[code]};`;
|
||||
if (code === "1") style += "font-weight:bold;";
|
||||
if (code === "3") style += "font-style:italic;";
|
||||
if (code === "4") style += "text-decoration:underline;";
|
||||
}
|
||||
return style ? `<span style="${style}">` : "";
|
||||
});
|
||||
|
||||
return `<pre class="ansi-output">${html}</pre>`;
|
||||
}
|
||||
|
||||
async function createZipBlob(files) {
|
||||
const crc32 = (data) => {
|
||||
let crc = 0xFFFFFFFF;
|
||||
@@ -427,12 +693,15 @@ function buildIframeContent(content, contentType, theme, excluded = []) {
|
||||
margin: 16px 0;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
color: ${theme.fg};
|
||||
}
|
||||
ul, ol { padding-left: 24px; }
|
||||
li { margin: 4px 0; }
|
||||
h1:first-child, h2:first-child, h3:first-child, h4:first-child, h5:first-child, h6:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
ul, ol { padding-left: 24px; margin: 8px 0; }
|
||||
li { margin: 2px 0; }
|
||||
.list-item {
|
||||
background: rgba(0,0,0,0.2);
|
||||
border: 1px solid ${theme.border};
|
||||
@@ -501,6 +770,82 @@ function buildIframeContent(content, contentType, theme, excluded = []) {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.json-tree pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
font-size: 13px;
|
||||
}
|
||||
.json-key { color: #9cdcfe; }
|
||||
.json-string { color: #ce9178; }
|
||||
.json-number { color: #b5cea8; }
|
||||
.json-boolean { color: #569cd6; }
|
||||
.json-null { color: #569cd6; }
|
||||
.json-bracket { color: ${theme.fg}; }
|
||||
.json-toggle {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.json-toggle:hover { opacity: 0.8; }
|
||||
.json-count {
|
||||
color: #6a9955;
|
||||
font-size: 11px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.json-collapsible {
|
||||
margin-left: 16px;
|
||||
}
|
||||
.json-collapsible.collapsed {
|
||||
display: none;
|
||||
}
|
||||
.csv-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.csv-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
}
|
||||
.csv-table th, .csv-table td {
|
||||
border: 1px solid ${theme.border};
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
.csv-table th {
|
||||
background: rgba(0,0,0,0.3);
|
||||
font-weight: bold;
|
||||
}
|
||||
.csv-table tr:nth-child(even) {
|
||||
background: rgba(0,0,0,0.1);
|
||||
}
|
||||
.ansi-output {
|
||||
background: #1e1e1e;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-family: "Fira Code", Consolas, Monaco, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.mermaid {
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
}
|
||||
.math-block {
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.task-item {
|
||||
list-style: none;
|
||||
margin-left: -20px;
|
||||
}
|
||||
.task-item input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
accent-color: ${theme.accent};
|
||||
}
|
||||
.task-item.checked {
|
||||
color: ${theme.fg};
|
||||
}
|
||||
`;
|
||||
|
||||
const LIST_SEPARATOR = "\n---LIST_SEPARATOR---\n";
|
||||
@@ -578,6 +923,23 @@ function buildIframeContent(content, contentType, theme, excluded = []) {
|
||||
case "svg":
|
||||
bodyContent = `<div style="display:flex;justify-content:center;align-items:center;min-height:100%;padding:16px;">${content}</div>`;
|
||||
break;
|
||||
case "json":
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
bodyContent = `<div class="json-tree"><pre>${renderJsonTree(parsed)}</pre></div>`;
|
||||
} catch {
|
||||
bodyContent = renderCodeContent(content, "javascript");
|
||||
}
|
||||
break;
|
||||
case "csv":
|
||||
bodyContent = renderCsvTable(content);
|
||||
break;
|
||||
case "yaml":
|
||||
bodyContent = renderCodeContent(content, "yaml");
|
||||
break;
|
||||
case "ansi":
|
||||
bodyContent = renderAnsiContent(content);
|
||||
break;
|
||||
case "python":
|
||||
case "javascript":
|
||||
case "css":
|
||||
@@ -628,7 +990,7 @@ function buildIframeContent(content, contentType, theme, excluded = []) {
|
||||
.token.italic { font-style: italic; }
|
||||
`;
|
||||
|
||||
const needsPrism = ["python", "javascript", "css"].includes(contentType) ||
|
||||
const needsPrism = ["python", "javascript", "css", "yaml"].includes(contentType) ||
|
||||
content.includes("```") || content.includes("<code");
|
||||
|
||||
const prismScripts = needsPrism && STATE.prismScripts ? `
|
||||
@@ -636,13 +998,59 @@ function buildIframeContent(content, contentType, theme, excluded = []) {
|
||||
<script>document.addEventListener('DOMContentLoaded', () => Prism.highlightAll());<\/script>
|
||||
` : "";
|
||||
|
||||
const jsonScript = contentType === "json" ? `
|
||||
<script>
|
||||
function toggleJson(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.classList.toggle('collapsed');
|
||||
}
|
||||
<\/script>
|
||||
` : "";
|
||||
|
||||
const hasMermaid = /```(?:mermaid|flow|flex)/.test(content) || content.includes("<div class=\"mermaid\">");
|
||||
const mermaidScript = hasMermaid && STATE.mermaidScript ? `
|
||||
<script>${STATE.mermaidScript}<\/script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
mermaid.initialize({ startOnLoad: true, theme: 'dark' });
|
||||
});
|
||||
<\/script>
|
||||
` : "";
|
||||
|
||||
const hasLatex = content.includes("$$") || content.includes("\\(") || content.includes("\\[") || /\$[^$\n]+\$/.test(content);
|
||||
const katexStyles = hasLatex && STATE.katexCss ? `<style>${STATE.katexCss}</style>` : "";
|
||||
const katexScript = hasLatex && STATE.katexCore && STATE.katexAutoRender ? `
|
||||
<script>${STATE.katexCore}<\/script>
|
||||
<script>${STATE.katexAutoRender}<\/script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof renderMathInElement === 'function') {
|
||||
renderMathInElement(document.body, {
|
||||
delimiters: [
|
||||
{left: '$$', right: '$$', display: true},
|
||||
{left: '$', right: '$', display: false},
|
||||
{left: '\\\\(', right: '\\\\)', display: false},
|
||||
{left: '\\\\[', right: '\\\\]', display: true}
|
||||
],
|
||||
ignoredTags: ['script', 'noscript', 'style', 'textarea', 'option'],
|
||||
throwOnError: false
|
||||
});
|
||||
}
|
||||
});
|
||||
<\/script>
|
||||
` : "";
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>${baseStyles}${needsPrism ? prismTheme : ""}</style>
|
||||
${katexStyles}
|
||||
${prismScripts}
|
||||
${jsonScript}
|
||||
${mermaidScript}
|
||||
${katexScript}
|
||||
</head>
|
||||
<body>${bodyContent}</body>
|
||||
</html>`;
|
||||
@@ -1219,8 +1627,8 @@ function ensureElementsForNode(node) {
|
||||
`;
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.setAttribute("sandbox", "allow-scripts allow-forms allow-popups allow-modals allow-pointer-lock allow-downloads");
|
||||
iframe.setAttribute("allow", "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen");
|
||||
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-pointer-lock allow-downloads");
|
||||
iframe.setAttribute("allow", "fullscreen");
|
||||
iframe.setAttribute("allowfullscreen", "true");
|
||||
iframe.style.cssText = `
|
||||
position: absolute;
|
||||
@@ -1355,6 +1763,10 @@ function updateIframeContent(node, elements) {
|
||||
python: "Python",
|
||||
javascript: "JavaScript",
|
||||
css: "CSS",
|
||||
json: "JSON",
|
||||
yaml: "YAML",
|
||||
csv: "CSV",
|
||||
ansi: "Terminal",
|
||||
text: "Text",
|
||||
};
|
||||
elements.typeLabel.textContent = `Type: ${typeNames[contentType] || "Unknown"}`;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,(function(e){return function(){"use strict";var t={771:function(t){t.exports=e}},r={};function n(e){var i=r[e];if(void 0!==i)return i.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,n),a.exports}n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var i={};return function(){n.d(i,{default:function(){return s}});var e=n(771),t=n.n(e),r=function(e,t,r){for(var n=r,i=0,a=e.length;n<t.length;){var o=t[n];if(i<=0&&t.slice(n,n+a)===e)return n;"\\"===o?n++:"{"===o?i++:"}"===o&&i--,n++}return-1},a=/^\\begin{/,o=function(e,t){for(var n,i=[],o=new RegExp("("+t.map((function(e){return e.left.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&")})).join("|")+")");-1!==(n=e.search(o));){n>0&&(i.push({type:"text",data:e.slice(0,n)}),e=e.slice(n));var l=t.findIndex((function(t){return e.startsWith(t.left)}));if(-1===(n=r(t[l].right,e,t[l].left.length)))break;var d=e.slice(0,n+t[l].right.length),s=a.test(d)?d:e.slice(t[l].left.length,n);i.push({type:"math",data:s,rawData:d,display:t[l].display}),e=e.slice(n+t[l].right.length)}return""!==e&&i.push({type:"text",data:e}),i},l=function(e,r){var n=o(e,r.delimiters);if(1===n.length&&"text"===n[0].type)return null;for(var i=document.createDocumentFragment(),a=0;a<n.length;a++)if("text"===n[a].type)i.appendChild(document.createTextNode(n[a].data));else{var l=document.createElement("span"),d=n[a].data;r.displayMode=n[a].display;try{r.preProcess&&(d=r.preProcess(d)),t().render(d,l,r)}catch(e){if(!(e instanceof t().ParseError))throw e;r.errorCallback("KaTeX auto-render: Failed to parse `"+n[a].data+"` with ",e),i.appendChild(document.createTextNode(n[a].rawData));continue}i.appendChild(l)}return i},d=function e(t,r){for(var n=0;n<t.childNodes.length;n++){var i=t.childNodes[n];if(3===i.nodeType){for(var a=i.textContent,o=i.nextSibling,d=0;o&&o.nodeType===Node.TEXT_NODE;)a+=o.textContent,o=o.nextSibling,d++;var s=l(a,r);if(s){for(var f=0;f<d;f++)i.nextSibling.remove();n+=s.childNodes.length-1,t.replaceChild(s,i)}else n+=d}else 1===i.nodeType&&function(){var t=" "+i.className+" ";-1===r.ignoredTags.indexOf(i.nodeName.toLowerCase())&&r.ignoredClasses.every((function(e){return-1===t.indexOf(" "+e+" ")}))&&e(i,r)}()}},s=function(e,t){if(!e)throw new Error("No element provided to render");var r={};for(var n in t)t.hasOwnProperty(n)&&(r[n]=t[n]);r.delimiters=r.delimiters||[{left:"$$",right:"$$",display:!0},{left:"\\(",right:"\\)",display:!1},{left:"\\begin{equation}",right:"\\end{equation}",display:!0},{left:"\\begin{align}",right:"\\end{align}",display:!0},{left:"\\begin{alignat}",right:"\\end{alignat}",display:!0},{left:"\\begin{gather}",right:"\\end{gather}",display:!0},{left:"\\begin{CD}",right:"\\end{CD}",display:!0},{left:"\\[",right:"\\]",display:!0}],r.ignoredTags=r.ignoredTags||["script","noscript","style","textarea","pre","code","option"],r.ignoredClasses=r.ignoredClasses||[],r.errorCallback=r.errorCallback||console.error,r.macros=r.macros||{},d(e,r)}}(),i=i.default}()}));
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2029
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user