diff --git a/astro-portabletext/README.md b/astro-portabletext/README.md index 8baabd2..f958aaa 100644 --- a/astro-portabletext/README.md +++ b/astro-portabletext/README.md @@ -92,6 +92,7 @@ import { PortableText } from "astro-portabletext"; strong: /* */, underline: /* */ }, + text: /* renders string; use custom handler to change output */ hardBreak: /*
*/, } ``` diff --git a/astro-portabletext/components/PortableText.astro b/astro-portabletext/components/PortableText.astro index ae03c3d..edadd56 100644 --- a/astro-portabletext/components/PortableText.astro +++ b/astro-portabletext/components/PortableText.astro @@ -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"; @@ -84,6 +87,7 @@ const components = mergeComponents( underline: Mark, }, unknownMark: UnknownMark, + text: Text, hardBreak: HardBreak, }, componentOverrides @@ -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 -): [Component | string, ComponentProps[]] => { +): + | [Component | string, ComponentProps[]] + | [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 => { @@ -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 @@ -243,7 +285,7 @@ function* renderBlocks() { { [...renderBlocks()].map(function render(props) { - const [Cmp, children] = prepareForRender(props); + const [Cmp, children = []] = prepareForRender(props); return !isComponent(Cmp) ? ( diff --git a/astro-portabletext/components/Text.astro b/astro-portabletext/components/Text.astro new file mode 100644 index 0000000..f7c5357 --- /dev/null +++ b/astro-portabletext/components/Text.astro @@ -0,0 +1,9 @@ +--- +import type { TextNode, Props as $ } from "../lib/types"; + +export type Props = $; + +const { node } = Astro.props; +--- + +{node.text} diff --git a/astro-portabletext/lib/types.ts b/astro-portabletext/lib/types.ts index a7228c0..da6d236 100644 --- a/astro-portabletext/lib/types.ts +++ b/astro-portabletext/lib/types.ts @@ -97,6 +97,10 @@ export interface PortableTextComponents { * Used when a {@link PortableTextComponents.mark mark} component isn't found */ unknownMark: Component>; + /** + * How text should be rendered + */ + text: Component; /** * How line breaks should be rendered */ diff --git a/lab/src/components/TextReplace.astro b/lab/src/components/TextReplace.astro new file mode 100644 index 0000000..573d5c4 --- /dev/null +++ b/lab/src/components/TextReplace.astro @@ -0,0 +1,10 @@ +--- +import type { TextNode, Props as $ } from "astro-portabletext/types"; + +export type Props = $; + +const { node } = Astro.props; +const replacedText = node.text.replace('programmer', 'JavaScript developer').replace('arrays', 'callbacks'); +--- + +{replacedText} diff --git a/lab/src/components/TextStyleBySplit.astro b/lab/src/components/TextStyleBySplit.astro new file mode 100644 index 0000000..0d45f5f --- /dev/null +++ b/lab/src/components/TextStyleBySplit.astro @@ -0,0 +1,17 @@ +--- +import type { TextNode, Props as $ } from "astro-portabletext/types"; + +export type Props = $; + +const { node } = Astro.props; +--- + +{node.text.split(' ').map((it, idx) => idx === 0 ? ( + <> {it} +) : it)} + + \ No newline at end of file diff --git a/lab/src/components/TextStylebyIndex.astro b/lab/src/components/TextStylebyIndex.astro new file mode 100644 index 0000000..9745911 --- /dev/null +++ b/lab/src/components/TextStylebyIndex.astro @@ -0,0 +1,19 @@ +--- +import type { TextNode, Props as $ } from "astro-portabletext/types"; + +export type Props = $; + +const { node, index } = Astro.props; +--- +{index === 1 ? ( + <> +   + {node.text.trim()} + +) : node.text} + + \ No newline at end of file diff --git a/lab/src/pages/text/default.astro b/lab/src/pages/text/default.astro new file mode 100644 index 0000000..e4a178a --- /dev/null +++ b/lab/src/pages/text/default.astro @@ -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", + }, + ], + }, +]; +--- + + + + diff --git a/lab/src/pages/text/replace.astro b/lab/src/pages/text/replace.astro new file mode 100644 index 0000000..0ea040a --- /dev/null +++ b/lab/src/pages/text/replace.astro @@ -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.", + }, + ], + }, +]; +--- + + + + diff --git a/lab/src/pages/text/style-by-index.astro b/lab/src/pages/text/style-by-index.astro new file mode 100644 index 0000000..81d2b72 --- /dev/null +++ b/lab/src/pages/text/style-by-index.astro @@ -0,0 +1,29 @@ +--- +import { PortableText } from "astro-portabletext"; +import Layout from "../../layouts/Default.astro"; +import TextStylebyIndex from "../../components/TextStylebyIndex.astro"; + +const blocks = [ + { + _type: "block", + children: [ + { + _type: "span", + text: "Red", + }, + { + _type: "span", + text: " Green", + }, + { + _type: "span", + text: " Blue", + }, + ], + }, +]; +--- + + + + diff --git a/lab/src/pages/text/style-by-split.astro b/lab/src/pages/text/style-by-split.astro new file mode 100644 index 0000000..a67f664 --- /dev/null +++ b/lab/src/pages/text/style-by-split.astro @@ -0,0 +1,21 @@ +--- +import { PortableText } from "astro-portabletext"; +import Layout from "../../layouts/Default.astro"; +import TextStyleBySplit from "../../components/TextStyleBySplit.astro"; + +const blocks = [ + { + _type: "block", + children: [ + { + _type: "span", + text: "Yellow Orange", + }, + ], + }, +]; +--- + + + + diff --git a/lab/src/pages/text/undefined.astro b/lab/src/pages/text/undefined.astro new file mode 100644 index 0000000..c4bec3e --- /dev/null +++ b/lab/src/pages/text/undefined.astro @@ -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", + }, + ], + }, +]; +--- + + + + diff --git a/lab/src/test/text.test.js b/lab/src/test/text.test.js new file mode 100644 index 0000000..97700b4 --- /dev/null +++ b/lab/src/test/text.test.js @@ -0,0 +1,56 @@ +import { suite } from "uvu"; +import * as assert from "uvu/assert"; +import { fetchContent } from "../utils.mjs"; + +const text = suite("text"); + +text("should have `hello world`", async () => { + const $ = await fetchContent("text/default"); + const $el = $("p"); + + assert.is($el.length, 1); + assert.is($el.text(), "hello world"); +}); + +text("should have `hello world` with undefined component", async () => { + const $ = await fetchContent("text/undefined"); + const $el = $("p"); + + assert.is($el.length, 1); + assert.is($el.text(), "hello world"); +}); + +text("should change joke", async () => { + const $ = await fetchContent("text/replace"); + const $el = $("p"); + + assert.is($el.length, 1); + assert.is( + $el.text(), + "Why did the JavaScript developer quit his job? Because he didn't get callbacks." + ); +}); + +text("should style first word by string split", async () => { + const $ = await fetchContent("text/style-by-split"); + const $head = $("head"); + const $p = $("p"); + + assert.is($head.children("style").length, 1); + assert.is($p.length, 1); + assert.is($p.children("span").length, 1); + assert.is($p.children("span").text(), "Yellow"); +}); + +text("should style first word by index position", async () => { + const $ = await fetchContent("text/style-by-index"); + const $head = $("head"); + const $p = $("p"); + + assert.is($head.children("style").length, 1); + assert.is($p.length, 1); + assert.is($p.children("span").length, 1); + assert.is($p.children("span").text(), "Green"); +}); + +text.run();