@storystick/react 0.0.0-stage → 0.2.0-rc.1
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/LICENSE +112 -0
- package/NOTICE +17 -0
- package/PUBLIC-API.md +38 -0
- package/README.md +226 -2
- package/dist/.source-hash +1 -0
- package/dist/authoring-skeleton.d.ts +5 -0
- package/dist/authoring-skeleton.js +29 -0
- package/dist/file.d.ts +18 -0
- package/dist/file.js +44 -0
- package/dist/hydrate-slice.d.ts +3 -0
- package/dist/hydrate-slice.js +1 -0
- package/dist/image-component.d.ts +26 -0
- package/dist/image-component.js +79 -0
- package/dist/image-element.d.ts +6 -0
- package/dist/image-element.js +68 -0
- package/dist/image.d.ts +3 -0
- package/dist/image.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/link-adapter.d.ts +2 -0
- package/dist/link-adapter.js +2 -0
- package/dist/link-component.d.ts +53 -0
- package/dist/link-component.js +97 -0
- package/dist/link.d.ts +3 -0
- package/dist/link.js +3 -0
- package/dist/preview-generation.d.ts +21 -0
- package/dist/preview-generation.js +134 -0
- package/dist/preview-host/preview-assistant-continuation.d.ts +9 -0
- package/dist/preview-host/preview-assistant-continuation.js +39 -0
- package/dist/preview-host/preview-authoring-client.d.ts +12 -0
- package/dist/preview-host/preview-authoring-client.js +108 -0
- package/dist/preview-host/preview-editor-continuation.d.ts +13 -0
- package/dist/preview-host/preview-editor-continuation.js +68 -0
- package/dist/preview-host/preview-standalone-controls.d.ts +18 -0
- package/dist/preview-host/preview-standalone-controls.js +528 -0
- package/dist/preview-host/preview-studio-destination.d.ts +20 -0
- package/dist/preview-host/preview-studio-destination.js +55 -0
- package/dist/preview-host/use-standalone-presentation.d.ts +15 -0
- package/dist/preview-host/use-standalone-presentation.js +223 -0
- package/dist/preview-icons.d.ts +10 -0
- package/dist/preview-icons.js +8 -0
- package/dist/preview-interaction-control.d.ts +6 -0
- package/dist/preview-interaction-control.js +39 -0
- package/dist/preview-page-state.d.ts +12 -0
- package/dist/preview-page-state.js +11 -0
- package/dist/preview-panel-state.d.ts +15 -0
- package/dist/preview-panel-state.js +28 -0
- package/dist/preview-provenance-publisher-model.d.ts +5 -0
- package/dist/preview-provenance-publisher-model.js +9 -0
- package/dist/preview-provenance-publisher.d.ts +15 -0
- package/dist/preview-provenance-publisher.js +68 -0
- package/dist/preview-provenance.d.ts +6 -0
- package/dist/preview-provenance.js +4 -0
- package/dist/preview-scope.d.ts +128 -0
- package/dist/preview-scope.js +161 -0
- package/dist/preview-slice-scope.d.ts +32 -0
- package/dist/preview-slice-scope.js +33 -0
- package/dist/preview-text-props.d.ts +18 -0
- package/dist/preview-text-props.js +13 -0
- package/dist/preview-toolbar-layout.d.ts +42 -0
- package/dist/preview-toolbar-layout.js +139 -0
- package/dist/preview-toolbar-panel.d.ts +14 -0
- package/dist/preview-toolbar-panel.js +6 -0
- package/dist/preview-toolbar-resize-handles.d.ts +9 -0
- package/dist/preview-toolbar-resize-handles.js +14 -0
- package/dist/preview-toolbar-shell.d.ts +19 -0
- package/dist/preview-toolbar-shell.js +144 -0
- package/dist/public-rich-text.d.ts +15 -0
- package/dist/public-rich-text.js +11 -0
- package/dist/public-slice-zone.d.ts +7 -0
- package/dist/public-slice-zone.js +6 -0
- package/dist/query-cache.d.ts +14 -0
- package/dist/query-cache.js +289 -0
- package/dist/query-preview-loader.d.ts +9 -0
- package/dist/query-preview-loader.js +28 -0
- package/dist/query-preview-runtime.d.ts +3 -0
- package/dist/query-preview-runtime.js +3 -0
- package/dist/rendering.d.ts +29 -0
- package/dist/rendering.js +17 -0
- package/dist/resize-edge.d.ts +8 -0
- package/dist/resize-edge.js +33 -0
- package/dist/rich-text.d.ts +150 -0
- package/dist/rich-text.js +618 -0
- package/dist/slice-render-failure.d.ts +8 -0
- package/dist/slice-render-failure.js +46 -0
- package/dist/slice-zone.d.ts +82 -0
- package/dist/slice-zone.js +273 -0
- package/dist/slice.d.ts +16 -0
- package/dist/slice.js +46 -0
- package/dist/storystick-mark.d.ts +3 -0
- package/dist/storystick-mark.js +6 -0
- package/dist/use-generated-query.d.ts +37 -0
- package/dist/use-generated-query.js +308 -0
- package/dist/use-momentary-preview-navigate-mode.d.ts +11 -0
- package/dist/use-momentary-preview-navigate-mode.js +158 -0
- package/dist/use-preview-perspective-intent.d.ts +53 -0
- package/dist/use-preview-perspective-intent.js +227 -0
- package/dist/use-preview-pointer-drag.d.ts +16 -0
- package/dist/use-preview-pointer-drag.js +98 -0
- package/dist/use-preview-toolbar-layout.d.ts +36 -0
- package/dist/use-preview-toolbar-layout.js +141 -0
- package/dist/use-render-mode-debounce.d.ts +31 -0
- package/dist/use-render-mode-debounce.js +143 -0
- package/dist/video.d.ts +21 -0
- package/dist/video.js +63 -0
- package/package.json +154 -3
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { isPreviewSliceDirectInvocationCandidate, readPreviewSliceScopeAdapter, } from './preview-slice-scope.js';
|
|
3
|
+
import { readContentOrigin, prepareContentSource, } from '@storystick/client/internal';
|
|
4
|
+
/**
|
|
5
|
+
* RichText — renders ProseMirror JSON to React elements.
|
|
6
|
+
*
|
|
7
|
+
* A lightweight tree walker with no TipTap dependency.
|
|
8
|
+
* Uses one semantic `components` registry for blocks, marks and embeds.
|
|
9
|
+
*
|
|
10
|
+
* <RichText content={post.fields.body} components={{ embeds: sliceComponents }} />
|
|
11
|
+
*/
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { stripPreviewProvenanceTextMarkers } from '@storystick/types/preview-provenance';
|
|
14
|
+
import { previewTextProps } from './preview-text-props.js';
|
|
15
|
+
import { getReferencedDocumentsSidecar, richTextToPlainText, isSectionAnchorSlug, resolveLink as resolveLinkValue, } from '@storystick/types';
|
|
16
|
+
import { hydrateSliceWithDiagnostics } from './hydrate-slice.js';
|
|
17
|
+
import { projectHydratedSlice, renderSliceFallback, reportSliceFailure, validateSliceVariation, } from './slice-render-failure.js';
|
|
18
|
+
import { stampPreviewScope } from './preview-scope.js';
|
|
19
|
+
import { extractSliceFields, getPreviewDocumentId, isPublishedSnapshotContext, makeSliceRenderId, serializeSliceModel, } from '@storystick/preview/provenance/slice-model';
|
|
20
|
+
import { buildDocumentProvenance, buildSliceProvenance, fieldSegment, previewPathKey, richTextTextNodeSegment, } from './preview-provenance.js';
|
|
21
|
+
// ─────────────────────────────────────────────
|
|
22
|
+
// Content parsing
|
|
23
|
+
// ─────────────────────────────────────────────
|
|
24
|
+
function parseContent(content) {
|
|
25
|
+
if (!content)
|
|
26
|
+
return null;
|
|
27
|
+
let doc = content;
|
|
28
|
+
if (typeof content === 'string') {
|
|
29
|
+
try {
|
|
30
|
+
doc = JSON.parse(content);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const node = doc;
|
|
37
|
+
if (!node || typeof node !== 'object' || !node.type)
|
|
38
|
+
return null;
|
|
39
|
+
if (!node.content?.length)
|
|
40
|
+
return null;
|
|
41
|
+
return node;
|
|
42
|
+
}
|
|
43
|
+
// ─────────────────────────────────────────────
|
|
44
|
+
// Default mark renderers
|
|
45
|
+
// ─────────────────────────────────────────────
|
|
46
|
+
function defaultMark(mark, children, key, link, title) {
|
|
47
|
+
switch (mark.type) {
|
|
48
|
+
case 'bold':
|
|
49
|
+
return _jsx("strong", { children: children }, key);
|
|
50
|
+
case 'italic':
|
|
51
|
+
return _jsx("em", { children: children }, key);
|
|
52
|
+
case 'underline':
|
|
53
|
+
return _jsx("u", { children: children }, key);
|
|
54
|
+
case 'strike':
|
|
55
|
+
return _jsx("s", { children: children }, key);
|
|
56
|
+
case 'code':
|
|
57
|
+
return _jsx("code", { children: children }, key);
|
|
58
|
+
case 'link': {
|
|
59
|
+
const status = link &&
|
|
60
|
+
link.status !== 'ok' &&
|
|
61
|
+
link.status !== 'external' &&
|
|
62
|
+
link.status !== 'asset'
|
|
63
|
+
? link.status
|
|
64
|
+
: undefined;
|
|
65
|
+
return (_jsx("a", { ...link?.props, title: title, "data-ss-link-status": status, children: children }, key));
|
|
66
|
+
}
|
|
67
|
+
default:
|
|
68
|
+
// Custom mark — render as span with attrs
|
|
69
|
+
return _jsx("span", { children: children }, key);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ─────────────────────────────────────────────
|
|
73
|
+
// Tree walker
|
|
74
|
+
// ─────────────────────────────────────────────
|
|
75
|
+
const STANDARD_MARK_ORDER = [
|
|
76
|
+
'link',
|
|
77
|
+
'bold',
|
|
78
|
+
'italic',
|
|
79
|
+
'underline',
|
|
80
|
+
'strike',
|
|
81
|
+
'code',
|
|
82
|
+
];
|
|
83
|
+
function markOrder(left, right) {
|
|
84
|
+
const rank = (name) => {
|
|
85
|
+
const index = STANDARD_MARK_ORDER.indexOf(name);
|
|
86
|
+
return index < 0 ? STANDARD_MARK_ORDER.length : index;
|
|
87
|
+
};
|
|
88
|
+
return rank(left) - rank(right) || (left < right ? -1 : left > right ? 1 : 0);
|
|
89
|
+
}
|
|
90
|
+
const DEFAULT_WALK_POSITION = {
|
|
91
|
+
block: { type: 'paragraph' },
|
|
92
|
+
ancestors: [],
|
|
93
|
+
};
|
|
94
|
+
const BUILTIN_MARKS = new Set([
|
|
95
|
+
'bold',
|
|
96
|
+
'italic',
|
|
97
|
+
'underline',
|
|
98
|
+
'strike',
|
|
99
|
+
'code',
|
|
100
|
+
'link',
|
|
101
|
+
]);
|
|
102
|
+
function reportRichTextDiagnostic(ctx, diagnostic) {
|
|
103
|
+
ctx.onDiagnostic?.(diagnostic);
|
|
104
|
+
if (!ctx.onDiagnostic && process.env.NODE_ENV !== 'production') {
|
|
105
|
+
console.warn(`[Storystick RichText] ${diagnostic.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function renderContext(position) {
|
|
109
|
+
return {
|
|
110
|
+
block: position.block,
|
|
111
|
+
ancestors: [...position.ancestors],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function authoredLinkTitle(mark) {
|
|
115
|
+
const title = mark.attrs?.title;
|
|
116
|
+
if (typeof title !== 'string')
|
|
117
|
+
return undefined;
|
|
118
|
+
return stripPreviewProvenanceTextMarkers(title) || undefined;
|
|
119
|
+
}
|
|
120
|
+
function extractText(node, parts) {
|
|
121
|
+
if (node.text)
|
|
122
|
+
parts.push(stripPreviewProvenanceTextMarkers(node.text));
|
|
123
|
+
for (const child of node.content ?? [])
|
|
124
|
+
extractText(child, parts);
|
|
125
|
+
}
|
|
126
|
+
function visibleNodeText(node) {
|
|
127
|
+
const parts = [];
|
|
128
|
+
extractText(node, parts);
|
|
129
|
+
return parts.length > 0 ? parts.join('') : undefined;
|
|
130
|
+
}
|
|
131
|
+
function headingLevel(node) {
|
|
132
|
+
const level = node.attrs?.level;
|
|
133
|
+
return typeof level === 'number' &&
|
|
134
|
+
Number.isInteger(level) &&
|
|
135
|
+
level >= 1 &&
|
|
136
|
+
level <= 6
|
|
137
|
+
? level
|
|
138
|
+
: 2;
|
|
139
|
+
}
|
|
140
|
+
function headingAnchorId(node) {
|
|
141
|
+
const anchor = node.attrs?._anchor;
|
|
142
|
+
if (anchor === null || typeof anchor !== 'object' || Array.isArray(anchor)) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
const slug = anchor.slug;
|
|
146
|
+
return typeof slug === 'string' && isSectionAnchorSlug(slug)
|
|
147
|
+
? slug
|
|
148
|
+
: undefined;
|
|
149
|
+
}
|
|
150
|
+
function currentList(ancestors) {
|
|
151
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
152
|
+
const ancestor = ancestors[index];
|
|
153
|
+
if (ancestor?.type === 'list') {
|
|
154
|
+
return { list: ancestor.list, depth: ancestor.depth };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { list: 'bullet', depth: 1 };
|
|
158
|
+
}
|
|
159
|
+
function renderNode(node, index, ctx, position, path) {
|
|
160
|
+
const key = `rt-${index}`;
|
|
161
|
+
// Text node — apply marks
|
|
162
|
+
if (node.type === 'text') {
|
|
163
|
+
const textNodeIndex = ctx.nextTextNodeIndex++;
|
|
164
|
+
const textProps = previewTextProps(node.text ?? '');
|
|
165
|
+
let result = textProps.children;
|
|
166
|
+
if (node.marks?.length) {
|
|
167
|
+
// Canonical nesting does not depend on the order in which marks were applied.
|
|
168
|
+
const marks = [...node.marks].sort((a, b) => markOrder(a.type, b.type));
|
|
169
|
+
for (let i = marks.length - 1; i >= 0; i--) {
|
|
170
|
+
const mark = marks[i];
|
|
171
|
+
const markKey = `${key}-m${i}`;
|
|
172
|
+
// Resolved once, above the override branch: a custom link renderer and
|
|
173
|
+
// the default one see the same href, from the same resolution path.
|
|
174
|
+
const link = mark.type === 'link'
|
|
175
|
+
? resolveLinkValue(mark.attrs, {
|
|
176
|
+
source: readContentOrigin(mark.attrs) ??
|
|
177
|
+
ctx.linkSource ??
|
|
178
|
+
ctx.referencedDocs,
|
|
179
|
+
})
|
|
180
|
+
: undefined;
|
|
181
|
+
const title = mark.type === 'link' ? authoredLinkTitle(mark) : undefined;
|
|
182
|
+
const context = renderContext(position);
|
|
183
|
+
const MarkOverride = ctx.components?.marks?.[mark.type];
|
|
184
|
+
if (mark.type === 'link' && MarkOverride && link) {
|
|
185
|
+
result = (_jsx(MarkOverride, { link: link, title: title, context: context, children: result }, markKey));
|
|
186
|
+
}
|
|
187
|
+
else if (MarkOverride) {
|
|
188
|
+
result = (_jsx(MarkOverride, { context: context, ...(mark.attrs?.value !== undefined
|
|
189
|
+
? { value: mark.attrs.value }
|
|
190
|
+
: {}), children: result }, markKey));
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
if (!BUILTIN_MARKS.has(mark.type)) {
|
|
194
|
+
reportRichTextDiagnostic(ctx, {
|
|
195
|
+
reason: 'missing-mark-renderer',
|
|
196
|
+
type: mark.type,
|
|
197
|
+
path,
|
|
198
|
+
message: `No RichText mark renderer is registered for "${mark.type}"; its children were preserved.`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
result = defaultMark(mark, result, markKey, link, title);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const contextualAttrs = provenanceAttrsForTextNode(ctx, textNodeIndex);
|
|
206
|
+
const provenanceAttrs = Object.keys(contextualAttrs).length > 0
|
|
207
|
+
? contextualAttrs
|
|
208
|
+
: textProps['data-ss-p'] ? { 'data-ss-p': textProps['data-ss-p'] } : {};
|
|
209
|
+
if (Object.keys(provenanceAttrs).length > 0) {
|
|
210
|
+
ctx.provenanceTextNodeCount += 1;
|
|
211
|
+
result = (_jsx("span", { suppressHydrationWarning: true, ...provenanceAttrs, ...(node.marks?.some(mark => mark.type === 'link')
|
|
212
|
+
? { 'data-ss-richtext-link': '' }
|
|
213
|
+
: {}), children: result }, `${key}-p`));
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
// Slice embed — dispatch to component registry
|
|
218
|
+
if (node.type === 'sliceEmbed') {
|
|
219
|
+
const embedIndex = ctx.nextEmbedIndex++;
|
|
220
|
+
const embedType = node.attrs?._type;
|
|
221
|
+
if (typeof embedType !== 'string' || embedType.length === 0) {
|
|
222
|
+
const failure = {
|
|
223
|
+
reason: 'missing-type',
|
|
224
|
+
slice: isRecord(node.attrs) ? node.attrs : {},
|
|
225
|
+
index: embedIndex,
|
|
226
|
+
};
|
|
227
|
+
reportSliceFailure(failure, ctx.onRenderFailure);
|
|
228
|
+
return renderSliceFallback(ctx.sliceFallback, failure, `missing-embed-type-${embedIndex}`, ctx.previewing || process.env.NODE_ENV !== 'production');
|
|
229
|
+
}
|
|
230
|
+
const rawEmbedKey = node.attrs?._key;
|
|
231
|
+
const embedKey = typeof rawEmbedKey === 'string' && rawEmbedKey.length > 0
|
|
232
|
+
? rawEmbedKey
|
|
233
|
+
: key;
|
|
234
|
+
const Component = ctx.components?.embeds?.[embedType];
|
|
235
|
+
if (!Component) {
|
|
236
|
+
const failure = {
|
|
237
|
+
reason: 'unregistered-type',
|
|
238
|
+
slice: { _type: embedType, _key: embedKey },
|
|
239
|
+
index: embedIndex,
|
|
240
|
+
registeredTypes: Object.keys(ctx.components?.embeds ?? {}).sort(),
|
|
241
|
+
};
|
|
242
|
+
reportSliceFailure(failure, ctx.onRenderFailure);
|
|
243
|
+
reportRichTextDiagnostic(ctx, {
|
|
244
|
+
reason: 'missing-embed-renderer',
|
|
245
|
+
type: embedType,
|
|
246
|
+
path,
|
|
247
|
+
message: `No RichText embed renderer is registered for "${embedType}".`,
|
|
248
|
+
});
|
|
249
|
+
return renderSliceFallback(ctx.sliceFallback, failure, embedKey, ctx.previewing || process.env.NODE_ENV !== 'production');
|
|
250
|
+
}
|
|
251
|
+
// Flatten attrs.data into a SliceData shape
|
|
252
|
+
const rawData = node.attrs?.data;
|
|
253
|
+
if (rawData !== undefined && !isRecord(rawData)) {
|
|
254
|
+
const failure = {
|
|
255
|
+
reason: 'invalid-slice',
|
|
256
|
+
value: rawData,
|
|
257
|
+
index: embedIndex,
|
|
258
|
+
};
|
|
259
|
+
reportSliceFailure(failure, ctx.onRenderFailure);
|
|
260
|
+
return renderSliceFallback(ctx.sliceFallback, failure, embedKey, ctx.previewing || process.env.NODE_ENV !== 'production');
|
|
261
|
+
}
|
|
262
|
+
const data = rawData ?? {};
|
|
263
|
+
const slice = {
|
|
264
|
+
...data,
|
|
265
|
+
_type: embedType,
|
|
266
|
+
_key: embedKey,
|
|
267
|
+
...(node.attrs &&
|
|
268
|
+
Object.prototype.hasOwnProperty.call(node.attrs, '_variation')
|
|
269
|
+
? { _variation: node.attrs._variation }
|
|
270
|
+
: {}),
|
|
271
|
+
};
|
|
272
|
+
const variationFailure = validateSliceVariation(slice, ctx.contracts, embedIndex);
|
|
273
|
+
if (variationFailure) {
|
|
274
|
+
reportSliceFailure(variationFailure, ctx.onRenderFailure);
|
|
275
|
+
return renderSliceFallback(ctx.sliceFallback, variationFailure, embedKey, ctx.previewing || process.env.NODE_ENV !== 'production');
|
|
276
|
+
}
|
|
277
|
+
const sourceRefPath = ctx.sliceField === undefined
|
|
278
|
+
? undefined
|
|
279
|
+
: `${ctx.parentPath ? `${ctx.parentPath}.` : ''}${ctx.sliceField}[${embedIndex}]._model`;
|
|
280
|
+
const contentOrigin = readContentOrigin(slice);
|
|
281
|
+
const hydration = contentOrigin
|
|
282
|
+
? { slice, failures: contentOrigin.failures ?? [] }
|
|
283
|
+
: hydrateSliceWithDiagnostics(slice, ctx.source, { sourceRefPath });
|
|
284
|
+
if (hydration.failures.length > 0) {
|
|
285
|
+
const failures = hydration.failures.map((failure) => failure.reason === 'missing-model'
|
|
286
|
+
? { ...failure, slice, index: embedIndex }
|
|
287
|
+
: { reason: 'invalid-bindings', slice, index: embedIndex });
|
|
288
|
+
failures.forEach((failure) => reportSliceFailure(failure, ctx.onRenderFailure));
|
|
289
|
+
return renderSliceFallback(ctx.sliceFallback, failures[0], embedKey, ctx.previewing || process.env.NODE_ENV !== 'production');
|
|
290
|
+
}
|
|
291
|
+
const hydrated = projectHydratedSlice(hydration.slice, ctx.contracts);
|
|
292
|
+
const hydratedFields = extractSliceFields(hydrated);
|
|
293
|
+
let propSlice = hydrated;
|
|
294
|
+
const componentContext = ctx.context;
|
|
295
|
+
if (ctx.previewing &&
|
|
296
|
+
ctx.previewDocId !== null &&
|
|
297
|
+
ctx.sliceField !== undefined &&
|
|
298
|
+
typeof slice._key === 'string' &&
|
|
299
|
+
slice._key.length > 0) {
|
|
300
|
+
const embedPathPrefix = ctx.parentPath
|
|
301
|
+
? `${ctx.parentPath}.${ctx.sliceField}`
|
|
302
|
+
: ctx.sliceField;
|
|
303
|
+
const modelJson = serializeSliceModel({
|
|
304
|
+
v: 129,
|
|
305
|
+
renderId: ctx.embedRenderId,
|
|
306
|
+
doc: ctx.previewDocId,
|
|
307
|
+
i: contentOrigin?.index ?? embedIndex,
|
|
308
|
+
type: embedType,
|
|
309
|
+
fields: hydratedFields,
|
|
310
|
+
zoneField: ctx.sliceField,
|
|
311
|
+
zoneFieldSource: 'explicit',
|
|
312
|
+
parentAnchors: contentOrigin?.parentAnchors ?? ctx.parentAnchors,
|
|
313
|
+
...(contentOrigin
|
|
314
|
+
? {
|
|
315
|
+
source: {
|
|
316
|
+
path: `${embedPathPrefix}[${contentOrigin.index ?? embedIndex}]`,
|
|
317
|
+
anchors: [
|
|
318
|
+
...contentOrigin.parentAnchors,
|
|
319
|
+
{ pathPrefix: embedPathPrefix, key: slice._key },
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
: {}),
|
|
324
|
+
});
|
|
325
|
+
if (modelJson !== null) {
|
|
326
|
+
propSlice = stampPreviewScope(hydrated, modelJson);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const bindings = buildSliceProvenance(contentOrigin ?? ctx.provenanceContext, slice);
|
|
330
|
+
const props = { slice: propSlice, context: componentContext, bindings };
|
|
331
|
+
const adapter = readPreviewSliceScopeAdapter();
|
|
332
|
+
const documentId = contentOrigin?.documentId ?? ctx.previewDocId;
|
|
333
|
+
if (adapter &&
|
|
334
|
+
documentId &&
|
|
335
|
+
isPreviewSliceDirectInvocationCandidate(Component)) {
|
|
336
|
+
const scoped = adapter({
|
|
337
|
+
component: Component,
|
|
338
|
+
props,
|
|
339
|
+
scope: {
|
|
340
|
+
routeFrame: null,
|
|
341
|
+
documentId,
|
|
342
|
+
sliceField: ctx.sliceField ?? 'richtext',
|
|
343
|
+
slicePath: contentOrigin?.path ??
|
|
344
|
+
`${ctx.sliceField ?? 'richtext'}[${embedIndex}]`,
|
|
345
|
+
sliceKey: embedKey,
|
|
346
|
+
sliceType: embedType,
|
|
347
|
+
sliceIndex: contentOrigin?.index ?? embedIndex,
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
return (_jsx(React.Fragment, { children: scoped }, embedKey));
|
|
351
|
+
}
|
|
352
|
+
return _jsx(Component, { ...props }, embedKey);
|
|
353
|
+
}
|
|
354
|
+
const renderChildren = (childPosition) => node.content?.map((child, childIndex) => renderNode(child, childIndex, ctx, childPosition, [...path, childIndex])) ?? null;
|
|
355
|
+
const renderTextBlockChildren = (childPosition) => {
|
|
356
|
+
const before = ctx.provenanceTextNodeCount;
|
|
357
|
+
const children = renderChildren(childPosition);
|
|
358
|
+
// Explicit opt-in geometry survives custom block renderers without reading
|
|
359
|
+
// arbitrary customer DOM. Keep each leaf's provenance for precise arrivals.
|
|
360
|
+
return ctx.provenanceTextNodeCount > before
|
|
361
|
+
? _jsx("span", { "data-ss-richtext-block": "", suppressHydrationWarning: true, children: children })
|
|
362
|
+
: children;
|
|
363
|
+
};
|
|
364
|
+
switch (node.type) {
|
|
365
|
+
case 'paragraph': {
|
|
366
|
+
const block = position.block.type === 'listItem'
|
|
367
|
+
? position.block
|
|
368
|
+
: { type: 'paragraph' };
|
|
369
|
+
const childPosition = { ...position, block };
|
|
370
|
+
const children = renderTextBlockChildren(childPosition);
|
|
371
|
+
const Component = ctx.components?.block?.paragraph;
|
|
372
|
+
return Component ? (_jsx(Component, { text: visibleNodeText(node), context: renderContext(childPosition), children: children }, key)) : (_jsx("p", { children: children }, key));
|
|
373
|
+
}
|
|
374
|
+
case 'heading': {
|
|
375
|
+
const level = headingLevel(node);
|
|
376
|
+
const id = headingAnchorId(node);
|
|
377
|
+
const childPosition = {
|
|
378
|
+
...position,
|
|
379
|
+
block: { type: 'heading', level },
|
|
380
|
+
};
|
|
381
|
+
const children = renderTextBlockChildren(childPosition);
|
|
382
|
+
const Component = ctx.components?.block?.[`h${level}`];
|
|
383
|
+
if (Component) {
|
|
384
|
+
return (_jsx(Component, { id: id, text: visibleNodeText(node), context: renderContext(childPosition), children: children }, key));
|
|
385
|
+
}
|
|
386
|
+
const Tag = `h${level}`;
|
|
387
|
+
return (_jsx(Tag, { id: id, children: children }, key));
|
|
388
|
+
}
|
|
389
|
+
case 'blockquote': {
|
|
390
|
+
const block = { type: 'blockquote' };
|
|
391
|
+
const childPosition = {
|
|
392
|
+
block,
|
|
393
|
+
ancestors: [...position.ancestors, { type: 'blockquote' }],
|
|
394
|
+
};
|
|
395
|
+
const children = renderChildren(childPosition);
|
|
396
|
+
const Component = ctx.components?.block?.blockquote;
|
|
397
|
+
return Component ? (_jsx(Component, { text: visibleNodeText(node), context: { block, ancestors: [...position.ancestors] }, children: children }, key)) : (_jsx("blockquote", { children: children }, key));
|
|
398
|
+
}
|
|
399
|
+
case 'codeBlock': {
|
|
400
|
+
const childPosition = {
|
|
401
|
+
...position,
|
|
402
|
+
block: { type: 'code' },
|
|
403
|
+
};
|
|
404
|
+
const children = renderTextBlockChildren(childPosition);
|
|
405
|
+
const language = typeof node.attrs?.language === 'string'
|
|
406
|
+
? node.attrs.language
|
|
407
|
+
: undefined;
|
|
408
|
+
const Component = ctx.components?.block?.code;
|
|
409
|
+
return Component ? (_jsx(Component, { text: visibleNodeText(node), language: language, context: renderContext(childPosition), children: children }, key)) : (_jsx("pre", { "data-language": language, children: _jsx("code", { children: children }) }, key));
|
|
410
|
+
}
|
|
411
|
+
case 'horizontalRule': {
|
|
412
|
+
const Component = ctx.components?.block?.divider;
|
|
413
|
+
return Component ? _jsx(Component, {}, key) : _jsx("hr", {}, key);
|
|
414
|
+
}
|
|
415
|
+
case 'hardBreak': {
|
|
416
|
+
const Component = ctx.components?.hardBreak;
|
|
417
|
+
return Component ? _jsx(Component, {}, key) : _jsx("br", {}, key);
|
|
418
|
+
}
|
|
419
|
+
case 'bulletList':
|
|
420
|
+
case 'orderedList': {
|
|
421
|
+
const list = node.type === 'bulletList' ? 'bullet' : 'ordered';
|
|
422
|
+
const depth = currentList(position.ancestors).depth +
|
|
423
|
+
(position.ancestors.some((ancestor) => ancestor.type === 'list')
|
|
424
|
+
? 1
|
|
425
|
+
: 0);
|
|
426
|
+
const childPosition = {
|
|
427
|
+
...position,
|
|
428
|
+
ancestors: [...position.ancestors, { type: 'list', list, depth }],
|
|
429
|
+
};
|
|
430
|
+
const children = renderChildren(childPosition);
|
|
431
|
+
if (list === 'ordered') {
|
|
432
|
+
const start = typeof node.attrs?.start === 'number' ? node.attrs.start : undefined;
|
|
433
|
+
const Component = ctx.components?.list?.ordered;
|
|
434
|
+
return Component ? (_jsx(Component, { start: start, children: children }, key)) : (_jsx("ol", { start: start, children: children }, key));
|
|
435
|
+
}
|
|
436
|
+
const Component = ctx.components?.list?.bullet;
|
|
437
|
+
return Component ? (_jsx(Component, { children: children }, key)) : (_jsx("ul", { children: children }, key));
|
|
438
|
+
}
|
|
439
|
+
case 'listItem': {
|
|
440
|
+
const list = currentList(position.ancestors);
|
|
441
|
+
const block = {
|
|
442
|
+
type: 'listItem',
|
|
443
|
+
list: list.list,
|
|
444
|
+
depth: list.depth,
|
|
445
|
+
};
|
|
446
|
+
const childPosition = {
|
|
447
|
+
block,
|
|
448
|
+
ancestors: [
|
|
449
|
+
...position.ancestors,
|
|
450
|
+
{ type: 'listItem', list: list.list, depth: list.depth },
|
|
451
|
+
],
|
|
452
|
+
};
|
|
453
|
+
const children = renderChildren(childPosition);
|
|
454
|
+
const Component = ctx.components?.listItem?.[list.list];
|
|
455
|
+
return Component ? (_jsx(Component, { children: children }, key)) : (_jsx("li", { children: children }, key));
|
|
456
|
+
}
|
|
457
|
+
default: {
|
|
458
|
+
reportRichTextDiagnostic(ctx, {
|
|
459
|
+
reason: 'unknown-block',
|
|
460
|
+
type: node.type,
|
|
461
|
+
path,
|
|
462
|
+
message: `Unknown RichText block "${node.type}"; its children were preserved.`,
|
|
463
|
+
});
|
|
464
|
+
const children = renderChildren(position);
|
|
465
|
+
return children ? _jsx("div", { children: children }, key) : null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// ─────────────────────────────────────────────
|
|
470
|
+
// Component
|
|
471
|
+
// ─────────────────────────────────────────────
|
|
472
|
+
function renderRichText(props, inline) {
|
|
473
|
+
const { content: inputContent, source, components, sliceFallback, contracts, onRenderFailure, onDiagnostic, context, bindings, provenancePath, sliceField, parentAnchors, parentPath, className, fallback, } = props;
|
|
474
|
+
const content = prepareContentSource(inputContent, source);
|
|
475
|
+
const origin = readContentOrigin(content);
|
|
476
|
+
const doc = parseContent(content);
|
|
477
|
+
if (!doc) {
|
|
478
|
+
return (fallback ?? null);
|
|
479
|
+
}
|
|
480
|
+
const technicalSource = origin ?? source?.document;
|
|
481
|
+
// Preparing a raw rich-text field gives the parsed value a compact
|
|
482
|
+
// ContentOrigin. That origin deliberately carries reference/address data,
|
|
483
|
+
// but not the complete document provenance map. Keep resolution on the
|
|
484
|
+
// origin while reading direct-field provenance from the explicit document.
|
|
485
|
+
const provenanceSource = source?.document ?? origin;
|
|
486
|
+
const referencedDocs = getReferencedDocumentsSidecar(technicalSource);
|
|
487
|
+
const linkSource = technicalSource;
|
|
488
|
+
const previewDocId = origin?.documentId ?? getPreviewDocumentId(source?.document);
|
|
489
|
+
const effectiveSliceField = origin?.path.split('.').at(-1) ?? sliceField ?? source?.path;
|
|
490
|
+
const effectiveParentAnchors = origin?.parentAnchors ?? parentAnchors ?? [];
|
|
491
|
+
const effectiveParentPath = origin?.path.includes('.')
|
|
492
|
+
? origin.path.slice(0, origin.path.lastIndexOf('.'))
|
|
493
|
+
: parentPath;
|
|
494
|
+
const inferredDocumentProvenance = !bindings && previewDocId !== null
|
|
495
|
+
? buildDocumentProvenance(provenanceSource, {
|
|
496
|
+
documentId: previewDocId,
|
|
497
|
+
})
|
|
498
|
+
: undefined;
|
|
499
|
+
const effectiveProvenance = bindings ?? inferredDocumentProvenance;
|
|
500
|
+
const effectiveProvenancePath = provenancePath ?? inferTopLevelProvenancePath(origin?.path ?? source?.path);
|
|
501
|
+
const provenanceContext = technicalSource;
|
|
502
|
+
const previewing = origin
|
|
503
|
+
? origin.preview === true
|
|
504
|
+
: previewDocId !== null && !isPublishedSnapshotContext(source?.document);
|
|
505
|
+
const embedRenderId = previewing && previewDocId !== null
|
|
506
|
+
? makeSliceRenderId({
|
|
507
|
+
scopeDocId: previewDocId,
|
|
508
|
+
slices: collectEmbedIdentities(doc),
|
|
509
|
+
scopeKey: `${effectiveParentPath ?? 'root'}:${effectiveSliceField}`,
|
|
510
|
+
})
|
|
511
|
+
: '';
|
|
512
|
+
const ctx = {
|
|
513
|
+
components: components,
|
|
514
|
+
sliceFallback,
|
|
515
|
+
contracts,
|
|
516
|
+
onRenderFailure,
|
|
517
|
+
onDiagnostic,
|
|
518
|
+
context,
|
|
519
|
+
source: technicalSource,
|
|
520
|
+
referencedDocs,
|
|
521
|
+
linkSource,
|
|
522
|
+
provenance: effectiveProvenance,
|
|
523
|
+
provenancePath: effectiveProvenancePath,
|
|
524
|
+
provenanceContext,
|
|
525
|
+
sliceField: effectiveSliceField,
|
|
526
|
+
parentPath: effectiveParentPath,
|
|
527
|
+
parentAnchors: effectiveParentAnchors,
|
|
528
|
+
previewDocId,
|
|
529
|
+
previewing,
|
|
530
|
+
embedRenderId,
|
|
531
|
+
nextTextNodeIndex: 0,
|
|
532
|
+
provenanceTextNodeCount: 0,
|
|
533
|
+
nextEmbedIndex: 0,
|
|
534
|
+
};
|
|
535
|
+
let renderNodes = doc.content;
|
|
536
|
+
if (inline) {
|
|
537
|
+
const validInlineShape = renderNodes.length === 1 && renderNodes[0]?.type === 'paragraph';
|
|
538
|
+
if (!validInlineShape) {
|
|
539
|
+
reportRichTextDiagnostic(ctx, {
|
|
540
|
+
reason: 'inline-shape',
|
|
541
|
+
path: [],
|
|
542
|
+
message: 'RichText.Inline expected exactly one paragraph; block wrappers were omitted while preserving renderable children.',
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
renderNodes = renderNodes.flatMap((node) => node.type === 'text' ||
|
|
546
|
+
node.type === 'hardBreak' ||
|
|
547
|
+
node.type === 'sliceEmbed'
|
|
548
|
+
? [node]
|
|
549
|
+
: (node.content ?? []));
|
|
550
|
+
}
|
|
551
|
+
const renderedNodes = renderNodes.map((node, index) => renderNode(node, index, ctx, DEFAULT_WALK_POSITION, [index]));
|
|
552
|
+
const rendered = inline && ctx.provenanceTextNodeCount > 0
|
|
553
|
+
? _jsx("span", { "data-ss-richtext-block": "", suppressHydrationWarning: true, children: renderedNodes })
|
|
554
|
+
: renderedNodes;
|
|
555
|
+
if (className) {
|
|
556
|
+
return _jsx("div", { className: className, children: rendered });
|
|
557
|
+
}
|
|
558
|
+
return _jsx(_Fragment, { children: rendered });
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* A raw primitive cannot carry ContentOrigin metadata. For the common direct
|
|
562
|
+
* document-field case, the explicit ContentSource still gives RichText enough
|
|
563
|
+
* information to recover its document provenance without app-owned plumbing.
|
|
564
|
+
* Indexed paths keep requiring an explicit provenancePath because their stable
|
|
565
|
+
* array-item keys cannot be inferred from a numeric string alone.
|
|
566
|
+
*/
|
|
567
|
+
function inferTopLevelProvenancePath(path) {
|
|
568
|
+
if (!path || path.includes('['))
|
|
569
|
+
return undefined;
|
|
570
|
+
const segments = path.split('.').filter(Boolean);
|
|
571
|
+
return segments.length > 0 ? segments.map(fieldSegment) : undefined;
|
|
572
|
+
}
|
|
573
|
+
function RichTextRoot(props) {
|
|
574
|
+
return renderRichText(props, false);
|
|
575
|
+
}
|
|
576
|
+
function RichTextInline(props) {
|
|
577
|
+
return renderRichText(props, true);
|
|
578
|
+
}
|
|
579
|
+
export const RichText = Object.assign(RichTextRoot, {
|
|
580
|
+
Inline: RichTextInline,
|
|
581
|
+
});
|
|
582
|
+
function isRecord(value) {
|
|
583
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
584
|
+
}
|
|
585
|
+
function collectEmbedIdentities(root) {
|
|
586
|
+
const out = [];
|
|
587
|
+
const visit = (node, index) => {
|
|
588
|
+
if (node.type === 'sliceEmbed') {
|
|
589
|
+
const embedType = node.attrs?._type;
|
|
590
|
+
const key = node.attrs?._key;
|
|
591
|
+
if (typeof embedType === 'string' && embedType.length > 0) {
|
|
592
|
+
out.push({
|
|
593
|
+
_type: embedType,
|
|
594
|
+
_key: typeof key === 'string' ? key : `rt-${index}`,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
node.content?.forEach((child, childIndex) => visit(child, childIndex));
|
|
599
|
+
};
|
|
600
|
+
visit(root, 0);
|
|
601
|
+
return out;
|
|
602
|
+
}
|
|
603
|
+
function provenanceAttrsForTextNode(ctx, textNodeIndex) {
|
|
604
|
+
if (!ctx.provenance || !ctx.provenancePath)
|
|
605
|
+
return {};
|
|
606
|
+
const path = [...ctx.provenancePath, richTextTextNodeSegment(textNodeIndex)];
|
|
607
|
+
return ctx.provenance.paths[previewPathKey(path)] ?? {};
|
|
608
|
+
}
|
|
609
|
+
// ─────────────────────────────────────────────
|
|
610
|
+
// toText utility
|
|
611
|
+
// ─────────────────────────────────────────────
|
|
612
|
+
/**
|
|
613
|
+
* Extract plain text from richtext content.
|
|
614
|
+
* Handles both JSON string and parsed ProseMirror document.
|
|
615
|
+
*/
|
|
616
|
+
export function toText(content, separator = ' ') {
|
|
617
|
+
return richTextToPlainText(content, separator);
|
|
618
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
2
|
+
export { validateSliceVariation, projectHydratedSlice } from '@storystick/types';
|
|
3
|
+
export type { SliceRenderFailure, SliceContract, SliceContractRegistry } from '@storystick/types';
|
|
4
|
+
import type { SliceRenderFailure } from '@storystick/types';
|
|
5
|
+
export type SliceFallback = ReactNode | ((failure: SliceRenderFailure) => ReactNode);
|
|
6
|
+
export type SliceRenderFailureHandler = (failure: SliceRenderFailure) => void;
|
|
7
|
+
export declare function reportSliceFailure(failure: SliceRenderFailure, onRenderFailure: SliceRenderFailureHandler | undefined): void;
|
|
8
|
+
export declare function renderSliceFallback(fallback: SliceFallback | undefined, failure: SliceRenderFailure, key: string, visiblyFail: boolean): ReactElement | null;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export { validateSliceVariation, projectHydratedSlice } from '@storystick/types';
|
|
3
|
+
export function reportSliceFailure(failure, onRenderFailure) {
|
|
4
|
+
onRenderFailure?.(failure);
|
|
5
|
+
if (process.env.NODE_ENV === 'production')
|
|
6
|
+
return;
|
|
7
|
+
switch (failure.reason) {
|
|
8
|
+
case 'invalid-slice':
|
|
9
|
+
console.warn(`StorystickError: Slice at index ${failure.index} is not an object.`);
|
|
10
|
+
return;
|
|
11
|
+
case 'missing-type':
|
|
12
|
+
console.warn(`StorystickError: Slice at index ${failure.index} has no "_type" field.\n` +
|
|
13
|
+
'Each slice must have: { _type: string, _key: string, ...fields }');
|
|
14
|
+
return;
|
|
15
|
+
case 'unregistered-type':
|
|
16
|
+
console.warn(`StorystickError: No component registered for slice type "${failure.slice._type}".\n` +
|
|
17
|
+
`Registered types: ${failure.registeredTypes.join(', ') || '(none)'}\n` +
|
|
18
|
+
`Fix: add "${failure.slice._type}" to the slice component registry.`);
|
|
19
|
+
return;
|
|
20
|
+
case 'invalid-variation':
|
|
21
|
+
console.warn(`StorystickError: Unknown variation ${JSON.stringify(failure.variation)} for slice "${failure.slice._type}".\n` +
|
|
22
|
+
`Registered variations: ${failure.registeredVariations.join(', ') || '(base only)'}\n` +
|
|
23
|
+
'Fix: omit _variation for the base slice or use a declared named variation.');
|
|
24
|
+
return;
|
|
25
|
+
case 'missing-model':
|
|
26
|
+
console.warn(`StorystickError: Model "${failure.modelId}" for slice "${failure.slice._type}" is missing from the response sidecar.`);
|
|
27
|
+
return;
|
|
28
|
+
case 'invalid-bindings':
|
|
29
|
+
console.warn(`StorystickError: Field bindings for slice "${failure.slice._type}" are malformed.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function renderSliceFallback(fallback, failure, key, visiblyFail) {
|
|
33
|
+
const rendered = fallback === undefined
|
|
34
|
+
? visiblyFail
|
|
35
|
+
? React.createElement('div', {
|
|
36
|
+
role: 'alert',
|
|
37
|
+
'data-storystick-slice-failure': failure.reason,
|
|
38
|
+
}, `Slice could not render (${failure.reason}).`)
|
|
39
|
+
: null
|
|
40
|
+
: typeof fallback === 'function'
|
|
41
|
+
? fallback(failure)
|
|
42
|
+
: fallback;
|
|
43
|
+
if (rendered === null || rendered === undefined || rendered === false)
|
|
44
|
+
return null;
|
|
45
|
+
return React.createElement(React.Fragment, { key }, rendered);
|
|
46
|
+
}
|