import { cloneElement, isValidElement, type ReactNode } from "react"; /** * Recursively traverses a ReactNode tree and extracts the text content. * @param node - The ReactNode to process. * @returns A string containing the concatenated text content of the ReactNode tree. */ export function getTextContent(node: ReactNode): string { if (node === null || typeof node === "boolean") return ""; if (typeof node === "string" || typeof node === "number") return String(node); if (Array.isArray(node)) return node.map(getTextContent).join(""); if (isValidElement(node)) { const props = node.props as { children?: ReactNode }; return getTextContent(props.children); } return ""; } /** * Recursively traverses a ReactNode tree and removes leading whitespace and a specified pattern from the beginning of text nodes. * @param node - The ReactNode to process. * @param pattern - The regular expression pattern to match and remove from the beginning of text nodes. * @returns A new ReactNode with the leading whitespace and specified pattern removed from text nodes. */ export function stripLeadingMatch(node: ReactNode, pattern: RegExp): ReactNode { if (typeof node === "string") { const trimmed = node.replace(/^\s+/, ""); if (pattern.test(trimmed)) { return trimmed.replace(pattern, "").replace(/^\s+/, ""); } return node; } if (Array.isArray(node)) { let stripped = false; return node.map((child) => { if (stripped) return child; if (typeof child === "string" && child.trim() === "") return child; // skip whitespace nodes if (typeof child === "string") { const trimmed = child.replace(/^\s+/, ""); if (pattern.test(trimmed)) { stripped = true; return trimmed.replace(pattern, "").replace(/^\s+/, ""); } return child; } if (isValidElement(child)) { stripped = true; return stripLeadingMatch(child, pattern); } return child; }); } if (isValidElement(node)) { const props = node.props as { children?: ReactNode }; return cloneElement(node, {}, stripLeadingMatch(props.children, pattern)); } return node; }