@hajimism/dek 0.3.3 → 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 +136 -0
- package/README.ja.md +2 -2
- package/README.md +2 -2
- package/package.json +5 -3
- package/skills/dek/SKILL.md +10 -3
- package/skills/dek-design/SKILL.md +42 -0
- package/src/cli/address.ts +95 -18
- package/src/cli/build.ts +2 -2
- package/src/cli/check.ts +2 -1
- package/src/cli/commands.ts +57 -19
- package/src/cli/contract.ts +58 -4
- package/src/cli/files.ts +2 -5
- package/src/cli/flags.ts +1 -1
- package/src/cli/format.ts +5 -4
- package/src/cli/goto.ts +44 -11
- package/src/cli/init.ts +10 -5
- package/src/cli/lint.ts +36 -16
- package/src/cli/ls.ts +5 -1
- package/src/cli/main.ts +51 -14
- package/src/cli/new.ts +28 -8
- package/src/cli/pptx.ts +9 -4
- package/src/cli/ref.ts +92 -9
- package/src/cli/result.ts +47 -6
- package/src/cli/scope.ts +57 -29
- package/src/cli/shot.ts +78 -6
- package/src/cli/show.ts +15 -2
- package/src/cli/text.ts +88 -11
- package/src/cli/theme.ts +58 -10
- package/src/cli/tty.ts +16 -2
- package/src/cli/voice.ts +15 -9
- package/src/core/alignment.ts +320 -0
- package/src/core/annotation-rows.ts +16 -4
- package/src/core/annotations.ts +19 -1
- 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/config.ts +61 -12
- 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/diagnostic.ts +5 -1
- package/src/core/escape.ts +43 -0
- 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/line-break.ts +275 -0
- package/src/core/lint/asset-refs.ts +50 -8
- package/src/core/lint/files.ts +30 -5
- package/src/core/lint/keys.ts +7 -3
- package/src/core/lint/slide-html.ts +24 -19
- package/src/core/lint/slide-script.ts +7 -5
- package/src/core/mv.ts +57 -27
- package/src/core/order.ts +14 -0
- package/src/core/pack.ts +2 -2
- package/src/core/path.ts +8 -2
- package/src/core/playwright-module.d.ts +13 -1
- package/src/core/playwright-motion.ts +1 -0
- package/src/core/playwright-sheet.ts +1 -0
- package/src/core/playwright-visual.ts +207 -30
- package/src/core/playwright.ts +279 -44
- package/src/core/pptx-package.ts +461 -33
- package/src/core/pptx-paint.ts +1168 -0
- package/src/core/pptx-plan.ts +879 -0
- package/src/core/pptx-text.ts +204 -47
- package/src/core/pptx-theme.ts +222 -0
- package/src/core/pptx.ts +56 -8
- package/src/core/prose.ts +23 -0
- package/src/core/pseudo-text.ts +32 -10
- package/src/core/ref-name.ts +52 -8
- package/src/core/ref.ts +308 -54
- package/src/core/resolve.ts +23 -11
- 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/morph.ts +1 -1
- package/src/core/shot/motion.ts +2 -3
- package/src/core/shot/still.ts +2 -2
- package/src/core/shot-cache.ts +26 -17
- package/src/core/slide-eval-worker.ts +1 -0
- package/src/core/slide-measure.ts +51 -1
- package/src/core/slide-script-eval.ts +12 -8
- package/src/core/slide-script.ts +114 -20
- package/src/core/spawn.ts +131 -32
- package/src/core/sync.ts +5 -14
- 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-cache.ts +2 -0
- package/src/core/visual.ts +182 -24
- package/src/core/voice.ts +5 -6
- package/src/core/zod.ts +9 -1
- package/src/server/auth.ts +21 -0
- package/src/server/dev.ts +26 -7
- package/src/server/lock.ts +50 -7
- package/src/server/routes.ts +7 -2
- package/src/server/serial.ts +8 -2
- package/src/theme/default.css +4 -0
- package/src/video/bake.ts +6 -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/scope.ts
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { join, relative } from "node:path";
|
|
3
2
|
import { DekError } from "../core/error.ts";
|
|
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,
|
|
8
|
-
|
|
9
|
-
readRefMeta,
|
|
9
|
+
type RefSource,
|
|
10
10
|
refLicense,
|
|
11
|
+
refReadOnlyError,
|
|
11
12
|
refState,
|
|
13
|
+
requireProject,
|
|
12
14
|
} from "../core/ref.ts";
|
|
13
15
|
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";
|
|
20
23
|
import { suggest } from "../core/suggest.ts";
|
|
21
24
|
|
|
22
|
-
export { requireSection };
|
|
25
|
+
export { requireProject, requireSection };
|
|
23
26
|
|
|
24
27
|
export type Scope = {
|
|
25
28
|
project: Project;
|
|
@@ -52,18 +55,6 @@ export type DecksTarget = {
|
|
|
52
55
|
ref?: RefInfo;
|
|
53
56
|
};
|
|
54
57
|
|
|
55
|
-
export function requireProject(cwd: string): Project {
|
|
56
|
-
const project = resolveProject(cwd);
|
|
57
|
-
if (existsSync(join(project.root, REF_MARKER))) {
|
|
58
|
-
const name = readRefMeta(project.root)?.name ?? "<ref>";
|
|
59
|
-
throw new DekError("this directory is inside a ref; refs are read-only", {
|
|
60
|
-
path: project.root,
|
|
61
|
-
hint: `run dek from your own project instead: \`dekc show ${name} <slug>\``,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
return project;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
58
|
export function inferDeckName(project: Project, cwd: string): string | undefined {
|
|
68
59
|
const located = locateDeck(project.root, cwd);
|
|
69
60
|
if (!located) {
|
|
@@ -139,9 +130,7 @@ export function resolveScope(cwd: string, options: { deck?: string } = {}): Scop
|
|
|
139
130
|
return { project };
|
|
140
131
|
}
|
|
141
132
|
if (isRefName(name)) {
|
|
142
|
-
throw
|
|
143
|
-
hint: `start a deck on its theme with \`dekc new <name> --theme-from ${name}\`, or read it with \`dekc show ${name} <slug>\` and write what you need in your own deck's theme`,
|
|
144
|
-
});
|
|
133
|
+
throw refReadOnlyError(name);
|
|
145
134
|
}
|
|
146
135
|
const deck = project.decks.find((entry) => entry.name === name);
|
|
147
136
|
if (!deck) {
|
|
@@ -149,10 +138,11 @@ export function resolveScope(cwd: string, options: { deck?: string } = {}): Scop
|
|
|
149
138
|
if (failed) {
|
|
150
139
|
throw failed.error;
|
|
151
140
|
}
|
|
152
|
-
const
|
|
141
|
+
const near = suggest(name, [
|
|
153
142
|
...project.decks.map((entry) => entry.name),
|
|
154
143
|
...project.failed.map((entry) => entry.name),
|
|
155
144
|
]);
|
|
145
|
+
const guess = near === undefined ? undefined : shellName(near);
|
|
156
146
|
throw new DekError(`deck "${name}" not found`, {
|
|
157
147
|
path: join(project.root, "decks", name),
|
|
158
148
|
hint: guess ? `did you mean \`${guess}\`? run \`dekc ls\` for every deck` : "run `dekc ls`",
|
|
@@ -235,6 +225,25 @@ export type RefInfo = {
|
|
|
235
225
|
/** One deck to read: the project's own, or a ref. */
|
|
236
226
|
export type ReadableDeck = DeckTarget & { ref?: RefInfo };
|
|
237
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
|
+
|
|
238
247
|
/**
|
|
239
248
|
* What a command declares it works on, resolved: one deck or the decks in scope. Only a command
|
|
240
249
|
* whose spec says `refs` reads a ref; it never writes, so a ref cannot reach a command that
|
|
@@ -277,18 +286,16 @@ function resolveRef(cwd: string, arg: string): ReadableDeck & { ref: RefInfo } {
|
|
|
277
286
|
hint:
|
|
278
287
|
source.kind === "file"
|
|
279
288
|
? "add it from the pack you were sent: `dekc ref <file>.tgz`"
|
|
280
|
-
: `run \`dekc ref ${arg}\``,
|
|
289
|
+
: `run \`dekc ref ${shellQuote(arg)}\``,
|
|
281
290
|
});
|
|
282
291
|
}
|
|
283
292
|
// What adds the ref again: a pack's ref from its file, any other from its name.
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
? `dekc ref ${packFile(source.deck)}`
|
|
287
|
-
: `dekc ref ${source.name}@${pinned}`;
|
|
293
|
+
const added = source.kind === "file" ? packFile(source.deck) : `${source.name}@${pinned}`;
|
|
294
|
+
const again = `dekc ref ${shellQuote(added)}`;
|
|
288
295
|
if (source.kind === "github" && source.rev !== undefined && source.rev !== pinned) {
|
|
289
296
|
throw new DekError(`"${arg}" is not the pinned version, ${pinned.slice(0, 7)}`, {
|
|
290
297
|
path: project.configPath,
|
|
291
|
-
hint: `run \`dekc ref ${arg}\` to pin that version, or drop @${source.rev}`,
|
|
298
|
+
hint: `run \`dekc ref ${shellQuote(arg)}\` to pin that version, or drop @${source.rev}`,
|
|
292
299
|
});
|
|
293
300
|
}
|
|
294
301
|
if (!fetched) {
|
|
@@ -302,7 +309,7 @@ function resolveRef(cwd: string, arg: string): ReadableDeck & { ref: RefInfo } {
|
|
|
302
309
|
if (!found) {
|
|
303
310
|
const failed = snapshot.failed.find((entry) => entry.name === source.deck);
|
|
304
311
|
if (failed) {
|
|
305
|
-
throw failed.error;
|
|
312
|
+
throw unreadableRef(failed.error, source);
|
|
306
313
|
}
|
|
307
314
|
throw new DekError(`ref "${source.name}" has no deck "${source.deck}"`, {
|
|
308
315
|
path: dir,
|
|
@@ -311,7 +318,28 @@ function resolveRef(cwd: string, arg: string): ReadableDeck & { ref: RefInfo } {
|
|
|
311
318
|
}
|
|
312
319
|
return {
|
|
313
320
|
project: snapshot,
|
|
314
|
-
|
|
321
|
+
// Read where it is, but cached in this project: nothing is written in a snapshot.
|
|
322
|
+
deck: {
|
|
323
|
+
...found,
|
|
324
|
+
cache: { dir: join(project.root, ".cache", relative(project.root, dir)), root: project.root },
|
|
325
|
+
},
|
|
315
326
|
ref: { name: source.name, rev: pinned, dir, license: refLicense(dir) },
|
|
316
327
|
};
|
|
317
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,9 +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";
|
|
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";
|
|
2
8
|
import { parseMorphAt, shotMorph } from "../core/shot/morph.ts";
|
|
3
9
|
import { type ShotMotion, shotMotion } from "../core/shot/motion.ts";
|
|
4
10
|
import { type ShotFile, shotDeck, shotSheet } from "../core/shot/still.ts";
|
|
5
11
|
import { playerScript } from "../runtime/player.ts";
|
|
6
|
-
import type
|
|
12
|
+
import { hiddenField, hiddenInFiles, type ReadableDeck } from "./scope.ts";
|
|
7
13
|
import { usageError } from "./usage.ts";
|
|
8
14
|
|
|
9
15
|
export type ShotCliResult = {
|
|
@@ -12,6 +18,11 @@ export type ShotCliResult = {
|
|
|
12
18
|
sheets?: string[];
|
|
13
19
|
/** With --motion, each beat's frames and the ms into its go each shows. */
|
|
14
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[];
|
|
15
26
|
};
|
|
16
27
|
|
|
17
28
|
type ShotCommandOptions = {
|
|
@@ -31,13 +42,64 @@ export type ShotMode =
|
|
|
31
42
|
| { kind: "morph"; from: string; to: string; at: number };
|
|
32
43
|
|
|
33
44
|
export async function shotCommand(
|
|
34
|
-
|
|
45
|
+
target: ReadableDeck,
|
|
35
46
|
options: ShotCommandOptions,
|
|
36
47
|
): Promise<ShotCliResult> {
|
|
37
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 } : {};
|
|
38
100
|
switch (mode.kind) {
|
|
39
101
|
case "sheet": {
|
|
40
|
-
const { sheets, shots } = await shotSheet({ project, deck });
|
|
102
|
+
const { sheets, shots } = await shotSheet({ project, deck }, runner);
|
|
41
103
|
return { shots, sheets };
|
|
42
104
|
}
|
|
43
105
|
case "motion": {
|
|
@@ -47,6 +109,7 @@ export async function shotCommand(
|
|
|
47
109
|
slug: mode.slug,
|
|
48
110
|
...(mode.step === undefined ? {} : { step: mode.step }),
|
|
49
111
|
playerScript: await playerScript(),
|
|
112
|
+
...runner,
|
|
50
113
|
},
|
|
51
114
|
);
|
|
52
115
|
return { shots: [], sheets, motion: beats };
|
|
@@ -54,12 +117,21 @@ export async function shotCommand(
|
|
|
54
117
|
case "morph": {
|
|
55
118
|
const shots = await shotMorph(
|
|
56
119
|
{ project, deck },
|
|
57
|
-
{
|
|
120
|
+
{
|
|
121
|
+
from: mode.from,
|
|
122
|
+
to: mode.to,
|
|
123
|
+
at: mode.at,
|
|
124
|
+
playerScript: await playerScript(),
|
|
125
|
+
...runner,
|
|
126
|
+
},
|
|
58
127
|
);
|
|
59
128
|
return { shots };
|
|
60
129
|
}
|
|
61
130
|
case "still": {
|
|
62
|
-
const shots = await shotDeck(
|
|
131
|
+
const shots = await shotDeck(
|
|
132
|
+
{ project, deck },
|
|
133
|
+
{ slug: mode.slug, step: mode.step, ...runner },
|
|
134
|
+
);
|
|
63
135
|
return { shots };
|
|
64
136
|
}
|
|
65
137
|
}
|
|
@@ -91,7 +163,7 @@ export function parseShotMode(options: ShotCommandOptions): ShotMode {
|
|
|
91
163
|
if (slug) {
|
|
92
164
|
refuse(
|
|
93
165
|
"--sheet with a slug",
|
|
94
|
-
`--sheet tiles every slide; for one slide's beats, run \`dekc shot ${slug} --motion\``,
|
|
166
|
+
`--sheet tiles every slide; for one slide's beats, run \`dekc shot ${shellQuote(slug)} --motion\``,
|
|
95
167
|
);
|
|
96
168
|
}
|
|
97
169
|
return { kind: "sheet" };
|
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
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// How each command's result reads as text; commands.ts says which command prints which.
|
|
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
|
+
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";
|
|
7
|
+
import { unprinted } from "../core/prose.ts";
|
|
4
8
|
import { packFile, parseRefSource } from "../core/ref-name.ts";
|
|
5
9
|
import { formatClock } from "../core/timing.ts";
|
|
6
10
|
import type { AnnotationsCliResult } from "./annotations.ts";
|
|
@@ -16,6 +20,7 @@ import type { MvResult } from "./mv.ts";
|
|
|
16
20
|
import type { NewResult } from "./new.ts";
|
|
17
21
|
import type { PackCliResult } from "./pack.ts";
|
|
18
22
|
import type { PdfCliResult } from "./pdf.ts";
|
|
23
|
+
import type { PptxCliResult } from "./pptx.ts";
|
|
19
24
|
import type { RefCliResult } from "./ref.ts";
|
|
20
25
|
import type { ShotCliResult } from "./shot.ts";
|
|
21
26
|
import type { ShowResult } from "./show.ts";
|
|
@@ -50,7 +55,7 @@ export function formatReport(diagnostics: Diagnostic[], opts?: { color?: boolean
|
|
|
50
55
|
byRule.set(id, (byRule.get(id) ?? 0) + 1);
|
|
51
56
|
}
|
|
52
57
|
const rules = [...byRule]
|
|
53
|
-
.sort(([a, x], [b, y]) => y - x || a
|
|
58
|
+
.sort(([a, x], [b, y]) => y - x || compareCodeUnits(a, b))
|
|
54
59
|
.map(([id, n]) => `${id} ×${n}`);
|
|
55
60
|
return `${list}\n${countSummary(diagnostics)}: ${rules.join(", ")}`;
|
|
56
61
|
}
|
|
@@ -66,6 +71,39 @@ export function formatSkipped(skipped: SkippedCheck[] | undefined, color = false
|
|
|
66
71
|
.join("\n");
|
|
67
72
|
}
|
|
68
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
|
+
|
|
69
107
|
/**
|
|
70
108
|
* The slides sync kept though their section is gone, for stderr: sync removes only what it wrote,
|
|
71
109
|
* so saying nothing would read as a deck with nothing left over.
|
|
@@ -230,6 +268,27 @@ export function formatPdf(data: PdfCliResult): string {
|
|
|
230
268
|
return data.outs.map((path) => `wrote ${path}`).join("\n");
|
|
231
269
|
}
|
|
232
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Each file written, and what it laid over its pictures, naming the slides that kept everything
|
|
273
|
+
* in the picture, which are the ones to rewrite toward shapes.
|
|
274
|
+
*/
|
|
275
|
+
export function formatPptx(data: PptxCliResult): string {
|
|
276
|
+
return data.outs
|
|
277
|
+
.flatMap((path, index) => {
|
|
278
|
+
const lifted = data.lifted?.[index];
|
|
279
|
+
if (!lifted) {
|
|
280
|
+
return [`wrote ${path}`];
|
|
281
|
+
}
|
|
282
|
+
const none = lifted.untouched.length > 0 ? `; nothing on ${lifted.untouched.join(", ")}` : "";
|
|
283
|
+
const count = (n: number, one: string) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
284
|
+
return [
|
|
285
|
+
`wrote ${path}`,
|
|
286
|
+
` laid ${count(lifted.shapes, "shape")} and ${count(lifted.pictures, "picture")} over the picture${none}`,
|
|
287
|
+
];
|
|
288
|
+
})
|
|
289
|
+
.join("\n");
|
|
290
|
+
}
|
|
291
|
+
|
|
233
292
|
export function formatPack(data: PackCliResult): string {
|
|
234
293
|
return [formatPdf(data), ...data.warnings.map((warning) => `warning: ${warning}`)].join("\n");
|
|
235
294
|
}
|
|
@@ -258,15 +317,17 @@ export function formatLs(data: LsListResult | LsDeckResult): string {
|
|
|
258
317
|
].join("\n");
|
|
259
318
|
}
|
|
260
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);
|
|
261
322
|
const meta: Array<[string, string]> = [];
|
|
262
323
|
if (data.ref) {
|
|
263
324
|
meta.push(["rev", data.ref.rev.slice(0, 7)]);
|
|
264
325
|
}
|
|
265
326
|
if (data.event) {
|
|
266
|
-
meta.push(["event", data.event]);
|
|
327
|
+
meta.push(["event", line(data.event)]);
|
|
267
328
|
}
|
|
268
329
|
if (data.date) {
|
|
269
|
-
meta.push(["date", data.date]);
|
|
330
|
+
meta.push(["date", line(data.date)]);
|
|
270
331
|
}
|
|
271
332
|
if (data.duration) {
|
|
272
333
|
meta.push(["duration", data.duration]);
|
|
@@ -304,7 +365,7 @@ export function formatLs(data: LsListResult | LsDeckResult): string {
|
|
|
304
365
|
];
|
|
305
366
|
const countLabel = formatSkipped(data.skipped) || diagnosticCount(data.diagnostics.length);
|
|
306
367
|
return [
|
|
307
|
-
`${data.ref?.name ?? data.name} ${data.title}`,
|
|
368
|
+
`${data.ref?.name ?? data.name} ${line(data.title)}`,
|
|
308
369
|
...meta.map(([key, value]) => `${padEndWidth(key, keyWidth)} ${value}`),
|
|
309
370
|
"",
|
|
310
371
|
formatTable(headers, rows, [...align]),
|
|
@@ -346,6 +407,7 @@ export function formatTheme(data: ThemeResult): string {
|
|
|
346
407
|
...data.layouts.map(
|
|
347
408
|
(layout) => ` ${layout.name}${layout.example === undefined ? " (no example)" : ""}`,
|
|
348
409
|
),
|
|
410
|
+
...(data.unprintedLayouts ? [` and ${unprinted(data.unprintedLayouts)}`] : []),
|
|
349
411
|
"",
|
|
350
412
|
"CLASSES",
|
|
351
413
|
` ${data.classes.join(" ")}`,
|
|
@@ -360,7 +422,7 @@ export function formatTheme(data: ThemeResult): string {
|
|
|
360
422
|
"TOKENS",
|
|
361
423
|
...data.tokens.map((token) => ` ${token.name.padEnd(width)} ${token.value}`),
|
|
362
424
|
"",
|
|
363
|
-
|
|
425
|
+
`Markup for a layout: dekc theme ${data.ref ? `${shellQuote(data.ref.name)} ` : ""}<layout>`,
|
|
364
426
|
].join("\n");
|
|
365
427
|
}
|
|
366
428
|
|
|
@@ -401,7 +463,11 @@ export function formatVoice(data: VoiceCliResult): string {
|
|
|
401
463
|
}
|
|
402
464
|
}
|
|
403
465
|
|
|
404
|
-
/**
|
|
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
|
+
*/
|
|
405
471
|
export function formatShow(data: ShowResult): string {
|
|
406
472
|
const parts: Array<[string, string | null]> = [
|
|
407
473
|
["script.md", data.script],
|
|
@@ -411,9 +477,20 @@ export function formatShow(data: ShowResult): string {
|
|
|
411
477
|
["theme.css (the rules this slide uses)", data.theme || null],
|
|
412
478
|
["assets", data.assets.length > 0 ? data.assets.join("\n") : null],
|
|
413
479
|
];
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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---");
|
|
417
494
|
}
|
|
418
495
|
|
|
419
496
|
/** What a read does for a ref whose snapshot is not there: fetch it, or unpack its pack. */
|
|
@@ -437,7 +514,7 @@ export function formatRef(data: RefCliResult): string {
|
|
|
437
514
|
head,
|
|
438
515
|
...data.warnings.map((warning) => `warning: ${warning}`),
|
|
439
516
|
...(data.pack ? [`commit ${data.pack}: a clone unpacks the ref from it`] : []),
|
|
440
|
-
`read it with \`dekc ls ${data.name}\` and \`dekc show ${data.name} <slug>\``,
|
|
517
|
+
`read it with \`dekc ls ${shellQuote(data.name)}\` and \`dekc show ${shellQuote(data.name)} <slug>\``,
|
|
441
518
|
].join("\n");
|
|
442
519
|
}
|
|
443
520
|
case "list":
|
|
@@ -449,7 +526,7 @@ export function formatRef(data: RefCliResult): string {
|
|
|
449
526
|
data.refs.map((ref) => [
|
|
450
527
|
ref.name,
|
|
451
528
|
short(ref.rev),
|
|
452
|
-
ref.fetched ? (ref.title ?? "") : notFetched(ref.name),
|
|
529
|
+
ref.fetched ? foreignLine(ref.title ?? "", TITLE_MAX) : notFetched(ref.name),
|
|
453
530
|
ref.slides === undefined ? "" : String(ref.slides),
|
|
454
531
|
]),
|
|
455
532
|
["left", "left", "left", "right"],
|
package/src/cli/theme.ts
CHANGED
|
@@ -2,9 +2,12 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { type CssToken, parseCss } from "../core/css.ts";
|
|
3
3
|
import { deckPaths } from "../core/deck-paths.ts";
|
|
4
4
|
import { DekError } from "../core/error.ts";
|
|
5
|
+
import { isPlainWord } from "../core/escape.ts";
|
|
6
|
+
import { foreignList, type HiddenText, hiddenText } from "../core/foreign-text.ts";
|
|
7
|
+
import { listShown, unprinted } from "../core/prose.ts";
|
|
5
8
|
import { readDeckFile } from "../core/resolve.ts";
|
|
6
9
|
import { type ThemeLayout, themeFacts } from "../core/theme-facts.ts";
|
|
7
|
-
import type
|
|
10
|
+
import { hiddenField, type ReadableDeck, type RefInfo } from "./scope.ts";
|
|
8
11
|
|
|
9
12
|
export type ThemeResult = {
|
|
10
13
|
/** The deck's theme.css: the one lint and the slides use. */
|
|
@@ -15,11 +18,16 @@ export type ThemeResult = {
|
|
|
15
18
|
stateClasses: string[];
|
|
16
19
|
/** The tokens every slide can `var()`. */
|
|
17
20
|
tokens: CssToken[];
|
|
21
|
+
/** The layouts whose names are plain words. A name is the theme's text, maybe a pack's. */
|
|
18
22
|
layouts: ThemeLayout[];
|
|
23
|
+
/** How many layouts the theme has whose names are no plain word, left out of `layouts`. */
|
|
24
|
+
unprintedLayouts?: number;
|
|
19
25
|
/** Set when one layout was asked for; text output prints its example alone. */
|
|
20
26
|
layout?: { name: string; example: string };
|
|
21
27
|
/** Set when the deck is a ref. */
|
|
22
28
|
ref?: RefInfo;
|
|
29
|
+
/** A ref's lines of theme.css that hold characters a person cannot see. */
|
|
30
|
+
hidden?: HiddenText[];
|
|
23
31
|
};
|
|
24
32
|
|
|
25
33
|
/** The deck's theme, or one layout's example markup when `name` is given. */
|
|
@@ -31,32 +39,72 @@ export function themeCommand({ deck, ref }: ReadableDeck, name?: string): ThemeR
|
|
|
31
39
|
hint: "copy the project theme.css into the deck, or run `dekc new <name>` for a fresh deck",
|
|
32
40
|
});
|
|
33
41
|
}
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
);
|
|
42
|
+
const css = readDeckFile(deck.dir, path) ?? "";
|
|
43
|
+
const { classes, stateClasses, layouts, tokens } = themeFacts(parseCss(css));
|
|
44
|
+
const printed = layouts.filter((layout) => isPlainWord(layout.name));
|
|
45
|
+
const left = layouts.length - printed.length;
|
|
37
46
|
const result: ThemeResult = {
|
|
38
47
|
path,
|
|
39
48
|
classes: [...classes].sort(),
|
|
40
49
|
stateClasses: [...stateClasses].sort(),
|
|
41
50
|
tokens,
|
|
42
|
-
layouts,
|
|
51
|
+
layouts: printed,
|
|
52
|
+
...(left > 0 ? { unprintedLayouts: left } : {}),
|
|
43
53
|
...(ref ? { ref } : {}),
|
|
54
|
+
...hiddenField(ref, hiddenText(css, path)),
|
|
44
55
|
};
|
|
45
56
|
if (!name) {
|
|
46
57
|
return result;
|
|
47
58
|
}
|
|
48
59
|
const layout = layouts.find((entry) => entry.name === name);
|
|
49
60
|
if (!layout) {
|
|
50
|
-
throw
|
|
51
|
-
path,
|
|
52
|
-
hint: `use one of: ${layouts.map((entry) => entry.name).join(", ") || "(none)"}`,
|
|
53
|
-
});
|
|
61
|
+
throw layoutNotFound(name, layouts, path, ref);
|
|
54
62
|
}
|
|
55
63
|
if (layout.example === undefined) {
|
|
56
64
|
throw new DekError(`layout "${name}" has no example in theme.css`, {
|
|
57
65
|
path,
|
|
58
|
-
hint:
|
|
66
|
+
hint: ref
|
|
67
|
+
? `a ref is read-only; read the markup of a slide that uses it with \`dekc show ${ref.name} <slug>\``
|
|
68
|
+
: `add a /* @layout ${name} ... */ comment with its markup above .slide[data-layout="${name}"]`,
|
|
59
69
|
});
|
|
60
70
|
}
|
|
61
71
|
return { ...result, layout: { name, example: layout.example } };
|
|
62
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A layout the theme does not define, named among the layouts whose names are plain words. A
|
|
76
|
+
* ref's layout names are its author's, so for a ref they go in the message, as foreignList shows
|
|
77
|
+
* them, and the hint is dek's words.
|
|
78
|
+
*/
|
|
79
|
+
function layoutNotFound(
|
|
80
|
+
name: string,
|
|
81
|
+
layouts: ThemeLayout[],
|
|
82
|
+
path: string,
|
|
83
|
+
ref: RefInfo | undefined,
|
|
84
|
+
): DekError {
|
|
85
|
+
const names = layouts.map((entry) => entry.name);
|
|
86
|
+
if (!ref) {
|
|
87
|
+
return new DekError(`layout "${name}" is not defined in theme.css`, {
|
|
88
|
+
path,
|
|
89
|
+
hint: `use one of: ${listShown(names, isPlainWord, unprinted) || "(none)"}`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const shown = names.filter(isPlainWord);
|
|
93
|
+
const left = names.length - shown.length;
|
|
94
|
+
const others = left > 0 ? unprinted(left) : "";
|
|
95
|
+
const defined =
|
|
96
|
+
shown.length === 0
|
|
97
|
+
? others
|
|
98
|
+
: others
|
|
99
|
+
? `${foreignList(shown)}, and ${others}`
|
|
100
|
+
: foreignList(shown);
|
|
101
|
+
return shown.length > 0
|
|
102
|
+
? new DekError(`layout "${name}" is not defined in theme.css, which defines ${defined}`, {
|
|
103
|
+
path,
|
|
104
|
+
hint: `pass one of them: \`dekc theme ${ref.name} <layout>\``,
|
|
105
|
+
})
|
|
106
|
+
: new DekError(
|
|
107
|
+
`layout "${name}" is not defined in theme.css, which defines ${defined || "none"}`,
|
|
108
|
+
{ path, hint: `read the markup of a slide with \`dekc show ${ref.name} <slug>\`` },
|
|
109
|
+
);
|
|
110
|
+
}
|