@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.ja.md +1 -1
  3. package/README.md +1 -1
  4. package/package.json +2 -1
  5. package/skills/dek/SKILL.md +10 -3
  6. package/skills/dek-design/SKILL.md +42 -0
  7. package/src/cli/address.ts +84 -14
  8. package/src/cli/build.ts +2 -2
  9. package/src/cli/check.ts +2 -1
  10. package/src/cli/commands.ts +54 -13
  11. package/src/cli/contract.ts +18 -1
  12. package/src/cli/files.ts +9 -6
  13. package/src/cli/flags.ts +1 -1
  14. package/src/cli/goto.ts +3 -1
  15. package/src/cli/init.ts +10 -5
  16. package/src/cli/lint.ts +4 -3
  17. package/src/cli/main.ts +7 -5
  18. package/src/cli/new.ts +41 -10
  19. package/src/cli/pack.ts +21 -0
  20. package/src/cli/pptx.ts +9 -4
  21. package/src/cli/ref.ts +243 -9
  22. package/src/cli/result.ts +5 -2
  23. package/src/cli/scope.ts +25 -27
  24. package/src/cli/shot.ts +2 -1
  25. package/src/cli/text.ts +45 -4
  26. package/src/cli/theme.ts +16 -2
  27. package/src/cli/voice.ts +4 -3
  28. package/src/core/alignment.ts +320 -0
  29. package/src/core/annotation-rows.ts +16 -4
  30. package/src/core/annotations.ts +19 -1
  31. package/src/core/config-keys.ts +6 -2
  32. package/src/core/config.ts +88 -12
  33. package/src/core/diagnostic.ts +5 -1
  34. package/src/core/escape.ts +43 -0
  35. package/src/core/licenses/CC-BY-4.0.txt +396 -0
  36. package/src/core/licenses/CC0-1.0.txt +121 -0
  37. package/src/core/licenses.ts +20 -0
  38. package/src/core/line-break.ts +275 -0
  39. package/src/core/lint/asset-refs.ts +6 -2
  40. package/src/core/lint/files.ts +30 -5
  41. package/src/core/lint/keys.ts +7 -3
  42. package/src/core/lint/slide-html.ts +3 -1
  43. package/src/core/lint/slide-script.ts +7 -5
  44. package/src/core/mv.ts +30 -10
  45. package/src/core/pack.ts +137 -0
  46. package/src/core/playwright-module.d.ts +1 -0
  47. package/src/core/playwright-visual.ts +180 -20
  48. package/src/core/playwright.ts +202 -25
  49. package/src/core/pptx-package.ts +461 -33
  50. package/src/core/pptx-paint.ts +1168 -0
  51. package/src/core/pptx-plan.ts +879 -0
  52. package/src/core/pptx-text.ts +204 -47
  53. package/src/core/pptx-theme.ts +222 -0
  54. package/src/core/pptx.ts +55 -8
  55. package/src/core/prose.ts +23 -0
  56. package/src/core/pseudo-text.ts +32 -10
  57. package/src/core/ref-name.ts +100 -17
  58. package/src/core/ref.ts +318 -34
  59. package/src/core/resolve.ts +20 -9
  60. package/src/core/shot/morph.ts +1 -1
  61. package/src/core/shot/motion.ts +2 -3
  62. package/src/core/shot/still.ts +2 -2
  63. package/src/core/shot-cache.ts +25 -17
  64. package/src/core/slide-measure.ts +51 -1
  65. package/src/core/slide-script-eval.ts +10 -8
  66. package/src/core/slide-script.ts +114 -20
  67. package/src/core/sync.ts +3 -3
  68. package/src/core/visual-cache.ts +2 -0
  69. package/src/core/visual.ts +154 -15
  70. package/src/core/voice.ts +3 -5
  71. package/src/core/zod.ts +9 -1
  72. package/src/runtime/annotate-view.ts +4 -2
  73. package/src/server/dev.ts +15 -4
  74. package/src/server/lock.ts +48 -5
  75. package/src/server/routes.ts +7 -2
  76. package/src/theme/default.css +4 -0
  77. package/src/video/bake.ts +6 -2
@@ -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 measured line by line,
88
- * then the page shot without it, to `screenshotPath`.
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
- /** Each page's text, in the order of the request's pages, with the font each run was drawn in. */
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<{ slug: string; description: string; boxes: PptxTextBox[] }>;
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: ({ overflows, contrasts, drawErrors = [], collisions = [], fills = [] }) =>
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
- ? { overflows, contrasts, drawErrors, collisions, fills }
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, boxes } = value as Record<string, unknown>;
457
- return isString(slug) && isString(description) && isArrayOf(boxes, isPptxBox);
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 isPptxBox(value: unknown): value is PptxTextBox {
575
+ function isPptxLayer(value: unknown): value is PptxLayer<PptxPictureFile> {
461
576
  if (!value || typeof value !== "object") {
462
577
  return false;
463
578
  }
464
- const box = value as Record<string, unknown>;
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
- ["x", "y", "width", "height", "lineHeight"].every((key) => isNumber(box[key])) &&
467
- isArrayOf(box.runs, (run: unknown): run is PptxTextBox["runs"][number] => {
468
- if (!run || typeof run !== "object") {
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 { text, size, alpha, letterSpacing, color, font } = run as Record<string, unknown>;
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;