@bettercms-ai/richtext 0.11.0 → 0.12.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.11.0",
3
+ "version": "0.12.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
@@ -1,7 +1,9 @@
1
1
  import { toHTML, type PortableTextHtmlComponents } from "@portabletext/to-html";
2
2
  import {
3
+ BUILTIN_CALLOUT_KEY,
3
4
  BUILTIN_EMBED_KEY,
4
5
  BUILTIN_IMAGE_KEY,
6
+ BUILTIN_TOGGLE_KEY,
5
7
  PT_TYPES,
6
8
  pullQuoteHref,
7
9
  type PortableText,
@@ -307,6 +309,42 @@ function embedHtml(value: unknown, _classes: HtmlClasses): string {
307
309
  return `<iframe src="${esc(src)}"${title ? ` title="${esc(title)}"` : ""}${IFRAME_ATTRS}></iframe>`;
308
310
  }
309
311
 
312
+ /** Plain multi-line text as paragraphs: a blank line starts a new one, a single newline is a `<br>`. */
313
+ const paragraphs = (text: string): string =>
314
+ text.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean)
315
+ .map((p) => `<p>${p.split("\n").map(esc).join("<br>")}</p>`).join("");
316
+
317
+ const CALLOUT_TONES = new Set(["info", "tip", "warning"]);
318
+
319
+ /**
320
+ * A callout: a titled note in its tone. Its payload is its own text, so it renders as content — a marker
321
+ * here was the whole note missing from every delivered page. An unknown tone renders as `info`.
322
+ */
323
+ function calloutHtml(value: unknown): string {
324
+ const f = fieldsOf(value);
325
+ const tone = CALLOUT_TONES.has(str(f.tone)) ? str(f.tone) : "info";
326
+ const title = str(f.title);
327
+ const body = paragraphs(typeof f.message === "string" ? f.message : "");
328
+ if (!title && !body) return "";
329
+ return `<div class="bcms-callout bcms-callout--${tone}" data-rt="callout" data-callout="${tone}" role="note">`
330
+ + (title ? `<p class="bcms-callout__title" data-title>${esc(title)}</p>` : "")
331
+ + `<div class="bcms-callout__body" data-body>${body}</div></div>`;
332
+ }
333
+
334
+ /** A toggle: `<details>`, so it opens and closes on a page with no script. */
335
+ function toggleHtml(value: unknown): string {
336
+ const f = fieldsOf(value);
337
+ const title = str(f.title);
338
+ const body = paragraphs(typeof f.body === "string" ? f.body : "");
339
+ if (!title && !body) return "";
340
+ return `<details class="bcms-toggle" data-rt="toggle"><summary class="bcms-toggle__title">${esc(title)}</summary>`
341
+ + `<div class="bcms-toggle__body" data-body>${body}</div></details>`;
342
+ }
343
+
344
+ /** Built-in blocks this serialiser renders from their own fields (no project lookup): read-time html re-derives them. */
345
+ export const isSelfRenderedBlock = (node: unknown): boolean =>
346
+ isRecord(node) && (node.schemaKey === BUILTIN_CALLOUT_KEY || node.schemaKey === BUILTIN_TOGGLE_KEY);
347
+
310
348
  /**
311
349
  * A video: the file itself where the URL is one, the provider's player where it is a provider's.
312
350
  *
@@ -486,6 +524,8 @@ const buildComponents = (
486
524
  const v = value as { schemaKey?: unknown };
487
525
  if (v.schemaKey === BUILTIN_IMAGE_KEY) return imageHtml(value, classes);
488
526
  if (v.schemaKey === BUILTIN_EMBED_KEY) return embedHtml(value, classes);
527
+ if (v.schemaKey === BUILTIN_CALLOUT_KEY) return calloutHtml(value);
528
+ if (v.schemaKey === BUILTIN_TOGGLE_KEY) return toggleHtml(value);
489
529
  /**
490
530
  * A pre-P1c TABLE is the exception to the marker, and it has to be: it resolves against
491
531
  * nothing. Its rows are text, held right here, so a marker would DELETE visible content.
package/src/types.ts CHANGED
@@ -462,6 +462,9 @@ export const STORABLE_BLOCK_TYPES: ReadonlySet<string> = new Set<string>([
462
462
  */
463
463
  export const BUILTIN_IMAGE_KEY = "builtin:image" as const;
464
464
  export const BUILTIN_EMBED_KEY = "builtin:video" as const;
465
+ /** A callout (`{ tone: info|tip|warning, title, message }`) and a toggle (`{ title, body }`): plain text, read straight. */
466
+ export const BUILTIN_CALLOUT_KEY = "builtin:callout" as const;
467
+ export const BUILTIN_TOGGLE_KEY = "builtin:toggle" as const;
465
468
 
466
469
  /**
467
470
  * The `schemaKey` of a component embedded in rich text. A wire value shared with the dashboard