From 4f086c03e3341266eb9d284f427c6143cf31a412 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Fri, 3 Jul 2026 06:05:32 +0000 Subject: [PATCH] Decode HTML entities in CSL text fields CrossRef's CSL-JSON metadata sometimes contains HTML entities in title and journal name fields. --- .changeset/silly-phones-warn.md | 5 +++++ packages/citation-js-utils/package.json | 2 ++ packages/citation-js-utils/src/index.ts | 6 ++++-- .../citation-js-utils/tests/basic.spec.ts | 20 +++++++++++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 .changeset/silly-phones-warn.md diff --git a/.changeset/silly-phones-warn.md b/.changeset/silly-phones-warn.md new file mode 100644 index 0000000000..afa8bbb6ae --- /dev/null +++ b/.changeset/silly-phones-warn.md @@ -0,0 +1,5 @@ +--- +'citation-js-utils': patch +--- + +Decode HTML entities in CrossRef citation data diff --git a/packages/citation-js-utils/package.json b/packages/citation-js-utils/package.json index 352f537b0c..1f658721cb 100644 --- a/packages/citation-js-utils/package.json +++ b/packages/citation-js-utils/package.json @@ -38,9 +38,11 @@ "@citation-js/core": "^0.7.18", "@citation-js/plugin-bibtex": "^0.7.18", "@citation-js/plugin-csl": "^0.7.18", + "he": "^1.2.0", "sanitize-html": "^2.7.0" }, "devDependencies": { + "@types/he": "^1.2.0", "@types/sanitize-html": "^2.6.2" } } diff --git a/packages/citation-js-utils/src/index.ts b/packages/citation-js-utils/src/index.ts index 6c3abc080f..6f2b5af590 100644 --- a/packages/citation-js-utils/src/index.ts +++ b/packages/citation-js-utils/src/index.ts @@ -2,6 +2,7 @@ import { Cite, plugins } from '@citation-js/core'; import { doi as doiUtils } from 'doi-utils'; import { clean as cleanCSL } from '@citation-js/core/lib/plugins/input/csl.js'; import sanitizeHtml from 'sanitize-html'; +import he from 'he'; import '@citation-js/plugin-bibtex'; import '@citation-js/plugin-csl'; @@ -267,7 +268,8 @@ export function getCitationRenderers(data: CSL[]): CitationRenderer { if (!c.id) { c.id = formatLabel(c); } - // Trim the titles, DOIs, etc. on load + // Trim the titles, DOIs, etc. on load; decode HTML entities from + // CrossRef metadata. [ 'title', 'note', @@ -279,7 +281,7 @@ export function getCitationRenderers(data: CSL[]): CitationRenderer { 'DOI', 'ISSN', ].forEach((tag) => { - if (c[tag] && typeof c[tag] === 'string') c[tag] = c[tag].trim(); + if (c[tag] && typeof c[tag] === 'string') c[tag] = he.decode(c[tag].trim()); }); // Trim the DOIs and URLs (these are encoded) on load if (c.URL) c.URL = c.URL.replace(/^(%20)*/, '').replace(/(%20)*$/, ''); diff --git a/packages/citation-js-utils/tests/basic.spec.ts b/packages/citation-js-utils/tests/basic.spec.ts index ea75c89215..154255d0df 100644 --- a/packages/citation-js-utils/tests/basic.spec.ts +++ b/packages/citation-js-utils/tests/basic.spec.ts @@ -79,6 +79,26 @@ describe('yearFromCitation', () => { }); }); +describe('HTML entity decoding in CSL fields', () => { + it('decodes HTML entities in title and container-title from CrossRef CSL-JSON', () => { + const csl = [ + { + id: 'test2024', + type: 'article-journal', + title: 'Signal & Image Processing & More', + 'container-title': 'Computers & Geosciences', + author: [{ family: 'Smith', given: 'J.' }], + issued: { 'date-parts': [[2024]] }, + DOI: '10.1234/test', + }, + ]; + const renderers = getCitationRenderers(csl); + const cite = renderers['test2024']; + expect(cite.cite.title).toEqual('Signal & Image Processing & More'); + expect(cite.cite['container-title']).toEqual('Computers & Geosciences'); + }); +}); + describe('firstNonDoiUrl', () => { it('no url returns undefined', async () => { expect(firstNonDoiUrl('my citation', 'abc123')).toEqual(undefined);