@broberg/cms-inline-edit 0.4.20 → 0.5.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/dist/index.js CHANGED
@@ -8,6 +8,36 @@ function applyFieldSlice(current, original, next) {
8
8
  return current.slice(0, first) + next + current.slice(first + original.length);
9
9
  }
10
10
 
11
+ // src/token-safe.ts
12
+ var TEXT_NODE = 3;
13
+ var ELEMENT_NODE = 1;
14
+ function hasTokenChips(el) {
15
+ return el.querySelector("[data-cms-token]") !== null;
16
+ }
17
+ function serializeTokenSafe(el) {
18
+ let out = "";
19
+ el.childNodes.forEach((node) => {
20
+ if (node.nodeType === TEXT_NODE) {
21
+ out += node.textContent ?? "";
22
+ return;
23
+ }
24
+ if (node.nodeType !== ELEMENT_NODE) return;
25
+ const element = node;
26
+ const token = element.getAttribute("data-cms-token");
27
+ if (token) {
28
+ out += token;
29
+ } else {
30
+ out += serializeTokenSafe(element);
31
+ }
32
+ });
33
+ return out;
34
+ }
35
+ function lockTokenChips(el) {
36
+ el.querySelectorAll("[data-cms-token]").forEach((chip) => {
37
+ chip.setAttribute("contenteditable", "false");
38
+ });
39
+ }
40
+
11
41
  // src/index.ts
