Skip to content

fix(html): extract <details>/<summary> content instead of discarding it - #4451

Open
r0h1tb wants to merge 3 commits into
Unstructured-IO:mainfrom
r0h1tb:fix/html-content-elements
Open

r0h1tb wants to merge 3 commits into
Unstructured-IO:mainfrom
r0h1tb:fix/html-content-elements

Conversation

@r0h1tb

@r0h1tb r0h1tb commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Problem

<details> and <summary> were both mapped to RemovedBlock, so partition_html() discarded the whole subtree. On the FAQ page from the issue, every question and answer disappears and the caller gets a shorter element list rather than an error:

>>> partition_html(text="<h1>Support FAQ</h1>"
...   "<details><summary>Which Bluetooth profiles are supported?</summary>"
...   "<p>Your device supports A2DP and HFP profiles.</p></details>")
[<Title 'Support FAQ'>]          # both the question and the answer are gone

Fix

<details> is an ordinary block container, so it becomes Flow, like <div>.

<summary> gets its own Heading subclass. Its label is emitted as a Title, including when it follows an expand/collapse icon or sits in a <p>/<div> wrapper, so chunk_by_title() opens a new section at each question and the question stays with its own answer.

The label has no heading level of its own, so its category_depth is one below h6, plus one per enclosing <details>. That nests it under whatever heading precedes it, ends it at the next heading of any level, and nests an inner <details> label under its parent's.

<summary> was also listed twice, under both phrasing and the removed-form group; there is now one mapping.

Tests

New tests in test_partition.py cover extraction, chunk sections, a <details> with no summary, nesting, icon- and wrapper-led labels, annotations and page number on the label, and parent_id/category_depth for a heading before and after a <details> and for nested <details>. The hierarchy tests fail without the depth change (parent_id is None).

partition/html, partition/common, documents and chunking: 1114 passed before, 1117 after, 40 skipped both times. test_common.py and chunking/test_basic.py can't be collected here without the extras, same on main. Ruff is clean.

Rebased on main, version 0.27.19.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

Shadow auto-approve: would auto-approve. Bug fix in HTML parsing: previously dropped <details>/<summary> content is now extracted, with tests pinned; low-risk mapping change.

Re-trigger cubic

@r0h1tb
r0h1tb force-pushed the fix/html-content-elements branch from 7b06863 to 67191e5 Compare August 21, 2026 21:43
@cragwolfe

Copy link
Copy Markdown
Contributor

High — a leading image or block wrapper makes a <summary> lose its Title boundary.

unstructured/partition/html/parser.py:1023 maps summary to Heading, but Heading supplies Title only to the initial text/phrasing run in Flow.iter_elements() (parser.py:365-376). Since img is a non-phrasing ImageBlock, <summary><img src="/chevron.svg" alt="">Second question?</summary> first emits the image, then processes Second question? through the generic tail path without Heading._ElementCls; it becomes Text, not Title. A <div>/<p>-wrapped question follows the same child-block path. As a result, chunk_by_title(..., combine_text_under_n_chars=0) never sees the second FAQ boundary and can merge that question/answer into the preceding section.

Please add summary-specific traversal that preserves the title role for the summary label across leading images and ordinary block wrappers, while keeping the details body and post-summary tail outside the title and avoiding a duplicate title for a nested heading. The regression should assert exact element types/order and two separate chunks; covering link/emphasis and page metadata on the label would also protect the annotations carried by this path.

(authored by codex)

@r0h1tb
r0h1tb force-pushed the fix/html-content-elements branch from 67191e5 to 51dabe8 Compare September 26, 2026 22:53
@r0h1tb

r0h1tb commented Sep 26, 2026

Copy link
Copy Markdown
Contributor Author

Good catch, thanks. Fixed in 51dabe8, and rebased onto main since the branch had gone stale.

<summary> now has its own element class that emits every run of label text as a Title, including text after a leading image like the chevron and text inside plain <p>/<div> wrappers (recursively). Images, a nested <h3> and other specialized blocks keep their own types, so a nested heading still produces exactly one Title. The summary's tail and the rest of the <details> body are still handled by the parent, so they stay outside the title.

New tests assert the exact element types and order plus two separate chunks for the icon case, and cover the <p>/<div> wrappers, a nested heading, text after the summary, and emphasis, link and page-number metadata on a wrapped label. The html, chunking, documents, md and text tests go from 1129 passed on main to 1139 on this branch, with no failures.

@cragwolfe cragwolfe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding

  • [P1] Preserve heading hierarchy for summary-generated Titles — unstructured/partition/html/parser.py:475-483. Summary._iter_label_elements() now emits every summary run as Title, but _ElementAccumulator._category_depth() maps a non-h1–h6 Title to depth 0. The shared set_element_hierarchy() stack then treats each summary as a top-level heading. I reproduced this on the exact head 51dabe8436708a28cb1713a90ad7ca00f78805b9: <h2>FAQ</h2><details><summary>Q1</summary><p>A1</p></details><h2>Shipping</h2><p>x</p> produces Q1 with parent_id=None instead of FAQ, and incorrectly assigns Shipping under Q1; nested Outer/Inner summaries are also siblings instead of parent/child. This silently regresses parent_id/section hierarchy for existing headings and affects downstream HTML/ontology/RAG consumers.

