Skip to content

fix: add JATS and AsciiDoc captions under the parent of their item - #5039

Open
ska2704 wants to merge 1 commit into
docling-project:mainfrom
ska2704:fix/jats-asciidoc-caption-parent
Open

ska2704 wants to merge 1 commit into
docling-project:mainfrom
ska2704:fix/jats-asciidoc-caption-parent

Conversation

@ska2704

@ska2704 ska2704 commented Oct 27, 2026

Copy link
Copy Markdown

The JATS backend (figures and tables) and the AsciiDoc backend (pictures, tables and code block titles) created captions with doc.add_text() and no parent, so each caption became a child of body. The captioned item stayed in its section, and the captions were listed after the last section of the document.

The fix passes the item's parent to the caption. The caption is now a sibling that comes just before its item, the same layout the Office backends use since #4190 and the HTML backend uses for table captions. In AsciiDoc the parent expression was repeated for the caption and the item, so it is now computed once in _add_picture_item and _add_block.

I found this while checking tree invariants over the test inputs of the non-PDF backends (each child points back to its parent, no orphans, and every caption has the same parent as its item, or is its child). After this change the only remaining failure in that check is the EPUB sample, which goes through the HTML backend and is covered by #5036 / #5037.

Issue resolved by this Pull Request:
Resolves #5038

Checklist:

  • Documentation has been updated, if necessary. (not needed, no user-facing API change)
  • Examples have been added, if necessary. (not needed)
  • Tests have been added, if necessary.

Review evidence:

Before, for a JATS article with a figure and a table in section "One", followed by section "Two":

    section_header: 'One'
      text: 'Para one.'
      picture: ''
      table: ''
    section_header: 'Two'
      text: 'Para two.'
  caption: 'Figure 1. Fig cap.'
  caption: 'Table 1. Tab cap.'

After:

    section_header: 'One'
      text: 'Para one.'
      caption: 'Figure 1. Fig cap.'
      picture: ''
      caption: 'Table 1. Tab cap.'
      table: ''
    section_header: 'Two'
      text: 'Para two.'

AsciiDoc with .Title before an image, a table and a listing block gives the same before/after shape.

Reference data: regenerated with DOCLING_GEN_TEST_DATA=1 for the three JATS samples (elife-56337, pntd.0008301, pone.0234687). I compared old and new JSON with a script: the texts, tables and pictures are identical, and only the caption parent refs and the children lists change (13 figure captions and 9 table captions move from body to their section). The .itxt diffs are the same move. No Markdown reference changed, the AsciiDoc ground truth is Markdown only, so no AsciiDoc data changed.

Tests:

  • New test_backend_jats_captions.py::test_jats_captions_stay_in_the_section_of_their_figure_and_table and test_backend_asciidoc.py::test_block_title_caption_stays_in_the_section_of_its_block (picture, table, code). All 4 fail on main with the caption parent #/body and pass with the fix. The JATS test is in its own module because test_backend_jats.py is at 1494 lines and the max-lines hook stops at 1500.
  • uv run pytest tests/test_backend_jats*.py tests/test_backend_asciidoc.py: 155 passed.
  • uv run pytest tests/test_backend_*.py tests/test_groundtruth_diff.py -n 4: 1530 passed, 14 skipped. The 1 failure and 4 errors (test_backend_webp, the pdfium tests, test_conversion_result_json) also happen on unchanged main in my environment, because rtree and the OCR model extras are not installed there.
  • make validate: all hooks pass (ruff, ty, tach, max-lines, license header).

Not run: the full make test with all extras (PDF/OCR/VLM model tests). Those code paths do not call the two changed backends.

#4291 also edits caption code in jats_backend.py (the caption text), but different lines, so the two should not conflict.

The JATS backend (figures, tables) and the AsciiDoc backend (pictures,
tables, code blocks) created the caption with doc.add_text() and no
parent, so every caption became a child of the body. The captioned item
stays in its section, while the caption is listed after the last section
of the document.

Pass the item's parent to the caption, so the caption is a sibling that
comes just before its item, as the Office backends do since docling-project#4190.

Signed-off-by: SK Arin <162553727+ska2704@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

Thanks @ska2704, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🔴 1 of 2 protections blocking · waiting on 👀 reviews

Protection Waiting on
🔴 Require two reviewer for test updates 👀 reviews
🟢 Enforce conventional commit —

🔴 Require two reviewer for test updates

Waiting for

  • #approved-reviews-by >= 2
This rule is failing.

When test data is updated, we require two reviewers

  • #approved-reviews-by >= 2

Show 1 satisfied protection

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

@github-actions github-actions Bot added ai:ci-safe AI triage found no CI-safety concern (advisory) asciidoc issue related to asciidoc backend bug Something isn't working docling-document reading_order xml issue related to supported schema-specific XML formats labels Oct 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

