/* 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, """); } function escapeHtmlAttribute(value) { return String(value) .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) => `

${parseInlineWithoutFootnotes(paragraph)}

`) .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 `[${noteNumber}]`; }); const footnotesHtml = footnoteOrder .filter((id) => footnoteDefinitions.has(id)) .map((id) => { const normalizedId = normalizeFootnoteId(id); const backRefId = footnoteFirstRefId.get(id) || `fnref-${normalizedId}`; const backRefHtml = ``; const noteHtml = renderDefinitionContent(footnoteDefinitions.get(id) || "", { appendHtml: backRefHtml }); return `
  • ${noteHtml}
  • `; }) .join(""); if (!footnotesHtml) return markdownWithReferences; return `${markdownWithReferences}\n\n

      ${footnotesHtml}
    `; } 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 `
    $$\n${token.text}\n$$
    \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) => `
    ${renderDefinitionContent(definition)}
    `) .join(""); return `
    ${termHtml}
    ${definitionHtml}
    \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 `${marked.parseInline(token.text)}`; }, }; 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 `${marked.parseInline(token.text)}`; }, }; 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 `${marked.parseInline(token.text)}`; }, }; renderer.code = function(code, language) { if (language === "mermaid") { const uniqueId = `mermaid-diagram-worker-${mermaidIdCounter++}`; return `
    ${escapeHtml(code)}
    `; } const validLanguage = hljs && hljs.getLanguage(language) ? language : "plaintext"; const highlightedCode = hljs ? hljs.highlight(code, { language: validLanguage }).value : escapeHtml(code); return `
    ${highlightedCode}
    `; }; 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 `${text}`; }; 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.", }); } };