@iebh/reflib 2.5.8 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -35
- package/lib/formats.js +8 -0
- package/lib/writeFile.js +3 -2
- package/modules/bibtex.js +365 -0
- package/modules/default.js +1 -0
- package/modules/interface.js +1 -0
- package/modules/ris.js +17 -0
- package/package.json +15 -14
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ Compatibility
|
|
|
12
12
|
|
|
13
13
|
| Library | Extension(s) | Read | Write |
|
|
14
14
|
|------------------------|-----------------|--------------------|--------------------|
|
|
15
|
+
| BibTeX | `.bib` | :heavy_check_mark: | :heavy_check_mark: |
|
|
15
16
|
| Comma Separated Values | `.csv` | :x: | :x: |
|
|
16
17
|
| EndNote ENL | `.enl` | :heavy_check_mark: | (untested) |
|
|
17
18
|
| EndNote ENLX | `.enlx` | :heavy_check_mark: | :x: |
|
|
@@ -24,6 +25,8 @@ Compatibility
|
|
|
24
25
|
|
|
25
26
|
**Notes on different formats**:
|
|
26
27
|
|
|
28
|
+
* Not all formats are one-to-one translations, some have differing field definitions or type definitions
|
|
29
|
+
* BibTeX support is only provided for "well-formatted" files - i.e. correct use of newlines rather than one-single-line-per-ref, this is to avoid having to implement a full AST parser
|
|
27
30
|
* Medline seems to implement a totally different [publication type system](https://www.nlm.nih.gov/mesh/pubtypes.html) than others. Reflib will attempt to guess the best match, storing the original type in the `medlineType` key. Should the citation library be exported _back_ to Medline / `.nbib` files this key will take precedence to avoid data loss
|
|
28
31
|
|
|
29
32
|
|
|
@@ -36,40 +39,40 @@ Each reference has the following standardized fields, these are translated from
|
|
|
36
39
|
|
|
37
40
|
| Field | Type | Description |
|
|
38
41
|
|------------------|-----------------|----------------------------------------------------------------------------------------|
|
|
39
|
-
| recNumber | `
|
|
40
|
-
| type | `
|
|
41
|
-
| title | `
|
|
42
|
-
| journal | `
|
|
43
|
-
| authors | `
|
|
44
|
-
| date | `
|
|
45
|
-
| urls | `
|
|
46
|
-
| pages | `
|
|
47
|
-
| volume | `
|
|
48
|
-
| number | `
|
|
49
|
-
| isbn | `
|
|
50
|
-
| abstract | `
|
|
51
|
-
| label | `
|
|
52
|
-
| caption | `
|
|
53
|
-
| notes | `
|
|
54
|
-
| address | `
|
|
55
|
-
| researchNotes | `
|
|
56
|
-
| keywords | `
|
|
57
|
-
| accessDate | `
|
|
58
|
-
| accession | `
|
|
59
|
-
| doi | `
|
|
60
|
-
| section | `
|
|
61
|
-
| language | `
|
|
62
|
-
| researchNotes | `
|
|
63
|
-
| databaseProvider | `
|
|
64
|
-
| database | `
|
|
65
|
-
| workType | `
|
|
66
|
-
| custom1 | `
|
|
67
|
-
| custom2 | `
|
|
68
|
-
| custom3 | `
|
|
69
|
-
| custom4 | `
|
|
70
|
-
| custom5 | `
|
|
71
|
-
| custom6 | `
|
|
72
|
-
| custom7 | `
|
|
42
|
+
| recNumber | `Number` | The sorting number of the reference. Not present in RIS files |
|
|
43
|
+
| type | `String` | A supported [reference type](#reference-types) (e.g. journalArticle) |
|
|
44
|
+
| title | `String` | The reference's main title |
|
|
45
|
+
| journal | `String` | The reference's secondary title, this is usually the journal for most published papers |
|
|
46
|
+
| authors | `Array<String>` | An array of each Author in the originally specified format |
|
|
47
|
+
| date | `String` | The raw, internal date of the reference |
|
|
48
|
+
| urls | `Array<String>` | An array of each URL for the reference |
|
|
49
|
+
| pages | `String` | The page reference, usually in the format `123-4` |
|
|
50
|
+
| volume | `String` | |
|
|
51
|
+
| number | `String` | |
|
|
52
|
+
| isbn | `String` | |
|
|
53
|
+
| abstract | `String` | |
|
|
54
|
+
| label | `String` | |
|
|
55
|
+
| caption | `String` | |
|
|
56
|
+
| notes | `String` | |
|
|
57
|
+
| address | `String` | |
|
|
58
|
+
| researchNotes | `String` | |
|
|
59
|
+
| keywords | `Array<String>` | Optional list of keywords that apply to the reference |
|
|
60
|
+
| accessDate | `String` | |
|
|
61
|
+
| accession | `String` | [Accession numbers spec](https://support.nlm.nih.gov/knowledgebase/article/KA-03434/en-us), can sometimes be the PubMed ID |
|
|
62
|
+
| doi | `String` | |
|
|
63
|
+
| section | `String` | |
|
|
64
|
+
| language | `String` | |
|
|
65
|
+
| researchNotes | `String` | |
|
|
66
|
+
| databaseProvider | `String` | |
|
|
67
|
+
| database | `String` | |
|
|
68
|
+
| workType | `String` | |
|
|
69
|
+
| custom1 | `String` | |
|
|
70
|
+
| custom2 | `String` | |
|
|
71
|
+
| custom3 | `String` | |
|
|
72
|
+
| custom4 | `String` | |
|
|
73
|
+
| custom5 | `String` | |
|
|
74
|
+
| custom6 | `String` | |
|
|
75
|
+
| custom7 | `String` | |
|
|
73
76
|
|
|
74
77
|
|
|
75
78
|
Reference Types
|
|
@@ -251,7 +254,7 @@ writeStream(moduleId, outputStream, options)
|
|
|
251
254
|
Available: Node + Browser
|
|
252
255
|
Low level worker of `writeFile()`.
|
|
253
256
|
Return an object with methods to call to write to a given stream.
|
|
254
|
-
The returned object will have a `start()`, `end()` and `write(ref)` function which can be called to write to the original input stream.
|
|
257
|
+
The returned object will have a `start()`, `end()` and `write(ref)` (optional `middle(ref)`) function which can be called to write to the original input stream.
|
|
255
258
|
|
|
256
259
|
```javascript
|
|
257
260
|
// Convert a JSON file to EndNoteXML via a stream
|
package/lib/formats.js
CHANGED
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
* @property {boolean} canWrite Whether the format is supported when writing a citation library
|
|
11
11
|
*/
|
|
12
12
|
export let formats = {
|
|
13
|
+
bibtex: {
|
|
14
|
+
id: 'bibtex',
|
|
15
|
+
title: 'BibTeX',
|
|
16
|
+
titleShort: 'BibTeX',
|
|
17
|
+
ext: ['.bib'],
|
|
18
|
+
canRead: true,
|
|
19
|
+
canWrite: false,
|
|
20
|
+
},
|
|
13
21
|
csv: {
|
|
14
22
|
id: 'csv',
|
|
15
23
|
title: 'Comma Seperated Values',
|
package/lib/writeFile.js
CHANGED
|
@@ -22,8 +22,9 @@ export function writeFile(path, refs, options) {
|
|
|
22
22
|
|
|
23
23
|
return Promise.resolve()
|
|
24
24
|
.then(()=> writer.start())
|
|
25
|
-
.then(()=> refs.reduce((chain, ref) => // Write all refs as a series of promises
|
|
26
|
-
|
|
25
|
+
.then(()=> refs.reduce((chain, ref, refIndex, refs) => chain // Write all refs as a series of promises
|
|
26
|
+
.then(()=> writer.write(ref))
|
|
27
|
+
.then(()=> refIndex < refs.length && writer.middle && writer.middle(ref))
|
|
27
28
|
, Promise.resolve()))
|
|
28
29
|
.then(()=> writer.end())
|
|
29
30
|
.then(()=> fileStream.close())
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import Emitter from '../shared/emitter.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lookup enum for the current parser mode we are in
|
|
5
|
+
*
|
|
6
|
+
* @type {Object<Number>}
|
|
7
|
+
*/
|
|
8
|
+
const MODES = {
|
|
9
|
+
REF: 0,
|
|
10
|
+
FIELDS: 1,
|
|
11
|
+
FIELD_START: 2,
|
|
12
|
+
FIELD_VALUE: 3,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse a BibTeX file from a readable stream
|
|
18
|
+
*
|
|
19
|
+
* @see modules/interface.js
|
|
20
|
+
*
|
|
21
|
+
* @param {Stream} stream The readable stream to accept data from
|
|
22
|
+
* @param {Object} [options] Additional options to use when parsing
|
|
23
|
+
* @param {Boolean} [options.recNumberNumeric=true] Only process the BibTeX ID into a recNumber if its a finite numeric, otherwise disguard
|
|
24
|
+
* @param {Boolean} [options.recNumberRNPrefix=true] Accept `RN${NUMBER}` as recNumber if present
|
|
25
|
+
* @param {Boolean} [options.omitUnkown=false] If true, only keep known reconised fields
|
|
26
|
+
* @param {String} [options.fallbackType='unkown'] Reflib fallback type if the incoming type is unrecognised or unsupported
|
|
27
|
+
* @param {Set<String>} [options.fieldsOverwrite] Set of field names where the value is clobbered rather than appended if discovered more than once
|
|
28
|
+
*
|
|
29
|
+
* @returns {Object} A readable stream analogue defined in `modules/interface.js`
|
|
30
|
+
*/
|
|
31
|
+
export function readStream(stream, options) {
|
|
32
|
+
let settings = {
|
|
33
|
+
recNumberNumeric: true,
|
|
34
|
+
recNumberRNPrefix: true,
|
|
35
|
+
omitUnknown: false,
|
|
36
|
+
fallbackType: 'unknown',
|
|
37
|
+
fieldsOverwrite: new Set(['type']),
|
|
38
|
+
...options,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let emitter = Emitter();
|
|
42
|
+
let buffer = '';
|
|
43
|
+
let mode = MODES.REF;
|
|
44
|
+
let state; // Misc state storage when we're digesting ref data
|
|
45
|
+
let ref = {}; // Reference item being constructed
|
|
46
|
+
|
|
47
|
+
// Queue up the parser in the next tick (so we can return the emitter first)
|
|
48
|
+
setTimeout(()=> {
|
|
49
|
+
stream
|
|
50
|
+
.on('error', e => emitter.emit('error', e))
|
|
51
|
+
.on('end', ()=> emitter.emit('end'))
|
|
52
|
+
.on('data', chunkBuffer => {
|
|
53
|
+
emitter.emit('progress', stream.bytesRead);
|
|
54
|
+
buffer += chunkBuffer.toString(); // Append incomming data to the partial-buffer we're holding in memory
|
|
55
|
+
|
|
56
|
+
while (true) {
|
|
57
|
+
let match; // Regex storage for match groups
|
|
58
|
+
if ((mode == MODES.REF) && (match = /^\s*@(?<type>\w+?)\s*\{(?<id>.*?),/s.exec(buffer))) {
|
|
59
|
+
if (settings.recNumberNumeric && isFinite(match.groups.id)) { // Accept numeric recNumber
|
|
60
|
+
ref.recNumber = +match.groups.id;
|
|
61
|
+
} else if (settings.recNumberRNPrefix && /^RN\d+$/.test(match.groups.id)) {
|
|
62
|
+
ref.recNumber = +match.groups.id.slice(2);
|
|
63
|
+
} else if (!settings.recNumberNumeric && match.groups.id) { // Non numeric / finite ID - but we're allowed to accept it anyway
|
|
64
|
+
ref.recNumber = +match.groups.id;
|
|
65
|
+
} // Implied else - No ID, ignore
|
|
66
|
+
|
|
67
|
+
ref.type = match.groups.type;
|
|
68
|
+
mode = MODES.FIELDS;
|
|
69
|
+
state = null;
|
|
70
|
+
} else if (mode == MODES.FIELDS && (match = /^\s*(?<field>\w+?)\s*=\s*/s.exec(buffer))) {
|
|
71
|
+
mode = MODES.FIELD_START;
|
|
72
|
+
state = {field: match.groups.field};
|
|
73
|
+
} else if (mode == MODES.FIELDS && (match = /^\s*\}\s*/s.exec(buffer))) { // End of ref
|
|
74
|
+
emitter.emit('ref', tidyRef(ref, settings));
|
|
75
|
+
mode = MODES.REF;
|
|
76
|
+
ref = {};
|
|
77
|
+
state = null;
|
|
78
|
+
} else if (mode == MODES.FIELD_START && (match = /^\s*(?<fieldWrapper>"|{)\s*/.exec(buffer))) {
|
|
79
|
+
mode = MODES.FIELD_VALUE;
|
|
80
|
+
state.fieldWrapper = match.groups.fieldWrapper;
|
|
81
|
+
} else if (
|
|
82
|
+
// TODO: Note that we use `\r?\n` as delimiters for field values, this is a cheat to avoid having to implement a full AST parser
|
|
83
|
+
// This is a hack but since most BibTeX files use properly formatted BibTeX this should work in the majority of cases
|
|
84
|
+
// This WILL break if given one continuous line of BibTeX though
|
|
85
|
+
// - MC 2026-01-02
|
|
86
|
+
mode == MODES.FIELD_VALUE
|
|
87
|
+
&& (
|
|
88
|
+
(
|
|
89
|
+
state.fieldWrapper == '{'
|
|
90
|
+
&& (match = /^(?<value>.+?)(?<!\\%)\}\s*,?\s*$/sm.exec(buffer))
|
|
91
|
+
)
|
|
92
|
+
|| (
|
|
93
|
+
state.fieldWrapper == '"'
|
|
94
|
+
&& (match = /^(?<value>.+?)"\s*,?\s*$/sm.exec(buffer))
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
) {
|
|
98
|
+
mode = MODES.FIELDS;
|
|
99
|
+
if (ref[state.field] !== undefined && settings.fieldsOverwrite.has(state.field)) { // Already have content - and we should overwrite
|
|
100
|
+
ref[state.field] = unescape(match.groups.value);
|
|
101
|
+
} else if (ref[state.field] !== undefined) { // Already have content - append
|
|
102
|
+
ref[state.field] += '\n' + unescape(match.groups.value);
|
|
103
|
+
} else { // Populate initial value
|
|
104
|
+
ref[state.field] = unescape(match.groups.value);
|
|
105
|
+
}
|
|
106
|
+
state = null;
|
|
107
|
+
} else { // Implied else - No match to buffer, let it fill and process next data block
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Crop start of buffer to last match
|
|
112
|
+
buffer = buffer.slice(match[0].length);
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return emitter;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Tidy up a raw BibTeX reference before emitting
|
|
123
|
+
*
|
|
124
|
+
* @param {Object} ref The input raw ref to tidy
|
|
125
|
+
*
|
|
126
|
+
* @param {Object} settings Optimized settings object for fast access
|
|
127
|
+
*
|
|
128
|
+
* @returns {Object} The tidied ref
|
|
129
|
+
*/
|
|
130
|
+
export function tidyRef(ref, settings) {
|
|
131
|
+
return Object.fromEntries(
|
|
132
|
+
Object.entries(ref)
|
|
133
|
+
.map(([key, val]) => {
|
|
134
|
+
let rlField = translations.fields.btMap.get(key.toLowerCase());
|
|
135
|
+
|
|
136
|
+
if (key == 'type') { // Special conversion for type
|
|
137
|
+
let rlType = ref.type && translations.types.btMap.get(val.toLowerCase());
|
|
138
|
+
return rlType
|
|
139
|
+
? [key, rlType.rl] // Can translate incoming type to Reflib type
|
|
140
|
+
: [key, settings.fallbackType] // Unknown Reflib type varient
|
|
141
|
+
} else if (settings.omitUnkown && !rlField) { // Omit unknown fields
|
|
142
|
+
return;
|
|
143
|
+
} else if (rlField && rlField.array) { // Field needs array casting
|
|
144
|
+
return [rlField.rl, val.split(/\n*\s+and\s+/)];
|
|
145
|
+
} else if (rlField && rlField.rl) { // Known BT field but different RL field
|
|
146
|
+
return [rlField.rl, val];
|
|
147
|
+
} else if (!settings.omitUnkown) { // Everything else - add field
|
|
148
|
+
return [key, val];
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
.filter(Boolean) // Remove duds
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Translate a BibTeX encoded string into a regular JS String
|
|
158
|
+
*
|
|
159
|
+
* @param {String} str Input BibTeX encoded string
|
|
160
|
+
* @returns {String} Regular JS output string
|
|
161
|
+
*/
|
|
162
|
+
export function unescape(str) {
|
|
163
|
+
return str
|
|
164
|
+
.replace(/\/\*/g, '\n')
|
|
165
|
+
.replace(/\{\\\&\}/g, '&')
|
|
166
|
+
.replace(/\{\\\%\}/g, '%')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Translate a JS string into a BibTeX encoded string
|
|
172
|
+
*
|
|
173
|
+
* @param {String} str Input regular JS String
|
|
174
|
+
* @returns {String} BibTeX encoded string
|
|
175
|
+
*/
|
|
176
|
+
export function escape(str) {
|
|
177
|
+
return (''+str)
|
|
178
|
+
.replace(/\&/g, '{\\&}')
|
|
179
|
+
.replace(/%/g, '{\\%}')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Write a RIS file to a writable stream
|
|
185
|
+
*
|
|
186
|
+
* @see modules/interface.js
|
|
187
|
+
*
|
|
188
|
+
* @param {Stream} stream The writable stream to write to
|
|
189
|
+
*
|
|
190
|
+
* @param {Object} [options] Additional options to use when parsing
|
|
191
|
+
* @param {string} [options.defaultType='Misc'] Default citation type to assume when no other type is specified
|
|
192
|
+
* @param {string} [options.delimeter='\r'] How to split multi-line items
|
|
193
|
+
* @param {Boolean} [options.omitUnkown=false] If true, only keep known reconised fields
|
|
194
|
+
* @param {Set} [options.omitFields] Set of special fields to always omit, either because we are ignoring or because we have special treatment for them
|
|
195
|
+
* @param {Boolean} [options.recNumberRNPrefix=true] Rewrite recNumber fields as `RN${NUMBER}`
|
|
196
|
+
*
|
|
197
|
+
* @returns {Object} A writable stream analogue defined in `modules/interface.js`
|
|
198
|
+
*/
|
|
199
|
+
export function writeStream(stream, options) {
|
|
200
|
+
let settings = {
|
|
201
|
+
defaultType: 'Misc',
|
|
202
|
+
delimeter: '\n',
|
|
203
|
+
omitUnkown: false,
|
|
204
|
+
omitFields: new Set(['recNumber', 'type']),
|
|
205
|
+
recNumberRNPrefix: true,
|
|
206
|
+
...options,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
start() {
|
|
211
|
+
return Promise.resolve();
|
|
212
|
+
},
|
|
213
|
+
write: ref => {
|
|
214
|
+
// Fetch Reflib type definition
|
|
215
|
+
let rlType = ref.type && translations.types.rlMap.get(ref.type.toLowerCase());
|
|
216
|
+
let btType = rlType?.bt || settings.defaultType;
|
|
217
|
+
|
|
218
|
+
stream.write(
|
|
219
|
+
'@' + btType + '{'
|
|
220
|
+
+ (
|
|
221
|
+
ref.recNumber && settings.recNumberRNPrefix ? `RN${ref.recNumber},`
|
|
222
|
+
: ref.recNumber ? `${ref.recNumber},`
|
|
223
|
+
: ''
|
|
224
|
+
) + '\n'
|
|
225
|
+
+ Object.entries(ref)
|
|
226
|
+
.filter(([key, val]) =>
|
|
227
|
+
val // We have a non-nullish val
|
|
228
|
+
&& !settings.omitFields.has(key)
|
|
229
|
+
)
|
|
230
|
+
.reduce((buf, [rawKey, rawVal], keyIndex, keys) => {
|
|
231
|
+
// Fetch Reflib field definition
|
|
232
|
+
let rlField = translations.fields.rlMap.get(rawKey)
|
|
233
|
+
if (!rlField && settings.omitUnkown) return buf; // Unknown field mapping - skip if were omitting unknown fields
|
|
234
|
+
|
|
235
|
+
let key = rlField ? rlField.bt : rawKey; // Use Reflib->BibTeX field mapping if we have one, otherwise use raw key
|
|
236
|
+
let val = escape( // Escape input value, either as an Array via join or as a flat string
|
|
237
|
+
rawKey == 'authors' && Array.isArray(rawVal) ? rawVal.join('\nand ') // Special joining conditions for author field
|
|
238
|
+
: Array.isArray(rawVal) ? rawVal.join(', ') // Treat other arrays as a CSV
|
|
239
|
+
: rawVal // Splat everything else as a string
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return buf + // Return string buffer of ref under construction
|
|
243
|
+
`${key}={${val}}` // Append ref key=val pair to buffer
|
|
244
|
+
+ (keyIndex < keys.length-1 ? ',' : '') // Append comma (if non-last)
|
|
245
|
+
+ '\n' // Finish each field with a newline
|
|
246
|
+
}, '')
|
|
247
|
+
+ '}\n'
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return Promise.resolve();
|
|
251
|
+
},
|
|
252
|
+
middle() {
|
|
253
|
+
stream.write('\n');
|
|
254
|
+
},
|
|
255
|
+
end() {
|
|
256
|
+
return new Promise((resolve, reject) =>
|
|
257
|
+
stream.end(err => err ? reject(err) : resolve())
|
|
258
|
+
);
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Lookup tables for this module
|
|
266
|
+
* @type {Object}
|
|
267
|
+
* @property {Array<Object>} fields Field translations between Reflib (`rl`) and BibTeX format (`bt`)
|
|
268
|
+
*/
|
|
269
|
+
export let translations = {
|
|
270
|
+
// Field translations {{{
|
|
271
|
+
fields: {
|
|
272
|
+
collection: [
|
|
273
|
+
// Order by priority (highest at top)
|
|
274
|
+
{rl: 'address', bt: 'address'},
|
|
275
|
+
{rl: 'authors', bt: 'author', array: true},
|
|
276
|
+
{rl: 'doi', bt: 'doi'},
|
|
277
|
+
{rl: 'edition', bt: 'edition'},
|
|
278
|
+
{rl: 'editor', bt: 'editor'},
|
|
279
|
+
{rl: 'journal', bt: 'journal'},
|
|
280
|
+
{rl: 'notes', bt: 'note'},
|
|
281
|
+
{rl: 'number', bt: 'number'},
|
|
282
|
+
{rl: 'pages', bt: 'pages'},
|
|
283
|
+
{rl: 'title', bt: 'booktitle'},
|
|
284
|
+
{rl: 'title', bt: 'title'},
|
|
285
|
+
{rl: 'volume', bt: 'volume'},
|
|
286
|
+
{rl: 'isbn', bt: 'issn'},
|
|
287
|
+
|
|
288
|
+
// Misc
|
|
289
|
+
{bt: 'month'}, // Combined into {rl:'date'}
|
|
290
|
+
{bt: 'type'}, // Ignored
|
|
291
|
+
{bt: 'year'}, // Combined into {rl:'date'}
|
|
292
|
+
|
|
293
|
+
// Nonestandard but used anyway
|
|
294
|
+
{rl: 'abstract', bt: 'abstract'},
|
|
295
|
+
{rl: 'language', bt: 'language'},
|
|
296
|
+
{rl: 'keywords', bt: 'keywords', array: true},
|
|
297
|
+
{rl: 'urls', bt: 'url', array: true},
|
|
298
|
+
|
|
299
|
+
// Unknown how to translate these
|
|
300
|
+
// {bt: 'annote'},
|
|
301
|
+
// {bt: 'email'},
|
|
302
|
+
// {bt: 'chapter'},
|
|
303
|
+
// {bt: 'crossref'},
|
|
304
|
+
// {bt: 'howpublished'},
|
|
305
|
+
// {bt: 'institution'},
|
|
306
|
+
// {bt: 'key'},
|
|
307
|
+
// {bt: 'organization'},
|
|
308
|
+
// {bt: 'publisher'},
|
|
309
|
+
// {bt: 'school'},
|
|
310
|
+
// {bt: 'series'},
|
|
311
|
+
],
|
|
312
|
+
rlMap: new Map(),
|
|
313
|
+
btMap: new Map(),
|
|
314
|
+
},
|
|
315
|
+
// }}}
|
|
316
|
+
// Ref type translations {{{
|
|
317
|
+
types: {
|
|
318
|
+
collection: [
|
|
319
|
+
// Order by priority (highest at top)
|
|
320
|
+
{rl: 'journalArticle', bt: 'Article'},
|
|
321
|
+
{rl: 'book', bt: 'Book'},
|
|
322
|
+
{rl: 'bookSection', bt: 'InBook'},
|
|
323
|
+
{rl: 'conferencePaper', bt: 'Conference'},
|
|
324
|
+
{rl: 'conferenceProceedings', bt: 'InProceedings'},
|
|
325
|
+
{rl: 'report', bt: 'TechReport'},
|
|
326
|
+
{rl: 'thesis', bt: 'PHDThesis'},
|
|
327
|
+
{rl: 'unknown', bt: 'Misc'},
|
|
328
|
+
{rl: 'unpublished', bt: 'Unpublished'},
|
|
329
|
+
|
|
330
|
+
// Type aliases
|
|
331
|
+
{rl: 'journalArticle', bt: 'Journal Article'},
|
|
332
|
+
|
|
333
|
+
// Unknown how to translate these
|
|
334
|
+
{rl: 'Misc', bt: 'Booklet'},
|
|
335
|
+
{rl: 'Misc', bt: 'InCollection'},
|
|
336
|
+
{rl: 'Misc', bt: 'Manual'},
|
|
337
|
+
{rl: 'Misc', bt: 'MastersThesis'},
|
|
338
|
+
{rl: 'Misc', bt: 'Proceedings'},
|
|
339
|
+
],
|
|
340
|
+
rlMap: new Map(),
|
|
341
|
+
btMap: new Map(),
|
|
342
|
+
},
|
|
343
|
+
// }}}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* @see modules/interface.js
|
|
349
|
+
*/
|
|
350
|
+
export function setup() {
|
|
351
|
+
// Create lookup object of translations.fields with key as .rl / val as the full object
|
|
352
|
+
translations.fields.collection.forEach(c => {
|
|
353
|
+
if (c.rl) translations.fields.rlMap.set(c.rl.toLowerCase(), c);
|
|
354
|
+
if (c.bt) translations.fields.btMap.set(c.bt, c);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Create lookup object of ref.types with key as .rl / val as the full object
|
|
358
|
+
translations.types.collection.forEach(c => {
|
|
359
|
+
// Append each type to the set, accepting the first in each case as the priority
|
|
360
|
+
let rlLc = c.rl.toLowerCase();
|
|
361
|
+
let btLc = c.bt.toLowerCase();
|
|
362
|
+
if (c.rl && !translations.types.rlMap.has(rlLc)) translations.types.rlMap.set(rlLc, c);
|
|
363
|
+
if (c.bt && !translations.types.btMap.has(btLc)) translations.types.btMap.set(btLc, c);
|
|
364
|
+
});
|
|
365
|
+
}
|
package/modules/default.js
CHANGED
package/modules/interface.js
CHANGED
|
@@ -30,6 +30,7 @@ export function readStream(stream, options) {
|
|
|
30
30
|
* @returns {Object} An object which exposes methods to call to start, write and end the writing process. All methods MUST return a Promise
|
|
31
31
|
* @property {function<Promise>} start Function to call when beginning to write
|
|
32
32
|
* @property {function<Promise>} write Function called as `(ref)` when writing a single ref
|
|
33
|
+
* @property {function<promise>} [middle] Function to call after `write()` for each reference if the reference is NOT last, this works similar to `Array.prototype.join()`
|
|
33
34
|
* @property {function<Promise>} end Function to call when finishing writing, must resolve its Promise when the stream has closed successfully
|
|
34
35
|
*/
|
|
35
36
|
export function writeStream(stream, refs, options) {
|
package/modules/ris.js
CHANGED
|
@@ -9,6 +9,8 @@ import Emitter from '../shared/emitter.js';
|
|
|
9
9
|
* @param {Object} [options] Additional options to use when parsing
|
|
10
10
|
* @param {string} [options.defaultType='report'] Default citation type to assume when no other type is specified
|
|
11
11
|
* @param {string} [options.delimeter='\r'] How to split multi-line items
|
|
12
|
+
* @param {Boolean} [options.convertAbstract=true] If the `fallbackAbstract` field exists but not `abstract` use the former as the latter
|
|
13
|
+
* @param {Boolean} [options.convertCity=true] If the `fallbackCity` field exists but not `city` use the former as the latter
|
|
12
14
|
*
|
|
13
15
|
* @returns {Object} A readable stream analogue defined in `modules/interface.js`
|
|
14
16
|
*/
|
|
@@ -16,6 +18,8 @@ export function readStream(stream, options) {
|
|
|
16
18
|
let settings = {
|
|
17
19
|
defaultType: 'journalArticle',
|
|
18
20
|
delimeter: '\r',
|
|
21
|
+
convertAbtract: true,
|
|
22
|
+
convertCity: true,
|
|
19
23
|
...options,
|
|
20
24
|
};
|
|
21
25
|
|
|
@@ -132,6 +136,7 @@ export function writeStream(stream, options) {
|
|
|
132
136
|
*
|
|
133
137
|
* @param {string} refString Raw RIS string composing the start -> end of the ref
|
|
134
138
|
* @param {Object} settings Additional settings to pass, this should be initialized + parsed by the calling function for efficiency, see readStream() for full spec
|
|
139
|
+
* @param {Boolean} [settings.convertAbstract=true] If the `fallbackAbstract` field exists but not `abstract` use the former as the latter
|
|
135
140
|
* @returns {ReflibRef} The parsed reference
|
|
136
141
|
*/
|
|
137
142
|
export function parseRef(refString, settings) {
|
|
@@ -185,6 +190,18 @@ export function parseRef(refString, settings) {
|
|
|
185
190
|
delete ref._pageEnd;
|
|
186
191
|
}
|
|
187
192
|
// }}}
|
|
193
|
+
// FallbackAbstract -> Abstract (if the latter is missing) {{{
|
|
194
|
+
if ((settings.fallbackAbstract ?? true) && ref.fallbackAbstract && !ref.abstract) {
|
|
195
|
+
ref.abstract = ref.fallbackAbstract;
|
|
196
|
+
delete ref.fallbackAbstract;
|
|
197
|
+
}
|
|
198
|
+
// }}}
|
|
199
|
+
// FallbackCity -> City (if the latter is missing) {{{
|
|
200
|
+
if ((settings.fallbackCity ?? true) && ref.fallbackCity && !ref.abstract) {
|
|
201
|
+
ref.city = ref.fallbackCity;
|
|
202
|
+
delete ref.fallbackCity;
|
|
203
|
+
}
|
|
204
|
+
// }}}
|
|
188
205
|
|
|
189
206
|
return ref;
|
|
190
207
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iebh/reflib",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Reference / Citation reference library utilities",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"lint": "eslint
|
|
7
|
-
"test": "
|
|
6
|
+
"lint": "eslint",
|
|
7
|
+
"test": "testa",
|
|
8
8
|
"test:browser": "cd test/browser && npm run dev",
|
|
9
9
|
"test:watch": "nodemon --exec npm run test"
|
|
10
10
|
},
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
],
|
|
21
21
|
"author": "Matt Carter <m@ttcarter.com> (https://github.com/hash-bang)",
|
|
22
22
|
"contributors": [
|
|
23
|
-
"Connor Forbes <cforbes.software@gmail.com> (https://github.com/connorf25)"
|
|
23
|
+
"Connor Forbes <cforbes.software@gmail.com> (https://github.com/connorf25)",
|
|
24
|
+
"Irene Priya Jose (https://github.com/s5333384)",
|
|
25
|
+
"Tian Liang (https://github.com/Octian)"
|
|
24
26
|
],
|
|
25
27
|
"license": "MIT",
|
|
26
28
|
"bugs": {
|
|
@@ -44,22 +46,21 @@
|
|
|
44
46
|
"htmlparser2/lib/esm/WritableStream": "./modules/shims/WritableStream-browser.js",
|
|
45
47
|
"JSONStream": "./modules/shims/JSONStream-browser.js"
|
|
46
48
|
},
|
|
47
|
-
"devDependencies": {
|
|
48
|
-
"@momsfriendlydevco/eslint-config": "^2.3.1",
|
|
49
|
-
"chai": "^5.2.0",
|
|
50
|
-
"eslint": "^9.31.0",
|
|
51
|
-
"mocha": "^11.1.0",
|
|
52
|
-
"mocha-logger": "^1.0.8",
|
|
53
|
-
"nodemon": "^3.1.9",
|
|
54
|
-
"temp": "^0.9.4",
|
|
55
|
-
"vite-plugin-replace": "^0.1.1"
|
|
56
|
-
},
|
|
57
49
|
"dependencies": {
|
|
58
50
|
"@iebh/cacx": "^1.0.3",
|
|
59
51
|
"@zip.js/zip.js": "^2.7.57",
|
|
60
52
|
"htmlparser2": "^9.1.0",
|
|
61
53
|
"JSONStream": "^1.3.5",
|
|
54
|
+
"lodash-es": "^4.17.22",
|
|
62
55
|
"mitt": "^3.0.1",
|
|
63
56
|
"sql.js": "^1.12.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@momsfriendlydevco/eslint-config": "^2.3.1",
|
|
60
|
+
"@momsfriendlydevco/testa": "^1.1.2",
|
|
61
|
+
"eslint": "^9.31.0",
|
|
62
|
+
"nodemon": "^3.1.9",
|
|
63
|
+
"temp": "^0.9.4",
|
|
64
|
+
"vite-plugin-replace": "^0.1.1"
|
|
64
65
|
}
|
|
65
66
|
}
|