@abreen/tada 1.19.3 → 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.
- package/README.md +1 -0
- package/build/external-links-plugin.ts +12 -5
- package/build/superlink-plugin.ts +196 -0
- package/build/utils/markdown.ts +2 -0
- package/init/content/markdown.md +21 -0
- package/package.json +5 -2
- package/src/_alerts.scss +1 -1
- package/src/_content.scss +54 -0
- package/src/navigate/runtime.ts +7 -104
package/README.md
CHANGED
|
@@ -68,6 +68,7 @@ A static site generator. The successor to Presto.
|
|
|
68
68
|
* `??? question ... ???` renders a Q&A section; answer is hidden until click
|
|
69
69
|
- To display clickable multiple choice options, use a [task list][task-list]
|
|
70
70
|
* `+++ ... +++ ... +++` renders a two-column layout
|
|
71
|
+
* `[Title][Description](url)` renders a two-line superlink
|
|
71
72
|
* Special heading subtitles with `## Heading # A subtitle here`
|
|
72
73
|
* `{{{ _partial.md }}}` syntax for including partials
|
|
73
74
|
|
|
@@ -5,16 +5,23 @@ import type { SiteVariables } from './types';
|
|
|
5
5
|
|
|
6
6
|
const log = makeLogger(import.meta.url);
|
|
7
7
|
|
|
8
|
+
export function isExternalHref(
|
|
9
|
+
href: string,
|
|
10
|
+
siteVariables: SiteVariables,
|
|
11
|
+
): boolean {
|
|
12
|
+
if (!href.match(/^https?:\/\/.*$/)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const url = new URL(href);
|
|
16
|
+
return !siteVariables.internalDomains?.includes(url.host);
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
export default function externalLinks(
|
|
9
20
|
md: MarkdownIt,
|
|
10
21
|
siteVariables: SiteVariables,
|
|
11
22
|
): void {
|
|
12
23
|
function isExternal(href: string): boolean {
|
|
13
|
-
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
const url = new URL(href);
|
|
17
|
-
return !siteVariables.internalDomains?.includes(url.host);
|
|
24
|
+
return isExternalHref(href, siteVariables);
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
function findMatchingClose(children: Token[], openIdx: number): number {
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type MarkdownIt from 'markdown-it';
|
|
2
|
+
import type StateInline from 'markdown-it/lib/rules_inline/state_inline.mjs';
|
|
3
|
+
import { isExternalHref } from './external-links-plugin';
|
|
4
|
+
import type { SiteVariables } from './types';
|
|
5
|
+
|
|
6
|
+
const OPEN_BRACKET = 0x5b;
|
|
7
|
+
const CARET = 0x5e;
|
|
8
|
+
const OPEN_PAREN = 0x28;
|
|
9
|
+
const CLOSE_PAREN = 0x29;
|
|
10
|
+
const NEWLINE = 0x0a;
|
|
11
|
+
const LESS_THAN = 0x3c;
|
|
12
|
+
const RAW_ANCHOR_OPEN = /^<a[\s>]/i;
|
|
13
|
+
|
|
14
|
+
// Renders `[Title][Description](/destination.html)` as a two-line superlink.
|
|
15
|
+
// It must run before the built-in `link` rule, which would otherwise
|
|
16
|
+
// treat `[Title][Description]` as a reference link.
|
|
17
|
+
export default function superlinkPlugin(
|
|
18
|
+
md: MarkdownIt,
|
|
19
|
+
siteVariables: SiteVariables,
|
|
20
|
+
): void {
|
|
21
|
+
function skipSpaces(src: string, pos: number, max: number): number {
|
|
22
|
+
while (pos < max) {
|
|
23
|
+
const code = src.charCodeAt(pos);
|
|
24
|
+
if (!md.utils.isSpace(code) && code !== NEWLINE) {
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
pos++;
|
|
28
|
+
}
|
|
29
|
+
return pos;
|
|
30
|
+
}
|
|
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
|
+
|
|
72
|
+
function pushPart(
|
|
73
|
+
state: StateInline,
|
|
74
|
+
name: 'title' | 'description',
|
|
75
|
+
start: number,
|
|
76
|
+
end: number,
|
|
77
|
+
): void {
|
|
78
|
+
const open = state.push(`superlink_${name}_open`, 'span', 1);
|
|
79
|
+
open.attrs = [['class', `superlink-${name}`]];
|
|
80
|
+
|
|
81
|
+
const oldPos = state.pos;
|
|
82
|
+
const oldMax = state.posMax;
|
|
83
|
+
state.pos = start;
|
|
84
|
+
state.posMax = end;
|
|
85
|
+
state.md.inline.tokenize(state);
|
|
86
|
+
state.pos = oldPos;
|
|
87
|
+
state.posMax = oldMax;
|
|
88
|
+
|
|
89
|
+
state.push(`superlink_${name}_close`, 'span', -1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function superlinkRule(state: StateInline, silent: boolean): boolean {
|
|
93
|
+
const src = state.src;
|
|
94
|
+
const max = state.posMax;
|
|
95
|
+
|
|
96
|
+
if (src.charCodeAt(state.pos) !== OPEN_BRACKET) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Leave a footnote reference such as `[^1][source](url)` to the footnote
|
|
101
|
+
// plugin, whose rule would otherwise never see it.
|
|
102
|
+
if (src.charCodeAt(state.pos + 1) === CARET) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const titleStart = state.pos + 1;
|
|
107
|
+
const titleEnd = state.md.helpers.parseLinkLabel(state, state.pos, true);
|
|
108
|
+
if (titleEnd < 0) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The description label must follow the title label immediately.
|
|
113
|
+
const descriptionOpen = titleEnd + 1;
|
|
114
|
+
if (
|
|
115
|
+
descriptionOpen >= max ||
|
|
116
|
+
src.charCodeAt(descriptionOpen) !== OPEN_BRACKET
|
|
117
|
+
) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
const descriptionStart = descriptionOpen + 1;
|
|
121
|
+
const descriptionEnd = state.md.helpers.parseLinkLabel(
|
|
122
|
+
state,
|
|
123
|
+
descriptionOpen,
|
|
124
|
+
true,
|
|
125
|
+
);
|
|
126
|
+
if (descriptionEnd < 0) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
src.slice(titleStart, titleEnd).trim() === '' ||
|
|
132
|
+
src.slice(descriptionStart, descriptionEnd).trim() === ''
|
|
133
|
+
) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let pos = descriptionEnd + 1;
|
|
138
|
+
if (pos >= max || src.charCodeAt(pos) !== OPEN_PAREN) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
pos = skipSpaces(src, pos + 1, max);
|
|
142
|
+
if (pos >= max) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const destination = state.md.helpers.parseLinkDestination(src, pos, max);
|
|
147
|
+
if (!destination.ok) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const href = state.md.normalizeLink(destination.str);
|
|
151
|
+
if (!state.md.validateLink(href)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
pos = skipSpaces(src, destination.pos, max);
|
|
156
|
+
if (pos >= max || src.charCodeAt(pos) !== CLOSE_PAREN) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
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
|
+
|
|
168
|
+
if (!silent) {
|
|
169
|
+
const external = isExternalHref(href, siteVariables);
|
|
170
|
+
const open = state.push('superlink_open', 'a', 1);
|
|
171
|
+
open.attrs = [
|
|
172
|
+
['href', href],
|
|
173
|
+
['class', external ? 'button superlink external' : 'button superlink'],
|
|
174
|
+
];
|
|
175
|
+
if (external) {
|
|
176
|
+
open.attrs.push(['target', '_blank'], ['rel', 'noopener noreferrer']);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
pushPart(state, 'title', titleStart, titleEnd);
|
|
180
|
+
|
|
181
|
+
// The spans are blocks, so this collapses visually, but it keeps text
|
|
182
|
+
// extraction (search indexing, copy and paste) from gluing the last word
|
|
183
|
+
// of the title to the first word of the description.
|
|
184
|
+
state.push('text', '', 0).content = '\n';
|
|
185
|
+
|
|
186
|
+
pushPart(state, 'description', descriptionStart, descriptionEnd);
|
|
187
|
+
|
|
188
|
+
state.push('superlink_close', 'a', -1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
state.pos = pos + 1;
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
md.inline.ruler.before('link', 'superlink', superlinkRule);
|
|
196
|
+
}
|
package/build/utils/markdown.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { isBundledLanguage, isPlainTextLanguage } from '../site-variables';
|
|
|
12
12
|
import headingSubtitlePlugin from '../heading-subtitle-plugin';
|
|
13
13
|
import deflistIdPlugin from '../deflist-id-plugin';
|
|
14
14
|
import externalLinksPlugin from '../external-links-plugin';
|
|
15
|
+
import superlinkPlugin from '../superlink-plugin';
|
|
15
16
|
import { tocPlugin } from '../toc-plugin';
|
|
16
17
|
import columnsPlugin from '../columns-plugin';
|
|
17
18
|
import katexPlugin from './katex';
|
|
@@ -61,6 +62,7 @@ export function createMarkdown(
|
|
|
61
62
|
.use(markdownItDeflist)
|
|
62
63
|
.use(deflistIdPlugin)
|
|
63
64
|
.use(externalLinksPlugin, siteVariables)
|
|
65
|
+
.use(superlinkPlugin, siteVariables)
|
|
64
66
|
.use(tocPlugin)
|
|
65
67
|
.use(columnsPlugin)
|
|
66
68
|
.use(katexPlugin)
|
package/init/content/markdown.md
CHANGED
|
@@ -373,6 +373,27 @@ static boolean isOdd(int n) {
|
|
|
373
373
|
```
|
|
374
374
|
+++
|
|
375
375
|
|
|
376
|
+
### Superlinks
|
|
377
|
+
|
|
378
|
+
Use
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
[Home][Back to the front page](index.html)
|
|
382
|
+
[Markdown][You are here](markdown.html)
|
|
383
|
+
[KaTeX][The library behind math support](https://katex.org/)
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
to create a link with a title and a description:
|
|
387
|
+
|
|
388
|
+
[Home][Back to the front page](index.html)
|
|
389
|
+
[Markdown][You are here](markdown.html)
|
|
390
|
+
[KaTeX][The library behind math support](https://katex.org/)
|
|
391
|
+
|
|
392
|
+
Superlinks are rendered as block elements, so don't use them inline, in the
|
|
393
|
+
middle of a paragraph, or in a heading. Put them on their own lines, as above.
|
|
394
|
+
A superlink to another website, like the last one above, opens in a new tab and
|
|
395
|
+
shows an external link icon in place of the chevron.
|
|
396
|
+
|
|
376
397
|
---
|
|
377
398
|
|
|
378
399
|
## Additional features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abreen/tada",
|
|
3
|
-
"version": "1.19.
|
|
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.
|
|
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": [
|
package/src/_alerts.scss
CHANGED
package/src/_content.scss
CHANGED
|
@@ -164,6 +164,60 @@ select:focus {
|
|
|
164
164
|
border-color: var(--fg-color);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
a.superlink {
|
|
168
|
+
position: relative;
|
|
169
|
+
display: block;
|
|
170
|
+
padding-block: calc(var(--gap) / 2);
|
|
171
|
+
padding-inline: var(--gap)
|
|
172
|
+
calc(var(--gap) * 2 + var(--icon-breadcrumb-separator-size));
|
|
173
|
+
font-size: inherit;
|
|
174
|
+
text-align: left;
|
|
175
|
+
|
|
176
|
+
+ a.superlink {
|
|
177
|
+
margin-top: var(--gap);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.superlink-title {
|
|
181
|
+
display: block;
|
|
182
|
+
font-weight: 600;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.superlink-description {
|
|
186
|
+
display: block;
|
|
187
|
+
font-weight: normal;
|
|
188
|
+
color: var(--fg2-color);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
&::after {
|
|
192
|
+
@include contained-mask(var(--icon-breadcrumb-separator));
|
|
193
|
+
|
|
194
|
+
position: absolute;
|
|
195
|
+
top: 50%;
|
|
196
|
+
right: var(--gap);
|
|
197
|
+
width: var(--icon-breadcrumb-separator-size);
|
|
198
|
+
height: var(--icon-breadcrumb-separator-size);
|
|
199
|
+
content: '';
|
|
200
|
+
background-color: var(--fg2-color);
|
|
201
|
+
transform: translateY(-50%);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// External superlinks get the external-link icon instead of the chevron from
|
|
205
|
+
// the shared `a.external` rules above, which win on mask, size and content.
|
|
206
|
+
// They also set currentcolor, so restore the subdued color. The icon's box
|
|
207
|
+
// is smaller than the chevron's, so shift it left to share the chevron's
|
|
208
|
+
// center, which keeps the two optically aligned.
|
|
209
|
+
&.external::after {
|
|
210
|
+
right: calc(
|
|
211
|
+
var(--gap) +
|
|
212
|
+
(
|
|
213
|
+
var(--icon-breadcrumb-separator-size) - var(--icon-external-link-size)
|
|
214
|
+
) /
|
|
215
|
+
2
|
|
216
|
+
);
|
|
217
|
+
background-color: var(--fg2-color);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
167
221
|
button.icon-button {
|
|
168
222
|
display: inline-flex;
|
|
169
223
|
align-items: center;
|
package/src/navigate/runtime.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
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);
|