12
42
  var DEFAULT_LABELS = {
13
43
  bold: "Fed",
@@ -20,7 +50,27 @@ var DEFAULT_LABELS = {
20
50
  done: "F\xE6rdig",
21
51
  saving: "Gemmer\u2026",
22
52
  saved: "Gemt \u2713",
23
- error: "Fejl \u2014 pr\xF8v igen"
53
+ error: "Fejl \u2014 pr\xF8v igen",
54
+ link: "Link",
55
+ linkTabPage: "Side p\xE5 sitet",
56
+ linkTabUrl: "Fri adresse",
57
+ linkSearch: "S\xF8g efter en side\u2026",
58
+ linkText: "Linktekst",
59
+ linkTextAuto: "F\xF8lger sidens titel",
60
+ linkTextAutoHint: "<b>Tom = f\xF8lger sidens titel.</b> Omd\xF8bes siden, retter teksten sig selv. Skriver du din egen tekst, bliver den st\xE5ende.",
61
+ linkTextOwnHint: "Linket viser <b>din egen tekst</b>. Den bliver st\xE5ende, ogs\xE5 hvis siden omd\xF8bes.",
62
+ linkLiveHint: "Linket peger p\xE5 <b>siden</b> \u2014 ikke p\xE5 en adresse. Flyttes siden, eller f\xE5r den et nyt navn, retter linket sig selv.",
63
+ linkUrlHint: "En fri adresse peger pr\xE6cis d\xE9r, du skriver. Den f\xF8lger <b>ikke</b> med, hvis m\xE5let flytter sig.",
64
+ linkUrl: "Adresse",
65
+ linkInsert: "Inds\xE6t link",
66
+ linkSave: "Gem \xE6ndring",
67
+ linkCancel: "Annull\xE9r",
68
+ linkRemove: "Fjern link",
69
+ linkRemoveConfirm: "Fjern?",
70
+ linkYes: "Ja",
71
+ linkNo: "Nej",
72
+ linkEmpty: "Ingen sider fundet",
73
+ linkLoading: "Henter sider\u2026"
24
74
  };
25
75
  var uiLabels = DEFAULT_LABELS;
26
76
  function resolveOptions(options) {
@@ -122,6 +172,7 @@ function isExpired(token) {
122
172
  var SQUARE_PEN_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></svg>';
123
173
  var UL_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>';
124
174
  var OL_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="10" x2="21" y1="6" y2="6"/><line x1="10" x2="21" y1="12" y2="12"/><line x1="10" x2="21" y1="18" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>';
175
+ var LINK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7"/><path d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/></svg>';
125
176
  function makeIcon() {
126
177
  const icon = document.createElement("span");
127
178
  icon.style.cssText = "display:inline-flex;line-height:0;flex:0 0 auto";
@@ -228,14 +279,16 @@ function wireField(el, token, options) {
228
279
  if (el.getAttribute("contenteditable") === "true") return;
229
280
  e.preventDefault();
230
281
  e.stopPropagation();
231
- el.dataset.cmsOriginalValue = el.textContent ?? "";
282
+ const tokenSafe = hasTokenChips(el);
283
+ el.dataset.cmsOriginalValue = tokenSafe ? serializeTokenSafe(el) : el.textContent ?? "";
232
284
  el.setAttribute("contenteditable", "true");
285
+ if (tokenSafe) lockTokenChips(el);
233
286
  el.focus();
234
287
  });
235
288
  el.addEventListener("blur", () => {
236
289
  el.removeAttribute("contenteditable");
237
290
  const original = el.dataset.cmsOriginalValue ?? "";
238
- const current = el.textContent ?? "";
291
+ const current = hasTokenChips(el) ? serializeTokenSafe(el) : el.textContent ?? "";
239
292
  if (current.trim() === original.trim()) return;
240
293
  void saveField(el, current.trim(), token, options);
241
294
  });
@@ -278,6 +331,7 @@ function deactivateRich() {
278
331
  el.removeAttribute("contenteditable");
279
332
  el.classList.remove("cms-rich-editing");
280
333
  hideRichToolbar();
334
+ hideLinkDialog();
281
335
  if (el.innerHTML !== originalHtml) {
282
336
  const value = mode === "html" ? el.innerHTML : htmlToMarkdown(el.innerHTML);
283
337
  void saveField(el, value, token, options);
@@ -323,6 +377,10 @@ function buildRichToolbar() {
323
377
  t.appendChild(toolbarButton(UL_SVG, uiLabels.unorderedList, () => document.execCommand("insertUnorderedList")));
324
378
  t.appendChild(toolbarButton(OL_SVG, uiLabels.orderedList, () => document.execCommand("insertOrderedList")));
325
379
  t.appendChild(sep());
380
+ const linkBtn = toolbarButton(LINK_SVG, uiLabels.link, () => toggleLinkDialog(linkBtn));
381
+ linkBtn.setAttribute("data-testid", "inline-toolbar-link");
382
+ t.appendChild(linkBtn);
383
+ t.appendChild(sep());
326
384
  const clrLabel = document.createElement("label");
327
385
  clrLabel.style.cssText = "display:flex;align-items:center;gap:5px;color:#9aa4b2;font-size:12px;cursor:pointer;";
328
386
  clrLabel.textContent = uiLabels.color;
@@ -356,6 +414,231 @@ var EMOJIS = "\u{1F600} \u{1F603} \u{1F604} \u{1F601} \u{1F606} \u{1F609} \u{1F6
356
414
  );
357
415
  var emojiPicker = null;
358
416
  var savedRange = null;
417
+ var linkDialog = null;
418
+ var linkPages = null;
419
+ var linkPicked = null;
420
+ var linkEditing = null;
421
+ function hideLinkDialog() {
422
+ if (linkDialog) linkDialog.style.display = "none";
423
+ linkPicked = null;
424
+ linkEditing = null;
425
+ }
426
+ function anchorAtCaret() {
427
+ const sel = window.getSelection();
428
+ if (!sel || sel.rangeCount === 0) return null;
429
+ let node = sel.getRangeAt(0).startContainer;
430
+ while (node && node !== document.body) {
431
+ if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "A") {
432
+ return node;
433
+ }
434
+ node = node.parentNode;
435
+ }
436
+ return null;
437
+ }
438
+ async function fetchLinkablePages() {
439
+ if (linkPages) return linkPages;
440
+ if (!richCtx) return [];
441
+ const { options, token } = richCtx;
442
+ const url = `${options.cmsBaseUrl}/api/inline-edit/pages?site=${encodeURIComponent(options.siteId)}`;
443
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
444
+ if (!res.ok) throw new Error(`pages ${res.status}`);
445
+ const body = await res.json();
446
+ linkPages = body.pages ?? [];
447
+ return linkPages;
448
+ }
449
+ function toggleLinkDialog(anchor) {
450
+ if (linkDialog && linkDialog.style.display === "block") {
451
+ hideLinkDialog();
452
+ return;
453
+ }
454
+ const sel = window.getSelection();
455
+ if (sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();
456
+ linkEditing = anchorAtCaret();
457
+ if (!linkDialog) linkDialog = buildLinkDialog();
458
+ renderLinkDialog();
459
+ const rect = anchor.getBoundingClientRect();
460
+ linkDialog.style.top = `${rect.bottom + 8}px`;
461
+ linkDialog.style.left = `${Math.min(Math.max(8, rect.left - 180), window.innerWidth - 400)}px`;
462
+ linkDialog.style.display = "block";
463
+ }
464
+ function buildLinkDialog() {
465
+ const d = document.createElement("div");
466
+ d.setAttribute("data-cms-inline-edit-toolbar", "");
467
+ d.setAttribute("data-testid", "inline-link-dialog");
468
+ d.style.cssText = "position:fixed;z-index:2147483646;background:#1c2027;border:1px solid #3a3f4a;border-radius:10px;width:392px;display:none;box-shadow:0 8px 32px rgba(0,0,0,.5);font-family:system-ui,sans-serif;color:#fff;overflow:hidden;";
469
+ d.addEventListener("mousedown", (e) => {
470
+ const t = e.target;
471
+ if (t.tagName !== "INPUT") e.preventDefault();
472
+ e.stopPropagation();
473
+ });
474
+ document.body.appendChild(d);
475
+ return d;
476
+ }
477
+ function renderLinkDialog() {
478
+ const d = linkDialog;
479
+ if (!d) return;
480
+ const L = uiLabels;
481
+ const editing = !!linkEditing;
482
+ const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
483
+ const onPageTab = !editing || !!existingRef;
484
+ d.innerHTML = `<div style="display:flex;gap:4px;padding:8px 8px 0"><button type="button" data-testid="inline-link-tab-page" data-tab="page" style="${tabCss(onPageTab)}">${L.linkTabPage}</button><button type="button" data-testid="inline-link-tab-url" data-tab="url" style="${tabCss(!onPageTab)}">${L.linkTabUrl}</button></div><div style="padding:10px 12px 12px"><div data-pane="page" style="display:${onPageTab ? "block" : "none"}"><input data-testid="inline-link-search" data-role="search" placeholder="${L.linkSearch}" style="${fieldCss()}"><div data-role="list" data-testid="inline-link-list" style="margin-top:8px;max-height:196px;overflow-y:auto;border:1px solid #3a3f4a;border-radius:8px;background:#141821"></div></div><div data-pane="url" style="display:${onPageTab ? "none" : "block"}"><label style="${labelCss()}">${L.linkUrl}</label><input data-testid="inline-link-url" data-role="url" placeholder="https://\u2026" style="${fieldCss()}"></div><label style="${labelCss()}">${L.linkText}</label><input data-testid="inline-link-text" data-role="text" placeholder="${L.linkTextAuto}" style="${fieldCss()}"><p data-role="hint" style="font-size:11.5px;color:#9aa3b2;margin:6px 0 0;line-height:1.5"></p><div data-role="live" style="display:flex;gap:8px;margin-top:12px;padding:9px 10px;background:rgba(0,178,255,.08);border:1px solid rgba(0,178,255,.28);border-radius:8px"><p style="margin:0;font-size:11.5px;color:#c9d1dd;line-height:1.55">${L.linkLiveHint}</p></div><div style="display:flex;gap:8px;align-items:center;margin-top:14px"><div data-role="remove"></div><div style="flex:1"></div><button type="button" data-testid="inline-link-cancel" data-role="cancel" style="${btnCss(false)}">${L.linkCancel}</button><button type="button" data-testid="inline-link-submit" data-role="submit" style="${btnCss(true)}" disabled>${editing ? L.linkSave : L.linkInsert}</button></div></div>`;
485
+ const q = (role) => d.querySelector(`[data-role="${role}"]`);
486
+ const text = q("text");
487
+ const urlIn = q("url");
488
+ const search = q("search");
489
+ if (editing) {
490
+ text.value = linkEditing?.getAttribute("data-cms-ref-label") === "auto" ? "" : linkEditing?.textContent ?? "";
491
+ if (!existingRef) urlIn.value = linkEditing?.getAttribute("href") ?? "";
492
+ q("remove").appendChild(buildRemoveLink());
493
+ }
494
+ d.querySelectorAll("[data-tab]").forEach((btn) => {
495
+ btn.onclick = () => {
496
+ const page = btn.dataset.tab === "page";
497
+ d.querySelectorAll("[data-tab]").forEach((b) => {
498
+ b.setAttribute("style", tabCss(b.dataset.tab === "page" ? page : !page));
499
+ });
500
+ d.querySelector('[data-pane="page"]').style.display = page ? "block" : "none";
501
+ d.querySelector('[data-pane="url"]').style.display = page ? "none" : "block";
502
+ q("live").style.display = page ? "flex" : "none";
503
+ syncLinkDialog();
504
+ };
505
+ });
506
+ search.oninput = () => renderLinkList(search.value);
507
+ text.oninput = syncLinkDialog;
508
+ urlIn.oninput = syncLinkDialog;
509
+ q("cancel").onclick = hideLinkDialog;
510
+ q("submit").onclick = applyLink;
511
+ renderLinkList("");
512
+ syncLinkDialog();
513
+ }
514
+ function buildRemoveLink() {
515
+ const wrap = document.createElement("div");
516
+ const btn = document.createElement("button");
517
+ btn.type = "button";
518
+ btn.setAttribute("data-testid", "inline-link-remove");
519
+ btn.textContent = uiLabels.linkRemove;
520
+ btn.style.cssText = "background:none;border:none;color:#ff8a8a;font-size:12.5px;cursor:pointer;padding:0 2px;";
521
+ btn.onclick = () => {
522
+ wrap.innerHTML = `<span style="font-size:11.5px;color:#ff8a8a;font-weight:500;padding:0 2px">${uiLabels.linkRemoveConfirm}</span><button type="button" data-testid="inline-link-remove-yes" style="font-size:11px;padding:2px 8px;border-radius:4px;border:none;background:#c0392b;color:#fff;cursor:pointer;line-height:1.4;margin-left:6px">${uiLabels.linkYes}</button><button type="button" data-testid="inline-link-remove-no" style="font-size:11px;padding:2px 8px;border-radius:4px;border:1px solid #3a3f4a;background:none;color:#fff;cursor:pointer;line-height:1.4;margin-left:6px">${uiLabels.linkNo}</button>`;
523
+ wrap.querySelector('[data-testid="inline-link-remove-no"]').onclick = () => {
524
+ wrap.innerHTML = "";
525
+ wrap.appendChild(btn);
526
+ };
527
+ wrap.querySelector('[data-testid="inline-link-remove-yes"]').onclick = () => {
528
+ if (linkEditing) {
529
+ const parent = linkEditing.parentNode;
530
+ while (linkEditing.firstChild) parent?.insertBefore(linkEditing.firstChild, linkEditing);
531
+ parent?.removeChild(linkEditing);
532
+ }
533
+ hideLinkDialog();
534
+ };
535
+ };
536
+ wrap.appendChild(btn);
537
+ return wrap;
538
+ }
539
+ function renderLinkList(filter) {
540
+ const d = linkDialog;
541
+ if (!d) return;
542
+ const list = d.querySelector('[data-role="list"]');
543
+ if (!list) return;
544
+ const paint = (pages) => {
545
+ const f = filter.trim().toLowerCase();
546
+ const shown = pages.filter((p) => !f || `${p.title} ${p.path}`.toLowerCase().includes(f));
547
+ if (!shown.length) {
548
+ list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#9aa3b2">${uiLabels.linkEmpty}</div>`;
549
+ return;
550
+ }
551
+ list.innerHTML = "";
552
+ shown.forEach((p) => {
553
+ const on = linkPicked?.collection === p.collection && linkPicked?.slug === p.slug;
554
+ const row = document.createElement("div");
555
+ row.setAttribute("data-testid", `inline-link-page-${p.collection}-${p.slug}`);
556
+ row.style.cssText = "display:flex;align-items:center;gap:10px;padding:8px 10px;cursor:pointer;border-bottom:1px solid #232833;" + (on ? "background:rgba(0,178,255,.14);" : "");
557
+ row.innerHTML = `<div style="min-width:0;flex:1"><div style="font-size:13.5px;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escapeHtml(p.title)}</div><div style="font-size:11.5px;color:#9aa3b2;font-family:ui-monospace,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escapeHtml(p.path)}</div></div><span style="flex:none;font-size:10px;text-transform:uppercase;color:#9aa3b2;border:1px solid #3a3f4a;border-radius:4px;padding:2px 6px;white-space:nowrap">${escapeHtml(p.label)}</span>`;
558
+ row.onclick = () => {
559
+ linkPicked = p;
560
+ renderLinkList(filter);
561
+ syncLinkDialog();
562
+ };
563
+ list.appendChild(row);
564
+ });
565
+ };
566
+ if (linkPages) {
567
+ paint(linkPages);
568
+ return;
569
+ }
570
+ list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#9aa3b2">${uiLabels.linkLoading}</div>`;
571
+ fetchLinkablePages().then((pages) => {
572
+ const ref = linkEditing?.getAttribute("data-cms-ref");
573
+ if (ref && !linkPicked) {
574
+ const [c, ...rest] = ref.split(":");
575
+ linkPicked = pages.find((p) => p.collection === c && p.slug === rest.join(":")) ?? null;
576
+ }
577
+ paint(pages);
578
+ syncLinkDialog();
579
+ }).catch(() => {
580
+ list.innerHTML = `<div style="padding:10px;font-size:12.5px;color:#ff8a8a">${uiLabels.error}</div>`;
581
+ });
582
+ }
583
+ function syncLinkDialog() {
584
+ const d = linkDialog;
585
+ if (!d) return;
586
+ const q = (role) => d.querySelector(`[data-role="${role}"]`);
587
+ const onPage = d.querySelector('[data-pane="page"]').style.display !== "none";
588
+ const own = q("text").value.trim();
589
+ q("hint").innerHTML = own ? uiLabels.linkTextOwnHint : onPage ? uiLabels.linkTextAutoHint : uiLabels.linkUrlHint;
590
+ q("live").style.display = onPage ? "flex" : "none";
591
+ q("submit").disabled = onPage ? !linkPicked : !q("url").value.trim();
592
+ }
593
+ function applyLink() {
594
+ const d = linkDialog;
595
+ if (!d || !richCtx) return;
596
+ const q = (role) => d.querySelector(`[data-role="${role}"]`);
597
+ const onPage = d.querySelector('[data-pane="page"]').style.display !== "none";
598
+ const own = q("text").value.trim();
599
+ const href = onPage ? linkPicked?.path ?? "" : q("url").value.trim();
600
+ if (!href) return;
601
+ const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : "";
602
+ const label = own || (onPage ? linkPicked?.title ?? href : href);
603
+ if (linkEditing) {
604
+ linkEditing.setAttribute("href", href);
605
+ if (ref) linkEditing.setAttribute("data-cms-ref", ref);
606
+ else linkEditing.removeAttribute("data-cms-ref");
607
+ if (ref && !own) linkEditing.setAttribute("data-cms-ref-label", "auto");
608
+ else linkEditing.removeAttribute("data-cms-ref-label");
609
+ linkEditing.textContent = label;
610
+ hideLinkDialog();
611
+ return;
612
+ }
613
+ const a = document.createElement("a");
614
+ a.setAttribute("href", href);
615
+ if (ref) {
616
+ a.setAttribute("data-cms-ref", ref);
617
+ if (!own) a.setAttribute("data-cms-ref-label", "auto");
618
+ }
619
+ const sel = window.getSelection();
620
+ if (savedRange && sel) {
621
+ sel.removeAllRanges();
622
+ sel.addRange(savedRange);
623
+ const range = sel.getRangeAt(0);
624
+ a.textContent = own || range.toString() || label;
625
+ range.deleteContents();
626
+ range.insertNode(a);
627
+ sel.removeAllRanges();
628
+ } else {
629
+ a.textContent = label;
630
+ richCtx.el.appendChild(a);
631
+ }
632
+ savedRange = null;
633
+ hideLinkDialog();
634
+ }
635
+ function escapeHtml(v) {
636
+ return v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
637
+ }
638
+ var tabCss = (on) => "flex:1;background:" + (on ? "#2a2f38" : "none") + ";border:1px solid " + (on ? "#3a3f4a" : "transparent") + ";color:" + (on ? "#fff" : "#9aa3b2") + ";height:32px;border-radius:7px;font-size:13px;cursor:pointer;font-weight:500;font-family:inherit;";
639
+ var fieldCss = () => "width:100%;background:#141821;border:1px solid #3a3f4a;color:#fff;height:34px;border-radius:7px;padding:0 10px;font-size:13.5px;outline:none;font-family:inherit;box-sizing:border-box;";
640
+ var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
641
+ var btnCss = (primary) => "height:33px;border-radius:7px;font-size:13px;cursor:pointer;padding:0 14px;font-weight:600;font-family:inherit;" + (primary ? "background:#00b2ff;color:#04121b;border:none;" : "background:none;border:1px solid #3a3f4a;color:#fff;font-weight:500;");
359
642
  function toggleEmojiPicker(anchor) {
360
643
  if (!emojiPicker) emojiPicker = buildEmojiPicker();
361
644
  if (emojiPicker.style.display === "block") {
@@ -439,14 +722,70 @@ function setDeepField(data, path, value) {
439
722
  function htmlToMarkdown(html) {
440
723
  const container = document.createElement("div");
441
724
  container.innerHTML = html;
725
+ reattachOrphanListItems(container);
442
726
  return serializeBlockChildren(container).replace(/\n{3,}/g, "\n\n").trim() + "\n";
443
727
  }
728
+ function isList(el) {
729
+ if (!el) return false;
730
+ const tag = el.tagName.toLowerCase();
731
+ return tag === "ul" || tag === "ol";
732
+ }
733
+ function reattachOrphanListItems(container) {
734
+ const orphans = Array.from(container.children).filter(
735
+ (c) => c.tagName.toLowerCase() === "li"
736
+ );
737
+ for (const li of orphans) {
738
+ const prev = li.previousElementSibling;
739
+ const next = li.nextElementSibling;
740
+ if (isList(prev)) {
741
+ prev.appendChild(li);
742
+ } else if (isList(next)) {
743
+ next.insertBefore(li, next.firstChild);
744
+ } else {
745
+ const list = container.ownerDocument.createElement("ul");
746
+ li.replaceWith(list);
747
+ list.appendChild(li);
748
+ }
749
+ }
750
+ }
751
+ var BLOCK_TAGS = /* @__PURE__ */ new Set([
752
+ "h1",
753
+ "h2",
754
+ "h3",
755
+ "h4",
756
+ "h5",
757
+ "h6",
758
+ "p",
759
+ "div",
760
+ "ul",
761
+ "ol",
762
+ "li",
763
+ "blockquote",
764
+ "pre",
765
+ "hr",
766
+ "table"
767
+ ]);
768
+ function isBlockNode(node) {
769
+ return node.nodeType === Node.ELEMENT_NODE && BLOCK_TAGS.has(node.tagName.toLowerCase());
770
+ }
444
771
  function serializeBlockChildren(parent) {
445
772
  const blocks = [];
446
- parent.childNodes.forEach((node) => {
447
- const s = serializeBlock(node).trim();
773
+ let inlineRun = "";
774
+ const flushInline = () => {
775
+ const s = inlineRun.trim();
448
776
  if (s) blocks.push(s);
777
+ inlineRun = "";
778
+ };
779
+ parent.childNodes.forEach((node) => {
780
+ if (isBlockNode(node)) {
781
+ flushInline();
782
+ const s = serializeBlock(node).trim();
783
+ if (s) blocks.push(s);
784
+ } else {
785
+ inlineRun += serializeInlineNode(node);
786
+ }
449
787
  });
788
+ flushInline();
450
789
  return blocks.join("\n\n");
451
790
  }
452
791
  function serializeBlock(node) {
@@ -474,6 +813,11 @@ function serializeBlock(node) {
474
813
  return serializeList(el, false);
475
814
  case "ol":
476
815
  return serializeList(el, true);
816
+ // Backstop for an orphaned <li> that reattachOrphanListItems could not
817
+ // place (e.g. nested inside another block). Without this it falls through
818
+ // to the inline default and loses its marker — silent content loss.
819
+ case "li":
820
+ return "- " + serializeInline(el).trim();
477
821
  case "blockquote":
478
822
  return serializeBlockChildren(el).split("\n").map((l) => l ? "> " + l : ">").join("\n");
479
823
  case "pre":
@@ -515,14 +859,23 @@ function serializeList(listEl, ordered) {
515
859
  });
516
860
  return items.join("\n");
517
861
  }
862
+ function escapeAttr(value) {
863
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
864
+ }
518
865
  function serializeInline(node) {
519
866
  let out = "";
520
867
  node.childNodes.forEach((child) => {
521
- if (child.nodeType === Node.TEXT_NODE) {
522
- out += (child.textContent ?? "").replace(/\s+/g, " ");
523
- return;
524
- }
525
- if (child.nodeType !== Node.ELEMENT_NODE) return;
868
+ out += serializeInlineNode(child);
869
+ });
870
+ return out;
871
+ }
872
+ function serializeInlineNode(child) {
873
+ if (child.nodeType === Node.TEXT_NODE) {
874
+ return (child.textContent ?? "").replace(/\s+/g, " ");
875
+ }
876
+ if (child.nodeType !== Node.ELEMENT_NODE) return "";
877
+ {
878
+ let out = "";
526
879
  const el = child;
527
880
  const tag = el.tagName.toLowerCase();
528
881
  const inner = serializeInline(el);
@@ -543,7 +896,15 @@ function serializeInline(node) {
543
896
  break;
544
897
  case "a": {
545
898
  const href = el.getAttribute("href") || "";
546
- out += href ? `[${inner}](${href})` : inner;
899
+ const ref = el.getAttribute("data-cms-ref");
900
+ if (ref) {
901
+ const attrs = [`href="${escapeAttr(href)}"`, `data-cms-ref="${escapeAttr(ref)}"`];
902
+ const label = el.getAttribute("data-cms-ref-label");
903
+ if (label) attrs.push(`data-cms-ref-label="${escapeAttr(label)}"`);
904
+ out += `<a ${attrs.join(" ")}>${inner}</a>`;
905
+ } else {
906
+ out += href ? `[${inner}](${href})` : inner;
907
+ }
547
908
  break;
548
909
  }
549
910
  case "img": {
@@ -564,8 +925,8 @@ function serializeInline(node) {
564
925
  default:
565
926
  out += inner;
566
927
  }
567
- });
568
- return out;
928
+ return out;
929
+ }
569
930
  }
570
931
  async function saveField(el, value, token, options) {
571
932
  const collection = el.dataset.cmsCollection;
@@ -672,6 +1033,7 @@ export {
672
1033
  buildConnectUrl,
673
1034
  disconnect,
674
1035
  getConnectedToken,
1036
+ htmlToMarkdown,
675
1037
  initInlineEdit,
676
1038
  rescanFields
677
1039
  };