diff --git a/README.md b/README.md index fbe3355..ae988bf 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,49 @@ Use `resolver` to enable and control the rendering of embedded components, and ` /> ``` +### Content via prop + +By default, content in `nodes` is handled automatically and passed via slots keeping configuration as follows: + +```js +heading: ({ attrs: { level } }) => ({ + component: Text, + props: { variant: `h${level}` }, +}), +``` +This implies that implementation of `Text` is as simple as: +```js +--- +const { variant } = Astro.props; +const Component = variant || "p"; +--- + + + + +``` +However in some cases, the users do implementation via props only, thus without slots: +```js +--- +const { variant, text } = Astro.props; +const Component = variant || "p"; +--- + + + {text} + +``` +This way the content must be handled explictly in the resolver function and passed via prop: +```js +heading: ({ attrs: { level }, content }) => ({ + component: Text, + props: { + variant: `h${level}`, + text: content?.[0].text, + }, +}), +``` + ## Schema The schema has `nodes` and `marks` to be configurable: @@ -155,32 +198,32 @@ The schema has `nodes` and `marks` to be configurable: ```js schema={{ nodes: { - heading: ({ attrs }) => ({ ... }), + heading: (node) => ({ ... }), paragraph: () => ({ ... }), text: () => ({ ... }), hard_break: () => ({ ... }), bullet_list: () => ({ ... }), - ordered_list: ({ attrs }) => ({ ... }), + ordered_list: (node) => ({ ... }), list_item: () => ({ ... }), horizontal_rule: () => ({ ... }), blockquote: () => ({ ... }), - image: ({ attrs }) => ({ ... }), - code_block: ({ attrs }) => ({ ... }), - emoji: ({ attrs }) => ({ ... }), + image: (node) => ({ ... }), + code_block: (node) => ({ ... }), + emoji: (node) => ({ ... }), }, marks: { - link: ({ attrs }) => { ... }, + link: (mark) => { ... }, bold: () => ({ ... }), underline: () => ({ ... }), italic: () => ({ ... }), - styled: ({ attrs }) => { ... }, + styled: (mark) => { ... }, strike: () => ({ ... }), superscript: () => ({ ... }), subscript: () => ({ ... }), code: () => ({ ... }), - anchor: ({ attrs }) => ({ ... }), - textStyle: ({ attrs }) => ({ ... }), - highlight: ({ attrs }) => ({ ... }), + anchor: (mark) => ({ ... }), + textStyle: (mark) => ({ ... }), + highlight: (mark) => ({ ... }), }; }} ``` diff --git a/demo/src/components/CodeBlock.astro b/demo/src/components/CodeBlock.astro deleted file mode 100644 index 25f80a0..0000000 --- a/demo/src/components/CodeBlock.astro +++ /dev/null @@ -1,36 +0,0 @@ ---- -export type Props = { - syntax: string; -}; -const { syntax } = Astro.props; ---- - -
-  {syntax.charAt(0).toUpperCase() + syntax.slice(1)}
-
-
- - diff --git a/demo/src/components/Heading.astro b/demo/src/components/Heading.astro new file mode 100644 index 0000000..c69b20a --- /dev/null +++ b/demo/src/components/Heading.astro @@ -0,0 +1,14 @@ +--- +type HeadingTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + +export type Props = { + as: HeadingTag; + text: string; +}; + +const { as: Element = "h1", text, ...props } = Astro.props; +--- + + + {text} + diff --git a/demo/src/storyblok/RichText.astro b/demo/src/storyblok/RichText.astro index c672f43..791d5eb 100644 --- a/demo/src/storyblok/RichText.astro +++ b/demo/src/storyblok/RichText.astro @@ -1,7 +1,6 @@ --- -import RichTextRenderer, { - type RichTextType, -} from "storyblok-rich-text-astro-renderer/RichTextRenderer.astro"; +import { type RichTextType } from "storyblok-rich-text-astro-renderer"; +import RichTextRenderer from "storyblok-rich-text-astro-renderer/RichTextRenderer.astro"; import { storyblokEditable } from "@storyblok/astro"; import Link from "../components/Link.astro"; import StoryblokComponent from "@storyblok/astro/StoryblokComponent.astro"; @@ -9,9 +8,10 @@ import Text from "../components/Text.astro"; import BulletList from "../components/BulletList.astro"; import Blockquote from "../components/Blockquote.astro"; import Picture from "../components/Picture.astro"; -import CodeBlock from "../components/CodeBlock.astro"; import InlineCode from "../components/InlineCode.astro"; import Styled from "../components/Styled.astro"; +import Heading from "../components/Heading.astro"; +import { Code } from "astro:components"; export interface Props { blok: { @@ -27,9 +27,12 @@ const { text } = blok; content={text} schema={{ nodes: { - heading: ({ attrs: { level } }) => ({ - component: Text, - props: { variant: `h${level}` }, + heading: ({ attrs: { level }, content }) => ({ + component: Heading, + props: { + as: `h${level}`, + text: content?.[0].text, + }, }), paragraph: () => ({ component: Text, @@ -62,9 +65,13 @@ const { text } = blok; component: Picture, props: attrs, }), - code_block: ({ attrs }) => ({ - component: CodeBlock, - props: { syntax: attrs.class?.split("-")[1] }, + code_block: ({ attrs, content }) => ({ + component: Code, // native Astro Code component + props: { + lang: attrs.class?.split("-")[1], + theme: "solarized-dark", + code: content?.[0].text, + }, }), }, marks: { diff --git a/lib/src/types.ts b/lib/src/types.ts index a54ca9f..e0bf6f4 100644 --- a/lib/src/types.ts +++ b/lib/src/types.ts @@ -8,37 +8,37 @@ export type ComponentNode = { content?: string | ComponentNode[]; }; -type ResolverAttrs = ({ attrs }: { attrs: Attrs }) => ComponentNode; +type Resolver = (node: Node) => ComponentNode; type Resolver = () => ComponentNode; export type Schema = { nodes?: { - heading?: ResolverAttrs; - paragraph?: Resolver; - text?: Resolver; - hard_break?: Resolver; - bullet_list?: Resolver; - ordered_list?: ResolverAttrs; - list_item?: Resolver; - horizontal_rule?: Resolver; - blockquote?: Resolver; - image?: ResolverAttrs; - code_block?: ResolverAttrs; - emoji?: ResolverAttrs; + heading?: Resolver; + paragraph?: Resolver; + text?: Resolver; + hard_break?: Resolver; + bullet_list?: Resolver; + ordered_list?: Resolver; + list_item?: Resolver; + horizontal_rule?: Resolver; + blockquote?: Resolver
; + image?: Resolver; + code_block?: Resolver; + emoji?: Resolver; }; marks?: { - link?: ResolverAttrs; + link?: Resolver; bold?: Resolver; underline?: Resolver; italic?: Resolver; - styled?: ResolverAttrs; + styled?: Resolver; strike?: Resolver; superscript?: Resolver; subscript?: Resolver; code?: Resolver; - anchor?: ResolverAttrs; - textStyle?: ResolverAttrs; - highlight?: ResolverAttrs; + anchor?: Resolver; + textStyle?: Resolver; + highlight?: Resolver; }; }; diff --git a/lib/src/utils/resolveRichTextToNodes.spec.ts b/lib/src/utils/resolveRichTextToNodes.spec.ts index 1240452..95f7186 100644 --- a/lib/src/utils/resolveRichTextToNodes.spec.ts +++ b/lib/src/utils/resolveRichTextToNodes.spec.ts @@ -233,6 +233,30 @@ describe("resolveNode", () => { content: [{ content: "Hello from rich text" }], }); + // with schema override - content via prop + expect( + resolveNode(node, { + schema: { + nodes: { + heading: ({ attrs: { level }, content }) => ({ + component: Text, + props: { + as: `h${level}`, + text: content?.[0].text, // content was resolved explicitly to pass via prop + }, + }), + }, + }, + }) + ).toStrictEqual({ + component: Text, + props: { + as: "h1", + text: "Hello from rich text", + }, + content: [{ content: "Hello from rich text" }], + }); + // empty content expect(resolveNode(emptyNode)).toStrictEqual({ component: "br", diff --git a/lib/src/utils/resolveRichTextToNodes.ts b/lib/src/utils/resolveRichTextToNodes.ts index eb275e7..038c1aa 100644 --- a/lib/src/utils/resolveRichTextToNodes.ts +++ b/lib/src/utils/resolveRichTextToNodes.ts @@ -2,6 +2,7 @@ import type { ComponentNode, Mark, Options, + Resolver, RichTextType, Schema, SchemaNode, @@ -14,7 +15,7 @@ export const resolveMark = ( schema?: Schema ): ComponentNode => { if (mark.type === "link") { - const resolverFn = schema?.marks?.[mark.type]; + const resolverFn: Resolver = schema?.marks?.[mark.type]; const attrs = { ...mark.attrs }; const { linktype = "url" } = mark.attrs; @@ -39,7 +40,7 @@ export const resolveMark = ( component: "a", props: attrs, content, - ...resolverFn?.({ attrs: mark.attrs }), + ...resolverFn?.(mark), }; } @@ -78,7 +79,7 @@ export const resolveMark = ( component: "span", props: attrs, content, - ...resolverFn?.({ attrs }), + ...resolverFn?.(mark), }; } @@ -126,7 +127,7 @@ export const resolveMark = ( component: "span", content, props: attrs, - ...resolverFn?.({ attrs }), + ...resolverFn?.(mark), }; } @@ -140,7 +141,7 @@ export const resolveMark = ( props: { style: { color: attrs.color }, }, - ...resolverFn?.({ attrs }), + ...resolverFn?.(mark), }; } @@ -154,7 +155,7 @@ export const resolveMark = ( props: { style: { backgroundColor: attrs.color }, }, - ...resolverFn?.({ attrs }), + ...resolverFn?.(mark), }; } }; @@ -165,9 +166,9 @@ export const resolveNode = ( options: Options = {} ): ComponentNode => { const { schema } = options; - const resolverFn = schema?.nodes?.[node.type]; if (node.type === "heading") { + const resolverFn = schema?.nodes?.[node.type]; const { content, attrs } = node; // empty line @@ -180,77 +181,87 @@ export const resolveNode = ( return { component: `h${attrs.level}`, content: content.map((node) => resolveNode(node, options)), - ...resolverFn?.({ attrs }), + ...resolverFn?.(node), }; } if (node.type === "hard_break") { + const resolverFn = schema?.nodes?.[node.type]; + return { component: "br", - ...resolverFn?.(), + ...resolverFn?.(node), }; } if (node.type === "horizontal_rule") { + const resolverFn = schema?.nodes?.[node.type]; + return { component: "hr", - ...resolverFn?.(), + ...resolverFn?.(node), }; } if (node.type === "blockquote") { + const resolverFn = schema?.nodes?.[node.type]; const { content } = node; return { component: "blockquote", content: content.map((node) => resolveNode(node, options)), - ...resolverFn?.(), + ...resolverFn?.(node), }; } if (node.type === "image") { + const resolverFn = schema?.nodes?.[node.type]; const { attrs } = node; const { src, alt } = attrs; return { component: "img", props: { src, alt }, - ...resolverFn?.({ attrs }), + ...resolverFn?.(node), }; } if (node.type === "code_block") { + const resolverFn = schema?.nodes?.[node.type]; const { attrs, content } = node; return { component: "pre", props: { class: attrs.class }, content: content.map((node) => resolveNode(node, options)), - ...resolverFn?.({ attrs }), + ...resolverFn?.(node), }; } if (node.type === "ordered_list") { - const { content, attrs } = node; + const resolverFn = schema?.nodes?.[node.type]; + const { content } = node; return { component: "ol", content: content.map((node) => resolveNode(node, options)), - ...resolverFn?.({ attrs }), + ...resolverFn?.(node), }; } if (node.type === "bullet_list") { + const resolverFn = schema?.nodes?.[node.type]; const { content } = node; return { component: "ul", content: content.map((node) => resolveNode(node, options)), - ...resolverFn?.(), + ...resolverFn?.(node), }; } if (node.type === "list_item") { + const resolverFn = schema?.nodes?.[node.type]; const { content } = node; return { @@ -266,11 +277,12 @@ export const resolveNode = ( return resolveNode(node, options); }), - ...resolverFn?.(), + ...resolverFn?.(node), }; } if (node.type === "text") { + const resolverFn = schema?.nodes?.[node.type]; const { text, marks } = node; if (marks) { @@ -281,17 +293,18 @@ export const resolveNode = ( return { content: marked, - ...resolverFn?.(), + ...resolverFn?.(node), }; } return { content: text, - ...resolverFn?.(), + ...resolverFn?.(node), }; } if (node.type === "paragraph") { + const resolverFn = schema?.nodes?.[node.type]; const { content } = node; // empty line @@ -304,7 +317,7 @@ export const resolveNode = ( return { component: "p", content: content.map((node) => resolveNode(node, options)), - ...resolverFn?.(), + ...resolverFn?.(node), }; } @@ -322,12 +335,13 @@ export const resolveNode = ( } if (node.type === "emoji") { + const resolverFn = schema?.nodes?.[node.type]; const { attrs } = node; const { emoji } = attrs; return { content: emoji, - ...resolverFn?.({ attrs }), + ...resolverFn?.(node), }; } };