@portabletext/astro 0.0.0 → 0.1.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 +7 -0
- package/LICENSE +15 -0
- package/README.md +275 -27
- package/components/Block.astro +36 -0
- package/components/HardBreak.astro +7 -0
- package/components/List.astro +24 -0
- package/components/ListItem.astro +9 -0
- package/components/Mark.astro +32 -0
- package/components/PortableText.astro +393 -0
- package/components/Text.astro +9 -0
- package/components/UnknownBlock.astro +7 -0
- package/components/UnknownList.astro +7 -0
- package/components/UnknownListItem.astro +7 -0
- package/components/UnknownMark.astro +7 -0
- package/components/UnknownType.astro +21 -0
- package/lib/astro.d.ts +9 -0
- package/lib/components.ts +4 -0
- package/lib/context.ts +21 -0
- package/lib/index.ts +2 -0
- package/lib/internal.ts +103 -0
- package/lib/types.ts +469 -0
- package/lib/utils.d.ts +26 -0
- package/lib/utils.ts +3 -0
- package/lib/warnings.ts +32 -0
- package/package.json +89 -7
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
isPortableTextBlock,
|
|
4
|
+
isPortableTextListItemBlock,
|
|
5
|
+
isPortableTextToolkitList,
|
|
6
|
+
isPortableTextToolkitSpan,
|
|
7
|
+
isPortableTextToolkitTextNode,
|
|
8
|
+
nestLists,
|
|
9
|
+
buildMarksTree,
|
|
10
|
+
LIST_NEST_MODE_HTML,
|
|
11
|
+
} from "@portabletext/toolkit";
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
Component,
|
|
15
|
+
Context,
|
|
16
|
+
MissingComponentHandler,
|
|
17
|
+
NodeType,
|
|
18
|
+
PortableTextComponents,
|
|
19
|
+
PortableTextProps,
|
|
20
|
+
Props as ComponentProps,
|
|
21
|
+
RenderOptions,
|
|
22
|
+
TypedObject,
|
|
23
|
+
} from "../lib/types";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
isComponent,
|
|
27
|
+
mergeComponents,
|
|
28
|
+
setNodeComponents,
|
|
29
|
+
getNodeComponents,
|
|
30
|
+
} from "../lib/internal";
|
|
31
|
+
|
|
32
|
+
import { getWarningMessage, printWarning } from "../lib/warnings";
|
|
33
|
+
import { key as contextRef } from "../lib/context";
|
|
34
|
+
|
|
35
|
+
import Block from "./Block.astro";
|
|
36
|
+
import HardBreak from "./HardBreak.astro";
|
|
37
|
+
import List from "./List.astro";
|
|
38
|
+
import ListItem from "./ListItem.astro";
|
|
39
|
+
import Mark from "./Mark.astro";
|
|
40
|
+
import Text from "./Text.astro";
|
|
41
|
+
import UnknownBlock from "./UnknownBlock.astro";
|
|
42
|
+
import UnknownList from "./UnknownList.astro";
|
|
43
|
+
import UnknownListItem from "./UnknownListItem.astro";
|
|
44
|
+
import UnknownMark from "./UnknownMark.astro";
|
|
45
|
+
import UnknownType from "./UnknownType.astro";
|
|
46
|
+
|
|
47
|
+
export type Props = PortableTextProps;
|
|
48
|
+
|
|
49
|
+
const {
|
|
50
|
+
value,
|
|
51
|
+
components: componentOverrides = {},
|
|
52
|
+
listNestingMode = LIST_NEST_MODE_HTML,
|
|
53
|
+
onMissingComponent = true,
|
|
54
|
+
} = Astro.props;
|
|
55
|
+
|
|
56
|
+
const components = mergeComponents(
|
|
57
|
+
{
|
|
58
|
+
type: {},
|
|
59
|
+
unknownType: UnknownType,
|
|
60
|
+
block: {
|
|
61
|
+
h1: Block,
|
|
62
|
+
h2: Block,
|
|
63
|
+
h3: Block,
|
|
64
|
+
h4: Block,
|
|
65
|
+
h5: Block,
|
|
66
|
+
h6: Block,
|
|
67
|
+
blockquote: Block,
|
|
68
|
+
normal: Block,
|
|
69
|
+
},
|
|
70
|
+
unknownBlock: UnknownBlock,
|
|
71
|
+
list: {
|
|
72
|
+
bullet: List,
|
|
73
|
+
number: List,
|
|
74
|
+
menu: List,
|
|
75
|
+
},
|
|
76
|
+
unknownList: UnknownList,
|
|
77
|
+
listItem: {
|
|
78
|
+
bullet: ListItem,
|
|
79
|
+
number: ListItem,
|
|
80
|
+
menu: ListItem,
|
|
81
|
+
},
|
|
82
|
+
unknownListItem: UnknownListItem,
|
|
83
|
+
mark: {
|
|
84
|
+
code: Mark,
|
|
85
|
+
em: Mark,
|
|
86
|
+
link: Mark,
|
|
87
|
+
"strike-through": Mark,
|
|
88
|
+
strong: Mark,
|
|
89
|
+
underline: Mark,
|
|
90
|
+
},
|
|
91
|
+
unknownMark: UnknownMark,
|
|
92
|
+
text: Text,
|
|
93
|
+
hardBreak: HardBreak,
|
|
94
|
+
},
|
|
95
|
+
componentOverrides
|
|
96
|
+
) as PortableTextComponents;
|
|
97
|
+
|
|
98
|
+
const noop = () => {};
|
|
99
|
+
|
|
100
|
+
const missingComponentHandler = ((
|
|
101
|
+
handler: unknown
|
|
102
|
+
): MissingComponentHandler => {
|
|
103
|
+
if (typeof handler === "function") {
|
|
104
|
+
return handler as MissingComponentHandler;
|
|
105
|
+
}
|
|
106
|
+
return !handler ? noop : printWarning;
|
|
107
|
+
})(onMissingComponent);
|
|
108
|
+
|
|
109
|
+
const asComponentProps = <T extends TypedObject>(
|
|
110
|
+
node: T,
|
|
111
|
+
index: number,
|
|
112
|
+
isInline: boolean
|
|
113
|
+
): ComponentProps<T> => ({
|
|
114
|
+
node,
|
|
115
|
+
index,
|
|
116
|
+
isInline,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const provideComponent = (
|
|
120
|
+
nodeType: NodeType,
|
|
121
|
+
type: string,
|
|
122
|
+
fallbackComponent: Component
|
|
123
|
+
): Component => {
|
|
124
|
+
const component: Component | undefined = ((entry) => {
|
|
125
|
+
return entry[type as keyof typeof entry] || entry;
|
|
126
|
+
})(components[nodeType]);
|
|
127
|
+
|
|
128
|
+
if (isComponent(component)) {
|
|
129
|
+
return component;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
missingComponentHandler(getWarningMessage(nodeType, type), {
|
|
133
|
+
nodeType,
|
|
134
|
+
type,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return fallbackComponent;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// The local render function will override these options
|
|
141
|
+
let fallbackRenderOptions: Required<RenderOptions> | undefined;
|
|
142
|
+
|
|
143
|
+
const portableTextRender = (options: RenderOptions, isInline?: boolean) => {
|
|
144
|
+
if (!fallbackRenderOptions) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
"[PortableText portableTextRender] fallbackRenderOptions is undefined"
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const renderChildren = (children?: TypedObject[], inline = false) => {
|
|
151
|
+
return children?.map(portableTextRender(options, inline)) ?? [];
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const renderOptions = { ...fallbackRenderOptions, ...options };
|
|
155
|
+
|
|
156
|
+
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
|
+
) {
|
|
161
|
+
if (!isComponent(handler)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`[PortableText render] No handler found for node type ${node._type}.`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return handler(props);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (isPortableTextToolkitList(node)) {
|
|
171
|
+
const UnknownComponent = components.unknownList ?? UnknownList;
|
|
172
|
+
setNodeComponents(node, List, UnknownComponent);
|
|
173
|
+
|
|
174
|
+
return run(renderOptions.list, {
|
|
175
|
+
Component: provideComponent("list", node.listItem, UnknownComponent),
|
|
176
|
+
props: asComponentProps(node, index, false),
|
|
177
|
+
children: renderChildren(node.children, false),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (isPortableTextListItemBlock(node)) {
|
|
182
|
+
const { listItem, ...blockNode } = node;
|
|
183
|
+
const isStyled = node.style && node.style !== "normal";
|
|
184
|
+
// Apply block style if defined (and not "normal"), otherwise render with marks.
|
|
185
|
+
node.children = isStyled
|
|
186
|
+
? renderNode(blockNode, index)
|
|
187
|
+
: buildMarksTree(node);
|
|
188
|
+
|
|
189
|
+
const UnknownComponent = components.unknownListItem ?? UnknownListItem;
|
|
190
|
+
setNodeComponents(node, ListItem, UnknownComponent);
|
|
191
|
+
|
|
192
|
+
return run(renderOptions.listItem, {
|
|
193
|
+
Component: provideComponent(
|
|
194
|
+
"listItem",
|
|
195
|
+
node.listItem,
|
|
196
|
+
UnknownComponent
|
|
197
|
+
),
|
|
198
|
+
props: asComponentProps(node, index, false),
|
|
199
|
+
children: isStyled
|
|
200
|
+
? node.children
|
|
201
|
+
: renderChildren(node.children, true),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (isPortableTextToolkitSpan(node)) {
|
|
206
|
+
const UnknownComponent = components.unknownMark ?? UnknownMark;
|
|
207
|
+
setNodeComponents(node, Mark, UnknownComponent);
|
|
208
|
+
|
|
209
|
+
return run(renderOptions.mark, {
|
|
210
|
+
Component: provideComponent("mark", node.markType, UnknownComponent),
|
|
211
|
+
props: asComponentProps(node, index, true),
|
|
212
|
+
children: renderChildren(node.children, true),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (isPortableTextBlock(node)) {
|
|
217
|
+
node.style ??= "normal"; /* Make sure style has been set */
|
|
218
|
+
node.children = buildMarksTree(node);
|
|
219
|
+
|
|
220
|
+
const UnknownComponent = components.unknownBlock ?? UnknownBlock;
|
|
221
|
+
setNodeComponents(node, Block, UnknownComponent);
|
|
222
|
+
|
|
223
|
+
return run(renderOptions.block, {
|
|
224
|
+
Component: provideComponent("block", node.style, UnknownComponent),
|
|
225
|
+
props: asComponentProps(node, index, false),
|
|
226
|
+
children: renderChildren(node.children, true),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (isPortableTextToolkitTextNode(node)) {
|
|
231
|
+
const isHardBreak = "\n" === node.text;
|
|
232
|
+
const props = asComponentProps(node, index, true);
|
|
233
|
+
|
|
234
|
+
if (isHardBreak) {
|
|
235
|
+
return run(renderOptions.hardBreak, {
|
|
236
|
+
Component: isComponent(components.hardBreak)
|
|
237
|
+
? components.hardBreak
|
|
238
|
+
: HardBreak,
|
|
239
|
+
props,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return run(renderOptions.text, {
|
|
244
|
+
Component: isComponent(components.text) ? components.text : Text,
|
|
245
|
+
props,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Custom type
|
|
250
|
+
const UnknownComponent = components.unknownType ?? UnknownType;
|
|
251
|
+
|
|
252
|
+
return run(renderOptions.type, {
|
|
253
|
+
Component: provideComponent("type", node._type, UnknownComponent),
|
|
254
|
+
props: asComponentProps(
|
|
255
|
+
node,
|
|
256
|
+
index,
|
|
257
|
+
isInline ?? false /* default to block */
|
|
258
|
+
),
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
(globalThis as any)[contextRef] = (
|
|
264
|
+
node: TypedObject & { children?: TypedObject[] }
|
|
265
|
+
): Context => ({
|
|
266
|
+
getDefaultComponent: provideDefaultComponent.bind(null, node),
|
|
267
|
+
getUnknownComponent: provideUnknownComponent.bind(null, node),
|
|
268
|
+
render: (options) => node.children?.map(portableTextRender(options)),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Returns the `default` component related to the passed in node
|
|
272
|
+
const provideDefaultComponent = (node: TypedObject) => {
|
|
273
|
+
const DefaultComponent = getNodeComponents(node)?.Default;
|
|
274
|
+
if (DefaultComponent) return DefaultComponent;
|
|
275
|
+
|
|
276
|
+
// Cache missed use manual lookup
|
|
277
|
+
if (import.meta.env.DEV) {
|
|
278
|
+
// oxlint-disable-next-line no-console
|
|
279
|
+
console.warn(
|
|
280
|
+
`[@portabletext/astro] Cache missed for default component on node type "${node._type}". Ensure you are passing in the original "node" prop.`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (isPortableTextToolkitList(node)) return List;
|
|
285
|
+
if (isPortableTextListItemBlock(node)) return ListItem;
|
|
286
|
+
if (isPortableTextToolkitSpan(node)) return Mark;
|
|
287
|
+
if (isPortableTextBlock(node)) return Block;
|
|
288
|
+
|
|
289
|
+
if (isPortableTextToolkitTextNode(node)) {
|
|
290
|
+
return "\n" === node.text ? HardBreak : Text;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return UnknownType;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Returns the `unknown` component related to the passed in node
|
|
297
|
+
const provideUnknownComponent = (node: TypedObject) => {
|
|
298
|
+
const UnknownComponent = getNodeComponents(node)?.Unknown;
|
|
299
|
+
if (UnknownComponent) return UnknownComponent;
|
|
300
|
+
|
|
301
|
+
// Cache missed use manual lookup
|
|
302
|
+
if (import.meta.env.DEV) {
|
|
303
|
+
// oxlint-disable-next-line no-console
|
|
304
|
+
console.warn(
|
|
305
|
+
`[@portabletext/astro] Cache missed for unknown component on node type "${node._type}". Ensure you are passing in the original "node" prop.`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (isPortableTextToolkitList(node)) {
|
|
310
|
+
return components.unknownList ?? UnknownList;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (isPortableTextListItemBlock(node)) {
|
|
314
|
+
return components.unknownListItem ?? UnknownListItem;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (isPortableTextToolkitSpan(node)) {
|
|
318
|
+
return components.unknownMark ?? UnknownMark;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (isPortableTextBlock(node)) {
|
|
322
|
+
return components.unknownBlock ?? UnknownBlock;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!isPortableTextToolkitTextNode(node)) {
|
|
326
|
+
return components.unknownType ?? UnknownType;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
throw new Error(
|
|
330
|
+
`[PortableText getUnknownComponent] Unable to provide component with node type ${node._type}`
|
|
331
|
+
);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Make sure we have an array of blocks
|
|
335
|
+
const blocks = Array.isArray(value) ? value : value ? [value] : [];
|
|
336
|
+
const nodes = nestLists(blocks, listNestingMode);
|
|
337
|
+
|
|
338
|
+
const render = (options: NonNullable<typeof fallbackRenderOptions>) => {
|
|
339
|
+
fallbackRenderOptions = options;
|
|
340
|
+
return portableTextRender(options);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Create a slot renderer for a specific slot
|
|
344
|
+
const createSlotRenderer = (slotName: string) =>
|
|
345
|
+
Astro.slots.render.bind(Astro.slots, slotName);
|
|
346
|
+
|
|
347
|
+
type SlotRenderer = ReturnType<typeof createSlotRenderer>;
|
|
348
|
+
|
|
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
|
+
);
|
|
364
|
+
|
|
365
|
+
type RenderNode = (
|
|
366
|
+
slotRenderer: SlotRenderer | undefined
|
|
367
|
+
) => (
|
|
368
|
+
props: Parameters<NonNullable<RenderOptions[keyof RenderOptions]>>[0]
|
|
369
|
+
) => any;
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
{
|
|
373
|
+
(() => {
|
|
374
|
+
const renderNode: RenderNode = (slotRenderer) => {
|
|
375
|
+
return ({ Component, props, children }) =>
|
|
376
|
+
slotRenderer?.([{ Component, props, children }]) ?? (
|
|
377
|
+
<Component {...(props as any)}>{children}</Component>
|
|
378
|
+
);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
return nodes.map(
|
|
382
|
+
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),
|
|
390
|
+
})
|
|
391
|
+
);
|
|
392
|
+
})()
|
|
393
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { TypedObject, Props as $ } from "../lib/types";
|
|
3
|
+
import { getWarningMessage } from "../lib/warnings";
|
|
4
|
+
|
|
5
|
+
export type Props = $<TypedObject>;
|
|
6
|
+
|
|
7
|
+
const { node, isInline } = Astro.props;
|
|
8
|
+
const warning = getWarningMessage("type", node._type);
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
isInline ? (
|
|
13
|
+
<span style="display:none" data-portabletext-unknown="type">
|
|
14
|
+
{warning}
|
|
15
|
+
</span>
|
|
16
|
+
) : (
|
|
17
|
+
<div style="display:none" data-portabletext-unknown="type">
|
|
18
|
+
{warning}
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
package/lib/astro.d.ts
ADDED
package/lib/context.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {TypedObject} from '@portabletext/types'
|
|
2
|
+
|
|
3
|
+
import type {Context} from './types'
|
|
4
|
+
|
|
5
|
+
export const key = Symbol('@portabletext/astro')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This function returns rendering utility functions within a Portable Text tree. It should
|
|
9
|
+
* only be used within an Astro component that has been passed into the PortableText `components` prop.
|
|
10
|
+
* It follows a naming convention similar to React hooks, though it is not a hook as such.
|
|
11
|
+
*
|
|
12
|
+
* @param node - The Portable Text node that was passed into the Astro component
|
|
13
|
+
* @returns Rendering utility functions
|
|
14
|
+
*/
|
|
15
|
+
export function usePortableText(node: TypedObject) {
|
|
16
|
+
if (!(key in globalThis)) {
|
|
17
|
+
throw new Error(`PortableText "context" has not been initialised`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (globalThis as any)[key](node) as Context
|
|
21
|
+
}
|
package/lib/index.ts
ADDED
package/lib/internal.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type {Component, ComponentOrRecord, SomePortableTextComponents, TypedObject} from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper for component to throw an error
|
|
5
|
+
* @param err
|
|
6
|
+
*/
|
|
7
|
+
export function throwError(err: Error | string): never {
|
|
8
|
+
throw err
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns true if `it` is component
|
|
13
|
+
*/
|
|
14
|
+
export function isComponent(it: unknown): it is Component {
|
|
15
|
+
return typeof it === 'function'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Merges two {@link SomePortableTextComponents} objects, giving priority to overrides.
|
|
20
|
+
*
|
|
21
|
+
* This function combines two component objects used in Portable Text rendering.
|
|
22
|
+
* If both objects have the same key, the value from `overrides` takes precedence.
|
|
23
|
+
* This is useful for customizing the rendering of specific components while keeping
|
|
24
|
+
* the default behavior for others.
|
|
25
|
+
*
|
|
26
|
+
* @typeParam Components - The type of the base components object.
|
|
27
|
+
* @typeParam Overrides - The type of the overrides components object.
|
|
28
|
+
*
|
|
29
|
+
* @param components - The base components object.
|
|
30
|
+
* @param overrides - The overrides components object.
|
|
31
|
+
*
|
|
32
|
+
* @returns A new object with the merged components.
|
|
33
|
+
*/
|
|
34
|
+
export function mergeComponents<
|
|
35
|
+
Components extends SomePortableTextComponents,
|
|
36
|
+
Overrides extends SomePortableTextComponents,
|
|
37
|
+
>(components: Components, overrides: Overrides) {
|
|
38
|
+
const cmps = {...components} as Record<string, ComponentOrRecord>
|
|
39
|
+
|
|
40
|
+
for (const [key, override] of Object.entries(overrides)) {
|
|
41
|
+
const current = components[key as keyof typeof components]
|
|
42
|
+
|
|
43
|
+
const value =
|
|
44
|
+
!current || isComponent(override) || isComponent(current)
|
|
45
|
+
? override
|
|
46
|
+
: {
|
|
47
|
+
...(current as Record<string, Component>),
|
|
48
|
+
...(override as Record<string, Component>),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
cmps[key] = value
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return cmps as {
|
|
55
|
+
[Key in keyof (Components & Overrides)]: Key extends keyof (Overrides | Components)
|
|
56
|
+
? Overrides[Key] extends Component
|
|
57
|
+
? Overrides[Key]
|
|
58
|
+
: Components[Key] extends Component
|
|
59
|
+
? Overrides[Key]
|
|
60
|
+
: (Overrides & Components)[Key]
|
|
61
|
+
: (Overrides & Components)[Key]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* ========================
|
|
67
|
+
* Node Components Registry
|
|
68
|
+
* ========================
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
type ResolvedComponents = {
|
|
72
|
+
Default: Component
|
|
73
|
+
Unknown: Component
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const nodeComponentsMap = new WeakMap<TypedObject, ResolvedComponents>()
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Binds the resolved components to a specific node object.
|
|
80
|
+
* @internal
|
|
81
|
+
*
|
|
82
|
+
* @remarks
|
|
83
|
+
* This uses the node's _object reference_ as the key. This enables the `Context` API via
|
|
84
|
+
* `usePortableText` to look up which components were assigned to this specific node during rendering.
|
|
85
|
+
*
|
|
86
|
+
* @param node - The node object to be used as the key.
|
|
87
|
+
* @param Default - The resolved default component for this node.
|
|
88
|
+
* @param Unknown - The resolved fallback (unknown) component for this node.
|
|
89
|
+
*/
|
|
90
|
+
export function setNodeComponents(node: TypedObject, Default: Component, Unknown: Component): void {
|
|
91
|
+
nodeComponentsMap.set(node, {Default, Unknown})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Retrieves the components bound to a specific node object.
|
|
96
|
+
* @internal
|
|
97
|
+
*
|
|
98
|
+
* @param node - The node object to look up (by reference).
|
|
99
|
+
* @returns The component pair, or `undefined` if this exact node object was not registered.
|
|
100
|
+
*/
|
|
101
|
+
export function getNodeComponents(node: TypedObject): ResolvedComponents | undefined {
|
|
102
|
+
return nodeComponentsMap.get(node)
|
|
103
|
+
}
|