Skip to content

Commit aeedf67

Browse files
committed
Refactor rich text renderer and components for improved type safety and structure
1 parent 9ba8581 commit aeedf67

File tree

6 files changed

+381
-104
lines changed

6 files changed

+381
-104
lines changed

packages/optimizely-cms-sdk/src/components/richText/base.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import {
22
buildRenderTree,
3-
extractTextContent,
43
decodeHTML,
5-
mapAttributes,
64
type Node,
7-
type Element,
85
type RenderNode,
96
type RendererConfig,
107
} from './renderer.js';
@@ -49,20 +46,6 @@ export abstract class BaseRichTextRenderer<
4946
return buildRenderTree(nodes, this.config);
5047
}
5148

52-
/**
53-
* Framework-agnostic text extraction (shared across all implementations)
54-
*/
55-
protected extractText(nodes: Node[]): string {
56-
return extractTextContent(nodes);
57-
}
58-
59-
/**
60-
* Framework-agnostic attribute processing (shared across all implementations)
61-
*/
62-
protected processAttributes(node: Element): Record<string, unknown> {
63-
return mapAttributes(node);
64-
}
65-
6649
/**
6750
* Framework-agnostic HTML entity decoding (shared across all implementations)
6851
*/

packages/optimizely-cms-sdk/src/components/richText/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ export type {
44
Node,
55
ElementType,
66
MarkType,
7-
Content,
8-
ElementOfType,
9-
TextWithMark,
107
RendererConfig,
118
HtmlComponentConfig,
129
BaseElementRendererProps,
@@ -22,6 +19,7 @@ export {
2219
mapAttributes,
2320
getTextMarks,
2421
extractTextContent,
22+
createElementData,
2523
decodeHTML,
2624
defaultElementTypeMap,
2725
defaultMarkTypeMap,

packages/optimizely-cms-sdk/src/components/richText/renderer.ts

Lines changed: 164 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,89 @@
1+
/**
2+
* Available types for generic elements that don't need special properties
3+
*/
4+
export type GenericElementType =
5+
| 'paragraph'
6+
| 'heading-one'
7+
| 'heading-two'
8+
| 'heading-three'
9+
| 'heading-four'
10+
| 'heading-five'
11+
| 'heading-six'
12+
| 'bulleted-list'
13+
| 'numbered-list'
14+
| 'list-item'
15+
| 'quote'
16+
| 'code'
17+
| 'pre'
18+
| 'var'
19+
| 'samp'
20+
| 'div'
21+
| 'richText'
22+
| 'br'
23+
| 'table'
24+
| 'tbody'
25+
| 'tr';
26+
27+
/**
28+
* Base element properties shared by all element types
29+
*/
30+
export interface BaseElement {
31+
children: Node[];
32+
class?: string; // allow headless CMS to pass CSS classes
33+
[key: string]: unknown; // custom attributes
34+
}
35+
36+
/**
37+
* Generic element for types that don't need special properties
38+
*/
39+
export interface GenericElement extends BaseElement {
40+
type: GenericElementType;
41+
}
42+
43+
/**
44+
* Link element with required href (mapped from url)
45+
*/
46+
export interface LinkElement extends BaseElement {
47+
type: 'link';
48+
url: string; // Required for links - will be mapped to href
49+
target?: '_blank' | '_self' | '_parent' | '_top';
50+
rel?: string;
51+
title?: string;
52+
}
53+
54+
/**
55+
* Image element with required src (mapped from url)
56+
*/
57+
export interface ImageElement extends BaseElement {
58+
type: 'image';
59+
url: string; // Required for images - will be mapped to src
60+
alt?: string;
61+
title?: string;
62+
width?: number | string;
63+
height?: number | string;
64+
loading?: 'lazy' | 'eager';
65+
}
66+
67+
/**
68+
* Table cell elements with table-specific attributes
69+
*/
70+
export interface TableCellElement extends BaseElement {
71+
type: 'td' | 'th';
72+
colspan?: number;
73+
rowspan?: number;
74+
scope?: 'col' | 'row' | 'colgroup' | 'rowgroup';
75+
}
76+
77+
/**
78+
* Element node (blocks and inline elements) - Discriminated Union
79+
* Based on Slate.js JSON structure with type-specific properties
80+
*/
81+
export type Element =
82+
| GenericElement
83+
| LinkElement
84+
| ImageElement
85+
| TableCellElement;
86+
187
/**
288
* Text node with formatting marks
389
* Based on Slate.js JSON structure
@@ -12,18 +98,6 @@ export type Text = {
1298
[key: string]: unknown; // allow custom marks (e.g., highlight, color)
1399
};
14100

15-
/**
16-
* Element node (blocks and inline elements)
17-
* Based on Slate.js JSON structure
18-
*/
19-
export type Element = {
20-
type: string; // e.g., 'paragraph', 'heading-one', 'link', 'image'
21-
children: Node[];
22-
url?: string; // common on 'link', 'image', 'video'
23-
class?: string; // allow headless CMS to pass CSS classes
24-
[key: string]: unknown; // custom attributes
25-
};
26-
27101
/**
28102
* Union type for all possible nodes (text or element)
29103
* Based on Slate.js JSON structure
@@ -44,25 +118,6 @@ export function isElement(node: Node): node is Element {
44118
return !isText(node);
45119
}
46120

47-
/**
48-
* Utility type to extract text content from Slate.js content
49-
*/
50-
export type Content = Node[];
51-
52-
/**
53-
* Utility type for working with specific element types
54-
*/
55-
export type ElementOfType<T extends ElementType> = Element & {
56-
type: T;
57-
};
58-
59-
/**
60-
* Utility type for working with text nodes with specific marks
61-
*/
62-
export type TextWithMark<T extends MarkType> = Text & {
63-
[K in T]: true;
64-
};
65-
66121
/**
67122
* Props for element renderer components (framework-agnostic)
68123
*/
@@ -145,33 +200,9 @@ export interface RichTextPropsBase<
145200

146201
/**
147202
* Available element types in the default implementation
203+
* Derived from the actual Element discriminated union to ensure consistency
148204
*/
149-
export type ElementType =
150-
| 'paragraph'
151-
| 'heading-one'
152-
| 'heading-two'
153-
| 'heading-three'
154-
| 'heading-four'
155-
| 'heading-five'
156-
| 'heading-six'
157-
| 'bulleted-list'
158-
| 'numbered-list'
159-
| 'list-item'
160-
| 'table'
161-
| 'tbody'
162-
| 'tr'
163-
| 'td'
164-
| 'th'
165-
| 'quote'
166-
| 'link'
167-
| 'image'
168-
| 'br'
169-
| 'code'
170-
| 'pre'
171-
| 'var'
172-
| 'samp'
173-
| 'div'
174-
| 'richText';
205+
export type ElementType = Element['type'];
175206

176207
/**
177208
* Available text marks in the default implementation
@@ -229,20 +260,34 @@ export function mapAttributes(node: Element): Record<string, unknown> {
229260
nodeProps.class = node.class;
230261
}
231262

232-
// Map URL-ish attributes based on common element semantics
233-
if ('url' in node) {
234-
switch (node.type) {
235-
case 'link':
236-
nodeProps.href = node.url;
237-
break;
238-
case 'image':
239-
case 'video':
240-
nodeProps.src = node.url;
241-
break;
242-
default:
263+
// Map URL-ish attributes based on specific element types (type-safe)
264+
switch (node.type) {
265+
case 'link':
266+
nodeProps.href = node.url;
267+
if (node.target) nodeProps.target = node.target;
268+
if (node.rel) nodeProps.rel = node.rel;
269+
if (node.title) nodeProps.title = node.title;
270+
break;
271+
case 'image':
272+
nodeProps.src = node.url;
273+
if (node.alt) nodeProps.alt = node.alt;
274+
if (node.title) nodeProps.title = node.title;
275+
if (node.width) nodeProps.width = node.width;
276+
if (node.height) nodeProps.height = node.height;
277+
if (node.loading) nodeProps.loading = node.loading;
278+
break;
279+
case 'td':
280+
case 'th':
281+
if (node.colspan) nodeProps.colspan = node.colspan;
282+
if (node.rowspan) nodeProps.rowspan = node.rowspan;
283+
if (node.scope) nodeProps.scope = node.scope;
284+
break;
285+
default:
286+
// For generic elements, check if they have a url and map it as data-url
287+
if ('url' in node && node.url) {
243288
nodeProps['data-url'] = node.url;
244-
break;
245-
}
289+
}
290+
break;
246291
}
247292

248293
return nodeProps;
@@ -274,6 +319,55 @@ export function extractTextContent(children: Node[]): string {
274319
.join('');
275320
}
276321

322+
/**
323+
* Creates type-safe element data based on element type and attributes
324+
* This is a utility function that can be used by framework-specific renderers
325+
*/
326+
export function createElementData(
327+
type: string,
328+
attributes: Record<string, unknown> = {}
329+
): Element {
330+
const baseProps = { children: [], ...attributes };
331+
332+
switch (type) {
333+
case 'link':
334+
return {
335+
type: 'link',
336+
url: (attributes.url as string) || (attributes.href as string) || '',
337+
target: attributes.target as any,
338+
rel: attributes.rel as string,
339+
title: attributes.title as string,
340+
...baseProps,
341+
};
342+
case 'image':
343+
return {
344+
type: 'image',
345+
url: (attributes.url as string) || (attributes.src as string) || '',
346+
alt: attributes.alt as string,
347+
title: attributes.title as string,
348+
width: attributes.width as number | string,
349+
height: attributes.height as number | string,
350+
loading: attributes.loading as 'lazy' | 'eager',
351+
...baseProps,
352+
};
353+
case 'td':
354+
case 'th':
355+
return {
356+
type: type as 'td' | 'th',
357+
colspan: attributes.colspan as number,
358+
rowspan: attributes.rowspan as number,
359+
scope: attributes.scope as any,
360+
...baseProps,
361+
};
362+
default:
363+
// For generic elements, we need to cast to the proper generic type
364+
return {
365+
type: type as any, // Type assertion for generic elements
366+
...baseProps,
367+
} as Element;
368+
}
369+
}
370+
277371
/**
278372
* Minimal HTML entity decoder to avoid extra deps
279373
*/

packages/optimizely-cms-sdk/src/react/richText/index.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ export {
44
generateDefaultLeafs,
55
createHtmlComponent,
66
createLeafComponent,
7+
createLinkComponent,
8+
createImageComponent,
9+
createTableCellComponent,
710
} from './lib.js';
811

912
export type {
1013
ElementProps,
1114
LeafProps,
12-
ElementRenderer,
13-
LeafRenderer,
15+
ElementRendererProps,
16+
LinkElementProps,
17+
ImageElementProps,
18+
TableCellElementRendererProps,
1419
ElementMap,
1520
LeafMap,
1621
RichTextProps,
@@ -20,6 +25,12 @@ export type {
2025
Node,
2126
Element,
2227
Text,
28+
LinkElement,
29+
ImageElement,
30+
TableCellElement,
31+
GenericElement,
32+
GenericElementType,
33+
BaseElement,
2334
ElementType,
2435
MarkType,
2536
} from '../../components/richText/renderer.js';

0 commit comments

Comments
 (0)