@uniweb/scholar 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +297 -0
- package/package.json +49 -0
- package/src/bibliography/formatters/apa.js +352 -0
- package/src/bibliography/formatters/chicago.js +339 -0
- package/src/bibliography/formatters/ieee.js +332 -0
- package/src/bibliography/formatters/index.js +56 -0
- package/src/bibliography/formatters/mla.js +335 -0
- package/src/bibliography/index.js +42 -0
- package/src/bibliography/parsers/bibtex.js +405 -0
- package/src/bibliography/parsers/index.js +7 -0
- package/src/bibliography/utils/authors.js +277 -0
- package/src/bibliography/utils/dates.js +228 -0
- package/src/bibliography/utils/index.js +8 -0
- package/src/citations/Bibliography.jsx +129 -0
- package/src/citations/Citation.jsx +196 -0
- package/src/citations/CitationProvider.jsx +147 -0
- package/src/citations/index.js +16 -0
- package/src/components/AuthorList.jsx +147 -0
- package/src/components/BibtexBlock.jsx +160 -0
- package/src/components/CiteButton.jsx +175 -0
- package/src/components/DoiLink.jsx +97 -0
- package/src/components/index.js +12 -0
- package/src/index.js +35 -0
- package/src/math/Equation.jsx +163 -0
- package/src/math/EquationRef.jsx +63 -0
- package/src/math/Math.jsx +115 -0
- package/src/math/index.js +12 -0
- package/src/math/katex-loader.js +105 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bibliography Module
|
|
3
|
+
*
|
|
4
|
+
* Utilities for formatting bibliographic references in various citation styles.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/scholar/bibliography
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Formatters
|
|
10
|
+
export {
|
|
11
|
+
formatReference,
|
|
12
|
+
formatApa,
|
|
13
|
+
formatMla,
|
|
14
|
+
formatChicago,
|
|
15
|
+
formatIeee,
|
|
16
|
+
formatters,
|
|
17
|
+
} from './formatters/index.js'
|
|
18
|
+
|
|
19
|
+
// Parsers
|
|
20
|
+
export { parseBibtex, exportBibtex } from './parsers/index.js'
|
|
21
|
+
|
|
22
|
+
// Utilities
|
|
23
|
+
export {
|
|
24
|
+
formatAuthors,
|
|
25
|
+
parseAuthorName,
|
|
26
|
+
parseAuthorString,
|
|
27
|
+
getInitials,
|
|
28
|
+
formatApaAuthors,
|
|
29
|
+
formatMlaAuthors,
|
|
30
|
+
formatChicagoAuthors,
|
|
31
|
+
formatIeeeAuthors,
|
|
32
|
+
} from './utils/authors.js'
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
formatDate,
|
|
36
|
+
parseDate,
|
|
37
|
+
getYear,
|
|
38
|
+
formatApaDate,
|
|
39
|
+
formatMlaDate,
|
|
40
|
+
formatChicagoDate,
|
|
41
|
+
formatIeeeDate,
|
|
42
|
+
} from './utils/dates.js'
|
|
@@ -0,0 +1,405 @@
|
|
|
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
|
+
}
|