Skip to content

Commit

Permalink
fix: use typecards to simplify the type
Browse files Browse the repository at this point in the history
  • Loading branch information
thebuilder committed Nov 8, 2024
1 parent 1ab539c commit 0702500
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 79 deletions.
142 changes: 76 additions & 66 deletions src/UmbracoRichText.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { decode } from "html-entities";
import React from "react";
import type {
RichTextElementModel,
RouteAttributes,
UmbracoBlockContext,
import {
type RichTextElementModel,
type RouteAttributes,
type UmbracoBlockContext,
isHtmlElement,
isRootElement,
isTextElement,
isUmbracoBlock,
} from "./types/RichTextTypes";
import { parseStyle } from "./utils/parse-style";

Expand Down Expand Up @@ -86,34 +90,16 @@ function RichTextElement({
} & Pick<RichTextProps, "renderBlock" | "renderNode" | "htmlAttributes">) {
if (!element || element.tag === "#comment" || element.tag === "#root")
return null;
if (element.tag === "#text") {

if (isTextElement(element)) {
// Decode HTML entities in text nodes
return decode(element.text);
}

let children = element.elements?.map((node, index) => (
<RichTextElement
key={index}
element={node}
blocks={blocks}
renderBlock={renderBlock}
renderNode={renderNode}
meta={{
ancestor: element.tag,
previous: element.elements?.[index - 1]?.tag,
next: element.elements?.[index + 1]?.tag,
}}
/>
));
if (!children?.length) children = undefined;

// If the tag is a block, skip the normal rendering and render the block
if (
element.tag === "umb-rte-block" ||
element.tag === "umb-rte-block-inline"
) {
if (isUmbracoBlock(element)) {
const block = blocks?.find(
(block) => block.content?.id === element.attributes["content-id"],
(block) => block.content?.id === element.attributes?.["content-id"],
);
if (renderBlock && block) {
return renderBlock(block);
Expand All @@ -127,60 +113,84 @@ function RichTextElement({
return null;
}

const { route, style, class: className, ...attributes } = element.attributes;
const defaultAttributes = htmlAttributes[element.tag];
if (element.tag === "a" && route?.path) {
attributes.href = route?.path;
}
if (isHtmlElement(element)) {
const children: React.ReactNode =
element.elements.map((node, index) => (
<RichTextElement
key={index}
element={node}
blocks={blocks}
renderBlock={renderBlock}
renderNode={renderNode}
meta={{
ancestor: element.tag,
previous: element.elements?.[index - 1]?.tag,
next: element.elements?.[index + 1]?.tag,
}}
/>
)) || null;

if (className) {
if (defaultAttributes?.className) {
// Merge the default class with the class attribute
attributes.className = `${defaultAttributes.className} ${className}`;
} else {
attributes.className = className;
const {
route,
style,
class: className,
...attributes
} = element.attributes;
const defaultAttributes = htmlAttributes[element.tag];
if (element.tag === "a" && route?.path) {
attributes.href = route?.path;
}
}

if (typeof style === "string") {
attributes.style = parseStyle(style);
}
if (className) {
if (defaultAttributes?.className) {
// Merge the default class with the class attribute
attributes.className = `${defaultAttributes.className} ${className}`;
} else {
attributes.className = className;
}
}

if (renderNode) {
const output = renderNode({
// biome-ignore lint/suspicious/noExplicitAny: Avoid complicated TypeScript logic by using any. The type will be corrected in the implementation.
tag: element.tag as any,
attributes: {
...defaultAttributes,
...attributes,
} as Record<string, unknown>,
children,
route,
meta: meta || {},
});
if (typeof style === "string") {
attributes.style = parseStyle(style);
}

if (output !== undefined) {
// If we got a valid output from the renderElement function, we return it
// `null` we will render nothing, but `undefined` fallback to the default element
return output;
if (renderNode) {
const output = renderNode({
// biome-ignore lint/suspicious/noExplicitAny: Avoid complicated TypeScript logic by using any. The type will be corrected in the implementation.
tag: element.tag as any,
attributes: {
...defaultAttributes,
...attributes,
} as Record<string, unknown>,
children,
route,
meta: meta || {},
});

if (output !== undefined) {
// If we got a valid output from the renderElement function, we return it
// `null` we will render nothing, but `undefined` fallback to the default element
return output;
}
}
}

return React.createElement(
element.tag,
htmlAttributes[element.tag]
? { ...defaultAttributes, ...attributes }
: attributes,
children,
);
return React.createElement(
element.tag,
htmlAttributes[element.tag]
? { ...defaultAttributes, ...attributes }
: attributes,
children,
);
}
return undefined;
}

/**
* Component for rendering a rich text component
*/
export function UmbracoRichText(props: RichTextProps) {
const rootElement = props.data;
if (rootElement?.tag === "#root" && rootElement.elements) {
if (isRootElement(rootElement)) {
return (
<>
{rootElement.elements.map((element, index) => (

Check failure on line 196 in src/UmbracoRichText.tsx

View workflow job for this annotation

GitHub Actions / build

src/__tests__/UmbracoRichText.browser.test.tsx > should not render if the root element is empty

TypeError: Cannot read properties of undefined (reading 'map') ❯ UmbracoRichText src/UmbracoRichText.tsx:196:30 ❯ renderWithHooks ../../../../../node_modules/.vite/deps/vitest-browser-react.js:11549:26 ❯ mountIndeterminateComponent ../../../../../node_modules/.vite/deps/vitest-browser-react.js:14927:21 ❯ beginWork ../../../../../node_modules/.vite/deps/vitest-browser-react.js:15915:22 ❯ beginWork$1 ../../../../../node_modules/.vite/deps/vitest-browser-react.js:19754:22 ❯ performUnitOfWork ../../../../../node_modules/.vite/deps/vitest-browser-react.js:19202:20 ❯ workLoopSync ../../../../../node_modules/.vite/deps/vitest-browser-react.js:19138:13 ❯ renderRootSync ../../../../../node_modules/.vite/deps/vitest-browser-react.js:19117:15 ❯ recoverFromConcurrentError ../../../../../node_modules/.vite/deps/vitest-browser-react.js:18737:28 ❯ performConcurrentWorkOnRoot ../../../../../node_modules/.vite/deps/vitest-browser-react.js:18685:30
Expand Down
54 changes: 46 additions & 8 deletions src/types/RichTextTypes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
import type { Overwrite } from "../utils/helper-types";

export function isRootElement(data: RichTextElementModel | undefined): data is {
tag: "#root";
attributes?: Record<string, unknown>;
elements: RichTextElementModel[];
blocks?: Array<UmbracoBlockContext>;
} {
return !!data && data.tag === "#root";
}

export function isTextElement(
data: RichTextElementModel,
): data is { tag: "#text"; text: string } {
return data.tag === "#text";
}

export function isCommentElement(
data: RichTextElementModel,
): data is { tag: "#comment"; text: string } {
return data.tag === "#comment";
}

export function isUmbracoBlock(data: RichTextElementModel): data is {
tag: string;
attributes: {
"content-id": string;
};
elements: RichTextElementModel[];
} {
return data.tag === "umb-rte-block" || data.tag === "umb-rte-block-inline";
}

export function isHtmlElement(data: RichTextElementModel): data is {
tag: keyof HTMLElementTagNameMap;
attributes: Record<string, unknown> & { route?: RouteAttributes };
elements: RichTextElementModel[];
} {
return "elements" in data;
}

interface BaseBlockItemModel {
content?: {
id: string;
Expand Down Expand Up @@ -67,14 +106,7 @@ export type RichTextElementModel =
text: string;
}
| {
tag: "umb-rte-block";
attributes: {
"content-id": string;
};
elements: RichTextElementModel[];
}
| {
tag: "umb-rte-block-inline";
tag: "umb-rte-block" | "umb-rte-block-inline";
attributes: {
"content-id": string;
};
Expand All @@ -84,4 +116,10 @@ export type RichTextElementModel =
tag: keyof HTMLElementTagNameMap;
attributes: Record<string, unknown> & { route?: RouteAttributes };
elements?: RichTextElementModel[];
}
| {
tag: string;
attributes?: Record<string, unknown> & { route?: RouteAttributes };
elements?: RichTextElementModel[];
blocks?: Array<UmbracoBlockContext>;
};
15 changes: 10 additions & 5 deletions src/utils/rich-text-converter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { decode } from "html-entities";
import type { RichTextElementModel } from "../types/RichTextTypes";
import {
type RichTextElementModel,
isCommentElement,
isHtmlElement,
isTextElement,
} from "../types/RichTextTypes";

const arrayContentLength = (arr: string[]) => arr.join("").length;

Expand All @@ -12,7 +17,7 @@ function iterateRichText(
options: Options,
) {
// Iterate over the elements in the rich text, and find all `#text` elements
if (data.tag === "#text") {
if (isTextElement(data)) {
// Decode the text and remove any extra whitespace/line breaks
const decodedText = decode(data.text).trim().replace(/\s+/g, " ");
// If the text is the first element, or the first character is a special character, don't add a space
Expand All @@ -24,11 +29,11 @@ function iterateRichText(
return acc;
}

if (data.tag === "#comment" || options.ignoreTags?.includes(data.tag)) {
if (isCommentElement(data) || options.ignoreTags?.includes(data.tag)) {
return acc;
}

if (data.elements) {
if (isHtmlElement(data)) {
for (let i = 0; i < data.elements.length; i++) {
iterateRichText(data.elements[i], acc, options);
if (options.maxLength && arrayContentLength(acc) >= options.maxLength) {
Expand All @@ -55,7 +60,7 @@ function findElement(
return undefined;
}

if (data.elements) {
if (isHtmlElement(data)) {
for (let i = 0; i < data.elements.length; i++) {
const result = findElement(data.elements[i], tag);
if (result) {
Expand Down

0 comments on commit 0702500

Please sign in to comment.