rehype-citation
Version:
rehype plugin to add citation and bibliography from bibtex files
663 lines (662 loc) • 20.4 kB
JavaScript
import { plugins, util } from '../core/index.js';
import { parse as parseDate } from '@citation-js/date';
import '../plugin-yaml/index.js';
/**
* Format: Citation File Format (CFF) version 1.2.0
* Spec: https://github.com/citation-file-format/citation-file-format/blob/1.2.0/schema-guide.md
*/
const TYPES_TO_TARGET = {
art: 'graphic',
article: 'article-journal',
audiovisual: 'motion_picture',
bill: 'bill',
blog: 'post-weblog',
book: 'book',
catalogue: 'collection',
conference: 'event',
'conference-paper': 'paper-conference',
data: 'dataset',
database: 'dataset',
dictionary: 'entry-dictionary',
'edited-work': 'document',
encyclopedia: 'entry-encyclopedia',
'film-broadcast': 'broadcast',
generic: 'document',
'government-document': 'regulation',
grant: 'document',
hearing: 'hearing',
'historical-work': 'classic',
'legal-case': 'legal_case',
'legal-rule': 'legislation',
'magazine-article': 'article-magazine',
manual: 'report',
map: 'map',
multimedia: 'motion_picture',
music: 'musical_score',
'newspaper-article': 'article-newspaper',
pamphlet: 'pamphlet',
patent: 'patent',
'personal-communication': 'personal_communication',
proceedings: 'book',
report: 'report',
serial: 'periodical',
slides: 'speech',
software: 'software',
'software-code': 'software',
'software-container': 'software',
'software-executable': 'software',
'software-virtual-machine': 'software',
'sound-recording': 'song',
standard: 'standard',
statute: 'legislation',
thesis: 'thesis',
unpublished: 'article',
video: 'motion_picture',
website: 'webpage',
};
const TYPES_TO_SOURCE = {
article: 'article',
'article-journal': 'article',
'article-magazine': 'magazine-article',
'article-newspaper': 'newspaper-article',
bill: 'bill',
book: 'book',
broadcast: 'film-broadcast',
chapter: 'generic',
classic: 'historical-work',
collection: 'catalogue',
dataset: 'data',
document: 'generic',
entry: 'generic',
'entry-dictionary': 'dictionary',
'entry-encyclopedia': 'encyclopedia',
event: 'conference',
figure: 'generic',
graphic: 'art',
hearing: 'hearing',
interview: 'sound-recording',
legal_case: 'legal-case',
legislation: 'statute',
manuscript: 'historical-work',
map: 'map',
motion_picture: 'film-broadcast',
musical_score: 'music',
pamphlet: 'pamphlet',
'paper-conference': 'conference-paper',
patent: 'patent',
performance: 'generic',
periodical: 'serial',
personal_communication: 'personal-communication',
post: 'serial',
'post-weblog': 'blog',
regulation: 'government-document',
report: 'report',
review: 'generic',
'review-book': 'generic',
software: 'software',
song: 'sound-recording',
speech: 'slides',
standard: 'standard',
thesis: 'thesis',
treaty: 'generic',
webpage: 'website',
};
const ENTITY_PROPS = [
{ source: 'family-names', target: 'family' },
{ source: 'given-names', target: 'given' },
{ source: 'name-particle', target: 'non-dropping-particle' },
{ source: 'name-suffix', target: 'suffix' },
{ source: 'name', target: 'literal' },
{ source: 'orcid', target: '_orcid' },
];
const entity = new util.Translator(ENTITY_PROPS);
const PROP_CONVERTERS = {
names: {
toTarget(names) {
return names.map(entity.convertToTarget);
},
toSource(names) {
return names.map(entity.convertToSource);
},
},
publisher: {
toTarget({ name, city, region, country }) {
const place = [city, region, country].filter(Boolean).join(', ');
return [name, place || undefined];
},
toSource(name, place) {
const entity = { name };
if (place) {
// Parse the following:
// - Country
// - City, Country
// - City, Region, Country
const parts = place.split(', ');
entity.country = parts.pop();
if (parts.length === 2) {
entity.region = parts.pop();
}
if (parts.length === 1) {
entity.city = parts.pop();
}
}
return entity;
},
},
date: {
toTarget(date) {
if (date instanceof Date) {
return parseDate(date.toISOString());
}
else {
return parseDate(new Date(date).toISOString());
}
},
toSource(date) {
if (date.raw) {
return date.raw;
}
const [year, month, day] = date['date-parts'][0];
if (day) {
return new Date(Date.UTC(year, month - 1, day));
}
else if (month) {
return new Date(Date.UTC(year, month - 1));
}
else {
return new Date(Date.UTC(year));
}
},
},
};
const SHARED_PROPS = [
'abstract',
{ source: 'authors', target: 'author', convert: PROP_CONVERTERS.names },
// TODO cff: commit
// TODO cff: contact
{
source: 'date-released',
target: 'issued',
when: { target: { type: 'software' } },
convert: PROP_CONVERTERS.date,
},
{ source: 'doi', target: 'DOI' },
{
source: 'identifiers',
target: ['DOI', 'ISBN', 'ISSN', 'PMCID', 'PMID', 'URL'],
convert: {
toTarget(identifiers) {
const newIdentifiers = Array(6).fill(undefined);
for (const { type, value } of identifiers) {
if (!this.doi && type === 'doi') {
newIdentifiers[0] = value;
}
if (!this.url && type === 'url') {
newIdentifiers[5] = value;
}
if (type === 'other' && value.startsWith('urn:isbn:')) {
newIdentifiers[1] = value.slice(9);
}
if (type === 'other' && value.startsWith('urn:issn:')) {
newIdentifiers[2] = value.slice(9);
}
if (type === 'other' && value.startsWith('pmcid:')) {
newIdentifiers[3] = value.slice(6);
}
if (type === 'other' && value.startsWith('pmid:')) {
newIdentifiers[4] = value.slice(5);
}
}
return newIdentifiers;
},
toSource(doi, isbn, issn, pmcid, pmid, url) {
return [
doi && { type: 'doi', value: doi },
url && { type: 'url', value: url },
isbn && { type: 'other', value: `urn:isbn:${isbn}` },
issn && { type: 'other', value: `urn:issn:${issn}` },
pmcid && { type: 'other', value: `pmcid:${pmcid}` },
pmid && { type: 'other', value: `pmid:${pmid}` },
].filter(Boolean);
},
},
},
{
source: 'keywords',
target: 'keyword',
convert: {
toTarget(keywords) {
return keywords.join(',');
},
toSource(keywords) {
return keywords.split(/,\s*/g);
},
},
},
// TODO cff: license
// TODO cff: license-url
// TODO cff: message *
// TODO cff: repository
// TODO cff: repository-code
// TODO cff: repository-artifact
{
source: 'title',
target: 'title',
when: {
source: { term: false, entry: false },
target: {
type(type) {
return !['entry', 'entry-dictionary', 'entry-encyclopedia'].includes(type);
},
},
},
},
{
source: 'title',
target: 'container-title',
when: {
source: { entry: true, journal: false },
target: { type: ['entry'] },
},
},
{
source: 'title',
target: 'container-title',
when: {
source: { term: true, journal: false },
target: { type: ['entry-dictionary', 'entry-encyclopedia'] },
},
},
{ source: 'url', target: 'URL' },
'version',
];
const MAIN_PROPS = [
// TYPES
{
source: 'type',
target: 'type',
convert: {
toSource(type) {
return type === 'dataset' ? 'dataset' : 'software';
},
toTarget(type) {
return type === 'dataset' ? 'dataset' : 'software';
},
},
},
// Include main mappings
...SHARED_PROPS,
];
const REF_PROPS = [
// Include main mappings
...SHARED_PROPS,
// ABBREVIATION
{ source: 'abbreviation', target: 'title-short' },
{ source: 'abbreviation', target: 'shortTitle' },
// COLLECTIONS
// TODO cff: collection-doi
// TODO cff: collection-type
'collection-title',
// COMMUNICATION
{ source: 'recipients', target: 'recipient', convert: PROP_CONVERTERS.names },
{ source: 'senders', target: 'authors', convert: PROP_CONVERTERS.names },
// CONFERENCE
{
source: 'conference',
target: ['event-title', 'event-date', 'event-place', 'event'],
convert: {
toSource(name, date, place, nameFallback) {
const entity = { name: name || nameFallback };
if (place) {
entity.location = place;
}
if (date) {
entity['date-start'] = PROP_CONVERTERS.date.toSource(date);
if (date['date-parts'] && date['date-parts'].length === 2) {
entity['date-end'] = PROP_CONVERTERS.date.toSource({
'date-parts': [date['date-parts'][1]],
});
}
}
return entity;
},
toTarget(event) {
return [
event.name,
parseDate(event['date-start'].toISOString(), event['date-end'].toISOString()),
event.location,
];
},
},
},
// COPYRIGHT
// TODO cff: contact
// TODO cff: copyright
// DATABASE
{ source: 'database', target: 'source' },
// TODO cff: database-provider NOTE entity
// DATE
{ source: 'date-accessed', target: 'accessed', convert: PROP_CONVERTERS.date },
{
source: 'date-downloaded',
target: 'accessed',
convert: PROP_CONVERTERS.date,
when: { source: { 'date-accessed': false }, target: false },
},
{
source: 'date-published',
target: 'issued',
convert: PROP_CONVERTERS.date,
when: {
source: { 'date-released': false },
target() {
return this.type !== 'book' || !this.version;
},
},
},
{
source: ['year', 'month'],
target: 'issued',
when: { source: { 'date-published': false, 'date-released': false, year: true } },
convert: {
toTarget(year, month) {
const date = month ? [year, month] : [year];
return { 'date-parts': [date] };
},
toSource(issued) {
const [year, month] = issued['date-parts'][0];
return [year, month];
},
},
},
{
source: 'year-original',
target: 'original-date',
convert: {
toTarget(year) {
return { 'date-parts': [[year]] };
},
toSource(date) {
return date['date-parts'][0][0];
},
},
},
// EDITION
'edition',
// EDITORS
{ source: 'editors', target: 'editor', convert: PROP_CONVERTERS.names },
{ source: 'editors-series', target: 'collection-editor', convert: PROP_CONVERTERS.names },
// ENTRY
{
source: 'entry',
target: 'title',
when: {
source: { term: false },
target: { type: 'entry' },
},
},
{
source: 'term',
target: 'title',
when: {
target: { type: ['entry-dictionary', 'entry-encyclopedia'] },
},
},
// FORMAT
{ source: 'format', target: 'dimensions' },
'medium',
// GENRE
{
source: 'data-type',
target: 'genre',
when: {
target: {
type(type) {
return type !== 'thesis';
},
},
},
},
{
source: 'thesis-type',
target: 'genre',
when: {
source: { 'data-type': false },
target: { type: 'thesis' },
},
},
// IDENTIFIERS
{ source: 'isbn', target: 'ISBN' },
{ source: 'issn', target: 'ISSN' },
// TODO cff: nihmsid
{ source: 'pmcid', target: 'PMCID' },
// ISSUE
'issue',
// JOURNAL
{ source: 'journal', target: 'container-title' },
{ source: 'volume-title', target: 'volume-title' },
{
source: 'issue-title',
target: 'volume-title',
when: {
source: { 'volume-title': false },
target: false,
},
},
// TODO cff: issue-date
// LANGUAGE
{
source: 'languages',
target: 'language',
when: {
target: true,
// NOTE: possible values not as strict in csl, so test (crudely) if the value is ok first
source: {
language(code) {
return /[a-z]{2,3}/.test(code);
},
},
},
convert: {
// NOTE: CSL can only hold one language
toSource(language) {
return [language];
},
toTarget(languages) {
return languages[0];
},
},
},
// LOCATION
{
source: 'location',
target: ['archive', 'archive-place'],
convert: PROP_CONVERTERS.publisher,
},
// LOCATION (CODE)
// TODO cff: filename
// TODO cff: loc-start
// TODO cff: loc-end
// NOTES
{ source: 'notes', target: 'note', when: { source: { scope: false } } },
{ source: 'scope', target: 'note', when: { target: false } },
// NUMBER
'number',
// PATENT
{
source: 'patent-states',
target: 'jurisdiction',
// NOTE: CSL jurisdiction can contain more than just US states
when: { target: false },
convert: {
toTarget(states) {
return states.join(', ');
},
},
},
// PUBLISHER
{
source: ['institution', 'department'],
target: ['publisher', 'publisher-place'],
when: { source: { publisher: false }, target: { type: 'thesis' } },
convert: {
toTarget(institution, department) {
const [name, place] = PROP_CONVERTERS.publisher.toTarget(institution);
return [department ? `${department}, ${name}` : name, place];
},
toSource(name, place) {
return [PROP_CONVERTERS.publisher.toSource(name, place)];
},
},
},
{
source: 'publisher',
target: ['publisher', 'publisher-place'],
when: {
target: {
type(type) {
return type !== 'thesis';
},
},
},
convert: PROP_CONVERTERS.publisher,
},
// SECTION
'section',
// STATUS
{
source: 'status',
target: 'status',
when: {
source: true,
// NOTE: possible values not as strict in csl, so test if the value is ok first
target: {
status: [
'in-preparation',
'abstract',
'submitted',
'in-press',
'advance-online',
'preprint',
],
},
},
},
// PAGES
{ source: 'start', target: 'page-first', when: { target: { page: false } } },
{
source: ['start', 'end'],
target: 'page',
convert: {
toTarget(start, end) {
return end ? `${start}-${end}` : start;
},
toSource(page) {
const [start, end] = page.split('-');
return end ? [start, end] : [start];
},
},
},
{ source: 'pages', target: 'number-of-pages' },
// TRANSLATORS
{ source: 'translators', target: 'translator', convert: PROP_CONVERTERS.names },
// TYPES
{
source: 'type',
target: 'type',
convert: {
toTarget(type) {
return TYPES_TO_TARGET[type] || 'document';
},
toSource(type) {
if (type === 'book' && this['event-title']) {
return 'proceedings';
}
return TYPES_TO_SOURCE[type] || 'generic';
},
},
},
// VOLUMES
'volume',
{ source: 'number-volumes', target: 'number-of-volumes' },
];
const mainTranslator = new util.Translator(MAIN_PROPS);
const refTranslator = new util.Translator(REF_PROPS);
const CFF_VERSION = '1.2.0';
/** Add doi or url as unique id if available to make citation easy */
function addId(entry) {
if ('DOI' in entry) {
entry.id = entry.DOI;
}
else if ('URL' in entry) {
entry.id = entry.URL.replace('http://', '').replace('https://', '');
}
}
function parse(input) {
const main = mainTranslator.convertToTarget(input);
if (input['cff-version'] <= '1.1.0') {
main.type = TYPES_TO_TARGET.software;
}
main._cff_mainReference = true;
addId(main);
const output = [main];
if (input['preferred-citation']) {
const preferredCitation = refTranslator.convertToTarget(input['preferred-citation']);
addId(preferredCitation);
output.push(preferredCitation);
}
if (Array.isArray(input.references)) {
output.push(...input.references.map(refTranslator.convertToTarget));
}
return output;
}
function format(input, options = {}) {
input = input.slice();
const { main, preferred, cffVersion = CFF_VERSION, message = 'Please cite the following works when using this software.', } = options;
let preferredCitation;
const preferredIndex = input.findIndex((entry) => preferred && entry.id === preferred);
if (cffVersion >= '1.2.0' && preferredIndex > -1) {
preferredCitation = refTranslator.convertToSource(...input.splice(preferredIndex, 1));
}
let mainIndex = input.findIndex((entry) => (main ? entry.id === main : entry._cff_mainReference));
mainIndex = mainIndex > -1 ? mainIndex : 0;
const mainRef = input[mainIndex]
? mainTranslator.convertToSource(...input.splice(mainIndex, 1))
: {};
if (mainRef && cffVersion < '1.2.0') {
delete mainRef.type;
}
const cff = { 'cff-version': cffVersion, message, ...mainRef };
if (preferredCitation) {
cff['preferred-citation'] = preferredCitation;
}
if (input.length) {
// @ts-ignore
cff.references = input.map(refTranslator.convertToSource);
}
return cff;
}
plugins.add('@cff', {
input: {
'@cff/object': {
parseType: {
dataType: 'SimpleObject',
propertyConstraint: {
props: 'cff-version',
},
},
parse,
},
},
output: {
cff(data, options = {}) {
const output = format(data, options);
if (options.type === 'object') {
return output;
}
else {
return plugins.output.format('yaml', output);
}
},
},
});