diff --git a/example/package-lock.json b/example/package-lock.json index 5a26f864d..40092a46a 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -11,8 +11,10 @@ "dependencies": { "@remoteoss/remote-flows": "file://..", "axios": "1.15.2", + "dompurify": "3.4.1", "dotenv": "17.4.2", "express": "5.2.1", + "html-react-parser": "6.0.1", "jsonwebtoken": "9.0.3", "react": "18.3.1", "react-dom": "18.3.1", @@ -22,6 +24,7 @@ }, "devDependencies": { "@playwright/test": "1.59.1", + "@types/dompurify": "3.0.5", "@types/node": "24.12.2", "@types/react-dom": "19.2.3", "@types/react-syntax-highlighter": "15.5.13", @@ -92,7 +95,7 @@ "glob": "13.0.6", "gzip-size": "7.0.0", "jsdom": "29.0.2", - "msw": "2.13.4", + "msw": "2.13.5", "oxfmt": "0.46.0", "oxlint": "1.61.0", "tsup": "8.5.1", @@ -306,9 +309,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -326,9 +326,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -346,9 +343,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -366,9 +360,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -386,9 +377,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -406,9 +394,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -426,9 +411,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -446,9 +428,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -639,9 +618,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -659,9 +635,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -679,9 +652,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -699,9 +669,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -719,9 +686,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -739,9 +703,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -839,6 +800,16 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -868,7 +839,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -893,6 +864,13 @@ "@types/react": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -1237,7 +1215,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -1297,6 +1275,82 @@ "node": ">=8" } }, + "node_modules/dom-serializer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.0.0.tgz", + "integrity": "sha512-x+9D6nkC8tdXOQUS32egtZpZFLP90+HBZmWjuT920srbJvD/zPgFB9t4k3pEhlw5BQrXStQtRc1Y1zuriXk+Nw==", + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", + "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/domhandler": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", + "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz", + "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", + "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^3.0.0", + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -1344,6 +1398,18 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1739,6 +1805,75 @@ "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==" }, + "node_modules/html-dom-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-7.0.1.tgz", + "integrity": "sha512-loRBDTCY/05/jAC63J1X9ID+xjRucmpLkIcQO0IRbOubBo5ucnpUpyXXob9UMXOskMZlu7KPsDP/2KOMelzJNA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/remarkablemark" + } + ], + "license": "MIT", + "dependencies": { + "domhandler": "6.0.1", + "htmlparser2": "12.0.0" + } + }, + "node_modules/html-react-parser": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-6.0.1.tgz", + "integrity": "sha512-tIie2HSIk2Ct1tdupjd/DhBjskxN/NL5J4ncbUnk2smBr5UIfpPpitUo0imGfBM0BlOL7ac8RcqEwne1jXTcsQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/remarkablemark" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/html-react-parser" + } + ], + "license": "MIT", + "dependencies": { + "domhandler": "6.0.1", + "html-dom-parser": "7.0.1", + "react-property": "2.0.2", + "style-to-js": "1.1.21" + }, + "peerDependencies": { + "@types/react": "0.14 || 15 || 16 || 17 || 18 || 19", + "react": "0.14 || 15 || 16 || 17 || 18 || 19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-12.0.0.tgz", + "integrity": "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "domutils": "^4.0.2", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -1786,6 +1921,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2787,6 +2928,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/react-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.2.tgz", + "integrity": "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==", + "license": "MIT" + }, "node_modules/react-syntax-highlighter": { "version": "16.1.1", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", @@ -3124,6 +3271,24 @@ "node": ">= 0.8" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", diff --git a/example/package.json b/example/package.json index ca8ae68aa..b1c1b7ba4 100644 --- a/example/package.json +++ b/example/package.json @@ -17,8 +17,10 @@ "dependencies": { "@remoteoss/remote-flows": "file://..", "axios": "1.15.2", + "dompurify": "3.4.1", "dotenv": "17.4.2", "express": "5.2.1", + "html-react-parser": "6.0.1", "jsonwebtoken": "9.0.3", "react": "18.3.1", "react-dom": "18.3.1", @@ -28,6 +30,7 @@ }, "devDependencies": { "@playwright/test": "1.59.1", + "@types/dompurify": "3.0.5", "@types/node": "24.12.2", "@types/react-dom": "19.2.3", "@types/react-syntax-highlighter": "15.5.13", diff --git a/example/src/Components.tsx b/example/src/Components.tsx index b087ae4e0..d1cc067f1 100644 --- a/example/src/Components.tsx +++ b/example/src/Components.tsx @@ -9,6 +9,21 @@ import type { import { FileUploader } from '@remoteoss/remote-flows/internals'; //import { ZendeskDialog } from './ZendeskDialog'; +const renderDescription = ( + desc?: React.ReactNode | string, + transformHtml?: (html: string) => React.ReactNode, +) => { + if (!desc) { + return null; + } + + if (typeof desc === 'string' && transformHtml) { + return transformHtml(desc); + } + + return

{desc}

; +}; + // you can define HTML button attributes or event props that exist in your Button like variant, size, etc. const Button = ({ children, @@ -57,9 +72,7 @@ const Input = ({ field, fieldData, fieldState }: FieldComponentProps) => { /> )} - {fieldData.description && ( -

{fieldData.description}

- )} + {renderDescription(fieldData.description, fieldData.transformHtml)} {fieldState.error && (

{fieldState.error.message}

)} @@ -111,9 +124,7 @@ const Select = ({ field, fieldData, fieldState }: FieldComponentProps) => { - {fieldData.description && ( -

{fieldData.description}

- )} + {renderDescription(fieldData.description, fieldData.transformHtml)} {fieldState.error && (

{fieldState.error.message}

@@ -134,9 +145,7 @@ const Textarea = ({ field, fieldData, fieldState }: FieldComponentProps) => { maxLength={fieldData.maxLength} {...field} /> - {fieldData.description && ( -

{fieldData.description}

- )} + {renderDescription(fieldData.description, fieldData.transformHtml)} {fieldState.error && (

{fieldState.error.message}

)} @@ -166,9 +175,7 @@ const Radio = ({ field, fieldData, fieldState }: FieldComponentProps) => { ); })} - {fieldData.description && ( -

{fieldData.description}

- )} + {renderDescription(fieldData.description, fieldData.transformHtml)} {hasError &&

{fieldState.error?.message}

} ); @@ -183,9 +190,7 @@ const Checkbox = ({ field, fieldData, fieldState }: FieldComponentProps) => { - {fieldData.description && ( -

{fieldData.description}

- )} + {renderDescription(fieldData.description, fieldData.transformHtml)} {hasError &&

{fieldState.error?.message}

} ); @@ -236,9 +241,7 @@ export const Countries = ({ - {fieldData.description && ( -

{fieldData.description}

- )} + {renderDescription(fieldData.description, fieldData.transformHtml)} {fieldState.error && (

{fieldState.error.message}

@@ -283,9 +286,7 @@ const FileUploadField = ({ accept={fieldData.accept} multiple={fieldData.multiple} /> - {fieldData.description && ( -

{fieldData.description}

- )} + {renderDescription(fieldData.description, fieldData.transformHtml)} {fieldState.error && (

{fieldState.error.message}

)} @@ -309,9 +310,7 @@ const DatePickerInput = ({ field?.onChange?.(e.target.value); }} /> - {fieldData.description && ( -

{fieldData.description}

- )} + {renderDescription(fieldData.description, fieldData.transformHtml)} {fieldState.error && (

{fieldState.error.message}

)} diff --git a/example/src/Onboarding.tsx b/example/src/Onboarding.tsx index 7f3ea2ebc..7b91c89d7 100644 --- a/example/src/Onboarding.tsx +++ b/example/src/Onboarding.tsx @@ -19,6 +19,7 @@ import { ReviewOnboardingStep } from './ReviewOnboardingStep'; import { OnboardingAlertStatuses } from './OnboardingAlertStatuses'; import { RemoteFlows } from './RemoteFlows'; import { AlertError } from './AlertError'; +import { transformHtmlToComponents } from './utils/transformHtml'; import './css/main.css'; export const InviteSection = ({ @@ -355,7 +356,10 @@ const OnboardingWithProps = ({ employmentId, externalId, }: OnboardingFormData) => ( - + { + const [isOpen, setIsOpen] = useState(false); + + return ( +
setIsOpen(e.currentTarget.open)} + > + + {summary} + +
{children}
+
+ ); +}; diff --git a/example/src/utils/transformHtml.tsx b/example/src/utils/transformHtml.tsx new file mode 100644 index 000000000..067e058fc --- /dev/null +++ b/example/src/utils/transformHtml.tsx @@ -0,0 +1,57 @@ +import parse, { + domToReact, + HTMLReactParserOptions, + Element, + DOMNode, +} from 'html-react-parser'; +import DOMPurify from 'dompurify'; +import { $TSFixMe } from '@remoteoss/remote-flows'; +import { Accordion } from '../components/Accordion'; + +export const transformHtmlToComponents = (htmlContent: string) => { + // 1. Sanitize HTML first (IMPORTANT for security) + const clean = DOMPurify.sanitize(htmlContent); + + // 2. Define transformation options + const options: HTMLReactParserOptions = { + replace: (domNode) => { + // Check if it's an element node + if (domNode.type === 'tag' && domNode.name === 'details') { + const element = domNode as Element; + const dataComponent = element.attribs?.['data-component']; + + // Transform
to custom Accordion + if (dataComponent === 'Accordion') { + // Find the tag + const summaryNode = element.children?.find( + (child: $TSFixMe) => + child.type === 'tag' && child.name === 'summary', + ); + + // Extract summary content + const summary = summaryNode + ? domToReact( + (summaryNode as Element).children as DOMNode[], + options, + ) + : 'Details'; + + // Get all other content (not the summary) + const content = element.children?.filter( + (child: $TSFixMe) => + !(child.type === 'tag' && child.name === 'summary'), + ); + + return ( + + {domToReact((content || []) as $TSFixMe[], options)} + + ); + } + } + }, + }; + + // 3. Parse and transform + return parse(clean, options); +}; diff --git a/src/RemoteFlowsProvider.tsx b/src/RemoteFlowsProvider.tsx index 5a3ca8c12..84b2332e6 100644 --- a/src/RemoteFlowsProvider.tsx +++ b/src/RemoteFlowsProvider.tsx @@ -35,8 +35,10 @@ function RemoteFlowContextWrapper({ export function FormFieldsProvider({ children, components: userComponents = {}, + transformHtmlToComponents, }: PropsWithChildren<{ components?: Components; + transformHtmlToComponents?: RemoteFlowsSDKProps['transformHtmlToComponents']; }>) { // Merge user components with lazy defaults // User-provided components take precedence, lazy defaults are only used as fallback @@ -48,8 +50,17 @@ export function FormFieldsProvider({ } as Components; }, [userComponents]); + // Memoize the context value to avoid unnecessary re-renders + const contextValue = useMemo( + () => ({ + components: resolvedComponents, + transformHtmlToComponents, + }), + [resolvedComponents, transformHtmlToComponents], + ); + return ( - + } delay={200} /> @@ -73,6 +84,7 @@ export function RemoteFlows({ errorBoundary = { useParentErrorBoundary: true }, debug = false, credentials, + transformHtmlToComponents, }: PropsWithChildren) { // WE NEED TO FIX: react-hooks/refs - Cannot access refs during render // eslint-disable-next-line react-hooks/refs @@ -89,7 +101,10 @@ export function RemoteFlows({ client={remoteApiClient} > - + & { /** * Optional HTML transformer function passed from RemoteFlows context. * Use this in custom field components to transform HTML descriptions into React components. + * @example + * ```tsx + * const CustomInput = ({ fieldData }: FieldComponentProps) => { + * const renderDescription = (desc: string) => { + * if (fieldData.transformHtml) { + * return fieldData.transformHtml(desc); + * } + * return
{desc}
; + * }; + * return ( + *
+ * + * {fieldData.description && renderDescription(fieldData.description)} + *
+ * ); + * }; + * ``` */ transformHtml?: (html: string) => React.ReactNode; }; diff --git a/src/types/remoteFlows.ts b/src/types/remoteFlows.ts index 0134c52eb..1eb4b1c80 100644 --- a/src/types/remoteFlows.ts +++ b/src/types/remoteFlows.ts @@ -219,6 +219,32 @@ export type RemoteFlowsSDKProps = Omit & { * @default undefined (credentials not included) */ credentials?: RequestCredentials; + /** + * Optional function to transform HTML strings into React components. + * Allows partners to replace specific HTML patterns (e.g.,
) + * with custom React components. + * + * @param htmlContent - The raw HTML string to transform (unsanitized) + * @returns React elements or the original HTML + * + * @remarks + * Security: This function receives UNSANITIZED HTML. If you're using html-react-parser, + * note that it does NOT sanitize HTML by default. You are responsible for sanitizing + * untrusted HTML before parsing. Consider using DOMPurify or sanitize-html. + * + * @example + * ```tsx + * import parse, { domToReact } from 'html-react-parser'; + * import DOMPurify from 'dompurify'; + * + * function transformHtmlToComponents(htmlContent: string) { + * // Sanitize first (recommended) + * const clean = DOMPurify.sanitize(htmlContent); + * return parse(clean, parseOptions); + * } + * ``` + */ + transformHtmlToComponents?: (htmlContent: string) => ReactNode; }; // oxlint-disable-next-line typescript/no-explicit-any