diff --git a/README.md b/README.md index fbe3355..aefd734 100644 --- a/README.md +++ b/README.md @@ -104,9 +104,7 @@ const { text } = blok; ## Advanced usage Sensible default resolvers for marks and nodes are provided out-of-the-box. You only have to provide custom ones if you want to -override the default behavior. - -Use `resolver` to enable and control the rendering of embedded components, and `schema` to control how you want the nodes and marks be rendered: +override the default behavior: ```js { + const currentYear = new Date().getFullYear().toString(); + return { + content: text.replaceAll("{currentYear}", currentYear), + }; }} {...storyblokEditable(blok)} /> ``` +- `schema` - controls how you want the nodes and marks be rendered +- `resolver` - enables and controls the rendering of embedded components +- `textResolver` - controls the rendering of the plain text. Useful if you need some text preprocessing (translation, sanitization, etc.) + ## Schema The schema has `nodes` and `marks` to be configurable: diff --git a/demo/src/pages/index.astro b/demo/src/pages/index.astro index cda75c2..c26660b 100644 --- a/demo/src/pages/index.astro +++ b/demo/src/pages/index.astro @@ -397,6 +397,15 @@ const richTextFromStoryblok: RichTextType = { }, ], }, + { + type: "paragraph", + content: [ + { + text: "© {currentYear} Company X. All Rights Reserved", + type: "text", + }, + ], + }, ], }; diff --git a/demo/src/storyblok/RichText.astro b/demo/src/storyblok/RichText.astro index c672f43..528031f 100644 --- a/demo/src/storyblok/RichText.astro +++ b/demo/src/storyblok/RichText.astro @@ -94,5 +94,11 @@ const { text } = blok; props: { blok }, }; }} + textResolver={(text) => { + const currentYear = new Date().getFullYear().toString(); + return { + content: text.replaceAll("{currentYear}", currentYear), + }; + }} {...storyblokEditable(blok)} /> diff --git a/lib/RichTextRenderer.astro b/lib/RichTextRenderer.astro index 4fce1ce..3f337bc 100644 --- a/lib/RichTextRenderer.astro +++ b/lib/RichTextRenderer.astro @@ -17,11 +17,28 @@ export type Props = { ``` */ resolver: Options["resolver"]; + /** + Function to control the rendering of the plain text. Useful for text preprocessing, f.e. + ``` + // replaces {currentYear} substring with the actual value in all texts + textResolver={(text) => { + const currentYear = new Date().getFullYear().toString(); + return { + content: text.replaceAll("{currentYear}", currentYear), + }; + }} + ``` + */ + textResolver: Options["textResolver"]; }; -const { content, schema, resolver, ...props } = Astro.props; +const { content, schema, resolver, textResolver, ...props } = Astro.props; -const nodes = resolveRichTextToNodes(content, { schema, resolver }); +const nodes = resolveRichTextToNodes(content, { + schema, + resolver, + textResolver, +}); ---
diff --git a/lib/src/types.ts b/lib/src/types.ts index a54ca9f..0bbbe66 100644 --- a/lib/src/types.ts +++ b/lib/src/types.ts @@ -45,6 +45,7 @@ export type Schema = { export type Options = { schema?: Schema; resolver?: (blok: SbBlok) => ComponentNode; + textResolver?: (str: string) => ComponentNode; }; export type Anchor = { diff --git a/lib/src/utils/resolveRichTextToNodes.spec.ts b/lib/src/utils/resolveRichTextToNodes.spec.ts index 5195187..b5b3807 100644 --- a/lib/src/utils/resolveRichTextToNodes.spec.ts +++ b/lib/src/utils/resolveRichTextToNodes.spec.ts @@ -113,6 +113,83 @@ describe("resolveNode", () => { }); }); + it("text with textResolver", () => { + const node: SchemaNode = { + text: "Hello {name}", + type: "text", + }; + + const textResolver = (text: string) => { + return { + content: text.replace("{name}", "World"), + }; + }; + + // default + expect( + resolveNode(node, { + textResolver, + }) + ).toStrictEqual({ + content: "Hello World", + }); + + // with marks + expect( + resolveNode( + { + ...node, + marks: [{ type: "bold" }], + }, + { + textResolver, + } + ) + ).toStrictEqual({ + content: [ + { + component: "b", + content: [ + { + content: "Hello World", + }, + ], + }, + ], + }); + + // with schema override + expect( + resolveNode(node, { + schema: { + nodes: { + text: () => ({ + component: "p", + props: { class: "class-1" }, + }), + }, + }, + textResolver: (text) => ({ + content: text.replace("{name}", "World"), + component: "span", + props: { class: "class-2" }, + }), + }) + ).toStrictEqual({ + component: "p", + props: { + class: "class-1", + }, + content: [ + { + component: "span", + props: { class: "class-2" }, + content: "Hello World", + }, + ], + }); + }); + it("paragraph", () => { const node: SchemaNode = { type: "paragraph", diff --git a/lib/src/utils/resolveRichTextToNodes.ts b/lib/src/utils/resolveRichTextToNodes.ts index f025cc3..7c20988 100644 --- a/lib/src/utils/resolveRichTextToNodes.ts +++ b/lib/src/utils/resolveRichTextToNodes.ts @@ -164,7 +164,7 @@ export const resolveNode = ( node: SchemaNode, options: Options = {} ): ComponentNode => { - const { schema } = options; + const { schema, textResolver } = options; const resolverFn = schema?.nodes?.[node.type]; if (node.type === "heading") { @@ -274,7 +274,13 @@ export const resolveNode = ( const { text, marks } = node; if (marks) { - let marked: ComponentNode[] = [{ content: text }]; + let marked: ComponentNode[] = [ + { + content: text, + ...textResolver?.(text), + }, + ]; + [...marks].reverse().forEach((mark) => { marked = [resolveMark(marked, mark, schema)]; }); @@ -285,9 +291,21 @@ export const resolveNode = ( }; } + const resolverResult = resolverFn?.(); + const textResolverResult = textResolver?.(text); + + if (resolverResult && textResolverResult) { + return { + component: resolverResult.component, + props: resolverResult.props, + content: [textResolverResult], + }; + } + return { content: text, - ...resolverFn?.(), + ...textResolverResult, + ...resolverResult, }; }