From 3100648e97bac62e745dfadc65b99a15423bebd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti-Jussi=20Nyg=C3=A5rd?= Date: Sat, 25 Apr 2026 19:00:57 +0300 Subject: [PATCH] pkp/pkp-lib#12392 Support Funder Data --- CrossrefExportDeployment.php | 9 +++ filter/ArticleCrossrefXmlFilter.php | 87 ++++++++++++++++++++++++++++- filter/IssueCrossrefXmlFilter.php | 2 + 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/CrossrefExportDeployment.php b/CrossrefExportDeployment.php index b61ca44..d245001 100644 --- a/CrossrefExportDeployment.php +++ b/CrossrefExportDeployment.php @@ -30,6 +30,7 @@ class CrossrefExportDeployment public const CROSSREF_XMLNS_AI = 'http://www.crossref.org/AccessIndicators.xsd'; public const CROSSREF_XMLNS_XML = 'http://www.w3.org/XML/1998/namespace'; public const CROSSREF_XMLNS_REL = 'http://www.crossref.org/relations.xsd'; + public const CROSSREF_XMLNS_FR = 'http://www.crossref.org/fundref.xsd'; public Context $_context; @@ -97,6 +98,14 @@ public function getXmlSchemaLocation(): string return static::CROSSREF_XSI_SCHEMALOCATION; } + /** + * Get the FundRef namespace URN + */ + public function getFundrefNamespace(): string + { + return static::CROSSREF_XMLNS_FR; + } + /** * Get the JATS namespace URN */ diff --git a/filter/ArticleCrossrefXmlFilter.php b/filter/ArticleCrossrefXmlFilter.php index 0ca412b..393298c 100644 --- a/filter/ArticleCrossrefXmlFilter.php +++ b/filter/ArticleCrossrefXmlFilter.php @@ -241,7 +241,9 @@ public function createJournalArticleNode(DOMDocument $doc, Publication $publicat // rel:program $this->appendRelationships($doc, $journalArticleNode, $this->versionsDois); } else { - // if no crossmark element is used, append ai:program here + // if no crossmark element is used, append program nodes here + // fr:program (FundRef) + $this->appendFundrefNode($doc, $journalArticleNode, $publication); // ai:program (AccessIndicators) element, that contains the license URL $this->appendProgramNode($doc, $journalArticleNode, $publication); } @@ -439,6 +441,86 @@ public function appendProgramNode(DOMDocument $doc, DOMElement $parentNode, Publ } } + /** + * Append fr:program (FundRef) node with funding information + */ + public function appendFundrefNode(DOMDocument $doc, DOMElement $parentNode, Publication $publication): void + { + /** @var CrossrefExportDeployment $deployment */ + $deployment = $this->getDeployment(); + + $funders = $publication->getData('funders'); + + if (empty($funders)) { + return; + } + + $locale = $publication->getData('locale'); + + $programNode = $doc->createElementNS($deployment->getFundrefNamespace(), 'fr:program'); + $programNode->setAttribute('name', 'fundref'); + + foreach ($funders as $funder) { + + $groupNode = $doc->createElementNS($deployment->getFundrefNamespace(), 'fr:assertion'); + $groupNode->setAttribute('name', 'fundgroup'); + + $funderName = $funder->getLocalizedData('name', $locale); + + $rorNode = null; + if (!empty($funder->ror)) { + $rorNode = $doc->createElementNS($deployment->getFundrefNamespace(), 'fr:assertion', $funder->ror); + $rorNode->setAttribute('name', 'ror'); + } + + if (!empty($funderName)) { + $funderNameNode = $doc->createElementNS($deployment->getFundrefNamespace(), 'fr:assertion', htmlspecialchars($funderName, ENT_COMPAT, 'UTF-8')); + $funderNameNode->setAttribute('name', 'funder_name'); + if ($rorNode) { + $funderNameNode->appendChild($rorNode); + } + $groupNode->appendChild($funderNameNode); + } elseif ($rorNode) { + $groupNode->appendChild($rorNode); + } + + if (!empty($funder->grants)) { + foreach ($funder->grants as $grant) { + + $awardNode = null; + if (!empty($grant['grantNumber'])) { + $awardNode = $doc->createElementNS($deployment->getFundrefNamespace(), 'fr:assertion', htmlspecialchars($grant['grantNumber'], ENT_COMPAT, 'UTF-8')); + $awardNode->setAttribute('name', 'award_number'); + } + + if (!empty($grant['grantDoi'])) { + $grantDoiNode = $doc->createElementNS($deployment->getFundrefNamespace(), 'fr:assertion', htmlspecialchars($grant['grantDoi'], ENT_COMPAT, 'UTF-8')); + $grantDoiNode->setAttribute('name', 'grant_doi'); + + if ($awardNode) { + $grantDoiNode->appendChild($awardNode); + } + + $groupNode->appendChild($grantDoiNode); + + } elseif ($awardNode) { + $groupNode->appendChild($awardNode); + } + } + } + + if ($groupNode->hasChildNodes()) { + $programNode->appendChild($groupNode); + } + + } + + if ($programNode->hasChildNodes()) { + $parentNode->appendChild($programNode); + } + + } + /** * Append the collection node 'collection property="crawler-based"' to the doi data node. */ @@ -715,6 +797,9 @@ public function appendCrossmarkNode(DOMDocument $doc, DOMElement $parentNode, ar $customMetadataNode->appendChild($assertionFundingStatementNode); } + // fr:program (FundRef) + $this->appendFundrefNode($doc, $customMetadataNode, $publication); + // ai:program (AccessIndicators) element, that contains the license URL $this->appendProgramNode($doc, $customMetadataNode, $publication); diff --git a/filter/IssueCrossrefXmlFilter.php b/filter/IssueCrossrefXmlFilter.php index 3df04e5..8d16e9d 100644 --- a/filter/IssueCrossrefXmlFilter.php +++ b/filter/IssueCrossrefXmlFilter.php @@ -88,6 +88,8 @@ public function createRootNode(DOMDocument $doc): DOMElement $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:jats', $deployment->getJATSNamespace()); $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ai', $deployment->getAINamespace()); $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:rel', $deployment->getRelNamespace()); + $rootNode->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:fr', $deployment->getFundrefNamespace()); + $rootNode->setAttribute('version', $deployment->getXmlSchemaVersion()); $rootNode->setAttribute('xsi:schemaLocation', $deployment->getNamespace() . ' ' . $deployment->getSchemaFilename()); return $rootNode;