@portabletext/astro 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +91 -28
- package/components/PortableText.astro +146 -56
- package/lib/internal.ts +81 -1
- package/lib/warnings.ts +10 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @portabletext/astro
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#4](https://github.com/portabletext/astro-portabletext/pull/4) [`0fd7cd3`](https://github.com/portabletext/astro-portabletext/commit/0fd7cd372df10919c6ff88d7533f4f7a0293a42e) Thanks [@msfragala](https://github.com/msfragala)! - Allow `PortableText` slots to be scoped to a specific type with `nodeType:type`, e.g. `block:h1`, `mark:link`, `list:bullet`, `listItem:number` and `type:myCustomType`. A scoped slot takes precedence over the node type it belongs to.
|
|
8
|
+
|
|
9
|
+
Slots now also take precedence over `render` from `usePortableText`. Previously a component reached through the `components` prop could displace a slot by rendering its children with `render`, so a slot for that node type never applied.
|
|
10
|
+
|
|
11
|
+
`Component` is now resolved lazily, so a slot that renders its own markup no longer reports a missing component. This makes a scoped slot enough on its own to render a custom type, which has no default component.
|
|
12
|
+
|
|
3
13
|
## 0.1.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -7,11 +7,7 @@
|
|
|
7
7
|
Render [Portable Text](https://portabletext.org) with [Astro](https://astro.build).
|
|
8
8
|
|
|
9
9
|
> [!NOTE]
|
|
10
|
-
> This package is a fork of [`astro-portabletext`](https://github.com/theisel/astro-portabletext)
|
|
11
|
-
> by [Tom Theisel](https://github.com/theisel), maintained under the
|
|
12
|
-
> [`@portabletext`](https://github.com/portabletext) organization. All credit for the original
|
|
13
|
-
> design and implementation goes to Tom. It remains distributed under the original
|
|
14
|
-
> [ISC License](./LICENSE). See [Migrating from `astro-portabletext`](#migrating-from-astro-portabletext).
|
|
10
|
+
> This package is a fork of [`astro-portabletext`](https://github.com/theisel/astro-portabletext) by [Tom Theisel](https://github.com/theisel), maintained under the [`@portabletext`](https://github.com/portabletext) organization. All credit for the original design and implementation goes to Tom. It remains distributed under the original [ISC License](./LICENSE). See [Migrating from `astro-portabletext`](#migrating-from-astro-portabletext).
|
|
15
11
|
|
|
16
12
|
## Table of contents
|
|
17
13
|
|
|
@@ -40,8 +36,7 @@ npm install @portabletext/astro
|
|
|
40
36
|
|
|
41
37
|
## Basic usage
|
|
42
38
|
|
|
43
|
-
Import the `PortableText` component and pass it a Portable Text value. The library provides sensible
|
|
44
|
-
defaults for rendering all standard Portable Text elements, which you can override.
|
|
39
|
+
Import the `PortableText` component and pass it a Portable Text value. The library provides sensible defaults for rendering all standard Portable Text elements, which you can override.
|
|
45
40
|
|
|
46
41
|
```astro
|
|
47
42
|
---
|
|
@@ -66,9 +61,7 @@ const portableText = [
|
|
|
66
61
|
|
|
67
62
|
## Sanity integration
|
|
68
63
|
|
|
69
|
-
This library's predecessor is
|
|
70
|
-
[officially recommended](https://www.sanity.io/plugins/sanity-astro#rendering-rich-text-and-block-content-with-portable-text)
|
|
71
|
-
by [Sanity](https://sanity.io) for rendering Portable Text in Astro projects. Helpful resources:
|
|
64
|
+
This library's predecessor is [officially recommended](https://www.sanity.io/plugins/sanity-astro#rendering-rich-text-and-block-content-with-portable-text) by [Sanity](https://sanity.io) for rendering Portable Text in Astro projects. Helpful resources:
|
|
72
65
|
|
|
73
66
|
- [Sanity integration for Astro](https://www.sanity.io/plugins/sanity-astro)
|
|
74
67
|
- [Guide: building a blog with Sanity and Astro](https://www.sanity.io/guides/sanity-astro-blog)
|
|
@@ -77,9 +70,7 @@ by [Sanity](https://sanity.io) for rendering Portable Text in Astro projects. He
|
|
|
77
70
|
|
|
78
71
|
### Default components
|
|
79
72
|
|
|
80
|
-
Default components are provided for all standard features of the Portable Text spec, with logical
|
|
81
|
-
HTML defaults. Provided components are merged with the defaults, so you only need to provide the
|
|
82
|
-
things you want to override.
|
|
73
|
+
Default components are provided for all standard features of the Portable Text spec, with logical HTML defaults. Provided components are merged with the defaults, so you only need to provide the things you want to override.
|
|
83
74
|
|
|
84
75
|
<details>
|
|
85
76
|
<summary>View the default structure and output</summary>
|
|
@@ -126,8 +117,7 @@ things you want to override.
|
|
|
126
117
|
|
|
127
118
|
### Custom components
|
|
128
119
|
|
|
129
|
-
Custom components give you control over how each node is rendered. Map a component to a whole node
|
|
130
|
-
type, or to a specific property (style, mark type, list item type, etc.) of that node type.
|
|
120
|
+
Custom components give you control over how each node is rendered. Map a component to a whole node type, or to a specific property (style, mark type, list item type, etc.) of that node type.
|
|
131
121
|
|
|
132
122
|
```astro
|
|
133
123
|
---
|
|
@@ -165,8 +155,7 @@ const components = {
|
|
|
165
155
|
<PortableText value={portableText} components={components} />
|
|
166
156
|
```
|
|
167
157
|
|
|
168
|
-
Each custom component receives `node`, `index` and `isInline` props, and renders any children
|
|
169
|
-
through a `<slot />`. For example, a custom `link` mark:
|
|
158
|
+
Each custom component receives `node`, `index` and `isInline` props, and renders any children through a `<slot />`. For example, a custom `link` mark:
|
|
170
159
|
|
|
171
160
|
```astro
|
|
172
161
|
---
|
|
@@ -183,9 +172,7 @@ const href = node.markDef?.href
|
|
|
183
172
|
|
|
184
173
|
### Slots
|
|
185
174
|
|
|
186
|
-
Slots provide a flexible way to enhance rendering by passing additional props to the resolved
|
|
187
|
-
component - for example applying custom classes or wrapping elements - without replacing the default
|
|
188
|
-
component entirely.
|
|
175
|
+
Slots provide a flexible way to enhance rendering by passing additional props to the resolved component - for example applying custom classes or wrapping elements - without replacing the default component entirely.
|
|
189
176
|
|
|
190
177
|
```astro
|
|
191
178
|
---
|
|
@@ -211,6 +198,86 @@ const portableText = [
|
|
|
211
198
|
</style>
|
|
212
199
|
```
|
|
213
200
|
|
|
201
|
+
A slot named after a node type applies to every node of that type: `type`, `block`, `list`, `listItem`, `mark`, `text` and `hardBreak`. To target a single block style, list type, mark type or custom type, scope the slot name with `nodeType:type`.
|
|
202
|
+
|
|
203
|
+
```astro
|
|
204
|
+
<PortableText value={portableText}>
|
|
205
|
+
<fragment slot="block:h1">
|
|
206
|
+
{({Component, props, children}) => (
|
|
207
|
+
<Component {...props} class="heading">{children}</Component>
|
|
208
|
+
)}
|
|
209
|
+
</fragment>
|
|
210
|
+
<fragment slot="mark:link">
|
|
211
|
+
{({Component, props, children}) => (
|
|
212
|
+
<Component {...props} rel="noopener">{children}</Component>
|
|
213
|
+
)}
|
|
214
|
+
</fragment>
|
|
215
|
+
<fragment slot="type:callout">
|
|
216
|
+
{({props}) => <aside class="callout">{props.node.text}</aside>}
|
|
217
|
+
</fragment>
|
|
218
|
+
</PortableText>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
A slot only has a `Component` to render when the node type has one. Standard block styles, list types and mark types all ship defaults, but `components.type` starts out empty - a custom type has no default component. So a slot for a custom type either renders its own markup, as `type:callout` does above, or registers a component to receive:
|
|
222
|
+
|
|
223
|
+
```astro
|
|
224
|
+
<PortableText value={portableText} components={{type: {callout: Callout}}}>
|
|
225
|
+
<fragment slot="type:callout">
|
|
226
|
+
{({Component, props}) => <Component {...props} class="callout" />}
|
|
227
|
+
</fragment>
|
|
228
|
+
</PortableText>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Asking for `Component` when the node type has none gives you the unknown-type component and reports the missing component through [`onMissingComponent`](#portabletext-component-properties). A slot that never touches `Component` renders cleanly.
|
|
232
|
+
|
|
233
|
+
A scoped slot takes precedence over the node type it belongs to, so `block` can handle every block while `block:h1` handles headings.
|
|
234
|
+
|
|
235
|
+
```astro
|
|
236
|
+
<PortableText value={portableText}>
|
|
237
|
+
<!-- Every block except `h1` -->
|
|
238
|
+
<fragment slot="block">
|
|
239
|
+
{({Component, props, children}) => (
|
|
240
|
+
<Component {...props} class="block">{children}</Component>
|
|
241
|
+
)}
|
|
242
|
+
</fragment>
|
|
243
|
+
<fragment slot="block:h1">
|
|
244
|
+
{({Component, props, children}) => (
|
|
245
|
+
<Component {...props} class="heading">{children}</Component>
|
|
246
|
+
)}
|
|
247
|
+
</fragment>
|
|
248
|
+
</PortableText>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The `text` and `hardBreak` node types have no types of their own, so they cannot be scoped.
|
|
252
|
+
|
|
253
|
+
#### Slots and the `components` prop
|
|
254
|
+
|
|
255
|
+
A slot does not replace the [`components`](#customizing-components) prop, it wraps it. The `Component` handed to a slot is whatever the `components` prop resolved to for that node, so the two compose - the slot decides how the resolved component is rendered.
|
|
256
|
+
|
|
257
|
+
```astro
|
|
258
|
+
<PortableText value={portableText} components={{block: {h1: MyHeading}}}>
|
|
259
|
+
<fragment slot="block:h1">
|
|
260
|
+
<!-- `Component` is `MyHeading` -->
|
|
261
|
+
{({Component, props, children}) => (
|
|
262
|
+
<Component {...props} class="heading">{children}</Component>
|
|
263
|
+
)}
|
|
264
|
+
</fragment>
|
|
265
|
+
</PortableText>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Slots belong to the author of the `PortableText` element, so they always win. A component reached through the `components` prop can customize its own children with [`render`](#useportabletext), but a slot for that node type takes precedence over it.
|
|
269
|
+
|
|
270
|
+
```astro
|
|
271
|
+
<PortableText value={portableText} components={{block: MyBlock}}>
|
|
272
|
+
<!-- Applies even if `MyBlock` renders its children with `render({mark: ...})` -->
|
|
273
|
+
<fragment slot="mark">
|
|
274
|
+
{({Component, props, children}) => (
|
|
275
|
+
<Component {...props} class="mark">{children}</Component>
|
|
276
|
+
)}
|
|
277
|
+
</fragment>
|
|
278
|
+
</PortableText>
|
|
279
|
+
```
|
|
280
|
+
|
|
214
281
|
## `PortableText` component properties
|
|
215
282
|
|
|
216
283
|
| Property | Type | Description |
|
|
@@ -228,8 +295,7 @@ import {usePortableText, mergeComponents, toPlainText, spanToPlainText} from '@p
|
|
|
228
295
|
|
|
229
296
|
### `usePortableText`
|
|
230
297
|
|
|
231
|
-
Within a component passed into the `components` prop, `usePortableText(node)` returns rendering
|
|
232
|
-
utilities scoped to that node: `getDefaultComponent()`, `getUnknownComponent()` and `render()`.
|
|
298
|
+
Within a component passed into the `components` prop, `usePortableText(node)` returns rendering utilities scoped to that node: `getDefaultComponent()`, `getUnknownComponent()` and `render()`.
|
|
233
299
|
|
|
234
300
|
```astro
|
|
235
301
|
---
|
|
@@ -248,8 +314,7 @@ const Default = getDefaultComponent()
|
|
|
248
314
|
|
|
249
315
|
### `mergeComponents`
|
|
250
316
|
|
|
251
|
-
Merges two component maps, giving priority to the overrides. Useful for extending a shared base set
|
|
252
|
-
of components.
|
|
317
|
+
Merges two component maps, giving priority to the overrides. Useful for extending a shared base set of components.
|
|
253
318
|
|
|
254
319
|
```js
|
|
255
320
|
import {mergeComponents} from '@portabletext/astro'
|
|
@@ -261,8 +326,7 @@ const components = mergeComponents(baseComponents, {
|
|
|
261
326
|
|
|
262
327
|
### `toPlainText`
|
|
263
328
|
|
|
264
|
-
Renders one or more Portable Text blocks as a plain string - handy for meta descriptions or
|
|
265
|
-
generating slugs. `spanToPlainText` does the same for a single span's children.
|
|
329
|
+
Renders one or more Portable Text blocks as a plain string - handy for meta descriptions or generating slugs. `spanToPlainText` does the same for a single span's children.
|
|
266
330
|
|
|
267
331
|
```astro
|
|
268
332
|
---
|
|
@@ -275,8 +339,7 @@ const text = toPlainText(node)
|
|
|
275
339
|
|
|
276
340
|
## Migrating from `astro-portabletext`
|
|
277
341
|
|
|
278
|
-
`@portabletext/astro` is a drop-in fork of [`astro-portabletext`](https://github.com/theisel/astro-portabletext).
|
|
279
|
-
To migrate, swap the dependency and update your imports:
|
|
342
|
+
`@portabletext/astro` is a drop-in fork of [`astro-portabletext`](https://github.com/theisel/astro-portabletext). To migrate, swap the dependency and update your imports:
|
|
280
343
|
|
|
281
344
|
```diff
|
|
282
345
|
- import {PortableText} from 'astro-portabletext'
|
|
@@ -24,12 +24,20 @@ import type {
|
|
|
24
24
|
|
|
25
25
|
import {
|
|
26
26
|
isComponent,
|
|
27
|
+
isSlotName,
|
|
27
28
|
mergeComponents,
|
|
28
29
|
setNodeComponents,
|
|
29
30
|
getNodeComponents,
|
|
31
|
+
toSlotName,
|
|
30
32
|
} from "../lib/internal";
|
|
33
|
+
import type { SlotNodeType } from "../lib/internal";
|
|
31
34
|
|
|
32
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
getWarningMessage,
|
|
37
|
+
ignoredChildrenWarning,
|
|
38
|
+
printWarning,
|
|
39
|
+
unknownSlotWarning,
|
|
40
|
+
} from "../lib/warnings";
|
|
33
41
|
import { key as contextRef } from "../lib/context";
|
|
34
42
|
|
|
35
43
|
import Block from "./Block.astro";
|
|
@@ -137,6 +145,18 @@ const provideComponent = (
|
|
|
137
145
|
return fallbackComponent;
|
|
138
146
|
};
|
|
139
147
|
|
|
148
|
+
type RunArgs = {
|
|
149
|
+
nodeType: SlotNodeType;
|
|
150
|
+
/** Block style, list type, mark type or custom `_type`, when there is one */
|
|
151
|
+
type?: string;
|
|
152
|
+
// Loosely typed: each `RenderOptions` entry narrows `node` to its own type
|
|
153
|
+
handler: ((handlerProps: any) => any) | undefined;
|
|
154
|
+
/** Called only if something actually asks for the node's `Component` */
|
|
155
|
+
resolveComponent: () => Component;
|
|
156
|
+
props: ComponentProps<TypedObject>;
|
|
157
|
+
children?: unknown;
|
|
158
|
+
};
|
|
159
|
+
|
|
140
160
|
// The local render function will override these options
|
|
141
161
|
let fallbackRenderOptions: Required<RenderOptions> | undefined;
|
|
142
162
|
|
|
@@ -154,25 +174,55 @@ const portableTextRender = (options: RenderOptions, isInline?: boolean) => {
|
|
|
154
174
|
const renderOptions = { ...fallbackRenderOptions, ...options };
|
|
155
175
|
|
|
156
176
|
return function renderNode(node: TypedObject, index: number): any {
|
|
157
|
-
function run
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
177
|
+
function run({
|
|
178
|
+
nodeType,
|
|
179
|
+
type,
|
|
180
|
+
handler,
|
|
181
|
+
resolveComponent,
|
|
182
|
+
props,
|
|
183
|
+
children,
|
|
184
|
+
}: RunArgs) {
|
|
185
|
+
let component: Component | undefined;
|
|
186
|
+
|
|
187
|
+
const handlerProps = {
|
|
188
|
+
// Resolved lazily. A slot rendering its own markup never asks for
|
|
189
|
+
// `Component`, and so must not trigger a missing component warning.
|
|
190
|
+
get Component() {
|
|
191
|
+
return (component ??= resolveComponent());
|
|
192
|
+
},
|
|
193
|
+
props,
|
|
194
|
+
children,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// A slot always wins. `renderOptions` may have been overridden by a component
|
|
198
|
+
// from the `components` prop calling `render` from `usePortableText`, which
|
|
199
|
+
// must not take the slot away from the author of this `PortableText`.
|
|
200
|
+
const slotRenderer = provideSlot(nodeType, type);
|
|
201
|
+
|
|
202
|
+
if (slotRenderer) {
|
|
203
|
+
return slotRenderer([handlerProps]);
|
|
204
|
+
}
|
|
205
|
+
|
|
161
206
|
if (!isComponent(handler)) {
|
|
162
207
|
throw new Error(
|
|
163
208
|
`[PortableText render] No handler found for node type ${node._type}.`
|
|
164
209
|
);
|
|
165
210
|
}
|
|
166
211
|
|
|
167
|
-
return handler(
|
|
212
|
+
return handler(handlerProps);
|
|
168
213
|
}
|
|
169
214
|
|
|
170
215
|
if (isPortableTextToolkitList(node)) {
|
|
216
|
+
const listType = node.listItem;
|
|
171
217
|
const UnknownComponent = components.unknownList ?? UnknownList;
|
|
172
218
|
setNodeComponents(node, List, UnknownComponent);
|
|
173
219
|
|
|
174
|
-
return run(
|
|
175
|
-
|
|
220
|
+
return run({
|
|
221
|
+
nodeType: "list",
|
|
222
|
+
type: listType,
|
|
223
|
+
handler: renderOptions.list,
|
|
224
|
+
resolveComponent: () =>
|
|
225
|
+
provideComponent("list", listType, UnknownComponent),
|
|
176
226
|
props: asComponentProps(node, index, false),
|
|
177
227
|
children: renderChildren(node.children, false),
|
|
178
228
|
});
|
|
@@ -189,12 +239,12 @@ const portableTextRender = (options: RenderOptions, isInline?: boolean) => {
|
|
|
189
239
|
const UnknownComponent = components.unknownListItem ?? UnknownListItem;
|
|
190
240
|
setNodeComponents(node, ListItem, UnknownComponent);
|
|
191
241
|
|
|
192
|
-
return run(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
242
|
+
return run({
|
|
243
|
+
nodeType: "listItem",
|
|
244
|
+
type: listItem,
|
|
245
|
+
handler: renderOptions.listItem,
|
|
246
|
+
resolveComponent: () =>
|
|
247
|
+
provideComponent("listItem", listItem, UnknownComponent),
|
|
198
248
|
props: asComponentProps(node, index, false),
|
|
199
249
|
children: isStyled
|
|
200
250
|
? node.children
|
|
@@ -203,11 +253,16 @@ const portableTextRender = (options: RenderOptions, isInline?: boolean) => {
|
|
|
203
253
|
}
|
|
204
254
|
|
|
205
255
|
if (isPortableTextToolkitSpan(node)) {
|
|
256
|
+
const markType = node.markType;
|
|
206
257
|
const UnknownComponent = components.unknownMark ?? UnknownMark;
|
|
207
258
|
setNodeComponents(node, Mark, UnknownComponent);
|
|
208
259
|
|
|
209
|
-
return run(
|
|
210
|
-
|
|
260
|
+
return run({
|
|
261
|
+
nodeType: "mark",
|
|
262
|
+
type: markType,
|
|
263
|
+
handler: renderOptions.mark,
|
|
264
|
+
resolveComponent: () =>
|
|
265
|
+
provideComponent("mark", markType, UnknownComponent),
|
|
211
266
|
props: asComponentProps(node, index, true),
|
|
212
267
|
children: renderChildren(node.children, true),
|
|
213
268
|
});
|
|
@@ -217,11 +272,16 @@ const portableTextRender = (options: RenderOptions, isInline?: boolean) => {
|
|
|
217
272
|
node.style ??= "normal"; /* Make sure style has been set */
|
|
218
273
|
node.children = buildMarksTree(node);
|
|
219
274
|
|
|
275
|
+
const style = node.style;
|
|
220
276
|
const UnknownComponent = components.unknownBlock ?? UnknownBlock;
|
|
221
277
|
setNodeComponents(node, Block, UnknownComponent);
|
|
222
278
|
|
|
223
|
-
return run(
|
|
224
|
-
|
|
279
|
+
return run({
|
|
280
|
+
nodeType: "block",
|
|
281
|
+
type: style,
|
|
282
|
+
handler: renderOptions.block,
|
|
283
|
+
resolveComponent: () =>
|
|
284
|
+
provideComponent("block", style, UnknownComponent),
|
|
225
285
|
props: asComponentProps(node, index, false),
|
|
226
286
|
children: renderChildren(node.children, true),
|
|
227
287
|
});
|
|
@@ -232,25 +292,36 @@ const portableTextRender = (options: RenderOptions, isInline?: boolean) => {
|
|
|
232
292
|
const props = asComponentProps(node, index, true);
|
|
233
293
|
|
|
234
294
|
if (isHardBreak) {
|
|
235
|
-
return run(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
295
|
+
return run({
|
|
296
|
+
nodeType: "hardBreak",
|
|
297
|
+
handler: renderOptions.hardBreak,
|
|
298
|
+
resolveComponent: () =>
|
|
299
|
+
isComponent(components.hardBreak)
|
|
300
|
+
? components.hardBreak
|
|
301
|
+
: HardBreak,
|
|
239
302
|
props,
|
|
240
303
|
});
|
|
241
304
|
}
|
|
242
305
|
|
|
243
|
-
return run(
|
|
244
|
-
|
|
306
|
+
return run({
|
|
307
|
+
nodeType: "text",
|
|
308
|
+
handler: renderOptions.text,
|
|
309
|
+
resolveComponent: () =>
|
|
310
|
+
isComponent(components.text) ? components.text : Text,
|
|
245
311
|
props,
|
|
246
312
|
});
|
|
247
313
|
}
|
|
248
314
|
|
|
249
315
|
// Custom type
|
|
316
|
+
const customType = node._type;
|
|
250
317
|
const UnknownComponent = components.unknownType ?? UnknownType;
|
|
251
318
|
|
|
252
|
-
return run(
|
|
253
|
-
|
|
319
|
+
return run({
|
|
320
|
+
nodeType: "type",
|
|
321
|
+
type: customType,
|
|
322
|
+
handler: renderOptions.type,
|
|
323
|
+
resolveComponent: () =>
|
|
324
|
+
provideComponent("type", customType, UnknownComponent),
|
|
254
325
|
props: asComponentProps(
|
|
255
326
|
node,
|
|
256
327
|
index,
|
|
@@ -346,47 +417,66 @@ const createSlotRenderer = (slotName: string) =>
|
|
|
346
417
|
|
|
347
418
|
type SlotRenderer = ReturnType<typeof createSlotRenderer>;
|
|
348
419
|
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
420
|
+
const slotRenderers = new Map<string, SlotRenderer | undefined>();
|
|
421
|
+
|
|
422
|
+
const provideSlotRenderer = (slotName: string): SlotRenderer | undefined => {
|
|
423
|
+
if (!slotRenderers.has(slotName)) {
|
|
424
|
+
slotRenderers.set(
|
|
425
|
+
slotName,
|
|
426
|
+
Astro.slots.has(slotName) ? createSlotRenderer(slotName) : undefined
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return slotRenderers.get(slotName);
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Resolves the slot to render a node with.
|
|
435
|
+
* A slot scoped to a type, e.g. `block:h1`, takes precedence over `block`.
|
|
436
|
+
*/
|
|
437
|
+
const provideSlot = (
|
|
438
|
+
nodeType: SlotNodeType,
|
|
439
|
+
type?: string
|
|
440
|
+
): SlotRenderer | undefined =>
|
|
441
|
+
(type ? provideSlotRenderer(toSlotName(nodeType, type)) : undefined) ??
|
|
442
|
+
provideSlotRenderer(nodeType);
|
|
443
|
+
|
|
444
|
+
// A slot targeting an unrecognised node type never renders, so let the author
|
|
445
|
+
// know rather than silently ignoring it.
|
|
446
|
+
if (import.meta.env.DEV) {
|
|
447
|
+
for (const slotName of Object.keys(Astro.slots)) {
|
|
448
|
+
if (isSlotName(slotName)) continue;
|
|
449
|
+
|
|
450
|
+
printWarning(
|
|
451
|
+
"default" === slotName
|
|
452
|
+
? ignoredChildrenWarning()
|
|
453
|
+
: unknownSlotWarning(slotName)
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
364
457
|
|
|
458
|
+
// Slots are resolved by `portableTextRender`, so every node type falls back to
|
|
459
|
+
// simply rendering its resolved component.
|
|
365
460
|
type RenderNode = (
|
|
366
|
-
slotRenderer: SlotRenderer | undefined
|
|
367
|
-
) => (
|
|
368
461
|
props: Parameters<NonNullable<RenderOptions[keyof RenderOptions]>>[0]
|
|
369
462
|
) => any;
|
|
370
463
|
---
|
|
371
464
|
|
|
372
465
|
{
|
|
373
466
|
(() => {
|
|
374
|
-
const renderNode: RenderNode = (
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
<Component {...(props as any)}>{children}</Component>
|
|
378
|
-
);
|
|
379
|
-
};
|
|
467
|
+
const renderNode: RenderNode = ({ Component, props, children }) => (
|
|
468
|
+
<Component {...(props as any)}>{children}</Component>
|
|
469
|
+
);
|
|
380
470
|
|
|
381
471
|
return nodes.map(
|
|
382
472
|
render({
|
|
383
|
-
type: renderNode
|
|
384
|
-
block: renderNode
|
|
385
|
-
list: renderNode
|
|
386
|
-
listItem: renderNode
|
|
387
|
-
mark: renderNode
|
|
388
|
-
text: renderNode
|
|
389
|
-
hardBreak: renderNode
|
|
473
|
+
type: renderNode,
|
|
474
|
+
block: renderNode,
|
|
475
|
+
list: renderNode,
|
|
476
|
+
listItem: renderNode,
|
|
477
|
+
mark: renderNode,
|
|
478
|
+
text: renderNode,
|
|
479
|
+
hardBreak: renderNode,
|
|
390
480
|
})
|
|
391
481
|
);
|
|
392
482
|
})()
|
package/lib/internal.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Component,
|
|
3
|
+
ComponentOrRecord,
|
|
4
|
+
NodeType,
|
|
5
|
+
SomePortableTextComponents,
|
|
6
|
+
TypedObject,
|
|
7
|
+
} from './types'
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* Helper for component to throw an error
|
|
@@ -62,6 +68,80 @@ export function mergeComponents<
|
|
|
62
68
|
}
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
/**
|
|
72
|
+
* =====
|
|
73
|
+
* Slots
|
|
74
|
+
* =====
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A node type that a `PortableText` slot can target. `text` and `hardBreak` have
|
|
79
|
+
* no types of their own, so they round out the scopeable `NodeType`s.
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
export type SlotNodeType = NodeType | 'text' | 'hardBreak'
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Node types that a `PortableText` slot can target, mapped to whether the slot
|
|
86
|
+
* name can be scoped to a specific type, e.g. `block:h1`.
|
|
87
|
+
* @internal
|
|
88
|
+
*
|
|
89
|
+
* @remarks
|
|
90
|
+
* `satisfies` keeps this map exhaustive - adding a `NodeType` will not compile
|
|
91
|
+
* until it is given an entry here.
|
|
92
|
+
*/
|
|
93
|
+
const slotNodeTypes = {
|
|
94
|
+
type: true,
|
|
95
|
+
block: true,
|
|
96
|
+
list: true,
|
|
97
|
+
listItem: true,
|
|
98
|
+
mark: true,
|
|
99
|
+
text: false,
|
|
100
|
+
hardBreak: false,
|
|
101
|
+
} as const satisfies Record<SlotNodeType, boolean>
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The node types that a `PortableText` slot can target.
|
|
105
|
+
* @internal
|
|
106
|
+
*/
|
|
107
|
+
export const slotNames: readonly SlotNodeType[] = Object.keys(slotNodeTypes) as SlotNodeType[]
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Builds the name of the slot that renders the given node type, optionally
|
|
111
|
+
* scoped to a specific `type` such as a block style or a mark type.
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
export function toSlotName(nodeType: string, type?: string): string {
|
|
115
|
+
return type ? `${nodeType}:${type}` : nodeType
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Returns true if `slotName` targets a node type that is rendered by `PortableText`.
|
|
120
|
+
* @internal
|
|
121
|
+
*
|
|
122
|
+
* @remarks
|
|
123
|
+
* The scope of a scoped slot name, e.g. the `h1` of `block:h1`, cannot be verified
|
|
124
|
+
* upfront as block styles, mark types and custom types are user defined.
|
|
125
|
+
*/
|
|
126
|
+
export function isSlotName(slotName: string): boolean {
|
|
127
|
+
const separator = slotName.indexOf(':')
|
|
128
|
+
|
|
129
|
+
// `Object.hasOwn` rather than `in`, so inherited keys such as `toString` and
|
|
130
|
+
// `constructor` are not mistaken for node types.
|
|
131
|
+
if (separator === -1) {
|
|
132
|
+
return Object.hasOwn(slotNodeTypes, slotName)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const nodeType = slotName.slice(0, separator)
|
|
136
|
+
const scope = slotName.slice(separator + 1)
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
Object.hasOwn(slotNodeTypes, nodeType) &&
|
|
140
|
+
slotNodeTypes[nodeType as SlotNodeType] &&
|
|
141
|
+
scope.length > 0
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
65
145
|
/**
|
|
66
146
|
* ========================
|
|
67
147
|
* Node Components Registry
|
package/lib/warnings.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {slotNames} from './internal'
|
|
1
2
|
import type {NodeType} from './types'
|
|
2
3
|
|
|
3
4
|
const getTemplate = (prop: string, type: string): string =>
|
|
@@ -14,6 +15,15 @@ export const unknownListWarning = (listItem: string): string => getTemplate('lis
|
|
|
14
15
|
export const unknownListItemWarning = (listStyle: string): string =>
|
|
15
16
|
getTemplate('listItem', listStyle)
|
|
16
17
|
|
|
18
|
+
export const unknownSlotWarning = (slotName: string): string =>
|
|
19
|
+
`PortableText slot "${slotName}" does not target a node type and will be ignored. ` +
|
|
20
|
+
`Expected ${slotNames.map((it) => `"${it}"`).join(', ')}, ` +
|
|
21
|
+
`optionally scoped to a type, e.g. "block:h1"`
|
|
22
|
+
|
|
23
|
+
export const ignoredChildrenWarning = (): string =>
|
|
24
|
+
'PortableText was given children that are not assigned to a slot, so they will be ignored. ' +
|
|
25
|
+
'Assign them to a node type, e.g. <fragment slot="block">'
|
|
26
|
+
|
|
17
27
|
export const getWarningMessage = (nodeType: NodeType, type: string) => {
|
|
18
28
|
const fncs = {
|
|
19
29
|
block: unknownBlockWarning,
|