This module also allows you to manipulate the intermediate JSON structure prior to converting it into Portable text or if you prefer to work with a tree structure, rather than portable text.
Output of nodeParse
and browserParse
methods is a simple tree structure, defined by the following interface.
interface ParseResult {
children: DomNode[];
}
DomNode
is a union of DomHtmlNode
and DomTextNode
, which together define the full HTML tree structure:
The structure can be modified using transformToJson
method which accepts ParseResult
as the first argument and an optional customResolvers
object, which can contain two methods resolveDomTextNode
and resolveDomHtmlNode
. Each method is responsible for manipulating its respective node type, allowing you to transform the output as per your requirements.
Example use of the transformToJson
method:
const transformJsonWithCustomResolvers = (result: ParseResult) => transformToJson(result, {
resolveDomTextNode: customResolveDomTextNode,
resolveDomHtmlNode: customResolveDomHtmlNode
})
const customResolveDomTextNode: ResolveDomTextNodeType = node => {
return {
text: node.content
};
}
const customResolveDomHtmlNode: ResolveDomHtmlNodeType = (node, traverse) => {
let result = {
tag: node.tagName
};
switch (node.tagName) {
case 'figure': {
const figureObject = {
'imageId': node.attributes['data-image-id']
};
result = { ...result, ...figureObject }
break;
}
case "img": {
const imgObject = {
'src': node.attributes['src'],
'alt': node.attributes['alt']
}
result = { ...result, ...imgObject }
break;
}
case 'ol': {
const tdObject = {
'tag': 'ol'
};
result = { ...result, ...tdObject }
break;
}
case 'ul': {
const tdObject = {
'tag': 'ul'
};
result = { ...result, ...tdObject }
break;
}
case 'li': {
let tdObject = {
'tag': 'li',
'text': node.children[0].type === 'text' ? node.children[0].content : ""
};
if (node.children.length > 1) {
tdObject = { ...tdObject, ...{ children: node.children.slice(1).map(node => traverse(node)) } }
}
return { ...result, ...tdObject }
}
case "object": {
if (node.attributes['type'] === 'application/kenticocloud') {
const linkedItemObject = {
codeName: node.attributes['data-codename']
};
result = { ...result, ...linkedItemObject }
}
break;
}
default: {
}
}
return result;
}
const originalTree = browserParse(richTextValue);
const transformedTree = transformJsonWithCustomResolvers(originalTree);
If you prefer working with a tree structure, rather than Portable text, you can implement resolution around the ParseResult
tree. See examples below.
const parsedTree = browserParse(richTextValue);
const resolve = (domNode: DomNode): string => {
switch (node.type) {
case "tag": {
if (isLinkedItem(node)) {
return resolveLinkedItem(node);
} else if (isImage(node)) {
return resolveImage(node);
} else if (isItemLink(node)) {
return resolveItemLink(node);
} else {
// Recursively calls `resolve` for node's children
return resolveHtmlElement(node);
}
}
case "text":
return node.content;
default:
throw new Error("Invalid input.");
}
};
const resolvedHtml = parsedTree.children.map(resolve).join("");
Resolution method implementation varies based on the use cases. This is just a showcase to present how to get information for node specific data.
const resolveHtmlElement = (node: DomHtmlNode): string => {
const attributes = Object.entries(node.attributes)
.map(([key, value]) => `${key}="${value}"`)
.join(" ");
const openingTag = `<${node.tagName} ${attributes}>`;
const closingTag = `</${node.tagName}>`;
// Recursively calls `resolve` for node's children
return `${openingTag}${node.children.map(link).join("")}${closingTag}`;
};
Image attributes contain just the information parsed from HTML. Image context is being returned as a part of the Delivery API response - in the sample below being loaded by getImage
method.
resolveImage = (node: DomHtmlNode): string => {
const imageId = node.attributes["data-asset-id"];
const image = getImage(imageId);
return `<img src=${image.url}/>`;
};
Link attributes contain just the information parsed from HTML. Link context is being returned as a part of the Delivery API response - in the sample below being loaded by getLink
method.
const resolveItemLink = (node: DomHtmlNode): string => {
const linkId = node.attributes["data-item-id"];
const link = getLink(linkId);
return `<a href="${resolveLink(link)}">${node.children.map(link).join()}</a>`;
};
Linked item attributes contain just the information parsed from HTML. Linked item context is being returned as a part of the Delivery API response - in the sample below being loaded by getLinkedContentItem
method.
const resolveLinkedItem = (node: DomHtmlNode): string => {
const itemCodeName = node.attributes["data-codename"];
const item = getLinkedContentItem(itemCodeName);
switch (item.system.type) {
case "quote":
return `<quote>${item.elements.quote_text.value}</quote>`;
// ...
default:
return `<strong>UNSUPPORTED CONTENT ITEM</strong>`;
}
};
// assumes element prop comes from JS SDK
type Props = Readonly<{
element: Elements.RichTextElement;
className: string;
}>;
const RichText: React.FC<Props> = ({element, className}) => {
const [richTextContent, setRichTextContent] = useState<JSX.Element[] | null>(null);
useEffect(() => {
const parsedTree = browserParse(element.value);
const resolve = (domNode: DomNode, index: number): JSX.Element => {
switch (domNode.type) {
case 'tag': {
// traverse tree recursively
const resolvedChildElements = domNode.children.map(node => resolve(node, index));
// omit children parameter for non-pair elements like <br>
if (isUnpairedElement(domNode)) {
return React.createElement(domNode.tagName, {...domNode.attributes});
}
if (isLinkedItem(domNode)) {
const linkedItem = element.linkedItems.find(item => item.system.codename === domNode.attributes['data-codename']);
switch (linkedItem?.system.type) {
case 'youtube_video': {
return <YoutubeVideo key={index} id={linkedItem.elements.videoId.value} />;
}
// resolution for other types
default: {
return <div key={index}>Failed resolving item {linkedItem.system.codename}. Resolver for type {linkedItem.system.type} not implemented.</div>;
}
}
// if (isImage(domNode)) {...}
// if (isLink(domNode)) {...}
}
const attributes = { ...domNode.attributes, key: index };
return React.createElement(domNode.tagName, attributes, resolvedChildElements);
}
case: 'text': {
return <React.Fragment key={index}>{domNode.content}</React.Fragment>
}
default: throw new Error("Invalid input.")
}
}
const result = parsedTree.children.map((node, index) => resolve(node, index));
setRichTextContent(result);
}, [element]);
return (
<div className={className}>
{richTextContent}
</div>
);
};