@hajimism/dek 0.3.3 → 0.5.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 +136 -0
- package/README.ja.md +2 -2
- package/README.md +2 -2
- package/package.json +5 -3
- package/skills/dek/SKILL.md +10 -3
- package/skills/dek-design/SKILL.md +42 -0
- package/src/cli/address.ts +95 -18
- package/src/cli/build.ts +2 -2
- package/src/cli/check.ts +2 -1
- package/src/cli/commands.ts +57 -19
- package/src/cli/contract.ts +58 -4
- package/src/cli/files.ts +2 -5
- package/src/cli/flags.ts +1 -1
- package/src/cli/format.ts +5 -4
- package/src/cli/goto.ts +44 -11
- package/src/cli/init.ts +10 -5
- package/src/cli/lint.ts +36 -16
- package/src/cli/ls.ts +5 -1
- package/src/cli/main.ts +51 -14
- package/src/cli/new.ts +28 -8
- package/src/cli/pptx.ts +9 -4
- package/src/cli/ref.ts +92 -9
- package/src/cli/result.ts +47 -6
- package/src/cli/scope.ts +57 -29
- package/src/cli/shot.ts +78 -6
- package/src/cli/show.ts +15 -2
- package/src/cli/text.ts +88 -11
- package/src/cli/theme.ts +58 -10
- package/src/cli/tty.ts +16 -2
- package/src/cli/voice.ts +15 -9
- 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/assets.ts +18 -10
- package/src/core/attribute-value.ts +188 -0
- package/src/core/build.ts +3 -3
- package/src/core/config.ts +61 -12
- package/src/core/css-scan.ts +173 -39
- package/src/core/css-transform.ts +57 -11
- package/src/core/css.ts +19 -13
- package/src/core/diagnostic.ts +5 -1
- package/src/core/escape.ts +43 -0
- package/src/core/foreign-text.ts +138 -0
- package/src/core/github.ts +1 -0
- package/src/core/html-entities.ts +318 -0
- package/src/core/html-scan.ts +49 -27
- package/src/core/html.ts +21 -15
- package/src/core/line-break.ts +275 -0
- package/src/core/lint/asset-refs.ts +50 -8
- package/src/core/lint/files.ts +30 -5
- package/src/core/lint/keys.ts +7 -3
- package/src/core/lint/slide-html.ts +24 -19
- package/src/core/lint/slide-script.ts +7 -5
- package/src/core/mv.ts +57 -27
- package/src/core/order.ts +14 -0
- package/src/core/pack.ts +2 -2
- package/src/core/path.ts +8 -2
- package/src/core/playwright-module.d.ts +13 -1
- package/src/core/playwright-motion.ts +1 -0
- package/src/core/playwright-sheet.ts +1 -0
- package/src/core/playwright-visual.ts +207 -30
- package/src/core/playwright.ts +279 -44
- 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 +56 -8
- package/src/core/prose.ts +23 -0
- package/src/core/pseudo-text.ts +32 -10
- package/src/core/ref-name.ts +52 -8
- package/src/core/ref.ts +308 -54
- package/src/core/resolve.ts +23 -11
- package/src/core/rumdl.ts +51 -25
- package/src/core/safe-fs.ts +19 -2
- package/src/core/sandbox.ts +217 -0
- 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 +26 -17
- package/src/core/slide-eval-worker.ts +1 -0
- package/src/core/slide-measure.ts +51 -1
- package/src/core/slide-script-eval.ts +12 -8
- package/src/core/slide-script.ts +114 -20
- package/src/core/spawn.ts +131 -32
- package/src/core/sync.ts +5 -14
- package/src/core/tar.ts +315 -0
- package/src/core/text-contrast.ts +2 -0
- package/src/core/toml-keys.ts +116 -51
- package/src/core/url-attributes.ts +27 -9
- package/src/core/visual-cache.ts +2 -0
- package/src/core/visual.ts +182 -24
- package/src/core/voice.ts +5 -6
- package/src/core/zod.ts +9 -1
- package/src/server/auth.ts +21 -0
- package/src/server/dev.ts +26 -7
- package/src/server/lock.ts +50 -7
- package/src/server/routes.ts +7 -2
- package/src/server/serial.ts +8 -2
- package/src/theme/default.css +4 -0
- package/src/video/bake.ts +6 -2
- package/src/video/mux.ts +27 -3
- package/src/video/recorder.ts +3 -2
- package/src/video/worker.ts +1 -0
- package/src/voice/engine.ts +81 -18
package/src/core/playwright.ts
CHANGED
|
@@ -2,13 +2,17 @@ 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";
|
|
13
|
+
import { chromiumFailure, launchFailure, runDrawingWorker, type SandboxMode } from "./sandbox.ts";
|
|
10
14
|
import type { MotionBeat, SheetSpec } from "./sheet.ts";
|
|
11
|
-
import { exitWorker, readWorkerRequest,
|
|
15
|
+
import { exitWorker, readWorkerRequest, workerCommand } from "./spawn.ts";
|
|
12
16
|
import type { Position } from "./step.ts";
|
|
13
17
|
|
|
14
18
|
/** One slide page to measure; a page that names a `screenshotPath` is shot there too. */
|
|
@@ -41,7 +45,7 @@ export type MotionSpec = {
|
|
|
41
45
|
};
|
|
42
46
|
|
|
43
47
|
/** What to measure on each page; a screenshot is asked for by the page's `screenshotPath`. */
|
|
44
|
-
export type PageAction = "overflow" | "contrast" | "fill";
|
|
48
|
+
export type PageAction = "overflow" | "contrast" | "fill" | "breaks" | "alignment";
|
|
45
49
|
|
|
46
50
|
type Viewport = { width: number; height: number };
|
|
47
51
|
|
|
@@ -84,8 +88,9 @@ export type MotionRequest = {
|
|
|
84
88
|
};
|
|
85
89
|
|
|
86
90
|
/**
|
|
87
|
-
* Each slide's still page drawn for a PPTX, at twice its size: its text
|
|
88
|
-
* then the page shot without
|
|
91
|
+
* Each slide's still page drawn for a PPTX, at twice its size: its text, boxes, and pictures
|
|
92
|
+
* measured, each picture drawn alone beside `screenshotPath`, then the page shot without what
|
|
93
|
+
* is laid over it, to `screenshotPath`.
|
|
89
94
|
*/
|
|
90
95
|
export type PptxRequest = {
|
|
91
96
|
kind: "pptx";
|
|
@@ -93,9 +98,22 @@ export type PptxRequest = {
|
|
|
93
98
|
pages: Array<{ html: string; slug: string; screenshotPath: string }>;
|
|
94
99
|
};
|
|
95
100
|
|
|
96
|
-
/**
|
|
101
|
+
/** A picture on its way to the package: the PNG the worker wrote, and the SVG it read. */
|
|
102
|
+
export type PptxPictureFile = { png: string; svg?: string };
|
|
103
|
+
|
|
104
|
+
/** Each page's layers, in the order of the request's pages, with the font each run was drawn in. */
|
|
97
105
|
export type PptxResponse = {
|
|
98
|
-
slides: Array<{
|
|
106
|
+
slides: Array<{
|
|
107
|
+
slug: string;
|
|
108
|
+
description: string;
|
|
109
|
+
layers: PptxLayer<PptxPictureFile>[];
|
|
110
|
+
/** Whether anything is left in the slide's picture; false for nothing. */
|
|
111
|
+
picture?: boolean;
|
|
112
|
+
/** The slide's background, when PowerPoint paints it. */
|
|
113
|
+
background?: PptxFill;
|
|
114
|
+
/** What the slide says of the deck's theme. */
|
|
115
|
+
theme?: PptxPageTheme;
|
|
116
|
+
}>;
|
|
99
117
|
};
|
|
100
118
|
|
|
101
119
|
/** What the Playwright worker is asked; the kind says which payload comes with it. */
|
|
@@ -119,9 +137,15 @@ export type ContrastFinding = {
|
|
|
119
137
|
/**
|
|
120
138
|
* For text below its threshold, which stylesheet draws it so: "theme" when it stays below
|
|
121
139
|
* with the slide's own CSS taken away, "slide" when that CSS brings it there. Absent when the
|
|
122
|
-
* text passes, or shows no glyph without the slide's CSS.
|
|
140
|
+
* text passes, or shows no glyph without the slide's CSS. Absent too when it shows none with the
|
|
141
|
+
* slide script's draw undone and clears without the slide's CSS: either may have drawn it so.
|
|
123
142
|
*/
|
|
124
143
|
origin?: ContrastOrigin;
|
|
144
|
+
/**
|
|
145
|
+
* For text below its threshold that shows with the slide's own CSS and no glyph without it: the
|
|
146
|
+
* theme alone was never measured, so either stylesheet may hold its colors.
|
|
147
|
+
*/
|
|
148
|
+
onlyWithSlideCss?: true;
|
|
125
149
|
};
|
|
126
150
|
|
|
127
151
|
export type ContrastOrigin = "theme" | "slide" | "script";
|
|
@@ -144,6 +168,30 @@ type CollisionFinding = Collision & { slug: string; step: string };
|
|
|
144
168
|
/** How much of the frame one page fills, and where. */
|
|
145
169
|
export type FillFinding = Fill & { slug: string; step: string };
|
|
146
170
|
|
|
171
|
+
/**
|
|
172
|
+
* A line break that reads wrong on one page, with what decides where the fix goes: the slide's own
|
|
173
|
+
* `word-break` and the text's `text-wrap`, as the page computes them.
|
|
174
|
+
*/
|
|
175
|
+
type LineBreakFinding = {
|
|
176
|
+
slug: string;
|
|
177
|
+
step: string;
|
|
178
|
+
box: string;
|
|
179
|
+
text?: string;
|
|
180
|
+
lines: string[];
|
|
181
|
+
wordBreak: string;
|
|
182
|
+
textWrap: string;
|
|
183
|
+
} & LineBreakKind;
|
|
184
|
+
|
|
185
|
+
/** Two items on one page that nearly line up and do not, each named as the page shows it. */
|
|
186
|
+
type MisalignmentFinding = Omit<Misalignment, "element" | "other"> & {
|
|
187
|
+
slug: string;
|
|
188
|
+
step: string;
|
|
189
|
+
box: string;
|
|
190
|
+
text?: string;
|
|
191
|
+
other: string;
|
|
192
|
+
otherText?: string;
|
|
193
|
+
};
|
|
194
|
+
|
|
147
195
|
export type PagesResponse = {
|
|
148
196
|
overflows: OverflowFinding[];
|
|
149
197
|
contrasts: ContrastFinding[];
|
|
@@ -153,6 +201,10 @@ export type PagesResponse = {
|
|
|
153
201
|
collisions?: CollisionFinding[];
|
|
154
202
|
/** One for each page, when `fill` was asked for; absent from a runner that has none. */
|
|
155
203
|
fills?: FillFinding[];
|
|
204
|
+
/** Absent from a runner that has none to report. */
|
|
205
|
+
lineBreaks?: LineBreakFinding[];
|
|
206
|
+
/** Absent from a runner that has none to report. */
|
|
207
|
+
misalignments?: MisalignmentFinding[];
|
|
156
208
|
};
|
|
157
209
|
|
|
158
210
|
export type MotionResponse = {
|
|
@@ -186,6 +238,11 @@ type SpawnTimeoutOptions = {
|
|
|
186
238
|
timeoutMs?: number;
|
|
187
239
|
};
|
|
188
240
|
|
|
241
|
+
type RunnerOptions = SpawnTimeoutOptions & {
|
|
242
|
+
/** The pages hold a ref's deck: Chromium's sandbox only, unless the user accepted otherwise. */
|
|
243
|
+
ref?: boolean;
|
|
244
|
+
};
|
|
245
|
+
|
|
189
246
|
const requireFromDek = createRequire(import.meta.url);
|
|
190
247
|
|
|
191
248
|
export function resolvePlaywrightModule(): string | undefined {
|
|
@@ -248,49 +305,85 @@ export async function importPlaywright(): Promise<typeof import("playwright")> {
|
|
|
248
305
|
return import(pathToFileURL(spec).href) as Promise<typeof import("playwright")>;
|
|
249
306
|
}
|
|
250
307
|
|
|
308
|
+
/** Playwright's Chromium, or what stands in for it in tests. */
|
|
309
|
+
type Chromium = typeof import("playwright").chromium;
|
|
310
|
+
|
|
251
311
|
/**
|
|
252
312
|
* A Playwright worker's whole life: load Playwright, read the request, launch Chromium, answer
|
|
253
313
|
* with what `handle` returns, and close the browser. Any failure exits with its reason on stderr.
|
|
314
|
+
* Where Chromium's sandbox did not start, the answer says why in `unsandboxed`, and is only that
|
|
315
|
+
* when the request's `sandbox` required it, or with why Chromium did not start without it either
|
|
316
|
+
* in `failed`. `chromium` stands in for Playwright's in tests.
|
|
254
317
|
*/
|
|
255
318
|
export async function runPlaywrightWorker<Request, Response>(
|
|
256
319
|
handle: (browser: Browser, request: Request) => Promise<Response>,
|
|
320
|
+
chromium?: Chromium,
|
|
257
321
|
): Promise<void> {
|
|
258
|
-
let
|
|
322
|
+
let launcher: Chromium;
|
|
259
323
|
try {
|
|
260
|
-
|
|
324
|
+
launcher = chromium ?? (await importPlaywright()).chromium;
|
|
261
325
|
} catch {
|
|
262
326
|
exitWorker(`playwright not found; ${PLAYWRIGHT_INSTALL}`);
|
|
263
327
|
}
|
|
264
|
-
const request = await readWorkerRequest<Request>();
|
|
328
|
+
const request = await readWorkerRequest<Request & { sandbox?: unknown }>();
|
|
265
329
|
try {
|
|
266
|
-
|
|
330
|
+
// Anything but "preferred", or nothing, requires the sandbox.
|
|
331
|
+
const sandbox = request.sandbox === "preferred" ? "preferred" : "required";
|
|
332
|
+
const launched = await launchChromium(launcher, sandbox);
|
|
333
|
+
if (!launched.browser) {
|
|
334
|
+
const { browser: _none, ...said } = launched;
|
|
335
|
+
process.stdout.write(`${JSON.stringify(said)}\n`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const { browser, unsandboxed } = launched;
|
|
267
339
|
try {
|
|
268
340
|
const response = await handle(browser, request);
|
|
269
|
-
|
|
341
|
+
const answer = unsandboxed === undefined ? response : { ...response, unsandboxed };
|
|
342
|
+
process.stdout.write(`${JSON.stringify(answer)}\n`);
|
|
270
343
|
} finally {
|
|
271
344
|
await browser.close();
|
|
272
345
|
}
|
|
273
346
|
} catch (error) {
|
|
274
|
-
|
|
347
|
+
// The parent's hint keeps the start of stderr, and Playwright's opens with a command line.
|
|
348
|
+
exitWorker(chromiumFailure(error) ?? error);
|
|
275
349
|
}
|
|
276
350
|
}
|
|
277
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Chromium with its sandbox, or without it and why; or none: where the sandbox was required, or
|
|
354
|
+
* where Chromium did not start without it either.
|
|
355
|
+
*/
|
|
356
|
+
type Launched =
|
|
357
|
+
| { browser: Browser; unsandboxed?: string }
|
|
358
|
+
| { browser?: never; unsandboxed: string; failed?: string };
|
|
359
|
+
|
|
278
360
|
/**
|
|
279
361
|
* Headless Chromium with its sandbox, which Playwright leaves off by default: slide scripts run in
|
|
280
|
-
* it, a ref's included.
|
|
281
|
-
*
|
|
362
|
+
* it, a ref's included. When that launch fails, as in a container running as root, it launches
|
|
363
|
+
* again without one only when `sandbox` is "preferred", and says why in Chromium's words, whether
|
|
364
|
+
* or not they name the sandbox; when that fails too, it says both.
|
|
282
365
|
*/
|
|
283
|
-
async function launchChromium(chromium:
|
|
366
|
+
export async function launchChromium(chromium: Chromium, sandbox: SandboxMode): Promise<Launched> {
|
|
367
|
+
let first: unknown;
|
|
284
368
|
try {
|
|
285
|
-
return await chromium.launch({ headless: true, chromiumSandbox: true });
|
|
286
|
-
} catch {
|
|
287
|
-
|
|
369
|
+
return { browser: await chromium.launch({ headless: true, chromiumSandbox: true }) };
|
|
370
|
+
} catch (error) {
|
|
371
|
+
first = error;
|
|
372
|
+
}
|
|
373
|
+
const unsandboxed = launchFailure(first);
|
|
374
|
+
if (sandbox !== "preferred") {
|
|
375
|
+
return { unsandboxed };
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
return { browser: await chromium.launch({ headless: true }), unsandboxed };
|
|
379
|
+
} catch (second) {
|
|
380
|
+
return { unsandboxed, failed: launchFailure(second) };
|
|
288
381
|
}
|
|
289
382
|
}
|
|
290
383
|
|
|
291
384
|
export async function defaultPlaywrightRunner(
|
|
292
385
|
request: VisualRequest,
|
|
293
|
-
options:
|
|
386
|
+
options: RunnerOptions = {},
|
|
294
387
|
): Promise<VisualResponse | null> {
|
|
295
388
|
if (!playwrightResolved()) {
|
|
296
389
|
return null;
|
|
@@ -298,6 +391,14 @@ export async function defaultPlaywrightRunner(
|
|
|
298
391
|
return spawnRunner(workerCommand(process.env.DEK_PLAYWRIGHT || workerPath()), request, options);
|
|
299
392
|
}
|
|
300
393
|
|
|
394
|
+
/**
|
|
395
|
+
* The default runner for a ref's pages: someone else's slide scripts, which Chromium runs only in
|
|
396
|
+
* its sandbox unless the user set DEK_ALLOW_UNSANDBOXED=1 (ADR 29).
|
|
397
|
+
*/
|
|
398
|
+
export function refPlaywrightRunner(request: VisualRequest): Promise<VisualResponse | null> {
|
|
399
|
+
return defaultPlaywrightRunner(request, { ref: true });
|
|
400
|
+
}
|
|
401
|
+
|
|
301
402
|
function workerPath(): string {
|
|
302
403
|
return moduleFilePath(new URL("./playwright-worker.ts", import.meta.url));
|
|
303
404
|
}
|
|
@@ -305,12 +406,13 @@ function workerPath(): string {
|
|
|
305
406
|
function spawnRunner(
|
|
306
407
|
cmd: string[],
|
|
307
408
|
request: VisualRequest,
|
|
308
|
-
options:
|
|
409
|
+
options: RunnerOptions,
|
|
309
410
|
): Promise<VisualResponse> {
|
|
310
|
-
return
|
|
411
|
+
return runDrawingWorker(cmd, request, (text) => parseVisualResponse(text, request.kind), {
|
|
311
412
|
label: "Playwright worker failed",
|
|
312
413
|
hint: `${PLAYWRIGHT_INSTALL} or check DEK_PLAYWRIGHT`,
|
|
313
414
|
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
415
|
+
...(options.ref ? { ref: true } : {}),
|
|
314
416
|
});
|
|
315
417
|
}
|
|
316
418
|
|
|
@@ -318,13 +420,23 @@ function spawnRunner(
|
|
|
318
420
|
const RESPONSE_PARSERS: {
|
|
319
421
|
[K in keyof VisualResponses]: (fields: Record<string, unknown>) => VisualResponses[K] | null;
|
|
320
422
|
} = {
|
|
321
|
-
pages: ({
|
|
423
|
+
pages: ({
|
|
424
|
+
overflows,
|
|
425
|
+
contrasts,
|
|
426
|
+
drawErrors = [],
|
|
427
|
+
collisions = [],
|
|
428
|
+
fills = [],
|
|
429
|
+
lineBreaks = [],
|
|
430
|
+
misalignments = [],
|
|
431
|
+
}) =>
|
|
322
432
|
isArrayOf(overflows, isOverflowFinding) &&
|
|
323
433
|
isArrayOf(contrasts, isContrastFinding) &&
|
|
324
434
|
isArrayOf(drawErrors, isDrawErrorFinding) &&
|
|
325
435
|
isArrayOf(collisions, isCollisionFinding) &&
|
|
326
|
-
isArrayOf(fills, isFillFinding)
|
|
327
|
-
|
|
436
|
+
isArrayOf(fills, isFillFinding) &&
|
|
437
|
+
isArrayOf(lineBreaks, isLineBreakFinding) &&
|
|
438
|
+
isArrayOf(misalignments, isMisalignmentFinding)
|
|
439
|
+
? { overflows, contrasts, drawErrors, collisions, fills, lineBreaks, misalignments }
|
|
328
440
|
: null,
|
|
329
441
|
pdf: () => ({}),
|
|
330
442
|
morph: () => ({}),
|
|
@@ -401,6 +513,37 @@ function isFillFinding(value: unknown): value is FillFinding {
|
|
|
401
513
|
);
|
|
402
514
|
}
|
|
403
515
|
|
|
516
|
+
function isLineBreakFinding(value: unknown): value is LineBreakFinding {
|
|
517
|
+
if (!isFinding(value)) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
const { kind, lines, phrased, tail, wordBreak, textWrap } = value;
|
|
521
|
+
return (
|
|
522
|
+
isArrayOf(lines, isString) &&
|
|
523
|
+
isString(wordBreak) &&
|
|
524
|
+
isString(textWrap) &&
|
|
525
|
+
((kind === "split" && isArrayOf(phrased, isString)) || (kind === "short" && isString(tail)))
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const ALIGNED_EDGES: ReadonlySet<unknown> = new Set<AlignedEdge>([
|
|
530
|
+
"top",
|
|
531
|
+
"middle",
|
|
532
|
+
"bottom",
|
|
533
|
+
"left",
|
|
534
|
+
"center",
|
|
535
|
+
"right",
|
|
536
|
+
]);
|
|
537
|
+
|
|
538
|
+
function isMisalignmentFinding(value: unknown): value is MisalignmentFinding {
|
|
539
|
+
// Both name two elements by their boxes and their texts, as a collision does.
|
|
540
|
+
if (!isCollisionFinding(value)) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
const { edge, of, by } = value as Record<string, unknown>;
|
|
544
|
+
return ALIGNED_EDGES.has(edge) && (of === "box" || of === "text") && isNumber(by);
|
|
545
|
+
}
|
|
546
|
+
|
|
404
547
|
function isDrawErrorFinding(value: unknown): value is DrawErrorFinding {
|
|
405
548
|
if (!value || typeof value !== "object") {
|
|
406
549
|
return false;
|
|
@@ -438,14 +581,15 @@ function isContrastFinding(value: unknown): value is ContrastFinding {
|
|
|
438
581
|
if (!isFinding(value)) {
|
|
439
582
|
return false;
|
|
440
583
|
}
|
|
441
|
-
const { ratio, fg, bg, fontSize, fontWeight, origin } = value;
|
|
584
|
+
const { ratio, fg, bg, fontSize, fontWeight, origin, onlyWithSlideCss } = value;
|
|
442
585
|
return (
|
|
443
586
|
isNumber(ratio) &&
|
|
444
587
|
isString(fg) &&
|
|
445
588
|
isString(bg) &&
|
|
446
589
|
isNumber(fontSize) &&
|
|
447
590
|
isNumber(fontWeight) &&
|
|
448
|
-
(origin === undefined || origin === "theme" || origin === "slide" || origin === "script")
|
|
591
|
+
(origin === undefined || origin === "theme" || origin === "slide" || origin === "script") &&
|
|
592
|
+
(onlyWithSlideCss === undefined || onlyWithSlideCss === true)
|
|
449
593
|
);
|
|
450
594
|
}
|
|
451
595
|
|
|
@@ -453,34 +597,125 @@ function isPptxSlide(value: unknown): value is PptxResponse["slides"][number] {
|
|
|
453
597
|
if (!value || typeof value !== "object") {
|
|
454
598
|
return false;
|
|
455
599
|
}
|
|
456
|
-
const { slug, description,
|
|
457
|
-
|
|
600
|
+
const { slug, description, layers, picture, background, theme } = value as Record<
|
|
601
|
+
string,
|
|
602
|
+
unknown
|
|
603
|
+
>;
|
|
604
|
+
return (
|
|
605
|
+
isString(slug) &&
|
|
606
|
+
isString(description) &&
|
|
607
|
+
isArrayOf(layers, isPptxLayer) &&
|
|
608
|
+
(picture === undefined || typeof picture === "boolean") &&
|
|
609
|
+
(background === undefined || isFill(background)) &&
|
|
610
|
+
(theme === undefined || (Boolean(theme) && typeof theme === "object"))
|
|
611
|
+
);
|
|
458
612
|
}
|
|
459
613
|
|
|
460
|
-
function
|
|
614
|
+
function isFill(value: unknown): value is PptxFill {
|
|
461
615
|
if (!value || typeof value !== "object") {
|
|
462
616
|
return false;
|
|
463
617
|
}
|
|
464
|
-
const
|
|
618
|
+
const fill = value as Record<string, unknown>;
|
|
619
|
+
return fill.kind === "solid"
|
|
620
|
+
? isPaint(fill.paint)
|
|
621
|
+
: fill.kind === "linear" &&
|
|
622
|
+
isNumber(fill.angle) &&
|
|
623
|
+
isArrayOf(
|
|
624
|
+
fill.stops,
|
|
625
|
+
(stop: unknown): stop is object =>
|
|
626
|
+
Boolean(stop) &&
|
|
627
|
+
typeof stop === "object" &&
|
|
628
|
+
isNumber((stop as Record<string, unknown>).at) &&
|
|
629
|
+
isPaint((stop as Record<string, unknown>).paint),
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function isPptxLayer(value: unknown): value is PptxLayer<PptxPictureFile> {
|
|
634
|
+
if (!value || typeof value !== "object") {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
const layer = value as Record<string, unknown>;
|
|
638
|
+
const at = ["x", "y", "width", "height"].every((key) => isNumber(layer[key]));
|
|
639
|
+
switch (layer.kind) {
|
|
640
|
+
case "text":
|
|
641
|
+
return isPptxBox(layer);
|
|
642
|
+
case "rule":
|
|
643
|
+
return (
|
|
644
|
+
["x1", "y1", "x2", "y2", "width"].every((key) => isNumber(layer[key])) &&
|
|
645
|
+
isPaint(layer.paint)
|
|
646
|
+
);
|
|
647
|
+
case "picture":
|
|
648
|
+
return (
|
|
649
|
+
at &&
|
|
650
|
+
isString(layer.png) &&
|
|
651
|
+
isString(layer.description) &&
|
|
652
|
+
(layer.svg === undefined || isString(layer.svg))
|
|
653
|
+
);
|
|
654
|
+
case "shape":
|
|
655
|
+
return (
|
|
656
|
+
at &&
|
|
657
|
+
isNumber(layer.radius) &&
|
|
658
|
+
(layer.fill === undefined || isPaint(layer.fill)) &&
|
|
659
|
+
isArrayOf(layer.layers, isPptxLayer) &&
|
|
660
|
+
(layer.text === undefined || isShapeText(layer.text))
|
|
661
|
+
);
|
|
662
|
+
default:
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function isPaint(value: unknown): boolean {
|
|
668
|
+
if (!value || typeof value !== "object") {
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
const { color, alpha } = value as Record<string, unknown>;
|
|
672
|
+
return isArrayOf(color, isNumber) && isNumber(alpha);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function isShapeText(value: unknown): boolean {
|
|
676
|
+
if (!value || typeof value !== "object") {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
const { inset, paragraphs } = value as Record<string, unknown>;
|
|
465
680
|
return (
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
681
|
+
Boolean(inset) &&
|
|
682
|
+
typeof inset === "object" &&
|
|
683
|
+
isArrayOf(paragraphs, (paragraph: unknown): paragraph is object => {
|
|
684
|
+
if (!paragraph || typeof paragraph !== "object") {
|
|
469
685
|
return false;
|
|
470
686
|
}
|
|
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
|
-
);
|
|
687
|
+
const { runs, lineHeight } = paragraph as Record<string, unknown>;
|
|
688
|
+
return isNumber(lineHeight) && isArrayOf(runs, isPptxRun);
|
|
480
689
|
})
|
|
481
690
|
);
|
|
482
691
|
}
|
|
483
692
|
|
|
693
|
+
function isPptxBox(value: unknown): value is PptxTextBox {
|
|
694
|
+
if (!value || typeof value !== "object") {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
const box = value as Record<string, unknown>;
|
|
698
|
+
return (
|
|
699
|
+
["x", "y", "width", "height", "lineHeight"].every((key) => isNumber(box[key])) &&
|
|
700
|
+
isArrayOf(box.runs, isPptxRun)
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function isPptxRun(run: unknown): run is PptxTextBox["runs"][number] {
|
|
705
|
+
if (!run || typeof run !== "object") {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
const { text, size, alpha, letterSpacing, color, font } = run as Record<string, unknown>;
|
|
709
|
+
return (
|
|
710
|
+
isString(text) &&
|
|
711
|
+
isNumber(size) &&
|
|
712
|
+
isNumber(alpha) &&
|
|
713
|
+
isNumber(letterSpacing) &&
|
|
714
|
+
isArrayOf(color, isNumber) &&
|
|
715
|
+
(font === undefined || isString(font))
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
484
719
|
function isMotionBeat(value: unknown): value is MotionBeat {
|
|
485
720
|
if (!value || typeof value !== "object") {
|
|
486
721
|
return false;
|