@abreen/tada 1.19.4 → 1.19.5

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.
@@ -8,6 +8,8 @@ const CARET = 0x5e;
8
8
  const OPEN_PAREN = 0x28;
9
9
  const CLOSE_PAREN = 0x29;
10
10
  const NEWLINE = 0x0a;
11
+ const LESS_THAN = 0x3c;
12
+ const RAW_ANCHOR_OPEN = /^<a[\s>]/i;
11
13
 
12
14
  // Renders `[Title][Description](/destination.html)` as a two-line superlink.
13
15
  // It must run before the built-in `link` rule, which would otherwise
@@ -27,6 +29,46 @@ export default function superlinkPlugin(
27
29
  return pos;
28
30
  }
29
31
 
32
+ // Whether a `<…>` construct (an autolink or inline HTML) renders an anchor.
33
+ // It is parsed alone with a throwaway `env`, and nothing in it can reach the
34
+ // superlink rule again.
35
+ function isAnchorMarkup(state: StateInline, markup: string): boolean {
36
+ const tokens: StateInline['tokens'] = [];
37
+ state.md.inline.parse(markup, state.md, {}, tokens);
38
+ return tokens.some(
39
+ token =>
40
+ token.type === 'link_open' ||
41
+ (token.type === 'html_inline' && RAW_ANCHOR_OPEN.test(token.content)),
42
+ );
43
+ }
44
+
45
+ // `parseLinkLabel` rejects bracketed links but not autolinks (`<https://…>`)
46
+ // or raw `<a>` tags. An anchor inside the superlink anchor is invalid HTML:
47
+ // the parser closes the outer one early, stranding the description.
48
+ //
49
+ // This walks the label the way `parseLinkLabel` does, with the silent
50
+ // `skipToken`, so the state's position cache keeps the work linear and rules
51
+ // that register things in `env`, such as inline footnotes, do not run.
52
+ function labelHasAnchor(
53
+ state: StateInline,
54
+ start: number,
55
+ end: number,
56
+ ): boolean {
57
+ const oldPos = state.pos;
58
+ let found = false;
59
+ state.pos = start;
60
+ while (state.pos < end && !found) {
61
+ const pos = state.pos;
62
+ state.md.inline.skipToken(state);
63
+ found =
64
+ state.src.charCodeAt(pos) === LESS_THAN &&
65
+ state.pos - pos > 1 &&
66
+ isAnchorMarkup(state, state.src.slice(pos, state.pos));
67
+ }
68
+ state.pos = oldPos;
69
+ return found;
70
+ }
71
+
30
72
  function pushPart(
31
73
  state: StateInline,
32
74
  name: 'title' | 'description',
@@ -115,6 +157,14 @@ export default function superlinkPlugin(
115
157
  return false;
116
158
  }
117
159
 
160
+ // Last, so labels are only scanned when everything else already matches
161
+ if (
162
+ labelHasAnchor(state, titleStart, titleEnd) ||
163
+ labelHasAnchor(state, descriptionStart, descriptionEnd)
164
+ ) {
165
+ return false;
166
+ }
167
+
118
168
  if (!silent) {
119
169
  const external = isExternalHref(href, siteVariables);
120
170
  const open = state.push('superlink_open', 'a', 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.19.4",
3
+ "version": "1.19.5",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -68,7 +68,7 @@
68
68
  "fontkit": "^2.0.4",
69
69
  "front-matter": "^4.0.2",
70
70
  "java-parser": "^3.0.1",
71
- "katex": "^0.16.45",
71
+ "katex": "^0.18.2",
72
72
  "lodash": "^4.18.1",
73
73
  "markdown-it": "^14.1.1",
74
74
  "markdown-it-anchor": "^9.2.0",
@@ -82,6 +82,9 @@
82
82
  "sharp": "^0.35.3",
83
83
  "shiki": "^4.0.2"
84
84
  },
85
+ "overrides": {
86
+ "katex": "^0.18.2"
87
+ },
85
88
  "lint-staged": {
86
89
  "*.{ts,js}": "prettier --write",
87
90
  "src/**/*.scss": [
@@ -11,25 +11,14 @@ import { swapHeaderTitle } from '../header';
11
11
  export const NAVIGATION_EVENT = 'tada:navigation';
12
12
 
13
13
  const LOADING_CURSOR_DELAY = 400;
14
- const HOLD_SCROLL_FRAMES = 6;
15
- const HOLD_SCROLL_RELEASE_EVENTS = [
16
- 'wheel',
17
- 'touchstart',
18
- 'keydown',
19
- 'pointerdown',
20
- ] as const;
21
14
 
22
15
  let currentAbortController: AbortController | null = null;
23
16
  let historyIndex = 0;
24
17
  let currentPath = '';
25
18
  let applyingFragment = false;
26
- let fragmentTargetRequest = 0;
27
19
 
28
20
  const scrollByIndex = new Map<number, number>();
29
21
  const scrollByLocation = new Map<string, number>();
30
- // Stylesheets the navigator added that have not loaded or failed yet. Those in
31
- // the initial document have settled before the deferred client script runs.
32
- const pendingStylesheets = new Map<Element, Promise<void>>();
33
22
 
34
23
  type Direction = 'forward' | 'back';
35
24
 
@@ -78,19 +67,7 @@ function updateHead(document: Document, newDoc: Document): void {
78
67
 
79
68
  for (const link of newDoc.querySelectorAll('link[rel="stylesheet"]')) {
80
69
  if (!existingHrefs.has(link.getAttribute('href'))) {
81
- const clone = link.cloneNode(true) as Element;
82
- pendingStylesheets.set(
83
- clone,
84
- new Promise(resolve => {
85
- const settle = () => {
86
- pendingStylesheets.delete(clone);
87
- resolve();
88
- };
89
- clone.addEventListener('load', settle, { once: true });
90
- clone.addEventListener('error', settle, { once: true });
91
- }),
92
- );
93
- document.head.appendChild(clone);
70
+ document.head.appendChild(link.cloneNode(true));
94
71
  }
95
72
  }
96
73
  }
@@ -266,53 +243,16 @@ export function isApplyingFragment(): boolean {
266
243
  // content loses it in every browser. A fragment navigation sets it in every
267
244
  // browser, but it can reset the entry's history state and scroll position, so
268
245
  // restore both.
269
- //
270
- // WebKit defers fragment scrolling until pending stylesheets load, which would
271
- // later move the page away from the restored scroll position. Wait for them
272
- // first, and skip the update if another request or navigation replaced it.
273
- //
274
- // That deferred scroll can also set :target before the wait ends, which would
275
- // leave nothing to replace. WebKit then scrolls to the target again on the next
276
- // frame, undoing a scroll back to the saved position. A fragment navigation of
277
- // our own does not have that problem, so after waiting, repeat it even when
278
- // :target matches, restoring the saved position the caller passed in. When the
279
- // last stylesheet finishes loading just after the content swap, WebKit can
280
- // still scroll to the target in the next rendering update, after our restore,
281
- // so hold that position for a few frames.
282
- export function applyFragmentTarget(
283
- window: Window,
284
- restoredTop?: number,
285
- ): void {
286
- const request = ++fragmentTargetRequest;
287
- const { href } = window.location;
288
- let waited = false;
289
- const apply = (): void => {
290
- if (request !== fragmentTargetRequest || window.location.href !== href) {
291
- return;
292
- }
293
- const pending = Array.from(pendingStylesheets.values());
294
- if (pending.length > 0) {
295
- waited = true;
296
- void Promise.all(pending).then(apply);
297
- return;
298
- }
299
- replaceFragmentTarget(window, waited ? restoredTop : undefined);
300
- };
301
- apply();
302
- }
303
-
304
- function replaceFragmentTarget(window: Window, forcedTop?: number): void {
246
+ export function applyFragmentTarget(window: Window): void {
305
247
  const { document, history, location } = window;
306
248
  if (
307
- forcedTop === undefined &&
308
249
  document.querySelector(':target') === getHashTarget(document, location.hash)
309
250
  ) {
310
251
  return;
311
252
  }
312
253
  const { href } = location;
313
254
  const state = history.state;
314
- const { scrollX } = window;
315
- const scrollY = forcedTop ?? window.scrollY;
255
+ const { scrollX, scrollY } = window;
316
256
  applyingFragment = true;
317
257
  try {
318
258
  globals.replaceLocation(
@@ -324,41 +264,6 @@ function replaceFragmentTarget(window: Window, forcedTop?: number): void {
324
264
  }
325
265
  history.replaceState(state, '', href);
326
266
  window.scrollTo({ left: scrollX, top: scrollY });
327
- if (forcedTop !== undefined) {
328
- holdScroll(window, scrollX, scrollY);
329
- }
330
- }
331
-
332
- // Scroll back to the given position on each of the next few frames if the
333
- // browser moved the page, until the visitor scrolls or the URL changes
334
- function holdScroll(window: Window, left: number, top: number): void {
335
- const { href } = window.location;
336
- let held = true;
337
- const release = (): void => {
338
- held = false;
339
- for (const type of HOLD_SCROLL_RELEASE_EVENTS) {
340
- window.removeEventListener(type, release);
341
- }
342
- };
343
- for (const type of HOLD_SCROLL_RELEASE_EVENTS) {
344
- window.addEventListener(type, release, { passive: true });
345
- }
346
- let frames = 0;
347
- const tick = (): void => {
348
- if (!held || window.location.href !== href) {
349
- release();
350
- return;
351
- }
352
- if (Math.abs(window.scrollY - top) > 1) {
353
- window.scrollTo({ left, top });
354
- }
355
- if (++frames < HOLD_SCROLL_FRAMES) {
356
- window.requestAnimationFrame(tick);
357
- } else {
358
- release();
359
- }
360
- };
361
- window.requestAnimationFrame(tick);
362
267
  }
363
268
 
364
269
  export async function navigateToUrl(
@@ -472,12 +377,10 @@ export async function navigateToUrl(
472
377
  teardownPerPageComponents();
473
378
  swapContent(document, newDoc);
474
379
  if (!pushHistory) {
475
- // Restore the entry's :target before updateHead adds stylesheets, so it
476
- // applies right away unless an earlier stylesheet is still loading
477
- applyFragmentTarget(
478
- window,
479
- typeof scrollTarget === 'number' ? scrollTarget : undefined,
480
- );
380
+ // Restore the entry's :target before updateHead adds stylesheets. WebKit
381
+ // defers fragment scrolling until they load, which would move the page
382
+ // away from the entry's restored scroll position.
383
+ applyFragmentTarget(window);
481
384
  }
482
385
  updateHead(document, newDoc);
483
386
  mountAppearancePickerForPage(window);