@hajimism/dek 0.2.0 → 0.2.2
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/CHANGELOG.md +32 -0
- package/README.ja.md +3 -1
- package/README.md +3 -1
- package/package.json +3 -1
- package/skills/dek/SKILL.md +44 -0
- package/src/cli/annotations.ts +26 -0
- package/src/cli/commands.ts +66 -0
- package/src/cli/contract.ts +75 -4
- package/src/cli/files.ts +3 -3
- package/src/cli/flags.ts +1 -1
- package/src/cli/format.ts +3 -0
- package/src/cli/pptx.ts +20 -0
- package/src/cli/serve.ts +1 -1
- package/src/cli/show.ts +17 -2
- package/src/cli/text.ts +43 -0
- package/src/cli/theme.ts +7 -1
- package/src/core/annotation-rows.ts +176 -0
- package/src/core/annotations.ts +362 -0
- package/src/core/chrome.ts +3 -1
- package/src/core/diagnostic.ts +1 -0
- package/src/core/document.ts +17 -2
- package/src/core/html-scan.ts +54 -0
- package/src/core/html.ts +144 -14
- package/src/core/lint/slide-html.ts +92 -2
- package/src/core/lint.ts +8 -1
- package/src/core/live-protocol.ts +4 -1
- package/src/core/page.ts +7 -0
- package/src/core/playwright-module.d.ts +4 -1
- package/src/core/playwright-visual.ts +74 -1
- package/src/core/playwright.ts +51 -1
- package/src/core/pptx-package.ts +312 -0
- package/src/core/pptx-text.ts +363 -0
- package/src/core/pptx.ts +75 -0
- package/src/core/slide-measure.ts +1 -0
- package/src/core/sync.ts +13 -5
- package/src/core/theme-excerpt.ts +61 -9
- package/src/core/theme-facts.ts +12 -2
- package/src/core/zip.ts +77 -0
- package/src/runtime/annotate-pick.ts +144 -0
- package/src/runtime/annotate-place.ts +46 -0
- package/src/runtime/annotate-view.ts +752 -0
- package/src/runtime/annotate.ts +19 -0
- package/src/runtime/browser.ts +5 -1
- package/src/runtime/live.ts +1 -0
- package/src/runtime/player.ts +15 -7
- package/src/runtime/routes.ts +3 -0
- package/src/server/auth.ts +3 -1
- package/src/server/dev.ts +2 -1
- package/src/server/pages.ts +2 -1
- package/src/server/routes.ts +77 -4
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
// Annotate mode's entry: compiled apart from the player and embedded only on the dev server's
|
|
3
|
+
// pages for the speaker, so a built file carries none of it.
|
|
4
|
+
|
|
5
|
+
import { PAGE_ID, readPageConfig } from "../core/page.ts";
|
|
6
|
+
import type { PresenterSlide } from "../core/presenter-state.ts";
|
|
7
|
+
import { createAnnotateView } from "./annotate-view.ts";
|
|
8
|
+
import { deckUrl, liveTokenQuery } from "./routes.ts";
|
|
9
|
+
|
|
10
|
+
const dataEl = document.getElementById(PAGE_ID.data);
|
|
11
|
+
if (dataEl?.textContent) {
|
|
12
|
+
const page = readPageConfig(document.body);
|
|
13
|
+
createAnnotateView({
|
|
14
|
+
deck: page.deck,
|
|
15
|
+
slides: JSON.parse(dataEl.textContent) as PresenterSlide[],
|
|
16
|
+
url: `${deckUrl(location.pathname, { kind: "annotations" })}${liveTokenQuery(page.liveToken)}`,
|
|
17
|
+
fetch: (input, init) => fetch(input, init),
|
|
18
|
+
});
|
|
19
|
+
}
|
package/src/runtime/browser.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
|
|
3
3
|
import { isLiveEvent } from "../core/live-protocol.ts";
|
|
4
|
-
import { PAGE_ID, readPageConfig } from "../core/page.ts";
|
|
4
|
+
import { ANNOTATIONS_CHANGED, PAGE_ID, readPageConfig } from "../core/page.ts";
|
|
5
5
|
import { deckStops, moveTarget, positionsEqual } from "../core/position.ts";
|
|
6
6
|
import type { PresenterSlide } from "../core/presenter-state.ts";
|
|
7
7
|
import type { Position } from "../core/step.ts";
|
|
@@ -185,6 +185,10 @@ if (dataEl?.textContent) {
|
|
|
185
185
|
}
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
|
+
if (raw.type === "annotations") {
|
|
189
|
+
document.dispatchEvent(new Event(ANNOTATIONS_CHANGED));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
188
192
|
const event = await hydrateLiveEvent(raw, location.pathname);
|
|
189
193
|
if (!event) {
|
|
190
194
|
location.reload();
|
package/src/runtime/live.ts
CHANGED
package/src/runtime/player.ts
CHANGED
|
@@ -2,27 +2,35 @@ import { join } from "node:path";
|
|
|
2
2
|
|
|
3
3
|
export { liveReloadScript } from "./live-reload.ts";
|
|
4
4
|
|
|
5
|
-
let
|
|
5
|
+
let player: Promise<string> | undefined;
|
|
6
|
+
let annotate: Promise<string> | undefined;
|
|
6
7
|
|
|
8
|
+
/** The player every deck page runs. */
|
|
7
9
|
export function playerScript(): Promise<string> {
|
|
8
|
-
|
|
9
|
-
return
|
|
10
|
+
player ??= compile("browser.ts", "player");
|
|
11
|
+
return player;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
/** Annotate mode, which only the dev server's pages for the speaker run, after the player. */
|
|
15
|
+
export function annotateScript(): Promise<string> {
|
|
16
|
+
annotate ??= compile("annotate.ts", "annotate mode");
|
|
17
|
+
return annotate;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function compile(entry: string, what: string): Promise<string> {
|
|
13
21
|
const result = await Bun.build({
|
|
14
|
-
entrypoints: [join(import.meta.dir,
|
|
22
|
+
entrypoints: [join(import.meta.dir, entry)],
|
|
15
23
|
target: "browser",
|
|
16
24
|
format: "iife",
|
|
17
25
|
minify: true,
|
|
18
26
|
});
|
|
19
27
|
if (!result.success) {
|
|
20
28
|
const details = result.logs.map((log) => String(log)).join("\n");
|
|
21
|
-
throw new Error(`failed to compile
|
|
29
|
+
throw new Error(`failed to compile ${what}\n${details}`);
|
|
22
30
|
}
|
|
23
31
|
const output = result.outputs[0];
|
|
24
32
|
if (!output) {
|
|
25
|
-
throw new Error(
|
|
33
|
+
throw new Error(`failed to compile ${what}: no output`);
|
|
26
34
|
}
|
|
27
35
|
return output.text();
|
|
28
36
|
}
|
package/src/runtime/routes.ts
CHANGED
|
@@ -20,6 +20,8 @@ export type DeckRoute =
|
|
|
20
20
|
| { kind: "goto" }
|
|
21
21
|
/** The beats the presenter marked to rewrite: listed, and one marked or unmarked. */
|
|
22
22
|
| { kind: "marks" }
|
|
23
|
+
/** The notes annotate mode keeps on elements of the slides: listed, and one added or changed. */
|
|
24
|
+
| { kind: "annotations" }
|
|
23
25
|
/** A file under the deck's `assets/`, decoded. */
|
|
24
26
|
| { kind: "asset"; path: string };
|
|
25
27
|
|
|
@@ -34,6 +36,7 @@ const FIXED: Record<Fixed, string> = {
|
|
|
34
36
|
current: "/current",
|
|
35
37
|
goto: "/goto",
|
|
36
38
|
marks: "/marks",
|
|
39
|
+
annotations: "/annotations",
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
/** The path of `route` below its deck's prefix. */
|
package/src/server/auth.ts
CHANGED
|
@@ -86,7 +86,7 @@ export type Exposure = "audience" | "presenter";
|
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
88
|
* What each dev server route reveals. The script, its notes, the voice read from it, moving the
|
|
89
|
-
* deck,
|
|
89
|
+
* deck, the marks left on its beats, and the annotations on its slides belong to the presenter. The server routes only by these names, so a new route does
|
|
90
90
|
* not compile until it is placed here.
|
|
91
91
|
*/
|
|
92
92
|
export const ROUTE_EXPOSURE = {
|
|
@@ -101,6 +101,7 @@ export const ROUTE_EXPOSURE = {
|
|
|
101
101
|
voice: "presenter",
|
|
102
102
|
control: "presenter",
|
|
103
103
|
marks: "presenter",
|
|
104
|
+
annotations: "presenter",
|
|
104
105
|
missing: "audience",
|
|
105
106
|
} as const satisfies Record<string, Exposure>;
|
|
106
107
|
|
|
@@ -117,6 +118,7 @@ export const EVENT_EXPOSURE: Record<LiveEventType, Exposure> = {
|
|
|
117
118
|
"reload-script": "audience",
|
|
118
119
|
diagnostics: "presenter",
|
|
119
120
|
timeline: "presenter",
|
|
121
|
+
annotations: "presenter",
|
|
120
122
|
};
|
|
121
123
|
|
|
122
124
|
/** True when a request cleared for `seen` may read what `exposure` names. */
|
package/src/server/dev.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { join } from "node:path";
|
|
|
2
2
|
import { DekError } from "../core/error.ts";
|
|
3
3
|
import type { PlaywrightRunner } from "../core/playwright.ts";
|
|
4
4
|
import { locateDeck, type Project, resolveProject } from "../core/resolve.ts";
|
|
5
|
-
import { liveReloadScript, playerScript } from "../runtime/player.ts";
|
|
5
|
+
import { annotateScript, liveReloadScript, playerScript } from "../runtime/player.ts";
|
|
6
6
|
import {
|
|
7
7
|
clearance,
|
|
8
8
|
mayRead,
|
|
@@ -71,6 +71,7 @@ export async function startDevServer(options: DevServerOptions): Promise<DevServ
|
|
|
71
71
|
const embed = {
|
|
72
72
|
playerScript: await playerScript(),
|
|
73
73
|
liveReloadScript: liveReloadScript(),
|
|
74
|
+
annotateScript: await annotateScript(),
|
|
74
75
|
};
|
|
75
76
|
|
|
76
77
|
const pages = createDevPages({
|
package/src/server/pages.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type DevPages = {
|
|
|
13
13
|
|
|
14
14
|
/** The pages a dev server renders, each kept until `clear`. */
|
|
15
15
|
export function createDevPages(options: {
|
|
16
|
-
embed: { playerScript: string; liveReloadScript: string };
|
|
16
|
+
embed: { playerScript: string; liveReloadScript: string; annotateScript: string };
|
|
17
17
|
remote: boolean;
|
|
18
18
|
/** Embedded on the presenter page, so its stream and moves are let through. */
|
|
19
19
|
password: string | undefined;
|
|
@@ -44,6 +44,7 @@ export function createDevPages(options: {
|
|
|
44
44
|
mode,
|
|
45
45
|
includeNotes: mode === "presenter" || !remote,
|
|
46
46
|
liveReloadScript: embed.liveReloadScript,
|
|
47
|
+
annotateScript: embed.annotateScript,
|
|
47
48
|
...(mode === "presenter" && password ? { liveToken: password } : {}),
|
|
48
49
|
},
|
|
49
50
|
}),
|
package/src/server/routes.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import type { AnnotationRow } from "../core/annotation-rows.ts";
|
|
4
|
+
import { AnnotationOp, applyAnnotationOp, listAnnotations } from "../core/annotations.ts";
|
|
3
5
|
import { fileInside, readTheme } from "../core/assets.ts";
|
|
4
6
|
import { DekError, errorFields } from "../core/error.ts";
|
|
5
7
|
import { escapeAttr, escapeHtml } from "../core/escape.ts";
|
|
@@ -75,10 +77,16 @@ export function routeRequest(req: Request, ctx: RouteContext): Routed {
|
|
|
75
77
|
// A slide the script does not list has no place in the deck, so no page shows it.
|
|
76
78
|
const sections = deck?.deck.sections ?? [];
|
|
77
79
|
const index = sections.findIndex((section) => section.slug === route.slug);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
+
const slides = deck && index >= 0 ? deckSlides(deck) : undefined;
|
|
81
|
+
const html = slides?.written(route.slug);
|
|
82
|
+
const source = slides?.source(route.slug);
|
|
83
|
+
return html && source
|
|
80
84
|
? htmlResponse(
|
|
81
|
-
stampSlide(html, {
|
|
85
|
+
stampSlide(html, {
|
|
86
|
+
slug: route.slug,
|
|
87
|
+
place: slidePlace(sections, index),
|
|
88
|
+
source,
|
|
89
|
+
}),
|
|
82
90
|
)
|
|
83
91
|
: notFound();
|
|
84
92
|
},
|
|
@@ -110,6 +118,16 @@ export function routeRequest(req: Request, ctx: RouteContext): Routed {
|
|
|
110
118
|
respond: () =>
|
|
111
119
|
deck ? marksResponse(req, ctx.project().root, deck) : jsonNotFound(deckName),
|
|
112
120
|
};
|
|
121
|
+
case "annotations":
|
|
122
|
+
return {
|
|
123
|
+
route: "annotations",
|
|
124
|
+
respond: () =>
|
|
125
|
+
deck
|
|
126
|
+
? annotationsResponse(req, ctx.project().root, deck, () =>
|
|
127
|
+
ctx.hub.emit({ type: "annotations", deck: deckName }),
|
|
128
|
+
)
|
|
129
|
+
: jsonNotFound(deckName),
|
|
130
|
+
};
|
|
113
131
|
case "asset": {
|
|
114
132
|
const asset = dir && safeDeckAsset(dir, route.path);
|
|
115
133
|
return asset
|
|
@@ -211,6 +229,61 @@ async function marksResponse(req: Request, root: string, deck: ProjectDeck): Pro
|
|
|
211
229
|
}
|
|
212
230
|
}
|
|
213
231
|
|
|
232
|
+
/**
|
|
233
|
+
* The deck's annotations: GET lists them as the files have them now, and a POST of an operation
|
|
234
|
+
* adds, edits, removes, clears, or puts back. Every change is told to the deck's other pages,
|
|
235
|
+
* so tabs and devices annotating one deck show the same notes. One that names nothing on the
|
|
236
|
+
* slide as it is now, sent from a page an edit left behind, is refused.
|
|
237
|
+
*/
|
|
238
|
+
async function annotationsResponse(
|
|
239
|
+
req: Request,
|
|
240
|
+
root: string,
|
|
241
|
+
deck: ProjectDeck,
|
|
242
|
+
changed: () => void,
|
|
243
|
+
): Promise<Response> {
|
|
244
|
+
const rows = (annotations: AnnotationRow[]) =>
|
|
245
|
+
annotations.map((row) => ({
|
|
246
|
+
...row,
|
|
247
|
+
targets: row.targets.map((target) => ({ ...target, path: relative(root, target.path) })),
|
|
248
|
+
changed: row.changed.map((path) => relative(root, path)),
|
|
249
|
+
}));
|
|
250
|
+
try {
|
|
251
|
+
if (req.method === "GET") {
|
|
252
|
+
return jsonResponse({ ok: true, annotations: rows(listAnnotations(root, deck)) });
|
|
253
|
+
}
|
|
254
|
+
if (req.method !== "POST") {
|
|
255
|
+
return new Response("Method not allowed", { status: 405 });
|
|
256
|
+
}
|
|
257
|
+
let body: unknown;
|
|
258
|
+
try {
|
|
259
|
+
body = await req.json();
|
|
260
|
+
} catch {
|
|
261
|
+
body = undefined;
|
|
262
|
+
}
|
|
263
|
+
const op = AnnotationOp.safeParse(body);
|
|
264
|
+
if (!op.success) {
|
|
265
|
+
return jsonResponse(
|
|
266
|
+
{
|
|
267
|
+
ok: false,
|
|
268
|
+
error: {
|
|
269
|
+
message:
|
|
270
|
+
'the request names no operation: send {"op": "add" | "edit" | "remove" | "clear" | "restore", …}',
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
400,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const done = applyAnnotationOp(root, deck, op.data);
|
|
277
|
+
changed();
|
|
278
|
+
return jsonResponse({ ok: true, ...done, annotations: rows(done.annotations) });
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (!(error instanceof DekError)) {
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
return jsonResponse({ ok: false, error: errorFields(error) }, 409);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
214
287
|
function jsonNotFound(deckName: string): Response {
|
|
215
288
|
return jsonResponse({ ok: false, error: { message: `deck "${deckName}" not found` } }, 404);
|
|
216
289
|
}
|