single-file-core 1.5.87 → 1.5.89

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/core/helper.js CHANGED
@@ -139,8 +139,18 @@ export {
139
139
  NESTING_TRACK_ID_ATTRIBUTE_NAME
140
140
  };
141
141
 
142
+ let userScriptHandlerObserver;
143
+
142
144
  function initUserScriptHandler() {
143
- addEventListener(ON_INIT_USERSCRIPT_EVENT, ({ detail }) => globalThis[WAIT_FOR_USERSCRIPT_PROPERTY_NAME] = async (eventPrefixName, options) => {
145
+ addEventListener(ON_INIT_USERSCRIPT_EVENT, onInitUserScript);
146
+ if (!userScriptHandlerObserver) {
147
+ userScriptHandlerObserver = new MutationObserver(initUserScriptHandler);
148
+ userScriptHandlerObserver.observe(globalThis.document, { childList: true });
149
+ }
150
+ }
151
+
152
+ function onInitUserScript({ detail }) {
153
+ globalThis[WAIT_FOR_USERSCRIPT_PROPERTY_NAME] = async (eventPrefixName, options) => {
144
154
  const userScriptOptions = Object.assign({}, options);
145
155
  delete userScriptOptions.win;
146
156
  delete userScriptOptions.doc;
@@ -181,8 +191,7 @@ function initUserScriptHandler() {
181
191
  } else {
182
192
  resolvePromiseResponse();
183
193
  }
184
- });
185
- new MutationObserver(initUserScriptHandler).observe(globalThis.document, { childList: true });
194
+ };
186
195
  }
187
196
 
188
197
  function initDoc(doc) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "single-file-core",
3
- "version": "1.5.87",
3
+ "version": "1.5.89",
4
4
  "description": "SingleFile Core",
5
5
  "author": "Gildas Lormeau",
6
6
  "license": "AGPL-3.0-or-later",
@@ -21,13 +21,13 @@
21
21
  * Source.
22
22
  */
23
23
 
24
- /* global DOMParser */
24
+ /* global DOMParser, setTimeout */
25
25
 
26
26
  export {
27
27
  display
28
28
  };
29
29
 
30
- async function display(document, docContent, { disableFramePointerEvents } = {}) {
30
+ async function display(document, docContent, { disableFramePointerEvents, inPlace } = {}) {
31
31
  docContent = docContent.replace(/<noscript/gi, "<template disabled-noscript");
32
32
  docContent = docContent.replace(/<\/noscript/gi, "</template");
33
33
  const doc = (new DOMParser()).parseFromString(docContent, "text/html");
@@ -38,10 +38,48 @@ async function display(document, docContent, { disableFramePointerEvents } = {})
38
38
  element.style.setProperty(pointerEvents, "none", "important");
39
39
  });
40
40
  }
