@abreen/tada 1.17.0 → 1.18.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 +41 -7
- package/bin/tada.ts +23 -9
- package/bin/validators.ts +34 -1
- package/build/branding.ts +41 -0
- package/build/build-manifest.ts +10 -5
- package/build/{watch/compiler-types.d.ts → build-types.d.ts} +8 -5
- package/build/build-validation.ts +176 -0
- package/build/bundle.ts +75 -65
- package/build/columns-plugin.ts +27 -1
- package/build/config-loader.ts +3 -3
- package/build/custom-fonts.ts +8 -5
- package/build/deflist-id-plugin.ts +2 -2
- package/build/generate-favicon.ts +7 -6
- package/build/generate-fonts.ts +8 -15
- package/build/generate-katex-assets.ts +19 -17
- package/build/generate-web-app-manifest.ts +3 -11
- package/build/lodash-template.ts +14 -0
- package/build/output-publication.ts +238 -0
- package/build/pagefind.ts +49 -21
- package/build/pdf-text.ts +41 -1
- package/build/pipeline.ts +95 -101
- package/build/site-assets.ts +46 -0
- package/build/site-build.ts +267 -0
- package/build/source-model.ts +223 -229
- package/build/source-records.ts +69 -81
- package/build/template-globals.ts +3 -0
- package/build/templates.ts +12 -8
- package/build/toc-plugin.ts +21 -21
- package/build/types.d.ts +38 -38
- package/build/util.ts +0 -4
- package/build/utils/code.ts +51 -117
- package/build/utils/final-html.ts +171 -47
- package/build/utils/jdi-runner/LiterateRunner.java +9 -1
- package/build/utils/literate-java.ts +11 -7
- package/build/utils/markdown-partials.ts +4 -3
- package/build/utils/markdown.ts +6 -17
- package/build/utils/paths.ts +3 -0
- package/build/utils/plain-text.ts +18 -0
- package/build/utils/render.ts +197 -154
- package/build/utils/shiki-highlighter.ts +11 -0
- package/build/utils/source-template.ts +2 -2
- package/build/utils/trace-core.ts +8 -83
- package/build/utils/trace.ts +38 -95
- package/build/validate-config-links.ts +39 -6
- package/build/watch/build-incremental.ts +46 -103
- package/build/watch/compiler.ts +158 -37
- package/build/watch/engine.ts +207 -162
- package/build/watch/index.ts +9 -2
- package/build/watch/planner.ts +88 -144
- package/build/watch/runtime.ts +21 -34
- package/build/watch/types.d.ts +32 -64
- package/package.json +4 -5
- package/schema/site.schema.json +18 -2
- package/src/_content.scss +447 -0
- package/src/_fonts.scss +90 -0
- package/src/_layout.scss +50 -35
- package/src/appearance-picker/{style.scss → _index.scss} +1 -1
- package/src/code/{style.scss → _index.scss} +61 -0
- package/src/header/{_base.scss → _index.scss} +98 -6
- package/src/index.ts +1 -17
- package/src/navigate/{style.scss → _index.scss} +20 -0
- package/src/navigate/eligible.ts +0 -40
- package/src/navigate/index.ts +50 -5
- package/src/navigate/lifecycle.ts +19 -2
- package/src/navigate/runtime.ts +20 -4
- package/src/page-update/index.ts +33 -20
- package/src/print/{style.scss → _index.scss} +3 -1
- package/src/search/index.ts +30 -2
- package/src/slides/{style.scss → _index.scss} +5 -0
- package/src/style.scss +24 -536
- package/src/timezone/index.ts +29 -3
- package/src/timezone/time-format.ts +4 -0
- package/src/toc/index.ts +6 -5
- package/src/trace/index.ts +11 -1
- package/templates/_author.html +2 -2
- package/templates/_download.html +1 -1
- package/templates/_footer.html +1 -1
- package/templates/_heading.html +3 -3
- package/templates/_nav.html +2 -2
- package/templates/_page-bottom.html +6 -0
- package/templates/_top.html +18 -14
- package/templates/code.html +22 -21
- package/templates/default.html +12 -11
- package/templates/literate.html +6 -5
- package/build/copy.ts +0 -82
- package/build/generate-content-assets.ts +0 -121
- package/build/utils/content-files.ts +0 -170
- package/build/watch/assets.ts +0 -103
- package/build/watch/build-full.ts +0 -97
- package/build/watch/build-helpers.ts +0 -66
- package/build/watch/build-result.ts +0 -19
- package/build/watch/fs-commit.ts +0 -153
- package/build/watch/mutations.ts +0 -73
- package/build/watch/snapshot.ts +0 -201
- package/build/watch/validation.ts +0 -50
- package/src/code.scss +0 -60
- package/src/critical.scss +0 -5
- package/src/header/style.scss +0 -91
- /package/src/{literate.scss → _literate.scss} +0 -0
- /package/src/{material-symbols.scss → _material-symbols.scss} +0 -0
- /package/src/anchor/{style.scss → _index.scss} +0 -0
- /package/src/page-update/{style.scss → _index.scss} +0 -0
- /package/src/question/{style.scss → _index.scss} +0 -0
- /package/src/search/{style.scss → _index.scss} +0 -0
- /package/src/timezone/{style.scss → _index.scss} +0 -0
- /package/src/toc/{style.scss → _index.scss} +0 -0
- /package/src/trace/{style.scss → _index.scss} +0 -0
|
@@ -1,3 +1,64 @@
|
|
|
1
|
+
@use '../mixins' as *;
|
|
2
|
+
|
|
3
|
+
body.code {
|
|
4
|
+
--code-gutter-width: 4rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.file-header,
|
|
8
|
+
.slides-header {
|
|
9
|
+
display: flex;
|
|
10
|
+
gap: var(--gap);
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
padding: var(--gap);
|
|
14
|
+
margin-bottom: var(--gap);
|
|
15
|
+
border: 1px solid var(--fg2-color);
|
|
16
|
+
border-radius: var(--border-radius);
|
|
17
|
+
|
|
18
|
+
tt,
|
|
19
|
+
code {
|
|
20
|
+
padding: 0;
|
|
21
|
+
background: none;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.code-row {
|
|
26
|
+
display: grid;
|
|
27
|
+
grid-template-columns: var(--code-gutter-width) 1fr;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.line-number {
|
|
31
|
+
position: sticky;
|
|
32
|
+
left: 0;
|
|
33
|
+
z-index: 1;
|
|
34
|
+
display: block;
|
|
35
|
+
padding: 0 0.75em;
|
|
36
|
+
margin-right: var(--gap);
|
|
37
|
+
font-family: var(--mono-font);
|
|
38
|
+
font-size-adjust: var(--mono-font-size-adjust);
|
|
39
|
+
font-feature-settings: var(--mono-font-feature-settings);
|
|
40
|
+
color: var(--fg2-color);
|
|
41
|
+
text-align: right;
|
|
42
|
+
text-decoration: none;
|
|
43
|
+
background: var(--bg2-color);
|
|
44
|
+
border-right: 1px solid var(--fg2-color);
|
|
45
|
+
|
|
46
|
+
@include user-select(none);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
a.line-number {
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
|
|
52
|
+
&:hover {
|
|
53
|
+
color: var(--fg-color);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&:target {
|
|
57
|
+
color: var(--bg-color);
|
|
58
|
+
background: var(--fg2-color);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
1
62
|
body.code .file-header {
|
|
2
63
|
h1 {
|
|
3
64
|
padding: 0;
|
|
@@ -191,6 +191,16 @@ header summary .logo {
|
|
|
191
191
|
background: var(--theme-color);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
header summary .logo-image {
|
|
195
|
+
flex: 0 0 auto;
|
|
196
|
+
width: auto;
|
|
197
|
+
max-width: 96px;
|
|
198
|
+
height: 24px;
|
|
199
|
+
margin: 0 8px 0 0;
|
|
200
|
+
pointer-events: none;
|
|
201
|
+
object-fit: contain;
|
|
202
|
+
}
|
|
203
|
+
|
|
194
204
|
header summary .site-title {
|
|
195
205
|
flex: 1 1 0;
|
|
196
206
|
min-width: 0;
|
|
@@ -230,12 +240,6 @@ header .to-top-container {
|
|
|
230
240
|
}
|
|
231
241
|
}
|
|
232
242
|
|
|
233
|
-
// Hide until the full stylesheet loads (prevents FOUC where the container
|
|
234
|
-
// is visible as a block element before it gets position: absolute)
|
|
235
|
-
header .results-container {
|
|
236
|
-
display: none;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
243
|
header .search-controls {
|
|
240
244
|
position: relative;
|
|
241
245
|
display: flex;
|
|
@@ -298,3 +302,91 @@ header.loading::after {
|
|
|
298
302
|
background-size: 200% 100%;
|
|
299
303
|
animation: shimmer 3s ease-in-out infinite;
|
|
300
304
|
}
|
|
305
|
+
|
|
306
|
+
header nav {
|
|
307
|
+
display: flex;
|
|
308
|
+
flex-wrap: wrap;
|
|
309
|
+
|
|
310
|
+
@include user-select(none);
|
|
311
|
+
|
|
312
|
+
gap: var(--gap);
|
|
313
|
+
margin: var(--gap);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
header nav div {
|
|
317
|
+
flex: 1 1 150px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
header nav ul {
|
|
321
|
+
padding: 0;
|
|
322
|
+
margin: 8px 0 0;
|
|
323
|
+
list-style-type: none;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
header nav ul li {
|
|
327
|
+
margin: 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
header nav a {
|
|
331
|
+
display: inline;
|
|
332
|
+
border: none;
|
|
333
|
+
|
|
334
|
+
@include subdued-link;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
header nav a.external {
|
|
338
|
+
border: none;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
@media only screen and (width >= 740px) {
|
|
342
|
+
header details nav {
|
|
343
|
+
min-height: 100px;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
header nav div > p {
|
|
348
|
+
padding-bottom: 2px;
|
|
349
|
+
font-style: italic;
|
|
350
|
+
border-bottom: 1px solid var(--fg-color);
|
|
351
|
+
|
|
352
|
+
@include user-select(none);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
:root[data-contrast-preference='high'] header:has(details[open]) {
|
|
356
|
+
background: var(--bg2-color);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
@media (hover: hover) {
|
|
360
|
+
header:has(details[open]):hover {
|
|
361
|
+
background: var(--bg-color-translucent);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
:root[data-contrast-preference='high'] header:hover {
|
|
365
|
+
background: var(--bg2-color);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
header .results-container {
|
|
370
|
+
position: absolute;
|
|
371
|
+
top: var(--header-collapsed-height);
|
|
372
|
+
right: 0;
|
|
373
|
+
left: 0;
|
|
374
|
+
z-index: 1000;
|
|
375
|
+
box-sizing: border-box;
|
|
376
|
+
max-width: 640px;
|
|
377
|
+
padding: var(--gap);
|
|
378
|
+
margin: 0 auto;
|
|
379
|
+
pointer-events: none;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
header ol {
|
|
383
|
+
flex: 1;
|
|
384
|
+
min-height: 0;
|
|
385
|
+
padding: 0;
|
|
386
|
+
overflow-y: auto;
|
|
387
|
+
|
|
388
|
+
.excerpt {
|
|
389
|
+
height: 5ex;
|
|
390
|
+
line-height: 2.5ex;
|
|
391
|
+
}
|
|
392
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
|
+
// The single Sass entry point; it loads every other stylesheet
|
|
1
2
|
import './style.scss';
|
|
2
3
|
|
|
3
|
-
import './material-symbols.scss';
|
|
4
|
-
import './anchor/style.scss';
|
|
5
|
-
import './code.scss';
|
|
6
|
-
import './code/style.scss';
|
|
7
|
-
import './literate.scss';
|
|
8
|
-
import './header/style.scss';
|
|
9
|
-
import './print/style.scss';
|
|
10
|
-
import './question/style.scss';
|
|
11
|
-
import './search/style.scss';
|
|
12
|
-
import './slides/style.scss';
|
|
13
|
-
import './timezone/style.scss';
|
|
14
|
-
import './toc/style.scss';
|
|
15
|
-
import './trace/style.scss';
|
|
16
|
-
import './appearance-picker/style.scss';
|
|
17
|
-
import './navigate/style.scss';
|
|
18
|
-
import './page-update/style.scss';
|
|
19
|
-
|
|
20
4
|
import mountSearch from './search';
|
|
21
5
|
import mountHeader from './header';
|
|
22
6
|
import mountTop from './top';
|
|
@@ -68,6 +68,26 @@ $movement-easing: cubic-bezier(0.2, 0, 0, 1);
|
|
|
68
68
|
mix-blend-mode: normal;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// The footer and appearance controls get their own stationary layer. Show only
|
|
72
|
+
// the incoming snapshot so disabled defaults and stale hover states cannot blend.
|
|
73
|
+
.page-bottom {
|
|
74
|
+
view-transition-name: page-bottom;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
::view-transition-group(page-bottom),
|
|
78
|
+
::view-transition-old(page-bottom),
|
|
79
|
+
::view-transition-new(page-bottom) {
|
|
80
|
+
animation: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
::view-transition-old(page-bottom) {
|
|
84
|
+
display: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
::view-transition-new(page-bottom) {
|
|
88
|
+
mix-blend-mode: normal;
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
// Forward navigation: old content slides left, new content slides in from right
|
|
72
92
|
.nav-forward::view-transition-old(root) {
|
|
73
93
|
animation:
|
package/src/navigate/eligible.ts
CHANGED
|
@@ -1,34 +1,3 @@
|
|
|
1
|
-
const NON_HTML_EXTENSIONS = new Set([
|
|
2
|
-
'.pdf',
|
|
3
|
-
'.java',
|
|
4
|
-
'.py',
|
|
5
|
-
'.png',
|
|
6
|
-
'.jpg',
|
|
7
|
-
'.jpeg',
|
|
8
|
-
'.gif',
|
|
9
|
-
'.svg',
|
|
10
|
-
'.zip',
|
|
11
|
-
'.tar',
|
|
12
|
-
'.gz',
|
|
13
|
-
'.json',
|
|
14
|
-
'.xml',
|
|
15
|
-
'.css',
|
|
16
|
-
'.js',
|
|
17
|
-
'.ts',
|
|
18
|
-
'.c',
|
|
19
|
-
'.h',
|
|
20
|
-
'.cpp',
|
|
21
|
-
'.rb',
|
|
22
|
-
'.go',
|
|
23
|
-
'.rs',
|
|
24
|
-
'.txt',
|
|
25
|
-
'.csv',
|
|
26
|
-
'.ico',
|
|
27
|
-
'.woff2',
|
|
28
|
-
'.mp4',
|
|
29
|
-
'.mp3',
|
|
30
|
-
]);
|
|
31
|
-
|
|
32
1
|
export function isEligibleLink(
|
|
33
2
|
href: string,
|
|
34
3
|
origin: string,
|
|
@@ -56,14 +25,5 @@ export function isEligibleLink(
|
|
|
56
25
|
return false;
|
|
57
26
|
}
|
|
58
27
|
|
|
59
|
-
const path = url.pathname;
|
|
60
|
-
const dotIndex = path.lastIndexOf('.');
|
|
61
|
-
if (dotIndex !== -1) {
|
|
62
|
-
const ext = path.slice(dotIndex).toLowerCase();
|
|
63
|
-
if (NON_HTML_EXTENSIONS.has(ext)) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
28
|
return true;
|
|
69
29
|
}
|
package/src/navigate/index.ts
CHANGED
|
@@ -24,11 +24,17 @@ function shouldIgnoreClick(
|
|
|
24
24
|
event: MouseEvent,
|
|
25
25
|
anchor: HTMLAnchorElement,
|
|
26
26
|
): boolean {
|
|
27
|
-
if (
|
|
27
|
+
if (
|
|
28
|
+
event.button !== 0 ||
|
|
29
|
+
event.ctrlKey ||
|
|
30
|
+
event.metaKey ||
|
|
31
|
+
event.shiftKey ||
|
|
32
|
+
event.altKey
|
|
33
|
+
) {
|
|
28
34
|
return true;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
if (anchor.target) {
|
|
37
|
+
if (anchor.hasAttribute('target') || anchor.hasAttribute('download')) {
|
|
32
38
|
return true;
|
|
33
39
|
}
|
|
34
40
|
|
|
@@ -42,6 +48,7 @@ function shouldIgnoreClick(
|
|
|
42
48
|
|
|
43
49
|
export default function mountNavigate(window: Window): () => void {
|
|
44
50
|
initNavigation(window);
|
|
51
|
+
let clearingFragment = false;
|
|
45
52
|
|
|
46
53
|
// Track scroll position on every scroll event. We keep the latest
|
|
47
54
|
// scrollY per navIndex so that back/forward navigation can restore
|
|
@@ -68,7 +75,7 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
68
75
|
return;
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
// Same-page
|
|
78
|
+
// Same-page links: handle locally so we can clear search and
|
|
72
79
|
// close the header without a full SPA fetch.
|
|
73
80
|
const url = new URL(anchor.href);
|
|
74
81
|
if (
|
|
@@ -84,10 +91,29 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
84
91
|
} else {
|
|
85
92
|
globals.setLocationHash(window, url.hash);
|
|
86
93
|
}
|
|
94
|
+
} else {
|
|
95
|
+
saveScrollPosition(window);
|
|
96
|
+
if (window.location.hash) {
|
|
97
|
+
const state = window.history.state;
|
|
98
|
+
// WebKit fires popstate synchronously while clearing the fragment.
|
|
99
|
+
// Ignore that internal navigation so it cannot restore the previous
|
|
100
|
+
// fragment-free entry's saved scroll position.
|
|
101
|
+
clearingFragment = true;
|
|
102
|
+
globals.setLocationHash(window, '');
|
|
103
|
+
// Native fragment clearing leaves a trailing #. Keep the authored
|
|
104
|
+
// URL and the current SPA history state on the new entry.
|
|
105
|
+
window.history.replaceState(state, '', url.href);
|
|
106
|
+
}
|
|
107
|
+
window.scrollTo({ top: 0 });
|
|
108
|
+
saveScrollPosition(window);
|
|
87
109
|
}
|
|
88
110
|
return;
|
|
89
111
|
}
|
|
90
112
|
|
|
113
|
+
if (!anchor.hasAttribute('data-tada-page')) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
91
117
|
event.preventDefault();
|
|
92
118
|
|
|
93
119
|
// Clear search and close header immediately, before the fetch
|
|
@@ -108,11 +134,30 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
108
134
|
}
|
|
109
135
|
|
|
110
136
|
function handlePopState(event: PopStateEvent) {
|
|
137
|
+
if (clearingFragment) {
|
|
138
|
+
clearingFragment = false;
|
|
139
|
+
if (!window.location.hash) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
111
144
|
const newPath = window.location.pathname + window.location.search;
|
|
112
145
|
if (newPath === getCurrentPath()) {
|
|
113
|
-
const
|
|
146
|
+
const locationKey = newPath + window.location.hash;
|
|
147
|
+
const savedY = getSavedLocationScroll(locationKey);
|
|
114
148
|
if (typeof savedY === 'number') {
|
|
115
|
-
|
|
149
|
+
// Fragment traversal can apply its native target scroll after popstate.
|
|
150
|
+
// Restore in the next task so our saved position wins. Capture savedY
|
|
151
|
+
// now because the native scroll event can update the location map first.
|
|
152
|
+
window.setTimeout(() => {
|
|
153
|
+
const currentLocationKey =
|
|
154
|
+
window.location.pathname +
|
|
155
|
+
window.location.search +
|
|
156
|
+
window.location.hash;
|
|
157
|
+
if (currentLocationKey === locationKey) {
|
|
158
|
+
window.scrollTo({ top: savedY });
|
|
159
|
+
}
|
|
160
|
+
}, 0);
|
|
116
161
|
return;
|
|
117
162
|
}
|
|
118
163
|
|
|
@@ -21,15 +21,31 @@ const PER_PAGE_COMPONENTS: Record<
|
|
|
21
21
|
trace: mountTrace,
|
|
22
22
|
slides: mountSlides,
|
|
23
23
|
print: mountPrint,
|
|
24
|
-
appearancePicker: mountAppearancePicker,
|
|
25
24
|
};
|
|
26
25
|
|
|
27
26
|
let cleanups: (() => void)[] = [];
|
|
27
|
+
let appearancePickerMounted = false;
|
|
28
|
+
|
|
29
|
+
// Navigation calls this during the swap so the new transition snapshot shows
|
|
30
|
+
// enabled switches with the visitor's preferences, rather than build defaults.
|
|
31
|
+
export function mountAppearancePickerForPage(window: Window): void {
|
|
32
|
+
if (appearancePickerMounted) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
appearancePickerMounted = true;
|
|
36
|
+
try {
|
|
37
|
+
cleanups.push(mountAppearancePicker(window));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (__IS_DEV__) {
|
|
40
|
+
console.error('Failed to mount appearancePicker component:', String(err));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
28
44
|
|
|
29
45
|
export async function mountPerPageComponents(
|
|
30
46
|
window: Window,
|
|
31
47
|
): Promise<() => void> {
|
|
32
|
-
|
|
48
|
+
mountAppearancePickerForPage(window);
|
|
33
49
|
|
|
34
50
|
const entries = Object.entries(PER_PAGE_COMPONENTS);
|
|
35
51
|
|
|
@@ -66,4 +82,5 @@ export function teardownPerPageComponents(): void {
|
|
|
66
82
|
}
|
|
67
83
|
}
|
|
68
84
|
cleanups = [];
|
|
85
|
+
appearancePickerMounted = false;
|
|
69
86
|
}
|
package/src/navigate/runtime.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getResponseValidators, type ResponseValidators } from '../validators';
|
|
2
|
+
import {
|
|
3
|
+
mountAppearancePickerForPage,
|
|
4
|
+
mountPerPageComponents,
|
|
5
|
+
teardownPerPageComponents,
|
|
6
|
+
} from './lifecycle';
|
|
2
7
|
import { globals } from '../globals';
|
|
3
8
|
|
|
4
9
|
export const NAVIGATION_EVENT = 'tada:navigation';
|
|
@@ -93,12 +98,22 @@ export function closeHeaderDetails(document: Document): void {
|
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
|
|
101
|
+
export interface NavigationEventDetail {
|
|
102
|
+
path: string;
|
|
103
|
+
validators: ResponseValidators;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function dispatchNavigationEvent(
|
|
107
|
+
window: Window,
|
|
108
|
+
validators: ResponseValidators,
|
|
109
|
+
): void {
|
|
97
110
|
const CustomEventCtor = (
|
|
98
111
|
window as unknown as { CustomEvent: typeof CustomEvent }
|
|
99
112
|
).CustomEvent;
|
|
100
113
|
window.dispatchEvent(
|
|
101
|
-
new CustomEventCtor(NAVIGATION_EVENT, {
|
|
114
|
+
new CustomEventCtor(NAVIGATION_EVENT, {
|
|
115
|
+
detail: { path: currentPath, validators },
|
|
116
|
+
}),
|
|
102
117
|
);
|
|
103
118
|
}
|
|
104
119
|
|
|
@@ -288,6 +303,7 @@ export async function navigateToUrl(
|
|
|
288
303
|
teardownPerPageComponents();
|
|
289
304
|
swapContent(document, newDoc);
|
|
290
305
|
updateHead(document, newDoc);
|
|
306
|
+
mountAppearancePickerForPage(window);
|
|
291
307
|
currentPath = parsed.pathname + parsed.search;
|
|
292
308
|
|
|
293
309
|
if (pushHistory) {
|
|
@@ -396,7 +412,7 @@ export async function navigateToUrl(
|
|
|
396
412
|
}
|
|
397
413
|
|
|
398
414
|
currentAbortController = null;
|
|
399
|
-
dispatchNavigationEvent(window);
|
|
415
|
+
dispatchNavigationEvent(window, getResponseValidators(response));
|
|
400
416
|
}
|
|
401
417
|
|
|
402
418
|
export async function refreshCurrentPage(window: Window): Promise<void> {
|
package/src/page-update/index.ts
CHANGED
|
@@ -72,7 +72,9 @@ export function mountPageUpdate(
|
|
|
72
72
|
const { container, toast, reloadButton, dismissButton } =
|
|
73
73
|
createToast(document);
|
|
74
74
|
|
|
75
|
-
let currentPath = window.location.pathname;
|
|
75
|
+
let currentPath = window.location.pathname + window.location.search;
|
|
76
|
+
let navigationGeneration = 0;
|
|
77
|
+
let disposed = false;
|
|
76
78
|
let baseline = EMPTY_RESPONSE_VALIDATORS;
|
|
77
79
|
let dismissedValidatorKey: string | null = null;
|
|
78
80
|
let nextValidator: ResponseValidators | null = null;
|
|
@@ -98,7 +100,7 @@ export function mountPageUpdate(
|
|
|
98
100
|
|
|
99
101
|
function scheduleNextCheck() {
|
|
100
102
|
clearTimer();
|
|
101
|
-
if (globals.isDocumentHidden(document)) {
|
|
103
|
+
if (disposed || globals.isDocumentHidden(document)) {
|
|
102
104
|
return;
|
|
103
105
|
}
|
|
104
106
|
timer = window.setTimeout(() => {
|
|
@@ -145,11 +147,16 @@ export function mountPageUpdate(
|
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
const requestedPath = currentPath;
|
|
148
|
-
|
|
150
|
+
const requestedGeneration = navigationGeneration;
|
|
149
151
|
checkInFlight = true;
|
|
150
152
|
try {
|
|
151
153
|
const validators = await fetchValidators(requestedPath);
|
|
152
|
-
if (
|
|
154
|
+
if (
|
|
155
|
+
!validators ||
|
|
156
|
+
disposed ||
|
|
157
|
+
requestedGeneration !== navigationGeneration ||
|
|
158
|
+
requestedPath !== currentPath
|
|
159
|
+
) {
|
|
153
160
|
return;
|
|
154
161
|
}
|
|
155
162
|
|
|
@@ -175,30 +182,32 @@ export function mountPageUpdate(
|
|
|
175
182
|
// best-effort
|
|
176
183
|
} finally {
|
|
177
184
|
checkInFlight = false;
|
|
178
|
-
if (
|
|
185
|
+
if (
|
|
186
|
+
rerunAfterCurrentCheck &&
|
|
187
|
+
!disposed &&
|
|
188
|
+
!globals.isDocumentHidden(document)
|
|
189
|
+
) {
|
|
179
190
|
rerunAfterCurrentCheck = false;
|
|
180
|
-
|
|
181
|
-
} else {
|
|
191
|
+
void checkForUpdate();
|
|
192
|
+
} else if (requestedGeneration === navigationGeneration) {
|
|
182
193
|
scheduleNextCheck();
|
|
183
194
|
}
|
|
184
195
|
}
|
|
185
|
-
|
|
186
|
-
if (shouldRerun) {
|
|
187
|
-
void checkForUpdate();
|
|
188
|
-
}
|
|
189
196
|
}
|
|
190
197
|
|
|
191
|
-
function resetState(
|
|
192
|
-
|
|
193
|
-
|
|
198
|
+
function resetState(
|
|
199
|
+
validators: ResponseValidators = EMPTY_RESPONSE_VALIDATORS,
|
|
200
|
+
) {
|
|
201
|
+
navigationGeneration++;
|
|
202
|
+
rerunAfterCurrentCheck = false;
|
|
203
|
+
currentPath = window.location.pathname + window.location.search;
|
|
204
|
+
baseline = validators;
|
|
194
205
|
dismissedValidatorKey = null;
|
|
195
206
|
nextValidator = null;
|
|
196
|
-
hasBaseline =
|
|
207
|
+
hasBaseline = hasUsableResponseValidators(validators);
|
|
197
208
|
hideToast();
|
|
198
209
|
clearTimer();
|
|
199
|
-
|
|
200
|
-
void checkForUpdate();
|
|
201
|
-
}
|
|
210
|
+
scheduleNextCheck();
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
function handleVisibilityChange() {
|
|
@@ -216,13 +225,16 @@ export function mountPageUpdate(
|
|
|
216
225
|
void checkForUpdate();
|
|
217
226
|
}
|
|
218
227
|
|
|
219
|
-
function handleNavigation() {
|
|
228
|
+
function handleNavigation(event: Event) {
|
|
220
229
|
if (__IS_DEV__) {
|
|
221
230
|
console.log(
|
|
222
231
|
`[page-update] navigation path=${window.location.pathname} visibility=${document.visibilityState} focus=${document.hasFocus()}`,
|
|
223
232
|
);
|
|
224
233
|
}
|
|
225
|
-
resetState(
|
|
234
|
+
resetState(
|
|
235
|
+
(event as CustomEvent<{ validators?: ResponseValidators }>).detail
|
|
236
|
+
?.validators,
|
|
237
|
+
);
|
|
226
238
|
}
|
|
227
239
|
|
|
228
240
|
function handleDismiss() {
|
|
@@ -272,6 +284,7 @@ export function mountPageUpdate(
|
|
|
272
284
|
}
|
|
273
285
|
|
|
274
286
|
return () => {
|
|
287
|
+
disposed = true;
|
|
275
288
|
clearTimer();
|
|
276
289
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
277
290
|
window.removeEventListener('focus', handleWindowFocus);
|
package/src/search/index.ts
CHANGED
|
@@ -141,6 +141,10 @@ function render(
|
|
|
141
141
|
a.id = `result-${i}`;
|
|
142
142
|
a.className = 'result';
|
|
143
143
|
a.href = result.url;
|
|
144
|
+
const isGeneratedPage = Boolean(result.template);
|
|
145
|
+
if (isGeneratedPage) {
|
|
146
|
+
a.setAttribute('data-tada-page', '');
|
|
147
|
+
}
|
|
144
148
|
a.tabIndex = 0;
|
|
145
149
|
|
|
146
150
|
const titleEl = doc.createElement('div');
|
|
@@ -177,6 +181,9 @@ function render(
|
|
|
177
181
|
for (const sub of subsToShow) {
|
|
178
182
|
const subA = doc.createElement('a');
|
|
179
183
|
subA.href = sub.url;
|
|
184
|
+
if (isGeneratedPage) {
|
|
185
|
+
subA.setAttribute('data-tada-page', '');
|
|
186
|
+
}
|
|
180
187
|
subA.className = 'sub-result';
|
|
181
188
|
|
|
182
189
|
const subTitle = doc.createElement('div');
|
|
@@ -281,6 +288,7 @@ export default (window: Window) => {
|
|
|
281
288
|
|
|
282
289
|
let pagefindLoadPromise: Promise<boolean> | null = null;
|
|
283
290
|
let pagefindLoadGeneration = 0;
|
|
291
|
+
let disposed = false;
|
|
284
292
|
|
|
285
293
|
function pagefindModulePath(generation: number) {
|
|
286
294
|
const path = applyBasePath('/pagefind/pagefind.js');
|
|
@@ -291,6 +299,9 @@ export default (window: Window) => {
|
|
|
291
299
|
}
|
|
292
300
|
|
|
293
301
|
async function loadPagefind(): Promise<boolean> {
|
|
302
|
+
if (disposed) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
294
305
|
if (pagefind) {
|
|
295
306
|
return true;
|
|
296
307
|
}
|
|
@@ -317,7 +328,18 @@ export default (window: Window) => {
|
|
|
317
328
|
}
|
|
318
329
|
pagefind = loadedPagefind;
|
|
319
330
|
return true;
|
|
320
|
-
})()
|
|
331
|
+
})().catch(err => {
|
|
332
|
+
if (generation === pagefindLoadGeneration) {
|
|
333
|
+
// Failed dynamic imports are cached by URL. Retry only on a later
|
|
334
|
+
// interaction, with a fresh URL, sharing one pending load.
|
|
335
|
+
pagefindLoadGeneration += 1;
|
|
336
|
+
console.log(`failed to load Pagefind: ${err}`);
|
|
337
|
+
if (state.showResults) {
|
|
338
|
+
render(input!, resultsContainer, state, false);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return false;
|
|
342
|
+
});
|
|
321
343
|
|
|
322
344
|
const promise = pagefindLoadPromise;
|
|
323
345
|
try {
|
|
@@ -343,6 +365,9 @@ export default (window: Window) => {
|
|
|
343
365
|
render(input!, resultsContainer, state, true);
|
|
344
366
|
}
|
|
345
367
|
const query = state.value;
|
|
368
|
+
if (!(await loadPagefind()) || updateId !== latestUpdateId) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
346
371
|
const nextState = await doSearch(query, window);
|
|
347
372
|
if (updateId !== latestUpdateId) {
|
|
348
373
|
return;
|
|
@@ -370,7 +395,7 @@ export default (window: Window) => {
|
|
|
370
395
|
function loadPagefindAndUpdate() {
|
|
371
396
|
loadPagefind()
|
|
372
397
|
.then(loadedCurrentPagefind => {
|
|
373
|
-
if (loadedCurrentPagefind && state.showResults) {
|
|
398
|
+
if (loadedCurrentPagefind && !disposed && state.showResults) {
|
|
374
399
|
queueUpdate();
|
|
375
400
|
}
|
|
376
401
|
})
|
|
@@ -601,6 +626,9 @@ export default (window: Window) => {
|
|
|
601
626
|
input.disabled = false;
|
|
602
627
|
|
|
603
628
|
return () => {
|
|
629
|
+
disposed = true;
|
|
630
|
+
pagefindLoadGeneration += 1;
|
|
631
|
+
invalidateUpdates();
|
|
604
632
|
window.removeEventListener(
|
|
605
633
|
PAGE_UPDATE_REFRESH_EVENT,
|
|
606
634
|
handlePageUpdateRefresh,
|