@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
|
@@ -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
|
}
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CiteButton Component
|
|
3
3
|
*
|
|
4
|
-
* A dropdown button that lets users copy citations in various formats
|
|
4
|
+
* A dropdown button that lets users copy citations in various formats,
|
|
5
|
+
* including rich text (HTML with formatting preserved for paste into Word/Docs).
|
|
5
6
|
*
|
|
6
7
|
* @module @uniweb/scholar/components/CiteButton
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import React, { useState, useRef, useEffect } from 'react'
|
|
10
|
-
import { formatReference } from '../bibliography/formatters/index.js'
|
|
11
|
-
import { exportBibtex } from '../bibliography/parsers/
|
|
11
|
+
import { formatReference, getAvailableStyles } from '../bibliography/formatters/index.js'
|
|
12
|
+
import { exportBibtex } from '../bibliography/parsers/index.js'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* CiteButton - Copy citation dropdown
|
|
15
16
|
*
|
|
16
17
|
* @param {Object} props
|
|
17
|
-
* @param {Object} props.publication - Publication data
|
|
18
|
-
* @param {Array<string>} [props.styles
|
|
18
|
+
* @param {Object} props.publication - Publication data (Scholar or CSL-JSON)
|
|
19
|
+
* @param {Array<string>} [props.styles] - Style names to show (defaults to all + bibtex)
|
|
19
20
|
* @param {string} [props.label='Cite'] - Button label
|
|
20
21
|
* @param {string} [props.className] - Additional CSS classes
|
|
21
22
|
*
|
|
@@ -24,7 +25,7 @@ import { exportBibtex } from '../bibliography/parsers/bibtex.js'
|
|
|
24
25
|
*/
|
|
25
26
|
export function CiteButton({
|
|
26
27
|
publication,
|
|
27
|
-
styles
|
|
28
|
+
styles,
|
|
28
29
|
label = 'Cite',
|
|
29
30
|
className,
|
|
30
31
|
...props
|
|
@@ -33,6 +34,16 @@ export function CiteButton({
|
|
|
33
34
|
const [copied, setCopied] = useState(null)
|
|
34
35
|
const dropdownRef = useRef(null)
|
|
35
36
|
|
|
37
|
+
// Default: all available citation styles + bibtex
|
|
38
|
+
const availableStyles = getAvailableStyles()
|
|
39
|
+
const styleIds = styles || [...availableStyles.map((s) => s.id), 'bibtex']
|
|
40
|
+
|
|
41
|
+
// Build label map from available styles
|
|
42
|
+
const styleLabels = Object.fromEntries(
|
|
43
|
+
availableStyles.map((s) => [s.id, s.label])
|
|
44
|
+
)
|
|
45
|
+
styleLabels.bibtex = 'BibTeX'
|
|
46
|
+
|
|
36
47
|
// Close dropdown when clicking outside
|
|
37
48
|
useEffect(() => {
|
|
38
49
|
function handleClickOutside(event) {
|
|
@@ -47,17 +58,35 @@ export function CiteButton({
|
|
|
47
58
|
}
|
|
48
59
|
}, [isOpen])
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Copy citation to clipboard.
|
|
63
|
+
* For citation styles, copies both plain text and rich HTML.
|
|
64
|
+
*/
|
|
51
65
|
async function copyToClipboard(style) {
|
|
52
66
|
try {
|
|
53
|
-
let text
|
|
54
67
|
if (style === 'bibtex') {
|
|
55
|
-
text = exportBibtex(publication)
|
|
68
|
+
const text = exportBibtex(publication)
|
|
69
|
+
await navigator.clipboard.writeText(text)
|
|
56
70
|
} else {
|
|
57
|
-
|
|
71
|
+
const entry = formatReference(publication, { style })
|
|
72
|
+
|
|
73
|
+
// Try rich-text copy (HTML + plain text) for formatting in Word/Docs
|
|
74
|
+
if (typeof ClipboardItem !== 'undefined') {
|
|
75
|
+
try {
|
|
76
|
+
const item = new ClipboardItem({
|
|
77
|
+
'text/html': new Blob([entry.html], { type: 'text/html' }),
|
|
78
|
+
'text/plain': new Blob([entry.text], { type: 'text/plain' }),
|
|
79
|
+
})
|
|
80
|
+
await navigator.clipboard.write([item])
|
|
81
|
+
} catch {
|
|
82
|
+
// Fallback to plain text if rich copy fails
|
|
83
|
+
await navigator.clipboard.writeText(entry.text)
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
await navigator.clipboard.writeText(entry.text)
|
|
87
|
+
}
|
|
58
88
|
}
|
|
59
89
|
|
|
60
|
-
await navigator.clipboard.writeText(text)
|
|
61
90
|
setCopied(style)
|
|
62
91
|
setTimeout(() => setCopied(null), 2000)
|
|
63
92
|
} catch (err) {
|
|
@@ -65,14 +94,6 @@ export function CiteButton({
|
|
|
65
94
|
}
|
|
66
95
|
}
|
|
67
96
|
|
|
68
|
-
const styleLabels = {
|
|
69
|
-
apa: 'APA',
|
|
70
|
-
mla: 'MLA',
|
|
71
|
-
chicago: 'Chicago',
|
|
72
|
-
ieee: 'IEEE',
|
|
73
|
-
bibtex: 'BibTeX',
|
|
74
|
-
}
|
|
75
|
-
|
|
76
97
|
return (
|
|
77
98
|
<div
|
|
78
99
|
ref={dropdownRef}
|
|
@@ -88,21 +109,34 @@ export function CiteButton({
|
|
|
88
109
|
display: 'inline-flex',
|
|
89
110
|
alignItems: 'center',
|
|
90
111
|
gap: '0.25rem',
|
|
91
|
-
padding: '0.
|
|
92
|
-
fontSize: '0.
|
|
93
|
-
fontWeight: '
|
|
94
|
-
color: '#
|
|
95
|
-
backgroundColor: '
|
|
96
|
-
border: '
|
|
97
|
-
borderRadius: '0.
|
|
112
|
+
padding: '0.125rem 0.5rem',
|
|
113
|
+
fontSize: '0.8125rem',
|
|
114
|
+
fontWeight: '400',
|
|
115
|
+
color: 'var(--subtle, #6b7280)',
|
|
116
|
+
backgroundColor: 'transparent',
|
|
117
|
+
border: 'none',
|
|
118
|
+
borderRadius: '0.25rem',
|
|
98
119
|
cursor: 'pointer',
|
|
120
|
+
letterSpacing: '0.01em',
|
|
99
121
|
}}
|
|
122
|
+
onMouseEnter={(e) => { e.target.style.color = 'var(--heading, #111827)' }}
|
|
123
|
+
onMouseLeave={(e) => { if (!isOpen) e.target.style.color = 'var(--subtle, #6b7280)' }}
|
|
100
124
|
>
|
|
125
|
+
{/* Quote icon */}
|
|
126
|
+
<svg
|
|
127
|
+
style={{ width: '0.875rem', height: '0.875rem', opacity: 0.7, flexShrink: 0 }}
|
|
128
|
+
fill="none"
|
|
129
|
+
stroke="currentColor"
|
|
130
|
+
viewBox="0 0 24 24"
|
|
131
|
+
>
|
|
132
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
|
|
133
|
+
</svg>
|
|
101
134
|
{label}
|
|
102
135
|
<svg
|
|
103
136
|
style={{
|
|
104
|
-
width: '
|
|
105
|
-
height: '
|
|
137
|
+
width: '0.625rem',
|
|
138
|
+
height: '0.625rem',
|
|
139
|
+
opacity: 0.5,
|
|
106
140
|
transform: isOpen ? 'rotate(180deg)' : 'rotate(0)',
|
|
107
141
|
transition: 'transform 0.15s',
|
|
108
142
|
}}
|
|
@@ -110,12 +144,7 @@ export function CiteButton({
|
|
|
110
144
|
stroke="currentColor"
|
|
111
145
|
viewBox="0 0 24 24"
|
|
112
146
|
>
|
|
113
|
-
<path
|
|
114
|
-
strokeLinecap="round"
|
|
115
|
-
strokeLinejoin="round"
|
|
116
|
-
strokeWidth={2}
|
|
117
|
-
d="M19 9l-7 7-7-7"
|
|
118
|
-
/>
|
|
147
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
119
148
|
</svg>
|
|
120
149
|
</button>
|
|
121
150
|
|
|
@@ -127,16 +156,17 @@ export function CiteButton({
|
|
|
127
156
|
top: '100%',
|
|
128
157
|
left: 0,
|
|
129
158
|
marginTop: '0.25rem',
|
|
130
|
-
minWidth: '
|
|
131
|
-
backgroundColor: '#ffffff',
|
|
132
|
-
border: '1px solid #e5e7eb',
|
|
133
|
-
borderRadius: '0.
|
|
134
|
-
boxShadow: '0 4px
|
|
159
|
+
minWidth: '8.5rem',
|
|
160
|
+
backgroundColor: 'var(--section, #ffffff)',
|
|
161
|
+
border: '1px solid var(--border, #e5e7eb)',
|
|
162
|
+
borderRadius: '0.5rem',
|
|
163
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
|
|
135
164
|
zIndex: 50,
|
|
136
165
|
overflow: 'hidden',
|
|
166
|
+
padding: '0.25rem',
|
|
137
167
|
}}
|
|
138
168
|
>
|
|
139
|
-
{
|
|
169
|
+
{styleIds.map((style) => (
|
|
140
170
|
<button
|
|
141
171
|
key={style}
|
|
142
172
|
type="button"
|
|
@@ -144,17 +174,18 @@ export function CiteButton({
|
|
|
144
174
|
style={{
|
|
145
175
|
display: 'block',
|
|
146
176
|
width: '100%',
|
|
147
|
-
padding: '0.
|
|
148
|
-
fontSize: '0.
|
|
177
|
+
padding: '0.375rem 0.625rem',
|
|
178
|
+
fontSize: '0.8125rem',
|
|
149
179
|
textAlign: 'left',
|
|
150
|
-
color: copied === style ? '#059669' : '#374151',
|
|
180
|
+
color: copied === style ? '#059669' : 'var(--body, #374151)',
|
|
151
181
|
backgroundColor: copied === style ? '#ecfdf5' : 'transparent',
|
|
152
182
|
border: 'none',
|
|
183
|
+
borderRadius: '0.25rem',
|
|
153
184
|
cursor: 'pointer',
|
|
154
185
|
}}
|
|
155
186
|
onMouseEnter={(e) => {
|
|
156
187
|
if (copied !== style) {
|
|
157
|
-
e.target.style.backgroundColor = '#f3f4f6'
|
|
188
|
+
e.target.style.backgroundColor = 'var(--card, #f3f4f6)'
|
|
158
189
|
}
|
|
159
190
|
}}
|
|
160
191
|
onMouseLeave={(e) => {
|
|
@@ -163,7 +194,9 @@ export function CiteButton({
|
|
|
163
194
|
}
|
|
164
195
|
}}
|
|
165
196
|
>
|
|
166
|
-
{copied === style
|
|
197
|
+
{copied === style
|
|
198
|
+
? '✓ Copied!'
|
|
199
|
+
: styleLabels[style] || style.toUpperCase()}
|
|
167
200
|
</button>
|
|
168
201
|
))}
|
|
169
202
|
</div>
|
|
@@ -50,6 +50,12 @@ export function DoiLink({
|
|
|
50
50
|
case 'link':
|
|
51
51
|
displayText = doiUrl
|
|
52
52
|
break
|
|
53
|
+
case 'label':
|
|
54
|
+
displayText = 'DOI'
|
|
55
|
+
break
|
|
56
|
+
case 'icon':
|
|
57
|
+
displayText = null
|
|
58
|
+
break
|
|
53
59
|
case 'full':
|
|
54
60
|
default:
|
|
55
61
|
displayText = `DOI: ${normalizedDoi}`
|
|
@@ -61,35 +67,35 @@ export function DoiLink({
|
|
|
61
67
|
target="_blank"
|
|
62
68
|
rel="noopener noreferrer"
|
|
63
69
|
className={className}
|
|
70
|
+
title={`DOI: ${normalizedDoi}`}
|
|
64
71
|
style={{
|
|
65
|
-
color: '#
|
|
72
|
+
color: 'var(--subtle, #6b7280)',
|
|
66
73
|
textDecoration: 'none',
|
|
67
74
|
display: 'inline-flex',
|
|
68
75
|
alignItems: 'center',
|
|
69
76
|
gap: '0.25rem',
|
|
77
|
+
fontSize: '0.8125rem',
|
|
78
|
+
transition: 'color 0.15s',
|
|
70
79
|
}}
|
|
80
|
+
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--primary, #2563eb)' }}
|
|
81
|
+
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--subtle, #6b7280)' }}
|
|
71
82
|
{...props}
|
|
72
83
|
>
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
strokeWidth={2}
|
|
89
|
-
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
|
90
|
-
/>
|
|
91
|
-
</svg>
|
|
92
|
-
)}
|
|
84
|
+
{/* DOI icon — external link */}
|
|
85
|
+
<svg
|
|
86
|
+
style={{ width: '0.875rem', height: '0.875rem', flexShrink: 0 }}
|
|
87
|
+
fill="none"
|
|
88
|
+
stroke="currentColor"
|
|
89
|
+
viewBox="0 0 24 24"
|
|
90
|
+
>
|
|
91
|
+
<path
|
|
92
|
+
strokeLinecap="round"
|
|
93
|
+
strokeLinejoin="round"
|
|
94
|
+
strokeWidth={1.5}
|
|
95
|
+
d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
|
96
|
+
/>
|
|
97
|
+
</svg>
|
|
98
|
+
{displayText && <span>{displayText}</span>}
|
|
93
99
|
</a>
|
|
94
100
|
)
|
|
95
101
|
}
|
package/src/index.js
CHANGED
|
@@ -4,21 +4,31 @@
|
|
|
4
4
|
* Provides math rendering, bibliographic references, and inline citations
|
|
5
5
|
* for academic and scientific content.
|
|
6
6
|
*
|
|
7
|
+
* Bibliography formatting is powered by @citestyle/* compiled styles,
|
|
8
|
+
* giving access to 6 citation styles with structured HTML output,
|
|
9
|
+
* year-suffix disambiguation, and cite collapsing.
|
|
10
|
+
*
|
|
7
11
|
* @module @uniweb/scholar
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
|
-
// Math components
|
|
11
|
-
|
|
14
|
+
// Math components — scholar no longer ships a math renderer. Plain
|
|
15
|
+
// LaTeX in markdown (`$x$`, `$$x$$`, fenced ```math) is compiled to
|
|
16
|
+
// MathML by the content pipeline and rendered natively by the browser.
|
|
17
|
+
// <Equation> / <EquationRef> cover numbered cross-referenceable
|
|
18
|
+
// equations, consuming pre-compiled MathML via `content.math`.
|
|
19
|
+
export { Equation, EquationRef } from './math/index.js'
|
|
12
20
|
|
|
13
|
-
// Bibliography
|
|
21
|
+
// Bibliography — formatting, parsing, normalization
|
|
14
22
|
export {
|
|
15
23
|
formatReference,
|
|
16
|
-
|
|
24
|
+
getAvailableStyles,
|
|
17
25
|
parseBibtex,
|
|
18
26
|
exportBibtex,
|
|
27
|
+
toCslJson,
|
|
28
|
+
fromCslJson,
|
|
19
29
|
} from './bibliography/index.js'
|
|
20
30
|
|
|
21
|
-
// Citation system
|
|
31
|
+
// Citation system — registry-powered
|
|
22
32
|
export {
|
|
23
33
|
CitationProvider,
|
|
24
34
|
Citation,
|
package/src/math/Equation.jsx
CHANGED
|
@@ -1,31 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Equation Component
|
|
3
3
|
*
|
|
4
|
-
* Renders a numbered equation
|
|
5
|
-
*
|
|
4
|
+
* Renders a numbered display equation from pre-compiled MathML.
|
|
5
|
+
*
|
|
6
|
+
* The MathML is produced by Uniweb's content pipeline at build time
|
|
7
|
+
* (see @uniweb/content-reader/math). This component does no math
|
|
8
|
+
* rendering of its own — it wraps the MathML string with numbering and
|
|
9
|
+
* cross-reference machinery, which means it is fully SSR-safe and adds
|
|
10
|
+
* zero runtime math library cost to sites.
|
|
11
|
+
*
|
|
12
|
+
* Authors write labeled display equations in markdown:
|
|
13
|
+
*
|
|
14
|
+
* ```math:einstein
|
|
15
|
+
* E = mc^2
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* A foundation's section component then iterates `content.math` and
|
|
19
|
+
* renders each labeled entry through <Equation>:
|
|
20
|
+
*
|
|
21
|
+
* <EquationProvider>
|
|
22
|
+
* {content.math.map(m => (
|
|
23
|
+
* <Equation key={m.id} id={m.id} mathml={m.mathml} />
|
|
24
|
+
* ))}
|
|
25
|
+
* </EquationProvider>
|
|
6
26
|
*
|
|
7
27
|
* @module @uniweb/scholar/math/Equation
|
|
8
28
|
*/
|
|
9
29
|
|
|
10
|
-
import React, {
|
|
11
|
-
import { loadKatex } from './katex-loader.js'
|
|
30
|
+
import React, { useContext, useEffect, useId } from 'react'
|
|
12
31
|
|
|
13
32
|
/**
|
|
14
|
-
* Context for tracking equation numbers
|
|
15
|
-
* Exported for use by EquationRef
|
|
33
|
+
* Context for tracking equation numbers. Exported for use by EquationRef.
|
|
16
34
|
*/
|
|
17
35
|
export const EquationContext = React.createContext(null)
|
|
18
36
|
|
|
19
37
|
/**
|
|
20
|
-
* Provider for equation numbering
|
|
21
|
-
*
|
|
38
|
+
* Provider for equation numbering. Wrap section content with this to
|
|
39
|
+
* enable automatic sequential numbering across all <Equation> instances.
|
|
22
40
|
*
|
|
23
41
|
* @param {Object} props
|
|
24
42
|
* @param {React.ReactNode} props.children
|
|
25
43
|
* @param {number} [props.startNumber=1] - Starting equation number
|
|
26
44
|
*/
|
|
27
45
|
export function EquationProvider({ children, startNumber = 1 }) {
|
|
28
|
-
const [equations, setEquations] = useState(new Map())
|
|
46
|
+
const [equations, setEquations] = React.useState(new Map())
|
|
29
47
|
const counterRef = React.useRef(startNumber)
|
|
30
48
|
|
|
31
49
|
const register = React.useCallback((id) => {
|
|
@@ -39,7 +57,7 @@ export function EquationProvider({ children, startNumber = 1 }) {
|
|
|
39
57
|
|
|
40
58
|
const getNumber = React.useCallback(
|
|
41
59
|
(id) => equations.get(id),
|
|
42
|
-
[equations]
|
|
60
|
+
[equations],
|
|
43
61
|
)
|
|
44
62
|
|
|
45
63
|
return (
|
|
@@ -50,77 +68,35 @@ export function EquationProvider({ children, startNumber = 1 }) {
|
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
/**
|
|
53
|
-
* Hook
|
|
71
|
+
* Hook for foundation code that needs direct access to the numbering
|
|
72
|
+
* context (e.g., to render a list of all equations on a page).
|
|
54
73
|
*/
|
|
55
74
|
export function useEquations() {
|
|
56
75
|
return useContext(EquationContext)
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
/**
|
|
60
|
-
* Equation
|
|
79
|
+
* Equation — numbered display equation rendered from pre-compiled MathML.
|
|
61
80
|
*
|
|
62
81
|
* @param {Object} props
|
|
63
|
-
* @param {string} props.
|
|
64
|
-
*
|
|
65
|
-
* @param {string}
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* @
|
|
69
|
-
* <EquationProvider>
|
|
70
|
-
* <Equation id="einstein">E = mc^2</Equation>
|
|
71
|
-
* <p>As shown in <EquationRef id="einstein" />, energy equals...</p>
|
|
72
|
-
* </EquationProvider>
|
|
82
|
+
* @param {string} [props.id] - Identifier for cross-referencing. If omitted,
|
|
83
|
+
* a stable React id is generated but will not be discoverable by <EquationRef>.
|
|
84
|
+
* @param {string} props.mathml - Pre-compiled MathML HTML string from the
|
|
85
|
+
* content pipeline (e.g., `content.math[i].mathml`).
|
|
86
|
+
* @param {string} [props.label] - Override the auto-number with a custom label.
|
|
87
|
+
* @param {string} [props.className] - Additional CSS classes on the wrapper.
|
|
73
88
|
*/
|
|
74
|
-
export function Equation({
|
|
75
|
-
const
|
|
89
|
+
export function Equation({ id, mathml, label, className, ...props }) {
|
|
90
|
+
const ctx = useContext(EquationContext)
|
|
76
91
|
const generatedId = useId()
|
|
77
92
|
const equationId = id || generatedId
|
|
78
|
-
const ctx = useContext(EquationContext)
|
|
79
|
-
|
|
80
|
-
// Extract LaTeX string from children
|
|
81
|
-
const latex = typeof children === 'string' ? children.trim() : ''
|
|
82
93
|
|
|
83
|
-
// Register equation for numbering
|
|
84
94
|
useEffect(() => {
|
|
85
|
-
if (ctx)
|
|
86
|
-
ctx.register(equationId)
|
|
87
|
-
}
|
|
95
|
+
if (ctx) ctx.register(equationId)
|
|
88
96
|
}, [ctx, equationId])
|
|
89
97
|
|
|
90
|
-
// Get equation number
|
|
91
98
|
const number = ctx?.getNumber(equationId)
|
|
92
|
-
|
|
93
|
-
// Render LaTeX
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
if (!latex) return
|
|
96
|
-
|
|
97
|
-
let cancelled = false
|
|
98
|
-
|
|
99
|
-
async function render() {
|
|
100
|
-
const katex = await loadKatex()
|
|
101
|
-
if (cancelled || !katex) return
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
const rendered = katex.renderToString(latex, {
|
|
105
|
-
displayMode: true,
|
|
106
|
-
throwOnError: false,
|
|
107
|
-
})
|
|
108
|
-
if (!cancelled) {
|
|
109
|
-
setHtml(rendered)
|
|
110
|
-
}
|
|
111
|
-
} catch (e) {
|
|
112
|
-
console.warn('[Equation] Render error:', e.message)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
render()
|
|
117
|
-
|
|
118
|
-
return () => {
|
|
119
|
-
cancelled = true
|
|
120
|
-
}
|
|
121
|
-
}, [latex])
|
|
122
|
-
|
|
123
|
-
const displayLabel = label || (number ? `(${number})` : '')
|
|
99
|
+
const displayLabel = label !== undefined ? label : number ? `(${number})` : ''
|
|
124
100
|
|
|
125
101
|
return (
|
|
126
102
|
<div
|
|
@@ -135,16 +111,10 @@ export function Equation({ children, id, label, className, ...props }) {
|
|
|
135
111
|
}}
|
|
136
112
|
{...props}
|
|
137
113
|
>
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
{
|
|
141
|
-
|
|
142
|
-
) : (
|
|
143
|
-
<code>{latex}</code>
|
|
144
|
-
)}
|
|
145
|
-
</div>
|
|
146
|
-
|
|
147
|
-
{/* Equation number */}
|
|
114
|
+
<div
|
|
115
|
+
style={{ flex: 1, textAlign: 'center' }}
|
|
116
|
+
dangerouslySetInnerHTML={{ __html: mathml || '' }}
|
|
117
|
+
/>
|
|
148
118
|
{displayLabel && (
|
|
149
119
|
<span
|
|
150
120
|
style={{
|