Where to perform content transformations?
I really appreciate the work you’ve put into this component, it’s been a lifesaver for me.
I use this to render my PortableText blog, but there are some transformations I’d like to make to its content (namely converting dumb quotes to smart quotes).
I’m a bit of an Astro novice — are there any hooks into the <PortableText> component to help me do that? Or should this transformation happen elsewhere, at another level?
Hi @samhenrigold, are you able to share some code, so I can help you out. You can approach the same outcome in various ways.
You’ll have to excuse the messiness but here’s the component file I’ve been using to wrap PortableText to include some custom block components.
I tried my best attempt at brute-forcing it, but this feels morally wrong.
All good, when in doubt use a hammer 😂
Could you paste a snippet of the footnote type payload. It looks like you are wanting to convert the quotation marks, or have I missed something? With the help of extending Mark / Block components we should be able to simplify things.
There’s a lot of footnote stuff in there since I’m doing all of the body parsing in this file (in Sanity, one of my custom marks is an inline footnote — I have to extract all of those and render them in a list at the end of my posts).
I’m trying to replace all quote marks with smart quotes in any mark or block that isn’t a code snippet. My solution for that is to just parse through the entire PT payload and do all of those checks for when to apply the transformation manually, but this feels fragile.
Correct me if I’m mistaken, but if I wanted to do this by extending marks/blocks, I’d have to create a wrapper for each type and do its own transformation? Like CustomEmphasis.astro or CustomListItem.astro and have each one do that transformation?
I'm thinking the library needs to implement a text handler, which would allow you to manipulate the string.
I do a fair number of these transforms, so a kosher way to do it would be amazing.
Another example is my code mark. I work at an iOS development agency, so we blog about lengthy Cocoa class names often. This tends to break on mobile, where class names often wrap to multiple lines in weird spots. So I created a custom code mark to inject zero-width spaces before capital letters in camel case strings. Again using the same brute force method:
---
// This component does basically nothing except add zero width spaces in between camelCase words so the browser knows where to break lines.
import { PortableText } from "astro-portabletext";
import type { Mark, Props as $, TextNode } from "astro-portabletext/types";
export type Props = $<Mark>;
const { node, ...attrs } = Astro.props;
function addZeroWidthSpaces(text: string): string {
return text.replace(/([a-z])([A-Z])/g, '$1\u200B$2')
.replace(/([A-Z])([A-Z][a-z])/g, '$1\u200B$2');
}
const processNode = (node: Mark | TextNode): Mark | TextNode => {
if ('text' in node) {
return { ...node, text: addZeroWidthSpaces(node.text) };
}
if (Array.isArray(node.children)) {
return { ...node, children: node.children.map(processNode) };
}
return node;
};
const processedNode = processNode(node);
---
<code {...attrs}><PortableText value={processedNode.children} /></code>
Thanks for that.
I have implemented a text handler but it doesn't solve everything. Leave it with me and I'll get back to you.
Thanks! Really appreciate your help; the library has been a godsend for me.
Hi @samhenrigold hope all is well. I have come up with a solution to your issue. I am working on the following branch feat--text-handler. I would appreciate your feedback and test against some of your data. You will be able to rewrite the example code in the following way.
---
// This component does basically nothing except add zero width spaces in between camelCase words so the browser knows where to break lines.
import { PortableText } from "astro-portabletext";
import type { Mark, Props as $, TextNode } from "astro-portabletext/types";
+ import { usePortableText } from "astro-portabletext/utils";
export type Props = $<Mark>;
const { node, ...attrs } = Astro.props;
+ const { render } = usePortableText(node);
function addZeroWidthSpaces(text: string): string {
return text.replace(/([a-z])([A-Z])/g, '$1\u200B$2')
.replace(/([A-Z])([A-Z][a-z])/g, '$1\u200B$2');
}
- const processNode = (node: Mark | TextNode): Mark | TextNode => {
- if ('text' in node) {
- return { ...node, text: addZeroWidthSpaces(node.text) };
- }
- if (Array.isArray(node.children)) {
- return { ...node, children: node.children.map(processNode) };
- }
- return node;
- };
- const processedNode = processNode(node);
---
- <code {...attrs}><PortableText value={processedNode.children} /></code>
+ <code {...attrs}>{render({
+ default: ({Component, props, children}) => <Component {...props}>{children}</Component>, // 👈 this may not be need but recommended, will handle all other related nodes gracefully based on the components set within <PortableText /> component
+ text: ({ props }) => addZeroWidthSpaces(props.node.text)
+ })}</code>