@broberg/cms-inline-edit 0.6.1 → 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 +85 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -1
- package/dist/index.d.ts +71 -1
- package/dist/index.js +78 -14
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +14 -1
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +18 -1
- package/dist/server/index.d.ts +18 -1
- package/dist/server/index.js +13 -1
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
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,22 @@ 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
|
+
}
|
|
92
|
+
function isDangerousUrl(raw) {
|
|
93
|
+
const v = (raw || "").replace(/[\u0000-\u0020]/g, "");
|
|
94
|
+
return /^(?:javascript|data|vbscript):/i.test(v);
|
|
95
|
+
}
|
|
70
96
|
|
|
71
97
|
// src/token-safe.ts
|
|
72
98
|
var TEXT_NODE = 3;
|
|
@@ -125,6 +151,7 @@ var DEFAULT_LABELS = {
|
|
|
125
151
|
linkNewTab: "\xC5bn i ny fane",
|
|
126
152
|
linkSchemeless: "<b>{v}</b> l\xE6ses som en side p\xE5 <b>dette</b> site \u2014 ikke som en adresse ude p\xE5 nettet.",
|
|
127
153
|
linkSchemelessFix: "Tilf\xF8j https://",
|
|
154
|
+
linkBlocked: "<b>{v}</b> kan ikke bruges som link \u2014 adresser af den type kan k\xF8re kode p\xE5 sitet.",
|
|
128
155
|
linkInsert: "Inds\xE6t link",
|
|
129
156
|
linkSave: "Gem \xE6ndring",
|
|
130
157
|
linkCancel: "Annull\xE9r",
|
|
@@ -594,7 +621,7 @@ function renderLinkDialog() {
|
|
|
594
621
|
}
|
|
595
622
|
const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
|
|
596
623
|
const onPageTab = !editing || !!existingRef;
|
|
597
|
-
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"
|
|
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>`;
|
|
598
625
|
const q = (role) => d.querySelector(`[data-role="${role}"]`);
|
|
599
626
|
const text = q("text");
|
|
600
627
|
const urlIn = q("url");
|
|
@@ -602,7 +629,7 @@ function renderLinkDialog() {
|
|
|
602
629
|
if (editing) {
|
|
603
630
|
text.value = linkEditing?.getAttribute("data-cms-ref-label") === "auto" ? "" : linkEditing?.textContent ?? "";
|
|
604
631
|
if (!existingRef) urlIn.value = linkEditing?.getAttribute("href") ?? "";
|
|
605
|
-
linkNewTab = linkEditing?.getAttribute("target") === "_blank";
|
|
632
|
+
linkNewTab = (linkEditing?.getAttribute("target") ?? "").trim().toLowerCase() === "_blank";
|
|
606
633
|
linkNewTabTouched = true;
|
|
607
634
|
q("remove").appendChild(buildRemoveLink());
|
|
608
635
|
}
|
|
@@ -737,30 +764,57 @@ function syncLinkDialog() {
|
|
|
737
764
|
const own = q("text").value.trim();
|
|
738
765
|
q("hint").innerHTML = own ? uiLabels.linkTextOwnHint : onPage ? uiLabels.linkTextAutoHint : uiLabels.linkUrlHint;
|
|
739
766
|
q("live").style.display = onPage ? "flex" : "none";
|
|
740
|
-
|
|
767
|
+
const submit = q("submit");
|
|
768
|
+
submit.disabled = onPage ? !linkPicked : !q("url").value.trim();
|
|
741
769
|
const raw = q("url").value;
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
770
|
+
const dangerous = !onPage && isDangerousUrl(raw);
|
|
771
|
+
const doubtful = !onPage && !dangerous && isSchemeless(raw);
|
|
772
|
+
const notice = q("schemeless");
|
|
773
|
+
const show = dangerous || doubtful;
|
|
774
|
+
if (dangerous) {
|
|
775
|
+
notice.setAttribute("style", noticeCss("blocked"));
|
|
776
|
+
notice.setAttribute("data-variant", "blocked");
|
|
777
|
+
q("schemeless-text").innerHTML = uiLabels.linkBlocked.replace(
|
|
778
|
+
"{v}",
|
|
779
|
+
() => escapeHtml(raw.trim())
|
|
780
|
+
);
|
|
781
|
+
submit.disabled = true;
|
|
782
|
+
} else if (doubtful) {
|
|
783
|
+
notice.setAttribute("style", noticeCss("schemeless"));
|
|
784
|
+
notice.setAttribute("data-variant", "schemeless");
|
|
745
785
|
q("schemeless-text").innerHTML = uiLabels.linkSchemeless.replace(
|
|
746
786
|
"{v}",
|
|
747
|
-
escapeHtml(raw.trim())
|
|
787
|
+
() => escapeHtml(raw.trim())
|
|
748
788
|
);
|
|
749
789
|
}
|
|
750
|
-
|
|
751
|
-
|
|
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
|
+
}
|
|
798
|
+
notice.style.display = show ? "block" : "none";
|
|
799
|
+
q("schemeless-text").style.color = dangerous ? "#f5c6c6" : "#e8d6b0";
|
|
800
|
+
q("add-scheme").style.display = doubtful ? "" : "none";
|
|
801
|
+
if (!linkNewTabTouched) {
|
|
802
|
+
linkNewTab = onPage ? false : isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
|
|
752
803
|
}
|
|
753
804
|
const box = q("newtab-box");
|
|
754
805
|
if (box) box.setAttribute("style", checkboxCss(linkNewTab));
|
|
755
806
|
q("newtab").setAttribute("aria-pressed", String(linkNewTab));
|
|
756
807
|
}
|
|
757
808
|
function setLinkTarget(el, newTab) {
|
|
809
|
+
const tokens = (el.getAttribute("rel") ?? "").split(/\s+/).filter(Boolean);
|
|
810
|
+
const kept = tokens.filter((t) => t.toLowerCase() !== "noopener");
|
|
758
811
|
if (newTab) {
|
|
759
812
|
el.setAttribute("target", "_blank");
|
|
760
|
-
el.setAttribute("rel", "noopener");
|
|
813
|
+
el.setAttribute("rel", ["noopener", ...kept].join(" "));
|
|
761
814
|
} else {
|
|
762
815
|
el.removeAttribute("target");
|
|
763
|
-
el.
|
|
816
|
+
if (kept.length) el.setAttribute("rel", kept.join(" "));
|
|
817
|
+
else el.removeAttribute("rel");
|
|
764
818
|
}
|
|
765
819
|
}
|
|
766
820
|
function applyLink() {
|
|
@@ -771,6 +825,7 @@ function applyLink() {
|
|
|
771
825
|
const own = q("text").value.trim();
|
|
772
826
|
const href = onPage ? linkPicked?.path ?? "" : q("url").value.trim();
|
|
773
827
|
if (!href) return;
|
|
828
|
+
if (isDangerousUrl(href)) return;
|
|
774
829
|
const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : "";
|
|
775
830
|
const label = own || (onPage ? linkPicked?.title ?? href : href);
|
|
776
831
|
const applyTarget = (el) => setLinkTarget(el, linkNewTab);
|
|
@@ -812,10 +867,15 @@ function escapeHtml(v) {
|
|
|
812
867
|
return v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
813
868
|
}
|
|
814
869
|
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;";
|
|
870
|
+
var noticeCss = (variant) => "display:none;margin-top:8px;padding:8px 10px;border-radius:8px;background:" + (variant === "blocked" ? "rgba(255,90,90,.10)" : "rgba(255,176,32,.10)") + ";border:1px solid " + (variant === "blocked" ? "rgba(255,90,90,.38)" : "rgba(255,176,32,.34)") + ";";
|
|
815
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;");
|
|
816
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;";
|
|
817
873
|
var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
|
|
818
|
-
var btnCss = (primary) => "height:33px;border-radius:7px;font-size:13px;
|
|
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;");
|
|
819
879
|
function toggleEmojiPicker(anchor) {
|
|
820
880
|
if (!emojiPicker) emojiPicker = buildEmojiPicker();
|
|
821
881
|
if (emojiPicker.style.display === "block") {
|
|
@@ -1044,7 +1104,7 @@ function preservedLinkAttrs(el) {
|
|
|
1044
1104
|
const name = attr.name.toLowerCase();
|
|
1045
1105
|
if (name === "href") continue;
|
|
1046
1106
|
if (name.startsWith("on")) continue;
|
|
1047
|
-
if (
|
|
1107
|
+
if (isDangerousUrl(attr.value)) continue;
|
|
1048
1108
|
out.push(`${name}="${escapeAttr(attr.value)}"`);
|
|
1049
1109
|
}
|
|
1050
1110
|
return out;
|
|
@@ -1103,6 +1163,10 @@ function serializeInlineNode(child) {
|
|
|
1103
1163
|
break;
|
|
1104
1164
|
case "a": {
|
|
1105
1165
|
const href = el.getAttribute("href") || "";
|
|
1166
|
+
if (isDangerousUrl(href)) {
|
|
1167
|
+
out += inner;
|
|
1168
|
+
break;
|
|
1169
|
+
}
|
|
1106
1170
|
const extras = preservedLinkAttrs(el);
|
|
1107
1171
|
if (extras.length > 0) {
|
|
1108
1172
|
out += `<a ${[`href="${escapeAttr(href)}"`, ...extras].join(" ")}>${inner}</a>`;
|
|
@@ -1239,13 +1303,19 @@ function injectStyles() {
|
|
|
1239
1303
|
applyFieldSlice,
|
|
1240
1304
|
buildConnectUrl,
|
|
1241
1305
|
disconnect,
|
|
1306
|
+
extractLinkTargets,
|
|
1242
1307
|
getConnectedToken,
|
|
1243
1308
|
htmlToMarkdown,
|
|
1244
1309
|
initInlineEdit,
|
|
1310
|
+
isBareEmail,
|
|
1311
|
+
isDangerousUrl,
|
|
1312
|
+
isExternalHost,
|
|
1313
|
+
isSchemeless,
|
|
1245
1314
|
positionPopover,
|
|
1246
1315
|
renderCurrentRefLine,
|
|
1247
1316
|
rescanFields,
|
|
1248
1317
|
resolveExistingRef,
|
|
1249
|
-
setLinkTarget
|
|
1318
|
+
setLinkTarget,
|
|
1319
|
+
withHttps
|
|
1250
1320
|
});
|
|
1251
1321
|
//# sourceMappingURL=index.cjs.map
|