linkml-reference-validator 0.5.0__tar.gz → 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/PKG-INFO +1 -1
  2. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/extract/xml.py +66 -0
  3. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/sources/doi.py +31 -1
  4. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/sources/pmid.py +94 -1
  5. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/validation/supporting_text_validator.py +10 -3
  6. linkml_reference_validator-0.5.1/tests/test_inline_abstract_headings.py +127 -0
  7. linkml_reference_validator-0.5.1/tests/test_welded_markers.py +184 -0
  8. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.claude/.mcp.json +0 -0
  9. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.claude/settings.json +0 -0
  10. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.copier-answers.yml +0 -0
  11. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.devcontainer/devcontainer.json +0 -0
  12. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.devcontainer/post_create.sh +0 -0
  13. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.editorconfig +0 -0
  14. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/actions/claude-code-action/action.yml +0 -0
  15. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/actions/claude-issue-summarize-action/action.yml +0 -0
  16. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/actions/claude-issue-triage-action/action.yml +0 -0
  17. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/ai-controllers.json +0 -0
  18. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/copilot-instructions.md +0 -0
  19. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/copilot-setup-steps.yml +0 -0
  20. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/dependabot.yml +0 -0
  21. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/workflows/claude-code-review.yml +0 -0
  22. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/workflows/claude-issue-summarize.yml +0 -0
  23. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/workflows/claude-issue-triage.yml +0 -0
  24. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/workflows/claude-ontology-review.yml +0 -0
  25. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/workflows/claude.yml +0 -0
  26. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/workflows/deploy-docs.yaml +0 -0
  27. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/workflows/dragon-ai.yml +0 -0
  28. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/workflows/main.yaml +0 -0
  29. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.github/workflows/pypi-publish.yaml +0 -0
  30. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.gitignore +0 -0
  31. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.goosehints +0 -0
  32. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.pre-commit-config.yaml +0 -0
  33. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/.yamllint.yaml +0 -0
  34. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/AGENTS.md +0 -0
  35. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/BUG_GEO_ACCESSION_TO_UID.md +0 -0
  36. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/BUG_ZENODO_DOI_AND_URL_HANDLING.md +0 -0
  37. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/CLAUDE.md +0 -0
  38. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/CLI-ISSUES.md +0 -0
  39. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/CODE_OF_CONDUCT.md +0 -0
  40. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/CONTRIBUTING.md +0 -0
  41. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/FEATURE_REQUEST_REPAIR.md +0 -0
  42. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/LICENSE +0 -0
  43. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/README.md +0 -0
  44. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/REVIEW2.md +0 -0
  45. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/REVIEW3.md +0 -0
  46. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/ai.just +0 -0
  47. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/benchmarks/repeated_quotes.py +0 -0
  48. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/copilot-instructions.md +0 -0
  49. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/about.md +0 -0
  50. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/assets/zotero-find-full-text.png +0 -0
  51. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/concepts/content-types.md +0 -0
  52. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/concepts/editorial-conventions.md +0 -0
  53. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/concepts/how-it-works.md +0 -0
  54. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/elements/.gitkeep +0 -0
  55. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/how-to/add-reference-source.md +0 -0
  56. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/how-to/export-cache-to-zotero.md +0 -0
  57. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/how-to/fetch-full-text-and-pdfs.md +0 -0
  58. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/how-to/repair-validation-errors.md +0 -0
  59. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/how-to/skip-unsupported-references.md +0 -0
  60. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/how-to/use-local-files-and-urls.md +0 -0
  61. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/how-to/validate-clinical-trials.md +0 -0
  62. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/how-to/validate-dois.md +0 -0
  63. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/how-to/validate-entrez.md +0 -0
  64. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/how-to/validate-obo-files.md +0 -0
  65. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/how-to/validate-titles.md +0 -0
  66. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/how-to/validate-urls.md +0 -0
  67. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/index.md +0 -0
  68. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/notebooks/01_getting_started.ipynb +0 -0
  69. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/notebooks/02_advanced_usage.ipynb +0 -0
  70. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/notebooks/03_python_api.ipynb +0 -0
  71. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/notebooks/04_obo_validation.ipynb +0 -0
  72. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/notebooks/05_geo_validation.ipynb +0 -0
  73. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/quickstart.md +0 -0
  74. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/reference/cache-format.md +0 -0
  75. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/reference/cli.md +0 -0
  76. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/reference/splitting.md +0 -0
  77. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/setup-guide.md +0 -0
  78. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/superpowers/plans/2026-06-12-modular-pdf-fulltext-fetching.md +0 -0
  79. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/superpowers/plans/2026-08-05-private-library-full-text.md +0 -0
  80. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/superpowers/specs/2026-06-12-modular-pdf-fulltext-fetching-design.md +0 -0
  81. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/superpowers/specs/2026-08-05-private-library-full-text-design.md +0 -0
  82. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/templates-linkml/README.md +0 -0
  83. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/todo.md +0 -0
  84. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/troubleshooting.md +0 -0
  85. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/docs/tutorials/complete-workflow.md +0 -0
  86. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/examples/README.md +0 -0
  87. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/justfile +0 -0
  88. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/mkdocs.yml +0 -0
  89. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/mypy.ini +0 -0
  90. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/notebooks/01_getting_started.ipynb +0 -0
  91. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/notebooks/02_advanced_usage.ipynb +0 -0
  92. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/notebooks/03_python_api.ipynb +0 -0
  93. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/notebooks/README.md +0 -0
  94. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/project/README.md +0 -0
  95. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/project.justfile +0 -0
  96. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/pyproject.toml +0 -0
  97. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/pytest.ini +0 -0
  98. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/skills/linkml-reference-validator/SKILL.md +0 -0
  99. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/skills/linkml-reference-validator/references/configuration.md +0 -0
  100. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/skills/linkml-reference-validator/references/guardrails.md +0 -0
  101. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/__init__.py +0 -0
  102. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/_version.py +0 -0
  103. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/cache.py +0 -0
  104. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/cli/__init__.py +0 -0
  105. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/cli/cache.py +0 -0
  106. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/cli/lookup.py +0 -0
  107. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/cli/repair.py +0 -0
  108. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/cli/shared.py +0 -0
  109. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/cli/validate.py +0 -0
  110. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/__init__.py +0 -0
  111. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/acquire.py +0 -0
  112. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/csl_export.py +0 -0
  113. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/extract/__init__.py +0 -0
  114. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/extract/base.py +0 -0
  115. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/extract/drawio.py +0 -0
  116. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/extract/html.py +0 -0
  117. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/extract/pdf.py +0 -0
  118. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/extract/svg.py +0 -0
  119. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/extract/svg_geometry.py +0 -0
  120. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/extract/untrusted_xml.py +0 -0
  121. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/fulltext/__init__.py +0 -0
  122. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/fulltext/base.py +0 -0
  123. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/fulltext/bioc.py +0 -0
  124. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/fulltext/bookshelf.py +0 -0
  125. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/fulltext/epmc.py +0 -0
  126. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/fulltext/epmc_preprint.py +0 -0
  127. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/fulltext/json_api.py +0 -0
  128. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/fulltext/loader.py +0 -0
  129. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/fulltext/openalex.py +0 -0
  130. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/fulltext/pmc.py +0 -0
  131. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/fulltext/unpaywall.py +0 -0
  132. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/fulltext/zotero.py +0 -0
  133. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/identifiers.py +0 -0
  134. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/reference_fetcher.py +0 -0
  135. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/rules.py +0 -0
  136. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/sources/__init__.py +0 -0
  137. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/sources/base.py +0 -0
  138. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/sources/clinicaltrials.py +0 -0
  139. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/sources/entrez.py +0 -0
  140. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/sources/file.py +0 -0
  141. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/sources/json_api.py +0 -0
  142. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/sources/loader.py +0 -0
  143. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/sources/ppr.py +0 -0
  144. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/sources/url.py +0 -0
  145. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/sources/utils.py +0 -0
  146. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/etl/text_extractor.py +0 -0
  147. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/field_detection.py +0 -0
  148. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/matching.py +0 -0
  149. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/models.py +0 -0
  150. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/plugins/REVIEW.md +0 -0
  151. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/plugins/__init__.py +0 -0
  152. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/plugins/reference_validation_plugin.py +0 -0
  153. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/validation/__init__.py +0 -0
  154. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/validation/fuzzy_text_utils.py +0 -0
  155. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/src/linkml_reference_validator/validation/repairer.py +0 -0
  156. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/__init__.py +0 -0
  157. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/conftest.py +0 -0
  158. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/data/test_data_deep_nesting_invalid.yaml +0 -0
  159. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/data/test_data_deep_nesting_valid.yaml +0 -0
  160. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/data/test_data_invalid.yaml +0 -0
  161. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/data/test_data_valid.yaml +0 -0
  162. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/data/test_schema.yaml +0 -0
  163. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/data/test_schema_deep_nesting.yaml +0 -0
  164. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/GEO_GSE12345.md +0 -0
  165. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/PMC_TEST001.md +0 -0
  166. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/PMC_TEST001.txt +0 -0
  167. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/PMC_TEST002.md +0 -0
  168. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/PMC_TEST002.txt +0 -0
  169. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/PMID_TEST001.md +0 -0
  170. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/PMID_TEST001.txt +0 -0
  171. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/PMID_TEST002.md +0 -0
  172. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/PMID_TEST002.txt +0 -0
  173. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/PMID_TEST003.md +0 -0
  174. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/PMID_TEST003.txt +0 -0
  175. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/bookshelf/esummary_20301710.xml +0 -0
  176. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/bookshelf/gene_NBK1116/men1.nxml +0 -0
  177. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/bookshelf/pubmed_20301710.xml +0 -0
  178. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/clinicaltrials_NCT00000001.md +0 -0
  179. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/fulltext_html/README.md +0 -0
  180. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/fulltext_html/dspace-32894695.html +0 -0
  181. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/fulltext_html/iris-19049553.html +0 -0
  182. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/fulltext_html/plos-0000308.html +0 -0
  183. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/jats/PMC5593426.xml +0 -0
  184. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/jats/README.md +0 -0
  185. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/fixtures/sample.obo +0 -0
  186. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_absent_abstract_recovery.py +0 -0
  187. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_acquire.py +0 -0
  188. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_bookshelf_provider.py +0 -0
  189. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_bot_challenge.py +0 -0
  190. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_cache_author_recovery.py +0 -0
  191. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_cache_filename_collisions.py +0 -0
  192. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_cache_read_robustness.py +0 -0
  193. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_cache_validation.py +0 -0
  194. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_cli.py +0 -0
  195. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_cli_cache_enrich.py +0 -0
  196. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_cli_cache_export.py +0 -0
  197. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_cli_cache_reference.py +0 -0
  198. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_cli_lookup.py +0 -0
  199. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_cli_repair.py +0 -0
  200. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_clinicaltrials_cache.py +0 -0
  201. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_clinicaltrials_integration.py +0 -0
  202. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_deep_nesting.py +0 -0
  203. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_doi_preprint.py +0 -0
  204. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_doi_url_encoding.py +0 -0
  205. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_drawio.py +0 -0
  206. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_e2e_integration.py +0 -0
  207. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_empty_snippets.py +0 -0
  208. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_epmc_preprint_provider.py +0 -0
  209. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_extractor_fidelity.py +0 -0
  210. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_extractors.py +0 -0
  211. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_field_detection.py +0 -0
  212. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_field_detection_qualifiers.py +0 -0
  213. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_file_reference_cache.py +0 -0
  214. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_frontmatter_and_doi_case.py +0 -0
  215. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_fulltext_loader.py +0 -0
  216. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_fulltext_providers.py +0 -0
  217. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_fuzzy_suggestions.py +0 -0
  218. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_geo_integration.py +0 -0
  219. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_greek_variants.py +0 -0
  220. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_html_fulltext_acceptance.py +0 -0
  221. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_html_scope_text.py +0 -0
  222. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_identifiers.py +0 -0
  223. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_jats_tables.py +0 -0
  224. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_json_api_source.py +0 -0
  225. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_ligature_normalization.py +0 -0
  226. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_long_cache_filenames.py +0 -0
  227. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_meaning_changes.py +0 -0
  228. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_meaning_symbols.py +0 -0
  229. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_models.py +0 -0
  230. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_multivalued_reference.py +0 -0
  231. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_no_cache_downgrade.py +0 -0
  232. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_normalized_content_cache.py +0 -0
  233. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_open_access_policy.py +0 -0
  234. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_optional_linkml_dependency.py +0 -0
  235. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_pdf_url_title.py +0 -0
  236. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_plugin_integration.py +0 -0
  237. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_plugin_value_shapes.py +0 -0
  238. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_pmc_fulltext.py +0 -0
  239. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_pmid_network.py +0 -0
  240. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_ppr_source.py +0 -0
  241. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_publisher_typography.py +0 -0
  242. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_real_validation.py +0 -0
  243. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_reference_fetcher.py +0 -0
  244. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_repair.py +0 -0
  245. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_repair_data_output.py +0 -0
  246. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_repair_thresholds.py +0 -0
  247. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_rules.py +0 -0
  248. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_serve_stale_html_optin.py +0 -0
  249. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_source_network_errors.py +0 -0
  250. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_source_utils.py +0 -0
  251. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_sources.py +0 -0
  252. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_split_supporting_text.py +0 -0
  253. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_supporting_text_validator.py +0 -0
  254. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_svg_edges.py +0 -0
  255. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_svg_extractor.py +0 -0
  256. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_text_extractor.py +0 -0
  257. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_title_only_shapes.py +0 -0
  258. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_title_validation.py +0 -0
  259. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_trust_cached_entries.py +0 -0
  260. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_unknown_slot_traversal.py +0 -0
  261. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_url_fragment_cache.py +0 -0
  262. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_url_html_sanitize.py +0 -0
  263. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_url_html_to_text.py +0 -0
  264. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_url_source_cache_version.py +0 -0
  265. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_validation_config.py +0 -0
  266. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_validation_counts.py +0 -0
  267. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_word_boundaries.py +0 -0
  268. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_xml_extractor_text.py +0 -0
  269. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/tests/test_zotero_provider.py +0 -0
  270. {linkml_reference_validator-0.5.0 → linkml_reference_validator-0.5.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: linkml_reference_validator
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: Validation of supporting text from references and publications
5
5
  Author-email: Chris Mungall <cjmungall@lbl.gov>
6
6
  License-Expression: Apache-2.0
@@ -119,6 +119,53 @@ BLOCK_ELEMENTS = frozenset(
119
119
  )
120
120
 
121
121
 
122
+ #: Dashes that can join the numbers of a citation range ("1-3", "1–3").
123
+ _DASHES = "-\u2010\u2011\u2012\u2013\u2014\u2015\u2212"
124
+
125
+
126
+ def _is_citation_marker(tag: Tag) -> bool:
127
+ """Whether ``tag`` is a bibliographic citation marker.
128
+
129
+ A JATS ``<xref ref-type="bibr">``, or a ``<sup>`` holding only such xrefs
130
+ and the commas and dashes between them. Inline markup is otherwise kept
131
+ contiguous (``H<sub>2</sub>O``), but a citation glued to the word before it
132
+ (``FOXL2<sup>8</sup>``) reads as another word, and one after a full stop
133
+ (``PGE2.<sup>41</sup>``) as a decimal number.
134
+
135
+ Examples:
136
+ >>> soup = BeautifulSoup('<p>A<sup><xref ref-type="bibr">8</xref></sup>'
137
+ ... '10<sup>6</sup></p>', "xml")
138
+ >>> [_is_citation_marker(sup) for sup in soup.find_all("sup")]
139
+ [True, False]
140
+ """
141
+ if tag.name == "xref":
142
+ return tag.get("ref-type") == "bibr"
143
+ if tag.name != "sup":
144
+ return False
145
+ xrefs = tag.find_all("xref")
146
+ if not xrefs or any(xref.get("ref-type") != "bibr" for xref in xrefs):
147
+ return False
148
+ loose = "".join(
149
+ str(child) for child in tag.children if type(child) in (NavigableString, CData)
150
+ )
151
+ return re.fullmatch(rf"[\s,;{re.escape(_DASHES)}]*", loose) is not None
152
+
153
+
154
+ def _welded_to_next_word(tag: Tag) -> bool:
155
+ """Whether the text right after ``tag`` starts with a letter, with no space.
156
+
157
+ That is what makes a bold lead-in a welded label (``R 2.6</bold>We``)
158
+ rather than part of a word (``IL</bold>-6``) or already spaced.
159
+
160
+ Examples:
161
+ >>> soup = BeautifulSoup("<x><p><bold>R 2.6</bold>We</p><p><bold>IL</bold>-6</p></x>", "xml")
162
+ >>> [_welded_to_next_word(b) for b in soup.find_all("bold")]
163
+ [True, False]
164
+ """
165
+ following = tag.next_sibling
166
+ return isinstance(following, NavigableString) and str(following)[:1].isalpha()
167
+
168
+
122
169
  def _block_text(node: Tag) -> str:
123
170
  """Keep inline text contiguous, separating blocks and excluding nested tables.
124
171
 
@@ -137,8 +184,27 @@ def _block_text(node: Tag) -> str:
137
184
  parts.append(str(child))
138
185
  elif isinstance(child, Tag) and child.name not in {"table", "table-wrap"}:
139
186
  value = walk(child)
187
+ lead_in = (
188
+ tag.name == "p"
189
+ and child.name == "bold"
190
+ and not "".join(parts).strip()
191
+ and _welded_to_next_word(child)
192
+ )
140
193
  if child.name in BLOCK_ELEMENTS:
141
194
  value = " " + value + " "
195
+ elif _is_citation_marker(child):
196
+ # No space before a dash that follows: "[1</xref>-3]" is
197
+ # a range, and " -3" would read as a minus sign.
198
+ following = child.next_sibling
199
+ dash_next = (
200
+ isinstance(following, NavigableString)
201
+ and str(following)[:1] in _DASHES
202
+ )
203
+ value = " " + value + ("" if dash_next else " ")
204
+ elif lead_in:
205
+ # A paragraph's bold lead-in label ("<bold>R 2.6</bold>We
206
+ # recommend") is not part of the sentence after it.
207
+ value = value + " "
142
208
  parts.append(value)
143
209
  return "".join(parts)
144
210
 
@@ -12,6 +12,7 @@ Examples:
12
12
  """
13
13
 
14
14
  import logging
15
+ import re
15
16
  import time
16
17
  from typing import Optional
17
18
 
@@ -33,6 +34,16 @@ from linkml_reference_validator.etl.sources.utils import (
33
34
  logger = logging.getLogger(__name__)
34
35
 
35
36
 
37
+ #: JATS elements that end a run of text in a Crossref abstract. Their local
38
+ #: names, so ``jats:p`` and a bare ``p`` both count.
39
+ _JATS_ABSTRACT_BLOCKS = frozenset(
40
+ {
41
+ "title", "p", "sec", "list", "list-item", "label", "caption", "def-item",
42
+ "disp-quote", "div", "br",
43
+ }
44
+ )
45
+
46
+
36
47
  @ReferenceSourceRegistry.register
37
48
  class DOISource(ReferenceSource):
38
49
  """Fetch references from Crossref using DOI, with DataCite fallback.
@@ -515,6 +526,15 @@ class DOISource(ReferenceSource):
515
526
  def _clean_abstract(self, abstract: str) -> str:
516
527
  """Clean JATS/XML markup from abstract text.
517
528
 
529
+ Block elements (titles, paragraphs, sections, list items, and HTML
530
+ ``div`` and ``br``) become paragraphs separated by a blank line.
531
+ Crossref nests a structured abstract's sections as ``<jats:title>`` and
532
+ ``<jats:p>`` with no text between them, so stripping the tags alone
533
+ welded each heading onto the next sentence ("AbstractDisease
534
+ overviewAcute myeloid..."), and a quote beginning at that sentence
535
+ failed whole-word matching. Whitespace inside a paragraph is
536
+ collapsed; inline markup stays contiguous.
537
+
518
538
  Args:
519
539
  abstract: Abstract text potentially containing JATS markup
520
540
 
@@ -525,8 +545,18 @@ class DOISource(ReferenceSource):
525
545
  >>> source = DOISource()
526
546
  >>> source._clean_abstract("<jats:p>Test abstract.</jats:p>")
527
547
  'Test abstract.'
548
+ >>> source._clean_abstract(
549
+ ... "<jats:title>Abstract</jats:title><jats:sec><jats:title>Aim"
550
+ ... "</jats:title><jats:p>To test <jats:italic>it</jats:italic>.</jats:p>"
551
+ ... "</jats:sec>")
552
+ 'Abstract\\n\\nAim\\n\\nTo test it.'
528
553
  """
529
554
  if not abstract:
530
555
  return ""
531
556
  soup = BeautifulSoup(abstract, "html.parser")
532
- return soup.get_text().strip()
557
+ for tag in soup.find_all(True):
558
+ if tag.name.rpartition(":")[2] in _JATS_ABSTRACT_BLOCKS:
559
+ tag.insert_before("\n\n")
560
+ tag.insert_after("\n\n")
561
+ paragraphs = (" ".join(chunk.split()) for chunk in re.split(r"\n\s*\n", soup.get_text()))
562
+ return "\n\n".join(paragraph for paragraph in paragraphs if paragraph)
@@ -37,6 +37,54 @@ from linkml_reference_validator.etl.sources.utils import (
37
37
  logger = logging.getLogger(__name__)
38
38
 
39
39
 
40
+ #: Section headings of structured abstracts. PubMed sometimes stores them
41
+ #: welded onto the next sentence with no markup at all ("IntroductionThe black
42
+ #: widow spider..."), which no tag can mark.
43
+ _ABSTRACT_HEADINGS = (
44
+ "Abstract|Background|Introduction|Objectives?|Aims?|Purpose|Importance|"
45
+ "Context|Design|Setting|Participants|Patients|Methods|Materials and Methods|"
46
+ "Interventions?|Measurements|Main Outcome Measures|Results|Findings|"
47
+ "Observations|Case Report|Case Presentation|Discussion|Interpretation|"
48
+ "Conclusions?|Conclusions and Relevance|Significance|Limitations"
49
+ )
50
+ _WELDED_HEADING = re.compile(
51
+ rf"(?:^|(?<=[.!?])|(?<=: ))[ \t]*({_ABSTRACT_HEADINGS})(?=[A-Z])"
52
+ )
53
+
54
+
55
+ def _set_off_welded_headings(text: str) -> str:
56
+ """Split a heading welded onto its sentence into a ``"Heading: "`` label.
57
+
58
+ Only a listed heading, at the start of the text or right after a sentence
59
+ ends, and directly followed by a capital letter, so "The Results were"
60
+ and "Methodsical" are left alone. A heading right after ": " also counts,
61
+ which splits chained headings ("AbstractBackgroundIt" becomes "Abstract:
62
+ Background: It") and a welded heading after any earlier label ("Aim:
63
+ MethodsWe"). Title-case headings only, on purpose: all-caps welds
64
+ ("RESULTSThe") and welds after a bracket ("exposures)Conclusions") are
65
+ left as they are.
66
+
67
+ Examples:
68
+ >>> _set_off_welded_headings("IntroductionThe spider. ResultsA total.")
69
+ 'Introduction: The spider.\\n\\nResults: A total.'
70
+ >>> _set_off_welded_headings("The Results were striking.")
71
+ 'The Results were striking.'
72
+ """
73
+
74
+ def label(match: re.Match[str]) -> str:
75
+ after_label = text[: match.start()].endswith(": ")
76
+ lead = "" if match.start() == 0 or after_label else "\n\n"
77
+ return lead + match.group(1) + ": "
78
+
79
+ # Repeat for chained headings: "AbstractBackgroundIt began." only exposes
80
+ # "Background" once "Abstract" has been split off.
81
+ while True:
82
+ updated = _WELDED_HEADING.sub(label, text)
83
+ if updated == text:
84
+ return text
85
+ text = updated
86
+
87
+
40
88
  @ReferenceSourceRegistry.register
41
89
  class PMIDSource(ReferenceSource):
42
90
  """Fetch references from PubMed using PMID.
@@ -236,7 +284,9 @@ class PMIDSource(ReferenceSource):
236
284
  """
237
285
  sections = []
238
286
  for node in element.find_all("AbstractText"):
239
- text = node.get_text().strip()
287
+ # Edits the parsed node in place; nothing reads it again.
288
+ PMIDSource._set_off_inline_headings(node)
289
+ text = _set_off_welded_headings(node.get_text().strip())
240
290
  if not text:
241
291
  continue
242
292
  label = node.get("Label")
@@ -245,6 +295,49 @@ class PMIDSource(ReferenceSource):
245
295
  joined = "\n\n".join(sections)
246
296
  return joined if joined else None
247
297
 
298
+ @staticmethod
299
+ def _set_off_inline_headings(node: Any) -> None:
300
+ """Turn inline bold section headings into ``"Heading: "`` labels, in place.
301
+
302
+ Some publishers supply a structured abstract as one ``AbstractText``
303
+ whose headings are inline ``<b>``, with no space after them
304
+ (``<b>Background</b>Neuroblastoma can arise...``), so ``get_text()``
305
+ welds each heading onto the next word and a quote beginning at that
306
+ word fails whole-word matching. A ``<b>`` or ``<strong>`` counts as a
307
+ heading only at the start of the text or right after a sentence ends,
308
+ and only when the text after it starts with a letter; bold inside a
309
+ sentence, or not welded to the next word, is left as it is.
310
+
311
+ Args:
312
+ node: An ``AbstractText`` element, modified in place
313
+
314
+ Examples:
315
+ >>> from bs4 import BeautifulSoup
316
+ >>> xml = ("<AbstractText><b>Background</b>It began."
317
+ ... "<b>Results</b>It worked.</AbstractText>")
318
+ >>> node = BeautifulSoup(xml, "xml").find("AbstractText")
319
+ >>> PMIDSource._set_off_inline_headings(node)
320
+ >>> node.get_text()
321
+ 'Background: It began.\\n\\nResults: It worked.'
322
+ """
323
+ for bold in node.find_all(["b", "strong"]):
324
+ before = "".join(
325
+ str(sibling.get_text() if hasattr(sibling, "get_text") else sibling)
326
+ for sibling in reversed(list(bold.previous_siblings))
327
+ ).rstrip()
328
+ if before and not before.endswith((".", "!", "?")):
329
+ continue
330
+ # Only a real weld: the next text starts with a letter, no space.
331
+ # "<b>IL</b>-6" and "<b>Importantly</b>," are not headings.
332
+ following = bold.next_sibling
333
+ if not (isinstance(following, str) and following[:1].isalpha()):
334
+ continue
335
+ heading = bold.get_text().strip()
336
+ if not heading:
337
+ continue
338
+ label = heading if heading.endswith(":") else f"{heading}:"
339
+ bold.replace_with(f"{chr(10) * 2 if before else ''}{label} ")
340
+
248
341
  def _parse_abstract(self, soup: BeautifulSoup) -> Optional[str]:
249
342
  """Parse the abstract from a PubMed article XML document.
250
343
 
@@ -62,6 +62,10 @@ def is_blank_text(value: object) -> bool:
62
62
  return isinstance(value, str) and not value.strip()
63
63
 
64
64
 
65
+ #: Digits a citation marker welded onto a word can carry: decimal digits and
66
+ #: the superscript digits that text printing the marker itself uses (``ears¹``).
67
+ _CITATION_DIGITS = r"\d¹²³⁴⁵⁶⁷⁸⁹⁰"
68
+
65
69
  #: Characters typesetters print for a minus sign: "-", the Unicode hyphens
66
70
  #: U+2010 and U+2011, the figure dash U+2012, the en dash, "−" (U+2212) and the
67
71
  #: small and fullwidth hyphen-minus. The em dash is left out: it is a
@@ -627,8 +631,9 @@ class SupportingTextValidator:
627
631
  One allowance: when the part ends in a letter, the reference word it
628
632
  ends on may carry trailing digits. Extraction welds superscript
629
633
  citations onto the word before them (JATS ``suppressor<sup>12</sup>``,
630
- PMC HTML and PDF text all give ``suppressor12``), and a quote of that
631
- sentence must not fail on its last word. A part ending in a digit gets
634
+ PMC HTML and PDF text all give ``suppressor12``, and text that prints
635
+ the citation itself as superscript gives ``ears¹``), and a quote of
636
+ that sentence must not fail on its last word. A part ending in a digit gets
632
637
  no allowance, so ``10`` never matches ``105``.
633
638
 
634
639
  An empty part (one that was all punctuation) is found, as with a
@@ -657,6 +662,8 @@ class SupportingTextValidator:
657
662
  True
658
663
  >>> SupportingTextValidator.contains_words("a dose of 10", "a dose of 105 mg")
659
664
  False
665
+ >>> SupportingTextValidator.contains_words("small ears", "small ears¹ ³ both")
666
+ True
660
667
  >>> SupportingTextValidator.contains_words("", "anything")
661
668
  True
662
669
  """
@@ -666,7 +673,7 @@ class SupportingTextValidator:
666
673
  return True
667
674
  if not part[-1].isalpha():
668
675
  return False
669
- citation = rf"(?:^| ){re.escape(part)}\d[\d,]*(?: |$)"
676
+ citation = rf"(?:^| ){re.escape(part)}[{_CITATION_DIGITS}][{_CITATION_DIGITS},]*(?: |$)"
670
677
  return re.search(citation, text) is not None
671
678
 
672
679
  def generate_suggested_fix(
@@ -0,0 +1,127 @@
1
+ """Inline section headings in a PubMed abstract stay separate words.
2
+
3
+ Some publishers supply a structured abstract as one ``AbstractText`` whose
4
+ section headings are inline bold, with no space after them:
5
+ ``<b>Background</b>Neuroblastoma can arise...``. ``get_text()`` then welds the
6
+ heading onto the first word ("BackgroundNeuroblastoma"), and since matching
7
+ became whole-word (#150) a correct quote beginning at that word fails.
8
+ PMID:33307929 is the real record this comes from.
9
+ """
10
+
11
+ from bs4 import BeautifulSoup
12
+
13
+ from linkml_reference_validator.etl.sources.pmid import PMIDSource
14
+ from linkml_reference_validator.models import ReferenceContent, ReferenceValidationConfig
15
+ from linkml_reference_validator.validation.supporting_text_validator import SupportingTextValidator
16
+
17
+ PMID_33307929 = (
18
+ "<Abstract><AbstractText><b>Background</b>Neuroblastoma can arise from "
19
+ "extra-adrenal sites in the paraspinal sympathetic chain, including the "
20
+ "presacral region, where they must be differentiated from an immature or "
21
+ "malignant neural lesion arising from a teratoma.<b>Case Report</b>We "
22
+ "describe two congenital presacral neuroblastomas.</AbstractText></Abstract>"
23
+ )
24
+
25
+
26
+ def render(xml: str) -> str:
27
+ """The abstract text the PMID source would cache for ``xml``."""
28
+ rendered = PMIDSource()._parse_abstract(BeautifulSoup(xml, "xml"))
29
+ assert rendered is not None
30
+ return rendered
31
+
32
+
33
+ def found(quote: str, content: str) -> bool:
34
+ """Whether ``quote`` validates against ``content``."""
35
+ validator = SupportingTextValidator(ReferenceValidationConfig())
36
+ reference = ReferenceContent(reference_id="PMID:1", content=content)
37
+ return validator.find_text_in_reference(quote, reference).found
38
+
39
+
40
+ def test_inline_headings_are_set_off_as_labels():
41
+ text = render(PMID_33307929)
42
+ assert text.startswith("Background: Neuroblastoma can arise")
43
+ assert "a teratoma.\n\nCase Report: We describe two" in text
44
+
45
+
46
+ def test_quotes_beginning_after_an_inline_heading_match():
47
+ text = render(PMID_33307929)
48
+ assert found("Neuroblastoma can arise from extra-adrenal sites", text)
49
+ assert found("We describe two congenital presacral neuroblastomas.", text)
50
+
51
+
52
+ def test_bold_inside_a_sentence_is_left_alone():
53
+ xml = (
54
+ "<Abstract><AbstractText>Variants in <b>BRCA1</b> and "
55
+ "<b>BRCA2</b>were found.</AbstractText></Abstract>"
56
+ )
57
+ assert render(xml) == "Variants in BRCA1 and BRCA2were found."
58
+
59
+
60
+ def test_a_heading_already_followed_by_a_space_or_colon_is_unchanged_in_words():
61
+ xml = (
62
+ "<Abstract><AbstractText><b>Background:</b> It began. "
63
+ "<b>Results</b> It worked.</AbstractText></Abstract>"
64
+ )
65
+ text = render(xml)
66
+ assert found("It began", text) and found("It worked", text)
67
+ assert "Background:" in text and "::" not in text
68
+
69
+
70
+ def test_labelled_abstract_text_is_unchanged():
71
+ xml = (
72
+ '<Abstract><AbstractText Label="METHODS">We ran a trial.</AbstractText>'
73
+ '<AbstractText Label="RESULTS">It worked.</AbstractText></Abstract>'
74
+ )
75
+ assert render(xml) == "METHODS: We ran a trial.\n\nRESULTS: It worked."
76
+
77
+
78
+ # PubMed itself sometimes stores the headings welded with no markup at all
79
+ # (PMID:39670790): "IntroductionThe black widow spider ... exposures.Conclusions
80
+ # Poison center data ...".
81
+ WELDED_PLAIN = (
82
+ "<Abstract><AbstractText>IntroductionThe black widow spider is common. "
83
+ "ResultsA total of 15,000 exposures were reported in 3.4% (n=521) of exposures."
84
+ "ConclusionsPoison center data indicate that most exposures are minor."
85
+ "</AbstractText></Abstract>"
86
+ )
87
+
88
+
89
+ def test_plain_welded_headings_are_set_off():
90
+ text = render(WELDED_PLAIN)
91
+ assert text.startswith("Introduction: The black widow spider")
92
+ assert "of exposures.\n\nConclusions: Poison center data" in text
93
+ assert found("The black widow spider is common", text)
94
+ assert found("Poison center data indicate that most exposures are minor", text)
95
+ assert found("A total of 15,000 exposures", text)
96
+
97
+
98
+ def test_heading_words_inside_sentences_are_left_alone():
99
+ xml = (
100
+ "<Abstract><AbstractText>The Results were striking. Methodsical work "
101
+ "followed.</AbstractText></Abstract>"
102
+ )
103
+ text = render(xml)
104
+ assert "The Results were striking." in text
105
+ assert "Methodsical work" in text
106
+
107
+
108
+ def test_leading_bold_followed_by_a_hyphen_is_not_a_label():
109
+ text = render("<Abstract><AbstractText><b>IL</b>-6 levels were raised.</AbstractText></Abstract>")
110
+ assert found("IL-6 levels were raised", text), text
111
+ assert "IL:" not in text
112
+
113
+
114
+ def test_bold_after_a_full_stop_but_not_welded_is_not_a_label():
115
+ xml = (
116
+ "<Abstract><AbstractText>It was observed. <b>Importantly</b>, the effect held, "
117
+ "vs. <b>BRCA2</b> carriers.</AbstractText></Abstract>"
118
+ )
119
+ text = render(xml)
120
+ assert "Importantly:" not in text and "BRCA2:" not in text
121
+ assert found("observed. Importantly, the effect held", text)
122
+
123
+
124
+ def test_chained_welded_headings_are_all_split():
125
+ text = render("<Abstract><AbstractText>AbstractBackgroundIt began.</AbstractText></Abstract>")
126
+ assert found("It began", text), text
127
+ assert text == "Abstract: Background: It began."
@@ -0,0 +1,184 @@
1
+ """Citation markers, lead-in labels and section titles stay separate words.
2
+
3
+ Matching became whole-word in #150, so text an extractor welds together now
4
+ fails a correct quote where a substring test used to pass it. Each case here is
5
+ from a real cached reference that a downstream knowledge base quoted:
6
+
7
+ - a JATS citation ``FOXL2<sup><xref ref-type="bibr">8</xref></sup>.`` read as
8
+ ``FOXL28.`` (PMC6851130), and ``PGE2.<sup>41</sup>`` as the number ``pge2.41``;
9
+ - a paragraph's bold lead-in label ``<bold>R 2.6</bold>We recommend`` read as
10
+ ``2.6We`` (PMC9066594);
11
+ - a Crossref JATS abstract ``<jats:title>Abstract</jats:title><jats:sec>
12
+ <jats:title>Disease overview</jats:title><jats:p>Acute myeloid...`` read as
13
+ ``AbstractDisease overviewAcute myeloid`` (DOI:10.1002/ajh.26822);
14
+ - a superscript citation range ``ears¹⁻³`` (PMID:21358632), which the
15
+ trailing-citation allowance did not cover because ``¹`` is not a decimal
16
+ digit.
17
+ """
18
+
19
+ import pytest
20
+
21
+ from linkml_reference_validator.etl.extract.xml import XMLExtractor
22
+ from linkml_reference_validator.etl.sources.doi import DOISource
23
+ from linkml_reference_validator.models import ReferenceContent, ReferenceValidationConfig
24
+ from linkml_reference_validator.validation.supporting_text_validator import (
25
+ SupportingTextValidator,
26
+ )
27
+
28
+
29
+ def extract(body: str) -> str:
30
+ text = XMLExtractor().extract(f"<article><body>{body}</body></article>".encode())
31
+ assert text is not None
32
+ return text
33
+
34
+
35
+ def found(quote: str, content: str) -> bool:
36
+ validator = SupportingTextValidator(ReferenceValidationConfig())
37
+ reference = ReferenceContent(reference_id="PMID:1", content=content)
38
+ return validator.find_text_in_reference(quote, reference).found
39
+
40
+
41
+ # --- JATS citation markers -------------------------------------------------
42
+
43
+
44
+ @pytest.mark.parametrize(
45
+ "body,quote",
46
+ [
47
+ (
48
+ '<p>It inhibits the expression of FOXL2<sup><xref ref-type="bibr" '
49
+ 'rid="CR8">8</xref></sup>. Here we add PITX2.</p>',
50
+ "inhibits the expression of FOXL2",
51
+ ),
52
+ (
53
+ '<p>caused by mutations in KCNA1<xref ref-type="bibr" rid="B18">18</xref>). '
54
+ "At last follow up.</p>",
55
+ "caused by mutations in KCNA1",
56
+ ),
57
+ (
58
+ '<p>and a deficiency of PGE2.<sup><xref ref-type="bibr" rid="B41">41</xref></sup>'
59
+ " Next sentence.</p>",
60
+ "a deficiency of PGE2",
61
+ ),
62
+ (
63
+ '<p>if left untreated.<sup><xref ref-type="bibr" rid="B2">2</xref>,'
64
+ '<xref ref-type="bibr" rid="B8">8</xref></sup>Damage to major organs.</p>',
65
+ "Damage to major organs",
66
+ ),
67
+ ],
68
+ )
69
+ def test_citation_markers_do_not_weld(body, quote):
70
+ text = extract(body)
71
+ assert found(quote, text), text
72
+
73
+
74
+ def test_citation_marker_still_reads_as_its_number():
75
+ text = extract('<p>shown before<sup><xref ref-type="bibr" rid="B8">8</xref></sup>.</p>')
76
+ assert "shown before 8" in text
77
+
78
+
79
+ def test_non_citation_superscripts_stay_attached():
80
+ """A unit exponent or a figure link is not a citation; only bibr is padded."""
81
+ text = extract(
82
+ '<p>at 10<sup>6</sup> cells, see <xref ref-type="fig" rid="F1">Figure 1</xref>'
83
+ "a, and H<sub>2</sub>O.</p>"
84
+ )
85
+ assert "10⁶" in text or "106" in text
86
+ assert "H2O" in text
87
+
88
+
89
+ # --- Bold lead-in labels ---------------------------------------------------
90
+
91
+
92
+ def test_leading_bold_label_is_set_off():
93
+ text = extract(
94
+ "<p><bold>R 2.6</bold>We recommend to use 17β-oestradiol for puberty induction.</p>"
95
+ )
96
+ assert found("We recommend to use 17β-oestradiol", text), text
97
+
98
+
99
+ def test_bold_inside_a_sentence_is_left_alone():
100
+ text = extract("<p>Variants in <bold>BRCA1</bold>were found.</p>")
101
+ assert "BRCA1were" in text
102
+
103
+
104
+ # --- Crossref JATS abstracts -----------------------------------------------
105
+
106
+ CROSSREF_ABSTRACT = (
107
+ "<jats:title>Abstract</jats:title>\n"
108
+ " <jats:sec>\n"
109
+ " <jats:title>Disease overview</jats:title>\n"
110
+ " <jats:p>Acute myeloid leukemia (AML) is a frequently fatal "
111
+ "bone marrow stem cell cancer.</jats:p>\n"
112
+ " </jats:sec>\n"
113
+ " <jats:sec><jats:title>Diagnosis</jats:title>"
114
+ "<jats:p>One updated and one <jats:italic>new</jats:italic> classification.</jats:p>"
115
+ "</jats:sec>"
116
+ )
117
+
118
+
119
+ def test_crossref_abstract_sections_are_separate_paragraphs():
120
+ text = DOISource()._clean_abstract(CROSSREF_ABSTRACT)
121
+ assert text.split("\n\n") == [
122
+ "Abstract",
123
+ "Disease overview",
124
+ "Acute myeloid leukemia (AML) is a frequently fatal bone marrow stem cell cancer.",
125
+ "Diagnosis",
126
+ "One updated and one new classification.",
127
+ ]
128
+ assert found("Acute myeloid leukemia (AML) is a frequently fatal", text)
129
+ assert found("One updated and one new classification", text)
130
+
131
+
132
+ def test_crossref_plain_paragraph_is_unchanged():
133
+ assert DOISource()._clean_abstract("<jats:p>Test abstract.</jats:p>") == "Test abstract."
134
+
135
+
136
+ # --- Superscript citations in plain text -----------------------------------
137
+
138
+
139
+ @pytest.mark.parametrize(
140
+ "quote,content",
141
+ [
142
+ ("markedly small ears", "patellae and markedly small ears¹⁻³. Both pre-"),
143
+ ("markedly small ears", "patellae and markedly small ears²³ and"),
144
+ ],
145
+ )
146
+ def test_superscript_citation_after_a_word_is_allowed(quote, content):
147
+ assert found(quote, content)
148
+
149
+
150
+ def test_superscript_does_not_extend_a_number():
151
+ """The allowance stays letter-only: a quote ending in a digit gets none."""
152
+ assert not found("a dose of 10", "a dose of 10⁶ cells")
153
+
154
+
155
+ @pytest.mark.parametrize(
156
+ "body,quote",
157
+ [
158
+ ("<p><bold>IL</bold>-6 levels were raised.</p>", "IL-6 levels were raised"),
159
+ ("<p><bold>BRCA1</bold> mutations were found.</p>", "BRCA1 mutations were found"),
160
+ ],
161
+ )
162
+ def test_leading_bold_that_is_not_welded_is_left_alone(body, quote):
163
+ """Only a bold followed directly by a letter is a welded label (IL-6 is not)."""
164
+ text = extract(body)
165
+ assert found(quote, text), text
166
+
167
+
168
+ def test_crossref_html_breaks_and_divs_separate_paragraphs():
169
+ text = DOISource()._clean_abstract("<div>First part.</div><div>Second part.</div>a<br/>b")
170
+ assert text.split("\n\n") == ["First part.", "Second part.", "a", "b"]
171
+
172
+
173
+ @pytest.mark.parametrize(
174
+ "body,quote",
175
+ [
176
+ ("<p>shown (<xref ref-type=\"bibr\" rid=\"B1\">1</xref>-3) before.</p>", "shown (1-3) before"),
177
+ ('<p>small ears<sup><xref ref-type="bibr" rid="B1">1</xref>–3</sup> in all.</p>', "small ears"),
178
+ ],
179
+ )
180
+ def test_citation_range_with_its_first_number_linked_keeps_its_dash(body, quote):
181
+ """A dash after a padded marker must not become a minus sign."""
182
+ text = extract(body)
183
+ assert found(quote, text), text
184
+ assert "neg" not in SupportingTextValidator.normalize_text(text), text