@hajimism/dek 0.2.1 → 0.2.2

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.ja.md +3 -1
  3. package/README.md +3 -1
  4. package/package.json +3 -1
  5. package/skills/dek/SKILL.md +44 -0
  6. package/src/cli/annotations.ts +26 -0
  7. package/src/cli/commands.ts +66 -0
  8. package/src/cli/contract.ts +75 -4
  9. package/src/cli/files.ts +3 -3
  10. package/src/cli/flags.ts +1 -1
  11. package/src/cli/format.ts +3 -0
  12. package/src/cli/pptx.ts +20 -0
  13. package/src/cli/show.ts +17 -2
  14. package/src/cli/text.ts +43 -0
  15. package/src/cli/theme.ts +7 -1
  16. package/src/core/annotation-rows.ts +176 -0
  17. package/src/core/annotations.ts +362 -0
  18. package/src/core/diagnostic.ts +1 -0
  19. package/src/core/html-scan.ts +54 -0
  20. package/src/core/html.ts +53 -8
  21. package/src/core/lint/slide-html.ts +92 -2
  22. package/src/core/lint.ts +8 -1
  23. package/src/core/live-protocol.ts +4 -1
  24. package/src/core/page.ts +6 -0
  25. package/src/core/playwright-module.d.ts +4 -1
  26. package/src/core/playwright-visual.ts +74 -1
  27. package/src/core/playwright.ts +51 -1
  28. package/src/core/pptx-package.ts +312 -0
  29. package/src/core/pptx-text.ts +363 -0
  30. package/src/core/pptx.ts +75 -0
  31. package/src/core/slide-measure.ts +1 -0
  32. package/src/core/sync.ts +13 -5
  33. package/src/core/theme-excerpt.ts +61 -9
  34. package/src/core/theme-facts.ts +12 -2
  35. package/src/core/zip.ts +77 -0
  36. package/src/runtime/annotate-pick.ts +2 -6
  37. package/src/runtime/annotate-view.ts +115 -101
  38. package/src/runtime/annotate.ts +5 -1
  39. package/src/runtime/browser.ts +5 -1
  40. package/src/runtime/live.ts +1 -0
  41. package/src/runtime/routes.ts +3 -0
  42. package/src/server/auth.ts +3 -1
  43. package/src/server/routes.ts +68 -1
  44. package/src/runtime/annotate-notes.ts +0 -207
