@hajimism/dek 0.1.0 → 0.2.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/CHANGELOG.md +31 -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/core/build.ts +0 -1
- package/src/core/config.ts +10 -16
- package/src/core/document.ts +3 -5
- 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/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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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.0](https://github.com/hajimism/dek/compare/v0.1.0...v0.2.0) (2026-10-01)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* 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.
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* let a deck own its class budget and speaking rate ([8dc9a07](https://github.com/hajimism/dek/commit/8dc9a07efb32b803e685a353ee628cae21a8bc56))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **lint:** count vbscript: and script-running data: URLs in DEK011 ([1b825b1](https://github.com/hajimism/dek/commit/1b825b1bffc33809ef0bdf3a8e5a1d45794a41a7))
|
|
20
|
+
|
|
21
|
+
## 0.1.0 (2026-10-01)
|
|
22
|
+
|
|
23
|
+
The first release on npm.
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* Published as `@hajimism/dek`. Create a project with `bunx @hajimism/dek init`, then install it with `bun add -d @hajimism/dek`.
|
|
28
|
+
|
|
29
|
+
### ⚠ BREAKING CHANGES
|
|
30
|
+
|
|
31
|
+
* 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.
|
|
3
|
+
"version": "0.2.0",
|
|
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/core/build.ts
CHANGED
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";
|
|
@@ -50,13 +49,12 @@ export function renderDeckHtml(
|
|
|
50
49
|
dir: string,
|
|
51
50
|
options: { playerScript: string; target: PageTarget },
|
|
52
51
|
): string {
|
|
53
|
-
|
|
54
|
-
return renderDeckDocument(deck, { ...options, config: loadConfig(project.configPath) });
|
|
52
|
+
return renderDeckDocument(resolveDeck(dir).deck, options);
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
export function renderDeckDocument(
|
|
58
56
|
deck: ProjectDeck,
|
|
59
|
-
options: {
|
|
57
|
+
options: { playerScript: string; target: PageTarget },
|
|
60
58
|
): string {
|
|
61
59
|
const { target } = options;
|
|
62
60
|
const mode: PageMode =
|
|
@@ -69,7 +67,7 @@ export function renderDeckDocument(
|
|
|
69
67
|
const standalone = !live;
|
|
70
68
|
const strictScripts = target.kind === "video";
|
|
71
69
|
const includeNotes = target.kind === "dev" ? target.includeNotes : target.kind === "build";
|
|
72
|
-
const data = presenterSlides(deck,
|
|
70
|
+
const data = presenterSlides(deck, scriptReader(target)).map((slide) =>
|
|
73
71
|
includeNotes ? slide : { ...slide, script: "" },
|
|
74
72
|
);
|
|
75
73
|
const slidesHtml = collectSlidesHtml(deck, { inline: standalone });
|
package/src/core/lint/keys.ts
CHANGED
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
unknownTomlKeys,
|
|
6
6
|
} from "../config-keys.ts";
|
|
7
7
|
import { type Diagnostic, diag } from "../diagnostic.ts";
|
|
8
|
+
import { readFrontmatterYaml } from "../parse.ts";
|
|
8
9
|
import { type Project, readTextIfExists } from "../resolve.ts";
|
|
10
|
+
import type { DeckSettings } from "../schema.ts";
|
|
9
11
|
import type { LintContext } from "./context.ts";
|
|
10
12
|
|
|
11
13
|
/*
|
|
@@ -21,10 +23,51 @@ export function tomlKeyDiagnostics(project: Project): Diagnostic[] {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/** DEK008 for the deck's frontmatter. */
|
|
24
|
-
export function frontmatterKeyDiagnostics(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
export function frontmatterKeyDiagnostics(ctx: LintContext): Diagnostic[] {
|
|
27
|
+
const { deck, script } = ctx;
|
|
28
|
+
return [
|
|
29
|
+
...(script ? unknownFrontmatterKeys(script.yaml, 2) : []).map((key) =>
|
|
30
|
+
unknownKeyDiagnostic(key, { file: "frontmatter", path: deck.scriptPath }),
|
|
31
|
+
),
|
|
32
|
+
...unfollowedSeedDiagnostics(ctx),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* DEK008 for a deck setting dek.toml sets and the deck leaves out. dek.toml only seeds `dekc new`,
|
|
38
|
+
* so a deck made before it, or without the key, keeps the default: said where the key would go,
|
|
39
|
+
* and only when the two differ, since otherwise the verdict is the same either way.
|
|
40
|
+
*/
|
|
41
|
+
function unfollowedSeedDiagnostics({ config, deck, script }: LintContext): Diagnostic[] {
|
|
42
|
+
if (!script || !config.seed) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const written = readFrontmatterKeys(script.yaml);
|
|
46
|
+
return Object.entries(config.seed).flatMap(([key, value]) => {
|
|
47
|
+
const used = deck.deck[key as keyof DeckSettings];
|
|
48
|
+
if (written.has(key) || value === used) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
return [
|
|
52
|
+
diag("DEK008", {
|
|
53
|
+
message: `dek.toml sets ${key} = ${value}, which this deck does not follow; it uses ${used}`,
|
|
54
|
+
path: deck.scriptPath,
|
|
55
|
+
line: script.bodyStartLine - 1,
|
|
56
|
+
hint: `to keep ${value}, add \`${key}: ${value}\` to the frontmatter; dek.toml only seeds \`dekc new\``,
|
|
57
|
+
data: { file: "frontmatter", key, value },
|
|
58
|
+
}),
|
|
59
|
+
];
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The top-level keys the frontmatter writes; none when its YAML does not parse. */
|
|
64
|
+
function readFrontmatterKeys(yaml: string): Set<string> {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = readFrontmatterYaml(yaml);
|
|
67
|
+
return new Set(parsed && typeof parsed === "object" ? Object.keys(parsed) : []);
|
|
68
|
+
} catch {
|
|
69
|
+
return new Set();
|
|
70
|
+
}
|
|
28
71
|
}
|
|
29
72
|
|
|
30
73
|
function unknownKeyDiagnostic(
|
|
@@ -272,7 +272,7 @@ function morphDiagnostics({ section, path, scan }: SlideHtml): Diagnostic[] {
|
|
|
272
272
|
/**
|
|
273
273
|
* DEK011: style or script inside the markup, which belongs in the slide's own .css or .ts. Every
|
|
274
274
|
* occurrence is its own finding: a `<style>` or `<script>` element, a `style` attribute, an event
|
|
275
|
-
* handler attribute, and a
|
|
275
|
+
* handler attribute, and a URL that runs script.
|
|
276
276
|
*/
|
|
277
277
|
function inlineCodeDiagnostics({ section, path, scan }: SlideHtml): Diagnostic[] {
|
|
278
278
|
return scan.elements
|
|
@@ -346,8 +346,11 @@ function inlineAttribute(
|
|
|
346
346
|
hint: `remove it; a slide takes no input, and motion goes in slides/${slug}.ts as a draw(t) function`,
|
|
347
347
|
};
|
|
348
348
|
}
|
|
349
|
-
|
|
350
|
-
|
|
349
|
+
const scheme = isUrlAttribute(tag, attribute.name)
|
|
350
|
+
? scriptUrlScheme(tag, attribute.value)
|
|
351
|
+
: undefined;
|
|
352
|
+
if (scheme) {
|
|
353
|
+
return { message: `slide contains a ${scheme} URL`, hint: motionHint(slug) };
|
|
351
354
|
}
|
|
352
355
|
return undefined;
|
|
353
356
|
}
|
|
@@ -358,13 +361,34 @@ function motionHint(slug: string): string {
|
|
|
358
361
|
|
|
359
362
|
/** `onclick`, `onload`, and every other attribute the browser runs as script. */
|
|
360
363
|
const EVENT_HANDLER_RE = /^on[a-z]+$/;
|
|
361
|
-
/**
|
|
362
|
-
|
|
363
|
-
|
|
364
|
+
/** Tags that load a URL as a document of its own, so an SVG there runs its scripts. */
|
|
365
|
+
const DOCUMENT_TAGS = new Set(["iframe", "frame", "object", "embed"]);
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The scheme of a URL that runs script, or undefined: `javascript:` and `vbscript:` always, and a
|
|
369
|
+
* `data:` URL holding HTML, or SVG where a tag loads it as a document. An image's `data:` stays.
|
|
370
|
+
* Read the way the URL parser reads it: tabs and newlines dropped, leading space trimmed, any case.
|
|
371
|
+
*/
|
|
372
|
+
function scriptUrlScheme(tag: string, value: string): string | undefined {
|
|
373
|
+
const url = value
|
|
364
374
|
.replace(/[\t\n\r]/g, "")
|
|
365
375
|
.trimStart()
|
|
366
|
-
.toLowerCase()
|
|
367
|
-
|
|
376
|
+
.toLowerCase();
|
|
377
|
+
if (url.startsWith("javascript:")) {
|
|
378
|
+
return "javascript:";
|
|
379
|
+
}
|
|
380
|
+
if (url.startsWith("vbscript:")) {
|
|
381
|
+
return "vbscript:";
|
|
382
|
+
}
|
|
383
|
+
if (url.startsWith("data:")) {
|
|
384
|
+
const type = url.slice("data:".length).split(/[;,]/, 1)[0]?.trim();
|
|
385
|
+
const runs =
|
|
386
|
+
type === "text/html" ||
|
|
387
|
+
type === "application/xhtml+xml" ||
|
|
388
|
+
(type === "image/svg+xml" && DOCUMENT_TAGS.has(tag));
|
|
389
|
+
return runs ? "data:" : undefined;
|
|
390
|
+
}
|
|
391
|
+
return undefined;
|
|
368
392
|
}
|
|
369
393
|
|
|
370
394
|
/**
|
|
@@ -290,6 +290,6 @@ function themeScopeHint(selector: string): string {
|
|
|
290
290
|
/** The theme's own findings, or DEK018 when there is no theme. */
|
|
291
291
|
export function themeDiagnostics(ctx: LintContext): Diagnostic[] {
|
|
292
292
|
return ctx.theme
|
|
293
|
-
? lintTheme(ctx.theme, { maxClasses: ctx.
|
|
293
|
+
? lintTheme(ctx.theme, { maxClasses: ctx.deck.deck.max_classes, deckDir: ctx.deck.dir })
|
|
294
294
|
: [missingThemeDiagnostic(ctx)];
|
|
295
295
|
}
|
package/src/core/lint/timing.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { DekConfig } from "../config.ts";
|
|
2
1
|
import { type Diagnostic, diag } from "../diagnostic.ts";
|
|
3
2
|
import type { ProjectDeck } from "../resolve.ts";
|
|
4
3
|
import type { Timeline } from "../timeline.ts";
|
|
@@ -13,7 +12,7 @@ const ESTIMATE_DRIFT_RATIO = 0.35;
|
|
|
13
12
|
/** DEK041 against the voice timeline when there is one, else against the reading-time estimate. */
|
|
14
13
|
export function timingDiagnostics(ctx: LintContext): Diagnostic[] {
|
|
15
14
|
const timeline = tryLoadCachedTimeline(ctx.deck.dir);
|
|
16
|
-
return timeline ? lintDuration(ctx.deck, timeline) : lintEstimate(ctx.deck
|
|
15
|
+
return timeline ? lintDuration(ctx.deck, timeline) : lintEstimate(ctx.deck);
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
function lintDuration(deck: ProjectDeck, timeline: Timeline): Diagnostic[] {
|
|
@@ -37,15 +36,12 @@ function lintDuration(deck: ProjectDeck, timeline: Timeline): Diagnostic[] {
|
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
/** DEK041 before any voice: the reading-time estimate `dekc ls` shows, against the budget. */
|
|
40
|
-
function lintEstimate(deck: ProjectDeck
|
|
39
|
+
function lintEstimate(deck: ProjectDeck): Diagnostic[] {
|
|
41
40
|
const budget = parseDurationSeconds(deck.deck.duration);
|
|
42
41
|
if (budget === undefined || budget <= 0) {
|
|
43
42
|
return [];
|
|
44
43
|
}
|
|
45
|
-
const estimate = sectionTiming(deck.deck.
|
|
46
|
-
(sum, row) => sum + row.estimateSeconds,
|
|
47
|
-
0,
|
|
48
|
-
);
|
|
44
|
+
const estimate = sectionTiming(deck.deck).reduce((sum, row) => sum + row.estimateSeconds, 0);
|
|
49
45
|
if (Math.abs(estimate - budget) / budget < ESTIMATE_DRIFT_RATIO) {
|
|
50
46
|
return [];
|
|
51
47
|
}
|
package/src/core/presenter.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { DekConfig } from "./config.ts";
|
|
2
1
|
import type { PresenterSlide } from "./presenter-state.ts";
|
|
3
2
|
import type { ProjectDeck } from "./resolve.ts";
|
|
4
3
|
import { type ScriptLine, scriptLines } from "./script-lines.ts";
|
|
@@ -18,10 +17,9 @@ export type ScriptReader = "speaker" | "audience";
|
|
|
18
17
|
|
|
19
18
|
export function presenterSlides(
|
|
20
19
|
deck: ProjectDeck,
|
|
21
|
-
config: DekConfig,
|
|
22
20
|
reader: ScriptReader = "speaker",
|
|
23
21
|
): PresenterSlide[] {
|
|
24
|
-
const timing = sectionTiming(deck.deck
|
|
22
|
+
const timing = sectionTiming(deck.deck);
|
|
25
23
|
const budgetBySlug = new Map(timing.map((row) => [row.slug, row.budgetSeconds]));
|
|
26
24
|
return deck.deck.sections.map((section) => {
|
|
27
25
|
const script = formatSectionScript(section);
|
package/src/core/schema.ts
CHANGED
|
@@ -52,6 +52,25 @@ export function inferLang(text: string): string {
|
|
|
52
52
|
return "en";
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* How a deck is judged: the theme's class budget and the speaking rate the estimate reads. A deck
|
|
57
|
+
* owns them in its frontmatter, so a project that changes later leaves a finished talk as it was;
|
|
58
|
+
* dek.toml may set the same keys, and `dekc new` copies them into the deck it makes.
|
|
59
|
+
*/
|
|
60
|
+
export const DeckSettings = z.object({
|
|
61
|
+
max_classes: z.number().describe("The most classes theme.css may define (DEK013)."),
|
|
62
|
+
cjk_per_minute: z.number().describe("Speaking rate for CJK text, in characters a minute."),
|
|
63
|
+
latin_per_minute: z.number().describe("Speaking rate for other text, in words a minute."),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export type DeckSettings = z.infer<typeof DeckSettings>;
|
|
67
|
+
|
|
68
|
+
export const DECK_SETTING_DEFAULTS: DeckSettings = {
|
|
69
|
+
max_classes: 40,
|
|
70
|
+
cjk_per_minute: 300,
|
|
71
|
+
latin_per_minute: 130,
|
|
72
|
+
};
|
|
73
|
+
|
|
55
74
|
/** Frontmatter fields written to `.dek/schema.json` for yaml-language-server. */
|
|
56
75
|
export const Frontmatter = z.object({
|
|
57
76
|
title: z.string(),
|
|
@@ -69,6 +88,11 @@ export const Frontmatter = z.object({
|
|
|
69
88
|
lang: Lang.optional().describe(
|
|
70
89
|
"BCP 47 tag for the page. Omit it to follow the script: kana → ja, Hangul → ko, Han → zh, else en.",
|
|
71
90
|
),
|
|
91
|
+
max_classes: DeckSettings.shape.max_classes.default(DECK_SETTING_DEFAULTS.max_classes),
|
|
92
|
+
cjk_per_minute: DeckSettings.shape.cjk_per_minute.default(DECK_SETTING_DEFAULTS.cjk_per_minute),
|
|
93
|
+
latin_per_minute: DeckSettings.shape.latin_per_minute.default(
|
|
94
|
+
DECK_SETTING_DEFAULTS.latin_per_minute,
|
|
95
|
+
),
|
|
72
96
|
});
|
|
73
97
|
|
|
74
98
|
export type Frontmatter = z.infer<typeof Frontmatter>;
|
package/src/core/sync.ts
CHANGED
|
@@ -252,7 +252,7 @@ A build system for talks. Write what you will say; dek builds, measures, and shi
|
|
|
252
252
|
- One \`##\` heading is one slide. HTML lives in \`slides/<id>.html\`.
|
|
253
253
|
- Each deck owns its \`theme.css\`. Before writing a slide, run \`dekc theme\` in the deck for the classes, tokens, and layouts it defines, and \`dekc theme <layout>\` for a layout's markup.
|
|
254
254
|
- Shared look lives in \`theme.css\`. Decoration only one slide uses lives in \`slides/<id>.css\`, which is scoped to that slide.
|
|
255
|
-
- Use only classes defined in \`theme.css\` or in that slide's own \`slides/<id>.css\`. The theme holds at most \`max_classes\` in \`
|
|
255
|
+
- Use only classes defined in \`theme.css\` or in that slide's own \`slides/<id>.css\`. The theme holds at most \`max_classes\` in the frontmatter of \`script.md\`, 40 by default (\`DEK013\`); classes in \`slides/<id>.css\` do not count, so keep a class only one slide uses there.
|
|
256
256
|
- A rule in \`slides/<id>.css\` weighs as if it were written at the end of \`theme.css\`: it beats the theme's \`.slide .x\`, but not a more specific rule such as a layout's \`.slide[data-layout="split"] .x\` or the beat state \`.slide.is-current [data-step]\`. To override one of those, write the same selector.
|
|
257
257
|
- Color, type, space, radius, and motion in either stylesheet use token \`var()\` only. A value only one slide uses can be a token of its own on that slide's \`.slide\` rule in \`slides/<id>.css\`.
|
|
258
258
|
- Do not add \`<style>\`, \`style=\`, \`<script>\`, event handler attributes (\`onclick=\` and the like), or \`javascript:\` URLs inside slide HTML.
|
package/src/core/timing.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Deck } from "./schema.ts";
|
|
2
2
|
|
|
3
3
|
type SectionBodies = {
|
|
4
4
|
body: string;
|
|
@@ -28,14 +28,23 @@ function speechChars(text: string): number {
|
|
|
28
28
|
return stripBlockquotes(text).replace(/\s+/g, "").length;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
/** The rate a deck is spoken at, from its own frontmatter. */
|
|
32
|
+
type SpeakingRate = Pick<Deck, "cjk_per_minute" | "latin_per_minute">;
|
|
33
|
+
|
|
34
|
+
/** What timing reads of a deck: its sections, its budget, and the rate it is spoken at. */
|
|
35
|
+
type TimedDeck = SpeakingRate & {
|
|
36
|
+
sections: Array<{ slug: string } & SectionBodies>;
|
|
37
|
+
duration?: string | undefined;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function estimateSeconds(text: string, rate: SpeakingRate): number {
|
|
32
41
|
const stripped = stripBlockquotes(text);
|
|
33
42
|
const cjk = [...stripped.matchAll(/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/gu)]
|
|
34
43
|
.length;
|
|
35
44
|
const rest = stripped.replace(/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/gu, " ");
|
|
36
45
|
const words = rest.split(/\s+/).filter(Boolean).length;
|
|
37
|
-
const cjkMinutes =
|
|
38
|
-
const latinMinutes =
|
|
46
|
+
const cjkMinutes = rate.cjk_per_minute > 0 ? cjk / rate.cjk_per_minute : 0;
|
|
47
|
+
const latinMinutes = rate.latin_per_minute > 0 ? words / rate.latin_per_minute : 0;
|
|
39
48
|
return Math.round((cjkMinutes + latinMinutes) * 60);
|
|
40
49
|
}
|
|
41
50
|
|
|
@@ -62,21 +71,17 @@ export type TimedSection = {
|
|
|
62
71
|
budgetSeconds?: number;
|
|
63
72
|
};
|
|
64
73
|
|
|
65
|
-
export function sectionTiming(
|
|
66
|
-
|
|
67
|
-
duration: string | undefined,
|
|
68
|
-
config: DekConfig,
|
|
69
|
-
): TimedSection[] {
|
|
70
|
-
const rows = sections.map((section) => {
|
|
74
|
+
export function sectionTiming(deck: TimedDeck): TimedSection[] {
|
|
75
|
+
const rows = deck.sections.map((section) => {
|
|
71
76
|
const text = speechText(section);
|
|
72
77
|
return {
|
|
73
78
|
slug: section.slug,
|
|
74
79
|
chars: speechChars(text),
|
|
75
|
-
estimateSeconds: estimateSeconds(text,
|
|
80
|
+
estimateSeconds: estimateSeconds(text, deck),
|
|
76
81
|
};
|
|
77
82
|
});
|
|
78
83
|
const totalChars = rows.reduce((sum, row) => sum + row.chars, 0);
|
|
79
|
-
const durationSeconds = parseDurationSeconds(duration);
|
|
84
|
+
const durationSeconds = parseDurationSeconds(deck.duration);
|
|
80
85
|
|
|
81
86
|
return rows.map((row) => ({
|
|
82
87
|
slug: row.slug,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { loadConfig } from "./config.ts";
|
|
2
1
|
import { renderDeckDocument } from "./document.ts";
|
|
3
2
|
import type { ResolvedDeck } from "./resolve.ts";
|
|
4
3
|
|
|
@@ -6,9 +5,8 @@ import type { ResolvedDeck } from "./resolve.ts";
|
|
|
6
5
|
* The whole deck as `dekc video` plays it: every slide, assets inlined, no notes, and the player
|
|
7
6
|
* runtime that walks it. Motion stills, morph frames and the video itself all run on this page.
|
|
8
7
|
*/
|
|
9
|
-
export function videoDocument({
|
|
8
|
+
export function videoDocument({ deck }: ResolvedDeck, playerScript: string): string {
|
|
10
9
|
return renderDeckDocument(deck, {
|
|
11
|
-
config: loadConfig(project.configPath),
|
|
12
10
|
playerScript,
|
|
13
11
|
target: { kind: "video" },
|
|
14
12
|
});
|