@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.d.cts CHANGED
@@ -12,6 +12,75 @@
12
12
  */
13
13
  declare function applyFieldSlice(current: string, original: string, next: string): string;
14
14
 
15
+ /**
16
+ * What kind of address did the editor actually type?
17
+ *
18
+ * The reason this is a module and not three lines inside the dialog: the
19
+ * content scanner (`scripts/scan-schemeless-links.mjs`) has to agree with the
20
+ * editor about what counts as a doubtful address. Two copies of the rule drift,
21
+ * and then the scanner reports a clean site the editor is still producing dead
22
+ * links on.
23
+ */
24
+ /**
25
+ * True when the browser will read this address as a path on the CURRENT page.
26
+ *
27
+ * `<a href="www.trailmem.com">` is valid HTML: it renders, it is styled, it
28
+ * lights up on hover. The browser just resolves it relative to the page it sits
29
+ * on, so it lands on /some/article/www.trailmem.com. Nothing about the markup
30
+ * looks wrong — only following the link reveals it.
31
+ *
32
+ * This DELIBERATELY does not try to decide whether the author meant an external
33
+ * host. `trailmem.com` and `index.html` are syntactically identical, and a rule
34
+ * that "fixes" the first breaks the second. It reports doubt; it never rewrites.
35
+ */
36
+ declare function isSchemeless(raw: string): boolean;
37
+ /**
38
+ * A bare email address — `name@host.tld`, no scheme, no path.
39
+ *
40
+ * Kept deliberately narrow: anything with a slash, a space or a second @ is
41
+ * not one, so an ordinary URL that happens to contain an @ (userinfo, a query)
42
+ * is untouched.
43
+ */
44
+ declare function isBareEmail(raw: string): boolean;
45
+ /** The one-click repair the dialog offers. Never applied on its own. */
46
+ declare function withHttps(raw: string): string;
47
+ /**
48
+ * Does this address leave the site the editor is standing on?
49
+ *
50
+ * Used ONLY to decide how the "open in a new tab" box starts out — and that
51
+ * starting state is visible in the box itself, so a wrong guess is something
52
+ * the editor can see and change, not a silent rewrite.
53
+ */
54
+ declare function isExternalHost(raw: string, currentHost: string): boolean;
55
+ /**
56
+ * Every link address inside a stored string — HTML anchors and Markdown links.
57
+ *
58
+ * Markdown IMAGE syntax is excluded on purpose: a relative image path
59
+ * (`![foto](uploads/x.jpg)`) is normal and correct, and flagging it would bury
60
+ * the real findings in noise.
61
+ */
62
+ declare function extractLinkTargets(text: string): Array<{
63
+ syntax: "html" | "markdown";
64
+ value: string;
65
+ }>;
66
+ /**
67
+ * Schemes that must never reach an `href`.
68
+ *
69
+ * `javascript:` in a link is script execution on click, in the SITE's own
70
+ * origin — which is where the editor's own edit-session token lives in
71
+ * localStorage. So one editor planting such a link, and any colleague clicking
72
+ * it, is a token handover. `data:` and `vbscript:` are the same class.
73
+ *
74
+ * The package ALREADY refuses these on every other attribute
75
+ * (preservedLinkAttrs drops `javascript:` values and every `on*` handler) —
76
+ * href was the one that escaped, because it is emitted separately from the
77
+ * attributes it sits beside.
78
+ *
79
+ * Whitespace and control characters are stripped first: browsers ignore them
80
+ * inside a scheme, so `java\tscript:` and ` javascript:` both run.
81
+ */
82
+ declare function isDangerousUrl(raw: string): boolean;
83
+
15
84
  /**
16
85
  * Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline
17
86
  * tooltips, the visible "colour" + "done" buttons, the emoji tooltip) and the
@@ -184,4 +253,4 @@ declare function setLinkTarget(el: HTMLElement, newTab: boolean): void;
184
253
  */
185
254
  declare function htmlToMarkdown(html: string): string;
186
255
 
187
- export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, getConnectedToken, htmlToMarkdown, initInlineEdit, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef, setLinkTarget };
256
+ export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, extractLinkTargets, getConnectedToken, htmlToMarkdown, initInlineEdit, isBareEmail, isDangerousUrl, isExternalHost, isSchemeless, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef, setLinkTarget, withHttps };
package/dist/index.d.ts CHANGED
@@ -12,6 +12,75 @@
12
12
  */
