@abreen/tada 1.8.1 → 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/bin/validators.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/templates.ts +0 -2
- 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 +47 -15
- package/build/utils/source-template.ts +27 -0
- 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/init/content/lectures/01/Rectangle.java +2 -0
- package/init/content/lectures/01/index.md +6 -0
- package/init/content/lectures/01/rectangle.py +5 -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
package/src/navigate/index.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { isEligibleLink } from './eligible';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
import {
|
|
3
|
+
clearSearch,
|
|
4
|
+
closeHeaderDetails,
|
|
5
|
+
getCurrentPath,
|
|
6
|
+
getSavedLocationScroll,
|
|
7
|
+
getHistoryIndex,
|
|
8
|
+
getSavedScroll,
|
|
9
|
+
initNavigation,
|
|
10
|
+
navigateToUrl,
|
|
11
|
+
saveScrollPosition,
|
|
12
|
+
setCurrentPath,
|
|
13
|
+
setHistoryIndex,
|
|
14
|
+
} from './runtime';
|
|
14
15
|
|
|
15
16
|
function findAnchor(event: MouseEvent): HTMLAnchorElement | null {
|
|
16
17
|
const target = event.target as HTMLElement | null;
|
|
@@ -37,251 +38,15 @@ function shouldIgnoreClick(
|
|
|
37
38
|
return false;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function updateHead(newDoc: Document): void {
|
|
41
|
-
document.title = newDoc.title;
|
|
42
|
-
|
|
43
|
-
const metaTags = ['description', 'author'];
|
|
44
|
-
for (const name of metaTags) {
|
|
45
|
-
const newMeta = newDoc.querySelector(`meta[name="${name}"]`);
|
|
46
|
-
const oldMeta = document.querySelector(`meta[name="${name}"]`);
|
|
47
|
-
if (newMeta && oldMeta) {
|
|
48
|
-
oldMeta.setAttribute('content', newMeta.getAttribute('content') ?? '');
|
|
49
|
-
} else if (newMeta && !oldMeta) {
|
|
50
|
-
document.head.appendChild(newMeta.cloneNode(true));
|
|
51
|
-
} else if (!newMeta && oldMeta) {
|
|
52
|
-
oldMeta.remove();
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const ogTags = ['og:title', 'og:author'];
|
|
57
|
-
for (const prop of ogTags) {
|
|
58
|
-
const newMeta = newDoc.querySelector(`meta[property="${prop}"]`);
|
|
59
|
-
const oldMeta = document.querySelector(`meta[property="${prop}"]`);
|
|
60
|
-
if (newMeta && oldMeta) {
|
|
61
|
-
oldMeta.setAttribute('content', newMeta.getAttribute('content') ?? '');
|
|
62
|
-
} else if (newMeta && !oldMeta) {
|
|
63
|
-
document.head.appendChild(newMeta.cloneNode(true));
|
|
64
|
-
} else if (!newMeta && oldMeta) {
|
|
65
|
-
oldMeta.remove();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Adopt stylesheet links from the new page that aren't already loaded
|
|
70
|
-
const existingHrefs = new Set(
|
|
71
|
-
Array.from(document.querySelectorAll('link[rel="stylesheet"]')).map(link =>
|
|
72
|
-
link.getAttribute('href'),
|
|
73
|
-
),
|
|
74
|
-
);
|
|
75
|
-
for (const link of newDoc.querySelectorAll('link[rel="stylesheet"]')) {
|
|
76
|
-
if (!existingHrefs.has(link.getAttribute('href'))) {
|
|
77
|
-
document.head.appendChild(link.cloneNode(true));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function swapContent(newDoc: Document): void {
|
|
83
|
-
const newContainer = newDoc.querySelector('.container');
|
|
84
|
-
const oldContainer = document.querySelector('.container');
|
|
85
|
-
if (newContainer && oldContainer) {
|
|
86
|
-
const imported = document.importNode(newContainer, true);
|
|
87
|
-
oldContainer.replaceChildren(...imported.childNodes);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Update body class (carries template name and toc-is-active)
|
|
91
|
-
document.body.className = newDoc.body.className;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function clearSearch(): void {
|
|
95
|
-
const input = document.querySelector(
|
|
96
|
-
'input.search.quick-search',
|
|
97
|
-
) as HTMLInputElement | null;
|
|
98
|
-
if (input && input.value) {
|
|
99
|
-
input.value = '';
|
|
100
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function performNavigation(
|
|
105
|
-
url: string,
|
|
106
|
-
scrollTarget: number | string | null,
|
|
107
|
-
direction: Direction,
|
|
108
|
-
isPush: boolean,
|
|
109
|
-
): Promise<void> {
|
|
110
|
-
if (currentAbortController) {
|
|
111
|
-
currentAbortController.abort();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const controller = new AbortController();
|
|
115
|
-
currentAbortController = controller;
|
|
116
|
-
|
|
117
|
-
const header = document.querySelector('header');
|
|
118
|
-
header?.classList.add('loading');
|
|
119
|
-
|
|
120
|
-
let response: Response;
|
|
121
|
-
try {
|
|
122
|
-
response = await fetch(url, { signal: controller.signal });
|
|
123
|
-
} catch (err: unknown) {
|
|
124
|
-
header?.classList.remove('loading');
|
|
125
|
-
if (err instanceof Error && err.name === 'AbortError') {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
// Fetch failed, fall back to full navigation
|
|
129
|
-
window.location.href = url;
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!response.ok) {
|
|
134
|
-
header?.classList.remove('loading');
|
|
135
|
-
window.location.href = url;
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const html = await response.text();
|
|
140
|
-
header?.classList.remove('loading');
|
|
141
|
-
const parser = new DOMParser();
|
|
142
|
-
const newDoc = parser.parseFromString(html, 'text/html');
|
|
143
|
-
|
|
144
|
-
if (!newDoc.querySelector('meta[name="generator"][content="Tada"]')) {
|
|
145
|
-
window.location.href = url;
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const doSwap = () => {
|
|
150
|
-
teardownPerPageComponents();
|
|
151
|
-
swapContent(newDoc);
|
|
152
|
-
updateHead(newDoc);
|
|
153
|
-
const parsed = new URL(url);
|
|
154
|
-
currentPath = parsed.pathname + parsed.search;
|
|
155
|
-
|
|
156
|
-
if (isPush) {
|
|
157
|
-
historyIndex++;
|
|
158
|
-
// Push without the hash; we add it back below via location.replace
|
|
159
|
-
// so the browser performs a real fragment navigation that updates
|
|
160
|
-
// :target and fires hashchange. pushState alone would not do
|
|
161
|
-
// either of those things.
|
|
162
|
-
const urlWithoutHash = parsed.origin + parsed.pathname + parsed.search;
|
|
163
|
-
window.history.pushState({ navIndex: historyIndex }, '', urlWithoutHash);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (typeof scrollTarget === 'string') {
|
|
167
|
-
// Trigger a real fragment navigation. location.replace overwrites
|
|
168
|
-
// the current entry's URL in place and runs fragment navigation
|
|
169
|
-
// (updating :target, firing hashchange, scrolling to the element).
|
|
170
|
-
// It wipes the entry's state object, so reapply it via
|
|
171
|
-
// replaceState.
|
|
172
|
-
const urlWithHash = parsed.pathname + parsed.search + '#' + scrollTarget;
|
|
173
|
-
window.location.replace(urlWithHash);
|
|
174
|
-
window.history.replaceState({ navIndex: historyIndex }, '', urlWithHash);
|
|
175
|
-
} else if (typeof scrollTarget === 'number') {
|
|
176
|
-
// Restore scroll position (popstate)
|
|
177
|
-
window.scrollTo({ top: scrollTarget });
|
|
178
|
-
} else {
|
|
179
|
-
window.scrollTo({ top: 0 });
|
|
180
|
-
}
|
|
181
|
-
scrollByIndex.set(historyIndex, window.scrollY);
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const doc = document as Document & {
|
|
185
|
-
startViewTransition?: (cb: () => void) => { finished: Promise<void> };
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
if (typeof doc.startViewTransition === 'function') {
|
|
189
|
-
// Only give the title its own transition group if it's visible
|
|
190
|
-
const titleEl = document.querySelector('.title-and-info');
|
|
191
|
-
const headerHeight =
|
|
192
|
-
document.querySelector('header')?.getBoundingClientRect().height ?? 0;
|
|
193
|
-
const titleVisible =
|
|
194
|
-
titleEl != null && titleEl.getBoundingClientRect().top >= headerHeight;
|
|
195
|
-
|
|
196
|
-
if (titleVisible) {
|
|
197
|
-
const h1 = titleEl.querySelector('h1') as HTMLElement | null;
|
|
198
|
-
const info = titleEl.querySelector('.info') as HTMLElement | null;
|
|
199
|
-
const breadcrumb = titleEl.querySelector(
|
|
200
|
-
'a.breadcrumb',
|
|
201
|
-
) as HTMLElement | null;
|
|
202
|
-
if (h1) {
|
|
203
|
-
h1.style.viewTransitionName = 'page-title';
|
|
204
|
-
}
|
|
205
|
-
if (info) {
|
|
206
|
-
info.style.viewTransitionName = 'page-info';
|
|
207
|
-
}
|
|
208
|
-
if (breadcrumb) {
|
|
209
|
-
breadcrumb.style.viewTransitionName = 'page-breadcrumb';
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
document.documentElement.classList.add(
|
|
214
|
-
direction === 'forward' ? 'nav-forward' : 'nav-back',
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
const transition = doc.startViewTransition(() => {
|
|
218
|
-
doSwap();
|
|
219
|
-
// Check title visibility on the new page independently (scroll
|
|
220
|
-
// has been restored by doSwap, so the position reflects the
|
|
221
|
-
// destination page)
|
|
222
|
-
const newTitleEl = document.querySelector('.title-and-info');
|
|
223
|
-
const newHeaderHeight =
|
|
224
|
-
document.querySelector('header')?.getBoundingClientRect().height ?? 0;
|
|
225
|
-
const newTitleVisible =
|
|
226
|
-
newTitleEl != null &&
|
|
227
|
-
newTitleEl.getBoundingClientRect().top >= newHeaderHeight;
|
|
228
|
-
if (newTitleVisible && newTitleEl) {
|
|
229
|
-
const h1 = newTitleEl.querySelector('h1') as HTMLElement | null;
|
|
230
|
-
const info = newTitleEl.querySelector('.info') as HTMLElement | null;
|
|
231
|
-
const breadcrumb = newTitleEl.querySelector(
|
|
232
|
-
'a.breadcrumb',
|
|
233
|
-
) as HTMLElement | null;
|
|
234
|
-
if (h1) {
|
|
235
|
-
h1.style.viewTransitionName = 'page-title';
|
|
236
|
-
}
|
|
237
|
-
if (info) {
|
|
238
|
-
info.style.viewTransitionName = 'page-info';
|
|
239
|
-
}
|
|
240
|
-
if (breadcrumb) {
|
|
241
|
-
breadcrumb.style.viewTransitionName = 'page-breadcrumb';
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
await transition.finished;
|
|
246
|
-
document.documentElement.classList.remove('nav-forward', 'nav-back');
|
|
247
|
-
// Clean up inline styles
|
|
248
|
-
const newH1 = document.querySelector(
|
|
249
|
-
'.title-and-info h1',
|
|
250
|
-
) as HTMLElement | null;
|
|
251
|
-
const newInfo = document.querySelector(
|
|
252
|
-
'.title-and-info .info',
|
|
253
|
-
) as HTMLElement | null;
|
|
254
|
-
if (newH1) {
|
|
255
|
-
newH1.style.viewTransitionName = '';
|
|
256
|
-
}
|
|
257
|
-
if (newInfo) {
|
|
258
|
-
newInfo.style.viewTransitionName = '';
|
|
259
|
-
}
|
|
260
|
-
const newBreadcrumb = document.querySelector(
|
|
261
|
-
'.title-and-info a.breadcrumb',
|
|
262
|
-
) as HTMLElement | null;
|
|
263
|
-
if (newBreadcrumb) {
|
|
264
|
-
newBreadcrumb.style.viewTransitionName = '';
|
|
265
|
-
}
|
|
266
|
-
} else {
|
|
267
|
-
doSwap();
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
await mountPerPageComponents(window);
|
|
271
|
-
currentAbortController = null;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
41
|
export default function mountNavigate(window: Window): () => void {
|
|
275
|
-
window
|
|
276
|
-
currentPath = window.location.pathname + window.location.search;
|
|
277
|
-
scrollByIndex.set(historyIndex, window.scrollY);
|
|
42
|
+
initNavigation(window);
|
|
278
43
|
|
|
279
44
|
// Track scroll position on every scroll event. We keep the latest
|
|
280
45
|
// scrollY per navIndex so that back/forward navigation can restore
|
|
281
46
|
// where the user was, not just scroll 0. Map writes are cheap; no
|
|
282
47
|
// need to debounce.
|
|
283
48
|
const saveScroll = () => {
|
|
284
|
-
|
|
49
|
+
saveScrollPosition(window);
|
|
285
50
|
};
|
|
286
51
|
window.addEventListener('scroll', saveScroll, { passive: true });
|
|
287
52
|
|
|
@@ -309,13 +74,8 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
309
74
|
url.search === window.location.search
|
|
310
75
|
) {
|
|
311
76
|
event.preventDefault();
|
|
312
|
-
clearSearch();
|
|
313
|
-
|
|
314
|
-
'header details',
|
|
315
|
-
) as HTMLDetailsElement | null;
|
|
316
|
-
if (details?.open) {
|
|
317
|
-
details.open = false;
|
|
318
|
-
}
|
|
77
|
+
clearSearch(window.document);
|
|
78
|
+
closeHeaderDetails(window.document);
|
|
319
79
|
if (url.hash) {
|
|
320
80
|
// location.hash assignment performs a real fragment navigation:
|
|
321
81
|
// updates :target, fires hashchange (which the TOC listens for),
|
|
@@ -329,27 +89,32 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
329
89
|
event.preventDefault();
|
|
330
90
|
|
|
331
91
|
// Clear search and close header immediately, before the fetch
|
|
332
|
-
clearSearch();
|
|
333
|
-
|
|
334
|
-
const details = window.document.querySelector(
|
|
335
|
-
'header details',
|
|
336
|
-
) as HTMLDetailsElement | null;
|
|
337
|
-
if (details?.open) {
|
|
338
|
-
details.open = false;
|
|
339
|
-
}
|
|
92
|
+
clearSearch(window.document);
|
|
93
|
+
closeHeaderDetails(window.document);
|
|
340
94
|
|
|
341
95
|
// Save current scroll position for the entry we're leaving
|
|
342
|
-
|
|
96
|
+
saveScrollPosition(window);
|
|
343
97
|
|
|
344
98
|
const hash = url.hash ? url.hash.slice(1) : null;
|
|
345
99
|
|
|
346
|
-
|
|
100
|
+
navigateToUrl(window, {
|
|
101
|
+
url: anchor.href,
|
|
102
|
+
scrollTarget: hash,
|
|
103
|
+
direction: 'forward',
|
|
104
|
+
pushHistory: true,
|
|
105
|
+
});
|
|
347
106
|
}
|
|
348
107
|
|
|
349
108
|
function handlePopState(event: PopStateEvent) {
|
|
350
109
|
const newPath = window.location.pathname + window.location.search;
|
|
351
|
-
if (newPath ===
|
|
352
|
-
|
|
110
|
+
if (newPath === getCurrentPath()) {
|
|
111
|
+
const savedY = getSavedLocationScroll(newPath + window.location.hash);
|
|
112
|
+
if (typeof savedY === 'number') {
|
|
113
|
+
window.scrollTo({ top: savedY });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Same page, different hash: fall back to the fragment target
|
|
353
118
|
const hash = window.location.hash.slice(1);
|
|
354
119
|
if (hash) {
|
|
355
120
|
const el = window.document.getElementById(hash);
|
|
@@ -361,19 +126,18 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
361
126
|
}
|
|
362
127
|
return;
|
|
363
128
|
}
|
|
364
|
-
currentPath = newPath;
|
|
365
129
|
|
|
130
|
+
setCurrentPath(newPath);
|
|
366
131
|
const targetIndex = event.state?.navIndex ?? 0;
|
|
367
|
-
const direction:
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
typeof savedY === 'number' ? savedY : null,
|
|
132
|
+
const direction = targetIndex >= getHistoryIndex() ? 'forward' : 'back';
|
|
133
|
+
setHistoryIndex(targetIndex);
|
|
134
|
+
const savedY = getSavedScroll(targetIndex);
|
|
135
|
+
navigateToUrl(window, {
|
|
136
|
+
url: window.location.href,
|
|
137
|
+
scrollTarget: typeof savedY === 'number' ? savedY : null,
|
|
374
138
|
direction,
|
|
375
|
-
false,
|
|
376
|
-
);
|
|
139
|
+
pushHistory: false,
|
|
140
|
+
});
|
|
377
141
|
}
|
|
378
142
|
|
|
379
143
|
// Use capture phase so the handler fires before stopPropagation
|
|
@@ -9,7 +9,7 @@ import { scheduleTask } from '../util';
|
|
|
9
9
|
|
|
10
10
|
const PER_PAGE_COMPONENTS: Record<
|
|
11
11
|
string,
|
|
12
|
-
(w: Window) => void | (() => void) | Promise<void>
|
|
12
|
+
(w: Window) => void | (() => void) | Promise<void | (() => void)>
|
|
13
13
|
> = {
|
|
14
14
|
toc: mountTableOfContents,
|
|
15
15
|
anchor: mountAnchor,
|