@broberg/cms-inline-edit 0.6.2 → 0.6.3

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.cjs CHANGED
@@ -23,14 +23,20 @@ __export(index_exports, {
23
23
  applyFieldSlice: () => applyFieldSlice,
24
24
  buildConnectUrl: () => buildConnectUrl,
25
25
  disconnect: () => disconnect,
26
+ extractLinkTargets: () => extractLinkTargets,
26
27
  getConnectedToken: () => getConnectedToken,
27
28
  htmlToMarkdown: () => htmlToMarkdown,
28
29
  initInlineEdit: () => initInlineEdit,
30
+ isBareEmail: () => isBareEmail,
31
+ isDangerousUrl: () => isDangerousUrl,
32
+ isExternalHost: () => isExternalHost,
33
+ isSchemeless: () => isSchemeless,
29
34
  positionPopover: () => positionPopover,
30
35
  renderCurrentRefLine: () => renderCurrentRefLine,
31
36
  rescanFields: () => rescanFields,
32
37
  resolveExistingRef: () => resolveExistingRef,
33
- setLinkTarget: () => setLinkTarget
38
+ setLinkTarget: () => setLinkTarget,
39
+ withHttps: () => withHttps
34
40
  });
35
41
  module.exports = __toCommonJS(index_exports);
36
42
 
