const CHART_HEADING_RE = /^(?:D\d{1,3}\b|Moon Chart)/; type HastProperties = { className?: string | string[] | null; [key: string]: unknown; }; type HastNode = { type: string; tagName?: string; value?: string; properties?: HastProperties; children?: HastNode[]; }; type HastRoot = { type: "root"; children: HastNode[]; }; function classNames(node: HastNode | undefined): string[] { const value = node?.properties?.className; if (Array.isArray(value)) return value.map(String); if (typeof value === "string") return value.split(/\s+/).filter(Boolean); return []; } function hasClass(node: HastNode | undefined, name: string): boolean { return classNames(node).includes(name); } function isWhitespace(node: HastNode | undefined): boolean { return Boolean(node && node.type === "text" && !String(node.value ?? "").trim()); } function flattenText(node: HastNode | undefined): string { if (!node) return ""; if (node.type === "text") return String(node.value ?? ""); if (!Array.isArray(node.children)) return ""; return node.children.map(flattenText).join(""); } function isElement(node: HastNode | undefined, tagName: string): node is HastNode { return Boolean(node && node.type === "element" && node.tagName === tagName); } function isChartHeading(node: HastNode | undefined): boolean { return isElement(node, "h4") && CHART_HEADING_RE.test(flattenText(node).trim()); } function isChartPre(node: HastNode | undefined): boolean { if (!isElement(node, "pre") || !Array.isArray(node.children)) return false; const code = node.children.find((child) => isElement(child, "code")); return hasClass(code, "language-jyotish-chart") || hasClass(code, "jyotish-chart"); } function skipWhitespace(nodes: readonly HastNode[], index: number): number { let cursor = index; while (cursor < nodes.length && isWhitespace(nodes[cursor])) cursor += 1; return cursor; } function element(tagName: string, className: string[], children: HastNode[]): HastNode { return { type: "element", tagName, properties: { className }, children, }; } function parseCard(nodes: readonly HastNode[], index: number): { card: HastNode; next: number } | null { const heading = nodes[index]; if (!isChartHeading(heading)) return null; const children: HastNode[] = [heading]; let cursor = skipWhitespace(nodes, index + 1); if (isElement(nodes[cursor], "p")) { const afterParagraph = skipWhitespace(nodes, cursor + 1); if (isChartPre(nodes[afterParagraph])) { children.push(nodes[cursor] as HastNode); cursor = afterParagraph; } } if (!isChartPre(nodes[cursor])) return null; children.push(nodes[cursor] as HastNode); return { card: element("div", ["personal-report-chart-card"], children), next: cursor + 1, }; } export function groupReportChartNodes(nodes: readonly HastNode[]): HastNode[] { const grouped: HastNode[] = []; let index = 0; while (index < nodes.length) { const first = parseCard(nodes, index); if (!first) { grouped.push(nodes[index] as HastNode); index += 1; continue; } const cards = [first.card]; index = first.next; while (index < nodes.length) { const peeked = skipWhitespace(nodes, index); const next = parseCard(nodes, peeked); if (!next) break; cards.push(next.card); index = next.next; } grouped.push(element( "div", cards.length === 1 ? ["personal-report-chart-grid", "is-single"] : ["personal-report-chart-grid"], cards, )); } return grouped; } export function reportChartGrid() { return function groupReportCharts(tree: HastRoot) { if (tree?.type !== "root" || !Array.isArray(tree.children)) return; tree.children = groupReportChartNodes(tree.children); }; }