Skip to content

Commit

Permalink
feat: generate RIS records from Mendeley references
Browse files Browse the repository at this point in the history
  • Loading branch information
customcommander committed May 10, 2021
1 parent d12d626 commit 638d0cf
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 1 deletion.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
* @license MIT
*/
module.exports = require('./parser.js');
module.exports.toMendeley = require('./mendeley').to;
module.exports.toMendeley = require('./mendeley').to;
module.exports.fromMendeley = require('./mendeley').from;
159 changes: 159 additions & 0 deletions src/mendeley.js
Original file line number Diff line number Diff line change
Expand Up @@ -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), '');
69 changes: 69 additions & 0 deletions test/features/mendeley.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand All @@ -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 -
"""
9 changes: 9 additions & 0 deletions test/steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down Expand Up @@ -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);
});
5 changes: 5 additions & 0 deletions test/world.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down

0 comments on commit 638d0cf

Please sign in to comment.