@hajimism/dek 0.3.2 → 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 +90 -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 +54 -13
- package/src/cli/contract.ts +18 -1
- package/src/cli/files.ts +9 -6
- 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 +41 -10
- package/src/cli/pack.ts +21 -0
- package/src/cli/pptx.ts +9 -4
- package/src/cli/ref.ts +243 -9
- package/src/cli/result.ts +5 -2
- package/src/cli/scope.ts +25 -27
- package/src/cli/shot.ts +2 -1
- package/src/cli/text.ts +45 -4
- 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-keys.ts +6 -2
- package/src/core/config.ts +88 -12
- package/src/core/diagnostic.ts +5 -1
- package/src/core/escape.ts +43 -0
- package/src/core/licenses/CC-BY-4.0.txt +396 -0
- package/src/core/licenses/CC0-1.0.txt +121 -0
- package/src/core/licenses.ts +20 -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 +137 -0
- 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 +100 -17
- package/src/core/ref.ts +318 -34
- 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 +3 -3
- 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/runtime/annotate-view.ts +4 -2
- 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/core/zod.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
2
|
import { DekError } from "./error.ts";
|
|
3
|
+
import { isPlainWord } from "./escape.ts";
|
|
3
4
|
|
|
4
5
|
const REFERENCE = "https://hajimism.github.io/dek/reference/config.html";
|
|
5
6
|
|
|
@@ -21,10 +22,17 @@ export function parseFailure(error: unknown): string {
|
|
|
21
22
|
);
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Each issue as `path: message`. A key in the path is the file's text, which a cloned project or
|
|
27
|
+
* a pack brings along, so one that is no plain word shows as `<key>`: repeated, it could close a
|
|
28
|
+
* code span or start a line of its own.
|
|
29
|
+
*/
|
|
24
30
|
export function formatZodIssues(error: z.ZodError): string {
|
|
25
31
|
return error.issues
|
|
26
32
|
.map((issue) => {
|
|
27
|
-
const path = issue.path
|
|
33
|
+
const path = issue.path
|
|
34
|
+
.map((key) => (isPlainWord(String(key)) ? String(key) : "<key>"))
|
|
35
|
+
.join(".");
|
|
28
36
|
// "expected string, received undefined" says less than "required".
|
|
29
37
|
const message =
|
|
30
38
|
issue.code === "invalid_type" && /received undefined$/.test(issue.message)
|
|
@@ -671,10 +671,12 @@ export function createAnnotateView(options: {
|
|
|
671
671
|
return;
|
|
672
672
|
}
|
|
673
673
|
event.stopPropagation();
|
|
674
|
-
|
|
674
|
+
// Safari sends the key that commits or cancels a conversion after compositionend, with
|
|
675
|
+
// isComposing false; keyCode 229 still marks it as the input method's.
|
|
676
|
+
if (event.target !== field || event.isComposing || event.keyCode === 229) {
|
|
675
677
|
return;
|
|
676
678
|
}
|
|
677
|
-
if (event.key === "Enter" && !event.shiftKey
|
|
679
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
678
680
|
event.preventDefault();
|
|
679
681
|
submit();
|
|
680
682
|
} else if (event.key === "Escape") {
|
package/src/server/dev.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { DekError } from "../core/error.ts";
|
|
3
3
|
import type { PlaywrightRunner } from "../core/playwright.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { isRefName, refReadOnlyError, requireProject } from "../core/ref.ts";
|
|
5
|
+
import { locateDeck, type Project } from "../core/resolve.ts";
|
|
5
6
|
import { annotateScript, livePlayerScript, liveReloadScript } from "../runtime/player.ts";
|
|
6
7
|
import {
|
|
7
8
|
clearance,
|
|
@@ -13,7 +14,12 @@ import {
|
|
|
13
14
|
} from "./auth.ts";
|
|
14
15
|
import type { EventFeed } from "./hub.ts";
|
|
15
16
|
import { generateRemotePassword, lanUrls } from "./lan.ts";
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
assertNoDevServer,
|
|
19
|
+
devServerUrl,
|
|
20
|
+
removeDevServerLock,
|
|
21
|
+
writeDevServerLock,
|
|
22
|
+
} from "./lock.ts";
|
|
17
23
|
import { createDevPages } from "./pages.ts";
|
|
18
24
|
import { createPairings, type Pairings } from "./pairing.ts";
|
|
19
25
|
import { createProjectWatch } from "./project-watch.ts";
|
|
@@ -63,7 +69,12 @@ type Access = {
|
|
|
63
69
|
type ServedRoutes = Omit<RouteContext, "upgrade">;
|
|
64
70
|
|
|
65
71
|
export async function startDevServer(options: DevServerOptions): Promise<DevServer> {
|
|
66
|
-
|
|
72
|
+
// A ref is read where it is, never served: the server syncs into what it serves, and runs its
|
|
73
|
+
// slides' scripts in the browser at its own origin.
|
|
74
|
+
const project = requireProject(options.cwd);
|
|
75
|
+
if (options.deck !== undefined && isRefName(options.deck)) {
|
|
76
|
+
throw refReadOnlyError(options.deck);
|
|
77
|
+
}
|
|
67
78
|
// Before the port and the watchers; writing the lock checks again, for a server that raced us.
|
|
68
79
|
assertNoDevServer(project.root);
|
|
69
80
|
const { scopedDeckName, deckDir } = serverScope(project, options);
|
|
@@ -113,7 +124,7 @@ export async function startDevServer(options: DevServerOptions): Promise<DevServ
|
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
const port = server.port ?? 0;
|
|
116
|
-
const url =
|
|
127
|
+
const url = devServerUrl(port);
|
|
117
128
|
const remoteUrls = [...new Set([url, ...(access.remote ? lanUrls(port) : [])])];
|
|
118
129
|
try {
|
|
119
130
|
writeDevServerLock(project.root, url, access.password, scopedDeckName);
|
package/src/server/lock.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { DekError } from "../core/error.ts";
|
|
4
|
-
import { writeInside } from "../core/safe-fs.ts";
|
|
4
|
+
import { readSourceIfExists, writeInside } from "../core/safe-fs.ts";
|
|
5
5
|
|
|
6
6
|
export type DevServerLock = {
|
|
7
7
|
url: string;
|
|
@@ -15,7 +15,14 @@ function serverLockPath(root: string): string {
|
|
|
15
15
|
return join(root, ".dek", "server.json");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Whether a process with this pid runs. Only a positive whole number names one process: 0 and a
|
|
20
|
+
* negative pid signal a whole group, which `kill` reports as alive.
|
|
21
|
+
*/
|
|
18
22
|
export function isPidAlive(pid: number): boolean {
|
|
23
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
19
26
|
try {
|
|
20
27
|
process.kill(pid, 0);
|
|
21
28
|
return true;
|
|
@@ -24,16 +31,52 @@ export function isPidAlive(pid: number): boolean {
|
|
|
24
31
|
}
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
/** The hosts a dev server's URL in its lock names: it writes this machine's loopback address. */
|
|
35
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
36
|
+
|
|
37
|
+
/** The URL a dev server on `port` writes in its lock, as `URL` spells it: port 80 takes none. */
|
|
38
|
+
export function devServerUrl(port: number): string {
|
|
39
|
+
return new URL(`http://127.0.0.1:${port}/`).href;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The lock's URL in the form the dev server writes it, `http://<loopback>:<port>/`, or nothing
|
|
44
|
+
* for any other. A server.json a repository brings along is its text, and the URL goes into
|
|
45
|
+
* hints and into the requests `dekc goto` sends. Port 80 is read with or without `:80`.
|
|
46
|
+
*/
|
|
47
|
+
function serverUrl(value: unknown): string | undefined {
|
|
48
|
+
if (typeof value !== "string") {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
let url: URL;
|
|
52
|
+
try {
|
|
53
|
+
url = new URL(value);
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const served =
|
|
58
|
+
url.protocol === "http:" &&
|
|
59
|
+
LOOPBACK_HOSTS.has(url.hostname) &&
|
|
60
|
+
url.pathname === "/" &&
|
|
61
|
+
url.search === "" &&
|
|
62
|
+
url.hash === "" &&
|
|
63
|
+
url.username === "" &&
|
|
64
|
+
url.password === "";
|
|
65
|
+
const written = url.port === "" ? [url.href, `http://${url.hostname}:80/`] : [url.href];
|
|
66
|
+
return served && written.includes(value) ? url.href : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
27
69
|
export function readDevServerLock(root: string): DevServerLock | undefined {
|
|
28
70
|
const path = serverLockPath(root);
|
|
29
71
|
if (!existsSync(path)) {
|
|
30
72
|
return undefined;
|
|
31
73
|
}
|
|
32
74
|
try {
|
|
33
|
-
const parsed = JSON.parse(
|
|
34
|
-
|
|
75
|
+
const parsed = JSON.parse(readSourceIfExists(path, root) ?? "") as DevServerLock;
|
|
76
|
+
const url = serverUrl(parsed.url);
|
|
77
|
+
if (url !== undefined && typeof parsed.pid === "number") {
|
|
35
78
|
return {
|
|
36
|
-
url
|
|
79
|
+
url,
|
|
37
80
|
pid: parsed.pid,
|
|
38
81
|
...(typeof parsed.password === "string" ? { password: parsed.password } : {}),
|
|
39
82
|
...(typeof parsed.deck === "string" ? { deck: parsed.deck } : {}),
|
package/src/server/routes.ts
CHANGED
|
@@ -8,7 +8,9 @@ import { escapeAttr, escapeHtml } from "../core/escape.ts";
|
|
|
8
8
|
import { deckSlides, htmlShell, slidePlace, stampSlide } from "../core/html.ts";
|
|
9
9
|
import { decodePosition } from "../core/live-protocol.ts";
|
|
10
10
|
import { markedPositions, toggleMark } from "../core/marks.ts";
|
|
11
|
+
import { deckProjectRoot } from "../core/path.ts";
|
|
11
12
|
import type { Project, ProjectDeck } from "../core/resolve.ts";
|
|
13
|
+
import { followInside } from "../core/safe-fs.ts";
|
|
12
14
|
import { voiceCacheFile } from "../core/voice.ts";
|
|
13
15
|
import { deckRoutePath, parseDeckRoute, splitDeckPath, type VoiceFile } from "../runtime/routes.ts";
|
|
14
16
|
import { EVENT_EXPOSURE, type Exposure, mayRead, type Route } from "./auth.ts";
|
|
@@ -149,7 +151,8 @@ export function routeRequest(req: Request, ctx: RouteContext): Routed {
|
|
|
149
151
|
const MISSING: Routed = { route: "missing", respond: notFound };
|
|
150
152
|
|
|
151
153
|
function voiceResponse(deck: ProjectDeck | undefined, file: VoiceFile): Response {
|
|
152
|
-
|
|
154
|
+
// A link out of the project is no voice of the deck's, and is not served.
|
|
155
|
+
const path = deck && followInside(voiceCacheFile(deck.dir, file), deckProjectRoot(deck.dir));
|
|
153
156
|
if (!path || !existsSync(path)) {
|
|
154
157
|
return notFound();
|
|
155
158
|
}
|
|
@@ -291,7 +294,9 @@ function jsonNotFound(deckName: string): Response {
|
|
|
291
294
|
function safeDeckAsset(deckDir: string, relative: string): string | undefined {
|
|
292
295
|
const root = join(deckDir, "assets");
|
|
293
296
|
const found = fileInside(join(root, relative), root);
|
|
294
|
-
|
|
297
|
+
// assets/ may itself be a link, and then everything under what it points to is "inside" it:
|
|
298
|
+
// what is served must also be in the project, as every file dek reads is.
|
|
299
|
+
return found.kind === "file" ? followInside(found.path, deckProjectRoot(deckDir)) : undefined;
|
|
295
300
|
}
|
|
296
301
|
|
|
297
302
|
function jsonResponse(body: unknown, status = 200): Response {
|
package/src/theme/default.css
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
background: var(--bg);
|
|
21
21
|
color: var(--fg);
|
|
22
22
|
font-family: var(--font-body);
|
|
23
|
+
/* Japanese breaks between phrases, and no paragraph ends on a word left alone. */
|
|
24
|
+
word-break: auto-phrase;
|
|
25
|
+
text-wrap: pretty;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
.slide .slide-title {
|
|
@@ -27,6 +30,7 @@
|
|
|
27
30
|
font-family: var(--font-title);
|
|
28
31
|
font-size: var(--size-title);
|
|
29
32
|
line-height: 1.2;
|
|
33
|
+
text-wrap: balance;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/* @layout title
|
package/src/video/bake.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type ResolvedDeck,
|
|
10
10
|
requireSection,
|
|
11
11
|
} from "../core/resolve.ts";
|
|
12
|
-
import { dropLinks, outputDir, outputPath, replaceFile } from "../core/safe-fs.ts";
|
|
12
|
+
import { dropLinks, outputDir, outputPath, replaceFile, requireInside } from "../core/safe-fs.ts";
|
|
13
13
|
import { logicalSize } from "../core/size.ts";
|
|
14
14
|
import { sliceTimeline, slideTimeRange, type Timeline } from "../core/timeline.ts";
|
|
15
15
|
import { videoDocument } from "../core/video-document.ts";
|
|
@@ -69,7 +69,11 @@ async function bakeProjectDeck(
|
|
|
69
69
|
hint: "run `dekc voice`",
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
// ffmpeg opens the audio itself, so it is resolved here: a link out of the project is an error.
|
|
73
|
+
let timeline = {
|
|
74
|
+
...loaded,
|
|
75
|
+
audio: requireInside(resolveTimelineAudio(loaded, timelinePath), project.root),
|
|
76
|
+
};
|
|
73
77
|
const fps = options.fps ?? 30;
|
|
74
78
|
const size = logicalSize(deck.deck.ratio);
|
|
75
79
|
const html = videoDocument({ project, deck }, await playerScript());
|