diff --git a/README.md b/README.md index 96fe7dc..dc151a3 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,28 @@ Dependending on their types some entries will be mapped to different fields in M | *any* | VL | volume | | RPRT | VL | series_number | +### From Mendeley to RIS + +It is also possible to generate RIS records from Mendeley references: (using the above table) + +```javascript +RIS.fromMendeley([{ type: 'journal' + , title: 'Moon 69' + , year: 1969 + , authors: [{last_name: 'Armstrong', first_name: 'Neil'}] + , identifiers: {doi: 'doi/123'}}]); + +//=> TY - JOUR +//=> TI - Moon 69 +//=> PY - 1969 +//=> AU - Armstrong, Neil +//=> DO - doi/123 +//=> ER - +//=> +``` + +*Note: Mendeley references are validated against [this schema](https://github.com/customcommander/ris/blob/master/src/mendeley.schema.json) before they converted into RIS records so it is possible to end up with less or no records at all if the Mendeley references are not valid.* + ## Development The following command will: diff --git a/src/index.js b/src/index.js index ab04b0a..8f200fb 100644 --- a/src/index.js +++ b/src/index.js @@ -3,4 +3,5 @@ * @license MIT */ module.exports = require('./parser.js'); -module.exports.toMendeley = require('./mendeley').to; \ No newline at end of file +module.exports.toMendeley = require('./mendeley').to; +module.exports.fromMendeley = require('./mendeley').from; diff --git a/src/mendeley.js b/src/mendeley.js index dd396f0..9110bbf 100644 --- a/src/mendeley.js +++ b/src/mendeley.js @@ -176,3 +176,162 @@ module.exports.to = risText => parser(risText).reduce((arr, ris) => { return arr; }, []); + + + +/***************************************************************************** + * FROM MENDELEY TO RIS * + *****************************************************************************/ + + + +const ris_copy = (field, value) => `${field} - ${value}\n`; +const ris_eor = () => 'ER - \n'; +const ris_ignore = () => ''; + +const ris_name = + (field, {last_name, first_name}) => + (first_name + ? ris_copy(field, `${last_name}, ${first_name}`) + : ris_copy(field, last_name)); + +const ris_dateaccess = + (field, value) => + ris_copy(field, value.replace(/-/g, '/')); + +const typeMendeleyToRIS = + { "bill": "BILL" + , "book": "BOOK" + , "case": "CASE" + , "book_section": "CHAP" + , "computer_program": "COMP" + , "conference_proceedings": "CONF" + , "encyclopedia_article": "ENCYC" + , "generic": "GEN" + , "hearing": "HEAR" + , "web_page": "ICOMM" + , "journal": "JFULL" + , "journal": "JOUR" + , "magazine_article": "MGZN" + , "film": "MPCT" + , "newspaper_article": "NEWS" + , "patent": "PAT" + , "report": "RPRT" + , "statute": "STAT" + , "thesis": "THES" + , "working_paper": "UNPB" }; + +const ris_type = + (field, value) => + ( value in typeMendeleyToRIS + ? ris_copy(field, typeMendeleyToRIS[value]) + : ris_copy(field, "GEN")); + + +/* +Keys in the map correspond to Mendeley references fields. +See https://dev.mendeley.com/methods/#documents + +Each Mendeley field is mapped to a RIS field e.g. abstract -> AB and the map +also defines how to handle values when moving them across domains. + +Most of the time we simply copy them from one domain into another but there +are certain fields that need to be converted. + +For example names in Mendeley are objects e.g. {last_name: 'Doe', first_name: 'John'} +which need to be converted to a string in RIS e.g. 'Doe, John'. + +There isn't an obvious RIS equivalent for some of the Mendeley fields. +Those are assigned a fake `??` RIS entry type and are simply ignored. +We want to keep them in this map so that we know which fields in Mendeley +won't be exported into RIS. (Also maybe one day some of these fields can be +assigned to actual RIS fields.) */ + +const mapFrom = + { "abstract": ["AB", ris_copy ] + , "accessed": ["DA", ris_dateaccess ] + , "authors": ["AU", ris_name ] + , "chapter": ["SE", ris_copy ] + , "citation_key": ["??", ris_ignore ] + , "city": ["CY", ris_copy ] + , "code": ["??", ris_ignore ] + , "country": ["??", ris_ignore ] + , "department": ["??", ris_ignore ] + , "edition": ["ET", ris_copy ] + , "editors": ["A2", ris_name ] + , "genre": ["??", ris_ignore ] + , "identifiers.arxiv": ["??", ris_ignore ] + , "identifiers.doi": ["DO", ris_copy ] + , "identifiers.isbn": ["SN", ris_copy ] + , "identifiers.issn": ["SN", ris_copy ] + , "identifiers.pii": ["??", ris_ignore ] + , "identifiers.pmid": ["AN", ris_copy ] + , "identifiers.pui": ["??", ris_ignore ] + , "identifiers.scopus": ["??", ris_ignore ] + , "identifiers.sgr": ["??", ris_ignore ] + , "institution": ["AU", ris_copy ] + , "issue": ["IS", ris_copy ] + , "keywords": ["KW", ris_copy ] + , "language": ["LA", ris_copy ] + , "medium": ["M3", ris_copy ] + , "notes": ["RN", ris_copy ] + , "pages": ["SP", ris_copy ] + , "patent_application_number": ["M1", ris_copy ] + , "patent_legal_status": ["C6", ris_copy ] + , "patent_owner": ["??", ris_ignore ] + , "publisher": ["PB", ris_copy ] + , "reprint_edition": ["??", ris_ignore ] + , "revision": ["??", ris_ignore ] + , "series": ["T3", ris_copy ] + , "series_editor": ["AU", ris_copy ] + , "series_number": ["VL", ris_copy ] + , "short_title": ["ST", ris_copy ] + , "source": ["T2", ris_copy ] + , "source_type": ["??", ris_ignore ] + , "tags": ["LB", ris_copy ] + , "title": ["TI", ris_copy ] + , "translators": ["TA", ris_name ] + , "type": ["TY", ris_type ] + , "user_context": ["??", ris_ignore ] + , "volume": ["VL", ris_copy ] + , "websites": ["UR", ris_copy ] + , "year": ["PY", ris_copy ]}; + +/* +Transform a Mendeley reference into a RIS record. +See mendeley.schema.json +*/ +const ris_record = ref => { + const rec = Object.entries(ref).reduce((ris, [mk, mv/*Mendeley key & value*/]) => { + let rk; //RIS key + let rv; //RIS value + if (mk == "identifiers") { + let {arxiv, doi, isbn, issn, pii, pmid, pui, scopus, sgr} = mv; + [rk, rv] = mapFrom['identifiers.arxiv' ]; ris += arxiv ? rv(rk, arxiv) : ''; + [rk, rv] = mapFrom['identifiers.doi' ]; ris += doi ? rv(rk, doi) : ''; + [rk, rv] = mapFrom['identifiers.isbn' ]; ris += isbn ? rv(rk, isbn) : ''; + [rk, rv] = mapFrom['identifiers.issn' ]; ris += issn ? rv(rk, issn) : ''; + [rk, rv] = mapFrom['identifiers.pii' ]; ris += pii ? rv(rk, pii) : ''; + [rk, rv] = mapFrom['identifiers.pmid' ]; ris += pmid ? rv(rk, pmid) : ''; + [rk, rv] = mapFrom['identifiers.pui' ]; ris += pui ? rv(rk, pui) : ''; + [rk, rv] = mapFrom['identifiers.scopus']; ris += scopus ? rv(rk, scopus) : ''; + [rk, rv] = mapFrom['identifiers.sgr' ]; ris += sgr ? rv(rk, sgr) : ''; + } else if (Array.isArray(mv)) { + [rk, rv] = mapFrom[mk]; + ris += mv.map(x => rv(rk, x)).join(''); + } else { + [rk, rv] = mapFrom[mk]; + ris += rv(rk, mv); + } + return ris; + }, ''); + if (!rec) return ''; + return rec + ris_eor(); +}; + +module.exports.from = + references => + references.reduce((ris, ref) => + ( validate(ref) + ? ris + ris_record(ref) + : ris), ''); diff --git a/test/features/mendeley.feature b/test/features/mendeley.feature index 42f6377..56f463a 100644 --- a/test/features/mendeley.feature +++ b/test/features/mendeley.feature @@ -506,6 +506,7 @@ Scenario: Report records # The point is to show validation does occur. # Not sure it's worth testing all fields as this would be quite long # and expensive to do. +@browser Scenario Outline: Validate Mendeley documents Given I convert this to Mendeley """ @@ -522,3 +523,71 @@ Scenario Outline: Validate Mendeley documents | DA | 2004 | | DA | 2021/02/29 | | ET | no longer than 20 characters | + +# From Mendeley to RIS + +@browser +Scenario: Mendeley references can be exported to RIS + Given I convert this from Mendeley + """ + [ { "type": "journal" + , "title": "lorem ipsum" + , "authors": [ {"last_name": "Doe"} + , {"last_name": "Doe", "first_name": "Jane"} + ] + , "editors": [ {"last_name": "Foo", "first_name": "Bar"}] + , "accessed": "2021-05-09" + , "websites": [ "https://example.com/1" + , "https://example.com/2" + , "https://example.com/3" + ] + , "keywords": [ "abc" + , "def" + , "ghi" + ] + , "institution": "University123" + , "identifiers": { "doi": "doi123" + , "pmid": "pmid123" + , "arxiv": "arxiv123" + } + } + ] + """ + Then I will get this RIS file + """ + TY - JOUR + TI - lorem ipsum + AU - Doe + AU - Doe, Jane + A2 - Foo, Bar + DA - 2021/05/09 + UR - https://example.com/1 + UR - https://example.com/2 + UR - https://example.com/3 + KW - abc + KW - def + KW - ghi + AU - University123 + DO - doi123 + AN - pmid123 + ER - + + """ + +@browser +Scenario: Invalid Mendeley references are ignored + Given I convert this from Mendeley + """ + [ {"type": "journal", "title": "lorem ipsum", "year": "not a number"} + , {"type": "journal", "title": "additional fields not allowed", "answer": 42} + , {"type": "foobarx", "title": "not a valid title"} + , {"type": "journal", "title": "this works"} + ] + """ + Then I will get this RIS file + """ + TY - JOUR + TI - this works + ER - + + """ \ No newline at end of file diff --git a/test/steps.js b/test/steps.js index 3d550e5..84d8927 100644 --- a/test/steps.js +++ b/test/steps.js @@ -11,6 +11,11 @@ defineStep('I convert this to Mendeley', async function (file) { this.list = await this.toMendeley(file); }); +defineStep('I convert this from Mendeley', async function (json) { + const references = JSON.parse(json); + this.risContent = await this.fromMendeley(references); +}); + defineStep('I have this file {word}', function (file) { this.file = fs.readFileSync(path.join(__dirname, 'samples', file), 'utf-8'); }); @@ -61,3 +66,7 @@ defineStep('I will get this object', function (text) { const obj = JSON.parse(text); assert.deepStrictEqual(this.list[0], obj); }); + +defineStep('I will get this RIS file', function (expected) { + assert.deepStrictEqual(this.risContent, expected); +}); diff --git a/test/world.js b/test/world.js index b2f3386..7164a4b 100644 --- a/test/world.js +++ b/test/world.js @@ -21,6 +21,11 @@ module.exports = class { if (!this.browser) return this.RIS.toMendeley(risContent); return this.page.evaluate(risContent_ => RIS.toMendeley(risContent_), risContent); } + + async fromMendeley(references) { + if (!this.browser) return this.RIS.fromMendeley(references); + return this.page.evaluate(references_ => RIS.fromMendeley(references_), references); + } }; module.exports.init = async function () {