@iebh/reflib 2.5.5 → 2.5.7

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