@bettercms-ai/richtext 0.6.1 → 0.7.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bettercms-ai/richtext",
3
- "version": "0.6.1",
3
+ "version": "0.7.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
@@ -235,6 +235,11 @@ export interface RenderOptions {
235
235
  * sits on each `<li>`, because a wrapper per item would split the list into one-item lists.
236
236
  */
237
237
  bindingBase?: string;
238
+ /**
239
+ * Renders an embedded block this serialiser cannot resolve itself (a component, say). A
240
+ * string replaces the marker; `undefined` keeps it.
241
+ */
242
+ renderBlock?: (value: Record<string, unknown>) => string | undefined;
238
243
  }
239
244
 
240
245
  /**
@@ -269,6 +274,7 @@ const bindingAttrs = (base: string, key: unknown): string =>
269
274
  const buildComponents = (
270
275
  classes: HtmlClasses = {},
271
276
  binding?: { base: string; keys: ReadonlySet<unknown> },
277
+ renderBlock?: RenderOptions["renderBlock"],
272
278
  ): Partial<PortableTextHtmlComponents> => ({
273
279
  block: tagOverrides(classes, STYLE_TAGS),
274
280
  list: tagOverrides(classes, { bullet: "ul", number: "ol", check: "ul" }),
@@ -315,6 +321,8 @@ const buildComponents = (
315
321
  */
316
322
  const legacy = builtInTableToTable(value);
317
323
  if (legacy) return tableHtml(legacy);
324
+ const rendered = renderBlock?.(value as Record<string, unknown>);
325
+ if (rendered !== undefined) return rendered;
318
326
  const key = typeof v.schemaKey === "string" && TOKEN.test(v.schemaKey.replace(/^builtin:/, ""))
319
327
  ? v.schemaKey
320
328
  : "block";
@@ -392,11 +400,12 @@ export function portableTextToHtml(
392
400
  opts: RenderOptions = {},
393
401
  ): string {
394
402
  if (!Array.isArray(value) || value.length === 0) return "";
395
- const components = opts.classes ? buildComponents(opts.classes) : defaultComponents;
403
+ const components =
404
+ opts.classes || opts.renderBlock ? buildComponents(opts.classes, undefined, opts.renderBlock) : defaultComponents;
396
405
 
397
406
  if (opts.bindingBase) {
398
407
  const base = opts.bindingBase;
399
- const bound = buildComponents(opts.classes, { base, keys: new Set(value.map((n) => n._key)) });
408
+ const bound = buildComponents(opts.classes, { base, keys: new Set(value.map((n) => n._key)) }, opts.renderBlock);
400
409
  const out: string[] = [];
401
410
  for (let i = 0; i < value.length; ) {
402
411
  const node = value[i];
package/src/read.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PORTABLE_TEXT_FORMAT, PT_TYPES, type PortableText } from "./types";
1
+ import { EMBEDDED_COMPONENT_KEY, PORTABLE_TEXT_FORMAT, PT_TYPES, type PortableText } from "./types";
2
2
  import { lexicalToPortableText } from "./from-lexical";
3
3
  import { migrateTable } from "./table";
4
4
  import { randomKey, type KeyGenerator } from "./key";
@@ -231,3 +231,26 @@ export function readStructuredContent(props: unknown): PortableText | null {
231
231
  : null;
232
232
  return arr && arr.length > 0 ? (arr as PortableText) : null;
233
233
  }
234
+
235
+ /** The `component` block an embed in rich text stands for. */
236
+ export interface EmbeddedComponentBlock {
237
+ type: "component";
238
+ /** The node's `_key` — unique within its rich-text field only. */
239
+ id: string;
240
+ props: { componentId: string; overrides: Record<string, unknown> };
241
+ }
242
+
243
+ /**
244
+ * A component embedded in rich text, as the ordinary `component` block every renderer already
245
+ * draws — so an embed gets the same resolver, cycle guard and identity attributes as a placed
246
+ * instance. Null for any other node.
247
+ */
248
+ export function embeddedComponentBlock(value: unknown): EmbeddedComponentBlock | null {
249
+ if (!isRecord(value) || value.schemaKey !== EMBEDDED_COMPONENT_KEY || !isRecord(value.fields)) return null;
250
+ const { componentId, overrides } = value.fields;
251
+ return {
252
+ type: "component",
253
+ id: typeof value._key === "string" ? value._key : "",
254
+ props: { componentId: String(componentId ?? ""), overrides: isRecord(overrides) ? overrides : {} },
255
+ };
256
+ }
package/src/types.ts CHANGED
@@ -276,6 +276,12 @@ export const STORABLE_BLOCK_TYPES: ReadonlySet<string> = new Set<string>([
276
276
  export const BUILTIN_IMAGE_KEY = "builtin:image" as const;
277
277
  export const BUILTIN_EMBED_KEY = "builtin:video" as const;
278
278
 
279
+ /**
280
+ * The `schemaKey` of a component embedded in rich text. A wire value shared with the dashboard
281
+ * (`COMPONENT_SCHEMA_KEY`) and the backend (`RICHTEXT_COMPONENT_KEY`).
282
+ */
283
+ export const EMBEDDED_COMPONENT_KEY = "component" as const;
284
+
279
285
  /**
280
286
  * Top-level nodes whose `_type` this platform cannot store or render, in document order.
281
287
  *