@abreen/tada 1.15.8 → 1.15.10
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/build/bundle.ts +55 -26
- package/build/colors.ts +2 -2
- package/build/config-files.ts +11 -21
- package/build/config-loader.ts +1 -1
- package/build/external-links-plugin.ts +1 -0
- package/build/generate-favicon.ts +0 -2
- package/build/generate-web-app-manifest.ts +0 -2
- package/build/pdf-text.ts +2 -2
- package/build/reachability.ts +1 -1
- package/build/site-variables.ts +1 -3
- package/build/template-globals.ts +7 -0
- package/build/templates.ts +1 -1
- package/build/util.ts +1 -22
- package/build/utils/literate-java.ts +1 -1
- package/build/utils/markdown-partials.ts +1 -1
- package/build/utils/shiki-highlighter.ts +1 -4
- package/build/utils/trace-layout.ts +2 -2
- package/build/validate-config-links.ts +51 -1
- package/build/watch/config-paths.ts +1 -1
- package/build/watch/planner.ts +1 -1
- package/build/watch/snapshot.ts +4 -16
- package/package.json +5 -5
- package/schema/authors.schema.json +1 -1
- package/src/navigate/eligible.ts +8 -1
- package/src/navigate/runtime.ts +57 -5
- package/src/page-update/index.ts +6 -2
- package/src/search/index.ts +105 -71
- package/src/search/pdf-utils.ts +1 -1
- package/src/slides/index.ts +1 -1
- package/src/timezone/index.ts +8 -44
- package/src/timezone/time-format.ts +43 -0
- package/src/toc/index.ts +7 -79
- package/src/toc/model.ts +78 -0
- package/src/top/index.ts +3 -2
- package/templates/_author.html +2 -2
- package/templates/_footer.html +1 -1
- package/templates/_heading.html +1 -1
- package/templates/_nav.html +2 -2
package/src/navigate/runtime.ts
CHANGED
|
@@ -130,6 +130,14 @@ function getLocationKey(window: Window): string {
|
|
|
130
130
|
);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
function isAbortError(err: unknown): boolean {
|
|
134
|
+
return err instanceof Error && err.name === 'AbortError';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isActiveNavigation(controller: AbortController): boolean {
|
|
138
|
+
return currentAbortController === controller && !controller.signal.aborted;
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
export function initNavigation(window: Window): void {
|
|
134
142
|
currentAbortController = null;
|
|
135
143
|
historyIndex = 0;
|
|
@@ -191,6 +199,8 @@ export async function navigateToUrl(
|
|
|
191
199
|
|
|
192
200
|
const controller = new AbortController();
|
|
193
201
|
currentAbortController = controller;
|
|
202
|
+
document.documentElement.classList.remove('nav-forward', 'nav-back');
|
|
203
|
+
cleanupViewTransitionNames(document);
|
|
194
204
|
|
|
195
205
|
const header = document.querySelector('header');
|
|
196
206
|
header?.classList.add('loading');
|
|
@@ -199,21 +209,46 @@ export async function navigateToUrl(
|
|
|
199
209
|
try {
|
|
200
210
|
response = await globals.fetch(url, { signal: controller.signal });
|
|
201
211
|
} catch (err: unknown) {
|
|
212
|
+
if (!isActiveNavigation(controller)) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
202
215
|
header?.classList.remove('loading');
|
|
203
|
-
if (err
|
|
216
|
+
if (isAbortError(err)) {
|
|
204
217
|
return;
|
|
205
218
|
}
|
|
206
219
|
globals.setLocationHref(window, url);
|
|
207
220
|
return;
|
|
208
221
|
}
|
|
209
222
|
|
|
223
|
+
if (!isActiveNavigation(controller)) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
210
227
|
if (!response.ok) {
|
|
211
228
|
header?.classList.remove('loading');
|
|
212
229
|
globals.setLocationHref(window, url);
|
|
213
230
|
return;
|
|
214
231
|
}
|
|
215
232
|
|
|
216
|
-
|
|
233
|
+
let html: string;
|
|
234
|
+
try {
|
|
235
|
+
html = await response.text();
|
|
236
|
+
} catch (err: unknown) {
|
|
237
|
+
if (!isActiveNavigation(controller)) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
header?.classList.remove('loading');
|
|
241
|
+
if (isAbortError(err)) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
globals.setLocationHref(window, url);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!isActiveNavigation(controller)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
217
252
|
header?.classList.remove('loading');
|
|
218
253
|
|
|
219
254
|
const DOMParserCtor = (window as unknown as { DOMParser: typeof DOMParser })
|
|
@@ -233,7 +268,11 @@ export async function navigateToUrl(
|
|
|
233
268
|
|
|
234
269
|
const parsed = new URL(url);
|
|
235
270
|
|
|
236
|
-
const doSwap = () => {
|
|
271
|
+
const doSwap = (): boolean => {
|
|
272
|
+
if (!isActiveNavigation(controller)) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
237
276
|
teardownPerPageComponents();
|
|
238
277
|
swapContent(document, newDoc);
|
|
239
278
|
updateHead(document, newDoc);
|
|
@@ -257,6 +296,7 @@ export async function navigateToUrl(
|
|
|
257
296
|
|
|
258
297
|
scrollByIndex.set(historyIndex, window.scrollY);
|
|
259
298
|
scrollByLocation.set(getLocationKey(window), window.scrollY);
|
|
299
|
+
return true;
|
|
260
300
|
};
|
|
261
301
|
|
|
262
302
|
const transitionDoc = document as Document & {
|
|
@@ -295,7 +335,9 @@ export async function navigateToUrl(
|
|
|
295
335
|
);
|
|
296
336
|
|
|
297
337
|
const transition = transitionDoc.startViewTransition(() => {
|
|
298
|
-
doSwap()
|
|
338
|
+
if (!doSwap()) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
299
341
|
|
|
300
342
|
const newTitleEl = document.querySelector('.title-and-info');
|
|
301
343
|
const newHeaderHeight =
|
|
@@ -323,13 +365,23 @@ export async function navigateToUrl(
|
|
|
323
365
|
});
|
|
324
366
|
|
|
325
367
|
await transition.finished;
|
|
368
|
+
if (!isActiveNavigation(controller)) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
326
372
|
document.documentElement.classList.remove('nav-forward', 'nav-back');
|
|
327
373
|
cleanupViewTransitionNames(document);
|
|
328
374
|
} else {
|
|
329
|
-
doSwap()
|
|
375
|
+
if (!doSwap()) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
330
378
|
}
|
|
331
379
|
|
|
332
380
|
await mountPerPageComponents(window);
|
|
381
|
+
if (!isActiveNavigation(controller)) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
333
385
|
currentAbortController = null;
|
|
334
386
|
dispatchNavigationEvent(window);
|
|
335
387
|
}
|
package/src/page-update/index.ts
CHANGED
|
@@ -10,8 +10,9 @@ import { NAVIGATION_EVENT, refreshCurrentPage } from '../navigate/runtime';
|
|
|
10
10
|
import { globals } from '../globals';
|
|
11
11
|
|
|
12
12
|
const POLL_INTERVAL_MS = 600_000;
|
|
13
|
+
export const PAGE_UPDATE_REFRESH_EVENT = 'tada:page-update-refresh';
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
interface PageUpdateOptions {
|
|
15
16
|
pollIntervalMs?: number;
|
|
16
17
|
refreshPage?: (window: Window) => Promise<void>;
|
|
17
18
|
}
|
|
@@ -247,7 +248,10 @@ export function mountPageUpdate(
|
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
function handleReloadClick() {
|
|
250
|
-
void refreshPage(window)
|
|
251
|
+
void refreshPage(window).then(() => {
|
|
252
|
+
const EventCtor = (window as unknown as { Event: typeof Event }).Event;
|
|
253
|
+
window.dispatchEvent(new EventCtor(PAGE_UPDATE_REFRESH_EVENT));
|
|
254
|
+
});
|
|
251
255
|
}
|
|
252
256
|
|
|
253
257
|
reloadButton.addEventListener('click', handleReloadClick);
|
package/src/search/index.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { getElement, applyBasePath } from '../util';
|
|
2
2
|
import { getPdfPageNumber, groupPdfResults } from './pdf-utils';
|
|
3
3
|
import type { Result } from './pdf-utils';
|
|
4
|
-
import {
|
|
5
|
-
getResponseValidators,
|
|
6
|
-
hasResponseValidatorsChanged,
|
|
7
|
-
type ResponseValidators,
|
|
8
|
-
} from '../validators';
|
|
4
|
+
import { PAGE_UPDATE_REFRESH_EVENT } from '../page-update';
|
|
9
5
|
import { globals } from '../globals';
|
|
10
6
|
|
|
11
|
-
const MAX_RESULTS = 24;
|
|
12
|
-
|
|
13
7
|
interface PagefindSubResult {
|
|
14
8
|
title?: string;
|
|
15
9
|
url: string;
|
|
@@ -34,17 +28,14 @@ interface PagefindSearchResult {
|
|
|
34
28
|
}
|
|
35
29
|
|
|
36
30
|
interface Pagefind {
|
|
31
|
+
destroy(): Promise<void>;
|
|
37
32
|
init(): Promise<void>;
|
|
33
|
+
options(options: { basePath: string }): Promise<void>;
|
|
38
34
|
search(query: string): Promise<{ results: PagefindSearchResult[] }>;
|
|
39
35
|
}
|
|
40
36
|
|
|
41
37
|
let pagefind: Pagefind | null = null;
|
|
42
38
|
|
|
43
|
-
// Unit tests need to clear the module-scoped Pagefind instance between cases.
|
|
44
|
-
export function resetPagefindForTest() {
|
|
45
|
-
pagefind = null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
39
|
type SubResult = { title: string; url: string; excerpt: string };
|
|
49
40
|
|
|
50
41
|
type State = {
|
|
@@ -66,8 +57,7 @@ async function doSearch(query: string, window: Window): Promise<SearchState> {
|
|
|
66
57
|
}
|
|
67
58
|
|
|
68
59
|
const search = await pagefind.search(query);
|
|
69
|
-
const
|
|
70
|
-
const data = await Promise.all(slice.map(r => r.data()));
|
|
60
|
+
const data = await Promise.all(search.results.map(r => r.data()));
|
|
71
61
|
|
|
72
62
|
const titlePostfix = __SITE_TITLE_POSTFIX__;
|
|
73
63
|
const results: Result[] = data.map((d: PagefindResult) => {
|
|
@@ -144,9 +134,9 @@ function render(
|
|
|
144
134
|
ol.tabIndex = -1;
|
|
145
135
|
ol.setAttribute('aria-label', 'Search results');
|
|
146
136
|
|
|
147
|
-
const totalVisible =
|
|
137
|
+
const totalVisible = state.results.length;
|
|
148
138
|
|
|
149
|
-
state.results.
|
|
139
|
+
state.results.forEach((result, i) => {
|
|
150
140
|
const a = doc.createElement('a');
|
|
151
141
|
a.id = `result-${i}`;
|
|
152
142
|
a.className = 'result';
|
|
@@ -180,7 +170,7 @@ function render(
|
|
|
180
170
|
li.setAttribute('aria-labelledby', `title-${i}`);
|
|
181
171
|
li.appendChild(a);
|
|
182
172
|
|
|
183
|
-
const subsToShow = result.subResults
|
|
173
|
+
const subsToShow = result.subResults;
|
|
184
174
|
if (subsToShow.length > 0) {
|
|
185
175
|
const subList = doc.createElement('ul');
|
|
186
176
|
subList.className = 'sub-results';
|
|
@@ -255,10 +245,8 @@ function render(
|
|
|
255
245
|
countSpan.textContent = 'No results';
|
|
256
246
|
} else if (n === 1) {
|
|
257
247
|
countSpan.textContent = 'One result';
|
|
258
|
-
} else if (n <= MAX_RESULTS) {
|
|
259
|
-
countSpan.textContent = `${n} results`;
|
|
260
248
|
} else {
|
|
261
|
-
countSpan.textContent =
|
|
249
|
+
countSpan.textContent = `${n} results`;
|
|
262
250
|
}
|
|
263
251
|
}
|
|
264
252
|
|
|
@@ -291,64 +279,56 @@ export default (window: Window) => {
|
|
|
291
279
|
// Unhide (hidden via inline style in template to prevent FOUC)
|
|
292
280
|
resultsDiv.style.display = '';
|
|
293
281
|
|
|
294
|
-
let
|
|
295
|
-
let
|
|
296
|
-
let indexCheckInFlight = false;
|
|
297
|
-
|
|
298
|
-
async function loadPagefind() {
|
|
299
|
-
if (pagefind) {
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
pagefind = (await globals.importModule(
|
|
304
|
-
applyBasePath('/pagefind/pagefind.js'),
|
|
305
|
-
)) as Pagefind;
|
|
306
|
-
|
|
307
|
-
await pagefind.init();
|
|
308
|
-
|
|
309
|
-
const res = await globals.fetch(
|
|
310
|
-
applyBasePath('/pagefind/pagefind-entry.json'),
|
|
311
|
-
{ cache: 'no-cache' },
|
|
312
|
-
);
|
|
282
|
+
let pagefindLoadPromise: Promise<boolean> | null = null;
|
|
283
|
+
let pagefindLoadGeneration = 0;
|
|
313
284
|
|
|
314
|
-
|
|
315
|
-
|
|
285
|
+
function pagefindModulePath(generation: number) {
|
|
286
|
+
const path = applyBasePath('/pagefind/pagefind.js');
|
|
287
|
+
if (generation === 0) {
|
|
288
|
+
return path;
|
|
316
289
|
}
|
|
290
|
+
return `${path}?tada-pagefind-refresh=${generation}`;
|
|
317
291
|
}
|
|
318
292
|
|
|
319
|
-
async function
|
|
320
|
-
if (
|
|
321
|
-
return;
|
|
293
|
+
async function loadPagefind(): Promise<boolean> {
|
|
294
|
+
if (pagefind) {
|
|
295
|
+
return true;
|
|
322
296
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (
|
|
331
|
-
return;
|
|
297
|
+
|
|
298
|
+
const generation = pagefindLoadGeneration;
|
|
299
|
+
pagefindLoadPromise ??= (async () => {
|
|
300
|
+
const loadedPagefind = (await globals.importModule(
|
|
301
|
+
pagefindModulePath(generation),
|
|
302
|
+
)) as Pagefind;
|
|
303
|
+
|
|
304
|
+
if (generation !== pagefindLoadGeneration) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
if (generation > 0) {
|
|
308
|
+
await loadedPagefind.options({ basePath: applyBasePath('/pagefind/') });
|
|
309
|
+
}
|
|
310
|
+
if (generation !== pagefindLoadGeneration) {
|
|
311
|
+
return false;
|
|
332
312
|
}
|
|
333
|
-
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
await loadPagefind();
|
|
338
|
-
const updateId = ++latestUpdateId;
|
|
339
|
-
await update(updateId);
|
|
313
|
+
await loadedPagefind.init();
|
|
314
|
+
if (generation !== pagefindLoadGeneration) {
|
|
315
|
+
await loadedPagefind.destroy();
|
|
316
|
+
return false;
|
|
340
317
|
}
|
|
341
|
-
|
|
342
|
-
|
|
318
|
+
pagefind = loadedPagefind;
|
|
319
|
+
return true;
|
|
320
|
+
})();
|
|
321
|
+
|
|
322
|
+
const promise = pagefindLoadPromise;
|
|
323
|
+
try {
|
|
324
|
+
return await promise;
|
|
343
325
|
} finally {
|
|
344
|
-
|
|
326
|
+
if (pagefindLoadPromise === promise) {
|
|
327
|
+
pagefindLoadPromise = null;
|
|
328
|
+
}
|
|
345
329
|
}
|
|
346
330
|
}
|
|
347
331
|
|
|
348
|
-
loadPagefind().catch(err => {
|
|
349
|
-
console.log(`failed to load Pagefind: ${err}`);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
332
|
const state: State = {
|
|
353
333
|
value: '',
|
|
354
334
|
showResults: false,
|
|
@@ -367,10 +347,11 @@ export default (window: Window) => {
|
|
|
367
347
|
if (updateId !== latestUpdateId) {
|
|
368
348
|
return;
|
|
369
349
|
}
|
|
370
|
-
if (nextState) {
|
|
371
|
-
|
|
372
|
-
state.totalResults = nextState.totalResults;
|
|
350
|
+
if (!nextState) {
|
|
351
|
+
return;
|
|
373
352
|
}
|
|
353
|
+
state.results = nextState.results;
|
|
354
|
+
state.totalResults = nextState.totalResults;
|
|
374
355
|
if (!state.showResults) {
|
|
375
356
|
return;
|
|
376
357
|
}
|
|
@@ -386,6 +367,55 @@ export default (window: Window) => {
|
|
|
386
367
|
latestUpdateId += 1;
|
|
387
368
|
}
|
|
388
369
|
|
|
370
|
+
function loadPagefindAndUpdate() {
|
|
371
|
+
loadPagefind()
|
|
372
|
+
.then(loadedCurrentPagefind => {
|
|
373
|
+
if (loadedCurrentPagefind && state.showResults) {
|
|
374
|
+
queueUpdate();
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
.catch(err => {
|
|
378
|
+
console.log(`failed to load Pagefind: ${err}`);
|
|
379
|
+
if (state.showResults) {
|
|
380
|
+
render(input!, resultsContainer, state, false);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function invalidatePagefind() {
|
|
386
|
+
pagefindLoadGeneration += 1;
|
|
387
|
+
pagefind = null;
|
|
388
|
+
pagefindLoadPromise = null;
|
|
389
|
+
invalidateUpdates();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function reloadPagefindAfterPageUpdateRefresh() {
|
|
393
|
+
const stalePagefind = pagefind;
|
|
394
|
+
invalidatePagefind();
|
|
395
|
+
if (state.showResults) {
|
|
396
|
+
render(input!, resultsContainer, state, true);
|
|
397
|
+
}
|
|
398
|
+
if (stalePagefind) {
|
|
399
|
+
try {
|
|
400
|
+
await stalePagefind.destroy();
|
|
401
|
+
} catch (err) {
|
|
402
|
+
console.log(`failed to destroy Pagefind: ${err}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
loadPagefindAndUpdate();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function handlePageUpdateRefresh() {
|
|
409
|
+
reloadPagefindAfterPageUpdateRefresh().catch(err => {
|
|
410
|
+
console.log(`failed to reload Pagefind: ${err}`);
|
|
411
|
+
if (state.showResults) {
|
|
412
|
+
render(input!, resultsContainer, state, false);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
loadPagefindAndUpdate();
|
|
418
|
+
|
|
389
419
|
function hide() {
|
|
390
420
|
if (!state.showResults) {
|
|
391
421
|
return;
|
|
@@ -422,7 +452,6 @@ export default (window: Window) => {
|
|
|
422
452
|
if (previous && !resultsContainer.contains(previous)) {
|
|
423
453
|
previousFocus = previous;
|
|
424
454
|
}
|
|
425
|
-
checkForIndexUpdate().catch(() => {});
|
|
426
455
|
if (!state.showResults) {
|
|
427
456
|
state.showResults = true;
|
|
428
457
|
queueUpdate();
|
|
@@ -568,8 +597,13 @@ export default (window: Window) => {
|
|
|
568
597
|
window.addEventListener('pointerdown', handleWindowPointerDown);
|
|
569
598
|
window.addEventListener('pointermove', handleWindowPointerMove);
|
|
570
599
|
window.addEventListener('keydown', handleWindowKeyDown);
|
|
600
|
+
window.addEventListener(PAGE_UPDATE_REFRESH_EVENT, handlePageUpdateRefresh);
|
|
571
601
|
|
|
572
602
|
return () => {
|
|
603
|
+
window.removeEventListener(
|
|
604
|
+
PAGE_UPDATE_REFRESH_EVENT,
|
|
605
|
+
handlePageUpdateRefresh,
|
|
606
|
+
);
|
|
573
607
|
window.removeEventListener('keydown', handleWindowKeyDown);
|
|
574
608
|
window.removeEventListener('pointermove', handleWindowPointerMove);
|
|
575
609
|
window.removeEventListener('pointerdown', handleWindowPointerDown);
|
package/src/search/pdf-utils.ts
CHANGED
package/src/slides/index.ts
CHANGED
package/src/timezone/index.ts
CHANGED
|
@@ -1,50 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PERIOD_STYLE,
|
|
3
|
+
detectPeriodStyle,
|
|
4
|
+
normalizeHM,
|
|
5
|
+
to12Hour,
|
|
6
|
+
type PeriodStyle,
|
|
7
|
+
} from './time-format';
|
|
8
|
+
|
|
1
9
|
const STORAGE_KEY = 'timezoneSelection';
|
|
2
10
|
const TIMEZONE_SELECT_LABEL = 'Time zone';
|
|
3
11
|
|
|
4
|
-
type PeriodStyle = [am: string, pm: string];
|
|
5
|
-
|
|
6
|
-
const DEFAULT_PERIOD_STYLE: PeriodStyle = ['a.m.', 'p.m.'];
|
|
7
|
-
|
|
8
|
-
const PERIOD_PATTERNS: { pattern: RegExp; style: PeriodStyle }[] = [
|
|
9
|
-
{ pattern: /[AP]\.M\./, style: ['A.M.', 'P.M.'] },
|
|
10
|
-
{ pattern: /[AP]M/, style: ['AM', 'PM'] },
|
|
11
|
-
{ pattern: /[ap]\.m\./, style: ['a.m.', 'p.m.'] },
|
|
12
|
-
{ pattern: /[ap]m/, style: ['am', 'pm'] },
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
export function detectPeriodStyle(text: string): PeriodStyle | null {
|
|
16
|
-
for (const { pattern, style } of PERIOD_PATTERNS) {
|
|
17
|
-
if (pattern.test(text)) {
|
|
18
|
-
return style;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function pad(n: number | string) {
|
|
25
|
-
return String(n).padStart(2, '0');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function to12Hour(
|
|
29
|
-
h: number,
|
|
30
|
-
m: number,
|
|
31
|
-
style: PeriodStyle | null = DEFAULT_PERIOD_STYLE,
|
|
32
|
-
) {
|
|
33
|
-
const hour12 = ((h + 11) % 12) + 1;
|
|
34
|
-
if (!style) {
|
|
35
|
-
return `${hour12}:${pad(m)}`;
|
|
36
|
-
}
|
|
37
|
-
const period = h >= 12 ? style[1] : style[0];
|
|
38
|
-
return `${hour12}:${pad(m)} ${period}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function normalizeHM(totalMinutes: number) {
|
|
42
|
-
const mins = ((totalMinutes % 1440) + 1440) % 1440;
|
|
43
|
-
const h = Math.floor(mins / 60);
|
|
44
|
-
const m = mins % 60;
|
|
45
|
-
return [h, m] as const;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
12
|
function getDayShift(totalMinutes: number) {
|
|
49
13
|
// Negative partial days like -30 minutes should still count as the previous day.
|
|
50
14
|
return Math.floor(totalMinutes / 1440);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type PeriodStyle = [am: string, pm: string];
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_PERIOD_STYLE: PeriodStyle = ['a.m.', 'p.m.'];
|
|
4
|
+
|
|
5
|
+
const PERIOD_PATTERNS: { pattern: RegExp; style: PeriodStyle }[] = [
|
|
6
|
+
{ pattern: /[AP]\.M\./, style: ['A.M.', 'P.M.'] },
|
|
7
|
+
{ pattern: /[AP]M/, style: ['AM', 'PM'] },
|
|
8
|
+
{ pattern: /[ap]\.m\./, style: ['a.m.', 'p.m.'] },
|
|
9
|
+
{ pattern: /[ap]m/, style: ['am', 'pm'] },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export function detectPeriodStyle(text: string): PeriodStyle | null {
|
|
13
|
+
for (const { pattern, style } of PERIOD_PATTERNS) {
|
|
14
|
+
if (pattern.test(text)) {
|
|
15
|
+
return style;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pad(n: number | string) {
|
|
22
|
+
return String(n).padStart(2, '0');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function to12Hour(
|
|
26
|
+
h: number,
|
|
27
|
+
m: number,
|
|
28
|
+
style: PeriodStyle | null = DEFAULT_PERIOD_STYLE,
|
|
29
|
+
) {
|
|
30
|
+
const hour12 = ((h + 11) % 12) + 1;
|
|
31
|
+
if (!style) {
|
|
32
|
+
return `${hour12}:${pad(m)}`;
|
|
33
|
+
}
|
|
34
|
+
const period = h >= 12 ? style[1] : style[0];
|
|
35
|
+
return `${hour12}:${pad(m)} ${period}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeHM(totalMinutes: number) {
|
|
39
|
+
const mins = ((totalMinutes % 1440) + 1440) % 1440;
|
|
40
|
+
const h = Math.floor(mins / 60);
|
|
41
|
+
const m = mins % 60;
|
|
42
|
+
return [h, m] as const;
|
|
43
|
+
}
|
package/src/toc/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { debounce } from '../util';
|
|
2
|
+
import {
|
|
3
|
+
alertToTableItem,
|
|
4
|
+
getHighlightIndexes,
|
|
5
|
+
headingToTableItem,
|
|
6
|
+
switchCurrent,
|
|
7
|
+
type Dinkus,
|
|
8
|
+
} from './model';
|
|
2
9
|
|
|
3
10
|
const LATENCY_MS = 50;
|
|
4
11
|
|
|
5
|
-
export type HeadingLevel = '1' | '2' | '3' | '4' | '5' | '6';
|
|
6
|
-
export type AlertType = 'warning' | 'note';
|
|
7
|
-
|
|
8
|
-
export type Alert = { type: AlertType; title: string };
|
|
9
|
-
export type Heading = { level: HeadingLevel; innerHtml: string; id: string };
|
|
10
|
-
export type Dinkus = { type: 'dinkus' };
|
|
11
|
-
|
|
12
12
|
function getContainer(parent: HTMLElement): HTMLElement | null {
|
|
13
13
|
return parent.querySelector('nav.toc');
|
|
14
14
|
}
|
|
@@ -40,27 +40,6 @@ function scrollIfNeeded(element: HTMLElement, doc: Document) {
|
|
|
40
40
|
container.scrollTo({ top: desiredScrollTop });
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function getHighlightIndexes(items: (Heading | Alert | Dinkus)[]) {
|
|
44
|
-
const indexes: (number | null)[] = [];
|
|
45
|
-
let currentHeadingIndex: number | null = null;
|
|
46
|
-
let tocIndex = 0;
|
|
47
|
-
|
|
48
|
-
items.forEach(item => {
|
|
49
|
-
if (!('level' in item) && item.type === 'dinkus') {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if ('level' in item) {
|
|
54
|
-
currentHeadingIndex = tocIndex;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
indexes.push(currentHeadingIndex ?? tocIndex);
|
|
58
|
-
tocIndex++;
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
return indexes;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
43
|
function getHeadingsAndAlerts(
|
|
65
44
|
parent: HTMLElement,
|
|
66
45
|
): (HTMLHeadingElement | HTMLDivElement)[] {
|
|
@@ -97,57 +76,6 @@ function getViewportActivationPoint(win: Window, doc: Document) {
|
|
|
97
76
|
return headerOffset + (win.innerHeight - headerOffset) / 3;
|
|
98
77
|
}
|
|
99
78
|
|
|
100
|
-
export function headingToTableItem(el: HTMLHeadingElement): Heading {
|
|
101
|
-
const level = el.tagName[1] as HeadingLevel;
|
|
102
|
-
|
|
103
|
-
const subtitle = el.querySelector('.heading-subtitle');
|
|
104
|
-
const subtitleText = subtitle?.textContent || '';
|
|
105
|
-
let mainText = el.textContent || '';
|
|
106
|
-
|
|
107
|
-
if (mainText.length > 0 && subtitleText.length > 0) {
|
|
108
|
-
mainText = mainText.replace(subtitleText, '').trim();
|
|
109
|
-
return {
|
|
110
|
-
level,
|
|
111
|
-
id: el.id,
|
|
112
|
-
innerHtml: `${mainText}: <span class="heading-subtitle">${subtitleText}</span>`,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
return { level, id: el.id, innerHtml: el.innerHTML };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function alertToTableItem(el: HTMLElement): Alert | null {
|
|
119
|
-
const classes = el.className
|
|
120
|
-
.split(' ')
|
|
121
|
-
.map(cl => cl.trim())
|
|
122
|
-
.filter(cl => cl != 'alert');
|
|
123
|
-
|
|
124
|
-
const firstClass = classes[0];
|
|
125
|
-
if (firstClass === 'warning' || firstClass === 'note') {
|
|
126
|
-
let title = el.querySelector('.title')?.innerHTML;
|
|
127
|
-
if (!title) {
|
|
128
|
-
if (firstClass === 'warning') {
|
|
129
|
-
title = 'Warning';
|
|
130
|
-
} else {
|
|
131
|
-
title = 'Note';
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return { type: firstClass, title };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function switchCurrent(
|
|
142
|
-
oldCurrent: HTMLElement | null,
|
|
143
|
-
newCurrent: HTMLElement,
|
|
144
|
-
) {
|
|
145
|
-
if (oldCurrent) {
|
|
146
|
-
oldCurrent.classList.remove('current');
|
|
147
|
-
}
|
|
148
|
-
newCurrent.classList.add('current');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
79
|
export default (window: Window) => {
|
|
152
80
|
const toc = window.document.querySelector('nav.toc') as HTMLElement;
|
|
153
81
|
if (toc == null) {
|