@bettercms-ai/richtext 0.12.0 → 0.12.1

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.12.0",
3
+ "version": "0.12.1",
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.",
@@ -95,7 +95,8 @@ function spansFrom(
95
95
  if (!isRecord(value)) return [];
96
96
  const node = value as LexNode;
97
97
 
98
- if (node.type === "link") {
98
+ // An `autolink` is Lexical's link for a URL typed as text: the same annotation, so the same mark.
99
+ if (node.type === "link" || node.type === "autolink") {
99
100
  const href = safeHref(node.url ?? "");
100
101
  // An unsafe href does not become an unsafe link and does not silently delete the label —
101
102
  // the text survives, unlinked.
package/src/ingest.ts CHANGED
@@ -795,11 +795,39 @@ const allowsBlockObject = (schema: PtSchema | undefined, name: string): boolean
795
795
  const isElement = (n: unknown): n is Element =>
796
796
  !!n && typeof n === "object" && (n as Node).nodeType === 1;
797
797
 
798
+ const BLOCK_TAGS = new Set([
799
+ "ADDRESS", "ARTICLE", "ASIDE", "BLOCKQUOTE", "DD", "DETAILS", "DIV", "DL", "DT", "FIGCAPTION", "FIGURE", "FOOTER",
800
+ "H1", "H2", "H3", "H4", "H5", "H6", "HEADER", "HR", "LI", "MAIN", "NAV", "OL", "P", "PRE", "SECTION", "TABLE", "UL",
801
+ ]);
802
+
803
+ /**
804
+ * The parser, minus the indentation between block elements. The converter skips that whitespace
805
+ * at the top level but turns it into an empty block inside a wrapper, so `<div><p>a</p>\n<p>b</p></div>`
806
+ * came back as three blocks. Only ASCII whitespace beside a block element goes: a space between two
807
+ * inline elements is a word break, and `&nbsp;` or a `<br>` is an author's blank line.
808
+ */
809
+ const withoutBlockWhitespace =
810
+ (parse: (html: string) => Document) =>
811
+ (html: string): Document => {
812
+ const doc = parse(html);
813
+ const walker = doc.createTreeWalker(doc.body ?? doc, 4 /* NodeFilter.SHOW_TEXT */);
814
+ const blank: Node[] = [];
815
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
816
+ const beside = [n.previousSibling, n.nextSibling].some((s) => !!s && BLOCK_TAGS.has(s.nodeName));
817
+ if (beside && /^[ \t\n\r]*$/.test(n.nodeValue ?? "") && n.parentNode?.nodeName !== "PRE") blank.push(n);
818
+ }
819
+ for (const n of blank) n.parentNode?.removeChild(n);
820
+ return doc;
821
+ };
822
+
798
823
  export function htmlToPortableText(html: string, opts: IngestOptions = {}): PortableText {
799
824
  if (typeof html !== "string" || html.trim() === "") return [];
800
825
  const key = opts.keyGenerator ?? randomKey;
826
+ const parse =
827
+ opts.parseHtml ?? (typeof DOMParser !== "undefined" ? (markup: string) => new DOMParser().parseFromString(markup, "text/html") : undefined);
801
828
  const converted = ptHtmlToPortableText(html, {
802
- ...(opts.parseHtml ? { parseHtml: opts.parseHtml } : {}),
829
+ // The browser's own parser when none is passed (Node must pass one), so both drop the same whitespace.
830
+ ...(parse ? { parseHtml: withoutBlockWhitespace(parse) } : {}),
803
831
  // `ptSchemaFor` returns a schema DEFINITION — the same object `EditorProvider` takes, so
804
832
  // the editor and this filter are literally configured from one value. The deserialiser
805
833
  // wants the COMPILED form, so the compile happens here rather than at every call site.