@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
package/src/trace/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TraceManifest, TraceChunkEntry } from './types';
|
|
2
|
+
import { globals } from '../globals';
|
|
2
3
|
|
|
3
4
|
interface WidgetState {
|
|
4
5
|
manifest: TraceManifest;
|
|
@@ -10,7 +11,7 @@ interface WidgetElements {
|
|
|
10
11
|
root: HTMLElement;
|
|
11
12
|
source: HTMLElement;
|
|
12
13
|
controls: HTMLElement;
|
|
13
|
-
|
|
14
|
+
content: HTMLElement;
|
|
14
15
|
diagram: HTMLElement;
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -33,7 +34,7 @@ async function loadChunk(
|
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
const url = chunkUrlFromManifest(manifestUrl, chunkIndex);
|
|
36
|
-
const res = await fetch(url);
|
|
37
|
+
const res = await globals.fetch(url);
|
|
37
38
|
const chunk: TraceChunkEntry[] = await res.json();
|
|
38
39
|
state.chunks.set(chunkIndex, chunk);
|
|
39
40
|
}
|
|
@@ -135,6 +136,84 @@ function updateStepControls(
|
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
function parsePositiveNumber(
|
|
140
|
+
rawValue: string | null | undefined,
|
|
141
|
+
): number | null {
|
|
142
|
+
if (!rawValue) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const parsed = Number.parseFloat(rawValue);
|
|
147
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getPresentationScale(diagram: HTMLElement, doc: Document): number {
|
|
151
|
+
if (!doc.body.classList.contains('is-presenting')) {
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const view = doc.defaultView;
|
|
156
|
+
const traceWidget = diagram.closest('.trace-widget') as HTMLElement | null;
|
|
157
|
+
const traceFontSize = parsePositiveNumber(
|
|
158
|
+
traceWidget && view ? view.getComputedStyle(traceWidget).fontSize : null,
|
|
159
|
+
);
|
|
160
|
+
const baseFontSize = parsePositiveNumber(
|
|
161
|
+
view ? view.getComputedStyle(doc.body).fontSize : null,
|
|
162
|
+
);
|
|
163
|
+
if (traceFontSize && baseFontSize) {
|
|
164
|
+
return traceFontSize / baseFontSize;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function scaleDiagramSvg(diagram: HTMLElement, doc: Document): void {
|
|
171
|
+
const svg = diagram.querySelector('.trace-memory') as SVGElement | null;
|
|
172
|
+
if (!svg) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const width = svg.dataset.traceBaseWidth ?? svg.getAttribute('width');
|
|
177
|
+
const height = svg.dataset.traceBaseHeight ?? svg.getAttribute('height');
|
|
178
|
+
if (!width || !height) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
svg.dataset.traceBaseWidth = width;
|
|
183
|
+
svg.dataset.traceBaseHeight = height;
|
|
184
|
+
|
|
185
|
+
const baseWidth = Number.parseFloat(width);
|
|
186
|
+
const baseHeight = Number.parseFloat(height);
|
|
187
|
+
if (!Number.isFinite(baseWidth) || !Number.isFinite(baseHeight)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const scale = getPresentationScale(diagram, doc);
|
|
192
|
+
svg.setAttribute('width', String(baseWidth * scale));
|
|
193
|
+
svg.setAttribute('height', String(baseHeight * scale));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function updateOutput(
|
|
197
|
+
content: HTMLElement,
|
|
198
|
+
output: string,
|
|
199
|
+
doc: Document,
|
|
200
|
+
): void {
|
|
201
|
+
const existing = content.querySelector(
|
|
202
|
+
':scope > .trace-output',
|
|
203
|
+
) as HTMLPreElement | null;
|
|
204
|
+
if (!output) {
|
|
205
|
+
existing?.remove();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const outputElement = existing ?? doc.createElement('pre');
|
|
210
|
+
if (!existing) {
|
|
211
|
+
outputElement.className = 'trace-output';
|
|
212
|
+
content.append(outputElement);
|
|
213
|
+
}
|
|
214
|
+
outputElement.textContent = output;
|
|
215
|
+
}
|
|
216
|
+
|
|
138
217
|
function renderWidgetState(
|
|
139
218
|
state: WidgetState,
|
|
140
219
|
elements: WidgetElements,
|
|
@@ -157,10 +236,11 @@ function renderWidgetState(
|
|
|
157
236
|
output += s.stdout;
|
|
158
237
|
}
|
|
159
238
|
}
|
|
160
|
-
elements.output
|
|
239
|
+
updateOutput(elements.content, output, doc);
|
|
161
240
|
|
|
162
241
|
const prevHeight = elements.diagram.scrollHeight;
|
|
163
242
|
elements.diagram.innerHTML = entry.svg;
|
|
243
|
+
scaleDiagramSvg(elements.diagram, doc);
|
|
164
244
|
|
|
165
245
|
// If the diagram grew past the visible area, scroll to show the new content
|
|
166
246
|
if (elements.diagram.scrollHeight > prevHeight) {
|
|
@@ -177,7 +257,7 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
|
|
|
177
257
|
|
|
178
258
|
let res: Response;
|
|
179
259
|
try {
|
|
180
|
-
res = await fetch(manifestUrl);
|
|
260
|
+
res = await globals.fetch(manifestUrl);
|
|
181
261
|
} catch {
|
|
182
262
|
return;
|
|
183
263
|
}
|
|
@@ -190,12 +270,9 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
|
|
|
190
270
|
|
|
191
271
|
await loadChunk(state, manifestUrl, 0);
|
|
192
272
|
|
|
193
|
-
const entry = getStep(state);
|
|
194
|
-
|
|
195
273
|
const source = root.querySelector('.trace-source') as HTMLElement;
|
|
274
|
+
const content = root.querySelector('.trace-content') as HTMLElement;
|
|
196
275
|
const diagram = root.querySelector('.trace-diagram') as HTMLElement;
|
|
197
|
-
const output = root.querySelector('.trace-output') as HTMLPreElement;
|
|
198
|
-
output.textContent = entry.stdout || '';
|
|
199
276
|
|
|
200
277
|
const controls = root.querySelector('.trace-controls') as HTMLElement;
|
|
201
278
|
const firstBtn = controls.querySelector('.trace-first') as HTMLButtonElement;
|
|
@@ -237,22 +314,47 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
|
|
|
237
314
|
next.focus();
|
|
238
315
|
});
|
|
239
316
|
|
|
240
|
-
const elements: WidgetElements = { root, source, controls,
|
|
241
|
-
|
|
242
|
-
updateStepControls(controls, state.currentStep, manifest.totalSteps, doc);
|
|
317
|
+
const elements: WidgetElements = { root, source, controls, content, diagram };
|
|
243
318
|
|
|
244
|
-
|
|
245
|
-
updateSourceHighlight(source, entry.line);
|
|
319
|
+
renderWidgetState(state, elements, doc);
|
|
246
320
|
}
|
|
247
321
|
|
|
248
|
-
export default function mountTrace(window: Window): void {
|
|
322
|
+
export default function mountTrace(window: Window): void | (() => void) {
|
|
249
323
|
const widgets = window.document.querySelectorAll('.trace-widget');
|
|
250
324
|
|
|
251
325
|
if (widgets.length === 0) {
|
|
252
326
|
return;
|
|
253
327
|
}
|
|
254
328
|
|
|
329
|
+
const refreshDiagramScales = (): void => {
|
|
330
|
+
for (const widget of widgets) {
|
|
331
|
+
const diagram = (widget as HTMLElement).querySelector(
|
|
332
|
+
'.trace-diagram',
|
|
333
|
+
) as HTMLElement | null;
|
|
334
|
+
if (diagram) {
|
|
335
|
+
scaleDiagramSvg(diagram, window.document);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const MutationObserverCtor = (window as Window & typeof globalThis)
|
|
341
|
+
.MutationObserver;
|
|
342
|
+
const bodyClassObserver = MutationObserverCtor
|
|
343
|
+
? new MutationObserverCtor(() => {
|
|
344
|
+
refreshDiagramScales();
|
|
345
|
+
})
|
|
346
|
+
: null;
|
|
347
|
+
|
|
348
|
+
bodyClassObserver?.observe(window.document.body, {
|
|
349
|
+
attributes: true,
|
|
350
|
+
attributeFilter: ['class'],
|
|
351
|
+
});
|
|
352
|
+
|
|
255
353
|
for (const widget of widgets) {
|
|
256
354
|
initWidget(widget as HTMLElement, window.document);
|
|
257
355
|
}
|
|
356
|
+
|
|
357
|
+
return () => {
|
|
358
|
+
bodyClassObserver?.disconnect();
|
|
359
|
+
};
|
|
258
360
|
}
|
package/src/trace/style.scss
CHANGED
package/src/util.ts
CHANGED
|
@@ -30,18 +30,22 @@ export function getElement(
|
|
|
30
30
|
return el as HTMLElement;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
function applyBasePathWithBasePath(subPath: string, basePath: string): string {
|
|
34
34
|
if (!subPath.startsWith('/')) {
|
|
35
35
|
throw new Error('invalid internal path, must start with "/": ' + subPath);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
let path =
|
|
38
|
+
let path = basePath;
|
|
39
39
|
if (path.endsWith('/')) {
|
|
40
40
|
path = path.slice(0, -1);
|
|
41
41
|
}
|
|
42
42
|
return path + subPath;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export function applyBasePath(subPath: string): string {
|
|
46
|
+
return applyBasePathWithBasePath(subPath, __SITE_BASE_PATH__);
|
|
47
|
+
}
|
|
48
|
+
|
|
45
49
|
export function scheduleTask(win: Window, fn: () => void) {
|
|
46
50
|
if (typeof win.requestIdleCallback === 'function') {
|
|
47
51
|
win.requestIdleCallback(fn);
|
package/templates/_author.html
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
<aside class="author">
|
|
2
2
|
Written by
|
|
3
3
|
<% if (page.author.url) { %>
|
|
4
|
-
<a href="<%=
|
|
4
|
+
<a href="<%= page.author.url %>">
|
|
5
5
|
<% } %>
|
|
6
6
|
<span class="text"><%= page.author.name %></span>
|
|
7
7
|
<% if (page.author.avatar) { %>
|
|
8
8
|
<img
|
|
9
9
|
width="32"
|
|
10
10
|
height="32"
|
|
11
|
-
src="<%=
|
|
11
|
+
src="<%= page.author.avatar %>"
|
|
12
12
|
alt="Picture of <%= page.author.name %>"
|
|
13
13
|
class="avatar"
|
|
14
14
|
/>
|
package/templates/_heading.html
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="title-and-info" data-pagefind-body>
|
|
2
2
|
<% if (page.parent) { %>
|
|
3
|
-
<a class="breadcrumb" href="<%=
|
|
3
|
+
<a class="breadcrumb" href="<%= page.parent %>" data-pagefind-ignore><%= page.parentLabel %></a>
|
|
4
4
|
<% } %>
|
|
5
5
|
<h1 data-pagefind-weight="10" data-pagefind-meta="title_html[data-title-html]" data-title-html="<%- page.titleHtml %>"><%= page.titleHtml %></h1>
|
|
6
6
|
|
package/templates/_nav.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<nav>
|
|
2
|
-
<%
|
|
2
|
+
<% config('nav').forEach(function(section) { %>
|
|
3
3
|
<div>
|
|
4
4
|
<p><%= section.title %></p>
|
|
5
5
|
<ul>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<li>
|
|
8
8
|
<a
|
|
9
9
|
class="<%= cx({disabled: link.disabled, external: link.external}) %>"
|
|
10
|
-
<% if (!link.disabled) { %>href="<%= link.internal
|
|
10
|
+
<% if (!link.disabled) { %>href="<%= link.internal || link.external %>"<% } %>
|
|
11
11
|
<% if (link.external) { %>target="_blank"<% } %>
|
|
12
12
|
>
|
|
13
13
|
<%= link.text %></a>
|
package/templates/_theme.scss
CHANGED
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
--heading-font: var(--font);
|
|
43
43
|
--heading-line-height: 1.3;
|
|
44
44
|
--mono-font: 'Google Sans Code', 'Consolas', 'Courier', monospace;
|
|
45
|
-
--font-size:
|
|
46
|
-
--font-size-smaller:
|
|
47
|
-
--font-size-small:
|
|
45
|
+
--font-size: 1rem;
|
|
46
|
+
--font-size-smaller: 0.9375em;
|
|
47
|
+
--font-size-small: 0.8125em;
|
|
48
48
|
--font-weight: 400;
|
|
49
49
|
--line-height: 1.7;
|
|
50
50
|
--mono-font-size: 0.94em;
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
--icon-anchor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 28'><path d='M11 4 L8 20 M17 4 L14 20 M5 9 H21 M3 15 H19' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
59
59
|
--icon-warning: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><polygon points='12,2 22,20 2,20' fill='none' stroke='black' stroke-width='1.5' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='black' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='16' x2='12' y2='16' stroke='black' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
60
60
|
--icon-note: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='none' stroke='black' stroke-width='1.5'/><line x1='12' y1='10' x2='12' y2='16' stroke='black' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='7' x2='12' y2='7' stroke='black' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
61
|
+
--icon-check: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><path d='M5 12.5 10 17 19 7' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
61
62
|
--icon-search: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><circle cx='11' cy='11' r='7' stroke='<%= iconColor %>' stroke-width='2' fill='none'/><line x1='16.5' y1='16.5' x2='22' y2='22' stroke='<%= iconColor %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
62
63
|
--icon-chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><polyline points='9 18 15 12 9 6' stroke='<%= iconColor %>' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
|
63
64
|
--icon-breadcrumb: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><path d='M3 7 H17 V22' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/><polyline points='11 16 17 22 23 16' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
package/templates/_top.html
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<% if (site.features.favicon) { %>
|
|
5
|
-
<link rel="icon" href="
|
|
6
|
-
<link rel="icon" type="image/svg+xml" href="
|
|
7
|
-
<link rel="icon" type="image/png" sizes="512x512" href="
|
|
8
|
-
<link rel="icon" type="image/png" sizes="256x256" href="
|
|
9
|
-
<link rel="icon" type="image/png" sizes="192x192" href="
|
|
10
|
-
<link rel="icon" type="image/png" sizes="128x128" href="
|
|
11
|
-
<link rel="icon" type="image/png" sizes="64x64" href="
|
|
12
|
-
<link rel="icon" type="image/png" sizes="48x48" href="
|
|
13
|
-
<link rel="icon" type="image/png" sizes="32x32" href="
|
|
14
|
-
<link rel="icon" type="image/png" sizes="16x16" href="
|
|
15
|
-
<link rel="apple-touch-icon" sizes="512x512" href="
|
|
16
|
-
<link rel="apple-touch-icon" sizes="256x256" href="
|
|
17
|
-
<link rel="manifest" href="
|
|
5
|
+
<link rel="icon" href="/favicon.ico" sizes="any" />
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
|
+
<link rel="icon" type="image/png" sizes="512x512" href="/favicon-512.png" />
|
|
8
|
+
<link rel="icon" type="image/png" sizes="256x256" href="/favicon-256.png" />
|
|
9
|
+
<link rel="icon" type="image/png" sizes="192x192" href="/favicon-192.png" />
|
|
10
|
+
<link rel="icon" type="image/png" sizes="128x128" href="/favicon-128.png" />
|
|
11
|
+
<link rel="icon" type="image/png" sizes="64x64" href="/favicon-64.png" />
|
|
12
|
+
<link rel="icon" type="image/png" sizes="48x48" href="/favicon-48.png" />
|
|
13
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
|
|
14
|
+
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png" />
|
|
15
|
+
<link rel="apple-touch-icon" sizes="512x512" href="/favicon-512.png" />
|
|
16
|
+
<link rel="apple-touch-icon" sizes="256x256" href="/favicon-256.png" />
|
|
17
|
+
<link rel="manifest" href="/manifest.json" />
|
|
18
18
|
<meta name="apple-mobile-web-app-title" content="<%= site.symbol %>">
|
|
19
19
|
<meta name="mobile-web-app-capable" content="yes">
|
|
20
20
|
<% } %>
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
{
|
|
39
39
|
"prerender": [{
|
|
40
40
|
"where": {
|
|
41
|
-
"href_matches": "<%=
|
|
41
|
+
"href_matches": "<%= speculationRulesHrefMatches %>"
|
|
42
42
|
},
|
|
43
43
|
"eagerness": "moderate"
|
|
44
44
|
}]
|
package/templates/default.html
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
<div class="main-content">
|
|
4
4
|
<%= render('_heading.html') %>
|
|
5
5
|
<main class="body" data-pagefind-body>
|
|
6
|
+
<% if (page.slides) { %>
|
|
7
|
+
<div class="slides-header" data-pagefind-ignore>
|
|
8
|
+
<button type="button" data-slides-present disabled>Present</button>
|
|
9
|
+
<label>
|
|
10
|
+
<input type="checkbox" data-slides-fullscreen checked disabled>
|
|
11
|
+
Full screen
|
|
12
|
+
</label>
|
|
13
|
+
</div>
|
|
14
|
+
<% } %>
|
|
6
15
|
<%= content %>
|
|
7
16
|
<%= render('_footer.html') %>
|
|
8
17
|
</main>
|
package/types/dev.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
declare
|
|
1
|
+
declare var __IS_DEV__: boolean;
|
|
@@ -5,8 +5,7 @@ interface TimeZone {
|
|
|
5
5
|
offsetMinutes?: number;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
declare
|
|
9
|
-
declare
|
|
10
|
-
declare
|
|
11
|
-
declare
|
|
12
|
-
declare const __SITE_TIMEZONES__: TimeZone[];
|
|
8
|
+
declare var __SITE_BASE_PATH__: string;
|
|
9
|
+
declare var __SITE_TITLE_POSTFIX__: string;
|
|
10
|
+
declare var __SITE_DEFAULT_TIMEZONE__: string;
|
|
11
|
+
declare var __SITE_TIMEZONES__: TimeZone[];
|
package/watch/fs-commit.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { globals, type Globals } from '../build/globals';
|
|
3
4
|
import type {
|
|
4
5
|
ApplyMutationsCommitPlan,
|
|
5
6
|
CommitPlan,
|
|
@@ -7,6 +8,17 @@ import type {
|
|
|
7
8
|
ReplaceRootCommitPlan,
|
|
8
9
|
} from './types';
|
|
9
10
|
|
|
11
|
+
type CommitPlanGlobals = Pick<Globals, 'now' | 'pid' | 'sleepSync'>;
|
|
12
|
+
|
|
13
|
+
const TRANSIENT_RENAME_ERROR_CODES = new Set([
|
|
14
|
+
'EACCES',
|
|
15
|
+
'EBUSY',
|
|
16
|
+
'ENOTEMPTY',
|
|
17
|
+
'EPERM',
|
|
18
|
+
]);
|
|
19
|
+
const RENAME_RETRY_DELAY_MS = 25;
|
|
20
|
+
const RENAME_RETRY_COUNT = 8;
|
|
21
|
+
|
|
10
22
|
function removeDirIfExists(dir: string): void {
|
|
11
23
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
12
24
|
}
|
|
@@ -15,6 +27,39 @@ function ensureParentDir(filePath: string): void {
|
|
|
15
27
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
16
28
|
}
|
|
17
29
|
|
|
30
|
+
function isTransientRenameError(
|
|
31
|
+
error: unknown,
|
|
32
|
+
sourcePath: string,
|
|
33
|
+
): error is NodeJS.ErrnoException {
|
|
34
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
35
|
+
return (
|
|
36
|
+
typeof code === 'string' &&
|
|
37
|
+
TRANSIENT_RENAME_ERROR_CODES.has(code) &&
|
|
38
|
+
fs.existsSync(sourcePath)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renameWithRetry(
|
|
43
|
+
sourcePath: string,
|
|
44
|
+
targetPath: string,
|
|
45
|
+
globals: CommitPlanGlobals,
|
|
46
|
+
): void {
|
|
47
|
+
for (let attempt = 0; attempt <= RENAME_RETRY_COUNT; attempt++) {
|
|
48
|
+
try {
|
|
49
|
+
fs.renameSync(sourcePath, targetPath);
|
|
50
|
+
return;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (
|
|
53
|
+
attempt === RENAME_RETRY_COUNT ||
|
|
54
|
+
!isTransientRenameError(error, sourcePath)
|
|
55
|
+
) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
globals.sleepSync(RENAME_RETRY_DELAY_MS);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
18
63
|
function pruneEmptyDirs(rootDir: string, filePath: string): void {
|
|
19
64
|
let currentDir = path.dirname(filePath);
|
|
20
65
|
const root = path.resolve(rootDir);
|
|
@@ -36,6 +81,7 @@ function applyFileMutation(
|
|
|
36
81
|
rootDir: string,
|
|
37
82
|
mutation: FileMutation,
|
|
38
83
|
stagedRoot: string,
|
|
84
|
+
globals: CommitPlanGlobals,
|
|
39
85
|
): void {
|
|
40
86
|
const targetPath = path.join(rootDir, mutation.path);
|
|
41
87
|
const stagedPath = path.join(stagedRoot, mutation.path);
|
|
@@ -44,7 +90,7 @@ function applyFileMutation(
|
|
|
44
90
|
ensureParentDir(stagedPath);
|
|
45
91
|
fs.writeFileSync(stagedPath, mutation.content);
|
|
46
92
|
ensureParentDir(targetPath);
|
|
47
|
-
|
|
93
|
+
renameWithRetry(stagedPath, targetPath, globals);
|
|
48
94
|
return;
|
|
49
95
|
}
|
|
50
96
|
|
|
@@ -52,7 +98,10 @@ function applyFileMutation(
|
|
|
52
98
|
pruneEmptyDirs(rootDir, targetPath);
|
|
53
99
|
}
|
|
54
100
|
|
|
55
|
-
function applyMutations(
|
|
101
|
+
function applyMutations(
|
|
102
|
+
plan: ApplyMutationsCommitPlan,
|
|
103
|
+
globals: CommitPlanGlobals,
|
|
104
|
+
): void {
|
|
56
105
|
const stagedRoot = fs.mkdtempSync(path.join(plan.rootDir, '.watch-stage-'));
|
|
57
106
|
try {
|
|
58
107
|
for (const mutation of plan.mutations) {
|
|
@@ -64,39 +113,43 @@ function applyMutations(plan: ApplyMutationsCommitPlan): void {
|
|
|
64
113
|
}
|
|
65
114
|
|
|
66
115
|
for (const mutation of plan.mutations) {
|
|
67
|
-
applyFileMutation(plan.rootDir, mutation, stagedRoot);
|
|
116
|
+
applyFileMutation(plan.rootDir, mutation, stagedRoot, globals);
|
|
68
117
|
}
|
|
69
118
|
} finally {
|
|
70
119
|
removeDirIfExists(stagedRoot);
|
|
71
120
|
}
|
|
72
121
|
}
|
|
73
122
|
|
|
74
|
-
function replaceRoot(
|
|
123
|
+
function replaceRoot(
|
|
124
|
+
plan: ReplaceRootCommitPlan,
|
|
125
|
+
globals: CommitPlanGlobals,
|
|
126
|
+
): void {
|
|
75
127
|
const { targetPath, stagedPath } = plan;
|
|
76
|
-
const backupDir = `${targetPath}.bak-${
|
|
128
|
+
const backupDir = `${targetPath}.bak-${globals.pid()}-${globals.now()}`;
|
|
77
129
|
const targetExists = fs.existsSync(targetPath);
|
|
78
130
|
|
|
79
131
|
try {
|
|
80
132
|
if (targetExists) {
|
|
81
|
-
|
|
133
|
+
renameWithRetry(targetPath, backupDir, globals);
|
|
82
134
|
}
|
|
83
|
-
|
|
135
|
+
renameWithRetry(stagedPath, targetPath, globals);
|
|
84
136
|
if (targetExists) {
|
|
85
137
|
removeDirIfExists(backupDir);
|
|
86
138
|
}
|
|
87
139
|
} catch (err) {
|
|
88
140
|
if (!fs.existsSync(targetPath) && fs.existsSync(backupDir)) {
|
|
89
|
-
|
|
141
|
+
renameWithRetry(backupDir, targetPath, globals);
|
|
90
142
|
}
|
|
91
143
|
throw err;
|
|
92
144
|
}
|
|
93
145
|
}
|
|
94
146
|
|
|
95
147
|
export function applyCommitPlan(plan: CommitPlan): void {
|
|
148
|
+
const runtimeGlobals: CommitPlanGlobals = globals;
|
|
96
149
|
if (plan.kind === 'replace-root') {
|
|
97
|
-
replaceRoot(plan);
|
|
150
|
+
replaceRoot(plan, runtimeGlobals);
|
|
98
151
|
return;
|
|
99
152
|
}
|
|
100
153
|
|
|
101
|
-
applyMutations(plan);
|
|
154
|
+
applyMutations(plan, runtimeGlobals);
|
|
102
155
|
}
|