Skip to content

Recover semantic structure from ARIA roles (div-soup → headings, lists) #49

Description

@morisil

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

Examples (DOM in → desired semantic tag out)

DOM computedRole Emit as
<div role="heading" aria-level="2">Title</div> heading h2 (level from aria-level, default 2)
<div role="list"><div role="listitem">a</div></div> list / listitem ul / li
<span role="img" aria-label="rocket">🚀</span> img an image/label node (see Notes)
<a>toggle</a> (no href) generic NOT a link — see #50 / leave as content

The anchor row is a useful side effect: <a> without href 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:

private val ROLE_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: listul, listitemli. 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 divh2, 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.

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; simplifyHtml already drops most. Add later only if a real capture shows value.
  • Ordered-list numbering recovery from role="list".

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions