From 3c8a793e981bca97c25e8546e5e3a279f97c5c11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 05:53:44 +0000 Subject: [PATCH 1/5] Initial plan From 2dccc7ac6e782457e5bc13c06e994842a0c90c0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 06:02:12 +0000 Subject: [PATCH 2/5] fix funding entities after acknowledgement overlap cleanup --- .../engines/FundingAcknowledgementParser.java | 84 +++++++++++++++++++ ...ingAcknowledgementParserIntegrationTest.kt | 46 ++++++++++ 2 files changed, 130 insertions(+) diff --git a/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java b/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java index 6909b3b955..ce7e4da1fa 100644 --- a/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java +++ b/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java @@ -244,6 +244,8 @@ public MutablePair, List, List survivingAnnotationCounts = collectSurvivingAnnotationCounts(paragraph); + if (survivingAnnotationCounts.isEmpty()) { + localEntities.setFundings(new ArrayList<>()); + return; + } + + List survivingFundings = new ArrayList<>(); + for (Funding funding : localEntities.getFundings()) { + if (consumeFirstMatchingFundingAnnotation(funding, survivingAnnotationCounts)) { + survivingFundings.add(funding); + } + } + localEntities.setFundings(survivingFundings); + } + + private static Map collectSurvivingAnnotationCounts(Element paragraph) { + Map survivingAnnotationCounts = new HashMap<>(); + Nodes survivingAnnotations = paragraph.query(".//*[local-name()='rs']"); + for (int i = 0; i < survivingAnnotations.size(); i++) { + Node survivingAnnotation = survivingAnnotations.get(i); + if (!(survivingAnnotation instanceof Element)) { + continue; + } + + Element annotationElement = (Element) survivingAnnotation; + Attribute typeAttribute = annotationElement.getAttribute("type"); + if (typeAttribute == null) { + continue; + } + + String key = annotationKey(typeAttribute.getValue(), annotationElement.getValue()); + survivingAnnotationCounts.merge(key, 1, Integer::sum); + } + + return survivingAnnotationCounts; + } + + private static boolean consumeFirstMatchingFundingAnnotation( + Funding funding, + Map survivingAnnotationCounts) { + return consumeAnnotation("funder", funding.getFunder() != null ? funding.getFunder().getFullName() : null, + survivingAnnotationCounts) + || consumeAnnotation("grantNumber", funding.getGrantNumber(), survivingAnnotationCounts) + || consumeAnnotation("grantName", funding.getGrantName(), survivingAnnotationCounts) + || consumeAnnotation("programName", funding.getProgramFullName(), survivingAnnotationCounts) + || consumeAnnotation("projectName", funding.getProjectFullName(), survivingAnnotationCounts); + } + + private static boolean consumeAnnotation( + String type, + String value, + Map survivingAnnotationCounts) { + if (StringUtils.isBlank(value)) { + return false; + } + + String key = annotationKey(type, value); + Integer count = survivingAnnotationCounts.get(key); + if (count == null || count == 0) { + return false; + } + + if (count == 1) { + survivingAnnotationCounts.remove(key); + } else { + survivingAnnotationCounts.put(key, count - 1); + } + + return true; + } + + private static String annotationKey(String type, String value) { + return type + "\u0000" + value; + } + /** * This method return a list of nodes corresponding to the annotations as they are positioned in * the text content of the target node. If the node is empty, should be used @see injectedAnnotationsInNode diff --git a/grobid-core/src/test/kotlin/org/grobid/core/engines/FundingAcknowledgementParserIntegrationTest.kt b/grobid-core/src/test/kotlin/org/grobid/core/engines/FundingAcknowledgementParserIntegrationTest.kt index c292bf9c57..a0e85020be 100644 --- a/grobid-core/src/test/kotlin/org/grobid/core/engines/FundingAcknowledgementParserIntegrationTest.kt +++ b/grobid-core/src/test/kotlin/org/grobid/core/engines/FundingAcknowledgementParserIntegrationTest.kt @@ -15,10 +15,13 @@ */ package org.grobid.core.engines +import org.grobid.core.GrobidModels import org.grobid.core.engines.config.GrobidAnalysisConfig import org.grobid.core.factory.AbstractEngineFactory import org.grobid.core.utilities.GrobidConfig import org.grobid.core.utilities.GrobidProperties +import org.hamcrest.CoreMatchers.containsString +import org.hamcrest.CoreMatchers.not import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.hasSize import org.junit.Before @@ -275,6 +278,49 @@ class FundingAcknowledgementParserIntegrationTest { assertThat(element.toXML(), CompareMatcher.isIdenticalTo(output)) } + @Test + fun testXmlFragmentProcessing_shouldDropFundingWithoutInjectableAnnotation() { + val input = """ +
+
Acknowledgements

provide public access to these results of federally sponsored research in accordance with the DOE Public Access Plan http

+
+ """.trimIndent() + + val parser = StubFundingAcknowledgementParser( + listOf("DOE", "Public", "Access", "Plan", "http"), + ) + + val config = GrobidAnalysisConfig.GrobidAnalysisConfigBuilder() + .withSentenceSegmentation(true) + .build() + + val (element, mutableTriple) = parser.processingXmlFragment(input, config) + + assertThat(mutableTriple.left, hasSize(0)) + assertThat(element.toXML(), not(containsString("type=\"funder\""))) + } + + private class StubFundingAcknowledgementParser( + private val funderTokens: List, + ) : FundingAcknowledgementParser(GrobidModels.DUMMY) { + override fun label(data: String): String { + val lines = data.lineSequence() + .filter { it.isNotBlank() } + .toList() + val start = (lines.size - funderTokens.size).coerceAtLeast(0) + + return lines.mapIndexed { index, line -> + val label = when { + index == start -> "I-" + index in (start + 1) until (start + funderTokens.size) -> "" + index == 0 -> "I-" + else -> "" + } + "$line\t$label" + }.joinToString("\n") + } + } + companion object { @JvmStatic @BeforeClass From d0206d14ff0aaeb655581cce9c062fc2063824df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 06:06:30 +0000 Subject: [PATCH 3/5] refine funding overlap regression fix --- .../engines/FundingAcknowledgementParser.java | 43 ++++++++++++++----- ...ingAcknowledgementParserIntegrationTest.kt | 6 +-- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java b/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java index ce7e4da1fa..fb4b7576c3 100644 --- a/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java +++ b/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java @@ -500,7 +500,7 @@ private static void synchronizeExtractedEntitiesWithAnnotations( return; } - Map survivingAnnotationCounts = collectSurvivingAnnotationCounts(paragraph); + Map survivingAnnotationCounts = collectSurvivingAnnotationCounts(paragraph); if (survivingAnnotationCounts.isEmpty()) { localEntities.setFundings(new ArrayList<>()); return; @@ -515,8 +515,8 @@ private static void synchronizeExtractedEntitiesWithAnnotations( localEntities.setFundings(survivingFundings); } - private static Map collectSurvivingAnnotationCounts(Element paragraph) { - Map survivingAnnotationCounts = new HashMap<>(); + private static Map collectSurvivingAnnotationCounts(Element paragraph) { + Map survivingAnnotationCounts = new HashMap<>(); Nodes survivingAnnotations = paragraph.query(".//*[local-name()='rs']"); for (int i = 0; i < survivingAnnotations.size(); i++) { Node survivingAnnotation = survivingAnnotations.get(i); @@ -530,7 +530,7 @@ private static Map collectSurvivingAnnotationCounts(Element par continue; } - String key = annotationKey(typeAttribute.getValue(), annotationElement.getValue()); + AnnotationSignature key = new AnnotationSignature(typeAttribute.getValue(), annotationElement.getValue()); survivingAnnotationCounts.merge(key, 1, Integer::sum); } @@ -539,7 +539,7 @@ private static Map collectSurvivingAnnotationCounts(Element par private static boolean consumeFirstMatchingFundingAnnotation( Funding funding, - Map survivingAnnotationCounts) { + Map survivingAnnotationCounts) { return consumeAnnotation("funder", funding.getFunder() != null ? funding.getFunder().getFullName() : null, survivingAnnotationCounts) || consumeAnnotation("grantNumber", funding.getGrantNumber(), survivingAnnotationCounts) @@ -551,14 +551,14 @@ private static boolean consumeFirstMatchingFundingAnnotation( private static boolean consumeAnnotation( String type, String value, - Map survivingAnnotationCounts) { + Map survivingAnnotationCounts) { if (StringUtils.isBlank(value)) { return false; } - String key = annotationKey(type, value); + AnnotationSignature key = new AnnotationSignature(type, value); Integer count = survivingAnnotationCounts.get(key); - if (count == null || count == 0) { + if (count == null) { return false; } @@ -571,8 +571,31 @@ private static boolean consumeAnnotation( return true; } - private static String annotationKey(String type, String value) { - return type + "\u0000" + value; + private static class AnnotationSignature { + private final String type; + private final String value; + + private AnnotationSignature(String type, String value) { + this.type = type; + this.value = value; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AnnotationSignature)) { + return false; + } + AnnotationSignature that = (AnnotationSignature) other; + return Objects.equals(type, that.type) && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(type, value); + } } /** diff --git a/grobid-core/src/test/kotlin/org/grobid/core/engines/FundingAcknowledgementParserIntegrationTest.kt b/grobid-core/src/test/kotlin/org/grobid/core/engines/FundingAcknowledgementParserIntegrationTest.kt index a0e85020be..42e359d696 100644 --- a/grobid-core/src/test/kotlin/org/grobid/core/engines/FundingAcknowledgementParserIntegrationTest.kt +++ b/grobid-core/src/test/kotlin/org/grobid/core/engines/FundingAcknowledgementParserIntegrationTest.kt @@ -301,18 +301,18 @@ class FundingAcknowledgementParserIntegrationTest { } private class StubFundingAcknowledgementParser( - private val funderTokens: List, + private val tokensToLabelAsFunder: List, ) : FundingAcknowledgementParser(GrobidModels.DUMMY) { override fun label(data: String): String { val lines = data.lineSequence() .filter { it.isNotBlank() } .toList() - val start = (lines.size - funderTokens.size).coerceAtLeast(0) + val start = (lines.size - tokensToLabelAsFunder.size).coerceAtLeast(0) return lines.mapIndexed { index, line -> val label = when { index == start -> "I-" - index in (start + 1) until (start + funderTokens.size) -> "" + index in (start + 1) until (start + tokensToLabelAsFunder.size) -> "" index == 0 -> "I-" else -> "" } From 60562e353fc43300335fa173ac9409ba2d5327a6 Mon Sep 17 00:00:00 2001 From: Luca Foppiano Date: Sun, 5 Jul 2026 23:38:11 +0100 Subject: [PATCH 4/5] chore: spotless Signed-off-by: Luca Foppiano --- .../org/grobid/core/engines/FundingAcknowledgementParser.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java b/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java index fb4b7576c3..cd906aea22 100644 --- a/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java +++ b/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java @@ -540,7 +540,9 @@ private static Map collectSurvivingAnnotationCount private static boolean consumeFirstMatchingFundingAnnotation( Funding funding, Map survivingAnnotationCounts) { - return consumeAnnotation("funder", funding.getFunder() != null ? funding.getFunder().getFullName() : null, + return consumeAnnotation( + "funder", + funding.getFunder() != null ? funding.getFunder().getFullName() : null, survivingAnnotationCounts) || consumeAnnotation("grantNumber", funding.getGrantNumber(), survivingAnnotationCounts) || consumeAnnotation("grantName", funding.getGrantName(), survivingAnnotationCounts) From c50ea441d9db7a7390ffeba317971bd7622f0f8f Mon Sep 17 00:00:00 2001 From: Luca Foppiano Date: Sun, 12 Jul 2026 21:56:31 +0100 Subject: [PATCH 5/5] fix: update algorithm to use linked refs to entities --- .../grobid/core/data/AnnotatedXMLElement.java | 21 ++ .../engines/FundingAcknowledgementParser.java | 182 ++++++++---------- ...ingAcknowledgementParserIntegrationTest.kt | 24 +++ 3 files changed, 129 insertions(+), 98 deletions(-) diff --git a/grobid-core/src/main/java/org/grobid/core/data/AnnotatedXMLElement.java b/grobid-core/src/main/java/org/grobid/core/data/AnnotatedXMLElement.java index 8de9ffbc26..8288dac27b 100644 --- a/grobid-core/src/main/java/org/grobid/core/data/AnnotatedXMLElement.java +++ b/grobid-core/src/main/java/org/grobid/core/data/AnnotatedXMLElement.java @@ -28,11 +28,24 @@ public class AnnotatedXMLElement { private OffsetPosition offsetPosition; private Element annotationNode; + /** + * The extracted entity this annotation was derived from (a Funding, Person or Affiliation). + * Kept so that, when an annotation cannot be injected back into the XML, the corresponding + * entity can be identified and dropped by reference instead of by fragile text matching. + */ + private Object entity; + public AnnotatedXMLElement(Element annotationNode, OffsetPosition offsetPosition) { this.annotationNode = annotationNode; this.offsetPosition = offsetPosition; } + public AnnotatedXMLElement(Element annotationNode, OffsetPosition offsetPosition, Object entity) { + this.annotationNode = annotationNode; + this.offsetPosition = offsetPosition; + this.entity = entity; + } + public OffsetPosition getOffsetPosition() { return offsetPosition; } @@ -48,4 +61,12 @@ public Element getAnnotationNode() { public void setAnnotationNode(Element annotationNode) { this.annotationNode = annotationNode; } + + public Object getEntity() { + return entity; + } + + public void setEntity(Object entity) { + this.entity = entity; + } } diff --git a/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java b/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java index cd906aea22..26a7dca6e5 100644 --- a/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java +++ b/grobid-core/src/main/java/org/grobid/core/engines/FundingAcknowledgementParser.java @@ -224,11 +224,13 @@ public MutablePair, List, List injectedAnnotations = Collections.newSetFromMap(new IdentityHashMap<>()); if (sentenceSegmentation) { Nodes sentences = paragraph.query(".//s"); @@ -236,15 +238,18 @@ public MutablePair, List, List getOffsetPositionsFromNodes(Nodes sentences) return sentencePositions; } - private static void updateParagraphNodeWithAnnotations(Node paragraph, List annotations) { + private static Set updateParagraphNodeWithAnnotations( + Node paragraph, + List annotations) { + Set injectedAnnotations = Collections.newSetFromMap(new IdentityHashMap<>()); int pos = 0; List newChildren = new ArrayList<>(); for (int i = 0; i < paragraph.getChildCount(); i++) { @@ -429,6 +437,7 @@ private static void updateParagraphNodeWithAnnotations(Node paragraph, List nodes = getNodesAnnotationsInTextNode(currentNode, annotationsInThisChunk, pos); newChildren.addAll(nodes); + injectedAnnotations.addAll(annotationsInThisChunk); } else { newChildren.add(currentNode); } @@ -446,9 +455,14 @@ private static void updateParagraphNodeWithAnnotations(Node paragraph, List annotations) { + private static Set updateSentencesNodesWithAnnotations( + Nodes sentences, + List annotations) { + Set injectedAnnotations = Collections.newSetFromMap(new IdentityHashMap<>()); int pos = 0; int sentenceStartOffset = 0; for (Node sentence : sentences) { @@ -469,6 +483,7 @@ private static void updateSentencesNodesWithAnnotations(Nodes sentences, List nodes = getNodesAnnotationsInTextNode(currentNode, annotationsInThisChunk, pos); newChildren.addAll(nodes); + injectedAnnotations.addAll(annotationsInThisChunk); } else { newChildren.add(currentNode); } @@ -491,113 +506,73 @@ private static void updateSentencesNodesWithAnnotations(Nodes sentences, List survivingAnnotationCounts = collectSurvivingAnnotationCounts(paragraph); - if (survivingAnnotationCounts.isEmpty()) { - localEntities.setFundings(new ArrayList<>()); + /** + * Removes the extracted entities (fundings, persons, affiliations) whose annotations were all + * dropped during injection into the XML. An entity is kept when it has no recorded annotation + * at all, or when at least one of its annotations was successfully injected; it is dropped only + * when it had annotations and none of them survived. The correspondence is resolved by object + * identity through {@link AnnotatedXMLElement#getEntity()}, so entities sharing the same textual + * value (e.g. two fundings with the same funder name) remain distinct. + */ + private static void pruneEntitiesWithoutInjectedAnnotation( + FundingAcknowledgmentParse localEntities, + List allAnnotations, + Set injectedAnnotations) { + if (localEntities == null) { return; } - List survivingFundings = new ArrayList<>(); - for (Funding funding : localEntities.getFundings()) { - if (consumeFirstMatchingFundingAnnotation(funding, survivingAnnotationCounts)) { - survivingFundings.add(funding); + Set entitiesWithAnyAnnotation = Collections.newSetFromMap(new IdentityHashMap<>()); + for (AnnotatedXMLElement annotation : allAnnotations) { + if (annotation.getEntity() != null) { + entitiesWithAnyAnnotation.add(annotation.getEntity()); } } - localEntities.setFundings(survivingFundings); - } - private static Map collectSurvivingAnnotationCounts(Element paragraph) { - Map survivingAnnotationCounts = new HashMap<>(); - Nodes survivingAnnotations = paragraph.query(".//*[local-name()='rs']"); - for (int i = 0; i < survivingAnnotations.size(); i++) { - Node survivingAnnotation = survivingAnnotations.get(i); - if (!(survivingAnnotation instanceof Element)) { - continue; + Set entitiesWithInjectedAnnotation = Collections.newSetFromMap(new IdentityHashMap<>()); + for (AnnotatedXMLElement annotation : injectedAnnotations) { + if (annotation.getEntity() != null) { + entitiesWithInjectedAnnotation.add(annotation.getEntity()); } - - Element annotationElement = (Element) survivingAnnotation; - Attribute typeAttribute = annotationElement.getAttribute("type"); - if (typeAttribute == null) { - continue; - } - - AnnotationSignature key = new AnnotationSignature(typeAttribute.getValue(), annotationElement.getValue()); - survivingAnnotationCounts.merge(key, 1, Integer::sum); } - return survivingAnnotationCounts; + localEntities.setFundings( + retainEntitiesWithInjectedAnnotation( + localEntities.getFundings(), + entitiesWithAnyAnnotation, + entitiesWithInjectedAnnotation)); + localEntities.setPersons( + retainEntitiesWithInjectedAnnotation( + localEntities.getPersons(), + entitiesWithAnyAnnotation, + entitiesWithInjectedAnnotation)); + localEntities.setAffiliations( + retainEntitiesWithInjectedAnnotation( + localEntities.getAffiliations(), + entitiesWithAnyAnnotation, + entitiesWithInjectedAnnotation)); } - private static boolean consumeFirstMatchingFundingAnnotation( - Funding funding, - Map survivingAnnotationCounts) { - return consumeAnnotation( - "funder", - funding.getFunder() != null ? funding.getFunder().getFullName() : null, - survivingAnnotationCounts) - || consumeAnnotation("grantNumber", funding.getGrantNumber(), survivingAnnotationCounts) - || consumeAnnotation("grantName", funding.getGrantName(), survivingAnnotationCounts) - || consumeAnnotation("programName", funding.getProgramFullName(), survivingAnnotationCounts) - || consumeAnnotation("projectName", funding.getProjectFullName(), survivingAnnotationCounts); - } - - private static boolean consumeAnnotation( - String type, - String value, - Map survivingAnnotationCounts) { - if (StringUtils.isBlank(value)) { - return false; - } - - AnnotationSignature key = new AnnotationSignature(type, value); - Integer count = survivingAnnotationCounts.get(key); - if (count == null) { - return false; - } - - if (count == 1) { - survivingAnnotationCounts.remove(key); - } else { - survivingAnnotationCounts.put(key, count - 1); - } - - return true; - } - - private static class AnnotationSignature { - private final String type; - private final String value; - - private AnnotationSignature(String type, String value) { - this.type = type; - this.value = value; + private static List retainEntitiesWithInjectedAnnotation( + List entities, + Set entitiesWithAnyAnnotation, + Set entitiesWithInjectedAnnotation) { + if (CollectionUtils.isEmpty(entities)) { + return entities; } - @Override - public boolean equals(Object other) { - if (this == other) { - return true; + List retained = new ArrayList<>(); + for (T entity : entities) { + if (!entitiesWithAnyAnnotation.contains(entity) + || entitiesWithInjectedAnnotation.contains(entity)) { + retained.add(entity); } - if (!(other instanceof AnnotationSignature)) { - return false; - } - AnnotationSignature that = (AnnotationSignature) other; - return Objects.equals(type, that.type) && Objects.equals(value, that.value); - } - - @Override - public int hashCode() { - return Objects.hash(type, value); } + return retained; } /** @@ -737,6 +712,8 @@ protected MutablePair, FundingAcknowledgmentParse> get List elements = new ArrayList<>(); List positions = new ArrayList<>(); + // the extracted entity each annotation belongs to, aligned with elements/positions + List owners = new ArrayList<>(); int posTokenization = 0; int posCharacters = 0; @@ -802,6 +779,7 @@ protected MutablePair, FundingAcknowledgmentParse> get entity.addAttribute(new Attribute("type", "funder")); entity.appendChild(clusterContent); elements.add(entity); + owners.add(funding); positions.add(new OffsetPosition(posTokenization, endPosTokenization)); } else if (clusterLabel.equals(FUNDING_GRANT_NAME)) { @@ -821,6 +799,7 @@ protected MutablePair, FundingAcknowledgmentParse> get entity.addAttribute(new Attribute("type", "grantName")); entity.appendChild(clusterContent); elements.add(entity); + owners.add(funding); positions.add(new OffsetPosition(posTokenization, endPosTokenization)); @@ -840,6 +819,7 @@ protected MutablePair, FundingAcknowledgmentParse> get entity.addAttribute(new Attribute("type", "person")); entity.appendChild(clusterContent); elements.add(entity); + owners.add(person); positions.add(new OffsetPosition(posTokenization, endPosTokenization)); @@ -859,6 +839,7 @@ protected MutablePair, FundingAcknowledgmentParse> get entity.addAttribute(new Attribute("type", "affiliation")); entity.appendChild(clusterContent); elements.add(entity); + owners.add(affiliation); positions.add(new OffsetPosition(posTokenization, endPosTokenization)); @@ -878,6 +859,7 @@ protected MutablePair, FundingAcknowledgmentParse> get entity.addAttribute(new Attribute("type", "institution")); entity.appendChild(clusterContent); elements.add(entity); + owners.add(institution); positions.add(new OffsetPosition(posTokenization, endPosTokenization)); @@ -898,6 +880,7 @@ protected MutablePair, FundingAcknowledgmentParse> get entity.addAttribute(new Attribute("subtype", "infrastructure")); entity.appendChild(clusterContent); elements.add(entity); + owners.add(institution); positions.add(new OffsetPosition(posTokenization, endPosTokenization)); @@ -927,6 +910,7 @@ protected MutablePair, FundingAcknowledgmentParse> get entity.addAttribute(new Attribute("type", "grantNumber")); entity.appendChild(clusterContent); elements.add(entity); + owners.add(funding); positions.add(new OffsetPosition(posTokenization, endPosTokenization)); @@ -947,6 +931,7 @@ protected MutablePair, FundingAcknowledgmentParse> get entity.addAttribute(new Attribute("type", "programName")); entity.appendChild(clusterContent); elements.add(entity); + owners.add(funding); positions.add(new OffsetPosition(posTokenization, endPosTokenization)); @@ -967,6 +952,7 @@ protected MutablePair, FundingAcknowledgmentParse> get entity.addAttribute(new Attribute("type", "projectName")); entity.appendChild(clusterContent); elements.add(entity); + owners.add(funding); positions.add(new OffsetPosition(posTokenization, endPosTokenization)); @@ -1005,7 +991,7 @@ protected MutablePair, FundingAcknowledgmentParse> get List annotations = new ArrayList<>(); for (int i = 0; i < elements.size(); i++) { - annotations.add(new AnnotatedXMLElement(elements.get(i), positions.get(i))); + annotations.add(new AnnotatedXMLElement(elements.get(i), positions.get(i), owners.get(i))); } return MutablePair.of(annotations, parsedStatement); diff --git a/grobid-core/src/test/kotlin/org/grobid/core/engines/FundingAcknowledgementParserIntegrationTest.kt b/grobid-core/src/test/kotlin/org/grobid/core/engines/FundingAcknowledgementParserIntegrationTest.kt index 42e359d696..8bd12e8807 100644 --- a/grobid-core/src/test/kotlin/org/grobid/core/engines/FundingAcknowledgementParserIntegrationTest.kt +++ b/grobid-core/src/test/kotlin/org/grobid/core/engines/FundingAcknowledgementParserIntegrationTest.kt @@ -300,6 +300,30 @@ class FundingAcknowledgementParserIntegrationTest { assertThat(element.toXML(), not(containsString("type=\"funder\""))) } + @Test + fun testXmlFragmentProcessing_shouldKeepFundingWithInjectableAnnotation() { + // The funder span falls entirely within plain text (no overlapping inline element), + // so its annotation can be injected and the funding must be retained. + val input = """ +
+
Acknowledgements

This work was supported by the National Science Foundation

+
+ """.trimIndent() + + val parser = StubFundingAcknowledgementParser( + listOf("National", "Science", "Foundation"), + ) + + val config = GrobidAnalysisConfig.GrobidAnalysisConfigBuilder() + .withSentenceSegmentation(true) + .build() + + val (element, mutableTriple) = parser.processingXmlFragment(input, config) + + assertThat(mutableTriple.left, hasSize(1)) + assertThat(element.toXML(), containsString("type=\"funder\"")) + } + private class StubFundingAcknowledgementParser( private val tokensToLabelAsFunder: List, ) : FundingAcknowledgementParser(GrobidModels.DUMMY) {