/**
* 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 `