markswift 0.0.0-stage → 2.0.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 (146) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/LICENSE +21 -0
  3. package/README.md +592 -2
  4. package/dist/cli-jdrbzqfa.js +2975 -0
  5. package/dist/cli-qchh0m4d.js +14041 -0
  6. package/dist/cli.js +637 -0
  7. package/dist/compiler/index.js +20 -0
  8. package/dist/index.js +22 -0
  9. package/dist/types/analysis/alignment.d.ts +12 -0
  10. package/dist/types/analysis/bands.d.ts +53 -0
  11. package/dist/types/analysis/breakpoints.d.ts +22 -0
  12. package/dist/types/analysis/class-hints.d.ts +21 -0
  13. package/dist/types/analysis/context.d.ts +49 -0
  14. package/dist/types/analysis/controls.d.ts +97 -0
  15. package/dist/types/analysis/flex-row.d.ts +14 -0
  16. package/dist/types/analysis/flex-sizing.d.ts +5 -0
  17. package/dist/types/analysis/flow-layout.d.ts +18 -0
  18. package/dist/types/analysis/generated.d.ts +16 -0
  19. package/dist/types/analysis/geometry.d.ts +50 -0
  20. package/dist/types/analysis/grid-tracks.d.ts +44 -0
  21. package/dist/types/analysis/grid.d.ts +5 -0
  22. package/dist/types/analysis/infer.d.ts +18 -0
  23. package/dist/types/analysis/invisible.d.ts +6 -0
  24. package/dist/types/analysis/layers.d.ts +15 -0
  25. package/dist/types/analysis/measured-layout.d.ts +6 -0
  26. package/dist/types/analysis/motion.d.ts +29 -0
  27. package/dist/types/analysis/naming.d.ts +6 -0
  28. package/dist/types/analysis/node-base.d.ts +13 -0
  29. package/dist/types/analysis/size-constraints.d.ts +5 -0
  30. package/dist/types/analysis/sizing.d.ts +24 -0
  31. package/dist/types/analysis/style-background.d.ts +24 -0
  32. package/dist/types/analysis/style-border.d.ts +7 -0
  33. package/dist/types/analysis/style-effects.d.ts +4 -0
  34. package/dist/types/analysis/style.d.ts +9 -0
  35. package/dist/types/analysis/svg/attributes.d.ts +10 -0
  36. package/dist/types/analysis/svg/build.d.ts +6 -0
  37. package/dist/types/analysis/svg/context.d.ts +31 -0
  38. package/dist/types/analysis/svg/geometry.d.ts +11 -0
  39. package/dist/types/analysis/svg/gradients.d.ts +6 -0
  40. package/dist/types/analysis/svg/markup.d.ts +23 -0
  41. package/dist/types/analysis/svg/text.d.ts +7 -0
  42. package/dist/types/analysis/svg/transforms.d.ts +19 -0
  43. package/dist/types/analysis/svg/traversal.d.ts +4 -0
  44. package/dist/types/analysis/table-borders.d.ts +19 -0
  45. package/dist/types/analysis/table.d.ts +36 -0
  46. package/dist/types/analysis/text-layout.d.ts +65 -0
  47. package/dist/types/analysis/text-runs.d.ts +30 -0
  48. package/dist/types/analysis/text.d.ts +9 -0
  49. package/dist/types/analysis/tooltips.d.ts +16 -0
  50. package/dist/types/analysis/typography.d.ts +21 -0
  51. package/dist/types/analysis/wrapping.d.ts +5 -0
  52. package/dist/types/api/bands.d.ts +58 -0
  53. package/dist/types/api/build.d.ts +51 -0
  54. package/dist/types/api/convert.d.ts +13 -0
  55. package/dist/types/api/types.d.ts +69 -0
  56. package/dist/types/browser/capture.d.ts +89 -0
  57. package/dist/types/browser/conditions.d.ts +25 -0
  58. package/dist/types/browser/fonts.d.ts +34 -0
  59. package/dist/types/browser/images.d.ts +3 -0
  60. package/dist/types/browser/layers.d.ts +132 -0
  61. package/dist/types/browser/motion.d.ts +44 -0
  62. package/dist/types/browser/settle.d.ts +27 -0
  63. package/dist/types/browser/sizing.d.ts +10 -0
  64. package/dist/types/browser/snapshot.d.ts +155 -0
  65. package/dist/types/browser/states.d.ts +36 -0
  66. package/dist/types/cli/args.d.ts +31 -0
  67. package/dist/types/cli/config.d.ts +78 -0
  68. package/dist/types/cli/css.d.ts +9 -0
  69. package/dist/types/cli/manifest.d.ts +20 -0
  70. package/dist/types/cli/output.d.ts +27 -0
  71. package/dist/types/cli/run.d.ts +4 -0
  72. package/dist/types/cli.d.ts +2 -0
  73. package/dist/types/compiler/index.d.ts +91 -0
  74. package/dist/types/compiler/naming.d.ts +1 -0
  75. package/dist/types/css/background.d.ts +16 -0
  76. package/dist/types/css/blend.d.ts +9 -0
  77. package/dist/types/css/color.d.ts +14 -0
  78. package/dist/types/css/counter.d.ts +10 -0
  79. package/dist/types/css/easing.d.ts +4 -0
  80. package/dist/types/css/effects.d.ts +18 -0
  81. package/dist/types/css/grid.d.ts +29 -0
  82. package/dist/types/css/index.d.ts +15 -0
  83. package/dist/types/css/media.d.ts +16 -0
  84. package/dist/types/css/paint.d.ts +33 -0
  85. package/dist/types/css/radius.d.ts +5 -0
  86. package/dist/types/css/raster.d.ts +10 -0
  87. package/dist/types/css/shadows.d.ts +3 -0
  88. package/dist/types/css/syntax.d.ts +12 -0
  89. package/dist/types/css/transforms.d.ts +45 -0
  90. package/dist/types/css/typography.d.ts +8 -0
  91. package/dist/types/fonts/converter.d.ts +16 -0
  92. package/dist/types/fonts/metadata.d.ts +6 -0
  93. package/dist/types/fonts/package.d.ts +12 -0
  94. package/dist/types/fonts/registration.d.ts +13 -0
  95. package/dist/types/fonts/source.d.ts +14 -0
  96. package/dist/types/fonts/types.d.ts +28 -0
  97. package/dist/types/html/elements.d.ts +8 -0
  98. package/dist/types/html/index.d.ts +4 -0
  99. package/dist/types/html/inputs.d.ts +4 -0
  100. package/dist/types/html/policy.d.ts +3 -0
  101. package/dist/types/html/types.d.ts +12 -0
  102. package/dist/types/index.d.ts +8 -0
  103. package/dist/types/input/identity.d.ts +3 -0
  104. package/dist/types/input/mime.d.ts +1 -0
  105. package/dist/types/input/root.d.ts +1 -0
  106. package/dist/types/input/server.d.ts +11 -0
  107. package/dist/types/input/source.d.ts +9 -0
  108. package/dist/types/input/types.d.ts +54 -0
  109. package/dist/types/model/fonts.d.ts +26 -0
  110. package/dist/types/model/interactive.d.ts +10 -0
  111. package/dist/types/model/layout.d.ts +528 -0
  112. package/dist/types/model/semantic.d.ts +984 -0
  113. package/dist/types/model/values.d.ts +125 -0
  114. package/dist/types/model/version.d.ts +4 -0
  115. package/dist/types/swift/borders.d.ts +13 -0
  116. package/dist/types/swift/components.d.ts +115 -0
  117. package/dist/types/swift/context.d.ts +184 -0
  118. package/dist/types/swift/control-state.d.ts +19 -0
  119. package/dist/types/swift/controls.d.ts +62 -0
  120. package/dist/types/swift/diagnostics.d.ts +3 -0
  121. package/dist/types/swift/effects.d.ts +23 -0
  122. package/dist/types/swift/emit.d.ts +6 -0
  123. package/dist/types/swift/format.d.ts +91 -0
  124. package/dist/types/swift/frames.d.ts +17 -0
  125. package/dist/types/swift/images.d.ts +25 -0
  126. package/dist/types/swift/layout.d.ts +5 -0
  127. package/dist/types/swift/measured-layout.d.ts +4 -0
  128. package/dist/types/swift/modifiers.d.ts +30 -0
  129. package/dist/types/swift/motion.d.ts +20 -0
  130. package/dist/types/swift/nodes.d.ts +4 -0
  131. package/dist/types/swift/paint.d.ts +22 -0
  132. package/dist/types/swift/runtime.d.ts +6 -0
  133. package/dist/types/swift/shadows.d.ts +7 -0
  134. package/dist/types/swift/states.d.ts +3 -0
  135. package/dist/types/swift/svg.d.ts +14 -0
  136. package/dist/types/swift/text-box.d.ts +12 -0
  137. package/dist/types/swift/text.d.ts +3 -0
  138. package/dist/types/swift/tokens/emit.d.ts +26 -0
  139. package/dist/types/swift/tokens/extract.d.ts +21 -0
  140. package/dist/types/swift/tokens/naming.d.ts +22 -0
  141. package/dist/types/swift/tokens/slots.d.ts +18 -0
  142. package/dist/types/swift/tokens/types.d.ts +77 -0
  143. package/dist/types/swift/tokens.d.ts +6 -0
  144. package/dist/types/swift/traversal.d.ts +2 -0
  145. package/dist/types/swift/wrapping.d.ts +4 -0
  146. package/package.json +66 -4
