@twango/twiki 0.0.0-stage → 0.1.1
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/.github/actions/product-cache/action.yml +32 -0
- package/.github/actions/product-cache/index.mjs +450 -0
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/bun.lock +1701 -0
- package/cli/runtime.test.ts +420 -0
- package/cli/runtime.ts +385 -0
- package/cli/twiki.ts +305 -0
- package/package.json +138 -4
- package/site/_headers +7 -0
- package/site/assets/fonts/OFL.txt +93 -0
- package/site/assets/fonts/OverusedGrotesk-SemiBold.ttf +0 -0
- package/site/assets/social-background.svg +38 -0
- package/site/bun.lock +1701 -0
- package/site/database/index.ts +7 -0
- package/site/database/schema.ts +95 -0
- package/site/drizzle.config.ts +7 -0
- package/site/migrations/0001_auth.sql +14 -0
- package/site/migrations/0002_site_access.sql +5 -0
- package/site/migrations/0003_github_username.sql +1 -0
- package/site/migrations/meta/0002_snapshot.json +432 -0
- package/site/migrations/meta/0003_snapshot.json +439 -0
- package/site/migrations/meta/_journal.json +27 -0
- package/site/package.json +120 -0
- package/site/src/app.d.ts +14 -0
- package/site/src/app.html +11 -0
- package/site/src/hooks.server.ts +12 -0
- package/site/src/lib/analytics-config.json +1 -0
- package/site/src/lib/analytics.ts +40 -0
- package/site/src/lib/auth-config.json +3 -0
- package/site/src/lib/components/SEO.svelte +113 -0
- package/site/src/lib/components/doc/AccessLink.svelte +27 -0
- package/site/src/lib/components/doc/Backlinks.svelte +59 -0
- package/site/src/lib/components/doc/Breadcrumbs.svelte +29 -0
- package/site/src/lib/components/doc/ContentMeta.svelte +61 -0
- package/site/src/lib/components/doc/DocPager.svelte +91 -0
- package/site/src/lib/components/doc/DocShell.svelte +115 -0
- package/site/src/lib/components/doc/LicenseNotice.svelte +26 -0
- package/site/src/lib/components/doc/LockedContent.svelte +29 -0
- package/site/src/lib/components/doc/NavTree.svelte +158 -0
- package/site/src/lib/components/doc/NoteActions.svelte +139 -0
- package/site/src/lib/components/doc/PublicationBadge.svelte +49 -0
- package/site/src/lib/components/doc/ReaderModeToggle.svelte +19 -0
- package/site/src/lib/components/doc/Sidebar.svelte +351 -0
- package/site/src/lib/components/doc/Toc.svelte +339 -0
- package/site/src/lib/components/doc/enhancements.ts +146 -0
- package/site/src/lib/components/doc/heading-tracker.svelte.ts +39 -0
- package/site/src/lib/components/doc/image-preview.ts +114 -0
- package/site/src/lib/components/doc/reader-mode.svelte.ts +40 -0
- package/site/src/lib/components/embeds/mermaid.ts +353 -0
- package/site/src/lib/components/files/FileCode.svelte +81 -0
- package/site/src/lib/components/files/FileExplorer.svelte +74 -0
- package/site/src/lib/components/files/FileHistory.svelte +170 -0
- package/site/src/lib/components/files/FileIcon.svelte +28 -0
- package/site/src/lib/components/files/FileRmd.svelte +209 -0
- package/site/src/lib/components/files/FileTabs.svelte +180 -0
- package/site/src/lib/components/files/FileTree.svelte +67 -0
- package/site/src/lib/components/files/blame-gutter.ts +45 -0
- package/site/src/lib/components/files/code-view.ts +98 -0
- package/site/src/lib/components/files/file-workspace.svelte.ts +100 -0
- package/site/src/lib/components/graph/GraphPanel.svelte +275 -0
- package/site/src/lib/components/graph/GraphView.svelte +100 -0
- package/site/src/lib/components/graph/graph-data.ts +137 -0
- package/site/src/lib/components/graph/graph-model.ts +103 -0
- package/site/src/lib/components/graph/index.ts +3 -0
- package/site/src/lib/components/graph/render-graph.ts +626 -0
- package/site/src/lib/components/popover/index.ts +1 -0
- package/site/src/lib/components/popover/link-popovers.ts +233 -0
- package/site/src/lib/components/popover/popover.css +82 -0
- package/site/src/lib/components/search/SearchButton.svelte +57 -0
- package/site/src/lib/components/search/SearchPalette.svelte +189 -0
- package/site/src/lib/components/search/index.ts +3 -0
- package/site/src/lib/components/search/pagefind-client.svelte.ts +129 -0
- package/site/src/lib/components/search/pagefind-query.ts +22 -0
- package/site/src/lib/components/search/pagefind-url.ts +12 -0
- package/site/src/lib/components/search/search-state.svelte.ts +22 -0
- package/site/src/lib/components/ui/IconButton.svelte +30 -0
- package/site/src/lib/components/ui/ThemeToggle.svelte +22 -0
- package/site/src/lib/components/ui/index.ts +1 -0
- package/site/src/lib/config.ts +9 -0
- package/site/src/lib/content-license.ts +7 -0
- package/site/src/lib/file-history.ts +37 -0
- package/site/src/lib/file-icons.ts +27 -0
- package/site/src/lib/file-tabs.ts +72 -0
- package/site/src/lib/files.ts +73 -0
- package/site/src/lib/github-user.ts +70 -0
- package/site/src/lib/metadata.ts +166 -0
- package/site/src/lib/notebook-nav.ts +63 -0
- package/site/src/lib/publication.ts +1 -0
- package/site/src/lib/rmd.ts +156 -0
- package/site/src/lib/server/access-admin.ts +33 -0
- package/site/src/lib/server/content-model.ts +253 -0
- package/site/src/lib/server/content-provider.ts +266 -0
- package/site/src/lib/server/content.ts +277 -0
- package/site/src/lib/server/runtime-content.ts +34 -0
- package/site/src/lib/site-config.json +9 -0
- package/site/src/lib/social-image.ts +13 -0
- package/site/src/lib/styles/article.css +607 -0
- package/site/src/lib/styles/foundation.css +138 -0
- package/site/src/lib/styles/print.css +115 -0
- package/site/src/lib/styles/shell.css +182 -0
- package/site/src/lib/text-exports.ts +5 -0
- package/site/src/lib/types.ts +140 -0
- package/site/src/lib/utils.ts +6 -0
- package/site/src/routes/+error.svelte +16 -0
- package/site/src/routes/+layout.server.ts +10 -0
- package/site/src/routes/+layout.svelte +72 -0
- package/site/src/routes/+layout.ts +3 -0
- package/site/src/routes/404/+page.svelte +13 -0
- package/site/src/routes/[...slug]/+page.server.ts +24 -0
- package/site/src/routes/[...slug]/+page.svelte +180 -0
- package/site/src/routes/[course]/files/[...file]/+page.svelte +373 -0
- package/site/src/routes/[course]/files/[...file]/+page.ts +38 -0
- package/site/src/routes/admin/access/+page.server.ts +53 -0
- package/site/src/routes/admin/access/+page.svelte +118 -0
- package/site/src/routes/graph.json/+server.ts +7 -0
- package/site/src/routes/index.xml/+server.ts +7 -0
- package/site/src/routes/layout.css +10 -0
- package/site/src/routes/robots.txt/+server.ts +9 -0
- package/site/src/routes/sitemap.xml/+server.ts +7 -0
- package/site/src/routes/tags/+page.server.ts +5 -0
- package/site/src/routes/tags/+page.svelte +38 -0
- package/site/src/routes/tags/[...tag]/+page.server.ts +22 -0
- package/site/src/routes/tags/[...tag]/+page.svelte +52 -0
- package/site/static/favicon.png +0 -0
- package/site/static/fonts/JetBrainsMono-Italic-VF.woff2 +0 -0
- package/site/static/fonts/JetBrainsMono-VF.woff2 +0 -0
- package/site/static/fonts/OverusedGrotesk-VF.woff2 +0 -0
- package/site/static/fonts/OverusedGroteskItalic-VF.woff2 +0 -0
- package/site/static/fonts/Virgil.woff2 +0 -0
- package/site/svelte.config.js +71 -0
- package/site/tooling/LICENSE-quartz +25 -0
- package/site/tooling/adapter.js +55 -0
- package/site/tooling/assets.ts +323 -0
- package/site/tooling/benchmark.ts +191 -0
- package/site/tooling/build.ts +407 -0
- package/site/tooling/cache.ts +133 -0
- package/site/tooling/check-diagrams.ts +164 -0
- package/site/tooling/check-links.ts +86 -0
- package/site/tooling/check-targets.ts +139 -0
- package/site/tooling/compiler.ts +1386 -0
- package/site/tooling/deploy.ts +112 -0
- package/site/tooling/fingerprint.ts +81 -0
- package/site/tooling/lib/app-version.js +92 -0
- package/site/tooling/lib/application-asset-urls.ts +27 -0
- package/site/tooling/lib/application-cache.ts +269 -0
- package/site/tooling/lib/asset-hash-cache.ts +116 -0
- package/site/tooling/lib/autotag.ts +99 -0
- package/site/tooling/lib/build-assembly.ts +148 -0
- package/site/tooling/lib/build-workspace.ts +386 -0
- package/site/tooling/lib/cache-selection.ts +70 -0
- package/site/tooling/lib/cloudflare-prewarm.js +40 -0
- package/site/tooling/lib/comments.ts +32 -0
- package/site/tooling/lib/content-cache.ts +244 -0
- package/site/tooling/lib/content-urls.ts +50 -0
- package/site/tooling/lib/course-cache.ts +298 -0
- package/site/tooling/lib/course-files.ts +281 -0
- package/site/tooling/lib/currency.ts +33 -0
- package/site/tooling/lib/deployment-config.ts +222 -0
- package/site/tooling/lib/deployment-manifest.ts +274 -0
- package/site/tooling/lib/deployment-prepare.ts +325 -0
- package/site/tooling/lib/deployment-upload.ts +293 -0
- package/site/tooling/lib/dev-git-state.ts +130 -0
- package/site/tooling/lib/dev-history.ts +86 -0
- package/site/tooling/lib/dev-social-images.ts +38 -0
- package/site/tooling/lib/dev-state.ts +107 -0
- package/site/tooling/lib/edition-builds.ts +184 -0
- package/site/tooling/lib/edition-paths.js +29 -0
- package/site/tooling/lib/empty-tag-route.js +23 -0
- package/site/tooling/lib/file-history.ts +201 -0
- package/site/tooling/lib/file-icons.ts +93 -0
- package/site/tooling/lib/file-policy.ts +38 -0
- package/site/tooling/lib/file-search.js +13 -0
- package/site/tooling/lib/git-revisions.ts +41 -0
- package/site/tooling/lib/history-pool.ts +74 -0
- package/site/tooling/lib/history-worker.ts +22 -0
- package/site/tooling/lib/knit-rmd.R +38 -0
- package/site/tooling/lib/lastmod.ts +201 -0
- package/site/tooling/lib/markdown-files.ts +51 -0
- package/site/tooling/lib/ofm.ts +634 -0
- package/site/tooling/lib/output.ts +60 -0
- package/site/tooling/lib/product-cache.ts +1006 -0
- package/site/tooling/lib/project-config.ts +288 -0
- package/site/tooling/lib/public-assets.ts +38 -0
- package/site/tooling/lib/public-links.ts +56 -0
- package/site/tooling/lib/publishing.ts +156 -0
- package/site/tooling/lib/r-runtime.ts +257 -0
- package/site/tooling/lib/rebuild-queue.ts +25 -0
- package/site/tooling/lib/rmd-preflight.ts +166 -0
- package/site/tooling/lib/rmd-preview-cache.ts +93 -0
- package/site/tooling/lib/rmd-previews.ts +129 -0
- package/site/tooling/lib/runtime-data-modules.ts +30 -0
- package/site/tooling/lib/runtime-packaging.ts +321 -0
- package/site/tooling/lib/search-cache.js +165 -0
- package/site/tooling/lib/search-inputs.js +129 -0
- package/site/tooling/lib/slug.ts +377 -0
- package/site/tooling/lib/social-images.ts +210 -0
- package/site/tooling/lib/text-exports.ts +51 -0
- package/site/tooling/lib/visit-elements.ts +17 -0
- package/site/tooling/live-test.ts +138 -0
- package/site/tooling/pipeline.ts +103 -0
- package/site/tooling/prepare.ts +3 -0
- package/site/tooling/publish.ts +28 -0
- package/site/tooling/r-cache.ts +45 -0
- package/site/tooling/r-render-profile.json +7 -0
- package/site/tooling/search.js +94 -0
- package/site/tooling/verify-deployment.ts +109 -0
- package/site/tooling/vite-content.ts +240 -0
- package/site/tooling/wrangler-paths.js +25 -0
- package/site/tooling/wrangler.ts +19 -0
- package/site/tsconfig.json +16 -0
- package/site/vite.config.ts +40 -0
- package/site/worker/access.ts +57 -0
- package/site/worker/analytics.ts +100 -0
- package/site/worker/auth-routes.ts +78 -0
- package/site/worker/auth.ts +78 -0
- package/site/worker/content-runtime.ts +226 -0
- package/site/worker/framing.ts +4 -0
- package/site/worker/gate.ts +147 -0
- package/site/worker/index.ts +9 -0
- package/site/worker/login.ts +62 -0
- package/site/worker/publication.ts +44 -0
- package/site/worker/runtime.ts +104 -0
- package/site/worker/tsconfig.json +13 -0
- package/site/wrangler.jsonc +37 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { FileBlame } from '$lib/file-history';
|
|
3
|
+
import type { createCodeView } from './code-view';
|
|
4
|
+
let {
|
|
5
|
+
text,
|
|
6
|
+
url,
|
|
7
|
+
filename,
|
|
8
|
+
onready,
|
|
9
|
+
onload,
|
|
10
|
+
blame,
|
|
11
|
+
oncommit = () => {}
|
|
12
|
+
}: {
|
|
13
|
+
text?: string;
|
|
14
|
+
url?: string;
|
|
15
|
+
filename: string;
|
|
16
|
+
onready?: (viewport: HTMLElement) => void;
|
|
17
|
+
onload?: (text: string) => void;
|
|
18
|
+
blame?: FileBlame;
|
|
19
|
+
oncommit?: (id: string) => void;
|
|
20
|
+
} = $props();
|
|
21
|
+
let editor = $state.raw<ReturnType<typeof createCodeView>>();
|
|
22
|
+
let host: HTMLDivElement;
|
|
23
|
+
let failed = $state(false);
|
|
24
|
+
let loading = $state(true);
|
|
25
|
+
let attempt = $state(0);
|
|
26
|
+
$effect(() => {
|
|
27
|
+
editor?.setBlame(blame, oncommit);
|
|
28
|
+
});
|
|
29
|
+
$effect(() => {
|
|
30
|
+
const source = text,
|
|
31
|
+
asset = url,
|
|
32
|
+
name = filename;
|
|
33
|
+
attempt;
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
let created: ReturnType<typeof createCodeView> | undefined;
|
|
36
|
+
failed = false;
|
|
37
|
+
loading = true;
|
|
38
|
+
const read = async () => {
|
|
39
|
+
if (source !== undefined) return source;
|
|
40
|
+
if (!asset) throw new Error('File unavailable');
|
|
41
|
+
const response = await fetch(asset, { signal: controller.signal });
|
|
42
|
+
if (!response.ok) throw new Error('File unavailable');
|
|
43
|
+
return response.text();
|
|
44
|
+
};
|
|
45
|
+
void Promise.all([import('./code-view'), read()])
|
|
46
|
+
.then(([{ createCodeView }, content]) => {
|
|
47
|
+
if (controller.signal.aborted) return;
|
|
48
|
+
onload?.(content);
|
|
49
|
+
created = createCodeView(host, content, name);
|
|
50
|
+
editor = created;
|
|
51
|
+
loading = false;
|
|
52
|
+
created.view.requestMeasure({
|
|
53
|
+
read: () => null,
|
|
54
|
+
write: () => {
|
|
55
|
+
if (!controller.signal.aborted && created) onready?.(created.view.scrollDOM);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
})
|
|
59
|
+
.catch(() => {
|
|
60
|
+
if (!controller.signal.aborted) {
|
|
61
|
+
failed = true;
|
|
62
|
+
loading = false;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return () => {
|
|
66
|
+
controller.abort();
|
|
67
|
+
created?.destroy();
|
|
68
|
+
editor = undefined;
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<div class="relative h-full min-w-0">
|
|
74
|
+
{#if failed}<p class="absolute inset-x-0 top-0 p-3 text-xs text-muted" role="alert">
|
|
75
|
+
Could not load the file. <button class="text-accent" onclick={() => attempt++}>Retry</button>
|
|
76
|
+
</p>
|
|
77
|
+
{:else if loading}<p class="absolute inset-x-0 top-0 p-3 text-xs text-muted" role="status">
|
|
78
|
+
Loading file…
|
|
79
|
+
</p>{/if}
|
|
80
|
+
<div class="file-code h-full min-w-0" bind:this={host}></div>
|
|
81
|
+
</div>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
import { tick } from 'svelte';
|
|
4
|
+
import { fileIndexUrl, fileRoute, fileTree, type CourseFile } from '$lib/files';
|
|
5
|
+
import FileTree from './FileTree.svelte';
|
|
6
|
+
|
|
7
|
+
const { course }: { course: string } = $props();
|
|
8
|
+
let viewport = $state<HTMLDivElement>();
|
|
9
|
+
let files = $state<CourseFile[]>();
|
|
10
|
+
let failed = $state(false);
|
|
11
|
+
let attempt = $state(0);
|
|
12
|
+
const browsing = $derived(page.data.kind === 'file-browser');
|
|
13
|
+
const current = $derived(browsing ? (page.params.file ?? '') : '');
|
|
14
|
+
const focus = $derived(
|
|
15
|
+
browsing
|
|
16
|
+
? current
|
|
17
|
+
: (files?.find((file) => file.note === decodeURI(page.url.pathname))?.path ?? '')
|
|
18
|
+
);
|
|
19
|
+
const tree = $derived(fileTree(files ?? []));
|
|
20
|
+
|
|
21
|
+
$effect(() => {
|
|
22
|
+
current;
|
|
23
|
+
files;
|
|
24
|
+
if (!viewport) return;
|
|
25
|
+
let cancelled = false;
|
|
26
|
+
void tick().then(() => {
|
|
27
|
+
if (cancelled || !viewport) return;
|
|
28
|
+
const active = viewport.querySelector<HTMLElement>('[aria-current="page"]');
|
|
29
|
+
if (!active) return;
|
|
30
|
+
const bounds = viewport.getBoundingClientRect();
|
|
31
|
+
const row = active.getBoundingClientRect();
|
|
32
|
+
if (row.top < bounds.top) viewport.scrollTop += row.top - bounds.top;
|
|
33
|
+
else if (row.bottom > bounds.bottom) viewport.scrollTop += row.bottom - bounds.bottom;
|
|
34
|
+
});
|
|
35
|
+
return () => {
|
|
36
|
+
cancelled = true;
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
$effect(() => {
|
|
40
|
+
attempt;
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
failed = false;
|
|
43
|
+
fetch(fileIndexUrl(course), { signal: controller.signal })
|
|
44
|
+
.then(async (response) => {
|
|
45
|
+
if (!response.ok) throw new Error('Files unavailable');
|
|
46
|
+
const result: CourseFile[] = await response.json();
|
|
47
|
+
if (!controller.signal.aborted) files = result;
|
|
48
|
+
})
|
|
49
|
+
.catch(() => {
|
|
50
|
+
if (!controller.signal.aborted) failed = true;
|
|
51
|
+
});
|
|
52
|
+
return () => controller.abort();
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<section class="flex h-full min-h-0 flex-col" aria-label="Course files" data-pagefind-ignore>
|
|
57
|
+
<div
|
|
58
|
+
bind:this={viewport}
|
|
59
|
+
class="min-h-0 flex-1 overflow-y-auto overscroll-contain [scrollbar-width:thin]"
|
|
60
|
+
>
|
|
61
|
+
{#if failed}
|
|
62
|
+
<p class="px-2 py-2 text-xs text-muted">
|
|
63
|
+
Couldn’t load files. <button class="cursor-pointer text-accent" onclick={() => attempt++}
|
|
64
|
+
>Retry</button
|
|
65
|
+
>
|
|
66
|
+
</p>
|
|
67
|
+
{:else if files}
|
|
68
|
+
<a class="mb-1 block px-2 py-1 text-xs text-muted hover:text-accent" href={fileRoute(course)}
|
|
69
|
+
>Browse directory <span class="float-right">{files.length} files</span></a
|
|
70
|
+
>
|
|
71
|
+
<FileTree nodes={tree} {course} {current} {focus} />
|
|
72
|
+
{:else}<p class="px-2 py-2 text-xs text-muted" role="status">Loading files…</p>{/if}
|
|
73
|
+
</div>
|
|
74
|
+
</section>
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
import { fly } from 'svelte/transition';
|
|
4
|
+
import { prefersReducedMotion } from 'svelte/motion';
|
|
5
|
+
import X from '@lucide/svelte/icons/x';
|
|
6
|
+
import type { FileHistory, FileBlame, FileChange } from '$lib/file-history';
|
|
7
|
+
let {
|
|
8
|
+
url,
|
|
9
|
+
open = $bindable(false),
|
|
10
|
+
blaming = $bindable(false),
|
|
11
|
+
selected = $bindable(''),
|
|
12
|
+
onblame,
|
|
13
|
+
onview
|
|
14
|
+
}: {
|
|
15
|
+
url: string;
|
|
16
|
+
open?: boolean;
|
|
17
|
+
blaming?: boolean;
|
|
18
|
+
selected?: string;
|
|
19
|
+
onblame: (blame: FileBlame | undefined) => void;
|
|
20
|
+
onview: (text: string, commit: FileChange) => void;
|
|
21
|
+
} = $props();
|
|
22
|
+
let history = $state<FileHistory>();
|
|
23
|
+
let blame = $state<FileBlame>();
|
|
24
|
+
let error = $state('');
|
|
25
|
+
let loadingDiff = $state('');
|
|
26
|
+
let pending: Promise<void> | undefined;
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
let selection = 0;
|
|
29
|
+
onDestroy(() => controller.abort());
|
|
30
|
+
async function load() {
|
|
31
|
+
if (pending) return pending;
|
|
32
|
+
pending = (async () => {
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
35
|
+
if (!response.ok) throw new Error('History unavailable');
|
|
36
|
+
history = await response.json();
|
|
37
|
+
} catch {
|
|
38
|
+
if (!controller.signal.aborted) error = 'Could not load history.';
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
return pending;
|
|
42
|
+
}
|
|
43
|
+
$effect(() => {
|
|
44
|
+
if (open || blaming) void load();
|
|
45
|
+
});
|
|
46
|
+
$effect(() => {
|
|
47
|
+
if (!blaming || !history?.blame) {
|
|
48
|
+
onblame(undefined);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (blame) {
|
|
52
|
+
onblame(blame);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const request = new AbortController();
|
|
56
|
+
void fetch(history.blame, { signal: request.signal })
|
|
57
|
+
.then(async (response) => {
|
|
58
|
+
if (!response.ok) throw new Error('Blame unavailable');
|
|
59
|
+
const result: FileBlame = await response.json();
|
|
60
|
+
if (!request.signal.aborted) blame = result;
|
|
61
|
+
})
|
|
62
|
+
.catch(() => {
|
|
63
|
+
if (!request.signal.aborted) {
|
|
64
|
+
error = 'Could not load blame.';
|
|
65
|
+
open = true;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return () => request.abort();
|
|
69
|
+
});
|
|
70
|
+
$effect(() => {
|
|
71
|
+
if (blaming && history && !history.blame) {
|
|
72
|
+
blaming = false;
|
|
73
|
+
open = true;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
async function view(commit: FileChange) {
|
|
77
|
+
selected = commit.id;
|
|
78
|
+
const request = ++selection;
|
|
79
|
+
loadingDiff = '';
|
|
80
|
+
if (!commit.diff) return;
|
|
81
|
+
loadingDiff = commit.id;
|
|
82
|
+
error = '';
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(commit.diff, { signal: controller.signal });
|
|
85
|
+
if (!response.ok) throw new Error('Diff unavailable');
|
|
86
|
+
const text = await response.text();
|
|
87
|
+
if (request === selection && selected === commit.id && !controller.signal.aborted)
|
|
88
|
+
onview(text, commit);
|
|
89
|
+
} catch {
|
|
90
|
+
if (!controller.signal.aborted && request === selection)
|
|
91
|
+
error = 'Could not load this change.';
|
|
92
|
+
} finally {
|
|
93
|
+
if (request === selection) loadingDiff = '';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
{#if open}
|
|
99
|
+
<aside
|
|
100
|
+
aria-label="File history"
|
|
101
|
+
class="absolute inset-y-0 right-0 z-20 flex w-80 max-w-full shrink-0 flex-col border-l border-border bg-bg md:static"
|
|
102
|
+
transition:fly={{ x: 8, duration: prefersReducedMotion.current ? 0 : 140 }}
|
|
103
|
+
>
|
|
104
|
+
<header class="flex items-center justify-between border-b border-border px-3 py-2 text-xs">
|
|
105
|
+
<span>History</span><button
|
|
106
|
+
class="text-muted hover:text-text"
|
|
107
|
+
aria-label="Close history"
|
|
108
|
+
onclick={() => (open = false)}><X size={14} /></button
|
|
109
|
+
>
|
|
110
|
+
</header>
|
|
111
|
+
{#if error}<div class="p-3 text-xs text-muted" role="alert">
|
|
112
|
+
{error}
|
|
113
|
+
<button
|
|
114
|
+
class="text-accent"
|
|
115
|
+
onclick={() => {
|
|
116
|
+
pending = undefined;
|
|
117
|
+
error = '';
|
|
118
|
+
void load();
|
|
119
|
+
}}>Retry</button
|
|
120
|
+
>
|
|
121
|
+
</div>{/if}
|
|
122
|
+
<div class="min-h-0 flex-1 overflow-y-auto">
|
|
123
|
+
{#if history}
|
|
124
|
+
{#if selected && blame?.commits[selected] && !history.commits.some((commit) => commit.id === selected)}
|
|
125
|
+
<div class="border-b border-border p-3 text-xs">
|
|
126
|
+
<p>{blame.commits[selected].subject}</p>
|
|
127
|
+
<p class="mt-1 text-muted">
|
|
128
|
+
{selected.slice(0, 7)} · This commit is from a merged branch. The list below follows first-parent
|
|
129
|
+
history.
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
{/if}
|
|
133
|
+
{#if history.notice}<p class="border-b border-border p-3 text-xs text-muted">
|
|
134
|
+
{history.notice}
|
|
135
|
+
</p>{/if}
|
|
136
|
+
<ol class="m-0 list-none divide-y divide-border p-0">
|
|
137
|
+
{#each history.commits as commit (commit.id)}
|
|
138
|
+
<li>
|
|
139
|
+
<button
|
|
140
|
+
class="w-full px-3 py-2.5 text-left hover:bg-surface"
|
|
141
|
+
class:bg-surface={selected === commit.id}
|
|
142
|
+
aria-pressed={selected === commit.id}
|
|
143
|
+
onclick={() => view(commit)}
|
|
144
|
+
>
|
|
145
|
+
<span class="block break-words text-xs text-text">{commit.subject}</span>
|
|
146
|
+
<span class="mt-1 block text-[0.6875rem] text-muted"
|
|
147
|
+
>{commit.author} · {commit.date.slice(0, 10)} ·
|
|
148
|
+
<span class="font-mono">{commit.id.slice(0, 7)}</span></span
|
|
149
|
+
>
|
|
150
|
+
{#if commit.previousPath}<span
|
|
151
|
+
class="mt-1 block break-all text-[0.6875rem] text-muted"
|
|
152
|
+
>Renamed from {commit.previousPath}</span
|
|
153
|
+
>{/if}
|
|
154
|
+
{#if selected === commit.id && commit.unavailable}<span
|
|
155
|
+
class="mt-1 block text-xs text-muted">{commit.unavailable}</span
|
|
156
|
+
>{/if}
|
|
157
|
+
{#if loadingDiff === commit.id}<span class="block text-xs text-muted" role="status"
|
|
158
|
+
>Loading changes…</span
|
|
159
|
+
>{/if}
|
|
160
|
+
</button>
|
|
161
|
+
</li>
|
|
162
|
+
{:else}<li class="p-3 text-xs text-muted">No committed history.</li>{/each}
|
|
163
|
+
</ol>
|
|
164
|
+
{:else if !error}<p class="p-3 text-xs text-muted" role="status">Loading history…</p>{/if}
|
|
165
|
+
</div>
|
|
166
|
+
{#if history}<footer class="border-t border-border px-3 py-2 text-[0.6875rem] text-muted">
|
|
167
|
+
First-parent history · {history.revision.slice(0, 7)}
|
|
168
|
+
</footer>{/if}
|
|
169
|
+
</aside>
|
|
170
|
+
{/if}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext } from 'svelte';
|
|
3
|
+
const theme = getContext<() => FileIconTheme>('wisconsin-file-icons');
|
|
4
|
+
import { selectFileIcon, type FileIconTheme } from '$lib/file-icons';
|
|
5
|
+
const {
|
|
6
|
+
name,
|
|
7
|
+
folder = false,
|
|
8
|
+
expanded = false
|
|
9
|
+
}: { name: string; folder?: boolean; expanded?: boolean } = $props();
|
|
10
|
+
const icon = $derived(selectFileIcon(theme(), name, folder, expanded));
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<span class="inline-flex size-4 shrink-0" aria-hidden="true" data-file-icon>
|
|
14
|
+
<img
|
|
15
|
+
src={`/_files/icons/${icon.light}.svg`}
|
|
16
|
+
class={icon.light !== icon.dark ? 'size-4 dark:hidden' : 'size-4'}
|
|
17
|
+
width="16"
|
|
18
|
+
height="16"
|
|
19
|
+
alt=""
|
|
20
|
+
/>
|
|
21
|
+
{#if icon.light !== icon.dark}<img
|
|
22
|
+
src={`/_files/icons/${icon.dark}.svg`}
|
|
23
|
+
class="hidden size-4 dark:block"
|
|
24
|
+
width="16"
|
|
25
|
+
height="16"
|
|
26
|
+
alt=""
|
|
27
|
+
/>{/if}
|
|
28
|
+
</span>
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import type { WebR } from 'webr';
|
|
4
|
+
import type { CourseFile } from '$lib/files';
|
|
5
|
+
import { parseRmd, type RmdBlock } from '$lib/rmd';
|
|
6
|
+
let {
|
|
7
|
+
url,
|
|
8
|
+
course,
|
|
9
|
+
path,
|
|
10
|
+
files,
|
|
11
|
+
onload
|
|
12
|
+
}: {
|
|
13
|
+
url: string;
|
|
14
|
+
course: string;
|
|
15
|
+
path: string;
|
|
16
|
+
files: CourseFile[];
|
|
17
|
+
onload: (text: string) => void;
|
|
18
|
+
} = $props();
|
|
19
|
+
type Block = RmdBlock & { output?: string; plots?: string[]; done?: boolean };
|
|
20
|
+
let title = $state('');
|
|
21
|
+
let blocks = $state<Block[]>([]);
|
|
22
|
+
let status = $state('Loading worksheet…');
|
|
23
|
+
let busy = $state(false);
|
|
24
|
+
let loaded = $state(false);
|
|
25
|
+
let runtime: WebR | undefined;
|
|
26
|
+
let runtimeReady = false;
|
|
27
|
+
let generation = 0;
|
|
28
|
+
const abort = new AbortController();
|
|
29
|
+
const button =
|
|
30
|
+
'rounded border border-border px-3 py-1.5 text-xs hover:bg-surface disabled:opacity-50';
|
|
31
|
+
|
|
32
|
+
onMount(() => {
|
|
33
|
+
void (async () => {
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(url, { signal: abort.signal });
|
|
36
|
+
if (!response.ok) throw new Error(`Worksheet unavailable (${response.status})`);
|
|
37
|
+
const source = await response.text();
|
|
38
|
+
const parsed = await parseRmd(source, course, path, files);
|
|
39
|
+
if (abort.signal.aborted) return;
|
|
40
|
+
onload(source);
|
|
41
|
+
title = parsed.title;
|
|
42
|
+
blocks = parsed.blocks;
|
|
43
|
+
loaded = true;
|
|
44
|
+
status =
|
|
45
|
+
'Run chunks in order, or run the whole worksheet. Edits last until you leave this view.';
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!abort.signal.aborted) status = String(error);
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
return () => {
|
|
51
|
+
abort.abort();
|
|
52
|
+
generation++;
|
|
53
|
+
runtime?.close();
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function reset() {
|
|
58
|
+
generation++;
|
|
59
|
+
runtime?.close();
|
|
60
|
+
runtime = undefined;
|
|
61
|
+
runtimeReady = false;
|
|
62
|
+
busy = false;
|
|
63
|
+
for (const block of blocks) {
|
|
64
|
+
block.output = '';
|
|
65
|
+
block.plots = [];
|
|
66
|
+
block.done = false;
|
|
67
|
+
}
|
|
68
|
+
status = 'R session reset. Run the worksheet to start again.';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function start(ticket: number) {
|
|
72
|
+
if (runtime) return runtime;
|
|
73
|
+
status = 'Loading R… The first run downloads the runtime.';
|
|
74
|
+
const { WebR, ChannelType } = await import('webr');
|
|
75
|
+
if (ticket !== generation) throw new Error('Session reset');
|
|
76
|
+
const r = new WebR({
|
|
77
|
+
baseUrl: 'https://webr.r-wasm.org/v0.5.8/',
|
|
78
|
+
channelType: ChannelType.PostMessage
|
|
79
|
+
});
|
|
80
|
+
runtime = r;
|
|
81
|
+
await r.init();
|
|
82
|
+
await r.evalRVoid('options(max.print=1000)');
|
|
83
|
+
if (blocks.some((b) => b.kind === 'r' && /\bknitr::/.test(b.code))) {
|
|
84
|
+
status = 'Loading knitr…';
|
|
85
|
+
await r.installPackages(['knitr']);
|
|
86
|
+
}
|
|
87
|
+
const directory = path.split('/').slice(0, -1).join('/');
|
|
88
|
+
for (const file of files.filter(
|
|
89
|
+
(f) =>
|
|
90
|
+
!f.locked &&
|
|
91
|
+
f.download &&
|
|
92
|
+
/\.(csv|tsv|txt|rds|rda|rdata)$/i.test(f.path) &&
|
|
93
|
+
f.path.split('/').slice(0, -1).join('/') === directory
|
|
94
|
+
)) {
|
|
95
|
+
const response = await fetch(file.download!, { signal: abort.signal });
|
|
96
|
+
if (!response.ok) throw new Error(`Could not load ${file.path} (${response.status})`);
|
|
97
|
+
await r.FS.writeFile(
|
|
98
|
+
`/home/web_user/${file.path.split('/').at(-1)}`,
|
|
99
|
+
new Uint8Array(await response.arrayBuffer())
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
runtimeReady = true;
|
|
103
|
+
return r;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function run(selected?: Block) {
|
|
107
|
+
if (busy) return;
|
|
108
|
+
busy = true;
|
|
109
|
+
const ticket = generation;
|
|
110
|
+
try {
|
|
111
|
+
const r = await start(ticket);
|
|
112
|
+
if (ticket !== generation) return;
|
|
113
|
+
const shelter = await new r.Shelter();
|
|
114
|
+
try {
|
|
115
|
+
for (const block of selected ? [selected] : blocks) {
|
|
116
|
+
if (ticket !== generation) return;
|
|
117
|
+
if (block.kind !== 'r' || (!selected && !block.evaluate)) continue;
|
|
118
|
+
status = `Running ${block.label}…`;
|
|
119
|
+
block.done = false;
|
|
120
|
+
block.output = '';
|
|
121
|
+
block.plots = [];
|
|
122
|
+
const result = await shelter.captureR(block.code, {
|
|
123
|
+
withAutoprint: true,
|
|
124
|
+
captureGraphics: true
|
|
125
|
+
});
|
|
126
|
+
try {
|
|
127
|
+
if (ticket !== generation) return;
|
|
128
|
+
block.output = result.output
|
|
129
|
+
.map((o) =>
|
|
130
|
+
typeof o.data === 'string' ? o.data : (o.data?.message ?? String(o.data))
|
|
131
|
+
)
|
|
132
|
+
.join('\n');
|
|
133
|
+
block.plots = result.images.map((image) => {
|
|
134
|
+
const canvas = document.createElement('canvas');
|
|
135
|
+
canvas.width = image.width;
|
|
136
|
+
canvas.height = image.height;
|
|
137
|
+
canvas.getContext('2d')!.drawImage(image, 0, 0);
|
|
138
|
+
return canvas.toDataURL();
|
|
139
|
+
});
|
|
140
|
+
block.done = true;
|
|
141
|
+
if (result.output.some((o) => o.type === 'error'))
|
|
142
|
+
throw new Error(`${block.label} failed. Fix the code and run it again.`);
|
|
143
|
+
} finally {
|
|
144
|
+
result.images.forEach((image) => image.close());
|
|
145
|
+
}
|
|
146
|
+
await shelter.purge();
|
|
147
|
+
}
|
|
148
|
+
status = 'Finished. R runs locally in your browser.';
|
|
149
|
+
} finally {
|
|
150
|
+
if (ticket === generation) await shelter.purge();
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (ticket === generation) {
|
|
154
|
+
status = String(error);
|
|
155
|
+
if (!runtimeReady) {
|
|
156
|
+
runtime?.close();
|
|
157
|
+
runtime = undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} finally {
|
|
161
|
+
if (ticket === generation) busy = false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
</script>
|
|
165
|
+
|
|
166
|
+
<div class="h-full overflow-auto p-4 sm:p-6">
|
|
167
|
+
<div class="mx-auto max-w-3xl">
|
|
168
|
+
<div class="mb-5 flex flex-wrap gap-2">
|
|
169
|
+
<button class={button} disabled={!loaded || busy} onclick={() => run()}>Run all</button>
|
|
170
|
+
<button class={button} disabled={!loaded} onclick={reset}
|
|
171
|
+
>{busy ? 'Stop and reset' : 'Reset session'}</button
|
|
172
|
+
>
|
|
173
|
+
</div>
|
|
174
|
+
<p class="mb-6 text-sm text-muted" role="status">{status}</p>
|
|
175
|
+
<h1 class="mb-6 text-2xl font-semibold">{title}</h1>
|
|
176
|
+
{#each blocks as block, index}
|
|
177
|
+
{#if block.kind === 'markdown'}
|
|
178
|
+
<div class="prose max-w-none dark:prose-invert">{@html block.html}</div>
|
|
179
|
+
{:else}
|
|
180
|
+
<section class="my-5 min-w-0 rounded border border-border" aria-label={block.label}>
|
|
181
|
+
<details open={block.show}>
|
|
182
|
+
<summary class="cursor-pointer px-3 py-2 text-xs text-muted"
|
|
183
|
+
>{block.label}{!block.evaluate ? ' · skipped by Run all' : ''}</summary
|
|
184
|
+
>
|
|
185
|
+
<textarea
|
|
186
|
+
class="block w-full resize-y bg-surface p-3 font-mono text-sm"
|
|
187
|
+
aria-label={`R code ${index}`}
|
|
188
|
+
bind:value={block.code}
|
|
189
|
+
rows={Math.min(18, Math.max(3, block.code.split('\n').length))}
|
|
190
|
+
spellcheck="false"
|
|
191
|
+
disabled={busy}
|
|
192
|
+
></textarea>
|
|
193
|
+
</details>
|
|
194
|
+
<div class="flex items-center gap-3 px-3 py-2">
|
|
195
|
+
<button class={button} disabled={busy} onclick={() => run(block)}>Run chunk</button>
|
|
196
|
+
{#if block.done}<span class="text-xs text-muted">Done</span>{/if}
|
|
197
|
+
</div>
|
|
198
|
+
{#if block.output}<pre
|
|
199
|
+
class="max-h-96 overflow-auto border-t border-border p-3 text-xs">{block.output}</pre>{/if}
|
|
200
|
+
{#each block.plots ?? [] as plot}<img
|
|
201
|
+
class="w-full bg-white"
|
|
202
|
+
src={plot}
|
|
203
|
+
alt={`R plot from ${block.label}`}
|
|
204
|
+
/>{/each}
|
|
205
|
+
</section>
|
|
206
|
+
{/if}
|
|
207
|
+
{/each}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|