@bettercms-ai/richtext 0.8.0 → 0.8.2

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.8.0",
3
+ "version": "0.8.2",
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/ingest.ts CHANGED
@@ -187,6 +187,35 @@ function tableRule(key: KeyGenerator, ingestCell: (html: string) => PortableText
187
187
  };
188
188
  }
189
189
 
190
+ /** A code block's text, line breaks included: Lexical writes them as `<br>`, not newlines. */
191
+ const codeText = (node: Node): string =>
192
+ node.nodeName.toLowerCase() === "br"
193
+ ? "\n"
194
+ : node.nodeType === 3
195
+ ? (node.textContent ?? "")
196
+ : [...node.childNodes].map(codeText).join("");
197
+
198
+ /** `language-x` on the `<code>` (Markdown, our renderer) or `data-language` on the `<pre>` (Lexical, Shiki). */
199
+ const codeLanguage = (pre: Element): string | undefined =>
200
+ /(?:^|\s)language-([\w+#-]+)/.exec(pre.querySelector("code")?.getAttribute("class") ?? "")?.[1] ??
201
+ (pre.getAttribute("data-language") || undefined);
202
+
203
+ /**
204
+ * `<pre>` → the `bcmsCode` block the renderer turns back into `<pre><code>`. Without it the
205
+ * deserialiser's default flattens a code block to a paragraph with a `code` mark and its
206
+ * newlines collapsed: every imported code fence became one line of inline code.
207
+ */
208
+ function codeRule(key: KeyGenerator): DeserializerRule {
209
+ return {
210
+ deserialize(el: Node, _next, createBlock) {
211
+ if (!isElement(el) || el.nodeName.toLowerCase() !== "pre") return undefined;
212
+ const language = codeLanguage(el);
213
+ const code = codeText(el).replace(/\n$/, "");
214
+ return createBlock({ _type: PT_TYPES.code, _key: key(), code, ...(language ? { language } : {}) });
215
+ },
216
+ };
217
+ }
218
+
190
219
  /**
191
220
  * An inline element Portable Text does not model → the `styled` annotation.
192
221
  *
@@ -384,9 +413,9 @@ function mediaRule(key: KeyGenerator, schema: PtSchema | undefined): Deserialize
384
413
  const allowsStyled = (schema: PtSchema | undefined): boolean =>
385
414
  !schema || schema.annotations.some((a) => a.name === PT_TYPES.styled);
386
415
 
387
- /** Does this field's schema carry the table block object? Asked by name, not by capability. */
388
- const allowsTables = (schema: PtSchema | undefined): boolean =>
389
- !schema || schema.blockObjects.some((o) => o.name === PT_TYPES.table);
416
+ /** Does this field's schema carry the named block object? Asked by name, not by capability. */
417
+ const allowsBlockObject = (schema: PtSchema | undefined, name: string): boolean =>
418
+ !schema || schema.blockObjects.some((o) => o.name === name);
390
419
 
391
420
  const isElement = (n: unknown): n is Element =>
392
421
  !!n && typeof n === "object" && (n as Node).nodeType === 1;
@@ -413,7 +442,8 @@ export function htmlToPortableText(html: string, opts: IngestOptions = {}): Port
413
442
  * accepts only images.
414
443
  */
415
444
  rules: [
416
- ...(allowsTables(opts.schema) ? [tableRule(key, (cell) => htmlToPortableText(cell, opts))] : []),
445
+ ...(allowsBlockObject(opts.schema, PT_TYPES.table) ? [tableRule(key, (cell) => htmlToPortableText(cell, opts))] : []),
446
+ ...(allowsBlockObject(opts.schema, PT_TYPES.code) ? [codeRule(key)] : []),
417
447
  mediaRule(key, opts.schema),
418
448
  ...(allowsStyled(opts.schema) ? [styledRule(key)] : []),
419
449
  ...(allowsLinks(opts.schema) ? [classedLinkRule(key)] : []),
package/src/styled.ts CHANGED
@@ -36,8 +36,14 @@ export const STYLED_TAGS = [
36
36
  "strong", "b", "em", "i", "u", "s", "mark", "code", "q", "cite", "dfn", "var", "samp", "ins", "del",
37
37
  ] as const;
38
38
 
39
- /** Tags whose bare form IS the design, so they are annotated even with no class. */
40
- export const BARE_IS_DESIGN: ReadonlySet<string> = new Set(["small", "sup", "sub", "kbd", "abbr", "time"]);
39
+ /**
40
+ * Tags whose bare form IS the design, so they are annotated even with no class: every styled tag
41
+ * that is neither a `<span>` nor a tag a decorator already holds. A keycap, a citation or a
42
+ * deleted price dropped to bare words is the page restyled by the author's first save.
43
+ */
44
+ export const BARE_IS_DESIGN: ReadonlySet<string> = new Set([
45
+ "small", "sup", "sub", "kbd", "abbr", "time", "q", "cite", "dfn", "var", "samp", "ins", "del",
46
+ ]);
41
47
 
42
48
  export type StyledTag = (typeof STYLED_TAGS)[number];
43
49