@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.
@@ -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
+ }
@@ -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,4 @@
1
+ export * as json from './json.js';
2
+ export * as endnoteXml from './endnoteXml.js';
3
+ export * as medline from './medline.js';
4
+ export * as ris from './ris.js';
@@ -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, '&amp;')
275
+ .replace(/\r/g, '&#xD;')
276
+ .replace(/</g, '&lt;')
277
+ .replace(/>/g, '&gt;')
278
+ .replace(/"/g, '&quot;')
279
+ .replace(/'/g, '&apos;');
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(/\&amp;/g, '&')
291
+ .replace(/\&#xD;/g, '\r')
292
+ .replace(/\&lt;/g, '<')
293
+ .replace(/\&gt;/g, '>')
294
+ .replace(/\&quot;/g, '"')
295
+ .replace(/\&apos;/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
+ }
@@ -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
+ }