@@ -1,207 +0,0 @@
1
- // What annotate mode keeps and hands over: notes on elements of a slide, each a snapshot of what
2
- // the page showed when it was written, and the Markdown an agent is given to act on them. No DOM
3
- // here, so all of it is decided by these functions alone.
4
-
5
- import { type BeatRef, resolveStop } from "../core/step.ts";
6
-
7
- /** A box on the slide, in logical pixels. */
8
- export type Box = { x: number; y: number; width: number; height: number };
9
-
10
- /** One element a note is about, as the page showed it when the note was written. */
11
- export type NoteTarget = {
12
- /** `<line>:<column>` of its start tag in `slides/<slug>.html`. */
13
- source: string;
14
- /** Its tag and its own classes, as `div.chevron`. */
15
- name: string;
16
- /** Its text, whitespace collapsed and cut short; empty for a shape. */
17
- text: string;
18
- box: Box;
19
- /** Where the slide itself was clicked, when the note is about a place rather than an element. */
20
- point?: { x: number; y: number };
21
- };
22
-
23
- /** One note, on one slide at one beat. */
24
- export type Note = {
25
- slug: string;
26
- /** The stop as `dekc shot --step` takes it: `0` as the slide arrives, else the beat. */
27
- step: string;
28
- targets: NoteTarget[];
29
- /** What the human wrote; it may be empty, when the words go in the chat. */
30
- text: string;
31
- /** Written before an edit that took its element away, so its line is as it was then. */
32
- stale?: true;
33
- };
34
-
35
- /** A slide as the page lists it. */
36
- export type SlideInfo = { slug: string; title: string; beats: BeatRef[] };
37
-
38
- /**
39
- * The notes in the order they are handed over: by slide and beat in talk order, then as they
40
- * were written. Markers on the page are numbered in this order too.
41
- */
42
- export function sortNotes<T extends Note>(slides: readonly SlideInfo[], notes: readonly T[]): T[] {
43
- const order = (slug: string): number => slides.findIndex((slide) => slide.slug === slug);
44
- const stop = (note: Note): number =>
45
- resolveStop(slides[order(note.slug)]?.beats ?? [], note.step) ?? Number.MAX_SAFE_INTEGER;
46
- return notes
47
- .map((note, index) => ({ note, index }))
48
- .sort(
49
- (a, b) =>
50
- order(a.note.slug) - order(b.note.slug) || stop(a.note) - stop(b.note) || a.index - b.index,
51
- )
52
- .map(({ note }) => note);
53
- }
54
-
55
- /**
56
- * The notes as one Markdown block for an agent: grouped by slide and beat in talk order, each
57
- * element with the file and line it is written at, and how to see the slide at that beat.
58
- */
59
- export function formatNotes(
60
- deck: string,
61
- slides: readonly SlideInfo[],
62
- notes: readonly Note[],
63
- ): string {
64
- if (notes.length === 0) {
65
- return "";
66
- }
67
- const groups = new Map<string, Note[]>();
68
- for (const note of sortNotes(slides, notes)) {
69
- const key = `${note.slug}\n${note.step}`;
70
- groups.set(key, [...(groups.get(key) ?? []), note]);
71
- }
72
-
73
- const lines = [
74
- `## Notes on decks/${deck}`,
75
- "",
76
- "Boxes and points are in the slide's own pixels, from its top left.",
77
- ];
78
- let number = 0;
79
- for (const group of groups.values()) {
80
- const first = group[0];
81
- if (!first) {
82
- continue;
83
- }
84
- const index = slides.findIndex((slide) => slide.slug === first.slug);
85
- const title = slides[index]?.title;
86
- lines.push(
87
- "",
88
- `### Slide ${index + 1} of ${slides.length}, ${first.slug}${title ? ` (${JSON.stringify(title)})` : ""}, ${beatLabel(first.step)}`,
89
- );
90
- for (const note of group) {
91
- number++;
92
- const where = (target: NoteTarget): string =>
93
- `${describe(target)} at decks/${deck}/slides/${note.slug}.html:${target.source} (${measure(target)})${note.stale ? ", as written before an edit; the line may have moved" : ""}`;
94
- lines.push("");
95
- if (note.targets.length === 1 && note.targets[0]) {
96
- lines.push(`${number}. ${where(note.targets[0])}`);
97
- } else {
98
- lines.push(`${number}. ${note.targets.length} elements:`);
99
- lines.push(...note.targets.map((target) => ` - ${where(target)}`));
100
- }
101
- if (note.text.trim() !== "") {
102
- lines.push(
103
- ...note.text
104
- .trim()
105
- .split(/\r?\n/)
106
- .map((line) => ` > ${line}`.trimEnd()),
107
- );
108
- }
109
- }
110
- lines.push("", `To see it: \`dekc shot ${deck} ${first.slug} --step ${first.step}\``);
111
- }
112
- return `${lines.join("\n")}\n`;
113
- }
114
-
115
- function beatLabel(step: string): string {
116
- if (step === "0") {
117
- return "as it arrives";
118
- }
119
- return /^\d+$/.test(step) ? `at beat ${step}` : `at beat \`${step}\``;
120
- }
121
-
122
- function describe(target: NoteTarget): string {
123
- return target.text === "" ? target.name : `${target.name} ${JSON.stringify(target.text)}`;
124
- }
125
-
126
- function measure(target: NoteTarget): string {
127
- if (target.point) {
128
- return `point ${target.point.x},${target.point.y}`;
129
- }
130
- const { x, y, width, height } = target.box;
131
- return `box ${x},${y} ${width}×${height}`;
132
- }
133
-
134
- /** An element on the slide as it is now, to match a note's target against. */
135
- type Present = { source: string; name: string; text: string };
136
-
137
- /**
138
- * The element a note's target is now, after an edit replaced its slide: the one at the same
139
- * place with the same name and text; else the only one with that name and text, wherever it
140
- * moved; else the one at the same place with the same name, whose text was rewritten, as an
141
- * agent does when it acts on the note. None when it cannot be told.
142
- */
143
- export function reanchor<T extends Present>(
144
- target: NoteTarget,
145
- present: readonly T[],
146
- ): T | undefined {
147
- const same = present.find(
148
- (el) => el.source === target.source && el.name === target.name && el.text === target.text,
149
- );
150
- if (same) {
151
- return same;
152
- }
153
- const moved = present.filter((el) => el.name === target.name && el.text === target.text);
154
- if (moved.length === 1) {
155
- return moved[0];
156
- }
157
- return present.find((el) => el.source === target.source && el.name === target.name);
158
- }
159
-
160
- /** The notes kept in storage, or none when what is there is not notes. */
161
- export function readNotes(raw: string | null): Note[] {
162
- let parsed: unknown;
163
- try {
164
- parsed = JSON.parse(raw ?? "");
165
- } catch {
166
- return [];
167
- }
168
- if (!isRecord(parsed) || parsed.version !== 1 || !Array.isArray(parsed.notes)) {
169
- return [];
170
- }
171
- return parsed.notes.filter(isNote);
172
- }
173
-
174
- /** The notes as storage keeps them. */
175
- export function writeNotes(notes: readonly Note[]): string {
176
- return JSON.stringify({ version: 1, notes });
177
- }
178
-
179
- function isNote(value: unknown): value is Note {
180
- return (
181
- isRecord(value) &&
182
- typeof value.slug === "string" &&
183
- typeof value.step === "string" &&
184
- typeof value.text === "string" &&
185
- (value.stale === undefined || value.stale === true) &&
186
- Array.isArray(value.targets) &&
187
- value.targets.length > 0 &&
188
- value.targets.every(isTarget)
189
- );
190
- }
191
-
192
- function isTarget(value: unknown): value is NoteTarget {
193
- return (
194
- isRecord(value) &&
195
- typeof value.source === "string" &&
196
- typeof value.name === "string" &&
197
- typeof value.text === "string" &&
198
- isRecord(value.box) &&
199
- [value.box.x, value.box.y, value.box.width, value.box.height].every(Number.isFinite) &&
200
- (value.point === undefined ||
201
- (isRecord(value.point) && Number.isFinite(value.point.x) && Number.isFinite(value.point.y)))
202
- );
203
- }
204
-
205
- function isRecord(value: unknown): value is Record<string, unknown> {
206
- return typeof value === "object" && value !== null && !Array.isArray(value);
207
- }