@hajimism/dek 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 +21 -0
- package/README.ja.md +75 -0
- package/README.md +75 -0
- package/package.json +65 -0
- package/src/cli/address.ts +53 -0
- package/src/cli/build.ts +85 -0
- package/src/cli/check.ts +97 -0
- package/src/cli/commands.ts +851 -0
- package/src/cli/contract.ts +444 -0
- package/src/cli/cues.ts +18 -0
- package/src/cli/files.ts +309 -0
- package/src/cli/flags.ts +295 -0
- package/src/cli/format.ts +156 -0
- package/src/cli/goto.ts +96 -0
- package/src/cli/init.ts +136 -0
- package/src/cli/keep-alive.ts +72 -0
- package/src/cli/lint.ts +84 -0
- package/src/cli/ls.ts +119 -0
- package/src/cli/main.ts +231 -0
- package/src/cli/marks.ts +16 -0
- package/src/cli/mv.ts +37 -0
- package/src/cli/new.ts +64 -0
- package/src/cli/pdf.ts +16 -0
- package/src/cli/qr.ts +466 -0
- package/src/cli/ref.ts +165 -0
- package/src/cli/rehearse.ts +42 -0
- package/src/cli/result.ts +161 -0
- package/src/cli/scope.ts +308 -0
- package/src/cli/serve.ts +125 -0
- package/src/cli/shot.ts +134 -0
- package/src/cli/show.ts +103 -0
- package/src/cli/sync.ts +37 -0
- package/src/cli/text.ts +479 -0
- package/src/cli/theme.ts +56 -0
- package/src/cli/tty.ts +81 -0
- package/src/cli/usage.ts +198 -0
- package/src/cli/video.ts +17 -0
- package/src/cli/voice.ts +113 -0
- package/src/cli.ts +4 -0
- package/src/core/assets.ts +239 -0
- package/src/core/build.ts +77 -0
- package/src/core/bundled-theme.ts +15 -0
- package/src/core/capture-go.ts +72 -0
- package/src/core/chrome.ts +128 -0
- package/src/core/config-keys.ts +82 -0
- package/src/core/config.ts +95 -0
- package/src/core/css-scan.ts +158 -0
- package/src/core/css-transform.ts +140 -0
- package/src/core/css-value.ts +127 -0
- package/src/core/css.ts +545 -0
- package/src/core/cue.ts +187 -0
- package/src/core/deck-paths.ts +34 -0
- package/src/core/diagnostic.ts +114 -0
- package/src/core/document.ts +218 -0
- package/src/core/error.ts +31 -0
- package/src/core/escape.ts +16 -0
- package/src/core/fill.ts +100 -0
- package/src/core/finish-beat.ts +30 -0
- package/src/core/github.ts +234 -0
- package/src/core/html-scan.ts +253 -0
- package/src/core/html.ts +234 -0
- package/src/core/in-page-go.ts +128 -0
- package/src/core/lines.ts +43 -0
- package/src/core/lint/asset-refs.ts +109 -0
- package/src/core/lint/context.ts +116 -0
- package/src/core/lint/files.ts +117 -0
- package/src/core/lint/keys.ts +48 -0
- package/src/core/lint/morph.ts +91 -0
- package/src/core/lint/script.ts +102 -0
- package/src/core/lint/shared-styles.ts +165 -0
- package/src/core/lint/slide-html.ts +466 -0
- package/src/core/lint/slide-script.ts +179 -0
- package/src/core/lint/theme-css.ts +295 -0
- package/src/core/lint/timing.ts +66 -0
- package/src/core/lint/tokens.ts +520 -0
- package/src/core/lint/voice.ts +76 -0
- package/src/core/lint.ts +137 -0
- package/src/core/live-protocol.ts +130 -0
- package/src/core/marks.ts +198 -0
- package/src/core/mv.ts +293 -0
- package/src/core/ogp.ts +99 -0
- package/src/core/optional.ts +24 -0
- package/src/core/overflow.ts +220 -0
- package/src/core/page-pool.ts +53 -0
- package/src/core/page.ts +66 -0
- package/src/core/parse.ts +307 -0
- package/src/core/path.ts +58 -0
- package/src/core/pdf.ts +71 -0
- package/src/core/playwright-module.d.ts +43 -0
- package/src/core/playwright-motion.ts +42 -0
- package/src/core/playwright-sheet.ts +32 -0
- package/src/core/playwright-visual.ts +391 -0
- package/src/core/playwright-worker.ts +5 -0
- package/src/core/playwright.ts +431 -0
- package/src/core/position.ts +146 -0
- package/src/core/presenter-state.ts +43 -0
- package/src/core/presenter.ts +131 -0
- package/src/core/prose.ts +16 -0
- package/src/core/pseudo-text.ts +67 -0
- package/src/core/rail-width.ts +4 -0
- package/src/core/ref-name.ts +110 -0
- package/src/core/ref.ts +238 -0
- package/src/core/resolve.ts +228 -0
- package/src/core/rumdl.ts +99 -0
- package/src/core/safe-fs.ts +196 -0
- package/src/core/sarif.ts +135 -0
- package/src/core/schema.ts +89 -0
- package/src/core/script-lines.ts +53 -0
- package/src/core/sheet.ts +223 -0
- package/src/core/shot/morph.ts +80 -0
- package/src/core/shot/motion.ts +187 -0
- package/src/core/shot/still.ts +216 -0
- package/src/core/shot-cache.ts +140 -0
- package/src/core/size.ts +11 -0
- package/src/core/skeleton-record.ts +51 -0
- package/src/core/skeleton.ts +89 -0
- package/src/core/slide-draw.ts +165 -0
- package/src/core/slide-eval-worker.ts +58 -0
- package/src/core/slide-measure.ts +272 -0
- package/src/core/slide-script-eval.ts +258 -0
- package/src/core/slide-script.ts +252 -0
- package/src/core/spawn.ts +128 -0
- package/src/core/step.ts +94 -0
- package/src/core/still-page.ts +70 -0
- package/src/core/suggest.ts +33 -0
- package/src/core/sync.ts +316 -0
- package/src/core/text-contrast.ts +466 -0
- package/src/core/theme-excerpt.ts +187 -0
- package/src/core/theme-facts.ts +51 -0
- package/src/core/timeline.ts +216 -0
- package/src/core/timing.ts +88 -0
- package/src/core/toml-keys.ts +349 -0
- package/src/core/url-attributes.ts +103 -0
- package/src/core/video-document.ts +15 -0
- package/src/core/visual-cache.ts +100 -0
- package/src/core/visual.ts +558 -0
- package/src/core/voice.ts +321 -0
- package/src/core/zod.ts +79 -0
- package/src/runtime/browser.ts +203 -0
- package/src/runtime/clone.ts +52 -0
- package/src/runtime/deck-control.ts +43 -0
- package/src/runtime/fit.ts +32 -0
- package/src/runtime/go.ts +56 -0
- package/src/runtime/input.ts +128 -0
- package/src/runtime/laser-view.ts +110 -0
- package/src/runtime/laser.ts +145 -0
- package/src/runtime/live-host.ts +33 -0
- package/src/runtime/live-reload.ts +29 -0
- package/src/runtime/live.ts +88 -0
- package/src/runtime/marks-view.ts +74 -0
- package/src/runtime/motion.ts +113 -0
- package/src/runtime/navigator.ts +189 -0
- package/src/runtime/player.ts +28 -0
- package/src/runtime/presenter-view.ts +289 -0
- package/src/runtime/presenter.ts +52 -0
- package/src/runtime/rail-view.ts +229 -0
- package/src/runtime/rail.ts +52 -0
- package/src/runtime/rehearse.ts +264 -0
- package/src/runtime/routes.ts +142 -0
- package/src/runtime/settle.ts +45 -0
- package/src/runtime/slide.d.ts +28 -0
- package/src/runtime/socket.ts +143 -0
- package/src/runtime/stage.ts +259 -0
- package/src/runtime/step.ts +139 -0
- package/src/runtime/window.ts +38 -0
- package/src/server/auth.ts +210 -0
- package/src/server/deck-snapshot.ts +146 -0
- package/src/server/dev.ts +251 -0
- package/src/server/hub.ts +104 -0
- package/src/server/lan.ts +51 -0
- package/src/server/lock.ts +97 -0
- package/src/server/pages.ts +71 -0
- package/src/server/pairing.ts +40 -0
- package/src/server/project-watch.ts +142 -0
- package/src/server/rooms.ts +133 -0
- package/src/server/routes.ts +286 -0
- package/src/server/serial.ts +25 -0
- package/src/server/watch.ts +308 -0
- package/src/theme/default.css +175 -0
- package/src/video/bake.ts +152 -0
- package/src/video/mux.ts +103 -0
- package/src/video/recorder.ts +115 -0
- package/src/video/sidecar.ts +60 -0
- package/src/video/worker.ts +51 -0
- package/src/voice/engine.ts +177 -0
- package/src/voice/synth.ts +127 -0
- package/src/voice/wav.ts +115 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { DekError } from "./error.ts";
|
|
3
|
+
import { escapeRegExp } from "./escape.ts";
|
|
4
|
+
import { listSlideFiles } from "./resolve.ts";
|
|
5
|
+
import { stillDrawScript } from "./slide-draw.ts";
|
|
6
|
+
|
|
7
|
+
/** Slide scripts are `slides/<slug>.ts`; plain JavaScript is valid TypeScript. */
|
|
8
|
+
const transpiler = new Bun.Transpiler({ loader: "ts" });
|
|
9
|
+
|
|
10
|
+
const DEFAULT_EXPORT_RE = /\bexport\s+default\b/;
|
|
11
|
+
/** In transpiled output, comments are gone and each statement starts a line. */
|
|
12
|
+
const EXPORT_DEFAULT_LINE_RE = /^export default\b/gm;
|
|
13
|
+
const DECLARATION_RE =
|
|
14
|
+
/^export default ((?:async\s+)?function\s*\*?\s*|class\s+)([A-Za-z_$][\w$]*)/;
|
|
15
|
+
|
|
16
|
+
export type SlideScript = {
|
|
17
|
+
slug: string;
|
|
18
|
+
path: string;
|
|
19
|
+
/** A classic script that registers the slide, safe to put inside `<script>`. */
|
|
20
|
+
code: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** What stops a slide script from running: where, when the source has a place for it, and the fix. */
|
|
24
|
+
export type SlideScriptProblem = { message: string; line?: number; hint?: string };
|
|
25
|
+
|
|
26
|
+
export const IMPORTS_MESSAGE = "imports are not supported; keep the slide script self-contained";
|
|
27
|
+
|
|
28
|
+
/** The 1-based line of the first line `re` matches, if any. */
|
|
29
|
+
export function lineMatching(code: string, re: RegExp): number | undefined {
|
|
30
|
+
const index = code.split("\n").findIndex((line) => re.test(line));
|
|
31
|
+
return index < 0 ? undefined : index + 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function withLine(problem: SlideScriptProblem, line: number | undefined): SlideScriptProblem {
|
|
35
|
+
return line === undefined ? problem : { ...problem, line };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* What stops a script from being one self-contained module whose only export is the default
|
|
40
|
+
* object, found without running it.
|
|
41
|
+
*/
|
|
42
|
+
export function staticProblems(code: string): SlideScriptProblem[] {
|
|
43
|
+
let scan: ReturnType<Bun.Transpiler["scan"]>;
|
|
44
|
+
try {
|
|
45
|
+
scan = transpiler.scan(code);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return [syntaxProblem(error)];
|
|
48
|
+
}
|
|
49
|
+
const problems: SlideScriptProblem[] = [];
|
|
50
|
+
if (scan.imports.length > 0) {
|
|
51
|
+
problems.push(
|
|
52
|
+
withLine(
|
|
53
|
+
{
|
|
54
|
+
message: IMPORTS_MESSAGE,
|
|
55
|
+
hint: "remove the import and write what it gave in this file; DekSlide is global, from .dek/slide.d.ts",
|
|
56
|
+
},
|
|
57
|
+
lineMatching(code, /^\s*import\b/),
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
for (const name of scan.exports.filter((entry) => entry !== "default")) {
|
|
62
|
+
problems.push(
|
|
63
|
+
withLine(
|
|
64
|
+
{
|
|
65
|
+
message: `only a default export is allowed; found "${name}"`,
|
|
66
|
+
hint: `drop export from "${name}", or make it part of the default export`,
|
|
67
|
+
},
|
|
68
|
+
lineMatching(
|
|
69
|
+
code,
|
|
70
|
+
new RegExp(`^\\s*export\\b(?!\\s+default\\b).*\\b${escapeRegExp(name)}\\b`),
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (!scan.exports.includes("default") || !DEFAULT_EXPORT_RE.test(code)) {
|
|
76
|
+
problems.push({
|
|
77
|
+
message: "missing export default",
|
|
78
|
+
hint: "end the script with export default { draw(slide, { t }) {} } satisfies DekSlide",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return problems;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const SYNTAX_HINT = "fix the syntax there; until the script parses, the slide shows without it";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A syntax error as Bun reports it: one message with its position, or several, of which the first
|
|
88
|
+
* says the most. "Parse error" alone names neither.
|
|
89
|
+
*/
|
|
90
|
+
function syntaxProblem(error: unknown): SlideScriptProblem {
|
|
91
|
+
type Located = { message?: string; position?: { line?: number } | null };
|
|
92
|
+
const first: Located =
|
|
93
|
+
error instanceof AggregateError && error.errors.length > 0
|
|
94
|
+
? (error.errors[0] as Located)
|
|
95
|
+
: (error as Located);
|
|
96
|
+
const message = first.message ?? messageOf(error);
|
|
97
|
+
const line = first.position?.line;
|
|
98
|
+
return {
|
|
99
|
+
message: `syntax error: ${message}`,
|
|
100
|
+
...(typeof line === "number" && line > 0 ? { line } : {}),
|
|
101
|
+
hint: SYNTAX_HINT,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Turns the module into a classic script that registers its default export
|
|
107
|
+
* as `window.__dekSlides[slug]`, each slide in its own function scope. Of the
|
|
108
|
+
* lines that start with `export default`, the real one is the only one whose
|
|
109
|
+
* rewrite parses; a template literal can hold look-alikes, a comment cannot.
|
|
110
|
+
*/
|
|
111
|
+
export function compileSlideScript(
|
|
112
|
+
code: string,
|
|
113
|
+
slug: string,
|
|
114
|
+
): { code: string } | { problem: SlideScriptProblem } {
|
|
115
|
+
let js: string;
|
|
116
|
+
try {
|
|
117
|
+
js = transpiler.transformSync(code);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return { problem: syntaxProblem(error) };
|
|
120
|
+
}
|
|
121
|
+
let firstError: unknown;
|
|
122
|
+
for (const match of js.matchAll(EXPORT_DEFAULT_LINE_RE)) {
|
|
123
|
+
const body = rewriteDefaultExport(js, match.index ?? 0);
|
|
124
|
+
try {
|
|
125
|
+
const wrapped = escapeForScriptTag(wrap(body, slug, false));
|
|
126
|
+
new Function(wrapped);
|
|
127
|
+
return { code: wrapped };
|
|
128
|
+
} catch (error) {
|
|
129
|
+
firstError ??= error;
|
|
130
|
+
if (parses(wrap(body, slug, true))) {
|
|
131
|
+
return {
|
|
132
|
+
problem: withLine(
|
|
133
|
+
{
|
|
134
|
+
message:
|
|
135
|
+
"top-level await is not supported; the slide script must finish when it loads",
|
|
136
|
+
hint: "drop the await: draw from t alone, with nothing to wait for",
|
|
137
|
+
},
|
|
138
|
+
lineMatching(code, /\bawait\b/),
|
|
139
|
+
),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
problem: firstError
|
|
146
|
+
? { message: `syntax error: ${messageOf(firstError)}`, hint: SYNTAX_HINT }
|
|
147
|
+
: { message: "missing export default" },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Keeps a declaration's name bound: `export default function draw` still defines `draw`. */
|
|
152
|
+
function rewriteDefaultExport(js: string, index: number): string {
|
|
153
|
+
const rest = js.slice(index);
|
|
154
|
+
const declaration = DECLARATION_RE.exec(rest);
|
|
155
|
+
if (declaration) {
|
|
156
|
+
const kept = rest.slice("export default ".length);
|
|
157
|
+
return `${js.slice(0, index)}${kept}\n__dekDefault = ${declaration[2]};`;
|
|
158
|
+
}
|
|
159
|
+
return `${js.slice(0, index)}__dekDefault =${rest.slice("export default".length)}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function wrap(body: string, slug: string, async: boolean): string {
|
|
163
|
+
return `(window.__dekSlides ||= {})[${JSON.stringify(slug)}] = (${async ? "async " : ""}function () {
|
|
164
|
+
"use strict";
|
|
165
|
+
var __dekDefault;
|
|
166
|
+
${body}
|
|
167
|
+
return __dekDefault;
|
|
168
|
+
})();
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parses(code: string): boolean {
|
|
173
|
+
try {
|
|
174
|
+
new Function(code);
|
|
175
|
+
return true;
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** `</script` and `<!--` would end or confuse the tag; `<\/` means the same inside JS. */
|
|
182
|
+
function escapeForScriptTag(code: string): string {
|
|
183
|
+
return code.replace(/<(\/script|!--)/gi, "<\\$1");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function messageOf(error: unknown): string {
|
|
187
|
+
return error instanceof Error ? error.message : String(error);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** A slide script as read from disk: compiled, or the problem that stops it. */
|
|
191
|
+
export type SlideScriptEntry =
|
|
192
|
+
| SlideScript
|
|
193
|
+
| { slug: string; path: string; problem: SlideScriptProblem };
|
|
194
|
+
|
|
195
|
+
/** Compiles every `slides/<slug>.ts`, or only `only`'s, without deciding what a problem means. */
|
|
196
|
+
export function loadSlideScripts(deckDir: string, only?: string): SlideScriptEntry[] {
|
|
197
|
+
return listSlideFiles(deckDir, ".ts")
|
|
198
|
+
.filter((slide) => only === undefined || slide.slug === only)
|
|
199
|
+
.map((slide) => {
|
|
200
|
+
const source = readFileSync(slide.path, "utf8");
|
|
201
|
+
const problem = staticProblems(source)[0];
|
|
202
|
+
const compiled = problem ? { problem } : compileSlideScript(source, slide.slug);
|
|
203
|
+
return { slug: slide.slug, path: slide.path, ...compiled };
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The scripts that can run. A broken one is skipped and left to lint (DEK016),
|
|
209
|
+
* unless `strict`, where the render refuses to build without it.
|
|
210
|
+
*/
|
|
211
|
+
export function usableSlideScripts(entries: SlideScriptEntry[], strict: boolean): SlideScript[] {
|
|
212
|
+
return entries.flatMap((entry) => {
|
|
213
|
+
if (!("problem" in entry)) {
|
|
214
|
+
return [entry];
|
|
215
|
+
}
|
|
216
|
+
if (strict) {
|
|
217
|
+
throw new DekError(`invalid slide script "${entry.slug}": ${entry.problem.message}`, {
|
|
218
|
+
path: entry.path,
|
|
219
|
+
hint: "run `dekc lint`",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return [];
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The deck's slide scripts, compiled. The dev server skips a broken one and
|
|
228
|
+
* lets lint report it (DEK016); `strict` renders refuse to build without it.
|
|
229
|
+
* `only` limits the read to one slide, for pages that show one slide.
|
|
230
|
+
*/
|
|
231
|
+
export function readSlideScripts(
|
|
232
|
+
deckDir: string,
|
|
233
|
+
options: { strict?: boolean; only?: string } = {},
|
|
234
|
+
): SlideScript[] {
|
|
235
|
+
return usableSlideScripts(loadSlideScripts(deckDir, options.only), options.strict ?? false);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** One tag per slide, so a script that throws while loading cannot stop the others. */
|
|
239
|
+
export function slideScriptTags(scripts: SlideScript[]): string {
|
|
240
|
+
return scripts
|
|
241
|
+
.map((script) => `<script data-dek-slides="${script.slug}">${script.code}</script>`)
|
|
242
|
+
.join("");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* What a still page runs: its slides' scripts, then the still script that draws
|
|
247
|
+
* them and ends every animation. Every still page carries it, since the theme's
|
|
248
|
+
* own animations need ending even where no slide has a script.
|
|
249
|
+
*/
|
|
250
|
+
export function stillPageScript(scripts: SlideScript[]): string {
|
|
251
|
+
return `${slideScriptTags(scripts)}<script>${stillDrawScript()}</script>`;
|
|
252
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { DekError } from "./error.ts";
|
|
2
|
+
|
|
3
|
+
export async function awaitPiped(proc: {
|
|
4
|
+
stdout: ReadableStream<Uint8Array> | number;
|
|
5
|
+
stderr: ReadableStream<Uint8Array> | number;
|
|
6
|
+
exited: Promise<number>;
|
|
7
|
+
}): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
8
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
9
|
+
new Response(proc.stdout as ReadableStream<Uint8Array>).text(),
|
|
10
|
+
new Response(proc.stderr as ReadableStream<Uint8Array>).text(),
|
|
11
|
+
proc.exited,
|
|
12
|
+
]);
|
|
13
|
+
return { stdout, stderr, exitCode };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** `bin` as a command line: a .ts worker runs under bun, anything else as is. */
|
|
17
|
+
export function workerCommand(bin: string, args: string[] = []): string[] {
|
|
18
|
+
return bin.endsWith(".ts") ? ["bun", "--no-install", bin, ...args] : [bin, ...args];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A browser that hangs must not hang dek; a whole visual run or capture fits well inside this. */
|
|
22
|
+
const DEFAULT_WORKER_TIMEOUT_MS = 10 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
type PipedOptions = {
|
|
25
|
+
/** The error message when the command cannot start or overruns, such as "ffmpeg failed". */
|
|
26
|
+
label: string;
|
|
27
|
+
/** The hint when the command cannot start. */
|
|
28
|
+
hint: string;
|
|
29
|
+
/** Written to the command's stdin, which is then closed. */
|
|
30
|
+
stdin?: string;
|
|
31
|
+
/** Covers the whole run, from spawn to exit. */
|
|
32
|
+
timeoutMs: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type Piped = { stdout: string; stderr: string; exitCode: number };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Runs `cmd` to its exit with its output piped back. A command that cannot start, or runs past
|
|
39
|
+
* `timeoutMs` and is killed, is a DekError named `label`; any exit code is the caller's to judge.
|
|
40
|
+
*/
|
|
41
|
+
export async function runPiped(cmd: string[], options: PipedOptions): Promise<Piped> {
|
|
42
|
+
let proc: Bun.Subprocess<"pipe", "pipe", "pipe">;
|
|
43
|
+
try {
|
|
44
|
+
proc = Bun.spawn(cmd, { stdin: "pipe", stdout: "pipe", stderr: "pipe" });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new DekError(options.label, { hint: options.hint, cause: error });
|
|
47
|
+
}
|
|
48
|
+
let timedOut = false;
|
|
49
|
+
const timer = setTimeout(() => {
|
|
50
|
+
timedOut = true;
|
|
51
|
+
proc.kill();
|
|
52
|
+
}, options.timeoutMs);
|
|
53
|
+
try {
|
|
54
|
+
// Read before writing: a command that answers while still reading must not block on a full pipe.
|
|
55
|
+
const piped = awaitPiped(proc);
|
|
56
|
+
if (options.stdin !== undefined) {
|
|
57
|
+
proc.stdin.write(options.stdin);
|
|
58
|
+
}
|
|
59
|
+
await proc.stdin.end();
|
|
60
|
+
const result = await piped;
|
|
61
|
+
if (timedOut) {
|
|
62
|
+
throw new DekError(options.label, {
|
|
63
|
+
hint: `it did not finish within ${Math.round(options.timeoutMs / 1000)}s and was stopped`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof DekError) {
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
throw new DekError(options.label, { hint: options.hint, cause: error });
|
|
72
|
+
} finally {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type JsonWorkerOptions = {
|
|
78
|
+
/** The error message for any failure, such as "Playwright worker failed". */
|
|
79
|
+
label: string;
|
|
80
|
+
/** The hint when the worker gives no reason of its own. */
|
|
81
|
+
hint: string;
|
|
82
|
+
/** Covers the whole run, from spawn to exit. */
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Runs a worker that takes one JSON request on stdin and answers with JSON on
|
|
88
|
+
* stdout. Every failure is a DekError named `label`: a worker that exits
|
|
89
|
+
* non-zero contributes its stderr as the hint, one that overruns the timeout
|
|
90
|
+
* is killed and says so, and output `parse` rejects is invalid JSON.
|
|
91
|
+
*/
|
|
92
|
+
export async function runJsonWorker<T>(
|
|
93
|
+
cmd: string[],
|
|
94
|
+
request: unknown,
|
|
95
|
+
parse: (stdout: string) => T | null,
|
|
96
|
+
options: JsonWorkerOptions,
|
|
97
|
+
): Promise<T> {
|
|
98
|
+
const { stdout, stderr, exitCode } = await runPiped(cmd, {
|
|
99
|
+
label: options.label,
|
|
100
|
+
hint: options.hint,
|
|
101
|
+
stdin: JSON.stringify(request),
|
|
102
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_WORKER_TIMEOUT_MS,
|
|
103
|
+
});
|
|
104
|
+
if (exitCode !== 0) {
|
|
105
|
+
throw new DekError(options.label, { hint: stderr.trim().slice(0, 200) || options.hint });
|
|
106
|
+
}
|
|
107
|
+
const parsed = parse(stdout);
|
|
108
|
+
if (parsed === null) {
|
|
109
|
+
throw new DekError(options.label, { hint: "worker returned invalid JSON" });
|
|
110
|
+
}
|
|
111
|
+
return parsed;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The worker's side of `runJsonWorker`: its request, or exit 2 with the reason on stderr. */
|
|
115
|
+
export async function readWorkerRequest<T>(): Promise<T> {
|
|
116
|
+
const text = await new Response(Bun.stdin).text();
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(text) as T;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return exitWorker(error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Exits a worker with the reason on stderr, so the parent's hint says what went wrong. */
|
|
125
|
+
export function exitWorker(error: unknown): never {
|
|
126
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
127
|
+
process.exit(2);
|
|
128
|
+
}
|
package/src/core/step.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export type BeatRef = {
|
|
2
|
+
id?: string;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Where the talk stands. `beatIndex` counts the beats reached on the slide: 0 is the slide as it
|
|
7
|
+
* arrives, with nothing bound to a beat shown yet, and k is its k-th `###`.
|
|
8
|
+
*/
|
|
9
|
+
export type Position = {
|
|
10
|
+
slideIndex: number;
|
|
11
|
+
beatIndex: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** How many positions a slide has: its arrival, then one per beat. */
|
|
15
|
+
export function stopCount(beats: BeatRef[]): number {
|
|
16
|
+
return beats.length + 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The stop with every beat reached. */
|
|
20
|
+
export function lastStop(beats: BeatRef[]): number {
|
|
21
|
+
return beats.length;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The beat reached last at `beatIndex`; none as the slide arrives. */
|
|
25
|
+
export function beatAt<T extends BeatRef>(beats: T[], beatIndex: number): T | undefined {
|
|
26
|
+
return beatIndex > 0 ? beats[beatIndex - 1] : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const STOP_NUMBER_RE = /^(0|[1-9]\d*)$/;
|
|
30
|
+
|
|
31
|
+
/** The beatIndex a stop token names: `0` to the beat count by position, or a beat id. */
|
|
32
|
+
export function resolveStop(beats: BeatRef[], token: string): number | undefined {
|
|
33
|
+
if (STOP_NUMBER_RE.test(token)) {
|
|
34
|
+
const index = Number(token);
|
|
35
|
+
return index <= lastStop(beats) ? index : undefined;
|
|
36
|
+
}
|
|
37
|
+
const found = beats.findIndex((beat) => beat.id === token);
|
|
38
|
+
return found < 0 ? undefined : found + 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The beat a `data-step` value names (1-based), by id or position. The arrival is no beat. */
|
|
42
|
+
export function resolveStep(beats: BeatRef[], value: string): number | undefined {
|
|
43
|
+
const index = resolveStop(beats, value);
|
|
44
|
+
return index === 0 ? undefined : index;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The value `data-step` takes for the beat at `index` (0-based): its id, or its 1-based position. */
|
|
48
|
+
export function beatStep(beats: BeatRef[], index: number): string {
|
|
49
|
+
return beats[index]?.id ?? String(index + 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The key `motion` and `draw` use for a position: `"0"` for the arrival, else the beat's step. */
|
|
53
|
+
export function stepKey(beats: BeatRef[], beatIndex: number): string {
|
|
54
|
+
return beatIndex <= 0 ? "0" : beatStep(beats, beatIndex - 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Every `stepKey` of a slide, arrival first. */
|
|
58
|
+
export function stepKeys(beats: BeatRef[]): string[] {
|
|
59
|
+
return Array.from({ length: stopCount(beats) }, (_, index) => stepKey(beats, index));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The `data-step` values shown once `beatIndex` beats have been reached. */
|
|
63
|
+
export function stepValuesForBeat(beats: BeatRef[], beatIndex: number): Set<string> {
|
|
64
|
+
const shown = new Set<string>();
|
|
65
|
+
for (let i = 0; i < beatIndex && i < beats.length; i++) {
|
|
66
|
+
shown.add(String(i + 1));
|
|
67
|
+
const id = beats[i]?.id;
|
|
68
|
+
if (id) {
|
|
69
|
+
shown.add(id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return shown;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The values a step may take on a slide: beat ids, then the position range. Empty without beats. */
|
|
76
|
+
export function stepChoices(beats: BeatRef[]): string[] {
|
|
77
|
+
if (beats.length === 0) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
return [
|
|
81
|
+
...beats.flatMap((beat) => (beat.id ? [beat.id] : [])),
|
|
82
|
+
beats.length === 1 ? "1" : `1-${beats.length}`,
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** "use hook, turn, or 1-2" for a slide with beats. */
|
|
87
|
+
export function formatStepChoices(choices: string[]): string {
|
|
88
|
+
const head = choices.slice(0, -1);
|
|
89
|
+
const last = choices[choices.length - 1] ?? "1";
|
|
90
|
+
if (head.length === 0) {
|
|
91
|
+
return `use ${last}`;
|
|
92
|
+
}
|
|
93
|
+
return head.length === 1 ? `use ${head[0]} or ${last}` : `use ${head.join(", ")}, or ${last}`;
|
|
94
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type DeckStyles, readDeckStyles } from "./assets.ts";
|
|
2
|
+
import { playerChromeCss } from "./chrome.ts";
|
|
3
|
+
import { type DeckSlides, deckSlides, htmlShell, slidePlace, stampSlide } from "./html.ts";
|
|
4
|
+
import { type ProjectDeck, requireSection } from "./resolve.ts";
|
|
5
|
+
import { logicalSize } from "./size.ts";
|
|
6
|
+
import { SLIDE_CSS_ID } from "./slide-measure.ts";
|
|
7
|
+
import {
|
|
8
|
+
loadSlideScripts,
|
|
9
|
+
type SlideScriptEntry,
|
|
10
|
+
stillPageScript,
|
|
11
|
+
usableSlideScripts,
|
|
12
|
+
} from "./slide-script.ts";
|
|
13
|
+
import { stepKey, stepValuesForBeat } from "./step.ts";
|
|
14
|
+
|
|
15
|
+
/** What a run of still pages shares, read once instead of once per page. */
|
|
16
|
+
export type SlideSources = {
|
|
17
|
+
deck: ProjectDeck;
|
|
18
|
+
styles: DeckStyles;
|
|
19
|
+
slides: DeckSlides;
|
|
20
|
+
scripts: SlideScriptEntry[];
|
|
21
|
+
/** Whether a broken slide script fails its page (shots) or is skipped (visual lint). */
|
|
22
|
+
strict: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function loadSlideSources(
|
|
26
|
+
deck: ProjectDeck,
|
|
27
|
+
options: { strict?: boolean } = {},
|
|
28
|
+
): SlideSources {
|
|
29
|
+
return {
|
|
30
|
+
deck,
|
|
31
|
+
styles: readDeckStyles(deck.dir, false),
|
|
32
|
+
slides: deckSlides(deck),
|
|
33
|
+
scripts: loadSlideScripts(deck.dir),
|
|
34
|
+
strict: options.strict ?? true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One slide alone on a page, held at `beatIndex`, as shots and visual lint measure it. Its own
|
|
40
|
+
* CSS sits in a style element of its own after the theme, and no other slide's comes along.
|
|
41
|
+
*/
|
|
42
|
+
export function renderSlideHtml(sources: SlideSources, slug: string, beatIndex: number): string {
|
|
43
|
+
const { deck } = sources;
|
|
44
|
+
const section = requireSection(deck, slug);
|
|
45
|
+
const { sections } = deck.deck;
|
|
46
|
+
const slide = stampSlide(sources.slides.section(slug), {
|
|
47
|
+
slug,
|
|
48
|
+
place: slidePlace(sections, sections.indexOf(section)),
|
|
49
|
+
shown: stepValuesForBeat(section.beats, beatIndex),
|
|
50
|
+
beat: { index: beatIndex, step: stepKey(section.beats, beatIndex) },
|
|
51
|
+
inline: { deckDir: deck.dir },
|
|
52
|
+
});
|
|
53
|
+
const scripts = usableSlideScripts(
|
|
54
|
+
sources.scripts.filter((entry) => entry.slug === slug),
|
|
55
|
+
sources.strict,
|
|
56
|
+
);
|
|
57
|
+
const own = sources.styles.slides.get(slug);
|
|
58
|
+
return htmlShell({
|
|
59
|
+
lang: deck.deck.lang,
|
|
60
|
+
head: `<style>${playerChromeCss(logicalSize(deck.deck.ratio))}</style>
|
|
61
|
+
<style>${sources.styles.theme}</style>${
|
|
62
|
+
own === undefined
|
|
63
|
+
? ""
|
|
64
|
+
: `
|
|
65
|
+
<style id="${SLIDE_CSS_ID}">${own}</style>`
|
|
66
|
+
}`,
|
|
67
|
+
body: `<div id="deck">${slide}</div>
|
|
68
|
+
${stillPageScript(scripts)}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** The candidate within two edits of `word` (transpositions count as one), closest first. */
|
|
2
|
+
export function suggest(word: string, candidates: string[]): string | undefined {
|
|
3
|
+
let best: { name: string; distance: number } | undefined;
|
|
4
|
+
for (const name of candidates) {
|
|
5
|
+
const distance = editDistance(word, name);
|
|
6
|
+
if (distance <= 2 && (!best || distance < best.distance)) {
|
|
7
|
+
best = { name, distance };
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return best?.name;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Optimal string alignment distance: insertions, deletions, substitutions, adjacent swaps. */
|
|
14
|
+
function editDistance(a: string, b: string): number {
|
|
15
|
+
const rows = Array.from({ length: a.length + 1 }, (_, i) =>
|
|
16
|
+
Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
|
|
17
|
+
);
|
|
18
|
+
const at = (i: number, j: number): number => rows[i]?.[j] ?? Number.POSITIVE_INFINITY;
|
|
19
|
+
for (let i = 1; i <= a.length; i++) {
|
|
20
|
+
for (let j = 1; j <= b.length; j++) {
|
|
21
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
22
|
+
let value = Math.min(at(i - 1, j) + 1, at(i, j - 1) + 1, at(i - 1, j - 1) + cost);
|
|
23
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
24
|
+
value = Math.min(value, at(i - 2, j - 2) + 1);
|
|
25
|
+
}
|
|
26
|
+
const row = rows[i];
|
|
27
|
+
if (row) {
|
|
28
|
+
row[j] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return at(a.length, b.length);
|
|
33
|
+
}
|