]*>([\s\S]*?)<\/table>|]*>([\s\S]*?)<\/p>/gi;
for (const m of html.matchAll(re)) {
const [whole, h2, h3, h4, table, p] = m;
if (h2 !== undefined) {
nodes.push({ kind: "h2", text: text(h2) });
} else if (h3 !== undefined) {
nodes.push({ kind: "h3", text: text(h3) });
} else if (h4 !== undefined) {
nodes.push({ kind: "h4", text: text(h4) });
} else if (table !== undefined) {
nodes.push(readTable(table));
} else if (p !== undefined) {
nodes.push({ kind: "p", text: text(p), isButton: /a-btn/.test(whole) });
}
}
return nodes;
}
function readTable(body: string): TableNode {
const headers: string[] = [];
for (const m of body.matchAll(/
]*>([\s\S]*?)<\/th>/gi)) {
headers.push(text(m[1] ?? ""));
}
// Label/value rows: a | immediately followed by a | .
const pairs: Array<{ label: string; value: string }> = [];
const pairRe = / | ]*>([\s\S]*?)<\/th>\s* | ]*>([\s\S]*?)<\/td>/gi;
for (const m of body.matchAll(pairRe)) {
pairs.push({ label: text(m[1] ?? ""), value: text(m[2] ?? "") });
}
// Row/cell grid, for column-oriented tables.
const rows: string[][] = [];
for (const tr of body.matchAll(/ | ]*>([\s\S]*?)<\/tr>/gi)) {
const cells: string[] = [];
for (const cell of (tr[1] ?? "").matchAll(
/]*>([\s\S]*?)<\/t\1>/gi,
)) {
cells.push(text(cell[2] ?? ""));
}
if (cells.length > 0) rows.push(cells);
}
return { kind: "table", pairs, headers, rows };
}
/** True when the document contains no nested elements. */
export function assertFlatTables(rawHtml: string): boolean {
let depth = 0;
for (const m of rawHtml.matchAll(/]*>|<\/table>/gi)) {
depth += m[0].startsWith("") ? -1 : 1;
if (depth > 1) return false;
}
return depth === 0;
}