@iebh/reflib 2.7.2 → 2.8.1

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.
@@ -1,638 +1,638 @@
1
- import Emitter from '../shared/emitter.js';
2
-
3
- /**
4
- * Read a Medline file as a stream
5
- *
6
- * @see modules/interface.js
7
- * @param {Stream} stream A readable stream analogue
8
- * @param {Object} [options] Additional options to use when parsing
9
- * @param {string} [options.defaultType='journalArticle'] Default citation type to assume when no other type is specified
10
- * @param {string} [options.delimeter='\r'] How to split multi-line items
11
- * @param {string} [options.reformatAuthors=true] Reformat Medline author format to more closely match the Reflib standard
12
- * @param {string} [options.journal='long'] Whether to use the 'long' journal name or the 'short' varient when parsing references
13
- * @param {boolean} [options.parseAddress=true] Try to recompose the `address` property from all author address information
14
- * @param {boolean} [options.parseDoi=true] Try to parse the DOI from the article identifiers
15
- * @param {boolean} [options.parseYear=true] If truthy try to parse the year field from the date
16
- *
17
- * @param {array<Object>} [options.fieldsReplace] If truthy adopt apply the field replacements, usually from medlineComplex fields to other values
18
- * @param {string} [options.fieldsReplace.from] Field to copy/move the value from, if undefined `reformat` must be specified
19
- * @param {string} options.fieldsReplace.to Field to copy/move the value to
20
- * @param {string} [options.fieldsReplace.delete=true] Whether to remove the orignal 'from' field if successful (i.e. reformat doesn't return false)
21
- * @param {function} [options.fieldsReplace.reformat] Optional function called as `(value, ref)` to provide the new field value. If return value is boolean `false` no action is taken
22
- *
23
- * @returns {Object} A readable stream analogue defined in `modules/interface.js`
24
- */
25
- export function readStream(stream, options) {
26
- let settings = {
27
- defaultType: 'journalArticle',
28
- delimeter: '\r',
29
- reformatAuthors: true,
30
- journal: 'long',
31
- parseAddress: true,
32
- parseDoi: true,
33
- parseYear: true,
34
- fieldsReplace: [],
35
- ...options,
36
- };
37
-
38
- // Settings parsing {{{
39
-
40
- /*
41
- settings.fieldsReplace.push({
42
- to: 'debugPre',
43
- reformat: (v, ref) => {
44
- console.log('DEBUG:PRE', ref);
45
- return false;
46
- },
47
- });
48
- */
49
-
50
- // Translate type
51
- settings.fieldsReplace.push({
52
- from: 'type',
53
- to: 'type',
54
- delete: false,
55
- reformat: v => translations.types.rawMap[v] || settings.defaultType,
56
- });
57
-
58
- // Reformat authors
59
- settings.fieldsReplace.push({
60
- to: 'authors',
61
- reformat: (authors, ref) => (ref.medlineAuthorsShort || ref.medlineAuthorsFull || []).map(author =>
62
- author.replace(/^(?<last>[\w-]+?) (?<initials>\w+)$/, (match, last, initials) => {
63
- return (
64
- last && initials ? last + ', ' + initials.split('').map(i => `${i}.`).join(' ')
65
- : last ? last
66
- : match
67
- )
68
- })
69
- ),
70
- });
71
-
72
- // Add rule for where the journal field comes from
73
- settings.fieldsReplace.push({
74
- to: 'journal',
75
- reformat: settings.journal.long
76
- ? (v, ref) => ref.medlineJournalFull || ref.medlineJournalShort
77
- : (v, ref) => ref.medlineJournalShort || ref.medlineJournalLong,
78
- });
79
-
80
- // Allow parsing of Address
81
- if (settings.parseAddress)
82
- settings.fieldsReplace.push({
83
- from: 'medlineAuthorsAffiliation',
84
- to: 'address',
85
- delete: false,
86
- reformat: v => {
87
- if (!v) return false;
88
- return v.join(settings.delimeter);
89
- },
90
- });
91
-
92
- // Allow parsing of DOIs
93
- if (settings.parseDoi)
94
- settings.fieldsReplace.push({
95
- from: 'medlineArticleID',
96
- to: 'doi',
97
- delete: false,
98
- reformat: v => /(?<doi>[\w\.\/_]+) \[doi\]/.exec(v)?.groups.doi || false,
99
- });
100
-
101
- // Allow parsing of years
102
- if (settings.parseYear)
103
- settings.fieldsReplace.push({
104
- from: 'date',
105
- to: 'year',
106
- delete: false,
107
- reformat: v => /(?<year>\d{4}\b)/.exec(v)?.groups.year || false,
108
- });
109
-
110
- /*
111
- settings.fieldsReplace.push({
112
- to: 'debugPost',
113
- reformat: (v, ref) => {
114
- console.log('DEBUG:POST', ref);
115
- return false;
116
- },
117
- });
118
- */
119
-
120
- // }}}
121
-
122
- let emitter = Emitter();
123
-
124
- let buffer = ''; // Incomming text buffer lines if the chunk we're given isn't enough to parse a reference yet
125
-
126
- // Queue up the parser in the next tick (so we can return the emitter first)
127
- setTimeout(()=> {
128
- stream
129
- .on('data', chunkBuffer => {
130
- emitter.emit('progress', stream.bytesRead);
131
- buffer += chunkBuffer.toString(); // Append incomming data to the partial-buffer we're holding in memory
132
-
133
- let bufferCrop = 0; // How many bytes to shift off the front of the buffer based on the last full reference we saw, should end up at the last byte offset of buffer that is valid to shift-truncate to
134
- let bufferSplitter = /(\r\n|\n){2,}/g; // RegExp to use per segment (multiple calls to .exec() stores state because JS is a hellscape)
135
-
136
- let bufferSegment;
137
-
138
- while (bufferSegment = bufferSplitter.exec(buffer)) {
139
- let parsedRef = parseRef(buffer.substring(bufferCrop, bufferSegment.index), settings); // Parse the ref from the start+end points
140
- emitter.emit('ref', parsedRef);
141
-
142
- bufferCrop = bufferSegment.index + bufferSegment[0].length; // Set start of next ref + cropping index to last seen offset + match
143
- }
144
-
145
- buffer = buffer.substring(bufferCrop); // Shift-truncate the buffer so we're ready to input more data on the next call
146
- })
147
- .on('error', e => emitter.emit('error', e))
148
- .on('end', ()=> {
149
- if (buffer.replace(/\s+/, '')) { // Anything left in the to-drain buffer?
150
- // Drain remaining buffer into parser before exiting
151
- emitter.emit('ref', parseRef(buffer, settings));
152
- }
153
-
154
- // Signal that we're done
155
- emitter.emit('end');
156
- })
157
- })
158
-
159
- return emitter;
160
- }
161
-
162
-
163
- /**
164
- * Write a Medline citation file to a stream
165
- * @see modules/interface.js
166
- *
167
- * @param {Stream} stream The stream to write to
168
- * @param {Object} [options] Additional options to use when parsing
169
- * @param {string} [options.defaultType='journalArticle'] Default citation type to assume when no other type is specified
170
- * @param {string} [options.delimeter='\r'] How to split multi-line items
171
- *
172
- * @returns {Object} A writable stream analogue defined in `modules/interface.js`
173
- */
174
- export function writeStream(stream, options) {
175
- let settings = {
176
- defaultType: 'journalArticle',
177
- delimeter: '\r',
178
- ...options,
179
- };
180
-
181
- return {
182
- start() {
183
- return Promise.resolve();
184
- },
185
- write: xRef => {
186
- let ref = { // Assign defaults if not already present
187
- type: settings.defaultType,
188
- title: '<NO TITLE>',
189
- ...xRef,
190
- };
191
-
192
- stream.write(
193
- translations.fields.collectionOutput
194
- .filter(f => ref[f.rl]) // Has field?
195
- .flatMap(f =>
196
- f.rl == 'type' // Translate type field
197
- ? 'TY - ' + (translations.types.rlMap.get(ref.type) || translations.types.rlMap.get(settings.defaultType)).raw
198
- : f.rl == 'title' // Special formatting for authors which should follow the title
199
- ? [
200
- 'TI - ' + ref.title,
201
- ...(ref.authors || []).flatMap((a, i) => [
202
- ref.medlineAuthorsFull?.[i] ? `FAU - ${ref.medlineAuthorsFull[i]}` : `FAU - ${ref.authors[i]}`,
203
- ref.medlineAuthorsShort?.[i] && `AU - ${ref.medlineAuthorsShort[i]}`,
204
- ref.medlineAuthorsAffiliation?.[i] && `AD - ${ref.medlineAuthorsAffiliation[i]}`,
205
- ref.medlineAuthorsId?.[i] && `AUID- ${ref.medlineAuthorsId[i]}`,
206
- ].filter(Boolean)),
207
- ]
208
- : f.outputSkip ? []
209
- : f.outputRepeat && Array.isArray(ref[f.rl]) // Repeat array types
210
- ? ref[f.rl].map(item => f.raw.padEnd(4, ' ') + '- ' + item)
211
- : Array.isArray(ref[f.rl]) // Flatten arrays into text
212
- ? f.raw.padEnd(4, ' ') + '- ' + ref[f.rl].join(settings.delimeter)
213
- : f.raw.padEnd(4, ' ') + '- ' + ref[f.rl] // Regular field output
214
- )
215
- .concat(['\n'])
216
- .join('\n')
217
- );
218
-
219
- return Promise.resolve();
220
- },
221
- end() {
222
- return new Promise((resolve, reject) =>
223
- stream.end(err => err ? reject(err) : resolve())
224
- );
225
- },
226
- };
227
- }
228
-
229
-
230
- /**
231
- * Parse a single RIS format reference from a block of text
232
- * This function is used internally by parseStream() for each individual reference
233
- *
234
- * @param {string} refString Raw RIS string composing the start -> end of the ref
235
- * @param {Object} settings Additional settings to pass, this should be initialized + parsed by the calling function for efficiency, see readStream() for full spec
236
- *
237
- * @returns {ReflibRef} A single reference, parsed form the incoming string
238
- */
239
- export function parseRef(refString, settings) {
240
- let ref = {}; // Reference under construction
241
- let lastField; // Last field object we saw, used to append values if they don't match the default RIS key=val one-liner
242
-
243
- refString
244
- .split(/[\r\n|\n]/) // Split into lines
245
- .forEach(line => {
246
- let parsedLine = /^\s*(?<key>[A-Z]+?)\s*-\s+(?<value>.*)$/s.exec(line)?.groups;
247
-
248
- if (!parsedLine) { // Doesn't match key=val spec
249
- line = line.trimStart();
250
- if (line.replace(/\s+/, '') && lastField) { // Line isn't just whitespace + We have a field to append to - append with \r delimiters
251
- if (lastField.inputArray) { // Treat each line feed like an array entry
252
- ref[lastField.rl].push(line);
253
- } else { // Assume we append each line entry as a single-line string
254
- ref[lastField.rl] += ' ' + line;
255
- }
256
- }
257
- return; // Stop processing this line
258
- }
259
-
260
- let fieldLookup = translations.fields.rawMap.get(parsedLine.key);
261
-
262
- if (lastField?.trimDotSuffix)
263
- ref[lastField.rl] = ref[lastField.rl].replace(/\.$/, '');
264
-
265
- if (!fieldLookup) { // Skip unknown field translations
266
- lastField = null;
267
- return;
268
- } else if (fieldLookup.inputArray) { // Should this `rl` key be treated like an appendable array?
269
- if (!ref[fieldLookup.rl]) { // Array doesn't exist yet
270
- ref[fieldLookup.rl] = [parsedLine.value];
271
- } else {
272
- ref[fieldLookup.rl].push(parsedLine.value);
273
- }
274
- lastField = fieldLookup;
275
- } else { // Simple key=val
276
- ref[fieldLookup.rl] = parsedLine.value;
277
- lastField = fieldLookup;
278
- }
279
- })
280
-
281
- // Post processing {{{
282
- // Apply field replacement / reformat rules
283
- if (settings.fieldsReplace?.length > 0)
284
- settings.fieldsReplace.forEach(replacement => {
285
- let newVal = replacement.from ? ref[replacement.from] : null;
286
-
287
- // Apply reformat if we have one
288
- if (replacement.reformat) {
289
- newVal = replacement.reformat(newVal, ref);
290
- if (newVal === false) return; // Skip boolean false
291
- }
292
-
293
- // Copy field 'from' -> 'to'
294
- ref[replacement.to] = newVal;
295
-
296
- // Delete 'from' field
297
- if (replacement.from && (replacement.delete ?? true))
298
- delete ref[replacement.from];
299
- })
300
- // }}}
301
-
302
- return ref;
303
- }
304
-
305
-
306
- /**
307
- * Lookup tables for this module
308
- * @type {Object}
309
- * @property {array<Object>} fields Field translations between Reflib (`rl`) and the raw format (`raw`)
310
- * @property {array<Object>} types Field translations between Reflib (`rl`) and the raw format types as raw text (`rawText`) and numeric ID (`rawId`)
311
- * @property {boolean} isArray Whether the field should append to any existing `rl` field and be treated like an array of data
312
- * @property {number|boolean} [sort] Sort order when outputting, use boolean `false` to disable the field on output
313
- * @property {boolean} [outputSkip=false] Dont output this field at all
314
- * @property {boolean} [outputRepeat=false] Whether to repeat the output field if multiple values are present, if disabled arrays are flattened into a string with newlines instead
315
- * @property {boolean} [inputArray=false] Forcably cast the field as an array when reading, even if there is only one value
316
- * @property {boolean} [trimDotSuffix=false] Remove any trailing dot character IF the input string spans multiple lines
317
- */
318
- export let translations = {
319
- // Field translations {{{
320
- fields: {
321
- collection: [
322
- // Based on the spec at https://www.nlm.nih.gov/bsd/mms/medlineelements.html
323
- // Any field beginning with `medline*` is non-standard but included to prevent data loss when converted back to Medline format
324
- {rl: 'medlinePMID', raw: 'PMID', sort: 0},
325
- {rl: 'medlineOwner', raw: 'OWN', sort: 1},
326
- {rl: 'medlineStatus', raw: 'STAT', sort: 2},
327
- {rl: 'medlineDateRevised', raw: 'DR', sort: 3},
328
- {rl: 'medlineISSN', raw: 'IS', outputRepeat: true, inputArray: true, sort: 4},
329
- {rl: 'date', raw: 'DP', sort: 5},
330
- {rl: 'title', raw: 'TI', sort: 6, trimDotSuffix: true},
331
- {rl: 'doi', raw: 'LID', sort: 7},
332
- {rl: 'abstract', raw: 'AB', sort: 8},
333
- {rl: 'medlineCopyright', raw: 'CI', sort: 7},
334
-
335
- // NOTE: Authors get special treatment when formatting so all these fields are skipped
336
- {rl: 'medlineAuthorsFull', raw: 'FAU', sort: 10, outputSkip: true, inputArray: true},
337
- {rl: 'medlineAuthorsShort', raw: 'AU', sort: 10, outputSkip: true, inputArray: true},
338
- {rl: 'medlineAuthorsAffiliation', raw: 'AD', sort: 10, outputSkip: true, inputArray: true},
339
- {rl: 'medlineAuthorsId', raw: 'AUID', sort: 10, outputSkip: true, inputArray: true},
340
-
341
- {rl: 'language', raw: 'LA', sort: 11},
342
- {rl: 'medlineGrantNumber', raw: 'GR', sort: 12},
343
- {rl: 'type', raw: 'PT', sort: 14},
344
- {rl: 'medlineTypeSecondary', raw: 'PTX'}, // Populated with any other write to PT field as array
345
- {rl: 'medlineDateElectronicPublication', raw: 'DEP', sort: 15},
346
- {rl: 'address', raw: 'PL', sort: 16},
347
- {rl: 'medlineJournalShort', raw: 'TA', sort: 17},
348
- {rl: 'medlineJournalFull', raw: 'JT', sort: 18},
349
- {rl: 'medlineNLMID', raw: 'JID', sort: 19},
350
- {rl: 'medlineSubset', raw: 'SB', sort: 20},
351
- {rl: 'medlineOwnerOtherTerm', raw: 'OTO', sort: 21},
352
- {rl: 'keywords', raw: 'OT', outputRepeat: true, inputArray: true, sort: 22},
353
- {rl: 'medlineEntrezDate', raw: 'EDAT', sort: 23},
354
- {rl: 'medlineDateMesh', raw: 'MHDA', sort: 24},
355
- {rl: 'medlinePublicationHistoryStatus', raw: 'PHST', outputRepeat: true, inputArray: true, sort: 25},
356
- {rl: 'medlineArticleID', raw: 'AID', sort: 26},
357
- {rl: 'medlineStatusPublication', raw: 'PST', sort: 27},
358
- {rl: 'medlineSource', raw: 'SO', sort: 27},
359
- {rl: 'notes', raw: 'GN', inputArray: true, outputRepeat: true, sort: 28},
360
-
361
- {rl: 'isbn', raw: 'ISBN'},
362
- {rl: 'volume', raw: 'VI'},
363
- {rl: 'medlineVolumeTitle', raw: 'VTI'},
364
- {rl: 'pages', raw: 'PG'},
365
- {rl: 'medlineInvestigatorAffiliation', raw: 'IRAD'},
366
- {rl: 'medlineInvestigatorName', raw: 'IR'},
367
- {rl: 'medlineInvestigatorNameFull', raw: 'FIR'},
368
- {rl: 'medlineTitleBook', raw: 'BTI'},
369
- {rl: 'medlineTitleCollection', raw: 'CTI'},
370
- {rl: 'medlineConflictOfInterestStatement', raw: 'COIS'},
371
- {rl: 'medlineCorporateAuthor', raw: 'CN'},
372
- {rl: 'medlineDateCreate', raw: 'CRDT'},
373
- {rl: 'medlineDateCreated', raw: 'DA'},
374
- {rl: 'medlineDateCompleted', raw: 'DCOM'},
375
- {rl: 'medlineEdition', raw: 'EN'},
376
- {rl: 'medlineEditor', raw: 'ED'},
377
- {rl: 'medlineEditorFull', raw: 'FED'},
378
- {rl: 'medlineGeneSymbol', raw: 'GS'},
379
- {rl: 'medlineISSN', raw: 'IS'},
380
- {rl: 'number', raw: 'IP'},
381
- {rl: 'medlineManuscriptID', raw: 'MID'},
382
- {rl: 'medlineMeshTerms', raw: 'MH', outputRepeat: true, inputArray: true},
383
- {rl: 'medlineReferenceCount', raw: 'RF'},
384
- {rl: 'medlineAbstractOther', raw: 'OAB'},
385
- {rl: 'medlineCopyrightOther', raw: 'OCI'},
386
- {rl: 'medlineIDOther', raw: 'OID'},
387
- {rl: 'medlinePersonalName', raw: 'PS'},
388
- {rl: 'medlinePersonalNameFull', raw: 'FPS'},
389
- {rl: 'medlinePublishingModel', raw: 'PUBM'},
390
- {rl: 'medlinePubMedCentralID', raw: 'PMC'},
391
- {rl: 'medlinePubMedCentralRelease', raw: 'PMCR'},
392
- {rl: 'medlineRegistryNumber', raw: 'RN'},
393
- {rl: 'medlineSubstanceName', raw: 'NM'},
394
- {rl: 'medlineSecondarySource', raw: 'SI'},
395
- {rl: 'medlineSpaceFlightMission', raw: 'SFM'},
396
- {rl: 'medlineSubset', raw: 'SB'},
397
- {rl: 'medlineTitleTransliterated', raw: 'TT', sort: 6},
398
- ],
399
- collectionOutput: [], // Sorted + filtered version of the above to use when outputting
400
- rawMap: new Map(), // Calculated later for quicker lookup
401
- rlMap: new Map(), // Calculated later for quicker lookup
402
- },
403
- // }}}
404
- // Ref type translations {{{
405
- types: {
406
- collection: [
407
- // Formats we support as a translation (or near-enough translation)
408
- // Note that the preferred translation should be first
409
-
410
- // High priority translations
411
- {raw: 'Blog', rl: 'blog'},
412
- {raw: 'Case Reports', rl: 'case'},
413
- {raw: 'Catalog', rl: 'catalog'},
414
- {raw: 'Chart', rl: 'chartOrTable'},
415
- {raw: 'Database', rl: 'aggregatedDatabase'},
416
- {raw: 'Dataset', rl: 'dataset'},
417
- {raw: 'Dictionary', rl: 'dictionary'},
418
- {raw: 'Encyclopedia', rl: 'encyclopedia'},
419
- {raw: 'Journal Article', rl: 'journalArticle'},
420
- {raw: 'Legal Case', rl: 'legalRuleOrRegulation'},
421
- {raw: 'Manuscript', rl: 'manuscript'},
422
- {raw: 'Map', rl: 'map'},
423
- {raw: 'Newspaper Article', rl: 'newspaperArticle'},
424
- {raw: 'Patent', rl: 'patent'},
425
- {raw: 'Preprint', rl: 'unpublished'},
426
- {raw: 'Tables', rl: 'chartOrTable'},
427
- {raw: 'Technical Report', rl: 'report'},
428
- {raw: 'unknown', rl: 'unknown'},
429
- {raw: 'Unpublished Work', rl: 'unpublished'},
430
- {raw: 'Video-Audio Media', rl: 'audioVisualMaterial'},
431
- {raw: 'Web Archive', rl: 'web'},
432
-
433
- // Lower priority translations
434
- {raw: 'Address', rl: 'personalCommunication'},
435
- {raw: 'Advertisement', rl: 'audioVisualMaterial'},
436
- {raw: 'Almanac', rl: 'book'},
437
- {raw: 'Anecdotes', rl: 'blog'},
438
- {raw: 'Animation', rl: 'filmOrBroadcast'},
439
- {raw: 'Annual Report', rl: 'report'},
440
- {raw: 'Aphorisms and Proverbs', rl: 'pamphlet'},
441
- {raw: 'Architectural Drawing', rl: 'figure'},
442
- {raw: 'Autobiography', rl: 'book'},
443
- {raw: 'Bibliography', rl: 'catalog'},
444
- {raw: 'Biobibliography', rl: 'book'},
445
- {raw: 'Biography', rl: 'book'},
446
- {raw: 'Book Illustrations', rl: 'audioVisualMaterial'},
447
- {raw: 'Book Review', rl: 'magazineArticle'},
448
- {raw: 'Caricature', rl: 'artwork'},
449
- {raw: 'Cartoon', rl: 'audioVisualMaterial'},
450
- {raw: 'Catalog, Bookseller', rl: 'catalog'},
451
- {raw: 'Catalog, Commercial', rl: 'catalog'},
452
- {raw: 'Catalog, Drug', rl: 'catalog'},
453
- {raw: 'Catalog, Publisher', rl: 'catalog'},
454
- {raw: 'Catalog, Union', rl: 'catalog'},
455
- {raw: 'Chronology', rl: 'book'},
456
- {raw: 'Classical Article', rl: 'classicalWork'},
457
- {raw: 'Clinical Conference', rl: 'conferenceProceedings'},
458
- {raw: 'Clinical Study', rl: 'dataset'},
459
- {raw: 'Clinical Trial, Phase III', rl: 'dataset'},
460
- {raw: 'Clinical Trial, Phase II', rl: 'dataset'},
461
- {raw: 'Clinical Trial, Phase I', rl: 'dataset'},
462
- {raw: 'Clinical Trial, Phase IV', rl: 'dataset'},
463
- {raw: 'Clinical Trial Protocol', rl: 'dataset'},
464
- {raw: 'Clinical Trial', rl: 'dataset'},
465
- {raw: 'Clinical Trial, Veterinary', rl: 'dataset'},
466
- {raw: 'Consensus Development Conference, NIH', rl: 'conferenceProceedings'},
467
- {raw: 'Consensus Development Conference', rl: 'conferenceProceedings'},
468
- {raw: 'Corrected and Republished Article', rl: 'journalArticle'},
469
- {raw: 'Database', rl: 'onlineDatabase'},
470
- {raw: 'Dictionary, Chemical', rl: 'dictionary'},
471
- {raw: 'Dictionary, Classical', rl: 'dictionary'},
472
- {raw: 'Dictionary, Dental', rl: 'dictionary'},
473
- {raw: 'Dictionary, Medical', rl: 'dictionary'},
474
- {raw: 'Dictionary, Pharmaceutic', rl: 'dictionary'},
475
- {raw: 'Dictionary, Polyglot', rl: 'dictionary'},
476
- {raw: 'Documentaries and Factual Films', rl: 'filmOrBroadcast'},
477
- {raw: 'Drawing', rl: 'audioVisualMaterial'},
478
- {raw: 'Ephemera', rl: 'artwork'},
479
- {raw: 'Eulogy', rl: 'personalCommunication'},
480
- {raw: 'Formulary, Dental', rl: 'dataset'},
481
- {raw: 'Formulary, Homeopathic', rl: 'dataset'},
482
- {raw: 'Formulary, Hospital', rl: 'dataset'},
483
- {raw: 'Formulary', rl: 'dataset'},
484
- {raw: 'Funeral Sermon', rl: 'personalCommunication'},
485
- {raw: 'Government Publication', rl: 'governmentDocument'},
486
- {raw: 'Graphic Novel', rl: 'artwork'},
487
- {raw: 'Historical Article', rl: 'ancientText'},
488
- {raw: 'Incunabula', rl: 'ancientText'},
489
- {raw: 'Incunabula', rl: 'ancientText'},
490
- {raw: 'Instructional Film and Video', rl: 'filmOrBroadcast'},
491
- {raw: 'Introductory Journal Article', rl: 'journalArticle'},
492
- {raw: 'Legislation', rl: 'statute'},
493
- {raw: 'Letter', rl: 'personalCommunication'},
494
- {raw: 'Manuscript, Medical', rl: 'manuscript'},
495
- {raw: 'Movable Books', rl: 'books'},
496
- {raw: 'News', rl: 'newspaperArticle'},
497
- {raw: 'Pharmacopoeia, Homeopathic', rl: 'book'},
498
- {raw: 'Pharmacopoeia', rl: 'book'},
499
- {raw: 'Photograph', rl: 'audioVisualMaterial'},
500
- {raw: 'Pictorial Work', rl: 'audioVisualMaterial'},
501
- {raw: 'Poetry', rl: 'artwork'},
502
- {raw: 'Portrait', rl: 'artwork'},
503
- {raw: 'Postcard', rl: 'audioVisualMaterial'},
504
- {raw: 'Poster', rl: 'audioVisualMaterial'},
505
- {raw: 'Public Service Announcement', rl: 'governmentDocument'},
506
- {raw: 'Research Support, N.I.H., Extramural', rl: 'grant'},
507
- {raw: 'Research Support, N.I.H., Intramural', rl: 'grant'},
508
- {raw: 'Research Support, Non-U.S. Gov\'t', rl: 'grant'},
509
- {raw: 'Research Support, U.S. Government', rl: 'grant'},
510
- {raw: 'Research Support, U.S. Gov\'t, Non-P.H.S.', rl: 'grant'},
511
- {raw: 'Research Support, U.S. Gov\'t, P.H.S.', rl: 'grant'},
512
- {raw: 'Resource Guide', rl: 'standard'},
513
- {raw: 'Retracted Publication', rl: 'journalArticle'},
514
- {raw: 'Retraction of Publication', rl: 'journalArticle'},
515
- {raw: 'Sermon', rl: 'journalArticle'},
516
- {raw: 'Statistics', rl: 'chartOrTable'},
517
- {raw: 'Study Guide', rl: 'standard'},
518
- {raw: 'Terminology', rl: 'catalog'},
519
- {raw: 'Textbook', rl: 'book'},
520
- {raw: 'Unedited Footage', rl: 'audioVisualMaterial'},
521
- {raw: 'Webcast', rl: 'web'},
522
- {raw: 'Wit and Humor', rl: 'artwork'},
523
-
524
-
525
- // Unsupported - Please map these and submit a PR if you think something obvious is mising
526
- {rl: '', raw: 'Abbreviations'},
527
- {rl: '', raw: 'Abstracts'},
528
- {rl: '', raw: 'Academic Dissertation'},
529
- {rl: '', raw: 'Account Book'},
530
- {rl: '', raw: 'Adaptive Clinical Trial'},
531
- {rl: '', raw: 'Atlas'},
532
- {rl: '', raw: 'Bookplate'},
533
- {rl: '', raw: 'Broadside'},
534
- {rl: '', raw: 'Calendar'},
535
- {rl: '', raw: 'Collected Correspondence'},
536
- {rl: '', raw: 'Collected Work'},
537
- {rl: '', raw: 'Collection'},
538
- {rl: '', raw: 'Comment'},
539
- {rl: '', raw: 'Comparative Study'},
540
- {rl: '', raw: 'Congress'},
541
- {rl: '', raw: 'Controlled Clinical Trial'},
542
- {rl: '', raw: 'Cookbook'},
543
- {rl: '', raw: 'Diary'},
544
- {rl: '', raw: 'Directory'},
545
- {rl: '', raw: 'Dispensatory'},
546
- {rl: '', raw: 'Duplicate Publication'},
547
- {rl: '', raw: 'Editorial'},
548
- {rl: '', raw: 'Electronic Supplementary Materials'},
549
- {rl: '', raw: 'English Abstract'},
550
- {rl: '', raw: 'Equivalence Trial'},
551
- {rl: '', raw: 'Essay'},
552
- {rl: '', raw: 'Evaluation Study'},
553
- {rl: '', raw: 'Examination Questions'},
554
- {rl: '', raw: 'Exhibition'},
555
- {rl: '', raw: 'Expression of Concern'},
556
- {rl: '', raw: 'Festschrift'},
557
- {rl: '', raw: 'Fictional Work'},
558
- {rl: '', raw: 'Form'},
559
- {rl: '', raw: 'legalRuleOrRegulation'},
560
- {rl: '', raw: 'Guidebook'},
561
- {rl: '', raw: 'Guideline'},
562
- {rl: '', raw: 'Handbook'},
563
- {rl: '', raw: 'Herbal'},
564
- {rl: '', raw: 'Index'},
565
- {rl: '', raw: 'Interactive Tutorial'},
566
- {rl: '', raw: 'Interview'},
567
- {rl: '', raw: 'Juvenile Literature'},
568
- {rl: '', raw: 'Laboratory Manual'},
569
- {rl: '', raw: 'Lecture Note'},
570
- {rl: '', raw: 'Meeting Abstract'},
571
- {rl: '', raw: 'Meta-Analysis'},
572
- {rl: '', raw: 'Monograph'},
573
- {rl: '', raw: 'Multicenter Study'},
574
- {rl: '', raw: 'Nurses Instruction'},
575
- {rl: '', raw: 'Observational Study'},
576
- {rl: '', raw: 'Observational Study, Veterinary'},
577
- {rl: '', raw: 'Outline'},
578
- {rl: '', raw: 'Overall'},
579
- {rl: '', raw: 'Patient Education Handout'},
580
- {rl: '', raw: 'Periodical'},
581
- {rl: '', raw: 'Periodical Index'},
582
- {rl: '', raw: 'Personal Narrative'},
583
- {rl: '', raw: 'Phrases'},
584
- {rl: '', raw: 'Popular Work'},
585
- {rl: '', raw: 'Practice Guideline'},
586
- {rl: '', raw: 'Pragmatic Clinical Trial'},
587
- {rl: '', raw: 'Price List'},
588
- {rl: '', raw: 'Problems and Exercises'},
589
- {rl: '', raw: 'Program'},
590
- {rl: '', raw: 'Programmed Instruction'},
591
- {rl: '', raw: 'Prospectus'},
592
- {rl: '', raw: 'Publication Components'},
593
- {rl: '', raw: 'Publication Formats'},
594
- {rl: '', raw: 'Published Erratum'},
595
- {rl: '', raw: 'Randomized Controlled Trial'},
596
- {rl: '', raw: 'Randomized Controlled Trial, Veterinary'},
597
- {rl: '', raw: 'Research Support, American Recovery and Reinvestment Act'},
598
- {rl: '', raw: 'Review'},
599
- {rl: '', raw: 'Scientific Integrity Review'},
600
- {rl: '', raw: 'Study Characteristics'},
601
- {rl: '', raw: 'Support of Research'},
602
- {rl: '', raw: 'Systematic Review'},
603
- {rl: '', raw: 'Twin Study'},
604
- {rl: '', raw: 'Union List'},
605
- {rl: '', raw: 'Validation Study'},
606
- ],
607
- rawMap: new Map(), // Calculated later for quicker lookup
608
- rlMap: new Map(), // Calculated later for quicker lookup
609
- },
610
- // }}}
611
- };
612
-
613
-
614
- /**
615
- * @see modules/interface.js
616
- */
617
- export function setup() {
618
- // Sort the field set by sort field
619
- translations.fields.collectionOutput = translations.fields.collection
620
- .filter(f => f.sort !== false)
621
- .sort((a, b) => (a.sort ?? 1000) == (b.sort ?? 1000) ? 0
622
- : (a.sort ?? 1000) < (b.sort ?? 1000) ? -1
623
- : 1
624
- )
625
-
626
- // Create lookup object of translations.fields with key as .rl / val as the full object
627
- translations.fields.collection.forEach(c => {
628
- translations.fields.rlMap.set(c.rl, c);
629
- translations.fields.rawMap.set(c.raw, c);
630
- });
631
-
632
- // Create lookup object of ref.types with key as .rl / val as the full object
633
- translations.types.collection.forEach(c => {
634
- translations.types.rlMap.set(c.rl, c);
635
- translations.types.rawMap.set(c.raw, c);
636
- });
637
-
638
- }
1
+ import Emitter from '../shared/emitter.js';
2
+
3
+ /**
4
+ * Read a Medline file as a stream
5
+ *
6
+ * @see modules/interface.js
7
+ * @param {Stream} stream A readable stream analogue
8
+ * @param {Object} [options] Additional options to use when parsing
9
+ * @param {string} [options.defaultType='journalArticle'] Default citation type to assume when no other type is specified
10
+ * @param {string} [options.delimeter='\r'] How to split multi-line items
11
+ * @param {string} [options.reformatAuthors=true] Reformat Medline author format to more closely match the Reflib standard
12
+ * @param {string} [options.journal='long'] Whether to use the 'long' journal name or the 'short' varient when parsing references
13
+ * @param {boolean} [options.parseAddress=true] Try to recompose the `address` property from all author address information
14
+ * @param {boolean} [options.parseDoi=true] Try to parse the DOI from the article identifiers
15
+ * @param {boolean} [options.parseYear=true] If truthy try to parse the year field from the date
16
+ *
17
+ * @param {array<Object>} [options.fieldsReplace] If truthy adopt apply the field replacements, usually from medlineComplex fields to other values
18
+ * @param {string} [options.fieldsReplace.from] Field to copy/move the value from, if undefined `reformat` must be specified
19
+ * @param {string} options.fieldsReplace.to Field to copy/move the value to
20
+ * @param {string} [options.fieldsReplace.delete=true] Whether to remove the orignal 'from' field if successful (i.e. reformat doesn't return false)
21
+ * @param {function} [options.fieldsReplace.reformat] Optional function called as `(value, ref)` to provide the new field value. If return value is boolean `false` no action is taken
22
+ *
23
+ * @returns {Object} A readable stream analogue defined in `modules/interface.js`
24
+ */
25
+ export function readStream(stream, options) {
26
+ let settings = {
27
+ defaultType: 'journalArticle',
28
+ delimeter: '\r',
29
+ reformatAuthors: true,
30
+ journal: 'long',
31
+ parseAddress: true,
32
+ parseDoi: true,
33
+ parseYear: true,
34
+ fieldsReplace: [],
35
+ ...options,
36
+ };
37
+
38
+ // Settings parsing {{{
39
+
40
+ /*
41
+ settings.fieldsReplace.push({
42
+ to: 'debugPre',
43
+ reformat: (v, ref) => {
44
+ console.log('DEBUG:PRE', ref);
45
+ return false;
46
+ },
47
+ });
48
+ */
49
+
50
+ // Translate type
51
+ settings.fieldsReplace.push({
52
+ from: 'type',
53
+ to: 'type',
54
+ delete: false,
55
+ reformat: v => translations.types.rawMap[v] || settings.defaultType,
56
+ });
57
+
58
+ // Reformat authors
59
+ settings.fieldsReplace.push({
60
+ to: 'authors',
61
+ reformat: (authors, ref) => (ref.medlineAuthorsShort || ref.medlineAuthorsFull || []).map(author =>
62
+ author.replace(/^(?<last>[\w-]+?) (?<initials>\w+)$/, (match, last, initials) => {
63
+ return (
64
+ last && initials ? last + ', ' + initials.split('').map(i => `${i}.`).join(' ')
65
+ : last ? last
66
+ : match
67
+ )
68
+ })
69
+ ),
70
+ });
71
+
72
+ // Add rule for where the journal field comes from
73
+ settings.fieldsReplace.push({
74
+ to: 'journal',
75
+ reformat: settings.journal.long
76
+ ? (v, ref) => ref.medlineJournalFull || ref.medlineJournalShort
77
+ : (v, ref) => ref.medlineJournalShort || ref.medlineJournalLong,
78
+ });
79
+
80
+ // Allow parsing of Address
81
+ if (settings.parseAddress)
82
+ settings.fieldsReplace.push({
83
+ from: 'medlineAuthorsAffiliation',
84
+ to: 'address',
85
+ delete: false,
86
+ reformat: v => {
87
+ if (!v) return false;
88
+ return v.join(settings.delimeter);
89
+ },
90
+ });
91
+
92
+ // Allow parsing of DOIs
93
+ if (settings.parseDoi)
94
+ settings.fieldsReplace.push({
95
+ from: 'medlineArticleID',
96
+ to: 'doi',
97
+ delete: false,
98
+ reformat: v => /(?<doi>[\w\.\/_]+) \[doi\]/.exec(v)?.groups.doi || false,
99
+ });
100
+
101
+ // Allow parsing of years
102
+ if (settings.parseYear)
103
+ settings.fieldsReplace.push({
104
+ from: 'date',
105
+ to: 'year',
106
+ delete: false,
107
+ reformat: v => /(?<year>\d{4}\b)/.exec(v)?.groups.year || false,
108
+ });
109
+
110
+ /*
111
+ settings.fieldsReplace.push({
112
+ to: 'debugPost',
113
+ reformat: (v, ref) => {
114
+ console.log('DEBUG:POST', ref);
115
+ return false;
116
+ },
117
+ });
118
+ */
119
+
120
+ // }}}
121
+
122
+ let emitter = Emitter();
123
+
124
+ let buffer = ''; // Incomming text buffer lines if the chunk we're given isn't enough to parse a reference yet
125
+
126
+ // Queue up the parser in the next tick (so we can return the emitter first)
127
+ setTimeout(()=> {
128
+ stream
129
+ .on('data', chunkBuffer => {
130
+ emitter.emit('progress', stream.bytesRead);
131
+ buffer += chunkBuffer.toString(); // Append incomming data to the partial-buffer we're holding in memory
132
+
133
+ let bufferCrop = 0; // How many bytes to shift off the front of the buffer based on the last full reference we saw, should end up at the last byte offset of buffer that is valid to shift-truncate to
134
+ let bufferSplitter = /(\r\n|\n){2,}/g; // RegExp to use per segment (multiple calls to .exec() stores state because JS is a hellscape)
135
+
136
+ let bufferSegment;
137
+
138
+ while (bufferSegment = bufferSplitter.exec(buffer)) {
139
+ let parsedRef = parseRef(buffer.substring(bufferCrop, bufferSegment.index), settings); // Parse the ref from the start+end points
140
+ emitter.emit('ref', parsedRef);
141
+
142
+ bufferCrop = bufferSegment.index + bufferSegment[0].length; // Set start of next ref + cropping index to last seen offset + match
143
+ }
144
+
145
+ buffer = buffer.substring(bufferCrop); // Shift-truncate the buffer so we're ready to input more data on the next call
146
+ })
147
+ .on('error', e => emitter.emit('error', e))
148
+ .on('end', ()=> {
149
+ if (buffer.replace(/\s+/, '')) { // Anything left in the to-drain buffer?
150
+ // Drain remaining buffer into parser before exiting
151
+ emitter.emit('ref', parseRef(buffer, settings));
152
+ }
153
+
154
+ // Signal that we're done
155
+ emitter.emit('end');
156
+ })
157
+ })
158
+
159
+ return emitter;
160
+ }
161
+
162
+
163
+ /**
164
+ * Write a Medline citation file to a stream
165
+ * @see modules/interface.js
166
+ *
167
+ * @param {Stream} stream The stream to write to
168
+ * @param {Object} [options] Additional options to use when parsing
169
+ * @param {string} [options.defaultType='journalArticle'] Default citation type to assume when no other type is specified
170
+ * @param {string} [options.delimeter='\r'] How to split multi-line items
171
+ *
172
+ * @returns {Object} A writable stream analogue defined in `modules/interface.js`
173
+ */
174
+ export function writeStream(stream, options) {
175
+ let settings = {
176
+ defaultType: 'journalArticle',
177
+ delimeter: '\r',
178
+ ...options,
179
+ };
180
+
181
+ return {
182
+ start() {
183
+ return Promise.resolve();
184
+ },
185
+ write: xRef => {
186
+ let ref = { // Assign defaults if not already present
187
+ type: settings.defaultType,
188
+ title: '<NO TITLE>',
189
+ ...xRef,
190
+ };
191
+
192
+ stream.write(
193
+ translations.fields.collectionOutput
194
+ .filter(f => ref[f.rl]) // Has field?
195
+ .flatMap(f =>
196
+ f.rl == 'type' // Translate type field
197
+ ? 'TY - ' + (translations.types.rlMap.get(ref.type) || translations.types.rlMap.get(settings.defaultType)).raw
198
+ : f.rl == 'title' // Special formatting for authors which should follow the title
199
+ ? [
200
+ 'TI - ' + ref.title,
201
+ ...(ref.authors || []).flatMap((a, i) => [
202
+ ref.medlineAuthorsFull?.[i] ? `FAU - ${ref.medlineAuthorsFull[i]}` : `FAU - ${ref.authors[i]}`,
203
+ ref.medlineAuthorsShort?.[i] && `AU - ${ref.medlineAuthorsShort[i]}`,
204
+ ref.medlineAuthorsAffiliation?.[i] && `AD - ${ref.medlineAuthorsAffiliation[i]}`,
205
+ ref.medlineAuthorsId?.[i] && `AUID- ${ref.medlineAuthorsId[i]}`,
206
+ ].filter(Boolean)),
207
+ ]
208
+ : f.outputSkip ? []
209
+ : f.outputRepeat && Array.isArray(ref[f.rl]) // Repeat array types
210
+ ? ref[f.rl].map(item => f.raw.padEnd(4, ' ') + '- ' + item)
211
+ : Array.isArray(ref[f.rl]) // Flatten arrays into text
212
+ ? f.raw.padEnd(4, ' ') + '- ' + ref[f.rl].join(settings.delimeter)
213
+ : f.raw.padEnd(4, ' ') + '- ' + ref[f.rl] // Regular field output
214
+ )
215
+ .concat(['\n'])
216
+ .join('\n')
217
+ );
218
+
219
+ return Promise.resolve();
220
+ },
221
+ end() {
222
+ return new Promise((resolve, reject) =>
223
+ stream.end(err => err ? reject(err) : resolve())
224
+ );
225
+ },
226
+ };
227
+ }
228
+
229
+
230
+ /**
231
+ * Parse a single RIS format reference from a block of text
232
+ * This function is used internally by parseStream() for each individual reference
233
+ *
234
+ * @param {string} refString Raw RIS string composing the start -> end of the ref
235
+ * @param {Object} settings Additional settings to pass, this should be initialized + parsed by the calling function for efficiency, see readStream() for full spec
236
+ *
237
+ * @returns {ReflibRef} A single reference, parsed form the incoming string
238
+ */
239
+ export function parseRef(refString, settings) {
240
+ let ref = {}; // Reference under construction
241
+ let lastField; // Last field object we saw, used to append values if they don't match the default RIS key=val one-liner
242
+
243
+ refString
244
+ .split(/[\r\n|\n]/) // Split into lines
245
+ .forEach(line => {
246
+ let parsedLine = /^\s*(?<key>[A-Z]+?)\s*-\s+(?<value>.*)$/s.exec(line)?.groups;
247
+
248
+ if (!parsedLine) { // Doesn't match key=val spec
249
+ line = line.trimStart();
250
+ if (line.replace(/\s+/, '') && lastField) { // Line isn't just whitespace + We have a field to append to - append with \r delimiters
251
+ if (lastField.inputArray) { // Treat each line feed like an array entry
252
+ ref[lastField.rl].push(line);
253
+ } else { // Assume we append each line entry as a single-line string
254
+ ref[lastField.rl] += ' ' + line;
255
+ }
256
+ }
257
+ return; // Stop processing this line
258
+ }
259
+
260
+ let fieldLookup = translations.fields.rawMap.get(parsedLine.key);
261
+
262
+ if (lastField?.trimDotSuffix)
263
+ ref[lastField.rl] = ref[lastField.rl].replace(/\.$/, '');
264
+
265
+ if (!fieldLookup) { // Skip unknown field translations
266
+ lastField = null;
267
+ return;
268
+ } else if (fieldLookup.inputArray) { // Should this `rl` key be treated like an appendable array?
269
+ if (!ref[fieldLookup.rl]) { // Array doesn't exist yet
270
+ ref[fieldLookup.rl] = [parsedLine.value];
271
+ } else {
272
+ ref[fieldLookup.rl].push(parsedLine.value);
273
+ }
274
+ lastField = fieldLookup;
275
+ } else { // Simple key=val
276
+ ref[fieldLookup.rl] = parsedLine.value;
277
+ lastField = fieldLookup;
278
+ }
279
+ })
280
+
281
+ // Post processing {{{
282
+ // Apply field replacement / reformat rules
283
+ if (settings.fieldsReplace?.length > 0)
284
+ settings.fieldsReplace.forEach(replacement => {
285
+ let newVal = replacement.from ? ref[replacement.from] : null;
286
+
287
+ // Apply reformat if we have one
288
+ if (replacement.reformat) {
289
+ newVal = replacement.reformat(newVal, ref);
290
+ if (newVal === false) return; // Skip boolean false
291
+ }
292
+
293
+ // Copy field 'from' -> 'to'
294
+ ref[replacement.to] = newVal;
295
+
296
+ // Delete 'from' field
297
+ if (replacement.from && (replacement.delete ?? true))
298
+ delete ref[replacement.from];
299
+ })
300
+ // }}}
301
+
302
+ return ref;
303
+ }
304
+
305
+
306
+ /**
307
+ * Lookup tables for this module
308
+ * @type {Object}
309
+ * @property {array<Object>} fields Field translations between Reflib (`rl`) and the raw format (`raw`)
310
+ * @property {array<Object>} types Field translations between Reflib (`rl`) and the raw format types as raw text (`rawText`) and numeric ID (`rawId`)
311
+ * @property {boolean} isArray Whether the field should append to any existing `rl` field and be treated like an array of data
312
+ * @property {number|boolean} [sort] Sort order when outputting, use boolean `false` to disable the field on output
313
+ * @property {boolean} [outputSkip=false] Dont output this field at all
314
+ * @property {boolean} [outputRepeat=false] Whether to repeat the output field if multiple values are present, if disabled arrays are flattened into a string with newlines instead
315
+ * @property {boolean} [inputArray=false] Forcably cast the field as an array when reading, even if there is only one value
316
+ * @property {boolean} [trimDotSuffix=false] Remove any trailing dot character IF the input string spans multiple lines
317
+ */
318
+ export let translations = {
319
+ // Field translations {{{
320
+ fields: {
321
+ collection: [
322
+ // Based on the spec at https://www.nlm.nih.gov/bsd/mms/medlineelements.html
323
+ // Any field beginning with `medline*` is non-standard but included to prevent data loss when converted back to Medline format
324
+ {rl: 'medlinePMID', raw: 'PMID', sort: 0},
325
+ {rl: 'medlineOwner', raw: 'OWN', sort: 1},
326
+ {rl: 'medlineStatus', raw: 'STAT', sort: 2},
327
+ {rl: 'medlineDateRevised', raw: 'DR', sort: 3},
328
+ {rl: 'medlineISSN', raw: 'IS', outputRepeat: true, inputArray: true, sort: 4},
329
+ {rl: 'date', raw: 'DP', sort: 5},
330
+ {rl: 'title', raw: 'TI', sort: 6, trimDotSuffix: true},
331
+ {rl: 'doi', raw: 'LID', sort: 7},
332
+ {rl: 'abstract', raw: 'AB', sort: 8},
333
+ {rl: 'medlineCopyright', raw: 'CI', sort: 7},
334
+
335
+ // NOTE: Authors get special treatment when formatting so all these fields are skipped
336
+ {rl: 'medlineAuthorsFull', raw: 'FAU', sort: 10, outputSkip: true, inputArray: true},
337
+ {rl: 'medlineAuthorsShort', raw: 'AU', sort: 10, outputSkip: true, inputArray: true},
338
+ {rl: 'medlineAuthorsAffiliation', raw: 'AD', sort: 10, outputSkip: true, inputArray: true},
339
+ {rl: 'medlineAuthorsId', raw: 'AUID', sort: 10, outputSkip: true, inputArray: true},
340
+
341
+ {rl: 'language', raw: 'LA', sort: 11},
342
+ {rl: 'medlineGrantNumber', raw: 'GR', sort: 12},
343
+ {rl: 'type', raw: 'PT', sort: 14},
344
+ {rl: 'medlineTypeSecondary', raw: 'PTX'}, // Populated with any other write to PT field as array
345
+ {rl: 'medlineDateElectronicPublication', raw: 'DEP', sort: 15},
346
+ {rl: 'address', raw: 'PL', sort: 16},
347
+ {rl: 'medlineJournalShort', raw: 'TA', sort: 17},
348
+ {rl: 'medlineJournalFull', raw: 'JT', sort: 18},
349
+ {rl: 'medlineNLMID', raw: 'JID', sort: 19},
350
+ {rl: 'medlineSubset', raw: 'SB', sort: 20},
351
+ {rl: 'medlineOwnerOtherTerm', raw: 'OTO', sort: 21},
352
+ {rl: 'keywords', raw: 'OT', outputRepeat: true, inputArray: true, sort: 22},
353
+ {rl: 'medlineEntrezDate', raw: 'EDAT', sort: 23},
354
+ {rl: 'medlineDateMesh', raw: 'MHDA', sort: 24},
355
+ {rl: 'medlinePublicationHistoryStatus', raw: 'PHST', outputRepeat: true, inputArray: true, sort: 25},
356
+ {rl: 'medlineArticleID', raw: 'AID', sort: 26},
357
+ {rl: 'medlineStatusPublication', raw: 'PST', sort: 27},
358
+ {rl: 'medlineSource', raw: 'SO', sort: 27},
359
+ {rl: 'notes', raw: 'GN', inputArray: true, outputRepeat: true, sort: 28},
360
+
361
+ {rl: 'isbn', raw: 'ISBN'},
362
+ {rl: 'volume', raw: 'VI'},
363
+ {rl: 'medlineVolumeTitle', raw: 'VTI'},
364
+ {rl: 'pages', raw: 'PG'},
365
+ {rl: 'medlineInvestigatorAffiliation', raw: 'IRAD'},
366
+ {rl: 'medlineInvestigatorName', raw: 'IR'},
367
+ {rl: 'medlineInvestigatorNameFull', raw: 'FIR'},
368
+ {rl: 'medlineTitleBook', raw: 'BTI'},
369
+ {rl: 'medlineTitleCollection', raw: 'CTI'},
370
+ {rl: 'medlineConflictOfInterestStatement', raw: 'COIS'},
371
+ {rl: 'medlineCorporateAuthor', raw: 'CN'},
372
+ {rl: 'medlineDateCreate', raw: 'CRDT'},
373
+ {rl: 'medlineDateCreated', raw: 'DA'},
374
+ {rl: 'medlineDateCompleted', raw: 'DCOM'},
375
+ {rl: 'medlineEdition', raw: 'EN'},
376
+ {rl: 'medlineEditor', raw: 'ED'},
377
+ {rl: 'medlineEditorFull', raw: 'FED'},
378
+ {rl: 'medlineGeneSymbol', raw: 'GS'},
379
+ {rl: 'medlineISSN', raw: 'IS'},
380
+ {rl: 'number', raw: 'IP'},
381
+ {rl: 'medlineManuscriptID', raw: 'MID'},
382
+ {rl: 'medlineMeshTerms', raw: 'MH', outputRepeat: true, inputArray: true},
383
+ {rl: 'medlineReferenceCount', raw: 'RF'},
384
+ {rl: 'medlineAbstractOther', raw: 'OAB'},
385
+ {rl: 'medlineCopyrightOther', raw: 'OCI'},
386
+ {rl: 'medlineIDOther', raw: 'OID'},
387
+ {rl: 'medlinePersonalName', raw: 'PS'},
388
+ {rl: 'medlinePersonalNameFull', raw: 'FPS'},
389
+ {rl: 'medlinePublishingModel', raw: 'PUBM'},
390
+ {rl: 'medlinePubMedCentralID', raw: 'PMC'},
391
+ {rl: 'medlinePubMedCentralRelease', raw: 'PMCR'},
392
+ {rl: 'medlineRegistryNumber', raw: 'RN'},
393
+ {rl: 'medlineSubstanceName', raw: 'NM'},
394
+ {rl: 'medlineSecondarySource', raw: 'SI'},
395
+ {rl: 'medlineSpaceFlightMission', raw: 'SFM'},
396
+ {rl: 'medlineSubset', raw: 'SB'},
397
+ {rl: 'medlineTitleTransliterated', raw: 'TT', sort: 6},
398
+ ],
399
+ collectionOutput: [], // Sorted + filtered version of the above to use when outputting
400
+ rawMap: new Map(), // Calculated later for quicker lookup
401
+ rlMap: new Map(), // Calculated later for quicker lookup
402
+ },
403
+ // }}}
404
+ // Ref type translations {{{
405
+ types: {
406
+ collection: [
407
+ // Formats we support as a translation (or near-enough translation)
408
+ // Note that the preferred translation should be first
409
+
410
+ // High priority translations
411
+ {raw: 'Blog', rl: 'blog'},
412
+ {raw: 'Case Reports', rl: 'case'},
413
+ {raw: 'Catalog', rl: 'catalog'},
414
+ {raw: 'Chart', rl: 'chartOrTable'},
415
+ {raw: 'Database', rl: 'aggregatedDatabase'},
416
+ {raw: 'Dataset', rl: 'dataset'},
417
+ {raw: 'Dictionary', rl: 'dictionary'},
418
+ {raw: 'Encyclopedia', rl: 'encyclopedia'},
419
+ {raw: 'Journal Article', rl: 'journalArticle'},
420
+ {raw: 'Legal Case', rl: 'legalRuleOrRegulation'},
421
+ {raw: 'Manuscript', rl: 'manuscript'},
422
+ {raw: 'Map', rl: 'map'},
423
+ {raw: 'Newspaper Article', rl: 'newspaperArticle'},
424
+ {raw: 'Patent', rl: 'patent'},
425
+ {raw: 'Preprint', rl: 'unpublished'},
426
+ {raw: 'Tables', rl: 'chartOrTable'},
427
+ {raw: 'Technical Report', rl: 'report'},
428
+ {raw: 'unknown', rl: 'unknown'},
429
+ {raw: 'Unpublished Work', rl: 'unpublished'},
430
+ {raw: 'Video-Audio Media', rl: 'audioVisualMaterial'},
431
+ {raw: 'Web Archive', rl: 'web'},
432
+
433
+ // Lower priority translations
434
+ {raw: 'Address', rl: 'personalCommunication'},
435
+ {raw: 'Advertisement', rl: 'audioVisualMaterial'},
436
+ {raw: 'Almanac', rl: 'book'},
437
+ {raw: 'Anecdotes', rl: 'blog'},
438
+ {raw: 'Animation', rl: 'filmOrBroadcast'},
439
+ {raw: 'Annual Report', rl: 'report'},
440
+ {raw: 'Aphorisms and Proverbs', rl: 'pamphlet'},
441
+ {raw: 'Architectural Drawing', rl: 'figure'},
442
+ {raw: 'Autobiography', rl: 'book'},
443
+ {raw: 'Bibliography', rl: 'catalog'},
444
+ {raw: 'Biobibliography', rl: 'book'},
445
+ {raw: 'Biography', rl: 'book'},
446
+ {raw: 'Book Illustrations', rl: 'audioVisualMaterial'},
447
+ {raw: 'Book Review', rl: 'magazineArticle'},
448
+ {raw: 'Caricature', rl: 'artwork'},
449
+ {raw: 'Cartoon', rl: 'audioVisualMaterial'},
450
+ {raw: 'Catalog, Bookseller', rl: 'catalog'},
451
+ {raw: 'Catalog, Commercial', rl: 'catalog'},
452
+ {raw: 'Catalog, Drug', rl: 'catalog'},
453
+ {raw: 'Catalog, Publisher', rl: 'catalog'},
454
+ {raw: 'Catalog, Union', rl: 'catalog'},
455
+ {raw: 'Chronology', rl: 'book'},
456
+ {raw: 'Classical Article', rl: 'classicalWork'},
457
+ {raw: 'Clinical Conference', rl: 'conferenceProceedings'},
458
+ {raw: 'Clinical Study', rl: 'dataset'},
459
+ {raw: 'Clinical Trial, Phase III', rl: 'dataset'},
460
+ {raw: 'Clinical Trial, Phase II', rl: 'dataset'},
461
+ {raw: 'Clinical Trial, Phase I', rl: 'dataset'},
462
+ {raw: 'Clinical Trial, Phase IV', rl: 'dataset'},
463
+ {raw: 'Clinical Trial Protocol', rl: 'dataset'},
464
+ {raw: 'Clinical Trial', rl: 'dataset'},
465
+ {raw: 'Clinical Trial, Veterinary', rl: 'dataset'},
466
+ {raw: 'Consensus Development Conference, NIH', rl: 'conferenceProceedings'},
467
+ {raw: 'Consensus Development Conference', rl: 'conferenceProceedings'},
468
+ {raw: 'Corrected and Republished Article', rl: 'journalArticle'},
469
+ {raw: 'Database', rl: 'onlineDatabase'},
470
+ {raw: 'Dictionary, Chemical', rl: 'dictionary'},
471
+ {raw: 'Dictionary, Classical', rl: 'dictionary'},
472
+ {raw: 'Dictionary, Dental', rl: 'dictionary'},
473
+ {raw: 'Dictionary, Medical', rl: 'dictionary'},
474
+ {raw: 'Dictionary, Pharmaceutic', rl: 'dictionary'},
475
+ {raw: 'Dictionary, Polyglot', rl: 'dictionary'},
476
+ {raw: 'Documentaries and Factual Films', rl: 'filmOrBroadcast'},
477
+ {raw: 'Drawing', rl: 'audioVisualMaterial'},
478
+ {raw: 'Ephemera', rl: 'artwork'},
479
+ {raw: 'Eulogy', rl: 'personalCommunication'},
480
+ {raw: 'Formulary, Dental', rl: 'dataset'},
481
+ {raw: 'Formulary, Homeopathic', rl: 'dataset'},
482
+ {raw: 'Formulary, Hospital', rl: 'dataset'},
483
+ {raw: 'Formulary', rl: 'dataset'},
484
+ {raw: 'Funeral Sermon', rl: 'personalCommunication'},
485
+ {raw: 'Government Publication', rl: 'governmentDocument'},
486
+ {raw: 'Graphic Novel', rl: 'artwork'},
487
+ {raw: 'Historical Article', rl: 'ancientText'},
488
+ {raw: 'Incunabula', rl: 'ancientText'},
489
+ {raw: 'Incunabula', rl: 'ancientText'},
490
+ {raw: 'Instructional Film and Video', rl: 'filmOrBroadcast'},
491
+ {raw: 'Introductory Journal Article', rl: 'journalArticle'},
492
+ {raw: 'Legislation', rl: 'statute'},
493
+ {raw: 'Letter', rl: 'personalCommunication'},
494
+ {raw: 'Manuscript, Medical', rl: 'manuscript'},
495
+ {raw: 'Movable Books', rl: 'books'},
496
+ {raw: 'News', rl: 'newspaperArticle'},
497
+ {raw: 'Pharmacopoeia, Homeopathic', rl: 'book'},
498
+ {raw: 'Pharmacopoeia', rl: 'book'},
499
+ {raw: 'Photograph', rl: 'audioVisualMaterial'},
500
+ {raw: 'Pictorial Work', rl: 'audioVisualMaterial'},
501
+ {raw: 'Poetry', rl: 'artwork'},
502
+ {raw: 'Portrait', rl: 'artwork'},
503
+ {raw: 'Postcard', rl: 'audioVisualMaterial'},
504
+ {raw: 'Poster', rl: 'audioVisualMaterial'},
505
+ {raw: 'Public Service Announcement', rl: 'governmentDocument'},
506
+ {raw: 'Research Support, N.I.H., Extramural', rl: 'grant'},
507
+ {raw: 'Research Support, N.I.H., Intramural', rl: 'grant'},
508
+ {raw: 'Research Support, Non-U.S. Gov\'t', rl: 'grant'},
509
+ {raw: 'Research Support, U.S. Government', rl: 'grant'},
510
+ {raw: 'Research Support, U.S. Gov\'t, Non-P.H.S.', rl: 'grant'},
511
+ {raw: 'Research Support, U.S. Gov\'t, P.H.S.', rl: 'grant'},
512
+ {raw: 'Resource Guide', rl: 'standard'},
513
+ {raw: 'Retracted Publication', rl: 'journalArticle'},
514
+ {raw: 'Retraction of Publication', rl: 'journalArticle'},
515
+ {raw: 'Sermon', rl: 'journalArticle'},
516
+ {raw: 'Statistics', rl: 'chartOrTable'},
517
+ {raw: 'Study Guide', rl: 'standard'},
518
+ {raw: 'Terminology', rl: 'catalog'},
519
+ {raw: 'Textbook', rl: 'book'},
520
+ {raw: 'Unedited Footage', rl: 'audioVisualMaterial'},
521
+ {raw: 'Webcast', rl: 'web'},
522
+ {raw: 'Wit and Humor', rl: 'artwork'},
523
+
524
+
525
+ // Unsupported - Please map these and submit a PR if you think something obvious is mising
526
+ {rl: '', raw: 'Abbreviations'},
527
+ {rl: '', raw: 'Abstracts'},
528
+ {rl: '', raw: 'Academic Dissertation'},
529
+ {rl: '', raw: 'Account Book'},
530
+ {rl: '', raw: 'Adaptive Clinical Trial'},
531
+ {rl: '', raw: 'Atlas'},
532
+ {rl: '', raw: 'Bookplate'},
533
+ {rl: '', raw: 'Broadside'},
534
+ {rl: '', raw: 'Calendar'},
535
+ {rl: '', raw: 'Collected Correspondence'},
536
+ {rl: '', raw: 'Collected Work'},
537
+ {rl: '', raw: 'Collection'},
538
+ {rl: '', raw: 'Comment'},
539
+ {rl: '', raw: 'Comparative Study'},
540
+ {rl: '', raw: 'Congress'},
541
+ {rl: '', raw: 'Controlled Clinical Trial'},
542
+ {rl: '', raw: 'Cookbook'},
543
+ {rl: '', raw: 'Diary'},
544
+ {rl: '', raw: 'Directory'},
545
+ {rl: '', raw: 'Dispensatory'},
546
+ {rl: '', raw: 'Duplicate Publication'},
547
+ {rl: '', raw: 'Editorial'},
548
+ {rl: '', raw: 'Electronic Supplementary Materials'},
549
+ {rl: '', raw: 'English Abstract'},
550
+ {rl: '', raw: 'Equivalence Trial'},
551
+ {rl: '', raw: 'Essay'},
552
+ {rl: '', raw: 'Evaluation Study'},
553
+ {rl: '', raw: 'Examination Questions'},
554
+ {rl: '', raw: 'Exhibition'},
555
+ {rl: '', raw: 'Expression of Concern'},
556
+ {rl: '', raw: 'Festschrift'},
557
+ {rl: '', raw: 'Fictional Work'},
558
+ {rl: '', raw: 'Form'},
559
+ {rl: '', raw: 'legalRuleOrRegulation'},
560
+ {rl: '', raw: 'Guidebook'},
561
+ {rl: '', raw: 'Guideline'},
562
+ {rl: '', raw: 'Handbook'},
563
+ {rl: '', raw: 'Herbal'},
564
+ {rl: '', raw: 'Index'},
565
+ {rl: '', raw: 'Interactive Tutorial'},
566
+ {rl: '', raw: 'Interview'},
567
+ {rl: '', raw: 'Juvenile Literature'},
568
+ {rl: '', raw: 'Laboratory Manual'},
569
+ {rl: '', raw: 'Lecture Note'},
570
+ {rl: '', raw: 'Meeting Abstract'},
571
+ {rl: '', raw: 'Meta-Analysis'},
572
+ {rl: '', raw: 'Monograph'},
573
+ {rl: '', raw: 'Multicenter Study'},
574
+ {rl: '', raw: 'Nurses Instruction'},
575
+ {rl: '', raw: 'Observational Study'},
576
+ {rl: '', raw: 'Observational Study, Veterinary'},
577
+ {rl: '', raw: 'Outline'},
578
+ {rl: '', raw: 'Overall'},
579
+ {rl: '', raw: 'Patient Education Handout'},
580
+ {rl: '', raw: 'Periodical'},
581
+ {rl: '', raw: 'Periodical Index'},
582
+ {rl: '', raw: 'Personal Narrative'},
583
+ {rl: '', raw: 'Phrases'},
584
+ {rl: '', raw: 'Popular Work'},
585
+ {rl: '', raw: 'Practice Guideline'},
586
+ {rl: '', raw: 'Pragmatic Clinical Trial'},
587
+ {rl: '', raw: 'Price List'},
588
+ {rl: '', raw: 'Problems and Exercises'},
589
+ {rl: '', raw: 'Program'},
590
+ {rl: '', raw: 'Programmed Instruction'},
591
+ {rl: '', raw: 'Prospectus'},
592
+ {rl: '', raw: 'Publication Components'},
593
+ {rl: '', raw: 'Publication Formats'},
594
+ {rl: '', raw: 'Published Erratum'},
595
+ {rl: '', raw: 'Randomized Controlled Trial'},
596
+ {rl: '', raw: 'Randomized Controlled Trial, Veterinary'},
597
+ {rl: '', raw: 'Research Support, American Recovery and Reinvestment Act'},
598
+ {rl: '', raw: 'Review'},
599
+ {rl: '', raw: 'Scientific Integrity Review'},
600
+ {rl: '', raw: 'Study Characteristics'},
601
+ {rl: '', raw: 'Support of Research'},
602
+ {rl: '', raw: 'Systematic Review'},
603
+ {rl: '', raw: 'Twin Study'},
604
+ {rl: '', raw: 'Union List'},
605
+ {rl: '', raw: 'Validation Study'},
606
+ ],
607
+ rawMap: new Map(), // Calculated later for quicker lookup
608
+ rlMap: new Map(), // Calculated later for quicker lookup
609
+ },
610
+ // }}}
611
+ };
612
+
613
+
614
+ /**
615
+ * @see modules/interface.js
616
+ */
617
+ export function setup() {
618
+ // Sort the field set by sort field
619
+ translations.fields.collectionOutput = translations.fields.collection
620
+ .filter(f => f.sort !== false)
621
+ .sort((a, b) => (a.sort ?? 1000) == (b.sort ?? 1000) ? 0
622
+ : (a.sort ?? 1000) < (b.sort ?? 1000) ? -1
623
+ : 1
624
+ )
625
+
626
+ // Create lookup object of translations.fields with key as .rl / val as the full object
627
+ translations.fields.collection.forEach(c => {
628
+ translations.fields.rlMap.set(c.rl, c);
629
+ translations.fields.rawMap.set(c.raw, c);
630
+ });
631
+
632
+ // Create lookup object of ref.types with key as .rl / val as the full object
633
+ translations.types.collection.forEach(c => {
634
+ translations.types.rlMap.set(c.rl, c);
635
+ translations.types.rawMap.set(c.raw, c);
636
+ });
637
+
638
+ }