From be99ea1f1c51114a5de759f32c5fbeddadaa0b9a Mon Sep 17 00:00:00 2001 From: "harshitha.d" Date: Wed, 24 Sep 2025 12:11:42 +0530 Subject: [PATCH 1/2] feat: add react component mapping functionality for json rte --- .talismanrc | 6 + src/Models/react-types.ts | 44 +++++++ src/helper/react-utils.ts | 195 ++++++++++++++++++++++++++++++ src/index.ts | 13 +- src/json-to-react.ts | 245 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 502 insertions(+), 1 deletion(-) create mode 100644 src/Models/react-types.ts create mode 100644 src/helper/react-utils.ts create mode 100644 src/json-to-react.ts diff --git a/.talismanrc b/.talismanrc index 9e803b5..09414e3 100644 --- a/.talismanrc +++ b/.talismanrc @@ -8,3 +8,9 @@ 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 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) => { + jsonToReact({ ...options, createElement }); + }; +} + +export { ReactRenderOptions, JsonToReactOptions } from './Models/react-types'; From ca1a4c1c6b4c5632709c15d53e887691ef8ce995 Mon Sep 17 00:00:00 2001 From: "harshitha.d" Date: Wed, 24 Sep 2025 12:16:20 +0530 Subject: [PATCH 2/2] test: add comprehensive tests for json to react and default react render options --- .talismanrc | 2 + __test__/json-to-react.test.ts | 447 +++++++++++++++++++++++++++++++++ 2 files changed, 449 insertions(+) create mode 100644 __test__/json-to-react.test.ts diff --git a/.talismanrc b/.talismanrc index 09414e3..10cdf70 100644 --- a/.talismanrc +++ b/.talismanrc @@ -14,3 +14,5 @@ fileignoreconfig: 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'); + }); +});