@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 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 { getWarningMessage, printWarning } from "../lib/warnings";
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<H extends (...args: any) => any, P = Parameters<H>[0]>(
158
- handler: H | undefined,
159
- props: P
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(props);
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(renderOptions.list, {
175
- Component: provideComponent("list", node.listItem, UnknownComponent),
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(renderOptions.listItem, {
193
- Component: provideComponent(
194
- "listItem",
195
- node.listItem,
196
- UnknownComponent
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(renderOptions.mark, {
210
- Component: provideComponent("mark", node.markType, UnknownComponent),
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(renderOptions.block, {
224
- Component: provideComponent("block", node.style, UnknownComponent),
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(renderOptions.hardBreak, {
236
- Component: isComponent(components.hardBreak)
237
- ? components.hardBreak
238
- : HardBreak,
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(renderOptions.text, {
244
- Component: isComponent(components.text) ? components.text : Text,
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(renderOptions.type, {
253
- Component: provideComponent("type", node._type, UnknownComponent),
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 slots = [
350
- "type",
351
- "block",
352
- "list",
353
- "listItem",
354
- "mark",
355
- "text",
356
- "hardBreak",
357
- ].reduce(
358
- (obj, name) => {
359
- obj[name] = Astro.slots.has(name) ? createSlotRenderer(name) : undefined;
360
- return obj;
361
- },
362
- {} as Record<string, SlotRenderer | undefined>
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 = (slotRenderer) => {
375
- return ({ Component, props, children }) =>
376
- slotRenderer?.([{ Component, props, children }]) ?? (
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(slots.type),
384
- block: renderNode(slots.block),
385
- list: renderNode(slots.list),
386
- listItem: renderNode(slots.listItem),
387
- mark: renderNode(slots.mark),
388
- text: renderNode(slots.text),
389
- hardBreak: renderNode(slots.hardBreak),
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 {Component, ComponentOrRecord, SomePortableTextComponents, TypedObject} from './types'
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/astro",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Render Portable Text with Astro",
5
5
  "keywords": [
6
6
  "astro",