/** * Opt-in HTML helpers for the chrischall MCP fleet. * * Isolated behind the `@chrischall/mcp-utils/html` subpath because it pulls the * heavy `node-html-parser` dependency — lean API-only MCPs shouldn't pay for it. * * Consolidates the HTML-scraping primitives that were independently * re-implemented across the realty cohort (homes / redfin / compass / * zillow / onehome) and a couple of content MCPs (opentable, infinitecampus): * * - `parsePropertyTable` / `findLinksUnderHeading` — heading-anchored DOM * scraping for the SSR property-detail tables and link lists. * - `extractJsonFromHtml` — the balanced-brace `__INITIAL_STATE__` walker * (opentable / compass / realty); regex can't handle nested objects and * escaped strings. * - `extractPlainTextFromHtml` — dependency-free script/style strip + entity * decode used to render Infinite Campus message bodies as plain text. * - `urlToPath` / `locationToSlug` / `buildIdExtractor` — the small URL atoms * that were byte-identical across the cohort. */ import { type HTMLElement } from 'node-html-parser'; export type { HTMLElement }; /** A scraped HTML table: column headers plus row-major cell text. */ export interface PropertyTable { /** Trimmed text of each header cell, in document order. */ headers: string[]; /** Each body row as an array of trimmed cell strings, in document order. */ rows: string[][]; } /** * Locate the `` under the heading matching `heading` and return its * column headers and body rows. * * Header cells are scoped to `` when present (falling back to all `
` * in the table). Body rows collect **both** `` and `` because the realty * portals use `` for the leading cell of every data row (the * year / date column) — dropping those would silently shift every column left * and corrupt the parsed record. * * Cell text has internal whitespace collapsed to single spaces and is trimmed. * * @param html Raw page HTML (or an HTML fragment). * @param heading Case-insensitive substring of the heading above the table. * @returns The parsed table, or `null` when no matching heading+table is found. * * @example parsePropertyTable(detailHtml, 'Tax History') * // { headers: ['Year', 'Property Tax'], rows: [['2023', '$1,200'], ...] } */ export declare function parsePropertyTable(html: string | HTMLElement, heading: string): PropertyTable | null; /** * Find every `` (or `selector`-matching element) that follows the first * heading matching `heading`, up to — but not including — the next sibling * heading. Useful for the "Homes for Sale Near" link lists at the bottom of a * detail page. * * Collects both direct-sibling anchors and anchors nested inside the * intervening sibling elements (lists, cards), in document order. * * @param root Raw HTML string or an already-parsed root element. * @param heading Case-insensitive substring of the heading to anchor on. * @param selector CSS selector for the elements to collect (default `'a'`). * @returns The matching elements, or an empty array when the heading is absent. */ export declare function findLinksUnderHeading(root: string | HTMLElement, heading: string, selector?: string): HTMLElement[]; /** * Extract an embedded JSON state object from a server-rendered HTML page by * walking the balanced brace/string structure after a marker. * * Handles both rendering forms seen across the fleet: * 1. `window.__INITIAL_STATE__ = {...};` — a JS assignment in a `