Skip to content

fix(jats): keep the target of an ext-link in a mixed citation - #4813

Merged
dolfim-ibm merged 2 commits into
docling-project:mainfrom
Thanatos9404:fix-jats-reference-ext-link
Oct 29, 2026
Merged

dolfim-ibm merged 2 commits into
docling-project:mainfrom
Thanatos9404:fix-jats-reference-ext-link

Conversation

@Thanatos9404

Copy link
Copy Markdown
Contributor

A <mixed-citation> in the reference list is flattened to plain text with _get_text, so an <ext-link> inside it loses its xlink:href. The target survives only when the link text repeats the URL. This is the citation case that #4029 left out of scope.

Issue resolved by this Pull Request:
Resolves #4793

Root cause

In _walk_linear, the mixed-citation branch did text = JatsDocumentBackend._get_text(child).strip() and added that string as a list item. _get_text only collects text, so every link target (and any styling) inside the citation was dropped.

Change

  • The mixed-citation branch now calls a new _add_mixed_citation. It walks the citation with _walk_linear, the inline walker that body paragraphs use, so each <ext-link> becomes a run with its hyperlink, the same way feat(jats): preserve external hyperlinks #4029 handles links in the body.
  • A citation with one run stays a single list item, as before. The run's formatting and hyperlink go on the list item. A citation with more runs becomes an empty list item with an inline group of the runs. That is the shape _add_footnote_group uses for footnotes and the HTML backend uses for list items with links.
  • Because the citation now goes through the paragraph walker, inline styling (<italic>, <bold>, <sub>, <sup>) and inline formulas in a mixed citation are kept too. No mixed citation in the existing fixtures has styling, so this changes no reference data. Real articles often style part of a citation, for example <italic>et al</italic>.. Such a citation now exports as *et al* ., the same as a styled body paragraph, until docling-core#693 lands. Inline formulas, which were dropped before, are now kept.
  • <element-citation> is not changed. It is rebuilt from its fields, and its text keeps the trailing space that test_jats_element_citation_empty_name_parts_are_omitted expects. Links inside an element citation (for example in <comment>) are still not converted; none of the fixtures has one, and that would be a separate change to _parse_element_citation. Like body paragraphs, _walk_linear reads only <ext-link>, so a <uri xlink:href> in a citation also still loses its target.