13
13
  declare function applyFieldSlice(current: string, original: string, next: string): string;
14
14
 
15
+ /**
16
+ * What kind of address did the editor actually type?
17
+ *
18
+ * The reason this is a module and not three lines inside the dialog: the
19
+ * content scanner (`scripts/scan-schemeless-links.mjs`) has to agree with the
20
+ * editor about what counts as a doubtful address. Two copies of the rule drift,
21
+ * and then the scanner reports a clean site the editor is still producing dead
22
+ * links on.
23
+ */
24
+ /**
25
+ * True when the browser will read this address as a path on the CURRENT page.
26
+ *
27
+ * `<a href="www.trailmem.com">` is valid HTML: it renders, it is styled, it
28
+ * lights up on hover. The browser just resolves it relative to the page it sits
29
+ * on, so it lands on /some/article/www.trailmem.com. Nothing about the markup
30
+ * looks wrong — only following the link reveals it.
31
+ *
32
+ * This DELIBERATELY does not try to decide whether the author meant an external
33
+ * host. `trailmem.com` and `index.html` are syntactically identical, and a rule
34
+ * that "fixes" the first breaks the second. It reports doubt; it never rewrites.
35
+ */
36
+ declare function isSchemeless(raw: string): boolean;
37
+ /**
38
+ * A bare email address — `name@host.tld`, no scheme, no path.
39
+ *
40
+ * Kept deliberately narrow: anything with a slash, a space or a second @ is
41
+ * not one, so an ordinary URL that happens to contain an @ (userinfo, a query)
42
+ * is untouched.
43
+ */
44
+ declare function isBareEmail(raw: string): boolean;
45
+ /** The one-click repair the dialog offers. Never applied on its own. */
46
+ declare function withHttps(raw: string): string;
47
+ /**
48
+ * Does this address leave the site the editor is standing on?
49
+ *
50
+ * Used ONLY to decide how the "open in a new tab" box starts out — and that
51
+ * starting state is visible in the box itself, so a wrong guess is something
52
+ * the editor can see and change, not a silent rewrite.
53
+ */
54
+ declare function isExternalHost(raw: string, currentHost: string): boolean;
55
+ /**
56
+ * Every link address inside a stored string — HTML anchors and Markdown links.
57
+ *
58
+ * Markdown IMAGE syntax is excluded on purpose: a relative image path
59
+ * (`![foto](uploads/x.jpg)`) is normal and correct, and flagging it would bury
60
+ * the real findings in noise.
61
+ */
62
+ declare function extractLinkTargets(text: string): Array<{
63
+ syntax: "html" | "markdown";
64
+ value: string;
65
+ }>;
66
+ /**
67
+ * Schemes that must never reach an `href`.
68
+ *
69
+ * `javascript:` in a link is script execution on click, in the SITE's own
70
+ * origin — which is where the editor's own edit-session token lives in
71
+ * localStorage. So one editor planting such a link, and any colleague clicking
72
+ * it, is a token handover. `data:` and `vbscript:` are the same class.
73
+ *
74
+ * The package ALREADY refuses these on every other attribute
75
+ * (preservedLinkAttrs drops `javascript:` values and every `on*` handler) —
76
+ * href was the one that escaped, because it is emitted separately from the
77
+ * attributes it sits beside.
78
+ *
79
+ * Whitespace and control characters are stripped first: browsers ignore them
80
+ * inside a scheme, so `java\tscript:` and ` javascript:` both run.
81
+ */
82
+ declare function isDangerousUrl(raw: string): boolean;
83
+
15
84
  /**
16
85
  * Labels for the in-editor UI — the rich-text toolbar (bold/italic/underline
17
86
  * tooltips, the visible "colour" + "done" buttons, the emoji tooltip) and the
@@ -184,4 +253,4 @@ declare function setLinkTarget(el: HTMLElement, newTab: boolean): void;
184
253
  */
185
254
  declare function htmlToMarkdown(html: string): string;
186
255
 
