// The blocks an environment's sections are made of (app/envs/contract.py): one renderer for every format, so a Harbor // task, a MiMo row and a NeMo Gym row are drawn by the same code. Text is escaped; inline Markdown is `code`, **bold** // and [links](https://...). A `custom` block is drawn by its renderer module (web/js/renderers/.js), loaded // first with loadRenderers(); it exports render(kind, data, ctx) -> HTML and, optionally, wire(root, ctx). import { esc, fmt, md } from "./util.js"; import { icon } from "./icons.js"; import { value, object } from "./render.js"; import { codeBlock } from "./files.js"; export function inline(text) { return esc(String(text ?? "")) .replace(/`([^`]+)`/g, "$1") .replace(/\*\*([^*]+)\*\*/g, "$1") .replace(/\[([^\]]+)\]\((https:\/\/[^)\s]+)\)/g, '$1'); } const pct = (n, d) => (d ? `${Math.round((100 * n) / d)}%` : "–"); function shares(b) { return `${b.rows.map(([label, n]) => `
${inline(label)} ${fmt.format(n)} · ${pct(n, b.of)}
`).join("")} ${b.note ? `
${inline(b.note)}
` : ""}`; } const cell = (v) => (v && typeof v === "object" && v.type === "shares" ? shares(v) : typeof v === "number" ? esc(fmt.format(v)) : inline(v)); // a section's blocks as HTML; `ctx.files` collects the file-viewer slots for the page to mount, `ctx.paths` (the files // they list) lets a code block offer to open its file there export function blocks(list, ctx = { files: [] }) { return (list || []).map((b) => { switch (b.type) { case "kv": return b.rows.length ? `
${b.rows.map(([k, v]) => `
${esc(k)}
${cell(v)}
`).join("")}
` : ""; case "markdown": return b.text?.trim() ? `
${md(b.text)}
` : `

Empty.

`; case "code": return `${b.label ? `
${esc(b.label)}${ctx.paths?.has(b.label) ? ` ` : ""}
` : ""}${codeBlock(b.text, b.path || b.label || "", { tall: true })}`; // the text is one flex item (the note is a row: icon, text), so its inline code wraps with the words case "note": return `

${icon(b.icon || "info", 13)}${inline(b.text)}

`; case "value": return b.value && typeof b.value === "object" && !Array.isArray(b.value) ? object(b.value, 0) : value(b.value); case "messages": return value(b.value, "messages"); case "steps": return `
    ${b.items.map(([n, t]) => `
  1. ${esc(n)} ${inline(t)}
  2. `).join("")}
`; case "shares": return shares(b); case "disclose": return `
${icon("chevronRight", 14, "chev")}${esc(b.label)}${blocks(b.blocks, ctx)}
`; case "files": { ctx.files.push(b); return `
`; } case "stats": return b.rows.length ? `
${b.rows.map(([k, v]) => `
${esc(k)}${inline(v)}
`).join("")}
` : ""; case "links": return ``; case "custom": { const r = ctx.renderers?.[b.renderer]; return r ? `
${r.render(b.kind, b.data, ctx)}
` : (b.text ? `
${md(b.text)}
` : ""); } default: return ""; } }).join(""); } // the renderer modules a page's blocks need, by name (web/js/renderers/.js) export async function loadRenderers(sections) { const names = new Set(); const walk = (list) => (list || []).forEach((b) => { if (b.type === "custom" && /^[a-z0-9-]{1,40}$/.test(b.renderer)) names.add(b.renderer); if (b.type === "disclose") walk(b.blocks); }); (sections || []).forEach((s) => walk(s.blocks)); const out = {}; await Promise.all([...names].map(async (n) => { try { out[n] = await import(`./renderers/${n}.js`); } catch { /* drawn from its text instead */ } })); return out; }