Behavior on the input from the issue:

  • Before (main): - BRAVO-CITATION. A work. 2020. https://example.org/c2-citation, and no text item has the hyperlink https://example.org/c2-citation.
  • After: - BRAVO-CITATION. A work. 2020. [https://example.org/c2-citation](https://example.org/c2-citation). The list item holds an inline group with the runs BRAVO-CITATION. A work. 2020. and https://example.org/c2-citation, and the second run has the hyperlink. The body link is unchanged.

Reference data

Only the six references that contain an <ext-link> change. I compared the old and new .json item by item (text, label, hyperlink, formatting, content layer): the other 178 mixed citations in the fixtures are identical.

  • pntd.0008301: 2 references (CGIAR-CSI SRTM and USGS NASA). In the first one, the link text http://Srtm.Csi.Cgiar.Org/ differs from its target, which was lost before.
  • pone.0234687: 3 references (FAO, IPCC and GREET).
  • ptag100: 1 reference, with a Search inSPIRE link inside <comment>. Its target https://inspirehep.net/literature?q=find%20EPRINT%202408.15986v3 was lost before.
  • elife-56337 and pmc2231364: no change (they have no mixed citations).

In the .md files the URL becomes a Markdown link. In the .itxt and .json files each changed reference is an empty list item with an inline group, and the items after it are renumbered. Two references show the existing inline group spacing of the serializer, ttps:// [www.ipcc.ch/...](...) and [hep-th] [ [Search inSPIRE](...) ]. Body paragraphs with links already export the same way, for example ( [https://www.inration.educagri.fr/fr/forum.php](...) ) in pone.0234687.nxml.md. The spacing is the serializer topic tracked in docling-core#693.

Checklist:

  • Documentation has been updated, if necessary.
  • Examples have been added, if necessary.
  • Tests have been added, if necessary.

Review evidence:

New tests in tests/test_backend_jats_references.py. tests/test_backend_jats.py has 1494 lines and the max-lines hook allows 1500, so the tests are in a new module that reuses get_converter, as tests/test_backend_pptx_charts.py does.

  • test_jats_mixed_citation_keeps_external_link_target: the input from the issue; checks the runs and the Markdown.
  • test_jats_mixed_citation_keeps_link_target_that_differs_from_its_text: the link text is doi:10.5555/survey and the target is https://doi.org/10.5555/survey.
  • test_jats_mixed_citation_with_one_run_stays_a_single_list_item: a plain citation keeps its old shape (no inline group), and a citation that is only a link keeps the link on the list item.
  • test_jats_mixed_citation_keeps_inline_styling: italic and bold runs in a citation.

All four fail on main (ffce191) and pass with this change.

Commands and results (Windows 11, Python 3.10.20, docling-core 2.100.0, environment from uv sync --frozen --group dev --all-packages --no-group docs --no-group examples --extra format-xml-jats):

Command Result
uv run pytest tests/test_backend_jats_references.py with the backend of main 4 failed
uv run pytest tests/test_backend_jats_references.py tests/test_backend_jats.py 90 passed
uv run pytest tests/test_backend_jats.py tests/test_backend_jats_references.py tests/test_input_doc.py tests/test_backend_optional_dependencies.py tests/test_data_gen_flag.py 155 passed
DOCLING_GEN_TEST_DATA=1 uv run pytest tests/test_backend_jats.py -k e2e reference data regenerated, reviewed as described above
uv run prek run --files <changed files> (what make validate runs) all hooks passed: trailing whitespace, end of files, merge conflicts, large files, license header, Ruff, Ruff format, ty, Tach, Tach module coverage, max lines
uv run ty check docling/backend/xml/jats_backend.py 0 errors; the same 5 warnings as on main

Not run: the full test suite and the ML suites. On Windows, the hooks that call python3 ran through a python3 alias of the project interpreter.

A <mixed-citation> was flattened to plain text with _get_text, so an
<ext-link> inside it lost its xlink:href. The target survived only when
the link text repeated the URL.

Walk the citation with _walk_linear, the inline walker of body
paragraphs, so the link becomes a hyperlink on its own run, as docling-project#4029
does in the body. A citation with one run stays a single list item. A
citation with more runs becomes an empty list item with an inline group,
as footnotes and HTML list items do. Element citations are unchanged.

Regenerate the JATS ground truth. Only the six references that contain
an ext-link change: two in pntd.0008301, three in pone.0234687 and one
in ptag100. The text of every other reference is unchanged.

Resolves docling-project#4793

Signed-off-by: Yashvardhan Thanvi <[email protected]>
@github-actions

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

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

@mergify

mergify Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 All 2 merge protections satisfied — ready to merge.

Show 2 satisfied protections

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

🟢 Require two reviewer for test updates

When test data is updated, we require two reviewers

  • #approved-reviews-by >= 2

@github-actions github-actions Bot added ai:ci-safe AI triage found no CI-safety concern (advisory) bug Something isn't working tests issue related to changes needed in the tests xml issue related to supported schema-specific XML formats labels Oct 14, 2026
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

AI triage (advisory)

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

The PR changes the JATS backend so that a <mixed-citation> keeps the xlink:href target of each <ext-link> and its inline styling. It adds _add_mixed_citation, which walks the citation with _walk_linear. It also adds tests and updates the JATS reference data.

CI safety: needs maintainer care before you approve CI.

  • .github/max-lines-ignore: changes CI workflows, actions, or CI scripts
2 related pull request(s) in the same code
PR State Signal Assessment
#4943 closed also references issue #4793 related: This PR is closed and not merged. It fixes the same issue with a similar _walk_linear change in the mixed-citation branch, so a maintainer may want to compare the two.
#4029 merged referenced as #4029 related: This merged PR added hyperlink support for body paragraphs. The current PR builds on that support and covers the mixed-citation case it left out.

Reference data changes

Change Files
structure 3
text 6
  • structure tests/data/jats/groundtruth/pntd.0008301.nxml.json: text: 109 → 115
  • structure tests/data/jats/groundtruth/pone.0234687.nxml.json: text: 435 → 441
  • structure tests/data/jats/groundtruth/ptag100.xml.json: text: 238 → 241
  • text tests/data/jats/groundtruth/pntd.0008301.nxml.itxt: +60 / -52 lines
  • text tests/data/jats/groundtruth/pntd.0008301.nxml.md: +2 / -2 lines
  • text tests/data/jats/groundtruth/pone.0234687.nxml.itxt: +70 / -61 lines
  • text tests/data/jats/groundtruth/pone.0234687.nxml.md: +3 / -3 lines
  • text tests/data/jats/groundtruth/ptag100.xml.itxt: +10 / -6 lines
  • text tests/data/jats/groundtruth/ptag100.xml.md: +1 / -1 lines

Assessment: expected. Mixed citations that contain links or styling now become several inline runs. This explains the extra text items in the .json files (109 to 115, 435 to 441, 238 to 241) and the shifted .itxt lines. The small .md changes match the new link and run output.

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 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 75f5de284e. It read the code and did not run it.

I found no merge-deciding defect. I traced _add_mixed_citation through _walk_linear, _strip_segments, and _emit_inline. A one-run citation stays a list item with its formatting and hyperlink. A multi-run citation becomes an empty list item with an inline group, as _add_footnote_group does. The regression tests cover the lost link target, the differing link text, the single-run case, and styling. I could not read the changed reference data, so the human reviewer must check the three groundtruth diffs. The new *et al* . spacing the PR describes is the visible effect to look for there.

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

@github-actions github-actions Bot added the ai:review-lgtm AI first review found no blocker or major issue (advisory) label Oct 14, 2026
dolfim-ibm
dolfim-ibm previously approved these changes Oct 14, 2026

@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

@codecov

codecov Bot commented Oct 14, 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 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 @Thanatos9404 for this contribution!

Just to keep the repo tidy, please do not create another test module for testing the references. Add the new tests in tests/test_backend_jats.py.

@Thanatos9404

Copy link
Copy Markdown
Contributor Author

@ceberam Happy to move them, but tests/test_backend_jats.py is at 1494 lines on main and the max-lines pre-commit hook fails above 1500 for files that are not in the [warn] list of .github/max-lines-ignore, so the four tests would push it over. That's why they're in their own module, like test_backend_jats_sub_articles.py from #4826. Should I move them anyway and add tests/test_backend_jats.py to [warn], or keep the separate file?

Move the four mixed-citation tests from
tests/test_backend_jats_references.py into tests/test_backend_jats.py
and remove the separate module, as asked in review. They now build the
reference list with convert_jats_body and reuse the _list_items,
_inline_group_items and _formatting_tuple helpers, like the
element-citation tests.

This takes tests/test_backend_jats.py to 1593 lines, past the 1500-line
limit of the max-lines hook, so add it to the [warn] list of
.github/max-lines-ignore.

Signed-off-by: Yashvardhan Thanvi <[email protected]>
@github-actions github-actions Bot removed ai:ci-safe AI triage found no CI-safety concern (advisory) ai:review-lgtm AI first review found no blocker or major issue (advisory) labels Oct 23, 2026
@Thanatos9404

Copy link
Copy Markdown
Contributor Author

Thanks @ceberam, moved the tests into tests/test_backend_jats.py and removed the separate module. That takes the file past the 1500-line max-lines limit, so I also added it to the [warn] list in .github/max-lines-ignore.

@github-actions github-actions Bot added the ai:ci-needs-care AI triage: check CI-safety findings before you approve CI label Oct 23, 2026

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

Great! 🚀

@dolfim-ibm
dolfim-ibm merged commit d518808 into docling-project:main Oct 29, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai:ci-needs-care AI triage: check CI-safety findings before you approve CI bug Something isn't working tests issue related to changes needed in the tests xml issue related to supported schema-specific XML formats

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JATS: <ext-link> inside a reference-list citation loses its target

3 participants