Skip to main content

SNAPSHOT_TREE_JS

Constant SNAPSHOT_TREE_JS 

Source
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.