@hajimism/dek 0.4.0 → 0.5.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 +73 -0
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/package.json +4 -3
- package/src/cli/address.ts +11 -4
- package/src/cli/commands.ts +47 -14
- package/src/cli/contract.ts +49 -3
- package/src/cli/files.ts +1 -0
- package/src/cli/format.ts +5 -4
- package/src/cli/goto.ts +41 -10
- package/src/cli/lint.ts +32 -13
- package/src/cli/ls.ts +5 -1
- package/src/cli/main.ts +46 -11
- package/src/cli/new.ts +14 -1
- package/src/cli/ref.ts +1 -0
- package/src/cli/result.ts +42 -4
- package/src/cli/scope.ts +40 -1
- package/src/cli/shot.ts +77 -6
- package/src/cli/show.ts +15 -2
- package/src/cli/text.ts +61 -9
- package/src/cli/theme.ts +49 -15
- package/src/cli/tty.ts +16 -2
- package/src/cli/voice.ts +11 -6
- package/src/core/assets.ts +18 -10
- package/src/core/attribute-value.ts +188 -0
- package/src/core/build.ts +3 -3
- package/src/core/css-scan.ts +173 -39
- package/src/core/css-transform.ts +57 -11
- package/src/core/css.ts +19 -13
- package/src/core/foreign-text.ts +138 -0
- package/src/core/github.ts +1 -0
- package/src/core/html-entities.ts +318 -0
- package/src/core/html-scan.ts +49 -27
- package/src/core/html.ts +21 -15
- package/src/core/lint/asset-refs.ts +44 -6
- package/src/core/lint/slide-html.ts +21 -18
- package/src/core/mv.ts +28 -18
- package/src/core/order.ts +14 -0
- package/src/core/path.ts +8 -2
- package/src/core/playwright-module.d.ts +12 -1
- package/src/core/playwright-motion.ts +1 -0
- package/src/core/playwright-sheet.ts +1 -0
- package/src/core/playwright-visual.ts +27 -10
- package/src/core/playwright.ts +77 -19
- package/src/core/pptx.ts +1 -0
- package/src/core/ref-name.ts +8 -3
- package/src/core/ref.ts +145 -47
- package/src/core/resolve.ts +3 -2
- package/src/core/rumdl.ts +51 -25
- package/src/core/safe-fs.ts +19 -2
- package/src/core/sandbox.ts +217 -0
- package/src/core/shot-cache.ts +1 -0
- package/src/core/slide-eval-worker.ts +1 -0
- package/src/core/slide-script-eval.ts +2 -0
- package/src/core/spawn.ts +131 -32
- package/src/core/sync.ts +3 -12
- package/src/core/tar.ts +315 -0
- package/src/core/text-contrast.ts +2 -0
- package/src/core/toml-keys.ts +116 -51
- package/src/core/url-attributes.ts +27 -9
- package/src/core/visual.ts +28 -9
- package/src/core/voice.ts +2 -1
- package/src/server/auth.ts +21 -0
- package/src/server/dev.ts +11 -3
- package/src/server/lock.ts +4 -4
- package/src/server/serial.ts +8 -2
- package/src/video/mux.ts +27 -3
- package/src/video/recorder.ts +3 -2
- package/src/video/worker.ts +1 -0
- package/src/voice/engine.ts +81 -18
package/src/cli/ls.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Diagnostic } from "../core/diagnostic.ts";
|
|
2
|
+
import type { HiddenText } from "../core/foreign-text.ts";
|
|
2
3
|
import { lintDeck, lintProject } from "../core/lint.ts";
|
|
3
4
|
import { listSlides, type Project, type ProjectDeck } from "../core/resolve.ts";
|
|
4
5
|
import { slideVideoSeconds, timelineSeconds } from "../core/timeline.ts";
|
|
5
6
|
import { sectionTiming } from "../core/timing.ts";
|
|
6
7
|
import { tryLoadCachedTimeline } from "../core/voice.ts";
|
|
7
8
|
import { type SkippedCheck, skippedChecks } from "./result.ts";
|
|
8
|
-
import type
|
|
9
|
+
import { type DecksTarget, hiddenField, hiddenInFiles, type RefInfo } from "./scope.ts";
|
|
9
10
|
|
|
10
11
|
export type LsListResult = {
|
|
11
12
|
kind: "list";
|
|
@@ -46,6 +47,8 @@ export type LsDeckResult = {
|
|
|
46
47
|
skipped?: SkippedCheck[];
|
|
47
48
|
/** Set when the deck is a ref. */
|
|
48
49
|
ref?: RefInfo;
|
|
50
|
+
/** A ref's lines of script.md that hold characters a person cannot see. */
|
|
51
|
+
hidden?: HiddenText[];
|
|
49
52
|
};
|
|
50
53
|
|
|
51
54
|
/** The one deck in scope, a ref included, or else every deck of the project. */
|
|
@@ -58,6 +61,7 @@ export function lsCommand(target: DecksTarget): LsListResult | LsDeckResult {
|
|
|
58
61
|
{ check: "lint", reason: "a ref is read-only; its problems are not yours to fix" },
|
|
59
62
|
]),
|
|
60
63
|
ref,
|
|
64
|
+
...hiddenField(ref, hiddenInFiles(deck.dir, [deck.scriptPath])),
|
|
61
65
|
};
|
|
62
66
|
}
|
|
63
67
|
if (deck) {
|
package/src/cli/main.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { parseCss } from "../core/css.ts";
|
|
2
|
+
import { deckPaths } from "../core/deck-paths.ts";
|
|
1
3
|
import { DekError } from "../core/error.ts";
|
|
2
4
|
import { shellQuote } from "../core/escape.ts";
|
|
3
5
|
import { either } from "../core/prose.ts";
|
|
4
6
|
import { isRefName } from "../core/ref-name.ts";
|
|
5
|
-
import { resolveProject } from "../core/resolve.ts";
|
|
7
|
+
import { type ProjectDeck, readDeckFile, resolveProject } from "../core/resolve.ts";
|
|
8
|
+
import { takeUnsandboxed } from "../core/sandbox.ts";
|
|
9
|
+
import { themeFacts } from "../core/theme-facts.ts";
|
|
6
10
|
import { addressHint, deckWord, type HintDeck, withPlaceholderNote } from "./address.ts";
|
|
7
11
|
import {
|
|
8
12
|
type AnySpec,
|
|
@@ -158,7 +162,7 @@ async function run(cwd: string, line: CommandLine): Promise<void> {
|
|
|
158
162
|
}
|
|
159
163
|
const scope = spec.scope;
|
|
160
164
|
const target = scope
|
|
161
|
-
? retypedWithDeck(line, cwd, () =>
|
|
165
|
+
? retypedWithDeck(line, cwd, call, () =>
|
|
162
166
|
resolveTarget(cwd, scope, {
|
|
163
167
|
deck: call.deck,
|
|
164
168
|
refs: spec.refs === true,
|
|
@@ -170,10 +174,13 @@ async function run(cwd: string, line: CommandLine): Promise<void> {
|
|
|
170
174
|
const ctx = { cwd, flags: line.values, args: call.args, target };
|
|
171
175
|
const data = await form.run?.(ctx);
|
|
172
176
|
if (spec.kind === "result") {
|
|
173
|
-
|
|
177
|
+
// Said on stderr as Chromium drew without its sandbox, and kept here for --json.
|
|
178
|
+
const drawn = { ...(data as object), ...takeUnsandboxed() };
|
|
179
|
+
writeSuccess({ command: call.name, data: drawn } as CliResult, {
|
|
174
180
|
json: line.values.json === true,
|
|
175
181
|
format: line.values.format,
|
|
176
182
|
cwd,
|
|
183
|
+
flags: line.values,
|
|
177
184
|
...(target?.deck && {
|
|
178
185
|
deck: { name: target.ref?.name ?? target.deck.name, dir: target.deck.dir },
|
|
179
186
|
}),
|
|
@@ -186,9 +193,10 @@ const NAMED_DECKS = 3;
|
|
|
186
193
|
|
|
187
194
|
/**
|
|
188
195
|
* `resolve`, with a missing deck hinted as the line that was typed, the deck named where the
|
|
189
|
-
* command takes it: one command per deck of the project
|
|
196
|
+
* command takes it: one command per deck of the project that has what the line names, so the
|
|
197
|
+
* hint runs as written.
|
|
190
198
|
*/
|
|
191
|
-
function retypedWithDeck<T>(line: CommandLine, cwd: string, resolve: () => T): T {
|
|
199
|
+
function retypedWithDeck<T>(line: CommandLine, cwd: string, call: Call, resolve: () => T): T {
|
|
192
200
|
try {
|
|
193
201
|
return resolve();
|
|
194
202
|
} catch (error) {
|
|
@@ -196,18 +204,43 @@ function retypedWithDeck<T>(line: CommandLine, cwd: string, resolve: () => T): T
|
|
|
196
204
|
throw error;
|
|
197
205
|
}
|
|
198
206
|
const typed = `\`dekc ${line.typed.map(shellQuote).join(" ")}\``;
|
|
199
|
-
const
|
|
207
|
+
const decks = error.decks.filter((deck) => runsIn(call, deck));
|
|
208
|
+
const [first] = decks;
|
|
200
209
|
// Each command is retyped for one deck; where to find a name left as <deck> is said once.
|
|
201
210
|
const retyped = (deck: HintDeck) => addressHint(typed, deck, cwd, { note: false });
|
|
202
|
-
const hint =
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
211
|
+
const hint =
|
|
212
|
+
error.decks.length === 0
|
|
213
|
+
? "run `dekc new <name>` to make a deck"
|
|
214
|
+
: !first
|
|
215
|
+
? "run `dekc ls`"
|
|
216
|
+
: decks.length > NAMED_DECKS
|
|
217
|
+
? `run ${retyped(first)}, or name another deck in place of ${deckWord(first.name)}; \`dekc ls\` lists them`
|
|
218
|
+
: `run ${either(decks.map(retyped))}`;
|
|
207
219
|
throw new DekError(error.message, { hint: withPlaceholderNote(hint), cause: error });
|
|
208
220
|
}
|
|
209
221
|
}
|
|
210
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Whether `call` finds in `deck` what it names: a section, the one `dekc mv` renames or the name
|
|
225
|
+
* script.md already gives it, and a layout of the deck's theme.
|
|
226
|
+
*/
|
|
227
|
+
function runsIn(call: Call, deck: ProjectDeck): boolean {
|
|
228
|
+
const { slug, old, layout } = call.args;
|
|
229
|
+
const has = (name: string | undefined) =>
|
|
230
|
+
deck.deck.sections.some((section) => section.slug === name);
|
|
231
|
+
return (
|
|
232
|
+
(slug === undefined || has(slug)) &&
|
|
233
|
+
(old === undefined || has(old) || has(call.args.new)) &&
|
|
234
|
+
(layout === undefined || themeLayouts(deck).includes(layout))
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The layouts a deck's theme defines; none when it has no theme.css. */
|
|
239
|
+
function themeLayouts(deck: ProjectDeck): string[] {
|
|
240
|
+
const css = readDeckFile(deck.dir, deckPaths(deck.dir).theme) ?? "";
|
|
241
|
+
return themeFacts(parseCss(css)).layouts.map((entry) => entry.name);
|
|
242
|
+
}
|
|
243
|
+
|
|
211
244
|
/** A session prints no result, so --json has nothing to shape, and a reader would wait forever. */
|
|
212
245
|
function sessionJsonError(name: string): DekError {
|
|
213
246
|
return new DekError(`${name} keeps running until stopped and prints no JSON`, {
|
|
@@ -220,6 +253,8 @@ function sessionJsonError(name: string): DekError {
|
|
|
220
253
|
* prints once, here: as JSON when the line asks for it, even a line too broken to parse.
|
|
221
254
|
*/
|
|
222
255
|
export async function main(argv: string[], cwd: string): Promise<void> {
|
|
256
|
+
// What a run before this one in the process drew is its own, even one that failed.
|
|
257
|
+
takeUnsandboxed();
|
|
223
258
|
let json = wantsJson(argv);
|
|
224
259
|
try {
|
|
225
260
|
const line = parseCommandLine(argv);
|
package/src/cli/new.ts
CHANGED
|
@@ -3,13 +3,20 @@ 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
|
+
import { type HiddenText, hiddenText } from "../core/foreign-text.ts";
|
|
6
7
|
import { DECK_NAME_HINT, isDeckName } from "../core/path.ts";
|
|
7
8
|
import { isRefName } from "../core/ref-name.ts";
|
|
8
9
|
import { readDeckFile } from "../core/resolve.ts";
|
|
9
10
|
import { readSourceIfExists } from "../core/safe-fs.ts";
|
|
10
11
|
import { syncDeck } from "../core/sync.ts";
|
|
11
12
|
import { applyPlan, deckPlan, nextSteps } from "./files.ts";
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
hiddenField,
|
|
15
|
+
type RefInfo,
|
|
16
|
+
requireDeckFromCwd,
|
|
17
|
+
requireProject,
|
|
18
|
+
resolveTarget,
|
|
19
|
+
} from "./scope.ts";
|
|
13
20
|
|
|
14
21
|
export type NewResult = {
|
|
15
22
|
name: string;
|
|
@@ -21,6 +28,11 @@ export type NewResult = {
|
|
|
21
28
|
next: string[];
|
|
22
29
|
/** Set when the theme came from a ref with no license. */
|
|
23
30
|
warnings: string[];
|
|
31
|
+
/**
|
|
32
|
+
* A ref's theme's lines, now the new deck's theme.css, that hold characters a person cannot
|
|
33
|
+
* see: the copy is the deck's own, so they are its author's to remove.
|
|
34
|
+
*/
|
|
35
|
+
hidden?: HiddenText[];
|
|
24
36
|
};
|
|
25
37
|
|
|
26
38
|
export function newCommand(options: { cwd: string; name: string; themeFrom?: string }): NewResult {
|
|
@@ -87,6 +99,7 @@ export function newCommand(options: { cwd: string; name: string; themeFrom?: str
|
|
|
87
99
|
`${ref.name} has no license; ask its author before you present or share a deck built on its theme`,
|
|
88
100
|
]
|
|
89
101
|
: [],
|
|
102
|
+
...hiddenField(ref, ref ? hiddenText(theme, deckPaths(dir).theme) : []),
|
|
90
103
|
};
|
|
91
104
|
}
|
|
92
105
|
|
package/src/cli/ref.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// biome-ignore lint/style/noRestrictedImports: rmdirSync, which takes packs/ and refs/ folders only while empty; removeInside would take what is in them.
|
|
1
2
|
import { existsSync, lstatSync, readdirSync, readFileSync, rmdirSync, statSync } from "node:fs";
|
|
2
3
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3
4
|
import { loadConfig } from "../core/config.ts";
|
package/src/cli/result.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Diagnostic, SkippedCheck } from "../core/diagnostic.ts";
|
|
2
2
|
import { DekError } from "../core/error.ts";
|
|
3
|
+
import { revealHidden } from "../core/foreign-text.ts";
|
|
3
4
|
import { PLAYWRIGHT_INSTALL, type PlaywrightRunner, playwrightReady } from "../core/playwright.ts";
|
|
4
5
|
import { addressHint, deckAround, type HintDeck } from "./address.ts";
|
|
5
6
|
import { type CliResult, outputOf } from "./commands.ts";
|
|
7
|
+
import type { FlagValues } from "./flags.ts";
|
|
6
8
|
import { displayPath, formatError, formatErrorText } from "./format.ts";
|
|
7
|
-
import { shouldColor, terminalSafe } from "./tty.ts";
|
|
9
|
+
import { plainText, shouldColor, terminalSafe } from "./tty.ts";
|
|
8
10
|
|
|
9
11
|
export type { CliResult, SkippedCheck };
|
|
10
12
|
|
|
@@ -41,6 +43,8 @@ export type WriteSuccessOptions = {
|
|
|
41
43
|
cwd?: string;
|
|
42
44
|
/** The one deck the command worked on, which hints that name no path are about. */
|
|
43
45
|
deck?: HintDeck;
|
|
46
|
+
/** The flags it ran with, which a failure's rerun repeats where they change what is checked. */
|
|
47
|
+
flags?: FlagValues;
|
|
44
48
|
};
|
|
45
49
|
|
|
46
50
|
/** Where a run's hints are read: the directory it ran in, and the deck it worked on, if one. */
|
|
@@ -86,7 +90,7 @@ export function writeSuccess(original: CliResult, options: WriteSuccessOptions):
|
|
|
86
90
|
const { cwd } = options;
|
|
87
91
|
const addressed = cwd === undefined ? original : addressResult(original, { cwd, ...options });
|
|
88
92
|
const result = cwd === undefined ? addressed : displayPaths(addressed, cwd);
|
|
89
|
-
const failed = output.failure?.(result.data);
|
|
93
|
+
const failed = output.failure?.(result.data, options.flags ?? {});
|
|
90
94
|
const failure =
|
|
91
95
|
failed?.hint === undefined || cwd === undefined
|
|
92
96
|
? failed
|
|
@@ -98,9 +102,13 @@ export function writeSuccess(original: CliResult, options: WriteSuccessOptions):
|
|
|
98
102
|
const data = output.json ? output.json(result.data) : (result.data as object);
|
|
99
103
|
process.stdout.write(`${JSON.stringify({ ...envelope, ...data })}\n`);
|
|
100
104
|
} else {
|
|
101
|
-
const
|
|
105
|
+
const plain = {
|
|
106
|
+
...result,
|
|
107
|
+
data: plainStrings(result.data, isAboutRef(result.data)),
|
|
108
|
+
} as CliResult;
|
|
109
|
+
const text = formatText(plain, shouldColor(process.stdout));
|
|
102
110
|
process.stdout.write(`${terminalSafe(text)}\n`);
|
|
103
|
-
const notes = output.notes?.(
|
|
111
|
+
const notes = output.notes?.(plain.data, shouldColor(process.stderr));
|
|
104
112
|
if (notes) {
|
|
105
113
|
process.stderr.write(`${terminalSafe(notes)}\n`);
|
|
106
114
|
}
|
|
@@ -126,6 +134,36 @@ export function displayPaths(result: CliResult, cwd: string): CliResult {
|
|
|
126
134
|
return { ...result, data: paths(result.data, (path) => displayPath(path, cwd)) } as CliResult;
|
|
127
135
|
}
|
|
128
136
|
|
|
137
|
+
/** A result that read a ref, as `ls`, `show`, and `theme` mark theirs. */
|
|
138
|
+
function isAboutRef(data: unknown): boolean {
|
|
139
|
+
return typeof data === "object" && data !== null && "ref" in data && data.ref !== undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Every string in a result as plainText, so the text a deck or a ref wrote reaches the terminal
|
|
144
|
+
* with no escape of its own; the text formatters paint dek's colors after. In a result about a
|
|
145
|
+
* ref, each character a person cannot see shows as `⟨U+XXXX⟩` too.
|
|
146
|
+
*/
|
|
147
|
+
function plainStrings<T>(value: T, reveal: boolean): T {
|
|
148
|
+
if (typeof value === "string") {
|
|
149
|
+
const plain = plainText(value);
|
|
150
|
+
return (reveal ? revealHidden(plain) : plain) as T;
|
|
151
|
+
}
|
|
152
|
+
if (Array.isArray(value)) {
|
|
153
|
+
return value.map((entry) => plainStrings(entry, reveal)) as T;
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
value !== null &&
|
|
157
|
+
typeof value === "object" &&
|
|
158
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
159
|
+
) {
|
|
160
|
+
return Object.fromEntries(
|
|
161
|
+
Object.entries(value).map(([key, entry]) => [key, plainStrings(entry, reveal)]),
|
|
162
|
+
) as T;
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
|
|
129
167
|
/** A result as text, without what did not run; see writeSuccess. */
|
|
130
168
|
export function formatText(result: CliResult, color = false): string {
|
|
131
169
|
return outputOf(result.command).text(result.data, color);
|
package/src/cli/scope.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { join, relative } from "node:path";
|
|
2
2
|
import { DekError } from "../core/error.ts";
|
|
3
3
|
import { shellName, shellQuote } from "../core/escape.ts";
|
|
4
|
+
import { foreignLine, type HiddenText, hiddenText, MESSAGE_MAX } from "../core/foreign-text.ts";
|
|
4
5
|
import {
|
|
5
6
|
isRefName,
|
|
6
7
|
packFile,
|
|
7
8
|
parseRefSource,
|
|
9
|
+
type RefSource,
|
|
8
10
|
refLicense,
|
|
9
11
|
refReadOnlyError,
|
|
10
12
|
refState,
|
|
@@ -14,6 +16,7 @@ import {
|
|
|
14
16
|
locateDeck,
|
|
15
17
|
type Project,
|
|
16
18
|
type ProjectDeck,
|
|
19
|
+
readDeckFile,
|
|
17
20
|
requireSection,
|
|
18
21
|
resolveProject,
|
|
19
22
|
} from "../core/resolve.ts";
|
|
@@ -222,6 +225,25 @@ export type RefInfo = {
|
|
|
222
225
|
/** One deck to read: the project's own, or a ref. */
|
|
223
226
|
export type ReadableDeck = DeckTarget & { ref?: RefInfo };
|
|
224
227
|
|
|
228
|
+
/**
|
|
229
|
+
* `{ hidden }` when a ref's files hold characters a person cannot see, nothing otherwise. Only a
|
|
230
|
+
* ref's are reported: a deck of the project's own is its author's, emoji joiners and all.
|
|
231
|
+
*/
|
|
232
|
+
export function hiddenField(
|
|
233
|
+
ref: RefInfo | undefined,
|
|
234
|
+
found: HiddenText[],
|
|
235
|
+
): { hidden?: HiddenText[] } {
|
|
236
|
+
return ref && found.length > 0 ? { hidden: found } : {};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** What the files at `paths` in the deck hold that a person cannot see, file by file. */
|
|
240
|
+
export function hiddenInFiles(deckDir: string, paths: string[]): HiddenText[] {
|
|
241
|
+
return paths.flatMap((path) => {
|
|
242
|
+
const text = readDeckFile(deckDir, path);
|
|
243
|
+
return text === undefined ? [] : hiddenText(text, path);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
225
247
|
/**
|
|
226
248
|
* What a command declares it works on, resolved: one deck or the decks in scope. Only a command
|
|
227
249
|
* whose spec says `refs` reads a ref; it never writes, so a ref cannot reach a command that
|
|
@@ -287,7 +309,7 @@ function resolveRef(cwd: string, arg: string): ReadableDeck & { ref: RefInfo } {
|
|
|
287
309
|
if (!found) {
|
|
288
310
|
const failed = snapshot.failed.find((entry) => entry.name === source.deck);
|
|
289
311
|
if (failed) {
|
|
290
|
-
throw failed.error;
|
|
312
|
+
throw unreadableRef(failed.error, source);
|
|
291
313
|
}
|
|
292
314
|
throw new DekError(`ref "${source.name}" has no deck "${source.deck}"`, {
|
|
293
315
|
path: dir,
|
|
@@ -304,3 +326,20 @@ function resolveRef(cwd: string, arg: string): ReadableDeck & { ref: RefInfo } {
|
|
|
304
326
|
ref: { name: source.name, rev: pinned, dir, license: refLicense(dir) },
|
|
305
327
|
};
|
|
306
328
|
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* A ref whose script.md cannot be read. The error's hint would say how to fix the file, quoting
|
|
332
|
+
* it, and a ref is not the reader's to fix: the message stays, on one line, and the hint is dek's.
|
|
333
|
+
*/
|
|
334
|
+
function unreadableRef(error: Error, source: RefSource): DekError {
|
|
335
|
+
const fields = error instanceof DekError ? error : undefined;
|
|
336
|
+
return new DekError(foreignLine(error.message, MESSAGE_MAX), {
|
|
337
|
+
...(fields?.path !== undefined ? { path: fields.path } : {}),
|
|
338
|
+
...(fields?.line !== undefined ? { line: fields.line } : {}),
|
|
339
|
+
hint:
|
|
340
|
+
source.kind === "file"
|
|
341
|
+
? "a ref is read-only, so its script.md is its author's to fix; ask them for a pack that reads, and add it with `dekc ref <file>.tgz`"
|
|
342
|
+
: `a ref is read-only, so its script.md is its author's to fix; pin a commit where it reads with \`dekc ref ${source.name}@<rev>\``,
|
|
343
|
+
cause: error,
|
|
344
|
+
});
|
|
345
|
+
}
|
package/src/cli/shot.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import { basename, dirname, extname } from "node:path";
|
|
2
|
+
import { deckPaths } from "../core/deck-paths.ts";
|
|
1
3
|
import { DekError } from "../core/error.ts";
|
|
2
|
-
import { shellQuote } from "../core/escape.ts";
|
|
4
|
+
import { shellName, shellQuote } from "../core/escape.ts";
|
|
5
|
+
import { foreignLine, type HiddenText, MESSAGE_MAX } from "../core/foreign-text.ts";
|
|
6
|
+
import { refPlaywrightRunner } from "../core/playwright.ts";
|
|
7
|
+
import { SLIDE_SIDECARS } from "../core/resolve.ts";
|
|
3
8
|
import { parseMorphAt, shotMorph } from "../core/shot/morph.ts";
|
|
4
9
|
import { type ShotMotion, shotMotion } from "../core/shot/motion.ts";
|
|
5
10
|
import { type ShotFile, shotDeck, shotSheet } from "../core/shot/still.ts";
|
|
6
11
|
import { playerScript } from "../runtime/player.ts";
|
|
7
|
-
import type
|
|
12
|
+
import { hiddenField, hiddenInFiles, type ReadableDeck } from "./scope.ts";
|
|
8
13
|
import { usageError } from "./usage.ts";
|
|
9
14
|
|
|
10
15
|
export type ShotCliResult = {
|
|
@@ -13,6 +18,11 @@ export type ShotCliResult = {
|
|
|
13
18
|
sheets?: string[];
|
|
14
19
|
/** With --motion, each beat's frames and the ms into its go each shows. */
|
|
15
20
|
motion?: ShotMotion["beats"];
|
|
21
|
+
/**
|
|
22
|
+
* A ref's lines, in the files of the slides drawn and in its theme, that hold characters a
|
|
23
|
+
* person cannot see.
|
|
24
|
+
*/
|
|
25
|
+
hidden?: HiddenText[];
|
|
16
26
|
};
|
|
17
27
|
|
|
18
28
|
type ShotCommandOptions = {
|
|
@@ -32,13 +42,64 @@ export type ShotMode =
|
|
|
32
42
|
| { kind: "morph"; from: string; to: string; at: number };
|
|
33
43
|
|
|
34
44
|
export async function shotCommand(
|
|
35
|
-
|
|
45
|
+
target: ReadableDeck,
|
|
36
46
|
options: ShotCommandOptions,
|
|
37
47
|
): Promise<ShotCliResult> {
|
|
38
48
|
const mode = parseShotMode(options);
|
|
49
|
+
try {
|
|
50
|
+
const result = await shoot(target, mode);
|
|
51
|
+
return target.ref
|
|
52
|
+
? { ...result, ...hiddenField(target.ref, drawnHidden(target, mode)) }
|
|
53
|
+
: result;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw refShotError(error, target);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What the files of the slides a shot drew, and its theme, hold that a person cannot see. */
|
|
60
|
+
function drawnHidden({ deck }: ReadableDeck, mode: ShotMode): HiddenText[] {
|
|
61
|
+
const all = deck.deck.sections.map((section) => section.slug);
|
|
62
|
+
const slugs =
|
|
63
|
+
mode.kind === "sheet"
|
|
64
|
+
? all
|
|
65
|
+
: mode.kind === "morph"
|
|
66
|
+
? [mode.from, mode.to]
|
|
67
|
+
: mode.slug === undefined
|
|
68
|
+
? all
|
|
69
|
+
: [mode.slug];
|
|
70
|
+
const paths = deckPaths(deck.dir);
|
|
71
|
+
const files = slugs.flatMap((slug) =>
|
|
72
|
+
([".html", ...SLIDE_SIDECARS] as const).map((ext) => paths.slide(slug, ext)),
|
|
73
|
+
);
|
|
74
|
+
return hiddenInFiles(deck.dir, [...new Set(files), paths.theme]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A ref's slide script that does not compile stops its slide's shot, as one of the project's
|
|
79
|
+
* would; Bun's message quotes the ref's source, so it is one foreignLine, and the fix is not lint,
|
|
80
|
+
* which refuses a ref, but its author's.
|
|
81
|
+
*/
|
|
82
|
+
function refShotError(error: unknown, { deck, ref }: ReadableDeck): unknown {
|
|
83
|
+
if (!ref || !(error instanceof DekError) || error.path === undefined) {
|
|
84
|
+
return error;
|
|
85
|
+
}
|
|
86
|
+
const path = error.path;
|
|
87
|
+
if (extname(path) !== ".ts" || dirname(path) !== deckPaths(deck.dir).slides) {
|
|
88
|
+
return error;
|
|
89
|
+
}
|
|
90
|
+
return new DekError(foreignLine(error.message, MESSAGE_MAX), {
|
|
91
|
+
path,
|
|
92
|
+
hint: `a ref's slide scripts are its author's to fix; \`dekc shot ${ref.name} <slug>\` still draws each of its other slides, and \`dekc show ${ref.name} ${shellName(basename(path, ".ts")) ?? "<slug>"}\` prints this one's`,
|
|
93
|
+
cause: error,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function shoot({ project, deck, ref }: ReadableDeck, mode: ShotMode): Promise<ShotCliResult> {
|
|
98
|
+
// A ref's slide scripts are someone else's: Chromium runs them only in its sandbox (ADR 29).
|
|
99
|
+
const runner = ref ? { runner: refPlaywrightRunner } : {};
|
|
39
100
|
switch (mode.kind) {
|
|
40
101
|
case "sheet": {
|
|
41
|
-
const { sheets, shots } = await shotSheet({ project, deck });
|
|
102
|
+
const { sheets, shots } = await shotSheet({ project, deck }, runner);
|
|
42
103
|
return { shots, sheets };
|
|
43
104
|
}
|
|
44
105
|
case "motion": {
|
|
@@ -48,6 +109,7 @@ export async function shotCommand(
|
|
|
48
109
|
slug: mode.slug,
|
|
49
110
|
...(mode.step === undefined ? {} : { step: mode.step }),
|
|
50
111
|
playerScript: await playerScript(),
|
|
112
|
+
...runner,
|
|
51
113
|
},
|
|
52
114
|
);
|
|
53
115
|
return { shots: [], sheets, motion: beats };
|
|
@@ -55,12 +117,21 @@ export async function shotCommand(
|
|
|
55
117
|
case "morph": {
|
|
56
118
|
const shots = await shotMorph(
|
|
57
119
|
{ project, deck },
|
|
58
|
-
{
|
|
120
|
+
{
|
|
121
|
+
from: mode.from,
|
|
122
|
+
to: mode.to,
|
|
123
|
+
at: mode.at,
|
|
124
|
+
playerScript: await playerScript(),
|
|
125
|
+
...runner,
|
|
126
|
+
},
|
|
59
127
|
);
|
|
60
128
|
return { shots };
|
|
61
129
|
}
|
|
62
130
|
case "still": {
|
|
63
|
-
const shots = await shotDeck(
|
|
131
|
+
const shots = await shotDeck(
|
|
132
|
+
{ project, deck },
|
|
133
|
+
{ slug: mode.slug, step: mode.step, ...runner },
|
|
134
|
+
);
|
|
64
135
|
return { shots };
|
|
65
136
|
}
|
|
66
137
|
}
|
package/src/cli/show.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { classifyAssetRef } from "../core/assets.ts";
|
|
2
2
|
import { cssUrls, parseCss } from "../core/css.ts";
|
|
3
3
|
import { deckPaths } from "../core/deck-paths.ts";
|
|
4
|
+
import { type HiddenText, hiddenText } from "../core/foreign-text.ts";
|
|
4
5
|
import { type HtmlScan, scanSlideHtml } from "../core/html-scan.ts";
|
|
5
6
|
import { sectionChunks, splitLines } from "../core/lines.ts";
|
|
6
7
|
import { readDeckFile } from "../core/resolve.ts";
|
|
7
8
|
import type { Section } from "../core/schema.ts";
|
|
8
9
|
import { themeExcerpt } from "../core/theme-excerpt.ts";
|
|
9
|
-
import { type ReadableDeck, type RefInfo, requireSection } from "./scope.ts";
|
|
10
|
+
import { hiddenField, type ReadableDeck, type RefInfo, requireSection } from "./scope.ts";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Everything one slide is made of, read in one call: enough to learn how it is
|
|
@@ -34,6 +35,8 @@ export type ShowResult = {
|
|
|
34
35
|
assets: string[];
|
|
35
36
|
/** Set when the deck is a ref. */
|
|
36
37
|
ref?: RefInfo;
|
|
38
|
+
/** A ref's lines, in the files show read, that hold characters a person cannot see. */
|
|
39
|
+
hidden?: HiddenText[];
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
export function showCommand({ deck, ref }: ReadableDeck, slug: string): ShowResult {
|
|
@@ -64,11 +67,14 @@ export function showCommand({ deck, ref }: ReadableDeck, slug: string): ShowResu
|
|
|
64
67
|
...(scan?.refs.map((ref) => ref.value) ?? []),
|
|
65
68
|
...(sheet ? cssUrls(sheet).map((url) => url.value) : []),
|
|
66
69
|
];
|
|
70
|
+
const script = sectionSource(deck, section);
|
|
71
|
+
const hiddenIn = (path: string, text: string | null) =>
|
|
72
|
+
text === null ? [] : hiddenText(text, path);
|
|
67
73
|
|
|
68
74
|
return {
|
|
69
75
|
slug: section.slug,
|
|
70
76
|
title: section.title,
|
|
71
|
-
script
|
|
77
|
+
script,
|
|
72
78
|
beats: section.beats.map(({ id, title, line }) => ({ ...(id ? { id } : {}), title, line })),
|
|
73
79
|
html,
|
|
74
80
|
css,
|
|
@@ -76,6 +82,13 @@ export function showCommand({ deck, ref }: ReadableDeck, slug: string): ShowResu
|
|
|
76
82
|
theme,
|
|
77
83
|
assets: existingRefs(refs, paths.slides, deck.dir),
|
|
78
84
|
...(ref ? { ref } : {}),
|
|
85
|
+
...hiddenField(ref, [
|
|
86
|
+
...hiddenText(script, deck.scriptPath, section.line),
|
|
87
|
+
...hiddenIn(paths.slide(slug, ".html"), html),
|
|
88
|
+
...hiddenIn(paths.slide(slug, ".css"), css),
|
|
89
|
+
...hiddenIn(paths.slide(slug, ".ts"), ts),
|
|
90
|
+
...hiddenIn(paths.theme, themeCss),
|
|
91
|
+
]),
|
|
79
92
|
};
|
|
80
93
|
}
|
|
81
94
|
|
package/src/cli/text.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// The pieces the dev server's log shares, such as diagnostics and tables, are in format.ts.
|
|
3
3
|
import type { Diagnostic, SkippedCheck } from "../core/diagnostic.ts";
|
|
4
4
|
import { shellQuote } from "../core/escape.ts";
|
|
5
|
+
import { foreignLine, type HiddenText, TITLE_MAX } from "../core/foreign-text.ts";
|
|
6
|
+
import { compareCodeUnits } from "../core/order.ts";
|
|
5
7
|
import { unprinted } from "../core/prose.ts";
|
|
6
8
|
import { packFile, parseRefSource } from "../core/ref-name.ts";
|
|
7
9
|
import { formatClock } from "../core/timing.ts";
|
|
@@ -53,7 +55,7 @@ export function formatReport(diagnostics: Diagnostic[], opts?: { color?: boolean
|
|
|
53
55
|
byRule.set(id, (byRule.get(id) ?? 0) + 1);
|
|
54
56
|
}
|
|
55
57
|
const rules = [...byRule]
|
|
56
|
-
.sort(([a, x], [b, y]) => y - x || a
|
|
58
|
+
.sort(([a, x], [b, y]) => y - x || compareCodeUnits(a, b))
|
|
57
59
|
.map(([id, n]) => `${id} ×${n}`);
|
|
58
60
|
return `${list}\n${countSummary(diagnostics)}: ${rules.join(", ")}`;
|
|
59
61
|
}
|
|
@@ -69,6 +71,39 @@ export function formatSkipped(skipped: SkippedCheck[] | undefined, color = false
|
|
|
69
71
|
.join("\n");
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/** How many code points a warning names before it leaves the rest to `--json`. */
|
|
75
|
+
const CODE_POINTS_SHOWN = 6;
|
|
76
|
+
|
|
77
|
+
/** What to do about the characters a person cannot see in a ref that a command read. */
|
|
78
|
+
const HIDDEN_HELP =
|
|
79
|
+
"text output shows each as ⟨U+XXXX⟩; a person reading the deck sees none of them, so leave them out of what you write from it";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A warning per line of a ref's files that holds characters a person cannot see, where they
|
|
83
|
+
* are and which, then `help`, what to do about them, once, for stderr.
|
|
84
|
+
*/
|
|
85
|
+
export function formatHidden(
|
|
86
|
+
hidden: HiddenText[] | undefined,
|
|
87
|
+
color = false,
|
|
88
|
+
help = HIDDEN_HELP,
|
|
89
|
+
): string {
|
|
90
|
+
if (hidden === undefined || hidden.length === 0) {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
const c = ansi(color);
|
|
94
|
+
const lines = hidden.map(({ path, line, column, codePoints }) => {
|
|
95
|
+
const distinct = [...new Set(codePoints)];
|
|
96
|
+
const named = distinct.slice(0, CODE_POINTS_SHOWN).join(", ");
|
|
97
|
+
const rest = distinct.length > CODE_POINTS_SHOWN ? ", …" : "";
|
|
98
|
+
const count =
|
|
99
|
+
codePoints.length === 1
|
|
100
|
+
? "1 character that shows as nothing or reorders the text"
|
|
101
|
+
: `${codePoints.length} characters that show as nothing or reorder the text`;
|
|
102
|
+
return `${c.yellow("warning:")} ${path}:${line}:${column}: ${count}: ${named}${rest}`;
|
|
103
|
+
});
|
|
104
|
+
return [...lines, ` ${c.yellow("help:")} ${help}`].join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
72
107
|
/**
|
|
73
108
|
* The slides sync kept though their section is gone, for stderr: sync removes only what it wrote,
|
|
74
109
|
* so saying nothing would read as a deck with nothing left over.
|
|
@@ -282,15 +317,17 @@ export function formatLs(data: LsListResult | LsDeckResult): string {
|
|
|
282
317
|
].join("\n");
|
|
283
318
|
}
|
|
284
319
|
case "deck": {
|
|
320
|
+
// A ref's frontmatter is someone else's: each value keeps to its line, as in AGENTS.md.
|
|
321
|
+
const line = (text: string) => (data.ref ? foreignLine(text, TITLE_MAX) : text);
|
|
285
322
|
const meta: Array<[string, string]> = [];
|
|
286
323
|
if (data.ref) {
|
|
287
324
|
meta.push(["rev", data.ref.rev.slice(0, 7)]);
|
|
288
325
|
}
|
|
289
326
|
if (data.event) {
|
|
290
|
-
meta.push(["event", data.event]);
|
|
327
|
+
meta.push(["event", line(data.event)]);
|
|
291
328
|
}
|
|
292
329
|
if (data.date) {
|
|
293
|
-
meta.push(["date", data.date]);
|
|
330
|
+
meta.push(["date", line(data.date)]);
|
|
294
331
|
}
|
|
295
332
|
if (data.duration) {
|
|
296
333
|
meta.push(["duration", data.duration]);
|
|
@@ -328,7 +365,7 @@ export function formatLs(data: LsListResult | LsDeckResult): string {
|
|
|
328
365
|
];
|
|
329
366
|
const countLabel = formatSkipped(data.skipped) || diagnosticCount(data.diagnostics.length);
|
|
330
367
|
return [
|
|
331
|
-
`${data.ref?.name ?? data.name} ${data.title}`,
|
|
368
|
+
`${data.ref?.name ?? data.name} ${line(data.title)}`,
|
|
332
369
|
...meta.map(([key, value]) => `${padEndWidth(key, keyWidth)} ${value}`),
|
|
333
370
|
"",
|
|
334
371
|
formatTable(headers, rows, [...align]),
|
|
@@ -426,7 +463,11 @@ export function formatVoice(data: VoiceCliResult): string {
|
|
|
426
463
|
}
|
|
427
464
|
}
|
|
428
465
|
|
|
429
|
-
/**
|
|
466
|
+
/**
|
|
467
|
+
* One labeled part per file, so a reader never confuses where a line came from: a part starts
|
|
468
|
+
* with a line `--- <file>`, and a file's own line that starts the same way is shown one space
|
|
469
|
+
* further in, so no file can start a part. A ref's slide says first whose it is.
|
|
470
|
+
*/
|
|
430
471
|
export function formatShow(data: ShowResult): string {
|
|
431
472
|
const parts: Array<[string, string | null]> = [
|
|
432
473
|
["script.md", data.script],
|
|
@@ -436,9 +477,20 @@ export function formatShow(data: ShowResult): string {
|
|
|
436
477
|
["theme.css (the rules this slide uses)", data.theme || null],
|
|
437
478
|
["assets", data.assets.length > 0 ? data.assets.join("\n") : null],
|
|
438
479
|
];
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
480
|
+
const ref = data.ref
|
|
481
|
+
? [`ref ${data.ref.name} at ${data.ref.rev.slice(0, 7)}: someone else's deck, read-only`]
|
|
482
|
+
: [];
|
|
483
|
+
return [
|
|
484
|
+
...ref,
|
|
485
|
+
...parts.flatMap(([label, body]) =>
|
|
486
|
+
body === null ? [] : [`--- ${label}\n${partLines(body.replace(/\n$/, ""))}`],
|
|
487
|
+
),
|
|
488
|
+
].join("\n\n");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** A part's lines, each that would start a part, after any spaces, given one space more. */
|
|
492
|
+
function partLines(body: string): string {
|
|
493
|
+
return body.replace(/^( *)---(?=[^\S\n])/gm, " $1---");
|
|
442
494
|
}
|
|
443
495
|
|
|
444
496
|
/** What a read does for a ref whose snapshot is not there: fetch it, or unpack its pack. */
|
|
@@ -474,7 +526,7 @@ export function formatRef(data: RefCliResult): string {
|
|
|
474
526
|
data.refs.map((ref) => [
|
|
475
527
|
ref.name,
|
|
476
528
|
short(ref.rev),
|
|
477
|
-
ref.fetched ? (ref.title ?? "") : notFetched(ref.name),
|
|
529
|
+
ref.fetched ? foreignLine(ref.title ?? "", TITLE_MAX) : notFetched(ref.name),
|
|
478
530
|
ref.slides === undefined ? "" : String(ref.slides),
|
|
479
531
|
]),
|
|
480
532
|
["left", "left", "left", "right"],
|