@twango/twiki 0.0.0-stage → 0.1.0-rc.0
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,146 @@
|
|
|
1
|
+
import type { Attachment } from 'svelte/attachments';
|
|
2
|
+
import { mermaidDiagrams } from '$lib/components/embeds/mermaid';
|
|
3
|
+
import { imagePreviews } from './image-preview';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Client-side enhancements applied to rendered markdown content. Each is an
|
|
7
|
+
* attachment that runs after the article element is in the DOM and re-runs when
|
|
8
|
+
* its keyed dependency (the current route key) changes.
|
|
9
|
+
*
|
|
10
|
+
* Ported from cca minus filePreviews/kotlinPlayground (dropped for wisconsin).
|
|
11
|
+
* `enhanceArticle` below is the single entry point composing all of them
|
|
12
|
+
* (copy buttons, callout folding, lazy mermaid hydration). External-link
|
|
13
|
+
* icons need no JS — they are baked in at build time (build-content.ts
|
|
14
|
+
* CrawlLinks port) and styled by layout.css `.external-icon`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export { mermaidDiagrams };
|
|
18
|
+
|
|
19
|
+
/** Single entry point: applies every content enhancement to the article. */
|
|
20
|
+
export function enhanceArticle(dep: unknown): Attachment<HTMLElement> {
|
|
21
|
+
return (node) => {
|
|
22
|
+
const cleanups = [
|
|
23
|
+
copyButtons(dep)(node),
|
|
24
|
+
headingAnchors(dep)(node),
|
|
25
|
+
calloutFold(dep)(node),
|
|
26
|
+
mermaidDiagrams(dep)(node),
|
|
27
|
+
imagePreviews(dep)(node)
|
|
28
|
+
];
|
|
29
|
+
return () => {
|
|
30
|
+
for (const cleanup of cleanups) cleanup?.();
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const LINK_ICON =
|
|
36
|
+
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>';
|
|
37
|
+
|
|
38
|
+
/** Restore Quartz's visible deep-link affordance without changing heading ids. */
|
|
39
|
+
export function headingAnchors(_dep: unknown): Attachment<HTMLElement> {
|
|
40
|
+
return (node) => {
|
|
41
|
+
const anchors = Array.from(
|
|
42
|
+
node.querySelectorAll<HTMLElement>('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]')
|
|
43
|
+
).map((heading) => {
|
|
44
|
+
const anchor = document.createElement('a');
|
|
45
|
+
anchor.className = 'heading-anchor';
|
|
46
|
+
anchor.href = `#${encodeURIComponent(heading.id)}`;
|
|
47
|
+
anchor.setAttribute('aria-label', `Link to ${heading.textContent?.trim() || 'heading'}`);
|
|
48
|
+
anchor.title = 'Link to this heading';
|
|
49
|
+
anchor.innerHTML = LINK_ICON;
|
|
50
|
+
heading.appendChild(anchor);
|
|
51
|
+
return anchor;
|
|
52
|
+
});
|
|
53
|
+
return () => anchors.forEach((anchor) => anchor.remove());
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const COPY_ICON =
|
|
58
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>';
|
|
59
|
+
const CHECK_ICON =
|
|
60
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Adds a copy-to-clipboard button to every Shiki code block. Keyed on `dep` so
|
|
64
|
+
* it re-runs when navigating between docs.
|
|
65
|
+
*/
|
|
66
|
+
export function copyButtons(_dep: unknown): Attachment<HTMLElement> {
|
|
67
|
+
return (node) => {
|
|
68
|
+
const blocks = Array.from(node.querySelectorAll('pre.shiki'));
|
|
69
|
+
const cleanups = blocks.map((pre) => {
|
|
70
|
+
const wrapper = document.createElement('div');
|
|
71
|
+
wrapper.className = 'code-block';
|
|
72
|
+
pre.replaceWith(wrapper);
|
|
73
|
+
wrapper.appendChild(pre);
|
|
74
|
+
|
|
75
|
+
const button = document.createElement('button');
|
|
76
|
+
button.type = 'button';
|
|
77
|
+
button.className = 'copy-button';
|
|
78
|
+
button.setAttribute('aria-label', 'Copy code');
|
|
79
|
+
button.innerHTML = COPY_ICON;
|
|
80
|
+
wrapper.appendChild(button);
|
|
81
|
+
|
|
82
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
83
|
+
const onClick = async () => {
|
|
84
|
+
try {
|
|
85
|
+
await navigator.clipboard.writeText(pre.querySelector('code')?.textContent ?? '');
|
|
86
|
+
button.classList.add('copied');
|
|
87
|
+
button.innerHTML = CHECK_ICON;
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
timer = setTimeout(() => {
|
|
90
|
+
button.classList.remove('copied');
|
|
91
|
+
button.innerHTML = COPY_ICON;
|
|
92
|
+
}, 1600);
|
|
93
|
+
} catch {
|
|
94
|
+
button.setAttribute('aria-label', 'Copy failed');
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
button.addEventListener('click', onClick);
|
|
98
|
+
|
|
99
|
+
return () => {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
button.removeEventListener('click', onClick);
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return () => cleanups.forEach((fn) => fn());
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Collapsible Obsidian callouts ([!type]- / [!type]+). The prebuild emits the
|
|
111
|
+
* Quartz fork's structure (blockquote.callout.is-collapsible[.is-collapsed] >
|
|
112
|
+
* .callout-title + .callout-content); collapse/expand is pure CSS
|
|
113
|
+
* (grid-template-rows keyed on .is-collapsed) so this only toggles the class.
|
|
114
|
+
* Ported from quartz/components/scripts/callout.inline.ts.
|
|
115
|
+
*/
|
|
116
|
+
export function calloutFold(_dep: unknown): Attachment<HTMLElement> {
|
|
117
|
+
return (node) => {
|
|
118
|
+
const titles = Array.from(
|
|
119
|
+
node.querySelectorAll<HTMLElement>('.callout.is-collapsible > .callout-title')
|
|
120
|
+
);
|
|
121
|
+
const cleanups = titles.map((title) => {
|
|
122
|
+
const callout = title.parentElement!;
|
|
123
|
+
const onClick = () => {
|
|
124
|
+
const collapsed = callout.classList.toggle('is-collapsed');
|
|
125
|
+
title.setAttribute('aria-expanded', String(!collapsed));
|
|
126
|
+
};
|
|
127
|
+
title.setAttribute('role', 'button');
|
|
128
|
+
title.setAttribute('tabindex', '0');
|
|
129
|
+
title.setAttribute('aria-expanded', String(!callout.classList.contains('is-collapsed')));
|
|
130
|
+
const onKey = (e: KeyboardEvent) => {
|
|
131
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
onClick();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
title.addEventListener('click', onClick);
|
|
137
|
+
title.addEventListener('keydown', onKey);
|
|
138
|
+
return () => {
|
|
139
|
+
title.removeEventListener('click', onClick);
|
|
140
|
+
title.removeEventListener('keydown', onKey);
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return () => cleanups.forEach((fn) => fn());
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { TocEntry } from '$lib/types';
|
|
2
|
+
|
|
3
|
+
export function createHeadingTracker(getItems: () => TocEntry[]) {
|
|
4
|
+
let active = $state('');
|
|
5
|
+
|
|
6
|
+
$effect(() => {
|
|
7
|
+
const headings = getItems()
|
|
8
|
+
.map((item) => document.getElementById(item.id))
|
|
9
|
+
.filter((element) => element !== null);
|
|
10
|
+
let frame = 0;
|
|
11
|
+
function update() {
|
|
12
|
+
frame = 0;
|
|
13
|
+
let current = headings[0]?.id ?? '';
|
|
14
|
+
for (const heading of headings) {
|
|
15
|
+
if (heading.getBoundingClientRect().top > 120) break;
|
|
16
|
+
current = heading.id;
|
|
17
|
+
}
|
|
18
|
+
active = current;
|
|
19
|
+
}
|
|
20
|
+
function schedule() {
|
|
21
|
+
if (!frame) frame = requestAnimationFrame(update);
|
|
22
|
+
}
|
|
23
|
+
update();
|
|
24
|
+
if (!headings.length) return;
|
|
25
|
+
window.addEventListener('scroll', schedule, { passive: true });
|
|
26
|
+
window.addEventListener('resize', schedule);
|
|
27
|
+
return () => {
|
|
28
|
+
cancelAnimationFrame(frame);
|
|
29
|
+
window.removeEventListener('scroll', schedule);
|
|
30
|
+
window.removeEventListener('resize', schedule);
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
get active() {
|
|
36
|
+
return active;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Attachment } from 'svelte/attachments';
|
|
2
|
+
import type PhotoSwipe from 'photoswipe';
|
|
3
|
+
import 'photoswipe/style.css';
|
|
4
|
+
|
|
5
|
+
/** Add previews to note images without taking over existing links or controls. */
|
|
6
|
+
export function imagePreviews(_dep: unknown): Attachment<HTMLElement> {
|
|
7
|
+
return (node) => {
|
|
8
|
+
let disposed = false;
|
|
9
|
+
let opening = false;
|
|
10
|
+
let viewer: PhotoSwipe | undefined;
|
|
11
|
+
const cleanups: (() => void)[] = [];
|
|
12
|
+
|
|
13
|
+
for (const image of node.querySelectorAll<HTMLImageElement>('img')) {
|
|
14
|
+
if (image.closest('a, button, [role="button"]')) continue;
|
|
15
|
+
const content = image.closest('picture') ?? image;
|
|
16
|
+
const link = document.createElement('a');
|
|
17
|
+
link.href = image.currentSrc || image.src;
|
|
18
|
+
link.className = 'image-preview';
|
|
19
|
+
link.style.cursor = 'zoom-in';
|
|
20
|
+
link.setAttribute('aria-label', `Enlarge image${image.alt ? `: ${image.alt}` : ''}`);
|
|
21
|
+
link.setAttribute('aria-haspopup', 'dialog');
|
|
22
|
+
link.setAttribute('data-sveltekit-reload', '');
|
|
23
|
+
content.replaceWith(link);
|
|
24
|
+
link.append(content);
|
|
25
|
+
|
|
26
|
+
const open = async (event: MouseEvent) => {
|
|
27
|
+
if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)
|
|
28
|
+
return;
|
|
29
|
+
// Broken or still-loading images retain a normal link to the original.
|
|
30
|
+
if (!image.naturalWidth || !image.naturalHeight) return;
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
if (opening || viewer) return;
|
|
33
|
+
opening = true;
|
|
34
|
+
link.focus({ preventScroll: true });
|
|
35
|
+
try {
|
|
36
|
+
const { default: PhotoSwipe } = await import('photoswipe');
|
|
37
|
+
if (disposed) return;
|
|
38
|
+
const duration = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 250;
|
|
39
|
+
viewer = new PhotoSwipe({
|
|
40
|
+
dataSource: [
|
|
41
|
+
{
|
|
42
|
+
src: image.currentSrc || image.src,
|
|
43
|
+
width: image.naturalWidth,
|
|
44
|
+
height: image.naturalHeight,
|
|
45
|
+
alt: image.alt,
|
|
46
|
+
element: link,
|
|
47
|
+
msrc: image.currentSrc || image.src
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
showHideAnimationType: 'zoom',
|
|
51
|
+
showAnimationDuration: duration,
|
|
52
|
+
hideAnimationDuration: duration,
|
|
53
|
+
zoomAnimationDuration: duration,
|
|
54
|
+
secondaryZoomLevel: 2,
|
|
55
|
+
maxZoomLevel: 4,
|
|
56
|
+
bgOpacity: 0.95
|
|
57
|
+
});
|
|
58
|
+
// Replay early controls after opening, with close taking priority.
|
|
59
|
+
const instance = viewer;
|
|
60
|
+
let ready = false;
|
|
61
|
+
let pending: 'close' | 'zoom' | undefined;
|
|
62
|
+
const close = instance.close.bind(instance);
|
|
63
|
+
const zoom = instance.toggleZoom.bind(instance);
|
|
64
|
+
instance.close = () => {
|
|
65
|
+
if (ready) close();
|
|
66
|
+
else pending = 'close';
|
|
67
|
+
};
|
|
68
|
+
instance.toggleZoom = () => {
|
|
69
|
+
if (ready) zoom();
|
|
70
|
+
else if (pending !== 'close') pending = pending === 'zoom' ? undefined : 'zoom';
|
|
71
|
+
};
|
|
72
|
+
const earlyEscape = (event: KeyboardEvent) => {
|
|
73
|
+
if (!ready && event.key === 'Escape') {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
event.stopImmediatePropagation();
|
|
76
|
+
pending = 'close';
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
document.addEventListener('keydown', earlyEscape, true);
|
|
80
|
+
instance.on('openingAnimationEnd', () => {
|
|
81
|
+
ready = true;
|
|
82
|
+
document.removeEventListener('keydown', earlyEscape, true);
|
|
83
|
+
queueMicrotask(() => {
|
|
84
|
+
if (disposed || viewer !== instance) return;
|
|
85
|
+
if (pending === 'close') close();
|
|
86
|
+
else if (pending === 'zoom') zoom();
|
|
87
|
+
pending = undefined;
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
viewer.on('destroy', () => {
|
|
91
|
+
document.removeEventListener('keydown', earlyEscape, true);
|
|
92
|
+
viewer = undefined;
|
|
93
|
+
});
|
|
94
|
+
viewer.init();
|
|
95
|
+
} catch {
|
|
96
|
+
if (!disposed) window.location.assign(link.href);
|
|
97
|
+
} finally {
|
|
98
|
+
opening = false;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
link.addEventListener('click', open);
|
|
102
|
+
cleanups.push(() => {
|
|
103
|
+
link.removeEventListener('click', open);
|
|
104
|
+
link.replaceWith(content);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
disposed = true;
|
|
110
|
+
viewer?.destroy();
|
|
111
|
+
cleanups.forEach((cleanup) => cleanup());
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { browser } from '$app/environment';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ReaderMode (fork-custom Quartz feature, rebuilt Svelte-native): a global
|
|
5
|
+
* runes store backing a header/sidebar toggle. When enabled, DocShell hides
|
|
6
|
+
* the Explorer sidebar and TOC rails so the article gets a distraction-free
|
|
7
|
+
* single column. Improves on Quartz (which reset on every full page load) by
|
|
8
|
+
* persisting to localStorage.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const STORAGE_KEY = 'readerMode';
|
|
12
|
+
|
|
13
|
+
function initial(): boolean {
|
|
14
|
+
if (!browser) return false;
|
|
15
|
+
try {
|
|
16
|
+
return localStorage.getItem(STORAGE_KEY) === 'true';
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class ReaderModeState {
|
|
23
|
+
enabled = $state(initial());
|
|
24
|
+
|
|
25
|
+
toggle(): void {
|
|
26
|
+
this.set(!this.enabled);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set(value: boolean): void {
|
|
30
|
+
this.enabled = value;
|
|
31
|
+
if (!browser) return;
|
|
32
|
+
try {
|
|
33
|
+
localStorage.setItem(STORAGE_KEY, String(value));
|
|
34
|
+
} catch {
|
|
35
|
+
// storage unavailable (private mode) — session-only toggle
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const readerMode = new ReaderModeState();
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import type { Attachment } from 'svelte/attachments';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lazy Mermaid renderer with the interaction contract from Quartz: source
|
|
5
|
+
* copy, fullscreen expansion, pointer pan, wheel/button zoom and reset.
|
|
6
|
+
* Markdown in this site is a trusted, private corpus, so `loose` security is
|
|
7
|
+
* retained for Quartz-compatible HTML labels.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type Mermaid = typeof import('mermaid').default;
|
|
11
|
+
|
|
12
|
+
let mermaidPromise: Promise<Mermaid> | null = null;
|
|
13
|
+
function loadMermaid(): Promise<Mermaid> {
|
|
14
|
+
mermaidPromise ??= import('mermaid').then((module) => module.default);
|
|
15
|
+
return mermaidPromise;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isDark(): boolean {
|
|
19
|
+
return document.documentElement.classList.contains('dark');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function mermaidTheme() {
|
|
23
|
+
const style = getComputedStyle(document.documentElement);
|
|
24
|
+
const value = (name: string, fallback: string) => style.getPropertyValue(name).trim() || fallback;
|
|
25
|
+
return {
|
|
26
|
+
fontFamily: value('--font-mono', 'monospace'),
|
|
27
|
+
primaryColor: value('--color-bg', '#ffffff'),
|
|
28
|
+
primaryTextColor: value('--color-text', '#1a1916'),
|
|
29
|
+
primaryBorderColor: value('--color-accent', '#c2413b'),
|
|
30
|
+
lineColor: value('--color-muted', '#78716c'),
|
|
31
|
+
secondaryColor: value('--color-surface', '#f5f3ef'),
|
|
32
|
+
tertiaryColor: value('--color-subtle', '#e7e2d9'),
|
|
33
|
+
clusterBkg: value('--color-bg', '#ffffff'),
|
|
34
|
+
edgeLabelBackground: value('--color-surface', '#f5f3ef')
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const COPY_ICON =
|
|
39
|
+
'<svg viewBox="0 0 24 24" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>';
|
|
40
|
+
const CHECK_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
|
|
41
|
+
const EXPAND_ICON =
|
|
42
|
+
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></svg>';
|
|
43
|
+
|
|
44
|
+
function makeButton(label: string, content: string, className = ''): HTMLButtonElement {
|
|
45
|
+
const button = document.createElement('button');
|
|
46
|
+
button.type = 'button';
|
|
47
|
+
button.className = `mermaid-control-button ${className}`.trim();
|
|
48
|
+
button.setAttribute('aria-label', label);
|
|
49
|
+
button.title = label;
|
|
50
|
+
button.innerHTML = content;
|
|
51
|
+
return button;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let idCounter = 0;
|
|
55
|
+
|
|
56
|
+
const printPreparers = new WeakMap<HTMLElement, () => Promise<void>>();
|
|
57
|
+
|
|
58
|
+
export async function prepareDiagramsForPrint(article: HTMLElement): Promise<void> {
|
|
59
|
+
await printPreparers.get(article)?.();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface Block {
|
|
63
|
+
pre: HTMLElement;
|
|
64
|
+
source: string;
|
|
65
|
+
container: HTMLDivElement;
|
|
66
|
+
content: HTMLDivElement;
|
|
67
|
+
copyButton: HTMLButtonElement;
|
|
68
|
+
expandButton: HTMLButtonElement;
|
|
69
|
+
rendered: boolean;
|
|
70
|
+
generation: number;
|
|
71
|
+
copyTimer?: ReturnType<typeof setTimeout>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function mermaidDiagrams(_dep: unknown): Attachment<HTMLElement> {
|
|
75
|
+
return (node) => {
|
|
76
|
+
const codes = Array.from(node.querySelectorAll<HTMLElement>('pre > code.mermaid'));
|
|
77
|
+
if (codes.length === 0) return;
|
|
78
|
+
|
|
79
|
+
let destroyed = false;
|
|
80
|
+
let closeExpanded: (() => void) | undefined;
|
|
81
|
+
|
|
82
|
+
const blocks: Block[] = codes.map((code) => {
|
|
83
|
+
const pre = code.parentElement as HTMLElement;
|
|
84
|
+
let source = code.textContent ?? '';
|
|
85
|
+
const clipboardSource = code.getAttribute('data-clipboard');
|
|
86
|
+
if (clipboardSource) {
|
|
87
|
+
try {
|
|
88
|
+
const parsed: unknown = JSON.parse(clipboardSource);
|
|
89
|
+
if (typeof parsed === 'string') source = parsed;
|
|
90
|
+
} catch {
|
|
91
|
+
// Keep textContent as the no-JS/error fallback.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const container = document.createElement('div');
|
|
96
|
+
container.className = 'mermaid-diagram';
|
|
97
|
+
container.style.display = 'none';
|
|
98
|
+
const content = document.createElement('div');
|
|
99
|
+
content.className = 'mermaid-inline-content';
|
|
100
|
+
const toolbar = document.createElement('div');
|
|
101
|
+
toolbar.className = 'mermaid-toolbar';
|
|
102
|
+
const expandButton = makeButton('Expand diagram', EXPAND_ICON, 'mermaid-expand-button');
|
|
103
|
+
const copyButton = makeButton('Copy diagram source', COPY_ICON, 'mermaid-copy-button');
|
|
104
|
+
toolbar.append(expandButton, copyButton);
|
|
105
|
+
container.append(content, toolbar);
|
|
106
|
+
pre.insertAdjacentElement('afterend', container);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
pre,
|
|
110
|
+
source,
|
|
111
|
+
container,
|
|
112
|
+
content,
|
|
113
|
+
copyButton,
|
|
114
|
+
expandButton,
|
|
115
|
+
rendered: false,
|
|
116
|
+
generation: 0
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
function closeModal(): void {
|
|
121
|
+
closeExpanded?.();
|
|
122
|
+
closeExpanded = undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function openModal(block: Block): void {
|
|
126
|
+
const sourceSvg = block.content.querySelector('svg');
|
|
127
|
+
if (!sourceSvg) return;
|
|
128
|
+
closeModal();
|
|
129
|
+
|
|
130
|
+
const modal = document.createElement('div');
|
|
131
|
+
modal.className = 'mermaid-modal';
|
|
132
|
+
modal.setAttribute('role', 'dialog');
|
|
133
|
+
modal.setAttribute('aria-modal', 'true');
|
|
134
|
+
modal.setAttribute('aria-label', 'Expanded diagram');
|
|
135
|
+
|
|
136
|
+
const panel = document.createElement('div');
|
|
137
|
+
panel.className = 'mermaid-modal-panel';
|
|
138
|
+
const controls = document.createElement('div');
|
|
139
|
+
controls.className = 'mermaid-modal-controls';
|
|
140
|
+
const zoomOut = makeButton('Zoom out', '−');
|
|
141
|
+
const reset = makeButton('Reset zoom', 'Reset');
|
|
142
|
+
reset.classList.add('mermaid-reset-button');
|
|
143
|
+
const zoomIn = makeButton('Zoom in', '+');
|
|
144
|
+
const close = makeButton('Close expanded diagram', '×');
|
|
145
|
+
controls.append(zoomOut, reset, zoomIn, close);
|
|
146
|
+
|
|
147
|
+
const viewport = document.createElement('div');
|
|
148
|
+
viewport.className = 'mermaid-modal-viewport';
|
|
149
|
+
const modalContent = document.createElement('div');
|
|
150
|
+
modalContent.className = 'mermaid-modal-content';
|
|
151
|
+
modalContent.append(sourceSvg.cloneNode(true));
|
|
152
|
+
viewport.appendChild(modalContent);
|
|
153
|
+
panel.append(controls, viewport);
|
|
154
|
+
modal.appendChild(panel);
|
|
155
|
+
document.body.appendChild(modal);
|
|
156
|
+
document.body.classList.add('mermaid-modal-open');
|
|
157
|
+
|
|
158
|
+
let scale = 1;
|
|
159
|
+
let panX = 0;
|
|
160
|
+
let panY = 0;
|
|
161
|
+
let dragging = false;
|
|
162
|
+
let pointerId = -1;
|
|
163
|
+
let startX = 0;
|
|
164
|
+
let startY = 0;
|
|
165
|
+
const applyTransform = () => {
|
|
166
|
+
modalContent.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
|
|
167
|
+
};
|
|
168
|
+
const changeZoom = (delta: number) => {
|
|
169
|
+
scale = Math.min(3, Math.max(0.5, scale + delta));
|
|
170
|
+
applyTransform();
|
|
171
|
+
};
|
|
172
|
+
const resetTransform = () => {
|
|
173
|
+
scale = 1;
|
|
174
|
+
panX = 0;
|
|
175
|
+
panY = 0;
|
|
176
|
+
applyTransform();
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
180
|
+
if (event.button !== 0) return;
|
|
181
|
+
dragging = true;
|
|
182
|
+
pointerId = event.pointerId;
|
|
183
|
+
startX = event.clientX - panX;
|
|
184
|
+
startY = event.clientY - panY;
|
|
185
|
+
viewport.setPointerCapture(pointerId);
|
|
186
|
+
viewport.classList.add('is-dragging');
|
|
187
|
+
};
|
|
188
|
+
const onPointerMove = (event: PointerEvent) => {
|
|
189
|
+
if (!dragging || event.pointerId !== pointerId) return;
|
|
190
|
+
panX = event.clientX - startX;
|
|
191
|
+
panY = event.clientY - startY;
|
|
192
|
+
applyTransform();
|
|
193
|
+
};
|
|
194
|
+
const onPointerUp = (event: PointerEvent) => {
|
|
195
|
+
if (event.pointerId !== pointerId) return;
|
|
196
|
+
dragging = false;
|
|
197
|
+
viewport.classList.remove('is-dragging');
|
|
198
|
+
if (viewport.hasPointerCapture(pointerId)) viewport.releasePointerCapture(pointerId);
|
|
199
|
+
pointerId = -1;
|
|
200
|
+
};
|
|
201
|
+
const onWheel = (event: WheelEvent) => {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
changeZoom(event.deltaY < 0 ? 0.1 : -0.1);
|
|
204
|
+
};
|
|
205
|
+
const onBackdrop = (event: MouseEvent) => {
|
|
206
|
+
if (event.target === modal) closeModal();
|
|
207
|
+
};
|
|
208
|
+
const onKeydown = (event: KeyboardEvent) => {
|
|
209
|
+
if (event.key === 'Escape') closeModal();
|
|
210
|
+
};
|
|
211
|
+
const onZoomOut = () => changeZoom(-0.1);
|
|
212
|
+
const onZoomIn = () => changeZoom(0.1);
|
|
213
|
+
|
|
214
|
+
viewport.addEventListener('pointerdown', onPointerDown);
|
|
215
|
+
viewport.addEventListener('pointermove', onPointerMove);
|
|
216
|
+
viewport.addEventListener('pointerup', onPointerUp);
|
|
217
|
+
viewport.addEventListener('pointercancel', onPointerUp);
|
|
218
|
+
viewport.addEventListener('wheel', onWheel, { passive: false });
|
|
219
|
+
modal.addEventListener('click', onBackdrop);
|
|
220
|
+
document.addEventListener('keydown', onKeydown);
|
|
221
|
+
zoomOut.addEventListener('click', onZoomOut);
|
|
222
|
+
zoomIn.addEventListener('click', onZoomIn);
|
|
223
|
+
reset.addEventListener('click', resetTransform);
|
|
224
|
+
|
|
225
|
+
const finish = () => {
|
|
226
|
+
viewport.removeEventListener('pointerdown', onPointerDown);
|
|
227
|
+
viewport.removeEventListener('pointermove', onPointerMove);
|
|
228
|
+
viewport.removeEventListener('pointerup', onPointerUp);
|
|
229
|
+
viewport.removeEventListener('pointercancel', onPointerUp);
|
|
230
|
+
viewport.removeEventListener('wheel', onWheel);
|
|
231
|
+
modal.removeEventListener('click', onBackdrop);
|
|
232
|
+
document.removeEventListener('keydown', onKeydown);
|
|
233
|
+
zoomOut.removeEventListener('click', onZoomOut);
|
|
234
|
+
zoomIn.removeEventListener('click', onZoomIn);
|
|
235
|
+
reset.removeEventListener('click', resetTransform);
|
|
236
|
+
close.removeEventListener('click', closeModal);
|
|
237
|
+
modal.remove();
|
|
238
|
+
document.body.classList.remove('mermaid-modal-open');
|
|
239
|
+
block.expandButton.focus();
|
|
240
|
+
};
|
|
241
|
+
closeExpanded = finish;
|
|
242
|
+
close.addEventListener('click', closeModal);
|
|
243
|
+
close.focus();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const buttonCleanups = blocks.flatMap((block) => {
|
|
247
|
+
const copy = async () => {
|
|
248
|
+
try {
|
|
249
|
+
await navigator.clipboard.writeText(block.source);
|
|
250
|
+
block.copyButton.classList.add('copied');
|
|
251
|
+
block.copyButton.setAttribute('aria-label', 'Diagram source copied');
|
|
252
|
+
block.copyButton.innerHTML = CHECK_ICON;
|
|
253
|
+
clearTimeout(block.copyTimer);
|
|
254
|
+
block.copyTimer = setTimeout(() => {
|
|
255
|
+
block.copyButton.classList.remove('copied');
|
|
256
|
+
block.copyButton.setAttribute('aria-label', 'Copy diagram source');
|
|
257
|
+
block.copyButton.innerHTML = COPY_ICON;
|
|
258
|
+
}, 1600);
|
|
259
|
+
} catch {
|
|
260
|
+
block.copyButton.setAttribute('aria-label', 'Copy failed');
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
const expand = () => openModal(block);
|
|
264
|
+
block.copyButton.addEventListener('click', copy);
|
|
265
|
+
block.expandButton.addEventListener('click', expand);
|
|
266
|
+
return [
|
|
267
|
+
() => block.copyButton.removeEventListener('click', copy),
|
|
268
|
+
() => block.expandButton.removeEventListener('click', expand)
|
|
269
|
+
];
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
async function render(block: Block): Promise<void> {
|
|
273
|
+
const generation = ++block.generation;
|
|
274
|
+
block.pre.dataset.state = 'loading';
|
|
275
|
+
try {
|
|
276
|
+
const mermaid = await loadMermaid();
|
|
277
|
+
if (destroyed || generation !== block.generation) return;
|
|
278
|
+
mermaid.initialize({
|
|
279
|
+
startOnLoad: false,
|
|
280
|
+
securityLevel: 'loose',
|
|
281
|
+
theme: isDark() ? 'dark' : 'base',
|
|
282
|
+
themeVariables: mermaidTheme()
|
|
283
|
+
});
|
|
284
|
+
const { svg } = await mermaid.render(`mermaid-${++idCounter}`, block.source);
|
|
285
|
+
if (destroyed || generation !== block.generation) return;
|
|
286
|
+
closeModal();
|
|
287
|
+
block.content.innerHTML = svg;
|
|
288
|
+
block.container.style.display = '';
|
|
289
|
+
block.pre.style.display = 'none';
|
|
290
|
+
block.expandButton.disabled = false;
|
|
291
|
+
delete block.pre.dataset.state;
|
|
292
|
+
block.rendered = true;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (destroyed || generation !== block.generation) return;
|
|
295
|
+
block.content.replaceChildren();
|
|
296
|
+
block.container.style.display = '';
|
|
297
|
+
block.pre.style.display = '';
|
|
298
|
+
block.pre.dataset.state = 'error';
|
|
299
|
+
block.expandButton.disabled = true;
|
|
300
|
+
const message = document.createElement('p');
|
|
301
|
+
message.className = 'mermaid-error';
|
|
302
|
+
message.textContent = `Failed to render diagram: ${error instanceof Error ? error.message : String(error)}`;
|
|
303
|
+
block.content.appendChild(message);
|
|
304
|
+
block.rendered = false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const observer = new IntersectionObserver(
|
|
309
|
+
(entries) => {
|
|
310
|
+
for (const entry of entries) {
|
|
311
|
+
if (!entry.isIntersecting) continue;
|
|
312
|
+
observer.unobserve(entry.target);
|
|
313
|
+
const block = blocks.find((candidate) => candidate.pre === entry.target);
|
|
314
|
+
if (block) void render(block);
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
{ rootMargin: '200px 0px' }
|
|
318
|
+
);
|
|
319
|
+
for (const block of blocks) observer.observe(block.pre);
|
|
320
|
+
|
|
321
|
+
printPreparers.set(node, async () => {
|
|
322
|
+
observer.disconnect();
|
|
323
|
+
await Promise.all(blocks.filter((block) => !block.rendered).map(render));
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
let wasDark = isDark();
|
|
327
|
+
const themeObserver = new MutationObserver(() => {
|
|
328
|
+
const dark = isDark();
|
|
329
|
+
if (dark === wasDark) return;
|
|
330
|
+
wasDark = dark;
|
|
331
|
+
for (const block of blocks) if (block.rendered) void render(block);
|
|
332
|
+
});
|
|
333
|
+
themeObserver.observe(document.documentElement, {
|
|
334
|
+
attributes: true,
|
|
335
|
+
attributeFilter: ['class']
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return () => {
|
|
339
|
+
printPreparers.delete(node);
|
|
340
|
+
destroyed = true;
|
|
341
|
+
closeModal();
|
|
342
|
+
observer.disconnect();
|
|
343
|
+
themeObserver.disconnect();
|
|
344
|
+
buttonCleanups.forEach((cleanup) => cleanup());
|
|
345
|
+
for (const block of blocks) {
|
|
346
|
+
clearTimeout(block.copyTimer);
|
|
347
|
+
block.container.remove();
|
|
348
|
+
block.pre.style.display = '';
|
|
349
|
+
delete block.pre.dataset.state;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
}
|