@hajimism/dek 0.3.3 → 0.4.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.ja.md +1 -1
  3. package/README.md +1 -1
  4. package/package.json +2 -1
  5. package/skills/dek/SKILL.md +10 -3
  6. package/skills/dek-design/SKILL.md +42 -0
  7. package/src/cli/address.ts +84 -14
  8. package/src/cli/build.ts +2 -2
  9. package/src/cli/check.ts +2 -1
  10. package/src/cli/commands.ts +11 -6
  11. package/src/cli/contract.ts +9 -1
  12. package/src/cli/files.ts +1 -5
  13. package/src/cli/flags.ts +1 -1
  14. package/src/cli/goto.ts +3 -1
  15. package/src/cli/init.ts +10 -5
  16. package/src/cli/lint.ts +4 -3
  17. package/src/cli/main.ts +7 -5
  18. package/src/cli/new.ts +14 -7
  19. package/src/cli/pptx.ts +9 -4
  20. package/src/cli/ref.ts +91 -9
  21. package/src/cli/result.ts +5 -2
  22. package/src/cli/scope.ts +17 -28
  23. package/src/cli/shot.ts +2 -1
  24. package/src/cli/text.ts +27 -2
  25. package/src/cli/theme.ts +16 -2
  26. package/src/cli/voice.ts +4 -3
  27. package/src/core/alignment.ts +320 -0
  28. package/src/core/annotation-rows.ts +16 -4
  29. package/src/core/annotations.ts +19 -1
  30. package/src/core/config.ts +61 -12
  31. package/src/core/diagnostic.ts +5 -1
  32. package/src/core/escape.ts +43 -0
  33. package/src/core/line-break.ts +275 -0
  34. package/src/core/lint/asset-refs.ts +6 -2
  35. package/src/core/lint/files.ts +30 -5
  36. package/src/core/lint/keys.ts +7 -3
  37. package/src/core/lint/slide-html.ts +3 -1
  38. package/src/core/lint/slide-script.ts +7 -5
  39. package/src/core/mv.ts +30 -10
  40. package/src/core/pack.ts +2 -2
  41. package/src/core/playwright-module.d.ts +1 -0
  42. package/src/core/playwright-visual.ts +180 -20
  43. package/src/core/playwright.ts +202 -25
  44. package/src/core/pptx-package.ts +461 -33
  45. package/src/core/pptx-paint.ts +1168 -0
  46. package/src/core/pptx-plan.ts +879 -0
  47. package/src/core/pptx-text.ts +204 -47
  48. package/src/core/pptx-theme.ts +222 -0
  49. package/src/core/pptx.ts +55 -8
  50. package/src/core/prose.ts +23 -0
  51. package/src/core/pseudo-text.ts +32 -10
  52. package/src/core/ref-name.ts +47 -8
  53. package/src/core/ref.ts +172 -16
  54. package/src/core/resolve.ts +20 -9
  55. package/src/core/shot/morph.ts +1 -1
  56. package/src/core/shot/motion.ts +2 -3
  57. package/src/core/shot/still.ts +2 -2
  58. package/src/core/shot-cache.ts +25 -17
  59. package/src/core/slide-measure.ts +51 -1
  60. package/src/core/slide-script-eval.ts +10 -8
  61. package/src/core/slide-script.ts +114 -20
  62. package/src/core/sync.ts +2 -2
  63. package/src/core/visual-cache.ts +2 -0
  64. package/src/core/visual.ts +154 -15
  65. package/src/core/voice.ts +3 -5
  66. package/src/core/zod.ts +9 -1
  67. package/src/server/dev.ts +15 -4
  68. package/src/server/lock.ts +48 -5
  69. package/src/server/routes.ts +7 -2
  70. package/src/theme/default.css +4 -0
  71. package/src/video/bake.ts +6 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,69 @@
2
2
 
3
3
  Release notes for `@hajimism/dek`. From 0.1.0 on, release-please writes this file from the Conventional Commits on `main`.
4
4
 
