Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add textResolver #35

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<RichTextRenderer
Expand Down Expand Up @@ -144,10 +142,20 @@ Use `resolver` to enable and control the rendering of embedded components, and `
props: { blok },
};
}}
textResolver={(text) => {
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:
Expand Down
9 changes: 9 additions & 0 deletions demo/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,15 @@ const richTextFromStoryblok: RichTextType = {
},
],
},
{
type: "paragraph",
content: [
{
text: "© {currentYear} Company X. All Rights Reserved",
type: "text",
},
],
},
],
};

Expand Down
6 changes: 6 additions & 0 deletions demo/src/storyblok/RichText.astro
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,11 @@ const { text } = blok;
props: { blok },
};
}}
textResolver={(text) => {
const currentYear = new Date().getFullYear().toString();
return {
content: text.replaceAll("{currentYear}", currentYear),
};
}}
alexander-lozovsky marked this conversation as resolved.
Show resolved Hide resolved
{...storyblokEditable(blok)}
/>
21 changes: 19 additions & 2 deletions lib/RichTextRenderer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
---

<div {...props}>
Expand Down
1 change: 1 addition & 0 deletions lib/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type Schema = {
export type Options = {
schema?: Schema;
resolver?: (blok: SbBlok) => ComponentNode;
textResolver?: (str: string) => ComponentNode;
};

export type Anchor = {
Expand Down
77 changes: 77 additions & 0 deletions lib/src/utils/resolveRichTextToNodes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
}),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this does look weird honestly, and I am sure it clashes with the existing API. The problem is that conceptually the textResolver should be meant for text resolving only, and should not return ComponentNode at all. This function is not about the node logic at all, but rather about manipulating the content property in whatever the node is.

to get same result you have described with solution we should better have the following API:

     schema: {
        nodes: {
          text: () => ({
            component: "p",
            props: { class: "class-1" },
          }),
        },
      },
      textResolver: (text) => text.replace("{name}", "World")

(the textResolver extends the capability of the node, but does not override it)

Alternatively, we can expect the shape of textResolver?: (str: string) => Pick<ComponentNode, 'content'>;
and the usage:

 textResolver: (text) => ({
    content: text.replace("{name}", "World"),
 }),

but again, this may misslead users that it is not the whole component node but rather just a content, so might be bad alternative.

What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should return ComponentNode for cases when you example you want to support ICU syntax. So in this case all the texts need to be rendered inside some .astro component

I can suggest to support both outputs:
textResolver?: (str: string) => ComponentNode | string;

})
).toStrictEqual({
component: "p",
props: {
class: "class-1",
},
content: [
{
component: "span",
props: { class: "class-2" },
content: "Hello World",
},
],
});
});

it("paragraph", () => {
const node: SchemaNode = {
type: "paragraph",
Expand Down
23 changes: 21 additions & 2 deletions lib/src/utils/resolveRichTextToNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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)];
});
Expand All @@ -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,
};
}

Expand Down
Loading