@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,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Citation Component
|
|
3
|
+
*
|
|
4
|
+
* Renders an inline citation that links to the bibliography.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/scholar/citations/Citation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useMemo } from 'react'
|
|
10
|
+
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
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Citation - Inline citation component
|
|
45
|
+
*
|
|
46
|
+
* @param {Object} props
|
|
47
|
+
* @param {string} [props.id] - Single reference ID to cite
|
|
48
|
+
* @param {Array<string>} [props.ids] - Multiple reference IDs to cite
|
|
49
|
+
* @param {string} [props.page] - Page number(s)
|
|
50
|
+
* @param {string} [props.prefix] - Text before citation (e.g., "see")
|
|
51
|
+
* @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
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Parenthetical citation: (Smith, 2024)
|
|
58
|
+
* <Citation id="smith2024" />
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // Narrative citation: Smith (2024)
|
|
62
|
+
* <Citation id="smith2024" format="narrative" />
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* // With page number: (Smith, 2024, p. 42)
|
|
66
|
+
* <Citation id="smith2024" page="42" />
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // Multiple sources: (Smith, 2024; Jones, 2023)
|
|
70
|
+
* <Citation ids={['smith2024', 'jones2023']} />
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // IEEE-style numeric: [1]
|
|
74
|
+
* <Citation id="smith2024" format="numeric" />
|
|
75
|
+
*/
|
|
76
|
+
export function Citation({
|
|
77
|
+
id,
|
|
78
|
+
ids,
|
|
79
|
+
page,
|
|
80
|
+
prefix,
|
|
81
|
+
suffix,
|
|
82
|
+
suppressAuthor = false,
|
|
83
|
+
format = 'parenthetical',
|
|
84
|
+
className,
|
|
85
|
+
...props
|
|
86
|
+
}) {
|
|
87
|
+
const { cite, getReference, style } = useCitation()
|
|
88
|
+
|
|
89
|
+
// Get all IDs to cite
|
|
90
|
+
const allIds = useMemo(() => {
|
|
91
|
+
if (ids?.length) return ids
|
|
92
|
+
if (id) return [id]
|
|
93
|
+
return []
|
|
94
|
+
}, [id, ids])
|
|
95
|
+
|
|
96
|
+
// Cite all references and get their info
|
|
97
|
+
const citations = useMemo(() => {
|
|
98
|
+
return allIds.map((refId) => {
|
|
99
|
+
const number = cite(refId)
|
|
100
|
+
const reference = getReference(refId)
|
|
101
|
+
return { id: refId, number, reference }
|
|
102
|
+
})
|
|
103
|
+
}, [allIds, cite, getReference])
|
|
104
|
+
|
|
105
|
+
// Format the citation text
|
|
106
|
+
const citationText = useMemo(() => {
|
|
107
|
+
if (!citations.length) return '??'
|
|
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
|
+
}
|
|
181
|
+
|
|
182
|
+
// Single link for all citations
|
|
183
|
+
const firstId = citations[0]?.id
|
|
184
|
+
return (
|
|
185
|
+
<a
|
|
186
|
+
href={firstId ? `#ref-${firstId}` : undefined}
|
|
187
|
+
className={className}
|
|
188
|
+
style={{ textDecoration: 'none' }}
|
|
189
|
+
{...props}
|
|
190
|
+
>
|
|
191
|
+
{citationText}
|
|
192
|
+
</a>
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export default Citation
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Citation Provider Component
|
|
3
|
+
*
|
|
4
|
+
* Provides context for managing citations throughout a document.
|
|
5
|
+
* Tracks which references have been cited and their order.
|
|
6
|
+
*
|
|
7
|
+
* @module @uniweb/scholar/citations/CitationProvider
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { createContext, useContext, useState, useMemo, useCallback } from 'react'
|
|
11
|
+
import { parseBibtex } from '../bibliography/parsers/bibtex.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Citation context
|
|
15
|
+
*/
|
|
16
|
+
export const CitationContext = createContext(null)
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hook to access citation context
|
|
20
|
+
*
|
|
21
|
+
* @returns {{ cite: Function, getCitation: Function, references: Array, style: string, cited: Map }}
|
|
22
|
+
*/
|
|
23
|
+
export function useCitation() {
|
|
24
|
+
const ctx = useContext(CitationContext)
|
|
25
|
+
if (!ctx) {
|
|
26
|
+
throw new Error('useCitation must be used within a CitationProvider')
|
|
27
|
+
}
|
|
28
|
+
return ctx
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* CitationProvider - Manages citation state for a document
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} props
|
|
35
|
+
* @param {Array|string} props.references - Array of reference objects or BibTeX string
|
|
36
|
+
* @param {string} [props.style='apa'] - Citation style (apa, mla, chicago, ieee)
|
|
37
|
+
* @param {React.ReactNode} props.children
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* <CitationProvider references={refs} style="apa">
|
|
41
|
+
* <p>According to <Citation id="smith2024" />, this is true.</p>
|
|
42
|
+
* <Bibliography />
|
|
43
|
+
* </CitationProvider>
|
|
44
|
+
*/
|
|
45
|
+
export function CitationProvider({ references, style = 'apa', children }) {
|
|
46
|
+
// Track cited references in order
|
|
47
|
+
const [cited, setCited] = useState(new Map())
|
|
48
|
+
|
|
49
|
+
// Parse references if BibTeX string
|
|
50
|
+
const refs = useMemo(() => {
|
|
51
|
+
if (!references) return []
|
|
52
|
+
if (typeof references === 'string') {
|
|
53
|
+
return parseBibtex(references)
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(references)) {
|
|
56
|
+
return references
|
|
57
|
+
}
|
|
58
|
+
return []
|
|
59
|
+
}, [references])
|
|
60
|
+
|
|
61
|
+
// Create a lookup map by id
|
|
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
|
+
*/
|
|
75
|
+
const cite = useCallback(
|
|
76
|
+
(id) => {
|
|
77
|
+
// Check if already cited
|
|
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]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get citation info without citing
|
|
98
|
+
*/
|
|
99
|
+
const getCitation = useCallback(
|
|
100
|
+
(id) => {
|
|
101
|
+
return {
|
|
102
|
+
number: cited.get(id),
|
|
103
|
+
reference: refsById.get(id),
|
|
104
|
+
isCited: cited.has(id),
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
[cited, refsById]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get reference by id
|
|
112
|
+
*/
|
|
113
|
+
const getReference = useCallback(
|
|
114
|
+
(id) => refsById.get(id),
|
|
115
|
+
[refsById]
|
|
116
|
+
)
|
|
117
|
+
|
|
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
|
+
const value = useMemo(
|
|
127
|
+
() => ({
|
|
128
|
+
cite,
|
|
129
|
+
getCitation,
|
|
130
|
+
getReference,
|
|
131
|
+
getCitedReferences,
|
|
132
|
+
references: refs,
|
|
133
|
+
refsById,
|
|
134
|
+
style,
|
|
135
|
+
cited,
|
|
136
|
+
}),
|
|
137
|
+
[cite, getCitation, getReference, getCitedReferences, refs, refsById, style, cited]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<CitationContext.Provider value={value}>
|
|
142
|
+
{children}
|
|
143
|
+
</CitationContext.Provider>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default CitationProvider
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Citations Module
|
|
3
|
+
*
|
|
4
|
+
* Components for managing inline citations and bibliographies.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/scholar/citations
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
CitationProvider,
|
|
11
|
+
CitationContext,
|
|
12
|
+
useCitation,
|
|
13
|
+
} from './CitationProvider.jsx'
|
|
14
|
+
|
|
15
|
+
export { Citation } from './Citation.jsx'
|
|
16
|
+
export { Bibliography } from './Bibliography.jsx'
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthorList Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a formatted list of authors with optional ORCID links.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/scholar/components/AuthorList
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import { formatAuthors } from '../bibliography/utils/authors.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* AuthorList - Formatted author names
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} props
|
|
16
|
+
* @param {Array|string} props.authors - Authors array or string
|
|
17
|
+
* @param {string} [props.style='apa'] - Formatting style
|
|
18
|
+
* @param {boolean} [props.showOrcid=true] - Show ORCID links if available
|
|
19
|
+
* @param {string} [props.separator=', '] - Separator between authors
|
|
20
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* <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
|
+
*/
|
|
28
|
+
export function AuthorList({
|
|
29
|
+
authors,
|
|
30
|
+
style = 'apa',
|
|
31
|
+
showOrcid = true,
|
|
32
|
+
separator = ', ',
|
|
33
|
+
className,
|
|
34
|
+
...props
|
|
35
|
+
}) {
|
|
36
|
+
// If authors is a string, just format it
|
|
37
|
+
if (typeof authors === 'string') {
|
|
38
|
+
return (
|
|
39
|
+
<span className={className} {...props}>
|
|
40
|
+
{formatAuthors(authors, { style })}
|
|
41
|
+
</span>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// If no array or empty, return nothing
|
|
46
|
+
if (!authors?.length) {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if any author has ORCID
|
|
51
|
+
const hasOrcid = showOrcid && authors.some((a) => a.orcid)
|
|
52
|
+
|
|
53
|
+
// If no ORCID to show, use simple formatted string
|
|
54
|
+
if (!hasOrcid) {
|
|
55
|
+
return (
|
|
56
|
+
<span className={className} {...props}>
|
|
57
|
+
{formatAuthors(authors, { style })}
|
|
58
|
+
</span>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Render with ORCID links
|
|
63
|
+
return (
|
|
64
|
+
<span className={className} {...props}>
|
|
65
|
+
{authors.map((author, index) => {
|
|
66
|
+
const isLast = index === authors.length - 1
|
|
67
|
+
const isSecondToLast = index === authors.length - 2
|
|
68
|
+
|
|
69
|
+
// Format name based on style and position
|
|
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
|
+
}
|
|
93
|
+
|
|
94
|
+
// Determine separator
|
|
95
|
+
let sep = ''
|
|
96
|
+
if (!isLast) {
|
|
97
|
+
if (authors.length === 2) {
|
|
98
|
+
sep = style === 'apa' ? ' & ' : ' and '
|
|
99
|
+
} else if (isSecondToLast) {
|
|
100
|
+
sep = style === 'apa' ? ', & ' : ', and '
|
|
101
|
+
} else {
|
|
102
|
+
sep = separator
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<React.Fragment key={index}>
|
|
108
|
+
<span style={{ whiteSpace: 'nowrap' }}>
|
|
109
|
+
{name}
|
|
110
|
+
{author.orcid && (
|
|
111
|
+
<a
|
|
112
|
+
href={`https://orcid.org/${author.orcid}`}
|
|
113
|
+
target="_blank"
|
|
114
|
+
rel="noopener noreferrer"
|
|
115
|
+
title={`ORCID: ${author.orcid}`}
|
|
116
|
+
style={{
|
|
117
|
+
marginLeft: '0.25rem',
|
|
118
|
+
display: 'inline-flex',
|
|
119
|
+
verticalAlign: 'middle',
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
<svg
|
|
123
|
+
style={{ width: '1rem', height: '1rem' }}
|
|
124
|
+
viewBox="0 0 256 256"
|
|
125
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
126
|
+
>
|
|
127
|
+
<path
|
|
128
|
+
d="M128 0C57.3 0 0 57.3 0 128s57.3 128 128 128 128-57.3 128-128S198.7 0 128 0z"
|
|
129
|
+
fill="#A6CE39"
|
|
130
|
+
/>
|
|
131
|
+
<path
|
|
132
|
+
d="M86.3 186.2H70.9V79.1h15.4v107.1zm22.7 0h-15V79.1h41.6c32.1 0 46.3 14.2 46.3 38.4 0 24.9-15.2 40.3-46.9 40.3h-26v28.4zm0-43.7h24.5c22.1 0 32.6-8.6 32.6-25.1 0-17.7-11.1-25-31.6-25h-25.5v50.1zm-43.4-79.4c5.9 0 10.6 4.8 10.6 10.6 0 5.9-4.8 10.6-10.6 10.6S55 79.6 55 73.7c0-5.8 4.8-10.6 10.6-10.6z"
|
|
133
|
+
fill="#fff"
|
|
134
|
+
/>
|
|
135
|
+
</svg>
|
|
136
|
+
</a>
|
|
137
|
+
)}
|
|
138
|
+
</span>
|
|
139
|
+
{sep}
|
|
140
|
+
</React.Fragment>
|
|
141
|
+
)
|
|
142
|
+
})}
|
|
143
|
+
</span>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default AuthorList
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BibtexBlock Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a BibTeX entry with copy functionality.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/scholar/components/BibtexBlock
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState } from 'react'
|
|
10
|
+
import { exportBibtex } from '../bibliography/parsers/bibtex.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* BibtexBlock - Display copyable BibTeX entry
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} props
|
|
16
|
+
* @param {Object|string} props.entry - Publication object or BibTeX string
|
|
17
|
+
* @param {boolean} [props.showCopyButton=true] - Show copy button
|
|
18
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <BibtexBlock entry={publication} />
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* <BibtexBlock entry="@article{smith2024, ...}" />
|
|
25
|
+
*/
|
|
26
|
+
export function BibtexBlock({
|
|
27
|
+
entry,
|
|
28
|
+
showCopyButton = true,
|
|
29
|
+
className,
|
|
30
|
+
...props
|
|
31
|
+
}) {
|
|
32
|
+
const [copied, setCopied] = useState(false)
|
|
33
|
+
|
|
34
|
+
// Get BibTeX string
|
|
35
|
+
const bibtex = typeof entry === 'string' ? entry : exportBibtex(entry)
|
|
36
|
+
|
|
37
|
+
// Copy to clipboard
|
|
38
|
+
async function handleCopy() {
|
|
39
|
+
try {
|
|
40
|
+
await navigator.clipboard.writeText(bibtex)
|
|
41
|
+
setCopied(true)
|
|
42
|
+
setTimeout(() => setCopied(false), 2000)
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error('Failed to copy:', err)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
className={className}
|
|
51
|
+
style={{
|
|
52
|
+
position: 'relative',
|
|
53
|
+
backgroundColor: '#f8fafc',
|
|
54
|
+
border: '1px solid #e2e8f0',
|
|
55
|
+
borderRadius: '0.5rem',
|
|
56
|
+
overflow: 'hidden',
|
|
57
|
+
}}
|
|
58
|
+
{...props}
|
|
59
|
+
>
|
|
60
|
+
{/* Header */}
|
|
61
|
+
<div
|
|
62
|
+
style={{
|
|
63
|
+
display: 'flex',
|
|
64
|
+
justifyContent: 'space-between',
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
padding: '0.5rem 1rem',
|
|
67
|
+
backgroundColor: '#f1f5f9',
|
|
68
|
+
borderBottom: '1px solid #e2e8f0',
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<span
|
|
72
|
+
style={{
|
|
73
|
+
fontSize: '0.75rem',
|
|
74
|
+
fontWeight: '600',
|
|
75
|
+
color: '#64748b',
|
|
76
|
+
textTransform: 'uppercase',
|
|
77
|
+
letterSpacing: '0.05em',
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
BibTeX
|
|
81
|
+
</span>
|
|
82
|
+
|
|
83
|
+
{showCopyButton && (
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={handleCopy}
|
|
87
|
+
style={{
|
|
88
|
+
display: 'inline-flex',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
gap: '0.25rem',
|
|
91
|
+
padding: '0.25rem 0.5rem',
|
|
92
|
+
fontSize: '0.75rem',
|
|
93
|
+
color: copied ? '#059669' : '#64748b',
|
|
94
|
+
backgroundColor: copied ? '#ecfdf5' : 'transparent',
|
|
95
|
+
border: 'none',
|
|
96
|
+
borderRadius: '0.25rem',
|
|
97
|
+
cursor: 'pointer',
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
{copied ? (
|
|
101
|
+
<>
|
|
102
|
+
<svg
|
|
103
|
+
style={{ width: '0.875rem', height: '0.875rem' }}
|
|
104
|
+
fill="none"
|
|
105
|
+
stroke="currentColor"
|
|
106
|
+
viewBox="0 0 24 24"
|
|
107
|
+
>
|
|
108
|
+
<path
|
|
109
|
+
strokeLinecap="round"
|
|
110
|
+
strokeLinejoin="round"
|
|
111
|
+
strokeWidth={2}
|
|
112
|
+
d="M5 13l4 4L19 7"
|
|
113
|
+
/>
|
|
114
|
+
</svg>
|
|
115
|
+
Copied
|
|
116
|
+
</>
|
|
117
|
+
) : (
|
|
118
|
+
<>
|
|
119
|
+
<svg
|
|
120
|
+
style={{ width: '0.875rem', height: '0.875rem' }}
|
|
121
|
+
fill="none"
|
|
122
|
+
stroke="currentColor"
|
|
123
|
+
viewBox="0 0 24 24"
|
|
124
|
+
>
|
|
125
|
+
<path
|
|
126
|
+
strokeLinecap="round"
|
|
127
|
+
strokeLinejoin="round"
|
|
128
|
+
strokeWidth={2}
|
|
129
|
+
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
130
|
+
/>
|
|
131
|
+
</svg>
|
|
132
|
+
Copy
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
135
|
+
</button>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* BibTeX content */}
|
|
140
|
+
<pre
|
|
141
|
+
style={{
|
|
142
|
+
margin: 0,
|
|
143
|
+
padding: '1rem',
|
|
144
|
+
fontSize: '0.875rem',
|
|
145
|
+
lineHeight: '1.6',
|
|
146
|
+
fontFamily:
|
|
147
|
+
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace',
|
|
148
|
+
color: '#334155',
|
|
149
|
+
overflowX: 'auto',
|
|
150
|
+
whiteSpace: 'pre-wrap',
|
|
151
|
+
wordBreak: 'break-word',
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
{bibtex}
|
|
155
|
+
</pre>
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default BibtexBlock
|