@iebh/reflib 2.4.7 → 2.4.9

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,429 +1,430 @@
1
- import camelCase from '../shared/camelCase.js';
2
- import Emitter from '../shared/emitter.js';
3
-
4
- // This import is overwritten by the 'browser' field in package.json with the shimmed version
5
- import { WritableStream as XMLParser } from 'htmlparser2/lib/WritableStream';
6
-
7
- /**
8
- * Read an EndnoteXML file, returning a stream analogue
9
- * @see modules/inhterface.js
10
- * @param {Stream} stream Stream primative to encapsulate
11
- * @returns {Object} A readable stream analogue defined in `modules/interface.js`
12
- */
13
- export function readStream(stream) {
14
- let emitter = Emitter();
15
-
16
- /**
17
- * The current reference being appended to
18
- * @type {Object}
19
- */
20
- let ref = {};
21
-
22
-
23
- /**
24
- * Stack of nodes we are currently traversed into
25
- * @type {array<Object>}
26
- */
27
- let stack = [];
28
-
29
-
30
- /**
31
- * Whether to append incoming text blocks to the previous block
32
- * This is necessary as XMLParser splits text into multiple calls so we need to know whether to append or treat this item as a continuation of the previous one
33
- * @type {boolean}
34
- */
35
- let textAppend = false;
36
-
37
- /**
38
- * The options/callbacks for the parser
39
- * @type {Object}
40
- */
41
- let parserOptions = {
42
- xmlMode: true,
43
- decodeEntities: false, // Handled below
44
- onopentag(name, attrs) {
45
- textAppend = false;
46
- stack.push({
47
- name: camelCase(name),
48
- attrs,
49
- });
50
- },
51
- onclosetag(name) {
52
- if (name == 'record') {
53
- if (ref.title) ref.title = ref.title // htmlparser2 handles the '<title>' tag in a really bizarre way so we have to pull apart the <style> bits when parsing
54
- .replace(/^.*<style.*>(.*)<\/style>.*$/m, '$1')
55
- .replace(/^\s+/, '')
56
- .replace(/\s+$/, '')
57
- emitter.emit('ref', translateRawToRef(ref));
58
- stack = []; // Trash entire stack when hitting end of <record/> node
59
- ref = {}; // Reset the ref state
60
- } else {
61
- stack.pop();
62
- }
63
- },
64
- ontext(text) {
65
- let parentName = stack[stack.length - 1]?.name;
66
- let gParentName = stack[stack.length - 2]?.name;
67
- if (parentName == 'title') {
68
- if (textAppend) {
69
- ref.title += text;
70
- } else {
71
- ref.title = text;
72
- }
73
- } else if (parentName == 'style' && gParentName == 'author') {
74
- if (!ref.authors) ref.authors = [];
75
- if (textAppend) {
76
- ref.authors[ref.authors.length - 1] += xmlUnescape(text);
77
- } else {
78
- ref.authors.push(xmlUnescape(text));
79
- }
80
- } else if (parentName == 'style' && gParentName == 'keyword') {
81
- if (!ref.keywords) ref.keywords = [];
82
- if (textAppend) {
83
- ref.keywords[ref.keywords.length - 1] += xmlUnescape(text);
84
- } else {
85
- ref.keywords.push(xmlUnescape(text));
86
- }
87
- } else if (parentName == 'style') { // Text within <style/> tag
88
- if (textAppend || ref[gParentName]) { // Text already exists? Append (handles node-expats silly multi-text per escape character "feature")
89
- ref[gParentName] += xmlUnescape(text);
90
- } else {
91
- ref[gParentName] = xmlUnescape(text);
92
- }
93
- } else if (['recNumber', 'refType'].includes(parentName)) { // Simple setters like <rec-number/>
94
- if (textAppend || ref[parentName]) {
95
- ref[parentName] += xmlUnescape(text);
96
- } else {
97
- ref[parentName] = xmlUnescape(text);
98
- }
99
- }
100
- textAppend = true; // Always set the next call to the text emitter handler as an append operation
101
- },
102
- onend() {
103
- emitter.emit('end');
104
- }
105
- }
106
-
107
- // Queue up the parser in the next tick (so we can return the emitter first)
108
- setTimeout(() => {
109
- if (typeof stream.pipe === 'function') {
110
- let parser = new XMLParser(parserOptions);
111
- stream.on('data', ()=> emitter.emit('progress', stream.bytesRead))
112
- stream.pipe(parser)
113
- return;
114
- } else {
115
- console.error('Error with stream, check "streamEmitter.js" if on browser')
116
- }
117
- })
118
-
119
- return emitter;
120
- }
121
-
122
-
123
- /**
124
- * @see modules/interface.js
125
- *
126
- * @param {Stream} stream Writable stream to output to
127
- * @param {Object} [options] Additional options to use when parsing
128
- * @param {string} [options.defaultType='journalArticle'] Default citation type to assume when no other type is specified
129
- * @param {string} [options.filePath="c:\\"] "Fake" internal source file path the citation library was exported from, must end with backslashes
130
- * @param {string} [options.fileName="EndNote.enl"] "Fake" internal source file name the citation library was exported from
131
- * @param {function} [options.formatDate] Date formatter to translate between a JS Date object and the EndNote YYYY-MM-DD format
132
- *
133
- * @returns {Object} A writable stream analogue defined in `modules/interface.js`
134
- */
135
- export function writeStream(stream, options) {
136
- let settings = {
137
- defaultType: 'journalArticle',
138
- filePath: 'c:\\',
139
- fileName: 'EndNote.enl',
140
- formatDate: value => value instanceof Date ? value.toISOString().substr(0, 10) : value,
141
- ...options,
142
- };
143
-
144
- // Cached values so we don't need to keep recomputing
145
- let encodedName = xmlEscape(settings.fileName);
146
- let refsSeen = 0;
147
-
148
- return {
149
- start: ()=> {
150
- stream.write('<?xml version="1.0" encoding="UTF-8" ?><xml><records>');
151
- return Promise.resolve();
152
- },
153
- write: ref => {
154
- let refType = translations.types.rlMap.get(ref.type || settings.defaultType);
155
- if (!refType) throw new Error(`Invalid reference type: "${ref.type}"`);
156
-
157
- refsSeen++;
158
- let recNumber = ref.recNumber || refsSeen;
159
-
160
- stream.write(
161
- '<record>'
162
- // Preamble
163
- + `<database name="${settings.fileName}" path="${settings.filePath}${settings.fileName}">${encodedName}</database>`
164
- + `<source-app name="EndNote" version="16.0">EndNote</source-app>`
165
- + `<rec-number>${recNumber}</rec-number>`
166
- + `<foreign-keys><key app="EN" db-id="s55prpsswfsepue0xz25pxai2p909xtzszzv">${recNumber}</key></foreign-keys>`
167
-
168
- // Type
169
- + `<ref-type name="${refType.rawText}">${refType.rawId}</ref-type>`
170
-
171
- // Authors
172
- + '<contributors><authors>'
173
- + (ref.authors || []).map(author => `<author><style face="normal" font="default" size="100%">${xmlEscape(author)}</style></author>`)
174
- + '</authors></contributors>'
175
-
176
- // Titles
177
- + '<titles>'
178
- + (ref.title ? `<title><style face="normal" font="default" size="100%">${xmlEscape(ref.title)}</style></title>` : '')
179
- + (ref.journal ? `<secondary-title><style face="normal" font="default" size="100%">${xmlEscape(ref.journal)}</style></secondary-title>` : '')
180
- + (ref.titleShort ? `<short-title><style face="normal" font="default" size="100%">${xmlEscape(ref.titleShort)}</style></short-title>` : '')
181
- + (ref.journalAlt ? `<alt-title><style face="normal" font="default" size="100%">${xmlEscape(ref.journalAlt)}</style></alt-title>` : '')
182
- + '</titles>'
183
-
184
- // Periodical
185
- + (ref.periodical ? `<periodical><full-title><style face="normal" font="default" size="100%">${xmlEscape(ref.periodical)}</style></full-title></periodical>` : '')
186
-
187
- // Simple field key/vals
188
- + [
189
- ['abstract', 'abstract'],
190
- ['accessDate', 'access-date'],
191
- ['accession', 'accession-num'],
192
- ['address', 'auth-address'],
193
- ['caption', 'caption'],
194
- ['databaseProvider', 'remote-database-provider'],
195
- ['database', 'remote-database-name'],
196
- ['doi', 'electronic-resource-num'],
197
- ['isbn', 'isbn'],
198
- ['accessionNum', 'accession-num'],
199
- ['label', 'label'],
200
- ['language', 'language'],
201
- ['notes', 'notes'],
202
- ['number', 'number'],
203
- ['pages', 'pages'],
204
- ['researchNotes', 'research-notes'],
205
- ['section', 'section'],
206
- ['volume', 'volume'],
207
- ['workType', 'work-type'],
208
- ['custom1', 'custom1'],
209
- ['custom2', 'custom2'],
210
- ['custom3', 'custom3'],
211
- ['custom4', 'custom4'],
212
- ['custom5', 'custom5'],
213
- ['custom6', 'custom6'],
214
- ['custom7', 'custom7'],
215
- ]
216
- .filter(([rlKey]) => ref[rlKey]) // Remove empty fields
217
- .map(([rlKey, rawKey]) =>
218
- `<${rawKey}><style face="normal" font="default" size="100%">${xmlEscape(ref[rlKey])}</style></${rawKey}>`
219
- )
220
- .join('')
221
-
222
- // Dates
223
- + (
224
- ref.date && ref.year && ref.date instanceof Date ?
225
- `<dates><year><style face="normal" font="default" size="100%">${xmlEscape(ref.year)}</style></year>`
226
- + `<pub-dates><date><style face="normal" font="default" size="100%">${settings.formatDate(ref.date)}</style></date></pub-dates></dates>`
227
- : ref.date && ref.year ?
228
- `<dates><year><style face="normal" font="default" size="100%">${xmlEscape(ref.year)}</style></year>`
229
- + `<pub-dates><date><style face="normal" font="default" size="100%">${ref.date}</style></date></pub-dates></dates>`
230
- : ref.date ?
231
- `<dates><pub-dates><date><style face="normal" font="default" size="100%">${xmlEscape(ref.date)}</style></date></pub-dates></dates>`
232
- : ref.year ?
233
- `<dates><year><style face="normal" font="default" size="100%">${xmlEscape(ref.year)}</style></year></dates>`
234
- : ''
235
- )
236
-
237
- // Urls
238
- + (ref.urls ?
239
- '<urls><related-urls>'
240
- + (ref.urls || [])
241
- .map(url => `<url><style face="normal" font="default" size="100%">${xmlEscape(url)}</style></url>`)
242
- .join('')
243
- + '</related-urls></urls>'
244
- : '')
245
-
246
- // Keywords
247
- + (ref.keywords ?
248
- '<keywords>'
249
- + (ref.keywords || [])
250
- .map(keyword => `<keyword><style face="normal" font="default" size="100%">${xmlEscape(keyword)}</style></keyword>`)
251
- .join('')
252
- + '</keywords>'
253
- : '')
254
-
255
- + '</record>'
256
- );
257
- return Promise.resolve();
258
- },
259
- end: ()=> {
260
- stream.write('</records></xml>');
261
- return new Promise((resolve, reject) =>
262
- stream.end(err => err ? reject(err) : resolve())
263
- );
264
- },
265
- };
266
- }
267
-
268
-
269
- /**
270
- * Utility function to take the raw XML output object and translate it into a Reflib object
271
- * @param {Object} xRef Raw XML object to process
272
- * @returns {Object} The translated Reflib object output
273
- */
274
- export function translateRawToRef(xRef) {
275
- let recOut = {
276
- ...Object.fromEntries(
277
- translations.fields.collection
278
- .filter(field => xRef[field.raw]) // Only include fields we have a value for
279
- .map(field => [ field.rl, xRef[field.raw] ]) // Translate Raw -> Reflib spec
280
- ),
281
- type: translations.types.rawMap.get(+xRef.refType || 17)?.rl,
282
- };
283
-
284
- return recOut;
285
- }
286
-
287
-
288
- /**
289
- * Default string -> XML encoder
290
- * @param {string} str The input string to encode
291
- * @returns {string} The XML "safe" string
292
- */
293
- export function xmlEscape(str) {
294
- return ('' + str)
295
- .replace(/&/g, '&amp;')
296
- .replace(/\r/g, '&#xD;')
297
- .replace(/</g, '&lt;')
298
- .replace(/>/g, '&gt;')
299
- .replace(/"/g, '&quot;')
300
- .replace(/'/g, '&apos;');
301
- }
302
-
303
-
304
- /**
305
- * Default XML -> string decodeer
306
- * @param {string} str The input string to decode
307
- * @returns {string} The actual string
308
- */
309
- export function xmlUnescape(str) {
310
- return ('' + str)
311
- .replace(/&amp;/g, '&')
312
- .replace(/&#xD;/g, '\r')
313
- .replace(/&lt;/g, '<')
314
- .replace(/&gt;/g, '>')
315
- .replace(/&quot;/g, '"')
316
- .replace(/&apos;/g, "'");
317
- }
318
-
319
-
320
- /**
321
- * Lookup tables for this module
322
- * @type {Object}
323
- * @property {array<Object>} fields Field translations between Reflib (`rl`) and the raw format (`raw`)
324
- * @property {array<Object>} types Field translations between Reflib (`rl`) and the raw format types as raw text (`rawText`) and numeric ID (`rawId`)
325
- */
326
- export let translations = {
327
- // Field translations {{{
328
- fields: {
329
- collection: [
330
- {rl: 'recNumber', raw: 'recNumber'},
331
- {rl: 'title', raw: 'title'},
332
- {rl: 'journal', raw: 'secondaryTitle'},
333
- {rl: 'address', raw: 'authAddress'},
334
- {rl: 'researchNotes', raw: 'researchNotes'},
335
- {rl: 'type', raw: 'FIXME'},
336
- {rl: 'authors', raw: 'authors'},
337
- {rl: 'pages', raw: 'pages'},
338
- {rl: 'volume', raw: 'volume'},
339
- {rl: 'number', raw: 'number'},
340
- {rl: 'isbn', raw: 'isbn'},
341
- {rl: 'accessionNum', raw: 'accessionNum'},
342
- {rl: 'abstract', raw: 'abstract'},
343
- {rl: 'label', raw: 'label'},
344
- {rl: 'caption', raw: 'caption'},
345
- {rl: 'notes', raw: 'notes'},
346
- {rl: 'custom1', raw: 'custom1'},
347
- {rl: 'custom2', raw: 'custom2'},
348
- {rl: 'custom3', raw: 'custom3'},
349
- {rl: 'custom4', raw: 'custom4'},
350
- {rl: 'custom5', raw: 'custom5'},
351
- {rl: 'custom6', raw: 'custom6'},
352
- {rl: 'custom7', raw: 'custom7'},
353
- {rl: 'doi', raw: 'electronicResourceNum'},
354
- {rl: 'year', raw: 'year'},
355
- {rl: 'date', raw: 'date'},
356
- {rl: 'keywords', raw: 'keywords'},
357
- {rl: 'urls', raw: 'urls'},
358
- ],
359
- },
360
- // }}}
361
- // Ref type translations {{{
362
- types: {
363
- collection: [
364
- {rl: 'aggregatedDatabase', rawText: 'Aggregated Database', rawId: 55},
365
- {rl: 'ancientText', rawText: 'Ancient Text', rawId: 51},
366
- {rl: 'artwork', rawText: 'Artwork', rawId: 2},
367
- {rl: 'audioVisualMaterial', rawText: 'Audiovisual Material', rawId: 3},
368
- {rl: 'bill', rawText: 'Bill', rawId: 4},
369
- {rl: 'blog', rawText: 'Blog', rawId: 56},
370
- {rl: 'book', rawText: 'Book', rawId: 6},
371
- {rl: 'bookSection', rawText: 'Book Section', rawId: 5},
372
- {rl: 'case', rawText: 'Case', rawId: 7},
373
- {rl: 'catalog', rawText: 'Catalog', rawId: 8},
374
- {rl: 'chartOrTable', rawText: 'Chart or Table', rawId: 38},
375
- {rl: 'classicalWork', rawText: 'Classical Work', rawId: 49},
376
- {rl: 'computerProgram', rawText: 'Computer Program', rawId: 9},
377
- {rl: 'conferencePaper', rawText: 'Conference Paper', rawId: 47},
378
- {rl: 'conferenceProceedings', rawText: 'Conference Proceedings', rawId: 10},
379
- {rl: 'dataset', rawText: 'Dataset', rawId: 59},
380
- {rl: 'dictionary', rawText: 'Dictionary', rawId: 52},
381
- {rl: 'editedBook', rawText: 'Edited Book', rawId: 28},
382
- {rl: 'electronicArticle', rawText: 'Electronic Article', rawId: 43},
383
- {rl: 'electronicBook', rawText: 'Electronic Book', rawId: 44},
384
- {rl: 'electronicBookSection', rawText: 'Electronic Book Section', rawId: 60},
385
- {rl: 'encyclopedia', rawText: 'Encyclopedia', rawId: 53},
386
- {rl: 'equation', rawText: 'Equation', rawId: 39},
387
- {rl: 'figure', rawText: 'Figure', rawId: 37},
388
- {rl: 'filmOrBroadcast', rawText: 'Film or Broadcast', rawId: 21},
389
- {rl: 'generic', rawText: 'Generic', rawId: 13},
390
- {rl: 'governmentDocument', rawText: 'Government Document', rawId: 46},
391
- {rl: 'grant', rawText: 'Grant', rawId: 54},
392
- {rl: 'hearing', rawText: 'Hearing', rawId: 14},
393
- {rl: 'journalArticle', rawText: 'Journal Article', rawId: 17},
394
- {rl: 'legalRuleOrRegulation', rawText: 'Legal Rule or Regulation', rawId: 50},
395
- {rl: 'magazineArticle', rawText: 'Magazine Article', rawId: 19},
396
- {rl: 'manuscript', rawText: 'Manuscript', rawId: 36},
397
- {rl: 'map', rawText: 'Map', rawId: 20},
398
- {rl: 'music', rawText: 'Music', rawId: 61},
399
- {rl: 'newspaperArticle', rawText: 'Newspaper Article', rawId: 23},
400
- {rl: 'onlineDatabase', rawText: 'Online Database', rawId: 45},
401
- {rl: 'onlineMultimedia', rawText: 'Online Multimedia', rawId: 48},
402
- {rl: 'pamphlet', rawText: 'Pamphlet', rawId: 24},
403
- {rl: 'patent', rawText: 'Patent', rawId: 25},
404
- {rl: 'personalCommunication', rawText: 'Personal Communication', rawId: 26},
405
- {rl: 'report', rawText: 'Report', rawId: 27},
406
- {rl: 'serial', rawText: 'Serial', rawId: 57},
407
- {rl: 'standard', rawText: 'Standard', rawId: 58},
408
- {rl: 'statute', rawText: 'Statute', rawId: 31},
409
- {rl: 'thesis', rawText: 'Thesis', rawId: 32},
410
- {rl: 'unpublished', rawText: 'Unpublished Work', rawId: 34},
411
- {rl: 'web', rawText: 'Web Page', rawId: 12},
412
- ],
413
- rlMap: new Map(), // Calculated later for quicker lookup
414
- rawMap: new Map(), // Calculated later for quicker lookup
415
- },
416
- // }}}
417
- };
418
-
419
-
420
- /**
421
- * @see modules/interface.js
422
- */
423
- export function setup() {
424
- // Create lookup object of translations.types with key as .rl / val as the full object
425
- translations.types.collection.forEach(c => {
426
- translations.types.rlMap.set(c.rl, c);
427
- translations.types.rawMap.set(c.rawId, c);
428
- });
429
- }
1
+ import camelCase from '../shared/camelCase.js';
2
+ import Emitter from '../shared/emitter.js';
3
+
4
+ // This import is overwritten by the 'browser' field in package.json with the shimmed version
5
+ import { WritableStream as XMLParser } from 'htmlparser2/lib/WritableStream';
6
+
7
+ /**
8
+ * Read an EndnoteXML file, returning a stream analogue
9
+ * @see modules/inhterface.js
10
+ * @param {Stream} stream Stream primative to encapsulate
11
+ * @returns {Object} A readable stream analogue defined in `modules/interface.js`
12
+ */
13
+ export function readStream(stream) {
14
+ let emitter = Emitter();
15
+
16
+ /**
17
+ * The current reference being appended to
18
+ * @type {Object}
19
+ */
20
+ let ref = {};
21
+
22
+
23
+ /**
24
+ * Stack of nodes we are currently traversed into
25
+ * @type {array<Object>}
26
+ */
27
+ let stack = [];
28
+
29
+
30
+ /**
31
+ * Whether to append incoming text blocks to the previous block
32
+ * This is necessary as XMLParser splits text into multiple calls so we need to know whether to append or treat this item as a continuation of the previous one
33
+ * @type {boolean}
34
+ */
35
+ let textAppend = false;
36
+
37
+ /**
38
+ * The options/callbacks for the parser
39
+ * @type {Object}
40
+ */
41
+ let parserOptions = {
42
+ xmlMode: true,
43
+ decodeEntities: false, // Handled below
44
+ onopentag(name, attrs) {
45
+ textAppend = false;
46
+ stack.push({
47
+ name: camelCase(name),
48
+ attrs,
49
+ });
50
+ },
51
+ onclosetag(name) {
52
+ if (name == 'record') {
53
+ if (ref.title) ref.title = ref.title // htmlparser2 handles the '<title>' tag in a really bizarre way so we have to pull apart the <style> bits when parsing
54
+ .replace(/^.*<style.*>(.*)<\/style>.*$/m, '$1')
55
+ .replace(/^\s+/, '')
56
+ .replace(/\s+$/, '')
57
+ emitter.emit('ref', translateRawToRef(ref));
58
+ stack = []; // Trash entire stack when hitting end of <record/> node
59
+ ref = {}; // Reset the ref state
60
+ } else {
61
+ stack.pop();
62
+ }
63
+ },
64
+ ontext(text) {
65
+ let parentName = stack[stack.length - 1]?.name;
66
+ let gParentName = stack[stack.length - 2]?.name;
67
+ if (parentName == 'title') {
68
+ if (textAppend) {
69
+ ref.title += text;
70
+ } else {
71
+ ref.title = text;
72
+ }
73
+ } else if (parentName == 'style' && gParentName == 'author') {
74
+ if (!ref.authors) ref.authors = [];
75
+ if (textAppend) {
76
+ ref.authors[ref.authors.length - 1] += xmlUnescape(text);
77
+ } else {
78
+ ref.authors.push(xmlUnescape(text));
79
+ }
80
+ } else if (parentName == 'style' && gParentName == 'keyword') {
81
+ if (!ref.keywords) ref.keywords = [];
82
+ if (textAppend) {
83
+ ref.keywords[ref.keywords.length - 1] += xmlUnescape(text);
84
+ } else {
85
+ ref.keywords.push(xmlUnescape(text));
86
+ }
87
+ } else if (parentName == 'style') { // Text within <style/> tag
88
+ if (textAppend || ref[gParentName]) { // Text already exists? Append (handles node-expats silly multi-text per escape character "feature")
89
+ ref[gParentName] += xmlUnescape(text);
90
+ } else {
91
+ ref[gParentName] = xmlUnescape(text);
92
+ }
93
+ } else if (['recNumber', 'refType'].includes(parentName)) { // Simple setters like <rec-number/>
94
+ if (textAppend || ref[parentName]) {
95
+ ref[parentName] += xmlUnescape(text);
96
+ } else {
97
+ ref[parentName] = xmlUnescape(text);
98
+ }
99
+ }
100
+ textAppend = true; // Always set the next call to the text emitter handler as an append operation
101
+ },
102
+ onend() {
103
+ emitter.emit('end');
104
+ }
105
+ }
106
+
107
+ // Queue up the parser in the next tick (so we can return the emitter first)
108
+ setTimeout(() => {
109
+ if (typeof stream.pipe === 'function') {
110
+ let parser = new XMLParser(parserOptions);
111
+ stream.on('data', ()=> emitter.emit('progress', stream.bytesRead))
112
+ stream.pipe(parser)
113
+ return;
114
+ } else {
115
+ console.error('Error with stream, check "streamEmitter.js" if on browser')
116
+ }
117
+ })
118
+
119
+ return emitter;
120
+ }
121
+
122
+
123
+ /**
124
+ * @see modules/interface.js
125
+ *
126
+ * @param {Stream} stream Writable stream to output to
127
+ * @param {Object} [options] Additional options to use when parsing
128
+ * @param {string} [options.defaultType='journalArticle'] Default citation type to assume when no other type is specified
129
+ * @param {string} [options.filePath="c:\\"] "Fake" internal source file path the citation library was exported from, must end with backslashes
130
+ * @param {string} [options.fileName="EndNote.enl"] "Fake" internal source file name the citation library was exported from
131
+ * @param {function} [options.formatDate] Date formatter to translate between a JS Date object and the EndNote YYYY-MM-DD format
132
+ *
133
+ * @returns {Object} A writable stream analogue defined in `modules/interface.js`
134
+ */
135
+ export function writeStream(stream, options) {
136
+ let settings = {
137
+ defaultType: 'journalArticle',
138
+ filePath: 'c:\\',
139
+ fileName: 'EndNote.enl',
140
+ formatDate: value => value instanceof Date ? value.toISOString().substr(0, 10) : value,
141
+ ...options,
142
+ };
143
+
144
+ // Cached values so we don't need to keep recomputing
145
+ let encodedName = xmlEscape(settings.fileName);
146
+ let refsSeen = 0;
147
+
148
+ return {
149
+ start: ()=> {
150
+ stream.write('<?xml version="1.0" encoding="UTF-8" ?><xml><records>');
151
+ return Promise.resolve();
152
+ },
153
+ write: ref => {
154
+ let refType = translations.types.rlMap.get(ref.type || settings.defaultType);
155
+ if (!refType) throw new Error(`Invalid reference type: "${ref.type}"`);
156
+
157
+ refsSeen++;
158
+ let recNumber = ref.recNumber || refsSeen;
159
+
160
+ stream.write(
161
+ '<record>'
162
+ // Preamble
163
+ + `<database name="${settings.fileName}" path="${settings.filePath}${settings.fileName}">${encodedName}</database>`
164
+ + `<source-app name="EndNote" version="16.0">EndNote</source-app>`
165
+ + `<rec-number>${recNumber}</rec-number>`
166
+ + `<foreign-keys><key app="EN" db-id="s55prpsswfsepue0xz25pxai2p909xtzszzv">${recNumber}</key></foreign-keys>`
167
+
168
+ // Type
169
+ + `<ref-type name="${refType.rawText}">${refType.rawId}</ref-type>`
170
+
171
+ // Authors
172
+ + '<contributors><authors>'
173
+ + (ref.authors || []).map(author => `<author><style face="normal" font="default" size="100%">${xmlEscape(author)}</style></author>`)
174
+ + '</authors></contributors>'
175
+
176
+ // Titles
177
+ + '<titles>'
178
+ + (ref.title ? `<title><style face="normal" font="default" size="100%">${xmlEscape(ref.title)}</style></title>` : '')
179
+ + (ref.journal ? `<secondary-title><style face="normal" font="default" size="100%">${xmlEscape(ref.journal)}</style></secondary-title>` : '')
180
+ + (ref.titleShort ? `<short-title><style face="normal" font="default" size="100%">${xmlEscape(ref.titleShort)}</style></short-title>` : '')
181
+ + (ref.journalAlt ? `<alt-title><style face="normal" font="default" size="100%">${xmlEscape(ref.journalAlt)}</style></alt-title>` : '')
182
+ + '</titles>'
183
+
184
+ // Periodical
185
+ + (ref.periodical ? `<periodical><full-title><style face="normal" font="default" size="100%">${xmlEscape(ref.periodical)}</style></full-title></periodical>` : '')
186
+
187
+ // Simple field key/vals
188
+ + [
189
+ ['abstract', 'abstract'],
190
+ ['accessDate', 'access-date'],
191
+ ['accession', 'accession-num'],
192
+ ['address', 'auth-address'],
193
+ ['caption', 'caption'],
194
+ ['databaseProvider', 'remote-database-provider'],
195
+ ['database', 'remote-database-name'],
196
+ ['doi', 'electronic-resource-num'],
197
+ ['isbn', 'isbn'],
198
+ ['accessionNum', 'accession-num'],
199
+ ['label', 'label'],
200
+ ['language', 'language'],
201
+ ['notes', 'notes'],
202
+ ['number', 'number'],
203
+ ['pages', 'pages'],
204
+ ['researchNotes', 'research-notes'],
205
+ ['section', 'section'],
206
+ ['volume', 'volume'],
207
+ ['workType', 'work-type'],
208
+ ['custom1', 'custom1'],
209
+ ['custom2', 'custom2'],
210
+ ['custom3', 'custom3'],
211
+ ['custom4', 'custom4'],
212
+ ['custom5', 'custom5'],
213
+ ['custom6', 'custom6'],
214
+ ['custom7', 'custom7'],
215
+ ]
216
+ .filter(([rlKey]) => ref[rlKey]) // Remove empty fields
217
+ .map(([rlKey, rawKey]) =>
218
+ `<${rawKey}><style face="normal" font="default" size="100%">${xmlEscape(ref[rlKey])}</style></${rawKey}>`
219
+ )
220
+ .join('')
221
+
222
+ // Dates
223
+ + (
224
+ ref.date && ref.year && ref.date instanceof Date ?
225
+ `<dates><year><style face="normal" font="default" size="100%">${xmlEscape(ref.year)}</style></year>`
226
+ + `<pub-dates><date><style face="normal" font="default" size="100%">${settings.formatDate(ref.date)}</style></date></pub-dates></dates>`
227
+ : ref.date && ref.year ?
228
+ `<dates><year><style face="normal" font="default" size="100%">${xmlEscape(ref.year)}</style></year>`
229
+ + `<pub-dates><date><style face="normal" font="default" size="100%">${ref.date}</style></date></pub-dates></dates>`
230
+ : ref.date ?
231
+ `<dates><pub-dates><date><style face="normal" font="default" size="100%">${xmlEscape(ref.date)}</style></date></pub-dates></dates>`
232
+ : ref.year ?
233
+ `<dates><year><style face="normal" font="default" size="100%">${xmlEscape(ref.year)}</style></year></dates>`
234
+ : ''
235
+ )
236
+
237
+ // Urls
238
+ + (ref.urls ?
239
+ '<urls><related-urls>'
240
+ + (ref.urls || [])
241
+ .map(url => `<url><style face="normal" font="default" size="100%">${xmlEscape(url)}</style></url>`)
242
+ .join('')
243
+ + '</related-urls></urls>'
244
+ : '')
245
+
246
+ // Keywords
247
+ + (ref.keywords ?
248
+ '<keywords>'
249
+ + (ref.keywords || [])
250
+ .map(keyword => `<keyword><style face="normal" font="default" size="100%">${xmlEscape(keyword)}</style></keyword>`)
251
+ .join('')
252
+ + '</keywords>'
253
+ : '')
254
+
255
+ + '</record>'
256
+ );
257
+ return Promise.resolve();
258
+ },
259
+ end: ()=> {
260
+ stream.write('</records></xml>');
261
+ return new Promise((resolve, reject) =>
262
+ stream.end(err => err ? reject(err) : resolve())
263
+ );
264
+ },
265
+ };
266
+ }
267
+
268
+
269
+ /**
270
+ * Utility function to take the raw XML output object and translate it into a Reflib object
271
+ * @param {Object} xRef Raw XML object to process
272
+ * @returns {Object} The translated Reflib object output
273
+ */
274
+ export function translateRawToRef(xRef) {
275
+ let recOut = {
276
+ ...Object.fromEntries(
277
+ translations.fields.collection
278
+ .filter(field => xRef[field.raw]) // Only include fields we have a value for
279
+ .map(field => [ field.rl, xRef[field.raw] ]) // Translate Raw -> Reflib spec
280
+ ),
281
+ type: translations.types.rawMap.get(+xRef.refType || 17)?.rl,
282
+ };
283
+
284
+ return recOut;
285
+ }
286
+
287
+
288
+ /**
289
+ * Default string -> XML encoder
290
+ * @param {string} str The input string to encode
291
+ * @returns {string} The XML "safe" string
292
+ */
293
+ export function xmlEscape(str) {
294
+ return ('' + str)
295
+ .replace(/&/g, '&amp;')
296
+ .replace(/\r/g, '&#xD;')
297
+ .replace(/</g, '&lt;')
298
+ .replace(/>/g, '&gt;')
299
+ .replace(/"/g, '&quot;')
300
+ .replace(/'/g, '&apos;');
301
+ }
302
+
303
+
304
+ /**
305
+ * Default XML -> string decodeer
306
+ * @param {string} str The input string to decode
307
+ * @returns {string} The actual string
308
+ */
309
+ export function xmlUnescape(str) {
310
+ return ('' + str)
311
+ .replace(/&amp;/g, '&')
312
+ .replace(/&#xD;/g, '\r')
313
+ .replace(/&lt;/g, '<')
314
+ .replace(/&gt;/g, '>')
315
+ .replace(/&quot;/g, '"')
316
+ .replace(/&apos;/g, "'");
317
+ }
318
+
319
+
320
+ /**
321
+ * Lookup tables for this module
322
+ * @type {Object}
323
+ * @property {array<Object>} fields Field translations between Reflib (`rl`) and the raw format (`raw`)
324
+ * @property {array<Object>} types Field translations between Reflib (`rl`) and the raw format types as raw text (`rawText`) and numeric ID (`rawId`)
325
+ */
326
+ export let translations = {
327
+ // Field translations {{{
328
+ fields: {
329
+ collection: [
330
+ {rl: 'recNumber', raw: 'recNumber'},
331
+ {rl: 'title', raw: 'title'},
332
+ {rl: 'journal', raw: 'secondaryTitle'},
333
+ {rl: 'address', raw: 'authAddress'},
334
+ {rl: 'researchNotes', raw: 'researchNotes'},
335
+ {rl: 'type', raw: 'FIXME'},
336
+ {rl: 'authors', raw: 'authors'},
337
+ {rl: 'pages', raw: 'pages'},
338
+ {rl: 'volume', raw: 'volume'},
339
+ {rl: 'number', raw: 'number'},
340
+ {rl: 'isbn', raw: 'isbn'},
341
+ {rl: 'accessionNum', raw: 'accessionNum'},
342
+ {rl: 'abstract', raw: 'abstract'},
343
+ {rl: 'label', raw: 'label'},
344
+ {rl: 'caption', raw: 'caption'},
345
+ {rl: 'notes', raw: 'notes'},
346
+ {rl: 'custom1', raw: 'custom1'},
347
+ {rl: 'custom2', raw: 'custom2'},
348
+ {rl: 'custom3', raw: 'custom3'},
349
+ {rl: 'custom4', raw: 'custom4'},
350
+ {rl: 'custom5', raw: 'custom5'},
351
+ {rl: 'custom6', raw: 'custom6'},
352
+ {rl: 'custom7', raw: 'custom7'},
353
+ {rl: 'doi', raw: 'electronicResourceNum'},
354
+ {rl: 'year', raw: 'year'},
355
+ {rl: 'date', raw: 'date'},
356
+ {rl: 'keywords', raw: 'keywords'},
357
+ {rl: 'urls', raw: 'urls'},
358
+ ],
359
+ },
360
+ // }}}
361
+ // Ref type translations {{{
362
+ types: {
363
+ collection: [
364
+ {rl: 'aggregatedDatabase', rawText: 'Aggregated Database', rawId: 55},
365
+ {rl: 'ancientText', rawText: 'Ancient Text', rawId: 51},
366
+ {rl: 'artwork', rawText: 'Artwork', rawId: 2},
367
+ {rl: 'audioVisualMaterial', rawText: 'Audiovisual Material', rawId: 3},
368
+ {rl: 'bill', rawText: 'Bill', rawId: 4},
369
+ {rl: 'blog', rawText: 'Blog', rawId: 56},
370
+ {rl: 'book', rawText: 'Book', rawId: 6},
371
+ {rl: 'bookSection', rawText: 'Book Section', rawId: 5},
372
+ {rl: 'case', rawText: 'Case', rawId: 7},
373
+ {rl: 'catalog', rawText: 'Catalog', rawId: 8},
374
+ {rl: 'chartOrTable', rawText: 'Chart or Table', rawId: 38},
375
+ {rl: 'classicalWork', rawText: 'Classical Work', rawId: 49},
376
+ {rl: 'computerProgram', rawText: 'Computer Program', rawId: 9},
377
+ {rl: 'conferencePaper', rawText: 'Conference Paper', rawId: 47},
378
+ {rl: 'conferenceProceedings', rawText: 'Conference Proceedings', rawId: 10},
379
+ {rl: 'dataset', rawText: 'Dataset', rawId: 59},
380
+ {rl: 'dictionary', rawText: 'Dictionary', rawId: 52},
381
+ {rl: 'editedBook', rawText: 'Edited Book', rawId: 28},
382
+ {rl: 'electronicArticle', rawText: 'Electronic Article', rawId: 43},
383
+ {rl: 'electronicBook', rawText: 'Electronic Book', rawId: 44},
384
+ {rl: 'electronicBookSection', rawText: 'Electronic Book Section', rawId: 60},
385
+ {rl: 'encyclopedia', rawText: 'Encyclopedia', rawId: 53},
386
+ {rl: 'equation', rawText: 'Equation', rawId: 39},
387
+ {rl: 'figure', rawText: 'Figure', rawId: 37},
388
+ {rl: 'filmOrBroadcast', rawText: 'Film or Broadcast', rawId: 21},
389
+ {rl: 'generic', rawText: 'Generic', rawId: 13},
390
+ {rl: 'governmentDocument', rawText: 'Government Document', rawId: 46},
391
+ {rl: 'grant', rawText: 'Grant', rawId: 54},
392
+ {rl: 'hearing', rawText: 'Hearing', rawId: 14},
393
+ {rl: 'journalArticle', rawText: 'Journal Article', rawId: 17},
394
+ {rl: 'legalRuleOrRegulation', rawText: 'Legal Rule or Regulation', rawId: 50},
395
+ {rl: 'magazineArticle', rawText: 'Magazine Article', rawId: 19},
396
+ {rl: 'manuscript', rawText: 'Manuscript', rawId: 36},
397
+ {rl: 'map', rawText: 'Map', rawId: 20},
398
+ {rl: 'music', rawText: 'Music', rawId: 61},
399
+ {rl: 'newspaperArticle', rawText: 'Newspaper Article', rawId: 23},
400
+ {rl: 'onlineDatabase', rawText: 'Online Database', rawId: 45},
401
+ {rl: 'onlineMultimedia', rawText: 'Online Multimedia', rawId: 48},
402
+ {rl: 'pamphlet', rawText: 'Pamphlet', rawId: 24},
403
+ {rl: 'patent', rawText: 'Patent', rawId: 25},
404
+ {rl: 'personalCommunication', rawText: 'Personal Communication', rawId: 26},
405
+ {rl: 'report', rawText: 'Report', rawId: 27},
406
+ {rl: 'serial', rawText: 'Serial', rawId: 57},
407
+ {rl: 'standard', rawText: 'Standard', rawId: 58},
408
+ {rl: 'statute', rawText: 'Statute', rawId: 31},
409
+ {rl: 'thesis', rawText: 'Thesis', rawId: 32},
410
+ {rl: 'unknown', rawText: 'unknown', rawId: 34},
411
+ {rl: 'unpublished', rawText: 'Unpublished Work', rawId: 34},
412
+ {rl: 'web', rawText: 'Web Page', rawId: 12},
413
+ ],
414
+ rlMap: new Map(), // Calculated later for quicker lookup
415
+ rawMap: new Map(), // Calculated later for quicker lookup
416
+ },
417
+ // }}}
418
+ };
419
+
420
+
421
+ /**
422
+ * @see modules/interface.js
423
+ */
424
+ export function setup() {
425
+ // Create lookup object of translations.types with key as .rl / val as the full object
426
+ translations.types.collection.forEach(c => {
427
+ translations.types.rlMap.set(c.rl, c);
428
+ translations.types.rawMap.set(c.rawId, c);
429
+ });
430
+ }