@hajimism/dek 0.1.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/LICENSE +21 -0
- package/README.ja.md +75 -0
- package/README.md +75 -0
- package/package.json +65 -0
- package/src/cli/address.ts +53 -0
- package/src/cli/build.ts +85 -0
- package/src/cli/check.ts +97 -0
- package/src/cli/commands.ts +851 -0
- package/src/cli/contract.ts +444 -0
- package/src/cli/cues.ts +18 -0
- package/src/cli/files.ts +309 -0
- package/src/cli/flags.ts +295 -0
- package/src/cli/format.ts +156 -0
- package/src/cli/goto.ts +96 -0
- package/src/cli/init.ts +136 -0
- package/src/cli/keep-alive.ts +72 -0
- package/src/cli/lint.ts +84 -0
- package/src/cli/ls.ts +119 -0
- package/src/cli/main.ts +231 -0
- package/src/cli/marks.ts +16 -0
- package/src/cli/mv.ts +37 -0
- package/src/cli/new.ts +64 -0
- package/src/cli/pdf.ts +16 -0
- package/src/cli/qr.ts +466 -0
- package/src/cli/ref.ts +165 -0
- package/src/cli/rehearse.ts +42 -0
- package/src/cli/result.ts +161 -0
- package/src/cli/scope.ts +308 -0
- package/src/cli/serve.ts +125 -0
- package/src/cli/shot.ts +134 -0
- package/src/cli/show.ts +103 -0
- package/src/cli/sync.ts +37 -0
- package/src/cli/text.ts +479 -0
- package/src/cli/theme.ts +56 -0
- package/src/cli/tty.ts +81 -0
- package/src/cli/usage.ts +198 -0
- package/src/cli/video.ts +17 -0
- package/src/cli/voice.ts +113 -0
- package/src/cli.ts +4 -0
- package/src/core/assets.ts +239 -0
- package/src/core/build.ts +77 -0
- package/src/core/bundled-theme.ts +15 -0
- package/src/core/capture-go.ts +72 -0
- package/src/core/chrome.ts +128 -0
- package/src/core/config-keys.ts +82 -0
- package/src/core/config.ts +95 -0
- package/src/core/css-scan.ts +158 -0
- package/src/core/css-transform.ts +140 -0
- package/src/core/css-value.ts +127 -0
- package/src/core/css.ts +545 -0
- package/src/core/cue.ts +187 -0
- package/src/core/deck-paths.ts +34 -0
- package/src/core/diagnostic.ts +114 -0
- package/src/core/document.ts +218 -0
- package/src/core/error.ts +31 -0
- package/src/core/escape.ts +16 -0
- package/src/core/fill.ts +100 -0
- package/src/core/finish-beat.ts +30 -0
- package/src/core/github.ts +234 -0
- package/src/core/html-scan.ts +253 -0
- package/src/core/html.ts +234 -0
- package/src/core/in-page-go.ts +128 -0
- package/src/core/lines.ts +43 -0
- package/src/core/lint/asset-refs.ts +109 -0
- package/src/core/lint/context.ts +116 -0
- package/src/core/lint/files.ts +117 -0
- package/src/core/lint/keys.ts +48 -0
- package/src/core/lint/morph.ts +91 -0
- package/src/core/lint/script.ts +102 -0
- package/src/core/lint/shared-styles.ts +165 -0
- package/src/core/lint/slide-html.ts +466 -0
- package/src/core/lint/slide-script.ts +179 -0
- package/src/core/lint/theme-css.ts +295 -0
- package/src/core/lint/timing.ts +66 -0
- package/src/core/lint/tokens.ts +520 -0
- package/src/core/lint/voice.ts +76 -0
- package/src/core/lint.ts +137 -0
- package/src/core/live-protocol.ts +130 -0
- package/src/core/marks.ts +198 -0
- package/src/core/mv.ts +293 -0
- package/src/core/ogp.ts +99 -0
- package/src/core/optional.ts +24 -0
- package/src/core/overflow.ts +220 -0
- package/src/core/page-pool.ts +53 -0
- package/src/core/page.ts +66 -0
- package/src/core/parse.ts +307 -0
- package/src/core/path.ts +58 -0
- package/src/core/pdf.ts +71 -0
- package/src/core/playwright-module.d.ts +43 -0
- package/src/core/playwright-motion.ts +42 -0
- package/src/core/playwright-sheet.ts +32 -0
- package/src/core/playwright-visual.ts +391 -0
- package/src/core/playwright-worker.ts +5 -0
- package/src/core/playwright.ts +431 -0
- package/src/core/position.ts +146 -0
- package/src/core/presenter-state.ts +43 -0
- package/src/core/presenter.ts +131 -0
- package/src/core/prose.ts +16 -0
- package/src/core/pseudo-text.ts +67 -0
- package/src/core/rail-width.ts +4 -0
- package/src/core/ref-name.ts +110 -0
- package/src/core/ref.ts +238 -0
- package/src/core/resolve.ts +228 -0
- package/src/core/rumdl.ts +99 -0
- package/src/core/safe-fs.ts +196 -0
- package/src/core/sarif.ts +135 -0
- package/src/core/schema.ts +89 -0
- package/src/core/script-lines.ts +53 -0
- package/src/core/sheet.ts +223 -0
- package/src/core/shot/morph.ts +80 -0
- package/src/core/shot/motion.ts +187 -0
- package/src/core/shot/still.ts +216 -0
- package/src/core/shot-cache.ts +140 -0
- package/src/core/size.ts +11 -0
- package/src/core/skeleton-record.ts +51 -0
- package/src/core/skeleton.ts +89 -0
- package/src/core/slide-draw.ts +165 -0
- package/src/core/slide-eval-worker.ts +58 -0
- package/src/core/slide-measure.ts +272 -0
- package/src/core/slide-script-eval.ts +258 -0
- package/src/core/slide-script.ts +252 -0
- package/src/core/spawn.ts +128 -0
- package/src/core/step.ts +94 -0
- package/src/core/still-page.ts +70 -0
- package/src/core/suggest.ts +33 -0
- package/src/core/sync.ts +316 -0
- package/src/core/text-contrast.ts +466 -0
- package/src/core/theme-excerpt.ts +187 -0
- package/src/core/theme-facts.ts +51 -0
- package/src/core/timeline.ts +216 -0
- package/src/core/timing.ts +88 -0
- package/src/core/toml-keys.ts +349 -0
- package/src/core/url-attributes.ts +103 -0
- package/src/core/video-document.ts +15 -0
- package/src/core/visual-cache.ts +100 -0
- package/src/core/visual.ts +558 -0
- package/src/core/voice.ts +321 -0
- package/src/core/zod.ts +79 -0
- package/src/runtime/browser.ts +203 -0
- package/src/runtime/clone.ts +52 -0
- package/src/runtime/deck-control.ts +43 -0
- package/src/runtime/fit.ts +32 -0
- package/src/runtime/go.ts +56 -0
- package/src/runtime/input.ts +128 -0
- package/src/runtime/laser-view.ts +110 -0
- package/src/runtime/laser.ts +145 -0
- package/src/runtime/live-host.ts +33 -0
- package/src/runtime/live-reload.ts +29 -0
- package/src/runtime/live.ts +88 -0
- package/src/runtime/marks-view.ts +74 -0
- package/src/runtime/motion.ts +113 -0
- package/src/runtime/navigator.ts +189 -0
- package/src/runtime/player.ts +28 -0
- package/src/runtime/presenter-view.ts +289 -0
- package/src/runtime/presenter.ts +52 -0
- package/src/runtime/rail-view.ts +229 -0
- package/src/runtime/rail.ts +52 -0
- package/src/runtime/rehearse.ts +264 -0
- package/src/runtime/routes.ts +142 -0
- package/src/runtime/settle.ts +45 -0
- package/src/runtime/slide.d.ts +28 -0
- package/src/runtime/socket.ts +143 -0
- package/src/runtime/stage.ts +259 -0
- package/src/runtime/step.ts +139 -0
- package/src/runtime/window.ts +38 -0
- package/src/server/auth.ts +210 -0
- package/src/server/deck-snapshot.ts +146 -0
- package/src/server/dev.ts +251 -0
- package/src/server/hub.ts +104 -0
- package/src/server/lan.ts +51 -0
- package/src/server/lock.ts +97 -0
- package/src/server/pages.ts +71 -0
- package/src/server/pairing.ts +40 -0
- package/src/server/project-watch.ts +142 -0
- package/src/server/rooms.ts +133 -0
- package/src/server/routes.ts +286 -0
- package/src/server/serial.ts +25 -0
- package/src/server/watch.ts +308 -0
- package/src/theme/default.css +175 -0
- package/src/video/bake.ts +152 -0
- package/src/video/mux.ts +103 -0
- package/src/video/recorder.ts +115 -0
- package/src/video/sidecar.ts +60 -0
- package/src/video/worker.ts +51 -0
- package/src/voice/engine.ts +177 -0
- package/src/voice/synth.ts +127 -0
- package/src/voice/wav.ts +115 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import type { Browser, BrowserContext, Page } from "playwright";
|
|
2
|
+
import { freezeTransition, loadVideoDoc } from "./capture-go.ts";
|
|
3
|
+
import { measureFill } from "./fill.ts";
|
|
4
|
+
import { finishBeat } from "./finish-beat.ts";
|
|
5
|
+
import {
|
|
6
|
+
crossesEdge,
|
|
7
|
+
findClippedText,
|
|
8
|
+
findCollisions,
|
|
9
|
+
findOverflows,
|
|
10
|
+
type OverflowOrigin,
|
|
11
|
+
samePlace,
|
|
12
|
+
} from "./overflow.ts";
|
|
13
|
+
import { visitInPages } from "./page-pool.ts";
|
|
14
|
+
import type {
|
|
15
|
+
ContrastFinding,
|
|
16
|
+
ContrastOrigin,
|
|
17
|
+
DoneResponse,
|
|
18
|
+
MorphRequest,
|
|
19
|
+
MotionRequest,
|
|
20
|
+
MotionResponse,
|
|
21
|
+
PagesRequest,
|
|
22
|
+
PagesResponse,
|
|
23
|
+
PdfRequest,
|
|
24
|
+
ResponseTo,
|
|
25
|
+
VisualPage,
|
|
26
|
+
VisualRequest,
|
|
27
|
+
} from "./playwright.ts";
|
|
28
|
+
import { captureMotion } from "./playwright-motion.ts";
|
|
29
|
+
import { renderSheets } from "./playwright-sheet.ts";
|
|
30
|
+
import { pseudoTextBoxes } from "./pseudo-text.ts";
|
|
31
|
+
import { motionSheets } from "./sheet.ts";
|
|
32
|
+
import {
|
|
33
|
+
type Box,
|
|
34
|
+
measureSlideInPage,
|
|
35
|
+
SLIDE_CSS_ID,
|
|
36
|
+
type SlideMeasure,
|
|
37
|
+
unmarkPseudoTextsInPage,
|
|
38
|
+
} from "./slide-measure.ts";
|
|
39
|
+
import { contrastThreshold, measureGlyphBoxes, measurePageTextContrasts } from "./text-contrast.ts";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Answers a visual request in a running browser: what the Playwright worker
|
|
43
|
+
* does between launching Chromium and writing its JSON. Findings keep the
|
|
44
|
+
* order of the request's pages.
|
|
45
|
+
*/
|
|
46
|
+
export async function runVisualRequest<R extends VisualRequest>(
|
|
47
|
+
browser: Browser,
|
|
48
|
+
request: R,
|
|
49
|
+
): Promise<ResponseTo<R>> {
|
|
50
|
+
const context = await browser.newContext({ viewport: request.viewport });
|
|
51
|
+
try {
|
|
52
|
+
return (await answer(context, request)) as ResponseTo<R>;
|
|
53
|
+
} finally {
|
|
54
|
+
await context.close();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function answer(context: BrowserContext, request: VisualRequest) {
|
|
59
|
+
switch (request.kind) {
|
|
60
|
+
case "pages":
|
|
61
|
+
return visitPages(context, request);
|
|
62
|
+
case "pdf":
|
|
63
|
+
return printPdf(context, request);
|
|
64
|
+
case "morph":
|
|
65
|
+
return shootMorph(context, request);
|
|
66
|
+
case "motion":
|
|
67
|
+
return shootMotion(context, request);
|
|
68
|
+
default: {
|
|
69
|
+
const unknown: never = request;
|
|
70
|
+
throw new Error(`unknown visual request ${JSON.stringify(unknown)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The whole print document as one PDF, a page per slide. */
|
|
76
|
+
async function printPdf(context: BrowserContext, request: PdfRequest): Promise<DoneResponse> {
|
|
77
|
+
const page = await context.newPage();
|
|
78
|
+
await page.setContent(request.html, { waitUntil: "load" });
|
|
79
|
+
await page.pdf({
|
|
80
|
+
path: request.pdfPath,
|
|
81
|
+
width: `${request.viewport.width}px`,
|
|
82
|
+
height: `${request.viewport.height}px`,
|
|
83
|
+
printBackground: true,
|
|
84
|
+
margin: { top: "0", right: "0", bottom: "0", left: "0" },
|
|
85
|
+
});
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The move between two slides, frozen at a moment and shot. */
|
|
90
|
+
async function shootMorph(context: BrowserContext, request: MorphRequest): Promise<DoneResponse> {
|
|
91
|
+
const page = await context.newPage();
|
|
92
|
+
await loadVideoDoc(page, request.html);
|
|
93
|
+
await freezeTransition(page, request.morph);
|
|
94
|
+
await page.screenshot({ path: request.screenshotPath, fullPage: false });
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A slide's beats in motion, then the sheets that tile them. */
|
|
99
|
+
async function shootMotion(
|
|
100
|
+
context: BrowserContext,
|
|
101
|
+
request: MotionRequest,
|
|
102
|
+
): Promise<MotionResponse> {
|
|
103
|
+
const motion = await captureMotion(await context.newPage(), request.html, request.motion);
|
|
104
|
+
const sheets = await renderSheets(
|
|
105
|
+
motionSheets(motion, {
|
|
106
|
+
slide: request.viewport,
|
|
107
|
+
title: request.motion.title,
|
|
108
|
+
dir: request.motion.dir,
|
|
109
|
+
}),
|
|
110
|
+
() => context.newPage(),
|
|
111
|
+
);
|
|
112
|
+
return { motion, sheets };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Every page measured and shot as the request asks; then its sheets. */
|
|
116
|
+
async function visitPages(context: BrowserContext, request: PagesRequest): Promise<PagesResponse> {
|
|
117
|
+
const found = await visitInPages(
|
|
118
|
+
request.pages,
|
|
119
|
+
() => context.newPage(),
|
|
120
|
+
(page, pageReq) => visitPage(page, pageReq, request),
|
|
121
|
+
);
|
|
122
|
+
// Sheets come last: they tile what the pages above just wrote.
|
|
123
|
+
if (request.sheets && request.sheets.length > 0) {
|
|
124
|
+
await renderSheets(request.sheets, () => context.newPage());
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
overflows: found.flatMap((page) => page.overflows),
|
|
128
|
+
contrasts: found.flatMap((page) => page.contrasts),
|
|
129
|
+
drawErrors: found.flatMap((page) => page.drawErrors ?? []),
|
|
130
|
+
collisions: found.flatMap((page) => page.collisions ?? []),
|
|
131
|
+
fills: found.flatMap((page) => page.fills ?? []),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A contrast finding below its threshold, with the key of the text it came from. */
|
|
136
|
+
type Failing = { finding: ContrastFinding; key: string };
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* An overflow found on the page, with the index of the element that crosses the edge: the
|
|
140
|
+
* frame's, or an ancestor's that cuts the element's text off.
|
|
141
|
+
*/
|
|
142
|
+
type Crossing = { finding: PagesResponse["overflows"][number]; element: number; frame: Box };
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* One text the page draws: an element's own, or one a pseudo-element draws. `key` finds the same
|
|
146
|
+
* text again when the page is measured a second time.
|
|
147
|
+
*/
|
|
148
|
+
type PageText = {
|
|
149
|
+
key: string;
|
|
150
|
+
/** For text a pseudo-element draws: its host's mark and which pseudo-element it is. */
|
|
151
|
+
pseudo?: { host: string; pseudo: "before" | "after" };
|
|
152
|
+
rects: Box[];
|
|
153
|
+
opacity: number;
|
|
154
|
+
fontSize: number;
|
|
155
|
+
fontWeight: number;
|
|
156
|
+
box: string;
|
|
157
|
+
text?: string;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** Every text on the page, each measured once: an ancestor's sample would repeat its children's. */
|
|
161
|
+
async function pageTexts(page: Page, measured: SlideMeasure): Promise<PageText[]> {
|
|
162
|
+
const own = measured.elements.flatMap((element, index): PageText[] =>
|
|
163
|
+
element.ownText && !element.decorative
|
|
164
|
+
? [
|
|
165
|
+
{
|
|
166
|
+
key: String(index),
|
|
167
|
+
rects: element.textRects,
|
|
168
|
+
opacity: element.opacity,
|
|
169
|
+
fontSize: element.fontSize,
|
|
170
|
+
fontWeight: element.fontWeight,
|
|
171
|
+
box: element.box,
|
|
172
|
+
...(element.text ? { text: element.text } : {}),
|
|
173
|
+
},
|
|
174
|
+
]
|
|
175
|
+
: [],
|
|
176
|
+
);
|
|
177
|
+
if (measured.pseudoTexts.length === 0) {
|
|
178
|
+
return own;
|
|
179
|
+
}
|
|
180
|
+
const boxes = await pseudoTextBoxes(page);
|
|
181
|
+
const pseudo = measured.pseudoTexts.flatMap(({ host, pseudo, ...text }): PageText[] => {
|
|
182
|
+
const key = `${host}::${pseudo}`;
|
|
183
|
+
const rect = boxes.get(key);
|
|
184
|
+
return rect ? [{ key, pseudo: { host, pseudo }, rects: [rect], ...text }] : [];
|
|
185
|
+
});
|
|
186
|
+
return [...own, ...pseudo];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The texts drawn over each other. Each text in a pair the boxes suggest is looked at again as it
|
|
191
|
+
* shows, drawn alone. A pseudo-element's, whose box can be far wider than its words as a
|
|
192
|
+
* full-width running head's is, is looked at with the box its glyphs cover. One none of whose
|
|
193
|
+
* glyphs shows was replaced when the text it crosses takes its place, as a count-up's next number
|
|
194
|
+
* does, and collides with nothing; under anything else, such as a callout laid over a label, the
|
|
195
|
+
* audience loses it, and it collides where it is laid out.
|
|
196
|
+
*/
|
|
197
|
+
async function textCollisions(page: Page, texts: PageText[]): Promise<Array<[PageText, PageText]>> {
|
|
198
|
+
const candidates = findCollisions(texts);
|
|
199
|
+
const paired = [...new Set(candidates.flat())];
|
|
200
|
+
if (paired.length === 0) {
|
|
201
|
+
return candidates;
|
|
202
|
+
}
|
|
203
|
+
const glyphs = await measureGlyphBoxes(
|
|
204
|
+
page,
|
|
205
|
+
paired.map(({ key, pseudo, rects }) =>
|
|
206
|
+
pseudo ? { ...pseudo, rects } : { element: Number(key), rects },
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
const glyphOf = new Map(paired.map((text, at) => [text, glyphs[at]]));
|
|
210
|
+
const drawn = (text: PageText): PageText => {
|
|
211
|
+
const glyph = glyphOf.get(text);
|
|
212
|
+
return glyph && text.pseudo ? { ...text, rects: [glyph] } : text;
|
|
213
|
+
};
|
|
214
|
+
return candidates.filter(([a, b]) =>
|
|
215
|
+
!glyphOf.get(a) || !glyphOf.get(b)
|
|
216
|
+
? !samePlace(a, b)
|
|
217
|
+
: findCollisions([drawn(a), drawn(b)]).length > 0,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** How each text measures against what it is drawn on, in the order of `texts`. */
|
|
222
|
+
function textContrasts(page: Page, texts: PageText[]) {
|
|
223
|
+
return measurePageTextContrasts(
|
|
224
|
+
page,
|
|
225
|
+
texts.map(({ rects, opacity }) => ({ rects, opacity })),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Measures one page as `actions` asks, and shoots it when it names a `screenshotPath`. */
|
|
230
|
+
async function visitPage(
|
|
231
|
+
page: Page,
|
|
232
|
+
pageReq: VisualPage,
|
|
233
|
+
{ actions, viewport }: Pick<PagesRequest, "actions" | "viewport">,
|
|
234
|
+
): Promise<PagesResponse> {
|
|
235
|
+
const response: PagesResponse = { overflows: [], contrasts: [], collisions: [], fills: [] };
|
|
236
|
+
const failing: Failing[] = [];
|
|
237
|
+
const crossing: Crossing[] = [];
|
|
238
|
+
await page.setContent(pageReq.html, { waitUntil: "load" });
|
|
239
|
+
const { slug, step } = pageReq;
|
|
240
|
+
response.drawErrors = await page.evaluate(() => window.__dekDrawErrors ?? []);
|
|
241
|
+
if (actions.length > 0) {
|
|
242
|
+
const measured = await page.evaluate(measureSlideInPage);
|
|
243
|
+
const texts = await pageTexts(page, measured);
|
|
244
|
+
// The audience sees the frame: a slide its CSS makes taller or scales up is cut to it.
|
|
245
|
+
const frame = measured.slideBox && {
|
|
246
|
+
left: Math.max(measured.slideBox.left, 0),
|
|
247
|
+
top: Math.max(measured.slideBox.top, 0),
|
|
248
|
+
right: Math.min(measured.slideBox.right, viewport.width),
|
|
249
|
+
bottom: Math.min(measured.slideBox.bottom, viewport.height),
|
|
250
|
+
};
|
|
251
|
+
if (actions.includes("fill") && frame) {
|
|
252
|
+
response.fills?.push({ slug, step, ...measureFill(frame, measured.elements) });
|
|
253
|
+
}
|
|
254
|
+
if (actions.includes("overflow") && frame) {
|
|
255
|
+
for (const { element, ...overflow } of [
|
|
256
|
+
...findOverflows(frame, measured.elements),
|
|
257
|
+
...findClippedText(measured.elements),
|
|
258
|
+
]) {
|
|
259
|
+
const finding = { slug, step, ...overflow };
|
|
260
|
+
response.overflows.push(finding);
|
|
261
|
+
crossing.push({ finding, element, frame });
|
|
262
|
+
}
|
|
263
|
+
for (const [one, other] of await textCollisions(page, texts)) {
|
|
264
|
+
response.collisions?.push({
|
|
265
|
+
slug,
|
|
266
|
+
step,
|
|
267
|
+
box: one.box,
|
|
268
|
+
...(one.text ? { text: one.text } : {}),
|
|
269
|
+
other: other.box,
|
|
270
|
+
...(other.text ? { otherText: other.text } : {}),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (actions.includes("contrast")) {
|
|
275
|
+
const measuredTexts = await textContrasts(page, texts);
|
|
276
|
+
texts.forEach((text, at) => {
|
|
277
|
+
const contrast = measuredTexts[at];
|
|
278
|
+
if (!contrast) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const finding: ContrastFinding = {
|
|
282
|
+
slug,
|
|
283
|
+
step,
|
|
284
|
+
ratio: contrast.ratio,
|
|
285
|
+
fontSize: text.fontSize,
|
|
286
|
+
fontWeight: text.fontWeight,
|
|
287
|
+
box: text.box,
|
|
288
|
+
...(text.text ? { text: text.text } : {}),
|
|
289
|
+
fg: `rgb(${contrast.fg.join(", ")})`,
|
|
290
|
+
bg: `rgb(${contrast.bg.join(", ")})`,
|
|
291
|
+
};
|
|
292
|
+
response.contrasts.push(finding);
|
|
293
|
+
if (finding.ratio < contrastThreshold(finding)) {
|
|
294
|
+
failing.push({ finding, key: text.key });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (pageReq.screenshotPath) {
|
|
300
|
+
await page.screenshot({ path: pageReq.screenshotPath, fullPage: false });
|
|
301
|
+
}
|
|
302
|
+
// After the shot: what follows takes the page apart, and it is not put back.
|
|
303
|
+
await attributeFindings(page, { failing, crossing });
|
|
304
|
+
await page.evaluate(unmarkPseudoTextsInPage);
|
|
305
|
+
return response;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Says what brought each finding about, by taking the page apart one layer at a time and
|
|
310
|
+
* measuring again: first what the slide's script drew, then the slide's own CSS. A text that
|
|
311
|
+
* clears its threshold, or an element that no longer crosses an edge, once a layer is gone is
|
|
312
|
+
* that layer's to fix. With both gone, a text that still fails is the theme's, and an element that
|
|
313
|
+
* still overflows is its content's: too much for the theme's sizes. The page is not put back.
|
|
314
|
+
*/
|
|
315
|
+
async function attributeFindings(
|
|
316
|
+
page: Page,
|
|
317
|
+
pending: { failing: Failing[]; crossing: Crossing[] },
|
|
318
|
+
): Promise<void> {
|
|
319
|
+
let { failing, crossing } = pending;
|
|
320
|
+
for (const layer of LAYERS) {
|
|
321
|
+
if (failing.length === 0 && crossing.length === 0) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!(await page.evaluate(layer.strip, SLIDE_CSS_ID))) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
// Transitions and animations the change sets off would otherwise be caught midway.
|
|
328
|
+
await page.evaluate(finishBeat);
|
|
329
|
+
const measured = await page.evaluate(measureSlideInPage);
|
|
330
|
+
crossing = crossing.filter(({ finding, element, frame }) => {
|
|
331
|
+
const now = measured.elements[element];
|
|
332
|
+
const still =
|
|
333
|
+
now !== undefined &&
|
|
334
|
+
(finding.clip === undefined ? crossesEdge(frame, now) : findClippedText([now]).length > 0);
|
|
335
|
+
if (now && !still) {
|
|
336
|
+
finding.origin = layer.origin;
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
return true;
|
|
340
|
+
});
|
|
341
|
+
if (failing.length === 0) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const texts = await pageTexts(page, measured);
|
|
345
|
+
const contrasts = await textContrasts(page, texts);
|
|
346
|
+
const now = new Map(texts.map((text, at) => [text.key, { text, contrast: contrasts[at] }]));
|
|
347
|
+
failing = failing.filter(({ finding, key }) => {
|
|
348
|
+
const measuredText = now.get(key);
|
|
349
|
+
if (
|
|
350
|
+
measuredText?.contrast &&
|
|
351
|
+
measuredText.contrast.ratio >= contrastThreshold(measuredText.text)
|
|
352
|
+
) {
|
|
353
|
+
finding.origin = layer.origin;
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
return true;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
for (const { finding } of failing) {
|
|
360
|
+
finding.origin = "theme";
|
|
361
|
+
}
|
|
362
|
+
for (const { finding } of crossing) {
|
|
363
|
+
finding.origin = "content";
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* What a slide adds over the theme, outermost first, each with how to take it away in the page;
|
|
369
|
+
* `strip` says whether there was anything to take.
|
|
370
|
+
*/
|
|
371
|
+
const LAYERS: Array<{
|
|
372
|
+
origin: Exclude<ContrastOrigin, "theme"> & OverflowOrigin;
|
|
373
|
+
strip: (slideCssId: string) => boolean;
|
|
374
|
+
}> = [
|
|
375
|
+
{
|
|
376
|
+
origin: "script",
|
|
377
|
+
strip: () => {
|
|
378
|
+
const undo = window.__dekUndoDraw;
|
|
379
|
+
undo?.();
|
|
380
|
+
return undo !== undefined;
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
origin: "slide",
|
|
385
|
+
strip: (id) => {
|
|
386
|
+
const style = document.getElementById(id);
|
|
387
|
+
style?.remove();
|
|
388
|
+
return style !== null;
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
];
|