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