@iebh/reflib 2.8.0 → 2.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/modules/ris.js CHANGED
@@ -1,383 +1,383 @@
1
- import Emitter from '../shared/emitter.js';
2
-
3
- /**
4
- * Parse a RIS file from a readable stream
5
- *
6
- * @see modules/interface.js
7
- *
8
- * @param {Stream} stream The readable stream to accept data from
9
- * @param {Object} [options] Additional options to use when parsing
10
- * @param {string} [options.defaultType='report'] Default citation type to assume when no other type is specified
11
- * @param {string} [options.delimeter='\r'] How to split multi-line items
12
- * @param {Boolean} [options.convertAbstract=true] If the `fallbackAbstract` field exists but not `abstract` use the former as the latter
13
- * @param {Boolean} [options.convertCity=true] If the `fallbackCity` field exists but not `city` use the former as the latter
14
- *
15
- * @returns {Object} A readable stream analogue defined in `modules/interface.js`
16
- */
17
- export function readStream(stream, options) {
18
- let settings = {
19
- defaultType: 'journalArticle',
20
- delimeter: '\r',
21
- convertAbtract: true,
22
- convertCity: true,
23
- ...options,
24
- };
25
-
26
- let emitter = Emitter();
27
-
28
- let buffer = ''; // Incomming text buffer lines if the chunk we're given isn't enough to parse a reference yet
29
-
30
- // Queue up the parser in the next tick (so we can return the emitter first)
31
- setTimeout(()=> {
32
- stream
33
- .on('data', chunkBuffer => {
34
- emitter.emit('progress', stream.bytesRead);
35
- buffer += chunkBuffer.toString(); // Append incomming data to the partial-buffer we're holding in memory
36
-
37
- 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
38
- let bufferSplitter = /(\r\n|\n)ER\s+-\s*(\r\n|\n)/g; // RegExp to use per segment (multiple calls to .exec() stores state because JS is a hellscape)
39
-
40
- let bufferSegment;
41
- while (bufferSegment = bufferSplitter.exec(buffer)) {
42
- let parsedRef = parseRef(buffer.substring(bufferCrop, bufferSegment.index), settings); // Parse the ref from the start+end points
43
-
44
- emitter.emit('ref', parsedRef);
45
-
46
- bufferCrop = bufferSegment.index + bufferSegment[0].length; // Set start of next ref + cropping index to last seen offset + match
47
- }
48
-
49
- buffer = buffer.substring(bufferCrop); // Shift-truncate the buffer so we're ready to input more data on the next call
50
- })
51
- .on('error', e => emitter.emit('error', e))
52
- .on('end', ()=> {
53
- if (buffer.replace(/\s+/, '')) { // Anything left in the to-drain buffer?
54
- // Drain remaining buffer into parser before exiting
55
- emitter.emit('ref', parseRef(buffer, settings));
56
- }
57
-
58
- // Signal that we're done
59
- emitter.emit('end');
60
- })
61
- })
62
-
63
- return emitter;
64
- }
65
-
66
-
67
- /**
68
- * Write a RIS file to a writable stream
69
- *
70
- * @see modules/interface.js
71
- *
72
- * @param {Stream} stream The writable stream to write to
73
- *
74
- * @param {Object} [options] Additional options to use when parsing
75
- * @param {string} [options.defaultType='journalArticle'] Default citation type to assume when no other type is specified
76
- * @param {string} [options.delimeter='\r'] How to split multi-line items
77
- *
78
- * @returns {Object} A writable stream analogue defined in `modules/interface.js`
79
- */
80
- export function writeStream(stream, options) {
81
- let settings = {
82
- defaultType: 'journalArticle',
83
- delimeter: '\r',
84
- ...options,
85
- };
86
-
87
-
88
- return {
89
- start() {
90
- return Promise.resolve();
91
- },
92
- write: xRef => {
93
- let ref = { // Assign defaults if not already present
94
- type: settings.defaultType,
95
- title: '<NO TITLE>',
96
- ...xRef,
97
- };
98
-
99
- // Parse `pages` back into `_pageStart` + `_pageEnd` meta keys
100
- if (xRef.pages) {
101
- var pageRanges = /^(?<_pageStart>.+?)(-(?<_pageEnd>.+))?$/.exec(xRef.pages)?.groups;
102
- Object.assign(ref, pageRanges);
103
- delete ref.pages;
104
- }
105
-
106
- stream.write(
107
- translations.fields.collectionOutput
108
- .filter(f => ref[f.rl]) // Has field?
109
- .flatMap(f =>
110
- f.rl == 'type' // Translate type field
111
- ? 'TY - ' + (translations.types.rlMap[ref.type] || 'JOUR')
112
- : f.outputRepeat && Array.isArray(ref[f.rl]) // Repeat array types
113
- ? ref[f.rl].map(item => `${f.raw} - ${item}`)
114
- : Array.isArray(ref[f.rl]) // Flatten arrays into text
115
- ? `${f.raw} - ${ref[f.rl].join(settings.delimeter)}`
116
- : `${f.raw} - ${ref[f.rl]}` // Regular field output
117
- )
118
- .concat(['ER - \n\n'])
119
- .join('\n')
120
- );
121
-
122
- return Promise.resolve();
123
- },
124
- end() {
125
- return new Promise((resolve, reject) =>
126
- stream.end(err => err ? reject(err) : resolve())
127
- );
128
- },
129
- };
130
- }
131
-
132
-
133
- /**
134
- * Parse a single RIS format reference from a block of text
135
- * This function is used internally by parseStream() for each individual reference
136
- *
137
- * @param {string} refString Raw RIS string composing the start -> end of the ref
138
- * @param {Object} settings Additional settings to pass, this should be initialized + parsed by the calling function for efficiency, see readStream() for full spec
139
- * @param {Boolean} [settings.convertAbstract=true] If the `fallbackAbstract` field exists but not `abstract` use the former as the latter
140
- * @returns {ReflibRef} The parsed reference
141
- */
142
- export function parseRef(refString, settings) {
143
- let ref = {}; // Reference under construction
144
- let lastField; // Last field object we saw, used to append values if they don't match the default RIS key=val one-liner
145
-
146
- refString
147
- .split(/[\r\n|\n]/) // Split into lines
148
- .forEach(line => {
149
- let parsedLine = /^\s*(?<key>[A-Z0-9]+?)\s+-\s+(?<value>.*)$/s.exec(line)?.groups;
150
- if (!parsedLine) { // Doesn't match key=val spec
151
- if (line.replace(/\s+/, '') && lastField) { // Line isn't just whitespace + We have a field to append to - append with \r delimiters
152
- if (lastField.inputArray) { // Treat each line feed like an array entry
153
- ref[lastField.rl].push(line);
154
- } else { // Assume we append each line entry as a string with settings.delimeter
155
- ref[lastField.rl] += settings.delimeter + line;
156
- }
157
- }
158
- return; // Stop processing this line
159
- }
160
-
161
- if (parsedLine.key == 'ER') return; // Skip 'ER' defiition lines - this is probably due to the buffer draining
162
- let fieldLookup = translations.fields.rawMap.get(parsedLine.key);
163
-
164
- if (!fieldLookup) { // Skip unknown field translations
165
- lastField = null;
166
- return;
167
- } else if (fieldLookup.rl == 'type') { // Special handling for ref types
168
- ref[fieldLookup.rl] = translations.types.rawMap.get(parsedLine.value)?.rl || settings.defaultType;
169
- lastField = fieldLookup; // Track last key so we can append to it on the next cycle
170
- } else if (fieldLookup.inputArray) { // Should this `rl` key be treated like an appendable array?
171
- if (!ref[fieldLookup.rl] || !Array.isArray(ref[fieldLookup.rl])) { // Array doesn't exist yet
172
- ref[fieldLookup.rl] = [parsedLine.value];
173
- } else {
174
- ref[fieldLookup.rl].push(parsedLine.value);
175
- }
176
- lastField = fieldLookup;
177
- } else { // Simple key=val
178
- ref[fieldLookup.rl] = parsedLine.value;
179
- lastField = fieldLookup;
180
- }
181
- })
182
-
183
- // Post processing
184
- // Page mangling {{{
185
- if (ref._pageStart || ref._pageEnd) {
186
- ref.pages = [ref._pageStart, ref._pageEnd]
187
- .filter(Boolean) // Remove duds
188
- .join('-');
189
- delete ref._pageStart;
190
- delete ref._pageEnd;
191
- }
192
- // }}}
193
- // FallbackAbstract -> Abstract (if the latter is missing) {{{
194
- if ((settings.fallbackAbstract ?? true) && ref.fallbackAbstract && !ref.abstract) {
195
- ref.abstract = ref.fallbackAbstract;
196
- delete ref.fallbackAbstract;
197
- }
198
- // }}}
199
- // FallbackCity -> City (if the latter is missing) {{{
200
- if ((settings.fallbackCity ?? true) && ref.fallbackCity && !ref.abstract) {
201
- ref.city = ref.fallbackCity;
202
- delete ref.fallbackCity;
203
- }
204
- // }}}
205
-
206
- return ref;
207
- }
208
-
209
-
210
- /**
211
- * Lookup tables for this module
212
- * @type {Object}
213
- * @property {array<Object>} fields Field translations between Reflib (`rl`) and the raw format (`raw`)
214
- * @property {array<Object>} types Field translations between Reflib (`rl`) and the raw format types as raw text (`rawText`) and numeric ID (`rawId`)
215
- * @property {boolean} isArray Whether the field should append to any existing `rl` field and be treated like an array of data
216
- * @property {number|boolean} [sort] Sort order when outputting, use boolean `false` to disable the field on output
217
- * @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
218
- * @property {boolean} [inputArray=false] Forcably cast the field as an array when reading, even if there is only one value
219
- */
220
- export let translations = {
221
- // Field translations {{{
222
- fields: {
223
- collection: [
224
- { rl: "authors", raw: "A1", sort: false, inputArray: true },
225
- { rl: "authors", raw: "A2", sort: false, inputArray: true },
226
- { rl: "authors", raw: "A3", sort: false, inputArray: true },
227
- { rl: "authors", raw: "A4", sort: false, inputArray: true },
228
- { rl: "abstract", raw: "AB" },
229
- { rl: "address", raw: "AD" },
230
- { rl: "accessionNum", raw: "AN" },
231
- {
232
- rl: "authors",
233
- raw: "AU",
234
- sort: 4,
235
- outputRepeat: true,
236
- inputArray: true,
237
- },
238
- { rl: "custom1", raw: "C1" },
239
- { rl: "custom2", raw: "C2" },
240
- { rl: "custom3", raw: "C3" },
241
- { rl: "custom4", raw: "C4" },
242
- { rl: "custom5", raw: "C5" },
243
- { rl: "custom6", raw: "C6" },
244
- { rl: "custom7", raw: "C7" },
245
- { rl: "custom8", raw: "C8" },
246
- { rl: "caption", raw: "CA" },
247
- { rl: "fallbackCity", raw: "CY" },
248
- { rl: "date", raw: "DA" },
249
- { rl: "database", raw: "DB" },
250
- { rl: "doi", raw: "DO" },
251
- { rl: "databaseProvider", raw: "DP" },
252
- { rl: "_pageEnd", raw: "EP", sort: 6 },
253
- { rl: "edition", raw: "ET", sort: 7 },
254
- { rl: "number", raw: "IS", sort: 8 },
255
- { rl: "journal", raw: "J1", sort: false },
256
- { rl: "journal", raw: "JF", sort: 3 },
257
- { rl: "keywords", raw: "KW", outputRepeat: true, inputArray: true },
258
- { rl: "urls", raw: "L1", sort: false, inputArray: true },
259
- { rl: "urls", raw: "L2", sort: false, inputArray: true },
260
- { rl: "urls", raw: "L3", sort: false, inputArray: true },
261
- { rl: "urls", raw: "L4", sort: false, inputArray: true },
262
- { rl: "language", raw: "LA" },
263
- { rl: "label", raw: "LB" },
264
- { rl: "urls", raw: "LK", sort: false, inputArray: true },
265
- { rl: "notes", raw: "N1" },
266
- { rl: "fallbackAbstract", raw: "N2" },
267
- { rl: "publisher", raw: "PB" },
268
- { rl: "year", raw: "PY" },
269
- { rl: "isbn", raw: "SN" },
270
- { rl: "_pageStart", raw: "SP", sort: 5 },
271
- { rl: "title", raw: "T1", sort: false },
272
- { rl: "journal", raw: "T2", sort: false },
273
- { rl: "title", raw: "TI", sort: 1 },
274
- { rl: "type", raw: "TY", sort: 0 }, // TY must be the lowest number
275
- { rl: "urls", raw: "UR", outputRepeat: true, inputArray: true },
276
- { rl: "volume", raw: "VL" },
277
- { rl: "date", raw: "Y1" },
278
- { rl: "accessDate", raw: "Y2" },
279
-
280
- // These are non-standard fields but we keep these here anyway to prevent data loss
281
- { rl: "RISID", raw: "ID" },
282
- { rl: "RISShortTitle", raw: "ST" },
283
- { rl: "RISOriginalPublication", raw: "OP" },
284
- ],
285
- collectionOutput: [], // Sorted + filtered version of the above to use when outputting
286
- rawMap: new Map(), // Calculated later for quicker lookup
287
- rlMap: new Map(), // Calculated later for quicker lookup
288
- },
289
- // }}}
290
- // Ref type translations {{{
291
- types: {
292
- collection: [
293
- // Place high-priority translations at the top (when we translate BACK we need to know which of multiple keys to prioritize)
294
- { rl: "audioVisualMaterial", raw: "ADVS" },
295
- { rl: "journalArticle", raw: "JOUR" },
296
- { rl: "personalCommunication", raw: "PCOMM" },
297
- { rl: "filmOrBroadcast", raw: "VIDEO" },
298
-
299
- // Low priority below this line
300
- { rl: "unknown", raw: "ABST" },
301
- { rl: "aggregatedDatabase", raw: "AGGR" },
302
- { rl: "ancientText", raw: "ANCIENT" },
303
- { rl: "artwork", raw: "ART" },
304
- { rl: "bill", raw: "BILL" },
305
- { rl: "blog", raw: "BLOG" },
306
- { rl: "book", raw: "BOOK" },
307
- { rl: "case", raw: "CASE" },
308
- { rl: "bookSection", raw: "CHAP" },
309
- { rl: "chartOrTable", raw: "CHART" },
310
- { rl: "classicalWork", raw: "CLSWK" },
311
- { rl: "computerProgram", raw: "COMP" },
312
- { rl: "conferenceProceedings", raw: "CONF" },
313
- { rl: "conferencePaper", raw: "CPAPER" },
314
- { rl: "catalog", raw: "CTLG" },
315
- { rl: "dataset", raw: "DATA" },
316
- { rl: "onlineDatabase", raw: "DBASE" },
317
- { rl: "dictionary", raw: "DICT" },
318
- { rl: "electronicBook", raw: "EBOOK" },
319
- { rl: "electronicBookSection", raw: "ECHAP" },
320
- { rl: "editedBook", raw: "EDBOOK" },
321
- { rl: "electronicArticle", raw: "EJOUR" },
322
- { rl: "web", raw: "ELEC" },
323
- { rl: "encyclopedia", raw: "ENCYC" },
324
- { rl: "equation", raw: "EQUA" },
325
- { rl: "figure", raw: "FIGURE" },
326
- { rl: "generic", raw: "GEN" },
327
- { rl: "governmentDocument", raw: "GOVDOC" },
328
- { rl: "grant", raw: "GRANT" },
329
- { rl: "hearing", raw: "HEARING" },
330
- { rl: "personalCommunication", raw: "ICOMM" },
331
- { rl: "newspaperArticle", raw: "INPR" },
332
- { rl: "journalArticle", raw: "JFULL" },
333
- { rl: "legalRuleOrRegulation", raw: "LEGAL" },
334
- { rl: "manuscript", raw: "MANSCPT" },
335
- { rl: "map", raw: "MAP" },
336
- { rl: "magazineArticle", raw: "MGZN" },
337
- { rl: "filmOrBroadcast", raw: "MPCT" },
338
- { rl: "onlineMultimedia", raw: "MULTI" },
339
- { rl: "music", raw: "MUSIC" },
340
- { rl: "newspaperArticle", raw: "NEWS" },
341
- { rl: "pamphlet", raw: "PAMP" },
342
- { rl: "patent", raw: "PAT" },
343
- { rl: "report", raw: "RPRT" },
344
- { rl: "serial", raw: "SER" },
345
- { rl: "audioVisualMaterial", raw: "SLIDE" },
346
- { rl: "audioVisualMaterial", raw: "SOUND" },
347
- { rl: "standard", raw: "STAND" },
348
- { rl: "statute", raw: "STAT" },
349
- { rl: "thesis", raw: "THES" },
350
- { rl: "unpublished", raw: "UNPB" },
351
- ],
352
- rawMap: new Map(), // Calculated later for quicker lookup
353
- rlMap: new Map(), // Calculated later for quicker lookup
354
- },
355
- // }}}
356
- };
357
-
358
-
359
- /**
360
- * @see modules/interface.js
361
- */
362
- export function setup() {
363
- // Sort the field set by sort field
364
- translations.fields.collectionOutput = translations.fields.collection
365
- .filter(f => f.sort !== false)
366
- .sort((a, b) => (a.sort ?? 1000) == (b.sort ?? 1000) ? 0
367
- : (a.sort ?? 1000) < (b.sort ?? 1000) ? -1
368
- : 1
369
- )
370
-
371
- // Create lookup object of translations.fields with key as .rl / val as the full object
372
- translations.fields.collection.forEach(c => {
373
- translations.fields.rlMap.set(c.rl, c);
374
- translations.fields.rawMap.set(c.raw, c);
375
- });
376
-
377
- // Create lookup object of ref.types with key as .rl / val as the full object
378
- translations.types.collection.forEach(c => {
379
- translations.types.rlMap.set(c.rl, c);
380
- translations.types.rawMap.set(c.raw, c);
381
- });
382
-
383
- }
1
+ import Emitter from '../shared/emitter.js';
2
+
3
+ /**
4
+ * Parse a RIS file from a readable stream
5
+ *
6
+ * @see modules/interface.js
7
+ *
8
+ * @param {Stream} stream The readable stream to accept data from
9
+ * @param {Object} [options] Additional options to use when parsing
10
+ * @param {string} [options.defaultType='report'] Default citation type to assume when no other type is specified
11
+ * @param {string} [options.delimeter='\r'] How to split multi-line items
12
+ * @param {Boolean} [options.convertAbstract=true] If the `fallbackAbstract` field exists but not `abstract` use the former as the latter
13
+ * @param {Boolean} [options.convertCity=true] If the `fallbackCity` field exists but not `city` use the former as the latter
14
+ *
15
+ * @returns {Object} A readable stream analogue defined in `modules/interface.js`
16
+ */
17
+ export function readStream(stream, options) {
18
+ let settings = {
19
+ defaultType: 'journalArticle',
20
+ delimeter: '\r',
21
+ convertAbtract: true,
22
+ convertCity: true,
23
+ ...options,
24
+ };
25
+
26
+ let emitter = Emitter();
27
+
28
+ let buffer = ''; // Incomming text buffer lines if the chunk we're given isn't enough to parse a reference yet
29
+
30
+ // Queue up the parser in the next tick (so we can return the emitter first)
31
+ setTimeout(()=> {
32
+ stream
33
+ .on('data', chunkBuffer => {
34
+ emitter.emit('progress', stream.bytesRead);
35
+ buffer += chunkBuffer.toString(); // Append incomming data to the partial-buffer we're holding in memory
36
+
37
+ 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
38
+ let bufferSplitter = /(\r\n|\n)ER\s+-\s*(\r\n|\n)/g; // RegExp to use per segment (multiple calls to .exec() stores state because JS is a hellscape)
39
+
40
+ let bufferSegment;
41
+ while (bufferSegment = bufferSplitter.exec(buffer)) {
42
+ let parsedRef = parseRef(buffer.substring(bufferCrop, bufferSegment.index), settings); // Parse the ref from the start+end points
43
+
44
+ emitter.emit('ref', parsedRef);
45
+
46
+ bufferCrop = bufferSegment.index + bufferSegment[0].length; // Set start of next ref + cropping index to last seen offset + match
47
+ }
48
+
49
+ buffer = buffer.substring(bufferCrop); // Shift-truncate the buffer so we're ready to input more data on the next call
50
+ })
51
+ .on('error', e => emitter.emit('error', e))
52
+ .on('end', ()=> {
53
+ if (buffer.replace(/\s+/, '')) { // Anything left in the to-drain buffer?
54
+ // Drain remaining buffer into parser before exiting
55
+ emitter.emit('ref', parseRef(buffer, settings));
56
+ }
57
+
58
+ // Signal that we're done
59
+ emitter.emit('end');
60
+ })
61
+ })
62
+
63
+ return emitter;
64
+ }
65
+
66
+
67
+ /**
68
+ * Write a RIS file to a writable stream
69
+ *
70
+ * @see modules/interface.js
71
+ *
72
+ * @param {Stream} stream The writable stream to write to
73
+ *
74
+ * @param {Object} [options] Additional options to use when parsing
75
+ * @param {string} [options.defaultType='journalArticle'] Default citation type to assume when no other type is specified
76
+ * @param {string} [options.delimeter='\r'] How to split multi-line items
77
+ *
78
+ * @returns {Object} A writable stream analogue defined in `modules/interface.js`
79
+ */
80
+ export function writeStream(stream, options) {
81
+ let settings = {
82
+ defaultType: 'journalArticle',
83
+ delimeter: '\r',
84
+ ...options,
85
+ };
86
+
87
+
88
+ return {
89
+ start() {
90
+ return Promise.resolve();
91
+ },
92
+ write: xRef => {
93
+ let ref = { // Assign defaults if not already present
94
+ type: settings.defaultType,
95
+ title: '<NO TITLE>',
96
+ ...xRef,
97
+ };
98
+
99
+ // Parse `pages` back into `_pageStart` + `_pageEnd` meta keys
100
+ if (xRef.pages) {
101
+ var pageRanges = /^(?<_pageStart>.+?)(-(?<_pageEnd>.+))?$/.exec(xRef.pages)?.groups;
102
+ Object.assign(ref, pageRanges);
103
+ delete ref.pages;
104
+ }
105
+
106
+ stream.write(
107
+ translations.fields.collectionOutput
108
+ .filter(f => ref[f.rl]) // Has field?
109
+ .flatMap(f =>
110
+ f.rl == 'type' // Translate type field
111
+ ? 'TY - ' + (translations.types.rlMap[ref.type] || 'JOUR')
112
+ : f.outputRepeat && Array.isArray(ref[f.rl]) // Repeat array types
113
+ ? ref[f.rl].map(item => `${f.raw} - ${item}`)
114
+ : Array.isArray(ref[f.rl]) // Flatten arrays into text
115
+ ? `${f.raw} - ${ref[f.rl].join(settings.delimeter)}`
116
+ : `${f.raw} - ${ref[f.rl]}` // Regular field output
117
+ )
118
+ .concat(['ER - \n\n'])
119
+ .join('\n')
120
+ );
121
+
122
+ return Promise.resolve();
123
+ },
124
+ end() {
125
+ return new Promise((resolve, reject) =>
126
+ stream.end(err => err ? reject(err) : resolve())
127
+ );
128
+ },
129
+ };
130
+ }
131
+
132
+
133
+ /**
134
+ * Parse a single RIS format reference from a block of text
135
+ * This function is used internally by parseStream() for each individual reference
136
+ *
137
+ * @param {string} refString Raw RIS string composing the start -> end of the ref
138
+ * @param {Object} settings Additional settings to pass, this should be initialized + parsed by the calling function for efficiency, see readStream() for full spec
139
+ * @param {Boolean} [settings.convertAbstract=true] If the `fallbackAbstract` field exists but not `abstract` use the former as the latter
140
+ * @returns {ReflibRef} The parsed reference
141
+ */
142
+ export function parseRef(refString, settings) {
143
+ let ref = {}; // Reference under construction
144
+ let lastField; // Last field object we saw, used to append values if they don't match the default RIS key=val one-liner
145
+
146
+ refString
147
+ .split(/[\r\n|\n]/) // Split into lines
148
+ .forEach(line => {
149
+ let parsedLine = /^\s*(?<key>[A-Z0-9]+?)\s+-\s+(?<value>.*)$/s.exec(line)?.groups;
150
+ if (!parsedLine) { // Doesn't match key=val spec
151
+ if (line.replace(/\s+/, '') && lastField) { // Line isn't just whitespace + We have a field to append to - append with \r delimiters
152
+ if (lastField.inputArray) { // Treat each line feed like an array entry
153
+ ref[lastField.rl].push(line);
154
+ } else { // Assume we append each line entry as a string with settings.delimeter
155
+ ref[lastField.rl] += settings.delimeter + line;
156
+ }
157
+ }
158
+ return; // Stop processing this line
159
+ }
160
+
161
+ if (parsedLine.key == 'ER') return; // Skip 'ER' defiition lines - this is probably due to the buffer draining
162
+ let fieldLookup = translations.fields.rawMap.get(parsedLine.key);
163
+
164
+ if (!fieldLookup) { // Skip unknown field translations
165
+ lastField = null;
166
+ return;
167
+ } else if (fieldLookup.rl == 'type') { // Special handling for ref types
168
+ ref[fieldLookup.rl] = translations.types.rawMap.get(parsedLine.value)?.rl || settings.defaultType;
169
+ lastField = fieldLookup; // Track last key so we can append to it on the next cycle
170
+ } else if (fieldLookup.inputArray) { // Should this `rl` key be treated like an appendable array?
171
+ if (!ref[fieldLookup.rl] || !Array.isArray(ref[fieldLookup.rl])) { // Array doesn't exist yet
172
+ ref[fieldLookup.rl] = [parsedLine.value];
173
+ } else {
174
+ ref[fieldLookup.rl].push(parsedLine.value);
175
+ }
176
+ lastField = fieldLookup;
177
+ } else { // Simple key=val
178
+ ref[fieldLookup.rl] = parsedLine.value;
179
+ lastField = fieldLookup;
180
+ }
181
+ })
182
+
183
+ // Post processing
184
+ // Page mangling {{{
185
+ if (ref._pageStart || ref._pageEnd) {
186
+ ref.pages = [ref._pageStart, ref._pageEnd]
187
+ .filter(Boolean) // Remove duds
188
+ .join('-');
189
+ delete ref._pageStart;
190
+ delete ref._pageEnd;
191
+ }
192
+ // }}}
193
+ // FallbackAbstract -> Abstract (if the latter is missing) {{{
194
+ if ((settings.fallbackAbstract ?? true) && ref.fallbackAbstract && !ref.abstract) {
195
+ ref.abstract = ref.fallbackAbstract;
196
+ delete ref.fallbackAbstract;
197
+ }
198
+ // }}}
199
+ // FallbackCity -> City (if the latter is missing) {{{
200
+ if ((settings.fallbackCity ?? true) && ref.fallbackCity && !ref.abstract) {
201
+ ref.city = ref.fallbackCity;
202
+ delete ref.fallbackCity;
203
+ }
204
+ // }}}
205
+
206
+ return ref;
207
+ }
208
+
209
+
210
+ /**
211
+ * Lookup tables for this module
212
+ * @type {Object}
213
+ * @property {array<Object>} fields Field translations between Reflib (`rl`) and the raw format (`raw`)
214
+ * @property {array<Object>} types Field translations between Reflib (`rl`) and the raw format types as raw text (`rawText`) and numeric ID (`rawId`)
215
+ * @property {boolean} isArray Whether the field should append to any existing `rl` field and be treated like an array of data
216
+ * @property {number|boolean} [sort] Sort order when outputting, use boolean `false` to disable the field on output
217
+ * @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
218
+ * @property {boolean} [inputArray=false] Forcably cast the field as an array when reading, even if there is only one value
219
+ */
220
+ export let translations = {
221
+ // Field translations {{{
222
+ fields: {
223
+ collection: [
224
+ { rl: "authors", raw: "A1", sort: false, inputArray: true },
225
+ { rl: "authors", raw: "A2", sort: false, inputArray: true },
226
+ { rl: "authors", raw: "A3", sort: false, inputArray: true },
227
+ { rl: "authors", raw: "A4", sort: false, inputArray: true },
228
+ { rl: "abstract", raw: "AB" },
229
+ { rl: "address", raw: "AD" },
230
+ { rl: "accessionNum", raw: "AN" },
231
+ {
232
+ rl: "authors",
233
+ raw: "AU",
234
+ sort: 4,
235
+ outputRepeat: true,
236
+ inputArray: true,
237
+ },
238
+ { rl: "custom1", raw: "C1" },
239
+ { rl: "custom2", raw: "C2" },
240
+ { rl: "custom3", raw: "C3" },
241
+ { rl: "custom4", raw: "C4" },
242
+ { rl: "custom5", raw: "C5" },
243
+ { rl: "custom6", raw: "C6" },
244
+ { rl: "custom7", raw: "C7" },
245
+ { rl: "custom8", raw: "C8" },
246
+ { rl: "caption", raw: "CA" },
247
+ { rl: "fallbackCity", raw: "CY" },
248
+ { rl: "date", raw: "DA" },
249
+ { rl: "database", raw: "DB" },
250
+ { rl: "doi", raw: "DO" },
251
+ { rl: "databaseProvider", raw: "DP" },
252
+ { rl: "_pageEnd", raw: "EP", sort: 6 },
253
+ { rl: "edition", raw: "ET", sort: 7 },
254
+ { rl: "number", raw: "IS", sort: 8 },
255
+ { rl: "journal", raw: "J1", sort: false },
256
+ { rl: "journal", raw: "JF", sort: 3 },
257
+ { rl: "keywords", raw: "KW", outputRepeat: true, inputArray: true },
258
+ { rl: "urls", raw: "L1", sort: false, inputArray: true },
259
+ { rl: "urls", raw: "L2", sort: false, inputArray: true },
260
+ { rl: "urls", raw: "L3", sort: false, inputArray: true },
261
+ { rl: "urls", raw: "L4", sort: false, inputArray: true },
262
+ { rl: "language", raw: "LA" },
263
+ { rl: "label", raw: "LB" },
264
+ { rl: "urls", raw: "LK", sort: false, inputArray: true },
265
+ { rl: "notes", raw: "N1" },
266
+ { rl: "fallbackAbstract", raw: "N2" },
267
+ { rl: "publisher", raw: "PB" },
268
+ { rl: "year", raw: "PY" },
269
+ { rl: "isbn", raw: "SN" },
270
+ { rl: "_pageStart", raw: "SP", sort: 5 },
271
+ { rl: "title", raw: "T1", sort: false },
272
+ { rl: "journal", raw: "T2", sort: false },
273
+ { rl: "title", raw: "TI", sort: 1 },
274
+ { rl: "type", raw: "TY", sort: 0 }, // TY must be the lowest number
275
+ { rl: "urls", raw: "UR", outputRepeat: true, inputArray: true },
276
+ { rl: "volume", raw: "VL" },
277
+ { rl: "date", raw: "Y1" },
278
+ { rl: "accessDate", raw: "Y2" },
279
+
280
+ // These are non-standard fields but we keep these here anyway to prevent data loss
281
+ { rl: "RISID", raw: "ID" },
282
+ { rl: "RISShortTitle", raw: "ST" },
283
+ { rl: "RISOriginalPublication", raw: "OP" },
284
+ ],
285
+ collectionOutput: [], // Sorted + filtered version of the above to use when outputting
286
+ rawMap: new Map(), // Calculated later for quicker lookup
287
+ rlMap: new Map(), // Calculated later for quicker lookup
288
+ },
289
+ // }}}
290
+ // Ref type translations {{{
291
+ types: {
292
+ collection: [
293
+ // Place high-priority translations at the top (when we translate BACK we need to know which of multiple keys to prioritize)
294
+ { rl: "audioVisualMaterial", raw: "ADVS" },
295
+ { rl: "journalArticle", raw: "JOUR" },
296
+ { rl: "personalCommunication", raw: "PCOMM" },
297
+ { rl: "filmOrBroadcast", raw: "VIDEO" },
298
+
299
+ // Low priority below this line
300
+ { rl: "unknown", raw: "ABST" },
301
+ { rl: "aggregatedDatabase", raw: "AGGR" },
302
+ { rl: "ancientText", raw: "ANCIENT" },
303
+ { rl: "artwork", raw: "ART" },
304
+ { rl: "bill", raw: "BILL" },
305
+ { rl: "blog", raw: "BLOG" },
306
+ { rl: "book", raw: "BOOK" },
307
+ { rl: "case", raw: "CASE" },
308
+ { rl: "bookSection", raw: "CHAP" },
309
+ { rl: "chartOrTable", raw: "CHART" },
310
+ { rl: "classicalWork", raw: "CLSWK" },
311
+ { rl: "computerProgram", raw: "COMP" },
312
+ { rl: "conferenceProceedings", raw: "CONF" },
313
+ { rl: "conferencePaper", raw: "CPAPER" },
314
+ { rl: "catalog", raw: "CTLG" },
315
+ { rl: "dataset", raw: "DATA" },
316
+ { rl: "onlineDatabase", raw: "DBASE" },
317
+ { rl: "dictionary", raw: "DICT" },
318
+ { rl: "electronicBook", raw: "EBOOK" },
319
+ { rl: "electronicBookSection", raw: "ECHAP" },
320
+ { rl: "editedBook", raw: "EDBOOK" },
321
+ { rl: "electronicArticle", raw: "EJOUR" },
322
+ { rl: "web", raw: "ELEC" },
323
+ { rl: "encyclopedia", raw: "ENCYC" },
324
+ { rl: "equation", raw: "EQUA" },
325
+ { rl: "figure", raw: "FIGURE" },
326
+ { rl: "generic", raw: "GEN" },
327
+ { rl: "governmentDocument", raw: "GOVDOC" },
328
+ { rl: "grant", raw: "GRANT" },
329
+ { rl: "hearing", raw: "HEARING" },
330
+ { rl: "personalCommunication", raw: "ICOMM" },
331
+ { rl: "newspaperArticle", raw: "INPR" },
332
+ { rl: "journalArticle", raw: "JFULL" },
333
+ { rl: "legalRuleOrRegulation", raw: "LEGAL" },
334
+ { rl: "manuscript", raw: "MANSCPT" },
335
+ { rl: "map", raw: "MAP" },
336
+ { rl: "magazineArticle", raw: "MGZN" },
337
+ { rl: "filmOrBroadcast", raw: "MPCT" },
338
+ { rl: "onlineMultimedia", raw: "MULTI" },
339
+ { rl: "music", raw: "MUSIC" },
340
+ { rl: "newspaperArticle", raw: "NEWS" },
341
+ { rl: "pamphlet", raw: "PAMP" },
342
+ { rl: "patent", raw: "PAT" },
343
+ { rl: "report", raw: "RPRT" },
344
+ { rl: "serial", raw: "SER" },
345
+ { rl: "audioVisualMaterial", raw: "SLIDE" },
346
+ { rl: "audioVisualMaterial", raw: "SOUND" },
347
+ { rl: "standard", raw: "STAND" },
348
+ { rl: "statute", raw: "STAT" },
349
+ { rl: "thesis", raw: "THES" },
350
+ { rl: "unpublished", raw: "UNPB" },
351
+ ],
352
+ rawMap: new Map(), // Calculated later for quicker lookup
353
+ rlMap: new Map(), // Calculated later for quicker lookup
354
+ },
355
+ // }}}
356
+ };
357
+
358
+
359
+ /**
360
+ * @see modules/interface.js
361
+ */
362
+ export function setup() {
363
+ // Sort the field set by sort field
364
+ translations.fields.collectionOutput = translations.fields.collection
365
+ .filter(f => f.sort !== false)
366
+ .sort((a, b) => (a.sort ?? 1000) == (b.sort ?? 1000) ? 0
367
+ : (a.sort ?? 1000) < (b.sort ?? 1000) ? -1
368
+ : 1
369
+ )
370
+
371
+ // Create lookup object of translations.fields with key as .rl / val as the full object
372
+ translations.fields.collection.forEach(c => {
373
+ translations.fields.rlMap.set(c.rl, c);
374
+ translations.fields.rawMap.set(c.raw, c);
375
+ });
376
+
377
+ // Create lookup object of ref.types with key as .rl / val as the full object
378
+ translations.types.collection.forEach(c => {
379
+ translations.types.rlMap.set(c.rl, c);
380
+ translations.types.rawMap.set(c.raw, c);
381
+ });
382
+
383
+ }