pub const SNAPSHOT_TREE_JS: &str = "/* bc:snapshot */\n// Approximate accessibility tree for engines without a native AX-tree\n// command (WebDriver BiDi / Firefox). Emits JSON in the shape of CDP\'s\n// `Accessibility.getFullAXTree` so the Rust renderer, `find`, and the ref\n// table are shared with the Chromium path.\n//\n// Element refs are integer ids kept in a page-side registry\n// (`window.__bcRefs`); the document token (`window.__bcDocToken`) is a\n// random per-document stamp reported as the root\'s `backendDOMNodeId`.\n//\n// Called as `(fn)(maxNodes)` through `script.callFunction`; returns a JSON\n// string so BiDi\'s object-depth serialisation limits never apply.\n(function (maxNodes) {\n const w = window;\n const doc = document;\n if (!w.__bcDocToken) {\n w.__bcDocToken = 4294967296 + Math.floor(Math.random() * (9007199254740992 - 4294967296));\n }\n if (!w.__bcRefs) w.__bcRefs = { nextId: 1, byId: new Map(), byEl: new WeakMap() };\n const reg = w.__bcRefs;\n const hasWeakRef = typeof WeakRef === \'function\';\n function idFor(el) {\n let id = reg.byEl.get(el);\n if (!id) {\n id = reg.nextId++;\n reg.byEl.set(el, id);\n reg.byId.set(id, hasWeakRef ? new WeakRef(el) : el);\n }\n return id;\n }\n\n const SKIP = new Set([\'SCRIPT\', \'STYLE\', \'NOSCRIPT\', \'TEMPLATE\', \'HEAD\', \'META\', \'LINK\', \'TITLE\', \'BASE\']);\n const SECTIONING = new Set([\'ARTICLE\', \'SECTION\', \'MAIN\', \'NAV\', \'ASIDE\']);\n const NAME_FROM_CONTENT = new Set([\n \'button\', \'link\', \'heading\', \'cell\', \'columnheader\', \'rowheader\', \'option\', \'tab\',\n \'menuitem\', \'menuitemcheckbox\', \'menuitemradio\', \'checkbox\', \'radio\', \'switch\',\n \'treeitem\', \'tooltip\', \'LabelText\', \'summary\',\n ]);\n const CHECKABLE = new Set([\'checkbox\', \'radio\', \'switch\', \'menuitemcheckbox\', \'menuitemradio\']);\n\n function collapse(s, max) {\n return String(s || \'\').replace(/\\s+/g, \' \').trim().slice(0, max || 200);\n }\n function attr(el, name) {\n const v = el.getAttribute(name);\n return v === null ? \'\' : v;\n }\n function inputType(el) {\n return (attr(el, \'type\') || \'text\').toLowerCase();\n }\n function visible(el) {\n if (el.hidden || attr(el, \'aria-hidden\') === \'true\') return false;\n if (typeof el.checkVisibility === \'function\') {\n try { return el.checkVisibility(); } catch (e) { /* fall through */ }\n }\n const cs = getComputedStyle(el);\n return cs.display !== \'none\' && cs.visibility !== \'hidden\';\n }\n function inSectioning(el) {\n let p = el.parentElement;\n while (p) {\n if (SECTIONING.has(p.tagName.toUpperCase())) return true;\n p = p.parentElement;\n }\n return false;\n }\n function childrenOf(el) {\n if (el.shadowRoot) return el.shadowRoot.childNodes;\n if (el.tagName.toUpperCase() === \'SLOT\') {\n const assigned = el.assignedNodes({ flatten: true });\n if (assigned.length) return assigned;\n }\n return el.childNodes;\n }\n\n function roleOf(el) {\n const aria = attr(el, \'role\').trim().split(/\\s+/)[0];\n if (aria) return aria;\n const tag = el.tagName.toUpperCase();\n switch (tag) {\n case \'A\': case \'AREA\': return el.hasAttribute(\'href\') ? \'link\' : \'generic\';\n case \'BUTTON\': return \'button\';\n case \'SUMMARY\': return \'button\';\n case \'INPUT\':\n switch (inputType(el)) {\n case \'button\': case \'submit\': case \'reset\': case \'image\': return \'button\';\n case \'checkbox\': return \'checkbox\';\n case \'radio\': return \'radio\';\n case \'search\': return \'searchbox\';\n case \'range\': return \'slider\';\n case \'number\': return \'spinbutton\';\n case \'hidden\': return null;\n default: return \'textbox\';\n }\n case \'TEXTAREA\': return \'textbox\';\n case \'SELECT\': return (el.multiple || el.size > 1) ? \'listbox\' : \'combobox\';\n case \'OPTION\': return \'option\';\n case \'IMG\': return \'image\';\n case \'H1\': case \'H2\': case \'H3\': case \'H4\': case \'H5\': case \'H6\': return \'heading\';\n case \'NAV\': return \'navigation\';\n case \'MAIN\': return \'main\';\n case \'ASIDE\': return \'complementary\';\n case \'FORM\': return \'form\';\n case \'DIALOG\': return \'dialog\';\n case \'ARTICLE\': return \'article\';\n case \'SECTION\':\n return el.hasAttribute(\'aria-label\') || el.hasAttribute(\'aria-labelledby\') ? \'region\' : \'generic\';\n case \'HEADER\': return inSectioning(el) ? \'generic\' : \'banner\';\n case \'FOOTER\': return inSectioning(el) ? \'generic\' : \'contentinfo\';\n case \'UL\': case \'OL\': case \'MENU\': return \'list\';\n case \'LI\': return \'listitem\';\n case \'TABLE\': return \'table\';\n case \'TR\': return \'row\';\n case \'TD\': return \'cell\';\n case \'TH\': return \'columnheader\';\n case \'P\': return \'paragraph\';\n case \'LABEL\': return \'LabelText\';\n case \'IFRAME\': case \'FRAME\': return \'Iframe\';\n case \'DETAILS\': case \'FIELDSET\': return \'group\';\n case \'HR\': return \'separator\';\n case \'PROGRESS\': return \'progressbar\';\n case \'METER\': return \'meter\';\n default: {\n const ce = attr(el, \'contenteditable\');\n if (el.hasAttribute(\'contenteditable\') && ce !== \'false\') return \'textbox\';\n return \'generic\';\n }\n }\n }\n\n // Visible text of a subtree, honouring aria-label / alt on descendants.\n function textOf(node, exclude, budget) {\n budget = budget || { left: 1000 };\n if (budget.left <= 0 || node === exclude) return \'\';\n if (node.nodeType === 3) {\n const t = node.nodeValue || \'\';\n budget.left -= t.length;\n return t;\n }\n if (node.nodeType !== 1) return \'\';\n const el = node;\n const tag = el.tagName.toUpperCase();\n if (SKIP.has(tag) || !visible(el)) return \'\';\n const al = attr(el, \'aria-label\');\n if (al.trim()) return al;\n if ((tag === \'IMG\' || tag === \'AREA\') && el.hasAttribute(\'alt\')) return attr(el, \'alt\');\n let out = \'\';\n for (const c of childrenOf(el)) out += textOf(c, exclude, budget) + \' \';\n return out;\n }\n\n function nameOf(el, role) {\n const tag = el.tagName.toUpperCase();\n const lb = attr(el, \'aria-labelledby\');\n if (lb.trim()) {\n const parts = lb.trim().split(/\\s+/).map((id) => {\n const t = el.ownerDocument.getElementById(id);\n return t ? textOf(t) : \'\';\n });\n const joined = collapse(parts.join(\' \'));\n if (joined) return { name: joined, from: \'labelledby\' };\n }\n const al = attr(el, \'aria-label\');\n if (al.trim()) return { name: collapse(al), from: \'aria-label\' };\n if (el.labels && el.labels.length) {\n const t = collapse(Array.from(el.labels).map((l) => textOf(l, el)).join(\' \'));\n if (t) return { name: t, from: \'label\' };\n }\n if (tag === \'IMG\' || tag === \'AREA\' || (tag === \'INPUT\' && inputType(el) === \'image\')) {\n if (el.hasAttribute(\'alt\')) return { name: collapse(attr(el, \'alt\')), from: \'alt\' };\n }\n if (tag === \'INPUT\') {\n const type = inputType(el);\n if (type === \'submit\' || type === \'reset\' || type === \'button\') {\n const v = el.value || (type === \'submit\' ? \'Submit\' : type === \'reset\' ? \'Reset\' : \'\');\n if (v) return { name: collapse(v), from: \'value\' };\n }\n }\n if (tag === \'FIELDSET\') {\n const lg = el.querySelector(\':scope > legend\');\n if (lg) return { name: collapse(textOf(lg)), from: \'legend\' };\n }\n if (tag === \'TABLE\') {\n const c = el.querySelector(\':scope > caption\');\n if (c) return { name: collapse(textOf(c)), from: \'caption\' };\n }\n if (tag === \'FIGURE\') {\n const c = el.querySelector(\':scope > figcaption\');\n if (c) return { name: collapse(textOf(c)), from: \'figcaption\' };\n }\n if (tag === \'SVG\') {\n const t = el.querySelector(\'title\');\n if (t) return { name: collapse(t.textContent), from: \'title\' };\n }\n if (tag === \'OPTION\') {\n // Options are not \"visible\" while the select is closed, so the\n // content walk below would skip them.\n const t = collapse(el.label || el.textContent);\n if (t) return { name: t, from: \'content\' };\n }\n if (NAME_FROM_CONTENT.has(role)) {\n const t = collapse(textOf(el));\n if (t) return { name: t, from: \'content\' };\n }\n const title = attr(el, \'title\');\n if (title.trim()) return { name: collapse(title), from: \'title\' };\n const ph = attr(el, \'placeholder\');\n if (ph.trim()) return { name: collapse(ph), from: \'placeholder\' };\n return { name: \'\', from: null };\n }\n\n function descriptionOf(el, from) {\n const db = attr(el, \'aria-describedby\');\n if (db.trim()) {\n const parts = db.trim().split(/\\s+/).map((id) => {\n const t = el.ownerDocument.getElementById(id);\n return t ? textOf(t) : \'\';\n });\n const joined = collapse(parts.join(\' \'));\n if (joined) return joined;\n }\n if (from !== \'title\' && attr(el, \'title\').trim()) return collapse(attr(el, \'title\'));\n if (from !== \'placeholder\' && attr(el, \'placeholder\').trim()) return collapse(attr(el, \'placeholder\'));\n return \'\';\n }\n\n function valueOf(el, role) {\n const tag = el.tagName.toUpperCase();\n if (tag === \'INPUT\') {\n const type = inputType(el);\n if ([\'password\', \'checkbox\', \'radio\', \'file\', \'submit\', \'button\', \'reset\', \'image\', \'hidden\'].includes(type)) return \'\';\n return collapse(el.value);\n }\n if (tag === \'TEXTAREA\') return collapse(el.value);\n if (tag === \'SELECT\') {\n const o = el.selectedOptions && el.selectedOptions[0];\n return o ? collapse(o.label || o.textContent) : \'\';\n }\n if (role === \'slider\' || role === \'spinbutton\' || role === \'progressbar\' || role === \'meter\') {\n return collapse(attr(el, \'aria-valuetext\') || attr(el, \'aria-valuenow\') || el.value || \'\');\n }\n if (role === \'textbox\' && el.isContentEditable) return collapse(el.textContent);\n return \'\';\n }\n\n function propsOf(el, role) {\n const props = [];\n const push = (n, v) => props.push({ name: n, value: { value: v } });\n const tag = el.tagName.toUpperCase();\n let disabled = attr(el, \'aria-disabled\') === \'true\';\n try { disabled = disabled || el.matches(\':disabled\'); } catch (e) { /* ignore */ }\n if (disabled) push(\'disabled\', true);\n const focusable = !disabled && (el.tabIndex >= 0 || el.hasAttribute(\'tabindex\'));\n if (focusable) push(\'focusable\', true);\n if (CHECKABLE.has(role)) {\n let c;\n if (el.indeterminate) c = \'mixed\';\n else if (tag === \'INPUT\') c = el.checked ? \'true\' : \'false\';\n else {\n const a = attr(el, \'aria-checked\');\n c = a === \'true\' ? \'true\' : a === \'mixed\' ? \'mixed\' : \'false\';\n }\n push(\'checked\', c);\n }\n const pressed = attr(el, \'aria-pressed\');\n if (pressed === \'true\' || pressed === \'mixed\') push(\'pressed\', pressed);\n if (attr(el, \'aria-expanded\') === \'true\') push(\'expanded\', true);\n else if (tag === \'SUMMARY\' && el.parentElement && el.parentElement.tagName.toUpperCase() === \'DETAILS\' && el.parentElement.open) push(\'expanded\', true);\n if (tag === \'OPTION\' ? el.selected : attr(el, \'aria-selected\') === \'true\') push(\'selected\', true);\n try { if (el.getRootNode().activeElement === el) push(\'focused\', true); } catch (e) { /* ignore */ }\n if (el.required || attr(el, \'aria-required\') === \'true\') push(\'required\', true);\n if (el.readOnly || attr(el, \'aria-readonly\') === \'true\') push(\'readonly\', true);\n if (role === \'heading\') {\n const m = /^H([1-6])$/.exec(tag);\n const lvl = attr(el, \'aria-level\') || (m ? m[1] : \'\');\n if (lvl) push(\'level\', Number(lvl));\n }\n if (role === \'link\' && el.href) push(\'url\', String(el.href).slice(0, 2048));\n return props;\n }\n\n const nodes = [];\n let count = 0;\n let truncated = false;\n let textSeq = 0;\n const root = {\n nodeId: \'root\',\n backendDOMNodeId: w.__bcDocToken,\n role: { value: \'RootWebArea\' },\n name: { value: collapse(doc.title) },\n childIds: [],\n };\n nodes.push(root);\n\n function emitText(parent, text) {\n const t = collapse(text, 1000);\n if (!t) return;\n if (count >= maxNodes) { truncated = true; return; }\n const id = \'t\' + (++textSeq);\n nodes.push({ nodeId: id, parentId: parent.nodeId, role: { value: \'StaticText\' }, name: { value: t }, childIds: [] });\n parent.childIds.push(id);\n count++;\n }\n\n function emitLeaf(el, parent, role, name) {\n if (count >= maxNodes) { truncated = true; return; }\n const num = idFor(el);\n const id = \'n\' + num;\n const n = { nodeId: id, parentId: parent.nodeId, backendDOMNodeId: num, role: { value: role }, name: { value: name }, childIds: [] };\n const props = propsOf(el, role);\n if (props.length) n.properties = props;\n nodes.push(n);\n parent.childIds.push(id);\n count++;\n }\n\n function walk(node, parent) {\n if (truncated) return;\n if (node.nodeType === 3) { emitText(parent, node.nodeValue); return; }\n if (node.nodeType !== 1) return;\n const el = node;\n const tag = el.tagName.toUpperCase();\n if (SKIP.has(tag)) return;\n if (tag !== \'OPTION\' && !visible(el)) return;\n if (tag === \'SVG\') {\n const nm = nameOf(el, \'image\');\n if (nm.name) emitLeaf(el, parent, \'image\', nm.name);\n return;\n }\n if (tag === \'IFRAME\' || tag === \'FRAME\') {\n emitLeaf(el, parent, \'Iframe\', nameOf(el, \'Iframe\').name);\n return;\n }\n const role = roleOf(el);\n if (role === null) return;\n const nm = nameOf(el, role);\n const value = valueOf(el, role);\n const props = propsOf(el, role);\n const emit = !(role === \'generic\' && !nm.name && !value && props.length === 0);\n let cur = parent;\n if (emit) {\n if (count >= maxNodes) { truncated = true; return; }\n const num = idFor(el);\n const id = \'n\' + num;\n const n = { nodeId: id, parentId: parent.nodeId, backendDOMNodeId: num, role: { value: role }, name: { value: nm.name }, childIds: [] };\n if (value) n.value = { value: value };\n const desc = descriptionOf(el, nm.from);\n if (desc) n.description = { value: desc };\n if (props.length) n.properties = props;\n nodes.push(n);\n parent.childIds.push(id);\n count++;\n cur = n;\n }\n // Form controls have no meaningful children; everything else recurses.\n if (tag === \'INPUT\' || tag === \'TEXTAREA\') return;\n for (const c of childrenOf(el)) walk(c, cur);\n }\n\n const start = doc.body || doc.documentElement;\n if (start) for (const c of childrenOf(start)) walk(c, root);\n return JSON.stringify({ nodes: nodes, truncated: truncated });\n})\n";Expand description
The walker itself; see the header comment in the file.