@hajimism/dek 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/package.json +2 -1
- package/skills/dek/SKILL.md +10 -3
- package/skills/dek-design/SKILL.md +42 -0
- package/src/cli/address.ts +84 -14
- package/src/cli/build.ts +2 -2
- package/src/cli/check.ts +2 -1
- package/src/cli/commands.ts +11 -6
- package/src/cli/contract.ts +9 -1
- package/src/cli/files.ts +1 -5
- package/src/cli/flags.ts +1 -1
- package/src/cli/goto.ts +3 -1
- package/src/cli/init.ts +10 -5
- package/src/cli/lint.ts +4 -3
- package/src/cli/main.ts +7 -5
- package/src/cli/new.ts +14 -7
- package/src/cli/pptx.ts +9 -4
- package/src/cli/ref.ts +91 -9
- package/src/cli/result.ts +5 -2
- package/src/cli/scope.ts +17 -28
- package/src/cli/shot.ts +2 -1
- package/src/cli/text.ts +27 -2
- package/src/cli/theme.ts +16 -2
- package/src/cli/voice.ts +4 -3
- 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/config.ts +61 -12
- package/src/core/diagnostic.ts +5 -1
- package/src/core/escape.ts +43 -0
- package/src/core/line-break.ts +275 -0
- package/src/core/lint/asset-refs.ts +6 -2
- package/src/core/lint/files.ts +30 -5
- package/src/core/lint/keys.ts +7 -3
- package/src/core/lint/slide-html.ts +3 -1
- package/src/core/lint/slide-script.ts +7 -5
- package/src/core/mv.ts +30 -10
- package/src/core/pack.ts +2 -2
- package/src/core/playwright-module.d.ts +1 -0
- package/src/core/playwright-visual.ts +180 -20
- package/src/core/playwright.ts +202 -25
- 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 +55 -8
- package/src/core/prose.ts +23 -0
- package/src/core/pseudo-text.ts +32 -10
- package/src/core/ref-name.ts +47 -8
- package/src/core/ref.ts +172 -16
- package/src/core/resolve.ts +20 -9
- 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 +25 -17
- package/src/core/slide-measure.ts +51 -1
- package/src/core/slide-script-eval.ts +10 -8
- package/src/core/slide-script.ts +114 -20
- package/src/core/sync.ts +2 -2
- package/src/core/visual-cache.ts +2 -0
- package/src/core/visual.ts +154 -15
- package/src/core/voice.ts +3 -5
- package/src/core/zod.ts +9 -1
- package/src/server/dev.ts +15 -4
- package/src/server/lock.ts +48 -5
- package/src/server/routes.ts +7 -2
- package/src/theme/default.css +4 -0
- package/src/video/bake.ts +6 -2
package/src/cli/pptx.ts
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
|
-
import { pptxDeck } from "../core/pptx.ts";
|
|
1
|
+
import { type PptxLifted, pptxDeck } from "../core/pptx.ts";
|
|
2
2
|
import type { DecksTarget } from "./scope.ts";
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* One file per deck, a single deck included, as `dekc pdf` answers, and what each file laid
|
|
6
|
+
* over its pictures, for the text output alone (`--json` is `outs`).
|
|
7
|
+
*/
|
|
8
|
+
export type PptxCliResult = { outs: string[]; lifted?: PptxLifted[] };
|
|
6
9
|
|
|
7
10
|
export async function pptxCommand(
|
|
8
11
|
{ project, decks }: DecksTarget,
|
|
9
12
|
options: { rootDist?: boolean; public?: boolean } = {},
|
|
10
13
|
): Promise<PptxCliResult> {
|
|
11
14
|
const outs: string[] = [];
|
|
15
|
+
const lifted: PptxLifted[] = [];
|
|
12
16
|
for (const entry of decks) {
|
|
13
17
|
const done = await pptxDeck(
|
|
14
18
|
{ project, deck: entry },
|
|
15
19
|
{ rootDist: options.rootDist, public: options.public },
|
|
16
20
|
);
|
|
17
21
|
outs.push(done.outPath);
|
|
22
|
+
lifted.push(done.lifted);
|
|
18
23
|
}
|
|
19
|
-
return { outs };
|
|
24
|
+
return { outs, lifted };
|
|
20
25
|
}
|
package/src/cli/ref.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync, lstatSync, readdirSync, readFileSync, rmdirSync } from "node:fs";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, rmdirSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { loadConfig } from "../core/config.ts";
|
|
4
4
|
import { DekError } from "../core/error.ts";
|
|
5
|
+
import { isPlainWord, shellQuote } from "../core/escape.ts";
|
|
5
6
|
import { downloadTarball, resolveRev } from "../core/github.ts";
|
|
6
7
|
import {
|
|
7
8
|
deckOfPack,
|
|
@@ -12,15 +13,17 @@ import {
|
|
|
12
13
|
pickSnapshot,
|
|
13
14
|
readPack,
|
|
14
15
|
readTarball,
|
|
16
|
+
refDir,
|
|
15
17
|
refLicense,
|
|
16
18
|
refState,
|
|
17
19
|
refTitle,
|
|
20
|
+
refuseLinkUnderPacks,
|
|
21
|
+
refuseLinkUnderRefs,
|
|
18
22
|
type Snapshot,
|
|
19
23
|
snapshotDigest,
|
|
20
24
|
writeSnapshot,
|
|
21
25
|
} from "../core/ref.ts";
|
|
22
|
-
import type { FileRefSource, GithubRefSource } from "../core/ref-name.ts";
|
|
23
|
-
import { readTextIfExists } from "../core/resolve.ts";
|
|
26
|
+
import type { FileRefSource, GithubRefSource, RefSource } from "../core/ref-name.ts";
|
|
24
27
|
import { readSourceIfExists, removeInside, requireInside, writeInside } from "../core/safe-fs.ts";
|
|
25
28
|
import { writeAgentsMd } from "../core/sync.ts";
|
|
26
29
|
import { setTableKey } from "../core/toml-keys.ts";
|
|
@@ -58,10 +61,12 @@ export type RefCliResult = RefAddResult | RefListResult | RefRmResult;
|
|
|
58
61
|
export async function restoreRef(cwd: string, arg: string): Promise<void> {
|
|
59
62
|
const source = parseRefSource(arg);
|
|
60
63
|
const project = requireProject(cwd);
|
|
61
|
-
const { pinned, fetched } = refState(project, source.name);
|
|
64
|
+
const { pinned, dir, fetched } = refState(project, source.name);
|
|
62
65
|
if (pinned === undefined || fetched) {
|
|
63
66
|
return;
|
|
64
67
|
}
|
|
68
|
+
// Said before a download that the write would refuse anyway.
|
|
69
|
+
refuseLinkUnderRefs(project.root, dir);
|
|
65
70
|
if (source.kind === "file") {
|
|
66
71
|
await unpackSnapshot(project.root, source, pinned);
|
|
67
72
|
return;
|
|
@@ -88,7 +93,7 @@ async function unpackSnapshot(root: string, source: FileRefSource, pin: string):
|
|
|
88
93
|
if ((await snapshotDigest(snapshot)) !== pin) {
|
|
89
94
|
throw new DekError(`${rel} is not the pack ${source.name} is pinned to`, {
|
|
90
95
|
path,
|
|
91
|
-
hint: `run \`dekc ref ${rel}\` to pin the pack that is there, or put back the one dek.toml pins`,
|
|
96
|
+
hint: `run \`dekc ref ${shellQuote(rel)}\` to pin the pack that is there, or put back the one dek.toml pins`,
|
|
92
97
|
});
|
|
93
98
|
}
|
|
94
99
|
await writeSnapshot(root, source.name, pin, snapshot);
|
|
@@ -128,7 +133,10 @@ export async function addRef(cwd: string, arg: string): Promise<RefAddResult> {
|
|
|
128
133
|
});
|
|
129
134
|
}
|
|
130
135
|
const project = requireProject(cwd);
|
|
136
|
+
refuseSharedFolders(project, source);
|
|
131
137
|
const { pinned: from, dir, fetched } = refState(project, source.name);
|
|
138
|
+
// Said before a download that the write would refuse anyway.
|
|
139
|
+
refuseLinkUnderRefs(project.root, dir);
|
|
132
140
|
const rev =
|
|
133
141
|
source.rev !== undefined && isPinnedRev(source.rev)
|
|
134
142
|
? source.rev
|
|
@@ -141,7 +149,7 @@ export async function addRef(cwd: string, arg: string): Promise<RefAddResult> {
|
|
|
141
149
|
changed = true;
|
|
142
150
|
}
|
|
143
151
|
if (from !== rev) {
|
|
144
|
-
const toml =
|
|
152
|
+
const toml = readSourceIfExists(project.configPath, project.root) ?? "";
|
|
145
153
|
writeInside(project.configPath, setTableKey(toml, "refs", source.name, rev), project.root);
|
|
146
154
|
changed = true;
|
|
147
155
|
}
|
|
@@ -166,6 +174,72 @@ export async function addRef(cwd: string, arg: string): Promise<RefAddResult> {
|
|
|
166
174
|
};
|
|
167
175
|
}
|
|
168
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Refuses a pin whose folders another pin, or a folder already on disk, takes. refs/ and packs/
|
|
179
|
+
* are named by the ref, and macOS and Windows read a name without its case, so `file/Talk` and
|
|
180
|
+
* `file/talk` would share one snapshot and one pack, and adding the second would replace the
|
|
181
|
+
* first's.
|
|
182
|
+
*/
|
|
183
|
+
function refuseSharedFolders(
|
|
184
|
+
project: { root: string; configPath: string },
|
|
185
|
+
source: RefSource,
|
|
186
|
+
): void {
|
|
187
|
+
const { name } = source;
|
|
188
|
+
const other = Object.keys(loadConfig(project.configPath).refs ?? {}).find(
|
|
189
|
+
(pinned) => pinned !== name && folded(pinned) === folded(name),
|
|
190
|
+
);
|
|
191
|
+
if (other !== undefined) {
|
|
192
|
+
throw new DekError(`${name} would share its folders with ${other}, which dek.toml pins`, {
|
|
193
|
+
path: project.configPath,
|
|
194
|
+
hint: `a ref's folders are named without their case on macOS and Windows; to pin ${name} in its place, run \`dekc ref rm ${shellQuote(other)}\` first`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const paths = [relative(project.root, refDir(project.root, name))];
|
|
198
|
+
if (source.kind === "file") {
|
|
199
|
+
paths.push(packFile(source.deck));
|
|
200
|
+
}
|
|
201
|
+
const left = paths
|
|
202
|
+
.map((path) => spelledOtherwise(project.root, path))
|
|
203
|
+
.find((path) => path !== undefined);
|
|
204
|
+
if (left !== undefined) {
|
|
205
|
+
const shown = isPlainWord(left) ? left : "another folder";
|
|
206
|
+
throw new DekError(
|
|
207
|
+
`${name} would share a folder with ${shown}, which is already in the project`,
|
|
208
|
+
{
|
|
209
|
+
path: join(project.root, left),
|
|
210
|
+
hint: "no ref in dek.toml pins it, so a ref removed by hand left it; delete it and add the ref again",
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** A name as a file system blind to case and form reads it: APFS folds ς as σ, and fi as fi. */
|
|
217
|
+
function folded(name: string): string {
|
|
218
|
+
return name.normalize("NFKC").toUpperCase().toLowerCase();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The path, under `root`, of a folder or file a file system blind to case and form would read as
|
|
223
|
+
* `path` though it is spelled otherwise, found segment by segment.
|
|
224
|
+
*/
|
|
225
|
+
function spelledOtherwise(root: string, path: string): string | undefined {
|
|
226
|
+
let dir = root;
|
|
227
|
+
const walked: string[] = [];
|
|
228
|
+
for (const segment of path.split(sep)) {
|
|
229
|
+
const entries = existsSync(dir) && statSync(dir).isDirectory() ? readdirSync(dir) : [];
|
|
230
|
+
const other = entries.find((entry) => entry !== segment && folded(entry) === folded(segment));
|
|
231
|
+
if (other !== undefined) {
|
|
232
|
+
return join(...walked, other);
|
|
233
|
+
}
|
|
234
|
+
if (!entries.includes(segment)) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
walked.push(segment);
|
|
238
|
+
dir = join(dir, segment);
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
169
243
|
/** An argument that ends in .tgz or .tar.gz is a pack's path, never a ref name. */
|
|
170
244
|
function isPackPath(arg: string): boolean {
|
|
171
245
|
return /\.(tgz|tar\.gz)$/i.test(arg);
|
|
@@ -187,9 +261,12 @@ async function addPack(cwd: string, arg: string): Promise<RefAddResult> {
|
|
|
187
261
|
}
|
|
188
262
|
const read = await readPackFile(given, undefined);
|
|
189
263
|
const { source, bytes } = read;
|
|
264
|
+
refuseSharedFolders(project, source);
|
|
190
265
|
const rev = await snapshotDigest(read);
|
|
191
266
|
const { pinned: from, dir, fetched } = refState(project, source.name);
|
|
192
267
|
const stored = join(project.root, packFile(source.deck));
|
|
268
|
+
// Before the first write: the copy below is written by name.
|
|
269
|
+
refuseLinkUnderPacks(project.root, source.deck);
|
|
193
270
|
|
|
194
271
|
let changed = false;
|
|
195
272
|
if (!(fetched && from === rev)) {
|
|
@@ -201,7 +278,7 @@ async function addPack(cwd: string, arg: string): Promise<RefAddResult> {
|
|
|
201
278
|
changed = true;
|
|
202
279
|
}
|
|
203
280
|
if (from !== rev) {
|
|
204
|
-
const toml =
|
|
281
|
+
const toml = readSourceIfExists(project.configPath, project.root) ?? "";
|
|
205
282
|
writeInside(project.configPath, setTableKey(toml, "refs", source.name, rev), project.root);
|
|
206
283
|
changed = true;
|
|
207
284
|
}
|
|
@@ -264,8 +341,13 @@ export function removeRef(cwd: string, arg: string): RefRmResult {
|
|
|
264
341
|
hint: "run `dekc ref` to list refs",
|
|
265
342
|
});
|
|
266
343
|
}
|
|
344
|
+
// Before anything changes: the deletes below must not reach what a link there points to.
|
|
345
|
+
refuseLinkUnderRefs(project.root, dir);
|
|
346
|
+
if (source.kind === "file") {
|
|
347
|
+
refuseLinkUnderPacks(project.root, source.deck);
|
|
348
|
+
}
|
|
267
349
|
if (pinned !== undefined) {
|
|
268
|
-
const toml =
|
|
350
|
+
const toml = readSourceIfExists(project.configPath, project.root) ?? "";
|
|
269
351
|
writeInside(
|
|
270
352
|
project.configPath,
|
|
271
353
|
setTableKey(toml, "refs", source.name, undefined),
|
package/src/cli/result.ts
CHANGED
|
@@ -13,15 +13,18 @@ export function skippedChecks(skipped: SkippedCheck[]): { skipped?: SkippedCheck
|
|
|
13
13
|
return skipped.length > 0 ? { skipped } : {};
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** What `--visual` measures in a browser, as a hint or a reason names it. */
|
|
17
|
+
export const RENDERED_RULES = "overflow, contrast, line breaks, and alignment";
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
|
-
* The
|
|
20
|
+
* The rules a browser measures, when a run leaves them out: why, and `rerun`, the command that
|
|
18
21
|
* measures them. Without Playwright, installing it comes first, whatever the reason was.
|
|
19
22
|
*/
|
|
20
23
|
export function visualSkipped(
|
|
21
24
|
rerun: string,
|
|
22
25
|
options: { reason: string; runner?: PlaywrightRunner },
|
|
23
26
|
): SkippedCheck {
|
|
24
|
-
const measure = `run \`${rerun}\` to measure
|
|
27
|
+
const measure = `run \`${rerun}\` to measure ${RENDERED_RULES}`;
|
|
25
28
|
return playwrightReady(options.runner)
|
|
26
29
|
? { check: "visual", reason: options.reason, hint: measure }
|
|
27
30
|
: {
|
package/src/cli/scope.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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
4
|
import {
|
|
5
5
|
isRefName,
|
|
6
6
|
packFile,
|
|
7
7
|
parseRefSource,
|
|
8
|
-
REF_MARKER,
|
|
9
|
-
readRefMeta,
|
|
10
8
|
refLicense,
|
|
9
|
+
refReadOnlyError,
|
|
11
10
|
refState,
|
|
11
|
+
requireProject,
|
|
12
12
|
} from "../core/ref.ts";
|
|
13
13
|
import {
|
|
14
14
|
locateDeck,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "../core/resolve.ts";
|
|
20
20
|
import { suggest } from "../core/suggest.ts";
|
|
21
21
|
|
|
22
|
-
export { requireSection };
|
|
22
|
+
export { requireProject, requireSection };
|
|
23
23
|
|
|
24
24
|
export type Scope = {
|
|
25
25
|
project: Project;
|
|
@@ -52,18 +52,6 @@ export type DecksTarget = {
|
|
|
52
52
|
ref?: RefInfo;
|
|
53
53
|
};
|
|
54
54
|
|
|
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
55
|
export function inferDeckName(project: Project, cwd: string): string | undefined {
|
|
68
56
|
const located = locateDeck(project.root, cwd);
|
|
69
57
|
if (!located) {
|
|
@@ -139,9 +127,7 @@ export function resolveScope(cwd: string, options: { deck?: string } = {}): Scop
|
|
|
139
127
|
return { project };
|
|
140
128
|
}
|
|
141
129
|
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
|
-
});
|
|
130
|
+
throw refReadOnlyError(name);
|
|
145
131
|
}
|
|
146
132
|
const deck = project.decks.find((entry) => entry.name === name);
|
|
147
133
|
if (!deck) {
|
|
@@ -149,10 +135,11 @@ export function resolveScope(cwd: string, options: { deck?: string } = {}): Scop
|
|
|
149
135
|
if (failed) {
|
|
150
136
|
throw failed.error;
|
|
151
137
|
}
|
|
152
|
-
const
|
|
138
|
+
const near = suggest(name, [
|
|
153
139
|
...project.decks.map((entry) => entry.name),
|
|
154
140
|
...project.failed.map((entry) => entry.name),
|
|
155
141
|
]);
|
|
142
|
+
const guess = near === undefined ? undefined : shellName(near);
|
|
156
143
|
throw new DekError(`deck "${name}" not found`, {
|
|
157
144
|
path: join(project.root, "decks", name),
|
|
158
145
|
hint: guess ? `did you mean \`${guess}\`? run \`dekc ls\` for every deck` : "run `dekc ls`",
|
|
@@ -277,18 +264,16 @@ function resolveRef(cwd: string, arg: string): ReadableDeck & { ref: RefInfo } {
|
|
|
277
264
|
hint:
|
|
278
265
|
source.kind === "file"
|
|
279
266
|
? "add it from the pack you were sent: `dekc ref <file>.tgz`"
|
|
280
|
-
: `run \`dekc ref ${arg}\``,
|
|
267
|
+
: `run \`dekc ref ${shellQuote(arg)}\``,
|
|
281
268
|
});
|
|
282
269
|
}
|
|
283
270
|
// 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}`;
|
|
271
|
+
const added = source.kind === "file" ? packFile(source.deck) : `${source.name}@${pinned}`;
|
|
272
|
+
const again = `dekc ref ${shellQuote(added)}`;
|
|
288
273
|
if (source.kind === "github" && source.rev !== undefined && source.rev !== pinned) {
|
|
289
274
|
throw new DekError(`"${arg}" is not the pinned version, ${pinned.slice(0, 7)}`, {
|
|
290
275
|
path: project.configPath,
|
|
291
|
-
hint: `run \`dekc ref ${arg}\` to pin that version, or drop @${source.rev}`,
|
|
276
|
+
hint: `run \`dekc ref ${shellQuote(arg)}\` to pin that version, or drop @${source.rev}`,
|
|
292
277
|
});
|
|
293
278
|
}
|
|
294
279
|
if (!fetched) {
|
|
@@ -311,7 +296,11 @@ function resolveRef(cwd: string, arg: string): ReadableDeck & { ref: RefInfo } {
|
|
|
311
296
|
}
|
|
312
297
|
return {
|
|
313
298
|
project: snapshot,
|
|
314
|
-
|
|
299
|
+
// Read where it is, but cached in this project: nothing is written in a snapshot.
|
|
300
|
+
deck: {
|
|
301
|
+
...found,
|
|
302
|
+
cache: { dir: join(project.root, ".cache", relative(project.root, dir)), root: project.root },
|
|
303
|
+
},
|
|
315
304
|
ref: { name: source.name, rev: pinned, dir, license: refLicense(dir) },
|
|
316
305
|
};
|
|
317
306
|
}
|
package/src/cli/shot.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DekError } from "../core/error.ts";
|
|
2
|
+
import { shellQuote } from "../core/escape.ts";
|
|
2
3
|
import { parseMorphAt, shotMorph } from "../core/shot/morph.ts";
|
|
3
4
|
import { type ShotMotion, shotMotion } from "../core/shot/motion.ts";
|
|
4
5
|
import { type ShotFile, shotDeck, shotSheet } from "../core/shot/still.ts";
|
|
@@ -91,7 +92,7 @@ export function parseShotMode(options: ShotCommandOptions): ShotMode {
|
|
|
91
92
|
if (slug) {
|
|
92
93
|
refuse(
|
|
93
94
|
"--sheet with a slug",
|
|
94
|
-
`--sheet tiles every slide; for one slide's beats, run \`dekc shot ${slug} --motion\``,
|
|
95
|
+
`--sheet tiles every slide; for one slide's beats, run \`dekc shot ${shellQuote(slug)} --motion\``,
|
|
95
96
|
);
|
|
96
97
|
}
|
|
97
98
|
return { kind: "sheet" };
|
package/src/cli/text.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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 { unprinted } from "../core/prose.ts";
|
|
4
6
|
import { packFile, parseRefSource } from "../core/ref-name.ts";
|
|
5
7
|
import { formatClock } from "../core/timing.ts";
|
|
6
8
|
import type { AnnotationsCliResult } from "./annotations.ts";
|
|
@@ -16,6 +18,7 @@ import type { MvResult } from "./mv.ts";
|
|
|
16
18
|
import type { NewResult } from "./new.ts";
|
|
17
19
|
import type { PackCliResult } from "./pack.ts";
|
|
18
20
|
import type { PdfCliResult } from "./pdf.ts";
|
|
21
|
+
import type { PptxCliResult } from "./pptx.ts";
|
|
19
22
|
import type { RefCliResult } from "./ref.ts";
|
|
20
23
|
import type { ShotCliResult } from "./shot.ts";
|
|
21
24
|
import type { ShowResult } from "./show.ts";
|
|
@@ -230,6 +233,27 @@ export function formatPdf(data: PdfCliResult): string {
|
|
|
230
233
|
return data.outs.map((path) => `wrote ${path}`).join("\n");
|
|
231
234
|
}
|
|
232
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Each file written, and what it laid over its pictures, naming the slides that kept everything
|
|
238
|
+
* in the picture, which are the ones to rewrite toward shapes.
|
|
239
|
+
*/
|
|
240
|
+
export function formatPptx(data: PptxCliResult): string {
|
|
241
|
+
return data.outs
|
|
242
|
+
.flatMap((path, index) => {
|
|
243
|
+
const lifted = data.lifted?.[index];
|
|
244
|
+
if (!lifted) {
|
|
245
|
+
return [`wrote ${path}`];
|
|
246
|
+
}
|
|
247
|
+
const none = lifted.untouched.length > 0 ? `; nothing on ${lifted.untouched.join(", ")}` : "";
|
|
248
|
+
const count = (n: number, one: string) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
249
|
+
return [
|
|
250
|
+
`wrote ${path}`,
|
|
251
|
+
` laid ${count(lifted.shapes, "shape")} and ${count(lifted.pictures, "picture")} over the picture${none}`,
|
|
252
|
+
];
|
|
253
|
+
})
|
|
254
|
+
.join("\n");
|
|
255
|
+
}
|
|
256
|
+
|
|
233
257
|
export function formatPack(data: PackCliResult): string {
|
|
234
258
|
return [formatPdf(data), ...data.warnings.map((warning) => `warning: ${warning}`)].join("\n");
|
|
235
259
|
}
|
|
@@ -346,6 +370,7 @@ export function formatTheme(data: ThemeResult): string {
|
|
|
346
370
|
...data.layouts.map(
|
|
347
371
|
(layout) => ` ${layout.name}${layout.example === undefined ? " (no example)" : ""}`,
|
|
348
372
|
),
|
|
373
|
+
...(data.unprintedLayouts ? [` and ${unprinted(data.unprintedLayouts)}`] : []),
|
|
349
374
|
"",
|
|
350
375
|
"CLASSES",
|
|
351
376
|
` ${data.classes.join(" ")}`,
|
|
@@ -360,7 +385,7 @@ export function formatTheme(data: ThemeResult): string {
|
|
|
360
385
|
"TOKENS",
|
|
361
386
|
...data.tokens.map((token) => ` ${token.name.padEnd(width)} ${token.value}`),
|
|
362
387
|
"",
|
|
363
|
-
|
|
388
|
+
`Markup for a layout: dekc theme ${data.ref ? `${shellQuote(data.ref.name)} ` : ""}<layout>`,
|
|
364
389
|
].join("\n");
|
|
365
390
|
}
|
|
366
391
|
|
|
@@ -437,7 +462,7 @@ export function formatRef(data: RefCliResult): string {
|
|
|
437
462
|
head,
|
|
438
463
|
...data.warnings.map((warning) => `warning: ${warning}`),
|
|
439
464
|
...(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>\``,
|
|
465
|
+
`read it with \`dekc ls ${shellQuote(data.name)}\` and \`dekc show ${shellQuote(data.name)} <slug>\``,
|
|
441
466
|
].join("\n");
|
|
442
467
|
}
|
|
443
468
|
case "list":
|
package/src/cli/theme.ts
CHANGED
|
@@ -2,6 +2,8 @@ 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 { listShown, unprinted } from "../core/prose.ts";
|
|
5
7
|
import { readDeckFile } from "../core/resolve.ts";
|
|
6
8
|
import { type ThemeLayout, themeFacts } from "../core/theme-facts.ts";
|
|
7
9
|
import type { ReadableDeck, RefInfo } from "./scope.ts";
|
|
@@ -15,7 +17,10 @@ export type ThemeResult = {
|
|
|
15
17
|
stateClasses: string[];
|
|
16
18
|
/** The tokens every slide can `var()`. */
|
|
17
19
|
tokens: CssToken[];
|
|
20
|
+
/** The layouts whose names are plain words. A name is the theme's text, maybe a pack's. */
|
|
18
21
|
layouts: ThemeLayout[];
|
|
22
|
+
/** How many layouts the theme has whose names are no plain word, left out of `layouts`. */
|
|
23
|
+
unprintedLayouts?: number;
|
|
19
24
|
/** Set when one layout was asked for; text output prints its example alone. */
|
|
20
25
|
layout?: { name: string; example: string };
|
|
21
26
|
/** Set when the deck is a ref. */
|
|
@@ -34,12 +39,15 @@ export function themeCommand({ deck, ref }: ReadableDeck, name?: string): ThemeR
|
|
|
34
39
|
const { classes, stateClasses, layouts, tokens } = themeFacts(
|
|
35
40
|
parseCss(readDeckFile(deck.dir, path) ?? ""),
|
|
36
41
|
);
|
|
42
|
+
const printed = layouts.filter((layout) => isPlainWord(layout.name));
|
|
43
|
+
const left = layouts.length - printed.length;
|
|
37
44
|
const result: ThemeResult = {
|
|
38
45
|
path,
|
|
39
46
|
classes: [...classes].sort(),
|
|
40
47
|
stateClasses: [...stateClasses].sort(),
|
|
41
48
|
tokens,
|
|
42
|
-
layouts,
|
|
49
|
+
layouts: printed,
|
|
50
|
+
...(left > 0 ? { unprintedLayouts: left } : {}),
|
|
43
51
|
...(ref ? { ref } : {}),
|
|
44
52
|
};
|
|
45
53
|
if (!name) {
|
|
@@ -49,7 +57,13 @@ export function themeCommand({ deck, ref }: ReadableDeck, name?: string): ThemeR
|
|
|
49
57
|
if (!layout) {
|
|
50
58
|
throw new DekError(`layout "${name}" is not defined in theme.css`, {
|
|
51
59
|
path,
|
|
52
|
-
hint: `use one of: ${
|
|
60
|
+
hint: `use one of: ${
|
|
61
|
+
listShown(
|
|
62
|
+
layouts.map((entry) => entry.name),
|
|
63
|
+
isPlainWord,
|
|
64
|
+
unprinted,
|
|
65
|
+
) || "(none)"
|
|
66
|
+
}`,
|
|
53
67
|
});
|
|
54
68
|
}
|
|
55
69
|
if (layout.example === undefined) {
|
package/src/cli/voice.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { DekError } from "../core/error.ts";
|
|
5
5
|
import { deckProjectRoot } from "../core/path.ts";
|
|
6
|
-
import { isCachedFile, writeInside } from "../core/safe-fs.ts";
|
|
6
|
+
import { isCachedFile, requireInside, writeInside } from "../core/safe-fs.ts";
|
|
7
7
|
import { loadVoiceDict, loadVoiceSettings, voiceCacheFile, writeVoiceDict } from "../core/voice.ts";
|
|
8
8
|
import {
|
|
9
9
|
engineBaseUrl,
|
|
@@ -91,8 +91,9 @@ export function pinVoice({ deck }: DeckTarget): VoiceCliResult {
|
|
|
91
91
|
const pinDir = join(deck.dir, "voice", "pin");
|
|
92
92
|
const pinTimeline = join(pinDir, "timeline.json");
|
|
93
93
|
const pinAudio = join(pinDir, "master.wav");
|
|
94
|
-
|
|
95
|
-
writeInside(
|
|
94
|
+
// The folders on the way may be links too; what is pinned comes from inside the project.
|
|
95
|
+
writeInside(pinTimeline, readFileSync(requireInside(timelinePath, root)), root);
|
|
96
|
+
writeInside(pinAudio, readFileSync(requireInside(audioPath, root)), root);
|
|
96
97
|
return { action: "pin", timelinePath: pinTimeline, audioPath: pinAudio };
|
|
97
98
|
}
|
|
98
99
|
|