@ingcreators/annot-product-docs 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,947 @@
1
+ import { annotSourceResolvers as e, test as t } from "@ingcreators/annot-playwright";
2
+ import { mkdir as n, readFile as r, readdir as i, writeFile as a } from "node:fs/promises";
3
+ import o from "js-yaml";
4
+ import s from "remark-frontmatter";
5
+ import c from "remark-mdx";
6
+ import l from "remark-parse";
7
+ import { unified as u } from "unified";
8
+ import { visit as d } from "unist-util-visit";
9
+ import { z as f } from "zod";
10
+ import { dirname as p, isAbsolute as ee, join as te, relative as m, resolve as h } from "node:path";
11
+ import { bboxAnnotationsToSvg as g } from "@ingcreators/annot-annotator";
12
+ //#region src/config.ts
13
+ var ne = f.enum([
14
+ "cover",
15
+ "history",
16
+ "list",
17
+ "screen",
18
+ "reference"
19
+ ]), re = f.object({
20
+ book: f.string().optional(),
21
+ sheet: f.string().optional(),
22
+ sheets: f.record(f.string(), f.string()).optional(),
23
+ role: ne.optional(),
24
+ order: f.number().optional()
25
+ }).strict().refine((e) => !(e.sheet && e.sheets), "Specify either `xlsx.sheet` (single sheet) or `xlsx.sheets` (multi-sheet), not both."), ie = f.record(f.string(), f.unknown()), _ = f.object({
26
+ id: f.string().min(1, "`annot.id` is required."),
27
+ title: f.string().optional(),
28
+ purpose: f.string().optional(),
29
+ meta: ie.optional(),
30
+ xlsx: re.optional()
31
+ }).strict(), ae = f.object({
32
+ template: f.string().optional(),
33
+ templateSheets: f.object({
34
+ cover: f.string().optional(),
35
+ history: f.string().optional(),
36
+ list: f.string().optional(),
37
+ screen: f.string().optional(),
38
+ reference: f.string().optional()
39
+ }).strict().optional()
40
+ }).strict(), v = f.object({
41
+ meta: f.record(f.string(), f.unknown()).optional(),
42
+ xlsx: f.object({
43
+ defaultBook: f.string().optional(),
44
+ books: f.record(f.string(), ae).optional()
45
+ }).strict().optional()
46
+ }).strict();
47
+ function oe(e) {
48
+ let t = v.safeParse(e);
49
+ if (!t.success) {
50
+ let e = t.error.issues.map((e) => ` - ${e.path.length ? e.path.join(".") + ": " : ""}${e.message}`).join("\n");
51
+ throw Error(`Invalid \`annot-docs.config.ts\`:\n${e}`);
52
+ }
53
+ return t.data;
54
+ }
55
+ function se(e) {
56
+ return (e.xlsx?.role ?? "screen") === "screen";
57
+ }
58
+ //#endregion
59
+ //#region src/mdx.ts
60
+ var y = "annot:snapshot", ce = "/annot:snapshot", b = "annot:attributes", le = "/annot:attributes", ue = /^\s*\/\*\s*([^\s*]+)\s*([\s\S]*?)\*\/\s*$/;
61
+ async function x(e) {
62
+ return S(await r(e, "utf8"), { filePath: e });
63
+ }
64
+ function S(e, t = {}) {
65
+ let n = u().use(l).use(s, ["yaml"]).use(c).parse(e), r = de(n, t.filePath);
66
+ if (!r) return null;
67
+ let i = [], a = [], o = [], f = [], p = {};
68
+ return d(n, (t) => {
69
+ switch (t.type) {
70
+ case "mdxJsxFlowElement":
71
+ case "mdxJsxTextElement":
72
+ pe(t, e, {
73
+ screens: i,
74
+ transitions: a,
75
+ history: o,
76
+ screenLists: f
77
+ });
78
+ return;
79
+ case "mdxFlowExpression":
80
+ case "mdxTextExpression":
81
+ ye(t, p);
82
+ return;
83
+ }
84
+ }), {
85
+ frontmatter: r,
86
+ screens: i,
87
+ transitions: a,
88
+ history: o,
89
+ screenLists: f,
90
+ commentBlocks: p,
91
+ source: e
92
+ };
93
+ }
94
+ function de(e, t) {
95
+ let n = e.children.find((e) => e.type === "yaml");
96
+ if (!n || !n.value) return null;
97
+ let r = o.load(n.value);
98
+ if (!r || typeof r != "object") return null;
99
+ let i = r.annot;
100
+ if (!i || typeof i != "object") return null;
101
+ let a = _.safeParse(i);
102
+ if (!a.success) {
103
+ let e = t ? ` in ${t}` : "";
104
+ throw Error(`Invalid \`annot:\` frontmatter${e}:\n${fe(a.error)}`);
105
+ }
106
+ return a.data;
107
+ }
108
+ function fe(e) {
109
+ return e.issues.map((e) => ` - ${e.path.length ? e.path.join(".") + ": " : ""}${e.message}`).join("\n");
110
+ }
111
+ function pe(e, t, n) {
112
+ let r = e;
113
+ switch (r.name) {
114
+ case "Screen":
115
+ n.screens.push(me(r, t));
116
+ return;
117
+ case "Transition":
118
+ n.transitions.push(ge(r, t));
119
+ return;
120
+ case "HistoryEntry":
121
+ n.history.push(_e(r, t));
122
+ return;
123
+ case "ScreenList":
124
+ n.screenLists.push(ve(r));
125
+ return;
126
+ }
127
+ }
128
+ function me(e, t) {
129
+ let n = C(e), r = [], i = (n) => {
130
+ let a = n;
131
+ if (a.name === "Overlay") {
132
+ r.push(he(a, t));
133
+ return;
134
+ }
135
+ if (!(a.name === "Screen" && a !== e)) for (let e of a.children ?? []) i(e);
136
+ };
137
+ for (let t of e.children ?? []) i(t);
138
+ let a = w(n.id);
139
+ if (!a) throw Error("<Screen> requires an `id` prop.");
140
+ return {
141
+ id: a,
142
+ src: w(n.src),
143
+ overlays: r
144
+ };
145
+ }
146
+ function he(e, t) {
147
+ let n = C(e), r = n.match;
148
+ if (!T(r)) throw Error("<Overlay> requires a `match` prop with at least `{ role, name }`.");
149
+ return {
150
+ match: r,
151
+ intent: w(n.intent),
152
+ number: xe(n.number),
153
+ body: E(e, t)
154
+ };
155
+ }
156
+ function ge(e, t) {
157
+ let n = C(e), r = n.trigger;
158
+ if (!T(r)) throw Error("<Transition> requires a `trigger` prop with at least `{ role, name }`.");
159
+ return {
160
+ trigger: r,
161
+ on: w(n.on),
162
+ to: w(n.to),
163
+ body: E(e, t)
164
+ };
165
+ }
166
+ function _e(e, t) {
167
+ let n = C(e), r = w(n.version) ?? "", i = w(n.date) ?? "", a = w(n.author) ?? "";
168
+ if (!r || !i) throw Error("<HistoryEntry> requires `version` and `date` props.");
169
+ return {
170
+ version: r,
171
+ date: i,
172
+ author: a,
173
+ body: E(e, t)
174
+ };
175
+ }
176
+ function ve(e) {
177
+ let t = C(e);
178
+ return {
179
+ book: w(t.book),
180
+ sort: w(t.sort)
181
+ };
182
+ }
183
+ function ye(e, t) {
184
+ let n = e.value;
185
+ if (typeof n != "string") return;
186
+ let r = n.match(ue);
187
+ if (!r) return;
188
+ let i = (r[1] ?? "").trim(), a = (r[2] ?? "").trim();
189
+ switch (i) {
190
+ case y:
191
+ t.snapshot = a;
192
+ return;
193
+ case b:
194
+ t.attributes = a;
195
+ return;
196
+ case ce:
197
+ case le: return;
198
+ }
199
+ }
200
+ function C(e) {
201
+ let t = {};
202
+ for (let n of e.attributes ?? []) {
203
+ let e = n;
204
+ if (e.type !== "mdxJsxAttribute" || !e.name) continue;
205
+ if (e.value === null || e.value === void 0) {
206
+ t[e.name] = !0;
207
+ continue;
208
+ }
209
+ if (typeof e.value == "string") {
210
+ t[e.name] = e.value;
211
+ continue;
212
+ }
213
+ let r = e.value;
214
+ r.type === "mdxJsxAttributeValueExpression" && typeof r.value == "string" && (t[e.name] = be(r.value));
215
+ }
216
+ return t;
217
+ }
218
+ function be(e) {
219
+ let t = e.trim();
220
+ if (t) {
221
+ if (t === "true") return !0;
222
+ if (t === "false") return !1;
223
+ if (t === "null") return null;
224
+ if (/^-?\d+(\.\d+)?$/.test(t)) return Number.parseFloat(t);
225
+ if (t.startsWith("\"") && t.endsWith("\"")) try {
226
+ return JSON.parse(t);
227
+ } catch {
228
+ return t.slice(1, -1);
229
+ }
230
+ if (t.startsWith("'") && t.endsWith("'")) {
231
+ let e = t.slice(1, -1), n = e.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
232
+ try {
233
+ return JSON.parse(`"${n}"`);
234
+ } catch {
235
+ return e;
236
+ }
237
+ }
238
+ if (t.startsWith("{") && t.endsWith("}") || t.startsWith("[") && t.endsWith("]")) {
239
+ let e = t.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)(\s*:)/g, "$1\"$2\"$3").replace(/'([^'\\]*)'/g, "\"$1\"").replace(/,(\s*[}\]])/g, "$1");
240
+ try {
241
+ return JSON.parse(e);
242
+ } catch {
243
+ return t;
244
+ }
245
+ }
246
+ return t;
247
+ }
248
+ }
249
+ function w(e) {
250
+ return typeof e == "string" ? e : void 0;
251
+ }
252
+ function xe(e) {
253
+ return typeof e == "number" ? e : void 0;
254
+ }
255
+ function T(e) {
256
+ if (!e || typeof e != "object") return !1;
257
+ let t = e;
258
+ return !(typeof t.role != "string" || typeof t.name != "string" || t.under !== void 0 && !T(t.under));
259
+ }
260
+ function E(e, t) {
261
+ let n = e.position;
262
+ if (!n) return "";
263
+ let r = t.slice(n.start.offset, n.end.offset);
264
+ if (/\/>\s*$/.test(r)) return "";
265
+ let i = r.indexOf(">"), a = r.lastIndexOf("</");
266
+ return i < 0 || a < 0 || a <= i ? "" : r.slice(i + 1, a).trim();
267
+ }
268
+ function D(e, t) {
269
+ let n = e;
270
+ return t.snapshot !== void 0 && (n = O(n, y, t.snapshot)), t.attributes !== void 0 && (n = O(n, b, t.attributes)), n;
271
+ }
272
+ function O(e, t, n) {
273
+ let r = new RegExp(String.raw`\{\s*/\*\s*` + Ce(t) + String.raw`\s*[\s\S]*?\*/\s*\}`, "m"), i = Se(t, n);
274
+ return r.test(e) ? e.replace(r, i) : `${e.trimEnd()}\n\n${i}\n`;
275
+ }
276
+ function Se(e, t) {
277
+ let n = t.trim();
278
+ return n ? `{/* ${e}\n${n}\n*/}` : `{/* ${e} */}`;
279
+ }
280
+ function Ce(e) {
281
+ return e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
282
+ }
283
+ //#endregion
284
+ //#region src/fixture.ts
285
+ var k = [
286
+ "type",
287
+ "required",
288
+ "placeholder",
289
+ "maxlength",
290
+ "minlength",
291
+ "pattern",
292
+ "min",
293
+ "max",
294
+ "step",
295
+ "disabled",
296
+ "readonly",
297
+ "checked",
298
+ "aria-required",
299
+ "aria-disabled",
300
+ "aria-readonly"
301
+ ], we = t.extend({
302
+ productDocs: async ({ page: e }, t) => {
303
+ await t({ sync: (t) => A(e, t) });
304
+ },
305
+ screen: async ({ page: e }, t) => {
306
+ let n = (t) => A(e, t);
307
+ await t({
308
+ sync: n,
309
+ capture: n
310
+ });
311
+ }
312
+ });
313
+ async function A(e, t) {
314
+ let n = await x(t.mdxPath);
315
+ if (!n) throw Error(`syncProductDocs: ${t.mdxPath} has no \`annot:\` frontmatter — refusing to write.`);
316
+ let r = n.screens.find((e) => e.id === t.id);
317
+ if (!r) throw Error(`syncProductDocs: ${t.mdxPath} has no <Screen id="${t.id}"> block.`);
318
+ let i = await (t.rootLocator ?? e.locator("body")).ariaSnapshot({
319
+ mode: "ai",
320
+ boxes: !0
321
+ }), o = await j(e, r.overlays, t.attributeWhitelist ?? k), s = D(n.source, {
322
+ snapshot: i.trim(),
323
+ attributes: o
324
+ });
325
+ await a(t.mdxPath, s, "utf8");
326
+ }
327
+ var Te = A;
328
+ async function j(e, t, n) {
329
+ let r = [];
330
+ for (let i of t) {
331
+ let t = e.getByRole(i.match.role, {
332
+ name: i.match.name,
333
+ exact: !0
334
+ });
335
+ if (await t.count() !== 1) continue;
336
+ let a = await t.evaluate((e, t) => {
337
+ let n = {};
338
+ for (let r of t) {
339
+ let t = e.getAttribute(r);
340
+ t !== null && (n[r] = t);
341
+ }
342
+ return n;
343
+ }, n), o = Object.keys(a);
344
+ if (o.length !== 0) {
345
+ r.push(`${i.match.role} "${i.match.name}":`);
346
+ for (let e of o) r.push(` ${e}: ${a[e]}`);
347
+ }
348
+ }
349
+ return r.join("\n");
350
+ }
351
+ //#endregion
352
+ //#region src/mdx-annotations.ts
353
+ function M(e) {
354
+ let t = [];
355
+ for (let n of e.split(/\r?\n/)) {
356
+ if (!n.trim()) continue;
357
+ let e = n.match(/^\s*-\s+([a-z]+)(?:\s+"([^"]*?)")?/);
358
+ if (!e) continue;
359
+ let r = e[1] ?? "", i = e[2] ?? "", a = n.match(/\[ref=(e\d+)\]/)?.[1], o = n.match(/\[box=(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)\]/);
360
+ !a || !o || t.push({
361
+ role: r,
362
+ name: i,
363
+ ref: a,
364
+ box: {
365
+ x: Number.parseFloat(o[1] ?? "0"),
366
+ y: Number.parseFloat(o[2] ?? "0"),
367
+ width: Number.parseFloat(o[3] ?? "0"),
368
+ height: Number.parseFloat(o[4] ?? "0")
369
+ }
370
+ });
371
+ }
372
+ return t;
373
+ }
374
+ function N(e, t, n) {
375
+ let r = [], i = 1;
376
+ for (let a of e) {
377
+ let e = t.find((e) => e.role === a.match.role && e.name === a.match.name);
378
+ if (!e) continue;
379
+ let o = a.number ?? i++;
380
+ r.push({
381
+ type: "numberedBadge",
382
+ bbox: e.box,
383
+ number: o,
384
+ placement: "auto",
385
+ imageWidth: n.width,
386
+ imageHeight: n.height,
387
+ intent: a.intent === "action" ? "warning" : a.intent === "required" ? "error" : "info"
388
+ });
389
+ }
390
+ return r;
391
+ }
392
+ async function P(e) {
393
+ let t = e.cwd ?? process.cwd(), n = await x(ee(e.mdxPath) ? e.mdxPath : h(t, e.mdxPath));
394
+ if (!n) throw Error(`resolveMdxAnnotations: ${e.mdxPath} has no \`annot:\` frontmatter — cannot resolve.`);
395
+ let r = n.screens.find((t) => t.id === e.screenId);
396
+ if (!r) throw Error(`resolveMdxAnnotations: ${e.mdxPath} has no <Screen id="${e.screenId}"> block.`);
397
+ let i = M(n.commentBlocks.snapshot ?? "");
398
+ return N(r.overlays, i, e.dims);
399
+ }
400
+ function Ee(e) {
401
+ return `<svg xmlns="http://www.w3.org/2000/svg">${g(e)}</svg>`;
402
+ }
403
+ function De(e) {
404
+ return e.length === 0 ? F() : `<svg xmlns="http://www.w3.org/2000/svg">${g(e)}</svg>`;
405
+ }
406
+ function F() {
407
+ return "<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>";
408
+ }
409
+ //#endregion
410
+ //#region src/playwright-screenshot-hook.ts
411
+ var I = Symbol.for("@ingcreators/annot-product-docs:mdx-resolver"), L = async ({ annot: e, page: t }) => {
412
+ if (!e.mdx) return null;
413
+ let { id: n, path: r } = e.mdx;
414
+ return {
415
+ prepare: () => A(t, {
416
+ id: n,
417
+ mdxPath: r
418
+ }),
419
+ resolveAnnotations: (e) => P({
420
+ mdxPath: r,
421
+ screenId: n,
422
+ dims: e
423
+ })
424
+ };
425
+ }, R = L;
426
+ R[I] || (R[I] = !0, e.push(L));
427
+ //#endregion
428
+ //#region src/resolver.ts
429
+ function z(e) {
430
+ let t = [], n = e.split(/\r?\n/), r = [];
431
+ for (let e of n) {
432
+ if (!e.trim()) continue;
433
+ let n = e.length - e.replace(/^\s+/, "").length, i = Math.floor(n / 2), a = e.match(/^\s*-\s+([a-z]+)(?:\s+"([^"]*?)")?/);
434
+ if (!a) continue;
435
+ let o = a[1] ?? "", s = a[2] ?? "";
436
+ for (; r.length > 0 && (r[r.length - 1]?.depth ?? -1) >= i;) r.pop();
437
+ let c = r.map((e) => ({
438
+ role: e.role,
439
+ name: e.name
440
+ })), l = e.match(/\[ref=(e\d+)\]/);
441
+ l && t.push({
442
+ role: o,
443
+ name: s,
444
+ ref: l[1] ?? "",
445
+ depth: i,
446
+ ancestors: c
447
+ }), /:\s*$/.test(e) && r.push({
448
+ depth: i,
449
+ role: o,
450
+ name: s
451
+ });
452
+ }
453
+ return t;
454
+ }
455
+ async function B(e, t, n) {
456
+ let r = Ae(t.filter((t) => t.role === e.role && t.name === e.name), e.under);
457
+ if (r.length === 0) return je(e, t);
458
+ if (r.length > 1) return {
459
+ ok: !1,
460
+ kind: "ambiguous",
461
+ reason: `Multiple elements match role="${e.role}" name="${e.name}". Use \`under\` to disambiguate.`,
462
+ candidates: r.map((e) => `[ref=${e.ref}]`)
463
+ };
464
+ let i = r[0], a = ke(n, e), o = await a.count();
465
+ return o === 0 ? {
466
+ ok: !1,
467
+ kind: "live-mismatch",
468
+ reason: `Snapshot says element exists but live locator failed: role="${e.role}" name="${e.name}".`
469
+ } : o > 1 ? {
470
+ ok: !1,
471
+ kind: "ambiguous",
472
+ reason: `Live locator returns ${o} elements for role="${e.role}" name="${e.name}".`
473
+ } : {
474
+ ok: !0,
475
+ locator: a,
476
+ entry: i
477
+ };
478
+ }
479
+ async function Oe(e, t, n) {
480
+ let r = [];
481
+ for (let i of e) r.push({
482
+ overlay: i,
483
+ result: await B(i.match, t, n)
484
+ });
485
+ return r;
486
+ }
487
+ function ke(e, t) {
488
+ let n = e.getByRole(t.role, {
489
+ name: t.name,
490
+ exact: !0
491
+ });
492
+ return t.under && (n = e.getByRole(t.under.role, {
493
+ name: t.under.name,
494
+ exact: !0
495
+ }).getByRole(t.role, {
496
+ name: t.name,
497
+ exact: !0
498
+ })), n;
499
+ }
500
+ function Ae(e, t) {
501
+ return t ? e.filter((e) => e.ancestors.some((e) => e.role === t.role && e.name === t.name)) : e;
502
+ }
503
+ function je(e, t) {
504
+ let n = t.filter((t) => t.name === e.name);
505
+ if (n.length === 1 && n[0].role !== e.role) return {
506
+ ok: !1,
507
+ kind: "role-changed",
508
+ reason: `Element with name="${e.name}" exists but has role="${n[0].role}", not "${e.role}".`,
509
+ suggestion: n[0].role
510
+ };
511
+ let r = t.filter((t) => t.role === e.role);
512
+ if (r.length > 0) {
513
+ let t = Me(e.name, r.map((e) => e.name));
514
+ if (t) return {
515
+ ok: !1,
516
+ kind: "renamed",
517
+ reason: `No element with role="${e.role}" name="${e.name}". Closest match: name="${t}".`,
518
+ suggestion: t
519
+ };
520
+ }
521
+ return {
522
+ ok: !1,
523
+ kind: "not-found",
524
+ reason: `No element with role="${e.role}" name="${e.name}".`
525
+ };
526
+ }
527
+ function Me(e, t) {
528
+ if (t.length === 0) return null;
529
+ let n = t[0], r = V(e, n);
530
+ for (let i of t.slice(1)) {
531
+ let t = V(e, i);
532
+ t > r && (n = i, r = t);
533
+ }
534
+ return r > .3 ? n : null;
535
+ }
536
+ function V(e, t) {
537
+ if (!e || !t) return 0;
538
+ let n = (e) => {
539
+ let t = /* @__PURE__ */ new Set();
540
+ for (let n = 0; n < e.length - 2; n++) t.add(e.slice(n, n + 3));
541
+ return t;
542
+ }, r = n(e), i = n(t);
543
+ if (r.size === 0 || i.size === 0) return 0;
544
+ let a = 0;
545
+ for (let e of r) i.has(e) && a++;
546
+ return a / Math.max(r.size, i.size);
547
+ }
548
+ //#endregion
549
+ //#region src/drift.ts
550
+ function H(e) {
551
+ let { screen: t, liveSnapshot: n, storedAttributesYaml: r, freshAttributesYaml: i } = e, a = [], o = /* @__PURE__ */ new Set();
552
+ for (let e of t.overlays) {
553
+ let r = n.filter((t) => t.role === e.match.role && t.name === e.match.name);
554
+ if (r.length > 1) {
555
+ a.push({
556
+ severity: "error",
557
+ kind: "duplicated",
558
+ screenId: t.id,
559
+ message: `Multiple elements match role="${e.match.role}" name="${e.match.name}". Use \`under\` to disambiguate.`,
560
+ match: e.match
561
+ });
562
+ continue;
563
+ }
564
+ if (r.length === 1) continue;
565
+ let i = n.filter((t) => t.name === e.match.name);
566
+ if (i.length === 1 && i[0].role !== e.match.role) {
567
+ let n = i[0];
568
+ o.add(`${n.role}|${n.name}`), a.push({
569
+ severity: "warning",
570
+ kind: "role-changed",
571
+ screenId: t.id,
572
+ message: `Element with name="${e.match.name}" exists but has role="${n.role}", not "${e.match.role}".`,
573
+ match: e.match,
574
+ suggestion: { role: n.role }
575
+ });
576
+ continue;
577
+ }
578
+ let s = n.filter((t) => t.role === e.match.role);
579
+ if (s.length > 0) {
580
+ let n = Ie(e.match.name, s.map((e) => e.name));
581
+ if (n) {
582
+ o.add(`${e.match.role}|${n}`), a.push({
583
+ severity: "warning",
584
+ kind: "renamed",
585
+ screenId: t.id,
586
+ message: `No element with role="${e.match.role}" name="${e.match.name}". Closest match: name="${n}".`,
587
+ match: e.match,
588
+ suggestion: { name: n }
589
+ });
590
+ continue;
591
+ }
592
+ }
593
+ a.push({
594
+ severity: "error",
595
+ kind: "removed",
596
+ screenId: t.id,
597
+ message: `No element with role="${e.match.role}" name="${e.match.name}".`,
598
+ match: e.match
599
+ });
600
+ }
601
+ let s = new Set(t.overlays.map((e) => `${e.match.role}|${e.match.name}`));
602
+ for (let e of n) {
603
+ if (!Fe(e.role)) continue;
604
+ let n = `${e.role}|${e.name}`;
605
+ s.has(n) || o.has(n) || a.push({
606
+ severity: "warning",
607
+ kind: "added",
608
+ screenId: t.id,
609
+ message: `New ${e.role} "${e.name}" on the page has no <Overlay>.`,
610
+ suggestion: {
611
+ role: e.role,
612
+ name: e.name
613
+ }
614
+ });
615
+ }
616
+ return r !== void 0 && i !== void 0 && G(r) !== G(i) && a.push({
617
+ severity: "info",
618
+ kind: "attribute-drift",
619
+ screenId: t.id,
620
+ message: "`annot:attributes` block no longer matches live element attributes. Run `annot docs sync` to update."
621
+ }), a;
622
+ }
623
+ function Ne(e) {
624
+ return H({
625
+ screen: e.screen,
626
+ liveSnapshot: z(e.liveSnapshotYaml),
627
+ storedAttributesYaml: e.storedAttributesYaml,
628
+ freshAttributesYaml: e.freshAttributesYaml
629
+ });
630
+ }
631
+ function U(e) {
632
+ let t = 0, n = 0, r = 0;
633
+ for (let i of e) i.severity === "error" ? t++ : i.severity === "warning" ? n++ : r++;
634
+ return {
635
+ errors: t,
636
+ warnings: n,
637
+ infos: r
638
+ };
639
+ }
640
+ var Pe = new Set([
641
+ "button",
642
+ "checkbox",
643
+ "combobox",
644
+ "link",
645
+ "menuitem",
646
+ "menuitemcheckbox",
647
+ "menuitemradio",
648
+ "option",
649
+ "radio",
650
+ "searchbox",
651
+ "slider",
652
+ "spinbutton",
653
+ "switch",
654
+ "tab",
655
+ "textbox",
656
+ "treeitem"
657
+ ]);
658
+ function Fe(e) {
659
+ return Pe.has(e);
660
+ }
661
+ function Ie(e, t) {
662
+ if (t.length === 0) return null;
663
+ let n = t[0], r = W(e, n);
664
+ for (let i of t.slice(1)) {
665
+ let t = W(e, i);
666
+ t > r && (n = i, r = t);
667
+ }
668
+ return r > .3 ? n : null;
669
+ }
670
+ function W(e, t) {
671
+ if (!e || !t) return 0;
672
+ let n = (e) => {
673
+ let t = /* @__PURE__ */ new Set();
674
+ for (let n = 0; n < e.length - 2; n++) t.add(e.slice(n, n + 3));
675
+ return t;
676
+ }, r = n(e), i = n(t);
677
+ if (r.size === 0 || i.size === 0) return 0;
678
+ let a = 0;
679
+ for (let e of r) i.has(e) && a++;
680
+ return a / Math.max(r.size, i.size);
681
+ }
682
+ function G(e) {
683
+ return e.split(/\r?\n/).map((e) => e.trimEnd()).filter((e) => e.length > 0).join("\n");
684
+ }
685
+ function K(e) {
686
+ return e.overlays.length > 0;
687
+ }
688
+ function q(e) {
689
+ return e.filter(K);
690
+ }
691
+ //#endregion
692
+ //#region src/cli.ts
693
+ async function Le(e, t = {}) {
694
+ let n = t.stdout ?? ((e) => process.stdout.write(`${e}\n`)), r = t.stderr ?? ((e) => process.stderr.write(`${e}\n`)), i = t.cwd ?? process.cwd(), [, , a, ...o] = e;
695
+ if (a !== "init" && a !== "sync" && a !== "lint") return r(Re), +!!a;
696
+ try {
697
+ switch (a) {
698
+ case "init": return await ze(o, {
699
+ cwd: i,
700
+ stdout: n,
701
+ stderr: r
702
+ });
703
+ case "sync": return await He(o, {
704
+ ...t,
705
+ cwd: i,
706
+ stdout: n,
707
+ stderr: r
708
+ });
709
+ case "lint": return await Ue(o, {
710
+ ...t,
711
+ cwd: i,
712
+ stdout: n,
713
+ stderr: r
714
+ });
715
+ }
716
+ } catch (e) {
717
+ return r(`annot docs ${a}: ${e.message}`), 1;
718
+ }
719
+ }
720
+ var Re = [
721
+ "annot docs <command> [options]",
722
+ "",
723
+ "Commands:",
724
+ " init Scaffold annot-docs.config.ts + sample files",
725
+ " sync --url <baseUrl> Re-capture snapshot + attrs into every annot MDX",
726
+ " lint --url <baseUrl> Report drift between annot MDXs and the live page",
727
+ "",
728
+ "Options:",
729
+ " --url <baseUrl> Base URL Playwright navigates to (sync / lint)",
730
+ " --root <dir> Override MDX search root (default: docs/)",
731
+ " --help Show this help"
732
+ ].join("\n");
733
+ async function ze(e, t) {
734
+ let { cwd: r, stdout: i } = t, o = [
735
+ {
736
+ path: "annot-docs.config.ts",
737
+ content: Be
738
+ },
739
+ {
740
+ path: "tests/docs/example.spec.ts",
741
+ content: J
742
+ },
743
+ {
744
+ path: "docs/books/example/SC-001-login.mdx",
745
+ content: Ve
746
+ }
747
+ ], s = 0;
748
+ for (let e of o) {
749
+ let t = h(r, e.path);
750
+ try {
751
+ await n(p(t), { recursive: !0 }), await a(t, e.content, { flag: "wx" }), i(` created ${e.path}`), s++;
752
+ } catch (t) {
753
+ if (t.code === "EEXIST") i(` exists ${e.path} (skipped)`);
754
+ else throw t;
755
+ }
756
+ }
757
+ return i(s === 0 ? "annot docs init: nothing to do (all targets exist)" : `annot docs init: wrote ${s} file(s)`), 0;
758
+ }
759
+ var Be = "// Living product docs config for annot.\n// See https://github.com/ingcreators/annot/blob/main/docs/plans/living-product-docs.md\nimport { defineConfig } from \"@ingcreators/annot-product-docs\";\n\nexport default defineConfig({\n meta: {\n projectName: \"Example\",\n },\n xlsx: {\n defaultBook: \"Screen spec\",\n books: {\n \"Screen spec\": {\n // Drop your customer-supplied template here (Phase 3) and\n // the Excel adapter will fill it. Until then the OSS\n // default layout applies.\n // template: \"./templates/customer-screen-spec.xlsx\",\n },\n },\n },\n});\n", J = "// Tour file — runs through every screen and refreshes the\n// matching MDX's annot:snapshot / annot:attributes blocks.\n//\n// Run with: pnpm playwright test tests/docs/\n\nimport { test } from \"@ingcreators/annot-product-docs\";\n\ntest.describe.configure({ mode: \"serial\" });\n\ntest(\"login flow\", async ({ page, productDocs }) => {\n await page.goto(\"/login\");\n await productDocs.sync({\n id: \"login\",\n mdxPath: \"docs/books/example/SC-001-login.mdx\",\n });\n});\n", Ve = "---\nannot:\n id: SC-001\n title: Login screen\n meta:\n author: TODO\n xlsx:\n book: Screen spec\n sheet: SC-001 Login\n role: screen\n order: 100\n---\n\nimport { Screen, Overlay } from \"@ingcreators/annot-product-docs-astro\";\n\n# Login screen\n\nEnter your credentials to access the system.\n\n<Screen id=\"login\" src=\"./shots/login.png\">\n\n<Overlay match={{ role: \"textbox\", name: \"Email\" }} intent=\"required\" number={1}>\n**Email** — Enter your registered email address.\n</Overlay>\n\n<Overlay match={{ role: \"button\", name: \"Sign in\" }} intent=\"action\" number={2}>\nClick to sign in.\n</Overlay>\n\n</Screen>\n";
760
+ async function He(e, t) {
761
+ let { cwd: n, stdout: r, stderr: i } = t, a = Z(e);
762
+ if (!a.url) return i("annot docs sync: --url <baseUrl> is required."), 1;
763
+ let o = await $(await Q(h(n, a.root ?? "docs")));
764
+ if (o.length === 0) return r(`annot docs sync: no MDX files with \`annot:\` frontmatter under ${a.root ?? "docs"}.`), 0;
765
+ r(`annot docs sync: ${o.length} MDX file(s) → ${a.url}`);
766
+ let { newPage: s, close: c } = await Y(a.url, t);
767
+ try {
768
+ for (let e of o) {
769
+ let t = await x(e);
770
+ if (!t) continue;
771
+ let i = q(t.screens);
772
+ if (i.length === 0) {
773
+ r(` skip ${m(n, e)} (no <Screen> blocks)`);
774
+ continue;
775
+ }
776
+ for (let t of i) {
777
+ let { page: i, dispose: a } = await s(t.src ?? "/");
778
+ try {
779
+ await A(i, {
780
+ id: t.id,
781
+ mdxPath: e
782
+ }), r(` synced ${m(n, e)} (screen=${t.id})`);
783
+ } finally {
784
+ await a();
785
+ }
786
+ }
787
+ }
788
+ } finally {
789
+ await c();
790
+ }
791
+ return 0;
792
+ }
793
+ async function Ue(e, t) {
794
+ let { cwd: n, stdout: r, stderr: i } = t, a = Z(e);
795
+ if (!a.url) return i("annot docs lint: --url <baseUrl> is required."), 1;
796
+ let o = await $(await Q(h(n, a.root ?? "docs")));
797
+ if (o.length === 0) return a.json ? r(JSON.stringify({
798
+ findings: [],
799
+ summary: {
800
+ errors: 0,
801
+ warnings: 0,
802
+ infos: 0
803
+ }
804
+ })) : r(`annot docs lint: no MDX files with \`annot:\` frontmatter under ${a.root ?? "docs"}.`), 0;
805
+ a.json || r(`annot docs lint: ${o.length} MDX file(s) → ${a.url}`);
806
+ let s = [], c = /* @__PURE__ */ new Set(), { newPage: l, close: u } = await Y(a.url, t);
807
+ try {
808
+ for (let e of o) {
809
+ let t = await x(e);
810
+ if (t) for (let r of q(t.screens)) {
811
+ let { page: t, dispose: i } = await l(r.src ?? "/");
812
+ try {
813
+ let i = H({
814
+ screen: r,
815
+ liveSnapshot: z(await t.locator("body").ariaSnapshot({ mode: "ai" }))
816
+ });
817
+ for (let t of i) s.push({
818
+ file: m(n, e),
819
+ finding: t
820
+ }), c.add(e);
821
+ } finally {
822
+ await i();
823
+ }
824
+ }
825
+ }
826
+ } finally {
827
+ await u();
828
+ }
829
+ if (a.json) {
830
+ let e = U(s.map((e) => e.finding));
831
+ r(JSON.stringify({
832
+ findings: s.map(({ file: e, finding: t }) => ({
833
+ file: e,
834
+ severity: t.severity,
835
+ kind: t.kind,
836
+ screenId: t.screenId,
837
+ message: t.message,
838
+ match: t.match,
839
+ suggestion: t.suggestion
840
+ })),
841
+ summary: e
842
+ }));
843
+ } else {
844
+ for (let { file: e, finding: t } of s) r(`${We(t.severity)} ${e} [${t.screenId}] ${t.kind}: ${t.message}`);
845
+ let e = U(s.map((e) => e.finding));
846
+ r(`annot docs lint: ${e.errors} error(s), ${e.warnings} warning(s), ${e.infos} info(s).`);
847
+ }
848
+ let d = 0;
849
+ if (a.fix && c.size > 0) {
850
+ let { newPage: e, close: n } = await Y(a.url, t);
851
+ try {
852
+ for (let t of c) {
853
+ let n = await x(t);
854
+ if (n) for (let r of q(n.screens)) {
855
+ let { page: n, dispose: i } = await e(r.src ?? "/");
856
+ try {
857
+ await A(n, {
858
+ id: r.id,
859
+ mdxPath: t
860
+ }), d++;
861
+ } finally {
862
+ await i();
863
+ }
864
+ }
865
+ }
866
+ } finally {
867
+ await n();
868
+ }
869
+ a.json || r(`annot docs lint --fix: refreshed snapshot/attributes for ${d} screen(s).`);
870
+ }
871
+ let f = U(s.map((e) => e.finding));
872
+ return a.ci ? +(f.errors + f.warnings > 0) : +(f.errors > 0);
873
+ }
874
+ async function Y(e, t) {
875
+ if (t.newPage) return {
876
+ async newPage(n) {
877
+ let { page: r, close: i } = await t.newPage(X(e, n));
878
+ return {
879
+ page: r,
880
+ dispose: i
881
+ };
882
+ },
883
+ async close() {}
884
+ };
885
+ let { chromium: n } = await import("playwright-core"), r = await n.launch({ headless: !0 });
886
+ return {
887
+ async newPage(t) {
888
+ let n = await r.newContext(), i = await n.newPage();
889
+ return await i.goto(X(e, t)), {
890
+ page: i,
891
+ dispose: async () => {
892
+ await n.close();
893
+ }
894
+ };
895
+ },
896
+ async close() {
897
+ await r.close();
898
+ }
899
+ };
900
+ }
901
+ function X(e, t) {
902
+ return /^https?:\/\//.test(t) ? t : t.startsWith("/") ? e.replace(/\/$/, "") + t : e;
903
+ }
904
+ function Z(e) {
905
+ let t = {};
906
+ for (let n = 0; n < e.length; n++) {
907
+ let r = e[n];
908
+ r === "--url" || r === "-u" ? t.url = e[++n] : r === "--root" || r === "-r" ? t.root = e[++n] : r === "--json" ? t.json = !0 : r === "--ci" ? t.ci = !0 : r === "--fix" && (t.fix = !0);
909
+ }
910
+ return t;
911
+ }
912
+ async function Q(e) {
913
+ let t = [], n = [e];
914
+ for (; n.length > 0;) {
915
+ let e = n.pop(), r;
916
+ try {
917
+ r = await i(e, { withFileTypes: !0 });
918
+ } catch (e) {
919
+ if (e.code === "ENOENT") continue;
920
+ throw e;
921
+ }
922
+ for (let i of r) {
923
+ let r = te(e, i.name);
924
+ if (i.isDirectory()) {
925
+ if (i.name === "node_modules" || i.name.startsWith(".")) continue;
926
+ n.push(r);
927
+ } else i.isFile() && i.name.endsWith(".mdx") && t.push(r);
928
+ }
929
+ }
930
+ return t.sort();
931
+ }
932
+ async function $(e) {
933
+ let t = [];
934
+ for (let n of e) try {
935
+ await x(n) && t.push(n);
936
+ } catch {}
937
+ return t;
938
+ }
939
+ function We(e) {
940
+ switch (e) {
941
+ case "error": return "ERROR ";
942
+ case "warning": return "WARN ";
943
+ case "info": return "INFO ";
944
+ }
945
+ }
946
+ //#endregion
947
+ export { k as DEFAULT_ATTR_WHITELIST, v as annotDocsConfigSchema, _ as annotFrontmatterSchema, N as buildBadgeAnnotations, Te as captureScreen, j as collectAttributesYaml, oe as defineConfig, H as detectDrift, Ne as detectDriftFromYaml, F as emptyAnnotationsSvg, $ as filterAnnotMdxFiles, K as isLintableScreen, se as isScreenRole, q as lintableScreens, Le as main, S as parseMdx, x as parseMdxFile, z as parseSnapshot, M as parseSnapshotBoxes, B as resolveMatch, P as resolveMdxAnnotations, Oe as resolveOverlays, U as summariseDrift, Ee as svgFromBadges, De as svgFromBboxAnnotations, A as syncProductDocs, we as test, D as updateCommentBlocks, Q as walkMdx };