@lotics/cli 0.263.0 → 0.266.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.
@@ -1,2381 +0,0 @@
1
- "use strict";
2
- var __loticsProbe = (() => {
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
-
21
- // src/probe_page.ts
22
- var probe_page_exports = {};
23
- __export(probe_page_exports, {
24
- PROBES: () => PROBES,
25
- contentHeight: () => contentHeight,
26
- doors: () => doors,
27
- editorSelector: () => editorSelector,
28
- focused: () => focused,
29
- openDoor: () => openDoor,
30
- openRecord: () => openRecord,
31
- openScreen: () => openScreen,
32
- overlaysOpen: () => overlaysOpen,
33
- paintState: () => paintState,
34
- recordRows: () => recordRows,
35
- rings: () => rings,
36
- route: () => route,
37
- run: () => run,
38
- screens: () => screens,
39
- surfaceReport: () => surfaceReport,
40
- textLeafCount: () => textLeafCount
41
- });
42
-
43
- // src/probe_collectors.ts
44
- function moneyColumns(root = document.body) {
45
- const money = /^[-+−]?\s?(?:(?:[$€]|USD|EUR|VND)\s?[\d.,]+\s?(?:[KMB]|tr|tỷ)?|[\d.,]+\s?(?:tr|tỷ)?\s?(?:₫|đ|VND|USD|EUR|[$€]))$/i;
46
- const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
47
- const bandGap = 48;
48
- const leaves = [];
49
- const walk = (node) => {
50
- if (node.nodeType === Node.TEXT_NODE) {
51
- const text = (node.textContent || "").trim();
52
- const parent = node.parentElement;
53
- if (!text || !parent || skipTag.test(parent.tagName)) return;
54
- if (typeof parent.checkVisibility === "function" && !parent.checkVisibility()) return;
55
- if (!money.test(text)) return;
56
- const range = document.createRange();
57
- range.selectNodeContents(node);
58
- let rect = range.getBoundingClientRect();
59
- if (rect.width === 0 && rect.height === 0) rect = parent.getBoundingClientRect();
60
- if (rect.width === 0 && rect.height === 0) return;
61
- leaves.push({
62
- text,
63
- left: Math.round(rect.left),
64
- right: Math.round(rect.right),
65
- top: Math.round(rect.top),
66
- bottom: Math.round(rect.bottom)
67
- });
68
- return;
69
- }
70
- node.childNodes.forEach(walk);
71
- };
72
- walk(root);
73
- const bands = [];
74
- for (const leaf of [...leaves].sort((a, b) => a.top - b.top)) {
75
- const band = bands[bands.length - 1];
76
- if (band !== void 0 && leaf.top - band.bottom <= bandGap) {
77
- band.items.push(leaf);
78
- band.bottom = Math.max(band.bottom, leaf.bottom);
79
- } else {
80
- bands.push({ bottom: leaf.bottom, items: [leaf] });
81
- }
82
- }
83
- const columns = [];
84
- for (const band of bands) {
85
- const clusters = [];
86
- for (const leaf of [...band.items].sort((a, b) => a.left - b.left)) {
87
- const cluster = clusters[clusters.length - 1];
88
- if (cluster !== void 0 && leaf.left < cluster.right) {
89
- cluster.items.push(leaf);
90
- cluster.right = Math.max(cluster.right, leaf.right);
91
- } else {
92
- clusters.push({ left: leaf.left, right: leaf.right, items: [leaf] });
93
- }
94
- }
95
- for (const cluster of clusters) {
96
- const firstByEdge = /* @__PURE__ */ new Map();
97
- for (const item of cluster.items) if (!firstByEdge.has(item.right)) firstByEdge.set(item.right, item.text);
98
- const edges = [...firstByEdge.entries()].sort((a, b) => a[0] - b[0]);
99
- columns.push({
100
- x: cluster.left,
101
- size: cluster.items.length,
102
- edges: edges.map(([edge]) => edge),
103
- samples: edges.map(([, text]) => text)
104
- });
105
- }
106
- }
107
- return { total: leaves.length, columns };
108
- }
109
- function reportedValues(root = document.body) {
110
- const numeric = /^[-+−]?\d[\d.,]*\s?(?:%|[A-Za-z₫đ$€]{1,4})?$/;
111
- const counted = /(?:%|[A-Za-z₫đ$€]{1,4})$/;
112
- const delta = /^[↑↓→▲▼]\s*[-+−]?\d[\d.,]*\s?%$/;
113
- const dateLike = /\d[/:-]\d/;
114
- const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
115
- const notAValue = 'a, button, [role="tab"], [role="button"], input, th, [role="columnheader"], nav';
116
- const pictureMin = 32;
117
- const nameReach = 360;
118
- const values = [];
119
- const labels = [];
120
- const deltaLeaves = [];
121
- const walk = (node) => {
122
- if (node.nodeType === Node.TEXT_NODE) {
123
- const text = (node.textContent || "").trim();
124
- const parent = node.parentElement;
125
- if (!text || !parent || skipTag.test(parent.tagName)) return;
126
- if (typeof parent.checkVisibility === "function" && !parent.checkVisibility()) return;
127
- const rect = parent.getBoundingClientRect();
128
- if (rect.width === 0 && rect.height === 0) return;
129
- const leaf = { text, left: Math.round(rect.left), right: Math.round(rect.right), top: Math.round(rect.top) };
130
- if (delta.test(text)) {
131
- deltaLeaves.push(leaf);
132
- return;
133
- }
134
- if (!numeric.test(text) || dateLike.test(text)) {
135
- labels.push(leaf);
136
- return;
137
- }
138
- if (parent.closest(notAValue)) return;
139
- values.push(leaf);
140
- return;
141
- }
142
- node.childNodes.forEach(walk);
143
- };
144
- walk(root);
145
- const clusters = [];
146
- for (const leaf of [...values].sort((a, b) => a.left - b.left || a.top - b.top)) {
147
- const cluster = clusters.find((c) => leaf.left < c.right && leaf.right > c.left);
148
- if (cluster !== void 0) {
149
- cluster.items.push(leaf);
150
- cluster.left = Math.min(cluster.left, leaf.left);
151
- cluster.right = Math.max(cluster.right, leaf.right);
152
- } else {
153
- clusters.push({ left: leaf.left, right: leaf.right, items: [leaf] });
154
- }
155
- }
156
- const nameOf2 = (left, right, top, inkLeft) => {
157
- const above = labels.filter((label) => label.top < top && top - label.top <= nameReach && label.left < right && label.right > left).sort((a, b) => b.top - a.top)[0];
158
- const beside = labels.filter((label) => Math.abs(label.top - top) <= 8 && label.right <= inkLeft).sort((a, b) => b.right - a.right)[0];
159
- return (above ?? beside)?.text.slice(0, 40) ?? "";
160
- };
161
- const columns = [];
162
- let bare = 0;
163
- let quantities = 0;
164
- for (const cluster of clusters) {
165
- const items = [...cluster.items].sort((a, b) => a.top - b.top);
166
- const ordinal = items.length >= 3 && items.every((item, index) => item.text === String(index + 1));
167
- if (ordinal) continue;
168
- const top = items[0];
169
- const scaled = items.filter((item) => counted.test(item.text)).length;
170
- bare += items.length - scaled;
171
- quantities += scaled;
172
- columns.push({
173
- slot: nameOf2(cluster.left, cluster.right, top.top, top.left),
174
- size: items.length,
175
- quantities: scaled,
176
- samples: items.slice(0, 4).map((item) => item.text)
177
- });
178
- }
179
- columns.sort((a, b) => b.size - a.size);
180
- const drawn = Array.from(root.querySelectorAll('[role="meter"], [role="progressbar"], canvas')).filter((el) => {
181
- const r = el.getBoundingClientRect();
182
- return r.width > 0 && r.height > 0;
183
- });
184
- const reads = (el) => {
185
- const role = el.getAttribute("role");
186
- if (role !== "meter" && role !== "progressbar") return true;
187
- const stated2 = (name) => {
188
- const raw = el.getAttribute(name);
189
- return raw === null || raw.trim() === "" ? Number.NaN : Number(raw);
190
- };
191
- const bound = (name, fallback) => {
192
- const value = stated2(name);
193
- return Number.isFinite(value) ? value : fallback;
194
- };
195
- const now = stated2("aria-valuenow");
196
- if (!Number.isFinite(now)) return false;
197
- const floor = bound("aria-valuemin", 0);
198
- const top = bound("aria-valuemax", 100);
199
- return top > floor && now < top;
200
- };
201
- const pictures = Array.from(root.querySelectorAll('[role="img"], svg, img')).filter((el) => {
202
- if (el.getAttribute("aria-hidden") === "true") return false;
203
- const r = el.getBoundingClientRect();
204
- return r.width >= pictureMin && r.height >= pictureMin;
205
- });
206
- const markMin = 4;
207
- const notData = 'a, button, input, nav, [role="banner"], [role="button"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"]';
208
- const marked = (el) => {
209
- if (el.closest(notData) !== null) return false;
210
- const r = el.getBoundingClientRect();
211
- return r.width >= markMin && r.height >= markMin && (r.width < pictureMin || r.height < pictureMin);
212
- };
213
- const glyphs = Array.from(root.querySelectorAll('[role="img"]')).filter(
214
- (el) => el.getAttribute("aria-hidden") !== "true" && (el.getAttribute("aria-label") || "").trim() !== "" && marked(el)
215
- );
216
- const already = [...drawn, ...pictures, ...glyphs];
217
- const swatches = Array.from(root.querySelectorAll("*")).filter((el) => {
218
- if ((el.textContent || "").trim() !== "") return false;
219
- if (already.some((device) => device === el || device.contains(el))) return false;
220
- if (!marked(el)) return false;
221
- const ground = getComputedStyle(el).backgroundColor;
222
- return ground !== "" && ground !== "transparent" && !/,\s*0(?:\.0+)?\)\s*$/.test(ground);
223
- });
224
- const tells = (marks, sign) => new Set(marks.map(sign)).size > 1 ? marks.length : 0;
225
- const categorical = tells(swatches, (el) => getComputedStyle(el).backgroundColor) + tells(glyphs, (el) => el.getAttribute("aria-label") || "");
226
- const tally = /* @__PURE__ */ new Map();
227
- const note = (kind, box) => {
228
- const slot = nameOf2(box.left, box.right, box.top, box.left);
229
- const key = `${kind}\0${slot}`;
230
- const seen = tally.get(key);
231
- if (seen === void 0) tally.set(key, { kind, slot, count: 1 });
232
- else seen.count += 1;
233
- };
234
- const noteElement = (kind, el) => {
235
- const r = el.getBoundingClientRect();
236
- note(kind, { left: Math.round(r.left), right: Math.round(r.right), top: Math.round(r.top) });
237
- };
238
- for (const el of drawn) noteElement(el.tagName === "CANVAS" ? "chart" : "meter", el);
239
- for (const el of pictures) noteElement("picture", el);
240
- for (const el of swatches) noteElement("swatch", el);
241
- for (const el of glyphs) noteElement("glyph", el);
242
- for (const leaf of deltaLeaves) note("delta", leaf);
243
- const deviceKinds = [...tally.values()].sort(
244
- (a, b) => b.count - a.count || (a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0)
245
- );
246
- const slotMax = pictureMin * 4;
247
- const isSlot = (el) => {
248
- if ((el.textContent || "").trim() !== "") return false;
249
- if (el.closest(notData) !== null) return false;
250
- const r = el.getBoundingClientRect();
251
- if (r.width < pictureMin || r.height < pictureMin) return false;
252
- return r.width <= slotMax && r.height <= slotMax;
253
- };
254
- const holdsPicture = (el) => {
255
- if (/^(IMG|CANVAS|VIDEO)$/.test(el.tagName)) return true;
256
- for (const media of el.querySelectorAll("img, canvas, video")) {
257
- const r = media.getBoundingClientRect();
258
- if (r.width > 0 && r.height > 0) return true;
259
- }
260
- for (const vector of el.querySelectorAll("svg")) {
261
- const r = vector.getBoundingClientRect();
262
- if (r.width >= pictureMin && r.height >= pictureMin) return true;
263
- }
264
- return false;
265
- };
266
- const slotBoxes = Array.from(root.querySelectorAll("*")).filter(
267
- (el) => isSlot(el) && !Array.from(el.children).some(isSlot)
268
- );
269
- const slotClusters = [];
270
- for (const el of slotBoxes) {
271
- const r = el.getBoundingClientRect();
272
- const box = { left: Math.round(r.left), right: Math.round(r.right), top: Math.round(r.top) };
273
- const cluster = slotClusters.find((c) => box.left < c.right && box.right > c.left);
274
- if (cluster !== void 0) {
275
- cluster.items.push(el);
276
- cluster.left = Math.min(cluster.left, box.left);
277
- cluster.right = Math.max(cluster.right, box.right);
278
- } else {
279
- slotClusters.push({ left: box.left, right: box.right, items: [el] });
280
- }
281
- }
282
- const markSlots = slotClusters.filter((cluster) => cluster.items.length >= 3).map((cluster) => {
283
- const tops = cluster.items.map((el) => Math.round(el.getBoundingClientRect().top));
284
- return {
285
- slot: nameOf2(cluster.left, cluster.right, Math.min(...tops), cluster.left),
286
- size: cluster.items.length,
287
- filled: cluster.items.filter(holdsPicture).length
288
- };
289
- }).sort((a, b) => b.size - a.size);
290
- const encoded = drawn.filter(reads).length + pictures.length + deltaLeaves.length + categorical;
291
- return {
292
- total: bare + quantities + encoded,
293
- bare,
294
- quantities,
295
- encoded,
296
- devices: drawn.length + pictures.length + deltaLeaves.length + swatches.length + glyphs.length,
297
- deviceKinds,
298
- columns,
299
- markSlots
300
- };
301
- }
302
- function stageWordColumns(root = document.body) {
303
- const tabs = [];
304
- for (const tab of Array.from(root.querySelectorAll('[role="tablist"] [role="tab"]'))) {
305
- const label = (tab.textContent || "").trim();
306
- if (label !== "" && !tabs.includes(label)) tabs.push(label);
307
- }
308
- if (tabs.length < 2) return { total: 0, wordsOnly: [] };
309
- const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
310
- const notContent = 'nav, [role="banner"], [role="tab"], [role="tablist"]';
311
- const markMin = 4;
312
- const pictureMin = 32;
313
- const nameReach = 360;
314
- const unpainted = (ground) => ground === "" || ground === "transparent" || /,\s*0(?:\.0+)?\)\s*$/.test(ground);
315
- const leaves = [];
316
- const labels = [];
317
- const walk = (node) => {
318
- if (node.nodeType === Node.TEXT_NODE) {
319
- const text = (node.textContent || "").trim();
320
- const parent = node.parentElement;
321
- if (!text || !parent || skipTag.test(parent.tagName)) return;
322
- if (typeof parent.checkVisibility === "function" && !parent.checkVisibility()) return;
323
- const rect = parent.getBoundingClientRect();
324
- if (rect.width === 0 && rect.height === 0) return;
325
- const box = {
326
- text,
327
- left: Math.round(rect.left),
328
- right: Math.round(rect.right),
329
- top: Math.round(rect.top)
330
- };
331
- labels.push(box);
332
- if (tabs.includes(text) && !parent.closest(notContent)) leaves.push({ el: parent, ...box });
333
- return;
334
- }
335
- node.childNodes.forEach(walk);
336
- };
337
- walk(root);
338
- const clusters = [];
339
- for (const leaf of [...leaves].sort((a, b) => a.left - b.left || a.top - b.top)) {
340
- const cluster = clusters.find((c) => leaf.left < c.right && leaf.right > c.left);
341
- if (cluster !== void 0) {
342
- cluster.items.push(leaf);
343
- cluster.left = Math.min(cluster.left, leaf.left);
344
- cluster.right = Math.max(cluster.right, leaf.right);
345
- } else {
346
- clusters.push({ left: leaf.left, right: leaf.right, items: [leaf] });
347
- }
348
- }
349
- const nameOf2 = (left, right, top) => {
350
- const above = labels.filter((label) => label.top < top && top - label.top <= nameReach && label.left < right && label.right > left).sort((a, b) => b.top - a.top)[0];
351
- const beside = labels.filter((label) => Math.abs(label.top - top) <= 8 && label.right <= left).sort((a, b) => b.right - a.right)[0];
352
- return (above ?? beside)?.text.slice(0, 40) ?? "";
353
- };
354
- const cellOf = (leaf, left, right) => {
355
- let cell = leaf.el;
356
- let node = leaf.el.parentElement;
357
- while (node !== null) {
358
- const r = node.getBoundingClientRect();
359
- if (Math.round(r.left) < left - pictureMin || Math.round(r.right) > right + pictureMin) break;
360
- cell = node;
361
- node = node.parentElement;
362
- }
363
- return cell;
364
- };
365
- const holdsMark = (cell, word) => Array.from(cell.querySelectorAll("*")).some((el) => {
366
- if (el === word || el.contains(word)) return false;
367
- const r = el.getBoundingClientRect();
368
- if (r.width < markMin || r.height < markMin) return false;
369
- if (el.getAttribute("aria-hidden") === "true") return false;
370
- if (el.getAttribute("role") === "img") return (el.getAttribute("aria-label") || "").trim() !== "";
371
- if (r.width >= pictureMin && r.height >= pictureMin) return false;
372
- if ((el.textContent || "").trim() !== "") return false;
373
- return !unpainted(getComputedStyle(el).backgroundColor);
374
- });
375
- const columns = [];
376
- const wordsOnly = [];
377
- for (const cluster of clusters) {
378
- if (cluster.items.length < 3) continue;
379
- const cells = cluster.items.map((leaf) => ({ leaf, cell: cellOf(leaf, cluster.left, cluster.right) }));
380
- const column = {
381
- slot: nameOf2(cluster.left, cluster.right, Math.min(...cluster.items.map((item) => item.top))),
382
- size: cluster.items.length,
383
- values: tabs.filter((tab) => cluster.items.some((item) => item.text === tab))
384
- };
385
- columns.push(column);
386
- const marked = cells.some(({ cell, leaf }) => holdsMark(cell, leaf.el)) || new Set(cells.map(({ cell }) => getComputedStyle(cell).backgroundColor)).size > 1;
387
- if (!marked) wordsOnly.push(column);
388
- }
389
- return { total: columns.length, wordsOnly };
390
- }
391
- function internalIds(root = document.body) {
392
- const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
393
- const id = /\b(?:opt|fld|tbl|rec|wfl|mbr|app|wsp)_[A-Za-z0-9]{6,}\b/g;
394
- const ids = /* @__PURE__ */ new Set();
395
- let total = 0;
396
- const walk = (node) => {
397
- if (node.nodeType === Node.TEXT_NODE) {
398
- const parent = node.parentElement;
399
- if (!parent || skipTag.test(parent.tagName)) return;
400
- const text = node.textContent || "";
401
- if (!text.trim()) return;
402
- total += 1;
403
- for (const match of text.match(id) ?? []) ids.add(match);
404
- return;
405
- }
406
- node.childNodes.forEach(walk);
407
- };
408
- walk(root);
409
- return { total, ids: [...ids] };
410
- }
411
- function tabStrips(root = document.body) {
412
- const all = Array.from(root.querySelectorAll('[role="tablist"]')).filter((el) => {
413
- const r = el.getBoundingClientRect();
414
- return r.width > 0 && r.height > 0;
415
- });
416
- const counted = all.filter((el) => el.getAttribute("data-order") !== "sequence");
417
- return {
418
- total: all.length,
419
- counted: counted.length,
420
- strips: counted.map((el) => Array.from(el.querySelectorAll('[role="tab"]')).map((tab) => (tab.textContent || "").trim()))
421
- };
422
- }
423
- var SINGLE_LINE_FIELDS = /* @__PURE__ */ new Set(["text", "search", "tel", "url", "email", "number"]);
424
- function clippingAncestor(el, stop) {
425
- for (let at = el.parentElement; at !== null; at = at.parentElement) {
426
- const style = getComputedStyle(at);
427
- const x = style.overflowX || style.overflow;
428
- if (x === "hidden" || x === "clip") return at;
429
- if (at === stop) return null;
430
- }
431
- return null;
432
- }
433
- function truncatedLeaves(root = document.body) {
434
- const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
435
- const clippedAway2 = /^inset\(\s*50%/;
436
- const breakable = /[\t\n\v\f\r \u200b\u3000]/;
437
- const cut = [];
438
- let total = 0;
439
- for (const el of Array.from(root.querySelectorAll("*"))) {
440
- if (skipTag.test(el.tagName) || el.children.length > 0) continue;
441
- const field = el instanceof HTMLInputElement && SINGLE_LINE_FIELDS.has(el.type) ? el : void 0;
442
- const text = (field === void 0 ? el.textContent || "" : field.value).trim();
443
- if (!text) continue;
444
- if (typeof el.checkVisibility === "function" && !el.checkVisibility()) continue;
445
- const style = getComputedStyle(el);
446
- const box = el.getBoundingClientRect();
447
- if (box.width <= 1 && box.height <= 1) continue;
448
- if (clippedAway2.test(style.getPropertyValue("clip-path"))) continue;
449
- total += 1;
450
- if (style.overflow === "visible") {
451
- const within2 = el.closest('.lotics-fact-groups, [role="listbox"]');
452
- const clip = within2 === null ? null : clippingAncestor(el, within2);
453
- if (clip !== null) {
454
- const edge = clip.getBoundingClientRect();
455
- const shown = Math.min(box.right, edge.right) - Math.max(box.left, edge.left);
456
- if (box.right > edge.right + 1 || box.left < edge.left - 1) cut.push({ text: text.slice(0, 40), axis: "width", has: Math.max(0, Math.round(shown)), needs: Math.round(box.width) });
457
- }
458
- continue;
459
- }
460
- const states = (property) => {
461
- const value = style.getPropertyValue(property);
462
- return value !== "" && value !== "none";
463
- };
464
- if ((field !== void 0 || states("-webkit-line-clamp") || states("line-clamp")) && breakable.test(text)) continue;
465
- if (el.scrollWidth > el.clientWidth + 1) {
466
- cut.push({ text: text.slice(0, 40), axis: "width", has: el.clientWidth, needs: el.scrollWidth });
467
- } else if (el.scrollHeight > el.clientHeight + 1) {
468
- cut.push({ text: text.slice(0, 40), axis: "height", has: el.clientHeight, needs: el.scrollHeight });
469
- }
470
- }
471
- return { total, cut };
472
- }
473
- function bannedGlyphs(root = document.body) {
474
- const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
475
- const found = [];
476
- let total = 0;
477
- const walk = (node) => {
478
- if (node.nodeType === Node.TEXT_NODE) {
479
- const parent = node.parentElement;
480
- if (!parent || skipTag.test(parent.tagName) || parent.tagName === "TEXTAREA" || parent.closest('[data-content], [contenteditable="true"]') !== null) return;
481
- const text = node.textContent || "";
482
- if (!text.trim()) return;
483
- total += 1;
484
- if (text.includes(" \xB7 ")) found.push(text.trim().slice(0, 40));
485
- return;
486
- }
487
- node.childNodes.forEach(walk);
488
- };
489
- walk(root);
490
- return { total, found };
491
- }
492
- function compactMoneyWords(root = document.body) {
493
- const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
494
- const any = /\d[\d.,]*\s?(?:[KMB](?![a-z])|tr(?![a-z])|tỷ)/i;
495
- const dongInEnglishWords = /\d[\d.,]*\s?[KMB]\s?(?:₫|đ|VND)|(?:₫|VND)\s?\d[\d.,]*\s?[KMB](?![a-z])/i;
496
- const dollarInVietnameseWords = /[$€]\s?\d[\d.,]*\s?(?:tr(?![a-z])|tỷ)|\d[\d.,]*\s?(?:tr|tỷ)\s?(?:[$€]|USD|EUR)(?![a-z])/i;
497
- const foreign = [];
498
- let total = 0;
499
- const walk = (node) => {
500
- if (node.nodeType === Node.TEXT_NODE) {
501
- const parent = node.parentElement;
502
- if (!parent || skipTag.test(parent.tagName)) return;
503
- const text = (node.textContent || "").trim();
504
- if (!text) return;
505
- if (any.test(text)) total += 1;
506
- if (dongInEnglishWords.test(text) || dollarInVietnameseWords.test(text)) foreign.push(text.slice(0, 40));
507
- return;
508
- }
509
- node.childNodes.forEach(walk);
510
- };
511
- walk(root);
512
- return { total, foreign };
513
- }
514
- function scanStackedPairs(root = document.body) {
515
- const px2 = (el) => parseFloat(getComputedStyle(el).fontSize);
516
- const weight = (el) => parseInt(getComputedStyle(el).fontWeight, 10) || 400;
517
- const isLeaf = (el) => el.children.length === 0 && (el.textContent || "").trim().length > 0;
518
- const leafOf = (el, depth = 3) => {
519
- if (isLeaf(el)) return el;
520
- if (depth === 0 || el.children.length === 0) return null;
521
- return leafOf(el.children[0], depth - 1);
522
- };
523
- const sameColumn = 8;
524
- const heading = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
525
- const collection = 'ul, ol, dl, table, [role="list"], [role="grid"], [role="table"]';
526
- const pairShaped = (box) => box.children.length === 2 && leafOf(box.children[0]) !== null && leafOf(box.children[1]) !== null;
527
- const names = (line, box) => {
528
- const id = line.getAttribute("id") || "";
529
- return id !== "" && (box.getAttribute("aria-labelledby") || "").split(/\s+/).includes(id);
530
- };
531
- let total = 0;
532
- const overDemoted = [];
533
- for (const el of Array.from(root.querySelectorAll("div"))) {
534
- const kids = Array.from(el.children);
535
- if (kids.length !== 2) continue;
536
- const [first, second] = kids;
537
- const subject = leafOf(first);
538
- const supporting = leafOf(second);
539
- if (!subject || !supporting) continue;
540
- if (first.closest('[role="banner"]')) continue;
541
- const a = first.getBoundingClientRect();
542
- const b = second.getBoundingClientRect();
543
- if (b.top < a.bottom - 2) continue;
544
- if (Math.abs(b.left - a.left) > sameColumn) continue;
545
- if (a.height === 0 || b.height === 0) continue;
546
- if (first.matches(heading) || first.querySelector(heading) !== null) continue;
547
- if ([first, subject].some((line) => [el, second].some((box) => names(line, box)))) continue;
548
- if (second.matches(collection) || Array.from(second.children).some(pairShaped)) continue;
549
- total += 1;
550
- if (px2(supporting) < px2(subject) && weight(subject) > weight(supporting)) {
551
- overDemoted.push({
552
- subject: (subject.textContent || "").trim().slice(0, 60),
553
- supporting: (supporting.textContent || "").trim().slice(0, 60),
554
- sizes: [px2(subject), px2(supporting)],
555
- weights: [weight(subject), weight(supporting)]
556
- });
557
- }
558
- }
559
- return { total, overDemoted };
560
- }
561
- function scanRowMarks(root = document.body) {
562
- const marks = [];
563
- const seen = [];
564
- const record = (el, text) => {
565
- if (seen.some((other) => other.contains(el) || el.contains(other))) return;
566
- seen.push(el);
567
- const r = el.getBoundingClientRect();
568
- marks.push({ text, w: Math.round(r.width), y: Math.round(r.y), x: Math.round(r.x) });
569
- };
570
- const inContent = (el) => el.closest('[role="banner"]') === null;
571
- for (const el of Array.from(root.querySelectorAll('[data-mark="identity"]'))) {
572
- const r = el.getBoundingClientRect();
573
- if (r.height === 0 || !inContent(el)) continue;
574
- record(el, (el.textContent || "").trim() || "\u25A3");
575
- }
576
- const initials = /^\p{L}{1,2}$/u;
577
- const grounded = (el, depth = 2) => {
578
- const cs = getComputedStyle(el);
579
- if (cs.backgroundColor !== "rgba(0, 0, 0, 0)" || cs.backgroundImage !== "none") return true;
580
- if (depth === 0) return false;
581
- return Array.from(el.children).some((child) => grounded(child, depth - 1));
582
- };
583
- for (const el of Array.from(root.querySelectorAll("*"))) {
584
- const r = el.getBoundingClientRect();
585
- if (r.width < 12 || r.width > 56) continue;
586
- if (Math.abs(r.width - r.height) > 1) continue;
587
- const cs = getComputedStyle(el);
588
- if ((parseFloat(cs.borderTopLeftRadius) || 0) < 3) continue;
589
- const text = (el.textContent || "").trim();
590
- if (!initials.test(text)) continue;
591
- if (!inContent(el)) continue;
592
- if (!grounded(el)) continue;
593
- record(el, text);
594
- }
595
- const byRow = /* @__PURE__ */ new Map();
596
- for (const m of marks) {
597
- const band = Math.round(m.y / 20) * 20;
598
- const list = byRow.get(band);
599
- if (list) list.push(m);
600
- else byRow.set(band, [m]);
601
- }
602
- const mismatches = [];
603
- for (const [band, row] of byRow) {
604
- const widths = new Set(row.map((m) => m.w));
605
- if (widths.size <= 1) continue;
606
- mismatches.push({ y: band, marks: row.sort((a, b) => a.x - b.x).map((m) => `${m.text}:${m.w}px`) });
607
- }
608
- return { total: marks.length, mismatches };
609
- }
610
-
611
- // src/probe_collectors/naming.ts
612
- var HOLDS_A_VALUE = 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [role="textbox"], [role="searchbox"], [role="combobox"], [role="listbox"], [role="spinbutton"], [role="slider"]';
613
- var HEADING = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
614
- function words(el) {
615
- return el === null ? "" : (el.textContent || "").replace(/\s+/g, " ").trim();
616
- }
617
- function pointedAt(el, attribute) {
618
- return (el.getAttribute(attribute) || "").split(/\s+/).filter((id) => id !== "").map((id) => el.ownerDocument.getElementById(id)).filter((target) => target !== null);
619
- }
620
- function stated(el) {
621
- const pointed = pointedAt(el, "aria-labelledby").map(words).filter((name) => name !== "").join(" ");
622
- if (pointed !== "") return pointed;
623
- return (el.getAttribute("aria-label") || "").trim();
624
- }
625
- function labelling(el) {
626
- const id = el.getAttribute("id") || "";
627
- const explicit = id === "" ? void 0 : Array.from(el.ownerDocument.querySelectorAll("label[for]")).find((label2) => label2.getAttribute("for") === id);
628
- const label = explicit ?? el.closest("label");
629
- if (label === null || label === void 0) return "";
630
- const said2 = [];
631
- const walk = (node) => {
632
- if (node === el) return;
633
- if (node.nodeType === Node.TEXT_NODE) {
634
- said2.push(node.textContent || "");
635
- return;
636
- }
637
- node.childNodes.forEach(walk);
638
- };
639
- walk(label);
640
- return said2.join(" ").replace(/\s+/g, " ").trim();
641
- }
642
- function regionName(region) {
643
- const said2 = stated(region);
644
- return said2 !== "" ? said2 : words(region.querySelector(HEADING));
645
- }
646
- function nameOf(el) {
647
- const said2 = stated(el);
648
- if (said2 !== "") return said2;
649
- if (el.tagName === "DETAILS") {
650
- const summary = words(el.querySelector("summary"));
651
- if (summary !== "") return summary;
652
- }
653
- const labelled = labelling(el);
654
- if (labelled !== "") return labelled;
655
- if (!el.matches(HOLDS_A_VALUE)) {
656
- const own = words(el);
657
- if (own !== "") return own;
658
- }
659
- const tooltip = (el.getAttribute("title") || "").trim();
660
- if (tooltip !== "") return tooltip;
661
- for (const region of pointedAt(el, "aria-controls")) {
662
- const named = regionName(region);
663
- if (named !== "") return named;
664
- }
665
- return "";
666
- }
667
-
668
- // src/probe_collectors/on_screen.ts
669
- var CLIP_RECT = /^rect\(\s*(-?[\d.]+)px[,\s]+(-?[\d.]+)px[,\s]+(-?[\d.]+)px[,\s]+(-?[\d.]+)px\s*\)$/;
670
- var INSET = /^inset\(([^)]*?)(?:\s+round\s+[^)]*)?\)$/;
671
- var inset = (value, extent) => value.endsWith("%") ? parseFloat(value) / 100 * extent : parseFloat(value);
672
- var meets = (near, far, extent) => extent > 0 && inset(near, extent) + inset(far, extent) >= extent;
673
- function clippedAway(el) {
674
- const style = getComputedStyle(el);
675
- const box = el.getBoundingClientRect();
676
- const rect = CLIP_RECT.exec(style.clip);
677
- if (rect !== null && (Number(rect[2]) - Number(rect[4]) <= 0 || Number(rect[3]) - Number(rect[1]) <= 0)) return true;
678
- const path = INSET.exec(style.clipPath);
679
- if (path === null || path[1].trim() === "") return false;
680
- const [top, right = top, bottom = top, left = right] = path[1].trim().split(/\s+/);
681
- return meets(left, right, box.width) || meets(top, bottom, box.height);
682
- }
683
- function onScreen(el) {
684
- if (el.closest("[inert]") !== null) return false;
685
- if (typeof el.checkVisibility === "function" && !el.checkVisibility()) return false;
686
- const shown = getComputedStyle(el).visibility;
687
- if (shown === "hidden" || shown === "collapse") return false;
688
- for (let up = el; up !== null; up = up.parentElement) {
689
- if (clippedAway(up)) return false;
690
- }
691
- const box = el.getBoundingClientRect();
692
- return box.width > 0 && box.height > 0;
693
- }
694
-
695
- // src/probe_collectors/scope.ts
696
- var OVERLAY = '[aria-modal="true"], dialog[open], [role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"]';
697
- var AWAY = '[aria-hidden="true"], [inert]';
698
- function measuredSurface() {
699
- const away = Array.from(document.querySelectorAll(AWAY));
700
- const open = Array.from(document.querySelectorAll(OVERLAY)).filter((el) => {
701
- if (!onScreen(el)) return false;
702
- if (away.some((hidden) => hidden.contains(el))) return false;
703
- if (el.matches('[aria-modal="true"], dialog[open]')) return true;
704
- return away.some((hidden) => hidden.parentElement === document.body && !hidden.contains(el));
705
- });
706
- const inner = open.filter((el) => !open.some((other) => other !== el && el.contains(other)));
707
- return inner[inner.length - 1] ?? document.body;
708
- }
709
- function within(root, selector) {
710
- const inside2 = Array.from(root.querySelectorAll(selector));
711
- return root.matches(selector) ? [root, ...inside2] : inside2;
712
- }
713
-
714
- // src/probe_collectors/anatomy.ts
715
- var RECORD_FRAME = ".lotics-record-frame";
716
- var SECTION_PAGES = `${RECORD_FRAME} nav [role="link"], ${RECORD_FRAME}__link [role="link"]`;
717
- var HEADER = ".lotics-record-header";
718
- var BLOCK = ".lotics-record-frame__block";
719
- var PRIMARY = '.lotics-button[data-variant="primary"]';
720
- var FIELD = `${RECORD_FRAME}__fields .lotics-form-field, .lotics-fact-groups__field`;
721
- function surfaces(root) {
722
- return within(root, RECORD_FRAME).filter(onScreen).map((frame) => {
723
- const head = frame.closest(".lotics-drawer__popup") ?? frame;
724
- const title = within(head, `${HEADER}__title`).find(onScreen);
725
- return { frame, root: head, name: words2(title) || nameOf(head) || "(unnamed record)" };
726
- });
727
- }
728
- function words2(el) {
729
- if (el === null || el === void 0) return "";
730
- return (el.textContent || "").replace(/\s+/g, " ").trim();
731
- }
732
- var said = (text) => text.toLowerCase().replace(/\s+/g, " ").replace(/:$/, "").trim();
733
- function headingPrimaries(root = document.body) {
734
- const drawn = surfaces(root);
735
- const loud = drawn.flatMap(
736
- (surface) => within(surface.frame, PRIMARY).filter((act) => onScreen(act) && act.closest(BLOCK) !== null && act.closest(".lotics-section-heading") !== null).map((act) => ({ record: surface.name, act: nameOf(act) || words2(act) }))
737
- );
738
- return { total: drawn.length, loud };
739
- }
740
- function factRepeats(root = document.body) {
741
- let total = 0;
742
- const repeated = surfaces(root).flatMap((surface) => {
743
- const header = within(surface.root, HEADER).find(onScreen);
744
- if (header === void 0) return [];
745
- const head = new Set(
746
- within(header, "*").filter((el) => el.childElementCount === 0 && el.closest('button, [role="button"], [role="menu"]') === null && onScreen(el)).map((el) => said(words2(el))).filter((text) => text.length > 1)
747
- );
748
- const fields = within(surface.frame, FIELD).filter((field) => onScreen(field) && !header.contains(field));
749
- total += fields.length;
750
- return fields.flatMap((field) => {
751
- const label = within(field, ".lotics-form-field__label")[0];
752
- const value = within(field, "*").filter((el) => el.childElementCount === 0 && (label === void 0 || !label.contains(el)) && onScreen(el)).map((el) => words2(el)).join(" ").trim();
753
- return value !== "" && head.has(said(value)) ? [{ record: surface.name, field: words2(label), value }] : [];
754
- });
755
- });
756
- return { total, repeated };
757
- }
758
- function emptyBlocks(root = document.body) {
759
- let total = 0;
760
- const empty = surfaces(root).flatMap((surface) => {
761
- const blocks = within(surface.frame, BLOCK).filter(onScreen);
762
- total += blocks.length;
763
- return blocks.flatMap((block) => {
764
- const body = Array.from(block.children).filter((child) => !child.matches(".lotics-section-heading") && onScreen(child));
765
- const drawn = body.some((child) => words2(child) !== "" || child.querySelector("img, svg, input, textarea, button, [contenteditable]") !== null);
766
- return drawn ? [] : [{ record: surface.name, block: block.getAttribute("aria-label") ?? "" }];
767
- });
768
- });
769
- return { total, empty };
770
- }
771
-
772
- // src/probe_collectors/clutter.ts
773
- var overlaySurface = '[role="dialog"], [role="alertdialog"], dialog[open]';
774
- var DRAWS = 'svg, img, picture, canvas, video, [role="img"], input, textarea, select, button, [role="button"], [role="textbox"], [role="combobox"], [role="checkbox"], [role="switch"], [role="meter"], [role="progressbar"]';
775
- function clutterCensus(root = document.body) {
776
- const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
777
- const chrome = 'nav, [role="banner"]';
778
- const recordSurface = '.lotics-record-frame, [role="dialog"]:not([data-side])';
779
- const headings = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
780
- const noteFloor = 3;
781
- const formFloor = 3;
782
- const hueStep = 30;
783
- const chromaFloor = 0.15;
784
- const sameColour = 24;
785
- const sameEdge = 8;
786
- const words5 = (el) => el === null ? "" : (el.textContent || "").trim().slice(0, 40);
787
- const surfaces2 = within(root, recordSurface).filter(onScreen);
788
- const called2 = (el) => el.matches(headings) ? words5(el) || "the section" : el.matches(recordSurface) ? words5(el.querySelector(".lotics-record-header__title, h1, h2")) || "the record" : "the screen";
789
- const rules = [];
790
- const primary = '[data-variant="primary"]';
791
- const acts = within(root, primary).filter(
792
- (el) => onScreen(el) && el.closest(chrome) === null && (el.parentElement === null || el.parentElement.closest(primary) === null)
793
- );
794
- const page = within(root, '[role="main"], main')[0] ?? root;
795
- const titled = /* @__PURE__ */ new Map();
796
- const held = /* @__PURE__ */ new Map();
797
- for (const act of acts) {
798
- const surface = act.closest(recordSurface) ?? page;
799
- let titles = titled.get(surface);
800
- if (titles === void 0) {
801
- titles = within(surface, headings).filter(onScreen);
802
- titled.set(surface, titles);
803
- }
804
- let owner = surface;
805
- for (const title of titles) {
806
- if (title.contains(act)) continue;
807
- if ((title.compareDocumentPosition(act) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0) owner = title;
808
- }
809
- const run2 = held.get(owner);
810
- if (run2 === void 0) held.set(owner, [act]);
811
- else run2.push(act);
812
- }
813
- let surplusActs = 0;
814
- const loud = [];
815
- for (const [owner, run2] of held) {
816
- if (run2.length <= 1) continue;
817
- surplusActs += run2.length - 1;
818
- if (loud.length < 3) loud.push(`${called2(owner)}: ${run2.length} primary acts \u2014 ${run2.map((act) => `"${words5(act)}"`).join(", ")}`);
819
- }
820
- rules.push({
821
- law: "hierarchy.md",
822
- rule: "One primary act per surface",
823
- total: acts.length,
824
- count: surplusActs,
825
- offenders: loud
826
- });
827
- const money = /^[-+−]?\s?(?:(?:[$€]|USD|EUR|VND)\s?[\d.,]+\s?(?:[KMB]|tr|tỷ)?|[\d.,]+\s?(?:tr|tỷ)?\s?(?:₫|đ|VND|USD|EUR|[$€]))$/i;
828
- const dated = /^\d{1,4}[/.-]\d{1,2}(?:[/.-]\d{1,4})?$/;
829
- const coded = /^(?=[^0-9]*[0-9])(?=[^A-Z]*[A-Z])[A-Z0-9][A-Z0-9/_.-]{4,}$/;
830
- const trim = /^[\s·•|,:;–—-]+|[\s·•|,:;–—-]+$/g;
831
- const beside = (el, value) => {
832
- const said2 = [];
833
- const walk = (node) => {
834
- if (node === value) return;
835
- if (node.nodeType === Node.TEXT_NODE) said2.push(node.textContent || "");
836
- else node.childNodes.forEach(walk);
837
- };
838
- walk(el);
839
- return said2.join(" ").replace(/\s+/g, " ").replace(trim, "");
840
- };
841
- const named = (value, surface) => {
842
- for (let up = value; up !== null && up !== surface; up = up.parentElement) {
843
- const said2 = stated(up);
844
- if (said2 !== "") return said2.replace(trim, "");
845
- const words6 = beside(up, value);
846
- if (words6 !== "") return words6.slice(0, 40);
847
- }
848
- return "";
849
- };
850
- const places = /* @__PURE__ */ new Map();
851
- const shown = /* @__PURE__ */ new Map();
852
- for (let index = 0; index < surfaces2.length; index++) {
853
- const walk = (node) => {
854
- if (node.nodeType === Node.TEXT_NODE) {
855
- const text = (node.textContent || "").trim();
856
- const parent = node.parentElement;
857
- if (text === "" || parent === null || skipTag.test(parent.tagName)) return;
858
- if (!onScreen(parent)) return;
859
- if (!money.test(text) && !dated.test(text) && !coded.test(text)) return;
860
- const label = named(parent, surfaces2[index]);
861
- const key = `${index}\0${label.toLowerCase()}\0${text}`;
862
- shown.set(key, { label, text });
863
- const edges = places.get(key);
864
- const left = Math.round(parent.getBoundingClientRect().left);
865
- if (edges === void 0) places.set(key, [left]);
866
- else edges.push(left);
867
- return;
868
- }
869
- node.childNodes.forEach(walk);
870
- };
871
- walk(surfaces2[index]);
872
- }
873
- let twice = 0;
874
- const repeated = [];
875
- for (const [key, edges] of places) {
876
- const columns = [];
877
- for (const left of edges) if (!columns.some((edge) => Math.abs(edge - left) <= sameEdge)) columns.push(left);
878
- if (columns.length < 2) continue;
879
- twice += 1;
880
- const fact = shown.get(key);
881
- if (fact !== void 0 && repeated.length < 3) {
882
- repeated.push(
883
- `${fact.label === "" ? "" : `"${fact.label}" \u2014 `}"${fact.text}" drawn in ${columns.length} places`
884
- );
885
- }
886
- }
887
- rules.push({
888
- law: "hierarchy.md",
889
- rule: "One fact, one place",
890
- total: places.size,
891
- count: twice,
892
- offenders: repeated
893
- });
894
- let boxes = 0;
895
- let overReserved = 0;
896
- const reserved = [];
897
- for (const box of within(root, 'textarea, [role="textbox"], [contenteditable="true"]')) {
898
- if (!onScreen(box) || box.closest(chrome) !== null) continue;
899
- const style = getComputedStyle(box);
900
- const line = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.5;
901
- if (!Number.isFinite(line) || line <= 0) continue;
902
- const pad = (parseFloat(style.paddingTop) || 0) + (parseFloat(style.paddingBottom) || 0);
903
- boxes += 1;
904
- const lines = Math.round((box.getBoundingClientRect().height - pad) / line);
905
- const written = Math.round((box.scrollHeight - pad) / line);
906
- if (lines <= noteFloor || written > 1) continue;
907
- overReserved += 1;
908
- if (reserved.length < 3) {
909
- const named2 = nameOf(box).slice(0, 40);
910
- reserved.push(`"${named2 === "" ? "(unnamed)" : named2}": ${lines} lines reserved for 1`);
911
- }
912
- }
913
- for (const surface of within(root, overlaySurface).filter(onScreen)) {
914
- const box = surface.getBoundingClientRect();
915
- if (box.height <= 0) continue;
916
- boxes += 1;
917
- const bands = [];
918
- const ink = (el) => {
919
- if (!onScreen(el)) return;
920
- const rect = el.getBoundingClientRect();
921
- const top = Math.max(rect.top, box.top);
922
- const bottom = Math.min(rect.bottom, box.bottom);
923
- if (bottom > top) bands.push([top, bottom]);
924
- };
925
- const walkInk = (node) => {
926
- if (node.nodeType === Node.TEXT_NODE) {
927
- const parent = node.parentElement;
928
- if (parent !== null && (node.textContent || "").trim() !== "" && !skipTag.test(parent.tagName)) ink(parent);
929
- return;
930
- }
931
- node.childNodes.forEach(walkInk);
932
- };
933
- walkInk(surface);
934
- for (const drawn of Array.from(surface.querySelectorAll(DRAWS))) ink(drawn);
935
- bands.sort((a, b) => a[0] - b[0]);
936
- let filled = 0;
937
- let empty = 0;
938
- let edge = null;
939
- for (const [top, bottom] of bands) {
940
- if (edge !== null && top <= edge) {
941
- if (bottom > edge) filled += bottom - edge;
942
- } else {
943
- if (edge !== null) empty = Math.max(empty, top - edge);
944
- filled += bottom - top;
945
- }
946
- edge = edge === null ? bottom : Math.max(edge, bottom);
947
- }
948
- if (empty <= filled) continue;
949
- overReserved += 1;
950
- if (reserved.length < 3) {
951
- reserved.push(`${called2(surface)}: ${Math.round(empty)}px of nothing, ${Math.round(filled)}px drawn`);
952
- }
953
- }
954
- rules.push({
955
- law: "hierarchy.md",
956
- rule: "Whitespace is structure",
957
- total: boxes,
958
- count: overReserved,
959
- offenders: reserved
960
- });
961
- const fields = within(root, ".lotics-form-field").filter(onScreen);
962
- const forms = /* @__PURE__ */ new Map();
963
- for (const field of fields) {
964
- const owner = field.closest('form, [role="form"]') ?? field.parentElement;
965
- if (owner === null) continue;
966
- const run2 = forms.get(owner);
967
- if (run2 === void 0) forms.set(owner, [field]);
968
- else run2.push(field);
969
- }
970
- let inForms = 0;
971
- let overMarked = 0;
972
- const marks = [];
973
- for (const run2 of forms.values()) {
974
- if (run2.length < formFloor) continue;
975
- inForms += run2.length;
976
- const carried = run2.filter((field) => field.querySelector(".lotics-form-field__mark") !== null);
977
- if (carried.length * 2 <= run2.length) continue;
978
- overMarked += carried.length;
979
- if (marks.length < 3) {
980
- marks.push(
981
- `${carried.length} of ${run2.length} fields marked \u2014 ` + carried.slice(0, 3).map((field) => `"${words5(field.querySelector(".lotics-form-field__label")) || words5(field)}"`).join(", ")
982
- );
983
- }
984
- }
985
- rules.push({
986
- law: "hierarchy.md",
987
- rule: "Markers on the minority side",
988
- total: inForms,
989
- count: overMarked,
990
- offenders: marks
991
- });
992
- const theme = getComputedStyle(document.documentElement);
993
- const rgbOf = (value) => {
994
- const channels = value.match(/^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/i);
995
- if (channels !== null) return [Number(channels[1]), Number(channels[2]), Number(channels[3])];
996
- const hex = value.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
997
- if (hex === null) return null;
998
- const digits = hex[1];
999
- const pair = (at) => parseInt(digits.length === 3 ? digits[at].repeat(2) : digits.slice(at * 2, at * 2 + 2), 16);
1000
- return [pair(0), pair(1), pair(2)];
1001
- };
1002
- const valence = [];
1003
- for (const token of ["--lotics-ink-danger", "--lotics-ink-warning", "--lotics-ink-success", "--lotics-ink-inactive"]) {
1004
- const ink = rgbOf(theme.getPropertyValue(token));
1005
- if (ink !== null) valence.push(ink);
1006
- }
1007
- const hues = /* @__PURE__ */ new Map();
1008
- let chromatic = 0;
1009
- if (valence.length > 0) {
1010
- const stateInk = '[role="status"], [role="alert"], [aria-invalid="true"], [aria-disabled="true"]';
1011
- const identityMark = '[data-mark="identity"]';
1012
- const walk = (node) => {
1013
- if (node.nodeType === Node.TEXT_NODE) {
1014
- const text = (node.textContent || "").trim();
1015
- const parent = node.parentElement;
1016
- if (text === "" || parent === null || skipTag.test(parent.tagName)) return;
1017
- if (!onScreen(parent) || parent.closest(chrome) !== null || parent.closest(stateInk) !== null || parent.closest(identityMark) !== null) return;
1018
- const ink = rgbOf(getComputedStyle(parent).color);
1019
- if (ink === null) return;
1020
- if (valence.some((tone) => Math.abs(tone[0] - ink[0]) + Math.abs(tone[1] - ink[1]) + Math.abs(tone[2] - ink[2]) <= sameColour)) return;
1021
- const red = ink[0] / 255;
1022
- const green = ink[1] / 255;
1023
- const blue = ink[2] / 255;
1024
- const top = Math.max(red, green, blue);
1025
- const bottom = Math.min(red, green, blue);
1026
- const light = (top + bottom) / 2;
1027
- const chroma = top - bottom;
1028
- const saturation = chroma === 0 ? 0 : chroma / (1 - Math.abs(2 * light - 1));
1029
- if (saturation < chromaFloor || light < 0.12 || light > 0.92) return;
1030
- const angle = top === red ? (green - blue) / chroma % 6 * 60 : top === green ? ((blue - red) / chroma + 2) * 60 : ((red - green) / chroma + 4) * 60;
1031
- const bucket = Math.round((angle + 360) % 360 / hueStep) * hueStep % 360;
1032
- chromatic += 1;
1033
- const tally = hues.get(bucket);
1034
- if (tally === void 0) hues.set(bucket, { count: 1, sample: text.slice(0, 40) });
1035
- else tally.count += 1;
1036
- return;
1037
- }
1038
- node.childNodes.forEach(walk);
1039
- };
1040
- walk(root);
1041
- }
1042
- const ranked = [...hues.entries()].sort((a, b) => b[1].count - a[1].count);
1043
- const second = ranked.slice(1);
1044
- rules.push({
1045
- law: "hierarchy.md",
1046
- rule: "Colour carries meaning only",
1047
- total: chromatic,
1048
- count: second.reduce((sum, [, tally]) => sum + tally.count, 0),
1049
- offenders: second.slice(0, 3).map(([bucket, tally]) => `a second hue at ${bucket}\xB0 on ${tally.count} runs \u2014 "${tally.sample}"`)
1050
- });
1051
- return rules;
1052
- }
1053
-
1054
- // src/probe_collectors/ink_and_air.ts
1055
- var HEADING2 = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
1056
- var GRAPHIC = 'svg, img, picture, canvas, [role="img"], [role="figure"]';
1057
- var CONTROL = 'a[href], button, summary, [role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="checkbox"], [role="switch"]';
1058
- var NAVIGATION = 'nav, [role="navigation"], [role="banner"]';
1059
- var EDITOR = 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [contenteditable="plaintext-only"], [role="textbox"], [role="searchbox"], [role="combobox"], [role="spinbutton"]';
1060
- var FIELD2 = EDITOR + ', [role="checkbox"], [role="switch"], [role="radio"], [role="slider"]';
1061
- var GROUP = '[role="group"], fieldset';
1062
- var TEXT_ENTRY = /* @__PURE__ */ new Set([
1063
- "text",
1064
- "search",
1065
- "url",
1066
- "tel",
1067
- "email",
1068
- "password",
1069
- "number",
1070
- "date",
1071
- "datetime-local",
1072
- "month",
1073
- "week",
1074
- "time"
1075
- ]);
1076
- var EDGES = ["top", "right", "bottom", "left"];
1077
- var HAIR = 0.5;
1078
- var words3 = (el) => el === null ? "" : (el.textContent || "").replace(/\s+/g, " ").trim();
1079
- var called = (el) => {
1080
- const named = nameOf(el).slice(0, 40);
1081
- return named === "" ? "(unnamed)" : named;
1082
- };
1083
- var px = (value) => {
1084
- const measure = parseFloat(value);
1085
- return Number.isFinite(measure) ? measure : 0;
1086
- };
1087
- var opaque = (colour) => {
1088
- if (colour === "") return true;
1089
- if (/^transparent$/i.test(colour)) return false;
1090
- const alpha = /^rgba\(\s*[\d.]+[,\s]+[\d.]+[,\s]+[\d.]+[,\s/]+([\d.]+)\s*\)/i.exec(colour);
1091
- return alpha === null || Number(alpha[1]) > 0;
1092
- };
1093
- var ringOf = (shadow) => {
1094
- if (shadow === "" || shadow === "none") return 0;
1095
- let widest = 0;
1096
- for (const segment of shadow.split(/,(?![^(]*\))/)) {
1097
- if (!opaque((/rgba?\([^)]*\)/i.exec(segment) ?? [""])[0])) continue;
1098
- const lengths = (segment.match(/-?[\d.]+px/g) ?? []).map((length) => parseFloat(length));
1099
- if (lengths.length < 3) continue;
1100
- const [x, y, blur, spread = 0] = lengths;
1101
- if (Math.abs(x) > HAIR || Math.abs(y) > HAIR) continue;
1102
- widest = Math.max(widest, spread > 0 ? spread : blur);
1103
- }
1104
- return widest;
1105
- };
1106
- var sidesOf = (style) => EDGES.filter((edge) => {
1107
- const line = style.getPropertyValue(`border-${edge}-style`);
1108
- if (line === "" || line === "none" || line === "hidden") return false;
1109
- if (px(style.getPropertyValue(`border-${edge}-width`)) <= 0) return false;
1110
- return opaque(style.getPropertyValue(`border-${edge}-color`));
1111
- }).length;
1112
- var closed = (style) => sidesOf(style) === EDGES.length || ringOf(style.boxShadow) > 0;
1113
- var editorsIn = (root) => within(root, EDITOR).filter((el) => {
1114
- if (!onScreen(el)) return false;
1115
- if (el.tagName !== "INPUT") return true;
1116
- return TEXT_ENTRY.has((el.getAttribute("type") || "text").toLowerCase());
1117
- });
1118
- function inkAndAirCensus(root = document.body) {
1119
- const rules = [];
1120
- const rank = (el) => {
1121
- const stated2 = parseInt(el.getAttribute("aria-level") || "", 10);
1122
- if (Number.isFinite(stated2)) return stated2;
1123
- const tag = /^H([1-6])$/.exec(el.tagName);
1124
- return tag === null ? 2 : Number(tag[1]);
1125
- };
1126
- const headings = within(root, HEADING2).filter(
1127
- (el) => onScreen(el) && el.closest(NAVIGATION) === null && words3(el) !== ""
1128
- );
1129
- const shallowest = headings.reduce((top, el) => Math.min(top, rank(el)), Infinity);
1130
- const pageTitle = headings.find((el) => rank(el) === shallowest);
1131
- const headerRow = (heading) => {
1132
- const own = words3(heading);
1133
- let row = heading;
1134
- for (let up = heading.parentElement; up !== null && up !== root; up = up.parentElement) {
1135
- if (words3(up) !== own) break;
1136
- row = up;
1137
- }
1138
- return row;
1139
- };
1140
- const graphicIn = (row) => Array.from(row.querySelectorAll(GRAPHIC)).find((mark) => onScreen(mark) && mark.closest(CONTROL) === null) ?? null;
1141
- let sections = 0;
1142
- let badged = 0;
1143
- const badges = [];
1144
- for (const heading of headings) {
1145
- if (heading === pageTitle) continue;
1146
- sections += 1;
1147
- const mark = graphicIn(headerRow(heading));
1148
- if (mark === null) continue;
1149
- badged += 1;
1150
- if (badges.length < 3) {
1151
- badges.push(`"${words3(heading).slice(0, 40)}": the heading row draws <${mark.tagName.toLowerCase()}>`);
1152
- }
1153
- }
1154
- rules.push({
1155
- law: "hierarchy.md",
1156
- rule: "Ink and air state what a thing is \u2192 A section's heading",
1157
- total: sections,
1158
- count: badged,
1159
- offenders: badges
1160
- });
1161
- const editors = editorsIn(root);
1162
- const fieldsIn = (box) => within(box, FIELD2).filter(onScreen);
1163
- const labelsIn = (box) => within(box, "label").filter((el) => onScreen(el) && words3(el) !== "");
1164
- const oneControl = (el) => el.matches(GROUP) || (el.getAttribute("aria-label") || "").trim() !== "" || (el.getAttribute("aria-labelledby") || "").trim() !== "";
1165
- const around = (outer, inner) => outer.left <= inner.left + HAIR && outer.top <= inner.top + HAIR && outer.right + HAIR >= inner.right && outer.bottom + HAIR >= inner.bottom;
1166
- let open = 0;
1167
- let units = 0;
1168
- const bare = [];
1169
- const claimed = /* @__PURE__ */ new Set();
1170
- for (const editor of editors) {
1171
- if (claimed.has(editor)) continue;
1172
- let unit = editor;
1173
- let parts = fieldsIn(editor);
1174
- let framed = closed(getComputedStyle(unit));
1175
- for (let up = unit.parentElement; !framed && up !== null && up !== root; up = up.parentElement) {
1176
- if (labelsIn(up).length > 0) break;
1177
- const held = fieldsIn(up);
1178
- if (held.some((field) => !parts.includes(field))) {
1179
- if (!oneControl(up) || held.some((field) => claimed.has(field))) break;
1180
- unit = up;
1181
- parts = held;
1182
- framed = closed(getComputedStyle(up));
1183
- continue;
1184
- }
1185
- if (!around(up.getBoundingClientRect(), unit.getBoundingClientRect())) continue;
1186
- framed = closed(getComputedStyle(up));
1187
- }
1188
- for (const part of parts) claimed.add(part);
1189
- units += 1;
1190
- if (framed) continue;
1191
- open += 1;
1192
- if (bare.length < 3) {
1193
- bare.push(`"${called(unit)}": ${sidesOf(getComputedStyle(unit))} of 4 sides painted at rest`);
1194
- }
1195
- }
1196
- rules.push({
1197
- law: "hierarchy.md",
1198
- rule: "Ink and air state what a thing is \u2192 An editor at rest",
1199
- total: units,
1200
- count: open,
1201
- offenders: bare
1202
- });
1203
- const drawn = (el) => Array.from(el.children).filter(onScreen);
1204
- const captionOf = (group) => {
1205
- const first = drawn(group)[0];
1206
- if (first === void 0) return null;
1207
- if (first.matches(HEADING2)) return first;
1208
- const inside2 = first.querySelector(HEADING2);
1209
- return inside2 !== null && onScreen(inside2) && words3(inside2) !== "" ? inside2 : null;
1210
- };
1211
- const rowGapOf = (group) => {
1212
- const rows = drawn(group).slice(1);
1213
- let gap = null;
1214
- for (let at = 1; at < rows.length; at++) {
1215
- const above = rows[at - 1].getBoundingClientRect();
1216
- const below = rows[at].getBoundingClientRect();
1217
- const air = below.top - above.bottom;
1218
- if (air < 0) continue;
1219
- gap = gap === null ? air : Math.min(gap, air);
1220
- }
1221
- return gap;
1222
- };
1223
- let pairs = 0;
1224
- let level = 0;
1225
- const merged = [];
1226
- for (const parent of [root, ...Array.from(root.querySelectorAll("*"))]) {
1227
- const groups = drawn(parent);
1228
- for (let at = 1; at < groups.length; at++) {
1229
- const first = groups[at - 1];
1230
- const second = groups[at];
1231
- const opens = captionOf(first);
1232
- const caption = captionOf(second);
1233
- if (opens === null || caption === null) continue;
1234
- const gap = rowGapOf(first) ?? rowGapOf(second);
1235
- if (gap === null) continue;
1236
- pairs += 1;
1237
- const above = caption.getBoundingClientRect().top - first.getBoundingClientRect().bottom;
1238
- if (above > gap) continue;
1239
- level += 1;
1240
- if (merged.length < 3) {
1241
- merged.push(
1242
- `"${words3(caption).slice(0, 40)}": ${Math.round(above)}px above the caption, ${Math.round(gap)}px between the rows it follows`
1243
- );
1244
- }
1245
- }
1246
- }
1247
- rules.push({
1248
- law: "hierarchy.md",
1249
- rule: "Ink and air state what a thing is \u2192 Two named groups",
1250
- total: pairs,
1251
- count: level,
1252
- offenders: merged
1253
- });
1254
- return rules;
1255
- }
1256
- function focusRingCensus(root = document.body) {
1257
- const clipperOf = (el) => {
1258
- for (let up = el.parentElement; up !== null; up = up.parentElement) {
1259
- const style = getComputedStyle(up);
1260
- const across = style.overflowX || style.overflow;
1261
- const down = style.overflowY || style.overflow;
1262
- if (across !== "" && across !== "visible" || down !== "" && down !== "visible") return up;
1263
- }
1264
- return null;
1265
- };
1266
- const shift = (before, range, short, over) => {
1267
- const wanted = over > 0 ? over : short > 0 ? -short : 0;
1268
- return Math.max(0, Math.min(range, before + wanted));
1269
- };
1270
- let rings2 = 0;
1271
- let cut = 0;
1272
- const clipped = [];
1273
- for (const editor of editorsIn(root)) {
1274
- const style = getComputedStyle(editor);
1275
- const line = style.outlineStyle;
1276
- const outline = line === "" || line === "none" ? 0 : px(style.outlineWidth) + px(style.outlineOffset);
1277
- const extent = Math.max(outline, ringOf(style.boxShadow));
1278
- if (extent <= 0) continue;
1279
- rings2 += 1;
1280
- const clipper = clipperOf(editor);
1281
- if (clipper === null) continue;
1282
- const scrolled = { top: clipper.scrollTop, left: clipper.scrollLeft };
1283
- const first = editor.getBoundingClientRect();
1284
- const port = clipper.getBoundingClientRect();
1285
- clipper.scrollTop = shift(
1286
- scrolled.top,
1287
- clipper.scrollHeight - clipper.clientHeight,
1288
- port.top - (first.top - extent),
1289
- first.bottom + extent - port.bottom
1290
- );
1291
- clipper.scrollLeft = shift(
1292
- scrolled.left,
1293
- clipper.scrollWidth - clipper.clientWidth,
1294
- port.left - (first.left - extent),
1295
- first.right + extent - port.right
1296
- );
1297
- const box = editor.getBoundingClientRect();
1298
- const clip = clipper.getBoundingClientRect();
1299
- const outside = [
1300
- { edge: "top", by: clip.top - (box.top - extent) },
1301
- { edge: "right", by: box.right + extent - clip.right },
1302
- { edge: "bottom", by: box.bottom + extent - clip.bottom },
1303
- { edge: "left", by: clip.left - (box.left - extent) }
1304
- ].filter((side) => side.by > HAIR);
1305
- clipper.scrollTop = scrolled.top;
1306
- clipper.scrollLeft = scrolled.left;
1307
- if (outside.length === 0) continue;
1308
- cut += 1;
1309
- if (clipped.length < 3) {
1310
- const worst = Math.round(Math.max(...outside.map((side) => side.by)));
1311
- clipped.push(
1312
- `"${called(editor)}": a ${extent}px focus ring cut by up to ${worst}px, on the ` + outside.map((side) => side.edge).join(", ")
1313
- );
1314
- }
1315
- }
1316
- return [
1317
- {
1318
- law: "hierarchy.md",
1319
- rule: "Ink and air state what a thing is \u2192 A focus ring",
1320
- total: rings2,
1321
- count: cut,
1322
- offenders: clipped
1323
- }
1324
- ];
1325
- }
1326
-
1327
- // src/probe_collectors/layout.ts
1328
- var RESERVED_MIN_WIDTH = 40;
1329
- var RESERVED_BLANK_SHARE = 0.5;
1330
- var CONTROL2 = 'button, [role="button"], a[href], [role="link"], input, select, textarea, [role="checkbox"], [role="switch"]';
1331
- function words4(el) {
1332
- if (el === null || el === void 0) return "";
1333
- const runs = [];
1334
- const walk = (node) => {
1335
- if (node.nodeType === Node.TEXT_NODE) runs.push(node.textContent || "");
1336
- else node.childNodes.forEach(walk);
1337
- };
1338
- walk(el);
1339
- return runs.join(" ").replace(/\s+/g, " ").trim().slice(0, 40);
1340
- }
1341
- var inside = (el, root, selector) => el.closest(selector) === root;
1342
- var stored = (row) => !row.hasAttribute("data-ghost");
1343
- function kitRegisters(root) {
1344
- const table = ".lotics-table";
1345
- const row = ".lotics-table__row";
1346
- return within(root, table).filter((register) => onScreen(register) && !register.hasAttribute("data-stacked")).map((register) => {
1347
- const rows = Array.from(register.querySelectorAll(row)).filter((one) => inside(one, register, table) && onScreen(one) && stored(one));
1348
- const headings = Array.from(register.querySelectorAll(".lotics-table__column")).filter((heading) => inside(heading, register, table));
1349
- const cellsOf = (one) => {
1350
- const box = Array.from(one.querySelectorAll(".lotics-table__cells")).find((cells) => inside(cells, one, row));
1351
- return box === void 0 ? [] : Array.from(box.children).filter((cell) => cell.classList.contains("lotics-table__cell"));
1352
- };
1353
- const gutterOf = (one) => Array.from(one.querySelectorAll(".lotics-table__trailing")).find((gutter) => inside(gutter, one, row));
1354
- const grid = rows.map(cellsOf);
1355
- const columns = Math.max(headings.length, ...grid.map((cells) => cells.length), 0);
1356
- const slots = Array.from({ length: columns }, (_, index) => ({
1357
- name: words4(headings[index]) || `column ${index + 1}`,
1358
- kind: "column",
1359
- heading: headings[index],
1360
- cells: grid.map((cells) => cells[index])
1361
- }));
1362
- const gutters = rows.map(gutterOf);
1363
- if (gutters.some((gutter) => gutter !== void 0)) slots.push({ name: "the row's acts", kind: "gutter", cells: gutters });
1364
- return { root: register, rows, slots };
1365
- });
1366
- }
1367
- function ariaRegisters(root) {
1368
- const table = '[role="table"], [role="grid"]';
1369
- return within(root, table).filter(onScreen).map((register) => {
1370
- const headings = Array.from(register.querySelectorAll('[role="columnheader"]')).filter((heading) => inside(heading, register, table)).map(words4);
1371
- const grid = Array.from(register.querySelectorAll('[role="row"]')).filter((one) => inside(one, register, table) && onScreen(one) && stored(one)).map((one) => ({
1372
- row: one,
1373
- cells: Array.from(one.querySelectorAll('[role="cell"], [role="gridcell"]')).filter(
1374
- (cell) => inside(cell, one, '[role="row"]')
1375
- )
1376
- })).filter((one) => one.cells.length > 0);
1377
- const columns = Math.max(headings.length, ...grid.map((one) => one.cells.length), 0);
1378
- return {
1379
- root: register,
1380
- rows: grid.map((one) => one.row),
1381
- slots: Array.from({ length: columns }, (_, index) => ({
1382
- name: headings[index] || `column ${index + 1}`,
1383
- kind: "column",
1384
- cells: grid.map((one) => one.cells[index])
1385
- }))
1386
- };
1387
- });
1388
- }
1389
- function inkSpan(cell) {
1390
- const box = cell.getBoundingClientRect();
1391
- let left = Number.POSITIVE_INFINITY;
1392
- let right = Number.NEGATIVE_INFINITY;
1393
- const take = (rect) => {
1394
- if (rect.width <= 0 || rect.height <= 0) return;
1395
- left = Math.min(left, Math.max(rect.left, box.left));
1396
- right = Math.max(right, Math.min(rect.right, box.right));
1397
- };
1398
- const walk = (node) => {
1399
- if (node.nodeType === Node.TEXT_NODE) {
1400
- const parent = node.parentElement;
1401
- if (parent === null || (node.textContent || "").trim() === "" || !onScreen(parent)) return;
1402
- const range = document.createRange();
1403
- range.selectNodeContents(node);
1404
- take(range.getBoundingClientRect());
1405
- return;
1406
- }
1407
- node.childNodes.forEach(walk);
1408
- };
1409
- walk(cell);
1410
- for (const drawn of within(cell, DRAWS)) if (onScreen(drawn)) take(drawn.getBoundingClientRect());
1411
- return right > left ? { left, right } : null;
1412
- }
1413
- var laidControls = (cell) => within(cell, CONTROL2).filter((control) => {
1414
- const rect = control.getBoundingClientRect();
1415
- const laid = typeof control.checkVisibility !== "function" || control.checkVisibility();
1416
- return laid && control.closest("[inert]") === null && rect.width > 0 && rect.height > 0;
1417
- });
1418
- function occupied(cell) {
1419
- if (cell === void 0) return 0;
1420
- const box = cell.getBoundingClientRect();
1421
- const ink = inkSpan(cell);
1422
- const spans = [
1423
- ...ink === null ? [] : [ink],
1424
- ...laidControls(cell).map((control) => {
1425
- const rect = control.getBoundingClientRect();
1426
- return { left: Math.max(rect.left, box.left), right: Math.min(rect.right, box.right) };
1427
- })
1428
- ].filter((span) => span.right > span.left);
1429
- if (spans.length === 0) return 0;
1430
- return Math.max(...spans.map((span) => span.right)) - Math.min(...spans.map((span) => span.left));
1431
- }
1432
- var leavesRoom = (slot, cell, width) => slot.kind === "column" ? occupied(cell) === 0 : width - occupied(cell) >= RESERVED_MIN_WIDTH;
1433
- var widthOf = (slot) => Math.round(Math.max(0, ...slot.cells.map((cell) => cell === void 0 ? 0 : cell.getBoundingClientRect().width)));
1434
- function reservedBlanks(root = document.body) {
1435
- let total = 0;
1436
- const found = [];
1437
- for (const register of [...kitRegisters(root), ...ariaRegisters(root)]) {
1438
- if (register.rows.length === 0) continue;
1439
- for (const slot of register.slots) {
1440
- const width = widthOf(slot);
1441
- if (width < RESERVED_MIN_WIDTH) continue;
1442
- total += 1;
1443
- const empty = slot.cells.filter((cell) => leavesRoom(slot, cell, width)).length;
1444
- if (empty <= register.rows.length * RESERVED_BLANK_SHARE) continue;
1445
- found.push({ slot: slot.name, kind: slot.kind, width, blank: empty, rows: register.rows.length });
1446
- }
1447
- }
1448
- return { total, blank: found };
1449
- }
1450
- function shedOf(register) {
1451
- const said2 = register.getAttribute("data-shed");
1452
- if (said2 === null) return [];
1453
- const parsed = JSON.parse(said2);
1454
- if (!Array.isArray(parsed)) throw new Error(`data-shed is not a list: ${said2.slice(0, 80)}`);
1455
- return parsed.map((entry) => {
1456
- const label = entry !== null && typeof entry === "object" ? Reflect.get(entry, "label") : void 0;
1457
- const width = entry !== null && typeof entry === "object" ? Reflect.get(entry, "width") : void 0;
1458
- if (typeof label !== "string" || typeof width !== "number") throw new Error(`data-shed names a column without a label and a width: ${said2.slice(0, 80)}`);
1459
- return { label, width };
1460
- });
1461
- }
1462
- function spare(slot) {
1463
- const width = widthOf(slot);
1464
- if (slot.kind === "gutter") {
1465
- const used = slot.cells.map(occupied).sort((a, b) => a - b);
1466
- return Math.max(0, width - (used[Math.floor(used.length * RESERVED_BLANK_SHARE)] ?? 0));
1467
- }
1468
- if (slot.heading?.hasAttribute("data-fixed") === true || slot.cells.some((cell) => cell?.hasAttribute("data-fixed") === true)) return 0;
1469
- const heading = slot.heading === void 0 ? null : inkSpan(slot.heading);
1470
- const drawn = Math.max(
1471
- heading === null ? 0 : heading.right - heading.left,
1472
- ...slot.cells.map((cell) => {
1473
- const span = cell === void 0 ? null : inkSpan(cell);
1474
- return span === null ? 0 : span.right - span.left;
1475
- })
1476
- );
1477
- return Math.max(0, width - drawn);
1478
- }
1479
- function slotGaps(register) {
1480
- const first = register.slots.map((slot) => slot.cells[0]).filter((cell) => cell !== void 0);
1481
- const boxes = first.map((cell) => cell.getBoundingClientRect()).sort((a, b) => a.left - b.left);
1482
- if (boxes.length < 2) return { gap: 0, beyond: 0 };
1483
- const drawn = boxes.slice(1).map((box, index) => Math.max(0, box.left - boxes[index].right));
1484
- const parent = first[0].parentElement;
1485
- const declared = parent === null ? Number.NaN : Number.parseFloat(getComputedStyle(parent).columnGap);
1486
- const gap = Number.isFinite(declared) ? declared : Math.min(...drawn);
1487
- return { gap: Math.round(gap), beyond: drawn.reduce((sum, one) => sum + Math.max(0, one - gap), 0) };
1488
- }
1489
- function shedWithRoom(root = document.body) {
1490
- let total = 0;
1491
- const found = [];
1492
- for (const register of kitRegisters(root)) {
1493
- const shed = shedOf(register.root);
1494
- if (shed.length === 0 || register.rows.length === 0) continue;
1495
- total += shed.length;
1496
- const { gap, beyond } = slotGaps(register);
1497
- const unused = Math.round(register.slots.reduce((sum, slot) => sum + spare(slot), 0) + beyond);
1498
- for (const column of shed) if (column.width + gap <= unused) found.push({ ...column, gap, unused });
1499
- }
1500
- return { total, shed: found };
1501
- }
1502
- var CREATE = "[data-create]";
1503
- var ITEM = '.lotics-row, .lotics-table__row, .lotics-table__block, .lotics-timeline__day, .lotics-region-state, [role="row"], [role="listitem"], li';
1504
- var HEADINGS = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
1505
- function offHeadingActs(root = document.body) {
1506
- const section = ".lotics-section";
1507
- let total = 0;
1508
- const off = [];
1509
- for (const one of within(root, section).filter(onScreen)) {
1510
- const heading = Array.from(one.children).find((child) => child.classList.contains("lotics-section-heading"));
1511
- if (heading === void 0) continue;
1512
- const titles = within(one, HEADINGS).filter((title) => inside(title, one, section) && onScreen(title));
1513
- for (const act of within(one, CREATE)) {
1514
- if (!onScreen(act) || !inside(act, one, section)) continue;
1515
- const name = nameOf(act) || words4(act);
1516
- const item = act.closest(ITEM);
1517
- if (item !== null && one.contains(item)) continue;
1518
- let owner;
1519
- for (const title2 of titles) {
1520
- if (!title2.contains(act) && (title2.compareDocumentPosition(act) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0) owner = title2;
1521
- }
1522
- if (owner !== void 0 && !heading.contains(owner)) continue;
1523
- total += 1;
1524
- const said2 = `"${words4(heading.querySelector(HEADINGS)) || words4(heading) || "(untitled)"}": "${name.slice(0, 40)}"`;
1525
- if (!heading.contains(act)) {
1526
- off.push(`${said2} drawn in the section's body`);
1527
- continue;
1528
- }
1529
- const title = heading.querySelector(HEADINGS);
1530
- if (title !== null && !title.contains(act) && act.getBoundingClientRect().top >= title.getBoundingClientRect().bottom) {
1531
- off.push(`${said2} wrapped under the heading`);
1532
- }
1533
- }
1534
- }
1535
- return { total, off };
1536
- }
1537
- var OF_TOTAL = /^\d[\d.,]*(?:\s*[–-]\s*\d[\d.,]*)?\s*(?:of|trên|\/)\s*(\d[\d.,]*)$/i;
1538
- var FIGURE = /^\d{1,3}(?:[.,]\d{3})+$|^\d+$/;
1539
- var figure = (text) => FIGURE.test(text) ? Number(text.replace(/[.,]/g, "")) : null;
1540
- function statedCounts(root = document.body) {
1541
- const stated2 = [];
1542
- for (const el of within(root, "*")) {
1543
- if (!onScreen(el) || el.closest('nav, [role="banner"]') !== null) continue;
1544
- const text = words4(el);
1545
- const total = OF_TOTAL.exec(text);
1546
- if (total !== null && !Array.from(el.children).some((child) => OF_TOTAL.test(words4(child)))) {
1547
- const value2 = figure(total[1]);
1548
- if (value2 !== null) stated2.push({ text, value: value2 });
1549
- continue;
1550
- }
1551
- const holder = el.closest('[role="tab"], .lotics-count');
1552
- if (el.childElementCount > 0 || holder === null) continue;
1553
- const value = figure(text);
1554
- if (value !== null) stated2.push({ text: words4(el.closest('[role="tab"]') ?? holder), value });
1555
- }
1556
- return stated2;
1557
- }
1558
-
1559
- // src/probe_collectors/right_form.ts
1560
- var DAY_RUN_PART = ".lotics-timeline__dayhead";
1561
- function rightFormCensus(root = document.body) {
1562
- const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
1563
- const columnFloor = 3;
1564
- const words5 = (el) => el === null ? "" : (el.textContent || "").trim().slice(0, 40);
1565
- const rules = [];
1566
- const choosers = within(root, 'select, [role="listbox"]').filter(onScreen);
1567
- let twoOption = 0;
1568
- const pairs = [];
1569
- for (const chooser of choosers) {
1570
- const options = Array.from(
1571
- chooser.tagName === "SELECT" ? chooser.querySelectorAll("option") : chooser.querySelectorAll('[role="option"]')
1572
- ).map((option) => (option.textContent || "").trim()).filter((label) => label !== "");
1573
- if (options.length !== 2) continue;
1574
- twoOption += 1;
1575
- if (pairs.length < 3) {
1576
- const field = chooser.closest(".lotics-form-field");
1577
- const named = nameOf(chooser).slice(0, 40) || words5(field === null ? null : field.querySelector(".lotics-form-field__label")) || "(unnamed)";
1578
- pairs.push(`"${named}": a two-option select \u2014 ${options.map((option) => `"${option}"`).join(" / ")}`);
1579
- }
1580
- }
1581
- rules.push({
1582
- law: "data_entry.md",
1583
- rule: "Choosing a CHOICE control \u2014 by the set, never taste",
1584
- total: choosers.length,
1585
- count: twoOption,
1586
- offenders: pairs
1587
- });
1588
- let dayRuns = false;
1589
- for (const sheet of Array.from(document.styleSheets)) {
1590
- try {
1591
- for (const rule of Array.from(sheet.cssRules)) {
1592
- if (rule.cssText.includes(DAY_RUN_PART)) {
1593
- dayRuns = true;
1594
- break;
1595
- }
1596
- }
1597
- } catch {
1598
- continue;
1599
- }
1600
- if (dayRuns) break;
1601
- }
1602
- const dated = /^\d{1,4}[/.-]\d{1,2}(?:[/.-]\d{1,4})?$/;
1603
- const leaves = [];
1604
- if (dayRuns) {
1605
- const walk = (node) => {
1606
- if (node.nodeType === Node.TEXT_NODE) {
1607
- const text = (node.textContent || "").trim();
1608
- const parent = node.parentElement;
1609
- if (text === "" || parent === null || skipTag.test(parent.tagName)) return;
1610
- if (!onScreen(parent)) return;
1611
- const box = parent.getBoundingClientRect();
1612
- leaves.push({
1613
- text,
1614
- left: Math.round(box.left),
1615
- right: Math.round(box.right),
1616
- top: Math.round(box.top),
1617
- bottom: Math.round(box.bottom),
1618
- date: dated.test(text)
1619
- });
1620
- return;
1621
- }
1622
- node.childNodes.forEach(walk);
1623
- };
1624
- walk(root);
1625
- }
1626
- const dates = leaves.filter((leaf) => leaf.date);
1627
- const headings = leaves.filter((leaf) => !leaf.date);
1628
- const leads = (leaf) => !leaves.some(
1629
- (other) => other.left < leaf.left && other.top < leaf.bottom && other.bottom > leaf.top
1630
- );
1631
- const stacks = [];
1632
- for (const leaf of [...dates].sort((a, b) => a.left - b.left || a.top - b.top)) {
1633
- const stack = stacks.find((column) => leaf.left < column.right && leaf.right > column.left);
1634
- if (stack !== void 0) {
1635
- stack.items.push(leaf);
1636
- stack.left = Math.min(stack.left, leaf.left);
1637
- stack.right = Math.max(stack.right, leaf.right);
1638
- } else {
1639
- stacks.push({ left: leaf.left, right: leaf.right, items: [leaf] });
1640
- }
1641
- }
1642
- const columns = stacks.filter(
1643
- (stack) => stack.items.length >= columnFloor && stack.items.every(leads)
1644
- );
1645
- let grouped = 0;
1646
- const runs = [];
1647
- for (const column of columns) {
1648
- const days = new Set(column.items.map((leaf) => leaf.text));
1649
- if (days.size >= column.items.length) continue;
1650
- grouped += 1;
1651
- if (runs.length < 3) {
1652
- const top = Math.min(...column.items.map((leaf) => leaf.top));
1653
- const over = headings.filter((label) => label.top < top && top - label.top <= 360 && label.left < column.right && label.right > column.left).sort((a, b) => b.top - a.top)[0];
1654
- runs.push(
1655
- `${over === void 0 ? "(unnamed)" : over.text}: ${column.items.length} rows over ${days.size} days, the date repeated per row`
1656
- );
1657
- }
1658
- }
1659
- rules.push({
1660
- law: "screen.md",
1661
- rule: "Which device answers the question \u2192 Read what happened, or what is planned, in time",
1662
- total: columns.length,
1663
- count: grouped,
1664
- offenders: runs
1665
- });
1666
- return rules;
1667
- }
1668
-
1669
- // src/probe_collectors/surface.ts
1670
- function surfaceChrome(root = document.body) {
1671
- const round = (n) => Math.round(n * 10) / 10;
1672
- const anchor = (el) => el === void 0 ? null : { text: (el.textContent || "").trim().slice(0, 80), left: round(el.getBoundingClientRect().left) };
1673
- const nav = within(root, "nav")[0] ?? null;
1674
- const navFirst = nav === null ? void 0 : Array.from(nav.querySelectorAll('a[href], [role="link"]')).filter(
1675
- (item) => onScreen(item) && (item.textContent || "").trim() !== ""
1676
- )[0];
1677
- const headings = within(root, "h1, h2, h3, h4, h5, h6").filter(
1678
- (el) => onScreen(el) && el.closest('nav, [role="banner"]') === null && (el.textContent || "").trim() !== ""
1679
- );
1680
- const title = headings[0];
1681
- const rank = (el) => Number(el.tagName.slice(1));
1682
- const heading = title === void 0 ? void 0 : headings.slice(1).find((el) => rank(el) > rank(title));
1683
- const primaries = within(root, '[data-variant="primary"]').filter(
1684
- (el) => onScreen(el) && el.closest('nav, [role="banner"]') === null
1685
- );
1686
- const folds = within(root, '[aria-expanded="false"]').filter(
1687
- (el) => onScreen(el) && el.getAttribute("aria-haspopup") === null
1688
- );
1689
- const foldNames = folds.map((el) => nameOf(el).slice(0, 60));
1690
- const folded = folds.reduce((sum, el) => {
1691
- const digits = /\d+/.exec((el.textContent || "").trim());
1692
- return sum + (digits === null ? 0 : Number(digits[0]));
1693
- }, 0);
1694
- return {
1695
- nav: anchor(navFirst),
1696
- title: anchor(title),
1697
- heading: anchor(heading),
1698
- primary: {
1699
- count: primaries.length,
1700
- names: primaries.map((el) => nameOf(el).slice(0, 40))
1701
- },
1702
- facts: { visible: labelValuePairs(root), folded, folds: foldNames }
1703
- };
1704
- }
1705
- function labelValuePairs(root) {
1706
- const px2 = (el) => parseFloat(getComputedStyle(el).fontSize);
1707
- const isLeaf = (el) => el.children.length === 0 && (el.textContent || "").trim().length > 0;
1708
- const leafOf = (el, depth = 3) => {
1709
- if (isLeaf(el)) return el;
1710
- if (depth === 0 || el.children.length === 0) return null;
1711
- return leafOf(el.children[0], depth - 1);
1712
- };
1713
- let pairs = 0;
1714
- for (const el of Array.from(root.querySelectorAll("div"))) {
1715
- const kids = Array.from(el.children);
1716
- if (kids.length !== 2) continue;
1717
- if (el.closest('nav, [role="banner"]') !== null) continue;
1718
- const label = leafOf(kids[0]);
1719
- const value = leafOf(kids[1]);
1720
- if (label === null || value === null) continue;
1721
- const a = kids[0].getBoundingClientRect();
1722
- const b = kids[1].getBoundingClientRect();
1723
- if (a.height === 0 || b.height === 0) continue;
1724
- if (px2(label) < px2(value)) pairs += 1;
1725
- }
1726
- return pairs;
1727
- }
1728
- function repeatedValues(root = document.body) {
1729
- const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
1730
- const counts = /* @__PURE__ */ new Map();
1731
- let total = 0;
1732
- for (const el of Array.from(root.querySelectorAll("*"))) {
1733
- if (skipTag.test(el.tagName) || el.children.length > 0) continue;
1734
- const text = (el.textContent || "").trim();
1735
- if (text.length < 3) continue;
1736
- if (!/\d/.test(text) || !/[^\d\s]/.test(text)) continue;
1737
- if (!onScreen(el)) continue;
1738
- total += 1;
1739
- counts.set(text, (counts.get(text) || 0) + 1);
1740
- }
1741
- const repeated = Array.from(counts.entries()).filter(([, times]) => times > 1).sort((a, b) => b[1] - a[1]).map(([text, times]) => ({ text: text.slice(0, 60), times }));
1742
- return { total, repeated };
1743
- }
1744
- function wrappedMoney(root = document.body) {
1745
- const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
1746
- const money = /[$€£¥₫₹]|\bUS\$/;
1747
- const wrapped = [];
1748
- let total = 0;
1749
- for (const el of Array.from(root.querySelectorAll("*"))) {
1750
- if (skipTag.test(el.tagName) || el.children.length > 0) continue;
1751
- const text = (el.textContent || "").trim();
1752
- if (text === "" || !money.test(text) || !/\d/.test(text)) continue;
1753
- if (!onScreen(el)) continue;
1754
- total += 1;
1755
- const range = document.createRange();
1756
- range.selectNodeContents(el);
1757
- const lines = Array.from(range.getClientRects()).filter((rect) => rect.width > 0).length;
1758
- if (lines > 1) wrapped.push({ text: text.slice(0, 50), lines });
1759
- }
1760
- return { total, wrapped };
1761
- }
1762
-
1763
- // src/probe_page.ts
1764
- var PROBES = {
1765
- money_edges: {
1766
- law: "reviewing.md",
1767
- section: "6",
1768
- fix: "One right edge per money column: give every figure in it the same alignment and the same unit."
1769
- },
1770
- reported_values: {
1771
- law: "reviewing.md",
1772
- section: "9",
1773
- fix: "Encode the column the screen is ABOUT \u2014 a mark, a meter, a chart or a picture \u2014 and leave the rest as text; a meter whose maximum is the bound it alarms at reads the same on every row, so it encodes nothing."
1774
- },
1775
- empty_marks: {
1776
- law: "reviewing.md",
1777
- section: "9",
1778
- fix: "Fill the picture column or drop it: a mark slot drawn empty on every row states nothing and takes a column's width."
1779
- },
1780
- stage_words: {
1781
- law: "reviewing.md",
1782
- section: "9",
1783
- fix: "A state is the one thing a register encodes with COLOUR: give the stage cell its option's swatch, or the glyph its verdict wears. The words stay; the mark is what the reader scans."
1784
- },
1785
- internal_ids: {
1786
- law: "reviewing.md",
1787
- section: "8",
1788
- fix: "A key is not a name: render the record's own label, and keep the id in the link."
1789
- },
1790
- tab_strips: {
1791
- law: "reviewing.md",
1792
- section: "8a",
1793
- fix: "One navigation strip per screen. A second is a filter wearing navigation's clothes \u2014 draw it as one."
1794
- },
1795
- truncation: {
1796
- law: "reviewing.md",
1797
- section: "8g",
1798
- fix: "Shed a trailing column, or widen this one until the value fits \u2014 a date, a code or a figure cut inside itself reads as a different one."
1799
- },
1800
- banned_glyph: {
1801
- law: "reviewing.md",
1802
- section: "10",
1803
- fix: "No punctuation doing a word's job \u2014 separate the two facts with layout, or write the word."
1804
- },
1805
- compact_money_words: {
1806
- law: "reviewing.md",
1807
- section: "10",
1808
- fix: "Abbreviate in the currency's own words: tr/t\u1EF7 beside \u20AB, K/M/B beside $ and \u20AC."
1809
- },
1810
- stacked_pairs: {
1811
- law: "reviewing.md",
1812
- section: "1",
1813
- fix: "Demote once, not twice: either the size steps down or the subject gets the weight."
1814
- },
1815
- row_marks: {
1816
- law: "reviewing.md",
1817
- section: "9",
1818
- fix: "One rung per row: a mark's size follows what the row is ABOUT, so the supporting mark takes the smaller one."
1819
- },
1820
- reserved_blank: {
1821
- law: "reviewing.md",
1822
- section: "8l",
1823
- fix: "Give the room back: a column or row-act gutter most rows leave empty goes, and what the few rows hold rides those rows instead."
1824
- },
1825
- shed_with_room: {
1826
- law: "reviewing.md",
1827
- section: "8m",
1828
- fix: "Seat the column the width can hold: reclaim what a slot reserves beyond what it must seat \u2014 a flexible column wider than its ink, a gutter held for acts most rows lack \u2014 before the fit gives a reading up."
1829
- },
1830
- action_off_heading: {
1831
- law: "reviewing.md",
1832
- section: "8n",
1833
- fix: "Mount the section's add in its heading row (`RecordSection.action`, a `SectionHeadingAdd`), and let what it opens stand in the body: one place for a section's add, whatever its body draws."
1834
- },
1835
- partial_count: {
1836
- law: "reviewing.md",
1837
- section: "8o",
1838
- fix: "State the server's count of the set, never the rows a page loaded: count the read (`useQuery`'s `total`) and step or badge against that total."
1839
- },
1840
- record_primary: {
1841
- law: "hierarchy.md",
1842
- section: "One primary act per surface",
1843
- fix: "Draw a block's add secondary: the record's primaries are its acts \u2014 the next step at a section's foot."
1844
- },
1845
- fact_repeats: {
1846
- law: "hierarchy.md",
1847
- section: "One fact, one place",
1848
- fix: "Take the field out of the facts: the header already states it \u2014 its title, its status, the line under it or its figure."
1849
- },
1850
- empty_block: {
1851
- law: "hierarchy.md",
1852
- section: "Whitespace is structure",
1853
- fix: "Draw the block's body, or its empty line: a block under a heading that draws nothing is a press that leads nowhere."
1854
- },
1855
- render: {
1856
- law: "reviewing.md",
1857
- section: "8",
1858
- fix: "Open the screen and read it: nothing below this was measured, so a clean run over the rest means nothing."
1859
- },
1860
- settle: {
1861
- law: "reviewing.md",
1862
- section: "9c",
1863
- fix: "A screen that never stops changing has a render that re-renders or a request that never lands \u2014 find which."
1864
- },
1865
- /** An exception the frame threw and nothing caught while the walk reached the surface — raised by the walk. */
1866
- crash: {
1867
- law: "reviewing.md",
1868
- section: "7",
1869
- fix: "Fix what threw: a reader who opens this surface meets it too. A SpecError is app.json stating what the runtime cannot draw, which `lotics scaffold check` should have refused \u2014 report that check gap."
1870
- },
1871
- /**
1872
- * The three CENSUSES. They differ from every probe above in shape: each one
1873
- * makes SEVERAL measurements and raises one finding per measurement that
1874
- * fired, so the signature here is the census's own frame and each detail line
1875
- * names the rule it is. A screen is cluttered, in the wrong form, or drawn as
1876
- * something it is not as a WHOLE, which is why none of them reduces to one
1877
- * number.
1878
- */
1879
- clutter: {
1880
- law: "hierarchy.md",
1881
- section: "Budgets a reviewer can count",
1882
- fix: "Move a fact down a tier, fold it, or take it off the screen \u2014 a budget failing is a tiering question, never a permission to shrink something."
1883
- },
1884
- right_form: {
1885
- law: "screen.md",
1886
- section: "Which device answers the question",
1887
- fix: "Draw what the JOB names: the device index gives the entry and the wrong device beside it, and a shape is picked by what the reader must do rather than by what the data is."
1888
- },
1889
- ink_and_air: {
1890
- law: "hierarchy.md",
1891
- section: "Ink and air state what a thing is",
1892
- fix: "Draw the thing as what it is: words for a heading, a closed frame for an editor, room for its focus ring, and more air between two named groups than between the rows inside either."
1893
- }
1894
- };
1895
- function signature(probe) {
1896
- return PROBES[probe];
1897
- }
1898
- function censusFindings(probe, rules) {
1899
- const fired = rules.filter((rule) => rule.count > 0);
1900
- if (fired.length === 0) return [];
1901
- return [
1902
- {
1903
- probe,
1904
- ...signature(probe),
1905
- total: rules.reduce((sum, rule) => sum + rule.total, 0),
1906
- count: fired.reduce((sum, rule) => sum + rule.count, 0),
1907
- detail: fired.map(
1908
- (rule) => `${rule.law} \xA7${rule.rule} \u2014 ${rule.count} of ${rule.total}` + (rule.offenders.length === 0 ? "" : `: ${rule.offenders.join(" | ")}`)
1909
- )
1910
- }
1911
- ];
1912
- }
1913
- var MONEY_EDGES_ALLOWED = 2;
1914
- var MONEY_COLUMN_MIN = 3;
1915
- var BARE_VALUES_FLOOR = 8;
1916
- var QUANTITY_COLUMN_MIN = 2;
1917
- var isUnread = (value) => typeof value === "object" && value !== null && typeof Reflect.get(value, "label") === "string" && typeof Reflect.get(value, "reason") === "string";
1918
- function unreadReadings(surface) {
1919
- return within(surface, "[data-lotics-unread]").flatMap((marker) => {
1920
- const stated2 = JSON.parse(marker.getAttribute("data-lotics-unread") ?? "");
1921
- if (!Array.isArray(stated2) || !stated2.every(isUnread)) throw new Error(`data-lotics-unread holds no list of { label, reason }: ${JSON.stringify(stated2).slice(0, 200)}`);
1922
- return stated2;
1923
- });
1924
- }
1925
- function run(options) {
1926
- const findings = [];
1927
- const surface = measuredSurface();
1928
- const money = moneyColumns(surface);
1929
- const ragged = money.columns.filter((column) => column.size >= MONEY_COLUMN_MIN && column.edges.length > MONEY_EDGES_ALLOWED);
1930
- if (ragged.length > 0) {
1931
- findings.push({
1932
- probe: "money_edges",
1933
- ...signature("money_edges"),
1934
- total: money.total,
1935
- count: ragged.length,
1936
- detail: ragged.slice(0, 5).map((column) => `x=${column.x}: ${column.edges.length} right edges over ${column.size} figures \u2014 ${column.samples.join(" | ")}`)
1937
- });
1938
- }
1939
- const values = reportedValues(surface);
1940
- const measurable = values.columns.some((column) => column.quantities >= QUANTITY_COLUMN_MIN) || values.devices > 0;
1941
- const emptyPictureColumns = values.markSlots.filter((column) => column.filled === 0);
1942
- const silentColumns = values.columns.filter(
1943
- (column) => column.quantities === 0 && column.size >= QUANTITY_COLUMN_MIN
1944
- );
1945
- const drawnAndSilent = values.devices > 0 && values.encoded === 0 && silentColumns.length > 0 && values.total >= BARE_VALUES_FLOOR;
1946
- const wordsOnly = values.bare >= BARE_VALUES_FLOOR && values.encoded === 0 && measurable;
1947
- if (emptyPictureColumns.length > 0) {
1948
- findings.push({
1949
- probe: "empty_marks",
1950
- ...signature("empty_marks"),
1951
- total: values.markSlots.reduce((sum, column) => sum + column.size, 0),
1952
- count: emptyPictureColumns.reduce((sum, column) => sum + column.size, 0),
1953
- detail: emptyPictureColumns.slice(0, 5).map(
1954
- (column) => `${column.slot === "" ? "(unnamed)" : column.slot}: ${column.size} picture slots, none holding a picture`
1955
- )
1956
- });
1957
- } else if (drawnAndSilent || wordsOnly) {
1958
- findings.push({
1959
- probe: "reported_values",
1960
- ...signature("reported_values"),
1961
- total: values.total,
1962
- count: values.bare,
1963
- detail: [
1964
- values.devices === 0 ? `${values.bare} unnamed figures as bare text, nothing drawn as a mark, meter, chart or picture` : `${values.devices} drawn, none of them reading a value; ${values.bare} unnamed figures as bare text`,
1965
- ...values.columns.slice(0, 5).map((column) => ` ${column.slot === "" ? "(unnamed)" : column.slot}: ${column.size} \u2014 ${column.samples.join(", ")}`)
1966
- ]
1967
- });
1968
- }
1969
- const ids = internalIds(surface);
1970
- if (ids.ids.length > 0) {
1971
- findings.push({ probe: "internal_ids", ...signature("internal_ids"), total: ids.total, count: ids.ids.length, detail: ids.ids.slice(0, 5) });
1972
- }
1973
- const stageWords = stageWordColumns(surface);
1974
- if (stageWords.wordsOnly.length > 0) {
1975
- findings.push({
1976
- probe: "stage_words",
1977
- ...signature("stage_words"),
1978
- total: stageWords.total,
1979
- count: stageWords.wordsOnly.reduce((sum, column) => sum + column.size, 0),
1980
- detail: stageWords.wordsOnly.slice(0, 5).map(
1981
- (column) => `${column.slot === "" ? "(unnamed)" : column.slot}: the stage column is words alone \u2014 ${column.size} rows of ${column.values.join(" / ")}, no swatch or glyph`
1982
- )
1983
- });
1984
- }
1985
- const strips = tabStrips(surface);
1986
- if (strips.counted > 1) {
1987
- findings.push({
1988
- probe: "tab_strips",
1989
- ...signature("tab_strips"),
1990
- total: strips.total,
1991
- count: strips.counted,
1992
- detail: strips.strips.map((strip) => strip.join(" / "))
1993
- });
1994
- }
1995
- if (options.truncation) {
1996
- const cut = truncatedLeaves(surface);
1997
- if (cut.cut.length > 0) {
1998
- findings.push({
1999
- probe: "truncation",
2000
- ...signature("truncation"),
2001
- total: cut.total,
2002
- count: cut.cut.length,
2003
- detail: cut.cut.slice(0, 5).map((leaf) => `"${leaf.text}" needs ${leaf.needs}px of ${leaf.axis} in ${leaf.has}px`)
2004
- });
2005
- }
2006
- }
2007
- const glyphs = bannedGlyphs(surface);
2008
- if (glyphs.found.length > 0) {
2009
- findings.push({ probe: "banned_glyph", ...signature("banned_glyph"), total: glyphs.total, count: glyphs.found.length, detail: glyphs.found.slice(0, 5) });
2010
- }
2011
- const compact = compactMoneyWords(surface);
2012
- if (compact.foreign.length > 0) {
2013
- findings.push({
2014
- probe: "compact_money_words",
2015
- ...signature("compact_money_words"),
2016
- total: compact.total,
2017
- count: compact.foreign.length,
2018
- detail: compact.foreign.slice(0, 5).map((text) => `"${text}" abbreviates in the other currency's words`)
2019
- });
2020
- }
2021
- const pairs = scanStackedPairs(surface);
2022
- if (pairs.overDemoted.length > 0) {
2023
- findings.push({
2024
- probe: "stacked_pairs",
2025
- ...signature("stacked_pairs"),
2026
- total: pairs.total,
2027
- count: pairs.overDemoted.length,
2028
- detail: pairs.overDemoted.slice(0, 5).map((pair) => `"${pair.subject}" ${pair.sizes[0]}px/${pair.weights[0]} over "${pair.supporting}" ${pair.sizes[1]}px/${pair.weights[1]}`)
2029
- });
2030
- }
2031
- const marks = scanRowMarks(surface);
2032
- if (marks.mismatches.length > 0) {
2033
- findings.push({
2034
- probe: "row_marks",
2035
- ...signature("row_marks"),
2036
- total: marks.total,
2037
- count: marks.mismatches.length,
2038
- detail: marks.mismatches.slice(0, 5).map((row) => `y=${row.y}: ${row.marks.join(", ")}`)
2039
- });
2040
- }
2041
- const reserved = reservedBlanks(surface);
2042
- if (reserved.blank.length > 0) {
2043
- findings.push({
2044
- probe: "reserved_blank",
2045
- ...signature("reserved_blank"),
2046
- total: reserved.total,
2047
- count: reserved.blank.length,
2048
- detail: reserved.blank.slice(0, 5).map(
2049
- (slot) => `${slot.slot}: ${slot.width}px, ${slot.kind === "gutter" ? `${RESERVED_MIN_WIDTH}px or more beside the acts` : "blank"} on ${slot.blank} of ${slot.rows} rows`
2050
- )
2051
- });
2052
- }
2053
- const shed = shedWithRoom(surface);
2054
- if (shed.shed.length > 0) {
2055
- findings.push({
2056
- probe: "shed_with_room",
2057
- ...signature("shed_with_room"),
2058
- total: shed.total,
2059
- count: shed.shed.length,
2060
- detail: shed.shed.slice(0, 5).map((column) => `"${column.label}" (${column.width}px + ${column.gap}px gap) shed beside ${column.unused}px the slots hold beyond what they seat`)
2061
- });
2062
- }
2063
- const adds = offHeadingActs(surface);
2064
- if (adds.off.length > 0) {
2065
- findings.push({
2066
- probe: "action_off_heading",
2067
- ...signature("action_off_heading"),
2068
- total: adds.total,
2069
- count: adds.off.length,
2070
- detail: adds.off.slice(0, 5)
2071
- });
2072
- }
2073
- const served = options.served;
2074
- if (served !== void 0) {
2075
- const stated2 = statedCounts(surface);
2076
- const partial = stated2.flatMap((count) => {
2077
- const read = served.reads.find(
2078
- (one) => one.in_hand === count.value && one.total !== count.value && !served.counted.some((counted) => counted.alias === one.alias && counted.value === count.value)
2079
- );
2080
- return read === void 0 ? [] : [`"${count.text}" states ${count.value} \u2014 the rows in hand of "${read.alias}", which the server counts ${read.total}`];
2081
- });
2082
- if (partial.length > 0) {
2083
- findings.push({
2084
- probe: "partial_count",
2085
- ...signature("partial_count"),
2086
- total: stated2.length,
2087
- count: partial.length,
2088
- detail: partial.slice(0, 5)
2089
- });
2090
- }
2091
- }
2092
- const primaries = headingPrimaries(surface);
2093
- if (primaries.loud.length > 0) {
2094
- findings.push({
2095
- probe: "record_primary",
2096
- ...signature("record_primary"),
2097
- total: primaries.total,
2098
- count: primaries.loud.length,
2099
- detail: primaries.loud.slice(0, 5).map((one) => `"${one.record}": "${one.act}" is primary in a block's heading row, beside the record's own acts`)
2100
- });
2101
- }
2102
- const repeats = factRepeats(surface);
2103
- if (repeats.repeated.length > 0) {
2104
- findings.push({
2105
- probe: "fact_repeats",
2106
- ...signature("fact_repeats"),
2107
- total: repeats.total,
2108
- count: repeats.repeated.length,
2109
- detail: repeats.repeated.slice(0, 5).map((one) => `"${one.record}": the fact "${one.field}" reads "${one.value}", which the header already says`)
2110
- });
2111
- }
2112
- const blocks = emptyBlocks(surface);
2113
- if (blocks.empty.length > 0) {
2114
- findings.push({
2115
- probe: "empty_block",
2116
- ...signature("empty_block"),
2117
- total: blocks.total,
2118
- count: blocks.empty.length,
2119
- detail: blocks.empty.slice(0, 5).map((one) => `"${one.record}": the block "${one.block}" draws nothing`)
2120
- });
2121
- }
2122
- findings.push(...censusFindings("clutter", clutterCensus(surface)));
2123
- findings.push(...censusFindings("right_form", rightFormCensus(surface)));
2124
- findings.push(...censusFindings("ink_and_air", [...inkAndAirCensus(surface), ...options.rings ?? []]));
2125
- return {
2126
- findings,
2127
- census: {
2128
- leaves: textLeafCount(surface),
2129
- money: money.total,
2130
- bare: values.bare,
2131
- quantities: values.quantities,
2132
- encoded: values.encoded,
2133
- devices: values.devices,
2134
- device_kinds: values.deviceKinds.map(
2135
- (device) => `${device.kind}\xD7${device.slot === "" ? "(unnamed)" : device.slot}: ${device.count}`
2136
- ),
2137
- strips: strips.counted,
2138
- unread: unreadReadings(surface)
2139
- }
2140
- };
2141
- }
2142
- function rings() {
2143
- return focusRingCensus(measuredSurface());
2144
- }
2145
- function editorSelector() {
2146
- return EDITOR;
2147
- }
2148
- function focused() {
2149
- const held = document.activeElement;
2150
- if (held === null || held === document.body) return null;
2151
- const named = nameOf(held).slice(0, 40);
2152
- return named === "" ? held.tagName.toLowerCase() : named;
2153
- }
2154
- function paintState() {
2155
- const root = document.getElementById("root");
2156
- return {
2157
- leaves: textLeafCount(),
2158
- loading: document.readyState === "loading",
2159
- booted: root !== null && root.hasChildNodes()
2160
- };
2161
- }
2162
- function textLeafCount(root = document.body) {
2163
- const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
2164
- let count = 0;
2165
- const walk = (node) => {
2166
- if (node.nodeType === Node.TEXT_NODE) {
2167
- const parent = node.parentElement;
2168
- if (parent && !skipTag.test(parent.tagName) && (node.textContent || "").trim()) count += 1;
2169
- return;
2170
- }
2171
- node.childNodes.forEach(walk);
2172
- };
2173
- walk(root);
2174
- return count;
2175
- }
2176
- function navItems() {
2177
- const nav = document.querySelector("nav");
2178
- if (nav === null) return [];
2179
- return Array.from(nav.querySelectorAll('a[href], [role="link"]')).filter(
2180
- (item) => (item.textContent || "").trim() !== ""
2181
- );
2182
- }
2183
- function tabItems() {
2184
- const strip = document.querySelector('[role="tablist"]');
2185
- if (strip === null) return [];
2186
- return Array.from(strip.querySelectorAll('[role="tab"]'));
2187
- }
2188
- function screens() {
2189
- const nav = navItems();
2190
- if (nav.length > 0) return { source: "nav", labels: nav.map((item) => (item.textContent || "").trim()) };
2191
- const tabs = tabItems();
2192
- if (tabs.length > 0) return { source: "tabs", labels: tabs.map((item) => (item.textContent || "").trim()) };
2193
- return { source: "root", labels: [] };
2194
- }
2195
- function openScreen(source, index) {
2196
- const items = source === "nav" ? navItems() : source === "tabs" ? tabItems() : [];
2197
- const item = items[index];
2198
- if (item === void 0) return false;
2199
- item.click();
2200
- return true;
2201
- }
2202
- function recordLinks(surface) {
2203
- const here = location.pathname + location.search;
2204
- const doors2 = Array.from(
2205
- surface.querySelectorAll('a[href], [data-opens="page"], [data-opens="event"], button, [role="button"], [role="link"]')
2206
- ).filter((door) => door.matches('a[href], [data-opens="page"], [data-opens="event"]') || isEmptyDoor(door));
2207
- return doors2.filter((door) => {
2208
- if (door.closest('nav, [role="banner"], [role="tablist"]') !== null) return false;
2209
- if (doors2.some((other) => other !== door && other.contains(door))) return false;
2210
- const rect = door.getBoundingClientRect();
2211
- if (rect.width === 0 || rect.height === 0) return false;
2212
- const href = door.getAttribute("href");
2213
- if (href === null) return true;
2214
- if (href === "" || href.startsWith("http") || href.startsWith("#")) return false;
2215
- return href !== here;
2216
- });
2217
- }
2218
- function recordRows() {
2219
- const surface = workedSurface();
2220
- return surface === null ? [] : recordLinks(surface).map(doorName);
2221
- }
2222
- function openRecord(index) {
2223
- const surface = workedSurface();
2224
- const door = surface === null ? void 0 : recordLinks(surface)[index];
2225
- if (door === void 0) return false;
2226
- door.click();
2227
- return true;
2228
- }
2229
- function workedSurface() {
2230
- const surface = measuredSurface();
2231
- return surface === document.body || within(surface, RECORD_FRAME).length > 0 ? surface : null;
2232
- }
2233
- var OVERLAY_ROLES = '[role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"]';
2234
- var NEVER_PRESS = /delete|remove|archive|discard|revoke|unlink|detach|cancel|reset|clear|approve|reject|send|publish|\bpay|submit|confirm|void|deactivate|disable|merge|overwrite|\bsign\b|issue|export|download|print|\bsave\b|\bapply\b/i;
2235
- var NEVER_PRESS_VI_WORDS = [
2236
- "x[o\xF3][a\xE1]",
2237
- "h[\u1EE7u][\u1EF7y]",
2238
- "g[\u1EE1o]",
2239
- "l[\u01B0u]u",
2240
- "duy[\u1EC7e]t",
2241
- "t[\u1EEBu] ch[\u1ED1o]i",
2242
- "g[\u1EEDu]i",
2243
- "thanh to[\xE1a]n",
2244
- "ph[\xE1a]t h[\xE0a]nh",
2245
- "x[\xE1a]c nh[\u1EADa]n",
2246
- "k[\xFDy]",
2247
- "ghi s[\u1ED5o]",
2248
- "kh[\xF3o]a",
2249
- "kho[\xE1a]",
2250
- "ho[\xE0a]n",
2251
- "ch[\u1ED1o]t",
2252
- "t[\xE1a]ch",
2253
- "g[\u1ED9o]p",
2254
- "xu[\u1EA5a]t",
2255
- "t[\u1EA3i]i v[\u1EC1e]",
2256
- "in",
2257
- "\u0111[\u0103a]ng xu[\u1EA5a]t"
2258
- ];
2259
- var NEVER_PRESS_VI = new RegExp(
2260
- `(?<![\\p{L}\\p{N}])(?:${NEVER_PRESS_VI_WORDS.join("|")})(?![\\p{L}\\p{N}])`,
2261
- "iu"
2262
- );
2263
- function doorName(el) {
2264
- return (nameOf(el) || el.getAttribute("href") || "").slice(0, 60);
2265
- }
2266
- function isChrome(el) {
2267
- return el.closest('nav, [role="banner"], [role="tablist"]') !== null;
2268
- }
2269
- function isEmptyDoor(el) {
2270
- if (el.childElementCount > 0) return false;
2271
- if ((el.textContent || "").trim() !== "") return false;
2272
- return (el.getAttribute("aria-label") || "").trim() !== "";
2273
- }
2274
- function route() {
2275
- return location.pathname;
2276
- }
2277
- function partyLinks(surface) {
2278
- const here = location.pathname + location.search;
2279
- const doors2 = recordLinks(surface);
2280
- for (const door of doors2) {
2281
- const row = isEmptyDoor(door) ? door.parentElement : door;
2282
- if (row === null) continue;
2283
- const links = Array.from(row.querySelectorAll('[role="link"], a[href]')).filter((link) => {
2284
- if (!onScreen(link) || isChrome(link)) return false;
2285
- if (doors2.some((other) => other === link)) return false;
2286
- if (link.getAttribute("aria-haspopup") !== null || link.getAttribute("aria-expanded") !== null) return false;
2287
- const href = link.getAttribute("href");
2288
- if (href === null) return true;
2289
- if (href === "" || href.startsWith("http") || href.startsWith("#")) return false;
2290
- return href !== here;
2291
- });
2292
- if (links.length > 0) return links;
2293
- }
2294
- return [];
2295
- }
2296
- function doorElements() {
2297
- const surface = workedSurface();
2298
- if (surface === null) return [];
2299
- const open = Array.from(document.querySelectorAll(OVERLAY_ROLES)).filter(
2300
- (overlay) => onScreen(overlay) && overlay !== surface && !overlay.contains(surface)
2301
- );
2302
- const inFront = (el) => surface.contains(el) && onScreen(el) && !open.some((overlay) => overlay.contains(el));
2303
- const reachable = (el) => inFront(el) && !isChrome(el) && el.getAttribute("aria-disabled") !== "true" && !(el instanceof HTMLButtonElement && el.disabled);
2304
- const records = recordLinks(surface).filter(
2305
- (el) => reachable(el) && el.getAttribute("aria-expanded") === null && el.getAttribute("aria-haspopup") === null
2306
- );
2307
- const parties = partyLinks(surface).filter(reachable);
2308
- const menus = Array.from(surface.querySelectorAll("[aria-haspopup]")).filter(
2309
- (el) => reachable(el) && el.getAttribute("aria-expanded") !== "true"
2310
- );
2311
- const folds = Array.from(surface.querySelectorAll('[aria-expanded="false"]')).filter(
2312
- (el) => reachable(el) && el.getAttribute("aria-haspopup") === null
2313
- );
2314
- const tabs = Array.from(surface.querySelectorAll('[role="tab"][aria-selected="false"]')).filter(
2315
- (el) => inFront(el) && el.closest('nav, [role="banner"]') === null
2316
- );
2317
- const sections = within(surface, SECTION_PAGES).filter((el) => el instanceof HTMLElement && inFront(el));
2318
- const acts = Array.from(
2319
- surface.querySelectorAll('[data-variant="primary"], [data-variant="secondary"]')
2320
- ).filter((el) => {
2321
- if (!reachable(el)) return false;
2322
- if (el.tagName !== "BUTTON" && el.getAttribute("role") !== "button") return false;
2323
- if (el.getAttribute("aria-haspopup") !== null || el.getAttribute("aria-expanded") !== null) return false;
2324
- if (el.getAttribute("type") === "submit") return false;
2325
- if (el.closest("form") !== null) return false;
2326
- const tone = el.getAttribute("data-tone");
2327
- if (tone === "danger" || tone === "warning") return false;
2328
- const name = `${doorName(el)} ${(el.textContent || "").trim()}`;
2329
- return !NEVER_PRESS.test(name) && !NEVER_PRESS_VI.test(name);
2330
- });
2331
- return [
2332
- // One of each state-changing kind — the first row, the acts menu, the first
2333
- // fold. A second of any is the same surface again; the acts a screen offers
2334
- // are not, so every one of those is walked, and neither are the PARTIES a
2335
- // row names — each column names a different one.
2336
- ...records.slice(0, 1).map((el) => ({ kind: "record", el })),
2337
- ...menus.slice(0, 1).map((el) => ({ kind: "menu", el })),
2338
- ...folds.slice(0, 1).map((el) => ({ kind: "fold", el })),
2339
- ...acts.map((el) => ({ kind: "dialog", el })),
2340
- ...parties.map((el) => ({ kind: "party", el })),
2341
- ...tabs.map((el) => ({ kind: "tab", el })),
2342
- ...sections.map((el) => ({ kind: "section", el }))
2343
- ];
2344
- }
2345
- function doors() {
2346
- return doorElements().map((door) => ({ kind: door.kind, name: doorName(door.el) }));
2347
- }
2348
- function openDoor(index) {
2349
- const door = doorElements()[index];
2350
- if (door === void 0) return false;
2351
- door.el.click();
2352
- return true;
2353
- }
2354
- function overlaysOpen() {
2355
- return Array.from(document.querySelectorAll(OVERLAY_ROLES)).filter(onScreen).length;
2356
- }
2357
- function contentHeight() {
2358
- let tallest = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
2359
- for (const el of Array.from(document.querySelectorAll("*"))) {
2360
- if (el.scrollHeight <= el.clientHeight) continue;
2361
- if (!onScreen(el)) continue;
2362
- tallest = Math.max(tallest, Math.ceil(el.getBoundingClientRect().top) + el.scrollHeight);
2363
- }
2364
- for (const overlay of Array.from(document.querySelectorAll(OVERLAY_ROLES))) {
2365
- if (!onScreen(overlay)) continue;
2366
- tallest = Math.max(tallest, Math.ceil(overlay.getBoundingClientRect().top) + overlay.scrollHeight);
2367
- }
2368
- return tallest;
2369
- }
2370
- function surfaceReport() {
2371
- const surface = measuredSurface();
2372
- const cut = truncatedLeaves(surface);
2373
- return {
2374
- ...surfaceChrome(surface),
2375
- repeated: repeatedValues(surface).repeated,
2376
- wrapped_money: wrappedMoney(surface).wrapped,
2377
- clipped: cut.cut
2378
- };
2379
- }
2380
- return __toCommonJS(probe_page_exports);
2381
- })();