Please fix the hierarchy semantics for summary-derived Titles, preserving real heading levels and making nested details/summary relationships explicit, then add regression assertions for parent IDs and category depths (including a heading after a details block and nested details). The current extraction and focused text/type tests do not catch this.

Verification: uv run --locked reproduction against the exact PR head confirmed the incorrect parent IDs; all reported exact-head CI checks were green, but CI does not cover this hierarchy case.

(authored by codex)

cragwolfe pushed a commit that referenced this pull request Sep 27, 2026
## Problem

`<dl>`, `<dt>` and `<dd>` are mapped to `RemovedBlock`, so
`partition_html()` drops definition lists and everything inside them.
Documentation gets hit hardest: Sphinx renders every documented
function, class and attribute as a `<dl>`, so partitioning an API
reference page returns the headings and nothing else.

```python
partition_html(text=(
    '<h1>API reference</h1><dl class="py function"><dt>connect(host, port=5432)</dt>'
    "<dd><p>Open a connection to the database server.</p></dd></dl>"
))
# [Title('API reference')]
```

Glossaries and key/value lists disappear the same way, with no error.

## Fix

Map `<dl>` to `ListBlock` and `<dd>` to `ListItemBlock`, which the
parser already anticipates: the `ListBlock` docstring says "maybe a
`<dl>` element at some point", and the list-depth code already counts
`dl` ancestors for `dd` items. `<dt>` becomes a plain `BlockItem`, so a
term gets its type from its text like any paragraph. The v2 (ontology)
parser already keeps definition lists.

## Tests

Two new tests in `test_partition.py`: a glossary (exact element types,
text and `category_depth`) and a nested Sphinx-style API reference (all
text kept, in order). Without the fix they return `[('Title',
'Glossary', 1)]` and `['API reference']`. The html, chunking, documents,
md, text and email tests go from 1201 passed on main to 1203, with no
failures.

This touches the same mapping table as #4451 (`<details>`/`<summary>`),
so whichever lands second will need a small rebase.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/Unstructured-IO/unstructured/pull/4502?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
r0h1tb added 3 commits October 5, 2026 20:26
Both tags were mapped to `RemovedBlock`, so `partition_html()` dropped every
disclosure widget along with everything inside it. Pages that build their FAQ or
documentation out of accordions lost all of their questions and answers, and the
caller got a shorter element list rather than an error.

`<details>` is an ordinary block container, so it becomes `Flow` like `<div>`.
`<summary>` is the widget's heading -- the spec calls it "a summary, caption, or
legend for the rest of the contents" -- so it becomes `Heading` and emits a
`Title`. That is what the reporter asked for, and it is what makes the fix
useful downstream: `chunk_by_title()` opens a new section at each `Title`, so a
question stays attached to its own answer instead of blending into its
neighbours.

`<summary>` was listed twice, under both phrasing and the removed-form group;
there is now a single mapping.

Resolves Unstructured-IO#3919.

Signed-off-by: Rohit Behera <126186063+r0h1tb@users.noreply.github.com>
…pper

A <summary> whose label followed an <img> (an expand/collapse chevron) or
sat inside a <p> or <div> came out as Text or NarrativeText, because
Heading only applies Title to its leading text. chunk_by_title() then saw
no boundary and merged that entry into the previous one.

Give <summary> its own element class that makes every run of label text a
Title, descending into plain wrappers while images, nested headings and
the like keep their own types.
A <summary> label is a Title with no heading tag, so it got
category_depth 0 and was treated as a top-level heading: it lost the
heading above it as its parent, the next h2 was nested under it, and a
nested <details> label became its sibling.

Place the label one level below h6, plus one per enclosing <details>,
so it nests under whatever heading precedes it and ends at the next
heading of any level.
@r0h1tb
r0h1tb force-pushed the fix/html-content-elements branch from 51dabe8 to fbd8e9b Compare October 5, 2026 15:02
@r0h1tb

r0h1tb commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in fbd8e9b. A summary label now gets category_depth one below h6, plus one per enclosing <details>, so it takes the heading before it as its parent and ends at the next heading. In your example Q1's parent is FAQ and Shipping is top-level again, and nested Outer/Inner labels come out as parent/child (depths 6 and 7). Added parent_id/category_depth assertions for both cases and for a wrapped label.

One thing I left alone here: a leading <img> in the summary (the chevron case) still drops the heading from the parent chain, because Image isn't a valid child of Title in HIERARCHY_RULE_SET. That's the same issue you flagged on #4503, so I'll handle it there instead of in both PRs.

Also rebased on main, now 0.27.19.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants