import parse, { attributesToProps, domToReact, Element, type HTMLReactParserOptions, } from "html-react-parser" import type { CSSProperties, ElementType, JSX } from "react" import { createElement } from "react" interface HtmlContentProps { html?: string | null className?: string as?: keyof JSX.IntrinsicElements style?: CSSProperties } const wordpressUrl = import.meta.env.VITE_WORDPRESS_URL?.replace(/\/$/, "") ?? "" const wordpressAssetPathPattern = /^\/(?:wp-content|wp-includes)\// const mediaHrefPattern = /\.(?:avif|gif|jpe?g|pdf|png|svg|webp)(?:[?#].*)?$/i const voidElements = new Set([ "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr", ]) function toWordpressUrl(value: string): string { if (!wordpressUrl || !wordpressAssetPathPattern.test(value)) return value return `${wordpressUrl}${value}` } function normalizeSrcSet(value: string): string { return value .split(",") .map((candidate) => { const parts = candidate.trim().split(/\s+/) const url = parts.shift() if (!url) return candidate return [toWordpressUrl(url), ...parts].join(" ") }) .join(", ") } function normalizeHtmlMediaAttributes( attributes: Record, ): Record { const normalized = { ...attributes } if (normalized.src) normalized.src = toWordpressUrl(normalized.src) if (normalized.poster) normalized.poster = toWordpressUrl(normalized.poster) if (normalized.srcset) normalized.srcset = normalizeSrcSet(normalized.srcset) if ( normalized.href && wordpressAssetPathPattern.test(normalized.href) && mediaHrefPattern.test(normalized.href) ) { normalized.href = toWordpressUrl(normalized.href) } return normalized } const parserOptions: HTMLReactParserOptions = { replace(domNode) { if (!(domNode instanceof Element) || !domNode.attribs) return const nextAttributes = normalizeHtmlMediaAttributes(domNode.attribs) const hasChanges = Object.entries(nextAttributes).some( ([key, value]) => domNode.attribs[key] !== value, ) if (!hasChanges) return const props = attributesToProps(nextAttributes, domNode.name) if (voidElements.has(domNode.name)) { return createElement(domNode.name, props) } return createElement( domNode.name, props, domToReact( domNode.children as Parameters[0], parserOptions, ), ) }, } export function HtmlContent({ html, className, as, style }: HtmlContentProps) { const Element: ElementType = as || "div" const content = html ? parse(html, parserOptions) : null if (!content) { return null } return createElement(Element, { className, style }, content) }