@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.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
|
+
* (``) 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
|
|
@@ -46,6 +115,7 @@ interface InlineEditLabels {
|
|
|
46
115
|
linkNewTab?: string;
|
|
47
116
|
linkSchemeless?: string;
|
|
48
117
|
linkSchemelessFix?: string;
|
|
118
|
+
linkBlocked?: string;
|
|
49
119
|
linkInsert?: string;
|
|
50
120
|
linkSave?: string;
|
|
51
121
|
linkCancel?: string;
|
|
@@ -183,4 +253,4 @@ declare function setLinkTarget(el: HTMLElement, newTab: boolean): void;
|
|
|
183
253
|
*/
|
|
184
254
|
declare function htmlToMarkdown(html: string): string;
|
|
185
255
|
|
|
186
|
-
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
|
+
* (``) 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
|
|
@@ -46,6 +115,7 @@ interface InlineEditLabels {
|
|
|
46
115
|
linkNewTab?: string;
|
|
47
116
|
linkSchemeless?: string;
|
|
48
117
|
linkSchemelessFix?: string;
|
|
118
|
+
linkBlocked?: string;
|
|
49
119
|
linkInsert?: string;
|
|
50
120
|
linkSave?: string;
|
|
51
121
|
linkCancel?: string;
|
|
@@ -183,4 +253,4 @@ declare function setLinkTarget(el: HTMLElement, newTab: boolean): void;
|
|
|
183
253
|
*/
|
|
184
254
|
declare function htmlToMarkdown(html: string): string;
|
|
185
255
|
|
|
186
|
-
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,22 @@ 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
|
+
}
|
|
50
|
+
function isDangerousUrl(raw) {
|
|
51
|
+
const v = (raw || "").replace(/[\u0000-\u0020]/g, "");
|
|
52
|
+
return /^(?:javascript|data|vbscript):/i.test(v);
|
|
53
|
+
}
|
|
34
54
|
|
|
35
55
|
// src/token-safe.ts
|
|
36
56
|
var TEXT_NODE = 3;
|
|
@@ -89,6 +109,7 @@ var DEFAULT_LABELS = {
|
|
|
89
109
|
linkNewTab: "\xC5bn i ny fane",
|
|
90
110
|
linkSchemeless: "<b>{v}</b> l\xE6ses som en side p\xE5 <b>dette</b> site \u2014 ikke som en adresse ude p\xE5 nettet.",
|
|
91
111
|
linkSchemelessFix: "Tilf\xF8j https://",
|
|
112
|
+
linkBlocked: "<b>{v}</b> kan ikke bruges som link \u2014 adresser af den type kan k\xF8re kode p\xE5 sitet.",
|
|
92
113
|
linkInsert: "Inds\xE6t link",
|
|
93
114
|
linkSave: "Gem \xE6ndring",
|
|
94
115
|
linkCancel: "Annull\xE9r",
|
|
@@ -558,7 +579,7 @@ function renderLinkDialog() {
|
|
|
558
579
|
}
|
|
559
580
|
const existingRef = linkEditing?.getAttribute("data-cms-ref") ?? "";
|
|
560
581
|
const onPageTab = !editing || !!existingRef;
|
|
561
|
-
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"
|
|
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>`;
|
|
562
583
|
const q = (role) => d.querySelector(`[data-role="${role}"]`);
|
|
563
584
|
const text = q("text");
|
|
564
585
|
const urlIn = q("url");
|
|
@@ -566,7 +587,7 @@ function renderLinkDialog() {
|
|
|
566
587
|
if (editing) {
|
|
567
588
|
text.value = linkEditing?.getAttribute("data-cms-ref-label") === "auto" ? "" : linkEditing?.textContent ?? "";
|
|
568
589
|
if (!existingRef) urlIn.value = linkEditing?.getAttribute("href") ?? "";
|
|
569
|
-
linkNewTab = linkEditing?.getAttribute("target") === "_blank";
|
|
590
|
+
linkNewTab = (linkEditing?.getAttribute("target") ?? "").trim().toLowerCase() === "_blank";
|
|
570
591
|
linkNewTabTouched = true;
|
|
571
592
|
q("remove").appendChild(buildRemoveLink());
|
|
572
593
|
}
|
|
@@ -701,30 +722,57 @@ function syncLinkDialog() {
|
|
|
701
722
|
const own = q("text").value.trim();
|
|
702
723
|
q("hint").innerHTML = own ? uiLabels.linkTextOwnHint : onPage ? uiLabels.linkTextAutoHint : uiLabels.linkUrlHint;
|
|
703
724
|
q("live").style.display = onPage ? "flex" : "none";
|
|
704
|
-
|
|
725
|
+
const submit = q("submit");
|
|
726
|
+
submit.disabled = onPage ? !linkPicked : !q("url").value.trim();
|
|
705
727
|
const raw = q("url").value;
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
728
|
+
const dangerous = !onPage && isDangerousUrl(raw);
|
|
729
|
+
const doubtful = !onPage && !dangerous && isSchemeless(raw);
|
|
730
|
+
const notice = q("schemeless");
|
|
731
|
+
const show = dangerous || doubtful;
|
|
732
|
+
if (dangerous) {
|
|
733
|
+
notice.setAttribute("style", noticeCss("blocked"));
|
|
734
|
+
notice.setAttribute("data-variant", "blocked");
|
|
735
|
+
q("schemeless-text").innerHTML = uiLabels.linkBlocked.replace(
|
|
736
|
+
"{v}",
|
|
737
|
+
() => escapeHtml(raw.trim())
|
|
738
|
+
);
|
|
739
|
+
submit.disabled = true;
|
|
740
|
+
} else if (doubtful) {
|
|
741
|
+
notice.setAttribute("style", noticeCss("schemeless"));
|
|
742
|
+
notice.setAttribute("data-variant", "schemeless");
|
|
709
743
|
q("schemeless-text").innerHTML = uiLabels.linkSchemeless.replace(
|
|
710
744
|
"{v}",
|
|
711
|
-
escapeHtml(raw.trim())
|
|
745
|
+
() => escapeHtml(raw.trim())
|
|
712
746
|
);
|
|
713
747
|
}
|
|
714
|
-
|
|
715
|
-
|
|
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
|
+
}
|
|
756
|
+
notice.style.display = show ? "block" : "none";
|
|
757
|
+
q("schemeless-text").style.color = dangerous ? "#f5c6c6" : "#e8d6b0";
|
|
758
|
+
q("add-scheme").style.display = doubtful ? "" : "none";
|
|
759
|
+
if (!linkNewTabTouched) {
|
|
760
|
+
linkNewTab = onPage ? false : isExternalHost(raw, typeof location !== "undefined" ? location.host : "");
|
|
716
761
|
}
|
|
717
762
|
const box = q("newtab-box");
|
|
718
763
|
if (box) box.setAttribute("style", checkboxCss(linkNewTab));
|
|
719
764
|
q("newtab").setAttribute("aria-pressed", String(linkNewTab));
|
|
720
765
|
}
|
|
721
766
|
function setLinkTarget(el, newTab) {
|
|
767
|
+
const tokens = (el.getAttribute("rel") ?? "").split(/\s+/).filter(Boolean);
|
|
768
|
+
const kept = tokens.filter((t) => t.toLowerCase() !== "noopener");
|
|
722
769
|
if (newTab) {
|
|
723
770
|
el.setAttribute("target", "_blank");
|
|
724
|
-
el.setAttribute("rel", "noopener");
|
|
771
|
+
el.setAttribute("rel", ["noopener", ...kept].join(" "));
|
|
725
772
|
} else {
|
|
726
773
|
el.removeAttribute("target");
|
|
727
|
-
el.
|
|
774
|
+
if (kept.length) el.setAttribute("rel", kept.join(" "));
|
|
775
|
+
else el.removeAttribute("rel");
|
|
728
776
|
}
|
|
729
777
|
}
|
|
730
778
|
function applyLink() {
|
|
@@ -735,6 +783,7 @@ function applyLink() {
|
|
|
735
783
|
const own = q("text").value.trim();
|
|
736
784
|
const href = onPage ? linkPicked?.path ?? "" : q("url").value.trim();
|
|
737
785
|
if (!href) return;
|
|
786
|
+
if (isDangerousUrl(href)) return;
|
|
738
787
|
const ref = onPage && linkPicked ? `${linkPicked.collection}:${linkPicked.slug}` : "";
|
|
739
788
|
const label = own || (onPage ? linkPicked?.title ?? href : href);
|
|
740
789
|
const applyTarget = (el) => setLinkTarget(el, linkNewTab);
|
|
@@ -776,10 +825,15 @@ function escapeHtml(v) {
|
|
|
776
825
|
return v.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
777
826
|
}
|
|
778
827
|
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;";
|
|
828
|
+
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)") + ";";
|
|
779
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;");
|
|
780
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;";
|
|
781
831
|
var labelCss = () => "font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#9aa3b2;margin:12px 0 5px;display:block;";
|
|
782
|
-
var btnCss = (primary) => "height:33px;border-radius:7px;font-size:13px;
|
|
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;");
|
|
783
837
|
function toggleEmojiPicker(anchor) {
|
|
784
838
|
if (!emojiPicker) emojiPicker = buildEmojiPicker();
|
|
785
839
|
if (emojiPicker.style.display === "block") {
|
|
@@ -1008,7 +1062,7 @@ function preservedLinkAttrs(el) {
|
|
|
1008
1062
|
const name = attr.name.toLowerCase();
|
|
1009
1063
|
if (name === "href") continue;
|
|
1010
1064
|
if (name.startsWith("on")) continue;
|
|
1011
|
-
if (
|
|
1065
|
+
if (isDangerousUrl(attr.value)) continue;
|
|
1012
1066
|
out.push(`${name}="${escapeAttr(attr.value)}"`);
|
|
1013
1067
|
}
|
|
1014
1068
|
return out;
|
|
@@ -1067,6 +1121,10 @@ function serializeInlineNode(child) {
|
|
|
1067
1121
|
break;
|
|
1068
1122
|
case "a": {
|
|
1069
1123
|
const href = el.getAttribute("href") || "";
|
|
1124
|
+
if (isDangerousUrl(href)) {
|
|
1125
|
+
out += inner;
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1070
1128
|
const extras = preservedLinkAttrs(el);
|
|
1071
1129
|
if (extras.length > 0) {
|
|
1072
1130
|
out += `<a ${[`href="${escapeAttr(href)}"`, ...extras].join(" ")}>${inner}</a>`;
|
|
@@ -1202,13 +1260,19 @@ export {
|
|
|
1202
1260
|
applyFieldSlice,
|
|
1203
1261
|
buildConnectUrl,
|
|
1204
1262
|
disconnect,
|
|
1263
|
+
extractLinkTargets,
|
|
1205
1264
|
getConnectedToken,
|
|
1206
1265
|
htmlToMarkdown,
|
|
1207
1266
|
initInlineEdit,
|
|
1267
|
+
isBareEmail,
|
|
1268
|
+
isDangerousUrl,
|
|
1269
|
+
isExternalHost,
|
|
1270
|
+
isSchemeless,
|
|
1208
1271
|
positionPopover,
|
|
1209
1272
|
renderCurrentRefLine,
|
|
1210
1273
|
rescanFields,
|
|
1211
1274
|
resolveExistingRef,
|
|
1212
|
-
setLinkTarget
|
|
1275
|
+
setLinkTarget,
|
|
1276
|
+
withHttps
|
|
1213
1277
|
};
|
|
1214
1278
|
//# sourceMappingURL=index.js.map
|