@uniweb/scholar 0.2.0 → 0.2.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/README.md +42 -32
- package/package.json +19 -6
- package/src/bibliography/formatters/index.js +62 -23
- package/src/bibliography/index.js +8 -29
- package/src/bibliography/normalize.js +234 -0
- package/src/bibliography/parsers/index.js +35 -1
- package/src/citations/Bibliography.jsx +26 -54
- package/src/citations/Citation.jsx +35 -145
- package/src/citations/CitationProvider.jsx +83 -85
- package/src/components/AuthorList.jsx +65 -40
- package/src/components/BibtexBlock.jsx +1 -1
- package/src/components/CiteButton.jsx +78 -45
- package/src/components/DoiLink.jsx +27 -21
- package/src/index.js +15 -5
- package/src/math/Equation.jsx +45 -75
- package/src/math/index.js +19 -4
- package/src/bibliography/formatters/apa.js +0 -352
- package/src/bibliography/formatters/chicago.js +0 -339
- package/src/bibliography/formatters/ieee.js +0 -332
- package/src/bibliography/formatters/mla.js +0 -335
- package/src/bibliography/parsers/bibtex.js +0 -405
- package/src/bibliography/utils/authors.js +0 -277
- package/src/bibliography/utils/dates.js +0 -228
- package/src/bibliography/utils/index.js +0 -8
- package/src/math/Math.jsx +0 -115
- package/src/math/katex-loader.js +0 -105
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BibTeX Parser
|
|
3
|
-
*
|
|
4
|
-
* Parses BibTeX format into normalized publication objects and exports
|
|
5
|
-
* publication objects back to BibTeX format.
|
|
6
|
-
*
|
|
7
|
-
* @module @uniweb/scholar/bibliography/parsers/bibtex
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { parseAuthorString } from '../utils/authors.js'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Parse a BibTeX field value, handling braces and quotes
|
|
14
|
-
*
|
|
15
|
-
* @param {string} value - Raw field value
|
|
16
|
-
* @returns {string} Cleaned value
|
|
17
|
-
*/
|
|
18
|
-
function parseFieldValue(value) {
|
|
19
|
-
if (!value) return ''
|
|
20
|
-
|
|
21
|
-
let cleaned = value.trim()
|
|
22
|
-
|
|
23
|
-
// Remove outer braces or quotes
|
|
24
|
-
if (
|
|
25
|
-
(cleaned.startsWith('{') && cleaned.endsWith('}')) ||
|
|
26
|
-
(cleaned.startsWith('"') && cleaned.endsWith('"'))
|
|
27
|
-
) {
|
|
28
|
-
cleaned = cleaned.slice(1, -1)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Remove nested braces (used for preserving case)
|
|
32
|
-
cleaned = cleaned.replace(/\{([^{}]*)\}/g, '$1')
|
|
33
|
-
|
|
34
|
-
// Handle common LaTeX commands
|
|
35
|
-
cleaned = cleaned
|
|
36
|
-
.replace(/\\&/g, '&')
|
|
37
|
-
.replace(/\\\$/g, '$')
|
|
38
|
-
.replace(/\\%/g, '%')
|
|
39
|
-
.replace(/\\_/g, '_')
|
|
40
|
-
.replace(/\\#/g, '#')
|
|
41
|
-
.replace(/~/g, ' ')
|
|
42
|
-
.replace(/--/g, '–')
|
|
43
|
-
.replace(/``/g, '"')
|
|
44
|
-
.replace(/''/g, '"')
|
|
45
|
-
|
|
46
|
-
return cleaned.trim()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Parse a single BibTeX entry
|
|
51
|
-
*
|
|
52
|
-
* @param {string} entryStr - BibTeX entry string
|
|
53
|
-
* @returns {Object|null} Parsed publication object
|
|
54
|
-
*/
|
|
55
|
-
function parseEntry(entryStr) {
|
|
56
|
-
// Match @type{key, ...}
|
|
57
|
-
const headerMatch = entryStr.match(/@(\w+)\s*\{\s*([^,\s]*)\s*,/)
|
|
58
|
-
if (!headerMatch) return null
|
|
59
|
-
|
|
60
|
-
const entryType = headerMatch[1].toLowerCase()
|
|
61
|
-
const citationKey = headerMatch[2]
|
|
62
|
-
|
|
63
|
-
// Extract fields
|
|
64
|
-
const fieldsStr = entryStr.slice(headerMatch[0].length)
|
|
65
|
-
const fields = {}
|
|
66
|
-
|
|
67
|
-
// Parse fields using a state machine approach
|
|
68
|
-
let currentField = ''
|
|
69
|
-
let currentValue = ''
|
|
70
|
-
let braceDepth = 0
|
|
71
|
-
let inQuotes = false
|
|
72
|
-
let inField = false
|
|
73
|
-
|
|
74
|
-
for (let i = 0; i < fieldsStr.length; i++) {
|
|
75
|
-
const char = fieldsStr[i]
|
|
76
|
-
const prevChar = i > 0 ? fieldsStr[i - 1] : ''
|
|
77
|
-
|
|
78
|
-
if (!inField) {
|
|
79
|
-
// Looking for field name
|
|
80
|
-
if (char === '=') {
|
|
81
|
-
currentField = currentValue.trim().toLowerCase()
|
|
82
|
-
currentValue = ''
|
|
83
|
-
inField = true
|
|
84
|
-
} else if (char !== ',' && char !== '}') {
|
|
85
|
-
currentValue += char
|
|
86
|
-
}
|
|
87
|
-
} else {
|
|
88
|
-
// Parsing field value
|
|
89
|
-
if (char === '{' && !inQuotes) {
|
|
90
|
-
braceDepth++
|
|
91
|
-
currentValue += char
|
|
92
|
-
} else if (char === '}' && !inQuotes) {
|
|
93
|
-
if (braceDepth > 0) {
|
|
94
|
-
braceDepth--
|
|
95
|
-
currentValue += char
|
|
96
|
-
} else {
|
|
97
|
-
// End of entry
|
|
98
|
-
if (currentField) {
|
|
99
|
-
fields[currentField] = parseFieldValue(currentValue)
|
|
100
|
-
}
|
|
101
|
-
break
|
|
102
|
-
}
|
|
103
|
-
} else if (char === '"' && prevChar !== '\\') {
|
|
104
|
-
inQuotes = !inQuotes
|
|
105
|
-
currentValue += char
|
|
106
|
-
} else if (char === ',' && braceDepth === 0 && !inQuotes) {
|
|
107
|
-
// End of field
|
|
108
|
-
if (currentField) {
|
|
109
|
-
fields[currentField] = parseFieldValue(currentValue)
|
|
110
|
-
}
|
|
111
|
-
currentField = ''
|
|
112
|
-
currentValue = ''
|
|
113
|
-
inField = false
|
|
114
|
-
} else {
|
|
115
|
-
currentValue += char
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Save last field if any
|
|
121
|
-
if (currentField && currentValue) {
|
|
122
|
-
fields[currentField] = parseFieldValue(currentValue)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Normalize to standard publication object
|
|
126
|
-
const pub = {
|
|
127
|
-
id: citationKey,
|
|
128
|
-
type: entryType,
|
|
129
|
-
...normalizeFields(fields, entryType),
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return pub
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Normalize BibTeX fields to standard publication object
|
|
137
|
-
*
|
|
138
|
-
* @param {Object} fields - Raw BibTeX fields
|
|
139
|
-
* @param {string} entryType - BibTeX entry type
|
|
140
|
-
* @returns {Object} Normalized fields
|
|
141
|
-
*/
|
|
142
|
-
function normalizeFields(fields, entryType) {
|
|
143
|
-
const normalized = {}
|
|
144
|
-
|
|
145
|
-
// Title
|
|
146
|
-
if (fields.title) {
|
|
147
|
-
normalized.title = fields.title
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Authors
|
|
151
|
-
if (fields.author) {
|
|
152
|
-
normalized.authors = parseAuthorString(fields.author)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Editors
|
|
156
|
-
if (fields.editor) {
|
|
157
|
-
normalized.editors = parseAuthorString(fields.editor)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Year
|
|
161
|
-
if (fields.year) {
|
|
162
|
-
normalized.year = parseInt(fields.year, 10) || fields.year
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Journal/booktitle
|
|
166
|
-
if (fields.journal) {
|
|
167
|
-
normalized.journal = fields.journal
|
|
168
|
-
}
|
|
169
|
-
if (fields.booktitle) {
|
|
170
|
-
normalized.bookTitle = fields.booktitle
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Volume, issue, pages
|
|
174
|
-
if (fields.volume) normalized.volume = fields.volume
|
|
175
|
-
if (fields.number) normalized.issue = fields.number
|
|
176
|
-
if (fields.pages) normalized.pages = fields.pages.replace('--', '–')
|
|
177
|
-
|
|
178
|
-
// Publisher info
|
|
179
|
-
if (fields.publisher) normalized.publisher = fields.publisher
|
|
180
|
-
if (fields.address) normalized.address = fields.address
|
|
181
|
-
|
|
182
|
-
// DOI and URL
|
|
183
|
-
if (fields.doi) {
|
|
184
|
-
normalized.doi = fields.doi.replace(/^https?:\/\/doi\.org\//, '')
|
|
185
|
-
}
|
|
186
|
-
if (fields.url) normalized.url = fields.url
|
|
187
|
-
|
|
188
|
-
// ISBN/ISSN
|
|
189
|
-
if (fields.isbn) normalized.isbn = fields.isbn
|
|
190
|
-
if (fields.issn) normalized.issn = fields.issn
|
|
191
|
-
|
|
192
|
-
// Abstract and keywords
|
|
193
|
-
if (fields.abstract) normalized.abstract = fields.abstract
|
|
194
|
-
if (fields.keywords) {
|
|
195
|
-
normalized.keywords = fields.keywords.split(/[,;]/).map((k) => k.trim())
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Thesis-specific
|
|
199
|
-
if (fields.school) normalized.school = fields.school
|
|
200
|
-
if (fields.institution) normalized.institution = fields.institution
|
|
201
|
-
|
|
202
|
-
// Edition
|
|
203
|
-
if (fields.edition) normalized.edition = fields.edition
|
|
204
|
-
|
|
205
|
-
// Note
|
|
206
|
-
if (fields.note) normalized.note = fields.note
|
|
207
|
-
|
|
208
|
-
return normalized
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Parse BibTeX string into array of publication objects
|
|
213
|
-
*
|
|
214
|
-
* @param {string} bibtex - BibTeX string
|
|
215
|
-
* @returns {Array<Object>} Array of publication objects
|
|
216
|
-
*
|
|
217
|
-
* @example
|
|
218
|
-
* const refs = parseBibtex(`
|
|
219
|
-
* @article{smith2024,
|
|
220
|
-
* author = {Smith, John and Doe, Jane},
|
|
221
|
-
* title = {A Study},
|
|
222
|
-
* journal = {Journal of Studies},
|
|
223
|
-
* year = {2024}
|
|
224
|
-
* }
|
|
225
|
-
* `)
|
|
226
|
-
*/
|
|
227
|
-
export function parseBibtex(bibtex) {
|
|
228
|
-
if (!bibtex || typeof bibtex !== 'string') {
|
|
229
|
-
return []
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const results = []
|
|
233
|
-
|
|
234
|
-
// Find all entries (@ followed by type and opening brace)
|
|
235
|
-
const entryPattern = /@(\w+)\s*\{/g
|
|
236
|
-
let match
|
|
237
|
-
const entryStarts = []
|
|
238
|
-
|
|
239
|
-
while ((match = entryPattern.exec(bibtex)) !== null) {
|
|
240
|
-
entryStarts.push(match.index)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Parse each entry
|
|
244
|
-
for (let i = 0; i < entryStarts.length; i++) {
|
|
245
|
-
const start = entryStarts[i]
|
|
246
|
-
const end = i < entryStarts.length - 1 ? entryStarts[i + 1] : bibtex.length
|
|
247
|
-
|
|
248
|
-
const entryStr = bibtex.slice(start, end)
|
|
249
|
-
const parsed = parseEntry(entryStr)
|
|
250
|
-
|
|
251
|
-
if (parsed) {
|
|
252
|
-
results.push(parsed)
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return results
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Escape a string for BibTeX
|
|
261
|
-
*
|
|
262
|
-
* @param {string} str - String to escape
|
|
263
|
-
* @returns {string} Escaped string
|
|
264
|
-
*/
|
|
265
|
-
function escapeBibtex(str) {
|
|
266
|
-
if (!str) return ''
|
|
267
|
-
return String(str)
|
|
268
|
-
.replace(/&/g, '\\&')
|
|
269
|
-
.replace(/%/g, '\\%')
|
|
270
|
-
.replace(/_/g, '\\_')
|
|
271
|
-
.replace(/#/g, '\\#')
|
|
272
|
-
.replace(/\$/g, '\\$')
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Format authors for BibTeX
|
|
277
|
-
*
|
|
278
|
-
* @param {Array} authors - Array of author objects
|
|
279
|
-
* @returns {string} BibTeX author string
|
|
280
|
-
*/
|
|
281
|
-
function formatBibtexAuthors(authors) {
|
|
282
|
-
if (!authors?.length) return ''
|
|
283
|
-
|
|
284
|
-
return authors
|
|
285
|
-
.map((a) => {
|
|
286
|
-
if (typeof a === 'string') return a
|
|
287
|
-
const suffix = a.suffix ? `, ${a.suffix}` : ''
|
|
288
|
-
return a.given ? `${a.family}${suffix}, ${a.given}` : a.family + suffix
|
|
289
|
-
})
|
|
290
|
-
.join(' and ')
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Export a publication object to BibTeX format
|
|
295
|
-
*
|
|
296
|
-
* @param {Object} pub - Publication object
|
|
297
|
-
* @returns {string} BibTeX string
|
|
298
|
-
*
|
|
299
|
-
* @example
|
|
300
|
-
* const bibtex = exportBibtex({
|
|
301
|
-
* id: 'smith2024',
|
|
302
|
-
* type: 'article',
|
|
303
|
-
* authors: [{ family: 'Smith', given: 'John' }],
|
|
304
|
-
* title: 'A Study',
|
|
305
|
-
* journal: 'Journal of Studies',
|
|
306
|
-
* year: 2024
|
|
307
|
-
* })
|
|
308
|
-
*/
|
|
309
|
-
export function exportBibtex(pub) {
|
|
310
|
-
if (!pub) return ''
|
|
311
|
-
|
|
312
|
-
const type = pub.type || pub.entryType || 'misc'
|
|
313
|
-
const key = pub.id || pub.citationKey || generateKey(pub)
|
|
314
|
-
|
|
315
|
-
const fields = []
|
|
316
|
-
|
|
317
|
-
// Authors
|
|
318
|
-
if (pub.authors?.length) {
|
|
319
|
-
fields.push(` author = {${formatBibtexAuthors(pub.authors)}}`)
|
|
320
|
-
} else if (pub.author) {
|
|
321
|
-
fields.push(` author = {${escapeBibtex(pub.author)}}`)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Editors
|
|
325
|
-
if (pub.editors?.length) {
|
|
326
|
-
fields.push(` editor = {${formatBibtexAuthors(pub.editors)}}`)
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Title
|
|
330
|
-
if (pub.title) {
|
|
331
|
-
fields.push(` title = {${escapeBibtex(pub.title)}}`)
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Journal/booktitle
|
|
335
|
-
if (pub.journal) {
|
|
336
|
-
fields.push(` journal = {${escapeBibtex(pub.journal)}}`)
|
|
337
|
-
}
|
|
338
|
-
if (pub.bookTitle) {
|
|
339
|
-
fields.push(` booktitle = {${escapeBibtex(pub.bookTitle)}}`)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Year
|
|
343
|
-
if (pub.year) {
|
|
344
|
-
fields.push(` year = {${pub.year}}`)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Volume, number, pages
|
|
348
|
-
if (pub.volume) fields.push(` volume = {${pub.volume}}`)
|
|
349
|
-
if (pub.issue) fields.push(` number = {${pub.issue}}`)
|
|
350
|
-
if (pub.pages) fields.push(` pages = {${pub.pages.replace('–', '--')}}`)
|
|
351
|
-
|
|
352
|
-
// Publisher
|
|
353
|
-
if (pub.publisher) {
|
|
354
|
-
fields.push(` publisher = {${escapeBibtex(pub.publisher)}}`)
|
|
355
|
-
}
|
|
356
|
-
if (pub.address) {
|
|
357
|
-
fields.push(` address = {${escapeBibtex(pub.address)}}`)
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// DOI and URL
|
|
361
|
-
if (pub.doi) fields.push(` doi = {${pub.doi}}`)
|
|
362
|
-
if (pub.url) fields.push(` url = {${pub.url}}`)
|
|
363
|
-
|
|
364
|
-
// ISBN/ISSN
|
|
365
|
-
if (pub.isbn) fields.push(` isbn = {${pub.isbn}}`)
|
|
366
|
-
if (pub.issn) fields.push(` issn = {${pub.issn}}`)
|
|
367
|
-
|
|
368
|
-
// Abstract
|
|
369
|
-
if (pub.abstract) {
|
|
370
|
-
fields.push(` abstract = {${escapeBibtex(pub.abstract)}}`)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Keywords
|
|
374
|
-
if (pub.keywords?.length) {
|
|
375
|
-
fields.push(` keywords = {${pub.keywords.join(', ')}}`)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// School (for thesis)
|
|
379
|
-
if (pub.school) {
|
|
380
|
-
fields.push(` school = {${escapeBibtex(pub.school)}}`)
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Edition
|
|
384
|
-
if (pub.edition) {
|
|
385
|
-
fields.push(` edition = {${pub.edition}}`)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return `@${type}{${key},\n${fields.join(',\n')}\n}`
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Generate a citation key from publication data
|
|
393
|
-
*
|
|
394
|
-
* @param {Object} pub - Publication object
|
|
395
|
-
* @returns {string} Generated key
|
|
396
|
-
*/
|
|
397
|
-
function generateKey(pub) {
|
|
398
|
-
const author =
|
|
399
|
-
pub.authors?.[0]?.family ||
|
|
400
|
-
(typeof pub.author === 'string' ? pub.author.split(/[,\s]/)[0] : 'unknown')
|
|
401
|
-
const year = pub.year || 'nd'
|
|
402
|
-
const title = pub.title?.split(/\s+/)[0]?.toLowerCase() || ''
|
|
403
|
-
|
|
404
|
-
return `${author.toLowerCase()}${year}${title}`.replace(/[^a-z0-9]/gi, '')
|
|
405
|
-
}
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Author Name Utilities
|
|
3
|
-
*
|
|
4
|
-
* Functions for parsing and formatting author names in various citation styles.
|
|
5
|
-
*
|
|
6
|
-
* @module @uniweb/scholar/bibliography/utils/authors
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Parse a single author name string into structured parts
|
|
11
|
-
*
|
|
12
|
-
* @param {string} name - Author name (e.g., "Smith, John A." or "John A. Smith")
|
|
13
|
-
* @returns {{ family: string, given: string, suffix?: string }}
|
|
14
|
-
*/
|
|
15
|
-
export function parseAuthorName(name) {
|
|
16
|
-
if (!name || typeof name !== 'string') {
|
|
17
|
-
return { family: '', given: '' }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const trimmed = name.trim()
|
|
21
|
-
|
|
22
|
-
// Check for "Last, First" format
|
|
23
|
-
if (trimmed.includes(',')) {
|
|
24
|
-
const parts = trimmed.split(',').map((p) => p.trim())
|
|
25
|
-
const family = parts[0]
|
|
26
|
-
const givenAndSuffix = parts.slice(1).join(' ').trim()
|
|
27
|
-
|
|
28
|
-
// Check for suffix (Jr., Sr., III, etc.)
|
|
29
|
-
const suffixMatch = givenAndSuffix.match(/\s+(Jr\.?|Sr\.?|III?|IV|V)$/i)
|
|
30
|
-
if (suffixMatch) {
|
|
31
|
-
return {
|
|
32
|
-
family,
|
|
33
|
-
given: givenAndSuffix.slice(0, suffixMatch.index).trim(),
|
|
34
|
-
suffix: suffixMatch[1],
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return { family, given: givenAndSuffix }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// "First Last" format - last word is family name
|
|
42
|
-
const words = trimmed.split(/\s+/)
|
|
43
|
-
if (words.length === 1) {
|
|
44
|
-
return { family: words[0], given: '' }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Check if last word is a suffix
|
|
48
|
-
const lastWord = words[words.length - 1]
|
|
49
|
-
if (/^(Jr\.?|Sr\.?|III?|IV|V)$/i.test(lastWord)) {
|
|
50
|
-
return {
|
|
51
|
-
family: words[words.length - 2] || '',
|
|
52
|
-
given: words.slice(0, -2).join(' '),
|
|
53
|
-
suffix: lastWord,
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
family: words[words.length - 1],
|
|
59
|
-
given: words.slice(0, -1).join(' '),
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Parse a string of multiple authors
|
|
65
|
-
*
|
|
66
|
-
* @param {string} authorString - Authors separated by "and", ";", or "&"
|
|
67
|
-
* @returns {Array<{ family: string, given: string, suffix?: string }>}
|
|
68
|
-
*/
|
|
69
|
-
export function parseAuthorString(authorString) {
|
|
70
|
-
if (!authorString || typeof authorString !== 'string') {
|
|
71
|
-
return []
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Split by "and", ";", or "&"
|
|
75
|
-
const authors = authorString
|
|
76
|
-
.split(/\s+and\s+|;\s*|&\s*/i)
|
|
77
|
-
.map((a) => a.trim())
|
|
78
|
-
.filter(Boolean)
|
|
79
|
-
|
|
80
|
-
return authors.map(parseAuthorName)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Get author initials
|
|
85
|
-
*
|
|
86
|
-
* @param {string} given - Given name(s)
|
|
87
|
-
* @param {Object} [options]
|
|
88
|
-
* @param {boolean} [options.periods=true] - Include periods after initials
|
|
89
|
-
* @param {boolean} [options.spaces=true] - Include spaces between initials
|
|
90
|
-
* @returns {string}
|
|
91
|
-
*/
|
|
92
|
-
export function getInitials(given, options = {}) {
|
|
93
|
-
const { periods = true, spaces = true } = options
|
|
94
|
-
|
|
95
|
-
if (!given) return ''
|
|
96
|
-
|
|
97
|
-
const names = given.split(/\s+/)
|
|
98
|
-
const initials = names
|
|
99
|
-
.map((name) => name.charAt(0).toUpperCase())
|
|
100
|
-
.map((initial) => (periods ? `${initial}.` : initial))
|
|
101
|
-
|
|
102
|
-
return initials.join(spaces ? ' ' : '')
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Format authors for APA style
|
|
107
|
-
*
|
|
108
|
-
* @param {Array} authors - Array of parsed author objects
|
|
109
|
-
* @param {number} [maxAuthors=20] - Maximum authors before using et al.
|
|
110
|
-
* @returns {string}
|
|
111
|
-
*/
|
|
112
|
-
export function formatApaAuthors(authors, maxAuthors = 20) {
|
|
113
|
-
if (!authors?.length) return ''
|
|
114
|
-
|
|
115
|
-
const formatOne = (author) => {
|
|
116
|
-
const initials = getInitials(author.given, { periods: true, spaces: true })
|
|
117
|
-
const suffix = author.suffix ? `, ${author.suffix}` : ''
|
|
118
|
-
return initials
|
|
119
|
-
? `${author.family}${suffix}, ${initials}`
|
|
120
|
-
: author.family + suffix
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (authors.length === 1) {
|
|
124
|
-
return formatOne(authors[0])
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (authors.length === 2) {
|
|
128
|
-
return `${formatOne(authors[0])} & ${formatOne(authors[1])}`
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (authors.length <= maxAuthors) {
|
|
132
|
-
const allButLast = authors.slice(0, -1).map(formatOne).join(', ')
|
|
133
|
-
return `${allButLast}, & ${formatOne(authors[authors.length - 1])}`
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// More than maxAuthors: first 19, ..., last
|
|
137
|
-
const first19 = authors.slice(0, 19).map(formatOne).join(', ')
|
|
138
|
-
return `${first19}, ... ${formatOne(authors[authors.length - 1])}`
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Format authors for MLA style
|
|
143
|
-
*
|
|
144
|
-
* @param {Array} authors - Array of parsed author objects
|
|
145
|
-
* @returns {string}
|
|
146
|
-
*/
|
|
147
|
-
export function formatMlaAuthors(authors) {
|
|
148
|
-
if (!authors?.length) return ''
|
|
149
|
-
|
|
150
|
-
const formatFirst = (author) => {
|
|
151
|
-
const suffix = author.suffix ? `, ${author.suffix}` : ''
|
|
152
|
-
return author.given
|
|
153
|
-
? `${author.family}${suffix}, ${author.given}`
|
|
154
|
-
: author.family + suffix
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const formatSubsequent = (author) => {
|
|
158
|
-
const suffix = author.suffix ? `, ${author.suffix}` : ''
|
|
159
|
-
return author.given
|
|
160
|
-
? `${author.given} ${author.family}${suffix}`
|
|
161
|
-
: author.family + suffix
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (authors.length === 1) {
|
|
165
|
-
return formatFirst(authors[0])
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (authors.length === 2) {
|
|
169
|
-
return `${formatFirst(authors[0])}, and ${formatSubsequent(authors[1])}`
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (authors.length === 3) {
|
|
173
|
-
return `${formatFirst(authors[0])}, ${formatSubsequent(authors[1])}, and ${formatSubsequent(authors[2])}`
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// 4+ authors: first author et al.
|
|
177
|
-
return `${formatFirst(authors[0])}, et al.`
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Format authors for Chicago style (author-date)
|
|
182
|
-
*
|
|
183
|
-
* @param {Array} authors - Array of parsed author objects
|
|
184
|
-
* @returns {string}
|
|
185
|
-
*/
|
|
186
|
-
export function formatChicagoAuthors(authors) {
|
|
187
|
-
if (!authors?.length) return ''
|
|
188
|
-
|
|
189
|
-
const formatOne = (author, isFirst = false) => {
|
|
190
|
-
const suffix = author.suffix ? `, ${author.suffix}` : ''
|
|
191
|
-
if (isFirst) {
|
|
192
|
-
return author.given
|
|
193
|
-
? `${author.family}${suffix}, ${author.given}`
|
|
194
|
-
: author.family + suffix
|
|
195
|
-
}
|
|
196
|
-
return author.given
|
|
197
|
-
? `${author.given} ${author.family}${suffix}`
|
|
198
|
-
: author.family + suffix
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (authors.length === 1) {
|
|
202
|
-
return formatOne(authors[0], true)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (authors.length === 2) {
|
|
206
|
-
return `${formatOne(authors[0], true)} and ${formatOne(authors[1])}`
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (authors.length === 3) {
|
|
210
|
-
return `${formatOne(authors[0], true)}, ${formatOne(authors[1])}, and ${formatOne(authors[2])}`
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// 4+ authors: first author et al.
|
|
214
|
-
return `${formatOne(authors[0], true)} et al.`
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Format authors for IEEE style
|
|
219
|
-
*
|
|
220
|
-
* @param {Array} authors - Array of parsed author objects
|
|
221
|
-
* @returns {string}
|
|
222
|
-
*/
|
|
223
|
-
export function formatIeeeAuthors(authors) {
|
|
224
|
-
if (!authors?.length) return ''
|
|
225
|
-
|
|
226
|
-
const formatOne = (author) => {
|
|
227
|
-
const initials = getInitials(author.given, { periods: true, spaces: true })
|
|
228
|
-
const suffix = author.suffix ? ` ${author.suffix}` : ''
|
|
229
|
-
return initials
|
|
230
|
-
? `${initials} ${author.family}${suffix}`
|
|
231
|
-
: author.family + suffix
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (authors.length === 1) {
|
|
235
|
-
return formatOne(authors[0])
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (authors.length === 2) {
|
|
239
|
-
return `${formatOne(authors[0])} and ${formatOne(authors[1])}`
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// 3+ authors in IEEE: first author et al. in citations, all in references
|
|
243
|
-
const allButLast = authors.slice(0, -1).map(formatOne).join(', ')
|
|
244
|
-
return `${allButLast}, and ${formatOne(authors[authors.length - 1])}`
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Format authors for a specific style
|
|
249
|
-
*
|
|
250
|
-
* @param {Array|string} authors - Authors array or string
|
|
251
|
-
* @param {Object} [options]
|
|
252
|
-
* @param {string} [options.style='apa'] - Citation style
|
|
253
|
-
* @param {number} [options.max=20] - Max authors before et al.
|
|
254
|
-
* @returns {string}
|
|
255
|
-
*/
|
|
256
|
-
export function formatAuthors(authors, options = {}) {
|
|
257
|
-
const { style = 'apa', max = 20 } = options
|
|
258
|
-
|
|
259
|
-
// Parse if string
|
|
260
|
-
const parsed =
|
|
261
|
-
typeof authors === 'string' ? parseAuthorString(authors) : authors
|
|
262
|
-
|
|
263
|
-
if (!parsed?.length) return ''
|
|
264
|
-
|
|
265
|
-
switch (style) {
|
|
266
|
-
case 'apa':
|
|
267
|
-
return formatApaAuthors(parsed, max)
|
|
268
|
-
case 'mla':
|
|
269
|
-
return formatMlaAuthors(parsed)
|
|
270
|
-
case 'chicago':
|
|
271
|
-
return formatChicagoAuthors(parsed)
|
|
272
|
-
case 'ieee':
|
|
273
|
-
return formatIeeeAuthors(parsed)
|
|
274
|
-
default:
|
|
275
|
-
return formatApaAuthors(parsed, max)
|
|
276
|
-
}
|
|
277
|
-
}
|