mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-19 09:36:59 +03:00
Five browser rules were reporting something other than what the reader sees, and a review that charges those numbers is charging noise. - `line-length` measured `rect.width / (fontSize * 0.5)`, the box's capacity. A paragraph in a 1022px column whose text stops at 571px was charged 142 characters a line it never rendered. The probe now hands back the client rects of the direct text one per line box (`direct_text_line_rects`), the characters divide between the lines in proportion to the ink each carries, and the charge needs more than one long line: the harm named is the eye tracking back to the start of the next line, which takes a column to do. - `cramped-padding` read the declared padding. A 44px control with `padding: 0 16px` and a flex-centred label has 12px of air above the label and was charged "0px vertical padding"; the measurement is now the inset between the rendered text and the inside of the border box. Its wrapper half read a text-bearing child's border box the same way, so a `<td>` that fills its table and insets its own text counted as flush; it reads the text now. - `gray-on-color` called anything under 0.85 relative luminance gray, which takes in every off-white: `#e8edf2` measures 0.84 there and 0.93 as lightness. Gray is now low chroma at the lightness the ink actually sits at (saturation, which is chroma normalized for lightness) and neither of the two neutral inks a coloured surface carries. The contrast check beside it is untouched, and the recorded vectors still pass. - `ai-color-palette` charged every hue between 160° and 200° on a dark ground as neon, which lit one ordinary teal accent 18 places on a page with nothing wrong with it. A gradient in a tell hue is still the pattern on its own; flat neon ink on near-black waits for a second tell hue to turn up somewhere on the page, because one saturated accent on a dark system is an accent. - `kicker-above-heading` reported against `body`, so a charged row had nothing to point at, and it fired on eyebrows a design document documents. It names the eyebrow element now, and stands down where the repository's DESIGN.md declares the class by name — the prose's backticked class selectors travel on the design-system config the colour and radius rules already read. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LQBUunp8QttxZqihybNmtL
220 lines
7.8 KiB
JavaScript
220 lines
7.8 KiB
JavaScript
// --- browser-bundle/10-probe.js ---
|
|
// The DOM probe the WASM rule core calls back into. Pure measurement: one
|
|
// function per DOM API the rules read (see crates/core/src/browser/dom.rs for
|
|
// the contract). Elements travel as handles (indexes into a registry; 0 is
|
|
// null). Nothing in here decides anything about a design.
|
|
|
|
const __els = [null];
|
|
let __ids = new WeakMap();
|
|
const __csCache = [null];
|
|
// Drop every handle (a new scan re-interns what it touches; JS keeps
|
|
// Elements, never handles, across calls).
|
|
function __resetRegistry() {
|
|
__els.length = 1;
|
|
__csCache.length = 1;
|
|
__ids = new WeakMap();
|
|
}
|
|
function __intern(el) {
|
|
if (!el) return 0;
|
|
let id = __ids.get(el);
|
|
if (id === undefined) {
|
|
id = __els.length;
|
|
__els.push(el);
|
|
__csCache.push(null);
|
|
__ids.set(el, id);
|
|
}
|
|
return id;
|
|
}
|
|
function __el(id) {
|
|
return __els[id] || null;
|
|
}
|
|
function __cs(id) {
|
|
let cs = __csCache[id];
|
|
if (!cs) {
|
|
cs = getComputedStyle(__els[id]);
|
|
__csCache[id] = cs;
|
|
}
|
|
return cs;
|
|
}
|
|
function __ids_of(list) {
|
|
const out = new Array(list.length);
|
|
for (let i = 0; i < list.length; i++) out[i] = __intern(list[i]);
|
|
return out;
|
|
}
|
|
const __SEL_ERR = 0xFFFFFFFF;
|
|
function __rectArray(r) {
|
|
return [r.x, r.y, r.width, r.height, r.top, r.right, r.bottom, r.left];
|
|
}
|
|
|
|
// The client rects of an element's non-blank direct text nodes, one per line
|
|
// box, in document order. Both text-rect probes read the page through this:
|
|
// the union one merges them, the line one hands them over as they are.
|
|
function __textLineRects(el) {
|
|
const node = __el(el);
|
|
const rects = [];
|
|
for (const child of node.childNodes) {
|
|
if (child.nodeType !== 3 || !(child.textContent || '').trim()) continue;
|
|
const range = document.createRange();
|
|
range.selectNodeContents(child);
|
|
for (const rect of range.getClientRects()) {
|
|
if (rect.width >= 1 && rect.height >= 1) rects.push(rect);
|
|
}
|
|
range.detach?.();
|
|
}
|
|
return rects;
|
|
}
|
|
|
|
const __impeccableDom = {
|
|
document_element() { return __intern(document.documentElement); },
|
|
body() { return __intern(document.body); },
|
|
query_all(root, selector) {
|
|
try {
|
|
const scope = root ? __el(root) : document;
|
|
return __ids_of(scope.querySelectorAll(selector));
|
|
} catch { return [__SEL_ERR]; }
|
|
},
|
|
query_one(root, selector) {
|
|
try {
|
|
const scope = root ? __el(root) : document;
|
|
return __intern(scope.querySelector(selector));
|
|
} catch { return __SEL_ERR; }
|
|
},
|
|
inner_width() { return window.innerWidth; },
|
|
inner_height() { return window.innerHeight; },
|
|
scroll_x() { return window.scrollX; },
|
|
scroll_y() { return window.scrollY; },
|
|
hostname() { return location.hostname; },
|
|
element_from_point(x, y) { return __intern(document.elementFromPoint(x, y)); },
|
|
elements_from_point(x, y) {
|
|
return typeof document.elementsFromPoint === 'function' ? __ids_of(document.elementsFromPoint(x, y)) : [];
|
|
},
|
|
css_escape(s) { return CSS.escape(s); },
|
|
// JSON `[[["prop","value"],...], ...]` of the first @keyframes rule named
|
|
// `name` (document.styleSheets order, nested rules walked breadth-first
|
|
// exactly like keyframesToggleVisibilityDOM); undefined when none.
|
|
keyframes(name) {
|
|
if (!name) return undefined;
|
|
for (const sheet of document.styleSheets) {
|
|
let rules;
|
|
try { rules = sheet.cssRules || sheet.rules; } catch { continue; }
|
|
if (!rules) continue;
|
|
const stack = [...rules];
|
|
while (stack.length) {
|
|
const rule = stack.shift();
|
|
if (rule.cssRules && rule.type !== 7) { stack.push(...rule.cssRules); continue; }
|
|
if (rule.type !== 7 || rule.name !== name) continue;
|
|
const frames = [];
|
|
for (const frame of rule.cssRules || []) {
|
|
const fs = frame.style;
|
|
if (!fs) continue;
|
|
const decls = [];
|
|
for (let i = 0; i < fs.length; i++) {
|
|
const prop = fs[i];
|
|
decls.push([prop, fs.getPropertyValue(prop)]);
|
|
}
|
|
frames.push(decls);
|
|
}
|
|
return JSON.stringify(frames);
|
|
}
|
|
}
|
|
return undefined;
|
|
},
|
|
linked_stylesheet_text() {
|
|
// The CSSOM walk lives in 15-snapshot.js so the standalone snapshot
|
|
// producer carries it too; both routes read the same corpus.
|
|
return __snapLinkedStylesheetText();
|
|
},
|
|
document_html_for_patterns() {
|
|
const docClone = document.documentElement.cloneNode(true);
|
|
for (const node of docClone.querySelectorAll('[id^="impeccable-live-"]')) node.remove();
|
|
return docClone.outerHTML;
|
|
},
|
|
tag_name(el) { return __el(el).tagName; },
|
|
namespace_uri(el) { return __el(el).namespaceURI || ''; },
|
|
parent(el) { return __intern(__el(el).parentElement); },
|
|
children(el) { return __ids_of(__el(el).children); },
|
|
previous_element_sibling(el) { return __intern(__el(el).previousElementSibling); },
|
|
next_element_sibling(el) { return __intern(__el(el).nextElementSibling); },
|
|
contains(a, b) { return __el(a).contains(__el(b)); },
|
|
matches(el, selector) {
|
|
try { return __el(el).matches(selector) ? 1 : 0; } catch { return __SEL_ERR; }
|
|
},
|
|
closest(el, selector) {
|
|
try { return __intern(__el(el).closest(selector)); } catch { return __SEL_ERR; }
|
|
},
|
|
attr(el, name) {
|
|
const v = __el(el).getAttribute(name);
|
|
return v == null ? undefined : v;
|
|
},
|
|
id_prop(el) {
|
|
const v = __el(el).id;
|
|
return typeof v === 'string' ? v : undefined;
|
|
},
|
|
class_name_prop(el) {
|
|
const v = __el(el).className;
|
|
return typeof v === 'string' ? v : undefined;
|
|
},
|
|
text_content(el) { return __el(el).textContent || ''; },
|
|
inner_text(el) {
|
|
const v = __el(el).innerText;
|
|
return typeof v === 'string' && v ? v : undefined;
|
|
},
|
|
direct_text_nodes(el) {
|
|
const out = [];
|
|
for (const n of __el(el).childNodes) {
|
|
if (n.nodeType === 3) out.push(n.textContent || '');
|
|
}
|
|
return out;
|
|
},
|
|
is_content_editable(el) { return !!__el(el).isContentEditable; },
|
|
hidden_prop(el) { return !!__el(el).hidden; },
|
|
style(el, prop) {
|
|
const v = __cs(el)[prop];
|
|
return v == null ? '' : String(v);
|
|
},
|
|
pseudo_style(el, pseudo, prop) {
|
|
let ps;
|
|
try { ps = getComputedStyle(__el(el), pseudo); } catch { return undefined; }
|
|
if (!ps) return undefined;
|
|
const v = ps[prop];
|
|
return v == null ? '' : String(v);
|
|
},
|
|
rect(el) {
|
|
const node = __el(el);
|
|
if (typeof node.getBoundingClientRect !== 'function') return [];
|
|
return __rectArray(node.getBoundingClientRect());
|
|
},
|
|
client_width(el) { return __el(el).clientWidth; },
|
|
client_height(el) { return __el(el).clientHeight; },
|
|
client_left(el) { return __el(el).clientLeft; },
|
|
scroll_width(el) { return __el(el).scrollWidth; },
|
|
scroll_left(el) { return __el(el).scrollLeft; },
|
|
offset_width(el) { return __el(el).offsetWidth; },
|
|
offset_height(el) { return __el(el).offsetHeight; },
|
|
check_visibility(el) {
|
|
const node = __el(el);
|
|
if (typeof node.checkVisibility !== 'function') return -1;
|
|
return node.checkVisibility({ checkOpacity: false, checkVisibilityCSS: true }) ? 1 : 0;
|
|
},
|
|
// getDirectTextRect(el) from the JS driver: union of the client rects of
|
|
// the element's non-blank direct text nodes.
|
|
direct_text_rect(el) {
|
|
const rects = __textLineRects(el);
|
|
if (rects.length === 0) return [];
|
|
const left = Math.min(...rects.map(r => r.left));
|
|
const top = Math.min(...rects.map(r => r.top));
|
|
const right = Math.max(...rects.map(r => r.right));
|
|
const bottom = Math.max(...rects.map(r => r.bottom));
|
|
return [left, top, right - left, bottom - top, top, right, bottom, left];
|
|
},
|
|
// The same rects, unmerged: getClientRects() returns one per line box, so
|
|
// this is the element's text line by line, flattened into eights.
|
|
direct_text_line_rects(el) {
|
|
const out = [];
|
|
for (const r of __textLineRects(el)) {
|
|
out.push(r.left, r.top, r.width, r.height, r.top, r.right, r.bottom, r.left);
|
|
}
|
|
return out;
|
|
},
|
|
};
|