/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */ import type { DOMNode, Element, Text } from 'html-dom-parser'; import type { JSX } from 'react'; import { cloneElement, createElement, isValidElement } from 'react'; import type { Props } from './attributes-to-props'; import attributesToProps from './attributes-to-props'; import type { HTMLReactParserOptions } from './types'; import { canTextBeChildOfNode, isCustomComponent, PRESERVE_CUSTOM_ATTRIBUTES, returnFirstArg, setStyleProp, } from './utilities'; const React = { cloneElement, createElement, isValidElement, } as const; /** * Converts DOM nodes to JSX element(s). * * @param nodes - DOM nodes. * @param options - Options. * @returns - String or JSX element(s). */ export default function domToReact( nodes: DOMNode[], options: HTMLReactParserOptions = {}, ): string | JSX.Element | JSX.Element[] { const reactElements: JSX.Element[] = []; const hasReplace = typeof options.replace === 'function'; const transform = options.transform ?? returnFirstArg; const { cloneElement, createElement, isValidElement } = options.library ?? React; const nodesLength = nodes.length; for (let index = 0; index < nodesLength; index++) { const node = nodes[index]; // replace with custom React element (if present) if (hasReplace) { let replaceElement = options.replace?.(node, index) as JSX.Element; if (isValidElement(replaceElement)) { // set "key" prop for sibling elements // https://react.dev/learn/rendering-lists#rules-of-keys if (nodesLength > 1) { replaceElement = cloneElement(replaceElement, { key: replaceElement.key ?? index, }); } reactElements.push( transform(replaceElement, node, index) as JSX.Element, ); continue; } } if (node.type === 'text') { const isWhitespace = !node.data.trim().length; // We have a whitespace node that can't be nested in its parent // so skip it if ( isWhitespace && node.parent && !canTextBeChildOfNode(node.parent as Element) ) { continue; } // Trim is enabled and we have a whitespace node // so skip it if (options.trim && isWhitespace) { continue; } // We have a text node that's not whitespace and it can be nested // in its parent so add it to the results reactElements.push(transform(node.data, node, index) as JSX.Element); continue; } const element = node as Element; let props: Props = {}; if (skipAttributesToProps(element)) { setStyleProp(element.attribs.style, element.attribs); props = element.attribs; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition } else if (element.attribs) { props = attributesToProps(element.attribs, element.name); } let children: ReturnType | undefined; switch (node.type) { case 'script': case 'style': // prevent text in