You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Real pages frequently build semantic structure out of <div>/<span> + ARIA roles. A 1:1 DOM capture loses it — a <div role="heading" aria-level="2"> captures as an anonymous div and never becomes a Markdown heading. The accessibility tree (and Element.computedRole) recovers the intended semantics. This is the inverse of the layout-table unwrap (which strips false structure); here we restore true structure, and for LLM-facing Markdown it is the higher-value direction.
Depends on #48 (shared introspection layer + flowElement treatment classifier).
The anchor row is a useful side effect: <a>withouthref reports computedRole == "generic", not "link", so the same signal distinguishes real links from dropdown/JS toggles for the link path.
Suggested approach
Add a Rename case to the #48 classifier driven by a computedRole → tag map, applied under respectAccessibility:
privatevalROLE_TO_TAG=mapOf(
"heading" to "h", // level resolved separately from aria-level"list" to "ul",
"listitem" to "li",
// "img" -> handled with name resolution, see Notes
)
Headings: map to h1..h6 from aria-level (clamp 1..6, default 2). The element's own tag is replaced; children flow as the heading's content.
Lists:list→ul, listitem→li. Note ARIA role="list" does not distinguish ordered/unordered — emit ul (Markdown rendering treats both; if an <ol>/role="list" with numbering matters, that's a follow-up).
Only upgrade when the role does not already match the tag — a native <h2> already has computedRole == "heading"; don't double-process. The classifier should compare the effective role against what the tag would already produce.
Notes / decisions for the implementer
role="img" naming ties into the icon-resolution layer (IconResolution.kt / IconResolvers.kt) and into computedName (Use computedName as the authoritative accessible-name source #51). A role="img" node's accessible name is the alt-equivalent. Decide whether this issue handles img or defers it to the name issue; recommend deferring img and shipping headings + lists first (the clear wins).
Attribute carry-over: when renaming div→h2, which source attributes survive? Recommend dropping presentational div attributes and keeping only semantically-relevant ones (id), consistent with how simplifyHtml treats unwrapped containers downstream.
Roles can be explicit (role="heading") or implicit (a native element). computedRole reports both uniformly, which is what we want — but it means the "don't upgrade a native <h2>" guard above is essential.
Test approach
Deterministic explicit roles only (per #48), under the ChromeHeadlessExperimentalA11y launcher:
<div role="heading" aria-level="3"> → h3.
<div role="heading"> (no level) → h2.
<div role="list"><div role="listitem">x</div></div> → ul > li.
A native <h2> is emitted once as h2 (no double-upgrade).
Fallback: with the flag off / role unreadable, the div is captured verbatim.
role="button", role="navigation", landmark roles — usually noise for Markdown; simplifyHtml already drops most. Add later only if a real capture shows value.
Summary
Real pages frequently build semantic structure out of
<div>/<span>+ ARIA roles. A 1:1 DOM capture loses it — a<div role="heading" aria-level="2">captures as an anonymousdivand never becomes a Markdown heading. The accessibility tree (andElement.computedRole) recovers the intended semantics. This is the inverse of the layout-table unwrap (which strips false structure); here we restore true structure, and for LLM-facing Markdown it is the higher-value direction.Depends on #48 (shared introspection layer +
flowElementtreatment classifier).Examples (DOM in → desired semantic tag out)
computedRole<div role="heading" aria-level="2">Title</div>headingh2(level fromaria-level, default2)<div role="list"><div role="listitem">a</div></div>list/listitemul/li<span role="img" aria-label="rocket">🚀</span>img<a>toggle</a>(nohref)genericThe anchor row is a useful side effect:
<a>withouthrefreportscomputedRole == "generic", not"link", so the same signal distinguishes real links from dropdown/JS toggles for the link path.Suggested approach
Add a
Renamecase to the #48 classifier driven by acomputedRole → tagmap, applied underrespectAccessibility:h1..h6fromaria-level(clamp 1..6, default 2). The element's own tag is replaced; children flow as the heading's content.list→ul,listitem→li. Note ARIArole="list"does not distinguish ordered/unordered — emitul(Markdown rendering treats both; if an<ol>/role="list"with numbering matters, that's a follow-up).<h2>already hascomputedRole == "heading"; don't double-process. The classifier should compare the effective role against what the tag would already produce.Notes / decisions for the implementer
role="img"naming ties into the icon-resolution layer (IconResolution.kt/IconResolvers.kt) and intocomputedName(Use computedName as the authoritative accessible-name source #51). Arole="img"node's accessible name is the alt-equivalent. Decide whether this issue handlesimgor defers it to the name issue; recommend deferringimgand shipping headings + lists first (the clear wins).div→h2, which source attributes survive? Recommend dropping presentationaldivattributes and keeping only semantically-relevant ones (id), consistent with howsimplifyHtmltreats unwrapped containers downstream.role="heading") or implicit (a native element).computedRolereports both uniformly, which is what we want — but it means the "don't upgrade a native<h2>" guard above is essential.Test approach
Deterministic explicit roles only (per #48), under the
ChromeHeadlessExperimentalA11ylauncher:<div role="heading" aria-level="3">→h3.<div role="heading">(no level) →h2.<div role="list"><div role="listitem">x</div></div>→ul > li.<h2>is emitted once ash2(no double-upgrade).divis captured verbatim.Out of scope
role="img"→ label/emoji (defer to Use computedName as the authoritative accessible-name source #51 unless bundled deliberately).role="button",role="navigation", landmark roles — usually noise for Markdown;simplifyHtmlalready drops most. Add later only if a real capture shows value.role="list".