@iebh/reflib 2.0.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/.eslintrc.cjs +14 -0
- package/.ignore +1 -0
- package/LICENSE +20 -0
- package/README.md +268 -0
- package/lib/browser.js +8 -0
- package/lib/default.js +7 -0
- package/lib/fields.js +161 -0
- package/lib/formats.js +61 -0
- package/lib/getModule.js +37 -0
- package/lib/identifyFormat.js +13 -0
- package/lib/readFile.js +34 -0
- package/lib/readStream.js +21 -0
- package/lib/uploadFile.js +69 -0
- package/lib/writeFile.js +23 -0
- package/lib/writeStream.js +16 -0
- package/modules/default.js +4 -0
- package/modules/endnoteXml.js +407 -0
- package/modules/interface.js +45 -0
- package/modules/json.js +59 -0
- package/modules/medline.js +625 -0
- package/modules/ris.js +345 -0
- package/package.json +51 -0
- package/shared/camelCase.js +16 -0
- package/shared/emitter.js +21 -0
- package/shared/streamEmitter.js +37 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {formats} from './formats.js';
|
|
2
|
+
import {identifyFormat} from './identifyFormat.js';
|
|
3
|
+
import {readStream} from './readStream.js';
|
|
4
|
+
import StreamEmitter from '../shared/streamEmitter.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Prompt the user for a file then read it as a Reflib event emitter
|
|
8
|
+
* @param {Object} [options] Additional options when prompting the user
|
|
9
|
+
* @param {File} [options.files] The File object to process, omitting this will prompt the user to select a file
|
|
10
|
+
* @param {function} [options.onStart] Async function called as `(File)` when starting the read stage
|
|
11
|
+
* @param {function} [options.onProgress] Function called as `(position, totalSize)` when processing the file
|
|
12
|
+
* @param {function} [options.onEnd] Async function called as `()` when the read stage has completed
|
|
13
|
+
* @param {*} [options.*] Additional settings to pass to `readStream()`
|
|
14
|
+
* @returns {Promise} A promise which will resolve with an array of extracted citations
|
|
15
|
+
*/
|
|
16
|
+
export function uploadFile(options) {
|
|
17
|
+
let settings = {...options};
|
|
18
|
+
|
|
19
|
+
if (!settings.file) { // No file provided - prompt the user via the DOM
|
|
20
|
+
return new Promise(resolve => {
|
|
21
|
+
// Create hidden layer we will use to wrap the actual file upload input box
|
|
22
|
+
let fileWrapper = document.createElement('div');
|
|
23
|
+
fileWrapper.style.display = 'none';
|
|
24
|
+
document.body.appendChild(fileWrapper);
|
|
25
|
+
|
|
26
|
+
// Create upload input
|
|
27
|
+
let uploader = document.createElement('input');
|
|
28
|
+
uploader.type = 'file';
|
|
29
|
+
uploader.accept = Object.values(formats) // Allow only uploading supported file extensions
|
|
30
|
+
.flatMap(f => f.ext)
|
|
31
|
+
.join(',');
|
|
32
|
+
|
|
33
|
+
uploader.addEventListener('change', e => {
|
|
34
|
+
document.body.removeChild(fileWrapper);
|
|
35
|
+
resolve(uploadFile({
|
|
36
|
+
file: e.target.files[0],
|
|
37
|
+
...options,
|
|
38
|
+
}));
|
|
39
|
+
});
|
|
40
|
+
fileWrapper.appendChild(uploader);
|
|
41
|
+
uploader.dispatchEvent(new MouseEvent('click'));
|
|
42
|
+
});
|
|
43
|
+
} else { // Read the File object and return an emitter
|
|
44
|
+
if (!(settings.file instanceof File)) throw new Error('Expected "file" setting to uploadFile() to be a File type');
|
|
45
|
+
let identifiedType = identifyFormat(settings.file.name);
|
|
46
|
+
if (!identifiedType) throw new Error(`Unidenfified file format from filename "${settings.file.name}"`);
|
|
47
|
+
|
|
48
|
+
let refs = [];
|
|
49
|
+
return Promise.resolve()
|
|
50
|
+
.then(()=> settings.onStart && settings.onStart(settings.file))
|
|
51
|
+
.then(()=> new Promise((resolve, reject) => {
|
|
52
|
+
let streamer = readStream(
|
|
53
|
+
identifiedType.id,
|
|
54
|
+
StreamEmitter(settings.file.stream().getReader()),
|
|
55
|
+
{
|
|
56
|
+
...settings,
|
|
57
|
+
size: settings.file.size,
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
.on('end', ()=> resolve(refs))
|
|
61
|
+
.on('error', reject)
|
|
62
|
+
.on('ref', ref => refs.push(ref))
|
|
63
|
+
|
|
64
|
+
if (settings.onProgress) streamer.on('progress', settings.onProgress);
|
|
65
|
+
}))
|
|
66
|
+
.then(()=> settings.onEnd && settings.onEnd())
|
|
67
|
+
.then(()=> refs)
|
|
68
|
+
}
|
|
69
|
+
}
|
package/lib/writeFile.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {createWriteStream} from 'node:fs';
|
|
2
|
+
import {identifyFormat} from './identifyFormat.js';
|
|
3
|
+
import {writeStream} from './writeStream.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Write a file to disk via the appropriate module
|
|
7
|
+
* @param {string} path The file path to write, the module to use will be determined from this
|
|
8
|
+
* @param {Object} [options] Additional options to pass to the file writer
|
|
9
|
+
* @param {string} [options.module] The module to use if overriding from the file path
|
|
10
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
11
|
+
*/
|
|
12
|
+
export function writeFile(path, refs, options) {
|
|
13
|
+
let format = options?.module || identifyFormat(path)?.id;
|
|
14
|
+
if (!format) throw new Error(`Unable to identify reference library format when saving file "${path}"`);
|
|
15
|
+
|
|
16
|
+
let writer = writeStream(format, createWriteStream(path), options);
|
|
17
|
+
return Promise.resolve()
|
|
18
|
+
.then(()=> writer.start())
|
|
19
|
+
.then(()=> refs.reduce((chain, ref) => // Write all refs as a series of promises
|
|
20
|
+
chain.then(()=> writer.write(ref))
|
|
21
|
+
, Promise.resolve()))
|
|
22
|
+
.then(()=> writer.end())
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {getModule} from './getModule.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Write an output stream via a given format ID
|
|
5
|
+
* This function is really just a multiplexor around each modules `writeStream` export
|
|
6
|
+
* @param {string} module The module ID as per `lib/formats.js`
|
|
7
|
+
* @param {Stream.Writable} stream Output stream to write to
|
|
8
|
+
* @param {Object} [options] Additional options to pass to the stream writer
|
|
9
|
+
* @returns {Object} An object composed of `{start, write, end}` functions
|
|
10
|
+
*/
|
|
11
|
+
export function writeStream(module, stream, options) {
|
|
12
|
+
if (!module) throw new Error('No module provided to parse with');
|
|
13
|
+
if (!stream) throw new Error('No stream provided to parse');
|
|
14
|
+
|
|
15
|
+
return getModule(module).writeStream(stream, options);
|
|
16
|
+
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import camelCase from '../shared/camelCase.js';
|
|
2
|
+
import Emitter from '../shared/emitter.js';
|
|
3
|
+
import {WritableStream as XMLParser} from 'htmlparser2/lib/WritableStream.js';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @see modules/interface.js
|
|
8
|
+
*/
|
|
9
|
+
export function readStream(stream) {
|
|
10
|
+
let emitter = Emitter();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The current reference being appended to
|
|
14
|
+
* @type {Object}
|
|
15
|
+
*/
|
|
16
|
+
let ref = {};
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Stack of nodes we are currently traversed into
|
|
21
|
+
* @type {array<Object>}
|
|
22
|
+
*/
|
|
23
|
+
let stack = [];
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether to append incomming text blocks to the previous block
|
|
28
|
+
* This is necessary as XMLParser splits text into multiple calls so we need to know whether to append or treat this item as a continuation of the previous one
|
|
29
|
+
* @type {boolean}
|
|
30
|
+
*/
|
|
31
|
+
let textAppend = false;
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
// Queue up the parser in the next tick (so we can return the emitter first)
|
|
35
|
+
setTimeout(()=> {
|
|
36
|
+
let parser = new XMLParser({
|
|
37
|
+
xmlMode: true,
|
|
38
|
+
decodeEntities: false, // Handled below
|
|
39
|
+
onopentag(name, attrs) {
|
|
40
|
+
textAppend = false;
|
|
41
|
+
stack.push({
|
|
42
|
+
name: camelCase(name),
|
|
43
|
+
attrs,
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
onclosetag(name) {
|
|
47
|
+
if (name == 'record') {
|
|
48
|
+
if (ref.title) ref.title = ref.title // htmlparser2 handles the '<title>' tag in a really bizare way so we have to pull apart the <style> bits when parsing
|
|
49
|
+
.replace(/^.*<style.*>(.*)<\/style>.*$/m, '$1')
|
|
50
|
+
.replace(/^\s+/, '')
|
|
51
|
+
.replace(/\s+$/, '')
|
|
52
|
+
emitter.emit('ref', translateRawToRef(ref));
|
|
53
|
+
stack = []; // Trash entire stack when hitting end of <record/> node
|
|
54
|
+
ref = {}; // Reset the ref state
|
|
55
|
+
} else {
|
|
56
|
+
stack.pop();
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
ontext(text) {
|
|
60
|
+
let parentName = stack[stack.length - 1]?.name;
|
|
61
|
+
let gParentName = stack[stack.length - 2]?.name;
|
|
62
|
+
if (parentName == 'title') {
|
|
63
|
+
if (textAppend) {
|
|
64
|
+
ref.title += text;
|
|
65
|
+
} else {
|
|
66
|
+
ref.title = text;
|
|
67
|
+
}
|
|
68
|
+
} else if (parentName == 'style' && gParentName == 'author') {
|
|
69
|
+
if (!ref.authors) ref.authors = [];
|
|
70
|
+
if (textAppend) {
|
|
71
|
+
ref.authors[ref.authors.length - 1] += xmlUnescape(text);
|
|
72
|
+
} else {
|
|
73
|
+
ref.authors.push(xmlUnescape(text));
|
|
74
|
+
}
|
|
75
|
+
} else if (parentName == 'style' && gParentName == 'keyword') {
|
|
76
|
+
if (!ref.keywords) ref.keywords = [];
|
|
77
|
+
if (textAppend) {
|
|
78
|
+
ref.keywords[ref.keywords.length - 1] += xmlUnescape(text);
|
|
79
|
+
} else {
|
|
80
|
+
ref.keywords.push(xmlUnescape(text));
|
|
81
|
+
}
|
|
82
|
+
} else if (parentName == 'style') { // Text within <style/> tag
|
|
83
|
+
if (textAppend || ref[gParentName]) { // Text already exists? Append (handles node-expats silly multi-text per escape character "feature")
|
|
84
|
+
ref[gParentName] += xmlUnescape(text);
|
|
85
|
+
} else {
|
|
86
|
+
ref[gParentName] = xmlUnescape(text);
|
|
87
|
+
}
|
|
88
|
+
} else if (['recNumber', 'refType'].includes(parentName)) { // Simple setters like <rec-number/>
|
|
89
|
+
if (textAppend || ref[parentName]) {
|
|
90
|
+
ref[parentName] += xmlUnescape(text);
|
|
91
|
+
} else {
|
|
92
|
+
ref[parentName] = xmlUnescape(text);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
textAppend = true; // Always set the next call to the text emitter handler as an append operation
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
stream.pipe(parser)
|
|
100
|
+
.on('finish', ()=> emitter.emit('end'))
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return emitter;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @see modules/interface.js
|
|
109
|
+
* @param {Object} [options] Additional options to use when parsing
|
|
110
|
+
* @param {string} [options.defaultType='journalArticle'] Default citation type to assume when no other type is specified
|
|
111
|
+
* @param {string} [options.filePath="c:\\"] "Fake" internal source file path the citation lirary was exported from, must end with backslashes
|
|
112
|
+
* @param {string} [options.fileName="EndNote.enl"] "Fake" internal source file name the citation lirary was exported from
|
|
113
|
+
* @param {function} [options.formatDate] Date formatter to translate between a JS Date object and the EndNote YYYY-MM-DD format
|
|
114
|
+
*/
|
|
115
|
+
export function writeStream(stream, options) {
|
|
116
|
+
let settings = {
|
|
117
|
+
defaultType: 'journalArticle',
|
|
118
|
+
filePath: 'c:\\',
|
|
119
|
+
fileName: 'EndNote.enl',
|
|
120
|
+
formatDate: value => value instanceof Date ? value.toISOString().substr(0, 10) : value,
|
|
121
|
+
...options,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Cached values so we don't need to keep recomputing
|
|
125
|
+
let encodedName = xmlEscape(settings.fileName);
|
|
126
|
+
let refsSeen = 0;
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
start: ()=> {
|
|
130
|
+
stream.write('<?xml version="1.0" encoding="UTF-8" ?><xml><records>');
|
|
131
|
+
return Promise.resolve();
|
|
132
|
+
},
|
|
133
|
+
write: ref => {
|
|
134
|
+
let refType = translations.types.rlMap.get(ref.type || settings.defaultType);
|
|
135
|
+
if (!refType) throw new Error(`Invalid reference type: "${ref.type}"`);
|
|
136
|
+
|
|
137
|
+
refsSeen++;
|
|
138
|
+
let recNumber = ref.recNumber || refsSeen;
|
|
139
|
+
|
|
140
|
+
stream.write(
|
|
141
|
+
'<record>'
|
|
142
|
+
// Preamble
|
|
143
|
+
+ `<database name="${settings.fileName}" path="${settings.filePath}${settings.fileName}">${encodedName}</database>`
|
|
144
|
+
+ `<source-app name="EndNote" version="16.0">EndNote</source-app>`
|
|
145
|
+
+ `<rec-number>${recNumber}</rec-number>`
|
|
146
|
+
+ `<foreign-keys><key app="EN" db-id="s55prpsswfsepue0xz25pxai2p909xtzszzv">${recNumber}</key></foreign-keys>`
|
|
147
|
+
|
|
148
|
+
// Type
|
|
149
|
+
+ `<ref-type name="${refType.rawText}">${refType.rawId}</ref-type>`
|
|
150
|
+
|
|
151
|
+
// Authors
|
|
152
|
+
+ '<contributors><authors>'
|
|
153
|
+
+ (ref.authors || []).map(author => `<author><style face="normal" font="default" size="100%">${xmlEscape(author)}</style></author>`)
|
|
154
|
+
+ '</authors></contributors>'
|
|
155
|
+
|
|
156
|
+
// Titles
|
|
157
|
+
+ '<titles>'
|
|
158
|
+
+ (ref.title ? `<title><style face="normal" font="default" size="100%">${xmlEscape(ref.title)}</style></title>` : '')
|
|
159
|
+
+ (ref.journal ? `<secondary-title><style face="normal" font="default" size="100%">${xmlEscape(ref.journal)}</style></secondary-title>` : '')
|
|
160
|
+
+ (ref.titleShort ? `<short-title><style face="normal" font="default" size="100%">${xmlEscape(ref.titleShort)}</style></short-title>` : '')
|
|
161
|
+
+ (ref.journalAlt ? `<alt-title><style face="normal" font="default" size="100%">${xmlEscape(ref.journalAlt)}</style></alt-title>` : '')
|
|
162
|
+
+ '</titles>'
|
|
163
|
+
|
|
164
|
+
// Periodical
|
|
165
|
+
+ (ref.periodical ? `<periodical><full-title><style face="normal" font="default" size="100%">${xmlEscape(ref.periodical)}</style></full-title></periodical>` : '')
|
|
166
|
+
|
|
167
|
+
// Simple field key/vals
|
|
168
|
+
+ [
|
|
169
|
+
['abstract', 'abstract'],
|
|
170
|
+
['accessDate', 'access-date'],
|
|
171
|
+
['accession', 'accession-num'],
|
|
172
|
+
['address', 'auth-address'],
|
|
173
|
+
['caption', 'caption'],
|
|
174
|
+
['databaseProvider', 'remote-database-provider'],
|
|
175
|
+
['database', 'remote-database-name'],
|
|
176
|
+
['doi', 'electronic-resource-num'],
|
|
177
|
+
['isbn', 'isbn'],
|
|
178
|
+
['label', 'label'],
|
|
179
|
+
['language', 'language'],
|
|
180
|
+
['notes', 'notes'],
|
|
181
|
+
['number', 'number'],
|
|
182
|
+
['pages', 'pages'],
|
|
183
|
+
['researchNotes', 'research-notes'],
|
|
184
|
+
['section', 'section'],
|
|
185
|
+
['volume', 'volume'],
|
|
186
|
+
['workType', 'work-type'],
|
|
187
|
+
['custom1', 'custom1'],
|
|
188
|
+
['custom2', 'custom2'],
|
|
189
|
+
['custom3', 'custom3'],
|
|
190
|
+
['custom4', 'custom4'],
|
|
191
|
+
['custom5', 'custom5'],
|
|
192
|
+
['custom6', 'custom6'],
|
|
193
|
+
['custom7', 'custom7'],
|
|
194
|
+
]
|
|
195
|
+
.filter(([rlKey]) => ref[rlKey]) // Remove empty fields
|
|
196
|
+
.map(([rlKey, rawKey]) =>
|
|
197
|
+
`<${rawKey}><style face="normal" font="default" size="100%">${xmlEscape(ref[rlKey])}</style></${rawKey}>`
|
|
198
|
+
)
|
|
199
|
+
.join('')
|
|
200
|
+
|
|
201
|
+
// Dates
|
|
202
|
+
+ (
|
|
203
|
+
ref.date && ref.year && ref.date instanceof Date ?
|
|
204
|
+
`<dates><year><style face="normal" font="default" size="100%">${xmlEscape(ref.year)}</style></year>`
|
|
205
|
+
+ `<pub-dates><date><style face="normal" font="default" size="100%">${settings.formatDate(ref.date)}</style></date></pub-dates></dates>`
|
|
206
|
+
: ref.date && ref.year ?
|
|
207
|
+
`<dates><year><style face="normal" font="default" size="100%">${xmlEscape(ref.year)}</style></year>`
|
|
208
|
+
+ `<pub-dates><date><style face="normal" font="default" size="100%">${ref.date}</style></date></pub-dates></dates>`
|
|
209
|
+
: ref.date ?
|
|
210
|
+
`<dates><pub-dates><date><style face="normal" font="default" size="100%">${xmlEscape(ref.date)}</style></date></pub-dates></dates>`
|
|
211
|
+
: ref.year ?
|
|
212
|
+
`<dates><year><style face="normal" font="default" size="100%">${xmlEscape(ref.year)}</style></year></dates>`
|
|
213
|
+
: ''
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
// Urls
|
|
217
|
+
+ (ref.urls ?
|
|
218
|
+
'<urls><related-urls>'
|
|
219
|
+
+ (ref.urls || [])
|
|
220
|
+
.map(url => `<url><style face="normal" font="default" size="100%">${xmlEscape(url)}</style></url>`)
|
|
221
|
+
.join('')
|
|
222
|
+
+ '</related-urls></urls>'
|
|
223
|
+
: '')
|
|
224
|
+
|
|
225
|
+
// Keywords
|
|
226
|
+
+ (ref.keywords ?
|
|
227
|
+
'<keywords>'
|
|
228
|
+
+ (ref.keywords || [])
|
|
229
|
+
.map(keyword => `<keyword><style face="normal" font="default" size="100%">${xmlEscape(keyword)}</style></keyword>`)
|
|
230
|
+
.join('')
|
|
231
|
+
+ '</keywords>'
|
|
232
|
+
: '')
|
|
233
|
+
|
|
234
|
+
+ '</record>'
|
|
235
|
+
);
|
|
236
|
+
return Promise.resolve();
|
|
237
|
+
},
|
|
238
|
+
end: ()=> {
|
|
239
|
+
stream.write('</records></xml>');
|
|
240
|
+
return new Promise((resolve, reject) =>
|
|
241
|
+
stream.end(err => err ? reject(err) : resolve())
|
|
242
|
+
);
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Utility function to take the raw XML output object and translate it into a RefLib object
|
|
250
|
+
* @param {Object} xRef Raw XML object to process
|
|
251
|
+
* @returns {Object} The translated RefLib object output
|
|
252
|
+
*/
|
|
253
|
+
export function translateRawToRef(xRef) {
|
|
254
|
+
let recOut = {
|
|
255
|
+
...Object.fromEntries(
|
|
256
|
+
translations.fields.collection
|
|
257
|
+
.filter(field => xRef[field.raw]) // Only include fields we have a value for
|
|
258
|
+
.map(field => [ field.rl, xRef[field.raw] ]) // Translate Raw -> Reflib spec
|
|
259
|
+
),
|
|
260
|
+
type: translations.types.rawMap.get(+xRef.refType || 17)?.rl,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return recOut;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Default string -> XML encoder
|
|
269
|
+
* @param {string} str The input string to encode
|
|
270
|
+
* @returns {string} The XML "safe" string
|
|
271
|
+
*/
|
|
272
|
+
export function xmlEscape(str) {
|
|
273
|
+
return ('' + str)
|
|
274
|
+
.replace(/&/g, '&')
|
|
275
|
+
.replace(/\r/g, '
')
|
|
276
|
+
.replace(/</g, '<')
|
|
277
|
+
.replace(/>/g, '>')
|
|
278
|
+
.replace(/"/g, '"')
|
|
279
|
+
.replace(/'/g, ''');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Default XML -> string decodeer
|
|
285
|
+
* @param {string} str The input string to decode
|
|
286
|
+
* @returns {string} The actual string
|
|
287
|
+
*/
|
|
288
|
+
export function xmlUnescape(str) {
|
|
289
|
+
return ('' + str)
|
|
290
|
+
.replace(/\&/g, '&')
|
|
291
|
+
.replace(/\
/g, '\r')
|
|
292
|
+
.replace(/\</g, '<')
|
|
293
|
+
.replace(/\>/g, '>')
|
|
294
|
+
.replace(/\"/g, '"')
|
|
295
|
+
.replace(/\'/g, "'");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Lookup tables for this module
|
|
301
|
+
* @type {Object}
|
|
302
|
+
* @property {array<Object>} fields Field translations between RefLib (`rl`) and the raw format (`raw`)
|
|
303
|
+
* @property {array<Object>} types Field translations between RefLib (`rl`) and the raw format types as raw text (`rawText`) and numeric ID (`rawId`)
|
|
304
|
+
*/
|
|
305
|
+
export let translations = {
|
|
306
|
+
// Field translations {{{
|
|
307
|
+
fields: {
|
|
308
|
+
collection: [
|
|
309
|
+
{rl: 'recNumber', raw: 'recNumber'},
|
|
310
|
+
{rl: 'title', raw: 'title'},
|
|
311
|
+
{rl: 'journal', raw: 'secondaryTitle'},
|
|
312
|
+
{rl: 'address', raw: 'authAddress'},
|
|
313
|
+
{rl: 'researchNotes', raw: 'researchNotes'},
|
|
314
|
+
{rl: 'type', raw: 'FIXME'},
|
|
315
|
+
{rl: 'authors', raw: 'authors'},
|
|
316
|
+
{rl: 'pages', raw: 'pages'},
|
|
317
|
+
{rl: 'volume', raw: 'volume'},
|
|
318
|
+
{rl: 'number', raw: 'number'},
|
|
319
|
+
{rl: 'isbn', raw: 'isbn'},
|
|
320
|
+
{rl: 'abstract', raw: 'abstract'},
|
|
321
|
+
{rl: 'label', raw: 'label'},
|
|
322
|
+
{rl: 'caption', raw: 'caption'},
|
|
323
|
+
{rl: 'notes', raw: 'notes'},
|
|
324
|
+
{rl: 'custom1', raw: 'custom1'},
|
|
325
|
+
{rl: 'custom2', raw: 'custom2'},
|
|
326
|
+
{rl: 'custom3', raw: 'custom3'},
|
|
327
|
+
{rl: 'custom4', raw: 'custom4'},
|
|
328
|
+
{rl: 'custom5', raw: 'custom5'},
|
|
329
|
+
{rl: 'custom6', raw: 'custom6'},
|
|
330
|
+
{rl: 'custom7', raw: 'custom7'},
|
|
331
|
+
{rl: 'doi', raw: 'electronicResourceNum'},
|
|
332
|
+
{rl: 'year', raw: 'year'},
|
|
333
|
+
{rl: 'date', raw: 'date'},
|
|
334
|
+
{rl: 'keywords', raw: 'keywords'},
|
|
335
|
+
{rl: 'urls', raw: 'urls'},
|
|
336
|
+
],
|
|
337
|
+
},
|
|
338
|
+
// }}}
|
|
339
|
+
// Ref type translations {{{
|
|
340
|
+
types: {
|
|
341
|
+
collection: [
|
|
342
|
+
{rl: 'aggregatedDatabase', rawText: 'Aggregated Database', rawId: 55},
|
|
343
|
+
{rl: 'ancientText', rawText: 'Ancient Text', rawId: 51},
|
|
344
|
+
{rl: 'artwork', rawText: 'Artwork', rawId: 2},
|
|
345
|
+
{rl: 'audioVisualMaterial', rawText: 'Audiovisual Material', rawId: 3},
|
|
346
|
+
{rl: 'bill', rawText: 'Bill', rawId: 4},
|
|
347
|
+
{rl: 'blog', rawText: 'Blog', rawId: 56},
|
|
348
|
+
{rl: 'book', rawText: 'Book', rawId: 6},
|
|
349
|
+
{rl: 'bookSection', rawText: 'Book Section', rawId: 5},
|
|
350
|
+
{rl: 'case', rawText: 'Case', rawId: 7},
|
|
351
|
+
{rl: 'catalog', rawText: 'Catalog', rawId: 8},
|
|
352
|
+
{rl: 'chartOrTable', rawText: 'Chart or Table', rawId: 38},
|
|
353
|
+
{rl: 'classicalWork', rawText: 'Classical Work', rawId: 49},
|
|
354
|
+
{rl: 'computerProgram', rawText: 'Computer Program', rawId: 9},
|
|
355
|
+
{rl: 'conferencePaper', rawText: 'Conference Paper', rawId: 47},
|
|
356
|
+
{rl: 'conferenceProceedings', rawText: 'Conference Proceedings', rawId: 10},
|
|
357
|
+
{rl: 'dataset', rawText: 'Dataset', rawId: 59},
|
|
358
|
+
{rl: 'dictionary', rawText: 'Dictionary', rawId: 52},
|
|
359
|
+
{rl: 'editedBook', rawText: 'Edited Book', rawId: 28},
|
|
360
|
+
{rl: 'electronicArticle', rawText: 'Electronic Article', rawId: 43},
|
|
361
|
+
{rl: 'electronicBook', rawText: 'Electronic Book', rawId: 44},
|
|
362
|
+
{rl: 'electronicBookSection', rawText: 'Electronic Book Section', rawId: 60},
|
|
363
|
+
{rl: 'encyclopedia', rawText: 'Encyclopedia', rawId: 53},
|
|
364
|
+
{rl: 'equation', rawText: 'Equation', rawId: 39},
|
|
365
|
+
{rl: 'figure', rawText: 'Figure', rawId: 37},
|
|
366
|
+
{rl: 'filmOrBroadcast', rawText: 'Film or Broadcast', rawId: 21},
|
|
367
|
+
{rl: 'generic', rawText: 'Generic', rawId: 13},
|
|
368
|
+
{rl: 'governmentDocument', rawText: 'Government Document', rawId: 46},
|
|
369
|
+
{rl: 'grant', rawText: 'Grant', rawId: 54},
|
|
370
|
+
{rl: 'hearing', rawText: 'Hearing', rawId: 14},
|
|
371
|
+
{rl: 'journalArticle', rawText: 'Journal Article', rawId: 17},
|
|
372
|
+
{rl: 'legalRuleOrRegulation', rawText:', Legal Rule or Regulation', rawId: 50},
|
|
373
|
+
{rl: 'magazineArticle', rawText: 'Magazine Article', rawId: 19},
|
|
374
|
+
{rl: 'manuscript', rawText: 'Manuscript', rawId: 36},
|
|
375
|
+
{rl: 'map', rawText: 'Map', rawId: 20},
|
|
376
|
+
{rl: 'music', rawText: 'Music', rawId: 61},
|
|
377
|
+
{rl: 'newspaperArticle', rawText: 'Newspaper Article', rawId: 23},
|
|
378
|
+
{rl: 'onlineDatabase', rawText: 'Online Database', rawId: 45},
|
|
379
|
+
{rl: 'onlineMultimedia', rawText: 'Online Multimedia', rawId: 48},
|
|
380
|
+
{rl: 'pamphlet', rawText: 'Pamphlet', rawId: 24},
|
|
381
|
+
{rl: 'patent', rawText: 'Patent', rawId: 25},
|
|
382
|
+
{rl: 'personalCommunication', rawText: 'Personal Communication', rawId: 26},
|
|
383
|
+
{rl: 'report', rawText: 'Report', rawId: 27},
|
|
384
|
+
{rl: 'serial', rawText: 'Serial', rawId: 57},
|
|
385
|
+
{rl: 'standard', rawText: 'Standard', rawId: 58},
|
|
386
|
+
{rl: 'statute', rawText: 'Statute', rawId: 31},
|
|
387
|
+
{rl: 'thesis', rawText: 'Thesis', rawId: 32},
|
|
388
|
+
{rl: 'unpublished', rawText: 'Unpublished Work', rawId: 34},
|
|
389
|
+
{rl: 'web', rawText: 'Web Page', rawId: 12},
|
|
390
|
+
],
|
|
391
|
+
rlMap: new Map(), // Calculated later for quicker lookup
|
|
392
|
+
rawMap: new Map(), // Calculated later for quicker lookup
|
|
393
|
+
},
|
|
394
|
+
// }}}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* @see modules/interface.js
|
|
400
|
+
*/
|
|
401
|
+
export function setup() {
|
|
402
|
+
// Create lookup object of translations.types with key as .rl / val as the full object
|
|
403
|
+
translations.types.collection.forEach(c => {
|
|
404
|
+
translations.types.rlMap.set(c.rl, c);
|
|
405
|
+
translations.types.rawMap.set(c.rawId, c);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic module interface
|
|
3
|
+
* This file serves no purpose other than to document the methods that each module can itself expose
|
|
4
|
+
*/
|
|
5
|
+
/* eslint-disable no-unused-vars */
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Provide the low-level function for a module to read a stream and emit refs
|
|
10
|
+
* @param {stream.Readable} stream Input stream to read from
|
|
11
|
+
* @param {Object} [options] Additional options to use when parsing
|
|
12
|
+
* @returns {EventEmitter} EventEmitter(-like) that should emit the below events
|
|
13
|
+
*
|
|
14
|
+
* @emits ref Emitted with a single ref object when found
|
|
15
|
+
* @emits end Emitted when parsing has completed
|
|
16
|
+
* @emits error Emitted when an error has been raised
|
|
17
|
+
*/
|
|
18
|
+
export function readStream(stream, options) {
|
|
19
|
+
// Stub
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Provide the low-level function for a module to write a stream
|
|
25
|
+
* @param {stream.Writable} stream Output stream to write to
|
|
26
|
+
* @param {array<Object>} refs Collection of refs to write
|
|
27
|
+
* @param {Object} [options] Additional options to use when writing
|
|
28
|
+
*
|
|
29
|
+
* @returns {Object} An object which exposes methods to call to start, write and end the writing process. All methods MUST return a Promise
|
|
30
|
+
* @property {function<Promise>} start Function to call when beginning to write
|
|
31
|
+
* @property {function<Promise>} write Function called as `(ref)` when writing a single ref
|
|
32
|
+
* @property {function<Promise>} end Function to call when finishing writing, must resolve its Promise when the stream has closed successfully
|
|
33
|
+
*/
|
|
34
|
+
export function writeStream(stream, refs, options) {
|
|
35
|
+
// Stub
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Function to run to set up any additional data before the module is needed
|
|
41
|
+
* This is generally used by worker functions which construct maps from collections for efficiency purposes
|
|
42
|
+
*/
|
|
43
|
+
export function setup() {
|
|
44
|
+
// Stub
|
|
45
|
+
}
|
package/modules/json.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import Emitter from '../shared/emitter.js';
|
|
2
|
+
import JSONStream from 'JSONStream';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @see modules/interface.js
|
|
6
|
+
*/
|
|
7
|
+
export function readStream(stream) {
|
|
8
|
+
let recNumber = 1;
|
|
9
|
+
let emitter = Emitter();
|
|
10
|
+
|
|
11
|
+
// Queue up the parser in the next tick (so we can return the emitter first)
|
|
12
|
+
setTimeout(()=>
|
|
13
|
+
stream.pipe(
|
|
14
|
+
JSONStream.parse('*')
|
|
15
|
+
.on('data', ref => emitter.emit('ref', {
|
|
16
|
+
recNumber: recNumber++,
|
|
17
|
+
...ref,
|
|
18
|
+
}))
|
|
19
|
+
.on('end', ()=> emitter.emit('end'))
|
|
20
|
+
.on('error', emitter.emit.bind('error'))
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return emitter;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @see modules/interface.js
|
|
30
|
+
* @param {Object} [options] Additional options to use when parsing
|
|
31
|
+
* @param {string} [options.lineSuffix='\n'] Optional line suffix for each output line of JSON
|
|
32
|
+
*/
|
|
33
|
+
export function writeStream(stream, options) {
|
|
34
|
+
let settings = {
|
|
35
|
+
lineSuffix: '\n',
|
|
36
|
+
...options,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
let lastRef; // Hold last refrence string in memory so we know when we've reached the end (last item shoulnd't have a closing comma)
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
start: ()=> {
|
|
43
|
+
stream.write('[\n');
|
|
44
|
+
return Promise.resolve();
|
|
45
|
+
},
|
|
46
|
+
write: ref => {
|
|
47
|
+
if (lastRef) stream.write(lastRef + ',' + settings.lineSuffix); // Flush last reference to disk with comma
|
|
48
|
+
lastRef = JSON.stringify(ref);
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
},
|
|
51
|
+
end: ()=> {
|
|
52
|
+
if (lastRef) stream.write(lastRef + settings.lineSuffix); // Flush final reference to disk without final comma
|
|
53
|
+
stream.write(']');
|
|
54
|
+
return new Promise((resolve, reject) =>
|
|
55
|
+
stream.end(err => err ? reject(err) : resolve())
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|