@daugasauron/dolly-host-html 0.1.5-rc.0 → 0.1.6
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/html.mjs +59 -49
- package/package.json +2 -2
package/html.mjs
CHANGED
|
@@ -56,13 +56,18 @@ const popupStyle = `
|
|
|
56
56
|
:user-invalid { border-color: #e07a5f; }
|
|
57
57
|
`;
|
|
58
58
|
|
|
59
|
-
// The
|
|
60
|
-
// and
|
|
61
|
-
// text and the elements and attributes below, copied into new elements of
|
|
62
|
-
// own
|
|
63
|
-
//
|
|
64
|
-
// fields and
|
|
65
|
-
|
|
59
|
+
// The popup's frame: srcdoc, sandboxed without allow-scripts, so nothing runs in it,
|
|
60
|
+
// and of the page's origin (allow-same-origin), so the page builds it: the document's
|
|
61
|
+
// text and the elements and attributes below, copied into new elements of the frame's
|
|
62
|
+
// own. DOMParser parses the document, which loads and runs nothing there, and nothing
|
|
63
|
+
// else of it exists in the popup. A form's submit event reaches the page (allow-forms),
|
|
64
|
+
// which takes its fields and stops the submission. Without allow-popups,
|
|
65
|
+
// allow-top-navigation or allow-modals it opens nothing and navigates no other page.
|
|
66
|
+
// The frame has no script or style element, which the page's policy, which a srcdoc
|
|
67
|
+
// frame takes, could refuse.
|
|
68
|
+
const frameSandbox = "allow-same-origin allow-forms";
|
|
69
|
+
function build(frame, { source, theme, font }, answer) {
|
|
70
|
+
const view = frame.contentWindow, page = frame.contentDocument;
|
|
66
71
|
const shown = new Set(("h1 h2 h3 h4 h5 h6 p br hr div span section article header footer main aside nav " +
|
|
67
72
|
"blockquote pre code kbd samp var em strong b i u s small mark sub sup abbr cite q dfn time data del ins " +
|
|
68
73
|
"figure figcaption address ul ol li dl dt dd table caption colgroup col thead tbody tfoot tr th td " +
|
|
@@ -88,6 +93,7 @@ function popup({ source, theme, font, origin }) {
|
|
|
88
93
|
// one, and a plain button does nothing without a script.
|
|
89
94
|
const types = { input: new Set(("text search email url tel number range date month week time datetime-local " +
|
|
90
95
|
"color checkbox radio hidden submit reset").split(" ")), button: new Set(["submit", "reset"]) };
|
|
96
|
+
const autofilled = new Set(["input", "select", "textarea"]);
|
|
91
97
|
const copy = (from, to) => {
|
|
92
98
|
for (const node of from.childNodes) {
|
|
93
99
|
if (node.nodeType === Node.TEXT_NODE) to.append(node.data);
|
|
@@ -96,37 +102,34 @@ function popup({ source, theme, font, origin }) {
|
|
|
96
102
|
if (!shown.has(node.localName)) { copy(node, to); continue; }
|
|
97
103
|
const type = node.getAttribute("type")?.toLowerCase();
|
|
98
104
|
if (types[node.localName] && type !== undefined && !types[node.localName].has(type)) continue;
|
|
99
|
-
const element =
|
|
105
|
+
const element = page.createElement(node.localName);
|
|
100
106
|
for (const name of [...attributes["*"], ...attributes[node.localName] ?? []]) {
|
|
101
107
|
if (node.hasAttribute(name)) element.setAttribute(name, node.getAttribute(name));
|
|
102
108
|
}
|
|
109
|
+
// The browser offers none of the user's saved addresses, cards or past entries: one chosen
|
|
110
|
+
// would fill every field of the program's form, seen or not. Chrome offers a saved address
|
|
111
|
+
// under "off" and none under a token it does not know; a control's unknown token defers to
|
|
112
|
+
// its form's "off", which Firefox takes (measured: tasks/20261112-074507-page-providers).
|
|
113
|
+
if (node.localName === "form") element.setAttribute("autocomplete", "off");
|
|
114
|
+
else if (autofilled.has(node.localName)) element.setAttribute("autocomplete", "dolly-program-field");
|
|
103
115
|
copy(node, element);
|
|
104
116
|
to.append(element);
|
|
105
117
|
}
|
|
106
118
|
};
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
const look = new view.CSSStyleSheet();
|
|
120
|
+
look.replaceSync(popupStyle);
|
|
121
|
+
page.adoptedStyleSheets = [look];
|
|
122
|
+
for (const [name, value] of Object.entries(theme)) page.documentElement.style.setProperty(name, value);
|
|
123
|
+
if (font) page.fonts.add(new view.FontFace(theme["--dolly-html-font"].split(",")[0].trim().replace(/^(["'])(.*)\1$/, "$2"), font));
|
|
124
|
+
copy(new DOMParser().parseFromString(source, "text/html").body, page.body);
|
|
125
|
+
page.addEventListener("submit", event => {
|
|
113
126
|
event.preventDefault();
|
|
114
|
-
|
|
127
|
+
const fields = String(new URLSearchParams(new view.FormData(event.target, event.submitter)));
|
|
128
|
+
if (fields.length <= DOLLY_HTML_FIELDS_MAX) answer(fields);
|
|
129
|
+
else answer.tooLarge();
|
|
115
130
|
});
|
|
116
|
-
addEventListener("keydown", event => { if (event.key === "Escape")
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// The frame: an opaque origin (no allow-same-origin) that may run the page's
|
|
121
|
-
// script (its nonce) and submit forms to it, and fetches nothing, as its
|
|
122
|
-
// policy says. Without allow-popups, allow-top-navigation or allow-modals it
|
|
123
|
-
// opens nothing and navigates no other page.
|
|
124
|
-
function frameSource({ source, theme, font }) {
|
|
125
|
-
const nonce = crypto.randomUUID();
|
|
126
|
-
const policy = `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; form-action 'none'; base-uri 'none'`;
|
|
127
|
-
const literal = JSON.stringify({ source, theme, font, origin: location.origin }).replaceAll("<", "\\u003c");
|
|
128
|
-
return `<!doctype html><html><head><meta http-equiv="Content-Security-Policy" content="${policy}">` +
|
|
129
|
-
`<style nonce="${nonce}">${popupStyle}</style></head><body><script nonce="${nonce}">(${popup})(${literal})</script></body></html>`;
|
|
131
|
+
page.addEventListener("keydown", event => { if (event.key === "Escape") answer(null); });
|
|
132
|
+
page.querySelector("input:not([type=hidden]), select, textarea, button")?.focus();
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
const pageStyle = `
|
|
@@ -139,21 +142,17 @@ const pageStyle = `
|
|
|
139
142
|
color: var(--dolly-color, ${themeProperties["--dolly-color"]}); background: transparent;
|
|
140
143
|
border: 1px solid currentColor; font: 14px/1.5 var(--dolly-html-font, ${themeProperties["--dolly-html-font"]}); cursor: pointer; }
|
|
141
144
|
#html-popup iframe { flex: 1; width: 100%; border: 0; }
|
|
145
|
+
#html-popup .html-popup-caption { margin: 0.3rem 0.6rem 0; font-size: 0.8rem; opacity: 0.75; }
|
|
146
|
+
#html-popup .html-popup-notice { margin: 0 0.6rem 0.3rem; color: #ff9b84; }
|
|
142
147
|
`;
|
|
143
148
|
|
|
144
|
-
// The embedding's font file,
|
|
149
|
+
// The embedding's font file, fetched once by the page: the popup fetches nothing.
|
|
145
150
|
const fontFiles = new Map();
|
|
146
151
|
function fontFile(style) {
|
|
147
152
|
const location = /^url\(\s*(["']?)(.+)\1\s*\)$/.exec(style.getPropertyValue("--dolly-html-font-file").trim())?.[2];
|
|
148
153
|
if (!location) return null;
|
|
149
154
|
const url = new URL(location, document.baseURI).href;
|
|
150
|
-
if (!fontFiles.has(url)) fontFiles.set(url, fetch(url).then(response => response.ok ? response.arrayBuffer() : null).
|
|
151
|
-
if (!buffer) return null;
|
|
152
|
-
const bytes = new Uint8Array(buffer);
|
|
153
|
-
let text = "";
|
|
154
|
-
for (let offset = 0; offset < bytes.length; offset += 32768) text += String.fromCharCode(...bytes.subarray(offset, offset + 32768));
|
|
155
|
-
return btoa(text);
|
|
156
|
-
}).catch(() => null));
|
|
155
|
+
if (!fontFiles.has(url)) fontFiles.set(url, fetch(url).then(response => response.ok ? response.arrayBuffer() : null).catch(() => null));
|
|
157
156
|
return fontFiles.get(url);
|
|
158
157
|
}
|
|
159
158
|
|
|
@@ -161,7 +160,10 @@ function fontFile(style) {
|
|
|
161
160
|
// user submits a form of it or closes it (its Close button, or Escape), or
|
|
162
161
|
// the program withdraws it.
|
|
163
162
|
export function browser({ mount, keyboard, send }) {
|
|
164
|
-
|
|
163
|
+
// An adopted style sheet, which a page's style-src without 'unsafe-inline' allows.
|
|
164
|
+
const look = new CSSStyleSheet();
|
|
165
|
+
look.replaceSync(pageStyle);
|
|
166
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, look];
|
|
165
167
|
let shown = null;
|
|
166
168
|
function remove() {
|
|
167
169
|
shown?.element?.remove();
|
|
@@ -172,13 +174,6 @@ export function browser({ mount, keyboard, send }) {
|
|
|
172
174
|
send({ type: "html-answer", popup: shown.popup, fields });
|
|
173
175
|
remove();
|
|
174
176
|
}
|
|
175
|
-
function received(event) {
|
|
176
|
-
if (!shown?.frame || event.source !== shown.frame.contentWindow) return;
|
|
177
|
-
const { fields, close } = event.data ?? {};
|
|
178
|
-
if (close === true) answer(null);
|
|
179
|
-
else if (typeof fields === "string" && fields.length <= DOLLY_HTML_FIELDS_MAX) answer(fields);
|
|
180
|
-
}
|
|
181
|
-
addEventListener("message", received);
|
|
182
177
|
return {
|
|
183
178
|
messages: {
|
|
184
179
|
async "html-show"({ popup, document: source }) {
|
|
@@ -193,15 +188,26 @@ export function browser({ mount, keyboard, send }) {
|
|
|
193
188
|
if (shown?.popup !== popup) return;
|
|
194
189
|
const element = document.createElement("div"), box = document.createElement("div");
|
|
195
190
|
const close = Object.assign(document.createElement("button"), { type: "button", textContent: "Close" });
|
|
191
|
+
// What the page says itself, outside the program's frame: whose page it is, and
|
|
192
|
+
// why an answer did not go.
|
|
193
|
+
const caption = Object.assign(document.createElement("p"), { className: "html-popup-caption",
|
|
194
|
+
textContent: "A page a program in Dolly shows" });
|
|
195
|
+
const notice = Object.assign(document.createElement("p"), { className: "html-popup-notice", hidden: true });
|
|
196
|
+
notice.setAttribute("role", "alert");
|
|
196
197
|
const frame = Object.assign(document.createElement("iframe"), { title: "A page a program in Dolly shows" });
|
|
197
198
|
element.id = "html-popup";
|
|
198
199
|
element.setAttribute("role", "dialog");
|
|
199
200
|
element.setAttribute("aria-label", "A page a program in Dolly shows");
|
|
200
|
-
frame.setAttribute("sandbox",
|
|
201
|
-
frame.srcdoc =
|
|
201
|
+
frame.setAttribute("sandbox", frameSandbox);
|
|
202
|
+
frame.srcdoc = "<!doctype html>";
|
|
203
|
+
const reply = Object.assign(fields => answer(fields), { tooLarge() {
|
|
204
|
+
notice.textContent = `This answer is over ${DOLLY_HTML_FIELDS_MAX / 1024} KiB, more than a program takes: shorten it.`;
|
|
205
|
+
notice.hidden = false;
|
|
206
|
+
} });
|
|
207
|
+
frame.addEventListener("load", () => { if (shown?.frame === frame) build(frame, { source, theme, font }, reply); }, { once: true });
|
|
202
208
|
close.addEventListener("click", () => answer(null));
|
|
203
209
|
element.addEventListener("keydown", event => { if (event.key === "Escape") answer(null); });
|
|
204
|
-
box.append(close, frame);
|
|
210
|
+
box.append(caption, close, notice, frame);
|
|
205
211
|
element.append(box);
|
|
206
212
|
shown = { popup, element, frame };
|
|
207
213
|
mount.append(element);
|
|
@@ -211,7 +217,11 @@ export function browser({ mount, keyboard, send }) {
|
|
|
211
217
|
},
|
|
212
218
|
// The popup's keys are its own; Ctrl+C there still interrupts the program.
|
|
213
219
|
claimsKey: event => shown?.element?.contains(event.target) ? "interrupt" : false,
|
|
214
|
-
dispose() {
|
|
220
|
+
dispose() {
|
|
221
|
+
shown?.element?.remove();
|
|
222
|
+
shown = null;
|
|
223
|
+
document.adoptedStyleSheets = document.adoptedStyleSheets.filter(sheet => sheet !== look);
|
|
224
|
+
},
|
|
215
225
|
};
|
|
216
226
|
}
|
|
217
227
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.1.
|
|
2
|
+
"version": "0.1.6",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"name": "@daugasauron/dolly-host-html",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@daugasauron/dolly": "0.1.
|
|
7
|
+
"@daugasauron/dolly": "0.1.6"
|
|
8
8
|
},
|
|
9
9
|
"description": "Dolly's html@0 host module: show the user an HTML page or form; the user answers or closes it.",
|
|
10
10
|
"repository": {
|