@abreen/tada 1.9.0 → 1.10.2

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.
Files changed (75) hide show
  1. package/README.md +24 -34
  2. package/bin/tada.ts +1 -1
  3. package/build/apply-base-path-plugin.ts +79 -17
  4. package/build/bundle.ts +9 -1
  5. package/build/content-watch.ts +3 -3
  6. package/build/copy.ts +30 -0
  7. package/build/generate-content-assets.ts +45 -59
  8. package/build/globals.ts +1 -1
  9. package/build/pagefind.ts +0 -7
  10. package/build/serve.ts +4 -0
  11. package/build/types.d.ts +19 -1
  12. package/build/util.ts +3 -0
  13. package/build/utils/content-files.ts +98 -0
  14. package/build/utils/include.ts +4 -0
  15. package/build/utils/python-runner/trace_runner.py +272 -0
  16. package/build/utils/render.ts +34 -8
  17. package/build/utils/trace-core.ts +203 -0
  18. package/build/utils/trace-java.ts +132 -0
  19. package/build/utils/trace-layout.ts +31 -3
  20. package/build/utils/trace-python.ts +26 -0
  21. package/build/utils/trace.ts +174 -294
  22. package/build/validate-internal-links-plugin.ts +16 -4
  23. package/build/watch/assets.ts +111 -0
  24. package/build/watch/build-full.ts +124 -0
  25. package/build/watch/build-incremental.ts +139 -0
  26. package/build/watch/compiler-types.d.ts +19 -0
  27. package/build/watch/compiler.ts +112 -0
  28. package/build/watch/index.ts +22 -0
  29. package/build/watch/mutations.ts +75 -0
  30. package/build/watch/planner.ts +230 -0
  31. package/build/watch/records.ts +190 -0
  32. package/build/watch/runtime.ts +143 -0
  33. package/build/watch/snapshot.ts +602 -0
  34. package/build/watch/validation.ts +36 -0
  35. package/build/watch-reload-client.ts +10 -5
  36. package/init/content/labs/01/index.md +6 -0
  37. package/init/content/labs/01/trace_demo.py +16 -0
  38. package/package.json +47 -24
  39. package/python/command.ts +95 -0
  40. package/python/module.ts +18 -0
  41. package/src/_alerts.scss +11 -8
  42. package/src/_base.scss +25 -29
  43. package/src/_layout.scss +17 -14
  44. package/src/_mixins.scss +10 -9
  45. package/src/anchor/style.scss +4 -2
  46. package/src/code/index.ts +9 -2
  47. package/src/code/style.scss +14 -13
  48. package/src/code.scss +14 -13
  49. package/src/header/_base.scss +21 -24
  50. package/src/header/style.scss +16 -12
  51. package/src/index.ts +3 -0
  52. package/src/literate/style.scss +4 -4
  53. package/src/navigate/index.ts +43 -279
  54. package/src/navigate/lifecycle.ts +1 -1
  55. package/src/navigate/runtime.ts +341 -0
  56. package/src/page-update/README.md +4 -0
  57. package/src/page-update/index.ts +280 -0
  58. package/src/page-update/style.scss +53 -0
  59. package/src/print/style.scss +4 -4
  60. package/src/question/style.scss +10 -6
  61. package/src/search/index.ts +9 -12
  62. package/src/search/style.scss +44 -42
  63. package/src/style.scss +63 -58
  64. package/src/timezone/index.ts +13 -3
  65. package/src/timezone/style.scss +11 -5
  66. package/src/toc/style.scss +17 -16
  67. package/src/trace/style.scss +27 -27
  68. package/src/validators.ts +45 -0
  69. package/templates/_footer.html +1 -1
  70. package/templates/_theme.scss +8 -19
  71. package/watch/engine.ts +296 -0
  72. package/watch/events.ts +10 -0
  73. package/watch/fs-commit.ts +102 -0
  74. package/watch/types.d.ts +103 -0
  75. package/build/watch.ts +0 -493
@@ -1,16 +1,17 @@
1
1
  import { isEligibleLink } from './eligible';
2
- import { mountPerPageComponents, teardownPerPageComponents } from './lifecycle';
3
-
4
- let currentAbortController: AbortController | null = null;
5
- let historyIndex = 0;
6
- let currentPath = '';
7
-
8
- // In-memory scroll positions keyed by navIndex. Populated on scroll and
9
- // consulted by handlePopState so returning to a previous entry via
10
- // back/forward restores the user's scroll, not just scroll 0.
11
- const scrollByIndex = new Map<number, number>();
12
-
13
- type Direction = 'forward' | 'back';
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.history.scrollRestoration = 'manual';
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
- scrollByIndex.set(historyIndex, window.scrollY);
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
- const details = window.document.querySelector(
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
- scrollByIndex.set(historyIndex, window.scrollY);
96
+ saveScrollPosition(window);
343
97
 
344
98
  const hash = url.hash ? url.hash.slice(1) : null;
345
99
 
346
- performNavigation(anchor.href, hash, 'forward', true);
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 === currentPath) {
352
- // Same page, different hash: scroll to the target
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: Direction =
368
- targetIndex >= historyIndex ? 'forward' : 'back';
369
- historyIndex = targetIndex;
370
- const savedY = scrollByIndex.get(targetIndex);
371
- performNavigation(
372
- window.location.href,
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,