@abreen/tada 1.11.3 → 1.13.4
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 +65 -64
- package/bin/tada.ts +61 -66
- package/build/build-manifest.ts +14 -3
- package/build/bundle.ts +0 -1
- package/build/config-files.ts +183 -0
- package/build/config-loader.ts +159 -0
- package/build/copy.ts +0 -83
- package/build/generate-content-assets.ts +75 -160
- package/build/globals.ts +48 -89
- package/build/log.ts +27 -18
- package/build/pagefind.ts +33 -26
- package/build/pipeline.ts +98 -79
- package/build/reachability.ts +51 -248
- package/build/serve.ts +35 -6
- package/build/site-variables.ts +20 -11
- package/build/source-model.ts +400 -0
- package/build/{watch/records.ts → source-records.ts} +111 -49
- package/build/template-globals.ts +91 -0
- package/build/templates.ts +50 -40
- package/build/test-helpers.ts +15 -0
- package/build/toc-plugin.ts +5 -1
- package/build/types.d.ts +9 -1
- package/build/util.ts +8 -0
- package/build/utils/code.ts +6 -16
- package/build/utils/content-files.ts +57 -126
- package/build/utils/final-html.ts +239 -0
- package/build/utils/markdown.ts +237 -17
- package/build/utils/paths.ts +2 -1
- package/build/utils/render.ts +108 -99
- package/build/utils/trace-core.ts +3 -7
- package/build/validate-config-links.ts +91 -14
- package/build/watch/assets.ts +0 -2
- package/build/watch/build-full.ts +14 -15
- package/build/watch/build-incremental.ts +15 -8
- package/build/watch/compiler-types.d.ts +6 -1
- package/build/watch/compiler.ts +27 -15
- package/build/watch/index.ts +2 -7
- package/build/watch/planner.ts +32 -16
- package/build/watch/reload.ts +4 -0
- package/build/watch/runtime.ts +29 -45
- package/build/watch/snapshot.ts +50 -402
- package/build/watch/validation.ts +6 -3
- package/build/watch-reachability-state.ts +16 -23
- package/build/watch-reload-client.ts +11 -4
- package/init/content/index.md +1 -1
- package/init/content/labs/02/index.md +61 -0
- package/init/content/labs/02/selection_sort.py +23 -0
- package/init/content/labs/index.md +4 -2
- package/init/content/lectures/01/index.md +1 -1
- package/init/content/markdown.md +1 -1
- package/package.json +3 -4
- package/src/_base.scss +67 -0
- package/src/anchor/README.md +9 -0
- package/src/anchor/index.ts +115 -16
- package/src/anchor/style.scss +70 -10
- package/src/code/index.ts +46 -18
- package/src/code.scss +2 -1
- package/src/globals.ts +47 -0
- package/src/index.ts +1 -0
- package/src/navigate/index.ts +2 -5
- package/src/navigate/lifecycle.ts +3 -1
- package/src/navigate/runtime.ts +6 -5
- package/src/page-update/index.ts +20 -16
- package/src/print/style.scss +0 -4
- package/src/search/index.ts +53 -25
- package/src/slides/index.ts +481 -0
- package/src/slides/style.scss +104 -0
- package/src/style.scss +8 -3
- package/src/timezone/index.ts +16 -20
- package/src/top/index.ts +2 -4
- package/src/trace/index.ts +116 -14
- package/src/trace/style.scss +1 -1
- package/src/util.ts +6 -2
- package/templates/_author.html +2 -2
- package/templates/_heading.html +1 -1
- package/templates/_nav.html +2 -2
- package/templates/_theme.scss +4 -3
- package/templates/_top.html +14 -14
- package/templates/default.html +9 -0
- package/types/dev.d.ts +1 -1
- package/types/site-variables.d.ts +4 -5
- package/watch/fs-commit.ts +63 -10
- package/build/apply-base-path-plugin.ts +0 -186
- package/build/content-watch.ts +0 -104
- package/build/validate-internal-links-plugin.ts +0 -207
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
parent: ../index.html
|
|
3
|
+
parentLabel: Labs
|
|
4
|
+
title: Lab 2
|
|
5
|
+
author: alex
|
|
6
|
+
description: A page demonstrating slides mode.
|
|
7
|
+
slides: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Selection sort
|
|
11
|
+
|
|
12
|
+
Selection sort is a well-known comparison-based sorting algorithm.
|
|
13
|
+
|
|
14
|
+
- Divides the array into a sorted left part and an unsorted right part
|
|
15
|
+
- Repeatedly scans left-to-right, looking for the smallest element
|
|
16
|
+
- Swaps the smallest in the right part into correct position in the left part
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<%= renderTrace('selection_sort.py') %>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Impressions
|
|
25
|
+
|
|
26
|
+
Selection sort's strength is its simplicity.
|
|
27
|
+
|
|
28
|
+
- `index_smallest()` always scans from `start` to the end
|
|
29
|
+
- It requires no additional memory (other than local variables)
|
|
30
|
+
|
|
31
|
+
However
|
|
32
|
+
|
|
33
|
+
- It has no `break` statements & can't return early
|
|
34
|
+
- No difference between best, average and worst cases
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Performance
|
|
39
|
+
|
|
40
|
+
- The outer loop always performs $n - 1$ iterations
|
|
41
|
+
- There are always $n - 1$ swaps
|
|
42
|
+
- At each value of `i`, `index_smallest()` iterates from `i + 1` to the end
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Comparisons
|
|
47
|
+
|
|
48
|
+
The innermost statement compares `nums[i]` and `nums[curr]`.
|
|
49
|
+
|
|
50
|
+
- when `i` is 0, the comparison is executed $n - 1$ times
|
|
51
|
+
- when `i` is 1, the innermost statement is executed $n - 2$ times
|
|
52
|
+
- $\dots$
|
|
53
|
+
- when `i` is $n - 3$, the innermost statement is executed two times
|
|
54
|
+
- when `i` is $n - 2$, the innermost statement is executed one time
|
|
55
|
+
|
|
56
|
+
Therefore the total number of comparisons is
|
|
57
|
+
|
|
58
|
+
$$
|
|
59
|
+
(n-1) + (n-2) + \dots + 1 = \sum_{i=1}^{n-1}i
|
|
60
|
+
$$
|
|
61
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
def swap(items, i, j):
|
|
2
|
+
temp = items[i]
|
|
3
|
+
items[i] = items[j]
|
|
4
|
+
items[j] = temp
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def index_smallest(nums, start):
|
|
8
|
+
curr = start
|
|
9
|
+
for i in range(start + 1, len(nums)):
|
|
10
|
+
if nums[i] < nums[curr]:
|
|
11
|
+
curr = i
|
|
12
|
+
|
|
13
|
+
return curr
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def selection(nums):
|
|
17
|
+
for i in range(len(nums) - 1):
|
|
18
|
+
swap(nums, i, index_smallest(nums, i))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
input1 = [25, 31, 12, 9]
|
|
22
|
+
selection(input1)
|
|
23
|
+
print(input1)
|
|
@@ -8,6 +8,8 @@ In each one-hour lab, we'll dive deeper and write code together, exploring how
|
|
|
8
8
|
to apply the concepts we learned in lecture to solve real-world problems and
|
|
9
9
|
write full-length programs.
|
|
10
10
|
|
|
11
|
-
0. [`VowelCounter` and user input](
|
|
11
|
+
0. [`VowelCounter` and user input](00/index.html)
|
|
12
12
|
|
|
13
|
-
1. [Memory diagrams](
|
|
13
|
+
1. [Memory diagrams](01/index.html)
|
|
14
|
+
|
|
15
|
+
2. [Slides mode demo](02/index.html)
|
|
@@ -10,7 +10,7 @@ Here is a sample description of the topics covered at the first lecture.
|
|
|
10
10
|
|
|
11
11
|
The value of `vars.foobar` is <%= vars.foobar %>.
|
|
12
12
|
|
|
13
|
-
You can add any variables you want to the `site.dev.
|
|
13
|
+
You can add any variables you want to the `site.dev.yaml` and `site.prod.yaml`
|
|
14
14
|
files under the `"vars"` property and access them using Lodash template syntax
|
|
15
15
|
from Markdown files, HTML files, and source code. (Lodash template subtitution
|
|
16
16
|
only works in source code files whose extension is listed in
|
package/init/content/markdown.md
CHANGED
|
@@ -126,7 +126,7 @@ The syntax for links is unchanged from Presto. However,
|
|
|
126
126
|
- external links (links whose domain is not one of `site.internalDomains`) are
|
|
127
127
|
automatically given `target="_blank"` and special styling
|
|
128
128
|
|
|
129
|
-
See `
|
|
129
|
+
See `site.dev.yaml` and `site.prod.yaml` to change `site.basePath`
|
|
130
130
|
and `site.internalDomains`.
|
|
131
131
|
|
|
132
132
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abreen/tada",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A static site generator",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"lint:sass": "bun run scripts/lint-sass.ts",
|
|
39
39
|
"typecheck": "tsc --noEmit",
|
|
40
40
|
"format": "prettier --write \"**/*.{ts,js,scss,json}\"",
|
|
41
|
+
"test:unit": "bun run scripts/test.ts unit",
|
|
41
42
|
"test:playwright": "bun run scripts/test.ts playwright",
|
|
42
43
|
"test:functional": "bun run scripts/test.ts functional",
|
|
43
44
|
"test:all": "bun run scripts/test.ts all",
|
|
@@ -79,8 +80,7 @@
|
|
|
79
80
|
"sass": "^1.99.0",
|
|
80
81
|
"sharp": "^0.34.5",
|
|
81
82
|
"shiki": "^4.0.2",
|
|
82
|
-
"string-strip-html": "^8.5.0"
|
|
83
|
-
"ws": "^8.20.0"
|
|
83
|
+
"string-strip-html": "^8.5.0"
|
|
84
84
|
},
|
|
85
85
|
"lint-staged": {
|
|
86
86
|
"*.{ts,js}": "prettier --write",
|
|
@@ -110,7 +110,6 @@
|
|
|
110
110
|
"@types/istanbul-reports": "^3.0.4",
|
|
111
111
|
"@types/lodash": "^4.17.24",
|
|
112
112
|
"@types/markdown-it-container": "^4.0.0",
|
|
113
|
-
"@types/ws": "^8.18.1",
|
|
114
113
|
"eslint": "^10.2.1",
|
|
115
114
|
"husky": "^9.1.7",
|
|
116
115
|
"istanbul-lib-coverage": "^3.2.2",
|
package/src/_base.scss
CHANGED
|
@@ -180,6 +180,73 @@ input[type='search'] {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
label:has(> input[type='checkbox']) {
|
|
184
|
+
display: inline-flex;
|
|
185
|
+
gap: 0.45em;
|
|
186
|
+
align-items: center;
|
|
187
|
+
font-size: var(--font-size-small);
|
|
188
|
+
font-weight: 500;
|
|
189
|
+
|
|
190
|
+
@include user-select(none);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
input[type='checkbox'] {
|
|
194
|
+
display: inline-grid;
|
|
195
|
+
flex: 0 0 auto;
|
|
196
|
+
place-content: center;
|
|
197
|
+
width: 1.5em;
|
|
198
|
+
height: 1.25em;
|
|
199
|
+
padding: 0;
|
|
200
|
+
margin: 0;
|
|
201
|
+
font: inherit;
|
|
202
|
+
color: var(--fg-color);
|
|
203
|
+
appearance: none;
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
background: var(--bg-color);
|
|
206
|
+
border: var(--control-border-width) solid var(--fg2-color);
|
|
207
|
+
border-radius: 0.25em;
|
|
208
|
+
|
|
209
|
+
&::after {
|
|
210
|
+
display: block;
|
|
211
|
+
width: 1.1em;
|
|
212
|
+
height: 1.1em;
|
|
213
|
+
content: '';
|
|
214
|
+
background-color: currentcolor;
|
|
215
|
+
opacity: 0;
|
|
216
|
+
|
|
217
|
+
@include contained-mask(var(--icon-check));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
&:checked {
|
|
221
|
+
&::after {
|
|
222
|
+
opacity: 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
&:disabled {
|
|
227
|
+
cursor: not-allowed;
|
|
228
|
+
opacity: 0.4;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
&:active:not(:disabled) {
|
|
232
|
+
color: var(--bg-color) !important;
|
|
233
|
+
background: var(--link-active-color) !important;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
&:focus {
|
|
237
|
+
outline: 2px solid var(--theme-color);
|
|
238
|
+
outline-offset: 1px;
|
|
239
|
+
background-color: var(--bg2-color);
|
|
240
|
+
border-color: var(--fg-color);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@media (hover: hover) {
|
|
245
|
+
input[type='checkbox']:hover:not(:disabled) {
|
|
246
|
+
background: var(--bg2-color);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
183
250
|
figure {
|
|
184
251
|
margin: 1rem 0;
|
|
185
252
|
|
package/src/anchor/README.md
CHANGED
|
@@ -4,3 +4,12 @@ Append a link to each heading having an `id` attribute.
|
|
|
4
4
|
|
|
5
5
|
When the link is clicked, `window.location` is updated and the heading is
|
|
6
6
|
highlighted using the same function as the `toc` component.
|
|
7
|
+
|
|
8
|
+
The heading link includes a real inline SVG hash icon, revealed on heading
|
|
9
|
+
hover or link focus.
|
|
10
|
+
|
|
11
|
+
For slide title headings (`div.slide > h2:first-child`), append a
|
|
12
|
+
`Present from this slide` icon button. Clicking the button dispatches a
|
|
13
|
+
bubbling `tada:slides-present` custom event with the slide index so the slides
|
|
14
|
+
component can open presentation mode from that slide using the current
|
|
15
|
+
`Full screen` checkbox state.
|
package/src/anchor/index.ts
CHANGED
|
@@ -2,32 +2,131 @@ function getElements(parent: HTMLElement): HTMLHeadingElement[] {
|
|
|
2
2
|
return Array.from(parent.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
function createIcon(window: Window, type: 'hash' | 'present'): SVGSVGElement {
|
|
6
|
+
const svg = window.document.createElementNS(
|
|
7
|
+
'http://www.w3.org/2000/svg',
|
|
8
|
+
'svg',
|
|
9
|
+
);
|
|
10
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
11
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
12
|
+
svg.setAttribute('width', '16');
|
|
13
|
+
svg.setAttribute('height', '16');
|
|
14
|
+
svg.setAttribute('fill', 'none');
|
|
15
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
16
|
+
svg.setAttribute('stroke-width', '1.5');
|
|
17
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
18
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
19
|
+
|
|
20
|
+
const paths =
|
|
21
|
+
type === 'hash'
|
|
22
|
+
? ['M10 3 8 21', 'M16 3l-2 18', 'M4 9h17', 'M3 15h17']
|
|
23
|
+
: [
|
|
24
|
+
'M5 5h14a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Z',
|
|
25
|
+
'M9 20h6',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const d of paths) {
|
|
29
|
+
const path = window.document.createElementNS(
|
|
30
|
+
'http://www.w3.org/2000/svg',
|
|
31
|
+
'path',
|
|
32
|
+
);
|
|
33
|
+
path.setAttribute('d', d);
|
|
34
|
+
svg.appendChild(path);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return svg;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getSlideIndex(slide: HTMLElement): number {
|
|
41
|
+
const rawIndex = slide.getAttribute('data-slide-index');
|
|
42
|
+
if (rawIndex != null) {
|
|
43
|
+
const parsed = Number.parseInt(rawIndex, 10);
|
|
44
|
+
if (Number.isFinite(parsed)) {
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const root = slide.closest('[data-slides-root]');
|
|
50
|
+
if (!root) {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return Array.from(root.querySelectorAll('.slide')).indexOf(slide);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isSlideTitleHeading(
|
|
58
|
+
heading: HTMLHeadingElement,
|
|
59
|
+
): heading is HTMLHeadingElement & { parentElement: HTMLElement } {
|
|
60
|
+
const slide = heading.parentElement;
|
|
61
|
+
return (
|
|
62
|
+
heading.tagName === 'H2' &&
|
|
63
|
+
slide?.tagName === 'DIV' &&
|
|
64
|
+
slide.classList.contains('slide') &&
|
|
65
|
+
slide.firstElementChild === heading
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
5
69
|
export default (window: Window) => {
|
|
70
|
+
const view = window as Window & typeof globalThis;
|
|
6
71
|
const elements = getElements(window.document.body);
|
|
7
|
-
|
|
8
|
-
return;
|
|
9
|
-
}
|
|
72
|
+
const cleanups: (() => void)[] = [];
|
|
10
73
|
|
|
11
74
|
elements.forEach(el => {
|
|
12
|
-
if (
|
|
75
|
+
if (el.id) {
|
|
76
|
+
const link = window.document.createElement('a');
|
|
77
|
+
link.className = 'heading-anchor';
|
|
78
|
+
link.href = `#${el.id}`;
|
|
79
|
+
link.title = 'Link to this heading';
|
|
80
|
+
|
|
81
|
+
// Move all existing child nodes into the link.
|
|
82
|
+
while (el.firstChild) {
|
|
83
|
+
link.appendChild(el.firstChild);
|
|
84
|
+
}
|
|
85
|
+
link.appendChild(createIcon(window, 'hash'));
|
|
86
|
+
el.appendChild(link);
|
|
87
|
+
|
|
88
|
+
const handleClick = () => {
|
|
89
|
+
el.focus();
|
|
90
|
+
};
|
|
91
|
+
link.addEventListener('click', handleClick);
|
|
92
|
+
cleanups.push(() => {
|
|
93
|
+
link.removeEventListener('click', handleClick);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!isSlideTitleHeading(el)) {
|
|
13
98
|
return;
|
|
14
99
|
}
|
|
15
100
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
101
|
+
const button = window.document.createElement('button');
|
|
102
|
+
button.type = 'button';
|
|
103
|
+
button.className = 'heading-present-button';
|
|
104
|
+
button.setAttribute('aria-label', 'Present from this slide');
|
|
105
|
+
button.title = 'Present from this slide';
|
|
106
|
+
button.appendChild(createIcon(window, 'present'));
|
|
107
|
+
el.appendChild(button);
|
|
20
108
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
link.appendChild(el.firstChild);
|
|
24
|
-
}
|
|
25
|
-
el.appendChild(link);
|
|
109
|
+
const slide = el.parentElement;
|
|
110
|
+
const slideIndex = getSlideIndex(slide);
|
|
26
111
|
|
|
27
|
-
|
|
28
|
-
|
|
112
|
+
const handlePresentClick = () => {
|
|
113
|
+
button.dispatchEvent(
|
|
114
|
+
new view.CustomEvent('tada:slides-present', {
|
|
115
|
+
bubbles: true,
|
|
116
|
+
detail: { slideIndex },
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
button.addEventListener('click', handlePresentClick);
|
|
121
|
+
|
|
122
|
+
cleanups.push(() => {
|
|
123
|
+
button.removeEventListener('click', handlePresentClick);
|
|
29
124
|
});
|
|
30
125
|
});
|
|
31
126
|
|
|
32
|
-
return () => {
|
|
127
|
+
return () => {
|
|
128
|
+
for (const cleanup of cleanups) {
|
|
129
|
+
cleanup();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
33
132
|
};
|
package/src/anchor/style.scss
CHANGED
|
@@ -7,29 +7,89 @@ a.heading-anchor {
|
|
|
7
7
|
@include subdued-link;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
.heading-anchor svg,
|
|
11
|
+
.heading-present-button svg {
|
|
11
12
|
display: inline-block;
|
|
12
13
|
width: 1em;
|
|
13
14
|
height: 1em;
|
|
15
|
+
vertical-align: middle;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.heading-anchor svg {
|
|
14
19
|
margin-left: 4px;
|
|
15
20
|
vertical-align: middle;
|
|
16
|
-
content: '';
|
|
17
21
|
opacity: 0;
|
|
22
|
+
}
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
.heading-present-button {
|
|
25
|
+
display: inline-flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
width: 1em;
|
|
29
|
+
height: 1em;
|
|
30
|
+
padding: 0;
|
|
31
|
+
margin-left: 4px;
|
|
32
|
+
font: inherit;
|
|
33
|
+
line-height: 1;
|
|
34
|
+
vertical-align: middle;
|
|
35
|
+
color: var(--fg-color);
|
|
36
|
+
background: transparent;
|
|
37
|
+
border: none;
|
|
38
|
+
border-radius: 0;
|
|
39
|
+
opacity: 0;
|
|
20
40
|
}
|
|
21
41
|
|
|
22
|
-
a.heading-anchor:focus-visible
|
|
42
|
+
a.heading-anchor:focus-visible svg,
|
|
43
|
+
.heading-present-button:focus {
|
|
23
44
|
opacity: 1;
|
|
24
45
|
}
|
|
25
46
|
|
|
47
|
+
.heading-present-button:focus {
|
|
48
|
+
outline-offset: 2px;
|
|
49
|
+
background-color: transparent;
|
|
50
|
+
border-color: transparent;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
button.heading-present-button:active:not(:disabled) {
|
|
54
|
+
color: var(--link-active-color) !important;
|
|
55
|
+
background: transparent !important;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
body.is-presenting .heading-present-button {
|
|
59
|
+
display: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
26
62
|
@media (hover: hover) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
63
|
+
button.heading-present-button:hover:not(:disabled) {
|
|
64
|
+
color: var(--fg2-color);
|
|
65
|
+
background: transparent;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
h1:hover a.heading-anchor svg,
|
|
69
|
+
h2:hover a.heading-anchor svg,
|
|
70
|
+
h3:hover a.heading-anchor svg,
|
|
71
|
+
h4:hover a.heading-anchor svg,
|
|
72
|
+
h5:hover a.heading-anchor svg,
|
|
73
|
+
h6:hover a.heading-anchor svg,
|
|
74
|
+
h1:hover .heading-present-button,
|
|
75
|
+
h2:hover .heading-present-button,
|
|
76
|
+
h3:hover .heading-present-button,
|
|
77
|
+
h4:hover .heading-present-button,
|
|
78
|
+
h5:hover .heading-present-button,
|
|
79
|
+
h6:hover .heading-present-button {
|
|
33
80
|
opacity: 1;
|
|
34
81
|
}
|
|
35
82
|
}
|
|
83
|
+
|
|
84
|
+
@media (hover: none) {
|
|
85
|
+
.heading-present-button {
|
|
86
|
+
display: none;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@media print {
|
|
91
|
+
.heading-anchor svg,
|
|
92
|
+
.heading-present-button {
|
|
93
|
+
display: none;
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/code/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { globals } from '../globals';
|
|
2
|
+
|
|
1
3
|
export default async function mount(
|
|
2
4
|
window: Window,
|
|
3
5
|
): Promise<void | (() => void)> {
|
|
@@ -6,7 +8,17 @@ export default async function mount(
|
|
|
6
8
|
return;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
document.
|
|
11
|
+
const codeBodyEl = document.querySelector<HTMLElement>('.code-body');
|
|
12
|
+
const scrollbarEl = document.querySelector<HTMLElement>('.code-scrollbar');
|
|
13
|
+
if (!codeBodyEl || !scrollbarEl) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const codeBody = codeBodyEl;
|
|
17
|
+
const scrollbar = scrollbarEl;
|
|
18
|
+
|
|
19
|
+
const cleanups: Array<() => void> = [];
|
|
20
|
+
|
|
21
|
+
const handleCopy = (e: ClipboardEvent) => {
|
|
10
22
|
const selection = window.getSelection();
|
|
11
23
|
if (!selection || selection.rangeCount === 0) {
|
|
12
24
|
return;
|
|
@@ -54,19 +66,23 @@ export default async function mount(
|
|
|
54
66
|
|
|
55
67
|
e.clipboardData!.setData('text/plain', lines.join('\n'));
|
|
56
68
|
e.preventDefault();
|
|
69
|
+
};
|
|
70
|
+
document.addEventListener('copy', handleCopy);
|
|
71
|
+
cleanups.push(() => {
|
|
72
|
+
document.removeEventListener('copy', handleCopy);
|
|
57
73
|
});
|
|
58
74
|
|
|
59
75
|
const downloadLink = document.querySelector<HTMLAnchorElement>(
|
|
60
76
|
'.file-header a[download]',
|
|
61
77
|
);
|
|
62
78
|
if (downloadLink && 'showSaveFilePicker' in window) {
|
|
63
|
-
|
|
79
|
+
const handleDownloadClick = async (e: MouseEvent) => {
|
|
64
80
|
e.preventDefault();
|
|
65
81
|
try {
|
|
66
82
|
const handle = await window.showSaveFilePicker!({
|
|
67
83
|
suggestedName: downloadLink.download,
|
|
68
84
|
});
|
|
69
|
-
const response = await fetch(downloadLink.href);
|
|
85
|
+
const response = await globals.fetch(downloadLink.href);
|
|
70
86
|
const writable = await handle.createWritable();
|
|
71
87
|
await writable.write(await response.blob());
|
|
72
88
|
await writable.close();
|
|
@@ -75,47 +91,59 @@ export default async function mount(
|
|
|
75
91
|
throw err;
|
|
76
92
|
}
|
|
77
93
|
}
|
|
94
|
+
};
|
|
95
|
+
downloadLink.addEventListener('click', handleDownloadClick);
|
|
96
|
+
cleanups.push(() => {
|
|
97
|
+
downloadLink.removeEventListener('click', handleDownloadClick);
|
|
78
98
|
});
|
|
79
99
|
}
|
|
80
100
|
|
|
81
|
-
const codeBody = document.querySelector<HTMLElement>('.code-body');
|
|
82
|
-
const scrollbar = document.querySelector<HTMLElement>('.code-scrollbar');
|
|
83
|
-
if (!codeBody || !scrollbar) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
101
|
const inner = scrollbar.firstElementChild as HTMLElement;
|
|
88
102
|
let syncing = false;
|
|
89
103
|
|
|
90
104
|
function updateScrollbar(): void {
|
|
91
|
-
const hasOverflow = codeBody
|
|
92
|
-
scrollbar
|
|
93
|
-
inner.style.width = codeBody
|
|
105
|
+
const hasOverflow = codeBody.scrollWidth > codeBody.clientWidth;
|
|
106
|
+
scrollbar.style.display = hasOverflow ? '' : 'none';
|
|
107
|
+
inner.style.width = codeBody.scrollWidth + 'px';
|
|
94
108
|
}
|
|
95
109
|
|
|
96
|
-
|
|
110
|
+
const handleCodeBodyScroll = () => {
|
|
97
111
|
if (syncing) {
|
|
98
112
|
return;
|
|
99
113
|
}
|
|
100
114
|
syncing = true;
|
|
101
|
-
scrollbar
|
|
115
|
+
scrollbar.scrollLeft = codeBody.scrollLeft;
|
|
102
116
|
syncing = false;
|
|
117
|
+
};
|
|
118
|
+
codeBody.addEventListener('scroll', handleCodeBodyScroll);
|
|
119
|
+
cleanups.push(() => {
|
|
120
|
+
codeBody.removeEventListener('scroll', handleCodeBodyScroll);
|
|
103
121
|
});
|
|
104
122
|
|
|
105
|
-
|
|
123
|
+
const handleScrollbarScroll = () => {
|
|
106
124
|
if (syncing) {
|
|
107
125
|
return;
|
|
108
126
|
}
|
|
109
127
|
syncing = true;
|
|
110
|
-
codeBody
|
|
128
|
+
codeBody.scrollLeft = scrollbar.scrollLeft;
|
|
111
129
|
syncing = false;
|
|
130
|
+
};
|
|
131
|
+
scrollbar.addEventListener('scroll', handleScrollbarScroll);
|
|
132
|
+
cleanups.push(() => {
|
|
133
|
+
scrollbar.removeEventListener('scroll', handleScrollbarScroll);
|
|
112
134
|
});
|
|
113
135
|
|
|
114
|
-
const resizeObserver =
|
|
136
|
+
const resizeObserver = globals.createResizeObserver(updateScrollbar);
|
|
115
137
|
resizeObserver.observe(codeBody);
|
|
116
138
|
updateScrollbar();
|
|
139
|
+
cleanups.push(() => {
|
|
140
|
+
resizeObserver.disconnect();
|
|
141
|
+
});
|
|
117
142
|
|
|
118
143
|
return () => {
|
|
119
|
-
|
|
144
|
+
cleanups
|
|
145
|
+
.splice(0)
|
|
146
|
+
.reverse()
|
|
147
|
+
.forEach(cleanup => cleanup());
|
|
120
148
|
};
|
|
121
149
|
}
|
package/src/code.scss
CHANGED
package/src/globals.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
type FetchFunction = (
|
|
2
|
+
input: RequestInfo | URL,
|
|
3
|
+
init?: RequestInit,
|
|
4
|
+
) => Promise<Response>;
|
|
5
|
+
|
|
6
|
+
type ImportModuleFunction = (specifier: string) => Promise<unknown>;
|
|
7
|
+
type LocationMutationFunction = (window: Window, value: string) => void;
|
|
8
|
+
|
|
9
|
+
export interface Globals {
|
|
10
|
+
replaceLocation: LocationMutationFunction;
|
|
11
|
+
createResizeObserver: (
|
|
12
|
+
callback: ResizeObserverCallback,
|
|
13
|
+
) => Pick<ResizeObserver, 'disconnect' | 'observe'>;
|
|
14
|
+
fetch: FetchFunction;
|
|
15
|
+
importModule: ImportModuleFunction;
|
|
16
|
+
isDocumentHidden: (document: Document) => boolean;
|
|
17
|
+
now: () => number;
|
|
18
|
+
setLocationHash: LocationMutationFunction;
|
|
19
|
+
setLocationHref: LocationMutationFunction;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const globals: Globals = {
|
|
23
|
+
replaceLocation(window, value) {
|
|
24
|
+
window.location.replace(value);
|
|
25
|
+
},
|
|
26
|
+
createResizeObserver(callback) {
|
|
27
|
+
return new ResizeObserver(callback);
|
|
28
|
+
},
|
|
29
|
+
fetch(input: RequestInfo | URL, init?: RequestInit) {
|
|
30
|
+
return fetch(input, init);
|
|
31
|
+
},
|
|
32
|
+
importModule(specifier) {
|
|
33
|
+
return import(specifier);
|
|
34
|
+
},
|
|
35
|
+
isDocumentHidden(document) {
|
|
36
|
+
return document.hidden;
|
|
37
|
+
},
|
|
38
|
+
now() {
|
|
39
|
+
return Date.now();
|
|
40
|
+
},
|
|
41
|
+
setLocationHash(window, value) {
|
|
42
|
+
window.location.hash = value;
|
|
43
|
+
},
|
|
44
|
+
setLocationHref(window, value) {
|
|
45
|
+
window.location.href = value;
|
|
46
|
+
},
|
|
47
|
+
};
|
package/src/index.ts
CHANGED