187
- export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, getConnectedToken, htmlToMarkdown, initInlineEdit, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef, setLinkTarget };
256
+ export { type InlineEditLabels, type InlineEditOptions, applyFieldSlice, buildConnectUrl, disconnect, extractLinkTargets, getConnectedToken, htmlToMarkdown, initInlineEdit, isBareEmail, isDangerousUrl, isExternalHost, isSchemeless, positionPopover, renderCurrentRefLine, rescanFields, resolveExistingRef, setLinkTarget, withHttps };
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ function applyFieldSlice(current, original, next) {
12
12
  function isSchemeless(raw) {
13
13
  const v = (raw || "").trim();
14
14
  if (!v) return false;
15
+ if (isBareEmail(v)) return false;
15
16
  if (/^[a-z][a-z0-9+.-]*:/i.test(v)) return false;
16
17
  if (v.startsWith("//")) return false;
17
18
  if (v.startsWith("/")) return false;
@@ -19,6 +20,9 @@ function isSchemeless(raw) {
19
20
  if (v.startsWith("?")) return false;
20
21
  return true;
21
22
  }
23
+ function isBareEmail(raw) {
24
+ return /^[^\s@/:?#]+@[^\s@/:?#]+\.[a-z]{2,}$/i.test((raw || "").trim());
25
+ }
22
26
  function withHttps(raw) {
23
27
  return `https://${(raw || "").trim().replace(/^\/+/, "")}`;
24
28
  }
@@ -31,6 +35,18 @@ function isExternalHost(raw, currentHost) {
31
35
  return false;
32
36
  }
33
37
  }
38
+ function extractLinkTargets(text) {
39
+ const out = [];
40
+ if (typeof text !== "string") return out;
41
+ for (const m of text.matchAll(/<a\b[^>]*?\bhref\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>]+))/gi)) {
42
+ out.push({ syntax: "html", value: m[2] ?? m[3] ?? m[4] ?? "" });
43
+ }
44
+ for (const m of text.matchAll(/(!)?\[(?:[^\]\[]|\[[^\]]*\])*\]\(([^)\s]+)/g)) {
45
+ if (m[1]) continue;
46
+ out.push({ syntax: "markdown", value: m[2] ?? "" });
47
+ }
48
+ return out;
49
+ }
34
50
  function isDangerousUrl(raw) {
35
51
  const v = (raw || "").replace(/[\u0000-\u0020]/g, "");
36
52
  return /^(?:javascript|data|vbscript):/i.test(v);
@@ -563,7 +579,7 @@ function renderLinkDialog() {
563
579
  }
564
580
  const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
565
581
  const onPageTab = !editing || !!existingRef;
566
- 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>`;
582
+ 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>`;
567
583
  const q = (role) => d.querySelector(`[data-role="${role}"]`);
568
584
  const text = q("text");
569
585
  const urlIn = q("url");
@@ -571,7 +587,7 @@ function renderLinkDialog() {
571
587
  if (editing) {
572
588
  text.value = linkEditing?.getAttribute("data-cms-ref-label") === "auto" ? "" : linkEditing?.textContent ?? "";
573
589
  if (!existingRef) urlIn.value = linkEditing?.getAttribute("href") ?? "";
574
- linkNewTab = linkEditing?.getAttribute("target") === "_blank";
590
+ linkNewTab = (linkEditing?.getAttribute("target") ?? "").trim().toLowerCase() === "_blank";
575
591
  linkNewTabTouched = true;
576
592
  q("remove").appendChild(buildRemoveLink());
577
593
  }
@@ -706,7 +722,8 @@ function syncLinkDialog() {
706
722
  const own = q("text").value.trim();
707
723
  q("hint").innerHTML = own ? uiLabels.linkTextOwnHint : onPage ? uiLabels.linkTextAutoHint : uiLabels.linkUrlHint;
708
724
  q("live").style.display = onPage ? "flex" : "none";
709
- q("submit").disabled = onPage ? !linkPicked : !q("url").value.trim();
725
+ const submit = q("submit");
726
+ submit.disabled = onPage ? !linkPicked : !q("url").value.trim();
710
727
  const raw = q("url").value;
711
728
  const dangerous = !onPage && isDangerousUrl(raw);
712
729
  const doubtful = !onPage && !dangerous && isSchemeless(raw);
@@ -717,34 +734,45 @@ function syncLinkDialog() {
717
734
  notice.setAttribute("data-variant", "blocked");
718
735
  q("schemeless-text").innerHTML = uiLabels.linkBlocked.replace(
719
736
  "{v}",
720
- escapeHtml(raw.trim())
737
+ () => escapeHtml(raw.trim())
721
738
  );
722
- q("submit").disabled = true;
739
+ submit.disabled = true;
723
740
  } else if (doubtful) {
724
741
  notice.setAttribute("style", noticeCss("schemeless"));
725
742
  notice.setAttribute("data-variant", "schemeless");
726
743
  q("schemeless-text").innerHTML = uiLabels.linkSchemeless.replace(
727
744
  "{v}",
728
- escapeHtml(raw.trim())
745
+ () => escapeHtml(raw.trim())
729
746
  );
730
747
  }
748
+ submit.setAttribute("style", btnCss(true, submit.disabled));
749
+ if (!show) {
750
+ notice.removeAttribute("data-variant");
751
+ notice.removeAttribute("data-testid");
752
+ q("schemeless-text").innerHTML = "";
753
+ } else {
754
+ notice.setAttribute("data-testid", dangerous ? "inline-link-blocked" : "inline-link-schemeless");
755
+ }
731
756
  notice.style.display = show ? "block" : "none";
732
757
  q("schemeless-text").style.color = dangerous ? "#f5c6c6" : "#e8d6b0";
733
758
  q("add-scheme").style.display = doubtful ? "" : "none";
734
- if (!onPage && !linkNewTabTouched) {
735
- linkNewTab = isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
759
+ if (!linkNewTabTouched) {
760
+ linkNewTab = onPage ? false : isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
736
761
  }
737
762
  const box = q("newtab-box");
738
763
  if (box) box.setAttribute("style", checkboxCss(linkNewTab));
739
764
  q("newtab").setAttribute("aria-pressed", String(linkNewTab));
740
765
  }
741
766
  function setLinkTarget(el, newTab) {
767
+ const tokens = (el.getAttribute("rel") ?? "").split(/\s+/).filter(Boolean);
768
+ const kept = tokens.filter((t) => t.toLowerCase() !== "noopener");
742
769
  if (newTab) {
743
770
  el.setAttribute("target", "_blank");
744
- el.setAttribute("rel", "noopener");
771
+ el.setAttribute("rel", ["noopener", ...kept].join(" "));
745
772
  } else {
746
773
  el.removeAttribute("target");
747
- el.removeAttribute("rel");
774
+ if (kept.length) el.setAttribute("rel", kept.join(" "));
775
+ else el.removeAttribute("rel");
748
776
  }
749
777
  }
750
778
  function applyLink() {
@@ -801,7 +829,11 @@ var noticeCss = (variant) => "display:none;margin-top:8px;padding:8px 10px;borde
801
829
  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;");
802
830
  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;";
803
831
  var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
804
- 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;");
832
+ 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
833
+ // card exists to remove: the editor clicks, nothing happens, and the dialog
834
+ // reads as broken rather than as refusing. It matters more now that `disabled`
835
+ // is what stops a javascript: address being inserted.
836
+ (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;");
805
837
  function toggleEmojiPicker(anchor) {
806
838
  if (!emojiPicker) emojiPicker = buildEmojiPicker();
807
839
  if (emojiPicker.style.display === "block") {
@@ -1030,7 +1062,7 @@ function preservedLinkAttrs(el) {
1030
1062
  const name = attr.name.toLowerCase();
1031
1063
  if (name === "href") continue;
1032
1064
  if (name.startsWith("on")) continue;
1033
- if (/^javascript:/i.test(attr.value.trim())) continue;
1065
+ if (isDangerousUrl(attr.value)) continue;
1034
1066
  out.push(`${name}="${escapeAttr(attr.value)}"`);
1035
1067
  }
1036
1068
  return out;
@@ -1228,13 +1260,19 @@ export {
1228
1260
  applyFieldSlice,
1229
1261
  buildConnectUrl,
1230
1262
  disconnect,
1263
+ extractLinkTargets,
1231
1264
  getConnectedToken,
1232
1265
  htmlToMarkdown,
1233
1266
  initInlineEdit,
1267
+ isBareEmail,
1268
+ isDangerousUrl,
1269
+ isExternalHost,
1270
+ isSchemeless,
1234
1271
  positionPopover,
1235
1272
  renderCurrentRefLine,
1236
1273
  rescanFields,
1237
1274
  resolveExistingRef,
1238
- setLinkTarget
1275
+ setLinkTarget,
1276
+ withHttps
1239
1277
  };
1240
1278
  //# sourceMappingURL=index.js.map