/**
* DOM utilities for WSX
*
* Provides helper functions for DOM manipulation and HTML parsing
*/
// JSX子元素类型
export type JSXChildren =
| string
| number
| HTMLElement
| SVGElement
| DocumentFragment
| Node
| JSXChildren[]
| null
| undefined
| boolean;
/**
* Convert HTML string to DOM nodes (elements and text)
*
* This function parses an HTML string and returns an array of DOM nodes
* that can be used directly in WSX JSX. Text nodes are converted to strings,
* while HTML/SVG elements are kept as DOM elements.
*
* @param html - HTML string to parse
* @returns Array of HTMLElement, SVGElement, or string (for text nodes)
*
* @example
* ```tsx
* const nodes = parseHTMLToNodes('
Hello World
');
* // Returns: [HTMLElement (), ...]
*
* return (
*
* {nodes}
*
* );
* ```
*/
export function parseHTMLToNodes(html: string): (HTMLElement | SVGElement | string)[] {
if (!html) return [];
// Create a temporary container to parse HTML
// Note: innerHTML is used here for framework-level HTML parsing utility
// This is an exception to the no-inner-html rule for framework code
const temp = document.createElement("div");
temp.innerHTML = html;
// Convert all child nodes to array
// Text nodes are converted to strings, elements are kept as-is
return Array.from(temp.childNodes).map((node) => {
if (node instanceof HTMLElement || node instanceof SVGElement) {
// 关键修复:标记解析出的元素为框架管理
// 这确保了 shouldPreserveElement 不会错误地保留这些元素
// 从而防止了在频繁重渲染(如 Markdown 输入)时的元素堆积
(node as HTMLElement & { __wsxManaged?: boolean }).__wsxManaged = true;
return node;
} else {
// Convert text nodes and other node types to strings
// Note: When these strings are processed by appendChildrenToElement,
// they will be wraped in managed text nodes.
return node.textContent || "";
}
});
}
/**
* 检测字符串是否包含HTML标签
* 使用更严格的检测:必须包含完整的 HTML 标签(开始和结束标签,或自闭合标签)
*/
export function isHTMLString(str: string): boolean {
const trimmed = str.trim();
if (!trimmed) return false;
// 更严格的检测:必须包含完整的 HTML 标签
// 1. 必须以 < 开头
// 2. 后面跟着字母(标签名)
// 3. 必须包含 > 来闭合标签
// 4. 排除单独的 < 或 > 符号(如数学表达式 "a < b")
const htmlTagPattern = /<[a-z][a-z0-9]*(\s[^>]*)?(\/>|>)/i;
// 额外检查:确保不是纯文本中的 < 和 >(如 "a < b" 或 "x > y")
// 如果字符串看起来像数学表达式或纯文本,不应该被检测为 HTML
const looksLikeMath = /^[^<]*<[^>]*>[^>]*$/.test(trimmed) && !htmlTagPattern.test(trimmed);
if (looksLikeMath) return false;
const result = htmlTagPattern.test(trimmed);
return result;
}
/**
* 扁平化子元素数组
* 自动检测HTML字符串并转换为DOM节点
*
* @param children - 子元素数组
* @param skipHTMLDetection - 是否跳过HTML检测(用于已解析的节点,避免无限递归)
* @param depth - 当前递归深度(防止无限递归,最大深度为 10)
*/
export function flattenChildren(
children: JSXChildren[],
skipHTMLDetection: boolean = false,
depth: number = 0
): JSXChildren[] {
// 防止无限递归:如果深度超过 10,停止处理
if (depth > 10) {
console.warn(
"[WSX] flattenChildren: Maximum depth exceeded, treating remaining children as text"
);
return children.filter(
(child): child is string | number =>
typeof child === "string" || typeof child === "number"
);
}
const result: JSXChildren[] = [];
for (const child of children) {
if (child === null || child === undefined || child === false) {
continue;
} else if (Array.isArray(child)) {
// 递归处理数组,保持 skipHTMLDetection 状态,增加深度
result.push(...flattenChildren(child, skipHTMLDetection, depth + 1));
} else if (typeof child === "string") {
// 如果跳过HTML检测,直接添加字符串(避免无限递归)
if (skipHTMLDetection) {
result.push(child);
} else if (isHTMLString(child)) {
// 自动检测HTML字符串并转换为DOM节点
try {
const nodes = parseHTMLToNodes(child);
if (nodes.length > 0) {
// 直接添加解析后的节点,不再递归处理(避免无限递归)
for (const node of nodes) {
result.push(node);
}
} else {
result.push(child);
}
} catch (error) {
console.warn("[WSX] Failed to parse HTML string, treating as text:", error);
result.push(child);
}
} else {
result.push(child);
}
} else if (child instanceof DocumentFragment) {
// 递归处理 DocumentFragment 中的子节点
const fragmentChildren = Array.from(child.childNodes);
result.push(...flattenChildren(fragmentChildren, skipHTMLDetection, depth + 1));
} else {
// 保持 Node 引用
result.push(child);
}
}
return result;
}