@portabletext/astro 0.0.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.
@@ -0,0 +1,183 @@
1
+ import type {
2
+ Component,
3
+ ComponentOrRecord,
4
+ NodeType,
5
+ SomePortableTextComponents,
6
+ TypedObject,
7
+ } from './types'
8
+
9
+ /**
10
+ * Helper for component to throw an error
11
+ * @param err
12
+ */
13
+ export function throwError(err: Error | string): never {
14
+ throw err
15
+ }
16
+
17
+ /**
18
+ * Returns true if `it` is component
19
+ */
20
+ export function isComponent(it: unknown): it is Component {
21
+ return typeof it === 'function'
22
+ }
23
+
24
+ /**
25
+ * Merges two {@link SomePortableTextComponents} objects, giving priority to overrides.
26
+ *
27
+ * This function combines two component objects used in Portable Text rendering.
28
+ * If both objects have the same key, the value from `overrides` takes precedence.
29
+ * This is useful for customizing the rendering of specific components while keeping
30
+ * the default behavior for others.
31
+ *
32
+ * @typeParam Components - The type of the base components object.
33
+ * @typeParam Overrides - The type of the overrides components object.
34
+ *
35
+ * @param components - The base components object.
36
+ * @param overrides - The overrides components object.
37
+ *
38
+ * @returns A new object with the merged components.
39
+ */
40
+ export function mergeComponents<
41
+ Components extends SomePortableTextComponents,
42
+ Overrides extends SomePortableTextComponents,
43
+ >(components: Components, overrides: Overrides) {
44
+ const cmps = {...components} as Record<string, ComponentOrRecord>
45
+
46
+ for (const [key, override] of Object.entries(overrides)) {
47
+ const current = components[key as keyof typeof components]
48
+
49
+ const value =
50
+ !current || isComponent(override) || isComponent(current)
51
+ ? override
52
+ : {
53
+ ...(current as Record<string, Component>),
54
+ ...(override as Record<string, Component>),
55
+ }
56
+
57
+ cmps[key] = value
58
+ }
59
+
60
+ return cmps as {
61
+ [Key in keyof (Components & Overrides)]: Key extends keyof (Overrides | Components)
62
+ ? Overrides[Key] extends Component
63
+ ? Overrides[Key]
64
+ : Components[Key] extends Component
65
+ ? Overrides[Key]
66
+ : (Overrides & Components)[Key]
67
+ : (Overrides & Components)[Key]
68
+ }
69
+ }
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
+
145
+ /**
146
+ * ========================
147
+ * Node Components Registry
148
+ * ========================
149
+ */
150
+
151
+ type ResolvedComponents = {
152
+ Default: Component
153
+ Unknown: Component
154
+ }
155
+
156
+ const nodeComponentsMap = new WeakMap<TypedObject, ResolvedComponents>()
157
+
158
+ /**
159
+ * Binds the resolved components to a specific node object.
160
+ * @internal
161
+ *
162
+ * @remarks
163
+ * This uses the node's _object reference_ as the key. This enables the `Context` API via
164
+ * `usePortableText` to look up which components were assigned to this specific node during rendering.
165
+ *
166
+ * @param node - The node object to be used as the key.
167
+ * @param Default - The resolved default component for this node.
168
+ * @param Unknown - The resolved fallback (unknown) component for this node.
169
+ */
170
+ export function setNodeComponents(node: TypedObject, Default: Component, Unknown: Component): void {
171
+ nodeComponentsMap.set(node, {Default, Unknown})
172
+ }
173
+
174
+ /**
175
+ * Retrieves the components bound to a specific node object.
176
+ * @internal
177
+ *
178
+ * @param node - The node object to look up (by reference).
179
+ * @returns The component pair, or `undefined` if this exact node object was not registered.
180
+ */
181
+ export function getNodeComponents(node: TypedObject): ResolvedComponents | undefined {
182
+ return nodeComponentsMap.get(node)
183
+ }
package/lib/types.ts ADDED
@@ -0,0 +1,469 @@
1
+ import type {
2
+ ToolkitListNestMode,
3
+ ToolkitNestedPortableTextSpan,
4
+ ToolkitPortableTextList,
5
+ ToolkitPortableTextListItem,
6
+ ToolkitTextNode,
7
+ } from '@portabletext/toolkit'
8
+ import type {
9
+ ArbitraryTypedObject,
10
+ PortableTextBlock,
11
+ PortableTextBlockStyle,
12
+ PortableTextMarkDefinition,
13
+ TypedObject,
14
+ } from '@portabletext/types'
15
+
16
+ export type {TypedObject} from '@portabletext/types'
17
+
18
+ /**
19
+ * Properties for the `PortableText` component
20
+ *
21
+ * @typeParam Value - Type of Portable Text payload
22
+ */
23
+ export interface PortableTextProps<
24
+ Value extends TypedObject = PortableTextBlock | ArbitraryTypedObject,
25
+ > {
26
+ /**
27
+ * Portable Text payload
28
+ */
29
+ value: Value | Value[]
30
+
31
+ /**
32
+ * Components for rendering
33
+ */
34
+ components?: SomePortableTextComponents
35
+
36
+ /**
37
+ * Function to call when faced with unknown types.
38
+ *
39
+ * @remarks
40
+ * - Prints a warning message to the console by default.
41
+ * - Use `false` to disable.
42
+ */
43
+ onMissingComponent?: MissingComponentHandler | boolean
44
+
45
+ /**
46
+ * Defines the nesting mode for lists. The value can be `html` or `direct`, and defaults to `html`.
47
+ *
48
+ * @remarks
49
+ * - `html` - Deeper list nodes will appear as a child of the last list item in the parent list
50
+ * - `direct` - Deeper list nodes will appear as a direct child of the parent list
51
+ *
52
+ * @see {@link https://portabletext.github.io/toolkit/types/ToolkitListNestMode.html ToolkitListNestMode}
53
+ */
54
+ listNestingMode?: ToolkitListNestMode
55
+ }
56
+
57
+ /**
58
+ * Defines how Portable Text types should be rendered.
59
+ */
60
+ export interface PortableTextComponents {
61
+ /**
62
+ * Component or mapping of components for rendering `custom` types.
63
+ */
64
+ type: ComponentOrRecord
65
+ /**
66
+ * Used when a {@link PortableTextComponents.type type} component isn't found.
67
+ */
68
+ unknownType: Component
69
+ /**
70
+ * Component or mapping of components for rendering `block` styles.
71
+ */
72
+ block: ComponentOrRecord<Block>
73
+ /**
74
+ * Used when a {@link PortableTextComponents.block block} component isn't found.
75
+ */
76
+ unknownBlock: Component<Block>
77
+ /**
78
+ * Component or mapping of components for rendering `list` item type.
79
+ */
80
+ list: ComponentOrRecord<List>
81
+ /**
82
+ * Used when a {@link PortableTextComponents.list list} component isn't found.
83
+ */
84
+ unknownList: Component<List>
85
+ /**
86
+ * Component or mapping of components for rendering `list` item type.
87
+ */
88
+ listItem: ComponentOrRecord<ListItem>
89
+ /**
90
+ * Used when a {@link PortableTextComponents.listItem listItem} component isn't found.
91
+ */
92
+ unknownListItem: Component<ListItem>
93
+ /**
94
+ * Component or mapping of components for rendering `mark` definition type.
95
+ */
96
+ mark: ComponentOrRecord<Mark<never>>
97
+ /**
98
+ * Used when a {@link PortableTextComponents.mark mark} component isn't found.
99
+ */
100
+ unknownMark: Component<Mark<never>>
101
+ /**
102
+ * Component for rendering `spans` of text.
103
+ * @remarks Added in: `v0.11.0`
104
+ */
105
+ text: Component<TextNode>
106
+ /**
107
+ * Component for rendering a newline `\n` of text.
108
+ */
109
+ hardBreak: Component<TextNode>
110
+ }
111
+
112
+ /**
113
+ * Defines how some Portable Text types should be rendered.
114
+ */
115
+ export type SomePortableTextComponents = Partial<PortableTextComponents>
116
+
117
+ /**
118
+ * Component Props
119
+ *
120
+ * @typeParam N - Type of Portable Text payload that this component will receive on its `node` property
121
+ */
122
+ export interface Props<N extends TypedObject> {
123
+ /**
124
+ * Portable Text data for this node
125
+ */
126
+ node: N
127
+ /**
128
+ * Index of the current node within its parent's child list
129
+ */
130
+ index: number
131
+ /**
132
+ * Indicates whether the node should render as an inline or block element
133
+ */
134
+ isInline: boolean
135
+ }
136
+
137
+ /**
138
+ * Alias to {@link https://portabletext.github.io/types/interfaces/PortableTextBlock.html PortableTextBlock}
139
+ * with `style` set to `normal` when undefined
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * ---
144
+ * import type { Block, Props as $ } from "@portabletext/astro/types";
145
+ *
146
+ * type Props = $<Block>;
147
+ * ---
148
+ * ```
149
+ *
150
+ * @remarks To concisely achieve the same result in the example, use the convenience type {@link BlockProps} instead.
151
+ */
152
+ export interface Block extends PortableTextBlock {
153
+ style: 'normal' | PortableTextBlockStyle
154
+ }
155
+
156
+ /**
157
+ * Convenience type for {@link Block} component props
158
+ *
159
+ * @remarks
160
+ * Added in: `v0.11.0`
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * ---
165
+ * import type { BlockProps } from "@portabletext/astro/types";
166
+ *
167
+ * type Props = BlockProps;
168
+ * ---
169
+ * ```
170
+ */
171
+ export type BlockProps = Props<Block>
172
+
173
+ /**
174
+ * Alias to {@link https://portabletext.github.io/toolkit/types/ToolkitPortableTextList.html ToolkitPortableTextList}
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * ---
179
+ * import type { List, Props as $ } from "@portabletext/astro/types";
180
+ *
181
+ * type Props = $<List>;
182
+ * ---
183
+ * ```
184
+ *
185
+ * @remarks To concisely achieve the same result in the example, use the convenience type {@link ListProps} instead.
186
+ */
187
+ export type List = ToolkitPortableTextList
188
+
189
+ /**
190
+ * Convenience type for {@link List} component props
191
+ *
192
+ * @remarks
193
+ * Added in: `v0.11.0`
194
+ *
195
+ * @example
196
+ * ```ts
197
+ * ---
198
+ * import type { ListProps } from "@portabletext/astro/types";
199
+ *
200
+ * type Props = ListProps;
201
+ * ---
202
+ * ```
203
+ */
204
+ export type ListProps = Props<List>
205
+
206
+ /**
207
+ * Alias to {@link https://portabletext.github.io/toolkit/interfaces/ToolkitPortableTextListItem.html ToolkitPortableTextListItem}
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * ---
212
+ * import type { ListItem, Props as $ } from "@portabletext/astro/types";
213
+ *
214
+ * type Props = $<ListItem>;
215
+ * ---
216
+ * ```
217
+ *
218
+ * @remarks To concisely achieve the same result in the example, use the convenience type {@link ListItemProps} instead.
219
+ */
220
+ export type ListItem = ToolkitPortableTextListItem
221
+
222
+ /**
223
+ * Convenience type for {@link ListItem} component props
224
+ *
225
+ * @remarks
226
+ * Added in: `v0.11.0`
227
+ *
228
+ * @example
229
+ * ```ts
230
+ * ---
231
+ * import type { ListItemProps } from "@portabletext/astro/types";
232
+ *
233
+ * type Props = ListItemProps;
234
+ * ---
235
+ * ```
236
+ */
237
+ export type ListItemProps = Props<ListItem>
238
+
239
+ /**
240
+ * Extends {@link https://portabletext.github.io/toolkit/interfaces/ToolkitNestedPortableTextSpan.html ToolkitNestedPortableTextSpan}
241
+ * with consisting `markDef` and `markKey` properties
242
+ *
243
+ * @typeParam MarkDef - Defines the shape of `markDef` property
244
+ *
245
+ * @remarks
246
+ * To concisely achieve the same result in the example, use the convenience type {@link MarkProps} instead.
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * ---
251
+ * import type { Mark, Props as $ } from "@portabletext/astro/types";
252
+ *
253
+ * type Greet = { msg: string };
254
+ * type Props = $<Mark<Greet>>;
255
+ * ---
256
+ * ```
257
+ */
258
+ export interface Mark<
259
+ MarkDef extends Record<string, unknown> | undefined = undefined,
260
+ > extends ToolkitNestedPortableTextSpan {
261
+ markDef: MarkDef & PortableTextMarkDefinition
262
+ markKey: string
263
+ }
264
+
265
+ /**
266
+ * Convenience type for {@link Mark} component props
267
+ *
268
+ * @remarks
269
+ * Added in: `v0.11.0`
270
+ *
271
+ * @example
272
+ * ```ts
273
+ * ---
274
+ * import type { MarkProps } from "@portabletext/astro/types";
275
+ *
276
+ * type Greet = { msg: string };
277
+ * type Props = MarkProps<Greet>;
278
+ * ---
279
+ * ```
280
+ */
281
+ export type MarkProps<MarkDef extends Record<string, unknown> | undefined = undefined> = Props<
282
+ Mark<MarkDef>
283
+ >
284
+
285
+ /**
286
+ * Alias to {@link https://portabletext.github.io/toolkit/interfaces/ToolkitTextNode.html ToolkitTextNode}
287
+ *
288
+ * @example
289
+ * ```ts
290
+ * ---
291
+ * import type { TextNode, Props as $ } from "@portabletext/astro/types";
292
+ *
293
+ * type Props = $<TextNode>;
294
+ * ---
295
+ * ```
296
+ *
297
+ * @remarks To concisely achieve the same result in the example, use the convenience type {@link TextNodeProps} instead.
298
+ */
299
+ export type TextNode = ToolkitTextNode
300
+
301
+ /**
302
+ * Convenience type for {@link TextNode} component props
303
+ *
304
+ * @remarks
305
+ * Added in: `v0.11.0`
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * ---
310
+ * import type { TextNodeProps } from "@portabletext/astro/types";
311
+ *
312
+ * type Props = TextNodeProps;
313
+ * ---
314
+ * ```
315
+ */
316
+ export type TextNodeProps = Props<TextNode>
317
+
318
+ /**
319
+ * The shape of the {@link PortableTextProps.onMissingComponent onMissingComponent} function
320
+ */
321
+ export type MissingComponentHandler = (
322
+ message: string,
323
+ context: {type: string; nodeType: NodeType},
324
+ ) => void
325
+
326
+ /**
327
+ * Properties for the `RenderHandler` function
328
+ *
329
+ * @typeParam T - Type of Portable Text payload
330
+ * @typeParam Children - Type of children
331
+ */
332
+ export type RenderHandlerProps<T extends TypedObject = TypedObject, Children = unknown> = {
333
+ /**
334
+ * The component that is associated with the Portable Text node.
335
+ */
336
+ Component: Component<T>
337
+ /**
338
+ * The component props
339
+ */
340
+ props: Props<T>
341
+ /**
342
+ * The children related to the Portable Text node.
343
+ * If the node is a custom {@link PortableTextComponents.type type} or a {@link TextNode}, then children will be `undefined`.
344
+ */
345
+ children?: Children
346
+ }
347
+
348
+ /**
349
+ * The shape of the render component function
350
+ *
351
+ * @typeParam T - Type of Portable Text payload
352
+ * @typeParam Children - Type of children
353
+ */
354
+ export type RenderHandler<T extends TypedObject = TypedObject, Children = unknown> = (
355
+ props: RenderHandlerProps<T, Children>,
356
+ ) => any
357
+
358
+ /**
359
+ * Options for the `render` function accessed via `usePortableText`
360
+ */
361
+ export type RenderOptions = {
362
+ type?: RenderHandler<TypedObject, never>
363
+ block?: RenderHandler<Block>
364
+ list?: RenderHandler<List>
365
+ listItem?: RenderHandler<ListItem>
366
+ mark?: RenderHandler<Mark>
367
+ text?: RenderHandler<TextNode, never>
368
+ hardBreak?: RenderHandler<TextNode, never>
369
+ }
370
+
371
+ /**
372
+ * Context object returned by `usePortableText`, providing utilities for rendering and customizing Portable Text components.
373
+ *
374
+ * The `Context` type includes functions to retrieve default or unknown components and
375
+ * to customize rendering behavior for specific node types.
376
+ */
377
+ export interface Context {
378
+ /**
379
+ * Retrieves the default `@portabletext/astro` component associated with a Portable Text node.
380
+ *
381
+ * @returns The default component for the node, such as `Block`, `List`, etc.
382
+ *
383
+ * @example
384
+ * ```ts
385
+ * ---
386
+ * const { getDefaultComponent } = usePortableText(node);
387
+ * const Component = getDefaultComponent();
388
+ * ---
389
+ * <Component {...Astro.props}>
390
+ * <slot />
391
+ * </Component>
392
+ * ```
393
+ */
394
+ getDefaultComponent: () => Component
395
+ /**
396
+ * Retrieves the `unknown` component associated with a Portable Text node.
397
+ *
398
+ * @returns The component used for unknown nodes, such as `unknownBlock` or `unknownList`.
399
+ *
400
+ * @example
401
+ * ```ts
402
+ * ---
403
+ * const { getUnknownComponent } = usePortableText(node);
404
+ * const Component = getUnknownComponent();
405
+ * ---
406
+ * <Component {...Astro.props}>
407
+ * <slot />
408
+ * </Component>
409
+ * ```
410
+ */
411
+ getUnknownComponent: () => Component
412
+ /**
413
+ * Customizes rendering for specific Portable Text node types.
414
+ *
415
+ * The `render` function enables developers to define custom behavior for specific node types,
416
+ * such as overriding the default text or mark rendering.
417
+ *
418
+ * @remarks
419
+ * Added in: `v0.11.0`
420
+ *
421
+ * @param options {@link RenderOptions} - Configuration for customizing node rendering
422
+ * @returns The desired output for the Portable Text node
423
+ *
424
+ * @example Basic usage
425
+ * ```ts
426
+ * ---
427
+ * import { usePortableText } from "@portabletext/astro";
428
+ *
429
+ * const { node } = Astro.props;
430
+ * const { getDefaultComponent, render } = usePortableText(node);
431
+ * const Component = getDefaultComponent();
432
+ * ---
433
+ * <Component {...Astro.props}>
434
+ * {render({
435
+ * text: ({ props }) => props.node.text.toUpperCase(),
436
+ * mark: ({ Component, props, children }) => (
437
+ * <Component {...props} class="custom-mark">{children}</Component>
438
+ * ),
439
+ * </Component>
440
+ *
441
+ * <style>
442
+ * .custom-mark {
443
+ * // some styles
444
+ * }
445
+ * </style>
446
+ * ```
447
+ */
448
+ render: (options: RenderOptions) => any
449
+ }
450
+
451
+ /**
452
+ * Generic Portable Text component
453
+ * @internal
454
+ */
455
+ export type Component<T extends TypedObject = any> = (props: Props<T>) => any
456
+
457
+ /**
458
+ * Defines a component or a mapping of components
459
+ * @internal
460
+ */
461
+ export type ComponentOrRecord<T extends TypedObject = any> =
462
+ | Component<T>
463
+ | Record<string, Component<T>>
464
+
465
+ /**
466
+ * Defines the type of Portable Text node
467
+ * @internal
468
+ */
469
+ export type NodeType = 'type' | 'block' | 'list' | 'listItem' | 'mark'
package/lib/utils.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ declare module '@portabletext/astro/utils' {
2
+ /**
3
+ * @deprecated Use `toPlainText` from `@portabletext/astro` instead
4
+ */
5
+ export function toPlainText(
6
+ ...args: Parameters<typeof import('@portabletext/toolkit').toPlainText>
7
+ ): ReturnType<typeof import('@portabletext/toolkit').toPlainText>
8
+ /**
9
+ * @deprecated Use `spanToPlainText` from `@portabletext/astro` instead
10
+ */
11
+ export function spanToPlainText(
12
+ ...args: Parameters<typeof import('@portabletext/toolkit').spanToPlainText>
13
+ ): ReturnType<typeof import('@portabletext/toolkit').spanToPlainText>
14
+ /**
15
+ * @deprecated Use `mergeComponents` from `@portabletext/astro` instead
16
+ */
17
+ export function mergeComponents(
18
+ ...args: Parameters<typeof import('./utils').mergeComponents>
19
+ ): ReturnType<typeof import('./utils').mergeComponents>
20
+ /**
21
+ * @deprecated Use `usePortableText` from `@portabletext/astro` instead
22
+ */
23
+ export function usePortableText(
24
+ ...args: Parameters<typeof import('./utils').usePortableText>
25
+ ): ReturnType<typeof import('./utils').usePortableText>
26
+ }
package/lib/utils.ts ADDED
@@ -0,0 +1,3 @@
1
+ export {toPlainText, spanToPlainText} from '@portabletext/toolkit'
2
+ export {mergeComponents} from './internal'
3
+ export {usePortableText} from './context'