@abreen/tada 1.9.0 → 1.10.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.
- package/README.md +24 -34
- package/bin/tada.ts +1 -1
- package/build/apply-base-path-plugin.ts +79 -17
- package/build/bundle.ts +9 -1
- package/build/content-watch.ts +3 -3
- package/build/copy.ts +30 -0
- package/build/generate-content-assets.ts +45 -59
- package/build/pagefind.ts +0 -7
- package/build/serve.ts +4 -0
- package/build/types.d.ts +19 -1
- package/build/util.ts +3 -0
- package/build/utils/content-files.ts +98 -0
- package/build/utils/include.ts +4 -0
- package/build/utils/python-runner/trace_runner.py +272 -0
- package/build/utils/render.ts +34 -8
- package/build/utils/trace-core.ts +203 -0
- package/build/utils/trace-java.ts +132 -0
- package/build/utils/trace-layout.ts +31 -3
- package/build/utils/trace-python.ts +26 -0
- package/build/utils/trace.ts +174 -294
- package/build/validate-internal-links-plugin.ts +16 -4
- package/build/watch/assets.ts +111 -0
- package/build/watch/build-full.ts +124 -0
- package/build/watch/build-incremental.ts +139 -0
- package/build/watch/compiler-types.d.ts +19 -0
- package/build/watch/compiler.ts +112 -0
- package/build/watch/index.ts +22 -0
- package/build/watch/mutations.ts +75 -0
- package/build/watch/planner.ts +230 -0
- package/build/watch/records.ts +190 -0
- package/build/watch/runtime.ts +143 -0
- package/build/watch/snapshot.ts +602 -0
- package/build/watch/validation.ts +36 -0
- package/build/watch-reload-client.ts +10 -5
- package/init/content/labs/01/index.md +6 -0
- package/init/content/labs/01/trace_demo.py +16 -0
- package/package.json +42 -23
- package/python/command.ts +95 -0
- package/python/module.ts +18 -0
- package/src/_alerts.scss +11 -8
- package/src/_base.scss +25 -29
- package/src/_layout.scss +17 -14
- package/src/_mixins.scss +10 -9
- package/src/anchor/style.scss +4 -2
- package/src/code/index.ts +9 -2
- package/src/code/style.scss +14 -13
- package/src/code.scss +14 -13
- package/src/header/_base.scss +21 -24
- package/src/header/style.scss +16 -12
- package/src/index.ts +3 -0
- package/src/literate/style.scss +4 -4
- package/src/navigate/index.ts +43 -279
- package/src/navigate/lifecycle.ts +1 -1
- package/src/navigate/runtime.ts +341 -0
- package/src/page-update/README.md +4 -0
- package/src/page-update/index.ts +280 -0
- package/src/page-update/style.scss +53 -0
- package/src/print/style.scss +4 -4
- package/src/question/style.scss +10 -6
- package/src/search/index.ts +9 -12
- package/src/search/style.scss +44 -42
- package/src/style.scss +63 -58
- package/src/timezone/style.scss +5 -5
- package/src/toc/style.scss +17 -16
- package/src/trace/style.scss +27 -27
- package/src/validators.ts +45 -0
- package/templates/_footer.html +1 -1
- package/templates/_theme.scss +8 -19
- package/watch/engine.ts +296 -0
- package/watch/events.ts +10 -0
- package/watch/fs-commit.ts +102 -0
- package/watch/types.d.ts +103 -0
- package/build/watch.ts +0 -493
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { mountPerPageComponents, teardownPerPageComponents } from './lifecycle';
|
|
2
|
+
|
|
3
|
+
export const NAVIGATION_EVENT = 'tada:navigation';
|
|
4
|
+
|
|
5
|
+
let currentAbortController: AbortController | null = null;
|
|
6
|
+
let historyIndex = 0;
|
|
7
|
+
let currentPath = '';
|
|
8
|
+
|
|
9
|
+
const scrollByIndex = new Map<number, number>();
|
|
10
|
+
const scrollByLocation = new Map<string, number>();
|
|
11
|
+
|
|
12
|
+
type Direction = 'forward' | 'back';
|
|
13
|
+
|
|
14
|
+
interface NavigationOptions {
|
|
15
|
+
url: string;
|
|
16
|
+
scrollTarget: number | string | null;
|
|
17
|
+
direction: Direction;
|
|
18
|
+
pushHistory: boolean;
|
|
19
|
+
useViewTransition?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function updateHead(document: Document, newDoc: Document): void {
|
|
23
|
+
document.title = newDoc.title;
|
|
24
|
+
|
|
25
|
+
const metaTags = ['description', 'author'];
|
|
26
|
+
for (const name of metaTags) {
|
|
27
|
+
const newMeta = newDoc.querySelector(`meta[name="${name}"]`);
|
|
28
|
+
const oldMeta = document.querySelector(`meta[name="${name}"]`);
|
|
29
|
+
if (newMeta && oldMeta) {
|
|
30
|
+
oldMeta.setAttribute('content', newMeta.getAttribute('content') ?? '');
|
|
31
|
+
} else if (newMeta && !oldMeta) {
|
|
32
|
+
document.head.appendChild(newMeta.cloneNode(true));
|
|
33
|
+
} else if (!newMeta && oldMeta) {
|
|
34
|
+
oldMeta.remove();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ogTags = ['og:title', 'og:author'];
|
|
39
|
+
for (const prop of ogTags) {
|
|
40
|
+
const newMeta = newDoc.querySelector(`meta[property="${prop}"]`);
|
|
41
|
+
const oldMeta = document.querySelector(`meta[property="${prop}"]`);
|
|
42
|
+
if (newMeta && oldMeta) {
|
|
43
|
+
oldMeta.setAttribute('content', newMeta.getAttribute('content') ?? '');
|
|
44
|
+
} else if (newMeta && !oldMeta) {
|
|
45
|
+
document.head.appendChild(newMeta.cloneNode(true));
|
|
46
|
+
} else if (!newMeta && oldMeta) {
|
|
47
|
+
oldMeta.remove();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const existingHrefs = new Set(
|
|
52
|
+
Array.from(document.querySelectorAll('link[rel="stylesheet"]')).map(link =>
|
|
53
|
+
link.getAttribute('href'),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
for (const link of newDoc.querySelectorAll('link[rel="stylesheet"]')) {
|
|
58
|
+
if (!existingHrefs.has(link.getAttribute('href'))) {
|
|
59
|
+
document.head.appendChild(link.cloneNode(true));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function swapContent(document: Document, newDoc: Document): void {
|
|
65
|
+
const newContainer = newDoc.querySelector('.container');
|
|
66
|
+
const oldContainer = document.querySelector('.container');
|
|
67
|
+
if (newContainer && oldContainer) {
|
|
68
|
+
const imported = document.importNode(newContainer, true);
|
|
69
|
+
oldContainer.replaceChildren(...imported.childNodes);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
document.body.className = newDoc.body.className;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function clearSearch(document: Document): void {
|
|
76
|
+
const input = document.querySelector(
|
|
77
|
+
'input.search.quick-search',
|
|
78
|
+
) as HTMLInputElement | null;
|
|
79
|
+
if (input && input.value) {
|
|
80
|
+
const EventCtor = document.defaultView?.Event ?? Event;
|
|
81
|
+
input.value = '';
|
|
82
|
+
input.dispatchEvent(new EventCtor('input', { bubbles: true }));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function closeHeaderDetails(document: Document): void {
|
|
87
|
+
const details = document.querySelector(
|
|
88
|
+
'header details',
|
|
89
|
+
) as HTMLDetailsElement | null;
|
|
90
|
+
if (details?.open) {
|
|
91
|
+
details.open = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function dispatchNavigationEvent(window: Window): void {
|
|
96
|
+
const CustomEventCtor = (
|
|
97
|
+
window as unknown as { CustomEvent: typeof CustomEvent }
|
|
98
|
+
).CustomEvent;
|
|
99
|
+
window.dispatchEvent(
|
|
100
|
+
new CustomEventCtor(NAVIGATION_EVENT, { detail: { path: currentPath } }),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function cleanupViewTransitionNames(document: Document): void {
|
|
105
|
+
const newH1 = document.querySelector(
|
|
106
|
+
'.title-and-info h1',
|
|
107
|
+
) as HTMLElement | null;
|
|
108
|
+
const newInfo = document.querySelector(
|
|
109
|
+
'.title-and-info .info',
|
|
110
|
+
) as HTMLElement | null;
|
|
111
|
+
const newBreadcrumb = document.querySelector(
|
|
112
|
+
'.title-and-info a.breadcrumb',
|
|
113
|
+
) as HTMLElement | null;
|
|
114
|
+
|
|
115
|
+
if (newH1) {
|
|
116
|
+
newH1.style.viewTransitionName = '';
|
|
117
|
+
}
|
|
118
|
+
if (newInfo) {
|
|
119
|
+
newInfo.style.viewTransitionName = '';
|
|
120
|
+
}
|
|
121
|
+
if (newBreadcrumb) {
|
|
122
|
+
newBreadcrumb.style.viewTransitionName = '';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getLocationKey(window: Window): string {
|
|
127
|
+
return (
|
|
128
|
+
window.location.pathname + window.location.search + window.location.hash
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function initNavigation(window: Window): void {
|
|
133
|
+
currentAbortController = null;
|
|
134
|
+
historyIndex = 0;
|
|
135
|
+
scrollByIndex.clear();
|
|
136
|
+
scrollByLocation.clear();
|
|
137
|
+
window.history.scrollRestoration = 'manual';
|
|
138
|
+
currentPath = window.location.pathname + window.location.search;
|
|
139
|
+
scrollByIndex.set(historyIndex, window.scrollY);
|
|
140
|
+
scrollByLocation.set(getLocationKey(window), window.scrollY);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function saveScrollPosition(window: Window): void {
|
|
144
|
+
scrollByIndex.set(historyIndex, window.scrollY);
|
|
145
|
+
scrollByLocation.set(getLocationKey(window), window.scrollY);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getCurrentPath(): string {
|
|
149
|
+
return currentPath;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function setCurrentPath(path: string): void {
|
|
153
|
+
currentPath = path;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function getHistoryIndex(): number {
|
|
157
|
+
return historyIndex;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function setHistoryIndex(index: number): void {
|
|
161
|
+
historyIndex = index;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function getSavedScroll(index: number): number | undefined {
|
|
165
|
+
return scrollByIndex.get(index);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function getSavedLocationScroll(
|
|
169
|
+
locationKey: string,
|
|
170
|
+
): number | undefined {
|
|
171
|
+
return scrollByLocation.get(locationKey);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function navigateToUrl(
|
|
175
|
+
window: Window,
|
|
176
|
+
options: NavigationOptions,
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
const { document } = window;
|
|
179
|
+
const {
|
|
180
|
+
url,
|
|
181
|
+
scrollTarget,
|
|
182
|
+
direction,
|
|
183
|
+
pushHistory,
|
|
184
|
+
useViewTransition = true,
|
|
185
|
+
} = options;
|
|
186
|
+
|
|
187
|
+
if (currentAbortController) {
|
|
188
|
+
currentAbortController.abort();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
currentAbortController = controller;
|
|
193
|
+
|
|
194
|
+
const header = document.querySelector('header');
|
|
195
|
+
header?.classList.add('loading');
|
|
196
|
+
|
|
197
|
+
let response: Response;
|
|
198
|
+
try {
|
|
199
|
+
response = await fetch(url, { signal: controller.signal });
|
|
200
|
+
} catch (err: unknown) {
|
|
201
|
+
header?.classList.remove('loading');
|
|
202
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
window.location.href = url;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
header?.classList.remove('loading');
|
|
211
|
+
window.location.href = url;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const html = await response.text();
|
|
216
|
+
header?.classList.remove('loading');
|
|
217
|
+
|
|
218
|
+
const DOMParserCtor = (window as unknown as { DOMParser: typeof DOMParser })
|
|
219
|
+
.DOMParser;
|
|
220
|
+
const parser = new DOMParserCtor();
|
|
221
|
+
const newDoc = parser.parseFromString(html, 'text/html');
|
|
222
|
+
if (!newDoc.querySelector('meta[name="generator"][content="Tada"]')) {
|
|
223
|
+
window.location.href = url;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const parsed = new URL(url);
|
|
228
|
+
|
|
229
|
+
const doSwap = () => {
|
|
230
|
+
teardownPerPageComponents();
|
|
231
|
+
swapContent(document, newDoc);
|
|
232
|
+
updateHead(document, newDoc);
|
|
233
|
+
currentPath = parsed.pathname + parsed.search;
|
|
234
|
+
|
|
235
|
+
if (pushHistory) {
|
|
236
|
+
historyIndex++;
|
|
237
|
+
const urlWithoutHash = parsed.origin + parsed.pathname + parsed.search;
|
|
238
|
+
window.history.pushState({ navIndex: historyIndex }, '', urlWithoutHash);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (typeof scrollTarget === 'string') {
|
|
242
|
+
const urlWithHash = parsed.pathname + parsed.search + '#' + scrollTarget;
|
|
243
|
+
window.location.replace(urlWithHash);
|
|
244
|
+
window.history.replaceState({ navIndex: historyIndex }, '', urlWithHash);
|
|
245
|
+
} else if (typeof scrollTarget === 'number') {
|
|
246
|
+
window.scrollTo({ top: scrollTarget });
|
|
247
|
+
} else {
|
|
248
|
+
window.scrollTo({ top: 0 });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
scrollByIndex.set(historyIndex, window.scrollY);
|
|
252
|
+
scrollByLocation.set(getLocationKey(window), window.scrollY);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const transitionDoc = document as Document & {
|
|
256
|
+
startViewTransition?: (cb: () => void) => { finished: Promise<void> };
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
if (
|
|
260
|
+
useViewTransition &&
|
|
261
|
+
typeof transitionDoc.startViewTransition === 'function'
|
|
262
|
+
) {
|
|
263
|
+
const titleEl = document.querySelector('.title-and-info');
|
|
264
|
+
const headerHeight =
|
|
265
|
+
document.querySelector('header')?.getBoundingClientRect().height ?? 0;
|
|
266
|
+
const titleVisible =
|
|
267
|
+
titleEl != null && titleEl.getBoundingClientRect().top >= headerHeight;
|
|
268
|
+
|
|
269
|
+
if (titleVisible) {
|
|
270
|
+
const h1 = titleEl.querySelector('h1') as HTMLElement | null;
|
|
271
|
+
const info = titleEl.querySelector('.info') as HTMLElement | null;
|
|
272
|
+
const breadcrumb = titleEl.querySelector(
|
|
273
|
+
'a.breadcrumb',
|
|
274
|
+
) as HTMLElement | null;
|
|
275
|
+
if (h1) {
|
|
276
|
+
h1.style.viewTransitionName = 'page-title';
|
|
277
|
+
}
|
|
278
|
+
if (info) {
|
|
279
|
+
info.style.viewTransitionName = 'page-info';
|
|
280
|
+
}
|
|
281
|
+
if (breadcrumb) {
|
|
282
|
+
breadcrumb.style.viewTransitionName = 'page-breadcrumb';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
document.documentElement.classList.add(
|
|
287
|
+
direction === 'forward' ? 'nav-forward' : 'nav-back',
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const transition = transitionDoc.startViewTransition(() => {
|
|
291
|
+
doSwap();
|
|
292
|
+
|
|
293
|
+
const newTitleEl = document.querySelector('.title-and-info');
|
|
294
|
+
const newHeaderHeight =
|
|
295
|
+
document.querySelector('header')?.getBoundingClientRect().height ?? 0;
|
|
296
|
+
const newTitleVisible =
|
|
297
|
+
newTitleEl != null &&
|
|
298
|
+
newTitleEl.getBoundingClientRect().top >= newHeaderHeight;
|
|
299
|
+
|
|
300
|
+
if (newTitleVisible && newTitleEl) {
|
|
301
|
+
const h1 = newTitleEl.querySelector('h1') as HTMLElement | null;
|
|
302
|
+
const info = newTitleEl.querySelector('.info') as HTMLElement | null;
|
|
303
|
+
const breadcrumb = newTitleEl.querySelector(
|
|
304
|
+
'a.breadcrumb',
|
|
305
|
+
) as HTMLElement | null;
|
|
306
|
+
if (h1) {
|
|
307
|
+
h1.style.viewTransitionName = 'page-title';
|
|
308
|
+
}
|
|
309
|
+
if (info) {
|
|
310
|
+
info.style.viewTransitionName = 'page-info';
|
|
311
|
+
}
|
|
312
|
+
if (breadcrumb) {
|
|
313
|
+
breadcrumb.style.viewTransitionName = 'page-breadcrumb';
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
await transition.finished;
|
|
319
|
+
document.documentElement.classList.remove('nav-forward', 'nav-back');
|
|
320
|
+
cleanupViewTransitionNames(document);
|
|
321
|
+
} else {
|
|
322
|
+
doSwap();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await mountPerPageComponents(window);
|
|
326
|
+
currentAbortController = null;
|
|
327
|
+
dispatchNavigationEvent(window);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export async function refreshCurrentPage(window: Window): Promise<void> {
|
|
331
|
+
clearSearch(window.document);
|
|
332
|
+
closeHeaderDetails(window.document);
|
|
333
|
+
saveScrollPosition(window);
|
|
334
|
+
await navigateToUrl(window, {
|
|
335
|
+
url: window.location.href,
|
|
336
|
+
scrollTarget: window.scrollY,
|
|
337
|
+
direction: 'forward',
|
|
338
|
+
pushHistory: false,
|
|
339
|
+
useViewTransition: false,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EMPTY_RESPONSE_VALIDATORS,
|
|
3
|
+
getPreferredValidatorKey,
|
|
4
|
+
getResponseValidators,
|
|
5
|
+
hasResponseValidatorsChanged,
|
|
6
|
+
hasUsableResponseValidators,
|
|
7
|
+
type ResponseValidators,
|
|
8
|
+
} from '../validators';
|
|
9
|
+
import { NAVIGATION_EVENT, refreshCurrentPage } from '../navigate/runtime';
|
|
10
|
+
|
|
11
|
+
const POLL_INTERVAL_MS = 60_000;
|
|
12
|
+
|
|
13
|
+
export interface PageUpdateOptions {
|
|
14
|
+
pollIntervalMs?: number;
|
|
15
|
+
refreshPage?: (window: Window) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createToast(document: Document): {
|
|
19
|
+
container: HTMLDivElement;
|
|
20
|
+
toast: HTMLDivElement;
|
|
21
|
+
reloadButton: HTMLButtonElement;
|
|
22
|
+
dismissButton: HTMLButtonElement;
|
|
23
|
+
} {
|
|
24
|
+
const container = document.createElement('div');
|
|
25
|
+
container.className = 'page-update-container';
|
|
26
|
+
|
|
27
|
+
const toast = document.createElement('div');
|
|
28
|
+
toast.className = 'page-update-toast';
|
|
29
|
+
toast.setAttribute('role', 'status');
|
|
30
|
+
toast.setAttribute('aria-live', 'polite');
|
|
31
|
+
|
|
32
|
+
const body = document.createElement('div');
|
|
33
|
+
body.className = 'page-update-body';
|
|
34
|
+
|
|
35
|
+
const message = document.createElement('div');
|
|
36
|
+
message.className = 'page-update-message';
|
|
37
|
+
message.textContent = 'A newer version of this page is available.';
|
|
38
|
+
|
|
39
|
+
const actions = document.createElement('div');
|
|
40
|
+
actions.className = 'page-update-actions';
|
|
41
|
+
|
|
42
|
+
const reloadButton = document.createElement('button');
|
|
43
|
+
reloadButton.type = 'button';
|
|
44
|
+
reloadButton.textContent = 'Reload';
|
|
45
|
+
|
|
46
|
+
const dismissButton = document.createElement('button');
|
|
47
|
+
dismissButton.type = 'button';
|
|
48
|
+
dismissButton.textContent = 'Dismiss';
|
|
49
|
+
|
|
50
|
+
actions.append(reloadButton, dismissButton);
|
|
51
|
+
body.append(message, actions);
|
|
52
|
+
toast.appendChild(body);
|
|
53
|
+
container.appendChild(toast);
|
|
54
|
+
document.body.appendChild(container);
|
|
55
|
+
|
|
56
|
+
return { container, toast, reloadButton, dismissButton };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function mountPageUpdate(
|
|
60
|
+
window: Window,
|
|
61
|
+
options: PageUpdateOptions = {},
|
|
62
|
+
): (() => void) | undefined {
|
|
63
|
+
const { document } = window;
|
|
64
|
+
if (!document.body) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
69
|
+
const refreshPage = options.refreshPage ?? refreshCurrentPage;
|
|
70
|
+
const { container, toast, reloadButton, dismissButton } =
|
|
71
|
+
createToast(document);
|
|
72
|
+
|
|
73
|
+
let currentPath = window.location.pathname;
|
|
74
|
+
let baseline = EMPTY_RESPONSE_VALIDATORS;
|
|
75
|
+
let dismissedValidatorKey: string | null = null;
|
|
76
|
+
let nextValidator: ResponseValidators | null = null;
|
|
77
|
+
let hasBaseline = false;
|
|
78
|
+
let timer: number | null = null;
|
|
79
|
+
let checkInFlight = false;
|
|
80
|
+
let rerunAfterCurrentCheck = false;
|
|
81
|
+
|
|
82
|
+
function hideToast() {
|
|
83
|
+
toast.classList.remove('is-showing');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function showToast() {
|
|
87
|
+
toast.classList.add('is-showing');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function clearTimer() {
|
|
91
|
+
if (timer !== null) {
|
|
92
|
+
window.clearTimeout(timer);
|
|
93
|
+
timer = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function scheduleNextCheck() {
|
|
98
|
+
clearTimer();
|
|
99
|
+
if (document.hidden) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
timer = window.setTimeout(() => {
|
|
103
|
+
void checkForUpdate();
|
|
104
|
+
}, pollIntervalMs);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function fetchValidators(
|
|
108
|
+
path: string,
|
|
109
|
+
): Promise<ResponseValidators | null> {
|
|
110
|
+
if (typeof __IS_DEV__ !== 'undefined' && __IS_DEV__) {
|
|
111
|
+
console.log(
|
|
112
|
+
`[page-update] HEAD ${path} visibility=${document.visibilityState} focus=${document.hasFocus()}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const res = await fetch(path, { method: 'HEAD', cache: 'no-cache' });
|
|
117
|
+
|
|
118
|
+
if (typeof __IS_DEV__ !== 'undefined' && __IS_DEV__) {
|
|
119
|
+
console.log(
|
|
120
|
+
`[page-update] HEAD ${path} -> ${res.status} etag=${res.headers.get('ETag')} last-modified=${res.headers.get('Last-Modified')} visibility=${document.visibilityState} focus=${document.hasFocus()}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const validators = getResponseValidators(res);
|
|
129
|
+
if (!hasUsableResponseValidators(validators)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return validators;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function checkForUpdate(): Promise<void> {
|
|
137
|
+
if (checkInFlight) {
|
|
138
|
+
rerunAfterCurrentCheck = true;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const requestedPath = currentPath;
|
|
143
|
+
let shouldRerun = false;
|
|
144
|
+
checkInFlight = true;
|
|
145
|
+
try {
|
|
146
|
+
const validators = await fetchValidators(requestedPath);
|
|
147
|
+
if (!validators || requestedPath !== currentPath) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!hasBaseline) {
|
|
152
|
+
baseline = validators;
|
|
153
|
+
hasBaseline = true;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!hasResponseValidatorsChanged(baseline, validators)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
nextValidator = validators;
|
|
162
|
+
const validatorKey = getPreferredValidatorKey(validators);
|
|
163
|
+
if (validatorKey !== null && validatorKey === dismissedValidatorKey) {
|
|
164
|
+
hideToast();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
showToast();
|
|
169
|
+
} catch {
|
|
170
|
+
// best-effort
|
|
171
|
+
} finally {
|
|
172
|
+
checkInFlight = false;
|
|
173
|
+
if (rerunAfterCurrentCheck && !document.hidden) {
|
|
174
|
+
rerunAfterCurrentCheck = false;
|
|
175
|
+
shouldRerun = true;
|
|
176
|
+
} else {
|
|
177
|
+
scheduleNextCheck();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (shouldRerun) {
|
|
182
|
+
void checkForUpdate();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function resetState() {
|
|
187
|
+
currentPath = window.location.pathname;
|
|
188
|
+
baseline = EMPTY_RESPONSE_VALIDATORS;
|
|
189
|
+
dismissedValidatorKey = null;
|
|
190
|
+
nextValidator = null;
|
|
191
|
+
hasBaseline = false;
|
|
192
|
+
hideToast();
|
|
193
|
+
clearTimer();
|
|
194
|
+
if (!document.hidden) {
|
|
195
|
+
void checkForUpdate();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function handleVisibilityChange() {
|
|
200
|
+
if (typeof __IS_DEV__ !== 'undefined' && __IS_DEV__) {
|
|
201
|
+
console.log(
|
|
202
|
+
`[page-update] visibilitychange hidden=${document.hidden} visibility=${document.visibilityState} focus=${document.hasFocus()}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
clearTimer();
|
|
207
|
+
if (document.hidden) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
void checkForUpdate();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function handleNavigation() {
|
|
215
|
+
if (typeof __IS_DEV__ !== 'undefined' && __IS_DEV__) {
|
|
216
|
+
console.log(
|
|
217
|
+
`[page-update] navigation path=${window.location.pathname} visibility=${document.visibilityState} focus=${document.hasFocus()}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
resetState();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function handleDismiss() {
|
|
224
|
+
dismissedValidatorKey =
|
|
225
|
+
nextValidator === null ? null : getPreferredValidatorKey(nextValidator);
|
|
226
|
+
hideToast();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function handleWindowFocus() {
|
|
230
|
+
if (typeof __IS_DEV__ !== 'undefined' && __IS_DEV__) {
|
|
231
|
+
console.log(
|
|
232
|
+
`[page-update] window focus visibility=${document.visibilityState} focus=${document.hasFocus()}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function handleWindowBlur() {
|
|
238
|
+
if (typeof __IS_DEV__ !== 'undefined' && __IS_DEV__) {
|
|
239
|
+
console.log(
|
|
240
|
+
`[page-update] window blur visibility=${document.visibilityState} focus=${document.hasFocus()}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function handleReloadClick() {
|
|
246
|
+
void refreshPage(window);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
reloadButton.addEventListener('click', handleReloadClick);
|
|
250
|
+
dismissButton.addEventListener('click', handleDismiss);
|
|
251
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
252
|
+
window.addEventListener('focus', handleWindowFocus);
|
|
253
|
+
window.addEventListener('blur', handleWindowBlur);
|
|
254
|
+
window.addEventListener(NAVIGATION_EVENT, handleNavigation);
|
|
255
|
+
|
|
256
|
+
if (!document.hidden) {
|
|
257
|
+
if (typeof __IS_DEV__ !== 'undefined' && __IS_DEV__) {
|
|
258
|
+
console.log(
|
|
259
|
+
`[page-update] mount path=${currentPath} interval=${pollIntervalMs} visibility=${document.visibilityState} focus=${document.hasFocus()}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
void checkForUpdate();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return () => {
|
|
267
|
+
clearTimer();
|
|
268
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
269
|
+
window.removeEventListener('focus', handleWindowFocus);
|
|
270
|
+
window.removeEventListener('blur', handleWindowBlur);
|
|
271
|
+
window.removeEventListener(NAVIGATION_EVENT, handleNavigation);
|
|
272
|
+
dismissButton.removeEventListener('click', handleDismiss);
|
|
273
|
+
reloadButton.removeEventListener('click', handleReloadClick);
|
|
274
|
+
container.remove();
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export default function mount(window: Window): (() => void) | undefined {
|
|
279
|
+
return mountPageUpdate(window);
|
|
280
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
.page-update-container {
|
|
2
|
+
position: fixed;
|
|
3
|
+
right: 0;
|
|
4
|
+
bottom: 0;
|
|
5
|
+
left: 0;
|
|
6
|
+
z-index: 1000;
|
|
7
|
+
display: flex;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
padding: 1rem;
|
|
10
|
+
pointer-events: none;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.page-update-toast {
|
|
14
|
+
display: none;
|
|
15
|
+
width: min(100%, 640px);
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
color: var(--fg-color);
|
|
18
|
+
pointer-events: auto;
|
|
19
|
+
background: var(--bg-color);
|
|
20
|
+
border: var(--control-border-width) solid var(--fg2-color);
|
|
21
|
+
border-radius: var(--border-radius);
|
|
22
|
+
box-shadow: var(--box-shadow);
|
|
23
|
+
|
|
24
|
+
&.is-showing {
|
|
25
|
+
display: block;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.page-update-body {
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: var(--gap);
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
padding: var(--gap);
|
|
34
|
+
|
|
35
|
+
@media (width <= 580px) {
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
align-items: stretch;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.page-update-message {
|
|
42
|
+
min-width: 0;
|
|
43
|
+
font-size: var(--font-size-small);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.page-update-actions {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex: 0 0 auto;
|
|
49
|
+
gap: var(--gap);
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: space-between;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/print/style.scss
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
@media print {
|
|
4
4
|
html,
|
|
5
5
|
body {
|
|
6
|
-
background: none !important;
|
|
7
6
|
color: #000;
|
|
7
|
+
background: none !important;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
hr {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
margin-top: 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
main a:not(.heading-anchor
|
|
28
|
+
main a:not(.heading-anchor, .footnote-ref, .line-number) {
|
|
29
29
|
@include link-underline;
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -76,10 +76,10 @@
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
section {
|
|
79
|
-
|
|
79
|
+
width: 100%;
|
|
80
80
|
padding: 0;
|
|
81
81
|
margin: 0;
|
|
82
|
-
|
|
82
|
+
border: none;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
a.heading-anchor::after {
|