@bettercms-ai/richtext 0.5.0 → 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/package.json +1 -1
- package/src/html.ts +47 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bettercms-ai/richtext",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "The one Portable Text contract for BetterCMS: schema, converters, and rendering. Shared by the backend, the dashboard and every delivery SDK.",
|
package/src/html.ts
CHANGED
|
@@ -230,6 +230,9 @@ export interface RenderOptions {
|
|
|
230
230
|
* Wrapping at the top level rather than inside the block components is deliberate: a block
|
|
231
231
|
* component only ever sees TEXT blocks, so a divider or an embedded image would get no
|
|
232
232
|
* address — and those are exactly the nodes an editor most wants to click.
|
|
233
|
+
*
|
|
234
|
+
* List items are the one exception: a run of them renders as ONE `<ul>`/`<ol>` and the address
|
|
235
|
+
* sits on each `<li>`, because a wrapper per item would split the list into one-item lists.
|
|
233
236
|
*/
|
|
234
237
|
bindingBase?: string;
|
|
235
238
|
}
|
|
@@ -253,10 +256,27 @@ const tagOverrides = <T extends string>(
|
|
|
253
256
|
return out;
|
|
254
257
|
};
|
|
255
258
|
|
|
256
|
-
|
|
259
|
+
/** The per-node address `portableTextToHtml({ bindingBase })` stamps. "" for a node with no key. */
|
|
260
|
+
const bindingAttrs = (base: string, key: unknown): string =>
|
|
261
|
+
typeof key === "string" && key
|
|
262
|
+
? ` data-bcms-field="${esc(base)}#${esc(key)}" data-bcms-kind="richtext"`
|
|
263
|
+
: "";
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* `keys` is the set of `_key`s the value actually carries: to-html invents one (`li-0-1`) for an
|
|
267
|
+
* item that has none, and an invented key is an address that resolves to nothing.
|
|
268
|
+
*/
|
|
269
|
+
const buildComponents = (
|
|
270
|
+
classes: HtmlClasses = {},
|
|
271
|
+
binding?: { base: string; keys: ReadonlySet<unknown> },
|
|
272
|
+
): Partial<PortableTextHtmlComponents> => ({
|
|
257
273
|
block: tagOverrides(classes, STYLE_TAGS),
|
|
258
274
|
list: tagOverrides(classes, { bullet: "ul", number: "ol", check: "ul" }),
|
|
259
|
-
|
|
275
|
+
// With a base, every item carries its own address ON the `<li>` — see `bindingBase`.
|
|
276
|
+
listItem: binding
|
|
277
|
+
? ({ value, children }: { value: { _key?: unknown }; children?: string }) =>
|
|
278
|
+
`<li${cls(classes, "li")}${binding.keys.has(value._key) ? bindingAttrs(binding.base, value._key) : ""}>${children ?? ""}</li>`
|
|
279
|
+
: tagOverrides(classes, { bullet: "li", number: "li", check: "li" }),
|
|
260
280
|
types: {
|
|
261
281
|
[PT_TYPES.divider]: () => "<hr>",
|
|
262
282
|
[PT_TYPES.table]: ({ value }) => tableHtml(value as PortableTextTable),
|
|
@@ -362,6 +382,9 @@ const buildComponents = (classes: HtmlClasses = {}): Partial<PortableTextHtmlCom
|
|
|
362
382
|
/** The undressed components, built once: the overwhelming majority of calls pass no classes. */
|
|
363
383
|
const defaultComponents = buildComponents();
|
|
364
384
|
|
|
385
|
+
const isListItem = (node: unknown): boolean =>
|
|
386
|
+
isRecord(node) && node._type === "block" && typeof node.listItem === "string" && !!node.listItem;
|
|
387
|
+
|
|
365
388
|
export function portableTextToHtml(
|
|
366
389
|
value: PortableText | null | undefined,
|
|
367
390
|
opts: RenderOptions = {},
|
|
@@ -370,16 +393,28 @@ export function portableTextToHtml(
|
|
|
370
393
|
const components = opts.classes ? buildComponents(opts.classes) : defaultComponents;
|
|
371
394
|
|
|
372
395
|
if (opts.bindingBase) {
|
|
373
|
-
const base =
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
396
|
+
const base = opts.bindingBase;
|
|
397
|
+
const bound = buildComponents(opts.classes, { base, keys: new Set(value.map((n) => n._key)) });
|
|
398
|
+
const out: string[] = [];
|
|
399
|
+
for (let i = 0; i < value.length; ) {
|
|
400
|
+
const node = value[i];
|
|
401
|
+
// A RUN of list items renders as ONE list, each `<li>` carrying its own address. Wrapping
|
|
402
|
+
// items one by one gave every item its own `<ol>`, so numbering restarted at 1 on every line
|
|
403
|
+
// and bullets sat a list-margin apart — the canvas no longer looked like the site.
|
|
404
|
+
if (isListItem(node)) {
|
|
405
|
+
let end = i + 1;
|
|
406
|
+
while (end < value.length && isListItem(value[end])) end++;
|
|
407
|
+
out.push(toHTML(value.slice(i, end) as never, { components: bound }));
|
|
408
|
+
i = end;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const inner = toHTML([node] as never, { components });
|
|
412
|
+
// A node with no key cannot be addressed. Render it rather than dropping it — an
|
|
413
|
+
// unaddressable paragraph is a small loss, a missing one is content disappearing.
|
|
414
|
+
out.push(node._key ? `<div${bindingAttrs(base, node._key)}>${inner}</div>` : inner);
|
|
415
|
+
i++;
|
|
416
|
+
}
|
|
417
|
+
return out.join("");
|
|
383
418
|
}
|
|
384
419
|
|
|
385
420
|
const html = toHTML(value as never, { components });
|