@@ -48,6 +54,7 @@ function applyFieldSlice(current, original, next) {
48
54
  function isSchemeless(raw) {
49
55
  const v = (raw || "").trim();
50
56
  if (!v) return false;
57
+ if (isBareEmail(v)) return false;
51
58
  if (/^[a-z][a-z0-9+.-]*:/i.test(v)) return false;
52
59
  if (v.startsWith("//")) return false;
53
60
  if (v.startsWith("/")) return false;
@@ -55,6 +62,9 @@ function isSchemeless(raw) {
55
62
  if (v.startsWith("?")) return false;
56
63
  return true;
57
64
  }
65
+ function isBareEmail(raw) {
66
+ return /^[^\s@/:?#]+@[^\s@/:?#]+\.[a-z]{2,}$/i.test((raw || "").trim());
67
+ }
58
68
  function withHttps(raw) {
59
69
  return `https://${(raw || "").trim().replace(/^\/+/, "")}`;
60
70
  }
@@ -67,6 +77,18 @@ function isExternalHost(raw, currentHost) {
67
77
  return false;
68
78
  }
69
79
  }
80
+ function extractLinkTargets(text) {
81
+ const out = [];
82
+ if (typeof text !== "string") return out;
83
+ for (const m of text.matchAll(/<a\b[^>]*?\bhref\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>]+))/gi)) {
84
+ out.push({ syntax: "html", value: m[2] ?? m[3] ?? m[4] ?? "" });
85
+ }
86
+ for (const m of text.matchAll(/(!)?\[(?:[^\]\[]|\[[^\]]*\])*\]\(([^)\s]+)/g)) {
87
+ if (m[1]) continue;
88
+ out.push({ syntax: "markdown", value: m[2] ?? "" });
89
+ }
90
+ return out;
91
+ }
70
92
  function isDangerousUrl(raw) {
71
93
  const v = (raw || "").replace(/[\u0000-\u0020]/g, "");
72
94
  return /^(?:javascript|data|vbscript):/i.test(v);
@@ -599,7 +621,7 @@ function renderLinkDialog() {
599
621
  }
600
622
  const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
601
623
  const onPageTab = !editing || !!existingRef;
602
- 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="current" data-testid="inline-link-current" style="display:none;margin-top:8px;padding:7px 10px;background:rgba(0,178,255,.08);border:1px solid rgba(0,178,255,.28);border-radius:8px;font-size:11.5px;color:#c9d1dd;line-height:1.45"></div><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 data-role="schemeless" data-testid="inline-link-schemeless" style="${noticeCss("schemeless")}"><p data-role="schemeless-text" style="margin:0;font-size:11.5px;line-height:1.5"></p><button type="button" data-testid="inline-link-add-scheme" data-role="add-scheme" style="${btnCss(false)};margin-top:7px;height:26px;font-size:12px">${L.linkSchemelessFix}</button></div></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><button type="button" data-role="newtab" data-testid="inline-link-newtab" aria-pressed="false" style="display:flex;align-items:center;gap:8px;margin-top:12px;background:none;border:none;padding:0;cursor:pointer;font-family:inherit;font-size:12.5px;color:#c9d1dd"><span data-role="newtab-box" style="${checkboxCss(false)}"></span><span>${L.linkNewTab}</span></button><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>`;
624
+ 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="current" data-testid="inline-link-current" style="display:none;margin-top:8px;padding:7px 10px;background:rgba(0,178,255,.08);border:1px solid rgba(0,178,255,.28);border-radius:8px;font-size:11.5px;color:#c9d1dd;line-height:1.45"></div><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 data-role="schemeless" style="${noticeCss("schemeless")}"><p data-role="schemeless-text" style="margin:0;font-size:11.5px;line-height:1.5"></p><button type="button" data-testid="inline-link-add-scheme" data-role="add-scheme" style="${btnCss(false)};margin-top:7px;height:26px;font-size:12px">${L.linkSchemelessFix}</button></div></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><button type="button" data-role="newtab" data-testid="inline-link-newtab" aria-pressed="false" style="display:flex;align-items:center;gap:8px;margin-top:12px;background:none;border:none;padding:0;cursor:pointer;font-family:inherit;font-size:12.5px;color:#c9d1dd"><span data-role="newtab-box" style="${checkboxCss(false)}"></span><span>${L.linkNewTab}</span></button><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, true)}" disabled>${editing ? L.linkSave : L.linkInsert}</button></div></div>`;
603
625
  const q = (role) => d.querySelector(`[data-role="${role}"]`);
604
626
  const text = q("text");
605
627
  const urlIn = q("url");
@@ -607,7 +629,7 @@ function renderLinkDialog() {
607
629
  if (editing) {
608
630
  text.value = linkEditing?.getAttribute("data-cms-ref-label") === "auto" ? "" : linkEditing?.textContent ?? "";
609
631
  if (!existingRef) urlIn.value = linkEditing?.getAttribute("href") ?? "";
610
- linkNewTab = linkEditing?.getAttribute("target") === "_blank";
632
+ linkNewTab = (linkEditing?.getAttribute("target") ?? "").trim().toLowerCase() === "_blank";
611
633
  linkNewTabTouched = true;
612
634
  q("remove").appendChild(buildRemoveLink());
613
635
  }
@@ -742,7 +764,8 @@ function syncLinkDialog() {
742
764
  const own = q("text").value.trim();
743
765
  q("hint").innerHTML = own ? uiLabels.linkTextOwnHint : onPage ? uiLabels.linkTextAutoHint : uiLabels.linkUrlHint;
744
766
  q("live").style.display = onPage ? "flex" : "none";
745
- q("submit").disabled = onPage ? !linkPicked : !q("url").value.trim();
767
+ const submit = q("submit");
768
+ submit.disabled = onPage ? !linkPicked : !q("url").value.trim();
746
769
  const raw = q("url").value;
747
770
  const dangerous = !onPage && isDangerousUrl(raw);
748
771
  const doubtful = !onPage && !dangerous && isSchemeless(raw);
@@ -753,34 +776,45 @@ function syncLinkDialog() {
753
776
  notice.setAttribute("data-variant", "blocked");
754
777
  q("schemeless-text").innerHTML = uiLabels.linkBlocked.replace(
755
778
  "{v}",
756
- escapeHtml(raw.trim())
779
+ () => escapeHtml(raw.trim())
757
780
  );
758
- q("submit").disabled = true;
781
+ submit.disabled = true;
759
782
  } else if (doubtful) {
760
783
  notice.setAttribute("style", noticeCss("schemeless"));
761
784
  notice.setAttribute("data-variant", "schemeless");
762
785
  q("schemeless-text").innerHTML = uiLabels.linkSchemeless.replace(
763
786
  "{v}",
764
- escapeHtml(raw.trim())
787
+ () => escapeHtml(raw.trim())
765
788
  );
766
789
  }
790
+ submit.setAttribute("style", btnCss(true, submit.disabled));
791
+ if (!show) {
792
+ notice.removeAttribute("data-variant");
793
+ notice.removeAttribute("data-testid");
794
+ q("schemeless-text").innerHTML = "";
795
+ } else {
796
+ notice.setAttribute("data-testid", dangerous ? "inline-link-blocked" : "inline-link-schemeless");
797
+ }
767
798
  notice.style.display = show ? "block" : "none";
768
799
  q("schemeless-text").style.color = dangerous ? "#f5c6c6" : "#e8d6b0";
769
800
  q("add-scheme").style.display = doubtful ? "" : "none";
770
- if (!onPage && !linkNewTabTouched) {
771
- linkNewTab = isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
801
+ if (!linkNewTabTouched) {
802
+ linkNewTab = onPage ? false : isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
772
803
  }
773
804
  const box = q("newtab-box");
774
805
  if (box) box.setAttribute("style", checkboxCss(linkNewTab));
775
806
  q("newtab").setAttribute("aria-pressed", String(linkNewTab));
776
807
  }
777
808
  function setLinkTarget(el, newTab) {
809
+ const tokens = (el.getAttribute("rel") ?? "").split(/\s+/).filter(Boolean);
810
+ const kept = tokens.filter((t) => t.toLowerCase() !== "noopener");
778
811
  if (newTab) {
779
812
  el.setAttribute("target", "_blank");
780
- el.setAttribute("rel", "noopener");
813
+ el.setAttribute("rel", ["noopener", ...kept].join(" "));
781
814
  } else {
782
815
  el.removeAttribute("target");
783
- el.removeAttribute("rel");
816
+ if (kept.length) el.setAttribute("rel", kept.join(" "));
817
+ else el.removeAttribute("rel");
784
818
  }
785
819
  }
786
820
  function applyLink() {
@@ -837,7 +871,11 @@ var noticeCss = (variant) => "display:none;margin-top:8px;padding:8px 10px;borde
837
871
  var checkboxCss = (on) => "width:16px;height:16px;flex:0 0 16px;border-radius:4px;box-sizing:border-box;display:inline-block;" + (on ? `background:#00b2ff;border:1px solid #00b2ff;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M4 8.5l2.6 2.6L12 5.8' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/></svg>");background-size:100% 100%;` : "background:#141821;border:1px solid #3a3f4a;");
838
872
  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;";
839
873
  var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
840
- 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;");
874
+ var btnCss = (primary, disabled = false) => "height:33px;border-radius:7px;font-size:13px;padding:0 14px;font-weight:600;font-family:inherit;" + // A disabled button that still looks clickable is the silent state this whole
875
+ // card exists to remove: the editor clicks, nothing happens, and the dialog
876
+ // reads as broken rather than as refusing. It matters more now that `disabled`
877
+ // is what stops a javascript: address being inserted.
878
+ (disabled ? "cursor:not-allowed;opacity:.42;" : "cursor:pointer;") + (primary ? "background:#00b2ff;color:#04121b;border:none;" : "background:none;border:1px solid #3a3f4a;color:#fff;font-weight:500;");
841
879
  function toggleEmojiPicker(anchor) {
842
880
  if (!emojiPicker) emojiPicker = buildEmojiPicker();
843
881
  if (emojiPicker.style.display === "block") {
@@ -1066,7 +1104,7 @@ function preservedLinkAttrs(el) {
1066
1104
  const name = attr.name.toLowerCase();
1067
1105
  if (name === "href") continue;
1068
1106
  if (name.startsWith("on")) continue;
1069
- if (/^javascript:/i.test(attr.value.trim())) continue;
1107
+ if (isDangerousUrl(attr.value)) continue;
1070
1108
  out.push(`${name}="${escapeAttr(attr.value)}"`);
1071
1109
  }
1072
1110
  return out;
@@ -1265,13 +1303,19 @@ function injectStyles() {
1265
1303
  applyFieldSlice,
1266
1304
  buildConnectUrl,
1267
1305
  disconnect,
1306
+ extractLinkTargets,
1268
1307
  getConnectedToken,
1269
1308
  htmlToMarkdown,
1270
1309
  initInlineEdit,
1310
+ isBareEmail,
1311
+ isDangerousUrl,
1312
+ isExternalHost,
1313
+ isSchemeless,
1271
1314
  positionPopover,
1272
1315
  renderCurrentRefLine,
1273
1316
  rescanFields,
1274
1317
  resolveExistingRef,
1275
- setLinkTarget
1318
+ setLinkTarget,
1319
+ withHttps
1276
1320
  });
1277
1321
  //# sourceMappingURL=index.cjs.map