Skip to content

fix(jats): stop rendering the literal text None in figure and table captions - #4291

Open
chrikrah wants to merge 2 commits into
docling-project:mainfrom
chrikrah:fix/jats-none-caption
Open

chrikrah wants to merge 2 commits into
docling-project:mainfrom
chrikrah:fix/jats-none-caption

Conversation

@chrikrah

Copy link
Copy Markdown
Contributor

Two places in the JATS backend write None into text a reader sees.

_add_figure_captions sets caption = None when a <fig> carries a <label> and no <caption>. That combination is legal, and common for a numbered but uncaptioned figure. The f-string at :917 then renders the four characters None straight after the label:

<fig id="f1"><label>Figure 1.</label><graphic/></fig>
caption text: 'Figure 1.None'

_add_tables reads table["label"] = node.xpath("label")[0].text with no guard at :1175. An empty <label/> has .text of None in lxml, so a table with neither a real label nor a caption still produces a caption item:

<table-wrap id="t1"><label/><table><tbody><tr><td>A</td><td>B</td></tr></tbody></table></table-wrap>
caption text: 'None'

After the change the first reads Figure 1. and the second emits no caption item at all.

The file already has the pattern

:634 reads etal_node[0].text or DEFAULT_TEXT_ETAL. Both sites now keep the empty-string default the surrounding code already uses.

Tests

$ python -m pytest tests/test_backend_jats.py -q
59 passed

$ python -m ruff check docling/backend/xml/jats_backend.py tests/test_backend_jats.py
All checks passed!
$ python -m ruff format --check docling/backend/xml/jats_backend.py tests/test_backend_jats.py
2 files already formatted

Two new cases. Revert jats_backend.py alone, keep them, and both fail.

No groundtruth file changed. grep -rl None tests/data/jats/groundtruth/ returns nothing, so no committed fixture reaches either path today, which is why this went uncaught.

Scope

The <fig> case needs label present and caption absent. A figure with both, or with neither, already rendered correctly. The table case needs an empty <label/> element rather than a missing one.

Duplicate search

Three open pull requests touch docling/backend/xml/jats_backend.py: #4272, #3891 and #3791. None mentions _add_figure_captions, table["label"] or caption = None.

gh issue list --state all --search "jats caption None" and gh pr list --state all --search "jats figure caption" return nothing covering this.

Checklist:

  • Documentation has been updated, if necessary.
    No document describes how a caption is composed from <label> and <caption>.
  • Examples have been added, if necessary.
    The change corrects existing output rather than adding a capability.
  • Tests have been added, if necessary.

@github-actions

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

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

@mergify

mergify Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 Merge protection satisfied — ready to merge.

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)(?:\(.+\))?(!)?:

@codecov

codecov Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@ceberam ceberam added bug Something isn't working xml issue related to supported schema-specific XML formats labels Sep 18, 2026
@chrikrah
chrikrah force-pushed the fix/jats-none-caption branch from 4a9b14e to 30857fd Compare September 18, 2026 08:26

@ceberam ceberam left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @chrikrah for your contribution!
Both fixes are correct. The _add_figure_captions change properly defaults caption to "" instead of None, preventing the literal string "None" from appearing after the label in the f-string. The _add_tables change applies the same or "" guard already used at line 638 for <etal>. Scope is narrow — no regressions, no performance impact.