5
+ ## [0.4.0](https://github.com/hajimism/dek/compare/v0.3.4...v0.4.0) (2026-10-27)
6
+
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ * **theme:** `dekc theme --json` lists in `layouts` only layouts whose names are plain words: letters, marks, and digits of any script and `@%+=:,./_-`, not starting with `=`. A layout whose name holds anything else, such as a space, a quote, `$`, `;`, or a backtick, is no longer in `layouts`; `unprintedLayouts`, present only when there are some, says how many were left out. `dekc theme <layout>` still prints such a layout's example.
11
+ * **ref:** a dek.toml whose [refs] pins a deck name with any other character, starting with a dash, not in NFC, or longer than 64 characters or 200 bytes, such as `owner/repo/talk(1)` or `file/a+b`, no longer loads. Delete the line the error points at, and for a pack its packs/<deck>.tgz and refs/file/<deck>, then pin the deck again under a name a ref can carry.
12
+
13
+ ### Bug Fixes
14
+
15
+ * **annotations:** show a deck, slide, or beat that is no plain word as a placeholder in the notes ([3b04e89](https://github.com/hajimism/dek/commit/3b04e89dc22f9ea421919c920594f53a9c02d3be))
16
+ * **cli:** say once where to find a deck's name a retyped command leaves as &lt;deck&gt; ([d4a57d9](https://github.com/hajimism/dek/commit/d4a57d9f7836b2ba2433dc634a136bb7e97be509))
17
+ * **config:** show a key that is no plain word as &lt;key&gt; in an error ([22476a7](https://github.com/hajimism/dek/commit/22476a712ecf0ee4e3c552b1e0fe6f35ac10cacf))
18
+ * **dev:** read .dek/server.json only as the dev server writes it ([c402cb6](https://github.com/hajimism/dek/commit/c402cb6282783425611274fbc0337cd6652add7e))
19
+ * **dev:** read the lock a dev server on port 80 writes ([665e0e2](https://github.com/hajimism/dek/commit/665e0e237c3a639a45fa36dc14ed8bd2e7ad9168))
20
+ * keep reads, writes, and suggested commands inside what the project and dek wrote ([23a667d](https://github.com/hajimism/dek/commit/23a667d199ee5f0b34040c4c33f0c0bcfd47f819))
21
+ * **lint:** name a remote file in DEK020's hint only when it is a plain word ([fb77d02](https://github.com/hajimism/dek/commit/fb77d028db1bb38bea6b43695838d52776b192a7))
22
+ * **lint:** name a slide or asset file in a hint only when it is a plain word ([eeb240c](https://github.com/hajimism/dek/commit/eeb240c418b615067bd66ff45ac6b70361d3bd1b))
23
+ * **lint:** show an unknown key that is no plain word as &lt;key&gt; in DEK008 ([f950def](https://github.com/hajimism/dek/commit/f950def7f80f06da46bd8c913ae84c8c849d5ddf))
24
+ * never follow a link under refs/ when writing a snapshot ([0c843ea](https://github.com/hajimism/dek/commit/0c843ea4193270dd1cc0fde4a04768e210efd911))
25
+ * never write or remove a pack's copy through a link under packs/ ([95980a6](https://github.com/hajimism/dek/commit/95980a631e90ee97eda24784ded50b042111512d))
26
+ * quote every name in a command dek suggests ([d1e608d](https://github.com/hajimism/dek/commit/d1e608d5676e46b7947bd5f43a3315269fd27773))
27
+ * read a project's files only through links that stay inside it ([7ae3815](https://github.com/hajimism/dek/commit/7ae38152eadd6f58d67034169ade9dd749038002))
28
+ * **ref:** accept only letters, digits, dots, dashes, and underscores in a ref's deck name ([1b430e1](https://github.com/hajimism/dek/commit/1b430e1ea3d0f66af2642a5f257af1cefcae3ac0))
29
+ * **ref:** fold names as macOS does when telling whether two refs share a folder ([fdb32ee](https://github.com/hajimism/dek/commit/fdb32ee103facc5a661f20dabf79fe8f91ae48cd))
30
+ * **ref:** refuse a pin whose folders another ref already takes ([0d550d8](https://github.com/hajimism/dek/commit/0d550d897b0cdad86ac3de181bf7fd031061bd66))
31
+ * refuse a decks folder that links out of the project ([83f2ff0](https://github.com/hajimism/dek/commit/83f2ff00079893b8e2dbb2fc5d5b7588435aa4dd))
32
+ * refuse to serve or write inside a ref, as every other command does ([665b2de](https://github.com/hajimism/dek/commit/665b2de7a3e6aa3b67ce6140d3eb2a15fb398ee6))
33
+ * serve a deck's assets only from inside the project ([86743cf](https://github.com/hajimism/dek/commit/86743cf731d97f664e95b75f1441dd238f0eeb11))
34
+ * take a folder for a ref only where dek wrote one, and name it from there ([62cc8df](https://github.com/hajimism/dek/commit/62cc8df0a509a99a55d9db7698683bc3be5523d4))
35
+ * **theme:** leave layouts whose names are no plain word out of --json ([625f6d3](https://github.com/hajimism/dek/commit/625f6d3dab0dfe16dacb272e371b551214621a86))
36
+ * **theme:** print only layout names that are plain words ([62b15d1](https://github.com/hajimism/dek/commit/62b15d18afb91d6adbc2641376d9ca899cb6bf20))
37
+
38
+
39
+ ### Documentation
40
+
41
+ * say dek follows no link under packs/ either ([5a11b2c](https://github.com/hajimism/dek/commit/5a11b2ca9960a134e261f09f80408fc770aa44da))
42
+
43
+ ## [0.3.4](https://github.com/hajimism/dek/compare/v0.3.3...v0.3.4) (2026-10-27)
44
+
45
+
46
+ ### Features
47
+
48
+ * **pptx:** lay out boxes, pictures, text, the theme, and backgrounds as PowerPoint's own ([#27](https://github.com/hajimism/dek/issues/27)) ([eb854e9](https://github.com/hajimism/dek/commit/eb854e9835508792d3e8cfb9dd4046e8750dab7f))
49
+ * **skills:** teach the review in the browser, and add an optional dek-design skill ([2c18b0e](https://github.com/hajimism/dek/commit/2c18b0e339160fb03ca6ce3915b89e37d82c9522))
50
+ * **skills:** teach the review in the browser, and add an optional dek-design skill ([1b04394](https://github.com/hajimism/dek/commit/1b04394023a239706a2fa29d168811e1a1be05d2))
51
+ * **visual:** report awkward line breaks and edges a few pixels off (DEK035, DEK036) ([#28](https://github.com/hajimism/dek/issues/28)) ([ce75fc1](https://github.com/hajimism/dek/commit/ce75fc14937d76ab457298a5aca9116a0fe3778e))
52
+
53
+
54
+ ### Bug Fixes
55
+
56
+ * refuse a slide script's imports before Bun reads it, so none runs while dek compiles ([2752356](https://github.com/hajimism/dek/commit/275235647869f1e0a040c080476141d3aaa1aaa6))
57
+ * refuse a slide script's imports before Bun reads it, so none runs while dek compiles ([198930c](https://github.com/hajimism/dek/commit/198930c1d41639cc9943f36a3a187ff6eab0f364))
58
+
59
+
60
+ ### Documentation
61
+
62
+ * **adr:** keep dependency code away from the tokens that publish ([103f17c](https://github.com/hajimism/dek/commit/103f17cebb37dd1750caa4cc242acedf55703809))
63
+ * **adr:** renumber the PPTX records after the line-break ones ([ca43352](https://github.com/hajimism/dek/commit/ca4335267b6eeed7131c4123a8a543a230d65875))
64
+ * **adr:** renumber the PPTX records after the line-break ones ([bef6e12](https://github.com/hajimism/dek/commit/bef6e12915ac301843297fb972cf36288bbf612c))
65
+ * **adr:** ship an optional design skill and raise the official skill's floor to 0.2.2 ([6f39dd1](https://github.com/hajimism/dek/commit/6f39dd1e8234297cb0fb72d93722a419d1921f23))
66
+ * name the optional dek-design skill, and the review the official skill teaches ([6908fa1](https://github.com/hajimism/dek/commit/6908fa195e16d79c75d5b77cda72f708798de34c))
67
+
5
68
  ## [0.3.3](https://github.com/hajimism/dek/compare/v0.3.2...v0.3.3) (2026-10-21)
6
69
 
7
70
 
package/README.ja.md CHANGED
@@ -44,7 +44,7 @@ bunx dekc build # 3. dist/2026-04-vite.html — トーク全体がこの 1
44
44
 
45
45
  台本を書いて `dekc` を叩けば、HTML を一行も書かずに同梱テーマで発表できます。
46
46
 
47
- エージェントと作るなら、公式スキルを `npx skills add hajimism/dek` で、Claude Code なら `/plugin marketplace add hajimism/dek` と `/plugin install dek@dek` で入れてください。プロジェクトの外にいるエージェントは [llms.txt](https://hajimism.github.io/dek/llms.txt) から始められます。[AI エージェントと作る](https://hajimism.github.io/dek/ja/guide/ai.html)を参照してください。
47
+ エージェントと作るなら、公式スキルを `npx skills add hajimism/dek --skill dek` で、Claude Code なら `/plugin marketplace add hajimism/dek` と `/plugin install dek@dek` で入れてください。`dek-design` も、同じ方法で選んで入れられます。プロジェクトの外にいるエージェントは [llms.txt](https://hajimism.github.io/dek/llms.txt) から始められます。[AI エージェントと作る](https://hajimism.github.io/dek/ja/guide/ai.html)を参照してください。
48
48
 
49
49
  続きは [はじめる](https://hajimism.github.io/dek/ja/guide/getting-started.html)へ。通しで 1 本作るなら [チュートリアル](https://hajimism.github.io/dek/ja/guide/tutorial.html)。コマンドの一覧は [CLI リファレンス](https://hajimism.github.io/dek/ja/reference/cli.html)にあります。
50
50
 
package/README.md CHANGED
@@ -44,7 +44,7 @@ bunx dekc build # 3. dist/2026-04-vite.html — the whole talk in one file
44
44
 
45
45
  Write the script, run `dekc`, and you can present with the bundled theme without writing a line of HTML.
46
46
 
47
- Working with an agent? Install the official skill with `npx skills add hajimism/dek`, or in Claude Code with `/plugin marketplace add hajimism/dek` and `/plugin install dek@dek`; an agent outside any project can start from [llms.txt](https://hajimism.github.io/dek/llms.txt). See [Working with AI Agents](https://hajimism.github.io/dek/guide/ai.html).
47
+ Working with an agent? Install the official skill with `npx skills add hajimism/dek --skill dek`, or in Claude Code with `/plugin marketplace add hajimism/dek` and `/plugin install dek@dek`; `dek-design` is an optional skill installed the same way; an agent outside any project can start from [llms.txt](https://hajimism.github.io/dek/llms.txt). See [Working with AI Agents](https://hajimism.github.io/dek/guide/ai.html).
48
48
 
49
49
  Continue with [Getting Started](https://hajimism.github.io/dek/guide/getting-started.html), or build a real deck end to end in the [Tutorial](https://hajimism.github.io/dek/guide/tutorial.html). Every command is listed in the [CLI reference](https://hajimism.github.io/dek/reference/cli.html).
50
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hajimism/dek",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "A build system for talks.",
5
5
  "license": "MIT",
6
6
  "author": "Hajime Nakagawa (https://github.com/hajimism)",
@@ -67,6 +67,7 @@
67
67
  "happy-dom": "21.0.1",
68
68
  "knip": "6.41.0",
69
69
  "playwright": "^1.63.0",
70
+ "publint": "0.3.25",
70
71
  "typescript": "^7.0.2",
71
72
  "vitepress": "^1.6.4"
72
73
  },
@@ -16,7 +16,7 @@ dek builds a talk from its script. `script.md` holds what will be said, in order
16
16
 
17
17
  ## First, ask the dek you have
18
18
 
19
- 1. `bunx dekc --version` and `bunx dekc help --agent`: every command and flag of the dek installed in this project. This file names few on purpose; use only what that list shows.
19
+ 1. `bunx dekc --version` and `bunx dekc help --agent`: every command and flag of the dek installed in this project. This file names few on purpose; use only what that list shows. It is written for dek 0.2.2 or newer; with an older one, follow only that list and `AGENTS.md`.
20
20
  2. Read dek's block in `AGENTS.md`. dek rewrites it for the installed version: the conventions lint checks, how to check a slide, and what to do before reporting a deck as done. Follow it as written.
21
21
  3. In the deck, `bunx dekc theme`: the classes, tokens, and layouts this deck's theme defines, and `bunx dekc theme <layout>` for a layout's markup. Use nothing else.
22
22
 
@@ -39,6 +39,13 @@ A hint that sends a fix to `theme.css` means every slide of the deck shares the
39
39
  - You have looked at `bunx dekc shot --sheet` and judged the deck's balance.
40
40
  - Say what you could not judge, such as the argument and how the timing feels when spoken, and hand it back to the author. Passing lint means nothing measurable is wrong, not that the talk is good.
41
41
 
42
- ## When the human points at something
42
+ ## When the human reviews the deck
43
43
 
44
- If the human marked beats while rehearsing or pointed at elements of a slide, `bunx dekc help --agent` lists the commands that read those notes, and `AGENTS.md` says how to use them. Leave clearing them to the human.
44
+ The human sees what lint cannot. Let them point at it:
45
+
46
+ 1. Start the dev server in the background, since it keeps running: `bunx dekc <deck> --port <free port>`, or your harness's preview tool if it has one.
47
+ 2. Give them the URL and ask them to press `a`, click what they want changed, write a note, and tell you when they are done. They may not know dek: name the keys, not the commands.
48
+ 3. `bunx dekc annotations <deck> --json`, and fix every note. Shoot the slide again with `bunx dekc shot <slug>` and compare it with the note's `shot`. Tell them in short what you changed.
49
+ 4. Ask them to look again, until they say the deck is done. Stop the dev server you started.
50
+
51
+ Leave `dekc annotations clear` to the human. If they marked beats while rehearsing, `AGENTS.md` says how to read those too.
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: dek-design
3
+ description: Make dek slides that read at a glance - a heading that states a claim, one focal point, and no visual difference without a meaning. Use when writing or reviewing the look of slides in a dek deck, or when a deck lints clean but still looks off.
4
+ ---
5
+
6
+ # dek-design
7
+
8
+ The audience sees a slide, holds it in mind, then understands it. Make each step cost as little as it can. Lint measures what a rule can decide, such as overflow and contrast; this skill is for what it cannot. Use only the classes, tokens, and layouts `bunx dekc theme` lists.
9
+
10
+ ## Before you start
11
+
12
+ - Projected or read: a talk is spoken, so the words live in `script.md` and a slide shows what the voice cannot. A deck sent to be read alone needs sentences that stand without the speaker.
13
+ - What the audience knows: do not use a term they do not know. If you must, tie it to something they do.
14
+
15
+ ## One slide
16
+
17
+ - The `##` heading is the slide's claim, in one sentence: "Builds fail on cold caches", not "Problems". Read in order, the headings tell the whole talk.
18
+ - The body is the evidence for the heading. Show it as a figure, a chart, or a short table where you can.
19
+ - Keep about four groups on a slide at most. With more, nest them or split the slide.
20
+
21
+ ## How it looks
22
+
23
+ - One thing stands out most on each slide, and it is the main point. Emphasis used everywhere is emphasis nowhere.
24
+ - Change color, type, or layout only when the meaning changes; the audience looks for a reason in every change, so never vary for variety. Mark sections with one layout used the same way each time.
25
+ - When things differ, make them differ clearly. A small difference is not seen as one.
26
+ - Line things up. Items side by side share their top, their bottom, and the line their text starts on; a row of cards is one height even when one holds less. Items stacked share their left edge. An edge a little off reads as a mistake, not a choice, and a shot shows it where markup does not.
27
+ - What sits close, aligns, or shares a color reads as one group. Do not set items of different rank as a row of equal cards.
28
+ - Position means something: time and steps run left to right, more or better goes up, the general sits above the specific. A person or anything with a direction faces into the slide or toward what it points at.
29
+ - The eye goes from the heading at top left, to the body in the middle, to the conclusion or next step at bottom right.
30
+
31
+ ## Charts
32
+
33
+ - A line for a trend, bars to compare values. Show amounts by position or length, not by area, angle, or hue.
34
+ - Label lines and bars directly instead of using a legend. Put each note next to what it explains.
35
+ - No icon or picture that only decorates.
36
+
37
+ ## Check
38
+
39
+ 1. `bunx dekc ls <deck>` lists the headings in order. Read only them: does the talk hold?
40
+ 2. `bunx dekc shot --sheet`, then each slide's own shot: where does the eye land first, and is that the main point? Read `fill` in `bunx dekc check <slug> --json` for where the slide is empty.
41
+ 3. In each slide's shot, edges line up: in a row, tops, bottoms, and the first lines of text; in a column, left edges. Fix any that do not.
42
+ 4. Every difference in position, color, size, and look has a reason you can say. Remove the ones that do not.
@@ -1,7 +1,8 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { basename, dirname, join } from "node:path";
3
+ import { shellName } from "../core/escape.ts";
3
4
  import { isInside } from "../core/path.ts";
4
- import { COMMANDS } from "./commands.ts";
5
+ import { type AnySpec, COMMANDS, subcommandsOf } from "./commands.ts";
5
6
 
6
7
  /** A deck a hint is about: the name a command takes, and the directory that makes it implicit. */
7
8
  export type HintDeck = { name: string; dir: string };
@@ -10,20 +11,72 @@ export type HintDeck = { name: string; dir: string };
10
11
  * A hint's commands as they run from `cwd`. Hints are written as if run inside the deck they are
11
12
  * about, where a command finds its deck by itself. Run anywhere else, a command that takes a deck
12
13
  * needs its name: as the first word outside every deck, and as `--deck` inside another deck,
13
- * where a first word names something in that deck instead. A command that already names a deck,
14
- * or takes none, is left as written.
14
+ * where a first word names something in that deck instead, or where the name would read as a
15
+ * subcommand or a flag. The name is one shell word, however it is spelled, or `<deck>` when no
16
+ * word holds it alike in every shell, with where to find it unless `note` is false, for a hint
17
+ * that joins several and says it once. A command that already names a deck, or takes none, is
18
+ * left as written.
15
19
  */
16
- export function addressHint(hint: string, deck: HintDeck | undefined, cwd: string): string {
20
+ export function addressHint(
21
+ hint: string,
22
+ deck: HintDeck | undefined,
23
+ cwd: string,
24
+ { note: noted = true }: { note?: boolean } = {},
25
+ ): string {
17
26
  if (deck === undefined || isInside(cwd, deck.dir)) {
18
27
  return hint;
19
28
  }
20
- const naming = deckAround(cwd) === undefined ? deck.name : `--deck ${deck.name}`;
21
- return hint.replace(COMMAND_RE, (whole, command: string, rest: string) => {
22
- if (!DECK_COMMANDS.has(command) || namesDeck(rest, deck.name)) {
23
- return whole;
24
- }
25
- return `\`dekc ${command} ${naming}${rest}\``;
26
- });
29
+ const word = shellName(deck.name) ?? DECK_PLACEHOLDER;
30
+ const inOther = deckAround(cwd) !== undefined;
31
+ // The note a placeholder adds names `dekc ls` for the project, not for the deck.
32
+ const note = hint.indexOf(PLACEHOLDER_NOTE);
33
+ let placed = false;
34
+ const addressed = hint.replace(
35
+ COMMAND_RE,
36
+ (whole, command: string, rest: string, offset: number) => {
37
+ const subcommand = SUBCOMMAND_WORDS.get(command)?.has(deck.name) === true;
38
+ // A first word that is a subcommand, or a flag, is never the deck's name.
39
+ const positional = !subcommand && !deck.name.startsWith("-");
40
+ if (
41
+ !DECK_COMMANDS.has(command) ||
42
+ namesDeck(rest, positional ? word : undefined) ||
43
+ (note >= 0 && offset >= note && offset < note + PLACEHOLDER_NOTE.length)
44
+ ) {
45
+ return whole;
46
+ }
47
+ placed ||= word === DECK_PLACEHOLDER;
48
+ const flag = inOther || subcommand;
49
+ return `\`dekc ${command} ${flag ? deckFlag(deck.name) : deckWord(deck.name)}${rest}\``;
50
+ },
51
+ );
52
+ return placed && note < 0 && noted ? `${addressed}${PLACEHOLDER_NOTE}` : addressed;
53
+ }
54
+
55
+ /** `hint` with where to find a deck's name, once, when one of its commands has `<deck>` for it. */
56
+ export function withPlaceholderNote(hint: string): string {
57
+ return hint.includes(DECK_PLACEHOLDER) && !hint.includes(PLACEHOLDER_NOTE)
58
+ ? `${hint}${PLACEHOLDER_NOTE}`
59
+ : hint;
60
+ }
61
+
62
+ /** Where a hint with `<deck>` in it says to find the deck's name. */
63
+ const PLACEHOLDER_NOTE = "; for <deck>, name the deck as `dekc ls` lists it";
64
+
65
+ /** What a command shows for a deck no word holds alike in every shell. */
66
+ const DECK_PLACEHOLDER = "<deck>";
67
+
68
+ /**
69
+ * A deck's name as the first word of a command: one shell word, `<deck>` when there is none, or
70
+ * `--deck=` when the name starts with a dash, which a first word would read as a flag.
71
+ */
72
+ export function deckWord(name: string): string {
73
+ return name.startsWith("-") ? deckFlag(name) : (shellName(name) ?? DECK_PLACEHOLDER);
74
+ }
75
+
76
+ /** A deck's name as `--deck`, joined with `=` when it starts with a dash, as a value must be. */
77
+ export function deckFlag(name: string): string {
78
+ const word = shellName(name) ?? DECK_PLACEHOLDER;
79
+ return name.startsWith("-") ? `--deck=${word}` : `--deck ${word}`;
27
80
  }
28
81
 
29
82
  /** A backquoted command: `dekc`, a command word, and what follows it up to the closing quote. */
@@ -33,9 +86,26 @@ const DECK_COMMANDS = new Set(
33
86
  Object.entries(COMMANDS).flatMap(([name, spec]) => ("scope" in spec && spec.scope ? [name] : [])),
34
87
  );
35
88
 
36
- function namesDeck(rest: string, name: string): boolean {
37
- const [first] = rest.trim().split(/\s+/);
38
- return first === name || first?.includes("/") === true || /(?:^| )--deck(?: |$)/.test(rest);
89
+ /** The first word of each command's subcommands, which a deck's name in its place would read as. */
90
+ const SUBCOMMAND_WORDS = new Map(
91
+ Object.entries(COMMANDS).map(([name, spec]) => [
92
+ name,
93
+ new Set(Object.keys(subcommandsOf(spec as AnySpec)).map((sub) => sub.split(" ")[0])),
94
+ ]),
95
+ );
96
+
97
+ /**
98
+ * Whether a command's words already name a deck: as `word`, the deck's name as one shell word,
99
+ * when the deck can be named first, as a ref, or with --deck.
100
+ */
101
+ function namesDeck(rest: string, word: string | undefined): boolean {
102
+ const words = rest.trim();
103
+ const [first] = words.split(/\s+/);
104
+ return (
105
+ (word !== undefined && (words === word || words.startsWith(`${word} `))) ||
106
+ first?.includes("/") === true ||
107
+ /(?:^| )--deck(?:[ =]|$)/.test(rest)
108
+ );
39
109
  }
40
110
 
41
111
  /**
package/src/cli/build.ts CHANGED
@@ -5,7 +5,7 @@ import { lintDeck, lintProject } from "../core/lint.ts";
5
5
  import { parsePublicUrl } from "../core/ogp.ts";
6
6
  import { PLAYWRIGHT_INSTALL, type PlaywrightRunner } from "../core/playwright.ts";
7
7
  import { playerScript } from "../runtime/player.ts";
8
- import { type SkippedCheck, skippedChecks, visualSkipped } from "./result.ts";
8
+ import { RENDERED_RULES, type SkippedCheck, skippedChecks, visualSkipped } from "./result.ts";
9
9
  import type { DecksTarget } from "./scope.ts";
10
10
 
11
11
  /**
@@ -77,7 +77,7 @@ export async function buildCommand(
77
77
  ...skippedChecks([
78
78
  ...skipped,
79
79
  visualSkipped("dekc lint --visual", {
80
- reason: "overflow and contrast are measured only by `dekc lint --visual`",
80
+ reason: `${RENDERED_RULES} are measured only by \`dekc lint --visual\``,
81
81
  ...(options.runner ? { runner: options.runner } : {}),
82
82
  }),
83
83
  ]),
package/src/cli/check.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Diagnostic } from "../core/diagnostic.ts";
2
2
  import { DekError } from "../core/error.ts";
3
+ import { shellQuote } from "../core/escape.ts";
3
4
  import type { Fill } from "../core/fill.ts";
4
5
  import { lintDeck } from "../core/lint.ts";
5
6
  import { type PlaywrightRunner, playwrightMissingError } from "../core/playwright.ts";
@@ -53,7 +54,7 @@ export async function checkCommand(
53
54
 
54
55
  const skipped: SkippedCheck[] = visual
55
56
  ? []
56
- : [visualSkipped(`dekc check ${slug}`, { reason: "Playwright is not installed" })];
57
+ : [visualSkipped(`dekc check ${shellQuote(slug)}`, { reason: "Playwright is not installed" })];
57
58
 
58
59
  // A live-only deck is done without voice, so asking for it skips the check rather than failing,
59
60
  // the way a missing Playwright skips visual.
@@ -1,4 +1,5 @@
1
1
  import { hasErrors } from "../core/diagnostic.ts";
2
+ import { shellQuote } from "../core/escape.ts";
2
3
  import { isRefName } from "../core/ref-name.ts";
3
4
  import { mergeSarif, toSarif } from "../core/sarif.ts";
4
5
  import type { CheckCliResult } from "./check.ts";
@@ -24,6 +25,7 @@ import {
24
25
  formatNew,
25
26
  formatPack,
26
27
  formatPdf,
28
+ formatPptx,
27
29
  formatRef,
28
30
  formatReport,
29
31
  formatShot,
@@ -444,7 +446,7 @@ export const COMMANDS = {
444
446
  text: formatCheck,
445
447
  notes: (data, color) => formatSkipped(data.skipped, color) || undefined,
446
448
  paths: withDisplayDiagnostics,
447
- failure: (data) => diagnosticFailure("check", data, `dekc check ${data.slug}`),
449
+ failure: (data) => diagnosticFailure("check", data, `dekc check ${shellQuote(data.slug)}`),
448
450
  },
449
451
  }),
450
452
  shot: result({
@@ -659,7 +661,7 @@ export const COMMANDS = {
659
661
  group: "Output",
660
662
  overview: [
661
663
  ["dekc lint [--fix]", "check script.md against slides; --fix syncs first"],
662
- ["dekc lint --visual", "add overflow and contrast rules"],
664
+ ["dekc lint --visual", "add overflow, contrast, line-break, and alignment rules"],
663
665
  ["dekc lint --format sarif", "print diagnostics as SARIF, for code scanning"],
664
666
  ],
665
667
  agent: ["dekc lint [--fix] [--visual] [--format sarif]"],
@@ -827,19 +829,22 @@ export const COMMANDS = {
827
829
  flags: ["root-dist", "public"],
828
830
  usage: ["dekc pptx [deck] [--root-dist] [--public]"],
829
831
  summary:
830
- "Write dist/<deck>.pptx, one slide per slide at its last beat: a picture of it with its text\nin text boxes to edit, and its script in the notes. Needs Playwright.",
832
+ "Write dist/<deck>.pptx, one slide per slide at its last beat: its plain boxes, pictures, and\ntext as PowerPoint's own over a picture of the rest, in the deck's theme, and its script in\nthe notes. Needs Playwright.",
831
833
  group: "Output",
832
834
  overview: [
833
- ["dekc pptx [--root-dist] [--public]", "write a PowerPoint file with editable text"],
835
+ [
836
+ "dekc pptx [--root-dist] [--public]",
837
+ "write a PowerPoint file with editable shapes and text",
838
+ ],
834
839
  ],
835
840
  agent: [
836
- "dekc pptx [--root-dist] [--public] text editable over a picture of each slide; script in notes",
841
+ "dekc pptx [--root-dist] [--public] boxes, pictures, and text editable over a picture of the rest; script in notes",
837
842
  ],
838
843
  run: async ({ target, flags }) => {
839
844
  const { pptxCommand } = await import("./pptx.ts");
840
845
  return pptxCommand(target, { rootDist: flags["root-dist"], public: flags.public });
841
846
  },
842
- output: { text: formatPdf },
847
+ output: { text: formatPptx, json: ({ outs }) => ({ outs }) },
843
848
  }),
844
849
  help: {
845
850
  kind: "help",
@@ -214,7 +214,15 @@ export const RESULT_FIELDS = {
214
214
  "State classes the theme selects that the player sets at runtime (is-current, is-shown); never written in markup.",
215
215
  ),
216
216
  tokens: z.array(z.strictObject({ name: z.string(), value: z.string() })),
217
- layouts: z.array(z.strictObject({ name: z.string(), example: z.string().optional() })),
217
+ layouts: z
218
+ .array(z.strictObject({ name: z.string(), example: z.string().optional() }))
219
+ .describe("The layouts whose names are plain words, which dek prints."),
220
+ unprintedLayouts: z
221
+ .number()
222
+ .optional()
223
+ .describe(
224
+ "How many layouts the theme has whose names are no plain word, left out of layouts; absent when none.",
225
+ ),
218
226
  layout: z.strictObject({ name: z.string(), example: z.string() }).optional(),
219
227
  ref: RefInfo.optional(),
220
228
  }),
package/src/cli/files.ts CHANGED
@@ -4,6 +4,7 @@ import { bundledTheme } from "../core/bundled-theme.ts";
4
4
  import type { DekConfig } from "../core/config.ts";
5
5
  import { deckPaths } from "../core/deck-paths.ts";
6
6
  import { DekError } from "../core/error.ts";
7
+ import { shellQuote } from "../core/escape.ts";
7
8
  import { walkUp } from "../core/optional.ts";
8
9
  import { PLAYWRIGHT_INSTALL } from "../core/playwright.ts";
9
10
  import { readSourceIfExists, writeInside } from "../core/safe-fs.ts";
@@ -316,8 +317,3 @@ function inNodeModules(root: string, ...path: string[]): boolean {
316
317
  walkUp(root, (dir) => existsSync(join(dir, "node_modules", ...path)) || undefined) === true
317
318
  );
318
319
  }
319
-
320
- /** `word` as one shell word: as it is when that is safe, else single-quoted. */
321
- export function shellQuote(word: string): string {
322
- return /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
323
- }
package/src/cli/flags.ts CHANGED
@@ -41,7 +41,7 @@ export const FLAGS = {
41
41
  fix: { type: "boolean", text: "create missing skeleton slides before linting" },
42
42
  visual: {
43
43
  type: "boolean",
44
- text: "add the rendered rules: overflow and contrast (needs Playwright)",
44
+ text: "add the rendered rules: overflow, contrast, line breaks, and alignment (needs Playwright)",
45
45
  },
46
46
  shot: { type: "boolean", text: "also write a screenshot and return its path" },
47
47
  voice: { type: "boolean", text: "also return each sentence's reading and duration" },
package/src/cli/goto.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { DekError } from "../core/error.ts";
2
2
  import { deckRoutePath } from "../runtime/routes.ts";
3
3
  import { isPidAlive, readDevServerLock } from "../server/lock.ts";
4
+ import { deckFlag, deckWord } from "./address.ts";
5
+ import { isTypedCommand } from "./commands.ts";
4
6
  import { type DeckTarget, requireSection } from "./scope.ts";
5
7
 
6
8
  export type NavResult = {
@@ -82,7 +84,7 @@ function refusal(
82
84
  ): DekError {
83
85
  if (status === 404 && lock.deck !== undefined && lock.deck !== deckName) {
84
86
  return new DekError(`the running dev server serves deck "${lock.deck}", not "${deckName}"`, {
85
- hint: `pass --deck ${lock.deck}, or stop the server at ${lock.url} and run \`dekc ${deckName}\``,
87
+ hint: `pass ${deckFlag(lock.deck)}, or stop the server at ${lock.url} and run \`dekc ${isTypedCommand(deckName) ? deckFlag(deckName) : deckWord(deckName)}\``,
86
88
  });
87
89
  }
88
90
  if (status === 401) {
package/src/cli/init.ts CHANGED
@@ -3,10 +3,12 @@ import { basename, join, resolve } from "node:path";
3
3
  import { parseCss, publishedTokens } from "../core/css.ts";
4
4
  import { deckPaths } from "../core/deck-paths.ts";
5
5
  import { DekError } from "../core/error.ts";
6
+ import { shellQuote } from "../core/escape.ts";
6
7
  import { REQUIRED_TOKENS } from "../core/lint/tokens.ts";
7
8
  import { walkUp } from "../core/optional.ts";
8
9
  import { DECK_NAME_HINT, isDeckName } from "../core/path.ts";
9
- import { readTextIfExists } from "../core/resolve.ts";
10
+ import { refuseInsideRef } from "../core/ref.ts";
11
+ import { readSourceIfExists } from "../core/safe-fs.ts";
10
12
  import {
11
13
  defaultTsconfig,
12
14
  dekFilePaths,
@@ -25,7 +27,6 @@ import {
25
27
  nextSteps,
26
28
  type PlannedPath,
27
29
  playwrightStep,
28
- shellQuote,
29
30
  } from "./files.ts";
30
31
 
31
32
  export type InitResult = {
@@ -84,7 +85,7 @@ export function initCommand(options: { cwd: string; dir?: string; deck?: string
84
85
  /** The tokens every deck needs that the project's theme.css does not publish on `.slide`. */
85
86
  function missingThemeTokens(root: string): string[] {
86
87
  const published = new Set(
87
- publishedTokens(parseCss(readTextIfExists(join(root, "theme.css")) ?? "")).map(
88
+ publishedTokens(parseCss(readSourceIfExists(join(root, "theme.css"), root) ?? "")).map(
88
89
  (token) => token.name,
89
90
  ),
90
91
  );
@@ -100,8 +101,10 @@ function syncDeckFiles(deckDir: string, created: string[]): FileChanges {
100
101
  /** Everything init writes but `.dek/` and AGENTS.md, in the order a reader meets it. */
101
102
  function projectPlan(root: string, deck: string | undefined): PlannedPath[] {
102
103
  const themePath = join(root, "theme.css");
103
- // A first deck is drawn in the theme that will be there: the one kept, or dek's.
104
- const theme = readTextIfExists(themePath) ?? defaultTheme();
104
+ // A first deck is drawn in the theme that will be there: the one kept, or dek's. A folder that
105
+ // is not there yet holds no theme.
106
+ const theme =
107
+ (existsSync(root) ? readSourceIfExists(themePath, root) : undefined) ?? defaultTheme();
105
108
  return [
106
109
  { path: join(root, "dek.toml"), contents: defaultToml() },
107
110
  { path: themePath, contents: defaultTheme() },
@@ -123,6 +126,8 @@ function checkTarget(root: string): void {
123
126
  if (existsSync(root) && !statSync(root).isDirectory()) {
124
127
  throw new DekError("not a directory", { path: root, hint: "pass a directory to init" });
125
128
  }
129
+ // A ref is someone else's project, read where it is: init would write its files into it.
130
+ refuseInsideRef(root);
126
131
  const outer = walkUp(root, (dir) => {
127
132
  const config = join(dir, "dek.toml");
128
133
  return existsSync(config) && statSync(config).isFile() ? dir : undefined;
package/src/cli/lint.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { relative } from "node:path";
3
3
  import type { Diagnostic } from "../core/diagnostic.ts";
4
+ import { shellQuote } from "../core/escape.ts";
4
5
  import { lintDeck, lintProject, unreadableScriptDiagnostics } from "../core/lint.ts";
5
6
  import { playwrightMissingError } from "../core/playwright.ts";
6
7
  import { resolveRumdlBin, runRumdl } from "../core/rumdl.ts";
7
8
  import { mergeSarif, type SarifLog } from "../core/sarif.ts";
8
9
  import { syncDeck } from "../core/sync.ts";
9
10
  import { lintVisualDeck } from "../core/visual.ts";
10
- import { type SkippedCheck, skippedChecks, visualSkipped } from "./result.ts";
11
+ import { RENDERED_RULES, type SkippedCheck, skippedChecks, visualSkipped } from "./result.ts";
11
12
  import type { DecksTarget } from "./scope.ts";
12
13
 
13
14
  export type LintCliResult = {
@@ -60,7 +61,7 @@ export async function lintCommand(
60
61
  ? []
61
62
  : [
62
63
  visualSkipped("dekc lint --visual", {
63
- reason: "overflow and contrast are measured only with --visual",
64
+ reason: `${RENDERED_RULES} are measured only with --visual`,
64
65
  }),
65
66
  ]),
66
67
  ]),
@@ -79,6 +80,6 @@ function rumdlSkipped(scriptPath: string, cwd: string): SkippedCheck {
79
80
  return {
80
81
  check: "rumdl",
81
82
  reason: "rumdl ran but gave no result",
82
- hint: `run \`${bin} check ${relative(cwd, scriptPath) || scriptPath}\` to see why`,
83
+ hint: `run \`${shellQuote(bin)} check ${shellQuote(relative(cwd, scriptPath) || scriptPath)}\` to see why`,
83
84
  };
84
85
  }
package/src/cli/main.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { DekError } from "../core/error.ts";
2
+ import { shellQuote } from "../core/escape.ts";
2
3
  import { either } from "../core/prose.ts";
3
4
  import { isRefName } from "../core/ref-name.ts";
4
5
  import { resolveProject } from "../core/resolve.ts";
5
- import { addressHint } from "./address.ts";
6
+ import { addressHint, deckWord, type HintDeck, withPlaceholderNote } from "./address.ts";
6
7
  import {
7
8
  type AnySpec,
8
9
  type CliResult,
@@ -13,7 +14,6 @@ import {
13
14
  subcommandAt,
14
15
  subcommandsOf,
15
16
  } from "./commands.ts";
16
- import { shellQuote } from "./files.ts";
17
17
  import { type CommandLine, parseCommandLine, wantsJson } from "./flags.ts";
18
18
  import { writeFailure, writeSuccess } from "./result.ts";
19
19
  import { DeckRequiredError, namesDeck, peelDeckArg, resolveTarget } from "./scope.ts";
@@ -197,12 +197,14 @@ function retypedWithDeck<T>(line: CommandLine, cwd: string, resolve: () => T): T
197
197
  }
198
198
  const typed = `\`dekc ${line.typed.map(shellQuote).join(" ")}\``;
199
199
  const [first] = error.decks;
200
+ // Each command is retyped for one deck; where to find a name left as <deck> is said once.
201
+ const retyped = (deck: HintDeck) => addressHint(typed, deck, cwd, { note: false });
200
202
  const hint = !first
201
203
  ? "run `dekc new <name>` to make a deck"
202
204
  : error.decks.length > NAMED_DECKS
203
- ? `run ${addressHint(typed, first, cwd)}, or name another deck in place of ${first.name}; \`dekc ls\` lists them`
204
- : `run ${either(error.decks.map((deck) => addressHint(typed, deck, cwd)))}`;
205
- throw new DekError(error.message, { hint, cause: error });
205
+ ? `run ${retyped(first)}, or name another deck in place of ${deckWord(first.name)}; \`dekc ls\` lists them`
206
+ : `run ${either(error.decks.map(retyped))}`;
207
+ throw new DekError(error.message, { hint: withPlaceholderNote(hint), cause: error });
206
208
  }
207
209
  }
208
210
 
package/src/cli/new.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { loadConfig } from "../core/config.ts";
4
4
  import { deckPaths } from "../core/deck-paths.ts";
5
5
  import { DekError } from "../core/error.ts";
6
6
  import { DECK_NAME_HINT, isDeckName } from "../core/path.ts";
7
7
  import { isRefName } from "../core/ref-name.ts";
8
+ import { readDeckFile } from "../core/resolve.ts";
9
+ import { readSourceIfExists } from "../core/safe-fs.ts";
8
10
  import { syncDeck } from "../core/sync.ts";
9
11
  import { applyPlan, deckPlan, nextSteps } from "./files.ts";
10
12
  import { type RefInfo, requireDeckFromCwd, requireProject, resolveTarget } from "./scope.ts";
@@ -44,12 +46,17 @@ export function newCommand(options: { cwd: string; name: string; themeFrom?: str
44
46
  ? resolveTarget(options.cwd, "deck", { refs: true, deck: options.themeFrom })
45
47
  : undefined;
46
48
  const ref = fromRef?.ref;
47
- const themeSource = fromRef
48
- ? deckPaths(fromRef.deck.dir).theme
49
+ const fromDeck = fromRef
50
+ ? fromRef.deck
49
51
  : options.themeFrom
50
- ? deckPaths(requireDeckFromCwd(options.cwd, options.themeFrom).deck.dir).theme
51
- : join(project.root, "theme.css");
52
- if (!existsSync(themeSource)) {
52
+ ? requireDeckFromCwd(options.cwd, options.themeFrom).deck
53
+ : undefined;
54
+ const themeSource = fromDeck ? deckPaths(fromDeck.dir).theme : join(project.root, "theme.css");
55
+ // What is copied is published with the new deck: a link out of its project is refused.
56
+ const source = fromDeck
57
+ ? readDeckFile(fromDeck.dir, themeSource)
58
+ : readSourceIfExists(themeSource, project.root);
59
+ if (source === undefined) {
53
60
  throw new DekError("theme.css not found", {
54
61
  path: themeSource,
55
62
  hint: ref
@@ -60,7 +67,7 @@ export function newCommand(options: { cwd: string; name: string; themeFrom?: str
60
67
  });
61
68
  }
62
69
 
63
- const theme = `${ref ? provenance(ref) : ""}${readFileSync(themeSource, "utf8")}`;
70
+ const theme = `${ref ? provenance(ref) : ""}${source}`;
64
71
  const { created } = applyPlan(
65
72
  deckPlan(project.root, name, theme, loadConfig(project.configPath)),
66
73
  );