41
- document.open();
42
- document.write(getDoctypeString(doc));
43
- document.write(doc.documentElement.outerHTML);
44
- document.close();
41
+ // the in-place swap avoids the document churn of document.open() but cannot
42
+ // change the compat mode and does not execute script elements; it morphs
43
+ // documentElement instead of replacing it so that document-level childList
44
+ // observers (e.g. extension content scripts watching for document.open())
45
+ // do not fire on every rendered page
46
+ if (inPlace && doc.compatMode == document.compatMode && !doc.querySelector("script")) {
47
+ // stylesheet links inserted from script are not render-blocking; preload
48
+ // them while the previous page is still displayed and keep the new page
49
+ // hidden until they are applied to avoid a flash of unstyled content
50
+ await Promise.all(Array.from(doc.querySelectorAll("link[rel~=stylesheet][href]")).map(linkElement => new Promise(resolve => {
51
+ const preloadElement = document.createElement("link");
52
+ preloadElement.rel = "preload";
53
+ preloadElement.as = "style";
54
+ preloadElement.href = linkElement.getAttribute("href");
55
+ preloadElement.onload = resolve;
56
+ preloadElement.onerror = resolve;
57
+ document.head.appendChild(preloadElement);
58
+ setTimeout(resolve, 500);
59
+ })));
60
+ const documentElement = document.documentElement;
61
+ const newDocumentElement = document.adoptNode(doc.documentElement);
62
+ while (documentElement.attributes.length) {
63
+ documentElement.removeAttribute(documentElement.attributes[0].name);
64
+ }
65
+ Array.from(newDocumentElement.attributes).forEach(attribute => documentElement.setAttribute(attribute.name, attribute.value));
66
+ documentElement.replaceChildren(...newDocumentElement.childNodes);
67
+ if (document.querySelector("link[rel~=stylesheet][href]")) {
68
+ const hideStyleElement = document.createElement("style");
69
+ hideStyleElement.textContent = "html{visibility:hidden}";
70
+ document.head.appendChild(hideStyleElement);
71
+ const start = Date.now();
72
+ while (Date.now() - start < 500 && Array.from(document.querySelectorAll("link[rel~=stylesheet][href]")).some(linkElement => !linkElement.sheet)) {
73
+ await new Promise(resolve => setTimeout(resolve, 10));
74
+ }
75
+ hideStyleElement.remove();
76
+ }
77
+ } else {
78
+ document.open();
79
+ document.write(getDoctypeString(doc));
80
+ document.write(doc.documentElement.outerHTML);
81
+ document.close();
82
+ }
45
83
  document.querySelectorAll("template[disabled-noscript]").forEach(element => {
46
84
  const noscriptElement = document.createElement("noscript");
47
85
  element.removeAttribute("disabled-noscript");
@@ -21,8 +21,6 @@
21
21
  * Source.
22
22
  */
23
23
 
24
- /* global globalThis */
25
-
26
24
  export {
27
25
  router
28
26
  };
@@ -31,10 +29,31 @@ async function router(content, { extract, display }) {
31
29
  const PAGES_PREFIX = "pages/";
32
30
  const PAGES_FILENAME = "sfz-pages.json";
33
31
  const ROUTE_PREFIX = "#sfz/";
34
- const { zip, document, location } = globalThis;
32
+ const TARGET_ATTRIBUTE = "data-sfz-target";
33
+ const TARGET_PSEUDO_CLASS = /:target(?![\w-])/g;
34
+ const VISITED_ATTRIBUTE = "data-sfz-visited";
35
+ const VISITED_PSEUDO_CLASS = /:visited(?![\w-])/g;
36
+ const VISITED_DEFAULT_COLOR = "#551a8b";
37
+ const UNARCHIVED_ATTRIBUTE = "data-sfz-unarchived";
38
+ // relative units, currentColor and opacity keep the marker legible on any
39
+ // page theme, and \2197 stays ASCII in the windows-1252 prelude
40
+ const UNARCHIVED_STYLE = "a[" + UNARCHIVED_ATTRIBUTE + "]::after{content:\" \\2197\";font-size:.75em;opacity:.7}";
41
+ const UNARCHIVED_TITLE = "Not saved in this archive";
42
+ const UNARCHIVED_PROTOCOLS = ["http:", "https:"];
43
+ const PREFETCH_DELAY = 100;
44
+ const { zip, document, location, history, CSS, setTimeout, clearTimeout } = globalThis;
35
45
  const cache = new Map();
36
46
  const urlToPath = new Map();
37
- let currentPath;
47
+ const scrollStates = new Map();
48
+ const visitedPaths = new Set();
49
+ const sessionKey = Math.random().toString(36).substring(2);
50
+ let currentPath, currentEntryId, targetStyleElement, prefetchTimeout;
51
+ let nextEntryId = 0;
52
+ try {
53
+ history.scrollRestoration = "manual";
54
+ } catch {
55
+ // ignored
56
+ }
38
57
  zip.configure({ useWebWorkers: true });
39
58
  const zipReader = new zip.ZipReader(content.readUint8Array ? content : new zip.BlobReader(content));
40
59
  const entries = await zipReader.getEntries();
@@ -42,7 +61,9 @@ async function router(content, { extract, display }) {
42
61
  if (!pagesEntry) {
43
62
  throw new Error("Pages data not found");
44
63
  }
45
- const { pages } = JSON.parse(await pagesEntry.getData(new zip.TextWriter()));
64
+ const manifest = JSON.parse(await pagesEntry.getData(new zip.TextWriter()));
65
+ const { pages } = manifest;
66
+ const aliases = new Map(Object.entries(manifest.aliases || {}));
46
67
  pages.forEach(page => {
47
68
  urlToPath.set(stripFragment(page.url), page.path);
48
69
  if (page.originalUrls) {
@@ -50,60 +71,396 @@ async function router(content, { extract, display }) {
50
71
  }
51
72
  });
52
73
  attachListeners();
53
- return renderRoute();
74
+ currentEntryId = getEntryId();
75
+ if (currentEntryId === null) {
76
+ currentEntryId = assignEntryId();
77
+ }
78
+ return renderRoute(true);
54
79
 
55
80
  // document.open() removes the listeners of the document and of its window,
56
81
  // re-attaching identical function references is idempotent
57
82
  function attachListeners() {
58
83
  globalThis.addEventListener("click", interceptClick, true);
84
+ globalThis.addEventListener("auxclick", interceptClick, true);
85
+ globalThis.addEventListener("mouseover", prefetchOnHover, true);
59
86
  globalThis.addEventListener("hashchange", onHashChange);
60
87
  }
61
88
 
62
89
  function onHashChange() {
63
- if (location.hash.startsWith(ROUTE_PREFIX) || !location.hash) {
64
- renderRoute().catch(error => globalThis.console.error(error));
65
- }
90
+ navigate().catch(error => globalThis.console.error(error));
66
91
  }
67
92
 
68
93
  function interceptClick(event) {
69
- let node = event.target;
70
- while (node && node.tagName != "A") {
71
- node = node.parentNode;
94
+ if (event.type == "auxclick" && event.button != 1) {
95
+ return;
72
96
  }
97
+ const node = findAnchor(event.target);
73
98
  if (node && node.href) {
74
- const path = urlToPath.get(stripFragment(node.href));
99
+ const fragment = getFragment(node.href);
100
+ let path = urlToPath.get(stripFragment(node.href));
101
+ if (path === undefined && fragment && stripFragment(node.href) == stripFragment(location.href)) {
102
+ path = currentPath;
103
+ }
75
104
  if (path !== undefined) {
76
105
  event.preventDefault();
77
- location.hash = ROUTE_PREFIX + path;
106
+ // modified and middle clicks open the archive deep link in a new
107
+ // tab instead of letting the browser open the live site URL
108
+ if (event.type == "auxclick" || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
109
+ globalThis.open(stripFragment(location.href) + ROUTE_PREFIX + path + (fragment || ""));
110
+ } else {
111
+ // the fragment stays encoded inside the route hash so that
112
+ // reloading or sharing the URL comes back to the same page
113
+ const previousHash = location.hash;
114
+ location.hash = ROUTE_PREFIX + path + (fragment || "");
115
+ if (location.hash == previousHash && fragment) {
116
+ clearTarget();
117
+ scrollToFragment(fragment);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ async function navigate() {
125
+ clearTarget();
126
+ scrollStates.set(currentEntryId, captureScrollState());
127
+ const continuityScrolls = captureElementScrolls();
128
+ const entryId = getEntryId();
129
+ const { routed, path, fragment } = parseRoute();
130
+ const willRender = routed && path != currentPath && Boolean(pages.find(page => page.path == path));
131
+ // the whole render and scroll sequence runs inside the view transition
132
+ // so that the crossfade ends on the final scroll position
133
+ if (willRender && document.startViewTransition && !prefersReducedMotion()) {
134
+ await document.startViewTransition(update).updateCallbackDone;
135
+ } else {
136
+ await update();
137
+ }
138
+
139
+ async function update() {
140
+ const rendered = await renderRoute();
141
+ if (rendered) {
142
+ focusContent();
143
+ }
144
+ applyScrollState(rendered);
145
+ }
146
+
147
+ function applyScrollState(rendered) {
148
+ if (entryId !== null) {
149
+ currentEntryId = entryId;
150
+ const scrollState = scrollStates.get(entryId);
151
+ if (scrollState) {
152
+ applyElementScrolls(scrollState.elements);
153
+ globalThis.scrollTo(scrollState.x, scrollState.y);
154
+ }
155
+ if (fragment) {
156
+ const target = findFragmentTarget(fragment);
157
+ if (target) {
158
+ markTarget(target);
159
+ }
160
+ }
161
+ } else {
162
+ currentEntryId = assignEntryId();
163
+ if (rendered) {
164
+ applyElementScrolls(continuityScrolls);
165
+ }
166
+ if (fragment) {
167
+ scrollToFragment(fragment);
168
+ } else if (rendered) {
169
+ globalThis.scrollTo(0, 0);
170
+ }
78
171
  }
79
172
  }
80
173
  }
81
174
 
82
- async function renderRoute() {
83
- const hash = decodeURIComponent(location.hash);
84
- const path = hash.startsWith(ROUTE_PREFIX) ? hash.substring(ROUTE_PREFIX.length) : pages[0].path;
175
+ async function renderRoute(initial) {
176
+ const { routed, path, fragment } = parseRoute();
177
+ if (!routed && !initial) {
178
+ return false;
179
+ }
85
180
  if (path == currentPath || !pages.find(page => page.path == path)) {
86
- return;
181
+ return false;
87
182
  }
88
183
  const docContent = await getPageContent(path);
89
184
  currentPath = path;
90
- await display(document, docContent);
185
+ await display(document, docContent, { inPlace: true });
91
186
  attachListeners();
187
+ visitedPaths.add(path);
188
+ markVisitedLinks();
189
+ if (manifest.markUnarchivedLinks) {
190
+ markUnarchivedLinks();
191
+ }
192
+ if (initial) {
193
+ if (fragment) {
194
+ scrollToFragment(fragment);
195
+ } else if (!routed && location.hash) {
196
+ scrollToFragment(location.hash);
197
+ }
198
+ }
199
+ return true;
200
+ }
201
+
202
+ function parseRoute() {
203
+ const hash = location.hash;
204
+ const routed = !hash || hash.startsWith(ROUTE_PREFIX);
205
+ let path = pages[0].path;
206
+ let fragment;
207
+ if (routed && hash) {
208
+ const route = hash.substring(ROUTE_PREFIX.length);
209
+ const indexFragment = route.indexOf("#");
210
+ path = decodeURIComponent(indexFragment == -1 ? route : route.substring(0, indexFragment));
211
+ fragment = indexFragment == -1 ? undefined : route.substring(indexFragment);
212
+ }
213
+ return { routed, path, fragment };
92
214
  }
93
215
 
94
- async function getPageContent(path) {
216
+ // the cache stores promises so that a click during a hover prefetch awaits
217
+ // the extraction in flight instead of starting a second one
218
+ function getPageContent(path) {
95
219
  if (!cache.has(path)) {
96
- const pageEntries = entries.filter(entry => path == "" ?
97
- !entry.filename.startsWith(PAGES_PREFIX) && entry.filename != PAGES_FILENAME :
98
- entry.filename.startsWith(path));
99
- const { docContent } = await extract(null, { entries: pageEntries, pagePath: path });
100
- cache.set(path, docContent);
220
+ const contentPromise = extractPageContent(path);
221
+ contentPromise.catch(() => cache.delete(path));
222
+ cache.set(path, contentPromise);
101
223
  }
102
224
  return cache.get(path);
103
225
  }
104
226
 
227
+ async function extractPageContent(path) {
228
+ const pageEntries = entries
229
+ .filter(entry => belongsToPage(entry.filename, path) && !aliases.has(entry.filename))
230
+ .concat(getAliasEntries(path));
231
+ const { docContent } = await extract(null, { entries: pageEntries, pagePath: path });
232
+ return docContent;
233
+ }
234
+
235
+ // deduplicated resources exist in the zip as symlink stand-ins for external
236
+ // extractors, they are resolved from the manifest alias map when extracting
237
+ function getAliasEntries(path) {
238
+ return Array.from(aliases)
239
+ .filter(([filename]) => belongsToPage(filename, path))
240
+ .map(([filename, canonicalFilename]) => {
241
+ const entry = entries.find(entry => entry.filename == canonicalFilename);
242
+ return entry && {
243
+ filename,
244
+ comment: entry.comment,
245
+ encrypted: entry.encrypted,
246
+ uncompressedSize: entry.uncompressedSize,
247
+ getData: (writer, options) => entry.getData(writer, options)
248
+ };
249
+ })
250
+ .filter(Boolean);
251
+ }
252
+
253
+ function belongsToPage(filename, path) {
254
+ return path == "" ?
255
+ !filename.startsWith(PAGES_PREFIX) && filename != PAGES_FILENAME :
256
+ filename.startsWith(path);
257
+ }
258
+
259
+ function prefetchOnHover(event) {
260
+ const node = findAnchor(event.target);
261
+ if (node && node.href) {
262
+ const path = urlToPath.get(stripFragment(node.href));
263
+ if (path !== undefined && path != currentPath && !cache.has(path)) {
264
+ clearTimeout(prefetchTimeout);
265
+ prefetchTimeout = setTimeout(() => getPageContent(path).catch(() => { }), PREFETCH_DELAY);
266
+ }
267
+ }
268
+ }
269
+
270
+ function findAnchor(node) {
271
+ while (node && node.tagName != "A") {
272
+ node = node.parentNode;
273
+ }
274
+ return node;
275
+ }
276
+
277
+ function prefersReducedMotion() {
278
+ return Boolean(globalThis.matchMedia && globalThis.matchMedia("(prefers-reduced-motion: reduce)").matches);
279
+ }
280
+
281
+ function getEntryId() {
282
+ const state = history.state;
283
+ if (state && state.sfzSession == sessionKey && typeof state.sfzEntry == "number") {
284
+ return state.sfzEntry;
285
+ }
286
+ return null;
287
+ }
288
+
289
+ function assignEntryId() {
290
+ const entryId = nextEntryId++;
291
+ try {
292
+ history.replaceState({ sfzSession: sessionKey, sfzEntry: entryId }, "");
293
+ } catch {
294
+ // ignored
295
+ }
296
+ return entryId;
297
+ }
298
+
299
+ function captureScrollState() {
300
+ return { x: globalThis.scrollX, y: globalThis.scrollY, elements: captureElementScrolls() };
301
+ }
302
+
303
+ function captureElementScrolls() {
304
+ const elementScrolls = [];
305
+ document.querySelectorAll("*").forEach(element => {
306
+ if (element.scrollTop || element.scrollLeft) {
307
+ const path = getElementPath(element);
308
+ if (path) {
309
+ elementScrolls.push({ path, top: element.scrollTop, left: element.scrollLeft });
310
+ }
311
+ }
312
+ });
313
+ return elementScrolls;
314
+ }
315
+
316
+ function applyElementScrolls(elementScrolls) {
317
+ elementScrolls.forEach(({ path, top, left }) => {
318
+ let element;
319
+ try {
320
+ element = document.querySelector(path);
321
+ } catch {
322
+ // ignored
323
+ }
324
+ if (element) {
325
+ element.scrollTop = top;
326
+ element.scrollLeft = left;
327
+ }
328
+ });
329
+ }
330
+
331
+ function getElementPath(element) {
332
+ const segments = [];
333
+ while (element && element.parentElement) {
334
+ if (element.id && CSS) {
335
+ segments.unshift("#" + CSS.escape(element.id));
336
+ return segments.join(">");
337
+ }
338
+ const parent = element.parentElement;
339
+ segments.unshift(element.tagName + ":nth-child(" + (Array.from(parent.children).indexOf(element) + 1) + ")");
340
+ element = parent;
341
+ }
342
+ return segments.join(">");
343
+ }
344
+
345
+ // the fragment is scrolled to from script because the real fragment is the
346
+ // route hash, which also means :target never matches in rendered pages; the
347
+ // :target rules of the page are cloned against a marker attribute instead
348
+ function scrollToFragment(fragment) {
349
+ const target = findFragmentTarget(fragment);
350
+ if (target) {
351
+ markTarget(target);
352
+ target.scrollIntoView();
353
+ }
354
+ }
355
+
356
+ function findFragmentTarget(fragment) {
357
+ const name = decodeURIComponent(fragment.substring(1));
358
+ let target = document.getElementById(name);
359
+ if (!target && CSS) {
360
+ target = document.querySelector("a[name=" + CSS.escape(name) + "]");
361
+ }
362
+ return target;
363
+ }
364
+
365
+ function markTarget(element) {
366
+ const cssText = getPseudoRules(TARGET_PSEUDO_CLASS, TARGET_ATTRIBUTE);
367
+ if (cssText) {
368
+ element.setAttribute(TARGET_ATTRIBUTE, "");
369
+ targetStyleElement = document.createElement("style");
370
+ targetStyleElement.textContent = cssText;
371
+ document.head.appendChild(targetStyleElement);
372
+ }
373
+ }
374
+
375
+ // :visited itself is privacy-gated, so visited routes are stamped with an
376
+ // attribute styled by the page's own :visited rules over a default color
377
+ function markVisitedLinks() {
378
+ const styleElement = document.createElement("style");
379
+ styleElement.textContent = "a[" + VISITED_ATTRIBUTE + "]{color:" + VISITED_DEFAULT_COLOR + "}" +
380
+ getPseudoRules(VISITED_PSEUDO_CLASS, VISITED_ATTRIBUTE);
381
+ document.head.appendChild(styleElement);
382
+ document.querySelectorAll("a[href]").forEach(anchorElement => {
383
+ const path = urlToPath.get(stripFragment(anchorElement.href));
384
+ if (path !== undefined && visitedPaths.has(path)) {
385
+ anchorElement.setAttribute(VISITED_ATTRIBUTE, "");
386
+ }
387
+ });
388
+ }
389
+
390
+ // links that leave the archive are stamped so that the reader knows before
391
+ // clicking; opt-in at packaging time because the marker alters the rendering
392
+ function markUnarchivedLinks() {
393
+ const styleElement = document.createElement("style");
394
+ styleElement.textContent = UNARCHIVED_STYLE;
395
+ document.head.appendChild(styleElement);
396
+ document.querySelectorAll("a[href]").forEach(anchorElement => {
397
+ if (UNARCHIVED_PROTOCOLS.includes(anchorElement.protocol) &&
398
+ urlToPath.get(stripFragment(anchorElement.href)) === undefined &&
399
+ stripFragment(anchorElement.href) != stripFragment(location.href)) {
400
+ anchorElement.setAttribute(UNARCHIVED_ATTRIBUTE, "");
401
+ if (!anchorElement.hasAttribute("title")) {
402
+ anchorElement.setAttribute("title", UNARCHIVED_TITLE);
403
+ }
404
+ }
405
+ });
406
+ }
407
+
408
+ function clearTarget() {
409
+ if (targetStyleElement) {
410
+ const markedElement = document.querySelector("[" + TARGET_ATTRIBUTE + "]");
411
+ if (markedElement) {
412
+ markedElement.removeAttribute(TARGET_ATTRIBUTE);
413
+ }
414
+ targetStyleElement.remove();
415
+ targetStyleElement = undefined;
416
+ }
417
+ }
418
+
419
+ function getPseudoRules(pseudoRegExp, attributeName) {
420
+ let cssText = "";
421
+ Array.from(document.styleSheets).forEach(styleSheet => {
422
+ try {
423
+ cssText += getPseudoRulesText(styleSheet.cssRules, pseudoRegExp, attributeName);
424
+ } catch {
425
+ // ignored
426
+ }
427
+ });
428
+ return cssText;
429
+ }
430
+
431
+ function getPseudoRulesText(cssRules, pseudoRegExp, attributeName) {
432
+ let cssText = "";
433
+ Array.from(cssRules).forEach(cssRule => {
434
+ if (cssRule.cssRules && cssRule.cssRules.length) {
435
+ const innerCssText = getPseudoRulesText(cssRule.cssRules, pseudoRegExp, attributeName);
436
+ if (innerCssText) {
437
+ cssText += cssRule.cssText.substring(0, cssRule.cssText.indexOf("{") + 1) + innerCssText + "}";
438
+ }
439
+ } else if (cssRule.selectorText) {
440
+ const selectorText = cssRule.selectorText.replace(pseudoRegExp, "[" + attributeName + "]");
441
+ if (selectorText != cssRule.selectorText) {
442
+ cssText += selectorText + "{" + cssRule.style.cssText + "}";
443
+ }
444
+ }
445
+ });
446
+ return cssText;
447
+ }
448
+
449
+ function focusContent() {
450
+ const headingElement = document.querySelector("h1") || document.body;
451
+ if (headingElement) {
452
+ headingElement.setAttribute("tabindex", "-1");
453
+ headingElement.focus({ preventScroll: true });
454
+ }
455
+ }
456
+
105
457
  function stripFragment(url) {
106
458
  const indexFragment = url.indexOf("#");
107
459
  return indexFragment == -1 ? url : url.substring(0, indexFragment);
108
460
  }
461
+
462
+ function getFragment(url) {
463
+ const indexFragment = url.indexOf("#");
464
+ return indexFragment == -1 ? undefined : url.substring(indexFragment);
465
+ }
109
466
  }
@@ -100,33 +100,35 @@ export {
100
100
  };
101
101
 
102
102
  function init() {
103
- globalThis.addEventListener("message", async event => {
104
- if (typeof event.data == "string" && event.data.startsWith(MESSAGE_PREFIX)) {
105
- event.preventDefault();
106
- event.stopPropagation();
107
- const message = JSON.parse(event.data.substring(MESSAGE_PREFIX.length));
108
- if (message.method == INIT_REQUEST_MESSAGE) {
109
- if (event.source) {
110
- sendMessage(event.source, { method: ACK_INIT_REQUEST_MESSAGE, windowId: message.windowId, sessionId: message.sessionId });
111
- }
112
- if (!TOP_WINDOW) {
113
- globalThis.stop();
114
- if (message.options.loadDeferredImages) {
115
- lazy.process(message.options);
116
- }
117
- await initRequestAsync(message);
103
+ globalThis.addEventListener("message", onMessage, true);
104
+ }
105
+
106
+ async function onMessage(event) {
107
+ if (typeof event.data == "string" && event.data.startsWith(MESSAGE_PREFIX)) {
108
+ event.preventDefault();
109
+ event.stopPropagation();
110
+ const message = JSON.parse(event.data.substring(MESSAGE_PREFIX.length));
111
+ if (message.method == INIT_REQUEST_MESSAGE) {
112
+ if (event.source) {
113
+ sendMessage(event.source, { method: ACK_INIT_REQUEST_MESSAGE, windowId: message.windowId, sessionId: message.sessionId });
114
+ }
115
+ if (!TOP_WINDOW) {
116
+ globalThis.stop();
117
+ if (message.options.loadDeferredImages) {
118
+ lazy.process(message.options);
118
119
  }
119
- } else if (message.method == ACK_INIT_REQUEST_MESSAGE) {
120
- clearFrameTimeout("requestTimeouts", message.sessionId, message.windowId);
121
- createFrameResponseTimeout(message.sessionId, message.windowId);
122
- } else if (message.method == CLEANUP_REQUEST_MESSAGE) {
123
- cleanupRequest(message);
124
- } else if (message.method == INIT_RESPONSE_MESSAGE && sessions.get(message.sessionId)) {
125
- const port = event.ports[0];
126
- port.onmessage = event => initResponse(event.data);
120
+ await initRequestAsync(message);
127
121
  }
122
+ } else if (message.method == ACK_INIT_REQUEST_MESSAGE) {
123
+ clearFrameTimeout("requestTimeouts", message.sessionId, message.windowId);
124
+ createFrameResponseTimeout(message.sessionId, message.windowId);
125
+ } else if (message.method == CLEANUP_REQUEST_MESSAGE) {
126
+ cleanupRequest(message);
127
+ } else if (message.method == INIT_RESPONSE_MESSAGE && sessions.get(message.sessionId)) {
128
+ const port = event.ports[0];
129
+ port.onmessage = event => initResponse(event.data);
128
130
  }
129
- }, true);
131
+ }
130
132
  }
131
133
 
132
134
  function getAsync(options) {