@hajimism/dek 0.3.1 → 0.3.3
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 +46 -0
- package/package.json +5 -5
- package/src/cli/commands.ts +45 -8
- package/src/cli/contract.ts +9 -0
- package/src/cli/files.ts +8 -1
- package/src/cli/new.ts +32 -8
- package/src/cli/pack.ts +21 -0
- package/src/cli/ref.ts +156 -4
- package/src/cli/scope.ts +14 -5
- package/src/cli/text.ts +18 -2
- package/src/core/config-keys.ts +6 -2
- package/src/core/config.ts +32 -5
- 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/pack.ts +137 -0
- package/src/core/pdf-text.ts +191 -0
- package/src/core/playwright-module.d.ts +6 -1
- package/src/core/playwright-visual.ts +40 -4
- package/src/core/playwright.ts +4 -1
- package/src/core/ref-name.ts +56 -12
- package/src/core/ref.ts +148 -20
- package/src/core/sync.ts +1 -1
- package/src/runtime/annotate-view.ts +4 -2
package/src/core/ref.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { outputDir } from "./safe-fs.ts";
|
|
|
11
11
|
export {
|
|
12
12
|
isPinnedRev,
|
|
13
13
|
isRefName,
|
|
14
|
+
packFile,
|
|
14
15
|
parseRefSource,
|
|
15
16
|
type RefSource,
|
|
16
17
|
} from "./ref-name.ts";
|
|
@@ -35,8 +36,10 @@ export const RefMeta = z.object({
|
|
|
35
36
|
export type RefMeta = z.infer<typeof RefMeta>;
|
|
36
37
|
|
|
37
38
|
export function refDir(root: string, name: string): string {
|
|
38
|
-
const
|
|
39
|
-
return
|
|
39
|
+
const source = parseRefSource(name);
|
|
40
|
+
return source.kind === "file"
|
|
41
|
+
? join(root, "refs", "file", source.deck)
|
|
42
|
+
: join(root, "refs", source.owner, source.repo, source.deck);
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
/** The marker of a snapshot, or nothing when it is missing or not one dek wrote. */
|
|
@@ -53,7 +56,7 @@ export function readRefMeta(dir: string): RefMeta | undefined {
|
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
const LICENSE_NAMES = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING"];
|
|
59
|
+
export const LICENSE_NAMES = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING"];
|
|
57
60
|
|
|
58
61
|
/** The snapshot's license file, relative to the snapshot, when the source had one. */
|
|
59
62
|
export function refLicense(dir: string): string | null {
|
|
@@ -86,7 +89,12 @@ export function refState(project: { root: string; configPath: string }, name: st
|
|
|
86
89
|
export type RepoFiles = Map<string, Blob>;
|
|
87
90
|
|
|
88
91
|
/** Beyond this, a snapshot is refused: a deck to read is a few hundred kilobytes. */
|
|
89
|
-
const MAX_SNAPSHOT_BYTES = 50 * 1024 * 1024;
|
|
92
|
+
export const MAX_SNAPSHOT_BYTES = 50 * 1024 * 1024;
|
|
93
|
+
|
|
94
|
+
/** A pack's tar, unzipped, may hold its files and their headers, not much more. */
|
|
95
|
+
const MAX_PACK_TAR_BYTES = 2 * MAX_SNAPSHOT_BYTES;
|
|
96
|
+
|
|
97
|
+
const PACK_HINT = "ask for a pack its author made with `dekc pack <deck>`";
|
|
90
98
|
|
|
91
99
|
/**
|
|
92
100
|
* Reads a GitHub tarball. Bun leaves out links; a path that is absolute, has
|
|
@@ -108,6 +116,64 @@ export async function readTarball(bytes: Uint8Array): Promise<RepoFiles> {
|
|
|
108
116
|
return files;
|
|
109
117
|
}
|
|
110
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Reads a pack from someone else. It is gunzipped through a count that stops at `maxTarBytes`
|
|
121
|
+
* before Bun takes any of it apart, so a small file that inflates without end is refused rather
|
|
122
|
+
* than held in memory; the tar is then read as a GitHub tarball is.
|
|
123
|
+
*/
|
|
124
|
+
export async function readPack(
|
|
125
|
+
bytes: Uint8Array,
|
|
126
|
+
limits: { maxBytes?: number; maxTarBytes?: number } = {},
|
|
127
|
+
): Promise<RepoFiles> {
|
|
128
|
+
const maxBytes = limits.maxBytes ?? MAX_SNAPSHOT_BYTES;
|
|
129
|
+
const maxTarBytes = limits.maxTarBytes ?? MAX_PACK_TAR_BYTES;
|
|
130
|
+
if (bytes.byteLength > maxBytes) {
|
|
131
|
+
throw new DekError(`the pack is larger than ${formatMb(maxBytes)}`, { hint: PACK_HINT });
|
|
132
|
+
}
|
|
133
|
+
const chunks: Uint8Array[] = [];
|
|
134
|
+
let size = 0;
|
|
135
|
+
try {
|
|
136
|
+
const reader = new Blob([new Uint8Array(bytes)])
|
|
137
|
+
.stream()
|
|
138
|
+
.pipeThrough(new DecompressionStream("gzip"))
|
|
139
|
+
.getReader();
|
|
140
|
+
for (;;) {
|
|
141
|
+
const { done, value } = await reader.read();
|
|
142
|
+
if (done) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
size += value.byteLength;
|
|
146
|
+
if (size > maxTarBytes) {
|
|
147
|
+
await reader.cancel();
|
|
148
|
+
throw new DekError(`the pack unpacks to more than ${formatMb(maxTarBytes)}`, {
|
|
149
|
+
hint: PACK_HINT,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
chunks.push(value);
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof DekError) {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
throw new DekError("the file is not a pack: a pack is a gzipped tar", { hint: PACK_HINT });
|
|
159
|
+
}
|
|
160
|
+
const tar = new Uint8Array(size);
|
|
161
|
+
let offset = 0;
|
|
162
|
+
for (const chunk of chunks) {
|
|
163
|
+
tar.set(chunk, offset);
|
|
164
|
+
offset += chunk.byteLength;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
return await readTarball(tar);
|
|
168
|
+
} catch {
|
|
169
|
+
throw new DekError("the file is not a pack: a pack is a gzipped tar", { hint: PACK_HINT });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatMb(bytes: number): string {
|
|
174
|
+
return bytes >= 1024 * 1024 ? `${bytes / 1024 / 1024} MB` : `${bytes} bytes`;
|
|
175
|
+
}
|
|
176
|
+
|
|
111
177
|
const DECK_SCRIPT_RE = /^(?:(.*)\/)?decks\/([^/]+)\/script\.md$/;
|
|
112
178
|
|
|
113
179
|
/** The decks in a repository: each `decks/<name>/script.md` beside its project's dek.toml. */
|
|
@@ -119,14 +185,32 @@ function repoDecks(files: RepoFiles): Array<{ name: string; prefix: string }> {
|
|
|
119
185
|
});
|
|
120
186
|
}
|
|
121
187
|
|
|
188
|
+
/** The name of the one deck a pack holds; a pack of none, or of more, is refused. */
|
|
189
|
+
export function deckOfPack(files: RepoFiles): string {
|
|
190
|
+
const names = [...new Set(repoDecks(files).map((deck) => deck.name))].sort();
|
|
191
|
+
const [name, ...others] = names;
|
|
192
|
+
if (name === undefined) {
|
|
193
|
+
throw new DekError("the pack holds no deck", {
|
|
194
|
+
hint: `a pack is one deck beside a dek.toml; ${PACK_HINT}`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (others.length > 0) {
|
|
198
|
+
throw new DekError(`the pack holds more than one deck: ${names.join(", ")}`, {
|
|
199
|
+
hint: `a pack is one deck; ${PACK_HINT}`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return name;
|
|
203
|
+
}
|
|
204
|
+
|
|
122
205
|
/** The directory of the project that holds the deck, as a prefix: "" or "sample/". */
|
|
123
206
|
export function findDeckInRepo(files: RepoFiles, source: RefSource): string {
|
|
124
|
-
const repo = `${source.owner}/${source.repo}`;
|
|
207
|
+
const repo = source.kind === "file" ? "the pack" : `${source.owner}/${source.repo}`;
|
|
208
|
+
const path = source.kind === "github" ? source.path : undefined;
|
|
125
209
|
const decks = repoDecks(files);
|
|
126
210
|
const matches = decks.filter(
|
|
127
211
|
(deck) =>
|
|
128
212
|
deck.name === source.deck &&
|
|
129
|
-
(
|
|
213
|
+
(path === undefined || `${deck.prefix}decks/${deck.name}` === path),
|
|
130
214
|
);
|
|
131
215
|
const [match, ...others] = matches;
|
|
132
216
|
if (!match) {
|
|
@@ -148,7 +232,7 @@ export function findDeckInRepo(files: RepoFiles, source: RefSource): string {
|
|
|
148
232
|
}
|
|
149
233
|
|
|
150
234
|
/** Inside the deck, only what reading it needs: not dist/, .cache/, or voice/. */
|
|
151
|
-
function keepsInDeck(path: string): boolean {
|
|
235
|
+
export function keepsInDeck(path: string): boolean {
|
|
152
236
|
return (
|
|
153
237
|
path === "script.md" ||
|
|
154
238
|
path === "theme.css" ||
|
|
@@ -157,17 +241,19 @@ function keepsInDeck(path: string): boolean {
|
|
|
157
241
|
);
|
|
158
242
|
}
|
|
159
243
|
|
|
244
|
+
/** The files a snapshot keeps, by their paths in it, and where the deck was in its source. */
|
|
245
|
+
export type Snapshot = {
|
|
246
|
+
files: Map<string, Blob>;
|
|
247
|
+
license: string | undefined;
|
|
248
|
+
/** The deck's directory in the source repository or pack. */
|
|
249
|
+
path: string;
|
|
250
|
+
};
|
|
251
|
+
|
|
160
252
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* beside the old one and swapped in, so a failure leaves the old one whole.
|
|
253
|
+
* What a snapshot keeps of a repository or a pack: its project's dek.toml, the deck's files that
|
|
254
|
+
* reading needs, and its license, laid out as the project it came from.
|
|
164
255
|
*/
|
|
165
|
-
export
|
|
166
|
-
root: string,
|
|
167
|
-
source: RefSource,
|
|
168
|
-
sha: string,
|
|
169
|
-
files: RepoFiles,
|
|
170
|
-
): Promise<{ dir: string; license: string | null; path: string }> {
|
|
256
|
+
export function pickSnapshot(files: RepoFiles, source: RefSource): Snapshot {
|
|
171
257
|
const prefix = findDeckInRepo(files, source);
|
|
172
258
|
const deckPrefix = `${prefix}decks/${source.deck}/`;
|
|
173
259
|
const license =
|
|
@@ -190,10 +276,52 @@ export async function installSnapshot(
|
|
|
190
276
|
hint: "a deck to read as a model should be far smaller; check its assets",
|
|
191
277
|
});
|
|
192
278
|
}
|
|
279
|
+
return { files: picked, license, path: `${prefix}decks/${source.deck}` };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* What a pack's ref is pinned to: the sha256 of every file the snapshot keeps, in byte order of
|
|
284
|
+
* their paths, each as its path, a NUL, its size in decimal digits, a NUL, and its bytes. It
|
|
285
|
+
* follows the files, not the tarball, whose headers carry the time it was made.
|
|
286
|
+
*/
|
|
287
|
+
export async function snapshotDigest(snapshot: Snapshot): Promise<string> {
|
|
288
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
289
|
+
const encoder = new TextEncoder();
|
|
290
|
+
const paths = [...snapshot.files.keys()].sort((a, b) =>
|
|
291
|
+
Buffer.compare(encoder.encode(a), encoder.encode(b)),
|
|
292
|
+
);
|
|
293
|
+
for (const path of paths) {
|
|
294
|
+
const blob = snapshot.files.get(path) ?? new Blob([]);
|
|
295
|
+
hasher.update(`${path}\0${blob.size}\0`);
|
|
296
|
+
hasher.update(new Uint8Array(await blob.arrayBuffer()));
|
|
297
|
+
}
|
|
298
|
+
return hasher.digest("hex");
|
|
299
|
+
}
|
|
193
300
|
|
|
301
|
+
/** Writes the snapshot of one deck at one commit under refs/; see writeSnapshot. */
|
|
302
|
+
export async function installSnapshot(
|
|
303
|
+
root: string,
|
|
304
|
+
source: RefSource,
|
|
305
|
+
sha: string,
|
|
306
|
+
files: RepoFiles,
|
|
307
|
+
): Promise<{ dir: string; license: string | null; path: string }> {
|
|
308
|
+
return writeSnapshot(root, source.name, sha, pickSnapshot(files, source));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Writes a snapshot under refs/, laid out as the project it came from so it resolves like any
|
|
313
|
+
* dek project, and marks it with what it is pinned to. It is written beside the old one and
|
|
314
|
+
* swapped in, so a failure leaves the old one whole.
|
|
315
|
+
*/
|
|
316
|
+
export async function writeSnapshot(
|
|
317
|
+
root: string,
|
|
318
|
+
name: string,
|
|
319
|
+
pin: string,
|
|
320
|
+
snapshot: Snapshot,
|
|
321
|
+
): Promise<{ dir: string; license: string | null; path: string }> {
|
|
194
322
|
// refs/ is dek's; a link on the way would aim the swap below, and its delete, elsewhere.
|
|
195
323
|
const refsDir = join(root, "refs");
|
|
196
|
-
const dir = refDir(root,
|
|
324
|
+
const dir = refDir(root, name);
|
|
197
325
|
outputDir(dirname(dir), root);
|
|
198
326
|
if (isLink(dir)) {
|
|
199
327
|
await rm(dir);
|
|
@@ -201,9 +329,9 @@ export async function installSnapshot(
|
|
|
201
329
|
const suffix = `${process.pid}-${Date.now().toString(36)}`;
|
|
202
330
|
const temp = join(refsDir, `.tmp-${suffix}`);
|
|
203
331
|
const old = join(refsDir, `.old-${suffix}`);
|
|
204
|
-
const meta: RefMeta = { name
|
|
332
|
+
const meta: RefMeta = { name, rev: pin, path: snapshot.path };
|
|
205
333
|
try {
|
|
206
|
-
for (const [path, blob] of
|
|
334
|
+
for (const [path, blob] of snapshot.files) {
|
|
207
335
|
await mkdir(dirname(join(temp, path)), { recursive: true });
|
|
208
336
|
await writeFile(join(temp, path), new Uint8Array(await blob.arrayBuffer()));
|
|
209
337
|
}
|
|
@@ -224,7 +352,7 @@ export async function installSnapshot(
|
|
|
224
352
|
await rm(old, { recursive: true, force: true });
|
|
225
353
|
}
|
|
226
354
|
}
|
|
227
|
-
return { dir, license: license ?? null, path: meta.path };
|
|
355
|
+
return { dir, license: snapshot.license ?? null, path: meta.path };
|
|
228
356
|
}
|
|
229
357
|
|
|
230
358
|
/** The snapshot deck's title and slide count, or nothing when it does not parse. */
|
package/src/core/sync.ts
CHANGED
|
@@ -336,7 +336,7 @@ function referencesSection(root: string): string {
|
|
|
336
336
|
return `
|
|
337
337
|
## References
|
|
338
338
|
|
|
339
|
-
Other people's decks, pinned in \`dek.toml\` \`[refs]\`, to read as models. They are read-only
|
|
339
|
+
Other people's decks, pinned in \`dek.toml\` \`[refs]\`, to read as models. They are read-only. To build a deck on a ref's layouts, start it with \`dekc new <name> --theme-from <ref>\`, which copies the ref's theme into it. Otherwise read a slide with \`dekc show <ref> <slug>\`, then write your own in this deck's theme.
|
|
340
340
|
|
|
341
341
|
${lines.join("\n")}
|
|
342
342
|
`;
|
|
@@ -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") {
|