${escapeHtml(code)}`));
+ rendered = rendered.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) => {
+ const href = sanitizeUrl(url);
+ if (!href) {
+ return `${label} (${url})`;
+ }
+ return addPlaceholder(
+ `${escapeHtml(label)}`
+ );
+ });
+
+ rendered = escapeHtml(rendered);
+ rendered = rendered.replace(/\*\*([^*]+)\*\*/g, "$1");
+ rendered = rendered.replace(/\*([^*]+)\*/g, "$1");
+ rendered = rendered.replace(/_([^_]+)_/g, "$1");
+
+ for (const placeholder of placeholders) {
+ rendered = rendered.replaceAll(placeholder.token, placeholder.html);
+ }
+
+ return rendered;
+}
+
+function renderMarkdown(text) {
+ const lines = String(text).replace(/\r\n?/g, "\n").split("\n");
+ const html = [];
+ let paragraphLines = [];
+ let quoteLines = [];
+ let listType = null;
+ let listItems = [];
+
+ const flushParagraph = () => {
+ if (!paragraphLines.length) return;
+ html.push(`${renderInlineMarkdown(paragraphLines.join(" "))}
`); + paragraphLines = []; + }; + + const flushQuote = () => { + if (!quoteLines.length) return; + const quoteBody = quoteLines.map((line) => renderInlineMarkdown(line)).join("`); + quoteLines = []; + }; + + const flushList = () => { + if (!listItems.length || !listType) return; + const items = listItems.map((item) => `${quoteBody}
${escapeHtml(codeLines.join("\n"))}`);
+ continue;
+ }
+
+ if (!trimmed) {
+ flushAll();
+ continue;
+ }
+
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
+ if (headingMatch) {
+ flushAll();
+ const level = headingMatch[1].length;
+ html.push(`