theisel / astro-portabletext

Render Portable Text with Astro
https://www.npmjs.com/package/astro-portabletext
ISC License
61 stars 0 forks source link

Where to perform content transformations? #158

Open samhenrigold opened 1 day ago

samhenrigold commented 1 day ago

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 component to help me do that? Or should this transformation happen elsewhere, at another level?

theisel commented 1 day ago

Hi @samhenrigold, are you able to share some code, so I can help you out. You can approach the same outcome in various ways.

samhenrigold commented 1 day ago

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.

theisel commented 1 day ago

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.

samhenrigold commented 1 day ago

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?

theisel commented 1 day ago

I'm thinking the library needs to implement a text handler, which would allow you to manipulate the string.

samhenrigold commented 1 day ago

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>