@uniweb/scholar 0.2.0 → 0.2.1
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/package.json +5 -1
- 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 +9 -3
- 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
|
@@ -1,44 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Citation Component
|
|
3
3
|
*
|
|
4
|
-
* Renders an inline citation
|
|
4
|
+
* Renders an inline citation using @citestyle/registry for proper
|
|
5
|
+
* author-date formatting, year-suffix disambiguation, numeric citations,
|
|
6
|
+
* and cite collapsing.
|
|
5
7
|
*
|
|
6
8
|
* @module @uniweb/scholar/citations/Citation
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import React, { useMemo } from 'react'
|
|
10
12
|
import { useCitation } from './CitationProvider.jsx'
|
|
11
|
-
import { getYear } from '../bibliography/utils/dates.js'
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Format author names for inline citation
|
|
15
|
-
*
|
|
16
|
-
* @param {Array} authors - Array of author objects
|
|
17
|
-
* @param {string} style - Citation style
|
|
18
|
-
* @returns {string}
|
|
19
|
-
*/
|
|
20
|
-
function formatInlineAuthors(authors, style) {
|
|
21
|
-
if (!authors?.length) return ''
|
|
22
|
-
|
|
23
|
-
const getName = (a) => {
|
|
24
|
-
if (typeof a === 'string') return a.split(',')[0].trim()
|
|
25
|
-
return a.family || a.lastName || ''
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (authors.length === 1) {
|
|
29
|
-
return getName(authors[0])
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (authors.length === 2) {
|
|
33
|
-
if (style === 'apa') {
|
|
34
|
-
return `${getName(authors[0])} & ${getName(authors[1])}`
|
|
35
|
-
}
|
|
36
|
-
return `${getName(authors[0])} and ${getName(authors[1])}`
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// 3+ authors
|
|
40
|
-
return `${getName(authors[0])} et al.`
|
|
41
|
-
}
|
|
42
13
|
|
|
43
14
|
/**
|
|
44
15
|
* Citation - Inline citation component
|
|
@@ -46,11 +17,10 @@ function formatInlineAuthors(authors, style) {
|
|
|
46
17
|
* @param {Object} props
|
|
47
18
|
* @param {string} [props.id] - Single reference ID to cite
|
|
48
19
|
* @param {Array<string>} [props.ids] - Multiple reference IDs to cite
|
|
49
|
-
* @param {string} [props.
|
|
20
|
+
* @param {string} [props.locator] - Page, chapter, or other locator
|
|
21
|
+
* @param {string} [props.label] - Locator label (e.g., 'page', 'chapter')
|
|
50
22
|
* @param {string} [props.prefix] - Text before citation (e.g., "see")
|
|
51
23
|
* @param {string} [props.suffix] - Text after citation
|
|
52
|
-
* @param {boolean} [props.suppressAuthor] - Show only year (for narrative citations)
|
|
53
|
-
* @param {string} [props.format='parenthetical'] - Format: 'parenthetical', 'narrative', or 'numeric'
|
|
54
24
|
* @param {string} [props.className] - Additional CSS classes
|
|
55
25
|
*
|
|
56
26
|
* @example
|
|
@@ -58,138 +28,58 @@ function formatInlineAuthors(authors, style) {
|
|
|
58
28
|
* <Citation id="smith2024" />
|
|
59
29
|
*
|
|
60
30
|
* @example
|
|
61
|
-
* //
|
|
62
|
-
* <Citation id="smith2024"
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* // With page number: (Smith, 2024, p. 42)
|
|
66
|
-
* <Citation id="smith2024" page="42" />
|
|
31
|
+
* // With page locator: (Smith, 2024, p. 42)
|
|
32
|
+
* <Citation id="smith2024" locator="42" label="page" />
|
|
67
33
|
*
|
|
68
34
|
* @example
|
|
69
35
|
* // Multiple sources: (Smith, 2024; Jones, 2023)
|
|
70
36
|
* <Citation ids={['smith2024', 'jones2023']} />
|
|
71
37
|
*
|
|
72
38
|
* @example
|
|
73
|
-
* //
|
|
74
|
-
* <Citation id="smith2024"
|
|
39
|
+
* // With prefix: (see Smith, 2024)
|
|
40
|
+
* <Citation id="smith2024" prefix="see" />
|
|
75
41
|
*/
|
|
76
42
|
export function Citation({
|
|
77
43
|
id,
|
|
78
44
|
ids,
|
|
79
|
-
|
|
45
|
+
locator,
|
|
46
|
+
label = 'page',
|
|
80
47
|
prefix,
|
|
81
48
|
suffix,
|
|
82
|
-
suppressAuthor = false,
|
|
83
|
-
format = 'parenthetical',
|
|
84
49
|
className,
|
|
85
50
|
...props
|
|
86
51
|
}) {
|
|
87
|
-
const { cite
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Numeric format (IEEE style)
|
|
110
|
-
if (format === 'numeric' || style === 'ieee') {
|
|
111
|
-
const numbers = citations.map((c) => c.number).join(', ')
|
|
112
|
-
return `[${numbers}]`
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Author-date formats
|
|
116
|
-
const parts = citations.map((c) => {
|
|
117
|
-
if (!c.reference) return c.id
|
|
118
|
-
|
|
119
|
-
const authors = c.reference.authors || []
|
|
120
|
-
const year = getYear(c.reference.year || c.reference.date)
|
|
121
|
-
const authorStr = formatInlineAuthors(authors, style)
|
|
122
|
-
|
|
123
|
-
if (suppressAuthor) {
|
|
124
|
-
return year || 'n.d.'
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!year) {
|
|
128
|
-
return authorStr || c.id
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return `${authorStr}, ${year}`
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
let text = parts.join('; ')
|
|
135
|
-
|
|
136
|
-
// Add page number
|
|
137
|
-
if (page) {
|
|
138
|
-
const pagePrefix = page.includes('-') || page.includes('–') ? 'pp.' : 'p.'
|
|
139
|
-
text += `, ${pagePrefix} ${page}`
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Add prefix/suffix
|
|
143
|
-
if (prefix) text = `${prefix} ${text}`
|
|
144
|
-
if (suffix) text = `${text}, ${suffix}`
|
|
145
|
-
|
|
146
|
-
// Wrap in parentheses for parenthetical format
|
|
147
|
-
if (format === 'parenthetical') {
|
|
148
|
-
return `(${text})`
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return text
|
|
152
|
-
}, [citations, format, style, suppressAuthor, page, prefix, suffix])
|
|
153
|
-
|
|
154
|
-
// For narrative format, we need to split author and year
|
|
155
|
-
if (format === 'narrative' && citations.length === 1 && citations[0].reference) {
|
|
156
|
-
const c = citations[0]
|
|
157
|
-
const authors = c.reference.authors || []
|
|
158
|
-
const year = getYear(c.reference.year || c.reference.date)
|
|
159
|
-
const authorStr = formatInlineAuthors(authors, style)
|
|
160
|
-
|
|
161
|
-
let yearPart = year || 'n.d.'
|
|
162
|
-
if (page) {
|
|
163
|
-
const pagePrefix = page.includes('-') || page.includes('–') ? 'pp.' : 'p.'
|
|
164
|
-
yearPart += `, ${pagePrefix} ${page}`
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return (
|
|
168
|
-
<span className={className} {...props}>
|
|
169
|
-
{prefix && `${prefix} `}
|
|
170
|
-
{authorStr}{' '}
|
|
171
|
-
<a
|
|
172
|
-
href={`#ref-${c.id}`}
|
|
173
|
-
style={{ textDecoration: 'none' }}
|
|
174
|
-
>
|
|
175
|
-
({yearPart})
|
|
176
|
-
</a>
|
|
177
|
-
{suffix && ` ${suffix}`}
|
|
178
|
-
</span>
|
|
179
|
-
)
|
|
180
|
-
}
|
|
52
|
+
const { cite } = useCitation()
|
|
53
|
+
|
|
54
|
+
// Build cite references array
|
|
55
|
+
const citeRefs = useMemo(() => {
|
|
56
|
+
const allIds = ids?.length ? ids : id ? [id] : []
|
|
57
|
+
return allIds.map((refId, i) => ({
|
|
58
|
+
id: refId,
|
|
59
|
+
// Only the first cite gets the locator/prefix/suffix
|
|
60
|
+
...(i === 0 && locator ? { locator, label } : {}),
|
|
61
|
+
...(i === 0 && prefix ? { prefix } : {}),
|
|
62
|
+
...(i === allIds.length - 1 && suffix ? { suffix } : {}),
|
|
63
|
+
}))
|
|
64
|
+
}, [id, ids, locator, label, prefix, suffix])
|
|
65
|
+
|
|
66
|
+
// Format through the registry — handles disambiguation, collapsing, numbering
|
|
67
|
+
const result = useMemo(() => {
|
|
68
|
+
if (!citeRefs.length) return { text: '??', html: '??' }
|
|
69
|
+
return cite(citeRefs)
|
|
70
|
+
}, [citeRefs, cite])
|
|
71
|
+
|
|
72
|
+
// Link to bibliography entry
|
|
73
|
+
const firstId = citeRefs[0]?.id
|
|
181
74
|
|
|
182
|
-
// Single link for all citations
|
|
183
|
-
const firstId = citations[0]?.id
|
|
184
75
|
return (
|
|
185
76
|
<a
|
|
186
77
|
href={firstId ? `#ref-${firstId}` : undefined}
|
|
187
78
|
className={className}
|
|
188
79
|
style={{ textDecoration: 'none' }}
|
|
80
|
+
dangerouslySetInnerHTML={{ __html: result.html }}
|
|
189
81
|
{...props}
|
|
190
|
-
|
|
191
|
-
{citationText}
|
|
192
|
-
</a>
|
|
82
|
+
/>
|
|
193
83
|
)
|
|
194
84
|
}
|
|
195
85
|
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Citation Provider Component
|
|
3
3
|
*
|
|
4
|
-
* Provides context
|
|
5
|
-
*
|
|
4
|
+
* Provides citation context powered by @citestyle/registry.
|
|
5
|
+
* Handles year-suffix disambiguation, bibliography sorting, cite collapsing,
|
|
6
|
+
* and numeric citation numbering — all automatically.
|
|
6
7
|
*
|
|
7
8
|
* @module @uniweb/scholar/citations/CitationProvider
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
import React, { createContext, useContext,
|
|
11
|
-
import {
|
|
11
|
+
import React, { createContext, useContext, useMemo, useCallback } from 'react'
|
|
12
|
+
import { createRegistry } from '@citestyle/registry'
|
|
13
|
+
import { parseBibtex } from '../bibliography/parsers/index.js'
|
|
14
|
+
import { compiledStyles } from '../bibliography/formatters/index.js'
|
|
15
|
+
import { toCslJson } from '../bibliography/normalize.js'
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* Citation context
|
|
@@ -18,7 +22,15 @@ export const CitationContext = createContext(null)
|
|
|
18
22
|
/**
|
|
19
23
|
* Hook to access citation context
|
|
20
24
|
*
|
|
21
|
-
* @returns {{
|
|
25
|
+
* @returns {{
|
|
26
|
+
* registry: Object,
|
|
27
|
+
* cite: Function,
|
|
28
|
+
* getBibliography: Function,
|
|
29
|
+
* getReference: Function,
|
|
30
|
+
* references: Array,
|
|
31
|
+
* style: string,
|
|
32
|
+
* styleMeta: Object
|
|
33
|
+
* }}
|
|
22
34
|
*/
|
|
23
35
|
export function useCitation() {
|
|
24
36
|
const ctx = useContext(CitationContext)
|
|
@@ -28,113 +40,99 @@ export function useCitation() {
|
|
|
28
40
|
return ctx
|
|
29
41
|
}
|
|
30
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Normalize references to CSL-JSON items.
|
|
45
|
+
* Accepts: BibTeX string, array of Scholar pubs, array of CSL-JSON items.
|
|
46
|
+
*/
|
|
47
|
+
function normalizeReferences(references) {
|
|
48
|
+
if (!references) return []
|
|
49
|
+
|
|
50
|
+
// BibTeX string → parse to CSL-JSON
|
|
51
|
+
if (typeof references === 'string') {
|
|
52
|
+
return parseBibtex(references)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!Array.isArray(references)) return []
|
|
56
|
+
|
|
57
|
+
// Detect if items are already CSL-JSON (have `issued` or `container-title`)
|
|
58
|
+
// or Scholar-shaped (have `authors`, `journal`, `year` as number)
|
|
59
|
+
return references.map((ref) => {
|
|
60
|
+
if (ref.issued || ref['container-title'] || ref['date-parts']) {
|
|
61
|
+
// Already CSL-JSON — ensure it has an id
|
|
62
|
+
return { id: ref.id || ref.citationKey || 'unknown', ...ref }
|
|
63
|
+
}
|
|
64
|
+
// Scholar-shaped → convert
|
|
65
|
+
return toCslJson(ref)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
31
69
|
/**
|
|
32
70
|
* CitationProvider - Manages citation state for a document
|
|
33
71
|
*
|
|
72
|
+
* Uses @citestyle/registry for proper disambiguation, sorting, and numbering.
|
|
73
|
+
*
|
|
34
74
|
* @param {Object} props
|
|
35
|
-
* @param {Array|string} props.references - Array of
|
|
36
|
-
* @param {string} [props.style='apa'] - Citation style
|
|
75
|
+
* @param {Array|string} props.references - Array of publications (Scholar or CSL-JSON) or BibTeX string
|
|
76
|
+
* @param {string} [props.style='apa'] - Citation style name
|
|
37
77
|
* @param {React.ReactNode} props.children
|
|
38
78
|
*
|
|
39
79
|
* @example
|
|
40
|
-
* <CitationProvider references={
|
|
80
|
+
* <CitationProvider references={pubs} style="apa">
|
|
41
81
|
* <p>According to <Citation id="smith2024" />, this is true.</p>
|
|
42
82
|
* <Bibliography />
|
|
43
83
|
* </CitationProvider>
|
|
44
84
|
*/
|
|
45
85
|
export function CitationProvider({ references, style = 'apa', children }) {
|
|
46
|
-
//
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
86
|
+
// Resolve compiled style
|
|
87
|
+
const compiled = useMemo(
|
|
88
|
+
() => compiledStyles[style.toLowerCase()] || compiledStyles.apa,
|
|
89
|
+
[style]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// Normalize all references to CSL-JSON
|
|
93
|
+
const cslItems = useMemo(
|
|
94
|
+
() => normalizeReferences(references),
|
|
95
|
+
[references]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// Create registry — handles disambiguation, sorting, numbering
|
|
99
|
+
const registry = useMemo(() => {
|
|
100
|
+
const reg = createRegistry(compiled)
|
|
101
|
+
if (cslItems.length) {
|
|
102
|
+
reg.addItems(cslItems)
|
|
57
103
|
}
|
|
58
|
-
return
|
|
59
|
-
}, [
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
const refsById = useMemo(() => {
|
|
63
|
-
const map = new Map()
|
|
64
|
-
refs.forEach((ref) => {
|
|
65
|
-
if (ref.id) map.set(ref.id, ref)
|
|
66
|
-
if (ref.citationKey) map.set(ref.citationKey, ref)
|
|
67
|
-
})
|
|
68
|
-
return map
|
|
69
|
-
}, [refs])
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Cite a reference by id
|
|
73
|
-
* Returns the citation number (order in which it was first cited)
|
|
74
|
-
*/
|
|
104
|
+
return reg
|
|
105
|
+
}, [compiled, cslItems])
|
|
106
|
+
|
|
107
|
+
// Convenience: format an inline citation cluster
|
|
75
108
|
const cite = useCallback(
|
|
76
|
-
(
|
|
77
|
-
|
|
78
|
-
if (cited.has(id)) {
|
|
79
|
-
return cited.get(id)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Add to cited list
|
|
83
|
-
const number = cited.size + 1
|
|
84
|
-
setCited((prev) => {
|
|
85
|
-
if (prev.has(id)) return prev
|
|
86
|
-
const next = new Map(prev)
|
|
87
|
-
next.set(id, number)
|
|
88
|
-
return next
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
return number
|
|
92
|
-
},
|
|
93
|
-
[cited]
|
|
109
|
+
(cites, ctx) => registry.cite(cites, ctx),
|
|
110
|
+
[registry]
|
|
94
111
|
)
|
|
95
112
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
(id) => {
|
|
101
|
-
return {
|
|
102
|
-
number: cited.get(id),
|
|
103
|
-
reference: refsById.get(id),
|
|
104
|
-
isCited: cited.has(id),
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
[cited, refsById]
|
|
113
|
+
// Convenience: get the full formatted bibliography
|
|
114
|
+
const getBibliography = useCallback(
|
|
115
|
+
(ctx) => registry.getBibliography(ctx),
|
|
116
|
+
[registry]
|
|
108
117
|
)
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
* Get reference by id
|
|
112
|
-
*/
|
|
119
|
+
// Look up a CSL-JSON item by id
|
|
113
120
|
const getReference = useCallback(
|
|
114
|
-
(id) =>
|
|
115
|
-
[
|
|
121
|
+
(id) => registry.getItem(id),
|
|
122
|
+
[registry]
|
|
116
123
|
)
|
|
117
124
|
|
|
118
|
-
/**
|
|
119
|
-
* Get all cited references in citation order
|
|
120
|
-
*/
|
|
121
|
-
const getCitedReferences = useCallback(() => {
|
|
122
|
-
const sorted = Array.from(cited.entries()).sort((a, b) => a[1] - b[1])
|
|
123
|
-
return sorted.map(([id]) => refsById.get(id)).filter(Boolean)
|
|
124
|
-
}, [cited, refsById])
|
|
125
|
-
|
|
126
125
|
const value = useMemo(
|
|
127
126
|
() => ({
|
|
127
|
+
registry,
|
|
128
128
|
cite,
|
|
129
|
-
|
|
129
|
+
getBibliography,
|
|
130
130
|
getReference,
|
|
131
|
-
|
|
132
|
-
references: refs,
|
|
133
|
-
refsById,
|
|
131
|
+
references: cslItems,
|
|
134
132
|
style,
|
|
135
|
-
|
|
133
|
+
styleMeta: compiled.meta,
|
|
136
134
|
}),
|
|
137
|
-
[cite,
|
|
135
|
+
[registry, cite, getBibliography, getReference, cslItems, style, compiled]
|
|
138
136
|
)
|
|
139
137
|
|
|
140
138
|
return (
|
|
@@ -2,15 +2,66 @@
|
|
|
2
2
|
* AuthorList Component
|
|
3
3
|
*
|
|
4
4
|
* Renders a formatted list of authors with optional ORCID links.
|
|
5
|
+
* Works with both Scholar-shaped author objects and CSL-JSON name objects.
|
|
5
6
|
*
|
|
6
7
|
* @module @uniweb/scholar/components/AuthorList
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import React from 'react'
|
|
10
|
-
import { formatAuthors } from '../bibliography/utils/authors.js'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Format a single author name for display.
|
|
14
|
+
*
|
|
15
|
+
* @param {{ family: string, given?: string, suffix?: string }} author
|
|
16
|
+
* @param {boolean} invertFirst - Show as "Last, First" (true) or "First Last" (false)
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
function formatName(author, invertFirst = false) {
|
|
20
|
+
if (typeof author === 'string') return author
|
|
21
|
+
|
|
22
|
+
const family = author.family || ''
|
|
23
|
+
const given = author.given || ''
|
|
24
|
+
const suffix = author.suffix ? `, ${author.suffix}` : ''
|
|
25
|
+
|
|
26
|
+
if (!given) return family + suffix
|
|
27
|
+
|
|
28
|
+
return invertFirst
|
|
29
|
+
? `${family}${suffix}, ${given}`
|
|
30
|
+
: `${given} ${family}${suffix}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format an array of authors into a display string.
|
|
35
|
+
*
|
|
36
|
+
* @param {Array} authors - Author objects
|
|
37
|
+
* @param {Object} options
|
|
38
|
+
* @param {string} [options.style='apa']
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
function formatAuthorsText(authors, { style = 'apa' } = {}) {
|
|
42
|
+
if (!authors?.length) return ''
|
|
43
|
+
|
|
44
|
+
const useAmpersand = style === 'apa'
|
|
45
|
+
const invertFirst = style === 'apa' || style === 'chicago' || style === 'mla'
|
|
46
|
+
|
|
47
|
+
if (authors.length === 1) {
|
|
48
|
+
return formatName(authors[0], invertFirst)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (authors.length === 2) {
|
|
52
|
+
const sep = useAmpersand ? ' & ' : ' and '
|
|
53
|
+
return formatName(authors[0], invertFirst) + sep + formatName(authors[1])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 3+ authors
|
|
57
|
+
const allButLast = authors.slice(0, -1).map((a, i) => formatName(a, i === 0 && invertFirst))
|
|
58
|
+
const last = formatName(authors[authors.length - 1])
|
|
59
|
+
const sep = useAmpersand ? ', & ' : ', and '
|
|
60
|
+
return allButLast.join(', ') + sep + last
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* AuthorList - Formatted author names with optional ORCID links
|
|
14
65
|
*
|
|
15
66
|
* @param {Object} props
|
|
16
67
|
* @param {Array|string} props.authors - Authors array or string
|
|
@@ -21,9 +72,6 @@ import { formatAuthors } from '../bibliography/utils/authors.js'
|
|
|
21
72
|
*
|
|
22
73
|
* @example
|
|
23
74
|
* <AuthorList authors={[{ family: 'Smith', given: 'John', orcid: '0000-0001-2345-6789' }]} />
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* <AuthorList authors="Smith, John and Doe, Jane" style="mla" />
|
|
27
75
|
*/
|
|
28
76
|
export function AuthorList({
|
|
29
77
|
authors,
|
|
@@ -33,71 +81,48 @@ export function AuthorList({
|
|
|
33
81
|
className,
|
|
34
82
|
...props
|
|
35
83
|
}) {
|
|
36
|
-
//
|
|
84
|
+
// String authors — just display
|
|
37
85
|
if (typeof authors === 'string') {
|
|
38
86
|
return (
|
|
39
87
|
<span className={className} {...props}>
|
|
40
|
-
{
|
|
88
|
+
{authors}
|
|
41
89
|
</span>
|
|
42
90
|
)
|
|
43
91
|
}
|
|
44
92
|
|
|
45
|
-
|
|
46
|
-
if (!authors?.length) {
|
|
47
|
-
return null
|
|
48
|
-
}
|
|
93
|
+
if (!authors?.length) return null
|
|
49
94
|
|
|
50
|
-
//
|
|
95
|
+
// If no ORCID to show, use simple formatted string
|
|
51
96
|
const hasOrcid = showOrcid && authors.some((a) => a.orcid)
|
|
52
97
|
|
|
53
|
-
// If no ORCID to show, use simple formatted string
|
|
54
98
|
if (!hasOrcid) {
|
|
55
99
|
return (
|
|
56
100
|
<span className={className} {...props}>
|
|
57
|
-
{
|
|
101
|
+
{formatAuthorsText(authors, { style })}
|
|
58
102
|
</span>
|
|
59
103
|
)
|
|
60
104
|
}
|
|
61
105
|
|
|
62
106
|
// Render with ORCID links
|
|
107
|
+
const useAmpersand = style === 'apa'
|
|
108
|
+
|
|
63
109
|
return (
|
|
64
110
|
<span className={className} {...props}>
|
|
65
111
|
{authors.map((author, index) => {
|
|
66
112
|
const isLast = index === authors.length - 1
|
|
67
113
|
const isSecondToLast = index === authors.length - 2
|
|
114
|
+
const isFirst = index === 0
|
|
115
|
+
const invertFirst = isFirst && (style === 'apa' || style === 'chicago' || style === 'mla')
|
|
68
116
|
|
|
69
|
-
|
|
70
|
-
let name
|
|
71
|
-
if (typeof author === 'string') {
|
|
72
|
-
name = author
|
|
73
|
-
} else {
|
|
74
|
-
const suffix = author.suffix ? `, ${author.suffix}` : ''
|
|
75
|
-
if (style === 'apa') {
|
|
76
|
-
// APA: Last, F.
|
|
77
|
-
const initials = author.given
|
|
78
|
-
? author.given
|
|
79
|
-
.split(/\s+/)
|
|
80
|
-
.map((n) => `${n.charAt(0)}.`)
|
|
81
|
-
.join(' ')
|
|
82
|
-
: ''
|
|
83
|
-
name = initials
|
|
84
|
-
? `${author.family}${suffix}, ${initials}`
|
|
85
|
-
: author.family + suffix
|
|
86
|
-
} else {
|
|
87
|
-
// Default: First Last
|
|
88
|
-
name = author.given
|
|
89
|
-
? `${author.given} ${author.family}${suffix}`
|
|
90
|
-
: author.family + suffix
|
|
91
|
-
}
|
|
92
|
-
}
|
|
117
|
+
const name = formatName(author, invertFirst)
|
|
93
118
|
|
|
94
119
|
// Determine separator
|
|
95
120
|
let sep = ''
|
|
96
121
|
if (!isLast) {
|
|
97
122
|
if (authors.length === 2) {
|
|
98
|
-
sep =
|
|
123
|
+
sep = useAmpersand ? ' & ' : ' and '
|
|
99
124
|
} else if (isSecondToLast) {
|
|
100
|
-
sep =
|
|
125
|
+
sep = useAmpersand ? ', & ' : ', and '
|
|
101
126
|
} else {
|
|
102
127
|
sep = separator
|
|
103
128
|
}
|