| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497 |
- /* global importScripts, marked, hljs */
- let librariesLoaded = false;
- let markedConfigured = false;
- let mermaidIdCounter = 0;
- const markedOptions = {
- gfm: true,
- breaks: true,
- pedantic: false,
- sanitize: false,
- smartypants: false,
- xhtml: false,
- headerIds: true,
- mangle: false,
- };
- const BLOCK_MATH_MARKER_PATTERN = /^\$\$/m;
- const BLOCK_MATH_PATTERN = /^\$\$[ \t]*\n?([\s\S]*?)\n?\$\$[ \t]*(?:\n|$)/;
- const DEFINITION_LIST_ITEM_PATTERN = /^:[ \t]+(.*)$/;
- const SUPERSCRIPT_PATTERN = /^\^(?!\s)([^^\n]*?\S)\^(?!\^)/;
- const SUBSCRIPT_PATTERN = /^~(?!~)(?!\s)([^~\n]*?\S)~(?!~)/;
- const HIGHLIGHT_PATTERN = /^==(?=\S)([\s\S]*?\S)==/;
- const MARKDOWN_LIST_MARKER_PATTERN = /^(\s*)(?:[-*+]\s+|\d+\.\s+|>\s+)/;
- const EMPTY_LINE_PATTERN = /^\s*$/;
- let suppressFootnotePreprocess = false;
- const footnoteDefinitions = new Map();
- const footnoteOrder = [];
- const footnoteRefCounts = new Map();
- const footnoteFirstRefId = new Map();
- let anonymousFootnoteCounter = 0;
- function escapeHtml(str) {
- return String(str)
- .replace(/&/g, "&")
- .replace(/</g, "<")
- .replace(/>/g, ">")
- .replace(/"/g, """);
- }
- function escapeHtmlAttribute(value) {
- return String(value)
- .replace(/&/g, "&")
- .replace(/"/g, """)
- .replace(/'/g, "'")
- .replace(/</g, "<")
- .replace(/>/g, ">");
- }
- function resetExtendedMarkdownState() {
- footnoteDefinitions.clear();
- footnoteOrder.length = 0;
- footnoteRefCounts.clear();
- footnoteFirstRefId.clear();
- anonymousFootnoteCounter = 0;
- }
- function normalizeFootnoteId(id) {
- const normalized = String(id || "")
- .trim()
- .toLowerCase()
- .replace(/[^a-z0-9_-]+/g, "-")
- .replace(/^-+|-+$/g, "");
- if (normalized) return normalized;
- anonymousFootnoteCounter += 1;
- return `footnote-${anonymousFootnoteCounter}`;
- }
- function parseInlineWithoutFootnotes(text) {
- suppressFootnotePreprocess = true;
- try {
- return marked.parseInline(text);
- } finally {
- suppressFootnotePreprocess = false;
- }
- }
- function renderDefinitionContent(content, options) {
- const appendHtml = options && options.appendHtml ? options.appendHtml : "";
- const paragraphs = String(content || "")
- .split(/\n(?:[ \t]*\n)+/)
- .map((paragraph) => paragraph.trim())
- .filter(Boolean);
- if (appendHtml) {
- if (paragraphs.length === 0) {
- paragraphs.push(appendHtml);
- } else {
- paragraphs[paragraphs.length - 1] = `${paragraphs[paragraphs.length - 1]} ${appendHtml}`;
- }
- }
- return paragraphs
- .map((paragraph) => `<p>${parseInlineWithoutFootnotes(paragraph)}</p>`)
- .join("");
- }
- function extractFootnoteDefinitions(markdown) {
- const lines = markdown.split("\n");
- const preservedLines = [];
- let index = 0;
- while (index < lines.length) {
- const match = /^([ \t]{0,3})\[\^([^\]\n]+)\]:[ \t]*(.*)$/.exec(lines[index]);
- if (!match) {
- preservedLines.push(lines[index]);
- index += 1;
- continue;
- }
- const baseIndent = match[1] || "";
- const id = match[2].trim();
- const definitionLines = [match[3] || ""];
- index += 1;
- while (index < lines.length) {
- const line = lines[index];
- if (!line.startsWith(baseIndent)) break;
- const lineAfterBase = line.slice(baseIndent.length);
- const indentedMatch = /^(?: {2,}|\t)(.*)$/.exec(lineAfterBase);
- if (indentedMatch) {
- definitionLines.push(indentedMatch[1]);
- index += 1;
- continue;
- }
- if (lineAfterBase.trim() === "") {
- const nextLine = lines[index + 1] || "";
- const nextAfterBase = nextLine.startsWith(baseIndent) ? nextLine.slice(baseIndent.length) : "";
- if (/^(?: {2,}|\t)/.test(nextAfterBase)) {
- definitionLines.push("");
- index += 1;
- continue;
- }
- }
- break;
- }
- footnoteDefinitions.set(id, definitionLines.join("\n").trim());
- }
- return preservedLines.join("\n");
- }
- function applyFootnotes(markdown) {
- const markdownWithReferences = markdown.replace(/\[\^([^\]\n]+)\]/g, function(match, idText) {
- const id = idText.trim();
- if (!id) return match;
- if (!footnoteOrder.includes(id)) footnoteOrder.push(id);
- const refCount = (footnoteRefCounts.get(id) || 0) + 1;
- footnoteRefCounts.set(id, refCount);
- const normalizedId = normalizeFootnoteId(id);
- const refId = `fnref-${normalizedId}${refCount > 1 ? `-${refCount}` : ""}`;
- if (!footnoteFirstRefId.has(id)) footnoteFirstRefId.set(id, refId);
- const noteNumber = footnoteOrder.indexOf(id) + 1;
- return `<sup id="${escapeHtmlAttribute(refId)}" class="footnote-ref"><a href="#fn-${escapeHtmlAttribute(normalizedId)}" aria-label="Footnote ${noteNumber}">[${noteNumber}]</a></sup>`;
- });
- const footnotesHtml = footnoteOrder
- .filter((id) => footnoteDefinitions.has(id))
- .map((id) => {
- const normalizedId = normalizeFootnoteId(id);
- const backRefId = footnoteFirstRefId.get(id) || `fnref-${normalizedId}`;
- const backRefHtml = `<a href="#${escapeHtmlAttribute(backRefId)}" class="footnote-backref" aria-label="Back to content">←</a>`;
- const noteHtml = renderDefinitionContent(footnoteDefinitions.get(id) || "", { appendHtml: backRefHtml });
- return `<li id="fn-${escapeHtmlAttribute(normalizedId)}">${noteHtml}</li>`;
- })
- .join("");
- if (!footnotesHtml) return markdownWithReferences;
- return `${markdownWithReferences}\n\n<section class="footnotes"><hr><ol>${footnotesHtml}</ol></section>`;
- }
- function configureMarked() {
- if (markedConfigured) return;
- const renderer = new marked.Renderer();
- const blockMathExtension = {
- name: "blockMath",
- level: "block",
- start(src) {
- const match = src.match(BLOCK_MATH_MARKER_PATTERN);
- return match ? match.index : undefined;
- },
- tokenizer(src) {
- const match = BLOCK_MATH_PATTERN.exec(src);
- if (!match) return undefined;
- return { type: "blockMath", raw: match[0], text: match[1] };
- },
- renderer(token) {
- return `<div class="math-block">$$\n${token.text}\n$$</div>\n`;
- },
- };
- const definitionListExtension = {
- name: "definitionList",
- level: "block",
- start(src) {
- const match = src.match(/\n:[ \t]+/);
- return match ? match.index + 1 : undefined;
- },
- tokenizer(src) {
- const lines = src.split("\n");
- if (lines.length < 2) return undefined;
- const term = lines[0];
- if (EMPTY_LINE_PATTERN.test(term) || MARKDOWN_LIST_MARKER_PATTERN.test(term)) return undefined;
- if (!DEFINITION_LIST_ITEM_PATTERN.test(lines[1])) return undefined;
- const definitions = [];
- const rawLines = [term];
- let index = 1;
- while (index < lines.length) {
- const itemMatch = DEFINITION_LIST_ITEM_PATTERN.exec(lines[index]);
- if (!itemMatch) break;
- rawLines.push(lines[index]);
- const definitionLines = [itemMatch[1]];
- index += 1;
- while (index < lines.length) {
- const line = lines[index];
- if (DEFINITION_LIST_ITEM_PATTERN.test(line)) break;
- if (EMPTY_LINE_PATTERN.test(line)) {
- const nextLine = lines[index + 1] || "";
- if (/^(?: {2,}|\t)/.test(nextLine)) {
- rawLines.push(line);
- definitionLines.push("");
- index += 1;
- continue;
- }
- break;
- }
- const continuationMatch = /^(?: {2,}|\t)(.*)$/.exec(line);
- if (!continuationMatch) break;
- rawLines.push(line);
- definitionLines.push(continuationMatch[1]);
- index += 1;
- }
- definitions.push(definitionLines.join("\n").trim());
- }
- if (definitions.length === 0) return undefined;
- let raw = rawLines.join("\n");
- if (src.startsWith(raw + "\n")) raw += "\n";
- return { type: "definitionList", raw, term: term.trim(), definitions };
- },
- renderer(token) {
- const termHtml = parseInlineWithoutFootnotes(token.term);
- const definitionHtml = token.definitions
- .map((definition) => `<dd>${renderDefinitionContent(definition)}</dd>`)
- .join("");
- return `<dl><dt>${termHtml}</dt>${definitionHtml}</dl>\n`;
- },
- };
- const superscriptExtension = {
- name: "superscript",
- level: "inline",
- start(src) {
- const index = src.indexOf("^");
- return index >= 0 ? index : undefined;
- },
- tokenizer(src) {
- const match = SUPERSCRIPT_PATTERN.exec(src);
- return match ? { type: "superscript", raw: match[0], text: match[1] } : undefined;
- },
- renderer(token) {
- return `<sup>${marked.parseInline(token.text)}</sup>`;
- },
- };
- const subscriptExtension = {
- name: "subscript",
- level: "inline",
- start(src) {
- const index = src.indexOf("~");
- return index >= 0 ? index : undefined;
- },
- tokenizer(src) {
- const match = SUBSCRIPT_PATTERN.exec(src);
- return match ? { type: "subscript", raw: match[0], text: match[1] } : undefined;
- },
- renderer(token) {
- return `<sub>${marked.parseInline(token.text)}</sub>`;
- },
- };
- const highlightExtension = {
- name: "highlight",
- level: "inline",
- start(src) {
- const index = src.indexOf("==");
- return index >= 0 ? index : undefined;
- },
- tokenizer(src) {
- const match = HIGHLIGHT_PATTERN.exec(src);
- return match ? { type: "highlight", raw: match[0], text: match[1] } : undefined;
- },
- renderer(token) {
- return `<mark>${marked.parseInline(token.text)}</mark>`;
- },
- };
- renderer.code = function(code, language) {
- if (language === "mermaid") {
- const uniqueId = `mermaid-diagram-worker-${mermaidIdCounter++}`;
- return `<div class="mermaid-container is-loading"><div class="mermaid" id="${uniqueId}" data-original-code="${encodeURIComponent(code)}">${escapeHtml(code)}</div></div>`;
- }
- const validLanguage = hljs && hljs.getLanguage(language) ? language : "plaintext";
- const highlightedCode = hljs
- ? hljs.highlight(code, { language: validLanguage }).value
- : escapeHtml(code);
- return `<pre><code class="hljs ${escapeHtmlAttribute(validLanguage)}">${highlightedCode}</code></pre>`;
- };
- renderer.heading = function(text, level, raw) {
- let id = raw
- .toLowerCase()
- .trim()
- .replace(/<[^>]*>/g, '')
- .replace(/\s+/g, '-')
- .replace(/[^\w-]/g, '')
- .replace(/-+/g, '-');
- if (!id) {
- id = `heading-worker-${Math.random().toString(36).substr(2, 9)}`;
- }
- return `<h${level} id="${id}">${text}</h${level}>`;
- };
- marked.use({
- extensions: [
- blockMathExtension,
- definitionListExtension,
- superscriptExtension,
- subscriptExtension,
- highlightExtension,
- ],
- hooks: {
- preprocess(markdown) {
- if (suppressFootnotePreprocess) return markdown;
- resetExtendedMarkdownState();
- const protectedMarkdown = markdown.replace(/\\\$/g, "$");
- return applyFootnotes(extractFootnoteDefinitions(protectedMarkdown));
- },
- },
- });
- marked.setOptions(Object.assign({}, markedOptions, { renderer }));
- markedConfigured = true;
- }
- function ensureLibraries(urls) {
- if (!librariesLoaded) {
- importScripts(urls.marked, urls.highlight);
- librariesLoaded = true;
- }
- configureMarked();
- }
- function isSegmentedPreviewSafe(markdown) {
- if (/^\s*---\r?\n[\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;
- if (/^\s{0,3}<\/?[a-zA-Z][\w:-]*(?:\s|>|\/>)/m.test(markdown)) return false;
- return true;
- }
- function hashString(value) {
- let hash = 2166136261;
- for (let i = 0; i < value.length; i += 1) {
- hash ^= value.charCodeAt(i);
- hash = Math.imul(hash, 16777619);
- }
- return (hash >>> 0).toString(36);
- }
- function splitMarkdownBlocks(markdown) {
- const normalized = String(markdown || "").replace(/\r\n/g, "\n");
- const lines = normalized.split("\n");
- const blocks = [];
- let buffer = [];
- let startLine = 1;
- let inFence = false;
- let fenceChar = "";
- let fenceLength = 0;
- let inMathBlock = false;
- function flush(endLine) {
- const source = buffer.join("\n").trimEnd();
- if (source.trim()) {
- blocks.push({
- source,
- startLine,
- endLine,
- });
- }
- buffer = [];
- }
- for (let index = 0; index < lines.length; index += 1) {
- const line = lines[index];
- const lineNumber = index + 1;
- const fenceMatch = /^ {0,3}(`{3,}|~{3,})/.exec(line);
- const trimmed = line.trim();
- if (fenceMatch) {
- const marker = fenceMatch[1];
- if (!inFence) {
- inFence = true;
- fenceChar = marker[0];
- fenceLength = marker.length;
- } else if (marker[0] === fenceChar && marker.length >= fenceLength) {
- inFence = false;
- }
- }
- if (!inFence && trimmed === "$$") {
- inMathBlock = !inMathBlock;
- }
- if (!inFence && !inMathBlock && trimmed === "") {
- flush(lineNumber);
- startLine = lineNumber + 1;
- continue;
- }
- if (buffer.length === 0) startLine = lineNumber;
- buffer.push(line);
- }
- flush(lines.length);
- return blocks;
- }
- function renderSegmentedMarkdown(markdown, options) {
- if (!isSegmentedPreviewSafe(markdown)) {
- return { mode: "full-required", reason: "unsafe-markdown" };
- }
- const blocks = splitMarkdownBlocks(markdown);
- if (blocks.length < (options.minimumBlocks || 1)) {
- return { mode: "full-required", reason: "too-few-blocks" };
- }
- const seenHashes = new Map();
- const renderedBlocks = blocks.map((block) => {
- const hash = hashString(block.source);
- const seenCount = seenHashes.get(hash) || 0;
- seenHashes.set(hash, seenCount + 1);
- const html = marked.parse(block.source);
- return {
- id: `preview-block-${hash}-${seenCount}`,
- hash,
- html,
- htmlLength: html.length,
- sourceLength: block.source.length,
- startLine: block.startLine,
- endLine: block.endLine,
- };
- });
- return {
- mode: "segmented",
- blocks: renderedBlocks,
- blockCount: renderedBlocks.length,
- };
- }
- self.onmessage = function(event) {
- const data = event.data || {};
- if (data.type !== "render") return;
- try {
- const options = data.options || {};
- ensureLibraries(options.libraryUrls || {});
- mermaidIdCounter = 0;
- const result = renderSegmentedMarkdown(data.markdown || "", options);
- self.postMessage({
- type: "render-result",
- requestId: data.requestId,
- result,
- });
- } catch (error) {
- self.postMessage({
- type: "render-error",
- requestId: data.requestId,
- error: error && error.message ? error.message : "Preview worker render failed.",
- });
- }
- };
|