/** * HTML to markdown conversion for web_fetch, mirroring Claude's WebFetch, which * converts pages to markdown before the model reads them. * * A regex pipeline, not a DOM: pi ships no HTML parser and the output is prose * for a model, not a rendering. Every pattern bounds its tag matches with * [^<>]* so a failed match stops at the next tag instead of rescanning to the * end of input, keeping the pass linear on hostile pages. Bare tag removal is a * scanner rather than a regex: see removeTags. */ const NAMED_ENTITIES: Record = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' } function decodeAllEntities(text: string): string { return text.replace(/&(?:#x([0-9a-fA-F]+)|#(\d+)|(amp|lt|gt|quot|apos|nbsp));/g, (token, hex?: string, dec?: string, named?: string) => { if (named) return NAMED_ENTITIES[named] ?? token const code = hex ? Number.parseInt(hex, 16) : Number(dec) return Number.isFinite(code) && code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : token }) } /** The HTML tokenizer's tag-open rule: `<` starts a tag only before a letter, `/`, * `!`, or `?`; any other `<` (as in `1 < 2`) is text. */ const TAG_OPEN = /^<[A-Za-z/!?]/ /** * Drop every tag in one linear pass. A regex strip can rebuild a tag out of nested * brackets: `ipt>` loses `` and becomes `