@hajimism/dek 0.4.0 → 0.5.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.ja.md +1 -1
  3. package/README.md +1 -1
  4. package/package.json +8 -7
  5. package/src/cli/address.ts +59 -15
  6. package/src/cli/commands.ts +47 -14
  7. package/src/cli/contract.ts +49 -3
  8. package/src/cli/files.ts +1 -0
  9. package/src/cli/format.ts +5 -4
  10. package/src/cli/goto.ts +41 -10
  11. package/src/cli/keep-alive.ts +12 -6
  12. package/src/cli/lint.ts +32 -13
  13. package/src/cli/ls.ts +5 -1
  14. package/src/cli/main.ts +48 -13
  15. package/src/cli/new.ts +14 -1
  16. package/src/cli/ref.ts +1 -0
  17. package/src/cli/result.ts +42 -4
  18. package/src/cli/scope.ts +40 -1
  19. package/src/cli/shot.ts +77 -6
  20. package/src/cli/show.ts +15 -2
  21. package/src/cli/text.ts +61 -9
  22. package/src/cli/theme.ts +50 -16
  23. package/src/cli/tty.ts +16 -2
  24. package/src/cli/voice.ts +11 -6
  25. package/src/core/assets.ts +18 -10
  26. package/src/core/attribute-value.ts +188 -0
  27. package/src/core/build.ts +3 -3
  28. package/src/core/css-scan.ts +173 -39
  29. package/src/core/css-transform.ts +57 -11
  30. package/src/core/css.ts +19 -13
  31. package/src/core/foreign-text.ts +138 -0
  32. package/src/core/github.ts +1 -0
  33. package/src/core/html-entities.ts +318 -0
  34. package/src/core/html-scan.ts +49 -27
  35. package/src/core/html.ts +21 -15
  36. package/src/core/lint/asset-refs.ts +44 -6
  37. package/src/core/lint/context.ts +2 -0
  38. package/src/core/lint/shared-styles.ts +26 -1
  39. package/src/core/lint/slide-html.ts +21 -18
  40. package/src/core/lint/theme-css.ts +64 -3
  41. package/src/core/lint.ts +15 -2
  42. package/src/core/mv.ts +28 -18
  43. package/src/core/order.ts +14 -0
  44. package/src/core/path.ts +8 -2
  45. package/src/core/playwright-module.d.ts +12 -1
  46. package/src/core/playwright-motion.ts +1 -0
  47. package/src/core/playwright-sheet.ts +1 -0
  48. package/src/core/playwright-visual.ts +27 -10
  49. package/src/core/playwright.ts +77 -19
  50. package/src/core/pptx.ts +1 -0
  51. package/src/core/ref-name.ts +8 -3
  52. package/src/core/ref.ts +145 -47
  53. package/src/core/resolve.ts +3 -2
  54. package/src/core/rumdl.ts +51 -25
  55. package/src/core/safe-fs.ts +19 -2
  56. package/src/core/sandbox.ts +217 -0
  57. package/src/core/shot-cache.ts +1 -0
  58. package/src/core/slide-eval-worker.ts +1 -0
  59. package/src/core/slide-measure.ts +1 -0
  60. package/src/core/slide-script-eval.ts +2 -0
  61. package/src/core/spawn.ts +136 -35
  62. package/src/core/sync.ts +3 -12
  63. package/src/core/tar.ts +315 -0
  64. package/src/core/text-contrast.ts +2 -0
  65. package/src/core/toml-keys.ts +116 -51
  66. package/src/core/url-attributes.ts +27 -9
  67. package/src/core/visual.ts +28 -9
  68. package/src/core/voice.ts +2 -1
  69. package/src/server/auth.ts +21 -0
  70. package/src/server/dev.ts +13 -5
  71. package/src/server/lock.ts +43 -13
  72. package/src/server/serial.ts +8 -2
  73. package/src/video/mux.ts +27 -3
  74. package/src/video/recorder.ts +3 -2
  75. package/src/video/worker.ts +1 -0
  76. package/src/voice/engine.ts +81 -18
  77. package/tsconfig.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,98 @@
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.5.1](https://github.com/hajimism/dek/compare/v0.5.0...v0.5.1) (2026-10-30)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **cli:** stop a dev server whose parent died before the server was up ([80b533f](https://github.com/hajimism/dek/commit/80b533fc691cfc104b6c83f7d99be4d73fc75c8d))
11
+ * **dev:** say when the running server serves another deck only ([333ae06](https://github.com/hajimism/dek/commit/333ae06818fa8ad09f5ed492ae33148cce9254fd))
12
+ * judge a command that skips its stdin by how it exits, not as one that cannot start ([103152c](https://github.com/hajimism/dek/commit/103152cd2132ba9c430ede4d7020844f7a8dcf9a))
13
+ * **lint:** free no DEK013 class a skeleton or a slide script may use ([e32638b](https://github.com/hajimism/dek/commit/e32638b12220055d8a5a329b9e93f9eda47371e8))
14
+ * **lint:** name the files a hint sends you to as paths from where the command ran ([5943dbe](https://github.com/hajimism/dek/commit/5943dbe1b11ac4529faae9cc1b98f22b4c63d214))
15
+ * **lint:** name the files a hint sends you to as paths from where the command ran ([c5fcce3](https://github.com/hajimism/dek/commit/c5fcce3101061a6eaad55b27d94019f5b028e762))
16
+ * **lint:** say how to stay within max_classes in DEK013 and DEK026 ([9c3078b](https://github.com/hajimism/dek/commit/9c3078bad4d93e5f381c99f5b6e0dd2c059f32dc))
17
+
18
+
19
+ ### Documentation
20
+
21
+ * say that DEK035 leaves a deck that breaks by phrase to Chromium ([c21b428](https://github.com/hajimism/dek/commit/c21b4284db4a46a231778a4fa8d42aac38c46b0d))
22
+ * say that DEK035 leaves a deck that breaks by phrase to Chromium ([dfef085](https://github.com/hajimism/dek/commit/dfef085bedf2d120c62a0f7418fd3d69f0a77001))
23
+
24
+ ## [0.5.0](https://github.com/hajimism/dek/compare/v0.4.0...v0.5.0) (2026-10-27)
25
+
26
+
27
+ ### ⚠ BREAKING CHANGES
28
+
29
+ * **ref:** a dek.toml that pins a ref whose name holds a control character or a character a person cannot see no longer loads until that line is removed; `dekc pack` refuses a deck so named until its folder is renamed; and `dekc new` and `dekc init --deck` refuse such a name, which they took before. Every other name works as before.
30
+ * where Chromium's sandbox does not start, `dekc shot <ref>` fails until DEK_ALLOW_UNSANDBOXED=1 is set.
31
+
32
+ ### Features
33
+
34
+ * **ref:** report the characters in a ref that a person cannot see ([5ca233e](https://github.com/hajimism/dek/commit/5ca233e186125e821f1341cc790636f26654274a))
35
+
36
+
37
+ ### Bug Fixes
38
+
39
+ * **build:** keep a minified stylesheet reading as its source where an escape meets a newline or a comment ([41fd1ba](https://github.com/hajimism/dek/commit/41fd1ba08abc63c8af1d9deb8e2b9d70bb5ceab5))
40
+ * **build:** minify a stylesheet into one that reads as its source does ([bdcffb9](https://github.com/hajimism/dek/commit/bdcffb9edaf7238b33bdf740dc4072c6f8cdaaa1))
41
+ * **cli:** at the project root, hint a command only for the decks that have what it names ([fe3f8fb](https://github.com/hajimism/dek/commit/fe3f8fb12c178a7d8bfcb40b4ed16b52d03a08ae))
42
+ * **cli:** hint `dekc ls` for a deck that is not there, not `dekc ls <it>` ([1ee4554](https://github.com/hajimism/dek/commit/1ee455439734f57eaf03c6c91fd78a9423f4812f))
43
+ * **cli:** name a ref by the ref in the hints about it ([e9d1a47](https://github.com/hajimism/dek/commit/e9d1a47fe282b766bed189212891f1aafcc38a94))
44
+ * **dev:** print a failure of the dev server's lint or voice run instead of dropping it ([14a8e8c](https://github.com/hajimism/dek/commit/14a8e8ceb75247d850407aeeef3bba56f5525a88))
45
+ * give up on a voice engine, a dev server, or a rumdl that never answers ([19023f2](https://github.com/hajimism/dek/commit/19023f2c3987c4e60f2fe7440d4e1d5369ec2f10))
46
+ * keep a file's mode when dek writes it anew ([085d7e8](https://github.com/hajimism/dek/commit/085d7e8e4be2895c975cd76668d4c7af91937d96))
47
+ * **lint:** list rumdl as skipped when what it printed is not SARIF ([74582e0](https://github.com/hajimism/dek/commit/74582e07153aeb77211083e87309cbe6214e8ba3))
48
+ * **lint:** name the path beside a stray space in DEK021's hint only when it is a plain word ([b030691](https://github.com/hajimism/dek/commit/b03069178ab6b318749c0296aca5fc916ebc2cec))
49
+ * **lint:** read a URL in DEK011 as the browser's URL parser reads it ([c0950a5](https://github.com/hajimism/dek/commit/c0950a59b6c25f1c561c101a07750b89c0a87283))
50
+ * **lint:** say to run lint --visual again after lint --visual fails ([a00da44](https://github.com/hajimism/dek/commit/a00da4437aadf6373a4e888213f26d6efdd7462a))
51
+ * **ls:** keep a ref's title, event, and date to one line, as AGENTS.md does ([af85b6e](https://github.com/hajimism/dek/commit/af85b6e8ef63175cfb3166465e533e6b361f166e))
52
+ * **mv:** write script.md, slides, and voice.toml whole, never over the old file ([a7a22b3](https://github.com/hajimism/dek/commit/a7a22b34a1fb073181966bec4a7c7e88b88757ce))
53
+ * print no color code a deck or a ref wrote, only dek's own ([27b6751](https://github.com/hajimism/dek/commit/27b6751eb48d84e2538983c618889f74389d5caa))
54
+ * read a pack's or a GitHub tarball's tar with dek's own reader, so it unpacks no more than was counted ([bdda596](https://github.com/hajimism/dek/commit/bdda596bf5974e6c30f5d273090327d0ed114a6b))
55
+ * read each attribute value as the browser does, its character references decoded ([65a58d1](https://github.com/hajimism/dek/commit/65a58d11497648690c539c8caa4bbe49c246a4c8))
56
+ * read the keys after an array or inline table written over several lines ([fa8f268](https://github.com/hajimism/dek/commit/fa8f2680bec36c3b092f5b7eef5749cd4146d64a))
57
+ * read url() and a string cut short by CR or FF where the browser does ([55296f2](https://github.com/hajimism/dek/commit/55296f2a8f04ffdea958b835132bc2ad4c8054d5))
58
+ * **ref:** keep a ref's names out of hints, and to one short line in messages ([67f7d19](https://github.com/hajimism/dek/commit/67f7d19268cb1032978d62a1a0fe1ec2a209197c))
59
+ * **ref:** refuse a pack whose deck's name holds a control character or one a person cannot see ([373f14f](https://github.com/hajimism/dek/commit/373f14f368057677c33d9c8bbaccc427e048d790))
60
+ * **sample:** set the note on why-dek's read-only card in ink ([586d09d](https://github.com/hajimism/dek/commit/586d09d7e706ae56b731ee9c5a4b6f1b03406d2e))
61
+ * say whenever Chromium draws without its sandbox, and draw a ref there only when asked ([293b3ff](https://github.com/hajimism/dek/commit/293b3ffd0fd12ff042831fc4816e96025d7336af))
62
+ * **server:** keep what the dev server serves out of other sites' pages and frames ([260b0f0](https://github.com/hajimism/dek/commit/260b0f081de5d9821312456abbec2849dfac7fd7))
63
+ * **shot:** send a ref's broken slide script to its author, not to lint ([c1045fa](https://github.com/hajimism/dek/commit/c1045faf8583ca61281b825811a172a72ec0bb6c))
64
+ * **show:** keep the name of keyframes written as a string in the theme excerpt ([072b078](https://github.com/hajimism/dek/commit/072b07848a2799f7d2e98f42962330c2e407bbee))
65
+ * **show:** say a slide is a ref's, and start a part on a line no file can write ([b6b2389](https://github.com/hajimism/dek/commit/b6b2389fe5cf8dfc9dac4fefa1eab88737d7075a))
66
+ * sort dict.toml, decks, and slide files the same way on every machine ([29a3f3c](https://github.com/hajimism/dek/commit/29a3f3c4c6073fbdc5808c9921d296fa8e4125cf))
67
+ * start every program dek runs with the environment as it is now ([ee6b319](https://github.com/hajimism/dek/commit/ee6b319d2772ff39ea82cf3dd8fd763d4fb130c8))
68
+ * stop a command at its timeout, even one that ignores SIGTERM or leaves a process behind ([3b05db7](https://github.com/hajimism/dek/commit/3b05db7ee9c17e68e5d580d744dc457856c29678))
69
+ * **tar:** leave AppleDouble names out, and number that record 27 ([c4fb0ef](https://github.com/hajimism/dek/commit/c4fb0efbb9cd0a73f5335f078fb9b08bca1d35ef))
70
+ * **video:** give up on an ffmpeg that never answers -version ([c0d3c6a](https://github.com/hajimism/dek/commit/c0d3c6a22d0dd8328782661ebc22384313d33487))
71
+ * **visual:** stop sending a DEK031 to theme.css when the theme alone was never measured ([b3321b3](https://github.com/hajimism/dek/commit/b3321b3302389743dd5f0606e2a6367e2635f14d))
72
+ * **voice:** stop waiting on a player that never ends after a say clip ([fc1082b](https://github.com/hajimism/dek/commit/fc1082bf3e8eb012bda822256fe51a067eb086b8))
73
+
74
+
75
+ ### Refactoring
76
+
77
+ * write the link preview image and remove the dev server's lock through safe-fs ([3ae94c8](https://github.com/hajimism/dek/commit/3ae94c8db6ae9bef4243bec3cc334e1a4a2b9e11))
78
+
79
+
80
+ ### Documentation
81
+
82
+ * **adr:** keep a ref's words out of dek's hints, and mark what a ref wrote wherever dek prints it ([e4f0787](https://github.com/hajimism/dek/commit/e4f07878aee0f0f887484b800778324a55e0f97d))
83
+ * **adr:** number the --json contract record 31 ([8108f37](https://github.com/hajimism/dek/commit/8108f3770b3c2b24efb57b2ed1064949a998e95b))
84
+ * **adr:** number the fast-check record 26, where this stack adds it ([389948e](https://github.com/hajimism/dek/commit/389948e6e8ef79842d8b429fac5e2d8e5d2ddeb2))
85
+ * **adr:** number the ref-words record 30, and point it at the trust record ([6925755](https://github.com/hajimism/dek/commit/692575572ab5a235ac1256c7b7b11e31a4ddc01f))
86
+ * **adr:** number the sandbox record 29, and point it at the trust record ([e082487](https://github.com/hajimism/dek/commit/e08248711777c3acabd8ac46c1c8a080302de961))
87
+ * **adr:** number the trust record 28, after the records already on this branch ([7a7fbd8](https://github.com/hajimism/dek/commit/7a7fbd8b9cb6f18af2d27f26a1fb962c3e85f30a))
88
+ * **adr:** read a pack's or a repository's tar with dek's own reader ([ba896c8](https://github.com/hajimism/dek/commit/ba896c84daec4a39fdc5d7fdd34c1cf5ccb52394))
89
+ * **adr:** read every attribute value as the browser does ([6a1c698](https://github.com/hajimism/dek/commit/6a1c698fbc74da129c034f175063d8c99a83f9bc))
90
+ * **adr:** say what dek does where Chromium's sandbox cannot start ([f2f940a](https://github.com/hajimism/dek/commit/f2f940ad4c0a216f9f0ec0085da72cfd15414282))
91
+ * **adr:** say whom dek trusts, and what each of them may make it do ([8f26099](https://github.com/hajimism/dek/commit/8f260990674b3c3105b40b276f9e90c6e013e1db))
92
+ * pass the suite timeout on every bun test line in CONTRIBUTING ([6c7182c](https://github.com/hajimism/dek/commit/6c7182cab0564f91088be06b5209c32b6f191efa))
93
+ * say how dek keeps a ref's words apart from its own ([756ba7c](https://github.com/hajimism/dek/commit/756ba7ca74ff05cbd109ef0eab270dea92c52b78))
94
+ * say how long dek waits on rumdl, a voice engine, and the dev server, and how large a ref may be ([e196cd8](https://github.com/hajimism/dek/commit/e196cd81ad83cd90286a91977258bb7c0882eaee))
95
+ * say lint reads an attribute's value and URL as the browser does ([4c67184](https://github.com/hajimism/dek/commit/4c671849309e7ff8015341862b9fc8ec860d0cb7))
96
+
5
97
  ## [0.4.0](https://github.com/hajimism/dek/compare/v0.3.4...v0.4.0) (2026-10-27)
6
98
 
7
99
 
package/README.ja.md CHANGED
@@ -64,7 +64,7 @@ bun run dev # または bun run lint:visual / bun run build
64
64
  ```bash
65
65
  bun install
66
66
  bunx playwright install chromium # 最初に一度。実ブラウザを動かすテストに使う
67
- bun test --watch
67
+ bun test --watch --timeout=20000
68
68
  bun run typecheck
69
69
  bun run check # Biome の format + lint
70
70
  bun run docs:dev # VitePress
package/README.md CHANGED
@@ -64,7 +64,7 @@ bun run dev # or bun run lint:visual / bun run build
64
64
  ```bash
65
65
  bun install
66
66
  bunx playwright install chromium # once, for the tests that drive a real browser
67
- bun test --watch
67
+ bun test --watch --timeout=20000
68
68
  bun run typecheck
69
69
  bun run check # Biome format + lint
70
70
  bun run docs:dev # VitePress
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hajimism/dek",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "A build system for talks.",
5
5
  "license": "MIT",
6
6
  "author": "Hajime Nakagawa (https://github.com/hajimism)",
@@ -43,8 +43,8 @@
43
43
  "ai-agents"
44
44
  ],
45
45
  "scripts": {
46
- "test": "bun test --parallel=4",
47
- "test:watch": "bun test --watch",
46
+ "test": "bun test --parallel=4 --timeout=20000",
47
+ "test:watch": "bun test --watch --timeout=20000",
48
48
  "typecheck": "tsc --noEmit",
49
49
  "knip": "knip",
50
50
  "schema": "bun scripts/cli-schema.ts",
@@ -61,11 +61,12 @@
61
61
  "zod": "^4.6.5"
62
62
  },
63
63
  "devDependencies": {
64
- "@biomejs/biome": "2.5.15",
65
- "@happy-dom/global-registrator": "21.0.1",
64
+ "@biomejs/biome": "2.6.2",
65
+ "@happy-dom/global-registrator": "21.0.4",
66
66
  "@types/bun": "^1.4.2",
67
- "happy-dom": "21.0.1",
68
- "knip": "6.41.0",
67
+ "fast-check": "4.10.2",
68
+ "happy-dom": "21.0.4",
69
+ "knip": "6.42.1",
69
70
  "playwright": "^1.63.0",
70
71
  "publint": "0.3.25",
71
72
  "typescript": "^7.0.2",
@@ -1,29 +1,49 @@
1
- import { existsSync } from "node:fs";
2
- import { basename, dirname, join } from "node:path";
3
- import { shellName } from "../core/escape.ts";
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { basename, dirname, join, relative } from "node:path";
3
+ import { isPlainWord, shellName } from "../core/escape.ts";
4
4
  import { isInside } from "../core/path.ts";
5
+ import { snapshotName } from "../core/ref.ts";
6
+ import { isRefName } from "../core/ref-name.ts";
5
7
  import { type AnySpec, COMMANDS, subcommandsOf } from "./commands.ts";
6
8
 
7
9
  /** A deck a hint is about: the name a command takes, and the directory that makes it implicit. */
8
10
  export type HintDeck = { name: string; dir: string };
9
11
 
10
12
  /**
11
- * A hint's commands as they run from `cwd`. Hints are written as if run inside the deck they are
12
- * about, where a command finds its deck by itself. Run anywhere else, a command that takes a deck
13
- * needs its name: as the first word outside every deck, and as `--deck` inside another deck,
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.
13
+ * A hint as it reads from `cwd`. Hints are written as if read inside the deck they are about:
14
+ * its commands are addressed as `addressCommands` says, and the deck files it names become paths
15
+ * from `cwd`, as the path of the diagnostic beside it is, except in a ref.
19
16
  */
20
17
  export function addressHint(
21
18
  hint: string,
22
19
  deck: HintDeck | undefined,
23
20
  cwd: string,
21
+ options: { note?: boolean } = {},
22
+ ): string {
23
+ if (deck === undefined) {
24
+ return hint;
25
+ }
26
+ const addressed = addressCommands(hint, deck, cwd, options);
27
+ // A ref's files are its author's to fix, so a hint about one names them as the ref does.
28
+ return isRefName(deck.name) ? addressed : addressFiles(addressed, deck.dir, cwd);
29
+ }
30
+
31
+ /**
32
+ * A hint's commands as they run from `cwd`. Inside the deck they are about, a command finds its
33
+ * deck by itself. Run anywhere else, a command that takes a deck needs its name: as the first
34
+ * word outside every deck, and as `--deck` inside another deck, where a first word names
35
+ * something in that deck instead, or where the name would read as a subcommand or a flag. The
36
+ * name is one shell word, however it is spelled, or `<deck>` when no word holds it alike in every
37
+ * shell, with where to find it unless `note` is false, for a hint that joins several and says it
38
+ * once. A command that already names a deck, or takes none, is left as written.
39
+ */
40
+ export function addressCommands(
41
+ hint: string,
42
+ deck: HintDeck,
43
+ cwd: string,
24
44
  { note: noted = true }: { note?: boolean } = {},
25
45
  ): string {
26
- if (deck === undefined || isInside(cwd, deck.dir)) {
46
+ if (isInside(cwd, deck.dir)) {
27
47
  return hint;
28
48
  }
29
49
  const word = shellName(deck.name) ?? DECK_PLACEHOLDER;
@@ -79,6 +99,24 @@ export function deckFlag(name: string): string {
79
99
  return name.startsWith("-") ? `--deck=${word}` : `--deck ${word}`;
80
100
  }
81
101
 
102
+ /**
103
+ * A deck file as a hint names it from the deck: a slide's own file, a voice file, theme.css, or
104
+ * script.md, or a project file reached with `../../`. Not part of a longer path or word. A path
105
+ * from `cwd` that is no plain word, through a folder a hint may not name, is left as written.
106
+ */
107
+ const DECK_FILE_RE =
108
+ /(?<![\w./-])(\.\.\/\.\.\/[\w./-]*[\w-]|(?:slides|voice)\/[\w.-]*[\w-]|theme\.css|script\.md)(?![\w/-])/g;
109
+
110
+ function addressFiles(hint: string, dir: string, cwd: string): string {
111
+ if (cwd === dir) {
112
+ return hint;
113
+ }
114
+ return hint.replace(DECK_FILE_RE, (file) => {
115
+ const path = relative(cwd, join(dir, file)) || ".";
116
+ return isPlainWord(path) ? path : file;
117
+ });
118
+ }
119
+
82
120
  /** A backquoted command: `dekc`, a command word, and what follows it up to the closing quote. */
83
121
  const COMMAND_RE = /`dekc ([a-z]+)((?: [^`]*)?)`/g;
84
122
 
@@ -110,13 +148,19 @@ function namesDeck(rest: string, word: string | undefined): boolean {
110
148
 
111
149
  /**
112
150
  * The deck `path` lies in: the directory under a project's `decks/`, found from the path alone.
113
- * A source path dek reports is absolute, so no project needs to be resolved to address it.
151
+ * A source path dek reports is absolute, so no project needs to be resolved to address it. A
152
+ * deck that is not there, such as the one a `deck "nope" not found` points at, is none, and a
153
+ * ref's deck, in a snapshot as dek writes one, is named by the ref, as every command that reads
154
+ * one takes it.
114
155
  */
115
156
  export function deckAround(path: string): HintDeck | undefined {
116
157
  for (let dir = path; dirname(dir) !== dir; dir = dirname(dir)) {
117
158
  const parent = dirname(dir);
118
- if (basename(parent) === "decks" && existsSync(join(dirname(parent), "dek.toml"))) {
119
- return { name: basename(dir), dir };
159
+ const root = dirname(parent);
160
+ if (basename(parent) === "decks" && existsSync(join(root, "dek.toml"))) {
161
+ return statSync(dir, { throwIfNoEntry: false })?.isDirectory()
162
+ ? { name: snapshotName(root) ?? basename(dir), dir }
163
+ : undefined;
120
164
  }
121
165
  }
122
166
  return undefined;
@@ -1,5 +1,6 @@
1
1
  import { hasErrors } from "../core/diagnostic.ts";
2
2
  import { shellQuote } from "../core/escape.ts";
3
+ import type { HiddenText } from "../core/foreign-text.ts";
3
4
  import { isRefName } from "../core/ref-name.ts";
4
5
  import { mergeSarif, toSarif } from "../core/sarif.ts";
5
6
  import type { CheckCliResult } from "./check.ts";
@@ -15,6 +16,7 @@ import {
15
16
  formatBuild,
16
17
  formatCheck,
17
18
  formatCues,
19
+ formatHidden,
18
20
  formatInit,
19
21
  formatKept,
20
22
  formatKeptTheme,
@@ -65,14 +67,17 @@ type AnyCtx = Omit<Ctx<[], DeckScope | undefined>, "args"> & {
65
67
  /** How a command's result is printed. Only `text` is required. */
66
68
  export type Output<D> = {
67
69
  text(data: D, color: boolean): string;
68
- /** What did not happen, for stderr next to the text: the result on stdout stays the result. */
70
+ /**
71
+ * What did not happen, or what is worth a look, such as what a ref hides, for stderr next to the
72
+ * text: the result on stdout stays the result.
73
+ */
69
74
  notes?(data: D, color: boolean): string | undefined;
70
75
  /** The `--json` fields next to `ok`; the data as-is when absent. */
71
76
  json?(data: D): object;
72
77
  /** The data with every path into the source tree as `display` shows it. */
73
78
  paths?(data: D, display: (path: string) => string): D;
74
- /** The error of a run that did its job and found one. */
75
- failure?(data: D): FormattedError | undefined;
79
+ /** The error of a run that did its job and found one; `flags` are the run's, for its rerun. */
80
+ failure?(data: D, flags: FlagValues): FormattedError | undefined;
76
81
  /** `--format sarif`. */
77
82
  sarif?(data: D): object;
78
83
  };
@@ -194,6 +199,20 @@ function withDisplayDiagnostics<D extends { diagnostics: CheckCliResult["diagnos
194
199
  return { ...data, diagnostics: displayDiagnostics(data.diagnostics, display) };
195
200
  }
196
201
 
202
+ /** What to do about the characters a person cannot see that a ref's theme brought into a new deck. */
203
+ const COPIED_HIDDEN_HELP =
204
+ "they came in with the ref's theme, and a person reading the deck sees none of them; remove them from theme.css unless you mean them";
205
+
206
+ /** The data with each hidden-character report's path as `display` shows it. */
207
+ function withDisplayHidden<D extends { hidden?: HiddenText[] }>(
208
+ data: D,
209
+ display: (path: string) => string,
210
+ ): D {
211
+ return data.hidden === undefined
212
+ ? data
213
+ : { ...data, hidden: data.hidden.map((entry) => ({ ...entry, path: display(entry.path) })) };
214
+ }
215
+
197
216
  /**
198
217
  * Every command, in the order `dekc help` lists them: the words and flags it takes, its help,
199
218
  * what it works on, how it runs, and how its result prints. Each module loads only when its
@@ -289,11 +308,12 @@ export const COMMANDS = {
289
308
  },
290
309
  output: {
291
310
  text: formatNew,
292
- paths: (data, display) => ({
293
- ...data,
294
- created: data.created.map(display),
295
- updated: data.updated.map(display),
296
- }),
311
+ notes: (data, color) => formatHidden(data.hidden, color, COPIED_HIDDEN_HELP) || undefined,
312
+ paths: (data, display) =>
313
+ withDisplayHidden(
314
+ { ...data, created: data.created.map(display), updated: data.updated.map(display) },
315
+ display,
316
+ ),
297
317
  },
298
318
  }),
299
319
  ls: result({
@@ -312,10 +332,12 @@ export const COMMANDS = {
312
332
  },
313
333
  output: {
314
334
  text: formatLs,
335
+ notes: (data, color) =>
336
+ (data.kind === "deck" && formatHidden(data.hidden, color)) || undefined,
315
337
  json: ({ kind: _kind, ...data }) => data,
316
338
  paths: (data, display) =>
317
339
  data.kind === "deck"
318
- ? withDisplayDiagnostics(data, display)
340
+ ? withDisplayHidden(withDisplayDiagnostics(data, display), display)
319
341
  : {
320
342
  ...withDisplayDiagnostics(data, display),
321
343
  decks: data.decks.map((deck) => withDisplayDiagnostics(deck, display)),
@@ -399,7 +421,11 @@ export const COMMANDS = {
399
421
  const { showCommand } = await import("./show.ts");
400
422
  return showCommand(target, args.slug);
401
423
  },
402
- output: { text: formatShow },
424
+ output: {
425
+ text: formatShow,
426
+ notes: (data, color) => formatHidden(data.hidden, color) || undefined,
427
+ paths: withDisplayHidden,
428
+ },
403
429
  }),
404
430
  theme: result({
405
431
  scope: "deck",
@@ -424,7 +450,8 @@ export const COMMANDS = {
424
450
  },
425
451
  output: {
426
452
  text: formatTheme,
427
- paths: (data, display) => ({ ...data, path: display(data.path) }),
453
+ notes: (data, color) => formatHidden(data.hidden, color) || undefined,
454
+ paths: (data, display) => withDisplayHidden({ ...data, path: display(data.path) }, display),
428
455
  },
429
456
  }),
430
457
  check: result({
@@ -486,7 +513,11 @@ export const COMMANDS = {
486
513
  motion: flags.motion,
487
514
  });
488
515
  },
489
- output: { text: formatShot },
516
+ output: {
517
+ text: formatShot,
518
+ notes: (data, color) => formatHidden(data.hidden, color) || undefined,
519
+ paths: withDisplayHidden,
520
+ },
490
521
  }),
491
522
  mv: result({
492
523
  scope: "deck",
@@ -674,7 +705,9 @@ export const COMMANDS = {
674
705
  notes: (data, color) => formatSkipped(data.skipped, color) || undefined,
675
706
  json: ({ rumdlSarif: _rumdlSarif, ...data }) => data,
676
707
  paths: withDisplayDiagnostics,
677
- failure: (data) => diagnosticFailure("lint", data, "dekc lint"),
708
+ // The rerun measures what this run did: without --visual, the errors it found may not show.
709
+ failure: (data, flags) =>
710
+ diagnosticFailure("lint", data, flags.visual ? "dekc lint --visual" : "dekc lint"),
678
711
  sarif: (data) => {
679
712
  const dek = data.diagnostics.filter((diagnostic) => diagnostic.id.startsWith("DEK"));
680
713
  return mergeSarif(toSarif(dek, { skipped: data.skipped ?? [] }), data.rumdlSarif);
@@ -844,7 +877,7 @@ export const COMMANDS = {
844
877
  const { pptxCommand } = await import("./pptx.ts");
845
878
  return pptxCommand(target, { rootDist: flags["root-dist"], public: flags.public });
846
879
  },
847
- output: { text: formatPptx, json: ({ outs }) => ({ outs }) },
880
+ output: { text: formatPptx, json: ({ lifted: _lifted, ...data }) => data },
848
881
  }),
849
882
  help: {
850
883
  kind: "help",
@@ -50,6 +50,19 @@ const SkippedCheck = z
50
50
  description: "A check that did not run, so an empty list of findings is no pass for it.",
51
51
  });
52
52
 
53
+ const Unsandboxed = z
54
+ .strictObject({
55
+ reason: z.string().describe("What Chromium said when it did not start with its sandbox."),
56
+ hint: z
57
+ .string()
58
+ .describe("What lets the sandbox start, and, unless already set, DEK_ALLOW_UNSANDBOXED=1."),
59
+ })
60
+ .meta({
61
+ id: "Unsandboxed",
62
+ description:
63
+ "Chromium drew without its sandbox, which did not start here, as in a container running as root. A ref is never drawn so unless DEK_ALLOW_UNSANDBOXED=1.",
64
+ });
65
+
53
66
  const Failure = z
54
67
  .strictObject({
55
68
  message: z.string(),
@@ -68,6 +81,27 @@ const RefInfo = z
68
81
  })
69
82
  .meta({ id: "RefInfo", description: "Set when the deck read is a ref." });
70
83
 
84
+ const HiddenText = z
85
+ .strictObject({
86
+ path: z.string(),
87
+ line: z.number().int(),
88
+ column: z
89
+ .number()
90
+ .int()
91
+ .describe("1-based, in UTF-16 units as editors count: where the first of them is."),
92
+ codePoints: z.array(z.string()).describe("Each of them on the line, in order, as U+XXXX."),
93
+ })
94
+ .meta({
95
+ id: "HiddenText",
96
+ description:
97
+ "A line of a ref's file that holds characters a person cannot see: those Unicode marks default-ignorable, such as tag characters, zero-width characters, the byte order mark, bidi controls, and Hangul fillers, but not variation selectors; the line and paragraph separators; and the interlinear annotation marks. Text output shows each as ⟨U+XXXX⟩.",
98
+ });
99
+
100
+ const hidden = z
101
+ .array(HiddenText)
102
+ .optional()
103
+ .describe("A ref's lines that hold characters a person cannot see; absent when none does.");
104
+
71
105
  const Position = z
72
106
  .strictObject({ slideIndex: z.number().int(), beatIndex: z.number().int() })
73
107
  .meta({ id: "Position" });
@@ -75,6 +109,9 @@ const Position = z
75
109
  const paths = z.array(z.string());
76
110
  const diagnostics = z.array(Diagnostic);
77
111
  const skipped = z.array(SkippedCheck).optional().describe("Absent when every check ran.");
112
+ const unsandboxed = Unsandboxed.optional().describe(
113
+ "Absent when every page drawn was drawn in Chromium's sandbox, or none was.",
114
+ );
78
115
 
79
116
  const nav = z.strictObject({
80
117
  slug: z.string(),
@@ -112,6 +149,7 @@ export const RESULT_FIELDS = {
112
149
  updated: paths,
113
150
  next: z.array(z.string()),
114
151
  warnings: z.array(z.string()).describe("Set when the theme came from a ref with no license."),
152
+ hidden,
115
153
  }),
116
154
  ls: z.union([
117
155
  z.strictObject({
@@ -149,6 +187,7 @@ export const RESULT_FIELDS = {
149
187
  diagnostics,
150
188
  skipped,
151
189
  ref: RefInfo.optional(),
190
+ hidden,
152
191
  }),
153
192
  ]),
154
193
  ref: z.union([
@@ -202,6 +241,7 @@ export const RESULT_FIELDS = {
202
241
  ),
203
242
  assets: paths,
204
243
  ref: RefInfo.optional(),
244
+ hidden,
205
245
  }),
206
246
  theme: z.strictObject({
207
247
  path: z.string(),
@@ -225,11 +265,13 @@ export const RESULT_FIELDS = {
225
265
  ),
226
266
  layout: z.strictObject({ name: z.string(), example: z.string() }).optional(),
227
267
  ref: RefInfo.optional(),
268
+ hidden,
228
269
  }),
229
270
  check: z.strictObject({
230
271
  slug: z.string(),
231
272
  diagnostics,
232
273
  skipped,
274
+ unsandboxed,
233
275
  shot: z.string().optional(),
234
276
  fill: z
235
277
  .strictObject({
@@ -289,6 +331,8 @@ export const RESULT_FIELDS = {
289
331
  )
290
332
  .optional()
291
333
  .describe("With --motion, each beat in order with its frames, the settled end last."),
334
+ unsandboxed,
335
+ hidden,
292
336
  }),
293
337
  mv: z.strictObject({
294
338
  from: z.string(),
@@ -378,7 +422,7 @@ export const RESULT_FIELDS = {
378
422
  z.strictObject({ action: z.literal("clear"), cleared: z.number().int() }),
379
423
  ]),
380
424
  sync: z.strictObject({ created: paths, updated: paths, removed: paths, kept: paths }),
381
- lint: z.strictObject({ diagnostics, skipped }),
425
+ lint: z.strictObject({ diagnostics, skipped, unsandboxed }),
382
426
  cues: z.strictObject({
383
427
  name: z.string(),
384
428
  cues: z.array(
@@ -417,15 +461,17 @@ export const RESULT_FIELDS = {
417
461
  images: paths.describe("dist/<deck>.png for each deck that got a link preview image."),
418
462
  diagnostics,
419
463
  skipped,
464
+ unsandboxed,
420
465
  }),
421
466
  video: z.strictObject({
422
467
  out: z.string(),
423
468
  vtt: z.string().optional(),
424
469
  chapters: z.string().optional(),
425
470
  credits: z.string().optional(),
471
+ unsandboxed,
426
472
  }),
427
- pdf: z.strictObject({ outs: paths }),
428
- pptx: z.strictObject({ outs: paths }),
473
+ pdf: z.strictObject({ outs: paths, unsandboxed }),
474
+ pptx: z.strictObject({ outs: paths, unsandboxed }),
429
475
  pack: z.strictObject({
430
476
  outs: paths,
431
477
  warnings: z.array(z.string()).describe("Once per project: the packs carry no license."),
package/src/cli/files.ts CHANGED
@@ -1,3 +1,4 @@
1
+ // biome-ignore lint/style/noRestrictedImports: mkdirSync and writeFileSync, for init and new, which create a file only where none is (wx); safe-fs replaces one.
1
2
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
3
  import { dirname, join, relative } from "node:path";
3
4
  import { bundledTheme } from "../core/bundled-theme.ts";
package/src/cli/format.ts CHANGED
@@ -2,7 +2,7 @@ import { isAbsolute, relative } from "node:path";
2
2
  import type { Diagnostic } from "../core/diagnostic.ts";
3
3
  import { type ErrorFields, errorFields } from "../core/error.ts";
4
4
  import type { LiveEvent } from "../core/live-protocol.ts";
5
- import { ansi, displayWidth, padEndWidth, padStartWidth } from "./tty.ts";
5
+ import { ansi, displayWidth, padEndWidth, padStartWidth, plainText } from "./tty.ts";
6
6
 
7
7
  function formatLocation(location: { path?: string; line?: number; column?: number }): string {
8
8
  if (location.path !== undefined && location.line !== undefined) {
@@ -56,17 +56,18 @@ export function formatError(error: unknown, opts?: { cwd?: string }): FormattedE
56
56
  : { ...fields, path: displayPath(fields.path, opts?.cwd) };
57
57
  }
58
58
 
59
+ /** An error as the terminal shows it: its own text plain, and dek's colors around it. */
59
60
  export function formatErrorText(error: unknown, opts?: { color?: boolean; cwd?: string }): string {
60
61
  const formatted = formatError(error, opts);
61
62
  const c = ansi(opts?.color === true);
62
- const parts = [`${c.bold(c.red("error:"))} ${formatted.message}`];
63
+ const parts = [`${c.bold(c.red("error:"))} ${plainText(formatted.message)}`];
63
64
  if (formatted.path) {
64
65
  const loc =
65
66
  formatted.line !== undefined ? `${formatted.path}:${formatted.line}` : formatted.path;
66
- parts.push(` ${c.cyan("-->")} ${loc}`);
67
+ parts.push(` ${c.cyan("-->")} ${plainText(loc)}`);
67
68
  }
68
69
  if (formatted.hint) {
69
- parts.push(` ${c.yellow("help:")} ${formatted.hint}`);
70
+ parts.push(` ${c.yellow("help:")} ${plainText(formatted.hint)}`);
70
71
  }
71
72
  return parts.join("\n");
72
73
  }