@@ -0,0 +1,2975 @@
1
+ import {
2
+ validateName,
3
+ LAYOUT_DOC_VERSION,
4
+ VALIDATION_MARK,
5
+ CONDITIONAL_MARK,
6
+ CONTAINER_MARK,
7
+ STYLE_WHITELIST,
8
+ round2,
9
+ cssUrls,
10
+ widthQuery,
11
+ matchesWidth,
12
+ PRESSABLE_SELECTOR,
13
+ TEXT_ENTRY_SELECTOR,
14
+ GENERATED_BY,
15
+ DEFAULT_BAND_DEVICES2,
16
+ planBands2,
17
+ compileLayout2,
18
+ needsProbe2,
19
+ validateLibrary2,
20
+ compileLayouts2
21
+ } from "./cli-qchh0m4d.js";
22
+
23
+ // src/api/build.ts
24
+ import { readFile as readFile3 } from "node:fs/promises";
25
+ import { relative, resolve as resolve3, sep } from "node:path";
26
+
27
+ // src/input/root.ts
28
+ var DEFAULT_ROOT = "[data-markswift-root], body:not(:has([data-markswift-root]))";
29
+
30
+ // src/input/server.ts
31
+ import { createReadStream } from "node:fs";
32
+ import { stat } from "node:fs/promises";
33
+ import { createServer } from "node:http";
34
+ import { resolve } from "node:path";
35
+
36
+ // src/input/mime.ts
37
+ import { extname } from "node:path";
38
+ var TYPES = {
39
+ ".html": "text/html;charset=utf-8",
40
+ ".htm": "text/html;charset=utf-8",
41
+ ".css": "text/css;charset=utf-8",
42
+ ".js": "text/javascript;charset=utf-8",
43
+ ".mjs": "text/javascript;charset=utf-8",
44
+ ".json": "application/json;charset=utf-8",
45
+ ".map": "application/json;charset=utf-8",
46
+ ".txt": "text/plain;charset=utf-8",
47
+ ".svg": "image/svg+xml",
48
+ ".png": "image/png",
49
+ ".jpg": "image/jpeg",
50
+ ".jpeg": "image/jpeg",
51
+ ".gif": "image/gif",
52
+ ".webp": "image/webp",
53
+ ".avif": "image/avif",
54
+ ".ico": "image/x-icon",
55
+ ".woff": "font/woff",
56
+ ".woff2": "font/woff2",
57
+ ".ttf": "font/ttf",
58
+ ".otf": "font/otf",
59
+ ".wasm": "application/wasm"
60
+ };
61
+ function mimeType(path) {
62
+ return TYPES[extname(path).toLowerCase()] ?? "application/octet-stream";
63
+ }
64
+
65
+ // src/input/server.ts
66
+ async function serveDir(rootDir) {
67
+ const root = resolve(rootDir);
68
+ const server = createServer(async (req, res) => {
69
+ const reply = (status, body) => res.writeHead(status, { "content-type": "text/plain" }).end(body);
70
+ const { port } = server.address();
71
+ if (req.headers.host !== `127.0.0.1:${port}`)
72
+ return reply(403, "forbidden");
73
+ let path;
74
+ try {
75
+ path = decodeURIComponent(new URL(req.url ?? "/", "http://127.0.0.1").pathname);
76
+ } catch {
77
+ return reply(400, "bad request");
78
+ }
79
+ if (path.split("/").some((s) => s.startsWith(".")))
80
+ return reply(404, "not found");
81
+ const local = resolve(root, "." + path);
82
+ if (local !== root && !local.startsWith(root + "/"))
83
+ return reply(403, "forbidden");
84
+ const info = await stat(local).catch(() => {
85
+ return;
86
+ });
87
+ if (!info?.isFile())
88
+ return reply(404, "not found");
89
+ res.writeHead(200, { "content-type": mimeType(local), "content-length": info.size, "cache-control": "no-store" });
90
+ createReadStream(local).pipe(res);
91
+ });
92
+ await new Promise((done, fail) => server.once("error", fail).listen(0, "127.0.0.1", done));
93
+ const { port } = server.address();
94
+ return {
95
+ origin: `http://127.0.0.1:${port}`,
96
+ stop() {
97
+ server.closeAllConnections();
98
+ server.close();
99
+ }
100
+ };
101
+ }
102
+
103
+ // src/browser/settle.ts
104
+ async function settle(page, options = {}) {
105
+ return await page.evaluate(async (at) => {
106
+ for (const img of document.images)
107
+ if (img.loading === "lazy")
108
+ img.loading = "eager";
109
+ await Promise.all([...document.images].map((img) => img.decode().catch(() => {})));
110
+ await document.fonts.ready;
111
+ for (const el of document.querySelectorAll("*")) {
112
+ const cv = getComputedStyle(el).contentVisibility;
113
+ if (cv === "auto")
114
+ el.style.setProperty("content-visibility", "visible", "important");
115
+ else if (cv === "hidden")
116
+ for (const c of el.children)
117
+ c.style.setProperty("display", "none", "important");
118
+ }
119
+ const recorded = at === null ? [] : (window.__markswiftStarted ?? []).filter((a) => a.playState === "finished");
120
+ let paused = 0;
121
+ for (const a of new Set([...document.getAnimations(), ...recorded])) {
122
+ if (at !== null) {
123
+ a.pause();
124
+ a.currentTime = at;
125
+ paused++;
126
+ continue;
127
+ }
128
+ try {
129
+ a.finish();
130
+ } catch {
131
+ a.cancel();
132
+ }
133
+ }
134
+ const frame = Promise.withResolvers();
135
+ requestAnimationFrame(() => requestAnimationFrame(() => frame.resolve()));
136
+ await frame.promise;
137
+ return paused;
138
+ }, options.at ?? null);
139
+ }
140
+ async function finishAnimations(page) {
141
+ await page.evaluate(() => {
142
+ for (const a of document.getAnimations()) {
143
+ try {
144
+ a.finish();
145
+ } catch {
146
+ a.cancel();
147
+ }
148
+ }
149
+ });
150
+ }
151
+ async function withFrozenProgress(page, cdp, shot) {
152
+ for (let attempt = 1;; attempt++) {
153
+ const hidden = await page.evaluateHandle(() => [...document.querySelectorAll("progress:indeterminate")].filter((p) => getComputedStyle(p).appearance !== "none").map((p) => {
154
+ const style = p.getAttribute("style");
155
+ p.style.setProperty("display", "none", "important");
156
+ return { p, style };
157
+ }));
158
+ try {
159
+ if (!await hidden.evaluate((list) => list.length))
160
+ return await shot(30000);
161
+ await cdp.send("Page.captureScreenshot", { clip: { x: 0, y: 0, width: 1, height: 1, scale: 1 } });
162
+ await cdp.send("Emulation.setVirtualTimePolicy", { policy: "pause" });
163
+ await hidden.evaluate((list) => {
164
+ for (const { p, style } of list)
165
+ if (style === null)
166
+ p.removeAttribute("style");
167
+ else
168
+ p.setAttribute("style", style);
169
+ });
170
+ try {
171
+ return await shot(FROZEN_SHOT_MS);
172
+ } catch (error) {
173
+ if (attempt >= FROZEN_ATTEMPTS || !(error instanceof Error && error.name === "TimeoutError"))
174
+ throw error;
175
+ } finally {
176
+ await cdp.send("Emulation.setVirtualTimePolicy", { policy: "advance" });
177
+ }
178
+ } finally {
179
+ await hidden.dispose();
180
+ }
181
+ }
182
+ }
183
+ var FROZEN_SHOT_MS = 8000;
184
+ var FROZEN_ATTEMPTS = 3;
185
+
186
+ // src/browser/fonts.ts
187
+ import { createHash } from "node:crypto";
188
+ async function collectFontFaces(page, sheetText) {
189
+ return await page.evaluate((sheetText) => {
190
+ const KEYWORD_WEIGHT = { normal: 400, bold: 700, lighter: 100, bolder: 700 };
191
+ const weightRange = (s) => {
192
+ const parts = s.trim().split(/\s+/).filter(Boolean);
193
+ const nums = parts.map((p) => KEYWORD_WEIGHT[p.toLowerCase()] ?? Number.parseFloat(p)).filter((n) => Number.isFinite(n));
194
+ if (!nums.length)
195
+ return [400, 400];
196
+ return [Math.min(...nums), Math.max(...nums)];
197
+ };
198
+ const splitSrc = (s) => {
199
+ const out = [];
200
+ let depth = 0, quote = "", start = 0;
201
+ for (let i = 0;i < s.length; i++) {
202
+ const c = s[i];
203
+ if (quote) {
204
+ if (c === quote && s[i - 1] !== "\\")
205
+ quote = "";
206
+ } else if (c === '"' || c === "'")
207
+ quote = c;
208
+ else if (c === "(")
209
+ depth++;
210
+ else if (c === ")")
211
+ depth--;
212
+ else if (c === "," && depth === 0) {
213
+ out.push(s.slice(start, i));
214
+ start = i + 1;
215
+ }
216
+ }
217
+ out.push(s.slice(start));
218
+ return out.map((p) => p.trim()).filter(Boolean);
219
+ };
220
+ const unquote = (s) => s.trim().replace(/^["']|["']$/g, "").trim();
221
+ const faces = [];
222
+ const unreadable = [];
223
+ const rulesOf = (sheet) => {
224
+ try {
225
+ return sheet.cssRules;
226
+ } catch {}
227
+ const text = sheet.href ? sheetText[sheet.href] : undefined;
228
+ if (text === undefined) {
229
+ unreadable.push(sheet.href ?? "inline stylesheet");
230
+ return;
231
+ }
232
+ const parsed = new CSSStyleSheet;
233
+ parsed.replaceSync(text);
234
+ return parsed.cssRules;
235
+ };
236
+ const visit = (rules, base) => {
237
+ for (const rule of rules) {
238
+ if (rule instanceof CSSFontFaceRule) {
239
+ const st = rule.style;
240
+ const family = unquote(st.getPropertyValue("font-family"));
241
+ if (!family)
242
+ continue;
243
+ const src = [];
244
+ for (const entry of splitSrc(st.getPropertyValue("src"))) {
245
+ const url = /url\(\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^)]*)\s*\)/.exec(entry);
246
+ if (!url)
247
+ continue;
248
+ const fmt = /format\(\s*("[^"]*"|'[^']*'|[^)]*)\s*\)/.exec(entry);
249
+ try {
250
+ src.push({ url: new URL(unquote(url[1]), base).href, ...fmt ? { format: unquote(fmt[1]) } : {} });
251
+ } catch {}
252
+ }
253
+ const styleDesc = unquote(st.getPropertyValue("font-style")).split(/\s+/)[0]?.toLowerCase();
254
+ faces.push({
255
+ family,
256
+ weight: weightRange(st.getPropertyValue("font-weight") || "normal"),
257
+ style: styleDesc === "italic" || styleDesc === "oblique" ? styleDesc : "normal",
258
+ ...st.getPropertyValue("unicode-range") ? { unicodeRange: st.getPropertyValue("unicode-range") } : {},
259
+ src,
260
+ loaded: false
261
+ });
262
+ } else if (rule instanceof CSSImportRule) {
263
+ if (rule.styleSheet) {
264
+ const r = rulesOf(rule.styleSheet);
265
+ if (r)
266
+ visit(r, rule.styleSheet.href ?? base);
267
+ }
268
+ } else if ("cssRules" in rule)
269
+ visit(rule.cssRules, base);
270
+ }
271
+ };
272
+ for (const sheet of document.styleSheets) {
273
+ if (sheet.ownerNode?.hasAttribute?.("data-markswift-harness"))
274
+ continue;
275
+ const rules = rulesOf(sheet);
276
+ if (rules)
277
+ visit(rules, sheet.href ?? document.baseURI);
278
+ }
279
+ const loaded = new Set;
280
+ document.fonts.forEach((f) => {
281
+ if (f.status !== "loaded")
282
+ return;
283
+ const s = f.style.split(/\s+/)[0].toLowerCase();
284
+ loaded.add(`${unquote(f.family).toLowerCase()}|${s === "italic" || s === "oblique" ? s : "normal"}|${weightRange(f.weight).join(" ")}`);
285
+ });
286
+ const seen = new Set;
287
+ const out = [];
288
+ for (const f of faces) {
289
+ if (!f.src.length)
290
+ continue;
291
+ const key = `${f.family.toLowerCase()}|${f.style}|${f.weight.join(" ")}|${f.unicodeRange ?? ""}|${f.src.map((s) => s.url).join(",")}`;
292
+ if (seen.has(key))
293
+ continue;
294
+ seen.add(key);
295
+ out.push({ ...f, loaded: loaded.has(`${f.family.toLowerCase()}|${f.style}|${f.weight.join(" ")}`) });
296
+ }
297
+ return { faces: out, unreadable };
298
+ }, sheetText);
299
+ }
300
+ var MAX_FONT_BYTES = 25 * 1024 * 1024;
301
+ async function readFontBytes(faces, allowRemote = true, signal) {
302
+ const files = [];
303
+ const byHash = {};
304
+ for (const face of faces) {
305
+ for (const src of face.src) {
306
+ if (src.url.startsWith("data:"))
307
+ continue;
308
+ if (!allowRemote)
309
+ continue;
310
+ if (!URL.canParse(src.url) || !["http:", "https:"].includes(new URL(src.url).protocol))
311
+ continue;
312
+ let bytes;
313
+ try {
314
+ const res = await fetch(src.url, { signal, redirect: "follow" });
315
+ if (!res.ok || Number(res.headers.get("content-length") ?? 0) > MAX_FONT_BYTES)
316
+ continue;
317
+ bytes = new Uint8Array(await res.arrayBuffer());
318
+ if (bytes.length > MAX_FONT_BYTES)
319
+ continue;
320
+ } catch (error) {
321
+ if (signal?.aborted)
322
+ throw error;
323
+ continue;
324
+ }
325
+ const hash = createHash("sha256").update(bytes).digest("hex").slice(0, 12);
326
+ if (!byHash[hash]) {
327
+ const sig = new TextDecoder("latin1").decode(bytes.subarray(0, 4));
328
+ const ext = sig === "wOF2" ? "woff2" : sig === "wOFF" ? "woff" : sig === "OTTO" ? "otf" : "ttf";
329
+ byHash[hash] = `_fonts/${hash}.${ext}`;
330
+ files.push({ file: byHash[hash], bytes });
331
+ }
332
+ src.file = byHash[hash];
333
+ if (/^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:|\/)/.test(src.url))
334
+ src.url = new URL(src.url).pathname;
335
+ }
336
+ }
337
+ return files;
338
+ }
339
+
340
+ // src/browser/capture.ts
341
+ import { chromium } from "playwright-core";
342
+
343
+ // src/browser/snapshot.ts
344
+ var SKIP_TAGS = {
345
+ script: true,
346
+ style: true,
347
+ link: true,
348
+ meta: true,
349
+ template: true,
350
+ noscript: true
351
+ };
352
+ var KEEP_WITHOUT_LAYOUT = {
353
+ defs: true,
354
+ lineargradient: true,
355
+ radialgradient: true,
356
+ stop: true,
357
+ clippath: true,
358
+ mask: true,
359
+ option: true,
360
+ optgroup: true
361
+ };
362
+ var PSEUDO_TAGS = { before: "::before", after: "::after", marker: "::marker" };
363
+ var pathChildren = (raw) => raw.children.filter((c) => c.type === 1 && c.pseudo === undefined);
364
+ function attachPreferredSizes(root, sizes, pseudoStyles) {
365
+ const walk = (raw, path) => {
366
+ if (Object.hasOwn(sizes, path))
367
+ raw.preferredSize = sizes[path];
368
+ if (Object.hasOwn(pseudoStyles, path))
369
+ raw.pseudoStyles = pseudoStyles[path];
370
+ for (const [i, child] of pathChildren(raw).entries())
371
+ walk(child, path ? `${path}/${i}` : String(i));
372
+ };
373
+ walk(root, "");
374
+ }
375
+ function attachMotion(root, motion) {
376
+ const walk = (raw, path) => {
377
+ if (Object.hasOwn(motion, path))
378
+ raw.motion = motion[path];
379
+ let i = 0;
380
+ for (const child of raw.children) {
381
+ if (child.type !== 1)
382
+ continue;
383
+ if (child.pseudo !== undefined) {
384
+ const key = path + PSEUDO_TAGS[child.pseudo];
385
+ if (Object.hasOwn(motion, key))
386
+ child.motion = motion[key];
387
+ continue;
388
+ }
389
+ walk(child, path ? `${path}/${i++}` : String(i++));
390
+ }
391
+ };
392
+ walk(root, "");
393
+ }
394
+ function rawAt(root, path) {
395
+ let raw = root;
396
+ for (const step of path ? path.split("/") : [])
397
+ raw = raw && pathChildren(raw)[Number(step)];
398
+ return raw;
399
+ }
400
+ function pathOf(root, raw) {
401
+ const steps = [];
402
+ for (let r = raw;r !== root; r = r.parent) {
403
+ if (!r.parent)
404
+ return;
405
+ steps.unshift(pathChildren(r.parent).indexOf(r));
406
+ }
407
+ return steps.join("/");
408
+ }
409
+ function snapshotToTree(snap, boxes, states) {
410
+ const doc = snap.documents[0];
411
+ if (!doc)
412
+ throw new Error("empty snapshot");
413
+ const S = snap.strings;
414
+ const str = (i) => i >= 0 ? S[i] : "";
415
+ const n = doc.nodes.nodeType.length;
416
+ const raws = new Array(n);
417
+ for (let i = 0;i < n; i++) {
418
+ const attrs = {};
419
+ const a = doc.nodes.attributes[i] ?? [];
420
+ for (let k = 0;k + 1 < a.length; k += 2)
421
+ attrs[str(a[k]).toLowerCase()] = str(a[k + 1]);
422
+ raws[i] = {
423
+ idx: i,
424
+ type: doc.nodes.nodeType[i],
425
+ name: str(doc.nodes.nodeName[i]).toLowerCase(),
426
+ value: str(doc.nodes.nodeValue[i]),
427
+ attrs,
428
+ styles: {},
429
+ children: []
430
+ };
431
+ const backend = doc.nodes.backendNodeId?.[i];
432
+ if (backend !== undefined)
433
+ raws[i].backend = backend;
434
+ }
435
+ for (let i = 0;i < n; i++) {
436
+ const p = doc.nodes.parentIndex[i];
437
+ if (p >= 0) {
438
+ raws[p].children.push(raws[i]);
439
+ raws[i].parent = raws[p];
440
+ }
441
+ }
442
+ liveFormState(doc.nodes, raws, str);
443
+ if (states)
444
+ for (const key of ["pressed", "focused"])
445
+ for (const [idx, delta] of states[key]) {
446
+ const r = raws[idx];
447
+ if (r)
448
+ r.states = { ...r.states, [key]: delta };
449
+ }
450
+ const pseudo = doc.nodes.pseudoType;
451
+ if (pseudo)
452
+ for (let k = 0;k < pseudo.index.length; k++)
453
+ raws[pseudo.index[k]].pseudo = str(pseudo.value[k]);
454
+ const L = doc.layout;
455
+ for (let li = 0;li < L.nodeIndex.length; li++) {
456
+ const r = raws[L.nodeIndex[li]];
457
+ const b = L.bounds[li];
458
+ r.bounds = { x: b[0], y: b[1], w: b[2], h: b[3] };
459
+ const o = L.offsetRects?.[li];
460
+ if (o)
461
+ r.offset = { x: o[0], y: o[1], w: o[2], h: o[3] };
462
+ const sv = L.styles[li] ?? [];
463
+ for (let k = 0;k < STYLE_WHITELIST.length; k++) {
464
+ const v = sv[k];
465
+ if (v !== undefined && v >= 0)
466
+ r.styles[STYLE_WHITELIST[k]] = S[v];
467
+ }
468
+ const t = L.text[li];
469
+ if (r.type === 3)
470
+ r.text = r.value.trim() ? r.value : t !== undefined && t >= 0 ? S[t] : "";
471
+ if (L.paintOrders)
472
+ r.paint = L.paintOrders[li];
473
+ if (r.pseudo !== undefined)
474
+ (r.parts ??= []).push({ bounds: { ...r.bounds }, text: t !== undefined && t >= 0 ? S[t] : undefined });
475
+ }
476
+ const LB = boxes?.documents[0]?.layout;
477
+ if (LB) {
478
+ const seen = new Map;
479
+ for (let li = 0;li < LB.nodeIndex.length; li++) {
480
+ const r = raws[LB.nodeIndex[li]];
481
+ const b = LB.bounds[li];
482
+ if (r && b) {
483
+ r.bounds = { x: b[0], y: b[1], w: b[2], h: b[3] };
484
+ if (r.parts) {
485
+ const k = seen.get(r) ?? 0;
486
+ seen.set(r, k + 1);
487
+ const part = r.parts[k];
488
+ if (part)
489
+ part.bounds = { ...r.bounds };
490
+ }
491
+ }
492
+ const o = LB.offsetRects?.[li];
493
+ if (r && o)
494
+ r.offset = { x: o[0], y: o[1], w: o[2], h: o[3] };
495
+ }
496
+ }
497
+ assignLineBoxes(boxes?.documents[0] ?? doc, raws);
498
+ if (pseudo)
499
+ for (const i of pseudo.index)
500
+ raws[i].box = pseudoBox(raws[i]);
501
+ const cur = doc.nodes.currentSourceURL;
502
+ if (cur) {
503
+ const base = str(doc.documentURL);
504
+ for (let k = 0;k < cur.index.length; k++)
505
+ markCurrentSrc(raws[cur.index[k]], str(cur.value[k]), base);
506
+ }
507
+ const html = raws.find((r) => r.type === 1 && r.name === "html");
508
+ if (!html)
509
+ throw new Error("no <html> in snapshot");
510
+ return html;
511
+ }
512
+ function liveFormState(nodes, raws, str) {
513
+ const input = nodes.inputValue;
514
+ if (input)
515
+ for (let k = 0;k < input.index.length; k++) {
516
+ const r = raws[input.index[k]];
517
+ const type = r?.attrs["type"]?.toLowerCase();
518
+ if (r?.name !== "input" || type === "checkbox" || type === "radio")
519
+ continue;
520
+ const live = str(input.value[k]);
521
+ if (live === (r.attrs["value"] ?? ""))
522
+ continue;
523
+ if (live)
524
+ r.attrs["value"] = live;
525
+ else
526
+ delete r.attrs["value"];
527
+ }
528
+ const text = nodes.textValue;
529
+ if (text)
530
+ for (let k = 0;k < text.index.length; k++) {
531
+ const r = raws[text.index[k]];
532
+ if (r?.name !== "textarea")
533
+ continue;
534
+ const live = str(text.value[k]);
535
+ const markup = r.children.map((c) => c.type === 3 ? c.value : "").join("");
536
+ if (live !== markup)
537
+ r.attrs["value"] = live;
538
+ }
539
+ for (const [column, tag, attr] of [
540
+ [nodes.inputChecked, "input", "checked"],
541
+ [nodes.optionSelected, "option", "selected"]
542
+ ]) {
543
+ if (!column)
544
+ continue;
545
+ for (const r of raws)
546
+ if (r.name === tag)
547
+ delete r.attrs[attr];
548
+ for (const i of column.index)
549
+ if (raws[i]?.name === tag)
550
+ raws[i].attrs[attr] = "";
551
+ }
552
+ }
553
+ function assignLineBoxes(document2, raws) {
554
+ const tb = document2.textBoxes;
555
+ const nodeIndex = document2.layout.nodeIndex;
556
+ for (let k = 0;k < tb.layoutIndex.length; k++) {
557
+ const r = raws[nodeIndex[tb.layoutIndex[k]]];
558
+ const b = tb.bounds[k];
559
+ if (!r || !b)
560
+ continue;
561
+ (r.lines ??= []).push({
562
+ bounds: { x: b[0], y: b[1], w: b[2], h: b[3] },
563
+ start: tb.start[k],
564
+ length: tb.length[k]
565
+ });
566
+ }
567
+ }
568
+ function pseudoBox(raw) {
569
+ const o = raw.offset;
570
+ if (!o)
571
+ return;
572
+ const position = raw.styles["position"] ?? "static";
573
+ if (position === "fixed")
574
+ return { ...o };
575
+ for (let a = raw.parent;a; a = a.parent) {
576
+ if (!a.bounds)
577
+ continue;
578
+ const positioned = (a.styles["position"] ?? "static") !== "static";
579
+ if (a.name === "body")
580
+ return positioned ? { x: a.bounds.x + o.x, y: a.bounds.y + o.y, w: o.w, h: o.h } : { ...o };
581
+ if (positioned || position === "static" && (a.name === "table" || a.name === "td" || a.name === "th")) {
582
+ const bl = parseFloat(a.styles["border-left-width"] ?? "") || 0;
583
+ const bt = parseFloat(a.styles["border-top-width"] ?? "") || 0;
584
+ return { x: a.bounds.x + bl + o.x, y: a.bounds.y + bt + o.y, w: o.w, h: o.h };
585
+ }
586
+ }
587
+ return;
588
+ }
589
+ function markCurrentSrc(img, currentSrc, base) {
590
+ if (!currentSrc)
591
+ return;
592
+ const resolve = (u) => {
593
+ try {
594
+ return new URL(u, base).href;
595
+ } catch {
596
+ return u;
597
+ }
598
+ };
599
+ const src = img.attrs["src"];
600
+ if (src !== undefined && resolve(src) === currentSrc)
601
+ return;
602
+ const candidates = srcsetUrls(img.attrs["srcset"] ?? "");
603
+ if (img.parent?.name === "picture") {
604
+ for (const s of img.parent.children)
605
+ if (s.type === 1 && s.name === "source")
606
+ candidates.push(...srcsetUrls(s.attrs["srcset"] ?? ""));
607
+ }
608
+ img.currentSrc = candidates.find((c) => resolve(c) === currentSrc) ?? currentSrc;
609
+ }
610
+ function srcsetUrls(srcset) {
611
+ const out = [];
612
+ const n = srcset.length;
613
+ let i = 0;
614
+ while (i < n) {
615
+ while (i < n && /[\s,]/.test(srcset[i]))
616
+ i++;
617
+ const start = i;
618
+ while (i < n && !/\s/.test(srcset[i]))
619
+ i++;
620
+ const url = srcset.slice(start, i);
621
+ if (!url)
622
+ break;
623
+ if (url.endsWith(","))
624
+ out.push(url.replace(/,+$/, ""));
625
+ else {
626
+ out.push(url);
627
+ while (i < n && srcset[i] !== ",")
628
+ i++;
629
+ }
630
+ }
631
+ return out;
632
+ }
633
+ function findByAttr(root, attr, value) {
634
+ if (root.attrs[attr] === value)
635
+ return root;
636
+ for (const c of root.children) {
637
+ const f = findByAttr(c, attr, value);
638
+ if (f)
639
+ return f;
640
+ }
641
+ return;
642
+ }
643
+ function toLayout(raw, origin, identity, path = "") {
644
+ if (raw.type !== 1 || SKIP_TAGS[raw.name])
645
+ return;
646
+ if (!raw.bounds && !KEEP_WITHOUT_LAYOUT[raw.name])
647
+ return;
648
+ return element(raw, origin, identity, path);
649
+ }
650
+ function element(raw, origin, identity, path) {
651
+ const node = {
652
+ tag: raw.name,
653
+ attrs: raw.attrs,
654
+ styles: raw.styles,
655
+ bounds: raw.bounds ? rebase(raw.bounds, origin) : { x: 0, y: 0, w: 0, h: 0 },
656
+ children: [],
657
+ ...raw.preferredSize ? { preferredSize: raw.preferredSize } : {}
658
+ };
659
+ const id = identity(raw.name, raw.attrs, path);
660
+ if (id !== undefined)
661
+ node.id = id;
662
+ if (raw.paint !== undefined)
663
+ node.paint = raw.paint;
664
+ if (raw.currentSrc !== undefined)
665
+ node.currentSrc = raw.currentSrc;
666
+ if (raw.states)
667
+ node.states = raw.states;
668
+ if (raw.motion)
669
+ node.motion = raw.motion;
670
+ if (raw.pseudoStyles)
671
+ node.pseudoStyles = raw.pseudoStyles;
672
+ appendChildren(raw, origin, identity, path, node.children);
673
+ return node;
674
+ }
675
+ function appendChildren(raw, origin, identity, path, out) {
676
+ let after;
677
+ let i = 0;
678
+ for (const c of raw.children) {
679
+ if (c.type === 3) {
680
+ if (c.bounds && c.text && (c.text.trim() || c.bounds.w > 0)) {
681
+ const text = {
682
+ tag: "#text",
683
+ attrs: {},
684
+ styles: {},
685
+ bounds: rebase(c.bounds, origin),
686
+ text: c.text,
687
+ children: []
688
+ };
689
+ if (c.lines && c.lines.length > 1)
690
+ text.lines = c.lines.map((l) => ({ bounds: rebase(l.bounds, origin), start: l.start, length: l.length }));
691
+ out.push(text);
692
+ } else if ((!raw.bounds || raw.name === "textarea") && c.value.trim()) {
693
+ out.push({
694
+ tag: "#text",
695
+ attrs: {},
696
+ styles: {},
697
+ bounds: { x: 0, y: 0, w: 0, h: 0 },
698
+ text: c.value,
699
+ children: []
700
+ });
701
+ }
702
+ continue;
703
+ }
704
+ if (c.type !== 1)
705
+ continue;
706
+ if (c.pseudo !== undefined) {
707
+ const p = pseudoElement(c, origin, identity, path);
708
+ if (!p)
709
+ continue;
710
+ if (c.pseudo === "after")
711
+ after = p;
712
+ else
713
+ out.push(p);
714
+ continue;
715
+ }
716
+ const childPath = path === "" ? `${i++}` : `${path}/${i++}`;
717
+ if (SKIP_TAGS[c.name])
718
+ continue;
719
+ if (c.bounds || KEEP_WITHOUT_LAYOUT[c.name])
720
+ out.push(element(c, origin, identity, childPath));
721
+ else if (hasLayout(c))
722
+ appendChildren(c, origin, identity, childPath, out);
723
+ }
724
+ if (after)
725
+ out.push(after);
726
+ }
727
+ function hasLayout(raw) {
728
+ return raw.bounds !== undefined || raw.children.some(hasLayout);
729
+ }
730
+ function pseudoElement(raw, origin, identity, parentPath) {
731
+ const tag = PSEUDO_TAGS[raw.pseudo];
732
+ if (!tag || !raw.bounds)
733
+ return;
734
+ const box = raw.box && raw.box.w > 0 && raw.box.h > 0 ? raw.box : undefined;
735
+ const pseudoPath = parentPath === "" ? tag : `${parentPath}/${tag}`;
736
+ const parts = raw.parts ?? [];
737
+ const texts = parts.filter((p) => p.text !== undefined).map((p) => p.bounds);
738
+ const contains = (o, i) => i.x >= o.x - 0.5 && i.y >= o.y - 0.5 && i.x + i.w <= o.x + o.w + 0.5 && i.y + i.h <= o.y + o.h + 0.5;
739
+ let urls = cssUrls(raw.styles["content"]);
740
+ if (raw.pseudo === "marker" && !urls.length)
741
+ urls = cssUrls(raw.styles["list-style-image"]);
742
+ const children = [];
743
+ let open;
744
+ let images = 0;
745
+ const close = () => {
746
+ if (open)
747
+ children.push({
748
+ tag: "#text",
749
+ attrs: {},
750
+ styles: {},
751
+ bounds: rebase(open.bounds, origin),
752
+ text: open.text,
753
+ children: []
754
+ });
755
+ open = undefined;
756
+ };
757
+ for (const [i, p] of parts.entries()) {
758
+ if (p.text !== undefined) {
759
+ if (!open)
760
+ open = { text: p.text, bounds: { ...p.bounds } };
761
+ else {
762
+ const a = open.bounds;
763
+ const x = Math.min(a.x, p.bounds.x);
764
+ const y = Math.min(a.y, p.bounds.y);
765
+ open = {
766
+ text: open.text + p.text,
767
+ bounds: {
768
+ x,
769
+ y,
770
+ w: Math.max(a.x + a.w, p.bounds.x + p.bounds.w) - x,
771
+ h: Math.max(a.y + a.h, p.bounds.y + p.bounds.h) - y
772
+ }
773
+ };
774
+ }
775
+ continue;
776
+ }
777
+ if (i === 0 || !(p.bounds.w > 0 && p.bounds.h > 0) || texts.some((t) => contains(p.bounds, t)))
778
+ continue;
779
+ const src = urls[images];
780
+ if (src === undefined)
781
+ continue;
782
+ close();
783
+ const img = { tag: "img", attrs: { src }, styles: {}, bounds: rebase(p.bounds, origin), children: [] };
784
+ const id = identity("img", img.attrs, `${pseudoPath}/${images++}`);
785
+ if (id !== undefined)
786
+ img.id = id;
787
+ children.push(img);
788
+ }
789
+ close();
790
+ if (!images && children.length === 1 && !children[0].text?.trim())
791
+ children.length = 0;
792
+ if (!box && !children.length)
793
+ return;
794
+ const node = {
795
+ tag,
796
+ attrs: {},
797
+ styles: raw.styles,
798
+ bounds: rebase(box ?? raw.bounds, origin),
799
+ children
800
+ };
801
+ const id = identity(tag, node.attrs, pseudoPath);
802
+ if (id !== undefined)
803
+ node.id = id;
804
+ if (raw.paint !== undefined)
805
+ node.paint = raw.paint;
806
+ if (raw.states)
807
+ node.states = raw.states;
808
+ if (raw.motion)
809
+ node.motion = raw.motion;
810
+ return node;
811
+ }
812
+ function rebase(b, o) {
813
+ return { x: round2(b.x - o.x), y: round2(b.y - o.y), w: round2(b.w), h: round2(b.h) };
814
+ }
815
+
816
+ // src/browser/sizing.ts
817
+ async function readPreferredSizes(page, selector) {
818
+ return page.evaluate((selector) => {
819
+ const root = document.querySelector(selector);
820
+ if (!root)
821
+ throw new Error(`capture root ${selector} not found`);
822
+ const PLACEHOLDER_KEYS = ["color", "-webkit-text-fill-color", "opacity", "font-style", "font-weight"];
823
+ const BACKDROP_KEYS = ["background-color", "background-image", "opacity", "filter"];
824
+ const out = {};
825
+ const pseudoStyles = {};
826
+ const walk = (el, path) => {
827
+ const typed = el;
828
+ const map = typed.computedStyleMap?.();
829
+ const width = map?.get("width")?.toString();
830
+ const height = map?.get("height")?.toString();
831
+ if (width !== undefined && height !== undefined) {
832
+ const entry = { width, height };
833
+ const position = map?.get("position")?.toString();
834
+ if (position === "absolute" || position === "fixed" || position === "relative" || position === "sticky") {
835
+ const inset = (k) => map?.get(k)?.toString() ?? "auto";
836
+ entry.insets = { top: inset("top"), right: inset("right"), bottom: inset("bottom"), left: inset("left") };
837
+ }
838
+ const autoMargins = ["top", "right", "bottom", "left"].filter((k) => map?.get(`margin-${k}`)?.toString() === "auto");
839
+ if (autoMargins.length)
840
+ entry.autoMargins = autoMargins;
841
+ const tracks = map?.get("grid-template-columns")?.toString();
842
+ if (tracks && tracks !== "none")
843
+ entry.gridTemplateColumns = tracks;
844
+ const display = map?.get("display")?.toString();
845
+ const rows = map?.get("grid-template-rows")?.toString();
846
+ if ((display === "grid" || display === "inline-grid") && rows && rows !== "none")
847
+ entry.gridTemplateRows = rows;
848
+ out[path] = entry;
849
+ }
850
+ if ((el.tagName === "INPUT" || el.tagName === "TEXTAREA") && el.hasAttribute("placeholder")) {
851
+ const ph = getComputedStyle(el, "::placeholder");
852
+ pseudoStyles[path] = {
853
+ placeholder: Object.fromEntries(PLACEHOLDER_KEYS.map((k) => [k, ph.getPropertyValue(k)]))
854
+ };
855
+ }
856
+ if (el.matches(":modal, :popover-open, :fullscreen") && getComputedStyle(el).getPropertyValue("overlay") === "auto") {
857
+ const bd = getComputedStyle(el, "::backdrop");
858
+ pseudoStyles[path] = {
859
+ ...pseudoStyles[path],
860
+ backdrop: Object.fromEntries(BACKDROP_KEYS.map((k) => [k, bd.getPropertyValue(k)]))
861
+ };
862
+ }
863
+ Array.from(el.children).forEach((child, i) => {
864
+ walk(child, path ? `${path}/${i}` : String(i));
865
+ });
866
+ };
867
+ walk(root, "");
868
+ return { sizes: out, pseudoStyles };
869
+ }, selector);
870
+ }
871
+
872
+ // src/browser/motion.ts
873
+ var collect = (arg) => {
874
+ const root = arg.root === undefined ? null : document.querySelector(arg.root);
875
+ const out = {
876
+ byPath: {},
877
+ byTarget: (arg.targets ?? []).map(() => []),
878
+ ambiguous: [],
879
+ script: 0,
880
+ paused: 0
881
+ };
882
+ const inner = (arg.targets ?? []).map(() => new Map);
883
+ const recorded = arg.targets ? window.__markswiftStarted ?? [] : [];
884
+ const SKIP = { offset: true, computedOffset: true, easing: true, composite: true };
885
+ for (const a of new Set([...document.getAnimations(), ...recorded])) {
886
+ const effect = a.effect;
887
+ const target = effect?.target;
888
+ if (!effect || !target)
889
+ continue;
890
+ let slot = arg.targets ? arg.targets.indexOf(target) : -1;
891
+ if (arg.targets && slot < 0)
892
+ slot = arg.targets.findIndex((t) => t.contains(target));
893
+ if (arg.targets ? slot < 0 : !root?.contains(target))
894
+ continue;
895
+ const kind = a.constructor.name;
896
+ if (kind === "Animation")
897
+ out.script++;
898
+ if (kind !== arg.kind)
899
+ continue;
900
+ if (a.playState === "paused") {
901
+ out.paused++;
902
+ continue;
903
+ }
904
+ const pseudo = effect.pseudoElement;
905
+ if (pseudo && pseudo !== "::before" && pseudo !== "::after")
906
+ continue;
907
+ const keyframes = effect.getKeyframes().map((k) => {
908
+ const values = {};
909
+ for (const [key, v] of Object.entries(k)) {
910
+ if (SKIP[key] || typeof v !== "string")
911
+ continue;
912
+ const css = key.startsWith("--") ? key : key.replace(/^css(?=[A-Z])/, "").replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`).replace(/^-/, "").replace(/^webkit-/, "-webkit-");
913
+ values[css] = v;
914
+ }
915
+ return { offset: k.computedOffset, easing: k.easing ?? "linear", values };
916
+ });
917
+ const timing = effect.getTiming();
918
+ const duration = typeof timing.duration === "number" ? timing.duration : 0;
919
+ const delay = timing.delay ?? 0;
920
+ let entry;
921
+ if (kind === "CSSTransition") {
922
+ const property = a.transitionProperty;
923
+ entry = {
924
+ property,
925
+ duration,
926
+ delay,
927
+ easing: timing.easing ?? "linear",
928
+ from: keyframes[0]?.values[property] ?? "",
929
+ to: keyframes.at(-1)?.values[property] ?? ""
930
+ };
931
+ } else {
932
+ const anim = {
933
+ name: a.animationName,
934
+ duration,
935
+ delay,
936
+ iterations: timing.iterations === Infinity ? "infinite" : timing.iterations ?? 1,
937
+ direction: timing.direction ?? "normal",
938
+ fill: !timing.fill || timing.fill === "auto" ? "none" : timing.fill,
939
+ keyframes
940
+ };
941
+ if (pseudo === "::before" || pseudo === "::after")
942
+ anim.pseudo = pseudo;
943
+ if (effect.composite !== "replace")
944
+ anim.composite = effect.composite;
945
+ const timeline = a.timeline;
946
+ if (timeline === null)
947
+ anim.timeline = "none";
948
+ else if (timeline.constructor.name !== "DocumentTimeline")
949
+ anim.timeline = timeline.constructor.name;
950
+ entry = anim;
951
+ }
952
+ if (arg.targets) {
953
+ if (target === arg.targets[slot])
954
+ out.byTarget[slot].push(entry);
955
+ else
956
+ inner[slot].set(target, [...inner[slot].get(target) ?? [], entry]);
957
+ } else {
958
+ let path = "";
959
+ for (let el = target;el !== root; el = el.parentElement) {
960
+ const i = Array.prototype.indexOf.call(el.parentElement.children, el);
961
+ path = path ? `${i}/${path}` : String(i);
962
+ }
963
+ (out.byPath[path + (pseudo ?? "")] ??= []).push(entry);
964
+ }
965
+ if (arg.finish) {
966
+ try {
967
+ a.finish();
968
+ } catch {
969
+ a.cancel();
970
+ }
971
+ }
972
+ }
973
+ inner.forEach((m, slot) => {
974
+ if (out.byTarget[slot].length || !m.size)
975
+ return;
976
+ if (m.size === 1)
977
+ out.byTarget[slot] = [...m.values()][0];
978
+ else
979
+ out.ambiguous.push(slot);
980
+ });
981
+ return out;
982
+ };
983
+ var scanSheets = () => {
984
+ const out = { startingStyle: 0, authorReduce: false };
985
+ const walk = (rules) => {
986
+ for (const r of rules) {
987
+ if (r.constructor.name === "CSSStartingStyleRule")
988
+ out.startingStyle++;
989
+ if (r instanceof CSSMediaRule && r.conditionText.includes("prefers-reduced-motion"))
990
+ out.authorReduce = true;
991
+ if (r instanceof CSSImportRule && r.styleSheet)
992
+ read(r.styleSheet);
993
+ if ("cssRules" in r)
994
+ walk(r.cssRules);
995
+ }
996
+ };
997
+ const read = (sheet) => {
998
+ let rules;
999
+ try {
1000
+ rules = sheet.cssRules;
1001
+ } catch {
1002
+ return;
1003
+ }
1004
+ walk(rules);
1005
+ };
1006
+ for (const sheet of document.styleSheets)
1007
+ read(sheet);
1008
+ return out;
1009
+ };
1010
+ async function readMotion(page, root) {
1011
+ const reduce = await page.evaluate(collect, { root, kind: "CSSAnimation", finish: false });
1012
+ await page.emulateMedia({ reducedMotion: "no-preference" });
1013
+ const normal = await page.evaluate(collect, { root, kind: "CSSAnimation", finish: false });
1014
+ await page.emulateMedia({ reducedMotion: "reduce" });
1015
+ const { startingStyle, authorReduce } = await page.evaluate(scanSheets);
1016
+ const motion = {};
1017
+ for (const path of new Set([...Object.keys(normal.byPath), ...Object.keys(reduce.byPath)])) {
1018
+ const animations = normal.byPath[path] ?? [];
1019
+ const reduced = reduce.byPath[path] ?? [];
1020
+ const m = {};
1021
+ if (animations.length)
1022
+ m.animations = animations;
1023
+ if (authorReduce || JSON.stringify(reduced) !== JSON.stringify(animations))
1024
+ m.reduced = reduced;
1025
+ if (m.animations || m.reduced)
1026
+ motion[path] = m;
1027
+ }
1028
+ const paused = Math.max(reduce.paused, normal.paused);
1029
+ const warnings = [];
1030
+ if (reduce.script)
1031
+ warnings.push(`${reduce.script} script animation(s) (Element.animate) are not translated`);
1032
+ if (paused)
1033
+ warnings.push(`${paused} paused animation(s) are captured at their settled look`);
1034
+ if (startingStyle)
1035
+ warnings.push(`@starting-style rules are not translated (${startingStyle})`);
1036
+ return { motion, warnings, authorReduce };
1037
+ }
1038
+ async function readTransitions(page, root) {
1039
+ const { byPath } = await page.evaluate(collect, { root, kind: "CSSTransition", finish: true });
1040
+ return byPath;
1041
+ }
1042
+ async function recordAnimations(page) {
1043
+ await page.evaluate(() => {
1044
+ const fresh = !window.__markswiftStarted;
1045
+ window.__markswiftStarted = [];
1046
+ if (!fresh)
1047
+ return;
1048
+ const add = (anims) => {
1049
+ const list = window.__markswiftStarted;
1050
+ for (const a of anims)
1051
+ if (!list.includes(a))
1052
+ list.push(a);
1053
+ };
1054
+ new MutationObserver(() => add(document.getAnimations().filter((a) => a instanceof CSSAnimation))).observe(document, { subtree: true, childList: true, attributes: true });
1055
+ document.addEventListener("animationstart", (e) => add(e.target.getAnimations()), true);
1056
+ });
1057
+ }
1058
+ async function readElementAnimations(page, targets) {
1059
+ await page.emulateMedia({ reducedMotion: "no-preference" });
1060
+ const { byTarget, ambiguous } = await page.evaluate(collect, {
1061
+ targets,
1062
+ kind: "CSSAnimation",
1063
+ finish: false
1064
+ });
1065
+ await page.emulateMedia({ reducedMotion: "reduce" });
1066
+ return {
1067
+ lists: byTarget,
1068
+ warnings: ambiguous.map((i) => `layer root ${i} has no animation of its own and several animated descendants; its enter/exit is not read`)
1069
+ };
1070
+ }
1071
+
1072
+ // src/browser/conditions.ts
1073
+ async function readConditionalRules(page, selector, viewportWidth, breakpoints = false) {
1074
+ const { rules, found } = await page.evaluate(({ selector, mark, conditionalMark, containerMark }) => {
1075
+ const found = [];
1076
+ const root = document.querySelector(selector);
1077
+ if (!root)
1078
+ return { rules: [], found };
1079
+ const SIZE = /\b(width|height|aspect-ratio|orientation|inline-size|block-size)\b/;
1080
+ const VALIDATION = /:(user-)?(in)?valid\b/;
1081
+ const PSEUDO_ELEMENT = /::?(before|after|marker|placeholder|selection|first-line|first-letter|backdrop|file-selector-button)\b/g;
1082
+ const matching = (sel) => {
1083
+ const plain = sel.replace(PSEUDO_ELEMENT, "");
1084
+ try {
1085
+ return [...root.matches(plain) ? [root] : [], ...root.querySelectorAll(plain)];
1086
+ } catch {
1087
+ return [];
1088
+ }
1089
+ };
1090
+ const hits = (sel) => matching(sel).length > 0;
1091
+ const index = new Map;
1092
+ const containers = new Map;
1093
+ const containerOf = (el, name) => {
1094
+ for (let a = el.parentElement;a && root.contains(a); a = a.parentElement) {
1095
+ const cs = getComputedStyle(a);
1096
+ if (cs.containerType !== "normal" && (!name || cs.containerName.split(/\s+/).includes(name)))
1097
+ return a;
1098
+ }
1099
+ return;
1100
+ };
1101
+ const stamp = (cond, sel) => {
1102
+ for (const el of matching(sel)) {
1103
+ let key = `media\x00${cond.query}`;
1104
+ let entry = { kind: "media", query: cond.query };
1105
+ if (cond.kind === "container") {
1106
+ const host = containerOf(el, cond.name);
1107
+ if (!host)
1108
+ continue;
1109
+ let serial = containers.get(host);
1110
+ if (serial === undefined) {
1111
+ serial = containers.size;
1112
+ containers.set(host, serial);
1113
+ host.setAttribute(containerMark, String(serial));
1114
+ }
1115
+ key = `container\x00${cond.query}\x00${serial}`;
1116
+ entry = { kind: "container", query: cond.query, container: serial };
1117
+ } else
1118
+ entry.matches = window.matchMedia(cond.query).matches;
1119
+ let i = index.get(key);
1120
+ if (i === undefined) {
1121
+ i = found.push(entry) - 1;
1122
+ index.set(key, i);
1123
+ }
1124
+ const marks = (el.getAttribute(conditionalMark) ?? "").split(" ").filter(Boolean);
1125
+ if (!marks.includes(String(i)))
1126
+ el.setAttribute(conditionalMark, [...marks, i].join(" "));
1127
+ }
1128
+ };
1129
+ const conditions = new Map;
1130
+ const walk = (rules, parent, condition) => {
1131
+ for (const rule of Array.from(rules)) {
1132
+ let sel = parent;
1133
+ let cond = condition;
1134
+ if (rule instanceof CSSStyleRule) {
1135
+ sel = parent === undefined ? rule.selectorText : rule.selectorText.includes("&") ? rule.selectorText.replaceAll("&", `:is(${parent})`) : `:is(${parent}) ${rule.selectorText}`;
1136
+ if (cond && hits(sel)) {
1137
+ conditions.get(cond.key).selectors.add(rule.selectorText);
1138
+ stamp(cond, sel);
1139
+ }
1140
+ if (VALIDATION.test(sel))
1141
+ for (const el of [root, ...root.querySelectorAll("*")])
1142
+ try {
1143
+ if (el.matches(sel))
1144
+ el.setAttribute(mark, "");
1145
+ } catch {}
1146
+ } else if ((rule instanceof CSSMediaRule || rule instanceof CSSContainerRule) && SIZE.test(rule.conditionText)) {
1147
+ cond = rule instanceof CSSMediaRule ? { key: `@media ${rule.conditionText}`, kind: "media", query: rule.conditionText, name: "" } : {
1148
+ key: `@container ${rule.conditionText}`,
1149
+ kind: "container",
1150
+ query: rule.containerQuery,
1151
+ name: rule.containerName
1152
+ };
1153
+ if (!conditions.has(cond.key))
1154
+ conditions.set(cond.key, { query: cond.query, selectors: new Set });
1155
+ if (parent && hits(parent)) {
1156
+ conditions.get(cond.key).selectors.add(parent);
1157
+ stamp(cond, parent);
1158
+ }
1159
+ }
1160
+ if ("cssRules" in rule)
1161
+ walk(rule.cssRules, sel, cond);
1162
+ }
1163
+ };
1164
+ for (const sheet of Array.from(document.styleSheets)) {
1165
+ try {
1166
+ walk(sheet.cssRules, undefined, undefined);
1167
+ } catch {}
1168
+ }
1169
+ return {
1170
+ rules: [...conditions].map(([condition, c]) => ({ condition, query: c.query, selectors: [...c.selectors] })),
1171
+ found
1172
+ };
1173
+ }, { selector, mark: VALIDATION_MARK, conditionalMark: CONDITIONAL_MARK, containerMark: CONTAINER_MARK });
1174
+ const warnings = rules.filter((c) => c.selectors.length && !(breakpoints && widthQuery(c.query))).map(({ condition, selectors }) => ({
1175
+ source: "capture",
1176
+ level: "approximated",
1177
+ feature: condition.startsWith("@container") ? "container-query" : "media-query",
1178
+ message: `${condition} restyles ${selectors.length} rule(s) matching captured elements (${selectors.slice(0, 5).join(", ")}${selectors.length > 5 ? ", …" : ""}); the view keeps the layout captured at a ${viewportWidth}px viewport and does not switch`
1179
+ }));
1180
+ return { warnings, found };
1181
+ }
1182
+ function applyConditionMarks(root, found) {
1183
+ const hosts = new Map;
1184
+ const visit = (n) => {
1185
+ const marks = n.attrs[CONDITIONAL_MARK];
1186
+ if (marks !== undefined) {
1187
+ n.conditional = [...new Set(marks.split(" ").filter(Boolean).map(Number))].sort((a, b) => a - b);
1188
+ delete n.attrs[CONDITIONAL_MARK];
1189
+ }
1190
+ const serial = n.attrs[CONTAINER_MARK];
1191
+ if (serial !== undefined) {
1192
+ hosts.set(Number(serial), n);
1193
+ delete n.attrs[CONTAINER_MARK];
1194
+ }
1195
+ n.children.forEach(visit);
1196
+ };
1197
+ visit(root);
1198
+ return found.map((f) => {
1199
+ if (f.kind === "media")
1200
+ return { kind: "media", query: f.query, matches: f.matches ?? false };
1201
+ const host = f.container === undefined ? undefined : hosts.get(f.container);
1202
+ if (!host?.id)
1203
+ return { kind: "container", query: f.query, matches: false };
1204
+ const px = (p) => Number.parseFloat(host.styles[p] ?? "") || 0;
1205
+ const content = host.bounds.w - px("padding-left") - px("padding-right") - px("border-left-width") - px("border-right-width");
1206
+ const q = widthQuery(f.query);
1207
+ return {
1208
+ kind: "container",
1209
+ query: f.query,
1210
+ container: host.id,
1211
+ matches: q !== undefined && matchesWidth(q, content)
1212
+ };
1213
+ });
1214
+ }
1215
+
1216
+ // src/browser/images.ts
1217
+ function inlineCapturedImages(root, baseURL, images) {
1218
+ const warnings = new Set;
1219
+ const resolve = (source) => {
1220
+ if (!source || source.startsWith("data:") || source.startsWith("#"))
1221
+ return source;
1222
+ let url;
1223
+ try {
1224
+ url = new URL(source, baseURL).href;
1225
+ } catch {
1226
+ url = source;
1227
+ }
1228
+ const bytes = images.get(url);
1229
+ if (bytes)
1230
+ return bytes;
1231
+ warnings.add(`Image could not be embedded: ${url}; supply a data URL or a reachable image resource.`);
1232
+ return url;
1233
+ };
1234
+ const visit = (node) => {
1235
+ if (node.tag === "img") {
1236
+ const source = node.currentSrc ?? node.attrs.src;
1237
+ if (source)
1238
+ node.currentSrc = resolve(source);
1239
+ }
1240
+ const attr = node.tag === "video" ? "poster" : node.tag === "input" && node.attrs.type?.toLowerCase() === "image" ? "src" : "";
1241
+ if (node.attrs[attr])
1242
+ node.attrs[attr] = resolve(node.attrs[attr]);
1243
+ for (const property of ["background-image", "mask-image"]) {
1244
+ const value = node.styles[property];
1245
+ if (value)
1246
+ node.styles[property] = value.replace(/url\(\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([^)]*))\s*\)/g, (match, double, single, bare) => {
1247
+ const source = (double ?? single ?? bare ?? "").trim();
1248
+ if (source.startsWith("data:") || source.startsWith("#"))
1249
+ return match;
1250
+ return `url(${JSON.stringify(resolve(source))})`;
1251
+ });
1252
+ }
1253
+ node.children.forEach(visit);
1254
+ };
1255
+ visit(root);
1256
+ return [...warnings];
1257
+ }
1258
+
1259
+ // src/browser/states.ts
1260
+ var PAINT_KEYS = [
1261
+ "background-color",
1262
+ "background-image",
1263
+ "color",
1264
+ "-webkit-text-fill-color",
1265
+ "border-top-color",
1266
+ "border-right-color",
1267
+ "border-bottom-color",
1268
+ "border-left-color",
1269
+ "box-shadow",
1270
+ "outline-color",
1271
+ "outline-style",
1272
+ "outline-width",
1273
+ "outline-offset",
1274
+ "opacity",
1275
+ "transform",
1276
+ "scale",
1277
+ "translate",
1278
+ "rotate",
1279
+ "filter",
1280
+ "text-decoration-color"
1281
+ ];
1282
+ var WATCH = ["display", "visibility"];
1283
+ var STATE_ARGS = { computedStyles: [...PAINT_KEYS, ...WATCH], includeDOMRects: true };
1284
+ var KEEP = Object.fromEntries(STATE_ARGS.computedStyles.map((k) => [k, true]));
1285
+ async function forceStates(cdp, nodeIds, classes) {
1286
+ await Promise.all(nodeIds.map((nodeId) => cdp.send("CSS.forcePseudoState", { nodeId, forcedPseudoClasses: classes })));
1287
+ }
1288
+ function decode(snap, names, byIndex) {
1289
+ const doc = snap.documents[0];
1290
+ const out = new Map;
1291
+ doc.layout.nodeIndex.forEach((index, li) => {
1292
+ const styles = {};
1293
+ (doc.layout.styles[li] ?? []).forEach((v, k) => {
1294
+ if (v >= 0 && KEEP[names[k]])
1295
+ styles[names[k]] = snap.strings[v];
1296
+ });
1297
+ const key = byIndex ? index : doc.nodes.backendNodeId[index];
1298
+ out.set(key, { index, styles, offset: doc.layout.offsetRects?.[li] });
1299
+ });
1300
+ return out;
1301
+ }
1302
+ function diffStates(rest, state, ignoreAutoOutline) {
1303
+ const deltas = new Map;
1304
+ const [r, s] = [rest.documents[0]?.nodes, state.documents[0]?.nodes];
1305
+ if (!r || !s)
1306
+ return { deltas, moved: 0, revealed: 0 };
1307
+ const byIndex = !r.backendNodeId || !s.backendNodeId;
1308
+ if (byIndex && r.nodeType.length !== s.nodeType.length)
1309
+ return { deltas, moved: 0, revealed: 0 };
1310
+ const before = decode(rest, STYLE_WHITELIST, byIndex);
1311
+ const after = decode(state, STATE_ARGS.computedStyles, byIndex);
1312
+ let moved = 0;
1313
+ let revealed = 0;
1314
+ for (const [key, b] of after) {
1315
+ const a = before.get(key);
1316
+ if (!a || WATCH.some((k) => a.styles[k] !== b.styles[k])) {
1317
+ revealed++;
1318
+ continue;
1319
+ }
1320
+ if (a.offset && b.offset && a.offset.some((v, i) => Math.abs(v - b.offset[i]) > 0.5)) {
1321
+ moved++;
1322
+ continue;
1323
+ }
1324
+ const outlineAuto = ignoreAutoOutline && b.styles["outline-style"] === "auto";
1325
+ const changed = {};
1326
+ for (const k of PAINT_KEYS) {
1327
+ if (outlineAuto && k.startsWith("outline-"))
1328
+ continue;
1329
+ const v = b.styles[k];
1330
+ if (v !== undefined && v !== a.styles[k])
1331
+ changed[k] = v;
1332
+ }
1333
+ if (Object.keys(changed).length)
1334
+ deltas.set(a.index, changed);
1335
+ }
1336
+ return { deltas, moved, revealed };
1337
+ }
1338
+ async function captureStates(cdp, root, rest, motion) {
1339
+ const out = {
1340
+ pressed: new Map,
1341
+ focused: new Map,
1342
+ transitions: {},
1343
+ warnings: []
1344
+ };
1345
+ await cdp.send("DOM.enable");
1346
+ let css = false;
1347
+ try {
1348
+ const { root: doc } = await cdp.send("DOM.getDocument", { depth: 0 });
1349
+ const plan = [
1350
+ ["pressed", PRESSABLE_SELECTOR, ["hover", "active"], ":active"],
1351
+ ["focused", TEXT_ENTRY_SELECTOR, ["focus", "focus-visible"], ":focus-visible"]
1352
+ ];
1353
+ for (const [state, selector, classes, label] of plan) {
1354
+ const { nodeIds } = await cdp.send("DOM.querySelectorAll", {
1355
+ nodeId: doc.nodeId,
1356
+ selector: `${root} :is(${selector}), ${root}:is(${selector})`
1357
+ });
1358
+ if (!nodeIds.length)
1359
+ continue;
1360
+ if (!css)
1361
+ await cdp.send("CSS.enable");
1362
+ css = true;
1363
+ const snapshot = async () => {
1364
+ const snap = await cdp.send("DOMSnapshot.captureSnapshot", STATE_ARGS);
1365
+ const { deltas, moved, revealed } = diffStates(rest, snap, state === "focused");
1366
+ out[state] = deltas;
1367
+ if (moved > 0)
1368
+ out.warnings.push(`${moved} element(s) change layout under ${label}; their ${state} paint is not reproduced`);
1369
+ if (revealed > 0)
1370
+ out.warnings.push(`${revealed} element(s) appear or disappear under ${label}; not reproduced`);
1371
+ };
1372
+ if (!motion) {
1373
+ await forceStates(cdp, nodeIds, [...classes]);
1374
+ try {
1375
+ await snapshot();
1376
+ } finally {
1377
+ await forceStates(cdp, nodeIds, []);
1378
+ }
1379
+ continue;
1380
+ }
1381
+ const { page, sheet } = motion;
1382
+ const passes = [];
1383
+ for (const media of ["no-preference", "reduce"]) {
1384
+ await page.emulateMedia({ reducedMotion: media });
1385
+ await finishAnimations(page);
1386
+ await sheet.evaluate((s) => {
1387
+ s.disabled = true;
1388
+ });
1389
+ await forceStates(cdp, nodeIds, [...classes]);
1390
+ let into = {};
1391
+ let back = {};
1392
+ try {
1393
+ into = await readTransitions(page, motion.root);
1394
+ if (media === "reduce")
1395
+ await snapshot();
1396
+ } finally {
1397
+ await forceStates(cdp, nodeIds, []);
1398
+ back = await readTransitions(page, motion.root);
1399
+ await sheet.evaluate((s) => {
1400
+ s.disabled = false;
1401
+ });
1402
+ }
1403
+ passes.push({ into, back });
1404
+ }
1405
+ const [normal, reduce] = passes;
1406
+ const paths = new Set([normal, reduce].flatMap((p) => [...Object.keys(p.into), ...Object.keys(p.back)]));
1407
+ for (const path of paths) {
1408
+ const pair = { in: normal.into[path] ?? [], out: normal.back[path] ?? [] };
1409
+ const reduced = { in: reduce.into[path] ?? [], out: reduce.back[path] ?? [] };
1410
+ const differs = JSON.stringify(reduced) !== JSON.stringify(pair);
1411
+ (out.transitions[path] ??= {})[state] = motion.authorReduce || differs ? { ...pair, reduced } : pair;
1412
+ }
1413
+ }
1414
+ if (motion)
1415
+ await finishAnimations(motion.page);
1416
+ } finally {
1417
+ if (css)
1418
+ await cdp.send("CSS.disable");
1419
+ await cdp.send("DOM.disable");
1420
+ }
1421
+ return out;
1422
+ }
1423
+
1424
+ // src/browser/layers.ts
1425
+ var LAYER_MARK = "data-markswift-layer-root";
1426
+ var MAX_TRIGGERS = 12;
1427
+ var MAX_DISMISS_BUTTONS = 8;
1428
+ var ROOTS_ARGS = { computedStyles: ["opacity"], includePaintOrder: true };
1429
+ var PRE_ARGS = { computedStyles: [...PAINT_KEYS] };
1430
+ var TRIGGER_RULES = {
1431
+ "[popovertarget]": { activation: "click", relation: "popovertarget" },
1432
+ "button[commandfor]:is([command=show-modal i], [command=show-popover i], [command=toggle-popover i])": {
1433
+ activation: "click",
1434
+ relation: "commandfor"
1435
+ },
1436
+ "[interestfor]": { activation: "hover", relation: "interestfor" },
1437
+ "details:not([open]) > summary:first-of-type": { activation: "click", relation: "details" },
1438
+ "[data-markswift-opens]": { activation: "hint" },
1439
+ "[data-slot=tooltip-trigger], [data-slot=hover-card-trigger]": { activation: "hover", scripted: true },
1440
+ "[data-slot=context-menu-trigger]": { activation: "contextmenu", scripted: true },
1441
+ "[aria-haspopup]:not([aria-haspopup=false]), [aria-expanded=false]": { activation: "click", scripted: true }
1442
+ };
1443
+ function laidOut(snap) {
1444
+ const { nodes, layout } = snap.documents[0];
1445
+ const name = (i) => snap.strings[nodes.nodeName[i]];
1446
+ const closed = new Map;
1447
+ nodes.parentIndex.forEach((p, i) => {
1448
+ if (name(i) === "DETAILS" && !nodes.attributes[i].some((s, k) => k % 2 === 0 && snap.strings[s] === "open"))
1449
+ closed.set(i, undefined);
1450
+ if (p >= 0 && closed.has(p) && closed.get(p) === undefined && name(i) === "SUMMARY")
1451
+ closed.set(p, i);
1452
+ });
1453
+ const hidden = (i) => {
1454
+ for (let c = i, p = nodes.parentIndex[i];p >= 0; c = p, p = nodes.parentIndex[p])
1455
+ if (closed.has(p) && closed.get(p) !== c)
1456
+ return true;
1457
+ return false;
1458
+ };
1459
+ return new Set(layout.nodeIndex.filter((i) => !hidden(i)).map((i) => nodes.backendNodeId[i]));
1460
+ }
1461
+ function layerRoots(before, snap, exclude) {
1462
+ const { nodes, layout } = snap.documents[0];
1463
+ const ids = nodes.backendNodeId;
1464
+ const pseudo = new Set(nodes.pseudoType?.index);
1465
+ const now = laidOut(snap);
1466
+ const fresh = new Set;
1467
+ const area = new Set;
1468
+ const entry = new Map;
1469
+ layout.nodeIndex.forEach((i, li) => {
1470
+ if (now.has(ids[i]) && !before.has(ids[i]))
1471
+ fresh.add(i);
1472
+ if (!entry.has(i))
1473
+ entry.set(i, li);
1474
+ const [, , w, h] = layout.bounds[li];
1475
+ if (w > 0 && h > 0)
1476
+ for (let p = i;p >= 0 && !area.has(p); p = nodes.parentIndex[p])
1477
+ area.add(p);
1478
+ });
1479
+ const paint = (i) => layout.paintOrders?.[entry.get(i)] ?? 0;
1480
+ return [...fresh].filter((i) => nodes.nodeType[i] === 1 && !pseudo.has(i) && !fresh.has(nodes.parentIndex[i]) && !exclude.has(ids[i]) && area.has(i) && snap.strings[layout.styles[entry.get(i)]?.[0] ?? -1] !== "0").sort((a, b) => paint(a) - paint(b)).map((i) => ids[i]);
1481
+ }
1482
+ function outsidePoint(rects, viewport) {
1483
+ const { width: w, height: h } = viewport;
1484
+ return [
1485
+ { x: 2, y: 2 },
1486
+ { x: w - 2, y: 2 },
1487
+ { x: 2, y: h - 2 },
1488
+ { x: w - 2, y: h - 2 }
1489
+ ].find((p) => !rects.some((r) => p.x >= r.x && p.x <= r.x + r.w && p.y >= r.y && p.y <= r.y + r.h));
1490
+ }
1491
+ function openDelta(rest, open) {
1492
+ const keys = PAINT_KEYS.filter((k) => open[k] !== undefined && open[k] !== rest[k]);
1493
+ return keys.length ? Object.fromEntries(keys.map((k) => [k, open[k]])) : undefined;
1494
+ }
1495
+ function layerIds(names) {
1496
+ const used = new Set;
1497
+ return names.map((name) => {
1498
+ const words = name.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().split(/[^a-z0-9]+/);
1499
+ const base = words.filter(Boolean).slice(0, 4).join("-") || "layer";
1500
+ let id = base;
1501
+ for (let n = 2;used.has(id); n++)
1502
+ id = `${base}-${n}`;
1503
+ used.add(id);
1504
+ return id;
1505
+ });
1506
+ }
1507
+ async function findTriggers(page, root) {
1508
+ const found = await page.evaluate(({ root, rules }) => {
1509
+ const top = document.querySelector(root);
1510
+ const scripted = document.scripts.length > 0 || [...document.querySelectorAll("*")].some((e) => [...e.attributes].some((a) => a.name.startsWith("on")));
1511
+ const matched = [];
1512
+ const triggers = [];
1513
+ const warnings = [];
1514
+ for (const el of top.querySelectorAll("*")) {
1515
+ if (matched.some((m) => m.contains(el)))
1516
+ continue;
1517
+ const rule = rules.find(([sel, r]) => (!r.scripted || scripted) && el.matches(sel))?.[1];
1518
+ if (!rule || !el.getClientRects().length)
1519
+ continue;
1520
+ if (el.matches(":disabled") || el.getAttribute("aria-disabled") === "true")
1521
+ continue;
1522
+ let activation = rule.activation;
1523
+ if (activation === "hint") {
1524
+ activation = el.getAttribute("data-markswift-opens") ?? "";
1525
+ if (!["click", "hover", "contextmenu"].includes(activation)) {
1526
+ warnings.push(`data-markswift-opens="${activation}" is not click, hover or contextmenu; the element is not opened`);
1527
+ continue;
1528
+ }
1529
+ }
1530
+ matched.push(el);
1531
+ const steps = [];
1532
+ for (let e = el;e !== top; e = e.parentElement)
1533
+ steps.unshift([...e.parentElement.children].indexOf(e));
1534
+ triggers.push({
1535
+ path: steps.join("/"),
1536
+ activation,
1537
+ ...rule.relation ? { relation: rule.relation } : {},
1538
+ name: el.getAttribute("aria-label")?.trim() || el.textContent?.trim() || el.getAttribute("title")?.trim() || ""
1539
+ });
1540
+ }
1541
+ return { triggers, warnings };
1542
+ }, { root, rules: Object.entries(TRIGGER_RULES) });
1543
+ const ids = layerIds(found.triggers.map((t) => t.name));
1544
+ return {
1545
+ triggers: found.triggers.map(({ name: _, ...t }, i) => ({
1546
+ ...t,
1547
+ id: ids[i]
1548
+ })),
1549
+ warnings: found.warnings
1550
+ };
1551
+ }
1552
+ async function activate(page, root, trigger) {
1553
+ const at = await page.evaluate(({ root, path }) => {
1554
+ let el = document.querySelector(root) ?? undefined;
1555
+ for (const step of path ? path.split("/") : [])
1556
+ el = el?.children[Number(step)];
1557
+ if (!el)
1558
+ return;
1559
+ el.scrollIntoView({ block: "center", inline: "center" });
1560
+ const r = el.getBoundingClientRect();
1561
+ return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
1562
+ }, { root, path: trigger.path });
1563
+ if (!at)
1564
+ return false;
1565
+ await recordAnimations(page);
1566
+ if (trigger.activation === "hover") {
1567
+ const mutated = await page.evaluateHandle(() => {
1568
+ const { promise, resolve } = Promise.withResolvers();
1569
+ const o = new MutationObserver(() => {
1570
+ o.disconnect();
1571
+ resolve();
1572
+ });
1573
+ o.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
1574
+ setTimeout(resolve, 1000);
1575
+ return { done: promise };
1576
+ });
1577
+ await page.mouse.move(at.x, at.y);
1578
+ await mutated.evaluate((m) => m.done);
1579
+ await mutated.dispose();
1580
+ } else {
1581
+ await page.mouse.click(at.x, at.y, { button: trigger.activation === "contextmenu" ? "right" : "left" });
1582
+ await page.mouse.move(0, 0);
1583
+ }
1584
+ await page.evaluate(async () => {
1585
+ const frame = Promise.withResolvers();
1586
+ requestAnimationFrame(() => requestAnimationFrame(() => frame.resolve()));
1587
+ await frame.promise;
1588
+ scrollTo(0, 0);
1589
+ });
1590
+ return true;
1591
+ }
1592
+ var snapshot = async (cdp, args) => await cdp.send("DOMSnapshot.captureSnapshot", args);
1593
+ var rootHidden = (page, root) => page.evaluate((root) => !!document.querySelector(root)?.closest("[aria-hidden=true], [inert]"), root);
1594
+ async function openLayer(page, cdp, root, rootMark, trigger) {
1595
+ const pre = await snapshot(cdp, PRE_ARGS);
1596
+ const before = laidOut(pre);
1597
+ const { nodes, layout } = pre.documents[0];
1598
+ const rest = new Map(layout.nodeIndex.map((i, li) => [
1599
+ nodes.backendNodeId[i],
1600
+ Object.fromEntries(PAINT_KEYS.flatMap((k, s) => layout.styles[li][s] >= 0 ? [[k, pre.strings[layout.styles[li][s]]]] : []))
1601
+ ]));
1602
+ const hidden = await rootHidden(page, root);
1603
+ if (!await activate(page, root, trigger))
1604
+ return;
1605
+ let open = await snapshot(cdp, ROOTS_ARGS);
1606
+ const triggerRaw = rawAt(findByAttr(snapshotToTree(open), rootMark, "1"), trigger.path);
1607
+ const exclude = new Set;
1608
+ const collect = (r) => {
1609
+ if (r.backend !== undefined)
1610
+ exclude.add(r.backend);
1611
+ r.children.forEach(collect);
1612
+ };
1613
+ if (triggerRaw)
1614
+ collect(triggerRaw);
1615
+ const mark = async (ids) => {
1616
+ await page.evaluate((m) => {
1617
+ for (const e of document.querySelectorAll(`[${m}]`))
1618
+ e.removeAttribute(m);
1619
+ }, LAYER_MARK);
1620
+ await cdp.send("DOM.getDocument", { depth: 0 });
1621
+ for (const [i, backendNodeId] of ids.entries()) {
1622
+ const { object } = await cdp.send("DOM.resolveNode", { backendNodeId });
1623
+ await cdp.send("Runtime.callFunctionOn", {
1624
+ objectId: object.objectId,
1625
+ functionDeclaration: "function (m, i) { this.setAttribute(m, i); }",
1626
+ arguments: [{ value: LAYER_MARK }, { value: String(i) }]
1627
+ });
1628
+ }
1629
+ };
1630
+ const roots = layerRoots(before, open, exclude);
1631
+ await mark(roots);
1632
+ const isOpen = async () => {
1633
+ open = await snapshot(cdp, ROOTS_ARGS);
1634
+ return layerRoots(before, open, exclude).length > 0;
1635
+ };
1636
+ return {
1637
+ rest,
1638
+ hidden,
1639
+ roots,
1640
+ isOpen,
1641
+ async reopen() {
1642
+ if (!await isOpen()) {
1643
+ await activate(page, root, trigger);
1644
+ await settle(page);
1645
+ open = await snapshot(cdp, ROOTS_ARGS);
1646
+ }
1647
+ await mark(layerRoots(before, open, exclude));
1648
+ }
1649
+ };
1650
+ }
1651
+ function dismissable(raw) {
1652
+ if (!raw.bounds || raw.bounds.w <= 0 || raw.bounds.h <= 0)
1653
+ return false;
1654
+ const role = raw.attrs.role?.trim().toLowerCase() ?? "";
1655
+ if (role === "button" || role === "option" || role.startsWith("menuitem"))
1656
+ return true;
1657
+ if (raw.name === "a")
1658
+ return raw.attrs.href === undefined;
1659
+ if (raw.name !== "button" || (raw.attrs.type?.toLowerCase() ?? "submit") !== "submit")
1660
+ return raw.name === "button";
1661
+ let form = raw.parent;
1662
+ while (form && form.name !== "form")
1663
+ form = form.parent;
1664
+ return !form || form.attrs.method?.toLowerCase() === "dialog";
1665
+ }
1666
+ function dismissCandidates(roots, paths, identity) {
1667
+ const out = [];
1668
+ roots.forEach((root, i) => {
1669
+ const walk = (raw, path) => {
1670
+ const full = [paths[i], path].filter(Boolean).join("/");
1671
+ if (dismissable(raw))
1672
+ out.push({ root: i, path, id: identity(raw.name, raw.attrs, full) ?? full });
1673
+ for (const [k, c] of pathChildren(raw).entries())
1674
+ walk(c, path ? `${path}/${k}` : String(k));
1675
+ };
1676
+ walk(root, "");
1677
+ });
1678
+ return out.slice(0, MAX_DISMISS_BUTTONS);
1679
+ }
1680
+ function assembleLayer(o) {
1681
+ const { trigger, marked } = o;
1682
+ const inside = marked.map((raw) => pathOf(o.rootRaw, raw));
1683
+ const paths = inside.map((p, i) => p ?? (i ? `${trigger.id}/${i}` : trigger.id));
1684
+ const nodes = marked.map((raw, i) => toLayout(raw, o.origin, o.identity, paths[i]));
1685
+ const any = (n, f) => f(n) || n.children.some((c) => any(c, f));
1686
+ const fixed = (raw) => raw.styles.position === "fixed";
1687
+ const placement = marked.every((raw, i) => inside[i] !== undefined && raw.styles.overlay !== "auto" && !fixed(raw)) ? "inflow" : o.info.some((r) => r.anchored) || marked.some((raw) => fixed(raw) && Math.abs(raw.bounds.x) < 0.5 && Math.abs(raw.bounds.y) < 0.5 && (raw.styles.transform !== "none" || raw.styles.translate !== "none")) ? "anchored" : "viewport";
1688
+ const triggerAttrs = rawAt(o.rootRaw, trigger.path)?.attrs ?? {};
1689
+ const inLayer = (attr) => {
1690
+ const ids = new Set(triggerAttrs[attr]?.split(/\s+/).filter(Boolean));
1691
+ return ids.size > 0 && nodes.some((n) => any(n, (d) => ids.has(d.attrs.id ?? "")));
1692
+ };
1693
+ const relation = trigger.relation ?? (inLayer("aria-controls") ? "aria-controls" : inLayer("aria-describedby") ? "aria-describedby" : "dom-diff");
1694
+ const layer = {
1695
+ id: trigger.id,
1696
+ trigger: trigger.trigger,
1697
+ activation: trigger.activation,
1698
+ relation,
1699
+ modal: o.info.some((r) => r.modal) || o.hiddenOnOpen || nodes.some((n) => any(n, (d) => d.attrs["aria-modal"] === "true")),
1700
+ placement,
1701
+ nodes,
1702
+ dismiss: { escape: false, outside: false, buttons: [] }
1703
+ };
1704
+ if (placement === "inflow") {
1705
+ const parent = (n) => n.children.some((c) => c.id === nodes[0].id) ? n : n.children.map(parent).find(Boolean);
1706
+ const p = parent(o.root);
1707
+ if (p?.id !== undefined) {
1708
+ layer.parent = p.id;
1709
+ layer.index = p.children.findIndex((c) => c.id === nodes[0].id);
1710
+ }
1711
+ }
1712
+ return { layer, paths };
1713
+ }
1714
+ function triggerPaint(rest, rootRaw, trigger, identity) {
1715
+ const paint = {};
1716
+ let changed = 0;
1717
+ const walk = (raw, path) => {
1718
+ const before = raw.backend === undefined ? undefined : rest.get(raw.backend);
1719
+ if (!before !== !raw.bounds)
1720
+ changed++;
1721
+ else if (before) {
1722
+ const delta = openDelta(before, raw.styles);
1723
+ const id = identity(raw.name, raw.attrs, path);
1724
+ if (delta && id !== undefined)
1725
+ paint[id] = delta;
1726
+ }
1727
+ for (const [k, c] of pathChildren(raw).entries())
1728
+ walk(c, path ? `${path}/${k}` : String(k));
1729
+ };
1730
+ const raw = rawAt(rootRaw, trigger.path);
1731
+ if (raw)
1732
+ walk(raw, trigger.path);
1733
+ return { paint, changed };
1734
+ }
1735
+ async function probeDismiss(page, opening, o) {
1736
+ await opening.reopen();
1737
+ const rects = await page.evaluate(({ mark, root, path, vw, vh }) => {
1738
+ const box = (e) => {
1739
+ const r = e.getBoundingClientRect();
1740
+ return { x: r.x, y: r.y, w: r.width, h: r.height };
1741
+ };
1742
+ const rs = [...document.querySelectorAll(`[${mark}]`)].map(box).filter((r) => !(r.x <= 0 && r.y <= 0 && r.x + r.w >= vw && r.y + r.h >= vh));
1743
+ let el = document.querySelector(root) ?? undefined;
1744
+ for (const step of path ? path.split("/") : [])
1745
+ el = el?.children[Number(step)];
1746
+ return el ? [...rs, box(el)] : rs;
1747
+ }, { mark: LAYER_MARK, root: o.root, path: o.trigger.path, vw: o.viewport.width, vh: o.viewport.height });
1748
+ const at = outsidePoint(rects, o.viewport);
1749
+ let outside = false;
1750
+ if (at) {
1751
+ await page.mouse.click(at.x, at.y);
1752
+ await settle(page);
1753
+ outside = !await opening.isOpen();
1754
+ } else
1755
+ o.warn(`layer ${o.trigger.id} covers every viewport corner; its outside-click dismissal was not probed`);
1756
+ const buttons = [];
1757
+ for (const c of o.candidates) {
1758
+ await opening.reopen();
1759
+ const point = await page.evaluate(({ sel, path }) => {
1760
+ let el = document.querySelector(sel) ?? undefined;
1761
+ for (const step of path ? path.split("/") : [])
1762
+ el = el?.children[Number(step)];
1763
+ if (!el)
1764
+ return;
1765
+ const r = el.getBoundingClientRect();
1766
+ return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
1767
+ }, { sel: `[${LAYER_MARK}="${c.root}"]`, path: c.path });
1768
+ if (!point)
1769
+ continue;
1770
+ await page.mouse.click(point.x, point.y);
1771
+ await settle(page);
1772
+ if (!await opening.isOpen())
1773
+ buttons.push(c.id);
1774
+ }
1775
+ return { escape: o.escape, outside, buttons };
1776
+ }
1777
+ async function captureLayers(capture, spec, rest, { probeDismiss }) {
1778
+ const triggers = rest.triggers ?? [];
1779
+ const warn = (message) => rest.warnings.push({ source: "capture", message });
1780
+ if (triggers.length > MAX_TRIGGERS) {
1781
+ const skipped = triggers.slice(MAX_TRIGGERS);
1782
+ warn(`${skipped.length} open-state trigger(s) beyond the first ${MAX_TRIGGERS} were not captured: ${skipped.slice(0, 5).map((t) => t.id).join(", ")}${skipped.length > 5 ? ", …" : ""}`);
1783
+ }
1784
+ const byId = new Map;
1785
+ const index = (n) => {
1786
+ if (n.id !== undefined)
1787
+ byId.set(n.id, n);
1788
+ n.children.forEach(index);
1789
+ };
1790
+ index(rest.layout.root);
1791
+ const layers = [];
1792
+ const shots = {};
1793
+ for (const trigger of triggers.slice(0, MAX_TRIGGERS)) {
1794
+ const open = await capture({ ...spec, open: trigger, probeDismiss });
1795
+ for (const w of open.warnings)
1796
+ if (!rest.warnings.some((r) => r.message === w.message))
1797
+ rest.warnings.push(w);
1798
+ if (!open.opened)
1799
+ continue;
1800
+ layers.push(open.opened.layer);
1801
+ shots[open.opened.layer.id] = { ...open.png ? { png: open.png } : {}, size: open.opened.size, trigger };
1802
+ for (const [id, delta] of Object.entries(open.opened.openPaint)) {
1803
+ const node = byId.get(id);
1804
+ if (node)
1805
+ node.states = { ...node.states, open: delta };
1806
+ }
1807
+ }
1808
+ if (layers.length)
1809
+ rest.layout.layers = layers;
1810
+ return shots;
1811
+ }
1812
+
1813
+ // src/browser/capture.ts
1814
+ var ROOT_MARK = "data-markswift-root-capture";
1815
+ var TRANSFORMED_MARK = "data-markswift-transformed";
1816
+ var RELATIVE_MARK = "data-markswift-relative";
1817
+ var NO_TRANSFORM = "transform: none !important; translate: none !important; rotate: none !important; scale: none !important;";
1818
+ var requestURLs = (r) => {
1819
+ const urls = [r.url()];
1820
+ for (let q = r.request().redirectedFrom();q; q = q.redirectedFrom())
1821
+ urls.push(q.url());
1822
+ return urls;
1823
+ };
1824
+ var UNCOMPILED_TAILWIND = /@tailwind\s+(base|components|utilities|variants)\b|@import\s+(url\(\s*)?["']tailwindcss["']|@apply\s/;
1825
+ function sniffImage(b) {
1826
+ if (b[0] === 137 && b.toString("latin1", 1, 4) === "PNG")
1827
+ return "image/png";
1828
+ if (b[0] === 255 && b[1] === 216 && b[2] === 255)
1829
+ return "image/jpeg";
1830
+ if (b.toString("latin1", 0, 4) === "GIF8")
1831
+ return "image/gif";
1832
+ if (b.toString("latin1", 0, 4) === "RIFF" && b.toString("latin1", 8, 12) === "WEBP")
1833
+ return "image/webp";
1834
+ return;
1835
+ }
1836
+ async function markTransformed(page) {
1837
+ await page.evaluate(([transformed, relative]) => {
1838
+ for (const el of document.querySelectorAll("*")) {
1839
+ const cs = getComputedStyle(el);
1840
+ if (cs.transform !== "none" || cs.translate !== "none" || cs.rotate !== "none" || cs.scale !== "none")
1841
+ el.setAttribute(transformed, "");
1842
+ if (cs.position === "relative")
1843
+ el.setAttribute(relative, "");
1844
+ }
1845
+ }, [TRANSFORMED_MARK, RELATIVE_MARK]);
1846
+ }
1847
+ async function cancelAncestorTransforms(page) {
1848
+ await page.evaluate((css) => {
1849
+ const s = document.createElement("style");
1850
+ s.textContent = css;
1851
+ document.body.appendChild(s);
1852
+ }, `:has([${ROOT_MARK}]) { ${NO_TRANSFORM} }
1853
+ [${TRANSFORMED_MARK}]:has([${ROOT_MARK}]) { will-change: transform !important; }
1854
+ [${RELATIVE_MARK}]:has([${ROOT_MARK}]) { inset: auto !important; }`);
1855
+ }
1856
+ async function noTransitions(page) {
1857
+ const sheet = await page.addStyleTag({ content: "* { transition: none !important; }" });
1858
+ await sheet.evaluate((s) => s.setAttribute("data-markswift-harness", ""));
1859
+ return sheet;
1860
+ }
1861
+
1862
+ class BrowserCapture {
1863
+ browser;
1864
+ context;
1865
+ stopping;
1866
+ opts;
1867
+ constructor(opts) {
1868
+ this.opts = opts;
1869
+ }
1870
+ async start() {
1871
+ const args = this.opts.restricted ? ["--host-resolver-rules=MAP * ~NOTFOUND"] : [];
1872
+ this.browser = await chromium.launch({ headless: true, channel: this.opts.channel ?? "chrome", args });
1873
+ this.context = await this.browser.newContext({
1874
+ viewport: this.opts.viewport,
1875
+ deviceScaleFactor: this.opts.dpr,
1876
+ reducedMotion: "reduce"
1877
+ });
1878
+ }
1879
+ get version() {
1880
+ return this.browser.version();
1881
+ }
1882
+ stop() {
1883
+ if (!this.browser)
1884
+ return Promise.resolve();
1885
+ this.stopping ??= (async () => {
1886
+ try {
1887
+ await this.context?.close();
1888
+ } finally {
1889
+ await this.browser.close();
1890
+ }
1891
+ })();
1892
+ return this.stopping;
1893
+ }
1894
+ async capture(spec, identity, source) {
1895
+ const page = await this.context.newPage();
1896
+ const viewport = spec.viewport ?? this.opts.viewport;
1897
+ const errors = [];
1898
+ const images = new Map;
1899
+ const imageReads = [];
1900
+ const sheets = new Map;
1901
+ const sheetReads = [];
1902
+ const favicon = (url) => url.endsWith("/favicon.ico");
1903
+ page.on("pageerror", (e) => errors.push(String(e)));
1904
+ page.on("console", (m) => {
1905
+ if (m.type() === "error" && !favicon(m.location().url))
1906
+ errors.push(m.text());
1907
+ });
1908
+ page.on("requestfailed", (r) => errors.push(`request failed: ${r.url()}`));
1909
+ page.on("response", (r) => {
1910
+ if (r.status() >= 400 && !favicon(r.url()))
1911
+ errors.push(`${r.status()} ${r.url()}`);
1912
+ const mime = r.headers()["content-type"]?.split(";")[0];
1913
+ if (r.ok() && (mime?.startsWith("image/") || r.request().resourceType() === "image")) {
1914
+ imageReads.push(r.body().then((bytes) => {
1915
+ const type = mime?.startsWith("image/") ? mime : sniffImage(bytes);
1916
+ if (!type) {
1917
+ errors.push(`Image response is not an image: ${r.url()}`);
1918
+ return;
1919
+ }
1920
+ for (const u of requestURLs(r))
1921
+ images.set(u, `data:${type};base64,${bytes.toString("base64")}`);
1922
+ }).catch(() => {
1923
+ errors.push(`Could not capture image bytes: ${r.url()}`);
1924
+ }));
1925
+ }
1926
+ if (r.ok() && r.request().resourceType() === "stylesheet") {
1927
+ sheetReads.push(r.text().then((text) => {
1928
+ for (const u of requestURLs(r))
1929
+ sheets.set(u, text);
1930
+ }).catch(() => {
1931
+ errors.push(`Could not read stylesheet: ${r.url()}`);
1932
+ }));
1933
+ }
1934
+ });
1935
+ try {
1936
+ await this.load(page, spec);
1937
+ const noTransition = await noTransitions(page);
1938
+ const motion = await readMotion(page, spec.root);
1939
+ errors.push(...motion.warnings);
1940
+ await settle(page);
1941
+ const cdp = await this.context.newCDPSession(page);
1942
+ await Promise.all(sheetReads);
1943
+ const { faces: fonts, unreadable } = await collectFontFaces(page, Object.fromEntries(sheets));
1944
+ for (const href of unreadable)
1945
+ errors.push(`Stylesheet could not be read: ${href}; its @font-face rules were not collected.`);
1946
+ const inlineStyles = await page.evaluate(() => [...document.querySelectorAll("style:not([data-markswift-harness])")].map((s) => s.textContent ?? ""));
1947
+ const tailwind = new Set;
1948
+ for (const text of inlineStyles)
1949
+ if (UNCOMPILED_TAILWIND.test(text))
1950
+ tailwind.add("inline <style>");
1951
+ for (const [url, text] of sheets)
1952
+ if (UNCOMPILED_TAILWIND.test(text))
1953
+ tailwind.add(url);
1954
+ for (const label of tailwind)
1955
+ errors.push(`Stylesheet ${label} contains uncompiled Tailwind directives (@tailwind / @import "tailwindcss" / @apply); compile it first (for example with the Tailwind CLI) — the capture saw unstyled markup.`);
1956
+ const fontFiles = await readFontBytes(fonts, !this.opts.restricted, this.opts.signal);
1957
+ const canvas = await page.evaluate(({ sel, mark }) => {
1958
+ const el = document.querySelector(sel);
1959
+ if (!el)
1960
+ throw new Error(`capture root ${sel} not found`);
1961
+ el.setAttribute(mark, "1");
1962
+ let canvas = "rgb(255, 255, 255)";
1963
+ for (let p = el;p; p = p.parentElement) {
1964
+ const bg = getComputedStyle(p).backgroundColor;
1965
+ if (!bg || bg === "transparent" || bg === "none")
1966
+ continue;
1967
+ const legacy = /^rgba?\(([^)]+)\)$/.exec(bg);
1968
+ const slash = /\/\s*([\d.]+)(%?)\s*\)\s*$/.exec(bg);
1969
+ const alpha = legacy ? parseFloat((legacy[1].split(",")[3] ?? "1").trim()) : slash ? parseFloat(slash[1]) / (slash[2] ? 100 : 1) : 1;
1970
+ if (alpha > 0.5) {
1971
+ canvas = bg;
1972
+ break;
1973
+ }
1974
+ }
1975
+ return canvas;
1976
+ }, { sel: spec.root, mark: ROOT_MARK });
1977
+ const rootSel = `[${ROOT_MARK}="1"]`;
1978
+ const found = spec.open ? undefined : await findTriggers(page, rootSel);
1979
+ errors.push(...found?.warnings ?? []);
1980
+ const opening = spec.open && await openLayer(page, cdp, rootSel, ROOT_MARK, spec.open);
1981
+ if (spec.open && !opening)
1982
+ errors.push(`trigger ${spec.open.id} is not in its open-state page; nothing opened`);
1983
+ const roots = await Promise.all((opening ? opening.roots : []).map(async (_, i) => await page.$(`[${LAYER_MARK}="${i}"]`)));
1984
+ const enter = roots.length ? await readElementAnimations(page, roots) : { lists: [], warnings: [] };
1985
+ errors.push(...enter.warnings);
1986
+ let animated = enter.lists.some((l) => l.length);
1987
+ if (opening) {
1988
+ await settle(page);
1989
+ await page.evaluate(() => document.activeElement?.blur());
1990
+ await settle(page);
1991
+ }
1992
+ const rootInfo = roots.length ? await page.evaluate((mark) => [...document.querySelectorAll(`[${mark}]`)].sort((a, b) => Number(a.getAttribute(mark)) - Number(b.getAttribute(mark))).map((el) => {
1993
+ const cs = getComputedStyle(el);
1994
+ return {
1995
+ modal: el.matches(":modal"),
1996
+ anchored: cs.getPropertyValue("position-area") !== "none" || cs.getPropertyValue("position-anchor") !== "normal"
1997
+ };
1998
+ }), LAYER_MARK) : [];
1999
+ const hiddenOnOpen = !!opening && roots.length > 0 && !opening.hidden && await rootHidden(page, rootSel);
2000
+ const conditional = await readConditionalRules(page, spec.root, viewport.width, spec.breakpoints);
2001
+ const { sizes, pseudoStyles } = await readPreferredSizes(page, spec.root);
2002
+ const layerSizes = await Promise.all(roots.map((_, i) => readPreferredSizes(page, `[${LAYER_MARK}="${i}"]`)));
2003
+ const snapArgs = { computedStyles: [...STYLE_WHITELIST], includePaintOrder: true, includeDOMRects: true };
2004
+ const snapA = await cdp.send("DOMSnapshot.captureSnapshot", snapArgs);
2005
+ const states = await captureStates(cdp, spec.open ? `[${LAYER_MARK}]` : rootSel, snapA, spec.open ? undefined : { page, sheet: noTransition, root: spec.root, authorReduce: motion.authorReduce });
2006
+ errors.push(...states.warnings);
2007
+ await markTransformed(page);
2008
+ await page.evaluate((css) => {
2009
+ const s = document.createElement("style");
2010
+ s.id = "__markswift_notransform";
2011
+ s.textContent = css;
2012
+ document.body.appendChild(s);
2013
+ }, `* { ${NO_TRANSFORM} }
2014
+ [${TRANSFORMED_MARK}] { will-change: transform !important; }
2015
+ [${RELATIVE_MARK}] { inset: auto !important; }`);
2016
+ const snapB = await cdp.send("DOMSnapshot.captureSnapshot", snapArgs);
2017
+ await page.evaluate(() => document.getElementById("__markswift_notransform")?.remove());
2018
+ const tree = snapshotToTree(snapA, snapB, states);
2019
+ const rootRaw = findByAttr(tree, ROOT_MARK, "1");
2020
+ if (!rootRaw)
2021
+ throw new Error(`capture root ${spec.root} not in snapshot`);
2022
+ attachPreferredSizes(rootRaw, sizes, pseudoStyles);
2023
+ for (const [path, transitions] of Object.entries(states.transitions))
2024
+ motion.motion[path] = { ...motion.motion[path], transitions };
2025
+ attachMotion(rootRaw, motion.motion);
2026
+ delete rootRaw.attrs[ROOT_MARK];
2027
+ const box = rootRaw.bounds;
2028
+ const root = box && toLayout(rootRaw, { x: box.x, y: box.y }, identity);
2029
+ if (!box || !root)
2030
+ throw new Error(`capture root ${spec.root} has no layout`);
2031
+ const conditions = applyConditionMarks(root, conditional.found);
2032
+ const size = { w: Math.floor(box.w), h: Math.floor(box.h) };
2033
+ const triggers = found?.triggers.map((t) => {
2034
+ const raw = rawAt(rootRaw, t.path);
2035
+ return { ...t, trigger: identity(raw.name, raw.attrs, t.path) ?? t.path };
2036
+ });
2037
+ let opened;
2038
+ let candidates = [];
2039
+ if (spec.open && opening && roots.length) {
2040
+ const marked = roots.map((_, i) => findByAttr(tree, LAYER_MARK, String(i)));
2041
+ marked.forEach((raw, i) => {
2042
+ delete raw.attrs[LAYER_MARK];
2043
+ attachPreferredSizes(raw, layerSizes[i].sizes, layerSizes[i].pseudoStyles);
2044
+ });
2045
+ const { layer, paths } = assembleLayer({
2046
+ trigger: spec.open,
2047
+ rootRaw,
2048
+ root,
2049
+ marked,
2050
+ origin: { x: box.x, y: box.y },
2051
+ identity,
2052
+ info: rootInfo,
2053
+ hiddenOnOpen
2054
+ });
2055
+ enter.lists.forEach((list, i) => {
2056
+ if (list.length)
2057
+ layer.nodes[i].motion = { ...layer.nodes[i].motion, enter: list };
2058
+ });
2059
+ const { paint, changed } = triggerPaint(opening.rest, rootRaw, spec.open, identity);
2060
+ if (changed)
2061
+ errors.push(`${changed} element(s) inside trigger ${spec.open.id} appear or disappear while its layer is open; not reproduced`);
2062
+ if (spec.probeDismiss && layer.placement !== "inflow")
2063
+ candidates = dismissCandidates(marked, paths, identity);
2064
+ opened = { layer, size, openPaint: paint };
2065
+ }
2066
+ let png;
2067
+ if (spec.screenshot) {
2068
+ const { x, y, w, h } = box;
2069
+ if (!(size.w > 0 && size.h > 0) || x + w <= 0 || y + h <= 0)
2070
+ throw new Error(`capture root ${spec.root} has an empty or off-page box (${w}×${h} at ${x},${y}); nothing to screenshot`);
2071
+ await cancelAncestorTransforms(page);
2072
+ png = await withFrozenProgress(page, cdp, (timeout) => page.screenshot({
2073
+ clip: { x, y, width: size.w, height: size.h },
2074
+ scale: "device",
2075
+ animations: "disabled",
2076
+ caret: "hide",
2077
+ type: "png",
2078
+ timeout
2079
+ }));
2080
+ }
2081
+ const readExit = async (layer) => {
2082
+ const exit = await readElementAnimations(page, roots);
2083
+ errors.push(...exit.warnings);
2084
+ exit.lists.forEach((list, i) => {
2085
+ if (list.length)
2086
+ layer.nodes[i].motion = { ...layer.nodes[i].motion, exit: list };
2087
+ });
2088
+ animated ||= exit.lists.some((l) => l.length);
2089
+ };
2090
+ if (opened && opening && spec.open && spec.probeDismiss && opened.layer.placement !== "inflow") {
2091
+ await recordAnimations(page);
2092
+ await page.keyboard.press("Escape");
2093
+ await readExit(opened.layer);
2094
+ await settle(page);
2095
+ opened.layer.dismiss = await probeDismiss(page, opening, {
2096
+ root: rootSel,
2097
+ trigger: spec.open,
2098
+ candidates,
2099
+ viewport,
2100
+ escape: !await opening.isOpen(),
2101
+ warn: (message) => errors.push(message)
2102
+ });
2103
+ }
2104
+ if (opened && spec.open && spec.probeDismiss && opened.layer.placement === "inflow") {
2105
+ await activate(page, rootSel, spec.open);
2106
+ await readExit(opened.layer);
2107
+ }
2108
+ if (animated && motion.authorReduce)
2109
+ errors.push("overlay enter/exit animations follow the Reduce Motion fallback (author prefers-reduced-motion rules for them are not captured)");
2110
+ await cdp.detach();
2111
+ await Promise.all(imageReads);
2112
+ const baseURI = await page.evaluate(() => document.baseURI);
2113
+ for (const node of [root, ...opened?.layer.nodes ?? []])
2114
+ errors.push(...inlineCapturedImages(node, baseURI, images));
2115
+ return {
2116
+ warnings: [...errors.map((message) => ({ source: "capture", message })), ...conditional.warnings],
2117
+ layout: {
2118
+ version: LAYOUT_DOC_VERSION,
2119
+ source,
2120
+ combo: { screen: spec.screen, theme: spec.theme },
2121
+ size,
2122
+ viewport: { x: -box.x || 0, y: -box.y || 0, w: viewport.width, h: viewport.height },
2123
+ conditions,
2124
+ dpr: this.opts.dpr,
2125
+ canvas,
2126
+ fonts,
2127
+ root
2128
+ },
2129
+ png,
2130
+ fontFiles,
2131
+ ...triggers ? { triggers } : {},
2132
+ ...opened ? { opened } : {}
2133
+ };
2134
+ } finally {
2135
+ await page.close();
2136
+ }
2137
+ }
2138
+ async load(page, spec) {
2139
+ if (spec.viewport)
2140
+ await page.setViewportSize(spec.viewport);
2141
+ await page.emulateMedia({ colorScheme: spec.theme });
2142
+ if (this.opts.restricted)
2143
+ await page.route("**/*", (route) => route.abort("aborted"));
2144
+ if (spec.html !== undefined) {
2145
+ const documentURL = new URL(spec.url);
2146
+ documentURL.hash = "";
2147
+ await page.route((url) => url.href === documentURL.href, (route) => route.request().isNavigationRequest() ? route.fulfill({
2148
+ contentType: "text/html; charset=utf-8",
2149
+ body: spec.html,
2150
+ ...this.opts.restricted ? {
2151
+ headers: {
2152
+ "Content-Security-Policy": "default-src 'none'; script-src 'none'; connect-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:; frame-src 'none'"
2153
+ }
2154
+ } : {}
2155
+ }) : this.opts.restricted ? route.abort() : route.continue());
2156
+ }
2157
+ await page.goto(spec.url, { waitUntil: "load" });
2158
+ await page.waitForSelector(spec.waitFor ?? spec.root, { state: "attached", timeout: 15000 });
2159
+ await spec.prepare?.(page);
2160
+ }
2161
+ async captureFrame(spec, frame, clip) {
2162
+ const page = await this.context.newPage();
2163
+ try {
2164
+ if (!frame.reduced)
2165
+ await page.emulateMedia({ reducedMotion: "no-preference" });
2166
+ await this.load(page, spec);
2167
+ await noTransitions(page);
2168
+ if (!await settle(page, { at: frame.t }))
2169
+ throw new Error(`${spec.screen}@t:${frame.t}: no animation to pause; the frame would be the settled look`);
2170
+ await page.evaluate(({ sel, mark }) => document.querySelector(sel)?.setAttribute(mark, "1"), {
2171
+ sel: spec.root,
2172
+ mark: ROOT_MARK
2173
+ });
2174
+ await markTransformed(page);
2175
+ await cancelAncestorTransforms(page);
2176
+ return await withFrozenProgress(page, await this.context.newCDPSession(page), (timeout) => page.screenshot({ clip, scale: "device", animations: "allow", caret: "hide", type: "png", timeout }));
2177
+ } finally {
2178
+ await page.close();
2179
+ }
2180
+ }
2181
+ async captureLayerFrame(spec, trigger, t, clip) {
2182
+ const page = await this.context.newPage();
2183
+ try {
2184
+ await page.emulateMedia({ reducedMotion: "no-preference" });
2185
+ await this.load(page, spec);
2186
+ await noTransitions(page);
2187
+ await settle(page);
2188
+ await page.evaluate(({ sel, mark }) => document.querySelector(sel)?.setAttribute(mark, "1"), {
2189
+ sel: spec.root,
2190
+ mark: ROOT_MARK
2191
+ });
2192
+ const cdp = await this.context.newCDPSession(page);
2193
+ const opening = await openLayer(page, cdp, `[${ROOT_MARK}="1"]`, ROOT_MARK, trigger);
2194
+ await cdp.detach();
2195
+ if (!opening?.roots.length)
2196
+ throw new Error(`trigger ${trigger.id} opened nothing`);
2197
+ await page.evaluate(() => document.activeElement?.blur());
2198
+ if (!await settle(page, { at: t }))
2199
+ throw new Error(`${spec.screen}@open:${trigger.id}@t:${t}: no animation to pause; the frame would be the settled look`);
2200
+ await markTransformed(page);
2201
+ await cancelAncestorTransforms(page);
2202
+ return await withFrozenProgress(page, await this.context.newCDPSession(page), (timeout) => page.screenshot({ clip, scale: "device", animations: "allow", caret: "hide", type: "png", timeout }));
2203
+ } finally {
2204
+ await page.close();
2205
+ }
2206
+ }
2207
+ }
2208
+
2209
+ // src/fonts/source.ts
2210
+ function coversLatin(range) {
2211
+ if (!range)
2212
+ return true;
2213
+ for (const part of range.split(",")) {
2214
+ const m = /u\+([0-9a-f?]+)(?:-([0-9a-f]+))?/i.exec(part.trim());
2215
+ if (!m)
2216
+ continue;
2217
+ const lo = Number.parseInt(m[1].replace(/\?/g, "0"), 16);
2218
+ const hi = m[2] ? Number.parseInt(m[2], 16) : Number.parseInt(m[1].replace(/\?/g, "f"), 16);
2219
+ if (lo <= 65 && 65 <= hi)
2220
+ return true;
2221
+ }
2222
+ return false;
2223
+ }
2224
+ function srcBytes(src, sources) {
2225
+ const data = /^data:[^,]*;base64,(.*)$/s.exec(src.url);
2226
+ if (data)
2227
+ return Uint8Array.from(Buffer.from(data[1], "base64"));
2228
+ if (src.url.startsWith("data:"))
2229
+ return new TextEncoder().encode(decodeURIComponent(src.url.slice(src.url.indexOf(",") + 1)));
2230
+ return src.file ? sources.get(src.file) : undefined;
2231
+ }
2232
+ var WEIGHT_SUBFAMILY = {
2233
+ 100: "Thin",
2234
+ 200: "ExtraLight",
2235
+ 300: "Light",
2236
+ 400: "Regular",
2237
+ 500: "Medium",
2238
+ 600: "SemiBold",
2239
+ 700: "Bold",
2240
+ 800: "ExtraBold",
2241
+ 900: "Black"
2242
+ };
2243
+ function sanitize(family) {
2244
+ return family.replace(/[^A-Za-z0-9]/g, "") || "Font";
2245
+ }
2246
+ function fontFileStem(family, subfamily, claimed) {
2247
+ const base = sanitize(family) + (subfamily === "Regular" ? "" : `-${subfamily.replace(/\s+/g, "")}`);
2248
+ const owner = claimed.get(base.toLowerCase());
2249
+ if (owner === undefined)
2250
+ return base;
2251
+ if (owner === family)
2252
+ return;
2253
+ for (let n = 2;; n++)
2254
+ if (!claimed.has(`${base}-${n}`.toLowerCase()))
2255
+ return `${base}-${n}`;
2256
+ }
2257
+
2258
+ // src/fonts/converter.ts
2259
+ import { execFile } from "node:child_process";
2260
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2261
+ import { tmpdir } from "node:os";
2262
+ import { join } from "node:path";
2263
+ import { promisify } from "node:util";
2264
+
2265
+ // src/fonts/convert-font.py
2266
+ var convert_font_default = `import json, sys
2267
+ from fontTools.ttLib import TTFont
2268
+
2269
+ src, dst, family, subfamily, want_rename = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] == "1"
2270
+ # Converting the same font twice must not rewrite head.modified to the wall clock.
2271
+ font = TTFont(src, recalcTimestamp=False)
2272
+ flavor = font.flavor
2273
+ name = font["name"]
2274
+ names = {i: name.getDebugName(i) for i in (1, 2, 4, 6, 16, 17)}
2275
+ renamed = False
2276
+ # The registered name is the file's, not the CSS one: shadcn's 'Geist Variable' is a file whose
2277
+ # name table says 'Geist', so \`Font.custom("Geist Variable")\` would silently render in SF Pro.
2278
+ # Rewriting the name table here keeps the CSS family name the single name the whole pipeline
2279
+ # uses, instead of teaching the emitter a second naming convention. Stale records for the same
2280
+ # ids are dropped first so no platform-specific leftover can win the lookup.
2281
+ if want_rename and family not in {v for v in names.values() if v}:
2282
+ ps = "".join(c for c in family if c.isalnum()) + ("-" + subfamily if subfamily != "Regular" else "")
2283
+ full = family if subfamily == "Regular" else family + " " + subfamily
2284
+ ids = (1, 2, 4, 6, 16, 17) if "fvar" in font else (1, 2, 4, 6)
2285
+ name.names = [n for n in name.names if n.nameID not in ids]
2286
+ for platform, encoding, lang in ((3, 1, 0x409), (1, 0, 0)):
2287
+ name.setName(family, 1, platform, encoding, lang)
2288
+ name.setName(subfamily, 2, platform, encoding, lang)
2289
+ name.setName(full, 4, platform, encoding, lang)
2290
+ name.setName(ps[:63], 6, platform, encoding, lang)
2291
+ if "fvar" in font:
2292
+ name.setName(family, 16, platform, encoding, lang)
2293
+ name.setName(subfamily, 17, platform, encoding, lang)
2294
+ renamed = True
2295
+ if flavor is not None:
2296
+ font.flavor = None
2297
+ if renamed or flavor is not None:
2298
+ font.save(dst)
2299
+ os2, hhea, head = font["OS/2"], font["hhea"], font["head"]
2300
+ typo = bool(os2.fsSelection & (1 << 7)) and os2.version >= 4
2301
+ print(json.dumps({
2302
+ "upem": head.unitsPerEm,
2303
+ "ascent": os2.sTypoAscender if typo else hhea.ascender,
2304
+ "descent": abs(os2.sTypoDescender if typo else hhea.descender),
2305
+ "lineGap": os2.sTypoLineGap if typo else hhea.lineGap,
2306
+ "useTypoMetrics": typo,
2307
+ "axes": {a.axisTag: [a.minValue, a.maxValue] for a in font["fvar"].axes} if "fvar" in font else {},
2308
+ "converted": flavor is not None,
2309
+ "renamed": renamed,
2310
+ "wrote": renamed or flavor is not None,
2311
+ }))
2312
+ `;
2313
+
2314
+ // src/fonts/converter.ts
2315
+ var run = promisify(execFile);
2316
+ var pythonFontConverter = async (bytes, family, subfamily, signal) => {
2317
+ const dir = await mkdtemp(join(tmpdir(), "markswift-font-"));
2318
+ try {
2319
+ const src = join(dir, "src"), dst = join(dir, "dst"), py = join(dir, "convert-font.py");
2320
+ await writeFile(src, bytes);
2321
+ await writeFile(py, convert_font_default);
2322
+ let out;
2323
+ try {
2324
+ ({ stdout: out } = await run("python3", [py, src, dst, family, subfamily, "1"], {
2325
+ signal,
2326
+ maxBuffer: 16 * 1024 * 1024
2327
+ }));
2328
+ } catch (error) {
2329
+ const e = error;
2330
+ if (e.code === "ENOENT")
2331
+ return "python3 not found on PATH (webfont conversion needs Python 3 with fontTools and brotli)";
2332
+ return (e.stderr?.trim().split(`
2333
+ `).pop() || `python3 failed: ${e.message}`).slice(0, 200);
2334
+ }
2335
+ let tool;
2336
+ try {
2337
+ tool = JSON.parse(out);
2338
+ } catch {
2339
+ return `unreadable fontTools output: ${out.slice(0, 120)}`;
2340
+ }
2341
+ const { wrote, ...result } = tool;
2342
+ return wrote ? { ...result, bytes: new Uint8Array(await readFile(dst)) } : result;
2343
+ } finally {
2344
+ await rm(dir, { recursive: true, force: true });
2345
+ }
2346
+ };
2347
+
2348
+ // src/fonts/registration.ts
2349
+ function plistFragment(files) {
2350
+ if (!files.length)
2351
+ return "";
2352
+ return [
2353
+ `<!-- ${GENERATED_BY} — merge into the app target's Info.plist. -->`,
2354
+ "<key>UIAppFonts</key>",
2355
+ "<array>",
2356
+ ...files.map((f) => ` <string>Fonts/${f}</string>`),
2357
+ "</array>",
2358
+ ""
2359
+ ].join(`
2360
+ `);
2361
+ }
2362
+ function registrationSwift(faces, owner) {
2363
+ const pad = owner === undefined ? "" : " ";
2364
+ const entries = faces.map((f) => f.file).sort().map((f) => `${pad} ${JSON.stringify(f)},`).join(`
2365
+ `);
2366
+ const doc = faces.map((f) => `/// - ${f.family} (${f.file}): weight ${f.weight[0]}–${f.weight[1]}${f.axes["wght"] ? ", variable wght" : ""}${f.converted ? ", converted from WOFF2" : ""}${f.renamed ? ", name table rewritten so the CSS family name resolves" : ""}`).join(`
2367
+ `);
2368
+ const register = `let urls = files.compactMap { bundle.url(forResource: "Fonts/" + $0, withExtension: nil) ?? bundle.url(forResource: $0, withExtension: nil) }
2369
+ var error: Unmanaged<CFError>?
2370
+ for url in urls {
2371
+ // \`kCTFontManagerErrorAlreadyRegistered\` (105) is expected when \`UIAppFonts\`
2372
+ // already loaded the file; every other failure would mean text renders in the
2373
+ // system font, so it is surfaced instead of ignored.
2374
+ if !CTFontManagerRegisterFontsForURL(url as CFURL, .process, &error), let taken = error?.takeRetainedValue() {
2375
+ let code = CFErrorGetCode(taken)
2376
+ if code != 105 { print("[markswift] font registration failed: \\(url.lastPathComponent) (\\(code))") }
2377
+ error = nil
2378
+ }
2379
+ }`.split(`
2380
+ `).map((line) => `${pad} ${line}`).join(`
2381
+ `);
2382
+ if (owner === undefined)
2383
+ return `// ${GENERATED_BY} — bundled webfaces of this library.
2384
+ import CoreText
2385
+ import SwiftUI
2386
+
2387
+ ${doc}
2388
+ public enum BundledFonts {
2389
+ /// File names under the \`Fonts/\` resource directory, in bundle order.
2390
+ static let files: [String] = [
2391
+ ${entries}
2392
+ ]
2393
+
2394
+ /// Registers every bundled face with CoreText, once per process. Call once at app launch
2395
+ /// (previews call it themselves).
2396
+ public static func register() {
2397
+ guard !registered else { return }
2398
+ registered = true
2399
+ let bundle = Bundle.module
2400
+ ${register}
2401
+ }
2402
+
2403
+ private nonisolated(unsafe) static var registered = false
2404
+ }
2405
+ `;
2406
+ return `// ${GENERATED_BY} — bundled webfaces of ${owner}.
2407
+ import CoreText
2408
+ import SwiftUI
2409
+
2410
+ ${doc}
2411
+ extension ${owner} {
2412
+ enum BundledFonts {
2413
+ /// File names under the \`Fonts/\` resource directory, in bundle order.
2414
+ static let files: [String] = [
2415
+ ${entries}
2416
+ ]
2417
+
2418
+ /// Registers every bundled face with CoreText, once per process. \`UIAppFonts\` covers the
2419
+ /// app target; this covers previews, tests and any target that loads the views from a
2420
+ /// package, where no \`Info.plist\` is read.
2421
+ static func register(in bundle: Bundle = .main) {
2422
+ guard !registered else { return }
2423
+ registered = true
2424
+ ${register}
2425
+ }
2426
+
2427
+ private nonisolated(unsafe) static var registered = false
2428
+ }
2429
+ }
2430
+ `;
2431
+ }
2432
+
2433
+ // src/fonts/package.ts
2434
+ async function packageFonts(faces, sources, convert = pythonFontConverter, signal) {
2435
+ const notes = [];
2436
+ const note = (level, message) => notes.push({ source: "fonts", level, feature: "font-family", message });
2437
+ const packaged = [];
2438
+ const assets = {};
2439
+ if (!faces.length)
2440
+ return { faces: [], assets, plist: "", notes };
2441
+ const groups = {};
2442
+ for (const f of faces) {
2443
+ const k = `${f.family}|${f.style}|${f.weight.join("-")}`;
2444
+ (groups[k] ??= []).push(f);
2445
+ }
2446
+ const claimed = new Map;
2447
+ for (const k of Object.keys(groups).sort()) {
2448
+ const group = groups[k];
2449
+ const latin = group.filter((f) => coversLatin(f.unicodeRange));
2450
+ const chosen = (latin.length ? latin : group).find((f) => f.loaded) ?? (latin.length ? latin[0] : group[0]);
2451
+ if (group.length > 1) {
2452
+ note("approximated", `font-family ${chosen.family} ships ${group.length} unicode-range subsets at weight ${chosen.weight.join("–")}; bundled the one covering Basic Latin, ${group.length - 1} dropped (iOS has no per-codepoint fallback inside a family)`);
2453
+ }
2454
+ if (!latin.length)
2455
+ note("unmapped", `font-family ${chosen.family} declares no face covering Basic Latin; bundled its first subset`);
2456
+ let bytes;
2457
+ let usedSrc;
2458
+ for (const src of chosen.src) {
2459
+ bytes = srcBytes(src, sources);
2460
+ if (bytes?.length) {
2461
+ usedSrc = src;
2462
+ break;
2463
+ }
2464
+ }
2465
+ if (!bytes?.length || !usedSrc) {
2466
+ note("unmapped", `font-family ${chosen.family} has no readable src (${chosen.src.map((s) => s.url.slice(0, 60)).join(", ") || "none"}) → not bundled, text falls back to the system font`);
2467
+ continue;
2468
+ }
2469
+ const sig = new TextDecoder("latin1").decode(bytes.subarray(0, 4));
2470
+ const staticWeight = chosen.weight[0] === chosen.weight[1] ? chosen.weight[0] : undefined;
2471
+ const weightName = staticWeight !== undefined && staticWeight !== 400 ? WEIGHT_SUBFAMILY[staticWeight] ?? `W${staticWeight}` : "";
2472
+ const subfamily = [weightName, chosen.style === "normal" ? "" : "Italic"].filter(Boolean).join(" ") || "Regular";
2473
+ const stem = fontFileStem(chosen.family, subfamily, claimed);
2474
+ if (stem === undefined) {
2475
+ note("approximated", `font-family ${chosen.family} declares another ${subfamily} face (${chosen.style}, weight ${chosen.weight.join("–")}); iOS registers one face per name, bundled the first`);
2476
+ continue;
2477
+ }
2478
+ const file = `${stem}.${sig === "OTTO" ? "otf" : "ttf"}`;
2479
+ const tool = await convert(bytes, chosen.family, subfamily, signal);
2480
+ if (typeof tool === "string") {
2481
+ const fmt = sig === "wOF2" ? "WOFF2" : sig === "wOFF" ? "WOFF" : "the source format";
2482
+ note("unmapped", `font-family ${chosen.family} not bundled (${tool}); text falls back to the system font` + (sig === "wOF2" || sig === "wOFF" ? ` — iOS cannot register ${fmt}; converting it needs python3 with fontTools + brotli` : ""));
2483
+ continue;
2484
+ }
2485
+ assets[`Fonts/${file}`] = tool.bytes ?? bytes;
2486
+ const metrics = {
2487
+ upem: tool.upem,
2488
+ ascent: tool.ascent,
2489
+ descent: tool.descent,
2490
+ lineGap: tool.lineGap,
2491
+ useTypoMetrics: tool.useTypoMetrics
2492
+ };
2493
+ packaged.push({
2494
+ family: chosen.family,
2495
+ file,
2496
+ style: chosen.style,
2497
+ weight: chosen.weight,
2498
+ axes: tool.axes,
2499
+ metrics,
2500
+ converted: tool.converted,
2501
+ renamed: tool.renamed
2502
+ });
2503
+ claimed.set(stem.toLowerCase(), chosen.family);
2504
+ if (!chosen.loaded)
2505
+ note("approximated", `font-family ${chosen.family} never loaded in the reference browser, so the reference pixels are a fallback face; bundled it anyway`);
2506
+ }
2507
+ for (const f of packaged) {
2508
+ if (!f.axes["wght"] && f.weight[0] !== f.weight[1]) {
2509
+ note("approximated", `font-family ${f.family} declares weight ${f.weight.join("–")} but the bundled file has no wght axis; weights other than ${f.weight[0]} are synthesised`);
2510
+ }
2511
+ }
2512
+ const files = packaged.map((f) => f.file).sort();
2513
+ return { faces: packaged, assets, plist: plistFragment(files), notes };
2514
+ }
2515
+
2516
+ // src/fonts/metadata.ts
2517
+ function metricsForFonts(pkg) {
2518
+ return Object.fromEntries(pkg.faces.map((face) => [face.family.toLowerCase(), { ...face.metrics }]));
2519
+ }
2520
+ function bundledFamilies(pkg) {
2521
+ const out = {};
2522
+ for (const f of pkg.faces) {
2523
+ if (f.style !== "normal")
2524
+ continue;
2525
+ const prev = out[f.family];
2526
+ const { wght: _w, opsz: _o, ...axes } = f.axes;
2527
+ out[f.family] = {
2528
+ weight: prev ? [Math.min(prev.weight[0], f.weight[0]), Math.max(prev.weight[1], f.weight[1])] : f.weight,
2529
+ ...f.axes["wght"] ? { wght: f.axes["wght"] } : prev?.wght ? { wght: prev.wght } : {},
2530
+ ...f.axes["opsz"] ? { opsz: f.axes["opsz"] } : prev?.opsz ? { opsz: prev.opsz } : {},
2531
+ ...Object.keys(axes).length ? { axes } : prev?.axes ? { axes: prev.axes } : {}
2532
+ };
2533
+ }
2534
+ return out;
2535
+ }
2536
+
2537
+ // src/input/source.ts
2538
+ import { readFile as readFile2 } from "node:fs/promises";
2539
+ import { basename, dirname, resolve as resolve2 } from "node:path";
2540
+ async function resolveSource(source) {
2541
+ if ("url" in source) {
2542
+ const url = httpURL(source.url);
2543
+ return { url, label: url, close() {} };
2544
+ }
2545
+ if ("file" in source) {
2546
+ const file = resolve2(source.file);
2547
+ const html = await readFile2(file, "utf8");
2548
+ const server = await serveDir(dirname(file));
2549
+ return {
2550
+ html,
2551
+ url: `${server.origin}/${encodeURIComponent(basename(file))}`,
2552
+ label: basename(file),
2553
+ close: () => server.stop()
2554
+ };
2555
+ }
2556
+ if (source.baseURL && source.assetsDir)
2557
+ throw new Error("choose baseURL or assetsDir, not both");
2558
+ const server = source.assetsDir ? await serveDir(resolve2(source.assetsDir)) : undefined;
2559
+ return {
2560
+ html: source.html,
2561
+ url: source.baseURL ? httpURL(source.baseURL) : `${server?.origin ?? "http://markswift.invalid"}/index.html`,
2562
+ label: "inline.html",
2563
+ close: () => server?.stop()
2564
+ };
2565
+ }
2566
+ function httpURL(value) {
2567
+ const url = new URL(value);
2568
+ if (url.protocol !== "http:" && url.protocol !== "https:")
2569
+ throw new Error("only http and https page URLs are supported");
2570
+ return url.href;
2571
+ }
2572
+
2573
+ // src/input/identity.ts
2574
+ var htmlIdentity = (_tag, attrs, path) => {
2575
+ if (attrs.id)
2576
+ return `#${attrs.id}`;
2577
+ return attrs["data-markswift"] ?? attrs["data-testid"] ?? attrs["data-name"] ?? (path === "" ? "root" : path);
2578
+ };
2579
+
2580
+ // src/api/bands.ts
2581
+ function validateBreakpoints(bp, prefix = "") {
2582
+ if (bp !== undefined && bp !== true && (!(bp.devices ?? []).every((d) => [d.width, d.height].every((n) => Number.isInteger(n) && n > 0)) || bp.max !== undefined && !(Number.isInteger(bp.max) && bp.max >= 320)))
2583
+ throw new Error(`${prefix}breakpoints devices must be positive integer sizes and max an integer ≥ 320`);
2584
+ }
2585
+ function planFor(bp, conditions, first) {
2586
+ return planBands2(conditions, {
2587
+ first,
2588
+ devices: (bp === true ? undefined : bp.devices) ?? DEFAULT_BAND_DEVICES2,
2589
+ max: (bp === true ? undefined : bp.max) ?? 1376
2590
+ });
2591
+ }
2592
+ async function captureThemes(renderer, req, timedOut) {
2593
+ const race = (spec) => Promise.race([renderer.capture(spec, htmlIdentity, req.label), timedOut]);
2594
+ const shot = async (scheme, probeDismiss) => {
2595
+ const spec = {
2596
+ screen: req.name,
2597
+ theme: scheme,
2598
+ url: req.url,
2599
+ html: req.html,
2600
+ root: req.root,
2601
+ waitFor: req.waitFor,
2602
+ viewport: req.viewport,
2603
+ ...req.breakpoints ? { breakpoints: true } : {},
2604
+ async prepare(page) {
2605
+ if (req.css)
2606
+ await page.addStyleTag({ content: req.css });
2607
+ if (scheme === "dark" && req.darkClass)
2608
+ await page.evaluate((c) => document.documentElement.classList.add(c), req.darkClass);
2609
+ await req.prepare?.(page);
2610
+ }
2611
+ };
2612
+ const capture = await race(spec);
2613
+ if (req.layers)
2614
+ await captureLayers(race, spec, capture, { probeDismiss });
2615
+ return capture;
2616
+ };
2617
+ const primary = await shot(req.theme === "dark" ? "dark" : "light", true);
2618
+ if (req.theme !== "adaptive")
2619
+ return { primary };
2620
+ const dark = await shot("dark", false);
2621
+ for (const layer of dark.layout.layers ?? []) {
2622
+ const light = primary.layout.layers?.find((l) => l.id === layer.id);
2623
+ if (light) {
2624
+ layer.dismiss = light.dismiss;
2625
+ layer.nodes.forEach((n, i) => {
2626
+ const exit = light.nodes[i]?.motion?.exit;
2627
+ if (exit)
2628
+ n.motion = { ...n.motion, exit };
2629
+ });
2630
+ } else
2631
+ dark.warnings.push({
2632
+ source: "capture",
2633
+ message: `layer ${layer.id} opens only in the dark theme; its dismissal was not probed`
2634
+ });
2635
+ }
2636
+ return { primary, dark };
2637
+ }
2638
+ async function captureBands(renderer, req, first, plan, timedOut) {
2639
+ const bands = [];
2640
+ for (const [i, capture] of plan.captures.entries())
2641
+ bands.push(capture.width === first.viewport.width && capture.height === first.viewport.height ? first.captured : await captureThemes(renderer, { ...req, viewport: capture, ...i > 0 ? { layers: false } : {} }, timedOut));
2642
+ for (const viewport of plan.extra)
2643
+ bands.push({ ...await captureThemes(renderer, { ...req, viewport, layers: false }, timedOut), extra: true });
2644
+ return bands.sort((a, b) => a.primary.layout.viewport.w - b.primary.layout.viewport.w);
2645
+ }
2646
+
2647
+ // src/api/convert.ts
2648
+ async function convert2(source, options = {}) {
2649
+ validateName(options.name ?? "ConvertedView");
2650
+ const viewport = options.viewport ?? { width: 390, height: 844 };
2651
+ const dpr = options.dpr ?? 1;
2652
+ if (![viewport.width, viewport.height].every((n) => Number.isInteger(n) && n > 0) || !Number.isFinite(dpr) || dpr <= 0) {
2653
+ throw new Error("viewport dimensions must be positive integers and dpr must be positive and finite");
2654
+ }
2655
+ const bp = options.breakpoints;
2656
+ validateBreakpoints(bp);
2657
+ const timeoutMs = options.timeoutMs ?? 120000;
2658
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
2659
+ throw new Error("timeoutMs must be positive and finite");
2660
+ if (["html", "file", "url"].filter((key) => (key in source)).length !== 1)
2661
+ throw new Error("provide exactly one of html, file or url");
2662
+ if (options.untrustedInput && (!("html" in source) || source.assetsDir || source.baseURL)) {
2663
+ throw new Error("untrustedInput accepts pasted HTML without assetsDir or baseURL only");
2664
+ }
2665
+ const theme = options.theme ?? "adaptive";
2666
+ if (theme !== "light" && theme !== "dark" && theme !== "adaptive")
2667
+ throw new Error("theme must be light, dark or adaptive");
2668
+ if (options.darkClass !== undefined && !/^\S+$/.test(options.darkClass))
2669
+ throw new Error("darkClass must be a single class name");
2670
+ let prepared;
2671
+ const deadline = AbortSignal.timeout(timeoutMs);
2672
+ const renderer = new BrowserCapture({
2673
+ viewport,
2674
+ dpr,
2675
+ channel: options.browserChannel,
2676
+ restricted: options.untrustedInput,
2677
+ signal: deadline
2678
+ });
2679
+ const stopOnTimeout = () => void renderer.stop().catch(() => {});
2680
+ deadline.addEventListener("abort", stopOnTimeout, { once: true });
2681
+ const timedOut = new Promise((_, reject) => deadline.addEventListener("abort", () => reject(deadline.reason), { once: true }));
2682
+ timedOut.catch(() => {});
2683
+ try {
2684
+ prepared = await resolveSource(source);
2685
+ const { html, url, label } = prepared;
2686
+ await renderer.start();
2687
+ deadline.throwIfAborted();
2688
+ const name = options.name ?? "ConvertedView";
2689
+ const req = {
2690
+ name,
2691
+ url,
2692
+ html,
2693
+ label,
2694
+ root: options.root ?? DEFAULT_ROOT,
2695
+ theme,
2696
+ css: options.css,
2697
+ darkClass: options.darkClass,
2698
+ waitFor: options.waitFor,
2699
+ prepare: options.prepare,
2700
+ ...bp ? { breakpoints: true } : {}
2701
+ };
2702
+ const layered = { ...req, layers: true };
2703
+ const first = await captureThemes(renderer, layered, timedOut);
2704
+ const plan = bp ? planFor(bp, first.primary.layout.conditions, viewport) : undefined;
2705
+ const bands = plan ? await captureBands(renderer, layered, { viewport, captured: first }, plan, timedOut) : [first];
2706
+ const { primary: capture, dark } = bands[0];
2707
+ const probeWidth = plan ? plan.probes[0] : viewport.width > 320 ? 320 : 390;
2708
+ const probe = options.probe !== false && probeWidth !== undefined && needsProbe2(capture.layout) ? (await captureThemes(renderer, {
2709
+ ...req,
2710
+ theme: capture.layout.combo.theme,
2711
+ viewport: { width: probeWidth, height: plan?.captures[0]?.height ?? viewport.height }
2712
+ }, timedOut)).primary.layout : undefined;
2713
+ const variantProbes = [];
2714
+ let band = 0;
2715
+ for (let i = 1;i < bands.length; i++) {
2716
+ const layout = bands[i].primary.layout;
2717
+ const width = bands[i].extra ? undefined : plan.probes[++band];
2718
+ const probed = options.probe !== false && width !== undefined && needsProbe2(layout) ? (await captureThemes(renderer, {
2719
+ ...req,
2720
+ theme: layout.combo.theme,
2721
+ viewport: { width, height: plan.captures[band].height }
2722
+ }, timedOut)).primary.layout : undefined;
2723
+ variantProbes.push(probed && probed.size.w !== layout.size.w ? probed : undefined);
2724
+ }
2725
+ const captures = bands.flatMap((b) => b.dark ? [b.primary, b.dark] : [b.primary]);
2726
+ const sources = new Map(captures.flatMap((c) => c.fontFiles).map((f) => [f.file, f.bytes]));
2727
+ const fonts = await packageFonts(captures.length > 1 ? unionFaces(captures.map((c) => c.layout.fonts)) : capture.layout.fonts, sources, undefined, deadline);
2728
+ deadline.throwIfAborted();
2729
+ const compiled = compileLayout2(capture.layout, {
2730
+ name,
2731
+ mode: options.mode,
2732
+ bundledFamilies: bundledFamilies(fonts),
2733
+ fontMetrics: metricsForFonts(fonts),
2734
+ dark: dark?.layout,
2735
+ probe: probe && probe.size.w !== capture.layout.size.w ? probe : undefined,
2736
+ palette: options.palette,
2737
+ ...bands.length > 1 ? {
2738
+ variants: bands.slice(1).map((b) => b.primary.layout),
2739
+ ...dark ? { darkVariants: bands.slice(1).map((b) => b.dark.layout) } : {},
2740
+ variantProbes
2741
+ } : {}
2742
+ });
2743
+ const assets = { ...compiled.assets, ...fonts.assets };
2744
+ if (fonts.faces.length) {
2745
+ compiled.files[`${name}+Fonts.swift`] = registrationSwift(fonts.faces, name);
2746
+ compiled.files[`${name}+UIAppFonts.plist.fragment`] = fonts.plist;
2747
+ }
2748
+ const pageWarnings = captures.length > 1 ? [...new Map(captures.flatMap((c) => c.warnings).map((w) => [w.message, w])).values()] : capture.warnings;
2749
+ const notes = (plan?.notes ?? []).map((message) => ({
2750
+ source: "capture",
2751
+ level: "approximated",
2752
+ feature: "media-query",
2753
+ message
2754
+ }));
2755
+ return {
2756
+ ...compiled,
2757
+ assets,
2758
+ warnings: [...pageWarnings, ...notes, ...fonts.notes, ...compiled.warnings],
2759
+ layout: capture.layout,
2760
+ ...dark ? { darkLayout: dark.layout } : {},
2761
+ ...plan ? {
2762
+ bands: bands.map((b) => ({
2763
+ layout: b.primary.layout,
2764
+ ...b.dark ? { darkLayout: b.dark.layout } : {}
2765
+ }))
2766
+ } : {}
2767
+ };
2768
+ } catch (error) {
2769
+ if (deadline.aborted)
2770
+ throw new Error(`conversion timed out after ${timeoutMs} ms`, { cause: error });
2771
+ throw error;
2772
+ } finally {
2773
+ deadline.removeEventListener("abort", stopOnTimeout);
2774
+ try {
2775
+ await renderer.stop();
2776
+ } finally {
2777
+ prepared?.close();
2778
+ }
2779
+ }
2780
+ }
2781
+ function unionFaces(lists) {
2782
+ const key = ({ loaded: _, ...face }) => JSON.stringify(face);
2783
+ const faces = new Map;
2784
+ for (const list of lists)
2785
+ for (const f of list) {
2786
+ const cur = faces.get(key(f));
2787
+ if (!cur)
2788
+ faces.set(key(f), f);
2789
+ else if (f.loaded && !cur.loaded)
2790
+ faces.set(key(f), { ...cur, loaded: true });
2791
+ }
2792
+ return [...faces.values()];
2793
+ }
2794
+ function convertHtml2(html, options = {}) {
2795
+ return convert2({ html }, options);
2796
+ }
2797
+
2798
+ // src/api/build.ts
2799
+ var RUNTIME_BUNDLE = "static var bundle: Bundle = .main";
2800
+ async function buildLibrary2(options) {
2801
+ const { library, components } = options;
2802
+ validateLibrary2(library, components.map((c) => c.name));
2803
+ const timeoutMs = options.timeoutMs ?? 600000;
2804
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
2805
+ throw new Error("timeoutMs must be positive and finite");
2806
+ const serveRoot = options.serveRoot === undefined ? undefined : resolve3(options.serveRoot);
2807
+ for (const c of components) {
2808
+ const { width, height } = c.viewport ?? { width: 390, height: 844 };
2809
+ if (![width, height].every((n) => Number.isInteger(n) && n > 0))
2810
+ throw new Error(`component ${c.name}: viewport dimensions must be positive integers`);
2811
+ if (c.darkClass !== undefined && !/^\S+$/.test(c.darkClass))
2812
+ throw new Error(`component ${c.name}: darkClass must be a single class name`);
2813
+ validateBreakpoints(c.breakpoints, `component ${c.name}: `);
2814
+ if ("url" in c.source) {
2815
+ const protocol = URL.canParse(c.source.url) ? new URL(c.source.url).protocol : "";
2816
+ if (protocol !== "http:" && protocol !== "https:")
2817
+ throw new Error(`component ${c.name}: only http and https page URLs are supported`);
2818
+ } else if (!serveRoot)
2819
+ throw new Error(`component ${c.name}: a file source needs serveRoot`);
2820
+ else if (!resolve3(serveRoot, c.source.file).startsWith(serveRoot + sep))
2821
+ throw new Error(`component ${c.name}: ${c.source.file} is outside serveRoot ${serveRoot}`);
2822
+ }
2823
+ const deadline = AbortSignal.timeout(timeoutMs);
2824
+ const renderers = new Map;
2825
+ const stopAll = () => Promise.allSettled([...renderers.values()].map((r) => r.started.finally(() => r.capture.stop())));
2826
+ const stopOnTimeout = () => void stopAll();
2827
+ deadline.addEventListener("abort", stopOnTimeout, { once: true });
2828
+ const timedOut = new Promise((_, reject) => deadline.addEventListener("abort", () => reject(deadline.reason), { once: true }));
2829
+ timedOut.catch(() => {});
2830
+ let server;
2831
+ try {
2832
+ if (components.some((c) => ("file" in c.source)))
2833
+ server = await serveDir(serveRoot);
2834
+ const captureOne = async (c) => {
2835
+ const viewport = c.viewport ?? { width: 390, height: 844 };
2836
+ const key = `${viewport.width}x${viewport.height}`;
2837
+ let renderer = renderers.get(key);
2838
+ if (!renderer) {
2839
+ const capture = new BrowserCapture({ viewport, dpr: 1, signal: deadline });
2840
+ renderer = { capture, started: capture.start() };
2841
+ renderers.set(key, renderer);
2842
+ }
2843
+ await Promise.race([renderer.started, timedOut]);
2844
+ deadline.throwIfAborted();
2845
+ let page;
2846
+ if ("url" in c.source)
2847
+ page = { url: c.source.url, label: c.source.url };
2848
+ else {
2849
+ const path = relative(serveRoot, resolve3(serveRoot, c.source.file));
2850
+ page = {
2851
+ url: `${server.origin}/${path.split(sep).map(encodeURIComponent).join("/")}`,
2852
+ html: await readFile3(resolve3(serveRoot, c.source.file), "utf8"),
2853
+ label: path.split(sep).join("/")
2854
+ };
2855
+ }
2856
+ const req = {
2857
+ name: c.name,
2858
+ ...page,
2859
+ root: c.root ?? DEFAULT_ROOT,
2860
+ theme: c.theme ?? "adaptive",
2861
+ css: c.css,
2862
+ darkClass: c.darkClass,
2863
+ waitFor: c.waitFor,
2864
+ layers: true,
2865
+ ...c.breakpoints ? { breakpoints: true } : {}
2866
+ };
2867
+ const first = await captureThemes(renderer.capture, req, timedOut);
2868
+ if (!c.breakpoints)
2869
+ return { bands: [first], notes: [] };
2870
+ const plan = planFor(c.breakpoints, first.primary.layout.conditions, viewport);
2871
+ const bands = await captureBands(renderer.capture, req, { viewport, captured: first }, plan, timedOut);
2872
+ return { bands, notes: plan.notes };
2873
+ };
2874
+ const captured = [];
2875
+ let next = 0;
2876
+ const worker = async () => {
2877
+ for (let i = next++;i < components.length; i = next++)
2878
+ captured[i] = await captureOne(components[i]);
2879
+ };
2880
+ await Promise.all(Array.from({ length: Math.min(4, components.length) }, worker));
2881
+ const all = captured.flatMap((c) => c.bands.flatMap((b) => b.dark ? [b.primary, b.dark] : [b.primary]));
2882
+ const fonts = await packageFonts(unionFaces(all.map((c) => c.layout.fonts)), new Map(all.flatMap((c) => c.fontFiles).map((f) => [f.file, f.bytes])), undefined, deadline);
2883
+ deadline.throwIfAborted();
2884
+ const compiled = compileLayouts2(components.map((c, i) => {
2885
+ const [first, ...rest] = captured[i].bands;
2886
+ return {
2887
+ name: c.name,
2888
+ layout: first.primary.layout,
2889
+ dark: first.dark?.layout,
2890
+ mode: c.mode,
2891
+ ...rest.length ? {
2892
+ variants: rest.map((b) => b.primary.layout),
2893
+ ...first.dark ? { darkVariants: rest.map((b) => b.dark.layout) } : {}
2894
+ } : {}
2895
+ };
2896
+ }), {
2897
+ library,
2898
+ bundledFamilies: bundledFamilies(fonts),
2899
+ fontMetrics: metricsForFonts(fonts),
2900
+ registerFonts: fonts.faces.length > 0,
2901
+ palette: options.palette
2902
+ });
2903
+ const dir = `Sources/${library}/`;
2904
+ const files = {};
2905
+ const assets = {};
2906
+ for (const [name, text] of Object.entries(compiled.files))
2907
+ files[dir + name] = text;
2908
+ if (fonts.faces.length)
2909
+ files[`${dir}MarkswiftFonts.swift`] = registrationSwift(fonts.faces);
2910
+ for (const [name, bytes] of Object.entries({ ...compiled.assets, ...fonts.assets }))
2911
+ assets[dir + name] = bytes;
2912
+ const resources = ["Fonts", "Images"].filter((d) => Object.keys(assets).some((k) => k.startsWith(`${dir}${d}/`)));
2913
+ if (resources.length) {
2914
+ const runtime = files[`${dir}MarkswiftRuntime.swift`].split(RUNTIME_BUNDLE);
2915
+ if (runtime.length !== 2)
2916
+ throw new Error(`MarkswiftRuntime.swift must contain "${RUNTIME_BUNDLE}" exactly once`);
2917
+ files[`${dir}MarkswiftRuntime.swift`] = runtime.join("static var bundle: Bundle = .module");
2918
+ }
2919
+ files["Package.swift"] = [
2920
+ "// swift-tools-version: 6.2",
2921
+ `// ${GENERATED_BY} — tool-owned: regenerate with \`markswift build\`.`,
2922
+ "import PackageDescription",
2923
+ "",
2924
+ "let package = Package(",
2925
+ ` name: "${library}",`,
2926
+ " platforms: [.iOS(.v18), .macOS(.v15)],",
2927
+ ` products: [.library(name: "${library}", targets: ["${library}"])],`,
2928
+ " targets: [",
2929
+ " .target(",
2930
+ ` name: "${library}",`,
2931
+ ...resources.length ? [` resources: [${resources.map((d) => `.copy("${d}")`).join(", ")}],`] : [],
2932
+ " swiftSettings: [.defaultIsolation(MainActor.self)]",
2933
+ " ),",
2934
+ " ]",
2935
+ ")",
2936
+ ""
2937
+ ].join(`
2938
+ `);
2939
+ const warnings = new Map;
2940
+ const add = (w) => {
2941
+ const key = `${w.component ?? ""}\x00${w.source}\x00${w.message}`;
2942
+ if (!warnings.has(key))
2943
+ warnings.set(key, w);
2944
+ };
2945
+ for (const [i, c] of components.entries()) {
2946
+ const { bands, notes } = captured[i];
2947
+ for (const w of bands.flatMap((b) => [...b.primary.warnings, ...b.dark?.warnings ?? []]))
2948
+ add({ ...w, component: c.name });
2949
+ for (const message of notes)
2950
+ add({ source: "capture", level: "approximated", feature: "media-query", component: c.name, message });
2951
+ for (const w of compiled.warnings)
2952
+ if (w.component === c.name)
2953
+ add(w);
2954
+ }
2955
+ for (const w of compiled.warnings)
2956
+ if (w.component === undefined)
2957
+ add(w);
2958
+ for (const w of fonts.notes)
2959
+ add(w);
2960
+ return { files, assets, warnings: [...warnings.values()], diagnostics: compiled.diagnostics };
2961
+ } catch (error) {
2962
+ if (deadline.aborted)
2963
+ throw new Error(`build timed out after ${timeoutMs} ms`, { cause: error });
2964
+ throw error;
2965
+ } finally {
2966
+ deadline.removeEventListener("abort", stopOnTimeout);
2967
+ try {
2968
+ await stopAll();
2969
+ } finally {
2970
+ server?.stop();
2971
+ }
2972
+ }
2973
+ }
2974
+
2975
+ export { mimeType, convert2, convertHtml2, buildLibrary2 };