@rathnasgala2/template 2.2.0 → 3.0.1

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.
@@ -1,136 +1,123 @@
1
1
  /**
2
- * The exact deterministic pre-paint appearance bootstrap script: a versioned
3
- * deterministic pre-paint bootstrap owned by `template`/`core` that resolves
4
- * local selection plus system preference and sets
5
- * the root state before first themed paint. It is covered by the artifact
6
- * digest, is CSP-compatible under `script-src 'self'` (therefore an external
7
- * file, not inline), touches no network, account, content or unrelated
8
- * storage key, and cannot hide the document indefinitely.
2
+ * The exact deterministic site script: the one and only browser script this
3
+ * renderer ever emits, an external same-origin file (CSP `script-src 'self'`)
4
+ * that touches no cookie, account or tracking identifier and no storage key
5
+ * other than the two namespaced ones named below. Every page is fully
6
+ * readable and navigable with JavaScript off; this script only enhances.
9
7
  *
10
- * This is the one and only browser bootstrap this renderer ever emits: the
11
- * appearance controller is the only browser
12
- * bootstrap it ships. Every name and value it embeds is read from
13
- * `internal/appearance/contract.js`, the one module that owns them, so the
14
- * server-rendered control markup and this script can never drift apart on a
15
- * literal.
8
+ * Two independent phases inside the one file:
16
9
  *
17
- * Two independent phases, both inside the same one external file so the
18
- * template never ships a second script:
10
+ * 1. A synchronous, top-level phase that runs when this blocking
11
+ * (non-`defer`/`async`/`module`) `<script src>` executes in `<head>`,
12
+ * before `<body>` is parsed and so before any themed paint: it resolves
13
+ * the stored colour-mode selection (or `system`) against
14
+ * `prefers-color-scheme` and sets the two root attributes, subscribes to
15
+ * live system-preference changes (applied only while the selection is
16
+ * `system`), and registers the cross-document view-transition listeners
17
+ * (`pageswap` / `pagereveal` fire before `DOMContentLoaded`). It never
18
+ * mutates `documentElement.style`, so it can never hide the document.
19
+ * 2. A `DOMContentLoaded` phase of independent enhancements, each wrapped in
20
+ * its own try/catch and each a no-op when its server-rendered markup is
21
+ * missing: the colour-mode toggle (system, light, dark, with a circular
22
+ * reveal via a same-document view transition), the search dialog (lazy
23
+ * `fetch` of the static index named by `data-search-index`; `/` and
24
+ * Ctrl/Cmd+K; arrow keys and Enter), code blocks (language label and Copy
25
+ * button), contents highlighting (IntersectionObserver on `data-toc`),
26
+ * the share group (copy link, bookmark in `localStorage`), and a
27
+ * reading-progress fallback where scroll-driven animations are
28
+ * unsupported.
19
29
  *
20
- * 1. A synchronous, top-level phase that runs immediately when this
21
- * blocking, non-`defer`/non-`async`/non-`module` `<script src>` executes
22
- * in `<head>` — before `<body>` is parsed, and so before any themed
23
- * paint. It resolves the stored selection (or `system`, gracefully
24
- * substituted for a missing/unreadable/invalid stored value — storage
25
- * failure never leaves the session unusable) against
26
- * `prefers-color-scheme` and sets both root attributes.
27
- * 2. A `DOMContentLoaded`-deferred phase that wires the server-rendered
28
- * `<select>` control's `change` event once `<body>` exists (it cannot run
29
- * any earlier: the control has not been parsed yet at phase 1's own
30
- * execution point) and subscribes to live `prefers-color-scheme` changes,
31
- * updating the resolved attribute only while the current selection is
32
- * exactly `system`: an explicit `light`/`dark` choice is not
33
- * overridden by later system changes.
30
+ * Security: every piece of text the script puts in the DOM is assigned with
31
+ * `textContent` or `setAttribute`; fetched search data is never parsed as
32
+ * HTML. The only `innerHTML` assignments are the script's own constant icon
33
+ * path strings. Localized strings are never embedded here: they are read
34
+ * from `data-*` attributes the server renders on `.g-toast` and on the
35
+ * toggle button.
34
36
  *
35
- * Deliberately absent: no `document.documentElement.style` mutation of any
36
- * kind (in particular, no hide-until-ready visibility toggle — this script
37
- * must never hide the document indefinitely, and never needs to,
38
- * since it only ever sets attributes, never inline style), no network
39
- * request, no cookie, no account/tracking identifier, no storage key other
40
- * than {@link APPEARANCE_STORAGE_KEY}.
37
+ * Every name and value below is read from `internal/appearance/contract.js`,
38
+ * the one module that owns them, so the server-rendered markup and this
39
+ * script can never drift apart on a literal.
41
40
  */
