/* markdown.js — Renderizador Markdown compacto (sem dependências). Cobre o que a documentação do projeto usa: títulos, listas, tabelas, blocos de código (```), código inline, negrito/itálico, links, citações, linhas horizontais e parágrafos. Faz escape de HTML para evitar injeção. */ const Markdown = (() => { // Marcador para proteger código inline durante o processamento. Usa U+E000 // (área de uso privado): não aparece em texto normal e sobrevive ao escape. const SENT = ""; function esc(s) { // Escapa também aspas: o href dos links é montado entre aspas duplas, então // um " no conteúdo poderia quebrar o atributo (injeção). return String(s) .replace(/&/g, "&").replace(//g, ">") .replace(/"/g, """); } // Formatação inline: código, negrito, itálico, links. Aplica escape antes. function inline(text) { // Protege trechos de código inline antes do escape geral. O marcador é // SENTSENT — um esquema baseado em espaços+dígitos colidia com qualquer // número cercado de espaços (" 8080 ") e quebrava a renderização. const codes = []; text = text.replace(/`([^`]+)`/g, (_, c) => { codes.push(c); return SENT + (codes.length - 1) + SENT; }); text = esc(text); // Links [texto](url) — só esquemas seguros; http(s) abre em nova aba. text = text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, t, href) => { // Link relativo para outro doc .md -> abre no próprio visualizador // (marcado com data-doc; o app intercepta o clique). const md = href.match(/(?:^|\/)([\w.-]+\.md)(?:#.*)?$/i); if (md && !/^https?:/i.test(href)) { return `${t}`; } const safe = /^(https?:|mailto:|file:|\/|\.\/|#)/i.test(href) ? href : "#"; const ext = /^https?:/i.test(safe) ? ' target="_blank" rel="noopener"' : ""; return `${t}`; }); // Negrito **...** text = text.replace(/\*\*([^*]+)\*\*/g, "$1"); // Itálico *...*: o conteúdo não pode começar/terminar com espaço. Isso // preserva globs em prosa (ex.: "*.gguf e *.bin" não vira itálico). text = text.replace(/(^|[^*])\*(\S(?:[^*]*?\S)?)\*(?!\*)/g, "$1$2"); // Sem itálico com _: colidiria com identificadores snake_case (model_key, // GPU, n_gpu_layers, ...), onipresentes na documentação. // Restaura o código inline. text = text.replace(new RegExp(SENT + "(\\d+)" + SENT, "g"), (_, i) => `${esc(codes[+i])}`); return text; } function render(md) { const lines = md.replace(/\r\n/g, "\n").split("\n"); const out = []; let i = 0; const closeList = (stack) => { while (stack.length) out.push(``); }; let listStack = []; while (i < lines.length) { let line = lines[i]; // Bloco de código cercado ``` const fence = line.match(/^```(.*)$/); if (fence) { closeList(listStack); const buf = []; i++; while (i < lines.length && !/^```/.test(lines[i])) { buf.push(lines[i]); i++; } i++; // pula o fechamento out.push(`
${esc(buf.join("\n"))}
`); continue; } // Linha horizontal if (/^(\s*[-*_]){3,}\s*$/.test(line) && !/^\s*[-*]\s/.test(line)) { closeList(listStack); out.push("
"); i++; continue; } // Títulos const h = line.match(/^(#{1,6})\s+(.*)$/); if (h) { closeList(listStack); const lvl = h[1].length; out.push(`${inline(h[2].trim())}`); i++; continue; } // Tabela (linha com | e a próxima de separação ---) if (/\|/.test(line) && i + 1 < lines.length && /^\s*\|?[\s:|-]+\|[\s:|-]*$/.test(lines[i + 1]) && /-/.test(lines[i + 1])) { closeList(listStack); const splitRow = (r) => r.replace(/^\s*\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim()); const header = splitRow(line); i += 2; let html = '' + header.map((c) => ``).join("") + ""; while (i < lines.length && /\|/.test(lines[i]) && lines[i].trim() !== "") { const cells = splitRow(lines[i]); html += "" + cells.map((c) => ``).join("") + ""; i++; } html += "
${inline(c)}
${inline(c)}
"; out.push(html); continue; } // Citação if (/^\s*>\s?/.test(line)) { closeList(listStack); const buf = []; while (i < lines.length && /^\s*>\s?/.test(lines[i])) { buf.push(lines[i].replace(/^\s*>\s?/, "")); i++; } out.push(`
${inline(buf.join(" "))}
`); continue; } // Listas (ordenada/não ordenada), 1 nível const ul = line.match(/^(\s*)[-*+]\s+(.*)$/); const ol = line.match(/^(\s*)\d+\.\s+(.*)$/); if (ul || ol) { const tag = ul ? "ul" : "ol"; if (listStack[listStack.length - 1] !== tag) { closeList(listStack); out.push(`<${tag}>`); listStack.push(tag); } out.push(`
  • ${inline((ul || ol)[2])}
  • `); i++; continue; } // Linha em branco if (line.trim() === "") { closeList(listStack); i++; continue; } // Parágrafo (junta linhas consecutivas) closeList(listStack); const para = [line]; i++; while (i < lines.length && lines[i].trim() !== "" && !/^(#{1,6}\s|```|\s*>|\s*[-*+]\s|\s*\d+\.\s)/.test(lines[i])) { para.push(lines[i]); i++; } out.push(`

    ${inline(para.join(" "))}

    `); } closeList(listStack); return out.join("\n"); } return { render }; })();