@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/playwright.ts
CHANGED
|
@@ -2,11 +2,14 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import type { Browser } from "playwright";
|
|
5
|
+
import type { AlignedEdge, Misalignment } from "./alignment.ts";
|
|
5
6
|
import { DekError } from "./error.ts";
|
|
6
7
|
import type { Fill } from "./fill.ts";
|
|
8
|
+
import type { LineBreakKind } from "./line-break.ts";
|
|
7
9
|
import { type Collision, EDGES, type Overflow } from "./overflow.ts";
|
|
8
10
|
import { moduleFilePath } from "./path.ts";
|
|
9
|
-
import type { PptxTextBox } from "./pptx-package.ts";
|
|
11
|
+
import type { PptxFill, PptxLayer, PptxTextBox } from "./pptx-package.ts";
|
|
12
|
+
import type { PptxPageTheme } from "./pptx-theme.ts";
|
|
10
13
|
import type { MotionBeat, SheetSpec } from "./sheet.ts";
|
|
11
14
|
import { exitWorker, readWorkerRequest, runJsonWorker, workerCommand } from "./spawn.ts";
|
|
12
15
|
import type { Position } from "./step.ts";
|
|
@@ -41,7 +44,7 @@ export type MotionSpec = {
|
|
|
41
44
|
};
|
|
42
45
|
|
|
43
46
|
/** What to measure on each page; a screenshot is asked for by the page's `screenshotPath`. */
|
|
44
|
-
export type PageAction = "overflow" | "contrast" | "fill";
|
|
47
|
+
export type PageAction = "overflow" | "contrast" | "fill" | "breaks" | "alignment";
|
|
45
48
|
|
|
46
49
|
type Viewport = { width: number; height: number };
|
|
47
50
|
|
|
@@ -84,8 +87,9 @@ export type MotionRequest = {
|
|
|
84
87
|
};
|
|
85
88
|
|
|
86
89
|
/**
|
|
87
|
-
* Each slide's still page drawn for a PPTX, at twice its size: its text
|
|
88
|
-
* then the page shot without
|
|
90
|
+
* Each slide's still page drawn for a PPTX, at twice its size: its text, boxes, and pictures
|
|
91
|
+
* measured, each picture drawn alone beside `screenshotPath`, then the page shot without what
|
|
92
|
+
* is laid over it, to `screenshotPath`.
|
|
89
93
|
*/
|
|
90
94
|
export type PptxRequest = {
|
|
91
95
|
kind: "pptx";
|
|
@@ -93,9 +97,22 @@ export type PptxRequest = {
|
|
|
93
97
|
pages: Array<{ html: string; slug: string; screenshotPath: string }>;
|
|
94
98
|
};
|
|
95
99
|
|
|
96
|
-
/**
|
|
100
|
+
/** A picture on its way to the package: the PNG the worker wrote, and the SVG it read. */
|
|
101
|
+
export type PptxPictureFile = { png: string; svg?: string };
|
|
102
|
+
|
|
103
|
+
/** Each page's layers, in the order of the request's pages, with the font each run was drawn in. */
|
|
97
104
|
export type PptxResponse = {
|
|
98
|
-
slides: Array<{
|
|
105
|
+
slides: Array<{
|
|
106
|
+
slug: string;
|
|
107
|
+
description: string;
|
|
108
|
+
layers: PptxLayer<PptxPictureFile>[];
|
|
109
|
+
/** Whether anything is left in the slide's picture; false for nothing. */
|
|
110
|
+
picture?: boolean;
|
|
111
|
+
/** The slide's background, when PowerPoint paints it. */
|
|
112
|
+
background?: PptxFill;
|
|
113
|
+
/** What the slide says of the deck's theme. */
|
|
114
|
+
theme?: PptxPageTheme;
|
|
115
|
+
}>;
|
|
99
116
|
};
|
|
100
117
|
|
|
101
118
|
/** What the Playwright worker is asked; the kind says which payload comes with it. */
|
|
@@ -144,6 +161,30 @@ type CollisionFinding = Collision & { slug: string; step: string };
|
|
|
144
161
|
/** How much of the frame one page fills, and where. */
|
|
145
162
|
export type FillFinding = Fill & { slug: string; step: string };
|
|
146
163
|
|
|
164
|
+
/**
|
|
165
|
+
* A line break that reads wrong on one page, with what decides where the fix goes: the slide's own
|
|
166
|
+
* `word-break` and the text's `text-wrap`, as the page computes them.
|
|
167
|
+
*/
|
|
168
|
+
type LineBreakFinding = {
|
|
169
|
+
slug: string;
|
|
170
|
+
step: string;
|
|
171
|
+
box: string;
|
|
172
|
+
text?: string;
|
|
173
|
+
lines: string[];
|
|
174
|
+
wordBreak: string;
|
|
175
|
+
textWrap: string;
|
|
176
|
+
} & LineBreakKind;
|
|
177
|
+
|
|
178
|
+
/** Two items on one page that nearly line up and do not, each named as the page shows it. */
|
|
179
|
+
type MisalignmentFinding = Omit<Misalignment, "element" | "other"> & {
|
|
180
|
+
slug: string;
|
|
181
|
+
step: string;
|
|
182
|
+
box: string;
|
|
183
|
+
text?: string;
|
|
184
|
+
other: string;
|
|
185
|
+
otherText?: string;
|
|
186
|
+
};
|
|
187
|
+
|
|
147
188
|
export type PagesResponse = {
|
|
148
189
|
overflows: OverflowFinding[];
|
|
149
190
|
contrasts: ContrastFinding[];
|
|
@@ -153,6 +194,10 @@ export type PagesResponse = {
|
|
|
153
194
|
collisions?: CollisionFinding[];
|
|
154
195
|
/** One for each page, when `fill` was asked for; absent from a runner that has none. */
|
|
155
196
|
fills?: FillFinding[];
|
|
197
|
+
/** Absent from a runner that has none to report. */
|
|
198
|
+
lineBreaks?: LineBreakFinding[];
|
|
199
|
+
/** Absent from a runner that has none to report. */
|
|
200
|
+
misalignments?: MisalignmentFinding[];
|
|
156
201
|
};
|
|
157
202
|
|
|
158
203
|
export type MotionResponse = {
|
|
@@ -318,13 +363,23 @@ function spawnRunner(
|
|
|
318
363
|
const RESPONSE_PARSERS: {
|
|
319
364
|
[K in keyof VisualResponses]: (fields: Record<string, unknown>) => VisualResponses[K] | null;
|
|
320
365
|
} = {
|
|
321
|
-
pages: ({
|
|
366
|
+
pages: ({
|
|
367
|
+
overflows,
|
|
368
|
+
contrasts,
|
|
369
|
+
drawErrors = [],
|
|
370
|
+
collisions = [],
|
|
371
|
+
fills = [],
|
|
372
|
+
lineBreaks = [],
|
|
373
|
+
misalignments = [],
|
|
374
|
+
}) =>
|
|
322
375
|
isArrayOf(overflows, isOverflowFinding) &&
|
|
323
376
|
isArrayOf(contrasts, isContrastFinding) &&
|
|
324
377
|
isArrayOf(drawErrors, isDrawErrorFinding) &&
|
|
325
378
|
isArrayOf(collisions, isCollisionFinding) &&
|
|
326
|
-
isArrayOf(fills, isFillFinding)
|
|
327
|
-
|
|
379
|
+
isArrayOf(fills, isFillFinding) &&
|
|
380
|
+
isArrayOf(lineBreaks, isLineBreakFinding) &&
|
|
381
|
+
isArrayOf(misalignments, isMisalignmentFinding)
|
|
382
|
+
? { overflows, contrasts, drawErrors, collisions, fills, lineBreaks, misalignments }
|
|
328
383
|
: null,
|
|
329
384
|
pdf: () => ({}),
|
|
330
385
|
morph: () => ({}),
|
|
@@ -401,6 +456,37 @@ function isFillFinding(value: unknown): value is FillFinding {
|
|
|
401
456
|
);
|
|
402
457
|
}
|
|
403
458
|
|
|
459
|
+
function isLineBreakFinding(value: unknown): value is LineBreakFinding {
|
|
460
|
+
if (!isFinding(value)) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
const { kind, lines, phrased, tail, wordBreak, textWrap } = value;
|
|
464
|
+
return (
|
|
465
|
+
isArrayOf(lines, isString) &&
|
|
466
|
+
isString(wordBreak) &&
|
|
467
|
+
isString(textWrap) &&
|
|
468
|
+
((kind === "split" && isArrayOf(phrased, isString)) || (kind === "short" && isString(tail)))
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const ALIGNED_EDGES: ReadonlySet<unknown> = new Set<AlignedEdge>([
|
|
473
|
+
"top",
|
|
474
|
+
"middle",
|
|
475
|
+
"bottom",
|
|
476
|
+
"left",
|
|
477
|
+
"center",
|
|
478
|
+
"right",
|
|
479
|
+
]);
|
|
480
|
+
|
|
481
|
+
function isMisalignmentFinding(value: unknown): value is MisalignmentFinding {
|
|
482
|
+
// Both name two elements by their boxes and their texts, as a collision does.
|
|
483
|
+
if (!isCollisionFinding(value)) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
const { edge, of, by } = value as Record<string, unknown>;
|
|
487
|
+
return ALIGNED_EDGES.has(edge) && (of === "box" || of === "text") && isNumber(by);
|
|
488
|
+
}
|
|
489
|
+
|
|
404
490
|
function isDrawErrorFinding(value: unknown): value is DrawErrorFinding {
|
|
405
491
|
if (!value || typeof value !== "object") {
|
|
406
492
|
return false;
|
|
@@ -453,34 +539,125 @@ function isPptxSlide(value: unknown): value is PptxResponse["slides"][number] {
|
|
|
453
539
|
if (!value || typeof value !== "object") {
|
|
454
540
|
return false;
|
|
455
541
|
}
|
|
456
|
-
const { slug, description,
|
|
457
|
-
|
|
542
|
+
const { slug, description, layers, picture, background, theme } = value as Record<
|
|
543
|
+
string,
|
|
544
|
+
unknown
|
|
545
|
+
>;
|
|
546
|
+
return (
|
|
547
|
+
isString(slug) &&
|
|
548
|
+
isString(description) &&
|
|
549
|
+
isArrayOf(layers, isPptxLayer) &&
|
|
550
|
+
(picture === undefined || typeof picture === "boolean") &&
|
|
551
|
+
(background === undefined || isFill(background)) &&
|
|
552
|
+
(theme === undefined || (Boolean(theme) && typeof theme === "object"))
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function isFill(value: unknown): value is PptxFill {
|
|
557
|
+
if (!value || typeof value !== "object") {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
const fill = value as Record<string, unknown>;
|
|
561
|
+
return fill.kind === "solid"
|
|
562
|
+
? isPaint(fill.paint)
|
|
563
|
+
: fill.kind === "linear" &&
|
|
564
|
+
isNumber(fill.angle) &&
|
|
565
|
+
isArrayOf(
|
|
566
|
+
fill.stops,
|
|
567
|
+
(stop: unknown): stop is object =>
|
|
568
|
+
Boolean(stop) &&
|
|
569
|
+
typeof stop === "object" &&
|
|
570
|
+
isNumber((stop as Record<string, unknown>).at) &&
|
|
571
|
+
isPaint((stop as Record<string, unknown>).paint),
|
|
572
|
+
);
|
|
458
573
|
}
|
|
459
574
|
|
|
460
|
-
function
|
|
575
|
+
function isPptxLayer(value: unknown): value is PptxLayer<PptxPictureFile> {
|
|
461
576
|
if (!value || typeof value !== "object") {
|
|
462
577
|
return false;
|
|
463
578
|
}
|
|
464
|
-
const
|
|
579
|
+
const layer = value as Record<string, unknown>;
|
|
580
|
+
const at = ["x", "y", "width", "height"].every((key) => isNumber(layer[key]));
|
|
581
|
+
switch (layer.kind) {
|
|
582
|
+
case "text":
|
|
583
|
+
return isPptxBox(layer);
|
|
584
|
+
case "rule":
|
|
585
|
+
return (
|
|
586
|
+
["x1", "y1", "x2", "y2", "width"].every((key) => isNumber(layer[key])) &&
|
|
587
|
+
isPaint(layer.paint)
|
|
588
|
+
);
|
|
589
|
+
case "picture":
|
|
590
|
+
return (
|
|
591
|
+
at &&
|
|
592
|
+
isString(layer.png) &&
|
|
593
|
+
isString(layer.description) &&
|
|
594
|
+
(layer.svg === undefined || isString(layer.svg))
|
|
595
|
+
);
|
|
596
|
+
case "shape":
|
|
597
|
+
return (
|
|
598
|
+
at &&
|
|
599
|
+
isNumber(layer.radius) &&
|
|
600
|
+
(layer.fill === undefined || isPaint(layer.fill)) &&
|
|
601
|
+
isArrayOf(layer.layers, isPptxLayer) &&
|
|
602
|
+
(layer.text === undefined || isShapeText(layer.text))
|
|
603
|
+
);
|
|
604
|
+
default:
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function isPaint(value: unknown): boolean {
|
|
610
|
+
if (!value || typeof value !== "object") {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
const { color, alpha } = value as Record<string, unknown>;
|
|
614
|
+
return isArrayOf(color, isNumber) && isNumber(alpha);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function isShapeText(value: unknown): boolean {
|
|
618
|
+
if (!value || typeof value !== "object") {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
const { inset, paragraphs } = value as Record<string, unknown>;
|
|
465
622
|
return (
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
623
|
+
Boolean(inset) &&
|
|
624
|
+
typeof inset === "object" &&
|
|
625
|
+
isArrayOf(paragraphs, (paragraph: unknown): paragraph is object => {
|
|
626
|
+
if (!paragraph || typeof paragraph !== "object") {
|
|
469
627
|
return false;
|
|
470
628
|
}
|
|
471
|
-
const {
|
|
472
|
-
return (
|
|
473
|
-
isString(text) &&
|
|
474
|
-
isNumber(size) &&
|
|
475
|
-
isNumber(alpha) &&
|
|
476
|
-
isNumber(letterSpacing) &&
|
|
477
|
-
isArrayOf(color, isNumber) &&
|
|
478
|
-
(font === undefined || isString(font))
|
|
479
|
-
);
|
|
629
|
+
const { runs, lineHeight } = paragraph as Record<string, unknown>;
|
|
630
|
+
return isNumber(lineHeight) && isArrayOf(runs, isPptxRun);
|
|
480
631
|
})
|
|
481
632
|
);
|
|
482
633
|
}
|
|
483
634
|
|
|
635
|
+
function isPptxBox(value: unknown): value is PptxTextBox {
|
|
636
|
+
if (!value || typeof value !== "object") {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
const box = value as Record<string, unknown>;
|
|
640
|
+
return (
|
|
641
|
+
["x", "y", "width", "height", "lineHeight"].every((key) => isNumber(box[key])) &&
|
|
642
|
+
isArrayOf(box.runs, isPptxRun)
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function isPptxRun(run: unknown): run is PptxTextBox["runs"][number] {
|
|
647
|
+
if (!run || typeof run !== "object") {
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
const { text, size, alpha, letterSpacing, color, font } = run as Record<string, unknown>;
|
|
651
|
+
return (
|
|
652
|
+
isString(text) &&
|
|
653
|
+
isNumber(size) &&
|
|
654
|
+
isNumber(alpha) &&
|
|
655
|
+
isNumber(letterSpacing) &&
|
|
656
|
+
isArrayOf(color, isNumber) &&
|
|
657
|
+
(font === undefined || isString(font))
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
484
661
|
function isMotionBeat(value: unknown): value is MotionBeat {
|
|
485
662
|
if (!value || typeof value !== "object") {
|
|
486
663
|
return false;
|