// Pure, LLM-free formatting for the intro/body of a gate widget. // The body arrives as a plain string with blank-line-separated paragraphs and // bullet lines prefixed by "•", "-" or "*". We fold it into structured blocks // so the renderer can draw a real list (hanging indent) and emphasise the // leading term of each bullet. export type IntroBlock = | { kind: "para"; text: string } | { kind: "list"; items: string[] }; const BULLET_RE = /^\s*[•\-*]\s+/; /** Fold raw intro text into paragraph and list blocks. */ export function parseIntroBlocks(intro: string): IntroBlock[] { const lines = intro.replace(/\r\n/g, "\n").split("\n"); const blocks: IntroBlock[] = []; let para: string[] = []; let list: string[] = []; const flushPara = () => { if (para.length) { blocks.push({ kind: "para", text: para.join(" ").trim() }); para = []; } }; const flushList = () => { if (list.length) { blocks.push({ kind: "list", items: list }); list = []; } }; for (const raw of lines) { const line = raw.trimEnd(); if (BULLET_RE.test(line)) { flushPara(); list.push(line.replace(BULLET_RE, "").trim()); } else if (line.trim() === "") { flushPara(); flushList(); } else { flushList(); para.push(line.trim()); } } flushPara(); flushList(); return blocks; } // Separators that mark the end of a leading term ("term → definition", // "term: definition"). Ordered longest-first is not needed since indexOf picks // the earliest match across all of them. const LEAD_SEPARATORS = [" → ", " -> ", " — ", " – ", ": "]; /** * Split a bullet into its leading term (to emphasise) and the remainder, * or return null when there is no clear short lead worth bolding. */ export function splitLead(item: string): { lead: string; rest: string } | null { let at = -1; for (const sep of LEAD_SEPARATORS) { const idx = item.indexOf(sep); if (idx > 0 && (at === -1 || idx < at)) at = idx; } if (at === -1) return null; const lead = item.slice(0, at); // Only emphasise a genuinely short lead: a label, not half a sentence. if (lead.length > 60 || lead.trim().split(/\s+/).length > 6) return null; return { lead, rest: item.slice(at) }; }