AI triage (advisory)

Checked commit d5ad100c9f. The analysis read the diff only. It did not run code from this pull request.

Fixes caption parenting in the JATS and AsciiDoc backends: captions created by doc.add_text() without a parent became children of body instead of siblings of their captioned item. The fix passes the item's parent to each caption call so captions stay inside their section.

Topic labels added: bug, asciidoc, xml, docling-document, reading_order.

CI safety: no concern found. A maintainer must still approve CI.

5 related pull request(s) in the same code
PR State Signal Assessment
#5037 open also references issue #5036; referenced as #5037 related: Fixes the same caption-parenting class of bug but in the HTML backend; no shared files with this PR.
#5010 open changes the same code related: Touches asciidoc_backend.py and tests/test_backend_asciidoc.py but fixes a different problem: filling table rows from the column count of the first line.
#3891 open changes the same code related: Touches both asciidoc_backend.py and jats_backend.py but for the inline-segment whitespace contract, not caption parenting.
#4291 open referenced as #4291 related: Touches jats_backend.py for a different JATS caption bug (literal "None" text in captions), not caption placement.
#4190 merged referenced as #4190 related: Already merged; it fixed caption parenting in PPTX, DOCX, and XLSX backends. This PR applies the same pattern to JATS and AsciiDoc, extending the work without conflicting with it.

Reference data changes

Change Files
structure 3
text 3
  • structure tests/data/jats/groundtruth/elife-56337.nxml.json: reading order changed
  • structure tests/data/jats/groundtruth/pntd.0008301.nxml.json: reading order changed
  • structure tests/data/jats/groundtruth/pone.0234687.nxml.json: reading order changed
  • text tests/data/jats/groundtruth/elife-56337.nxml.itxt: +271 / -271 lines
  • text tests/data/jats/groundtruth/pntd.0008301.nxml.itxt: +194 / -194 lines
  • text tests/data/jats/groundtruth/pone.0234687.nxml.itxt: +519 / -519 lines

Assessment: expected. The fix moves captions from body into their parent section, so reading order changes in the JSON files and caption lines are repositioned in the .itxt files. The equal addition and deletion counts confirm content is reordered, not altered.

The ai:* labels are advisory. They do not approve CI and they do not add tests:full. A new push runs this check again.

@github-actions github-actions Bot added the ai:review-lgtm AI first review found no blocker or major issue (advisory) label Oct 27, 2026

@github-actions github-actions 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.

AI first review (advisory): no blocker found

This review covers the full diff at d5ad100c9f. It read the code and did not run it.

The changes set the parent node for captions in the JATS and AsciiDoc backends so that captions remain siblings of their items. I verified the parent assignment and document structure across pictures, tables, and code blocks in both backends.

A maintainer makes the merge decision. This review never approves the pull request.

table_text: str = f"{label}{' ' if label and caption else ''}{caption}"
table_caption: TextItem | None = (
doc.add_text(label=DocItemLabel.CAPTION, text=table_text)
doc.add_text(label=DocItemLabel.CAPTION, text=table_text, parent=parent)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Small edge case in _add_table: the caption is created before the data is not None check. parse_table_data returns None when the table contains a nested table, so in that case the caption gets added without its table. Before this change that lone caption ended up at the end of body, but now it sits inside the section with nothing next to it. Maybe move the doc.add_text call inside the if data is not None: block so the caption is only added together with the table? A JATS table-wrap whose table contains another table would make a quick test for it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You're right that the caption is created before the data is not None check. That order is the same as on main, and I think it's the better one to keep. When parse_table_data returns None the table is skipped, and the caption is the only text left from that table-wrap. Moving add_text into the if would drop it from the output entirely, Markdown included, where main still prints it (as the last line of the document).

For a table-wrap whose table contains another table, the tree on this branch is:

section_header: 'One'
  text: 'Before.'
  caption: 'Table 1. Nested.'
  text: 'After.'
section_header: 'Two'
  text: 'Para two.'

On main the same caption is the last child of body, after section "Two". So the lone caption now sits where the table was instead of at the end. Real support for nested tables is the TODO in parse_table_data, and I'd rather keep that out of this PR. If the maintainers prefer to drop the caption when the table is skipped, that's a small change and I can add a test for it.

@codecov

codecov Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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

ai:ci-safe AI triage found no CI-safety concern (advisory) ai:review-lgtm AI first review found no blocker or major issue (advisory) asciidoc issue related to asciidoc backend bug Something isn't working docling-document reading_order xml issue related to supported schema-specific XML formats

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JATS and AsciiDoc: captions are children of body instead of the section of their item

3 participants