@lotics/cli 0.205.2 → 0.206.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/README.md +23 -0
- package/dist/probe_page.js +1441 -58
- package/dist/src/cli.js +16335 -10864
- package/dist/src/client.d.ts +4 -2
- package/docs/building_an_app.md +51 -3
- package/docs/cli_reference.md +11 -5
- package/docs/migration.md +57 -0
- package/package.json +1 -1
package/dist/probe_page.js
CHANGED
|
@@ -22,16 +22,25 @@ var __loticsProbe = (() => {
|
|
|
22
22
|
var probe_page_exports = {};
|
|
23
23
|
__export(probe_page_exports, {
|
|
24
24
|
PROBES: () => PROBES,
|
|
25
|
+
contentHeight: () => contentHeight,
|
|
26
|
+
doors: () => doors,
|
|
27
|
+
editorSelector: () => editorSelector,
|
|
28
|
+
focused: () => focused,
|
|
29
|
+
openDoor: () => openDoor,
|
|
25
30
|
openRecord: () => openRecord,
|
|
26
31
|
openScreen: () => openScreen,
|
|
32
|
+
overlaysOpen: () => overlaysOpen,
|
|
33
|
+
paintState: () => paintState,
|
|
27
34
|
recordRows: () => recordRows,
|
|
35
|
+
rings: () => rings,
|
|
28
36
|
run: () => run,
|
|
29
37
|
screens: () => screens,
|
|
38
|
+
surfaceReport: () => surfaceReport,
|
|
30
39
|
textLeafCount: () => textLeafCount
|
|
31
40
|
});
|
|
32
41
|
|
|
33
42
|
// src/probe_collectors.ts
|
|
34
|
-
function moneyColumns() {
|
|
43
|
+
function moneyColumns(root = document.body) {
|
|
35
44
|
const money = /^[-+−]?\s?(?:(?:[$€]|USD|EUR|VND)\s?[\d.,]+\s?(?:[KMB]|tr|tỷ)?|[\d.,]+\s?(?:tr|tỷ)?\s?(?:₫|đ|VND|USD|EUR|[$€]))$/i;
|
|
36
45
|
const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
|
|
37
46
|
const bandGap = 48;
|
|
@@ -59,7 +68,7 @@ var __loticsProbe = (() => {
|
|
|
59
68
|
}
|
|
60
69
|
node.childNodes.forEach(walk);
|
|
61
70
|
};
|
|
62
|
-
walk(
|
|
71
|
+
walk(root);
|
|
63
72
|
const bands = [];
|
|
64
73
|
for (const leaf of [...leaves].sort((a, b) => a.top - b.top)) {
|
|
65
74
|
const band = bands[bands.length - 1];
|
|
@@ -96,7 +105,7 @@ var __loticsProbe = (() => {
|
|
|
96
105
|
}
|
|
97
106
|
return { total: leaves.length, columns };
|
|
98
107
|
}
|
|
99
|
-
function reportedValues() {
|
|
108
|
+
function reportedValues(root = document.body) {
|
|
100
109
|
const numeric = /^[-+−]?\d[\d.,]*\s?(?:%|[A-Za-z₫đ$€]{1,4})?$/;
|
|
101
110
|
const counted = /(?:%|[A-Za-z₫đ$€]{1,4})$/;
|
|
102
111
|
const delta = /^[↑↓→▲▼]\s*[-+−]?\d[\d.,]*\s?%$/;
|
|
@@ -107,7 +116,7 @@ var __loticsProbe = (() => {
|
|
|
107
116
|
const nameReach = 360;
|
|
108
117
|
const values = [];
|
|
109
118
|
const labels = [];
|
|
110
|
-
|
|
119
|
+
const deltaLeaves = [];
|
|
111
120
|
const walk = (node) => {
|
|
112
121
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
113
122
|
const text = (node.textContent || "").trim();
|
|
@@ -116,11 +125,11 @@ var __loticsProbe = (() => {
|
|
|
116
125
|
if (typeof parent.checkVisibility === "function" && !parent.checkVisibility()) return;
|
|
117
126
|
const rect = parent.getBoundingClientRect();
|
|
118
127
|
if (rect.width === 0 && rect.height === 0) return;
|
|
128
|
+
const leaf = { text, left: Math.round(rect.left), right: Math.round(rect.right), top: Math.round(rect.top) };
|
|
119
129
|
if (delta.test(text)) {
|
|
120
|
-
|
|
130
|
+
deltaLeaves.push(leaf);
|
|
121
131
|
return;
|
|
122
132
|
}
|
|
123
|
-
const leaf = { text, left: Math.round(rect.left), right: Math.round(rect.right), top: Math.round(rect.top) };
|
|
124
133
|
if (!numeric.test(text) || dateLike.test(text)) {
|
|
125
134
|
labels.push(leaf);
|
|
126
135
|
return;
|
|
@@ -131,7 +140,7 @@ var __loticsProbe = (() => {
|
|
|
131
140
|
}
|
|
132
141
|
node.childNodes.forEach(walk);
|
|
133
142
|
};
|
|
134
|
-
walk(
|
|
143
|
+
walk(root);
|
|
135
144
|
const clusters = [];
|
|
136
145
|
for (const leaf of [...values].sort((a, b) => a.left - b.left || a.top - b.top)) {
|
|
137
146
|
const cluster = clusters.find((c) => leaf.left < c.right && leaf.right > c.left);
|
|
@@ -143,6 +152,11 @@ var __loticsProbe = (() => {
|
|
|
143
152
|
clusters.push({ left: leaf.left, right: leaf.right, items: [leaf] });
|
|
144
153
|
}
|
|
145
154
|
}
|
|
155
|
+
const nameOf2 = (left, right, top, inkLeft) => {
|
|
156
|
+
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];
|
|
157
|
+
const beside = labels.filter((label) => Math.abs(label.top - top) <= 8 && label.right <= inkLeft).sort((a, b) => b.right - a.right)[0];
|
|
158
|
+
return (above ?? beside)?.text.slice(0, 40) ?? "";
|
|
159
|
+
};
|
|
146
160
|
const columns = [];
|
|
147
161
|
let bare = 0;
|
|
148
162
|
let quantities = 0;
|
|
@@ -151,56 +165,229 @@ var __loticsProbe = (() => {
|
|
|
151
165
|
const ordinal = items.length >= 3 && items.every((item, index) => item.text === String(index + 1));
|
|
152
166
|
if (ordinal) continue;
|
|
153
167
|
const top = items[0];
|
|
154
|
-
const above = labels.filter((label) => label.top < top.top && top.top - label.top <= nameReach && label.left < cluster.right && label.right > cluster.left).sort((a, b) => b.top - a.top)[0];
|
|
155
|
-
const beside = labels.filter((label) => Math.abs(label.top - top.top) <= 8 && label.right <= top.left).sort((a, b) => b.right - a.right)[0];
|
|
156
168
|
const scaled = items.filter((item) => counted.test(item.text)).length;
|
|
157
|
-
bare += items.length;
|
|
169
|
+
bare += items.length - scaled;
|
|
158
170
|
quantities += scaled;
|
|
159
171
|
columns.push({
|
|
160
|
-
slot: (
|
|
172
|
+
slot: nameOf2(cluster.left, cluster.right, top.top, top.left),
|
|
161
173
|
size: items.length,
|
|
162
174
|
quantities: scaled,
|
|
163
175
|
samples: items.slice(0, 4).map((item) => item.text)
|
|
164
176
|
});
|
|
165
177
|
}
|
|
166
178
|
columns.sort((a, b) => b.size - a.size);
|
|
167
|
-
const drawn = Array.from(
|
|
179
|
+
const drawn = Array.from(root.querySelectorAll('[role="meter"], [role="progressbar"], canvas')).filter((el) => {
|
|
168
180
|
const r = el.getBoundingClientRect();
|
|
169
181
|
return r.width > 0 && r.height > 0;
|
|
170
182
|
});
|
|
171
183
|
const reads = (el) => {
|
|
172
184
|
const role = el.getAttribute("role");
|
|
173
185
|
if (role !== "meter" && role !== "progressbar") return true;
|
|
174
|
-
const
|
|
186
|
+
const stated2 = (name) => {
|
|
175
187
|
const raw = el.getAttribute(name);
|
|
176
188
|
return raw === null || raw.trim() === "" ? Number.NaN : Number(raw);
|
|
177
189
|
};
|
|
178
190
|
const bound = (name, fallback) => {
|
|
179
|
-
const value =
|
|
191
|
+
const value = stated2(name);
|
|
180
192
|
return Number.isFinite(value) ? value : fallback;
|
|
181
193
|
};
|
|
182
|
-
const now =
|
|
194
|
+
const now = stated2("aria-valuenow");
|
|
183
195
|
if (!Number.isFinite(now)) return false;
|
|
184
196
|
const floor = bound("aria-valuemin", 0);
|
|
185
197
|
const top = bound("aria-valuemax", 100);
|
|
186
198
|
return top > floor && now < top;
|
|
187
199
|
};
|
|
188
|
-
const pictures = Array.from(
|
|
200
|
+
const pictures = Array.from(root.querySelectorAll('[role="img"], svg, img')).filter((el) => {
|
|
189
201
|
if (el.getAttribute("aria-hidden") === "true") return false;
|
|
190
202
|
const r = el.getBoundingClientRect();
|
|
191
203
|
return r.width >= pictureMin && r.height >= pictureMin;
|
|
192
204
|
});
|
|
193
|
-
const
|
|
205
|
+
const markMin = 4;
|
|
206
|
+
const notData = 'a, button, input, nav, [role="banner"], [role="button"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"]';
|
|
207
|
+
const marked = (el) => {
|
|
208
|
+
if (el.closest(notData) !== null) return false;
|
|
209
|
+
const r = el.getBoundingClientRect();
|
|
210
|
+
return r.width >= markMin && r.height >= markMin && (r.width < pictureMin || r.height < pictureMin);
|
|
211
|
+
};
|
|
212
|
+
const glyphs = Array.from(root.querySelectorAll('[role="img"]')).filter(
|
|
213
|
+
(el) => el.getAttribute("aria-hidden") !== "true" && (el.getAttribute("aria-label") || "").trim() !== "" && marked(el)
|
|
214
|
+
);
|
|
215
|
+
const already = [...drawn, ...pictures, ...glyphs];
|
|
216
|
+
const swatches = Array.from(root.querySelectorAll("*")).filter((el) => {
|
|
217
|
+
if ((el.textContent || "").trim() !== "") return false;
|
|
218
|
+
if (already.some((device) => device === el || device.contains(el))) return false;
|
|
219
|
+
if (!marked(el)) return false;
|
|
220
|
+
const ground = getComputedStyle(el).backgroundColor;
|
|
221
|
+
return ground !== "" && ground !== "transparent" && !/,\s*0(?:\.0+)?\)\s*$/.test(ground);
|
|
222
|
+
});
|
|
223
|
+
const tells = (marks, sign) => new Set(marks.map(sign)).size > 1 ? marks.length : 0;
|
|
224
|
+
const categorical = tells(swatches, (el) => getComputedStyle(el).backgroundColor) + tells(glyphs, (el) => el.getAttribute("aria-label") || "");
|
|
225
|
+
const tally = /* @__PURE__ */ new Map();
|
|
226
|
+
const note = (kind, box) => {
|
|
227
|
+
const slot = nameOf2(box.left, box.right, box.top, box.left);
|
|
228
|
+
const key = `${kind}\0${slot}`;
|
|
229
|
+
const seen = tally.get(key);
|
|
230
|
+
if (seen === void 0) tally.set(key, { kind, slot, count: 1 });
|
|
231
|
+
else seen.count += 1;
|
|
232
|
+
};
|
|
233
|
+
const noteElement = (kind, el) => {
|
|
234
|
+
const r = el.getBoundingClientRect();
|
|
235
|
+
note(kind, { left: Math.round(r.left), right: Math.round(r.right), top: Math.round(r.top) });
|
|
236
|
+
};
|
|
237
|
+
for (const el of drawn) noteElement(el.tagName === "CANVAS" ? "chart" : "meter", el);
|
|
238
|
+
for (const el of pictures) noteElement("picture", el);
|
|
239
|
+
for (const el of swatches) noteElement("swatch", el);
|
|
240
|
+
for (const el of glyphs) noteElement("glyph", el);
|
|
241
|
+
for (const leaf of deltaLeaves) note("delta", leaf);
|
|
242
|
+
const deviceKinds = [...tally.values()].sort(
|
|
243
|
+
(a, b) => b.count - a.count || (a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0)
|
|
244
|
+
);
|
|
245
|
+
const slotMax = pictureMin * 4;
|
|
246
|
+
const isSlot = (el) => {
|
|
247
|
+
if ((el.textContent || "").trim() !== "") return false;
|
|
248
|
+
if (el.closest(notData) !== null) return false;
|
|
249
|
+
const r = el.getBoundingClientRect();
|
|
250
|
+
if (r.width < pictureMin || r.height < pictureMin) return false;
|
|
251
|
+
return r.width <= slotMax && r.height <= slotMax;
|
|
252
|
+
};
|
|
253
|
+
const holdsPicture = (el) => {
|
|
254
|
+
if (/^(IMG|CANVAS|VIDEO)$/.test(el.tagName)) return true;
|
|
255
|
+
for (const media of el.querySelectorAll("img, canvas, video")) {
|
|
256
|
+
const r = media.getBoundingClientRect();
|
|
257
|
+
if (r.width > 0 && r.height > 0) return true;
|
|
258
|
+
}
|
|
259
|
+
for (const vector of el.querySelectorAll("svg")) {
|
|
260
|
+
const r = vector.getBoundingClientRect();
|
|
261
|
+
if (r.width >= pictureMin && r.height >= pictureMin) return true;
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
};
|
|
265
|
+
const slotBoxes = Array.from(root.querySelectorAll("*")).filter(
|
|
266
|
+
(el) => isSlot(el) && !Array.from(el.children).some(isSlot)
|
|
267
|
+
);
|
|
268
|
+
const slotClusters = [];
|
|
269
|
+
for (const el of slotBoxes) {
|
|
270
|
+
const r = el.getBoundingClientRect();
|
|
271
|
+
const box = { left: Math.round(r.left), right: Math.round(r.right), top: Math.round(r.top) };
|
|
272
|
+
const cluster = slotClusters.find((c) => box.left < c.right && box.right > c.left);
|
|
273
|
+
if (cluster !== void 0) {
|
|
274
|
+
cluster.items.push(el);
|
|
275
|
+
cluster.left = Math.min(cluster.left, box.left);
|
|
276
|
+
cluster.right = Math.max(cluster.right, box.right);
|
|
277
|
+
} else {
|
|
278
|
+
slotClusters.push({ left: box.left, right: box.right, items: [el] });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const markSlots = slotClusters.filter((cluster) => cluster.items.length >= 3).map((cluster) => {
|
|
282
|
+
const tops = cluster.items.map((el) => Math.round(el.getBoundingClientRect().top));
|
|
283
|
+
return {
|
|
284
|
+
slot: nameOf2(cluster.left, cluster.right, Math.min(...tops), cluster.left),
|
|
285
|
+
size: cluster.items.length,
|
|
286
|
+
filled: cluster.items.filter(holdsPicture).length
|
|
287
|
+
};
|
|
288
|
+
}).sort((a, b) => b.size - a.size);
|
|
289
|
+
const encoded = drawn.filter(reads).length + pictures.length + deltaLeaves.length + categorical;
|
|
194
290
|
return {
|
|
195
|
-
total: bare + encoded,
|
|
291
|
+
total: bare + quantities + encoded,
|
|
196
292
|
bare,
|
|
197
293
|
quantities,
|
|
198
294
|
encoded,
|
|
199
|
-
devices: drawn.length + pictures.length +
|
|
200
|
-
|
|
295
|
+
devices: drawn.length + pictures.length + deltaLeaves.length + swatches.length + glyphs.length,
|
|
296
|
+
deviceKinds,
|
|
297
|
+
columns,
|
|
298
|
+
markSlots
|
|
201
299
|
};
|
|
202
300
|
}
|
|
203
|
-
function
|
|
301
|
+
function stageWordColumns(root = document.body) {
|
|
302
|
+
const tabs = [];
|
|
303
|
+
for (const tab of Array.from(root.querySelectorAll('[role="tablist"] [role="tab"]'))) {
|
|
304
|
+
const label = (tab.textContent || "").trim();
|
|
305
|
+
if (label !== "" && !tabs.includes(label)) tabs.push(label);
|
|
306
|
+
}
|
|
307
|
+
if (tabs.length < 2) return { total: 0, wordsOnly: [] };
|
|
308
|
+
const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
|
|
309
|
+
const notContent = 'nav, [role="banner"], [role="tab"], [role="tablist"]';
|
|
310
|
+
const markMin = 4;
|
|
311
|
+
const pictureMin = 32;
|
|
312
|
+
const nameReach = 360;
|
|
313
|
+
const unpainted = (ground) => ground === "" || ground === "transparent" || /,\s*0(?:\.0+)?\)\s*$/.test(ground);
|
|
314
|
+
const leaves = [];
|
|
315
|
+
const labels = [];
|
|
316
|
+
const walk = (node) => {
|
|
317
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
318
|
+
const text = (node.textContent || "").trim();
|
|
319
|
+
const parent = node.parentElement;
|
|
320
|
+
if (!text || !parent || skipTag.test(parent.tagName)) return;
|
|
321
|
+
if (typeof parent.checkVisibility === "function" && !parent.checkVisibility()) return;
|
|
322
|
+
const rect = parent.getBoundingClientRect();
|
|
323
|
+
if (rect.width === 0 && rect.height === 0) return;
|
|
324
|
+
const box = {
|
|
325
|
+
text,
|
|
326
|
+
left: Math.round(rect.left),
|
|
327
|
+
right: Math.round(rect.right),
|
|
328
|
+
top: Math.round(rect.top)
|
|
329
|
+
};
|
|
330
|
+
labels.push(box);
|
|
331
|
+
if (tabs.includes(text) && !parent.closest(notContent)) leaves.push({ el: parent, ...box });
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
node.childNodes.forEach(walk);
|
|
335
|
+
};
|
|
336
|
+
walk(root);
|
|
337
|
+
const clusters = [];
|
|
338
|
+
for (const leaf of [...leaves].sort((a, b) => a.left - b.left || a.top - b.top)) {
|
|
339
|
+
const cluster = clusters.find((c) => leaf.left < c.right && leaf.right > c.left);
|
|
340
|
+
if (cluster !== void 0) {
|
|
341
|
+
cluster.items.push(leaf);
|
|
342
|
+
cluster.left = Math.min(cluster.left, leaf.left);
|
|
343
|
+
cluster.right = Math.max(cluster.right, leaf.right);
|
|
344
|
+
} else {
|
|
345
|
+
clusters.push({ left: leaf.left, right: leaf.right, items: [leaf] });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const nameOf2 = (left, right, top) => {
|
|
349
|
+
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];
|
|
350
|
+
const beside = labels.filter((label) => Math.abs(label.top - top) <= 8 && label.right <= left).sort((a, b) => b.right - a.right)[0];
|
|
351
|
+
return (above ?? beside)?.text.slice(0, 40) ?? "";
|
|
352
|
+
};
|
|
353
|
+
const cellOf = (leaf, left, right) => {
|
|
354
|
+
let cell = leaf.el;
|
|
355
|
+
let node = leaf.el.parentElement;
|
|
356
|
+
while (node !== null) {
|
|
357
|
+
const r = node.getBoundingClientRect();
|
|
358
|
+
if (Math.round(r.left) < left - pictureMin || Math.round(r.right) > right + pictureMin) break;
|
|
359
|
+
cell = node;
|
|
360
|
+
node = node.parentElement;
|
|
361
|
+
}
|
|
362
|
+
return cell;
|
|
363
|
+
};
|
|
364
|
+
const holdsMark = (cell, word) => Array.from(cell.querySelectorAll("*")).some((el) => {
|
|
365
|
+
if (el === word || el.contains(word)) return false;
|
|
366
|
+
const r = el.getBoundingClientRect();
|
|
367
|
+
if (r.width < markMin || r.height < markMin) return false;
|
|
368
|
+
if (el.getAttribute("aria-hidden") === "true") return false;
|
|
369
|
+
if (el.getAttribute("role") === "img") return (el.getAttribute("aria-label") || "").trim() !== "";
|
|
370
|
+
if (r.width >= pictureMin && r.height >= pictureMin) return false;
|
|
371
|
+
if ((el.textContent || "").trim() !== "") return false;
|
|
372
|
+
return !unpainted(getComputedStyle(el).backgroundColor);
|
|
373
|
+
});
|
|
374
|
+
const columns = [];
|
|
375
|
+
const wordsOnly = [];
|
|
376
|
+
for (const cluster of clusters) {
|
|
377
|
+
if (cluster.items.length < 3) continue;
|
|
378
|
+
const cells = cluster.items.map((leaf) => ({ leaf, cell: cellOf(leaf, cluster.left, cluster.right) }));
|
|
379
|
+
const column = {
|
|
380
|
+
slot: nameOf2(cluster.left, cluster.right, Math.min(...cluster.items.map((item) => item.top))),
|
|
381
|
+
size: cluster.items.length,
|
|
382
|
+
values: tabs.filter((tab) => cluster.items.some((item) => item.text === tab))
|
|
383
|
+
};
|
|
384
|
+
columns.push(column);
|
|
385
|
+
const marked = cells.some(({ cell, leaf }) => holdsMark(cell, leaf.el)) || new Set(cells.map(({ cell }) => getComputedStyle(cell).backgroundColor)).size > 1;
|
|
386
|
+
if (!marked) wordsOnly.push(column);
|
|
387
|
+
}
|
|
388
|
+
return { total: columns.length, wordsOnly };
|
|
389
|
+
}
|
|
390
|
+
function internalIds(root = document.body) {
|
|
204
391
|
const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
|
|
205
392
|
const id = /\b(?:opt|fld|tbl|rec|wfl|mbr|app|wsp)_[A-Za-z0-9]{6,}\b/g;
|
|
206
393
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -217,11 +404,11 @@ var __loticsProbe = (() => {
|
|
|
217
404
|
}
|
|
218
405
|
node.childNodes.forEach(walk);
|
|
219
406
|
};
|
|
220
|
-
walk(
|
|
407
|
+
walk(root);
|
|
221
408
|
return { total, ids: [...ids] };
|
|
222
409
|
}
|
|
223
|
-
function tabStrips() {
|
|
224
|
-
const all = Array.from(
|
|
410
|
+
function tabStrips(root = document.body) {
|
|
411
|
+
const all = Array.from(root.querySelectorAll('[role="tablist"]')).filter((el) => {
|
|
225
412
|
const r = el.getBoundingClientRect();
|
|
226
413
|
return r.width > 0 && r.height > 0;
|
|
227
414
|
});
|
|
@@ -232,13 +419,13 @@ var __loticsProbe = (() => {
|
|
|
232
419
|
strips: counted.map((el) => Array.from(el.querySelectorAll('[role="tab"]')).map((tab) => (tab.textContent || "").trim()))
|
|
233
420
|
};
|
|
234
421
|
}
|
|
235
|
-
function truncatedLeaves() {
|
|
422
|
+
function truncatedLeaves(root = document.body) {
|
|
236
423
|
const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
|
|
237
|
-
const
|
|
424
|
+
const clippedAway2 = /^inset\(\s*50%/;
|
|
238
425
|
const breakable = /[\t\n\v\f\r \u200b\u3000]/;
|
|
239
426
|
const cut = [];
|
|
240
427
|
let total = 0;
|
|
241
|
-
for (const el of Array.from(
|
|
428
|
+
for (const el of Array.from(root.querySelectorAll("*"))) {
|
|
242
429
|
if (skipTag.test(el.tagName) || el.children.length > 0) continue;
|
|
243
430
|
const text = (el.textContent || "").trim();
|
|
244
431
|
if (!text) continue;
|
|
@@ -246,7 +433,7 @@ var __loticsProbe = (() => {
|
|
|
246
433
|
const style = getComputedStyle(el);
|
|
247
434
|
const box = el.getBoundingClientRect();
|
|
248
435
|
if (box.width <= 1 && box.height <= 1) continue;
|
|
249
|
-
if (
|
|
436
|
+
if (clippedAway2.test(style.getPropertyValue("clip-path"))) continue;
|
|
250
437
|
total += 1;
|
|
251
438
|
if (style.overflow === "visible") continue;
|
|
252
439
|
const states = (property) => {
|
|
@@ -262,7 +449,7 @@ var __loticsProbe = (() => {
|
|
|
262
449
|
}
|
|
263
450
|
return { total, cut };
|
|
264
451
|
}
|
|
265
|
-
function bannedGlyphs() {
|
|
452
|
+
function bannedGlyphs(root = document.body) {
|
|
266
453
|
const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
|
|
267
454
|
const found = [];
|
|
268
455
|
let total = 0;
|
|
@@ -278,10 +465,10 @@ var __loticsProbe = (() => {
|
|
|
278
465
|
}
|
|
279
466
|
node.childNodes.forEach(walk);
|
|
280
467
|
};
|
|
281
|
-
walk(
|
|
468
|
+
walk(root);
|
|
282
469
|
return { total, found };
|
|
283
470
|
}
|
|
284
|
-
function compactMoneyWords() {
|
|
471
|
+
function compactMoneyWords(root = document.body) {
|
|
285
472
|
const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
|
|
286
473
|
const any = /\d[\d.,]*\s?(?:[KMB](?![a-z])|tr(?![a-z])|tỷ)/i;
|
|
287
474
|
const dongInEnglishWords = /\d[\d.,]*\s?[KMB]\s?(?:₫|đ|VND)|(?:₫|VND)\s?\d[\d.,]*\s?[KMB](?![a-z])/i;
|
|
@@ -300,11 +487,11 @@ var __loticsProbe = (() => {
|
|
|
300
487
|
}
|
|
301
488
|
node.childNodes.forEach(walk);
|
|
302
489
|
};
|
|
303
|
-
walk(
|
|
490
|
+
walk(root);
|
|
304
491
|
return { total, foreign };
|
|
305
492
|
}
|
|
306
|
-
function scanStackedPairs() {
|
|
307
|
-
const
|
|
493
|
+
function scanStackedPairs(root = document.body) {
|
|
494
|
+
const px2 = (el) => parseFloat(getComputedStyle(el).fontSize);
|
|
308
495
|
const weight = (el) => parseInt(getComputedStyle(el).fontWeight, 10) || 400;
|
|
309
496
|
const isLeaf = (el) => el.children.length === 0 && (el.textContent || "").trim().length > 0;
|
|
310
497
|
const leafOf = (el, depth = 3) => {
|
|
@@ -313,9 +500,16 @@ var __loticsProbe = (() => {
|
|
|
313
500
|
return leafOf(el.children[0], depth - 1);
|
|
314
501
|
};
|
|
315
502
|
const sameColumn = 8;
|
|
503
|
+
const heading = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
|
|
504
|
+
const collection = 'ul, ol, dl, table, [role="list"], [role="grid"], [role="table"]';
|
|
505
|
+
const pairShaped = (box) => box.children.length === 2 && leafOf(box.children[0]) !== null && leafOf(box.children[1]) !== null;
|
|
506
|
+
const names = (line, box) => {
|
|
507
|
+
const id = line.getAttribute("id") || "";
|
|
508
|
+
return id !== "" && (box.getAttribute("aria-labelledby") || "").split(/\s+/).includes(id);
|
|
509
|
+
};
|
|
316
510
|
let total = 0;
|
|
317
511
|
const overDemoted = [];
|
|
318
|
-
for (const el of Array.from(
|
|
512
|
+
for (const el of Array.from(root.querySelectorAll("div"))) {
|
|
319
513
|
const kids = Array.from(el.children);
|
|
320
514
|
if (kids.length !== 2) continue;
|
|
321
515
|
const [first, second] = kids;
|
|
@@ -328,19 +522,22 @@ var __loticsProbe = (() => {
|
|
|
328
522
|
if (b.top < a.bottom - 2) continue;
|
|
329
523
|
if (Math.abs(b.left - a.left) > sameColumn) continue;
|
|
330
524
|
if (a.height === 0 || b.height === 0) continue;
|
|
525
|
+
if (first.matches(heading) || first.querySelector(heading) !== null) continue;
|
|
526
|
+
if ([first, subject].some((line) => [el, second].some((box) => names(line, box)))) continue;
|
|
527
|
+
if (second.matches(collection) || Array.from(second.children).some(pairShaped)) continue;
|
|
331
528
|
total += 1;
|
|
332
|
-
if (
|
|
529
|
+
if (px2(supporting) < px2(subject) && weight(subject) > weight(supporting)) {
|
|
333
530
|
overDemoted.push({
|
|
334
531
|
subject: (subject.textContent || "").trim().slice(0, 60),
|
|
335
532
|
supporting: (supporting.textContent || "").trim().slice(0, 60),
|
|
336
|
-
sizes: [
|
|
533
|
+
sizes: [px2(subject), px2(supporting)],
|
|
337
534
|
weights: [weight(subject), weight(supporting)]
|
|
338
535
|
});
|
|
339
536
|
}
|
|
340
537
|
}
|
|
341
538
|
return { total, overDemoted };
|
|
342
539
|
}
|
|
343
|
-
function scanRowMarks() {
|
|
540
|
+
function scanRowMarks(root = document.body) {
|
|
344
541
|
const marks = [];
|
|
345
542
|
const seen = [];
|
|
346
543
|
const record = (el, text) => {
|
|
@@ -350,7 +547,7 @@ var __loticsProbe = (() => {
|
|
|
350
547
|
marks.push({ text, w: Math.round(r.width), y: Math.round(r.y), x: Math.round(r.x) });
|
|
351
548
|
};
|
|
352
549
|
const inContent = (el) => el.closest('[role="banner"]') === null;
|
|
353
|
-
for (const el of Array.from(
|
|
550
|
+
for (const el of Array.from(root.querySelectorAll('[data-mark="identity"]'))) {
|
|
354
551
|
const r = el.getBoundingClientRect();
|
|
355
552
|
if (r.height === 0 || !inContent(el)) continue;
|
|
356
553
|
record(el, (el.textContent || "").trim() || "\u25A3");
|
|
@@ -362,7 +559,7 @@ var __loticsProbe = (() => {
|
|
|
362
559
|
if (depth === 0) return false;
|
|
363
560
|
return Array.from(el.children).some((child) => grounded(child, depth - 1));
|
|
364
561
|
};
|
|
365
|
-
for (const el of Array.from(
|
|
562
|
+
for (const el of Array.from(root.querySelectorAll("*"))) {
|
|
366
563
|
const r = el.getBoundingClientRect();
|
|
367
564
|
if (r.width < 12 || r.width > 56) continue;
|
|
368
565
|
if (Math.abs(r.width - r.height) > 1) continue;
|
|
@@ -390,63 +587,1079 @@ var __loticsProbe = (() => {
|
|
|
390
587
|
return { total: marks.length, mismatches };
|
|
391
588
|
}
|
|
392
589
|
|
|
590
|
+
// src/probe_collectors/naming.ts
|
|
591
|
+
var HOLDS_A_VALUE = 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [role="textbox"], [role="searchbox"], [role="combobox"], [role="listbox"], [role="spinbutton"], [role="slider"]';
|
|
592
|
+
var HEADING = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
|
|
593
|
+
function words(el) {
|
|
594
|
+
return el === null ? "" : (el.textContent || "").replace(/\s+/g, " ").trim();
|
|
595
|
+
}
|
|
596
|
+
function pointedAt(el, attribute) {
|
|
597
|
+
return (el.getAttribute(attribute) || "").split(/\s+/).filter((id) => id !== "").map((id) => el.ownerDocument.getElementById(id)).filter((target) => target !== null);
|
|
598
|
+
}
|
|
599
|
+
function stated(el) {
|
|
600
|
+
const pointed = pointedAt(el, "aria-labelledby").map(words).filter((name) => name !== "").join(" ");
|
|
601
|
+
if (pointed !== "") return pointed;
|
|
602
|
+
return (el.getAttribute("aria-label") || "").trim();
|
|
603
|
+
}
|
|
604
|
+
function labelling(el) {
|
|
605
|
+
const id = el.getAttribute("id") || "";
|
|
606
|
+
const explicit = id === "" ? void 0 : Array.from(el.ownerDocument.querySelectorAll("label[for]")).find((label2) => label2.getAttribute("for") === id);
|
|
607
|
+
const label = explicit ?? el.closest("label");
|
|
608
|
+
if (label === null || label === void 0) return "";
|
|
609
|
+
const said = [];
|
|
610
|
+
const walk = (node) => {
|
|
611
|
+
if (node === el) return;
|
|
612
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
613
|
+
said.push(node.textContent || "");
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
node.childNodes.forEach(walk);
|
|
617
|
+
};
|
|
618
|
+
walk(label);
|
|
619
|
+
return said.join(" ").replace(/\s+/g, " ").trim();
|
|
620
|
+
}
|
|
621
|
+
function regionName(region) {
|
|
622
|
+
const said = stated(region);
|
|
623
|
+
return said !== "" ? said : words(region.querySelector(HEADING));
|
|
624
|
+
}
|
|
625
|
+
function nameOf(el) {
|
|
626
|
+
const said = stated(el);
|
|
627
|
+
if (said !== "") return said;
|
|
628
|
+
if (el.tagName === "DETAILS") {
|
|
629
|
+
const summary = words(el.querySelector("summary"));
|
|
630
|
+
if (summary !== "") return summary;
|
|
631
|
+
}
|
|
632
|
+
const labelled = labelling(el);
|
|
633
|
+
if (labelled !== "") return labelled;
|
|
634
|
+
if (!el.matches(HOLDS_A_VALUE)) {
|
|
635
|
+
const own = words(el);
|
|
636
|
+
if (own !== "") return own;
|
|
637
|
+
}
|
|
638
|
+
const tooltip = (el.getAttribute("title") || "").trim();
|
|
639
|
+
if (tooltip !== "") return tooltip;
|
|
640
|
+
for (const region of pointedAt(el, "aria-controls")) {
|
|
641
|
+
const named = regionName(region);
|
|
642
|
+
if (named !== "") return named;
|
|
643
|
+
}
|
|
644
|
+
return "";
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/probe_collectors/on_screen.ts
|
|
648
|
+
var CLIP_RECT = /^rect\(\s*(-?[\d.]+)px[,\s]+(-?[\d.]+)px[,\s]+(-?[\d.]+)px[,\s]+(-?[\d.]+)px\s*\)$/;
|
|
649
|
+
var INSET = /^inset\(([^)]*?)(?:\s+round\s+[^)]*)?\)$/;
|
|
650
|
+
var inset = (value, extent) => value.endsWith("%") ? parseFloat(value) / 100 * extent : parseFloat(value);
|
|
651
|
+
var meets = (near, far, extent) => extent > 0 && inset(near, extent) + inset(far, extent) >= extent;
|
|
652
|
+
function clippedAway(el) {
|
|
653
|
+
const style = getComputedStyle(el);
|
|
654
|
+
const box = el.getBoundingClientRect();
|
|
655
|
+
const rect = CLIP_RECT.exec(style.clip);
|
|
656
|
+
if (rect !== null && (Number(rect[2]) - Number(rect[4]) <= 0 || Number(rect[3]) - Number(rect[1]) <= 0)) return true;
|
|
657
|
+
const path = INSET.exec(style.clipPath);
|
|
658
|
+
if (path === null || path[1].trim() === "") return false;
|
|
659
|
+
const [top, right = top, bottom = top, left = right] = path[1].trim().split(/\s+/);
|
|
660
|
+
return meets(left, right, box.width) || meets(top, bottom, box.height);
|
|
661
|
+
}
|
|
662
|
+
function onScreen(el) {
|
|
663
|
+
if (el.closest("[inert]") !== null) return false;
|
|
664
|
+
if (typeof el.checkVisibility === "function" && !el.checkVisibility()) return false;
|
|
665
|
+
const shown = getComputedStyle(el).visibility;
|
|
666
|
+
if (shown === "hidden" || shown === "collapse") return false;
|
|
667
|
+
for (let up = el; up !== null; up = up.parentElement) {
|
|
668
|
+
if (clippedAway(up)) return false;
|
|
669
|
+
}
|
|
670
|
+
const box = el.getBoundingClientRect();
|
|
671
|
+
return box.width > 0 && box.height > 0;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/probe_collectors/scope.ts
|
|
675
|
+
var OVERLAY = '[aria-modal="true"], dialog[open], [role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"]';
|
|
676
|
+
var AWAY = '[aria-hidden="true"], [inert]';
|
|
677
|
+
function measuredSurface() {
|
|
678
|
+
const away = Array.from(document.querySelectorAll(AWAY));
|
|
679
|
+
const open = Array.from(document.querySelectorAll(OVERLAY)).filter((el) => {
|
|
680
|
+
if (!onScreen(el)) return false;
|
|
681
|
+
if (away.some((hidden) => hidden.contains(el))) return false;
|
|
682
|
+
if (el.matches('[aria-modal="true"], dialog[open]')) return true;
|
|
683
|
+
return away.some((hidden) => hidden.parentElement === document.body && !hidden.contains(el));
|
|
684
|
+
});
|
|
685
|
+
const inner = open.filter((el) => !open.some((other) => other !== el && el.contains(other)));
|
|
686
|
+
return inner[inner.length - 1] ?? document.body;
|
|
687
|
+
}
|
|
688
|
+
function within(root, selector) {
|
|
689
|
+
const inside = Array.from(root.querySelectorAll(selector));
|
|
690
|
+
return root.matches(selector) ? [root, ...inside] : inside;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/probe_collectors/clutter.ts
|
|
694
|
+
var overlaySurface = '[role="dialog"], [role="alertdialog"], dialog[open]';
|
|
695
|
+
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"]';
|
|
696
|
+
function clutterCensus(root = document.body) {
|
|
697
|
+
const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
|
|
698
|
+
const chrome = 'nav, [role="banner"]';
|
|
699
|
+
const recordSurface = '.lotics-record-page, [role="dialog"]';
|
|
700
|
+
const noteFloor = 3;
|
|
701
|
+
const formFloor = 3;
|
|
702
|
+
const hueStep = 30;
|
|
703
|
+
const chromaFloor = 0.15;
|
|
704
|
+
const sameColour = 24;
|
|
705
|
+
const sameEdge = 8;
|
|
706
|
+
const words3 = (el) => el === null ? "" : (el.textContent || "").trim().slice(0, 40);
|
|
707
|
+
const surfaces = within(root, recordSurface).filter(onScreen);
|
|
708
|
+
const called2 = (el) => el.matches(recordSurface) ? words3(el.querySelector(".lotics-record-page__heading, h1, h2")) || "the record" : "the screen";
|
|
709
|
+
const rules = [];
|
|
710
|
+
const primary = '[data-variant="primary"]';
|
|
711
|
+
const acts = within(root, primary).filter(
|
|
712
|
+
(el) => onScreen(el) && el.closest(chrome) === null && (el.parentElement === null || el.parentElement.closest(primary) === null)
|
|
713
|
+
);
|
|
714
|
+
const page = within(root, '[role="main"], main')[0] ?? root;
|
|
715
|
+
const held = /* @__PURE__ */ new Map();
|
|
716
|
+
for (const act of acts) {
|
|
717
|
+
const owner = act.closest(recordSurface) ?? page;
|
|
718
|
+
const run2 = held.get(owner);
|
|
719
|
+
if (run2 === void 0) held.set(owner, [act]);
|
|
720
|
+
else run2.push(act);
|
|
721
|
+
}
|
|
722
|
+
let surplusActs = 0;
|
|
723
|
+
const loud = [];
|
|
724
|
+
for (const [owner, run2] of held) {
|
|
725
|
+
if (run2.length <= 1) continue;
|
|
726
|
+
surplusActs += run2.length - 1;
|
|
727
|
+
if (loud.length < 3) loud.push(`${called2(owner)}: ${run2.length} primary acts \u2014 ${run2.map((act) => `"${words3(act)}"`).join(", ")}`);
|
|
728
|
+
}
|
|
729
|
+
rules.push({
|
|
730
|
+
law: "hierarchy.md",
|
|
731
|
+
rule: "One primary act per surface",
|
|
732
|
+
total: acts.length,
|
|
733
|
+
count: surplusActs,
|
|
734
|
+
offenders: loud
|
|
735
|
+
});
|
|
736
|
+
const money = /^[-+−]?\s?(?:(?:[$€]|USD|EUR|VND)\s?[\d.,]+\s?(?:[KMB]|tr|tỷ)?|[\d.,]+\s?(?:tr|tỷ)?\s?(?:₫|đ|VND|USD|EUR|[$€]))$/i;
|
|
737
|
+
const dated = /^\d{1,4}[/.-]\d{1,2}(?:[/.-]\d{1,4})?$/;
|
|
738
|
+
const coded = /^(?=[^0-9]*[0-9])(?=[^A-Z]*[A-Z])[A-Z0-9][A-Z0-9/_.-]{4,}$/;
|
|
739
|
+
const places = /* @__PURE__ */ new Map();
|
|
740
|
+
const shown = /* @__PURE__ */ new Map();
|
|
741
|
+
for (let index = 0; index < surfaces.length; index++) {
|
|
742
|
+
const walk = (node) => {
|
|
743
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
744
|
+
const text = (node.textContent || "").trim();
|
|
745
|
+
const parent = node.parentElement;
|
|
746
|
+
if (text === "" || parent === null || skipTag.test(parent.tagName)) return;
|
|
747
|
+
if (!onScreen(parent)) return;
|
|
748
|
+
if (!money.test(text) && !dated.test(text) && !coded.test(text)) return;
|
|
749
|
+
const key = `${index}\0${text}`;
|
|
750
|
+
shown.set(key, text);
|
|
751
|
+
const edges = places.get(key);
|
|
752
|
+
const left = Math.round(parent.getBoundingClientRect().left);
|
|
753
|
+
if (edges === void 0) places.set(key, [left]);
|
|
754
|
+
else edges.push(left);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
node.childNodes.forEach(walk);
|
|
758
|
+
};
|
|
759
|
+
walk(surfaces[index]);
|
|
760
|
+
}
|
|
761
|
+
let twice = 0;
|
|
762
|
+
const repeated = [];
|
|
763
|
+
for (const [key, edges] of places) {
|
|
764
|
+
const columns = [];
|
|
765
|
+
for (const left of edges) if (!columns.some((edge) => Math.abs(edge - left) <= sameEdge)) columns.push(left);
|
|
766
|
+
if (columns.length < 2) continue;
|
|
767
|
+
twice += 1;
|
|
768
|
+
if (repeated.length < 3) repeated.push(`"${shown.get(key)}" drawn in ${columns.length} places`);
|
|
769
|
+
}
|
|
770
|
+
rules.push({
|
|
771
|
+
law: "hierarchy.md",
|
|
772
|
+
rule: "One fact, one place",
|
|
773
|
+
total: places.size,
|
|
774
|
+
count: twice,
|
|
775
|
+
offenders: repeated
|
|
776
|
+
});
|
|
777
|
+
const absent = /^[—–-]$/;
|
|
778
|
+
let facts = 0;
|
|
779
|
+
let standing = 0;
|
|
780
|
+
const open = [];
|
|
781
|
+
for (const surface of surfaces) {
|
|
782
|
+
if (surface.querySelector(".lotics-record-facts__more") === null) continue;
|
|
783
|
+
for (const cell of Array.from(surface.querySelectorAll(".lotics-record-facts__fact"))) {
|
|
784
|
+
const value = cell.querySelector(".lotics-record-facts__value");
|
|
785
|
+
if (value === null || !onScreen(cell)) continue;
|
|
786
|
+
facts += 1;
|
|
787
|
+
if (!absent.test((value.textContent || "").trim())) continue;
|
|
788
|
+
standing += 1;
|
|
789
|
+
if (open.length < 3) open.push(`"${words3(cell.querySelector(".lotics-record-facts__label")) || words3(cell)}" is unstated and drawn`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
rules.push({
|
|
793
|
+
law: "hierarchy.md",
|
|
794
|
+
rule: "How a record derives them",
|
|
795
|
+
total: facts,
|
|
796
|
+
count: standing,
|
|
797
|
+
offenders: open
|
|
798
|
+
});
|
|
799
|
+
let boxes = 0;
|
|
800
|
+
let overReserved = 0;
|
|
801
|
+
const reserved = [];
|
|
802
|
+
for (const box of within(root, 'textarea, [role="textbox"], [contenteditable="true"]')) {
|
|
803
|
+
if (!onScreen(box) || box.closest(chrome) !== null) continue;
|
|
804
|
+
const style = getComputedStyle(box);
|
|
805
|
+
const line = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.5;
|
|
806
|
+
if (!Number.isFinite(line) || line <= 0) continue;
|
|
807
|
+
const pad = (parseFloat(style.paddingTop) || 0) + (parseFloat(style.paddingBottom) || 0);
|
|
808
|
+
boxes += 1;
|
|
809
|
+
const lines = Math.round((box.getBoundingClientRect().height - pad) / line);
|
|
810
|
+
const written = Math.round((box.scrollHeight - pad) / line);
|
|
811
|
+
if (lines <= noteFloor || written > 1) continue;
|
|
812
|
+
overReserved += 1;
|
|
813
|
+
if (reserved.length < 3) {
|
|
814
|
+
const named = nameOf(box).slice(0, 40);
|
|
815
|
+
reserved.push(`"${named === "" ? "(unnamed)" : named}": ${lines} lines reserved for 1`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
for (const surface of within(root, overlaySurface).filter(onScreen)) {
|
|
819
|
+
const box = surface.getBoundingClientRect();
|
|
820
|
+
if (box.height <= 0) continue;
|
|
821
|
+
boxes += 1;
|
|
822
|
+
const bands = [];
|
|
823
|
+
const ink = (el) => {
|
|
824
|
+
if (!onScreen(el)) return;
|
|
825
|
+
const rect = el.getBoundingClientRect();
|
|
826
|
+
const top = Math.max(rect.top, box.top);
|
|
827
|
+
const bottom = Math.min(rect.bottom, box.bottom);
|
|
828
|
+
if (bottom > top) bands.push([top, bottom]);
|
|
829
|
+
};
|
|
830
|
+
const walkInk = (node) => {
|
|
831
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
832
|
+
const parent = node.parentElement;
|
|
833
|
+
if (parent !== null && (node.textContent || "").trim() !== "" && !skipTag.test(parent.tagName)) ink(parent);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
node.childNodes.forEach(walkInk);
|
|
837
|
+
};
|
|
838
|
+
walkInk(surface);
|
|
839
|
+
for (const drawn of Array.from(surface.querySelectorAll(DRAWS))) ink(drawn);
|
|
840
|
+
bands.sort((a, b) => a[0] - b[0]);
|
|
841
|
+
let filled = 0;
|
|
842
|
+
let empty = 0;
|
|
843
|
+
let edge = null;
|
|
844
|
+
for (const [top, bottom] of bands) {
|
|
845
|
+
if (edge !== null && top <= edge) {
|
|
846
|
+
if (bottom > edge) filled += bottom - edge;
|
|
847
|
+
} else {
|
|
848
|
+
if (edge !== null) empty = Math.max(empty, top - edge);
|
|
849
|
+
filled += bottom - top;
|
|
850
|
+
}
|
|
851
|
+
edge = edge === null ? bottom : Math.max(edge, bottom);
|
|
852
|
+
}
|
|
853
|
+
if (empty <= filled) continue;
|
|
854
|
+
overReserved += 1;
|
|
855
|
+
if (reserved.length < 3) {
|
|
856
|
+
reserved.push(`${called2(surface)}: ${Math.round(empty)}px of nothing, ${Math.round(filled)}px drawn`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
rules.push({
|
|
860
|
+
law: "hierarchy.md",
|
|
861
|
+
rule: "Whitespace is structure",
|
|
862
|
+
total: boxes,
|
|
863
|
+
count: overReserved,
|
|
864
|
+
offenders: reserved
|
|
865
|
+
});
|
|
866
|
+
const fields = within(root, ".lotics-form-field").filter(onScreen);
|
|
867
|
+
const forms = /* @__PURE__ */ new Map();
|
|
868
|
+
for (const field of fields) {
|
|
869
|
+
const owner = field.closest('form, [role="form"]') ?? field.parentElement;
|
|
870
|
+
if (owner === null) continue;
|
|
871
|
+
const run2 = forms.get(owner);
|
|
872
|
+
if (run2 === void 0) forms.set(owner, [field]);
|
|
873
|
+
else run2.push(field);
|
|
874
|
+
}
|
|
875
|
+
let inForms = 0;
|
|
876
|
+
let overMarked = 0;
|
|
877
|
+
const marks = [];
|
|
878
|
+
for (const run2 of forms.values()) {
|
|
879
|
+
if (run2.length < formFloor) continue;
|
|
880
|
+
inForms += run2.length;
|
|
881
|
+
const carried = run2.filter((field) => field.querySelector(".lotics-form-field__mark") !== null);
|
|
882
|
+
if (carried.length * 2 <= run2.length) continue;
|
|
883
|
+
overMarked += carried.length;
|
|
884
|
+
if (marks.length < 3) {
|
|
885
|
+
marks.push(
|
|
886
|
+
`${carried.length} of ${run2.length} fields marked \u2014 ` + carried.slice(0, 3).map((field) => `"${words3(field.querySelector(".lotics-form-field__label")) || words3(field)}"`).join(", ")
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
rules.push({
|
|
891
|
+
law: "hierarchy.md",
|
|
892
|
+
rule: "Markers on the minority side",
|
|
893
|
+
total: inForms,
|
|
894
|
+
count: overMarked,
|
|
895
|
+
offenders: marks
|
|
896
|
+
});
|
|
897
|
+
const theme = getComputedStyle(document.documentElement);
|
|
898
|
+
const rgbOf = (value) => {
|
|
899
|
+
const channels = value.match(/^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/i);
|
|
900
|
+
if (channels !== null) return [Number(channels[1]), Number(channels[2]), Number(channels[3])];
|
|
901
|
+
const hex = value.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
902
|
+
if (hex === null) return null;
|
|
903
|
+
const digits = hex[1];
|
|
904
|
+
const pair = (at) => parseInt(digits.length === 3 ? digits[at].repeat(2) : digits.slice(at * 2, at * 2 + 2), 16);
|
|
905
|
+
return [pair(0), pair(1), pair(2)];
|
|
906
|
+
};
|
|
907
|
+
const valence = [];
|
|
908
|
+
for (const token of ["--lotics-ink-danger", "--lotics-ink-warning", "--lotics-ink-success", "--lotics-ink-inactive"]) {
|
|
909
|
+
const ink = rgbOf(theme.getPropertyValue(token));
|
|
910
|
+
if (ink !== null) valence.push(ink);
|
|
911
|
+
}
|
|
912
|
+
const hues = /* @__PURE__ */ new Map();
|
|
913
|
+
let chromatic = 0;
|
|
914
|
+
if (valence.length > 0) {
|
|
915
|
+
const stateInk = '[role="status"], [role="alert"], [aria-invalid="true"], [aria-disabled="true"]';
|
|
916
|
+
const walk = (node) => {
|
|
917
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
918
|
+
const text = (node.textContent || "").trim();
|
|
919
|
+
const parent = node.parentElement;
|
|
920
|
+
if (text === "" || parent === null || skipTag.test(parent.tagName)) return;
|
|
921
|
+
if (!onScreen(parent) || parent.closest(chrome) !== null || parent.closest(stateInk) !== null) return;
|
|
922
|
+
const ink = rgbOf(getComputedStyle(parent).color);
|
|
923
|
+
if (ink === null) return;
|
|
924
|
+
if (valence.some((tone) => Math.abs(tone[0] - ink[0]) + Math.abs(tone[1] - ink[1]) + Math.abs(tone[2] - ink[2]) <= sameColour)) return;
|
|
925
|
+
const red = ink[0] / 255;
|
|
926
|
+
const green = ink[1] / 255;
|
|
927
|
+
const blue = ink[2] / 255;
|
|
928
|
+
const top = Math.max(red, green, blue);
|
|
929
|
+
const bottom = Math.min(red, green, blue);
|
|
930
|
+
const light = (top + bottom) / 2;
|
|
931
|
+
const chroma = top - bottom;
|
|
932
|
+
const saturation = chroma === 0 ? 0 : chroma / (1 - Math.abs(2 * light - 1));
|
|
933
|
+
if (saturation < chromaFloor || light < 0.12 || light > 0.92) return;
|
|
934
|
+
const angle = top === red ? (green - blue) / chroma % 6 * 60 : top === green ? ((blue - red) / chroma + 2) * 60 : ((red - green) / chroma + 4) * 60;
|
|
935
|
+
const bucket = Math.round((angle + 360) % 360 / hueStep) * hueStep % 360;
|
|
936
|
+
chromatic += 1;
|
|
937
|
+
const tally = hues.get(bucket);
|
|
938
|
+
if (tally === void 0) hues.set(bucket, { count: 1, sample: text.slice(0, 40) });
|
|
939
|
+
else tally.count += 1;
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
node.childNodes.forEach(walk);
|
|
943
|
+
};
|
|
944
|
+
walk(root);
|
|
945
|
+
}
|
|
946
|
+
const ranked = [...hues.entries()].sort((a, b) => b[1].count - a[1].count);
|
|
947
|
+
const second = ranked.slice(1);
|
|
948
|
+
rules.push({
|
|
949
|
+
law: "hierarchy.md",
|
|
950
|
+
rule: "Colour carries meaning only",
|
|
951
|
+
total: chromatic,
|
|
952
|
+
count: second.reduce((sum, [, tally]) => sum + tally.count, 0),
|
|
953
|
+
offenders: second.slice(0, 3).map(([bucket, tally]) => `a second hue at ${bucket}\xB0 on ${tally.count} runs \u2014 "${tally.sample}"`)
|
|
954
|
+
});
|
|
955
|
+
return rules;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/probe_collectors/ink_and_air.ts
|
|
959
|
+
var HEADING2 = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
|
|
960
|
+
var GRAPHIC = 'svg, img, picture, canvas, [role="img"], [role="figure"]';
|
|
961
|
+
var CONTROL = 'a[href], button, summary, [role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="checkbox"], [role="switch"]';
|
|
962
|
+
var NAVIGATION = 'nav, [role="navigation"], [role="banner"]';
|
|
963
|
+
var EDITOR = 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [contenteditable="plaintext-only"], [role="textbox"], [role="searchbox"], [role="combobox"], [role="spinbutton"]';
|
|
964
|
+
var FIELD = EDITOR + ', [role="checkbox"], [role="switch"], [role="radio"], [role="slider"]';
|
|
965
|
+
var GROUP = '[role="group"], fieldset';
|
|
966
|
+
var TEXT_ENTRY = /* @__PURE__ */ new Set([
|
|
967
|
+
"text",
|
|
968
|
+
"search",
|
|
969
|
+
"url",
|
|
970
|
+
"tel",
|
|
971
|
+
"email",
|
|
972
|
+
"password",
|
|
973
|
+
"number",
|
|
974
|
+
"date",
|
|
975
|
+
"datetime-local",
|
|
976
|
+
"month",
|
|
977
|
+
"week",
|
|
978
|
+
"time"
|
|
979
|
+
]);
|
|
980
|
+
var EDGES = ["top", "right", "bottom", "left"];
|
|
981
|
+
var HAIR = 0.5;
|
|
982
|
+
var words2 = (el) => el === null ? "" : (el.textContent || "").replace(/\s+/g, " ").trim();
|
|
983
|
+
var called = (el) => {
|
|
984
|
+
const named = nameOf(el).slice(0, 40);
|
|
985
|
+
return named === "" ? "(unnamed)" : named;
|
|
986
|
+
};
|
|
987
|
+
var px = (value) => {
|
|
988
|
+
const measure = parseFloat(value);
|
|
989
|
+
return Number.isFinite(measure) ? measure : 0;
|
|
990
|
+
};
|
|
991
|
+
var opaque = (colour) => {
|
|
992
|
+
if (colour === "") return true;
|
|
993
|
+
if (/^transparent$/i.test(colour)) return false;
|
|
994
|
+
const alpha = /^rgba\(\s*[\d.]+[,\s]+[\d.]+[,\s]+[\d.]+[,\s/]+([\d.]+)\s*\)/i.exec(colour);
|
|
995
|
+
return alpha === null || Number(alpha[1]) > 0;
|
|
996
|
+
};
|
|
997
|
+
var ringOf = (shadow) => {
|
|
998
|
+
if (shadow === "" || shadow === "none") return 0;
|
|
999
|
+
let widest = 0;
|
|
1000
|
+
for (const segment of shadow.split(/,(?![^(]*\))/)) {
|
|
1001
|
+
if (!opaque((/rgba?\([^)]*\)/i.exec(segment) ?? [""])[0])) continue;
|
|
1002
|
+
const lengths = (segment.match(/-?[\d.]+px/g) ?? []).map((length) => parseFloat(length));
|
|
1003
|
+
if (lengths.length < 3) continue;
|
|
1004
|
+
const [x, y, blur, spread = 0] = lengths;
|
|
1005
|
+
if (Math.abs(x) > HAIR || Math.abs(y) > HAIR) continue;
|
|
1006
|
+
widest = Math.max(widest, spread > 0 ? spread : blur);
|
|
1007
|
+
}
|
|
1008
|
+
return widest;
|
|
1009
|
+
};
|
|
1010
|
+
var sidesOf = (style) => EDGES.filter((edge) => {
|
|
1011
|
+
const line = style.getPropertyValue(`border-${edge}-style`);
|
|
1012
|
+
if (line === "" || line === "none" || line === "hidden") return false;
|
|
1013
|
+
if (px(style.getPropertyValue(`border-${edge}-width`)) <= 0) return false;
|
|
1014
|
+
return opaque(style.getPropertyValue(`border-${edge}-color`));
|
|
1015
|
+
}).length;
|
|
1016
|
+
var closed = (style) => sidesOf(style) === EDGES.length || ringOf(style.boxShadow) > 0;
|
|
1017
|
+
var editorsIn = (root) => within(root, EDITOR).filter((el) => {
|
|
1018
|
+
if (!onScreen(el)) return false;
|
|
1019
|
+
if (el.tagName !== "INPUT") return true;
|
|
1020
|
+
return TEXT_ENTRY.has((el.getAttribute("type") || "text").toLowerCase());
|
|
1021
|
+
});
|
|
1022
|
+
function inkAndAirCensus(root = document.body) {
|
|
1023
|
+
const rules = [];
|
|
1024
|
+
const rank = (el) => {
|
|
1025
|
+
const stated2 = parseInt(el.getAttribute("aria-level") || "", 10);
|
|
1026
|
+
if (Number.isFinite(stated2)) return stated2;
|
|
1027
|
+
const tag = /^H([1-6])$/.exec(el.tagName);
|
|
1028
|
+
return tag === null ? 2 : Number(tag[1]);
|
|
1029
|
+
};
|
|
1030
|
+
const headings = within(root, HEADING2).filter(
|
|
1031
|
+
(el) => onScreen(el) && el.closest(NAVIGATION) === null && words2(el) !== ""
|
|
1032
|
+
);
|
|
1033
|
+
const shallowest = headings.reduce((top, el) => Math.min(top, rank(el)), Infinity);
|
|
1034
|
+
const pageTitle = headings.find((el) => rank(el) === shallowest);
|
|
1035
|
+
const headerRow = (heading) => {
|
|
1036
|
+
const own = words2(heading);
|
|
1037
|
+
let row = heading;
|
|
1038
|
+
for (let up = heading.parentElement; up !== null && up !== root; up = up.parentElement) {
|
|
1039
|
+
if (words2(up) !== own) break;
|
|
1040
|
+
row = up;
|
|
1041
|
+
}
|
|
1042
|
+
return row;
|
|
1043
|
+
};
|
|
1044
|
+
const graphicIn = (row) => Array.from(row.querySelectorAll(GRAPHIC)).find((mark) => onScreen(mark) && mark.closest(CONTROL) === null) ?? null;
|
|
1045
|
+
let sections = 0;
|
|
1046
|
+
let badged = 0;
|
|
1047
|
+
const badges = [];
|
|
1048
|
+
for (const heading of headings) {
|
|
1049
|
+
if (heading === pageTitle) continue;
|
|
1050
|
+
sections += 1;
|
|
1051
|
+
const mark = graphicIn(headerRow(heading));
|
|
1052
|
+
if (mark === null) continue;
|
|
1053
|
+
badged += 1;
|
|
1054
|
+
if (badges.length < 3) {
|
|
1055
|
+
badges.push(`"${words2(heading).slice(0, 40)}": the heading row draws <${mark.tagName.toLowerCase()}>`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
rules.push({
|
|
1059
|
+
law: "hierarchy.md",
|
|
1060
|
+
rule: "Ink and air state what a thing is \u2192 A section's heading",
|
|
1061
|
+
total: sections,
|
|
1062
|
+
count: badged,
|
|
1063
|
+
offenders: badges
|
|
1064
|
+
});
|
|
1065
|
+
const editors = editorsIn(root);
|
|
1066
|
+
const fieldsIn = (box) => within(box, FIELD).filter(onScreen);
|
|
1067
|
+
const labelsIn = (box) => within(box, "label").filter((el) => onScreen(el) && words2(el) !== "");
|
|
1068
|
+
const oneControl = (el) => el.matches(GROUP) || (el.getAttribute("aria-label") || "").trim() !== "" || (el.getAttribute("aria-labelledby") || "").trim() !== "";
|
|
1069
|
+
const around = (outer, inner) => outer.left <= inner.left + HAIR && outer.top <= inner.top + HAIR && outer.right + HAIR >= inner.right && outer.bottom + HAIR >= inner.bottom;
|
|
1070
|
+
let open = 0;
|
|
1071
|
+
let units = 0;
|
|
1072
|
+
const bare = [];
|
|
1073
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1074
|
+
for (const editor of editors) {
|
|
1075
|
+
if (claimed.has(editor)) continue;
|
|
1076
|
+
let unit = editor;
|
|
1077
|
+
let parts = fieldsIn(editor);
|
|
1078
|
+
let framed = closed(getComputedStyle(unit));
|
|
1079
|
+
for (let up = unit.parentElement; !framed && up !== null && up !== root; up = up.parentElement) {
|
|
1080
|
+
if (labelsIn(up).length > 0) break;
|
|
1081
|
+
const held = fieldsIn(up);
|
|
1082
|
+
if (held.some((field) => !parts.includes(field))) {
|
|
1083
|
+
if (!oneControl(up) || held.some((field) => claimed.has(field))) break;
|
|
1084
|
+
unit = up;
|
|
1085
|
+
parts = held;
|
|
1086
|
+
framed = closed(getComputedStyle(up));
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
if (!around(up.getBoundingClientRect(), unit.getBoundingClientRect())) continue;
|
|
1090
|
+
framed = closed(getComputedStyle(up));
|
|
1091
|
+
}
|
|
1092
|
+
for (const part of parts) claimed.add(part);
|
|
1093
|
+
units += 1;
|
|
1094
|
+
if (framed) continue;
|
|
1095
|
+
open += 1;
|
|
1096
|
+
if (bare.length < 3) {
|
|
1097
|
+
bare.push(`"${called(unit)}": ${sidesOf(getComputedStyle(unit))} of 4 sides painted at rest`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
rules.push({
|
|
1101
|
+
law: "hierarchy.md",
|
|
1102
|
+
rule: "Ink and air state what a thing is \u2192 An editor at rest",
|
|
1103
|
+
total: units,
|
|
1104
|
+
count: open,
|
|
1105
|
+
offenders: bare
|
|
1106
|
+
});
|
|
1107
|
+
const drawn = (el) => Array.from(el.children).filter(onScreen);
|
|
1108
|
+
const captionOf = (group) => {
|
|
1109
|
+
const first = drawn(group)[0];
|
|
1110
|
+
if (first === void 0) return null;
|
|
1111
|
+
if (first.matches(HEADING2)) return first;
|
|
1112
|
+
const inside = first.querySelector(HEADING2);
|
|
1113
|
+
return inside !== null && onScreen(inside) && words2(inside) !== "" ? inside : null;
|
|
1114
|
+
};
|
|
1115
|
+
const rowGapOf = (group) => {
|
|
1116
|
+
const rows = drawn(group).slice(1);
|
|
1117
|
+
let gap = null;
|
|
1118
|
+
for (let at = 1; at < rows.length; at++) {
|
|
1119
|
+
const above = rows[at - 1].getBoundingClientRect();
|
|
1120
|
+
const below = rows[at].getBoundingClientRect();
|
|
1121
|
+
const air = below.top - above.bottom;
|
|
1122
|
+
if (air < 0) continue;
|
|
1123
|
+
gap = gap === null ? air : Math.min(gap, air);
|
|
1124
|
+
}
|
|
1125
|
+
return gap;
|
|
1126
|
+
};
|
|
1127
|
+
let pairs = 0;
|
|
1128
|
+
let level = 0;
|
|
1129
|
+
const merged = [];
|
|
1130
|
+
for (const parent of [root, ...Array.from(root.querySelectorAll("*"))]) {
|
|
1131
|
+
const groups = drawn(parent);
|
|
1132
|
+
for (let at = 1; at < groups.length; at++) {
|
|
1133
|
+
const first = groups[at - 1];
|
|
1134
|
+
const second = groups[at];
|
|
1135
|
+
const opens = captionOf(first);
|
|
1136
|
+
const caption = captionOf(second);
|
|
1137
|
+
if (opens === null || caption === null) continue;
|
|
1138
|
+
const gap = rowGapOf(first) ?? rowGapOf(second);
|
|
1139
|
+
if (gap === null) continue;
|
|
1140
|
+
pairs += 1;
|
|
1141
|
+
const above = caption.getBoundingClientRect().top - first.getBoundingClientRect().bottom;
|
|
1142
|
+
if (above > gap) continue;
|
|
1143
|
+
level += 1;
|
|
1144
|
+
if (merged.length < 3) {
|
|
1145
|
+
merged.push(
|
|
1146
|
+
`"${words2(caption).slice(0, 40)}": ${Math.round(above)}px above the caption, ${Math.round(gap)}px between the rows it follows`
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
rules.push({
|
|
1152
|
+
law: "hierarchy.md",
|
|
1153
|
+
rule: "Ink and air state what a thing is \u2192 Two named groups",
|
|
1154
|
+
total: pairs,
|
|
1155
|
+
count: level,
|
|
1156
|
+
offenders: merged
|
|
1157
|
+
});
|
|
1158
|
+
return rules;
|
|
1159
|
+
}
|
|
1160
|
+
function focusRingCensus(root = document.body) {
|
|
1161
|
+
const clipperOf = (el) => {
|
|
1162
|
+
for (let up = el.parentElement; up !== null; up = up.parentElement) {
|
|
1163
|
+
const style = getComputedStyle(up);
|
|
1164
|
+
const across = style.overflowX || style.overflow;
|
|
1165
|
+
const down = style.overflowY || style.overflow;
|
|
1166
|
+
if (across !== "" && across !== "visible" || down !== "" && down !== "visible") return up;
|
|
1167
|
+
}
|
|
1168
|
+
return null;
|
|
1169
|
+
};
|
|
1170
|
+
const shift = (before, range, short, over) => {
|
|
1171
|
+
const wanted = over > 0 ? over : short > 0 ? -short : 0;
|
|
1172
|
+
return Math.max(0, Math.min(range, before + wanted));
|
|
1173
|
+
};
|
|
1174
|
+
let rings2 = 0;
|
|
1175
|
+
let cut = 0;
|
|
1176
|
+
const clipped = [];
|
|
1177
|
+
for (const editor of editorsIn(root)) {
|
|
1178
|
+
const style = getComputedStyle(editor);
|
|
1179
|
+
const line = style.outlineStyle;
|
|
1180
|
+
const outline = line === "" || line === "none" ? 0 : px(style.outlineWidth) + px(style.outlineOffset);
|
|
1181
|
+
const extent = Math.max(outline, ringOf(style.boxShadow));
|
|
1182
|
+
if (extent <= 0) continue;
|
|
1183
|
+
rings2 += 1;
|
|
1184
|
+
const clipper = clipperOf(editor);
|
|
1185
|
+
if (clipper === null) continue;
|
|
1186
|
+
const scrolled = { top: clipper.scrollTop, left: clipper.scrollLeft };
|
|
1187
|
+
const first = editor.getBoundingClientRect();
|
|
1188
|
+
const port = clipper.getBoundingClientRect();
|
|
1189
|
+
clipper.scrollTop = shift(
|
|
1190
|
+
scrolled.top,
|
|
1191
|
+
clipper.scrollHeight - clipper.clientHeight,
|
|
1192
|
+
port.top - (first.top - extent),
|
|
1193
|
+
first.bottom + extent - port.bottom
|
|
1194
|
+
);
|
|
1195
|
+
clipper.scrollLeft = shift(
|
|
1196
|
+
scrolled.left,
|
|
1197
|
+
clipper.scrollWidth - clipper.clientWidth,
|
|
1198
|
+
port.left - (first.left - extent),
|
|
1199
|
+
first.right + extent - port.right
|
|
1200
|
+
);
|
|
1201
|
+
const box = editor.getBoundingClientRect();
|
|
1202
|
+
const clip = clipper.getBoundingClientRect();
|
|
1203
|
+
const outside = [
|
|
1204
|
+
{ edge: "top", by: clip.top - (box.top - extent) },
|
|
1205
|
+
{ edge: "right", by: box.right + extent - clip.right },
|
|
1206
|
+
{ edge: "bottom", by: box.bottom + extent - clip.bottom },
|
|
1207
|
+
{ edge: "left", by: clip.left - (box.left - extent) }
|
|
1208
|
+
].filter((side) => side.by > HAIR);
|
|
1209
|
+
clipper.scrollTop = scrolled.top;
|
|
1210
|
+
clipper.scrollLeft = scrolled.left;
|
|
1211
|
+
if (outside.length === 0) continue;
|
|
1212
|
+
cut += 1;
|
|
1213
|
+
if (clipped.length < 3) {
|
|
1214
|
+
const worst = Math.round(Math.max(...outside.map((side) => side.by)));
|
|
1215
|
+
clipped.push(
|
|
1216
|
+
`"${called(editor)}": a ${extent}px focus ring cut by up to ${worst}px, on the ` + outside.map((side) => side.edge).join(", ")
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return [
|
|
1221
|
+
{
|
|
1222
|
+
law: "hierarchy.md",
|
|
1223
|
+
rule: "Ink and air state what a thing is \u2192 A focus ring",
|
|
1224
|
+
total: rings2,
|
|
1225
|
+
count: cut,
|
|
1226
|
+
offenders: clipped
|
|
1227
|
+
}
|
|
1228
|
+
];
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// src/probe_collectors/right_form.ts
|
|
1232
|
+
var DAY_RUN_PART = ".lotics-itinerary";
|
|
1233
|
+
var SIDE = 'aside, [role="complementary"], nav, [role="navigation"]';
|
|
1234
|
+
var IN_PAGE = 'button, summary, [role="button"], [role="link"], [role="tab"], [role="menuitem"], a[href^="#"]';
|
|
1235
|
+
var HEADING3 = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
|
|
1236
|
+
var RECORD_ROWS = 'table, [role="table"], [role="grid"], [role="treegrid"], [role="row"], [role="list"], ul, ol';
|
|
1237
|
+
var PARKED = [
|
|
1238
|
+
{
|
|
1239
|
+
is: 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [role="textbox"], [role="searchbox"], [role="combobox"], [role="spinbutton"], [role="slider"], [role="checkbox"], [role="radio"], [role="switch"], form, [role="form"]',
|
|
1240
|
+
called: "a field"
|
|
1241
|
+
},
|
|
1242
|
+
{ is: 'table, [role="table"], [role="grid"], [role="treegrid"], [role="row"], [role="listbox"]', called: "a list of rows" },
|
|
1243
|
+
{ is: 'article, [role="article"], [role="feed"], [role="log"]', called: "a conversation" },
|
|
1244
|
+
{ is: 'img, svg, picture, canvas, [role="img"], [role="figure"]', called: "a picture" },
|
|
1245
|
+
{ is: "a[href]", called: "a link out of the page" }
|
|
1246
|
+
];
|
|
1247
|
+
function rightFormCensus(root = document.body) {
|
|
1248
|
+
const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
|
|
1249
|
+
const recordSurface = '.lotics-record-page, [role="dialog"]';
|
|
1250
|
+
const columnFloor = 3;
|
|
1251
|
+
const twoTrackWidth = 1016;
|
|
1252
|
+
const trackHeight = 240;
|
|
1253
|
+
const words3 = (el) => el === null ? "" : (el.textContent || "").trim().slice(0, 40);
|
|
1254
|
+
const surfaces = within(root, recordSurface).filter(onScreen);
|
|
1255
|
+
const called2 = (el) => words3(el.querySelector(".lotics-record-page__heading, h1, h2")) || "the record";
|
|
1256
|
+
const readingInside = (side) => {
|
|
1257
|
+
for (const kind of PARKED) {
|
|
1258
|
+
for (const el of Array.from(side.querySelectorAll(kind.is))) {
|
|
1259
|
+
if (!onScreen(el) || el.closest(IN_PAGE) !== null) continue;
|
|
1260
|
+
const named = nameOf(el).slice(0, 40) || words3(el);
|
|
1261
|
+
return named === "" ? kind.called : `${kind.called} ("${named}")`;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return null;
|
|
1265
|
+
};
|
|
1266
|
+
const rules = [];
|
|
1267
|
+
const choosers = within(root, 'select, [role="listbox"]').filter(onScreen);
|
|
1268
|
+
let twoOption = 0;
|
|
1269
|
+
const pairs = [];
|
|
1270
|
+
for (const chooser of choosers) {
|
|
1271
|
+
const options = Array.from(
|
|
1272
|
+
chooser.tagName === "SELECT" ? chooser.querySelectorAll("option") : chooser.querySelectorAll('[role="option"]')
|
|
1273
|
+
).map((option) => (option.textContent || "").trim()).filter((label) => label !== "");
|
|
1274
|
+
if (options.length !== 2) continue;
|
|
1275
|
+
twoOption += 1;
|
|
1276
|
+
if (pairs.length < 3) {
|
|
1277
|
+
const field = chooser.closest(".lotics-form-field");
|
|
1278
|
+
const named = nameOf(chooser).slice(0, 40) || words3(field === null ? null : field.querySelector(".lotics-form-field__label")) || "(unnamed)";
|
|
1279
|
+
pairs.push(`"${named}": a two-option select \u2014 ${options.map((option) => `"${option}"`).join(" / ")}`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
rules.push({
|
|
1283
|
+
law: "data_entry.md",
|
|
1284
|
+
rule: "Choosing a CHOICE control \u2014 by option count, not by taste",
|
|
1285
|
+
total: choosers.length,
|
|
1286
|
+
count: twoOption,
|
|
1287
|
+
offenders: pairs
|
|
1288
|
+
});
|
|
1289
|
+
let dayRuns = false;
|
|
1290
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
1291
|
+
try {
|
|
1292
|
+
for (const rule of Array.from(sheet.cssRules)) {
|
|
1293
|
+
if (rule.cssText.includes(DAY_RUN_PART)) {
|
|
1294
|
+
dayRuns = true;
|
|
1295
|
+
break;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
} catch {
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
if (dayRuns) break;
|
|
1302
|
+
}
|
|
1303
|
+
const dated = /^\d{1,4}[/.-]\d{1,2}(?:[/.-]\d{1,4})?$/;
|
|
1304
|
+
const dates = [];
|
|
1305
|
+
const headings = [];
|
|
1306
|
+
if (dayRuns) {
|
|
1307
|
+
const walk = (node) => {
|
|
1308
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
1309
|
+
const text = (node.textContent || "").trim();
|
|
1310
|
+
const parent = node.parentElement;
|
|
1311
|
+
if (text === "" || parent === null || skipTag.test(parent.tagName)) return;
|
|
1312
|
+
if (!onScreen(parent)) return;
|
|
1313
|
+
const box = parent.getBoundingClientRect();
|
|
1314
|
+
const leaf = { text, left: Math.round(box.left), right: Math.round(box.right), top: Math.round(box.top) };
|
|
1315
|
+
if (dated.test(text)) dates.push(leaf);
|
|
1316
|
+
else headings.push(leaf);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
node.childNodes.forEach(walk);
|
|
1320
|
+
};
|
|
1321
|
+
walk(root);
|
|
1322
|
+
}
|
|
1323
|
+
const stacks = [];
|
|
1324
|
+
for (const leaf of [...dates].sort((a, b) => a.left - b.left || a.top - b.top)) {
|
|
1325
|
+
const stack = stacks.find((column) => leaf.left < column.right && leaf.right > column.left);
|
|
1326
|
+
if (stack !== void 0) {
|
|
1327
|
+
stack.items.push(leaf);
|
|
1328
|
+
stack.left = Math.min(stack.left, leaf.left);
|
|
1329
|
+
stack.right = Math.max(stack.right, leaf.right);
|
|
1330
|
+
} else {
|
|
1331
|
+
stacks.push({ left: leaf.left, right: leaf.right, items: [leaf] });
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const columns = stacks.filter((stack) => stack.items.length >= columnFloor);
|
|
1335
|
+
let grouped = 0;
|
|
1336
|
+
const runs = [];
|
|
1337
|
+
for (const column of columns) {
|
|
1338
|
+
const days = new Set(column.items.map((leaf) => leaf.text));
|
|
1339
|
+
if (days.size >= column.items.length) continue;
|
|
1340
|
+
grouped += 1;
|
|
1341
|
+
if (runs.length < 3) {
|
|
1342
|
+
const top = Math.min(...column.items.map((leaf) => leaf.top));
|
|
1343
|
+
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];
|
|
1344
|
+
runs.push(
|
|
1345
|
+
`${over === void 0 ? "(unnamed)" : over.text}: ${column.items.length} rows over ${days.size} days, the date repeated per row`
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
rules.push({
|
|
1350
|
+
law: "templates.md",
|
|
1351
|
+
rule: "Which device answers the question \u2192 Read a trip's stops in order",
|
|
1352
|
+
total: columns.length,
|
|
1353
|
+
count: grouped,
|
|
1354
|
+
offenders: runs
|
|
1355
|
+
});
|
|
1356
|
+
const namesItself = (region) => {
|
|
1357
|
+
const heading = Array.from(region.querySelectorAll(HEADING3)).find((el) => el.closest("summary") === null);
|
|
1358
|
+
return heading !== void 0 || region.querySelector(RECORD_ROWS) !== null;
|
|
1359
|
+
};
|
|
1360
|
+
let folds = 0;
|
|
1361
|
+
let sections = 0;
|
|
1362
|
+
const hidden = [];
|
|
1363
|
+
for (const surface of surfaces) {
|
|
1364
|
+
for (const fold of Array.from(surface.querySelectorAll(".lotics-accordion__trigger, details, [aria-expanded]"))) {
|
|
1365
|
+
if (!onScreen(fold) || fold.closest('[role="complementary"]') !== null) continue;
|
|
1366
|
+
if (fold.hasAttribute("aria-haspopup") || fold.matches(HOLDS_A_VALUE)) continue;
|
|
1367
|
+
folds += 1;
|
|
1368
|
+
const opened = fold.tagName === "DETAILS" ? [fold] : pointedAt(fold, "aria-controls");
|
|
1369
|
+
if (!fold.matches(".lotics-accordion__trigger") && !opened.some(namesItself)) continue;
|
|
1370
|
+
sections += 1;
|
|
1371
|
+
if (hidden.length < 3) {
|
|
1372
|
+
const named = nameOf(fold).slice(0, 40);
|
|
1373
|
+
hidden.push(`${called2(surface)}: "${named === "" ? "(unnamed)" : named}" is a section behind a fold`);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
rules.push({
|
|
1378
|
+
law: "templates.md",
|
|
1379
|
+
rule: "Which device answers the question \u2192 See what NAMES this record",
|
|
1380
|
+
total: folds,
|
|
1381
|
+
count: sections,
|
|
1382
|
+
offenders: hidden
|
|
1383
|
+
});
|
|
1384
|
+
let read = 0;
|
|
1385
|
+
let split = 0;
|
|
1386
|
+
const tracks = [];
|
|
1387
|
+
if (window.innerWidth >= twoTrackWidth) {
|
|
1388
|
+
for (const surface of surfaces) {
|
|
1389
|
+
read += 1;
|
|
1390
|
+
const box = surface.getBoundingClientRect();
|
|
1391
|
+
const owners = [surface, ...Array.from(surface.querySelectorAll("*"))];
|
|
1392
|
+
let found = null;
|
|
1393
|
+
for (const owner of owners) {
|
|
1394
|
+
const columnsHere = Array.from(owner.children).filter((child) => {
|
|
1395
|
+
if (!onScreen(child)) return false;
|
|
1396
|
+
if (child.matches(SIDE) || child.querySelector(SIDE) !== null) return false;
|
|
1397
|
+
const rect = child.getBoundingClientRect();
|
|
1398
|
+
return rect.height >= trackHeight && rect.height >= box.height * 0.5 && rect.width >= box.width * 0.2;
|
|
1399
|
+
});
|
|
1400
|
+
for (let a = 0; a < columnsHere.length && found === null; a++) {
|
|
1401
|
+
for (let b = a + 1; b < columnsHere.length && found === null; b++) {
|
|
1402
|
+
const left = columnsHere[a].getBoundingClientRect();
|
|
1403
|
+
const right = columnsHere[b].getBoundingClientRect();
|
|
1404
|
+
const apart = left.right <= right.left + 8 || right.right <= left.left + 8;
|
|
1405
|
+
const overlap = Math.min(left.bottom, right.bottom) - Math.max(left.top, right.top);
|
|
1406
|
+
const together = Math.max(left.right, right.right) - Math.min(left.left, right.left);
|
|
1407
|
+
if (apart && overlap >= Math.min(left.height, right.height) * 0.5 && together >= box.width * 0.7) {
|
|
1408
|
+
found = [columnsHere[a], columnsHere[b]];
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (found !== null) break;
|
|
1413
|
+
}
|
|
1414
|
+
if (found !== null) {
|
|
1415
|
+
split += 1;
|
|
1416
|
+
if (tracks.length < 3) {
|
|
1417
|
+
const left = Math.round(found[0].getBoundingClientRect().width);
|
|
1418
|
+
const right = Math.round(found[1].getBoundingClientRect().width);
|
|
1419
|
+
tracks.push(`${called2(surface)}: two columns side by side, ${left}px and ${right}px`);
|
|
1420
|
+
}
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
for (const side of Array.from(surface.querySelectorAll(SIDE))) {
|
|
1424
|
+
if (!onScreen(side)) continue;
|
|
1425
|
+
const rect = side.getBoundingClientRect();
|
|
1426
|
+
if (rect.width > box.width * 0.6) continue;
|
|
1427
|
+
const column = Array.from(surface.querySelectorAll("*")).some((other) => {
|
|
1428
|
+
if (other === side || side.contains(other) || other.contains(side) || !onScreen(other)) return false;
|
|
1429
|
+
const beside = other.getBoundingClientRect();
|
|
1430
|
+
const apart = beside.left >= rect.right - 8 || beside.right <= rect.left + 8;
|
|
1431
|
+
const overlap = Math.min(beside.bottom, rect.bottom) - Math.max(beside.top, rect.top);
|
|
1432
|
+
return apart && beside.width >= box.width * 0.4 && overlap >= rect.height * 0.5;
|
|
1433
|
+
});
|
|
1434
|
+
if (!column) continue;
|
|
1435
|
+
const parked = readingInside(side);
|
|
1436
|
+
if (parked === null) continue;
|
|
1437
|
+
split += 1;
|
|
1438
|
+
if (tracks.length < 3) tracks.push(`${called2(surface)}: ${parked} beside the reading column`);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
rules.push({
|
|
1443
|
+
law: "hierarchy.md",
|
|
1444
|
+
rule: "Depth \u2014 four surfaces, four jobs",
|
|
1445
|
+
total: read,
|
|
1446
|
+
count: split,
|
|
1447
|
+
offenders: tracks
|
|
1448
|
+
});
|
|
1449
|
+
return rules;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// src/probe_collectors/surface.ts
|
|
1453
|
+
function surfaceChrome(root = document.body) {
|
|
1454
|
+
const round = (n) => Math.round(n * 10) / 10;
|
|
1455
|
+
const anchor = (el) => el === void 0 ? null : { text: (el.textContent || "").trim().slice(0, 80), left: round(el.getBoundingClientRect().left) };
|
|
1456
|
+
const nav = within(root, "nav")[0] ?? null;
|
|
1457
|
+
const navFirst = nav === null ? void 0 : Array.from(nav.querySelectorAll('a[href], [role="link"]')).filter(
|
|
1458
|
+
(item) => onScreen(item) && (item.textContent || "").trim() !== ""
|
|
1459
|
+
)[0];
|
|
1460
|
+
const headings = within(root, "h1, h2, h3, h4, h5, h6").filter(
|
|
1461
|
+
(el) => onScreen(el) && el.closest('nav, [role="banner"]') === null && (el.textContent || "").trim() !== ""
|
|
1462
|
+
);
|
|
1463
|
+
const title = headings[0];
|
|
1464
|
+
const rank = (el) => Number(el.tagName.slice(1));
|
|
1465
|
+
const heading = title === void 0 ? void 0 : headings.slice(1).find((el) => rank(el) > rank(title));
|
|
1466
|
+
const primaries = within(root, '[data-variant="primary"]').filter(
|
|
1467
|
+
(el) => onScreen(el) && el.closest('nav, [role="banner"]') === null
|
|
1468
|
+
);
|
|
1469
|
+
const folds = within(root, '[aria-expanded="false"]').filter(
|
|
1470
|
+
(el) => onScreen(el) && el.getAttribute("aria-haspopup") === null
|
|
1471
|
+
);
|
|
1472
|
+
const foldNames = folds.map((el) => nameOf(el).slice(0, 60));
|
|
1473
|
+
const folded = folds.reduce((sum, el) => {
|
|
1474
|
+
const digits = /\d+/.exec((el.textContent || "").trim());
|
|
1475
|
+
return sum + (digits === null ? 0 : Number(digits[0]));
|
|
1476
|
+
}, 0);
|
|
1477
|
+
return {
|
|
1478
|
+
nav: anchor(navFirst),
|
|
1479
|
+
title: anchor(title),
|
|
1480
|
+
heading: anchor(heading),
|
|
1481
|
+
primary: {
|
|
1482
|
+
count: primaries.length,
|
|
1483
|
+
names: primaries.map((el) => nameOf(el).slice(0, 40))
|
|
1484
|
+
},
|
|
1485
|
+
facts: { visible: labelValuePairs(root), folded, folds: foldNames }
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function labelValuePairs(root) {
|
|
1489
|
+
const px2 = (el) => parseFloat(getComputedStyle(el).fontSize);
|
|
1490
|
+
const isLeaf = (el) => el.children.length === 0 && (el.textContent || "").trim().length > 0;
|
|
1491
|
+
const leafOf = (el, depth = 3) => {
|
|
1492
|
+
if (isLeaf(el)) return el;
|
|
1493
|
+
if (depth === 0 || el.children.length === 0) return null;
|
|
1494
|
+
return leafOf(el.children[0], depth - 1);
|
|
1495
|
+
};
|
|
1496
|
+
let pairs = 0;
|
|
1497
|
+
for (const el of Array.from(root.querySelectorAll("div"))) {
|
|
1498
|
+
const kids = Array.from(el.children);
|
|
1499
|
+
if (kids.length !== 2) continue;
|
|
1500
|
+
if (el.closest('nav, [role="banner"]') !== null) continue;
|
|
1501
|
+
const label = leafOf(kids[0]);
|
|
1502
|
+
const value = leafOf(kids[1]);
|
|
1503
|
+
if (label === null || value === null) continue;
|
|
1504
|
+
const a = kids[0].getBoundingClientRect();
|
|
1505
|
+
const b = kids[1].getBoundingClientRect();
|
|
1506
|
+
if (a.height === 0 || b.height === 0) continue;
|
|
1507
|
+
if (px2(label) < px2(value)) pairs += 1;
|
|
1508
|
+
}
|
|
1509
|
+
return pairs;
|
|
1510
|
+
}
|
|
1511
|
+
function repeatedValues(root = document.body) {
|
|
1512
|
+
const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
|
|
1513
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1514
|
+
let total = 0;
|
|
1515
|
+
for (const el of Array.from(root.querySelectorAll("*"))) {
|
|
1516
|
+
if (skipTag.test(el.tagName) || el.children.length > 0) continue;
|
|
1517
|
+
const text = (el.textContent || "").trim();
|
|
1518
|
+
if (text.length < 3) continue;
|
|
1519
|
+
if (!/\d/.test(text) || !/[^\d\s]/.test(text)) continue;
|
|
1520
|
+
if (!onScreen(el)) continue;
|
|
1521
|
+
total += 1;
|
|
1522
|
+
counts.set(text, (counts.get(text) || 0) + 1);
|
|
1523
|
+
}
|
|
1524
|
+
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 }));
|
|
1525
|
+
return { total, repeated };
|
|
1526
|
+
}
|
|
1527
|
+
function wrappedMoney(root = document.body) {
|
|
1528
|
+
const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
|
|
1529
|
+
const money = /[$€£¥₫₹]|\bUS\$/;
|
|
1530
|
+
const wrapped = [];
|
|
1531
|
+
let total = 0;
|
|
1532
|
+
for (const el of Array.from(root.querySelectorAll("*"))) {
|
|
1533
|
+
if (skipTag.test(el.tagName) || el.children.length > 0) continue;
|
|
1534
|
+
const text = (el.textContent || "").trim();
|
|
1535
|
+
if (text === "" || !money.test(text) || !/\d/.test(text)) continue;
|
|
1536
|
+
if (!onScreen(el)) continue;
|
|
1537
|
+
total += 1;
|
|
1538
|
+
const range = document.createRange();
|
|
1539
|
+
range.selectNodeContents(el);
|
|
1540
|
+
const lines = Array.from(range.getClientRects()).filter((rect) => rect.width > 0).length;
|
|
1541
|
+
if (lines > 1) wrapped.push({ text: text.slice(0, 50), lines });
|
|
1542
|
+
}
|
|
1543
|
+
return { total, wrapped };
|
|
1544
|
+
}
|
|
1545
|
+
|
|
393
1546
|
// src/probe_page.ts
|
|
394
1547
|
var PROBES = {
|
|
395
1548
|
money_edges: {
|
|
1549
|
+
law: "reviewing.md",
|
|
396
1550
|
section: "6",
|
|
397
1551
|
fix: "One right edge per money column: give every figure in it the same alignment and the same unit."
|
|
398
1552
|
},
|
|
399
1553
|
reported_values: {
|
|
1554
|
+
law: "reviewing.md",
|
|
400
1555
|
section: "9",
|
|
401
1556
|
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."
|
|
402
1557
|
},
|
|
1558
|
+
empty_marks: {
|
|
1559
|
+
law: "reviewing.md",
|
|
1560
|
+
section: "9",
|
|
1561
|
+
fix: "Fill the picture column or drop it: a mark slot drawn empty on every row states nothing and takes a column's width."
|
|
1562
|
+
},
|
|
1563
|
+
stage_words: {
|
|
1564
|
+
law: "reviewing.md",
|
|
1565
|
+
section: "9",
|
|
1566
|
+
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."
|
|
1567
|
+
},
|
|
403
1568
|
internal_ids: {
|
|
1569
|
+
law: "reviewing.md",
|
|
404
1570
|
section: "8",
|
|
405
1571
|
fix: "A key is not a name: render the record's own label, and keep the id in the link."
|
|
406
1572
|
},
|
|
407
1573
|
tab_strips: {
|
|
1574
|
+
law: "reviewing.md",
|
|
408
1575
|
section: "8a",
|
|
409
1576
|
fix: "One navigation strip per screen. A second is a filter wearing navigation's clothes \u2014 draw it as one."
|
|
410
1577
|
},
|
|
411
1578
|
truncation: {
|
|
1579
|
+
law: "reviewing.md",
|
|
412
1580
|
section: "8g",
|
|
413
1581
|
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."
|
|
414
1582
|
},
|
|
415
1583
|
banned_glyph: {
|
|
1584
|
+
law: "reviewing.md",
|
|
416
1585
|
section: "10",
|
|
417
1586
|
fix: "No punctuation doing a word's job \u2014 separate the two facts with layout, or write the word."
|
|
418
1587
|
},
|
|
419
1588
|
compact_money_words: {
|
|
1589
|
+
law: "reviewing.md",
|
|
420
1590
|
section: "10",
|
|
421
1591
|
fix: "Abbreviate in the currency's own words: tr/t\u1EF7 beside \u20AB, K/M/B beside $ and \u20AC."
|
|
422
1592
|
},
|
|
423
1593
|
stacked_pairs: {
|
|
1594
|
+
law: "reviewing.md",
|
|
424
1595
|
section: "1",
|
|
425
1596
|
fix: "Demote once, not twice: either the size steps down or the subject gets the weight."
|
|
426
1597
|
},
|
|
427
1598
|
row_marks: {
|
|
1599
|
+
law: "reviewing.md",
|
|
428
1600
|
section: "9",
|
|
429
1601
|
fix: "One rung per row: a mark's size follows what the row is ABOUT, so the supporting mark takes the smaller one."
|
|
430
1602
|
},
|
|
431
1603
|
render: {
|
|
1604
|
+
law: "reviewing.md",
|
|
432
1605
|
section: "8",
|
|
433
1606
|
fix: "Open the screen and read it: nothing below this was measured, so a clean run over the rest means nothing."
|
|
434
1607
|
},
|
|
435
1608
|
settle: {
|
|
1609
|
+
law: "reviewing.md",
|
|
436
1610
|
section: "9c",
|
|
437
1611
|
fix: "A screen that never stops changing has a render that re-renders or a request that never lands \u2014 find which."
|
|
1612
|
+
},
|
|
1613
|
+
/**
|
|
1614
|
+
* The three CENSUSES. They differ from every probe above in shape: each one
|
|
1615
|
+
* makes SEVERAL measurements and raises one finding per measurement that
|
|
1616
|
+
* fired, so the signature here is the census's own frame and each detail line
|
|
1617
|
+
* names the rule it is. A screen is cluttered, in the wrong form, or drawn as
|
|
1618
|
+
* something it is not as a WHOLE, which is why none of them reduces to one
|
|
1619
|
+
* number.
|
|
1620
|
+
*/
|
|
1621
|
+
clutter: {
|
|
1622
|
+
law: "hierarchy.md",
|
|
1623
|
+
section: "Budgets a reviewer can count",
|
|
1624
|
+
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."
|
|
1625
|
+
},
|
|
1626
|
+
right_form: {
|
|
1627
|
+
law: "templates.md",
|
|
1628
|
+
section: "Which device answers the question",
|
|
1629
|
+
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."
|
|
1630
|
+
},
|
|
1631
|
+
ink_and_air: {
|
|
1632
|
+
law: "hierarchy.md",
|
|
1633
|
+
section: "Ink and air state what a thing is",
|
|
1634
|
+
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."
|
|
438
1635
|
}
|
|
439
1636
|
};
|
|
440
1637
|
function signature(probe) {
|
|
441
1638
|
return PROBES[probe];
|
|
442
1639
|
}
|
|
1640
|
+
function censusFindings(probe, rules) {
|
|
1641
|
+
const fired = rules.filter((rule) => rule.count > 0);
|
|
1642
|
+
if (fired.length === 0) return [];
|
|
1643
|
+
return [
|
|
1644
|
+
{
|
|
1645
|
+
probe,
|
|
1646
|
+
...signature(probe),
|
|
1647
|
+
total: rules.reduce((sum, rule) => sum + rule.total, 0),
|
|
1648
|
+
count: fired.reduce((sum, rule) => sum + rule.count, 0),
|
|
1649
|
+
detail: fired.map(
|
|
1650
|
+
(rule) => `${rule.law} \xA7${rule.rule} \u2014 ${rule.count} of ${rule.total}` + (rule.offenders.length === 0 ? "" : `: ${rule.offenders.join(" | ")}`)
|
|
1651
|
+
)
|
|
1652
|
+
}
|
|
1653
|
+
];
|
|
1654
|
+
}
|
|
443
1655
|
var MONEY_EDGES_ALLOWED = 2;
|
|
444
1656
|
var MONEY_COLUMN_MIN = 3;
|
|
445
1657
|
var BARE_VALUES_FLOOR = 8;
|
|
446
1658
|
var QUANTITY_COLUMN_MIN = 2;
|
|
447
1659
|
function run(options) {
|
|
448
1660
|
const findings = [];
|
|
449
|
-
const
|
|
1661
|
+
const surface = measuredSurface();
|
|
1662
|
+
const money = moneyColumns(surface);
|
|
450
1663
|
const ragged = money.columns.filter((column) => column.size >= MONEY_COLUMN_MIN && column.edges.length > MONEY_EDGES_ALLOWED);
|
|
451
1664
|
if (ragged.length > 0) {
|
|
452
1665
|
findings.push({
|
|
@@ -457,25 +1670,53 @@ var __loticsProbe = (() => {
|
|
|
457
1670
|
detail: ragged.slice(0, 5).map((column) => `x=${column.x}: ${column.edges.length} right edges over ${column.size} figures \u2014 ${column.samples.join(" | ")}`)
|
|
458
1671
|
});
|
|
459
1672
|
}
|
|
460
|
-
const values = reportedValues();
|
|
1673
|
+
const values = reportedValues(surface);
|
|
461
1674
|
const measurable = values.columns.some((column) => column.quantities >= QUANTITY_COLUMN_MIN) || values.devices > 0;
|
|
462
|
-
|
|
1675
|
+
const emptyPictureColumns = values.markSlots.filter((column) => column.filled === 0);
|
|
1676
|
+
const silentColumns = values.columns.filter(
|
|
1677
|
+
(column) => column.quantities === 0 && column.size >= QUANTITY_COLUMN_MIN
|
|
1678
|
+
);
|
|
1679
|
+
const drawnAndSilent = values.devices > 0 && values.encoded === 0 && silentColumns.length > 0 && values.total >= BARE_VALUES_FLOOR;
|
|
1680
|
+
const wordsOnly = values.bare >= BARE_VALUES_FLOOR && values.encoded === 0 && measurable;
|
|
1681
|
+
if (emptyPictureColumns.length > 0) {
|
|
1682
|
+
findings.push({
|
|
1683
|
+
probe: "empty_marks",
|
|
1684
|
+
...signature("empty_marks"),
|
|
1685
|
+
total: values.markSlots.reduce((sum, column) => sum + column.size, 0),
|
|
1686
|
+
count: emptyPictureColumns.reduce((sum, column) => sum + column.size, 0),
|
|
1687
|
+
detail: emptyPictureColumns.slice(0, 5).map(
|
|
1688
|
+
(column) => `${column.slot === "" ? "(unnamed)" : column.slot}: ${column.size} picture slots, none holding a picture`
|
|
1689
|
+
)
|
|
1690
|
+
});
|
|
1691
|
+
} else if (drawnAndSilent || wordsOnly) {
|
|
463
1692
|
findings.push({
|
|
464
1693
|
probe: "reported_values",
|
|
465
1694
|
...signature("reported_values"),
|
|
466
1695
|
total: values.total,
|
|
467
1696
|
count: values.bare,
|
|
468
1697
|
detail: [
|
|
469
|
-
values.devices === 0 ? `${values.bare}
|
|
1698
|
+
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`,
|
|
470
1699
|
...values.columns.slice(0, 5).map((column) => ` ${column.slot === "" ? "(unnamed)" : column.slot}: ${column.size} \u2014 ${column.samples.join(", ")}`)
|
|
471
1700
|
]
|
|
472
1701
|
});
|
|
473
1702
|
}
|
|
474
|
-
const ids = internalIds();
|
|
1703
|
+
const ids = internalIds(surface);
|
|
475
1704
|
if (ids.ids.length > 0) {
|
|
476
1705
|
findings.push({ probe: "internal_ids", ...signature("internal_ids"), total: ids.total, count: ids.ids.length, detail: ids.ids.slice(0, 5) });
|
|
477
1706
|
}
|
|
478
|
-
const
|
|
1707
|
+
const stageWords = stageWordColumns(surface);
|
|
1708
|
+
if (stageWords.wordsOnly.length > 0) {
|
|
1709
|
+
findings.push({
|
|
1710
|
+
probe: "stage_words",
|
|
1711
|
+
...signature("stage_words"),
|
|
1712
|
+
total: stageWords.total,
|
|
1713
|
+
count: stageWords.wordsOnly.reduce((sum, column) => sum + column.size, 0),
|
|
1714
|
+
detail: stageWords.wordsOnly.slice(0, 5).map(
|
|
1715
|
+
(column) => `${column.slot === "" ? "(unnamed)" : column.slot}: the stage column is words alone \u2014 ${column.size} rows of ${column.values.join(" / ")}, no swatch or glyph`
|
|
1716
|
+
)
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
const strips = tabStrips(surface);
|
|
479
1720
|
if (strips.counted > 1) {
|
|
480
1721
|
findings.push({
|
|
481
1722
|
probe: "tab_strips",
|
|
@@ -486,7 +1727,7 @@ var __loticsProbe = (() => {
|
|
|
486
1727
|
});
|
|
487
1728
|
}
|
|
488
1729
|
if (options.truncation) {
|
|
489
|
-
const cut = truncatedLeaves();
|
|
1730
|
+
const cut = truncatedLeaves(surface);
|
|
490
1731
|
if (cut.cut.length > 0) {
|
|
491
1732
|
findings.push({
|
|
492
1733
|
probe: "truncation",
|
|
@@ -497,11 +1738,11 @@ var __loticsProbe = (() => {
|
|
|
497
1738
|
});
|
|
498
1739
|
}
|
|
499
1740
|
}
|
|
500
|
-
const glyphs = bannedGlyphs();
|
|
1741
|
+
const glyphs = bannedGlyphs(surface);
|
|
501
1742
|
if (glyphs.found.length > 0) {
|
|
502
1743
|
findings.push({ probe: "banned_glyph", ...signature("banned_glyph"), total: glyphs.total, count: glyphs.found.length, detail: glyphs.found.slice(0, 5) });
|
|
503
1744
|
}
|
|
504
|
-
const compact = compactMoneyWords();
|
|
1745
|
+
const compact = compactMoneyWords(surface);
|
|
505
1746
|
if (compact.foreign.length > 0) {
|
|
506
1747
|
findings.push({
|
|
507
1748
|
probe: "compact_money_words",
|
|
@@ -511,7 +1752,7 @@ var __loticsProbe = (() => {
|
|
|
511
1752
|
detail: compact.foreign.slice(0, 5).map((text) => `"${text}" abbreviates in the other currency's words`)
|
|
512
1753
|
});
|
|
513
1754
|
}
|
|
514
|
-
const pairs = scanStackedPairs();
|
|
1755
|
+
const pairs = scanStackedPairs(surface);
|
|
515
1756
|
if (pairs.overDemoted.length > 0) {
|
|
516
1757
|
findings.push({
|
|
517
1758
|
probe: "stacked_pairs",
|
|
@@ -521,7 +1762,7 @@ var __loticsProbe = (() => {
|
|
|
521
1762
|
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]}`)
|
|
522
1763
|
});
|
|
523
1764
|
}
|
|
524
|
-
const marks = scanRowMarks();
|
|
1765
|
+
const marks = scanRowMarks(surface);
|
|
525
1766
|
if (marks.mismatches.length > 0) {
|
|
526
1767
|
findings.push({
|
|
527
1768
|
probe: "row_marks",
|
|
@@ -531,20 +1772,46 @@ var __loticsProbe = (() => {
|
|
|
531
1772
|
detail: marks.mismatches.slice(0, 5).map((row) => `y=${row.y}: ${row.marks.join(", ")}`)
|
|
532
1773
|
});
|
|
533
1774
|
}
|
|
1775
|
+
findings.push(...censusFindings("clutter", clutterCensus(surface)));
|
|
1776
|
+
findings.push(...censusFindings("right_form", rightFormCensus(surface)));
|
|
1777
|
+
findings.push(...censusFindings("ink_and_air", [...inkAndAirCensus(surface), ...options.rings ?? []]));
|
|
534
1778
|
return {
|
|
535
1779
|
findings,
|
|
536
1780
|
census: {
|
|
537
|
-
leaves: textLeafCount(),
|
|
1781
|
+
leaves: textLeafCount(surface),
|
|
538
1782
|
money: money.total,
|
|
539
1783
|
bare: values.bare,
|
|
540
1784
|
quantities: values.quantities,
|
|
541
1785
|
encoded: values.encoded,
|
|
542
1786
|
devices: values.devices,
|
|
1787
|
+
device_kinds: values.deviceKinds.map(
|
|
1788
|
+
(device) => `${device.kind}\xD7${device.slot === "" ? "(unnamed)" : device.slot}: ${device.count}`
|
|
1789
|
+
),
|
|
543
1790
|
strips: strips.counted
|
|
544
1791
|
}
|
|
545
1792
|
};
|
|
546
1793
|
}
|
|
547
|
-
function
|
|
1794
|
+
function rings() {
|
|
1795
|
+
return focusRingCensus(measuredSurface());
|
|
1796
|
+
}
|
|
1797
|
+
function editorSelector() {
|
|
1798
|
+
return EDITOR;
|
|
1799
|
+
}
|
|
1800
|
+
function focused() {
|
|
1801
|
+
const held = document.activeElement;
|
|
1802
|
+
if (held === null || held === document.body) return null;
|
|
1803
|
+
const named = nameOf(held).slice(0, 40);
|
|
1804
|
+
return named === "" ? held.tagName.toLowerCase() : named;
|
|
1805
|
+
}
|
|
1806
|
+
function paintState() {
|
|
1807
|
+
const root = document.getElementById("root");
|
|
1808
|
+
return {
|
|
1809
|
+
leaves: textLeafCount(),
|
|
1810
|
+
loading: document.readyState === "loading",
|
|
1811
|
+
booted: root !== null && root.hasChildNodes()
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
function textLeafCount(root = document.body) {
|
|
548
1815
|
const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
|
|
549
1816
|
let count = 0;
|
|
550
1817
|
const walk = (node) => {
|
|
@@ -555,7 +1822,7 @@ var __loticsProbe = (() => {
|
|
|
555
1822
|
}
|
|
556
1823
|
node.childNodes.forEach(walk);
|
|
557
1824
|
};
|
|
558
|
-
walk(
|
|
1825
|
+
walk(root);
|
|
559
1826
|
return count;
|
|
560
1827
|
}
|
|
561
1828
|
function navItems() {
|
|
@@ -586,10 +1853,12 @@ var __loticsProbe = (() => {
|
|
|
586
1853
|
}
|
|
587
1854
|
function recordLinks() {
|
|
588
1855
|
const here = location.pathname + location.search;
|
|
589
|
-
const
|
|
590
|
-
|
|
1856
|
+
const doors2 = Array.from(
|
|
1857
|
+
document.querySelectorAll('a[href], [data-opens="page"], button, [role="button"], [role="link"]')
|
|
1858
|
+
).filter((door) => door.matches('a[href], [data-opens="page"]') || isEmptyDoor(door));
|
|
1859
|
+
return doors2.filter((door) => {
|
|
591
1860
|
if (door.closest('nav, [role="banner"], [role="tablist"]') !== null) return false;
|
|
592
|
-
if (
|
|
1861
|
+
if (doors2.some((other) => other !== door && other.contains(door))) return false;
|
|
593
1862
|
const rect = door.getBoundingClientRect();
|
|
594
1863
|
if (rect.width === 0 || rect.height === 0) return false;
|
|
595
1864
|
const href = door.getAttribute("href");
|
|
@@ -599,7 +1868,7 @@ var __loticsProbe = (() => {
|
|
|
599
1868
|
});
|
|
600
1869
|
}
|
|
601
1870
|
function recordRows() {
|
|
602
|
-
return recordLinks().map(
|
|
1871
|
+
return recordLinks().map(doorName);
|
|
603
1872
|
}
|
|
604
1873
|
function openRecord(index) {
|
|
605
1874
|
const door = recordLinks()[index];
|
|
@@ -607,5 +1876,119 @@ var __loticsProbe = (() => {
|
|
|
607
1876
|
door.click();
|
|
608
1877
|
return true;
|
|
609
1878
|
}
|
|
1879
|
+
var OVERLAY_ROLES = '[role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"]';
|
|
1880
|
+
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;
|
|
1881
|
+
var NEVER_PRESS_VI_WORDS = [
|
|
1882
|
+
"x[o\xF3][a\xE1]",
|
|
1883
|
+
"h[\u1EE7u][\u1EF7y]",
|
|
1884
|
+
"g[\u1EE1o]",
|
|
1885
|
+
"l[\u01B0u]u",
|
|
1886
|
+
"duy[\u1EC7e]t",
|
|
1887
|
+
"t[\u1EEBu] ch[\u1ED1o]i",
|
|
1888
|
+
"g[\u1EEDu]i",
|
|
1889
|
+
"thanh to[\xE1a]n",
|
|
1890
|
+
"ph[\xE1a]t h[\xE0a]nh",
|
|
1891
|
+
"x[\xE1a]c nh[\u1EADa]n",
|
|
1892
|
+
"k[\xFDy]",
|
|
1893
|
+
"ghi s[\u1ED5o]",
|
|
1894
|
+
"kh[\xF3o]a",
|
|
1895
|
+
"kho[\xE1a]",
|
|
1896
|
+
"ho[\xE0a]n",
|
|
1897
|
+
"ch[\u1ED1o]t",
|
|
1898
|
+
"t[\xE1a]ch",
|
|
1899
|
+
"g[\u1ED9o]p",
|
|
1900
|
+
"xu[\u1EA5a]t",
|
|
1901
|
+
"t[\u1EA3i]i v[\u1EC1e]",
|
|
1902
|
+
"in",
|
|
1903
|
+
"\u0111[\u0103a]ng xu[\u1EA5a]t"
|
|
1904
|
+
];
|
|
1905
|
+
var NEVER_PRESS_VI = new RegExp(
|
|
1906
|
+
`(?<![\\p{L}\\p{N}])(?:${NEVER_PRESS_VI_WORDS.join("|")})(?![\\p{L}\\p{N}])`,
|
|
1907
|
+
"iu"
|
|
1908
|
+
);
|
|
1909
|
+
function doorName(el) {
|
|
1910
|
+
return (nameOf(el) || el.getAttribute("href") || "").slice(0, 60);
|
|
1911
|
+
}
|
|
1912
|
+
function isChrome(el) {
|
|
1913
|
+
return el.closest('nav, [role="banner"], [role="tablist"]') !== null;
|
|
1914
|
+
}
|
|
1915
|
+
function isEmptyDoor(el) {
|
|
1916
|
+
if (el.childElementCount > 0) return false;
|
|
1917
|
+
if ((el.textContent || "").trim() !== "") return false;
|
|
1918
|
+
return (el.getAttribute("aria-label") || "").trim() !== "";
|
|
1919
|
+
}
|
|
1920
|
+
function doorElements() {
|
|
1921
|
+
const open = Array.from(document.querySelectorAll(OVERLAY_ROLES)).filter(onScreen);
|
|
1922
|
+
const reachable = (el) => onScreen(el) && !isChrome(el) && el.getAttribute("aria-disabled") !== "true" && !(el instanceof HTMLButtonElement && el.disabled) && // NEVER A CONTROL INSIDE AN OPEN OVERLAY. A dialog's own buttons are the
|
|
1923
|
+
// ones that commit it, and the walk photographs a dialog rather than
|
|
1924
|
+
// answering it.
|
|
1925
|
+
!open.some((overlay) => overlay.contains(el));
|
|
1926
|
+
const records = recordLinks().filter(
|
|
1927
|
+
(el) => reachable(el) && el.getAttribute("aria-expanded") === null && el.getAttribute("aria-haspopup") === null
|
|
1928
|
+
);
|
|
1929
|
+
const menus = Array.from(document.querySelectorAll("[aria-haspopup]")).filter(
|
|
1930
|
+
(el) => reachable(el) && el.getAttribute("aria-expanded") !== "true"
|
|
1931
|
+
);
|
|
1932
|
+
const folds = Array.from(document.querySelectorAll('[aria-expanded="false"]')).filter(
|
|
1933
|
+
(el) => reachable(el) && el.getAttribute("aria-haspopup") === null
|
|
1934
|
+
);
|
|
1935
|
+
const acts = Array.from(
|
|
1936
|
+
document.querySelectorAll('[data-variant="primary"], [data-variant="secondary"]')
|
|
1937
|
+
).filter((el) => {
|
|
1938
|
+
if (!reachable(el)) return false;
|
|
1939
|
+
if (el.tagName !== "BUTTON" && el.getAttribute("role") !== "button") return false;
|
|
1940
|
+
if (el.getAttribute("aria-haspopup") !== null || el.getAttribute("aria-expanded") !== null) return false;
|
|
1941
|
+
if (el.getAttribute("type") === "submit") return false;
|
|
1942
|
+
if (el.closest("form") !== null) return false;
|
|
1943
|
+
const tone = el.getAttribute("data-tone");
|
|
1944
|
+
if (tone === "danger" || tone === "warning") return false;
|
|
1945
|
+
const name = `${doorName(el)} ${(el.textContent || "").trim()}`;
|
|
1946
|
+
return !NEVER_PRESS.test(name) && !NEVER_PRESS_VI.test(name);
|
|
1947
|
+
});
|
|
1948
|
+
return [
|
|
1949
|
+
// One of each state-changing kind — the first row, the acts menu, the first
|
|
1950
|
+
// fold. A second of any is the same surface again; the acts a screen offers
|
|
1951
|
+
// are not, so every one of those is walked.
|
|
1952
|
+
...records.slice(0, 1).map((el) => ({ kind: "record", el })),
|
|
1953
|
+
...menus.slice(0, 1).map((el) => ({ kind: "menu", el })),
|
|
1954
|
+
...folds.slice(0, 1).map((el) => ({ kind: "fold", el })),
|
|
1955
|
+
...acts.map((el) => ({ kind: "dialog", el }))
|
|
1956
|
+
];
|
|
1957
|
+
}
|
|
1958
|
+
function doors() {
|
|
1959
|
+
return doorElements().map((door) => ({ kind: door.kind, name: doorName(door.el) }));
|
|
1960
|
+
}
|
|
1961
|
+
function openDoor(index) {
|
|
1962
|
+
const door = doorElements()[index];
|
|
1963
|
+
if (door === void 0) return false;
|
|
1964
|
+
door.el.click();
|
|
1965
|
+
return true;
|
|
1966
|
+
}
|
|
1967
|
+
function overlaysOpen() {
|
|
1968
|
+
return Array.from(document.querySelectorAll(OVERLAY_ROLES)).filter(onScreen).length;
|
|
1969
|
+
}
|
|
1970
|
+
function contentHeight() {
|
|
1971
|
+
let tallest = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
|
|
1972
|
+
for (const el of Array.from(document.querySelectorAll("*"))) {
|
|
1973
|
+
if (el.scrollHeight <= el.clientHeight) continue;
|
|
1974
|
+
if (!onScreen(el)) continue;
|
|
1975
|
+
tallest = Math.max(tallest, Math.ceil(el.getBoundingClientRect().top) + el.scrollHeight);
|
|
1976
|
+
}
|
|
1977
|
+
for (const overlay of Array.from(document.querySelectorAll(OVERLAY_ROLES))) {
|
|
1978
|
+
if (!onScreen(overlay)) continue;
|
|
1979
|
+
tallest = Math.max(tallest, Math.ceil(overlay.getBoundingClientRect().top) + overlay.scrollHeight);
|
|
1980
|
+
}
|
|
1981
|
+
return tallest;
|
|
1982
|
+
}
|
|
1983
|
+
function surfaceReport() {
|
|
1984
|
+
const surface = measuredSurface();
|
|
1985
|
+
const cut = truncatedLeaves(surface);
|
|
1986
|
+
return {
|
|
1987
|
+
...surfaceChrome(surface),
|
|
1988
|
+
repeated: repeatedValues(surface).repeated,
|
|
1989
|
+
wrapped_money: wrappedMoney(surface).wrapped,
|
|
1990
|
+
clipped: cut.cut
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
610
1993
|
return __toCommonJS(probe_page_exports);
|
|
611
1994
|
})();
|