@rathnasgala2/template 2.1.0 → 3.0.0
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/README.md +95 -59
- package/contracts/render-policy.jcs +1 -1
- package/contracts/theme-styling-contract.jcs +1 -1
- package/package.json +2 -2
- package/src/core/index.js +209 -60
- package/src/core/internal/appearance/base-layer.js +711 -137
- package/src/core/internal/appearance/bootstrap-script.js +570 -83
- package/src/core/internal/appearance/contract.js +6 -5
- package/src/core/internal/appearance/controller-markup.js +46 -55
- package/src/core/internal/appearance/minify-script.js +69 -0
- package/src/core/internal/appearance/styling-contract.js +199 -17
- package/src/core/internal/chrome.js +199 -0
- package/src/core/internal/components.js +379 -0
- package/src/core/internal/eleventy-render.js +13 -4
- package/src/core/internal/feeds.js +79 -8
- package/src/core/internal/icons.js +52 -0
- package/src/core/internal/llms.js +252 -0
- package/src/core/internal/media/pipeline.js +32 -17
- package/src/core/internal/messages.js +104 -0
- package/src/core/internal/page-kinds.js +922 -293
- package/src/core/internal/render-policy-content.js +2 -2
- package/src/core/internal/robots.js +78 -0
- package/src/core/internal/route-labels.js +31 -0
- package/src/core/internal/route.js +42 -0
- package/src/core/internal/search-index.js +5 -2
- package/src/core/internal/seo.js +57 -2
- package/src/core/internal/sitemap.js +5 -4
- package/src/core/internal/skeleton.js +73 -132
- package/src/core/internal/structured-data.js +247 -0
- package/src/core/internal/theme-assets.js +21 -2
|
@@ -1,136 +1,123 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The exact deterministic
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* the
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
-
|
|
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
|
|
55
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
147
|
-
|
|
148
|
-
var
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
+
);
|