/**
* HTML entity decoding shared by both halves of web-access.
*
* `web_fetch` decodes entities while turning HTML into readable text, and
* `web_search` decodes them in provider snippets. The table lives here because
* the two halves do not import each other.
*/
const NAMED_ENTITIES: Record = {
amp: "&",
lt: "<",
gt: ">",
quot: '"',
apos: "'",
nbsp: " ",
hellip: "…",
mdash: "—",
ndash: "–",
};
/** Strip tags (replacing each with `separator`) and decode entities. */
export function stripTagsAndDecode(html: string, separator = ""): string {
return decodeEntities(html.replace(/<[^>]*>/g, separator));
}
/** Decode named and numeric HTML entities. Unknown entities are left as-is. */
export function decodeEntities(input: string): string {
return input.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (match, body: string) => {
if (body.startsWith("#")) {
const hex = body[1] === "x" || body[1] === "X";
const code = Number.parseInt(body.slice(hex ? 2 : 1), hex ? 16 : 10);
if (!Number.isFinite(code) || code < 0 || code > 0x10ffff) return match;
try {
return String.fromCodePoint(code);
} catch {
return match;
}
}
return NAMED_ENTITIES[body.toLowerCase()] ?? match;
});
}