I have added a minor style comment below.
In addition, please review the inline comments. The three-line comment before caption: str = "" in _add_figure_captions explains why None was not used — that is authoring context, and the type annotation already says it all. Remove it. The single-line comment in _add_tables is more defensible (lxml's None-on-empty-element is non-obvious), but trim the second clause to remove the fix-reasoning, for instance: # lxml sets .text to None for an empty element; fall back to "".

Comment thread tests/test_backend_jats.py Outdated
Comment on lines +1041 to +1044
def _caption_texts(doc: DoclingDocument) -> list[str]:
return [
t.text for t in doc.texts if str(getattr(t, "label", "")).endswith("caption")
]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All items from doc.texts are TextItem instances and always have a .label field. The project coding standards prohibit broad getattr(...)/hasattr(...) patterns. The rest of the test file uses direct attribute access. Replace with:

Suggested change
def _caption_texts(doc: DoclingDocument) -> list[str]:
return [
t.text for t in doc.texts if str(getattr(t, "label", "")).endswith("caption")
]
def _caption_texts(doc: DoclingDocument) -> list[str]:
return [t.text for t in doc.texts if t.label == DocItemLabel.CAPTION]

chrikrah added a commit to chrikrah/docling that referenced this pull request Sep 26, 2026
Three points from ceberam, all about how the change reads:

- The comment before `caption: str = ""` in `_add_figure_captions` said what
  the annotation already says, so it is gone.
- The comment in `_add_tables` is down to the one line that carries the fact
  a reader cannot see: lxml sets `.text` to None for an empty element.
- `_caption_texts` in the test compares `t.label` to `DocItemLabel.CAPTION`
  directly. The `getattr` and the string suffix match were defensive for no
  reason; `DocItemLabel` was already imported.

No behaviour change. 59 passed in tests/test_backend_jats.py, and reverting
jats_backend.py alone to 815f34a still fails both new cases (2 failed,
57 passed), so the direct comparison pins the fix as well as the old helper.

Signed-off-by: Christopher Krah <48338417+chrikrah@users.noreply.github.com>
@chrikrah

Copy link
Copy Markdown
Contributor Author

All three taken, in c9f9b96.

The three-line comment in _add_figure_captions is gone. It was authoring context, as you say. The annotation caption: str = "" states what the value is, and why it beats None belongs in the pull request rather than the file.

The _add_tables comment is now the single line you suggested, with the fix-reasoning clause dropped:

# lxml sets .text to None for an empty element; fall back to "".

And _caption_texts uses direct attribute access:

return [t.text for t in doc.texts if t.label == DocItemLabel.CAPTION]

That file already imports DocItemLabel, so nothing else moved.

59 passed. Revert jats_backend.py alone, keep the tests, and 2 still fail, so the two cases pin the production change rather than the helper.

ruff check and ruff format --check are clean on both files.

…aptions

Two places write None into text a reader sees.

_add_figure_captions sets caption = None when a <fig> has a <label> and no
<caption>, which is legal and common for a numbered but uncaptioned figure. The
f-string at :917 then renders the four characters None straight after the label:

  <fig><label>Figure 1.</label><graphic/></fig>  ->  'Figure 1.None'

_add_tables reads table["label"] = node.xpath("label")[0].text without a
guard. An empty <label/> has .text of None in lxml, so a table with neither a
real label nor a caption still produces a caption item reading None:

  <table-wrap><label/><table>...</table></table-wrap>  ->  'None'

Both now keep the empty-string default the surrounding code already uses, the
same way :634 reads etal_node[0].text or DEFAULT_TEXT_ETAL.

Tests: 59 passed in tests/test_backend_jats.py. Two new cases; reverting
jats_backend.py alone fails both. No groundtruth file changes, and no committed
JATS groundtruth contains the string None today, so no current fixture reaches
either path.

Signed-off-by: Christopher Krah <48338417+chrikrah@users.noreply.github.com>
Three points from ceberam, all about how the change reads:

- The comment before `caption: str = ""` in `_add_figure_captions` said what
  the annotation already says, so it is gone.
- The comment in `_add_tables` is down to the one line that carries the fact
  a reader cannot see: lxml sets `.text` to None for an empty element.
- `_caption_texts` in the test compares `t.label` to `DocItemLabel.CAPTION`
  directly. The `getattr` and the string suffix match were defensive for no
  reason; `DocItemLabel` was already imported.

No behaviour change. 59 passed in tests/test_backend_jats.py, and reverting
jats_backend.py alone to 815f34a still fails both new cases (2 failed,
57 passed), so the direct comparison pins the fix as well as the old helper.

Signed-off-by: Christopher Krah <48338417+chrikrah@users.noreply.github.com>
@chrikrah
chrikrah force-pushed the fix/jats-none-caption branch from c9f9b96 to dab5b14 Compare September 26, 2026 10:30
@chrikrah

Copy link
Copy Markdown
Contributor Author

Rebased onto 2d5c590 to clear a conflict. #4272 landed a test in the same place in tests/test_backend_jats.py, so both sets of tests now sit side by side; nothing of either was dropped. Head is dab5b14.

64 passed

Revert jats_backend.py alone and keep the tests:

FAILED test_jats_labelled_figure_without_a_caption_omits_the_word_none
FAILED test_jats_table_with_an_empty_label_produces_no_caption
2 failed, 62 passed

ruff check and ruff format --check are clean on both files.

@dolfim-ibm dolfim-ibm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

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

bug Something isn't working xml issue related to supported schema-specific XML formats

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants