@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,277 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date Formatting Utilities
|
|
3
|
+
*
|
|
4
|
+
* Functions for formatting dates in various citation styles.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/scholar/bibliography/utils/dates
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Month names for formatting
|
|
11
|
+
*/
|
|
12
|
+
const MONTHS_FULL = [
|
|
13
|
+
'January',
|
|
14
|
+
'February',
|
|
15
|
+
'March',
|
|
16
|
+
'April',
|
|
17
|
+
'May',
|
|
18
|
+
'June',
|
|
19
|
+
'July',
|
|
20
|
+
'August',
|
|
21
|
+
'September',
|
|
22
|
+
'October',
|
|
23
|
+
'November',
|
|
24
|
+
'December',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const MONTHS_SHORT = [
|
|
28
|
+
'Jan.',
|
|
29
|
+
'Feb.',
|
|
30
|
+
'Mar.',
|
|
31
|
+
'Apr.',
|
|
32
|
+
'May',
|
|
33
|
+
'June',
|
|
34
|
+
'July',
|
|
35
|
+
'Aug.',
|
|
36
|
+
'Sep.',
|
|
37
|
+
'Oct.',
|
|
38
|
+
'Nov.',
|
|
39
|
+
'Dec.',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse a date from various formats
|
|
44
|
+
*
|
|
45
|
+
* @param {string|number|Date|Object} date - Date input
|
|
46
|
+
* @returns {{ year?: number, month?: number, day?: number }}
|
|
47
|
+
*/
|
|
48
|
+
export function parseDate(date) {
|
|
49
|
+
if (!date) return {}
|
|
50
|
+
|
|
51
|
+
// Already structured
|
|
52
|
+
if (typeof date === 'object' && !(date instanceof Date)) {
|
|
53
|
+
return {
|
|
54
|
+
year: date.year ?? date.Year,
|
|
55
|
+
month: date.month ?? date.Month,
|
|
56
|
+
day: date.day ?? date.Day,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// JavaScript Date
|
|
61
|
+
if (date instanceof Date) {
|
|
62
|
+
return {
|
|
63
|
+
year: date.getFullYear(),
|
|
64
|
+
month: date.getMonth() + 1,
|
|
65
|
+
day: date.getDate(),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Number (year only)
|
|
70
|
+
if (typeof date === 'number') {
|
|
71
|
+
return { year: date }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// String parsing
|
|
75
|
+
const str = String(date).trim()
|
|
76
|
+
|
|
77
|
+
// ISO format: YYYY-MM-DD
|
|
78
|
+
const isoMatch = str.match(/^(\d{4})(?:-(\d{1,2})(?:-(\d{1,2}))?)?/)
|
|
79
|
+
if (isoMatch) {
|
|
80
|
+
return {
|
|
81
|
+
year: parseInt(isoMatch[1], 10),
|
|
82
|
+
month: isoMatch[2] ? parseInt(isoMatch[2], 10) : undefined,
|
|
83
|
+
day: isoMatch[3] ? parseInt(isoMatch[3], 10) : undefined,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Year only
|
|
88
|
+
const yearMatch = str.match(/^\d{4}$/)
|
|
89
|
+
if (yearMatch) {
|
|
90
|
+
return { year: parseInt(str, 10) }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Month Year: "January 2024", "Jan 2024"
|
|
94
|
+
const monthYearMatch = str.match(
|
|
95
|
+
/^(\w+)\s+(\d{4})$/
|
|
96
|
+
)
|
|
97
|
+
if (monthYearMatch) {
|
|
98
|
+
const monthName = monthYearMatch[1].toLowerCase()
|
|
99
|
+
const monthIndex = MONTHS_FULL.findIndex(
|
|
100
|
+
(m) => m.toLowerCase().startsWith(monthName.slice(0, 3))
|
|
101
|
+
)
|
|
102
|
+
return {
|
|
103
|
+
year: parseInt(monthYearMatch[2], 10),
|
|
104
|
+
month: monthIndex !== -1 ? monthIndex + 1 : undefined,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Format date for APA style
|
|
113
|
+
*
|
|
114
|
+
* @param {Object} date - Parsed date object
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
export function formatApaDate(date) {
|
|
118
|
+
const parsed = parseDate(date)
|
|
119
|
+
|
|
120
|
+
if (!parsed.year) return 'n.d.'
|
|
121
|
+
|
|
122
|
+
if (parsed.month && parsed.day) {
|
|
123
|
+
return `${parsed.year}, ${MONTHS_FULL[parsed.month - 1]} ${parsed.day}`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (parsed.month) {
|
|
127
|
+
return `${parsed.year}, ${MONTHS_FULL[parsed.month - 1]}`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return String(parsed.year)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Format date for MLA style
|
|
135
|
+
*
|
|
136
|
+
* @param {Object} date - Parsed date object
|
|
137
|
+
* @returns {string}
|
|
138
|
+
*/
|
|
139
|
+
export function formatMlaDate(date) {
|
|
140
|
+
const parsed = parseDate(date)
|
|
141
|
+
|
|
142
|
+
if (!parsed.year) return ''
|
|
143
|
+
|
|
144
|
+
if (parsed.month && parsed.day) {
|
|
145
|
+
return `${parsed.day} ${MONTHS_SHORT[parsed.month - 1]} ${parsed.year}`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (parsed.month) {
|
|
149
|
+
return `${MONTHS_SHORT[parsed.month - 1]} ${parsed.year}`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return String(parsed.year)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Format date for Chicago style
|
|
157
|
+
*
|
|
158
|
+
* @param {Object} date - Parsed date object
|
|
159
|
+
* @returns {string}
|
|
160
|
+
*/
|
|
161
|
+
export function formatChicagoDate(date) {
|
|
162
|
+
const parsed = parseDate(date)
|
|
163
|
+
|
|
164
|
+
if (!parsed.year) return 'n.d.'
|
|
165
|
+
|
|
166
|
+
if (parsed.month && parsed.day) {
|
|
167
|
+
return `${MONTHS_FULL[parsed.month - 1]} ${parsed.day}, ${parsed.year}`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (parsed.month) {
|
|
171
|
+
return `${MONTHS_FULL[parsed.month - 1]} ${parsed.year}`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return String(parsed.year)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Format date for IEEE style
|
|
179
|
+
*
|
|
180
|
+
* @param {Object} date - Parsed date object
|
|
181
|
+
* @returns {string}
|
|
182
|
+
*/
|
|
183
|
+
export function formatIeeeDate(date) {
|
|
184
|
+
const parsed = parseDate(date)
|
|
185
|
+
|
|
186
|
+
if (!parsed.year) return ''
|
|
187
|
+
|
|
188
|
+
if (parsed.month) {
|
|
189
|
+
return `${MONTHS_SHORT[parsed.month - 1]} ${parsed.year}`
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return String(parsed.year)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Format date for a specific style
|
|
197
|
+
*
|
|
198
|
+
* @param {any} date - Date input
|
|
199
|
+
* @param {Object} [options]
|
|
200
|
+
* @param {string} [options.style='apa'] - Citation style
|
|
201
|
+
* @returns {string}
|
|
202
|
+
*/
|
|
203
|
+
export function formatDate(date, options = {}) {
|
|
204
|
+
const { style = 'apa' } = options
|
|
205
|
+
|
|
206
|
+
switch (style) {
|
|
207
|
+
case 'apa':
|
|
208
|
+
return formatApaDate(date)
|
|
209
|
+
case 'mla':
|
|
210
|
+
return formatMlaDate(date)
|
|
211
|
+
case 'chicago':
|
|
212
|
+
return formatChicagoDate(date)
|
|
213
|
+
case 'ieee':
|
|
214
|
+
return formatIeeeDate(date)
|
|
215
|
+
default:
|
|
216
|
+
return formatApaDate(date)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get year from date
|
|
222
|
+
*
|
|
223
|
+
* @param {any} date - Date input
|
|
224
|
+
* @returns {number|undefined}
|
|
225
|
+
*/
|
|
226
|
+
export function getYear(date) {
|
|
227
|
+
return parseDate(date).year
|
|
228
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bibliography Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a formatted list of references that have been cited in the document.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/scholar/citations/Bibliography
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useMemo } from 'react'
|
|
10
|
+
import { useCitation } from './CitationProvider.jsx'
|
|
11
|
+
import { formatReference } from '../bibliography/formatters/index.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Bibliography - Renders the list of cited references
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} props
|
|
17
|
+
* @param {string} [props.title='References'] - Section title
|
|
18
|
+
* @param {boolean} [props.showTitle=true] - Whether to show the title
|
|
19
|
+
* @param {boolean} [props.showAll=false] - Show all references, not just cited ones
|
|
20
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
21
|
+
* @param {string} [props.itemClassName] - CSS classes for each item
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <Bibliography title="Works Cited" />
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* // Show all references including uncited ones
|
|
28
|
+
* <Bibliography showAll />
|
|
29
|
+
*/
|
|
30
|
+
export function Bibliography({
|
|
31
|
+
title = 'References',
|
|
32
|
+
showTitle = true,
|
|
33
|
+
showAll = false,
|
|
34
|
+
className,
|
|
35
|
+
itemClassName,
|
|
36
|
+
...props
|
|
37
|
+
}) {
|
|
38
|
+
const { getCitedReferences, references, style, cited } = useCitation()
|
|
39
|
+
|
|
40
|
+
// Get references to display
|
|
41
|
+
const refsToShow = useMemo(() => {
|
|
42
|
+
if (showAll) {
|
|
43
|
+
return references
|
|
44
|
+
}
|
|
45
|
+
return getCitedReferences()
|
|
46
|
+
}, [showAll, references, getCitedReferences])
|
|
47
|
+
|
|
48
|
+
// Format references
|
|
49
|
+
const formattedRefs = useMemo(() => {
|
|
50
|
+
return refsToShow.map((ref) => {
|
|
51
|
+
const number = cited.get(ref.id) || cited.get(ref.citationKey)
|
|
52
|
+
return {
|
|
53
|
+
id: ref.id || ref.citationKey,
|
|
54
|
+
number,
|
|
55
|
+
formatted: formatReference(ref, { style }),
|
|
56
|
+
reference: ref,
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}, [refsToShow, style, cited])
|
|
60
|
+
|
|
61
|
+
if (!formattedRefs.length) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Use numbered list for IEEE style
|
|
66
|
+
const isNumeric = style === 'ieee'
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<section className={className} {...props}>
|
|
70
|
+
{showTitle && (
|
|
71
|
+
<h2
|
|
72
|
+
style={{
|
|
73
|
+
fontSize: '1.5rem',
|
|
74
|
+
fontWeight: '600',
|
|
75
|
+
marginBottom: '1rem',
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{title}
|
|
79
|
+
</h2>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
{isNumeric ? (
|
|
83
|
+
<ol
|
|
84
|
+
style={{
|
|
85
|
+
listStyleType: 'decimal',
|
|
86
|
+
paddingLeft: '2rem',
|
|
87
|
+
lineHeight: '1.8',
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{formattedRefs.map((ref) => (
|
|
91
|
+
<li
|
|
92
|
+
key={ref.id}
|
|
93
|
+
id={`ref-${ref.id}`}
|
|
94
|
+
className={itemClassName}
|
|
95
|
+
style={{
|
|
96
|
+
marginBottom: '0.75rem',
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
{ref.formatted}
|
|
100
|
+
</li>
|
|
101
|
+
))}
|
|
102
|
+
</ol>
|
|
103
|
+
) : (
|
|
104
|
+
<div
|
|
105
|
+
style={{
|
|
106
|
+
lineHeight: '1.8',
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{formattedRefs.map((ref) => (
|
|
110
|
+
<p
|
|
111
|
+
key={ref.id}
|
|
112
|
+
id={`ref-${ref.id}`}
|
|
113
|
+
className={itemClassName}
|
|
114
|
+
style={{
|
|
115
|
+
marginBottom: '0.75rem',
|
|
116
|
+
paddingLeft: '2rem',
|
|
117
|
+
textIndent: '-2rem',
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
{ref.formatted}
|
|
121
|
+
</p>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</section>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default Bibliography
|