@hajimism/dek 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.ja.md +6 -2
- package/README.md +6 -2
- package/package.json +11 -3
- package/src/cli/files.ts +14 -7
- package/src/cli/ls.ts +3 -5
- package/src/cli/new.ts +3 -2
- package/src/cli/serve.ts +1 -1
- package/src/core/build.ts +0 -1
- package/src/core/chrome.ts +3 -1
- package/src/core/config.ts +10 -16
- package/src/core/document.ts +20 -7
- package/src/core/html.ts +99 -14
- package/src/core/lint/keys.ts +47 -4
- package/src/core/lint/slide-html.ts +32 -8
- package/src/core/lint/theme-css.ts +1 -1
- package/src/core/lint/timing.ts +3 -7
- package/src/core/page.ts +1 -0
- package/src/core/presenter.ts +1 -3
- package/src/core/schema.ts +24 -0
- package/src/core/sync.ts +1 -1
- package/src/core/timing.ts +17 -12
- package/src/core/video-document.ts +1 -3
- package/src/runtime/annotate-notes.ts +207 -0
- package/src/runtime/annotate-pick.ts +148 -0
- package/src/runtime/annotate-place.ts +46 -0
- package/src/runtime/annotate-view.ts +738 -0
- package/src/runtime/annotate.ts +15 -0
- package/src/runtime/player.ts +15 -7
- package/src/server/dev.ts +2 -1
- package/src/server/pages.ts +2 -1
- package/src/server/routes.ts +9 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Release notes for `@hajimism/dek`. From 0.1.0 on, release-please writes this file from the Conventional Commits on `main`.
|
|
4
|
+
|
|
5
|
+
## [0.2.1](https://github.com/hajimism/dek/compare/v0.2.0...v0.2.1) (2026-10-06)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **dev:** annotate slide elements and copy the notes for an agent ([8565bd5](https://github.com/hajimism/dek/commit/8565bd5a8d385a84c10d4fa78cab1b7460a86389))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Documentation
|
|
14
|
+
|
|
15
|
+
* **sample:** show annotate mode on a roadmap in with-agents ([c1d9c4c](https://github.com/hajimism/dek/commit/c1d9c4c6ddd6306748bdd5544b868fbf3d170a6c))
|
|
16
|
+
|
|
17
|
+
## [0.2.0](https://github.com/hajimism/dek/compare/v0.1.0...v0.2.0) (2026-10-01)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### ⚠ BREAKING CHANGES
|
|
21
|
+
|
|
22
|
+
* max_classes, cjk_per_minute and latin_per_minute in dek.toml no longer apply to existing decks. Add them to each deck's script.md frontmatter to keep them; lint's DEK008 names each one.
|
|
23
|
+
|
|
24
|
+
### Features
|
|
25
|
+
|
|
26
|
+
* let a deck own its class budget and speaking rate ([8dc9a07](https://github.com/hajimism/dek/commit/8dc9a07efb32b803e685a353ee628cae21a8bc56))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
* **lint:** count vbscript: and script-running data: URLs in DEK011 ([1b825b1](https://github.com/hajimism/dek/commit/1b825b1bffc33809ef0bdf3a8e5a1d45794a41a7))
|
|
32
|
+
|
|
33
|
+
## 0.1.0 (2026-10-01)
|
|
34
|
+
|
|
35
|
+
The first release on npm.
|
|
36
|
+
|
|
37
|
+
### Features
|
|
38
|
+
|
|
39
|
+
* Published as `@hajimism/dek`. Create a project with `bunx @hajimism/dek init`, then install it with `bun add -d @hajimism/dek`.
|
|
40
|
+
|
|
41
|
+
### ⚠ BREAKING CHANGES
|
|
42
|
+
|
|
43
|
+
* The command is `dekc`, not `dek`. `dek` on npm is an unrelated package, and `bunx dek` ran it anywhere dek was not installed. Run `bunx dekc` inside a project; the package, `dek.toml`, `.dek/`, and the `DEK` lint codes keep their names.
|
package/README.ja.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# dek
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@hajimism/dek) [](https://github.com/hajimism/dek/actions/workflows/ci.yml) [](https://scorecard.dev/viewer/?uri=github.com/hajimism/dek)
|
|
4
|
+
|
|
3
5
|
**発表のビルドシステム。** 喋ることを書けば、あとは dek が組み立て、測り、届ける。
|
|
4
6
|
|
|
5
7
|
[ドキュメント](https://hajimism.github.io/dek/ja/) · [English README](./README.md)
|
|
@@ -20,7 +22,7 @@ dek は、ビルドシステムがコードを扱うように発表を扱う CLI
|
|
|
20
22
|
|
|
21
23
|
## クイックスタート
|
|
22
24
|
|
|
23
|
-
[Bun](https://bun.sh) 1.4
|
|
25
|
+
[Bun](https://bun.sh) 1.4 以上が必要です。dek は Bun でだけ動き、Node では動きません。`npx` ではなく `bunx` を使ってください。
|
|
24
26
|
|
|
25
27
|
```bash
|
|
26
28
|
bunx @hajimism/dek init my-talks --deck 2026-04-vite
|
|
@@ -66,10 +68,12 @@ bun run check # Biome の format + lint
|
|
|
66
68
|
bun run docs:dev # VitePress
|
|
67
69
|
```
|
|
68
70
|
|
|
69
|
-
CI では `biome ci`、`typecheck`、全テスト(ブラウザを使うものは Chromium で)、`docs:build
|
|
71
|
+
CI では `biome ci`、`typecheck`、`knip`、全テスト(ブラウザを使うものは Chromium で)、`docs:build`、`scripts/check-package.sh`(npm に出す tarball を入れて動かす)を走らせます。
|
|
70
72
|
|
|
71
73
|
ドキュメントサイトは GitHub Actions 経由で GitHub Pages に出します。初回だけ、リポジトリの設定で **Pages → Source** を **GitHub Actions** にしてください。
|
|
72
74
|
|
|
75
|
+
プルリクエストで通すチェックは [CONTRIBUTING.md](./CONTRIBUTING.md)、リリースごとの変更は [CHANGELOG.md](./CHANGELOG.md) にあります。脆弱性は [SECURITY.md](./SECURITY.md) のとおり非公開で報告してください。
|
|
76
|
+
|
|
73
77
|
## ライセンス
|
|
74
78
|
|
|
75
79
|
MIT
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# dek
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@hajimism/dek) [](https://github.com/hajimism/dek/actions/workflows/ci.yml) [](https://scorecard.dev/viewer/?uri=github.com/hajimism/dek)
|
|
4
|
+
|
|
3
5
|
**A build system for talks.** Write what you will say; dek builds, measures, and ships the rest.
|
|
4
6
|
|
|
5
7
|
[Documentation](https://hajimism.github.io/dek/) · [日本語 README](./README.ja.md)
|
|
@@ -20,7 +22,7 @@ The three principles and the comparison with Slidev are in [Why dek](https://haj
|
|
|
20
22
|
|
|
21
23
|
## Quick start
|
|
22
24
|
|
|
23
|
-
[Bun](https://bun.sh) 1.4 or later.
|
|
25
|
+
Requires [Bun](https://bun.sh) 1.4 or later. dek runs on Bun only: it does not run on Node, so use `bunx`, not `npx`.
|
|
24
26
|
|
|
25
27
|
```bash
|
|
26
28
|
bunx @hajimism/dek init my-talks --deck 2026-04-vite
|
|
@@ -66,10 +68,12 @@ bun run check # Biome format + lint
|
|
|
66
68
|
bun run docs:dev # VitePress
|
|
67
69
|
```
|
|
68
70
|
|
|
69
|
-
CI runs `biome ci`, `typecheck`, the full test suite (the browser tests in Chromium),
|
|
71
|
+
CI runs `biome ci`, `typecheck`, `knip`, the full test suite (the browser tests in Chromium), `docs:build`, and `scripts/check-package.sh`, which installs the packed tarball and runs it.
|
|
70
72
|
|
|
71
73
|
The documentation site deploys to GitHub Pages through Actions. Once, in the repository settings, set **Pages → Source** to **GitHub Actions**.
|
|
72
74
|
|
|
75
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the checks a pull request runs, and [CHANGELOG.md](./CHANGELOG.md) for what each release changed. Report security issues privately, as [SECURITY.md](./SECURITY.md) describes.
|
|
76
|
+
|
|
73
77
|
## License
|
|
74
78
|
|
|
75
79
|
MIT
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hajimism/dek",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A build system for talks.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": "Hajime Nakagawa (https://github.com/hajimism)",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"bin": {
|
|
8
9
|
"dekc": "src/cli.ts"
|
|
@@ -11,7 +12,8 @@
|
|
|
11
12
|
"src",
|
|
12
13
|
"LICENSE",
|
|
13
14
|
"README.md",
|
|
14
|
-
"README.ja.md"
|
|
15
|
+
"README.ja.md",
|
|
16
|
+
"CHANGELOG.md"
|
|
15
17
|
],
|
|
16
18
|
"repository": {
|
|
17
19
|
"type": "git",
|
|
@@ -31,9 +33,12 @@
|
|
|
31
33
|
"slides",
|
|
32
34
|
"presentation",
|
|
33
35
|
"talk",
|
|
36
|
+
"markdown",
|
|
34
37
|
"build",
|
|
35
38
|
"cli",
|
|
36
|
-
"html"
|
|
39
|
+
"html",
|
|
40
|
+
"bun",
|
|
41
|
+
"ai-agents"
|
|
37
42
|
],
|
|
38
43
|
"scripts": {
|
|
39
44
|
"test": "bun test --parallel=4",
|
|
@@ -61,5 +66,8 @@
|
|
|
61
66
|
"playwright": "^1.63.0",
|
|
62
67
|
"typescript": "^7.0.2",
|
|
63
68
|
"vitepress": "^1.6.4"
|
|
69
|
+
},
|
|
70
|
+
"overrides": {
|
|
71
|
+
"vite": "^6.4.3"
|
|
64
72
|
}
|
|
65
73
|
}
|
package/src/cli/files.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, relative } from "node:path";
|
|
3
3
|
import { bundledTheme } from "../core/bundled-theme.ts";
|
|
4
|
+
import type { DekConfig } from "../core/config.ts";
|
|
4
5
|
import { deckPaths } from "../core/deck-paths.ts";
|
|
5
6
|
import { DekError } from "../core/error.ts";
|
|
6
7
|
import { walkUp } from "../core/optional.ts";
|
|
@@ -72,12 +73,17 @@ const INSTALL_DEK = "bun add -d @hajimism/dek";
|
|
|
72
73
|
* shows sections, beats, and an estimate against the budget. A deck with a
|
|
73
74
|
* voice starts in Japanese, the language dek's voice reads; any other in English.
|
|
74
75
|
*/
|
|
75
|
-
export function defaultScript(
|
|
76
|
+
export function defaultScript(
|
|
77
|
+
title: string,
|
|
78
|
+
lang: "en" | "ja" = "en",
|
|
79
|
+
settings: DekConfig["seed"] = {},
|
|
80
|
+
): string {
|
|
81
|
+
const lines = Object.entries(settings).map(([key, value]) => `${key}: ${value}\n`);
|
|
76
82
|
return `---
|
|
77
83
|
# yaml-language-server: $schema=../../.dek/schema.json
|
|
78
84
|
title: ${JSON.stringify(title)}
|
|
79
85
|
duration: 1m
|
|
80
|
-
---
|
|
86
|
+
${lines.join("")}---
|
|
81
87
|
${STARTER_BODY[lang]}`;
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -230,18 +236,19 @@ function placeable(path: string, kind: "file" | "directory"): boolean {
|
|
|
230
236
|
}
|
|
231
237
|
|
|
232
238
|
/**
|
|
233
|
-
* A deck's files: its directories, the starter script, the theme it starts
|
|
234
|
-
*
|
|
235
|
-
*
|
|
239
|
+
* A new deck's files: its directories, the starter script, the theme it starts from, and what the
|
|
240
|
+
* project's dek.toml seeds, its deck settings and `[voice]`. From then on the deck owns them: a
|
|
241
|
+
* later change to dek.toml leaves it alone. The skeleton slides are sync's to write, once the
|
|
242
|
+
* theme they are drawn in is in place.
|
|
236
243
|
*/
|
|
237
244
|
export function deckPlan(
|
|
238
245
|
root: string,
|
|
239
246
|
name: string,
|
|
240
247
|
theme: string,
|
|
241
|
-
|
|
248
|
+
{ seed, voice }: Pick<DekConfig, "seed" | "voice"> = {},
|
|
242
249
|
): PlannedPath[] {
|
|
243
250
|
const paths = deckPaths(join(root, "decks", name));
|
|
244
|
-
const script = defaultScript(name, voice ? "ja" : "en");
|
|
251
|
+
const script = defaultScript(name, voice ? "ja" : "en", seed);
|
|
245
252
|
const plan: PlannedPath[] = [
|
|
246
253
|
{ path: paths.slides },
|
|
247
254
|
{ path: paths.assets },
|
package/src/cli/ls.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type DekConfig, loadConfig } from "../core/config.ts";
|
|
2
1
|
import type { Diagnostic } from "../core/diagnostic.ts";
|
|
3
2
|
import { lintDeck, lintProject } from "../core/lint.ts";
|
|
4
3
|
import { listSlides, type Project, type ProjectDeck } from "../core/resolve.ts";
|
|
@@ -54,7 +53,7 @@ export function lsCommand(target: DecksTarget): LsListResult | LsDeckResult {
|
|
|
54
53
|
const { project, deck, ref } = target;
|
|
55
54
|
if (deck && ref) {
|
|
56
55
|
return {
|
|
57
|
-
...formatDeck(deck,
|
|
56
|
+
...formatDeck(deck, project, { lint: false }),
|
|
58
57
|
...skippedChecks([
|
|
59
58
|
{ check: "lint", reason: "a ref is read-only; its problems are not yours to fix" },
|
|
60
59
|
]),
|
|
@@ -62,7 +61,7 @@ export function lsCommand(target: DecksTarget): LsListResult | LsDeckResult {
|
|
|
62
61
|
};
|
|
63
62
|
}
|
|
64
63
|
if (deck) {
|
|
65
|
-
return formatDeck(deck,
|
|
64
|
+
return formatDeck(deck, project);
|
|
66
65
|
}
|
|
67
66
|
return {
|
|
68
67
|
kind: "list",
|
|
@@ -85,11 +84,10 @@ function summarizeDeck(deck: ProjectDeck, project: Project): LsListResult["decks
|
|
|
85
84
|
|
|
86
85
|
function formatDeck(
|
|
87
86
|
deck: ProjectDeck,
|
|
88
|
-
config: DekConfig,
|
|
89
87
|
project: Project,
|
|
90
88
|
options: { lint: boolean } = { lint: true },
|
|
91
89
|
): LsDeckResult {
|
|
92
|
-
const timing = sectionTiming(deck.deck
|
|
90
|
+
const timing = sectionTiming(deck.deck);
|
|
93
91
|
const estimateTotal = timing.reduce((sum, row) => sum + row.estimateSeconds, 0);
|
|
94
92
|
const timeline = tryLoadCachedTimeline(deck.dir);
|
|
95
93
|
|
package/src/cli/new.ts
CHANGED
|
@@ -48,8 +48,9 @@ export function newCommand(options: { cwd: string; name: string; themeFrom?: str
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const theme = readFileSync(themeSource, "utf8");
|
|
51
|
-
const
|
|
52
|
-
|
|
51
|
+
const { created } = applyPlan(
|
|
52
|
+
deckPlan(project.root, name, theme, loadConfig(project.configPath)),
|
|
53
|
+
);
|
|
53
54
|
// As sync would. AGENTS.md and .dek/ follow the project theme and dek, so they change here
|
|
54
55
|
// only when either moved on since the last command that wrote them: say which.
|
|
55
56
|
const synced = syncDeck(dir);
|
package/src/cli/serve.ts
CHANGED
|
@@ -121,5 +121,5 @@ export function devBanner(
|
|
|
121
121
|
remoteUrls: string[],
|
|
122
122
|
options: Parameters<typeof remoteBanner>[2],
|
|
123
123
|
): string {
|
|
124
|
-
return `${remoteBanner(url, remoteUrls, options)}\n\np presenter view · s slide rail · Ctrl-C stops the server`;
|
|
124
|
+
return `${remoteBanner(url, remoteUrls, options)}\n\np presenter view · s slide rail · a annotate · Ctrl-C stops the server`;
|
|
125
125
|
}
|
package/src/core/build.ts
CHANGED
package/src/core/chrome.ts
CHANGED
|
@@ -60,10 +60,12 @@ ${indent(printPageCss({ width, height }))}
|
|
|
60
60
|
@media print { #dek-presenter, #dek-progress { display: none !important; } }
|
|
61
61
|
body[data-mode="video"] #dek-presenter, body[data-mode="video"] #dek-progress { display: none !important; }
|
|
62
62
|
#dek-presenter[hidden], #dek-progress[hidden], #dek-next-end[hidden] { display: none; }
|
|
63
|
-
#dek-mark-toggle { flex: none; width: 2.25rem; height: 2.25rem; padding: 0; border: 1px solid rgba(255,255,255,0.2); border-radius: 50%; background: transparent; color: rgba(255,255,255,0.45); cursor: pointer; font: 600 1rem/1 system-ui, sans-serif; }
|
|
63
|
+
#dek-mark-toggle, #dek-annotate-toggle { flex: none; width: 2.25rem; height: 2.25rem; padding: 0; border: 1px solid rgba(255,255,255,0.2); border-radius: 50%; background: transparent; color: rgba(255,255,255,0.45); cursor: pointer; font: 600 1rem/1 system-ui, sans-serif; }
|
|
64
64
|
#dek-mark-toggle::before { content: "✎"; }
|
|
65
65
|
#dek-mark-toggle[aria-pressed="true"] { border-color: #eab308; color: #eab308; }
|
|
66
66
|
#dek-mark-toggle[data-error] { border-color: #ef4444; }
|
|
67
|
+
#dek-annotate-toggle::before { content: "⌖"; }
|
|
68
|
+
#dek-annotate-toggle[aria-pressed="true"] { border-color: #3b82f6; color: #3b82f6; }
|
|
67
69
|
#dek-beats li.is-marked::after { content: " ✎"; color: #eab308; }
|
|
68
70
|
#dek-laser-toggle { flex: none; width: 2.25rem; height: 2.25rem; padding: 0; border: 1px solid rgba(255,255,255,0.2); border-radius: 50%; background: transparent; cursor: pointer; }
|
|
69
71
|
#dek-laser-toggle::before { content: ""; display: block; width: 10px; height: 10px; margin: auto; border-radius: 50%; background: rgba(255,255,255,0.45); }
|
package/src/core/config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { parsePublicUrl } from "./ogp.ts";
|
|
4
4
|
import { isPinnedRev, isPlainRefName } from "./ref-name.ts";
|
|
5
|
+
import { DeckSettings } from "./schema.ts";
|
|
5
6
|
import { parseTomlWith } from "./zod.ts";
|
|
6
7
|
|
|
7
8
|
export const Voice = z.object({
|
|
@@ -18,9 +19,7 @@ export const DekToml = z.object({
|
|
|
18
19
|
"write the absolute http(s) URL dist/ is served from, like https://example.com/talks/",
|
|
19
20
|
})
|
|
20
21
|
.optional(),
|
|
21
|
-
|
|
22
|
-
cjk_per_minute: z.number().optional(),
|
|
23
|
-
latin_per_minute: z.number().optional(),
|
|
22
|
+
...DeckSettings.partial().shape,
|
|
24
23
|
voice: Voice.optional(),
|
|
25
24
|
refs: z
|
|
26
25
|
.record(z.string(), z.string())
|
|
@@ -49,20 +48,16 @@ type VoiceDefaults = {
|
|
|
49
48
|
export type DekConfig = {
|
|
50
49
|
/** Where dist/ is served from, ending in a slash; link previews need it for og:image. */
|
|
51
50
|
url?: string;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
/**
|
|
52
|
+
* The deck settings `dekc new` writes into a new deck's frontmatter. Nothing else reads them: a
|
|
53
|
+
* deck is judged by its own, so changing dek.toml leaves the decks already made as they were.
|
|
54
|
+
*/
|
|
55
|
+
seed?: Partial<DeckSettings>;
|
|
55
56
|
voice?: VoiceDefaults;
|
|
56
57
|
/** `[refs]`: each ref name and the commit it is pinned to. */
|
|
57
58
|
refs?: Record<string, string>;
|
|
58
59
|
};
|
|
59
60
|
|
|
60
|
-
export const DEFAULT_CONFIG: DekConfig = {
|
|
61
|
-
maxClasses: 40,
|
|
62
|
-
cjkPerMinute: 300,
|
|
63
|
-
latinPerMinute: 130,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
61
|
export function parseDekToml(source: string, path?: string): DekConfig {
|
|
67
62
|
const data = parseTomlWith(DekToml, source, {
|
|
68
63
|
label: "dek.toml",
|
|
@@ -77,11 +72,10 @@ export function parseDekToml(source: string, path?: string): DekConfig {
|
|
|
77
72
|
}
|
|
78
73
|
: undefined;
|
|
79
74
|
const url = data.url === undefined ? undefined : parsePublicUrl(data.url);
|
|
75
|
+
const seed = DeckSettings.partial().parse(data);
|
|
80
76
|
return {
|
|
81
77
|
...(url ? { url } : {}),
|
|
82
|
-
|
|
83
|
-
cjkPerMinute: data.cjk_per_minute ?? DEFAULT_CONFIG.cjkPerMinute,
|
|
84
|
-
latinPerMinute: data.latin_per_minute ?? DEFAULT_CONFIG.latinPerMinute,
|
|
78
|
+
...(Object.keys(seed).length > 0 ? { seed } : {}),
|
|
85
79
|
...(voice ? { voice } : {}),
|
|
86
80
|
...(data.refs && Object.keys(data.refs).length > 0 ? { refs: data.refs } : {}),
|
|
87
81
|
};
|
|
@@ -89,7 +83,7 @@ export function parseDekToml(source: string, path?: string): DekConfig {
|
|
|
89
83
|
|
|
90
84
|
export function loadConfig(configPath: string): DekConfig {
|
|
91
85
|
if (!existsSync(configPath)) {
|
|
92
|
-
return {
|
|
86
|
+
return {};
|
|
93
87
|
}
|
|
94
88
|
return parseDekToml(readFileSync(configPath, "utf8"), configPath);
|
|
95
89
|
}
|
package/src/core/document.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { readTheme } from "./assets.ts";
|
|
2
2
|
import { playerChromeCss } from "./chrome.ts";
|
|
3
|
-
import { type DekConfig, loadConfig } from "./config.ts";
|
|
4
3
|
import { escapeAttr, escapeHtml } from "./escape.ts";
|
|
5
4
|
import { collectSlidesHtml, htmlShell } from "./html.ts";
|
|
6
5
|
import { type OgpImage, ogpHead } from "./ogp.ts";
|
|
@@ -25,6 +24,8 @@ export type PageTarget =
|
|
|
25
24
|
mode: "player" | "presenter";
|
|
26
25
|
includeNotes: boolean;
|
|
27
26
|
liveReloadScript: string;
|
|
27
|
+
/** Annotate mode, run after the player on a page that is the speaker's: one with the notes. */
|
|
28
|
+
annotateScript: string;
|
|
28
29
|
/**
|
|
29
30
|
* What a presenter page sends as `?token=` on its live channels at `/decks/<name>/events`
|
|
30
31
|
* and `/decks/<name>/ws`: neither an EventSource nor a WebSocket can set a header of its
|
|
@@ -50,13 +51,12 @@ export function renderDeckHtml(
|
|
|
50
51
|
dir: string,
|
|
51
52
|
options: { playerScript: string; target: PageTarget },
|
|
52
53
|
): string {
|
|
53
|
-
|
|
54
|
-
return renderDeckDocument(deck, { ...options, config: loadConfig(project.configPath) });
|
|
54
|
+
return renderDeckDocument(resolveDeck(dir).deck, options);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function renderDeckDocument(
|
|
58
58
|
deck: ProjectDeck,
|
|
59
|
-
options: {
|
|
59
|
+
options: { playerScript: string; target: PageTarget },
|
|
60
60
|
): string {
|
|
61
61
|
const { target } = options;
|
|
62
62
|
const mode: PageMode =
|
|
@@ -69,10 +69,10 @@ export function renderDeckDocument(
|
|
|
69
69
|
const standalone = !live;
|
|
70
70
|
const strictScripts = target.kind === "video";
|
|
71
71
|
const includeNotes = target.kind === "dev" ? target.includeNotes : target.kind === "build";
|
|
72
|
-
const data = presenterSlides(deck,
|
|
72
|
+
const data = presenterSlides(deck, scriptReader(target)).map((slide) =>
|
|
73
73
|
includeNotes ? slide : { ...slide, script: "" },
|
|
74
74
|
);
|
|
75
|
-
const slidesHtml = collectSlidesHtml(deck, { inline: standalone });
|
|
75
|
+
const slidesHtml = collectSlidesHtml(deck, { inline: standalone, sources: live });
|
|
76
76
|
const themeCss = readTheme(deck.dir, standalone);
|
|
77
77
|
const slideScripts = readSlideScripts(deck.dir, { strict: strictScripts });
|
|
78
78
|
const state = data[0] ? presenterState(data, { slideIndex: 0, beatIndex: 0 }) : undefined;
|
|
@@ -107,6 +107,7 @@ export function renderDeckDocument(
|
|
|
107
107
|
<p id="${PAGE_ID.elapsed}">0:00</p>
|
|
108
108
|
<p id="${PAGE_ID.budget}">${budget}</p>
|
|
109
109
|
${target.kind === "dev" ? markButton(deck.deck.lang) : ""}
|
|
110
|
+
${target.kind === "dev" ? annotateButton(deck.deck.lang) : ""}
|
|
110
111
|
<button id="${PAGE_ID.laserToggle}" type="button" aria-pressed="false" aria-label="${escapeAttr(laserLabel(deck.deck.lang))}" title="${escapeAttr(laserLabel(deck.deck.lang))}"></button>
|
|
111
112
|
</footer>
|
|
112
113
|
</aside>`
|
|
@@ -159,7 +160,8 @@ export function renderDeckDocument(
|
|
|
159
160
|
<script type="application/json" id="${PAGE_ID.data}">${jsonForScript(data)}</script>
|
|
160
161
|
${slideScriptTags(slideScripts)}
|
|
161
162
|
<script>${options.playerScript}</script>
|
|
162
|
-
${target.kind === "dev" ? `<script>${target.liveReloadScript}</script>` : ""}
|
|
163
|
+
${target.kind === "dev" ? `<script>${target.liveReloadScript}</script>` : ""}
|
|
164
|
+
${target.kind === "dev" && includeNotes ? `<script>${target.annotateScript}</script>` : ""}`,
|
|
163
165
|
});
|
|
164
166
|
}
|
|
165
167
|
|
|
@@ -199,6 +201,17 @@ function markButton(lang: string): string {
|
|
|
199
201
|
return `<button id="${PAGE_ID.markToggle}" type="button" aria-pressed="false" aria-label="${escapeAttr(label)}" title="${escapeAttr(label)}"></button>`;
|
|
200
202
|
}
|
|
201
203
|
|
|
204
|
+
/**
|
|
205
|
+
* The button that turns annotate mode on and off. Only the dev server's page has it: the notes
|
|
206
|
+
* name files and lines of a project on this machine, and a built file is someone else's to read.
|
|
207
|
+
*/
|
|
208
|
+
function annotateButton(lang: string): string {
|
|
209
|
+
const label = lang.toLowerCase().startsWith("ja")
|
|
210
|
+
? "スライドの要素に注釈を付ける (a)"
|
|
211
|
+
: "Annotate slide elements (a)";
|
|
212
|
+
return `<button id="${PAGE_ID.annotateToggle}" type="button" aria-pressed="false" aria-label="${escapeAttr(label)}" title="${escapeAttr(label)}"></button>`;
|
|
213
|
+
}
|
|
214
|
+
|
|
202
215
|
/** The laser button's name, with the key that does the same. */
|
|
203
216
|
function laserLabel(lang: string): string {
|
|
204
217
|
return lang.toLowerCase().startsWith("ja") ? "レーザーポインター (l)" : "Laser pointer (l)";
|
package/src/core/html.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assetInliner } from "./assets.ts";
|
|
2
2
|
import { deckPaths } from "./deck-paths.ts";
|
|
3
3
|
import { escapeAttr, escapeHtml } from "./escape.ts";
|
|
4
|
+
import { type LineColumn, lineLocator } from "./lines.ts";
|
|
4
5
|
import { isInside } from "./path.ts";
|
|
5
6
|
import { type ProjectDeck, readDeckFile } from "./resolve.ts";
|
|
6
7
|
import { FALLBACK_LANG, type Section } from "./schema.ts";
|
|
@@ -41,6 +42,11 @@ export function hasSlideClass(className: string | null | undefined): boolean {
|
|
|
41
42
|
* written. A slide that never closes runs to `</body>`, or to the end.
|
|
42
43
|
*/
|
|
43
44
|
export function extractSlideSection(html: string): string | undefined {
|
|
45
|
+
return locateSlideSection(html)?.section;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The first slide section, as `extractSlideSection` reads it, and the offset it starts at. */
|
|
49
|
+
function locateSlideSection(html: string): { section: string; start: number } | undefined {
|
|
44
50
|
const mark = uniqueMark(html);
|
|
45
51
|
let found = false;
|
|
46
52
|
let closed = false;
|
|
@@ -68,10 +74,13 @@ export function extractSlideSection(html: string): string | undefined {
|
|
|
68
74
|
: undefined;
|
|
69
75
|
// The parser closes an unclosed slide at the end of its parent, past `</body>`.
|
|
70
76
|
if (slide !== undefined && /<\/section\s*>$/i.test(slide)) {
|
|
71
|
-
return slide;
|
|
77
|
+
return { section: slide, start };
|
|
72
78
|
}
|
|
73
79
|
const bodyClose = html.slice(start).search(/<\/body>/i);
|
|
74
|
-
return
|
|
80
|
+
return {
|
|
81
|
+
section: bodyClose >= 0 ? html.slice(start, start + bodyClose) : html.slice(start),
|
|
82
|
+
start,
|
|
83
|
+
};
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
/** A deck's slide markup, each file read at most once. */
|
|
@@ -86,32 +95,43 @@ export type DeckSlides = {
|
|
|
86
95
|
* stops on them, because at the venue a deck that shows beats one that does not.
|
|
87
96
|
*/
|
|
88
97
|
section(slug: string): string;
|
|
98
|
+
/** Where the written section is in `slides/<slug>.html`; none for a skeleton. */
|
|
99
|
+
source(slug: string): SlideSource | undefined;
|
|
89
100
|
};
|
|
90
101
|
|
|
102
|
+
/** A slide file and the offset its section starts at: the section is `file` from `start`. */
|
|
103
|
+
type SlideSource = { file: string; start: number };
|
|
104
|
+
|
|
91
105
|
export function deckSlides(deck: ProjectDeck): DeckSlides {
|
|
92
106
|
const paths = deckPaths(deck.dir);
|
|
93
|
-
const read = new Map<string, string | undefined>();
|
|
107
|
+
const read = new Map<string, (SlideSource & { section: string }) | undefined>();
|
|
94
108
|
let layouts: SkeletonLayouts | undefined;
|
|
95
109
|
const skeleton = (slug: string): string | undefined => {
|
|
96
110
|
layouts ??= deckLayouts(deck.dir);
|
|
97
111
|
return skeletonHtml(deck.deck, slug, layouts);
|
|
98
112
|
};
|
|
99
|
-
const
|
|
113
|
+
const located = (slug: string): (SlideSource & { section: string }) | undefined => {
|
|
100
114
|
if (!read.has(slug)) {
|
|
101
115
|
const path = paths.slide(slug, ".html");
|
|
102
116
|
// A slug from a URL may try to climb out of slides/.
|
|
103
|
-
const
|
|
117
|
+
const file =
|
|
104
118
|
isSafeSlideSlug(slug) && isInside(path, paths.slides)
|
|
105
119
|
? readDeckFile(deck.dir, path)
|
|
106
120
|
: undefined;
|
|
107
|
-
|
|
121
|
+
const found = file === undefined ? undefined : locateSlideSection(file);
|
|
122
|
+
read.set(slug, file !== undefined && found ? { file, ...found } : undefined);
|
|
108
123
|
}
|
|
109
124
|
return read.get(slug);
|
|
110
125
|
};
|
|
126
|
+
const written = (slug: string): string | undefined => located(slug)?.section;
|
|
111
127
|
return {
|
|
112
128
|
has: (slug) => written(slug) !== undefined,
|
|
113
129
|
written,
|
|
114
130
|
section: (slug) => written(slug) ?? extractSlideSection(skeleton(slug) ?? "") ?? "",
|
|
131
|
+
source(slug) {
|
|
132
|
+
const found = located(slug);
|
|
133
|
+
return found && { file: found.file, start: found.start };
|
|
134
|
+
},
|
|
115
135
|
};
|
|
116
136
|
}
|
|
117
137
|
|
|
@@ -140,15 +160,24 @@ export type SlideStamp = {
|
|
|
140
160
|
beat?: { index: number; step: string };
|
|
141
161
|
/** Inline the deck's files as data: URIs, for a page that stands alone. */
|
|
142
162
|
inline?: { deckDir: string };
|
|
163
|
+
/**
|
|
164
|
+
* Where the section is written, for the dev server's pages: every start tag then says where
|
|
165
|
+
* in the file it is, as `data-dek-source="<line>:<column>"`, and the classes it is written
|
|
166
|
+
* with, as `data-dek-class`, so annotate mode can name it as the file does whatever a script
|
|
167
|
+
* adds to it.
|
|
168
|
+
*/
|
|
169
|
+
source?: SlideSource;
|
|
143
170
|
};
|
|
144
171
|
|
|
145
172
|
/**
|
|
146
173
|
* One slide section with what the page needs on it, in one parser pass: its `data-slug` unless
|
|
147
|
-
* it names its own, its place as `--dek-slide-number` and `--dek-slide-count`,
|
|
148
|
-
*
|
|
174
|
+
* it names its own, its place as `--dek-slide-number` and `--dek-slide-count`, for a still page
|
|
175
|
+
* the classes and beat the player would have set, and for the dev server where each tag is
|
|
176
|
+
* written.
|
|
149
177
|
*/
|
|
150
178
|
export function stampSlide(html: string, stamp: SlideStamp): string {
|
|
151
179
|
let done = false;
|
|
180
|
+
const spots = stamp.source && startTagSpots(html, stamp.source);
|
|
152
181
|
return rewriteHtml(html, (rewriter) => {
|
|
153
182
|
rewriter.on("section", {
|
|
154
183
|
element(el) {
|
|
@@ -186,21 +215,77 @@ export function stampSlide(html: string, stamp: SlideStamp): string {
|
|
|
186
215
|
if (stamp.inline) {
|
|
187
216
|
rewriter.on("*", assetInliner(stamp.inline.deckDir));
|
|
188
217
|
}
|
|
218
|
+
if (spots) {
|
|
219
|
+
// The same parser over the same markup meets the start tags in the same order.
|
|
220
|
+
let index = 0;
|
|
221
|
+
rewriter.on("*", {
|
|
222
|
+
element(el) {
|
|
223
|
+
const spot = spots[index++];
|
|
224
|
+
if (spot) {
|
|
225
|
+
el.setAttribute("data-dek-source", `${spot.line}:${spot.column}`);
|
|
226
|
+
if (spot.classes !== undefined) {
|
|
227
|
+
el.setAttribute("data-dek-class", spot.classes);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Where a start tag is written, and its classes as written; none when it has no `class`. */
|
|
237
|
+
type TagSpot = LineColumn & { classes?: string };
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Where each start tag of `html` is written in the file it was taken from, in document order,
|
|
241
|
+
* read before anything else touches the markup. A marker goes in front of each tag; where the
|
|
242
|
+
* markers land, less their own length, is where the tags were.
|
|
243
|
+
*/
|
|
244
|
+
function startTagSpots(html: string, source: SlideSource): TagSpot[] {
|
|
245
|
+
const mark = uniqueMark(html);
|
|
246
|
+
const classes: Array<string | null> = [];
|
|
247
|
+
const marked = rewriteHtml(html, (rewriter) => {
|
|
248
|
+
rewriter.on("*", {
|
|
249
|
+
element(el) {
|
|
250
|
+
el.before(mark, { html: true });
|
|
251
|
+
classes.push(el.getAttribute("class"));
|
|
252
|
+
},
|
|
253
|
+
});
|
|
189
254
|
});
|
|
255
|
+
const spot = lineLocator(source.file);
|
|
256
|
+
let offset = source.start;
|
|
257
|
+
return marked
|
|
258
|
+
.split(mark)
|
|
259
|
+
.slice(0, -1)
|
|
260
|
+
.map((before, index) => {
|
|
261
|
+
offset += before.length;
|
|
262
|
+
const written = classes[index];
|
|
263
|
+
return written === null || written === undefined
|
|
264
|
+
? spot(offset)
|
|
265
|
+
: { ...spot(offset), classes: written.split(/\s+/).filter(Boolean).join(" ") };
|
|
266
|
+
});
|
|
190
267
|
}
|
|
191
268
|
|
|
192
|
-
/**
|
|
193
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Every slide of the deck in script order, as the player pages hold them. `sources` says where
|
|
271
|
+
* each tag is written, for the dev server's pages.
|
|
272
|
+
*/
|
|
273
|
+
export function collectSlidesHtml(
|
|
274
|
+
deck: ProjectDeck,
|
|
275
|
+
options: { inline: boolean; sources?: boolean },
|
|
276
|
+
): string {
|
|
194
277
|
const slides = deckSlides(deck);
|
|
195
278
|
const { sections } = deck.deck;
|
|
196
279
|
return sections
|
|
197
|
-
.map((section, index) =>
|
|
198
|
-
|
|
280
|
+
.map((section, index) => {
|
|
281
|
+
const source = options.sources ? slides.source(section.slug) : undefined;
|
|
282
|
+
return stampSlide(slides.section(section.slug), {
|
|
199
283
|
slug: section.slug,
|
|
200
284
|
place: slidePlace(sections, index),
|
|
201
285
|
...(options.inline ? { inline: { deckDir: deck.dir } } : {}),
|
|
202
|
-
|
|
203
|
-
|
|
286
|
+
...(source ? { source } : {}),
|
|
287
|
+
});
|
|
288
|
+
})
|
|
204
289
|
.join("");
|
|
205
290
|
}
|
|
206
291
|
|