cms-renderer 0.5.2 → 0.6.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/README.md +278 -154
- package/dist/lib/block-renderer.d.ts +9 -10
- package/dist/lib/block-renderer.js +109 -239
- package/dist/lib/block-renderer.js.map +1 -1
- package/dist/lib/block-toolbar.d.ts +5 -5
- package/dist/lib/block-toolbar.js +89 -70
- package/dist/lib/block-toolbar.js.map +1 -1
- package/dist/lib/client-editable-block.d.ts +11 -10
- package/dist/lib/client-editable-block.js +248 -31
- package/dist/lib/client-editable-block.js.map +1 -1
- package/dist/lib/custom-schemas.js +77 -39
- package/dist/lib/custom-schemas.js.map +1 -1
- package/dist/lib/renderer.js +109 -284
- package/dist/lib/renderer.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,22 +1,7 @@
|
|
|
1
1
|
// lib/block-renderer.tsx
|
|
2
2
|
import React from "react";
|
|
3
|
-
import { BlockToolbar } from "./block-toolbar.js";
|
|
4
3
|
import { ClientEditableBlock } from "./client-editable-block.js";
|
|
5
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
var SKIP_SPAN_PARENTS = /* @__PURE__ */ new Set([
|
|
7
|
-
// SVG text-bearing elements — spans are not valid SVG children
|
|
8
|
-
"text",
|
|
9
|
-
"tspan",
|
|
10
|
-
"textPath",
|
|
11
|
-
"desc",
|
|
12
|
-
// Form elements whose text content must not be interrupted
|
|
13
|
-
"option",
|
|
14
|
-
"optgroup",
|
|
15
|
-
// Non-rendered elements
|
|
16
|
-
"script",
|
|
17
|
-
"style",
|
|
18
|
-
"noscript"
|
|
19
|
-
]);
|
|
20
5
|
function walkReactNode(node, visitors, ctx = {}) {
|
|
21
6
|
const path = ctx.path ?? [];
|
|
22
7
|
if (node == null || typeof node === "boolean") return node;
|
|
@@ -83,136 +68,118 @@ function extractContentValues(content, basePath = []) {
|
|
|
83
68
|
walk(content, basePath);
|
|
84
69
|
return map;
|
|
85
70
|
}
|
|
86
|
-
var CMS_EDITABLE_STYLES = `
|
|
87
|
-
.cms-block-toolbar {
|
|
88
|
-
position: fixed;
|
|
89
|
-
transform: translateX(-50%);
|
|
90
|
-
display: flex;
|
|
91
|
-
gap: 4px;
|
|
92
|
-
background: #1f2937;
|
|
93
|
-
border-radius: 6px;
|
|
94
|
-
padding: 4px;
|
|
95
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
96
|
-
opacity: 0;
|
|
97
|
-
pointer-events: none;
|
|
98
|
-
transition: opacity 0.15s ease;
|
|
99
|
-
z-index: 9999;
|
|
100
|
-
}
|
|
101
|
-
.cms-block-toolbar button {
|
|
102
|
-
display: flex;
|
|
103
|
-
align-items: center;
|
|
104
|
-
justify-content: center;
|
|
105
|
-
width: 28px;
|
|
106
|
-
height: 28px;
|
|
107
|
-
border: none;
|
|
108
|
-
background: transparent;
|
|
109
|
-
color: #9ca3af;
|
|
110
|
-
border-radius: 4px;
|
|
111
|
-
cursor: pointer;
|
|
112
|
-
transition: background 0.15s ease, color 0.15s ease;
|
|
113
|
-
}
|
|
114
|
-
.cms-block-toolbar button:hover {
|
|
115
|
-
background: #374151;
|
|
116
|
-
color: #fff;
|
|
117
|
-
}
|
|
118
|
-
.cms-block-toolbar button.delete:hover {
|
|
119
|
-
background: #dc2626;
|
|
120
|
-
color: #fff;
|
|
121
|
-
}
|
|
122
|
-
.cms-block-toolbar button:disabled {
|
|
123
|
-
opacity: 0.4;
|
|
124
|
-
cursor: not-allowed;
|
|
125
|
-
}
|
|
126
|
-
.cms-block-toolbar button:disabled:hover {
|
|
127
|
-
background: transparent;
|
|
128
|
-
color: #9ca3af;
|
|
129
|
-
}
|
|
130
|
-
.cms-block-toolbar svg {
|
|
131
|
-
width: 16px;
|
|
132
|
-
height: 16px;
|
|
133
|
-
}
|
|
134
|
-
`;
|
|
135
|
-
var CMS_EDITABLE_SCRIPT = `
|
|
136
|
-
(function() {
|
|
137
|
-
if (!window.__cmsEditableInitialized) {
|
|
138
|
-
window.__cmsEditableInitialized = true;
|
|
139
|
-
var _ab = null;
|
|
140
|
-
|
|
141
|
-
function _tb(bid) {
|
|
142
|
-
return document.querySelector('.cms-block-toolbar[data-block-id="' + bid + '"]');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function _show(bid, target) {
|
|
146
|
-
var tb = _tb(bid);
|
|
147
|
-
if (tb && target) {
|
|
148
|
-
var r = target.getBoundingClientRect();
|
|
149
|
-
tb.style.left = Math.round(r.left + r.width / 2) + 'px';
|
|
150
|
-
tb.style.top = Math.round(r.bottom + 8) + 'px';
|
|
151
|
-
tb.style.opacity = '1';
|
|
152
|
-
tb.style.pointerEvents = 'auto';
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function _hide(bid) {
|
|
157
|
-
var tb = _tb(bid);
|
|
158
|
-
if (tb) { tb.style.opacity = '0'; tb.style.pointerEvents = 'none'; }
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
document.addEventListener('mouseover', function(e) {
|
|
162
|
-
if (e.target.closest('.cms-block-toolbar')) return;
|
|
163
|
-
var bel = e.target.closest('[data-cms-block]');
|
|
164
|
-
var bid = bel ? bel.getAttribute('data-block-id') : null;
|
|
165
|
-
if (bid === _ab) return;
|
|
166
|
-
if (_ab) _hide(_ab);
|
|
167
|
-
_ab = bid;
|
|
168
|
-
if (bid) _show(bid, e.target);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
document.addEventListener('click', function(e) {
|
|
172
|
-
if (e.target.closest('.cms-block-toolbar')) return;
|
|
173
|
-
|
|
174
|
-
var path = e.composedPath ? e.composedPath() : [e.target];
|
|
175
|
-
var et = null;
|
|
176
|
-
for (var i = 0; i < path.length; i++) {
|
|
177
|
-
var n = path[i];
|
|
178
|
-
if (n.nodeType === 1 && n.hasAttribute && n.hasAttribute('data-cms-editable')) { et = n; break; }
|
|
179
|
-
}
|
|
180
|
-
if (!et) et = e.target.closest && e.target.closest('[data-cms-editable]');
|
|
181
|
-
|
|
182
|
-
if (et) {
|
|
183
|
-
if (window.parent && window.parent !== window) {
|
|
184
|
-
window.parent.postMessage({
|
|
185
|
-
type: 'cms-editable-click',
|
|
186
|
-
blockId: et.getAttribute('data-block-id'),
|
|
187
|
-
blockType: et.getAttribute('data-block-type'),
|
|
188
|
-
contentPath: et.getAttribute('data-content-path')
|
|
189
|
-
}, '*');
|
|
190
|
-
}
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
var bt = e.target.closest && e.target.closest('[data-cms-block]');
|
|
195
|
-
if (bt) {
|
|
196
|
-
if (window.parent && window.parent !== window) {
|
|
197
|
-
window.parent.postMessage({
|
|
198
|
-
type: 'cms-editable-click',
|
|
199
|
-
blockId: bt.getAttribute('data-block-id'),
|
|
200
|
-
blockType: bt.getAttribute('data-block-type'),
|
|
201
|
-
contentPath: null
|
|
202
|
-
}, '*');
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
})();
|
|
208
|
-
`;
|
|
209
71
|
function CmsEditableInit() {
|
|
210
72
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
211
|
-
/* @__PURE__ */ jsx("style", { children:
|
|
73
|
+
/* @__PURE__ */ jsx("style", { children: `
|
|
74
|
+
[data-cms-block]:hover {
|
|
75
|
+
outline: 2px solid #3b82f6;
|
|
76
|
+
outline-offset: 4px;
|
|
77
|
+
}
|
|
78
|
+
[data-cms-editable] {
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
border-radius: 2px;
|
|
81
|
+
}
|
|
82
|
+
[data-cms-editable]:hover {
|
|
83
|
+
outline: 2px solid #3b82f6;
|
|
84
|
+
outline-offset: 2px;
|
|
85
|
+
}
|
|
86
|
+
.cms-block-toolbar {
|
|
87
|
+
position: absolute;
|
|
88
|
+
bottom: 8px;
|
|
89
|
+
left: 50%;
|
|
90
|
+
transform: translateX(-50%);
|
|
91
|
+
display: flex;
|
|
92
|
+
gap: 4px;
|
|
93
|
+
background: #1f2937;
|
|
94
|
+
border-radius: 6px;
|
|
95
|
+
padding: 4px;
|
|
96
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
97
|
+
opacity: 0;
|
|
98
|
+
pointer-events: none;
|
|
99
|
+
transition: opacity 0.15s ease;
|
|
100
|
+
z-index: 1000;
|
|
101
|
+
}
|
|
102
|
+
[data-cms-block]:hover .cms-block-toolbar {
|
|
103
|
+
opacity: 1;
|
|
104
|
+
pointer-events: auto;
|
|
105
|
+
}
|
|
106
|
+
.cms-block-toolbar button {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: center;
|
|
110
|
+
width: 28px;
|
|
111
|
+
height: 28px;
|
|
112
|
+
border: none;
|
|
113
|
+
background: transparent;
|
|
114
|
+
color: #9ca3af;
|
|
115
|
+
border-radius: 4px;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
118
|
+
}
|
|
119
|
+
.cms-block-toolbar button:hover {
|
|
120
|
+
background: #374151;
|
|
121
|
+
color: #fff;
|
|
122
|
+
}
|
|
123
|
+
.cms-block-toolbar button.delete:hover {
|
|
124
|
+
background: #dc2626;
|
|
125
|
+
color: #fff;
|
|
126
|
+
}
|
|
127
|
+
.cms-block-toolbar button:disabled {
|
|
128
|
+
opacity: 0.4;
|
|
129
|
+
cursor: not-allowed;
|
|
130
|
+
}
|
|
131
|
+
.cms-block-toolbar button:disabled:hover {
|
|
132
|
+
background: transparent;
|
|
133
|
+
color: #9ca3af;
|
|
134
|
+
}
|
|
135
|
+
.cms-block-toolbar svg {
|
|
136
|
+
width: 16px;
|
|
137
|
+
height: 16px;
|
|
138
|
+
}
|
|
139
|
+
` }),
|
|
212
140
|
/* @__PURE__ */ jsx(
|
|
213
141
|
"script",
|
|
214
142
|
{
|
|
215
|
-
dangerouslySetInnerHTML: {
|
|
143
|
+
dangerouslySetInnerHTML: {
|
|
144
|
+
__html: `
|
|
145
|
+
(function() {
|
|
146
|
+
if (!window.__cmsEditableInitialized) {
|
|
147
|
+
window.__cmsEditableInitialized = true;
|
|
148
|
+
|
|
149
|
+
document.addEventListener('click', function(e) {
|
|
150
|
+
if (e.target.closest('.cms-block-toolbar')) return;
|
|
151
|
+
|
|
152
|
+
var editableTarget = e.target.closest('[data-cms-editable]');
|
|
153
|
+
if (editableTarget) {
|
|
154
|
+
var message = {
|
|
155
|
+
type: 'cms-editable-click',
|
|
156
|
+
blockId: editableTarget.getAttribute('data-block-id'),
|
|
157
|
+
blockType: editableTarget.getAttribute('data-block-type'),
|
|
158
|
+
contentPath: editableTarget.getAttribute('data-content-path')
|
|
159
|
+
};
|
|
160
|
+
if (window.parent && window.parent !== window) {
|
|
161
|
+
window.parent.postMessage(message, '*');
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
var blockTarget = e.target.closest('[data-cms-block]');
|
|
167
|
+
if (blockTarget) {
|
|
168
|
+
var message = {
|
|
169
|
+
type: 'cms-editable-click',
|
|
170
|
+
blockId: blockTarget.getAttribute('data-block-id'),
|
|
171
|
+
blockType: blockTarget.getAttribute('data-block-type'),
|
|
172
|
+
contentPath: null
|
|
173
|
+
};
|
|
174
|
+
if (window.parent && window.parent !== window) {
|
|
175
|
+
window.parent.postMessage(message, '*');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
})();
|
|
181
|
+
`
|
|
182
|
+
}
|
|
216
183
|
}
|
|
217
184
|
)
|
|
218
185
|
] });
|
|
@@ -247,38 +214,6 @@ function resolveComponent(registry, blockType, path) {
|
|
|
247
214
|
}
|
|
248
215
|
return registry[blockType];
|
|
249
216
|
}
|
|
250
|
-
function renderToWalkableTree(node, keyPrefix = "") {
|
|
251
|
-
if (node == null || typeof node === "boolean") return node;
|
|
252
|
-
if (typeof node === "string" || typeof node === "number") return node;
|
|
253
|
-
if (Array.isArray(node)) {
|
|
254
|
-
return node.map((child, i) => {
|
|
255
|
-
const result = renderToWalkableTree(child, `${keyPrefix}${i}-`);
|
|
256
|
-
if (React.isValidElement(result) && result.key == null) {
|
|
257
|
-
const existingKey = child?.key;
|
|
258
|
-
return React.cloneElement(result, { key: existingKey ?? `${keyPrefix}${i}` });
|
|
259
|
-
}
|
|
260
|
-
return result;
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
if (React.isValidElement(node)) {
|
|
264
|
-
const el = node;
|
|
265
|
-
const elProps = el.props;
|
|
266
|
-
if (typeof el.type === "function") {
|
|
267
|
-
try {
|
|
268
|
-
const rendered = el.type(el.props);
|
|
269
|
-
return renderToWalkableTree(rendered, keyPrefix);
|
|
270
|
-
} catch {
|
|
271
|
-
return node;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
if (elProps && "children" in elProps) {
|
|
275
|
-
const newChildren = renderToWalkableTree(elProps.children, keyPrefix);
|
|
276
|
-
return React.cloneElement(el, void 0, newChildren);
|
|
277
|
-
}
|
|
278
|
-
return node;
|
|
279
|
-
}
|
|
280
|
-
return node;
|
|
281
|
-
}
|
|
282
217
|
function BlockRenderer({
|
|
283
218
|
block,
|
|
284
219
|
registry,
|
|
@@ -298,73 +233,8 @@ function BlockRenderer({
|
|
|
298
233
|
return component;
|
|
299
234
|
}
|
|
300
235
|
const contentValueMap = extractContentValues(block.content);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
try {
|
|
304
|
-
const renderedTree = renderToWalkableTree(component);
|
|
305
|
-
const isWalkable = React.isValidElement(renderedTree) && typeof renderedTree.type === "string";
|
|
306
|
-
if (isWalkable) {
|
|
307
|
-
const usedPaths = /* @__PURE__ */ new Set();
|
|
308
|
-
renderedComponent = walkReactNode(renderedTree, {
|
|
309
|
-
onText: ({ value, key, path: path2, inSvg, parentType }) => {
|
|
310
|
-
if (inSvg) return value;
|
|
311
|
-
if (parentType && typeof parentType === "string" && SKIP_SPAN_PARENTS.has(parentType)) {
|
|
312
|
-
return value;
|
|
313
|
-
}
|
|
314
|
-
const matches = contentValueMap.get(value);
|
|
315
|
-
if (!matches || matches.length === 0) return value;
|
|
316
|
-
const match = matches.find((m) => !usedPaths.has(m.contentPath)) ?? matches[0];
|
|
317
|
-
if (!match) return value;
|
|
318
|
-
usedPaths.add(match.contentPath);
|
|
319
|
-
const spanKey = key ?? `${block.id}-${match.contentPath}-${path2.join("-")}`;
|
|
320
|
-
return /* @__PURE__ */ jsx(
|
|
321
|
-
"span",
|
|
322
|
-
{
|
|
323
|
-
"data-cms-editable": true,
|
|
324
|
-
"data-block-id": block.id,
|
|
325
|
-
"data-block-type": block.type,
|
|
326
|
-
"data-content-path": match.contentPath,
|
|
327
|
-
style: { display: "contents" },
|
|
328
|
-
children: value
|
|
329
|
-
},
|
|
330
|
-
spanKey
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
needsClientSideSpans = false;
|
|
335
|
-
}
|
|
336
|
-
} catch {
|
|
337
|
-
}
|
|
338
|
-
const contentEntries = needsClientSideSpans ? Array.from(contentValueMap.entries()).map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath })).filter((e) => !!e.p) : [];
|
|
339
|
-
const blockProps = {
|
|
340
|
-
"data-cms-block": "true",
|
|
341
|
-
"data-block-id": block.id,
|
|
342
|
-
"data-block-type": block.type
|
|
343
|
-
};
|
|
344
|
-
if (needsClientSideSpans) {
|
|
345
|
-
return /* @__PURE__ */ jsxs(
|
|
346
|
-
ClientEditableBlock,
|
|
347
|
-
{
|
|
348
|
-
blockId: block.id,
|
|
349
|
-
blockType: block.type,
|
|
350
|
-
contentEntries,
|
|
351
|
-
blockProps,
|
|
352
|
-
children: [
|
|
353
|
-
renderedComponent,
|
|
354
|
-
/* @__PURE__ */ jsx(BlockToolbar, { blockId: block.id }, "cms-toolbar")
|
|
355
|
-
]
|
|
356
|
-
},
|
|
357
|
-
block.id
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
const rootWithProps = React.isValidElement(renderedComponent) ? React.cloneElement(
|
|
361
|
-
renderedComponent,
|
|
362
|
-
blockProps
|
|
363
|
-
) : renderedComponent;
|
|
364
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
365
|
-
rootWithProps,
|
|
366
|
-
/* @__PURE__ */ jsx(BlockToolbar, { blockId: block.id }, "cms-toolbar")
|
|
367
|
-
] });
|
|
236
|
+
const contentEntries = Array.from(contentValueMap.entries()).map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath })).filter((e) => !!e.p);
|
|
237
|
+
return /* @__PURE__ */ jsx(ClientEditableBlock, { blockId: block.id, blockType: block.type, contentEntries, children: component });
|
|
368
238
|
}
|
|
369
239
|
export {
|
|
370
240
|
BlockRenderer,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../lib/block-renderer.tsx"],"sourcesContent":["/**\n * Block Renderer Component\n *\n * Dispatches block data to the appropriate component using the ComponentMap pattern.\n * This is the main entry point for rendering blocks from the CMS.\n */\n\nimport React from 'react';\nimport { BlockToolbar } from './block-toolbar';\nimport { ClientEditableBlock } from './client-editable-block';\nimport type { BlockComponentRegistry, BlockData, ResolvedRouteParams } from './types';\n\ntype TextInfo = {\n value: string;\n path: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n};\n\ntype ElementInfo = {\n element: React.ReactElement;\n path: Array<string | number>;\n};\n\ntype WalkVisitors = {\n /**\n * Called for every string/number child encountered.\n * Return:\n * - same string (or modified)\n * - a ReactNode (e.g. wrap in <span/>)\n */\n onText?: (info: TextInfo) => React.ReactNode;\n\n /**\n * Called for every ReactElement encountered (after children are processed).\n * Return:\n * - same element\n * - a cloned/modified element\n */\n onElement?: (info: ElementInfo) => React.ReactElement;\n};\n\n/**\n * Recursively maps a ReactNode tree, allowing transformations of text nodes and/or elements.\n * SSR-safe: does not touch DOM APIs.\n */\n// Elements where injecting a <span> would break layout or produce invalid HTML\nconst SKIP_SPAN_PARENTS = new Set([\n // SVG text-bearing elements — spans are not valid SVG children\n 'text',\n 'tspan',\n 'textPath',\n 'desc',\n // Form elements whose text content must not be interrupted\n 'option',\n 'optgroup',\n // Non-rendered elements\n 'script',\n 'style',\n 'noscript',\n]);\n\nexport function walkReactNode(\n node: React.ReactNode,\n visitors: WalkVisitors,\n ctx: {\n path?: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n } = {}\n): React.ReactNode {\n const path = ctx.path ?? [];\n\n // Fast-path primitives\n if (node == null || typeof node === 'boolean') return node;\n\n if (typeof node === 'string' || typeof node === 'number') {\n const value = String(node);\n return visitors.onText\n ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key, inSvg: ctx.inSvg })\n : node;\n }\n\n // Arrays\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, i],\n parentType: ctx.parentType,\n key: childKey,\n inSvg: ctx.inSvg,\n });\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `arr-${path.join('-')}-${i}` });\n }\n return result;\n });\n }\n\n // ReactElement (including Fragment)\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // Track SVG context so we never inject <span> inside SVG subtrees\n const nextInSvg = ctx.inSvg || el.type === 'svg';\n\n // Recurse into children (if any)\n const hasChildren = elProps && 'children' in elProps;\n const nextChildren = hasChildren\n ? React.Children.map(elProps.children as React.ReactNode, (child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, 'children', i],\n parentType: el.type as React.ElementType,\n key: childKey,\n inSvg: nextInSvg,\n });\n // Ensure children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `child-${path.join('-')}-${i}` });\n }\n return result;\n })\n : (elProps?.children as React.ReactNode);\n\n // Only clone if children changed (or if you want to force a clone)\n const cloned = hasChildren\n ? React.cloneElement(el, undefined, nextChildren as React.ReactNode)\n : el;\n\n return visitors.onElement ? visitors.onElement({ element: cloned, path }) : cloned;\n }\n\n // Functions, symbols, portals, etc. are rare here; return as-is\n return node;\n}\n\n// -----------------------------------------------------------------------------\n// Content Value Extraction\n// -----------------------------------------------------------------------------\n\ntype ContentMatch = {\n contentPath: string;\n value: string;\n};\n\n/**\n * Extracts all string values from a content object with their paths.\n * Returns a Map where keys are string values and values are arrays of content paths.\n */\nfunction extractContentValues(\n content: Record<string, unknown>,\n basePath: string[] = []\n): Map<string, ContentMatch[]> {\n const map = new Map<string, ContentMatch[]>();\n\n function walk(obj: unknown, path: string[]) {\n if (typeof obj === 'string' && obj.trim() !== '') {\n const contentPath = path.join('.');\n const existing = map.get(obj) || [];\n existing.push({ contentPath, value: obj });\n map.set(obj, existing);\n } else if (Array.isArray(obj)) {\n for (let index = 0; index < obj.length; index++) {\n walk(obj[index], [...path, String(index)]);\n }\n } else if (obj && typeof obj === 'object') {\n for (const [key, value] of Object.entries(obj)) {\n walk(value, [...path, key]);\n }\n }\n }\n\n walk(content, basePath);\n return map;\n}\n\n// -----------------------------------------------------------------------------\n// CmsEditableInit — global styles + event wiring, rendered ONCE per page\n// -----------------------------------------------------------------------------\n\nconst CMS_EDITABLE_STYLES = `\n .cms-block-toolbar {\n position: fixed;\n transform: translateX(-50%);\n display: flex;\n gap: 4px;\n background: #1f2937;\n border-radius: 6px;\n padding: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.15s ease;\n z-index: 9999;\n }\n .cms-block-toolbar button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #9ca3af;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover {\n background: #374151;\n color: #fff;\n }\n .cms-block-toolbar button.delete:hover {\n background: #dc2626;\n color: #fff;\n }\n .cms-block-toolbar button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n .cms-block-toolbar button:disabled:hover {\n background: transparent;\n color: #9ca3af;\n }\n .cms-block-toolbar svg {\n width: 16px;\n height: 16px;\n }\n`;\n\nconst CMS_EDITABLE_SCRIPT = `\n(function() {\n if (!window.__cmsEditableInitialized) {\n window.__cmsEditableInitialized = true;\n var _ab = null;\n\n function _tb(bid) {\n return document.querySelector('.cms-block-toolbar[data-block-id=\"' + bid + '\"]');\n }\n\n function _show(bid, target) {\n var tb = _tb(bid);\n if (tb && target) {\n var r = target.getBoundingClientRect();\n tb.style.left = Math.round(r.left + r.width / 2) + 'px';\n tb.style.top = Math.round(r.bottom + 8) + 'px';\n tb.style.opacity = '1';\n tb.style.pointerEvents = 'auto';\n }\n }\n\n function _hide(bid) {\n var tb = _tb(bid);\n if (tb) { tb.style.opacity = '0'; tb.style.pointerEvents = 'none'; }\n }\n\n document.addEventListener('mouseover', function(e) {\n if (e.target.closest('.cms-block-toolbar')) return;\n var bel = e.target.closest('[data-cms-block]');\n var bid = bel ? bel.getAttribute('data-block-id') : null;\n if (bid === _ab) return;\n if (_ab) _hide(_ab);\n _ab = bid;\n if (bid) _show(bid, e.target);\n });\n\n document.addEventListener('click', function(e) {\n if (e.target.closest('.cms-block-toolbar')) return;\n\n var path = e.composedPath ? e.composedPath() : [e.target];\n var et = null;\n for (var i = 0; i < path.length; i++) {\n var n = path[i];\n if (n.nodeType === 1 && n.hasAttribute && n.hasAttribute('data-cms-editable')) { et = n; break; }\n }\n if (!et) et = e.target.closest && e.target.closest('[data-cms-editable]');\n\n if (et) {\n if (window.parent && window.parent !== window) {\n window.parent.postMessage({\n type: 'cms-editable-click',\n blockId: et.getAttribute('data-block-id'),\n blockType: et.getAttribute('data-block-type'),\n contentPath: et.getAttribute('data-content-path')\n }, '*');\n }\n return;\n }\n\n var bt = e.target.closest && e.target.closest('[data-cms-block]');\n if (bt) {\n if (window.parent && window.parent !== window) {\n window.parent.postMessage({\n type: 'cms-editable-click',\n blockId: bt.getAttribute('data-block-id'),\n blockType: bt.getAttribute('data-block-type'),\n contentPath: null\n }, '*');\n }\n }\n });\n }\n})();\n`;\n\n/**\n * Renders the shared CSS and event-wiring script needed for the CMS edit\n * overlay. Render this **once** at page level when edit mode is active —\n * e.g. just before the block list in your route component.\n *\n * If you use `BlockRenderer` standalone (outside of `ParametricRoutePage`)\n * you must render `<CmsEditableInit />` yourself somewhere on the page.\n */\nexport function CmsEditableInit() {\n return (\n <>\n <style>{CMS_EDITABLE_STYLES}</style>\n <script\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Inline script for iframe postMessage click routing\n dangerouslySetInnerHTML={{ __html: CMS_EDITABLE_SCRIPT }}\n />\n </>\n );\n}\n\n// -----------------------------------------------------------------------------\n// Props\n// -----------------------------------------------------------------------------\n\ninterface BlockRendererProps {\n /**\n * The block data to render.\n * Must have a `type` field that maps to a registered component.\n */\n block: BlockData;\n registry: Partial<BlockComponentRegistry>;\n /**\n * If true, renders the component without any tree walking or editable wrappers.\n */\n disableEditable?: boolean;\n /**\n * Resolved route parameters from parametric routes.\n * Each key is a param name (e.g., \"country\") with its value, schema name, and full document.\n */\n routeParams?: ResolvedRouteParams;\n /**\n * The current URL path (e.g. \"/en/test\").\n * When provided, enables path-namespaced component lookup.\n * Registry keys like \"/{lang}/test Article\" will be matched against this path,\n * where `{x}` and `(x)` are treated as single-segment wildcards.\n */\n path?: string;\n}\n\n// -----------------------------------------------------------------------------\n// Path-namespaced component resolution\n// -----------------------------------------------------------------------------\n\n/**\n * Returns true if each segment of `path` matches the corresponding segment in\n * `pattern`, where a pattern segment wrapped in `{…}` or `(…)` is a wildcard.\n */\nfunction pathMatchesPattern(path: string, pattern: string): boolean {\n const pathSegs = path.split('/').filter(Boolean);\n const patternSegs = pattern.split('/').filter(Boolean);\n if (pathSegs.length !== patternSegs.length) return false;\n for (let i = 0; i < patternSegs.length; i++) {\n const seg = patternSegs[i];\n if (!seg) return false;\n if ((seg.startsWith('{') && seg.endsWith('}')) || (seg.startsWith('(') && seg.endsWith(')'))) {\n continue;\n }\n if (seg !== pathSegs[i]) return false;\n }\n return true;\n}\n\n/**\n * Resolves the component for `blockType` from the registry.\n *\n * When `path` is provided, keys of the form `\"/{pattern} BlockType\"` are\n * checked first. The first key whose path pattern matches the current path\n * and whose block-type suffix matches `blockType` wins. Falls back to a\n * direct `registry[blockType]` lookup.\n */\nfunction resolveComponent(\n registry: Partial<BlockComponentRegistry>,\n blockType: string,\n path?: string\n): BlockComponentRegistry[string] | undefined {\n if (path) {\n for (const key of Object.keys(registry)) {\n if (!key.startsWith('/')) continue;\n const spaceIdx = key.indexOf(' ');\n if (spaceIdx === -1) continue;\n const pathPattern = key.slice(0, spaceIdx);\n const registeredType = key.slice(spaceIdx + 1);\n if (registeredType !== blockType) continue;\n if (pathMatchesPattern(path, pathPattern)) {\n return registry[key];\n }\n }\n }\n return registry[blockType];\n}\n\n// -----------------------------------------------------------------------------\n// Component\n// -----------------------------------------------------------------------------\n\n/**\n * Recursively renders a React node, invoking function components to get their output.\n * This allows us to walk the full rendered tree, not just the element wrappers.\n */\nfunction renderToWalkableTree(node: React.ReactNode, keyPrefix = ''): React.ReactNode {\n if (node == null || typeof node === 'boolean') return node;\n if (typeof node === 'string' || typeof node === 'number') return node;\n\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n const result = renderToWalkableTree(child, `${keyPrefix}${i}-`);\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n // biome-ignore lint/suspicious/noExplicitAny: Adding key to element\n const existingKey = (child as any)?.key;\n return React.cloneElement(result, { key: existingKey ?? `${keyPrefix}${i}` });\n }\n return result;\n });\n }\n\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // If it's a function component, invoke it to get the rendered output\n if (typeof el.type === 'function') {\n try {\n // biome-ignore lint/complexity/noBannedTypes: Need to invoke React function component\n const rendered = (el.type as Function)(el.props);\n return renderToWalkableTree(rendered, keyPrefix);\n } catch {\n // If component throws (e.g., uses hooks), return as-is\n return node;\n }\n }\n\n // For host elements (div, span, etc.), recurse into children\n if (elProps && 'children' in elProps) {\n const newChildren = renderToWalkableTree(elProps.children as React.ReactNode, keyPrefix);\n return React.cloneElement(el, undefined, newChildren);\n }\n\n return node;\n }\n\n return node;\n}\n\n/**\n * Renders a single block by dispatching to the appropriate component.\n *\n * Uses the ComponentMap pattern: the block's `type` field determines which\n * component renders the block's `content`.\n *\n * Internally, it:\n * 1. Renders the component tree by invoking function components\n * 2. Extracts all string values from block.content\n * 3. Walks the rendered tree and wraps matching text nodes with spans\n */\nexport function BlockRenderer({\n block,\n registry,\n disableEditable,\n routeParams,\n path,\n}: BlockRendererProps) {\n const Component = resolveComponent(registry, block.type, path);\n\n if (!Component) {\n // Log warning in development, render nothing in production\n if (process.env.NODE_ENV === 'development') {\n console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);\n }\n return null;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type safety ensured by BlockData discriminated union\n const component = <Component content={block.content as any} routeParams={routeParams} />;\n\n if (disableEditable) {\n return component;\n }\n\n // Extract all string values from content with their paths\n const contentValueMap = extractContentValues(block.content as Record<string, unknown>);\n\n // Try server-side tree walking for text span injection (best-effort).\n // This works for sync server components but fails for client components,\n // async components, and components using hooks.\n let renderedComponent: React.ReactNode = component;\n let needsClientSideSpans = true;\n\n try {\n const renderedTree = renderToWalkableTree(component);\n\n // Check if tree walking produced host elements (div, section, etc.)\n // vs the original component reference (function type = couldn't be invoked)\n const isWalkable =\n React.isValidElement(renderedTree) &&\n typeof (renderedTree as React.ReactElement<Record<string, unknown>>).type === 'string';\n\n if (isWalkable) {\n const usedPaths = new Set<string>();\n\n renderedComponent = walkReactNode(renderedTree, {\n onText: ({ value, key, path, inSvg, parentType }) => {\n // Never inject spans inside SVG subtrees or elements that don't accept span children\n if (inSvg) return value;\n if (parentType && typeof parentType === 'string' && SKIP_SPAN_PARENTS.has(parentType)) {\n return value;\n }\n\n const matches = contentValueMap.get(value);\n if (!matches || matches.length === 0) return value;\n\n const match = matches.find((m) => !usedPaths.has(m.contentPath)) ?? matches[0];\n if (!match) return value;\n\n usedPaths.add(match.contentPath);\n const spanKey = key ?? `${block.id}-${match.contentPath}-${path.join('-')}`;\n\n return (\n <span\n key={spanKey}\n data-cms-editable\n data-block-id={block.id}\n data-block-type={block.type}\n data-content-path={match.contentPath}\n style={{ display: 'contents' }}\n >\n {value}\n </span>\n );\n },\n });\n\n needsClientSideSpans = false;\n }\n } catch {\n // Tree walking failed entirely, fall back to client-side injection\n }\n\n // Build content entries for client-side text matching when server-side fails\n const contentEntries = needsClientSideSpans\n ? Array.from(contentValueMap.entries())\n .map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath }))\n .filter((e): e is { v: string; p: string } => !!e.p)\n : [];\n\n const blockProps = {\n 'data-cms-block': 'true',\n 'data-block-id': block.id,\n 'data-block-type': block.type,\n };\n\n // Component uses hooks — can't walk server-side, inject attributes client-side\n if (needsClientSideSpans) {\n return (\n <ClientEditableBlock\n key={block.id}\n blockId={block.id}\n blockType={block.type}\n contentEntries={contentEntries}\n blockProps={blockProps}\n >\n {renderedComponent}\n <BlockToolbar key=\"cms-toolbar\" blockId={block.id} />\n </ClientEditableBlock>\n );\n }\n\n // Server-side walkable: stamp block attributes directly onto the component's root element\n const rootWithProps = React.isValidElement(renderedComponent)\n ? React.cloneElement(\n renderedComponent as React.ReactElement<Record<string, unknown>>,\n blockProps\n )\n : renderedComponent;\n\n return (\n <>\n {rootWithProps}\n <BlockToolbar key=\"cms-toolbar\" blockId={block.id} />\n </>\n );\n}\n"],"mappings":"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","names":["path"]}
|
|
1
|
+
{"version":3,"sources":["../../lib/block-renderer.tsx"],"sourcesContent":["/**\n * Block Renderer Component\n *\n * Dispatches block data to the appropriate component using the ComponentMap pattern.\n * This is the main entry point for rendering blocks from the CMS.\n */\n\nimport React from 'react';\nimport { ClientEditableBlock } from './client-editable-block';\nimport type { BlockComponentRegistry, BlockData, ResolvedRouteParams } from './types';\n\ntype TextInfo = {\n value: string;\n path: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n};\n\ntype ElementInfo = {\n element: React.ReactElement;\n path: Array<string | number>;\n};\n\ntype WalkVisitors = {\n /**\n * Called for every string/number child encountered.\n * Return:\n * - same string (or modified)\n * - a ReactNode (e.g. wrap in <span/>)\n */\n onText?: (info: TextInfo) => React.ReactNode;\n\n /**\n * Called for every ReactElement encountered (after children are processed).\n * Return:\n * - same element\n * - a cloned/modified element\n */\n onElement?: (info: ElementInfo) => React.ReactElement;\n};\n\n/**\n * Recursively maps a ReactNode tree, allowing transformations of text nodes and/or elements.\n * SSR-safe: does not touch DOM APIs.\n */\n// Elements where injecting a <span> would break layout or produce invalid HTML\nconst SKIP_SPAN_PARENTS = new Set([\n // SVG text-bearing elements — spans are not valid SVG children\n 'text',\n 'tspan',\n 'textPath',\n 'desc',\n // Form elements whose text content must not be interrupted\n 'option',\n 'optgroup',\n // Non-rendered elements\n 'script',\n 'style',\n 'noscript',\n]);\n\nexport function walkReactNode(\n node: React.ReactNode,\n visitors: WalkVisitors,\n ctx: {\n path?: Array<string | number>;\n parentType?: React.ElementType;\n key?: React.Key | null;\n inSvg?: boolean;\n } = {}\n): React.ReactNode {\n const path = ctx.path ?? [];\n\n // Fast-path primitives\n if (node == null || typeof node === 'boolean') return node;\n\n if (typeof node === 'string' || typeof node === 'number') {\n const value = String(node);\n return visitors.onText\n ? visitors.onText({ value, path, parentType: ctx.parentType, key: ctx.key, inSvg: ctx.inSvg })\n : node;\n }\n\n // Arrays\n if (Array.isArray(node)) {\n return node.map((child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, i],\n parentType: ctx.parentType,\n key: childKey,\n inSvg: ctx.inSvg,\n });\n // Ensure array children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `arr-${path.join('-')}-${i}` });\n }\n return result;\n });\n }\n\n // ReactElement (including Fragment)\n if (React.isValidElement(node)) {\n // biome-ignore lint/suspicious/noExplicitAny: React element props access\n const el = node as React.ReactElement<any>;\n const elProps = el.props as Record<string, unknown> | null;\n\n // Track SVG context so we never inject <span> inside SVG subtrees\n const nextInSvg = ctx.inSvg || el.type === 'svg';\n\n // Recurse into children (if any)\n const hasChildren = elProps && 'children' in elProps;\n const nextChildren = hasChildren\n ? React.Children.map(elProps.children as React.ReactNode, (child, i) => {\n // biome-ignore lint/suspicious/noExplicitAny: React child key access\n const childKey = (child as any)?.key ?? null;\n const result = walkReactNode(child, visitors, {\n path: [...path, 'children', i],\n parentType: el.type as React.ElementType,\n key: childKey,\n inSvg: nextInSvg,\n });\n // Ensure children have keys\n if (React.isValidElement(result) && result.key == null) {\n return React.cloneElement(result, { key: childKey ?? `child-${path.join('-')}-${i}` });\n }\n return result;\n })\n : (elProps?.children as React.ReactNode);\n\n // Only clone if children changed (or if you want to force a clone)\n const cloned = hasChildren\n ? React.cloneElement(el, undefined, nextChildren as React.ReactNode)\n : el;\n\n return visitors.onElement ? visitors.onElement({ element: cloned, path }) : cloned;\n }\n\n // Functions, symbols, portals, etc. are rare here; return as-is\n return node;\n}\n\n// -----------------------------------------------------------------------------\n// Content Value Extraction\n// -----------------------------------------------------------------------------\n\ntype ContentMatch = {\n contentPath: string;\n value: string;\n};\n\n/**\n * Extracts all string values from a content object with their paths.\n * Returns a Map where keys are string values and values are arrays of content paths.\n */\nfunction extractContentValues(\n content: Record<string, unknown>,\n basePath: string[] = []\n): Map<string, ContentMatch[]> {\n const map = new Map<string, ContentMatch[]>();\n\n function walk(obj: unknown, path: string[]) {\n if (typeof obj === 'string' && obj.trim() !== '') {\n const contentPath = path.join('.');\n const existing = map.get(obj) || [];\n existing.push({ contentPath, value: obj });\n map.set(obj, existing);\n } else if (Array.isArray(obj)) {\n for (let index = 0; index < obj.length; index++) {\n walk(obj[index], [...path, String(index)]);\n }\n } else if (obj && typeof obj === 'object') {\n for (const [key, value] of Object.entries(obj)) {\n walk(value, [...path, key]);\n }\n }\n }\n\n walk(content, basePath);\n return map;\n}\n\n// -----------------------------------------------------------------------------\n// CmsEditableInit — render once per page in edit mode\n// -----------------------------------------------------------------------------\n\n/**\n * Renders the shared CMS edit-mode styles and click-routing script.\n * Place this once at the top of your page when edit_mode is active.\n */\nexport function CmsEditableInit() {\n return (\n <>\n <style>{`\n [data-cms-block]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 4px;\n }\n [data-cms-editable] {\n cursor: pointer;\n border-radius: 2px;\n }\n [data-cms-editable]:hover {\n outline: 2px solid #3b82f6;\n outline-offset: 2px;\n }\n .cms-block-toolbar {\n position: absolute;\n bottom: 8px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 4px;\n background: #1f2937;\n border-radius: 6px;\n padding: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.15s ease;\n z-index: 1000;\n }\n [data-cms-block]:hover .cms-block-toolbar {\n opacity: 1;\n pointer-events: auto;\n }\n .cms-block-toolbar button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #9ca3af;\n border-radius: 4px;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .cms-block-toolbar button:hover {\n background: #374151;\n color: #fff;\n }\n .cms-block-toolbar button.delete:hover {\n background: #dc2626;\n color: #fff;\n }\n .cms-block-toolbar button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n }\n .cms-block-toolbar button:disabled:hover {\n background: transparent;\n color: #9ca3af;\n }\n .cms-block-toolbar svg {\n width: 16px;\n height: 16px;\n }\n `}</style>\n <script\n // biome-ignore lint/security/noDangerouslySetInnerHtml: Inline script for iframe postMessage click routing\n dangerouslySetInnerHTML={{\n __html: `\n (function() {\n if (!window.__cmsEditableInitialized) {\n window.__cmsEditableInitialized = true;\n\n document.addEventListener('click', function(e) {\n if (e.target.closest('.cms-block-toolbar')) return;\n\n var editableTarget = e.target.closest('[data-cms-editable]');\n if (editableTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: editableTarget.getAttribute('data-block-id'),\n blockType: editableTarget.getAttribute('data-block-type'),\n contentPath: editableTarget.getAttribute('data-content-path')\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n return;\n }\n\n var blockTarget = e.target.closest('[data-cms-block]');\n if (blockTarget) {\n var message = {\n type: 'cms-editable-click',\n blockId: blockTarget.getAttribute('data-block-id'),\n blockType: blockTarget.getAttribute('data-block-type'),\n contentPath: null\n };\n if (window.parent && window.parent !== window) {\n window.parent.postMessage(message, '*');\n }\n }\n });\n }\n })();\n `,\n }}\n />\n </>\n );\n}\n\n// -----------------------------------------------------------------------------\n// Props\n// -----------------------------------------------------------------------------\n\ninterface BlockRendererProps {\n /**\n * The block data to render.\n * Must have a `type` field that maps to a registered component.\n */\n block: BlockData;\n registry: Partial<BlockComponentRegistry>;\n /**\n * If true, renders the component without any tree walking or editable wrappers.\n */\n disableEditable?: boolean;\n /**\n * Resolved route parameters from parametric routes.\n * Each key is a param name (e.g., \"country\") with its value, schema name, and full document.\n */\n routeParams?: ResolvedRouteParams;\n /**\n * The current URL path (e.g. \"/en/test\").\n * When provided, enables path-namespaced component lookup.\n * Registry keys like \"/{lang}/test Article\" will be matched against this path,\n * where `{x}` and `(x)` are treated as single-segment wildcards.\n */\n path?: string;\n}\n\n// -----------------------------------------------------------------------------\n// Path-namespaced component resolution\n// -----------------------------------------------------------------------------\n\n/**\n * Returns true if each segment of `path` matches the corresponding segment in\n * `pattern`, where a pattern segment wrapped in `{…}` or `(…)` is a wildcard.\n */\nfunction pathMatchesPattern(path: string, pattern: string): boolean {\n const pathSegs = path.split('/').filter(Boolean);\n const patternSegs = pattern.split('/').filter(Boolean);\n if (pathSegs.length !== patternSegs.length) return false;\n for (let i = 0; i < patternSegs.length; i++) {\n const seg = patternSegs[i];\n if (!seg) return false;\n if ((seg.startsWith('{') && seg.endsWith('}')) || (seg.startsWith('(') && seg.endsWith(')'))) {\n continue;\n }\n if (seg !== pathSegs[i]) return false;\n }\n return true;\n}\n\n/**\n * Resolves the component for `blockType` from the registry.\n *\n * When `path` is provided, keys of the form `\"/{pattern} BlockType\"` are\n * checked first. The first key whose path pattern matches the current path\n * and whose block-type suffix matches `blockType` wins. Falls back to a\n * direct `registry[blockType]` lookup.\n */\nfunction resolveComponent(\n registry: Partial<BlockComponentRegistry>,\n blockType: string,\n path?: string\n): BlockComponentRegistry[string] | undefined {\n if (path) {\n for (const key of Object.keys(registry)) {\n if (!key.startsWith('/')) continue;\n const spaceIdx = key.indexOf(' ');\n if (spaceIdx === -1) continue;\n const pathPattern = key.slice(0, spaceIdx);\n const registeredType = key.slice(spaceIdx + 1);\n if (registeredType !== blockType) continue;\n if (pathMatchesPattern(path, pathPattern)) {\n return registry[key];\n }\n }\n }\n return registry[blockType];\n}\n\n// -----------------------------------------------------------------------------\n// Component\n// -----------------------------------------------------------------------------\n\n/**\n * Renders a single block by dispatching to the appropriate component.\n *\n * Uses the ComponentMap pattern: the block's `type` field determines which\n * component renders the block's `content`.\n *\n * In editable mode, wraps the block in a ClientEditableBlock that:\n * - Stamps data-cms-block attributes directly on the component's root element\n * - Injects data-cms-editable spans around matching text nodes\n * - Portals the BlockToolbar into the component's root element\n *\n * Render CmsEditableInit once at the page level to include the shared styles\n * and click-routing script.\n */\nexport function BlockRenderer({\n block,\n registry,\n disableEditable,\n routeParams,\n path,\n}: BlockRendererProps) {\n const Component = resolveComponent(registry, block.type, path);\n\n if (!Component) {\n // Log warning in development, render nothing in production\n if (process.env.NODE_ENV === 'development') {\n console.warn(`[BlockRenderer] Unknown block type: ${block.type}`);\n }\n return null;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type safety ensured by BlockData discriminated union\n const component = <Component content={block.content as any} routeParams={routeParams} />;\n\n if (disableEditable) {\n return component;\n }\n\n // Build content entries for client-side text matching\n const contentValueMap = extractContentValues(block.content as Record<string, unknown>);\n const contentEntries = Array.from(contentValueMap.entries())\n .map(([value, matches]) => ({ v: value, p: matches[0]?.contentPath }))\n .filter((e): e is { v: string; p: string } => !!e.p);\n\n return (\n <ClientEditableBlock blockId={block.id} blockType={block.type} contentEntries={contentEntries}>\n {component}\n </ClientEditableBlock>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React__default from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Block Toolbar Component
|
|
5
5
|
*
|
|
6
6
|
* Provides move up/down and delete controls for blocks in edit mode.
|
|
7
|
-
*
|
|
8
|
-
* Shown/positioned by the inline CMS script via data-block-id lookup.
|
|
7
|
+
* This is a Client Component because it requires onClick handlers.
|
|
9
8
|
*/
|
|
10
|
-
declare function BlockToolbar({ blockId }: {
|
|
9
|
+
declare function BlockToolbar({ blockId, style }: {
|
|
11
10
|
blockId: string;
|
|
12
|
-
|
|
11
|
+
style?: React__default.CSSProperties;
|
|
12
|
+
}): React__default.JSX.Element;
|
|
13
13
|
|
|
14
14
|
export { BlockToolbar };
|