diff --git a/README.md b/README.md index 72916bc..a0dceff 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.) + ### Content via prop By default, content in `nodes` is handled automatically and passed via slots keeping configuration as follows: 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 791d5eb..c326bf8 100644 --- a/demo/src/storyblok/RichText.astro +++ b/demo/src/storyblok/RichText.astro @@ -101,5 +101,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 e0bf6f4..232a02f 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 95f7186..6b1696c 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 038c1aa..0022e07 100644 --- a/lib/src/utils/resolveRichTextToNodes.ts +++ b/lib/src/utils/resolveRichTextToNodes.ts @@ -165,7 +165,7 @@ export const resolveNode = ( node: SchemaNode, options: Options = {} ): ComponentNode => { - const { schema } = options; + const { schema, textResolver } = options; if (node.type === "heading") { const resolverFn = schema?.nodes?.[node.type]; @@ -286,7 +286,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)]; }); @@ -297,9 +303,22 @@ export const resolveNode = ( }; } + const resolverResult = resolverFn?.(node); + const textResolverResult = textResolver?.(text); + + if (resolverResult && textResolverResult) { + return { + component: resolverResult.component, + props: resolverResult.props, + content: [textResolverResult], + }; + } + return { content: text, ...resolverFn?.(node), + ...textResolverResult, + ...resolverResult, }; }