diff --git a/.talismanrc b/.talismanrc index 9e803b5..10cdf70 100644 --- a/.talismanrc +++ b/.talismanrc @@ -8,3 +8,11 @@ fileignoreconfig: checksum: 3ba7af9ed1c1adef2e2bd5610099716562bebb8ba750d4b41ddda99fc9eaf115 - filename: .husky/pre-commit checksum: 5baabd7d2c391648163f9371f0e5e9484f8fb90fa2284cfc378732ec3192c193 +- filename: src/Models/react-types.ts + checksum: 10165b0c5f687c17e177df6b7a5014f174da4d8b0fdc7ed90cdeb6c0aba10808 +- filename: src/helper/react-utils.ts + checksum: bcbd28b040bd1c338fd0a677354267918136aac7557e5449b9dd76015d6a336d +- filename: src/json-to-react.ts + checksum: 13985223d0c1471b637df4da5badfba91500b34e0473d41e70d2fc4b356055a4 +- filename: __test__/json-to-react.test.ts + checksum: 592452cbbe1c887ffd71fc10d5cf86f9e6f6c7c549077deba3e7a81944c1d397 diff --git a/__test__/json-to-react.test.ts b/__test__/json-to-react.test.ts new file mode 100644 index 0000000..144cc9b --- /dev/null +++ b/__test__/json-to-react.test.ts @@ -0,0 +1,447 @@ +import { jsonToReact, defaultReactRenderOptions } from '../src/json-to-react'; +import { ReactRenderOptions } from '../src/Models/react-types'; +import NodeType from '../src/nodes/node-type'; + +// Mock React createElement for testing +const mockCreateElement = (type: any, props: any = {}, ...children: any[]) => ({ + type, + props: { ...props, children: children.length === 1 ? children[0] : children }, + key: props.key || null, +}); + +// Mock entry data for testing +const mockEntry = { + uid: 'test_entry', + rich_text_editor: { + type: 'doc', + children: [ + { + type: 'ol', + attrs: { style: 'color: red;', 'class-name': 'custom-list' }, + children: [ + { + type: 'li', + attrs: {}, + children: [{ text: 'First item' }] + }, + { + type: 'li', + attrs: {}, + children: [{ text: 'Second item' }] + } + ] + }, + { + type: 'p', + attrs: {}, + children: [ + { text: 'This is a ' }, + { text: 'bold', bold: true }, + { text: ' paragraph with ' }, + { text: 'italic', italic: true }, + { text: ' text.' } + ] + } + ] + }, + _embedded_items: {} +}; + +const mockArrayEntry = { + uid: 'test_entry_array', + rich_text_editor: [ + { + type: 'doc', + children: [ + { + type: 'h1', + attrs: { id: 'heading-1' }, + children: [{ text: 'Heading 1' }] + } + ] + }, + { + type: 'doc', + children: [ + { + type: 'p', + attrs: {}, + children: [{ text: 'Paragraph content' }] + } + ] + } + ], + _embedded_items: {} +}; + +describe('jsonToReact', () => { + beforeEach(() => { + // Reset mock entries before each test + mockEntry.rich_text_editor = { + type: 'doc', + children: [ + { + type: 'ol', + attrs: { style: 'color: red;', 'class-name': 'custom-list' }, + children: [ + { + type: 'li', + attrs: {}, + children: [{ text: 'First item' }] + }, + { + type: 'li', + attrs: {}, + children: [{ text: 'Second item' }] + } + ] + }, + { + type: 'p', + attrs: {}, + children: [ + { text: 'This is a ' }, + { text: 'bold', bold: true }, + { text: ' paragraph with ' }, + { text: 'italic', italic: true }, + { text: ' text.' } + ] + } + ] + }; + }); + + it('should convert basic JSON RTE to React elements', () => { + const entry = { ...mockEntry }; + + jsonToReact({ + entry, + paths: ['rich_text_editor'], + createElement: mockCreateElement + }); + + expect(entry.rich_text_editor).toBeDefined(); + expect(typeof entry.rich_text_editor).toBe('object'); + }); + + it('should handle custom render options', () => { + const entry = { ...mockEntry }; + + // Custom List component mock + const CustomList = (props: any) => mockCreateElement('div', { className: 'custom-list-component' }, props.children); + + const customRenderOptions: ReactRenderOptions = { + [NodeType.ORDER_LIST]: (node: any, children: any) => CustomList({ children }), + }; + + jsonToReact({ + entry, + paths: ['rich_text_editor'], + renderOptions: customRenderOptions, + createElement: mockCreateElement + }); + + expect(entry.rich_text_editor).toBeDefined(); + // The custom render option should be applied + }); + + it('should handle array of documents', () => { + const entry = { ...mockArrayEntry }; + + jsonToReact({ + entry, + paths: ['rich_text_editor'], + createElement: mockCreateElement + }); + + expect(Array.isArray(entry.rich_text_editor)).toBe(true); + expect(entry.rich_text_editor.length).toBe(2); + }); + + it('should handle array of entries', () => { + const entries = [{ ...mockEntry }, { ...mockEntry }]; + + jsonToReact({ + entry: entries, + paths: ['rich_text_editor'], + createElement: mockCreateElement + }); + + entries.forEach(entry => { + expect(entry.rich_text_editor).toBeDefined(); + }); + }); + + it('should preserve node attributes as React props', () => { + const entry = { + uid: 'test_entry', + rich_text_editor: { + type: 'doc', + children: [ + { + type: 'p', + attrs: { + style: 'color: blue; font-size: 16px;', + 'class-name': 'test-paragraph', + id: 'para-1' + }, + children: [{ text: 'Test paragraph' }] + } + ] + }, + _embedded_items: {} + }; + + jsonToReact({ + entry, + paths: ['rich_text_editor'], + createElement: mockCreateElement + }); + + // The converted element should preserve the attributes + expect(entry.rich_text_editor).toBeDefined(); + }); + + it('should handle text formatting', () => { + const entry = { + uid: 'test_entry', + rich_text_editor: { + type: 'doc', + children: [ + { + type: 'p', + attrs: {}, + children: [ + { text: 'Normal ' }, + { text: 'bold', bold: true }, + { text: ' and ' }, + { text: 'italic', italic: true }, + { text: ' and ' }, + { text: 'underlined', underline: true }, + { text: ' text.' } + ] + } + ] + }, + _embedded_items: {} + }; + + jsonToReact({ + entry, + paths: ['rich_text_editor'], + createElement: mockCreateElement + }); + + expect(entry.rich_text_editor).toBeDefined(); + }); + + it('should handle links with proper attributes', () => { + const entry = { + uid: 'test_entry', + rich_text_editor: { + type: 'doc', + children: [ + { + type: 'a', + attrs: { + href: 'https://example.com', + target: '_blank', + 'class-name': 'external-link' + }, + children: [{ text: 'External Link' }] + } + ] + }, + _embedded_items: {} + }; + + jsonToReact({ + entry, + paths: ['rich_text_editor'], + createElement: mockCreateElement + }); + + expect(entry.rich_text_editor).toBeDefined(); + }); + + it('should handle images with proper attributes', () => { + const entry = { + uid: 'test_entry', + rich_text_editor: { + type: 'doc', + children: [ + { + type: 'img', + attrs: { + src: 'https://example.com/image.jpg', + alt: 'Test image', + 'class-name': 'responsive-image' + }, + children: [] as any[] + } + ] + }, + _embedded_items: {} + }; + + jsonToReact({ + entry, + paths: ['rich_text_editor'], + createElement: mockCreateElement + }); + + expect(entry.rich_text_editor).toBeDefined(); + }); + + it('should handle tables with colgroup', () => { + const entry = { + uid: 'test_entry', + rich_text_editor: { + type: 'doc', + children: [ + { + type: 'table', + attrs: { + colWidths: [100, 200, 150] + }, + children: [ + { + type: 'tbody', + attrs: {}, + children: [ + { + type: 'tr', + attrs: {}, + children: [ + { + type: 'td', + attrs: {}, + children: [{ text: 'Cell 1' }] + }, + { + type: 'td', + attrs: {}, + children: [{ text: 'Cell 2' }] + }, + { + type: 'td', + attrs: {}, + children: [{ text: 'Cell 3' }] + } + ] + } + ] + } + ] + } + ] + }, + _embedded_items: {} + }; + + jsonToReact({ + entry, + paths: ['rich_text_editor'], + createElement: mockCreateElement + }); + + expect(entry.rich_text_editor).toBeDefined(); + }); + + it('should handle void table cells', () => { + const entry = { + uid: 'test_entry', + rich_text_editor: { + type: 'doc', + children: [ + { + type: 'table', + attrs: {}, + children: [ + { + type: 'tbody', + attrs: {}, + children: [ + { + type: 'tr', + attrs: {}, + children: [ + { + type: 'td', + attrs: { void: true }, + children: [] as any[] + }, + { + type: 'td', + attrs: {}, + children: [{ text: 'Valid cell' }] + } + ] + } + ] + } + ] + } + ] + }, + _embedded_items: {} + }; + + jsonToReact({ + entry, + paths: ['rich_text_editor'], + createElement: mockCreateElement + }); + + expect(entry.rich_text_editor).toBeDefined(); + }); + + it('should handle nested paths', () => { + const entry = { + uid: 'test_entry', + content: { + nested: { + rich_text_editor: { + type: 'doc', + children: [ + { + type: 'p', + attrs: {}, + children: [{ text: 'Nested content' }] + } + ] + } + } + }, + _embedded_items: {} + }; + + jsonToReact({ + entry, + paths: ['content.nested.rich_text_editor'], + createElement: mockCreateElement + }); + + expect(entry.content.nested.rich_text_editor).toBeDefined(); + }); +}); + +describe('defaultReactRenderOptions', () => { + it('should have all required node types', () => { + expect(defaultReactRenderOptions[NodeType.PARAGRAPH]).toBeDefined(); + expect(defaultReactRenderOptions[NodeType.ORDER_LIST]).toBeDefined(); + expect(defaultReactRenderOptions[NodeType.UNORDER_LIST]).toBeDefined(); + expect(defaultReactRenderOptions[NodeType.LIST_ITEM]).toBeDefined(); + expect(defaultReactRenderOptions[NodeType.HEADING_1]).toBeDefined(); + expect(defaultReactRenderOptions[NodeType.LINK]).toBeDefined(); + expect(defaultReactRenderOptions[NodeType.IMAGE]).toBeDefined(); + expect(defaultReactRenderOptions[NodeType.TABLE]).toBeDefined(); + expect(defaultReactRenderOptions.default).toBeDefined(); + }); + + it('should have function renderers', () => { + expect(typeof defaultReactRenderOptions[NodeType.PARAGRAPH]).toBe('function'); + expect(typeof defaultReactRenderOptions[NodeType.ORDER_LIST]).toBe('function'); + expect(typeof defaultReactRenderOptions.default).toBe('function'); + }); +}); diff --git a/src/Models/react-types.ts b/src/Models/react-types.ts new file mode 100644 index 0000000..67087e4 --- /dev/null +++ b/src/Models/react-types.ts @@ -0,0 +1,44 @@ +/** + * React integration types for Contentstack Utils + * These types are only used when React is available in the consuming application + */ + +// React types (defined locally to avoid React dependency) +export interface ReactElement { + type: any; + props: any; + key: string | number | null; +} + +export interface ReactNode { + [key: string]: any; +} + +export interface ReactComponent
{
+ (props: P): ReactElement | null;
+}
+
+// React-specific render function types
+export type ReactRenderNode = (node: import('../nodes/node').default, children: ReactNode[]) => ReactNode;
+export type ReactRenderMark = (text: string, props?: any) => ReactNode;
+
+// React render options interface
+export interface ReactRenderOptions {
+ [nodeType: string]: ReactRenderNode | ReactRenderMark | ReactNode;
+}
+
+// Props that can be extracted from Contentstack node attributes
+export interface ReactNodeProps {
+ style?: { [key: string]: string | number } | string;
+ className?: string;
+ id?: string;
+ [key: string]: any;
+}
+
+// React integration options
+export interface JsonToReactOptions {
+ entry: import('./embedded-object').EntryEmbedable | import('./embedded-object').EntryEmbedable[];
+ paths: string[];
+ renderOptions?: ReactRenderOptions;
+ createElement?: (type: any, props?: any, ...children: any[]) => ReactElement;
+}
diff --git a/src/helper/react-utils.ts b/src/helper/react-utils.ts
new file mode 100644
index 0000000..d8aa883
--- /dev/null
+++ b/src/helper/react-utils.ts
@@ -0,0 +1,195 @@
+/**
+ * React utility functions for Contentstack Utils
+ * This module provides utilities to convert JSON RTE to React elements
+ */
+
+import { ReactElement, ReactNode, ReactNodeProps } from '../Models/react-types';
+import Node from '../nodes/node';
+import TextNode from '../nodes/text-node';
+import { AnyNode } from '../json-to-html';
+
+/**
+ * Default React createElement function
+ * This is a fallback that assumes React is available globally
+ */
+let defaultCreateElement: (type: any, props?: any, ...children: any[]) => ReactElement;
+
+try {
+ // Try to use React if it's available
+ const React = (globalThis as any).React || (typeof window !== 'undefined' ? (window as any).React : null);
+ if (React && React.createElement) {
+ defaultCreateElement = React.createElement;
+ } else {
+ // Fallback createElement function
+ defaultCreateElement = (type: any, props: any = {}, ...children: any[]) => ({
+ type,
+ props: { ...props, children: children.length === 1 ? children[0] : children },
+ key: props.key || null,
+ });
+ }
+} catch (error) {
+ // Fallback createElement function
+ defaultCreateElement = (type: any, props: any = {}, ...children: any[]) => ({
+ type,
+ props: { ...props, children: children.length === 1 ? children[0] : children },
+ key: props.key || null,
+ });
+}
+
+/**
+ * Extract React props from Contentstack node attributes
+ */
+export function getReactProps(node: Node): ReactNodeProps {
+ const props: ReactNodeProps = {};
+
+ if (node.attrs) {
+ // Handle style attribute
+ if (node.attrs.style) {
+ if (typeof node.attrs.style === 'string') {
+ props.style = parseStyleString(node.attrs.style);
+ } else {
+ props.style = node.attrs.style;
+ }
+ }
+
+ // Handle class attribute (convert to className for React)
+ if (node.attrs['class-name']) {
+ props.className = node.attrs['class-name'];
+ }
+
+ // Handle id attribute
+ if (node.attrs.id) {
+ props.id = node.attrs.id;
+ }
+
+ // Handle other common attributes
+ if (node.attrs.href) props.href = node.attrs.href;
+ if (node.attrs.url && !props.href) props.href = node.attrs.url;
+ if (node.attrs.src) props.src = node.attrs.src;
+ if (node.attrs.alt) props.alt = node.attrs.alt;
+ if (node.attrs.target) props.target = node.attrs.target;
+ if (node.attrs.title) props.title = node.attrs.title;
+
+ // Handle table-specific attributes
+ if (node.attrs.rowSpan) props.rowSpan = node.attrs.rowSpan;
+ if (node.attrs.colSpan) props.colSpan = node.attrs.colSpan;
+ }
+
+ return props;
+}
+
+/**
+ * Parse CSS style string into React style object
+ */
+export function parseStyleString(styleStr: string): { [key: string]: string | number } {
+ const styles: any = {};
+
+ if (!styleStr || typeof styleStr !== 'string') {
+ return styles;
+ }
+
+ styleStr.split(';').forEach(rule => {
+ const colonIndex = rule.indexOf(':');
+ if (colonIndex === -1) return;
+
+ const property = rule.substring(0, colonIndex).trim();
+ const value = rule.substring(colonIndex + 1).trim();
+
+ if (property && value) {
+ // Convert kebab-case to camelCase for React
+ const camelProperty = property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
+ styles[camelProperty] = value;
+ }
+ });
+
+ return styles;
+}
+
+/**
+ * Convert text node to React element with formatting
+ */
+export function textNodeToReact(
+ node: TextNode,
+ createElement: (type: any, props?: any, ...children: any[]) => ReactElement = defaultCreateElement
+): ReactNode {
+ let element: any = node.text;
+
+ // Apply text formatting (innermost to outermost)
+ if (node.superscript) {
+ element = createElement('sup', {}, element);
+ }
+ if (node.subscript) {
+ element = createElement('sub', {}, element);
+ }
+ if (node.inlineCode) {
+ element = createElement('code', {}, element);
+ }
+ if (node.strikethrough) {
+ element = createElement('s', {}, element);
+ }
+ if (node.underline) {
+ element = createElement('u', {}, element);
+ }
+ if (node.italic) {
+ element = createElement('em', {}, element);
+ }
+ if (node.bold) {
+ element = createElement('strong', {}, element);
+ }
+
+ // Handle break
+ if (node.break) {
+ return [element, createElement('br', { key: 'break' })];
+ }
+
+ return element;
+}
+
+/**
+ * Check if a value is a React element
+ */
+export function isReactElement(value: any): value is ReactElement {
+ return value && typeof value === 'object' && 'type' in value && 'props' in value;
+}
+
+/**
+ * Add key prop to React element if it doesn't have one
+ */
+export function addKeyToReactElement(
+ element: ReactElement,
+ key: string | number,
+ createElement: (type: any, props?: any, ...children: any[]) => ReactElement = defaultCreateElement
+): ReactElement {
+ if (element.key !== null) {
+ return element;
+ }
+
+ return createElement(element.type, { ...element.props, key }, element.props.children);
+}
+
+/**
+ * Helper function to set nested property in an object
+ */
+export function setNestedProperty(obj: any, path: string, value: any): void {
+ const keys = path.split('.');
+ let current = obj;
+
+ for (let i = 0; i < keys.length - 1; i++) {
+ const key = keys[i];
+ if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
+ current[key] = {};
+ }
+ current = current[key];
+ }
+
+ current[keys[keys.length - 1]] = value;
+}
+
+/**
+ * Helper function to get nested property from an object
+ */
+export function getNestedProperty(obj: any, path: string): any {
+ return path.split('.').reduce((current, key) => current?.[key], obj);
+}
+
+export { defaultCreateElement };
diff --git a/src/index.ts b/src/index.ts
index 37cf3f3..06fcae4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -13,4 +13,15 @@ export { default as TextNode } from './nodes/text-node';
export { jsonToHTML } from './json-to-html'
export { GQL } from './gql'
export { addTags as addEditableTags } from './entry-editable'
-export { updateAssetURLForGQL } from './updateAssetURLForGQL'
\ No newline at end of file
+export { updateAssetURLForGQL } from './updateAssetURLForGQL'
+
+// React integration exports (optional - only use if React is available)
+export { jsonToReact, createJsonToReact, defaultReactRenderOptions } from './json-to-react';
+export { ReactRenderOptions, JsonToReactOptions, ReactRenderNode } from './Models/react-types';
+export {
+ getReactProps,
+ parseStyleString,
+ textNodeToReact,
+ setNestedProperty,
+ getNestedProperty
+} from './helper/react-utils';
\ No newline at end of file
diff --git a/src/json-to-react.ts b/src/json-to-react.ts
new file mode 100644
index 0000000..0892410
--- /dev/null
+++ b/src/json-to-react.ts
@@ -0,0 +1,245 @@
+/**
+ * JSON RTE to React converter for Contentstack Utils
+ * This module provides the main functionality to convert Contentstack JSON RTE to React elements
+ */
+
+import { JsonToReactOptions, ReactRenderOptions, ReactRenderNode, ReactNode, ReactElement } from './Models/react-types';
+import { EntryEmbedable } from './Models/embedded-object';
+import { findRenderContent } from './helper/find-render-content';
+import Document from './nodes/document';
+import Node from './nodes/node';
+import TextNode from './nodes/text-node';
+import NodeType from './nodes/node-type';
+import { AnyNode } from './json-to-html';
+import {
+ getReactProps,
+ textNodeToReact,
+ isReactElement,
+ addKeyToReactElement,
+ setNestedProperty,
+ defaultCreateElement
+} from './helper/react-utils';
+
+/**
+ * Default React render options for standard HTML elements
+ */
+export const defaultReactRenderOptions: ReactRenderOptions = {
+ [NodeType.DOCUMENT]: (node: Node, children: ReactNode[]) => children,
+
+ [NodeType.PARAGRAPH]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('p', getReactProps(node), ...children),
+
+ [NodeType.ORDER_LIST]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('ol', getReactProps(node), ...children),
+
+ [NodeType.UNORDER_LIST]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('ul', getReactProps(node), ...children),
+
+ [NodeType.LIST_ITEM]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('li', getReactProps(node), ...children),
+
+ [NodeType.HEADING_1]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('h1', getReactProps(node), ...children),
+
+ [NodeType.HEADING_2]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('h2', getReactProps(node), ...children),
+
+ [NodeType.HEADING_3]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('h3', getReactProps(node), ...children),
+
+ [NodeType.HEADING_4]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('h4', getReactProps(node), ...children),
+
+ [NodeType.HEADING_5]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('h5', getReactProps(node), ...children),
+
+ [NodeType.HEADING_6]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('h6', getReactProps(node), ...children),
+
+ [NodeType.LINK]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('a', getReactProps(node), ...children),
+
+ [NodeType.IMAGE]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('img', getReactProps(node)),
+
+ [NodeType.EMBED]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('iframe', getReactProps(node), ...children),
+
+ [NodeType.BLOCK_QUOTE]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('blockquote', getReactProps(node), ...children),
+
+ [NodeType.CODE]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('code', getReactProps(node), ...children),
+
+ [NodeType.HR]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('hr', getReactProps(node)),
+
+ [NodeType.TABLE]: (node: Node, children: ReactNode[]) => {
+ const props = getReactProps(node);
+
+ // Handle colgroup for table column widths
+ let colgroupElement = null;
+ if (node.attrs.colWidths && Array.isArray(node.attrs.colWidths)) {
+ const totalWidth = node.attrs.colWidths.reduce((sum: number, width: number) => sum + width, 0);
+ const cols = node.attrs.colWidths.map((colWidth: number, index: number) => {
+ const widthPercentage = (colWidth / totalWidth) * 100;
+ return defaultCreateElement('col', {
+ key: `col-${index}`,
+ style: { width: `${widthPercentage.toFixed(2)}%` }
+ });
+ });
+ colgroupElement = defaultCreateElement('colgroup', {
+ key: 'colgroup',
+ 'data-width': totalWidth
+ }, ...cols);
+ }
+
+ const tableChildren = colgroupElement ? [colgroupElement, ...children] : children;
+ return defaultCreateElement('table', props, ...tableChildren);
+ },
+
+ [NodeType.TABLE_HEADER]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('thead', getReactProps(node), ...children),
+
+ [NodeType.TABLE_BODY]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('tbody', getReactProps(node), ...children),
+
+ [NodeType.TABLE_FOOTER]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('tfoot', getReactProps(node), ...children),
+
+ [NodeType.TABLE_ROW]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('tr', getReactProps(node), ...children),
+
+ [NodeType.TABLE_HEAD]: (node: Node, children: ReactNode[]) => {
+ if (node.attrs.void) return null;
+ return defaultCreateElement('th', getReactProps(node), ...children);
+ },
+
+ [NodeType.TABLE_DATA]: (node: Node, children: ReactNode[]) => {
+ if (node.attrs.void) return null;
+ return defaultCreateElement('td', getReactProps(node), ...children);
+ },
+
+ [NodeType.FRAGMENT]: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('fragment', getReactProps(node), ...children),
+
+ // Default handler for unknown node types
+ default: (node: Node, children: ReactNode[]) =>
+ defaultCreateElement('div', getReactProps(node), ...children),
+};
+
+/**
+ * Convert a single node to React element
+ */
+function nodeToReact(
+ node: AnyNode,
+ renderOptions: ReactRenderOptions,
+ createElement: (type: any, props?: any, ...children: any[]) => ReactElement,
+ key?: string | number
+): ReactNode {
+ // Handle text nodes
+ if (!node.type) {
+ return textNodeToReact(node as TextNode, createElement);
+ }
+
+ // Process children recursively
+ const children = node.children ? node.children.map((child, index) =>
+ nodeToReact(child, renderOptions, createElement, index)
+ ).filter(child => child !== null && child !== undefined) : [];
+
+ // Get renderer for this node type
+ const renderer = renderOptions[node.type] || renderOptions.default;
+
+ if (typeof renderer === 'function') {
+ const result = (renderer as ReactRenderNode)(node, children);
+
+ // Add key if it's a React element and key is provided
+ if (isReactElement(result) && key !== undefined) {
+ return addKeyToReactElement(result, key, createElement);
+ }
+
+ return result;
+ }
+
+ // Fallback: return children wrapped in a div
+ return createElement('div', { key }, ...children);
+}
+
+/**
+ * Convert Document or Document array to React elements
+ */
+function documentToReact(
+ content: Document | Document[],
+ renderOptions: ReactRenderOptions,
+ createElement: (type: any, props?: any, ...children: any[]) => ReactElement
+): ReactNode | ReactNode[] {
+ if (content instanceof Array) {
+ return content.map((doc, index) => {
+ if (doc.type === 'doc') {
+ const children = doc.children ? doc.children.map((child, childIndex) =>
+ nodeToReact(child, renderOptions, createElement, childIndex)
+ ).filter(child => child !== null && child !== undefined) : [];
+
+ // For document arrays, wrap each document in a fragment or return children directly
+ return children.length === 1 ? children[0] : createElement('div', { key: index }, ...children);
+ }
+ return nodeToReact(doc as any, renderOptions, createElement, index);
+ });
+ } else if (content.type === 'doc') {
+ const children = content.children ? content.children.map((child, index) =>
+ nodeToReact(child, renderOptions, createElement, index)
+ ).filter(child => child !== null && child !== undefined) : [];
+
+ // For single document, return children directly or wrapped in fragment
+ return children.length === 1 ? children[0] : createElement('div', {}, ...children);
+ }
+
+ return nodeToReact(content as any, renderOptions, createElement);
+}
+
+/**
+ * Main function to convert JSON RTE to React elements
+ * This function modifies the entry object in place, replacing JSON RTE content with React elements
+ */
+export function jsonToReact(options: JsonToReactOptions): void {
+ const {
+ entry,
+ paths,
+ renderOptions = {},
+ createElement = defaultCreateElement
+ } = options;
+
+ // Merge default and custom render options
+ const mergedRenderOptions: ReactRenderOptions = {
+ ...defaultReactRenderOptions,
+ ...renderOptions
+ };
+
+ function processEntry(entryItem: EntryEmbedable): void {
+ for (const path of paths) {
+ findRenderContent(path, entryItem, (content: Document | Document[]) => {
+ const reactElement = documentToReact(content, mergedRenderOptions, createElement);
+ setNestedProperty(entryItem, path, reactElement);
+ return content as any; // Return original to satisfy the callback
+ });
+ }
+ }
+
+ if (entry instanceof Array) {
+ entry.forEach(processEntry);
+ } else {
+ processEntry(entry);
+ }
+}
+
+/**
+ * Create a jsonToReact function with custom createElement
+ * Useful when you want to use a specific React version or custom createElement function
+ */
+export function createJsonToReact(createElement: (type: any, props?: any, ...children: any[]) => ReactElement) {
+ return (options: Omit