@kadal/deck 0.0.0-stage → 0.1.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/LICENSE +661 -0
- package/LICENSING.md +40 -0
- package/README.md +170 -2
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/audience/index.d.ts +14 -0
- package/dist/audience/index.js +680 -0
- package/dist/backgrounds/index.d.ts +65 -0
- package/dist/backgrounds/index.js +604 -0
- package/dist/backgrounds/shared.d.ts +7 -0
- package/dist/backgrounds/shared.js +89 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +8 -0
- package/dist/cli/host.js +111 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +140 -0
- package/dist/cli/login.js +56 -0
- package/dist/cli/publish.js +159 -0
- package/dist/cli/vite.js +39 -0
- package/dist/compiler/code-meta.d.ts +33 -0
- package/dist/compiler/code-meta.js +81 -0
- package/dist/compiler/index.d.ts +8 -0
- package/dist/compiler/index.js +8 -0
- package/dist/compiler/magic-move.d.ts +26 -0
- package/dist/compiler/magic-move.js +33 -0
- package/dist/compiler/plain-text.js +7 -0
- package/dist/compiler/rehype-code.d.ts +18 -0
- package/dist/compiler/rehype-code.js +66 -0
- package/dist/compiler/rehype-glossary.d.ts +35 -0
- package/dist/compiler/rehype-glossary.js +211 -0
- package/dist/compiler/slots.d.ts +3 -0
- package/dist/compiler/slots.js +36 -0
- package/dist/compiler/split.d.ts +63 -0
- package/dist/compiler/split.js +195 -0
- package/dist/components/Background.d.ts +15 -0
- package/dist/components/Background.js +24 -0
- package/dist/components/Click.d.ts +54 -0
- package/dist/components/Click.js +129 -0
- package/dist/components/Code.d.ts +27 -0
- package/dist/components/Code.js +124 -0
- package/dist/components/Motion.d.ts +25 -0
- package/dist/components/Motion.js +45 -0
- package/dist/components/Sketch.d.ts +44 -0
- package/dist/components/Sketch.js +141 -0
- package/dist/components/Term.d.ts +63 -0
- package/dist/components/Term.js +492 -0
- package/dist/components/bling.d.ts +75 -0
- package/dist/components/bling.js +146 -0
- package/dist/components/content.d.ts +232 -0
- package/dist/components/content.js +637 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.js +88 -0
- package/dist/core/clicks.d.ts +45 -0
- package/dist/core/clicks.js +75 -0
- package/dist/core/hash.d.ts +33 -0
- package/dist/core/hash.js +49 -0
- package/dist/core/keyboard.d.ts +17 -0
- package/dist/core/keyboard.js +198 -0
- package/dist/core/layout.d.ts +16 -0
- package/dist/core/layout.js +25 -0
- package/dist/core/model.d.ts +259 -0
- package/dist/core/model.js +377 -0
- package/dist/core/navigation.d.ts +74 -0
- package/dist/core/navigation.js +244 -0
- package/dist/core/poster.js +30 -0
- package/dist/core/store.d.ts +20 -0
- package/dist/core/store.js +64 -0
- package/dist/core/touch.js +27 -0
- package/dist/draw/DrawingLayer.js +315 -0
- package/dist/draw/strokes.js +188 -0
- package/dist/export/index.d.ts +68 -0
- package/dist/export/index.js +251 -0
- package/dist/export/run.js +81 -0
- package/dist/glossary/model.d.ts +81 -0
- package/dist/glossary/model.js +156 -0
- package/dist/glossary/parse.d.ts +27 -0
- package/dist/glossary/parse.js +90 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +30 -0
- package/dist/live/LiveProvider.d.ts +20 -0
- package/dist/live/LiveProvider.js +450 -0
- package/dist/live/analytics.d.ts +15 -0
- package/dist/live/analytics.js +103 -0
- package/dist/live/client.d.ts +51 -0
- package/dist/live/client.js +193 -0
- package/dist/live/components.d.ts +64 -0
- package/dist/live/components.js +471 -0
- package/dist/live/context.d.ts +15 -0
- package/dist/live/context.js +28 -0
- package/dist/live/index.d.ts +7 -0
- package/dist/live/index.js +7 -0
- package/dist/live/protocol.d.ts +284 -0
- package/dist/live/protocol.js +42 -0
- package/dist/live/turnstile.js +49 -0
- package/dist/presenter/index.d.ts +20 -0
- package/dist/presenter/index.js +1211 -0
- package/dist/react/AutoAnimate.js +92 -0
- package/dist/react/Backgrounds.d.ts +11 -0
- package/dist/react/Backgrounds.js +154 -0
- package/dist/react/ConfettiLayer.js +85 -0
- package/dist/react/Deck.d.ts +35 -0
- package/dist/react/Deck.js +983 -0
- package/dist/react/Frame.d.ts +16 -0
- package/dist/react/Frame.js +174 -0
- package/dist/react/Hints.d.ts +3 -0
- package/dist/react/Hints.js +201 -0
- package/dist/react/Lightbox.js +54 -0
- package/dist/react/Overview.js +93 -0
- package/dist/react/PrintView.js +128 -0
- package/dist/react/ScrollView.js +128 -0
- package/dist/react/SlideFrame.d.ts +51 -0
- package/dist/react/SlideFrame.js +323 -0
- package/dist/react/Stage.js +151 -0
- package/dist/react/ThemeMenu.d.ts +5 -0
- package/dist/react/ThemeMenu.js +124 -0
- package/dist/react/Zoom.js +23 -0
- package/dist/react/chrome.js +339 -0
- package/dist/react/context.d.ts +165 -0
- package/dist/react/context.js +19 -0
- package/dist/react/hooks.d.ts +33 -0
- package/dist/react/hooks.js +70 -0
- package/dist/react/layouts.d.ts +21 -0
- package/dist/react/layouts.js +104 -0
- package/dist/react/mdx.d.ts +4 -0
- package/dist/react/mdx.js +33 -0
- package/dist/react/terms.d.ts +6 -0
- package/dist/react/terms.js +66 -0
- package/dist/renderers/Island.d.ts +57 -0
- package/dist/renderers/Island.js +223 -0
- package/dist/renderers/contract.d.ts +173 -0
- package/dist/renderers/contract.js +152 -0
- package/dist/renderers/index.d.ts +12 -0
- package/dist/renderers/index.js +16 -0
- package/dist/renderers/react.d.ts +16 -0
- package/dist/renderers/react.js +32 -0
- package/dist/renderers/web-components.d.ts +4 -0
- package/dist/renderers/web-components.js +103 -0
- package/dist/sketch/Canvas.js +49 -0
- package/dist/sketch/dsl.d.ts +131 -0
- package/dist/sketch/dsl.js +281 -0
- package/dist/sketch/index.d.ts +4 -0
- package/dist/sketch/index.js +3 -0
- package/dist/sketch/palette.d.ts +21 -0
- package/dist/sketch/palette.js +66 -0
- package/dist/sketch/render.d.ts +43 -0
- package/dist/sketch/render.js +198 -0
- package/dist/themes/base.css +1704 -0
- package/dist/themes/bling.css +592 -0
- package/dist/themes/glossary.css +317 -0
- package/dist/themes/live.css +1557 -0
- package/dist/themes/minimal.css +83 -0
- package/dist/themes/rla.css +92 -0
- package/dist/themes/sketch.css +121 -0
- package/dist/themes/stack.css +62 -0
- package/dist/vite/index.d.ts +15 -0
- package/dist/vite/index.js +320 -0
- package/package.json +159 -4
- package/tools/verify-island.mjs +150 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { POSTER_DEFAULT_SIZE, posterName } from "../core/poster.js";
|
|
2
|
+
import { splitDeck } from "../compiler/split.js";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
//#region src/export/index.ts
|
|
6
|
+
const DEFAULT_POSTER_WAIT = 5e3;
|
|
7
|
+
/** Every embedded page in the deck that has no poster of its own, with the size to capture. */
|
|
8
|
+
function posterJobs(deckDir, config, entry = "deck.mdx") {
|
|
9
|
+
const file = join(deckDir, entry);
|
|
10
|
+
if (!existsSync(file)) return [];
|
|
11
|
+
const split = splitDeck(readFileSync(file, "utf8"), {
|
|
12
|
+
file,
|
|
13
|
+
resolveInclude: (path, from) => {
|
|
14
|
+
const inc = join(dirname(from), path);
|
|
15
|
+
return existsSync(inc) ? {
|
|
16
|
+
file: inc,
|
|
17
|
+
text: readFileSync(inc, "utf8")
|
|
18
|
+
} : null;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
22
|
+
const add = (url, width, height, capture = url, wait = DEFAULT_POSTER_WAIT) => {
|
|
23
|
+
const name = posterName(url, width, height);
|
|
24
|
+
if (!jobs.has(name)) jobs.set(name, {
|
|
25
|
+
url,
|
|
26
|
+
capture,
|
|
27
|
+
width,
|
|
28
|
+
height,
|
|
29
|
+
file: name,
|
|
30
|
+
wait
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
for (const s of split.slides) {
|
|
34
|
+
const fm = s.frontmatter;
|
|
35
|
+
const layout = typeof fm.layout === "string" ? fm.layout : "";
|
|
36
|
+
if (/^iframe(-left|-right)?$/.test(layout) && typeof fm.url === "string" && !fm.poster) {
|
|
37
|
+
const half = layout !== "iframe";
|
|
38
|
+
add(fm.url, half ? Math.round(config.width / 2) : config.width, config.height, typeof fm.posterUrl === "string" ? fm.posterUrl : fm.url, typeof fm.posterWait === "number" ? fm.posterWait : DEFAULT_POSTER_WAIT);
|
|
39
|
+
}
|
|
40
|
+
for (const m of s.content.matchAll(/<Iframe\b([^>]*)>/g)) {
|
|
41
|
+
const attrs = m[1] ?? "";
|
|
42
|
+
if (/\bposter=/.test(attrs)) continue;
|
|
43
|
+
const src = /\bsrc=["']([^"']+)["']/.exec(attrs)?.[1];
|
|
44
|
+
const capture = /\bposterUrl=["']([^"']+)["']/.exec(attrs)?.[1];
|
|
45
|
+
const wait = /\bposterWait=\{?(\d+)\}?/.exec(attrs)?.[1];
|
|
46
|
+
if (src && /^https?:\/\//.test(src)) add(src, POSTER_DEFAULT_SIZE[0], POSTER_DEFAULT_SIZE[1], capture ?? src, wait ? Number(wait) : DEFAULT_POSTER_WAIT);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return [...jobs.values()];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Capture the posters that are missing (or all of them with `refresh`). WebGL pages render
|
|
53
|
+
* through SwiftShader; each page gets its job's `wait` after load for maps and fonts.
|
|
54
|
+
*/
|
|
55
|
+
async function capturePosters(deckDir, jobs, { refresh = false, log = console.log } = {}) {
|
|
56
|
+
const todo = jobs.filter((j) => refresh || !existsSync(join(deckDir, "public", j.file)));
|
|
57
|
+
if (!todo.length) return 0;
|
|
58
|
+
const { chromium } = await import("playwright");
|
|
59
|
+
const browser = await chromium.launch({ args: [
|
|
60
|
+
"--use-gl=angle",
|
|
61
|
+
"--use-angle=swiftshader",
|
|
62
|
+
"--enable-unsafe-swiftshader",
|
|
63
|
+
"--ignore-gpu-blocklist"
|
|
64
|
+
] });
|
|
65
|
+
let done = 0;
|
|
66
|
+
try {
|
|
67
|
+
for (const j of todo) {
|
|
68
|
+
const target = join(deckDir, "public", j.file);
|
|
69
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
70
|
+
const context = await browser.newContext({
|
|
71
|
+
viewport: {
|
|
72
|
+
width: j.width,
|
|
73
|
+
height: j.height
|
|
74
|
+
},
|
|
75
|
+
deviceScaleFactor: 1
|
|
76
|
+
});
|
|
77
|
+
const page = await context.newPage();
|
|
78
|
+
try {
|
|
79
|
+
log(`poster ${j.capture} at ${j.width}×${j.height}${j.capture !== j.url ? ` (for ${j.url})` : ""}`);
|
|
80
|
+
await page.goto(j.capture, {
|
|
81
|
+
waitUntil: "domcontentloaded",
|
|
82
|
+
timeout: 6e4
|
|
83
|
+
});
|
|
84
|
+
await page.waitForTimeout(j.wait);
|
|
85
|
+
await page.screenshot({
|
|
86
|
+
path: target,
|
|
87
|
+
type: "jpeg",
|
|
88
|
+
quality: 86
|
|
89
|
+
});
|
|
90
|
+
log(` → public/${j.file}`);
|
|
91
|
+
done++;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
log(` failed: ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`);
|
|
94
|
+
} finally {
|
|
95
|
+
await context.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
await browser.close();
|
|
100
|
+
}
|
|
101
|
+
return done;
|
|
102
|
+
}
|
|
103
|
+
async function exportDeck(options) {
|
|
104
|
+
const log = options.log ?? console.log;
|
|
105
|
+
const { chromium } = await import("playwright");
|
|
106
|
+
mkdirSync(options.outDir, { recursive: true });
|
|
107
|
+
const browser = await chromium.launch();
|
|
108
|
+
const result = {
|
|
109
|
+
files: [],
|
|
110
|
+
pages: 0
|
|
111
|
+
};
|
|
112
|
+
try {
|
|
113
|
+
const page = await (await browser.newContext({
|
|
114
|
+
deviceScaleFactor: options.scale ?? 2,
|
|
115
|
+
viewport: {
|
|
116
|
+
width: 1920,
|
|
117
|
+
height: 1080
|
|
118
|
+
}
|
|
119
|
+
})).newPage();
|
|
120
|
+
const url = new URL(options.url);
|
|
121
|
+
url.searchParams.set("print-pdf", "");
|
|
122
|
+
log(`open ${url}`);
|
|
123
|
+
await page.goto(url.toString(), { waitUntil: "networkidle" });
|
|
124
|
+
await page.waitForSelector(".deck-print-pages[data-ready]", { timeout: 12e4 });
|
|
125
|
+
await page.evaluate(() => document.fonts.ready);
|
|
126
|
+
await page.waitForTimeout(800);
|
|
127
|
+
const dims = await page.evaluate(() => {
|
|
128
|
+
const el = document.querySelector(".deck-print-page");
|
|
129
|
+
return {
|
|
130
|
+
width: el?.offsetWidth ?? 1920,
|
|
131
|
+
height: el?.offsetHeight ?? 1080,
|
|
132
|
+
pages: document.querySelectorAll(".deck-print-page").length
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
result.pages = dims.pages;
|
|
136
|
+
log(`pages ${dims.pages} at ${dims.width}×${dims.height}`);
|
|
137
|
+
if (options.pdf !== false) {
|
|
138
|
+
const path = join(options.outDir, `${options.slug}.pdf`);
|
|
139
|
+
await page.emulateMedia({ media: "print" });
|
|
140
|
+
await page.pdf({
|
|
141
|
+
path,
|
|
142
|
+
width: `${dims.width}px`,
|
|
143
|
+
height: `${dims.height}px`,
|
|
144
|
+
printBackground: true,
|
|
145
|
+
preferCSSPageSize: true,
|
|
146
|
+
margin: {
|
|
147
|
+
top: 0,
|
|
148
|
+
right: 0,
|
|
149
|
+
bottom: 0,
|
|
150
|
+
left: 0
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
await page.emulateMedia({ media: "screen" });
|
|
154
|
+
result.files.push({
|
|
155
|
+
kind: "pdf",
|
|
156
|
+
path,
|
|
157
|
+
size: readFileSync(path).byteLength
|
|
158
|
+
});
|
|
159
|
+
log(`pdf ${path}`);
|
|
160
|
+
}
|
|
161
|
+
if (options.png || options.pptx) {
|
|
162
|
+
const pngDir = join(options.outDir, "png");
|
|
163
|
+
mkdirSync(pngDir, { recursive: true });
|
|
164
|
+
const pages = await page.$$(".deck-print-page");
|
|
165
|
+
const shots = [];
|
|
166
|
+
for (let i = 0; i < pages.length; i++) {
|
|
167
|
+
const el = pages[i];
|
|
168
|
+
await el.scrollIntoViewIfNeeded();
|
|
169
|
+
const path = join(pngDir, `${String(i + 1).padStart(3, "0")}.png`);
|
|
170
|
+
await el.screenshot({
|
|
171
|
+
path,
|
|
172
|
+
type: "png"
|
|
173
|
+
});
|
|
174
|
+
const notes = await el.$eval(".deck-print-notes", (n) => n.innerText).catch(() => "");
|
|
175
|
+
shots.push({
|
|
176
|
+
path,
|
|
177
|
+
notes
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (options.png) {
|
|
181
|
+
for (const s of shots) result.files.push({
|
|
182
|
+
kind: "png",
|
|
183
|
+
path: s.path,
|
|
184
|
+
size: readFileSync(s.path).byteLength
|
|
185
|
+
});
|
|
186
|
+
log(`png ${shots.length} files in ${pngDir}`);
|
|
187
|
+
}
|
|
188
|
+
if (options.pptx) {
|
|
189
|
+
const path = join(options.outDir, `${options.slug}.pptx`);
|
|
190
|
+
await writePptx(path, shots, dims.width, dims.height, options.slug);
|
|
191
|
+
result.files.push({
|
|
192
|
+
kind: "pptx",
|
|
193
|
+
path,
|
|
194
|
+
size: readFileSync(path).byteLength
|
|
195
|
+
});
|
|
196
|
+
log(`pptx ${path}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
await browser.close();
|
|
201
|
+
}
|
|
202
|
+
if (options.upload) for (const f of result.files) {
|
|
203
|
+
if (f.kind === "png") continue;
|
|
204
|
+
const target = `${options.upload.replace(/\/$/, "")}/api/decks/${options.slug}/exports?kind=${f.kind}`;
|
|
205
|
+
const body = readFileSync(f.path);
|
|
206
|
+
const res = await fetch(target, {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: {
|
|
209
|
+
"content-type": f.kind === "pdf" ? "application/pdf" : "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
210
|
+
"content-length": String(body.byteLength),
|
|
211
|
+
...options.adminKey ? { authorization: `Bearer ${options.adminKey}` } : {}
|
|
212
|
+
},
|
|
213
|
+
body
|
|
214
|
+
});
|
|
215
|
+
const json = await res.json().catch(() => ({}));
|
|
216
|
+
if (!res.ok) log(`upload ${f.kind} failed: ${res.status} ${json.error ?? ""}`);
|
|
217
|
+
else {
|
|
218
|
+
f.uploaded = json.url;
|
|
219
|
+
log(`upload ${f.kind} → ${json.url}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
async function writePptx(path, shots, width, height, title) {
|
|
225
|
+
const mod = await import("pptxgenjs");
|
|
226
|
+
const pptx = new (mod.default ?? mod)();
|
|
227
|
+
const w = width / 96;
|
|
228
|
+
const h = height / 96;
|
|
229
|
+
pptx.defineLayout({
|
|
230
|
+
name: "deck",
|
|
231
|
+
width: w,
|
|
232
|
+
height: h
|
|
233
|
+
});
|
|
234
|
+
pptx.layout = "deck";
|
|
235
|
+
pptx.title = title;
|
|
236
|
+
for (const s of shots) {
|
|
237
|
+
const slide = pptx.addSlide();
|
|
238
|
+
slide.addImage({
|
|
239
|
+
data: `image/png;base64,${readFileSync(s.path).toString("base64")}`,
|
|
240
|
+
x: 0,
|
|
241
|
+
y: 0,
|
|
242
|
+
w,
|
|
243
|
+
h
|
|
244
|
+
});
|
|
245
|
+
if (s.notes.trim()) slide.addNotes(s.notes.trim());
|
|
246
|
+
}
|
|
247
|
+
const data = await pptx.write({ outputType: "nodebuffer" });
|
|
248
|
+
writeFileSync(path, data);
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
251
|
+
export { capturePosters, exportDeck, posterJobs };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { capturePosters, exportDeck, posterJobs } from "./index.js";
|
|
2
|
+
import { runVite, spawnVite } from "../cli/vite.js";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
//#region src/export/run.ts
|
|
6
|
+
/** `kadal-deck export [flags]` in a deck folder. Returns once every file is written. */
|
|
7
|
+
async function runExport(args, cwd = process.cwd()) {
|
|
8
|
+
const has = (f) => args.includes(f);
|
|
9
|
+
const val = (f) => {
|
|
10
|
+
const i = args.indexOf(f);
|
|
11
|
+
return i >= 0 ? args[i + 1] : void 0;
|
|
12
|
+
};
|
|
13
|
+
const slug = val("--slug") ?? basename(cwd);
|
|
14
|
+
/** The id the host keys this deck by (a published deck's room); defaults to the slug. */
|
|
15
|
+
const room = val("--room") ?? slug;
|
|
16
|
+
const outDir = resolve(cwd, val("--out") ?? "export");
|
|
17
|
+
let url = val("--url");
|
|
18
|
+
let preview = null;
|
|
19
|
+
if (!has("--no-posters")) {
|
|
20
|
+
const size = await deckSize();
|
|
21
|
+
const jobs = posterJobs(cwd, size);
|
|
22
|
+
const n = await capturePosters(cwd, jobs, { refresh: has("--refresh-posters") });
|
|
23
|
+
console.log(`poster ${jobs.length} embedded page(s), ${n} captured`);
|
|
24
|
+
if (has("--posters")) return;
|
|
25
|
+
}
|
|
26
|
+
/** The authored stage size, from deck.config.ts when it sets one. */
|
|
27
|
+
async function deckSize() {
|
|
28
|
+
try {
|
|
29
|
+
const mod = await import(pathToFileURL(resolve(cwd, "deck.config.ts")).href);
|
|
30
|
+
return {
|
|
31
|
+
width: mod.config?.width ?? 1920,
|
|
32
|
+
height: mod.config?.height ?? 1080
|
|
33
|
+
};
|
|
34
|
+
} catch {
|
|
35
|
+
return {
|
|
36
|
+
width: 1920,
|
|
37
|
+
height: 1080
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!url) {
|
|
42
|
+
console.log("build vite build");
|
|
43
|
+
await runVite(cwd, ["build"]);
|
|
44
|
+
const port = 4173 + Math.floor(Math.random() * 200);
|
|
45
|
+
preview = spawnVite(cwd, [
|
|
46
|
+
"preview",
|
|
47
|
+
"--port",
|
|
48
|
+
String(port),
|
|
49
|
+
"--strictPort"
|
|
50
|
+
], "ignore");
|
|
51
|
+
url = `http://localhost:${port}/${slug}/`;
|
|
52
|
+
await waitForServer(url, Date.now() + 2e4);
|
|
53
|
+
}
|
|
54
|
+
/** Poll until the preview answers (or the deadline passes) — one request every 200 ms. */
|
|
55
|
+
async function waitForServer(target, deadline) {
|
|
56
|
+
try {
|
|
57
|
+
if ((await fetch(target)).ok) return;
|
|
58
|
+
} catch {}
|
|
59
|
+
if (Date.now() >= deadline) return;
|
|
60
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
61
|
+
return waitForServer(target, deadline);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const result = await exportDeck({
|
|
65
|
+
url,
|
|
66
|
+
outDir,
|
|
67
|
+
slug: room,
|
|
68
|
+
pdf: !has("--no-pdf"),
|
|
69
|
+
png: has("--png"),
|
|
70
|
+
pptx: has("--pptx"),
|
|
71
|
+
scale: Number(val("--scale") ?? 2),
|
|
72
|
+
upload: has("--upload") ? val("--upload")?.startsWith("http") ? val("--upload") : process.env.DECK_HOST ?? "https://deck.kadal.cc" : null,
|
|
73
|
+
adminKey: process.env.DECK_ADMIN_KEY ?? null
|
|
74
|
+
});
|
|
75
|
+
console.log(`\ndone ${result.pages} pages, ${result.files.length} file(s)`);
|
|
76
|
+
} finally {
|
|
77
|
+
preview?.kill();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { runExport };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//#region src/glossary/model.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
* THE GLOSSARY MODEL — pure string work, shared by the compiler and the browser
|
|
5
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* A deck's key concepts and abbreviations live in one `glossary.yaml` beside `deck.mdx`. The
|
|
8
|
+
* compiler links the first mention of each term on every slide (`compiler/rehype-glossary.ts`),
|
|
9
|
+
* `<Term>` links one by hand, and the runtime shows the definition in a card after a short hover
|
|
10
|
+
* (`components/Term.tsx`). Both sides import this file, so they agree on ids, on what counts as a
|
|
11
|
+
* mention, and on how a definition's inline Markdown becomes HTML. No Node imports here.
|
|
12
|
+
*
|
|
13
|
+
* The matcher follows the one that links Paadam's notes (apps/paadam/mdx/glossary.ts): longest
|
|
14
|
+
* name first, whole words only, an optional plural — with one rule tightened for slides, where
|
|
15
|
+
* abbreviations dominate: a name with no lowercase letter (GIS, WGS84) or shorter than four
|
|
16
|
+
* characters matches in its exact case only, so "who" never becomes the WHO.
|
|
17
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
*/
|
|
19
|
+
export interface GlossaryEntry {
|
|
20
|
+
/** Stable slug of the name: what `<Term id>`, `see:` and the room's reveal message carry. */
|
|
21
|
+
id: string;
|
|
22
|
+
/** The term as the card titles it, and the first string the matcher looks for. */
|
|
23
|
+
name: string;
|
|
24
|
+
/** An abbreviation's expansion, shown under the title. */
|
|
25
|
+
full: string | null;
|
|
26
|
+
/** Other spellings that are the same term. */
|
|
27
|
+
aliases: string[];
|
|
28
|
+
/** A category chip: "Projection", "Statistics". */
|
|
29
|
+
tag: string | null;
|
|
30
|
+
link: {
|
|
31
|
+
href: string;
|
|
32
|
+
label: string;
|
|
33
|
+
} | null;
|
|
34
|
+
/** Ids of related entries, offered as chips that swap the card. */
|
|
35
|
+
see: string[];
|
|
36
|
+
/** False keeps the term out of automatic linking; `<Term>` still finds it. */
|
|
37
|
+
auto: boolean;
|
|
38
|
+
/** The definition and the optional second paragraph, as inline HTML (see `inlineHtml`). */
|
|
39
|
+
html: {
|
|
40
|
+
def: string;
|
|
41
|
+
more: string | null;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Deck-level options, from the headmatter's `glossary:` key (a path, `false`, or a mapping). */
|
|
45
|
+
export interface GlossaryOptions {
|
|
46
|
+
/** The glossary file, relative to the deck file; null switches the feature off. */
|
|
47
|
+
src: string | null;
|
|
48
|
+
/** Link the first mention on each slide, or every mention. */
|
|
49
|
+
repeat: "first" | "all";
|
|
50
|
+
/** Headings: only for terms the slide's body never mentions, always, or never. */
|
|
51
|
+
headings: "fallback" | boolean;
|
|
52
|
+
/** Extra component names whose children are left alone. */
|
|
53
|
+
skip: string[];
|
|
54
|
+
/** Milliseconds the pointer rests on a term before its card opens. */
|
|
55
|
+
delay: number;
|
|
56
|
+
/** Whether the presenter's reveals start out shared with the room. */
|
|
57
|
+
sync: boolean;
|
|
58
|
+
}
|
|
59
|
+
export declare const DEFAULT_GLOSSARY: GlossaryOptions;
|
|
60
|
+
/** "Plus Code" → "plus-code", "WGS84" → "wgs84". */
|
|
61
|
+
export declare function termId(name: string): string;
|
|
62
|
+
export interface TermMatch {
|
|
63
|
+
entry: GlossaryEntry;
|
|
64
|
+
index: number;
|
|
65
|
+
text: string;
|
|
66
|
+
}
|
|
67
|
+
export interface TermMatcher {
|
|
68
|
+
/** Every mention in `text`, left to right, never overlapping. */
|
|
69
|
+
find(text: string): TermMatch[];
|
|
70
|
+
/** The entry a whole string names (by name, alias or id), for `<Term>UTM</Term>`. */
|
|
71
|
+
resolve(text: string): GlossaryEntry | undefined;
|
|
72
|
+
}
|
|
73
|
+
export declare function buildMatcher(entries: GlossaryEntry[]): TermMatcher;
|
|
74
|
+
/**
|
|
75
|
+
* A definition's inline Markdown as HTML: **bold**, *emphasis*, `code`, [links](https://…) and
|
|
76
|
+
* $maths$. Everything else is escaped, so the result is safe to set as innerHTML. `math` renders
|
|
77
|
+
* TeX (the Vite plugin passes KaTeX at build time, which keeps KaTeX out of the browser bundle);
|
|
78
|
+
* without it, maths shows as its source.
|
|
79
|
+
*/
|
|
80
|
+
export declare function inlineHtml(source: string, math?: (tex: string) => string): string;
|
|
81
|
+
//#endregion
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
//#region src/glossary/model.ts
|
|
2
|
+
const DEFAULT_GLOSSARY = {
|
|
3
|
+
src: "glossary.yaml",
|
|
4
|
+
repeat: "first",
|
|
5
|
+
headings: "fallback",
|
|
6
|
+
skip: [],
|
|
7
|
+
delay: 700,
|
|
8
|
+
sync: false
|
|
9
|
+
};
|
|
10
|
+
/** Normalise the headmatter's `glossary:` value over a base (the defaults, or an earlier layer). */
|
|
11
|
+
function glossaryOptions(value, base = DEFAULT_GLOSSARY) {
|
|
12
|
+
if (value === void 0 || value === null || value === true) return { ...base };
|
|
13
|
+
if (value === false) return {
|
|
14
|
+
...base,
|
|
15
|
+
src: null
|
|
16
|
+
};
|
|
17
|
+
if (typeof value === "string") return {
|
|
18
|
+
...base,
|
|
19
|
+
src: value
|
|
20
|
+
};
|
|
21
|
+
if (typeof value !== "object" || Array.isArray(value)) return { ...base };
|
|
22
|
+
const v = value;
|
|
23
|
+
const out = { ...base };
|
|
24
|
+
if (typeof v.src === "string") out.src = v.src;
|
|
25
|
+
else if (v.src === false || v.src === null) out.src = null;
|
|
26
|
+
if (v.repeat === "first" || v.repeat === "all") out.repeat = v.repeat;
|
|
27
|
+
if (v.headings === "fallback" || typeof v.headings === "boolean") out.headings = v.headings;
|
|
28
|
+
if (Array.isArray(v.skip)) out.skip = v.skip.map(String);
|
|
29
|
+
if (typeof v.delay === "number" && v.delay >= 0) out.delay = v.delay;
|
|
30
|
+
if (typeof v.sync === "boolean") out.sync = v.sync;
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
/** "Plus Code" → "plus-code", "WGS84" → "wgs84". */
|
|
34
|
+
function termId(name) {
|
|
35
|
+
return name.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
36
|
+
}
|
|
37
|
+
/** An id for a one-off `<Term def>`: stable across builds and screens, distinct from any slug. */
|
|
38
|
+
function adhocId(text) {
|
|
39
|
+
let h = 2166136261;
|
|
40
|
+
for (let i = 0; i < text.length; i++) {
|
|
41
|
+
h ^= text.charCodeAt(i);
|
|
42
|
+
h = Math.imul(h, 16777619);
|
|
43
|
+
}
|
|
44
|
+
return `~${(h >>> 0).toString(36)}`;
|
|
45
|
+
}
|
|
46
|
+
/** A name that must match in its exact case: an abbreviation, or too short to trust otherwise. */
|
|
47
|
+
function isExactCase(word) {
|
|
48
|
+
return word.length < 4 || !/\p{Ll}/u.test(word);
|
|
49
|
+
}
|
|
50
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
51
|
+
const WORD = /[\p{L}\p{N}_]/u;
|
|
52
|
+
function buildMatcher(entries) {
|
|
53
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
54
|
+
const byId = /* @__PURE__ */ new Map();
|
|
55
|
+
const auto = [];
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
byId.set(entry.id, entry);
|
|
58
|
+
for (const word of [entry.name, ...entry.aliases]) {
|
|
59
|
+
const key = word.toLowerCase();
|
|
60
|
+
if (key.length < 2 || byKey.has(key)) continue;
|
|
61
|
+
byKey.set(key, {
|
|
62
|
+
entry,
|
|
63
|
+
word
|
|
64
|
+
});
|
|
65
|
+
if (entry.auto) auto.push(word);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
auto.sort((a, b) => b.length - a.length);
|
|
69
|
+
const re = auto.length ? new RegExp(`(^|[^\\p{L}\\p{N}_])(${auto.map(escapeRe).join("|")})(e?s)?(?![\\p{L}\\p{N}_])`, "giu") : null;
|
|
70
|
+
const lookup = (stem, requireAuto) => {
|
|
71
|
+
const hit = byKey.get(stem.toLowerCase());
|
|
72
|
+
if (!hit || requireAuto && !hit.entry.auto) return void 0;
|
|
73
|
+
if (isExactCase(hit.word) && hit.word !== stem) return void 0;
|
|
74
|
+
return hit;
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
find(text) {
|
|
78
|
+
if (!re) return [];
|
|
79
|
+
const out = [];
|
|
80
|
+
re.lastIndex = 0;
|
|
81
|
+
for (let m = re.exec(text); m; m = re.exec(text)) {
|
|
82
|
+
const lead = m[1] ?? "";
|
|
83
|
+
const stem = m[2] ?? "";
|
|
84
|
+
const plural = m[3] ?? "";
|
|
85
|
+
const index = m.index + lead.length;
|
|
86
|
+
const hit = lookup(stem, true);
|
|
87
|
+
const next = text[index + stem.length + plural.length] ?? "";
|
|
88
|
+
if (hit && !((lead === "-" || next === "-") && !isExactCase(hit.word))) out.push({
|
|
89
|
+
entry: hit.entry,
|
|
90
|
+
index,
|
|
91
|
+
text: stem + plural
|
|
92
|
+
});
|
|
93
|
+
else {
|
|
94
|
+
re.lastIndex = index + 1;
|
|
95
|
+
while (re.lastIndex < text.length && WORD.test(text[re.lastIndex])) re.lastIndex++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
},
|
|
100
|
+
resolve(text) {
|
|
101
|
+
const t = text.replace(/\s+/g, " ").trim();
|
|
102
|
+
if (!t) return void 0;
|
|
103
|
+
const direct = byId.get(t) ?? lookup(t, false)?.entry;
|
|
104
|
+
if (direct) return direct;
|
|
105
|
+
for (const cut of [/s$/i, /es$/i]) {
|
|
106
|
+
const stem = t.replace(cut, "");
|
|
107
|
+
if (stem !== t) {
|
|
108
|
+
const hit = lookup(stem, false);
|
|
109
|
+
if (hit) return hit.entry;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return byId.get(termId(t));
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
117
|
+
const SAFE_HREF = /^(https?:|mailto:|#|\/|\.{1,2}\/)/i;
|
|
118
|
+
/**
|
|
119
|
+
* A definition's inline Markdown as HTML: **bold**, *emphasis*, `code`, [links](https://…) and
|
|
120
|
+
* $maths$. Everything else is escaped, so the result is safe to set as innerHTML. `math` renders
|
|
121
|
+
* TeX (the Vite plugin passes KaTeX at build time, which keeps KaTeX out of the browser bundle);
|
|
122
|
+
* without it, maths shows as its source.
|
|
123
|
+
*/
|
|
124
|
+
function inlineHtml(source, math) {
|
|
125
|
+
const re = /`([^`]+)`|\$([^$\n]+)\$|\*\*([^*]+)\*\*|\*([^*\n]+)\*|\[([^\]]+)\]\(([^)\s]+)\)/g;
|
|
126
|
+
let out = "";
|
|
127
|
+
let last = 0;
|
|
128
|
+
for (const m of source.matchAll(re)) {
|
|
129
|
+
const start = m.index ?? 0;
|
|
130
|
+
out += escapeHtml(source.slice(last, start));
|
|
131
|
+
if (m[1] !== void 0) out += `<code>${escapeHtml(m[1])}</code>`;
|
|
132
|
+
else if (m[2] !== void 0) {
|
|
133
|
+
let html = "";
|
|
134
|
+
try {
|
|
135
|
+
html = math ? math(m[2]) : "";
|
|
136
|
+
} catch {
|
|
137
|
+
html = "";
|
|
138
|
+
}
|
|
139
|
+
out += html || `<code class="deck-term-tex">${escapeHtml(m[2])}</code>`;
|
|
140
|
+
} else if (m[3] !== void 0) out += `<strong>${inlineHtml(m[3], math)}</strong>`;
|
|
141
|
+
else if (m[4] !== void 0) out += `<em>${inlineHtml(m[4], math)}</em>`;
|
|
142
|
+
else if (m[5] !== void 0 && m[6] !== void 0) out += SAFE_HREF.test(m[6]) ? `<a href="${escapeHtml(m[6])}" target="_blank" rel="noopener noreferrer">${inlineHtml(m[5], math)}</a>` : inlineHtml(m[5], math);
|
|
143
|
+
last = start + m[0].length;
|
|
144
|
+
}
|
|
145
|
+
return out + escapeHtml(source.slice(last));
|
|
146
|
+
}
|
|
147
|
+
/** "epsg.io" from a link, for the card's source line. */
|
|
148
|
+
function linkLabel(href) {
|
|
149
|
+
try {
|
|
150
|
+
return new URL(href, "https://x.invalid").hostname.replace(/^www\./, "") || href;
|
|
151
|
+
} catch {
|
|
152
|
+
return href;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
export { DEFAULT_GLOSSARY, adhocId, buildMatcher, glossaryOptions, inlineHtml, isExactCase, linkLabel, termId };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { GlossaryEntry } from "./model.js";
|
|
2
|
+
//#region src/glossary/parse.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* `glossary.yaml` → entries. The key is the term as the card titles it; the value is either the
|
|
5
|
+
* definition itself or a mapping:
|
|
6
|
+
*
|
|
7
|
+
* GNSS: Satellite positioning — GPS, NavIC, Galileo and the rest.
|
|
8
|
+
* UTM:
|
|
9
|
+
* full: Universal Transverse Mercator
|
|
10
|
+
* def: Sixty projections, each 6° wide, that turn latitude and longitude into **metres**.
|
|
11
|
+
* more: Chennai sits in zone 44N.
|
|
12
|
+
* aliases: [UTM zone 44N]
|
|
13
|
+
* tag: Projection
|
|
14
|
+
* link: https://epsg.io/32644 # or { href, label }
|
|
15
|
+
* see: [WGS84, EPSG]
|
|
16
|
+
* auto: false # never linked automatically; <Term> still finds it
|
|
17
|
+
*
|
|
18
|
+
* Definitions take inline Markdown and $maths$; both are rendered here, at build time, so the
|
|
19
|
+
* browser receives finished HTML. Problems an author should hear about come back as `warnings`
|
|
20
|
+
* rather than failing the build: a talk should never stop compiling over a glossary typo.
|
|
21
|
+
*/
|
|
22
|
+
export interface ParsedGlossary {
|
|
23
|
+
entries: GlossaryEntry[];
|
|
24
|
+
warnings: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare function parseGlossary(source: string, where: string, math?: (tex: string) => string): ParsedGlossary;
|
|
27
|
+
//#endregion
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { inlineHtml, linkLabel, termId } from "./model.js";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
//#region src/glossary/parse.ts
|
|
4
|
+
const list = (value) => Array.isArray(value) ? value.map((v) => String(v).trim()).filter(Boolean) : typeof value === "string" ? value.split(/\s*[,;]\s*/).map((s) => s.trim()).filter(Boolean) : [];
|
|
5
|
+
const text = (value) => typeof value === "string" || typeof value === "number" ? String(value).replace(/\s+/g, " ").trim() || null : null;
|
|
6
|
+
function parseGlossary(source, where, math) {
|
|
7
|
+
const warnings = [];
|
|
8
|
+
let doc;
|
|
9
|
+
try {
|
|
10
|
+
doc = YAML.parse(source);
|
|
11
|
+
} catch (e) {
|
|
12
|
+
return {
|
|
13
|
+
entries: [],
|
|
14
|
+
warnings: [`${where}: ${e instanceof Error ? e.message : String(e)}`]
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (doc === null || doc === void 0) return {
|
|
18
|
+
entries: [],
|
|
19
|
+
warnings
|
|
20
|
+
};
|
|
21
|
+
if (typeof doc !== "object" || Array.isArray(doc)) return {
|
|
22
|
+
entries: [],
|
|
23
|
+
warnings: [`${where}: the glossary must be a YAML mapping of terms`]
|
|
24
|
+
};
|
|
25
|
+
const entries = [];
|
|
26
|
+
const pendingSee = /* @__PURE__ */ new Map();
|
|
27
|
+
const ids = /* @__PURE__ */ new Set();
|
|
28
|
+
for (const [rawName, value] of Object.entries(doc)) {
|
|
29
|
+
const name = rawName.trim();
|
|
30
|
+
const id = termId(name);
|
|
31
|
+
if (!id) {
|
|
32
|
+
warnings.push(`${where}: "${rawName}" has no letters or digits to make an id from`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (ids.has(id)) {
|
|
36
|
+
warnings.push(`${where}: "${name}" repeats an earlier term (id "${id}"); the first one wins`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const v = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
40
|
+
const def = text(typeof value === "object" ? v.def : value);
|
|
41
|
+
if (!def) {
|
|
42
|
+
warnings.push(`${where}: "${name}" has no definition`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const more = text(v.more);
|
|
46
|
+
const href = text(typeof v.link === "object" && v.link ? v.link.href : v.link);
|
|
47
|
+
const label = typeof v.link === "object" && v.link ? text(v.link.label) : null;
|
|
48
|
+
ids.add(id);
|
|
49
|
+
pendingSee.set(id, list(v.see));
|
|
50
|
+
entries.push({
|
|
51
|
+
id,
|
|
52
|
+
name,
|
|
53
|
+
full: text(v.full),
|
|
54
|
+
aliases: list(v.aliases),
|
|
55
|
+
tag: text(v.tag),
|
|
56
|
+
link: href ? {
|
|
57
|
+
href,
|
|
58
|
+
label: label ?? linkLabel(href)
|
|
59
|
+
} : null,
|
|
60
|
+
see: [],
|
|
61
|
+
auto: v.auto !== false,
|
|
62
|
+
html: {
|
|
63
|
+
def: inlineHtml(def, math),
|
|
64
|
+
more: more ? inlineHtml(more, math) : null
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const byWord = /* @__PURE__ */ new Map();
|
|
69
|
+
for (const e of entries) for (const w of [
|
|
70
|
+
e.name,
|
|
71
|
+
...e.aliases,
|
|
72
|
+
e.id
|
|
73
|
+
]) {
|
|
74
|
+
const key = w.toLowerCase();
|
|
75
|
+
const owner = byWord.get(key);
|
|
76
|
+
if (owner && owner !== e.id && w !== e.id) warnings.push(`${where}: "${w}" names both "${owner}" and "${e.id}"; the first one wins`);
|
|
77
|
+
else if (!owner) byWord.set(key, e.id);
|
|
78
|
+
}
|
|
79
|
+
for (const e of entries) for (const ref of pendingSee.get(e.id) ?? []) {
|
|
80
|
+
const target = byWord.get(ref.toLowerCase()) ?? byWord.get(termId(ref));
|
|
81
|
+
if (!target) warnings.push(`${where}: "${e.name}" says see "${ref}", which is not a term`);
|
|
82
|
+
else if (target !== e.id && !e.see.includes(target)) e.see.push(target);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
entries,
|
|
86
|
+
warnings
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
export { parseGlossary };
|