/* 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 ``;
});
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`;
}
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 ``;
}
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.",
});
}
};