Skip to content

Commit

Permalink
feat(component): enable custom text rendering in PortableText (#165)
Browse files Browse the repository at this point in the history
Provides users with fine-grained control over text node output via the new `text` option
  • Loading branch information
theisel authored Nov 27, 2024
1 parent 443d491 commit 80c147a
Show file tree
Hide file tree
Showing 13 changed files with 355 additions and 86 deletions.
1 change: 1 addition & 0 deletions astro-portabletext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import { PortableText } from "astro-portabletext";
strong: /* <strong {...attrs}><slot /></strong> */,
underline: /* <span {...attrs} style="text-decoration: underline;"><slot /></span> */
},
text: /* renders string; use custom handler to change output */
hardBreak: /* <br /> */,
}
```
Expand Down
214 changes: 128 additions & 86 deletions astro-portabletext/components/PortableText.astro
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,22 @@ import type {
TypedObject,
} from "../lib/types";
import type { Component, NodeType } from "../lib/internal";
import { isComponent, mergeComponents } from "../lib/internal";
import {
isComponent,
mergeComponents,
type Component,
type NodeType,
} from "../lib/internal";
import { getWarningMessage, printWarning } from "../lib/warnings";
import type { Context } from "../lib/context";
import { key as contextRef } from "../lib/context";
import { key as contextRef, type Context } from "../lib/context";
import Block from "./Block.astro";
import HardBreak from "./HardBreak.astro";
import List from "./List.astro";
import ListItem from "./ListItem.astro";
import Mark from "./Mark.astro";
import Text from "./Text.astro";
import UnknownBlock from "./UnknownBlock.astro";
import UnknownList from "./UnknownList.astro";
import UnknownListItem from "./UnknownListItem.astro";
Expand Down Expand Up @@ -84,6 +87,7 @@ const components = mergeComponents(
underline: Mark,
},
unknownMark: UnknownMark,
text: Text,
hardBreak: HardBreak,
},
componentOverrides
Expand Down Expand Up @@ -125,66 +129,97 @@ const asComponentProps = (
const provideComponent = (
nodeType: NodeType,
type: string
): Component | undefined => {
const component = components[nodeType];
return isComponent(component)
? component
: (component[type as keyof typeof component] ??
(missingComponentHandler(getWarningMessage(nodeType, type), {
nodeType,
type,
}) as undefined));
type: string,
fallbackComponent: Component
): Component => {
const component: Component | undefined = ((component) => {
return component[type as keyof typeof component] || component;
})(components[nodeType]);
if (isComponent(component)) {
return component;
}
missingComponentHandler(getWarningMessage(nodeType, type), {
nodeType,
type,
});
return fallbackComponent;
};
const prepareForRender = (
props: ComponentProps<TypedObject>
): [Component | string, ComponentProps<TypedObject>[]] => {
):
| [Component | string, ComponentProps<TypedObject>[]]
| [Component | string] => {
const { node } = props;
return isPortableTextToolkitList(node)
? [
provideComponent("list", node.listItem) ?? components.unknownList,
serializeChildren(node, false),
]
: isPortableTextListItemBlock(node)
? [
provideComponent("listItem", node.listItem) ??
components.unknownListItem,
serializeMarksTree(node).map((children) => {
if (node.style !== "normal") {
const { listItem, ...blockNode } = node;
children = serializeNode(false)(blockNode, 0);
}
return children;
}),
]
: isPortableTextToolkitSpan(node)
? [
provideComponent("mark", node.markType) ?? components.unknownMark,
serializeChildren(node, true),
]
: isPortableTextBlock(node)
? [
provideComponent(
"block",
node.style ??
(node.style = "normal") /* Make sure style has been set */
) ?? components.unknownBlock,
serializeMarksTree(node),
]
: isPortableTextToolkitTextNode(node)
? [
"\n" === node.text && isComponent(components.hardBreak)
? components.hardBreak
: node.text,
[],
]
: [
provideComponent("type", node._type) ?? components.unknownType,
[],
];
if (isPortableTextToolkitList(node)) {
return [
provideComponent(
"list",
node.listItem,
components.unknownList ?? UnknownList
),
serializeChildren(node, false),
];
}
if (isPortableTextListItemBlock(node)) {
return [
provideComponent(
"listItem",
node.listItem,
components.unknownListItem ?? UnknownListItem
),
serializeMarksTree(node).map((children) => {
if (node.style !== "normal") {
const { listItem, ...blockNode } = node;
children = serializeNode(false)(blockNode, 0);
}
return children;
}),
];
}
if (isPortableTextToolkitSpan(node)) {
return [
provideComponent(
"mark",
node.markType,
components.unknownMark ?? UnknownMark
),
serializeChildren(node, true),
];
}
if (isPortableTextBlock(node)) {
return [
provideComponent(
"block",
(node.style ??= "normal") /* Make sure style has been set */,
components.unknownBlock ?? UnknownBlock
),
serializeMarksTree(node),
];
}
if (isPortableTextToolkitTextNode(node)) {
return [
"\n" === node.text
? isComponent(components.hardBreak)
? components.hardBreak
: HardBreak
: isComponent(components.text)
? components.text
: Text,
];
}
return [
provideComponent("type", node._type, components.unknownType ?? UnknownType),
];
};
(globalThis as any)[contextRef] = (node: TypedObject): Context => {
Expand All @@ -196,36 +231,43 @@ const prepareForRender = (
// Returns the `default` component related to the passed in node
const provideDefaultComponent = (node: TypedObject) => {
return isPortableTextToolkitList(node)
? List
: isPortableTextListItemBlock(node)
? ListItem
: isPortableTextToolkitSpan(node)
? Mark
: isPortableTextBlock(node)
? Block
: isPortableTextToolkitTextNode(node)
? HardBreak
: UnknownType;
if (isPortableTextToolkitList(node)) return List;
if (isPortableTextListItemBlock(node)) return ListItem;
if (isPortableTextToolkitSpan(node)) return Mark;
if (isPortableTextBlock(node)) return Block;
if (isPortableTextToolkitTextNode(node)) {
return "\n" === node.text ? HardBreak : Text;
}
return UnknownType;
};
// Returns the `unknown` component related to the passed in node
const provideUnknownComponent = (node: TypedObject) => {
return isPortableTextToolkitList(node)
? components.unknownList
: isPortableTextListItemBlock(node)
? components.unknownListItem
: isPortableTextToolkitSpan(node)
? components.unknownMark
: isPortableTextBlock(node)
? components.unknownBlock
: !isPortableTextToolkitTextNode(node)
? components.unknownType
: (() => {
throw new Error(
`[PortableText getUnknownComponent] Unable to provide component with node type ${node._type}`
);
})();
if (isPortableTextToolkitList(node)) {
return components.unknownList ?? UnknownList;
}
if (isPortableTextListItemBlock(node)) {
return components.unknownListItem ?? UnknownListItem;
}
if (isPortableTextToolkitSpan(node)) {
return components.unknownMark ?? UnknownMark;
}
if (isPortableTextBlock(node)) {
return components.unknownBlock ?? UnknownBlock;
}
if (!isPortableTextToolkitTextNode(node)) {
return components.unknownType ?? UnknownType;
}
throw new Error(
`[PortableText getUnknownComponent] Unable to provide component with node type ${node._type}`
);
};
// Make sure we have an array of blocks
Expand All @@ -243,7 +285,7 @@ function* renderBlocks() {

{
[...renderBlocks()].map(function render(props) {
const [Cmp, children] = prepareForRender(props);
const [Cmp, children = []] = prepareForRender(props);

return !isComponent(Cmp) ? (
<Fragment set:text={Cmp} />
Expand Down
9 changes: 9 additions & 0 deletions astro-portabletext/components/Text.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
import type { TextNode, Props as $ } from "../lib/types";
export type Props = $<TextNode>;
const { node } = Astro.props;
---

{node.text}
4 changes: 4 additions & 0 deletions astro-portabletext/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ export interface PortableTextComponents {
* Used when a {@link PortableTextComponents.mark mark} component isn't found
*/
unknownMark: Component<Mark<never>>;
/**
* How text should be rendered
*/
text: Component<TextNode>;
/**
* How line breaks should be rendered
*/
Expand Down
10 changes: 10 additions & 0 deletions lab/src/components/TextReplace.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
import type { TextNode, Props as $ } from "astro-portabletext/types";
export type Props = $<TextNode>;
const { node } = Astro.props;
const replacedText = node.text.replace('programmer', 'JavaScript developer').replace('arrays', 'callbacks');
---

{replacedText}
17 changes: 17 additions & 0 deletions lab/src/components/TextStyleBySplit.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
import type { TextNode, Props as $ } from "astro-portabletext/types";
export type Props = $<TextNode>;
const { node } = Astro.props;
---

{node.text.split(' ').map((it, idx) => idx === 0 ? (
<>&nbsp;<span>{it}</span></>
) : it)}

<style>
span {
color: yellow;
}
</style>
19 changes: 19 additions & 0 deletions lab/src/components/TextStylebyIndex.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
import type { TextNode, Props as $ } from "astro-portabletext/types";
export type Props = $<TextNode>;
const { node, index } = Astro.props;
---
{index === 1 ? (
<>
&nbsp;
<span>{node.text.trim()}</span>
</>
) : node.text}

<style>
span {
color: green;
}
</style>
20 changes: 20 additions & 0 deletions lab/src/pages/text/default.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
import { PortableText } from "astro-portabletext";
import Layout from "../../layouts/Default.astro";
const blocks = [
{
_type: "block",
children: [
{
_type: "span",
text: "hello world",
},
],
},
];
---

<Layout>
<PortableText value={blocks} />
</Layout>
21 changes: 21 additions & 0 deletions lab/src/pages/text/replace.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
import { PortableText } from "astro-portabletext";
import Layout from "../../layouts/Default.astro";
import TextReplace from '../../components/TextReplace.astro';
const blocks = [
{
_type: "block",
children: [
{
_type: "span",
text: "Why did the programmer quit his job? Because he didn't get arrays.",
},
],
},
];
---

<Layout>
<PortableText value={blocks} components={{text: TextReplace}}/>
</Layout>
Loading

0 comments on commit 80c147a

Please sign in to comment.