42
41
 
42
+ import { minifyScript } from './minify-script.js';
43
43
  import {
44
44
  APPEARANCE_MODE_DARK,
45
45
  APPEARANCE_MODE_LIGHT,
46
46
  APPEARANCE_MODE_SYSTEM,
47
47
  APPEARANCE_RESOLVED_MODE_ATTRIBUTE,
48
48
  APPEARANCE_SELECTION_ATTRIBUTE,
49
- APPEARANCE_SELECT_ID,
49
+ APPEARANCE_TOGGLE_ID,
50
50
  APPEARANCE_STORAGE_KEY,
51
51
  } from './contract.js';
52
52
 
53
+ /** @type {string} the `localStorage` key holding saved article paths. */
54
+ export const SAVED_STORAGE_KEY = 'gala:saved:v1';
55
+
53
56
  /**
54
- * The exact bootstrap script source, byte-for-byte deterministic across
55
- * builds: every interpolated value below is a fixed literal from
56
- * `contract.js`, never a per-publication or per-build input.
57
+ * The exact script source, byte-for-byte deterministic across builds: every
58
+ * interpolated value is a fixed literal from `contract.js`.
57
59
  *
58
60
  * @type {string}
59
61
  */
60
- export const APPEARANCE_BOOTSTRAP_SCRIPT_SOURCE = `(function () {
62
+ const READABLE_SCRIPT_SOURCE = String.raw`(function () {
61
63
  'use strict';
62
64
  var STORAGE_KEY = ${JSON.stringify(APPEARANCE_STORAGE_KEY)};
65
+ var SAVED_KEY = ${JSON.stringify(SAVED_STORAGE_KEY)};
63
66
  var RESOLVED_ATTRIBUTE = ${JSON.stringify(APPEARANCE_RESOLVED_MODE_ATTRIBUTE)};
64
67
  var SELECTION_ATTRIBUTE = ${JSON.stringify(APPEARANCE_SELECTION_ATTRIBUTE)};
65
- var SELECT_ID = ${JSON.stringify(APPEARANCE_SELECT_ID)};
68
+ var TOGGLE_ID = ${JSON.stringify(APPEARANCE_TOGGLE_ID)};
66
69
  var MODE_LIGHT = ${JSON.stringify(APPEARANCE_MODE_LIGHT)};
67
70
  var MODE_DARK = ${JSON.stringify(APPEARANCE_MODE_DARK)};
68
71
  var MODE_SYSTEM = ${JSON.stringify(APPEARANCE_MODE_SYSTEM)};
72
+ var root = document.documentElement;
73
+ var NS = 'http://www.w3.org/2000/svg';
69
74
 
70
75
  function isKnownMode(value) {
71
76
  return value === MODE_LIGHT || value === MODE_DARK || value === MODE_SYSTEM;
72
77
  }
73
-
74
78
  function readStoredSelection() {
75
79
  try {
76
80
  var value = window.localStorage.getItem(STORAGE_KEY);
77
- if (isKnownMode(value)) {
78
- return value;
79
- }
81
+ if (isKnownMode(value)) return value;
80
82
  } catch (error) {
81
- // Storage blocked (private mode, disabled site data, quota): the
82
- // session stays usable and this load falls back to MODE_SYSTEM below.
83
+ // Storage blocked: fall back to MODE_SYSTEM below.
83
84
  }
84
85
  return MODE_SYSTEM;
85
86
  }
86
-
87
87
  function writeStoredSelection(mode) {
88
88
  try {
89
89
  window.localStorage.setItem(STORAGE_KEY, mode);
90
90
  } catch (error) {
91
- // Storage blocked: the selection simply does not persist past this
92
- // load; the visible page state this call site also applies still
93
- // updates normally.
91
+ // Storage blocked: the choice simply does not persist.
94
92
  }
95
93
  }
96
-
97
- function systemPrefersDark() {
94
+ function matches(query) {
98
95
  try {
99
- return !!(
100
- window.matchMedia &&
101
- window.matchMedia('(prefers-color-scheme: dark)').matches
102
- );
96
+ return !!(window.matchMedia && window.matchMedia(query).matches);
103
97
  } catch (error) {
104
98
  return false;
105
99
  }
106
100
  }
107
-
108
101
  function resolveMode(selection) {
109
- if (selection === MODE_LIGHT || selection === MODE_DARK) {
110
- return selection;
111
- }
112
- return systemPrefersDark() ? MODE_DARK : MODE_LIGHT;
102
+ if (selection === MODE_LIGHT || selection === MODE_DARK) return selection;
103
+ return matches('(prefers-color-scheme: dark)') ? MODE_DARK : MODE_LIGHT;
113
104
  }
114
-
115
105
  function applySelection(selection) {
116
- var root = document.documentElement;
117
106
  root.setAttribute(SELECTION_ATTRIBUTE, selection);
118
107
  root.setAttribute(RESOLVED_ATTRIBUTE, resolveMode(selection));
119
108
  }
109
+ function reducedMotion() {
110
+ return matches('(prefers-reduced-motion: reduce)');
111
+ }
120
112
 
121
113
  // Phase 1 (synchronous, pre-paint): resolve and apply immediately.
122
114
  applySelection(readStoredSelection());
123
115
 
124
- // Live system-preference changes, only while the reader's own selection is
125
- // still exactly "system" (an explicit light/dark choice is never
126
- // overridden by a later system change).
127
116
  if (window.matchMedia) {
128
117
  try {
129
118
  var media = window.matchMedia('(prefers-color-scheme: dark)');
130
119
  var onSystemChange = function () {
131
- if (readStoredSelection() === MODE_SYSTEM) {
132
- applySelection(MODE_SYSTEM);
133
- }
120
+ if (readStoredSelection() === MODE_SYSTEM) applySelection(MODE_SYSTEM);
134
121
  };
135
122
  if (typeof media.addEventListener === 'function') {
136
123
  media.addEventListener('change', onSystemChange);
@@ -138,22 +125,522 @@ export const APPEARANCE_BOOTSTRAP_SCRIPT_SOURCE = `(function () {
138
125
  media.addListener(onSystemChange);
139
126
  }
140
127
  } catch (error) {
141
- // No live system-change subscription available: the resolved mode
142
- // this load already applied simply stays static until next load.
128
+ // No live subscription: the resolved mode stays static until next load.
143
129
  }
144
130
  }
145
131
 
146
- // Phase 2 (deferred): wire the server-rendered control once it exists.
147
- document.addEventListener('DOMContentLoaded', function () {
148
- var select = document.getElementById(SELECT_ID);
149
- if (!select) return;
150
- select.value = readStoredSelection();
151
- select.addEventListener('change', function () {
152
- var next = select.value;
153
- if (!isKnownMode(next)) return;
154
- writeStoredSelection(next);
155
- applySelection(next);
156
- });
132
+ // ---- shared helpers ----
133
+ function make(tag, className, text) {
134
+ var node = document.createElement(tag);
135
+ if (className) node.className = className;
136
+ if (text !== undefined) node.textContent = text;
137
+ return node;
138
+ }
139
+ function svg(paths) {
140
+ var node = document.createElementNS(NS, 'svg');
141
+ node.setAttribute('class', 'g-icon');
142
+ node.setAttribute('viewBox', '0 0 24 24');
143
+ node.setAttribute('aria-hidden', 'true');
144
+ node.setAttribute('focusable', 'false');
145
+ node.innerHTML = paths;
146
+ return node;
147
+ }
148
+ var ICON_SEARCH = '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>';
149
+ var ICON_ARROW = '<path d="M5 12h14M13 6l6 6-6 6"/>';
150
+ var ICON_COPY = '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h8"/>';
151
+ var ICON_CHECK = '<path d="m5 12 5 5 9-10"/>';
152
+ var toast = null;
153
+ var toastTimer = 0;
154
+ function text(name, fallback) {
155
+ return (toast && toast.getAttribute('data-' + name)) || fallback;
156
+ }
157
+ function say(message, tone) {
158
+ if (!toast) return;
159
+ toast.textContent = message;
160
+ toast.setAttribute('data-tone', tone || 'info');
161
+ toast.setAttribute('data-show', '');
162
+ clearTimeout(toastTimer);
163
+ toastTimer = setTimeout(function () {
164
+ toast.removeAttribute('data-show');
165
+ }, 2200);
166
+ }
167
+ function inView(node) {
168
+ var rect = node.getBoundingClientRect();
169
+ return rect.bottom > 0 && rect.top < window.innerHeight;
170
+ }
171
+ function resolveUrl(href) {
172
+ try {
173
+ return new URL(href, window.location.href).href;
174
+ } catch (error) {
175
+ return '';
176
+ }
177
+ }
178
+ function writeClipboard(value) {
179
+ if (!(navigator.clipboard && navigator.clipboard.writeText)) {
180
+ return Promise.reject(new Error('no clipboard'));
181
+ }
182
+ return navigator.clipboard.writeText(value);
183
+ }
184
+
185
+ // ---- cross-document cover morph (fires before DOMContentLoaded) ----
186
+ function cardImageFor(url) {
187
+ var links = document.querySelectorAll('.g-card-link, .g-hero-media');
188
+ for (var i = 0; i < links.length; i += 1) {
189
+ if (resolveUrl(links[i].getAttribute('href')) !== url) continue;
190
+ var holder = links[i].closest('.g-card') || links[i];
191
+ var image = holder.querySelector('img');
192
+ if (image && inView(image)) return image;
193
+ }
194
+ return null;
195
+ }
196
+ function nameCover(node, transition) {
197
+ if (!node) return;
198
+ node.style.viewTransitionName = 'gala-cover';
199
+ var clear = function () {
200
+ node.style.viewTransitionName = '';
201
+ };
202
+ transition.finished.then(clear, clear);
203
+ }
204
+ window.addEventListener('pageswap', function (event) {
205
+ try {
206
+ if (!event.viewTransition) return;
207
+ var entry = event.activation && event.activation.entry;
208
+ var target = entry ? entry.url : '';
209
+ var cover = document.querySelector('.g-article-cover img');
210
+ nameCover(
211
+ (target && cardImageFor(target)) || (cover && inView(cover) ? cover : null),
212
+ event.viewTransition
213
+ );
214
+ } catch (error) {
215
+ // Cosmetic only.
216
+ }
217
+ });
218
+ window.addEventListener('pagereveal', function (event) {
219
+ try {
220
+ if (!event.viewTransition) return;
221
+ var cover = document.querySelector('.g-article-cover img');
222
+ var from = window.navigation && window.navigation.activation && window.navigation.activation.from;
223
+ nameCover(cover || (from && cardImageFor(from.url)), event.viewTransition);
224
+ } catch (error) {
225
+ // Cosmetic only.
226
+ }
157
227
  });
228
+
229
+ // ---- phase 2 enhancements ----
230
+ function enhanceMode() {
231
+ var button = document.getElementById(TOGGLE_ID);
232
+ if (!button) return;
233
+ var order = [MODE_SYSTEM, MODE_LIGHT, MODE_DARK];
234
+ var sync = function () {
235
+ var label = button.getAttribute('data-label-' + readStoredSelection());
236
+ if (label) {
237
+ button.setAttribute('aria-label', label);
238
+ button.setAttribute('title', label);
239
+ }
240
+ };
241
+ button.hidden = false;
242
+ sync();
243
+ button.addEventListener('click', function () {
244
+ var next = order[(order.indexOf(readStoredSelection()) + 1) % order.length];
245
+ var apply = function () {
246
+ writeStoredSelection(next);
247
+ applySelection(next);
248
+ sync();
249
+ };
250
+ if (
251
+ resolveMode(next) === root.getAttribute(RESOLVED_ATTRIBUTE) ||
252
+ typeof document.startViewTransition !== 'function' ||
253
+ reducedMotion()
254
+ ) {
255
+ apply();
256
+ return;
257
+ }
258
+ var rect = button.getBoundingClientRect();
259
+ var x = rect.left + rect.width / 2;
260
+ var y = rect.top + rect.height / 2;
261
+ var radius = Math.hypot(
262
+ Math.max(x, window.innerWidth - x),
263
+ Math.max(y, window.innerHeight - y)
264
+ );
265
+ root.classList.add('g-vt-reveal');
266
+ var transition = document.startViewTransition(apply);
267
+ var done = function () {
268
+ root.classList.remove('g-vt-reveal');
269
+ };
270
+ transition.ready
271
+ .then(function () {
272
+ root.animate(
273
+ { clipPath: ['circle(0px at ' + x + 'px ' + y + 'px)', 'circle(' + radius + 'px at ' + x + 'px ' + y + 'px)'] },
274
+ { duration: 650, easing: 'cubic-bezier(.2,.8,.2,1)', pseudoElement: '::view-transition-new(root)' }
275
+ );
276
+ })
277
+ .catch(function () {});
278
+ transition.finished.then(done, done);
279
+ });
280
+ }
281
+
282
+ function enhanceSearch() {
283
+ var button = document.querySelector('[data-action="search"]');
284
+ var indexUrl = button && button.getAttribute('data-search-index');
285
+ if (!button || !indexUrl || !window.fetch || typeof HTMLDialogElement === 'undefined') return;
286
+ var dialog = null;
287
+ var input = null;
288
+ var list = null;
289
+ var documents = null;
290
+ var loading = null;
291
+ var options = [];
292
+ var selected = -1;
293
+
294
+ var kbd = make('kbd', 'g-kbd', '/');
295
+ button.appendChild(kbd);
296
+ button.hidden = false;
297
+
298
+ function load() {
299
+ if (!loading) {
300
+ loading = window
301
+ .fetch(indexUrl, { credentials: 'same-origin' })
302
+ .then(function (response) {
303
+ if (!response.ok) throw new Error('index');
304
+ return response.json();
305
+ })
306
+ .then(function (data) {
307
+ documents = (data && Array.isArray(data.documents) ? data.documents : []).filter(function (doc) {
308
+ return doc && typeof doc.title === 'string' && typeof doc.route === 'string';
309
+ });
310
+ })
311
+ .catch(function () {
312
+ documents = null;
313
+ loading = null;
314
+ });
315
+ }
316
+ return loading;
317
+ }
318
+ function field(doc, name) {
319
+ var value = doc[name];
320
+ if (Array.isArray(value)) value = value.join(' ');
321
+ return typeof value === 'string' ? value.toLowerCase() : '';
322
+ }
323
+ function search(query) {
324
+ var terms = query.toLowerCase().split(/\s+/).filter(Boolean);
325
+ var scored = [];
326
+ documents.forEach(function (doc) {
327
+ var total = 0;
328
+ for (var i = 0; i < terms.length; i += 1) {
329
+ var term = terms[i];
330
+ var score =
331
+ (field(doc, 'title').indexOf(term) !== -1 ? 8 : 0) +
332
+ (field(doc, 'tags').indexOf(term) !== -1 ? 4 : 0) +
333
+ (field(doc, 'description').indexOf(term) !== -1 ? 2 : 0) +
334
+ (field(doc, 'excerpt').indexOf(term) !== -1 ? 1 : 0);
335
+ if (score === 0) return;
336
+ total += score;
337
+ }
338
+ scored.push({ doc: doc, score: total });
339
+ });
340
+ scored.sort(function (a, b) {
341
+ if (b.score !== a.score) return b.score - a.score;
342
+ return String(b.doc.publishedAt || '').localeCompare(String(a.doc.publishedAt || ''));
343
+ });
344
+ return scored.slice(0, 8).map(function (entry) {
345
+ return entry.doc;
346
+ });
347
+ }
348
+ function pathOf(route) {
349
+ var url = resolveUrl(route);
350
+ if (!/^https?:/.test(url)) return '';
351
+ var parsed = new URL(url);
352
+ return parsed.pathname + parsed.search + parsed.hash;
353
+ }
354
+ function select(index) {
355
+ selected = index;
356
+ options.forEach(function (option, i) {
357
+ option.setAttribute('aria-selected', String(i === index));
358
+ });
359
+ if (options[index]) {
360
+ input.setAttribute('aria-activedescendant', options[index].id);
361
+ options[index].scrollIntoView({ block: 'nearest' });
362
+ } else {
363
+ input.removeAttribute('aria-activedescendant');
364
+ }
365
+ }
366
+ function show(results, emptyKey, emptyFallback) {
367
+ list.textContent = '';
368
+ options = [];
369
+ results.forEach(function (doc, index) {
370
+ var path = pathOf(doc.route);
371
+ if (!path) return;
372
+ var item = make('li');
373
+ item.setAttribute('role', 'option');
374
+ item.id = 'gala-search-hit-' + index;
375
+ var link = make('a', 'g-search-hit');
376
+ link.setAttribute('href', path);
377
+ var body = make('span', 'g-search-hit-text');
378
+ body.appendChild(make('span', 'g-search-hit-title', doc.title));
379
+ var meta = Array.isArray(doc.tags) && doc.tags.length ? doc.tags.join(', ') : '';
380
+ meta = [meta, typeof doc.description === 'string' ? doc.description : ''].filter(Boolean).join(' - ');
381
+ if (meta) body.appendChild(make('span', 'g-search-hit-meta', meta));
382
+ link.appendChild(body);
383
+ link.appendChild(svg(ICON_ARROW));
384
+ item.appendChild(link);
385
+ list.appendChild(item);
386
+ options.push(item);
387
+ });
388
+ if (options.length === 0) {
389
+ list.appendChild(make('li', 'g-search-empty', text(emptyKey, emptyFallback)));
390
+ }
391
+ select(options.length ? 0 : -1);
392
+ }
393
+ function update() {
394
+ if (!documents) {
395
+ show([], 'search-unavailable', 'Search is unavailable right now.');
396
+ return;
397
+ }
398
+ var query = input.value.trim();
399
+ var results = query
400
+ ? search(query)
401
+ : documents.slice().sort(function (a, b) {
402
+ return String(b.publishedAt || '').localeCompare(String(a.publishedAt || ''));
403
+ }).slice(0, 6);
404
+ show(results, 'search-empty', 'No articles match your search.');
405
+ }
406
+ function build() {
407
+ dialog = make('dialog', 'g-search');
408
+ dialog.setAttribute('aria-label', text('search-label', 'Search'));
409
+ var box = make('div', 'g-search-box');
410
+ box.appendChild(svg(ICON_SEARCH));
411
+ input = make('input', 'g-search-input');
412
+ input.type = 'search';
413
+ input.setAttribute('autocomplete', 'off');
414
+ input.setAttribute('spellcheck', 'false');
415
+ input.setAttribute('placeholder', text('search-placeholder', 'Search'));
416
+ input.setAttribute('aria-label', text('search-label', 'Search'));
417
+ input.setAttribute('role', 'combobox');
418
+ input.setAttribute('aria-expanded', 'true');
419
+ input.setAttribute('aria-controls', 'gala-search-results');
420
+ box.appendChild(input);
421
+ list = make('ul', 'g-search-results');
422
+ list.id = 'gala-search-results';
423
+ list.setAttribute('role', 'listbox');
424
+ dialog.appendChild(box);
425
+ dialog.appendChild(list);
426
+ document.body.appendChild(dialog);
427
+ input.addEventListener('input', update);
428
+ input.addEventListener('keydown', function (event) {
429
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
430
+ if (!options.length) return;
431
+ event.preventDefault();
432
+ var step = event.key === 'ArrowDown' ? 1 : -1;
433
+ select((selected + step + options.length) % options.length);
434
+ } else if (event.key === 'Escape') {
435
+ // A search input would clear its text first; close straight away.
436
+ event.preventDefault();
437
+ dialog.close();
438
+ } else if (event.key === 'Enter') {
439
+ var link = options[Math.max(selected, 0)] && options[Math.max(selected, 0)].querySelector('a');
440
+ if (link) {
441
+ event.preventDefault();
442
+ window.location.assign(link.getAttribute('href'));
443
+ }
444
+ }
445
+ });
446
+ dialog.addEventListener('click', function (event) {
447
+ if (event.target === dialog) dialog.close();
448
+ });
449
+ }
450
+ function open() {
451
+ if (!dialog) build();
452
+ if (dialog.open) return;
453
+ input.value = '';
454
+ dialog.showModal();
455
+ input.focus();
456
+ list.textContent = '';
457
+ load().then(function () {
458
+ if (dialog.open) update();
459
+ });
460
+ }
461
+ button.addEventListener('click', open);
462
+ document.addEventListener('keydown', function (event) {
463
+ var active = document.activeElement;
464
+ var typing = active && (/^(INPUT|TEXTAREA|SELECT)$/.test(active.tagName) || active.isContentEditable);
465
+ if (
466
+ (event.key === '/' && !typing && !event.ctrlKey && !event.metaKey && !event.altKey) ||
467
+ ((event.metaKey || event.ctrlKey) && String(event.key).toLowerCase() === 'k')
468
+ ) {
469
+ event.preventDefault();
470
+ open();
471
+ }
472
+ });
473
+ }
474
+
475
+ function enhanceCode() {
476
+ var blocks = document.querySelectorAll('pre[class*="language-"]');
477
+ Array.prototype.forEach.call(blocks, function (pre) {
478
+ if (!pre.parentNode || pre.parentNode.classList.contains('g-codeblock')) return;
479
+ var match = /language-([A-Za-z0-9_+#-]+)/.exec(pre.className);
480
+ var wrapper = make('div', 'g-codeblock');
481
+ var bar = make('div', 'g-codebar');
482
+ if (match) bar.appendChild(make('span', 'g-codebar-lang', match[1]));
483
+ var copy = make('button', 'g-copy');
484
+ copy.type = 'button';
485
+ var idle = make('span', 'g-copy-idle');
486
+ idle.appendChild(svg(ICON_COPY));
487
+ idle.appendChild(document.createTextNode(' ' + text('copy', 'Copy')));
488
+ var done = make('span', 'g-copy-done');
489
+ done.appendChild(svg(ICON_CHECK));
490
+ done.appendChild(document.createTextNode(' ' + text('copied', 'Copied')));
491
+ copy.appendChild(idle);
492
+ copy.appendChild(done);
493
+ bar.appendChild(copy);
494
+ pre.parentNode.insertBefore(wrapper, pre);
495
+ wrapper.appendChild(bar);
496
+ wrapper.appendChild(pre);
497
+ copy.addEventListener('click', function () {
498
+ var code = pre.querySelector('code') || pre;
499
+ writeClipboard(code.textContent).then(
500
+ function () {
501
+ copy.setAttribute('data-copied', '');
502
+ setTimeout(function () {
503
+ copy.removeAttribute('data-copied');
504
+ }, 1600);
505
+ },
506
+ function () {
507
+ var selection = window.getSelection();
508
+ var range = document.createRange();
509
+ range.selectNodeContents(code);
510
+ selection.removeAllRanges();
511
+ selection.addRange(range);
512
+ say(text('code-selected', 'Code selected.'), 'warning');
513
+ }
514
+ );
515
+ });
516
+ });
517
+ }
518
+
519
+ function enhanceContents() {
520
+ var links = document.querySelectorAll('[data-toc]');
521
+ if (!links.length) return;
522
+ Array.prototype.forEach.call(links, function (link) {
523
+ link.addEventListener('click', function () {
524
+ var details = link.closest('details');
525
+ if (details) details.open = false;
526
+ });
527
+ });
528
+ if (!('IntersectionObserver' in window)) return;
529
+ var observer = new IntersectionObserver(
530
+ function (entries) {
531
+ entries.forEach(function (entry) {
532
+ if (!entry.isIntersecting) return;
533
+ Array.prototype.forEach.call(links, function (link) {
534
+ link.setAttribute('aria-current', String(link.getAttribute('data-toc') === entry.target.id));
535
+ });
536
+ });
537
+ },
538
+ { rootMargin: '-20% 0px -70% 0px' }
539
+ );
540
+ var seen = {};
541
+ Array.prototype.forEach.call(links, function (link) {
542
+ var id = link.getAttribute('data-toc');
543
+ var heading = id && !seen[id] ? document.getElementById(id) : null;
544
+ if (heading) {
545
+ seen[id] = true;
546
+ observer.observe(heading);
547
+ }
548
+ });
549
+ }
550
+
551
+ function readSaved() {
552
+ try {
553
+ var value = JSON.parse(window.localStorage.getItem(SAVED_KEY) || '[]');
554
+ return Array.isArray(value) ? value.filter(function (item) { return typeof item === 'string'; }) : [];
555
+ } catch (error) {
556
+ return [];
557
+ }
558
+ }
559
+ function writeSaved(items) {
560
+ try {
561
+ window.localStorage.setItem(SAVED_KEY, JSON.stringify(items.slice(-200)));
562
+ } catch (error) {
563
+ // Storage blocked: the pressed state still toggles for this view.
564
+ }
565
+ }
566
+ function enhanceShare() {
567
+ var group = document.querySelector('.g-share');
568
+ if (!group) return;
569
+ group.hidden = false;
570
+ var path = window.location.pathname;
571
+ var copyLink = group.querySelector('[data-action="copy-link"]');
572
+ var bookmark = group.querySelector('[data-action="bookmark"]');
573
+ if (copyLink) {
574
+ copyLink.addEventListener('click', function () {
575
+ writeClipboard(window.location.href).then(
576
+ function () {
577
+ say(text('link-copied', 'Link copied.'), 'success');
578
+ },
579
+ function () {
580
+ say(text('copy-blocked', 'Copying is blocked here.'), 'warning');
581
+ }
582
+ );
583
+ });
584
+ }
585
+ if (bookmark) {
586
+ bookmark.setAttribute('aria-pressed', String(readSaved().indexOf(path) !== -1));
587
+ bookmark.addEventListener('click', function () {
588
+ var on = bookmark.getAttribute('aria-pressed') !== 'true';
589
+ var items = readSaved().filter(function (item) { return item !== path; });
590
+ if (on) items.push(path);
591
+ writeSaved(items);
592
+ bookmark.setAttribute('aria-pressed', String(on));
593
+ say(on ? text('saved', 'Saved for later.') : text('unsaved', 'Removed.'), 'success');
594
+ });
595
+ }
596
+ }
597
+
598
+ function enhanceProgress() {
599
+ var bar = document.querySelector('.g-progress');
600
+ if (!bar) return;
601
+ var supported = !!(window.CSS && CSS.supports && CSS.supports('animation-timeline: scroll()'));
602
+ if (supported && !reducedMotion()) return;
603
+ var frame = 0;
604
+ var paint = function () {
605
+ frame = 0;
606
+ var max = root.scrollHeight - window.innerHeight;
607
+ bar.style.transform = 'scaleX(' + (max > 0 ? Math.min(1, window.scrollY / max) : 0) + ')';
608
+ };
609
+ window.addEventListener(
610
+ 'scroll',
611
+ function () {
612
+ if (!frame) frame = window.requestAnimationFrame(paint);
613
+ },
614
+ { passive: true }
615
+ );
616
+ paint();
617
+ }
618
+
619
+ function enhance() {
620
+ toast = document.querySelector('.g-toast');
621
+ [enhanceMode, enhanceSearch, enhanceCode, enhanceContents, enhanceShare, enhanceProgress].forEach(function (step) {
622
+ try {
623
+ step();
624
+ } catch (error) {
625
+ // One failed enhancement never blocks the others.
626
+ }
627
+ });
628
+ }
629
+ if (document.readyState === 'loading') {
630
+ document.addEventListener('DOMContentLoaded', enhance);
631
+ } else {
632
+ enhance();
633
+ }
158
634
  })();
159
635
  `;
636
+
637
+ /**
638
+ * The shipped script: {@link READABLE_SCRIPT_SOURCE} minified
639
+ * (`minify-script.js`: comments, indentation and blank lines removed, nothing
640
+ * renamed or reordered).
641
+ *
642
+ * @type {string}
643
+ */
644
+ export const APPEARANCE_BOOTSTRAP_SCRIPT_SOURCE = minifyScript(
645
+ READABLE_SCRIPT_SOURCE,
646
+ );