INTERACTIVE_ROLES = {
"button", "checkbox", "combobox", "link", "listbox", "menuitem",
"menuitemcheckbox", "menuitemradio", "option", "radio", "searchbox",
"slider", "spinbutton", "switch", "tab", "textbox", "treeitem"
}
CONTENT_ROLES = {
"heading", "main", "navigation", "region", "search"
}
LANDMARKS = {"main", "navigation", "region", "search", "form", "heading"}
JS_A11Y_EXTRACTOR = """
() => {
const INTERACTIVE_ROLES = new Set([
"button", "link", "checkbox", "menuitem", "option", "radio", "switch", "tab",
"treeitem", "textbox", "searchbox", "spinbutton", "combobox", "listbox", "slider"
]);
const results = [];
const isVisible = (el) => {
if (!el.getClientRects().length) return false;
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
if (style.pointerEvents === 'none') return false;
return true;
};
const walk = (node) => {
if (node.nodeType !== 1 || !isVisible(node)) return;
const rect = node.getBoundingClientRect();
if (rect.width < 2 || rect.height < 2) return;
const style = window.getComputedStyle(node);
const tagName = node.tagName;
const role = (node.getAttribute('role') || '').toLowerCase();
const isInput = ['INPUT', 'TEXTAREA', 'SELECT'].includes(tagName);
const isButtonOrLink = tagName === 'BUTTON' || tagName === 'A' || style.cursor === 'pointer';
const isInteractive = INTERACTIVE_ROLES.has(role) || isInput || isButtonOrLink;
if (isInteractive) {
const innerText = node.innerText || "";
const name = node.getAttribute('aria-label') || node.placeholder || innerText.substring(0, 50).trim() || node.value || tagName;
results.push({
tagName: tagName,
role: role || tagName.toLowerCase(),
name: (name || "").substring(0, 100).trim(),
placeholder: node.placeholder || '',
rect: {
x: Math.round(rect.x), y: Math.round(rect.y),
width: Math.round(rect.width), height: Math.round(rect.height)
}
});
}
for (const child of node.children) walk(child);
};
walk(document.body);
return results;
}
"""