@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,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CiteButton Component
|
|
3
|
+
*
|
|
4
|
+
* A dropdown button that lets users copy citations in various formats.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/scholar/components/CiteButton
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState, useRef, useEffect } from 'react'
|
|
10
|
+
import { formatReference } from '../bibliography/formatters/index.js'
|
|
11
|
+
import { exportBibtex } from '../bibliography/parsers/bibtex.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CiteButton - Copy citation dropdown
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} props
|
|
17
|
+
* @param {Object} props.publication - Publication data
|
|
18
|
+
* @param {Array<string>} [props.styles=['apa', 'mla', 'chicago', 'ieee', 'bibtex']] - Available citation styles
|
|
19
|
+
* @param {string} [props.label='Cite'] - Button label
|
|
20
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* <CiteButton publication={myPublication} styles={['apa', 'mla', 'bibtex']} />
|
|
24
|
+
*/
|
|
25
|
+
export function CiteButton({
|
|
26
|
+
publication,
|
|
27
|
+
styles = ['apa', 'mla', 'chicago', 'ieee', 'bibtex'],
|
|
28
|
+
label = 'Cite',
|
|
29
|
+
className,
|
|
30
|
+
...props
|
|
31
|
+
}) {
|
|
32
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
33
|
+
const [copied, setCopied] = useState(null)
|
|
34
|
+
const dropdownRef = useRef(null)
|
|
35
|
+
|
|
36
|
+
// Close dropdown when clicking outside
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
function handleClickOutside(event) {
|
|
39
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
40
|
+
setIsOpen(false)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (isOpen) {
|
|
45
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
46
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
47
|
+
}
|
|
48
|
+
}, [isOpen])
|
|
49
|
+
|
|
50
|
+
// Copy citation to clipboard
|
|
51
|
+
async function copyToClipboard(style) {
|
|
52
|
+
try {
|
|
53
|
+
let text
|
|
54
|
+
if (style === 'bibtex') {
|
|
55
|
+
text = exportBibtex(publication)
|
|
56
|
+
} else {
|
|
57
|
+
text = formatReference(publication, { style })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await navigator.clipboard.writeText(text)
|
|
61
|
+
setCopied(style)
|
|
62
|
+
setTimeout(() => setCopied(null), 2000)
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error('Failed to copy:', err)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const styleLabels = {
|
|
69
|
+
apa: 'APA',
|
|
70
|
+
mla: 'MLA',
|
|
71
|
+
chicago: 'Chicago',
|
|
72
|
+
ieee: 'IEEE',
|
|
73
|
+
bibtex: 'BibTeX',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
ref={dropdownRef}
|
|
79
|
+
className={className}
|
|
80
|
+
style={{ position: 'relative', display: 'inline-block' }}
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
{/* Trigger button */}
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
87
|
+
style={{
|
|
88
|
+
display: 'inline-flex',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
gap: '0.25rem',
|
|
91
|
+
padding: '0.375rem 0.75rem',
|
|
92
|
+
fontSize: '0.875rem',
|
|
93
|
+
fontWeight: '500',
|
|
94
|
+
color: '#374151',
|
|
95
|
+
backgroundColor: '#f3f4f6',
|
|
96
|
+
border: '1px solid #d1d5db',
|
|
97
|
+
borderRadius: '0.375rem',
|
|
98
|
+
cursor: 'pointer',
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{label}
|
|
102
|
+
<svg
|
|
103
|
+
style={{
|
|
104
|
+
width: '1rem',
|
|
105
|
+
height: '1rem',
|
|
106
|
+
transform: isOpen ? 'rotate(180deg)' : 'rotate(0)',
|
|
107
|
+
transition: 'transform 0.15s',
|
|
108
|
+
}}
|
|
109
|
+
fill="none"
|
|
110
|
+
stroke="currentColor"
|
|
111
|
+
viewBox="0 0 24 24"
|
|
112
|
+
>
|
|
113
|
+
<path
|
|
114
|
+
strokeLinecap="round"
|
|
115
|
+
strokeLinejoin="round"
|
|
116
|
+
strokeWidth={2}
|
|
117
|
+
d="M19 9l-7 7-7-7"
|
|
118
|
+
/>
|
|
119
|
+
</svg>
|
|
120
|
+
</button>
|
|
121
|
+
|
|
122
|
+
{/* Dropdown menu */}
|
|
123
|
+
{isOpen && (
|
|
124
|
+
<div
|
|
125
|
+
style={{
|
|
126
|
+
position: 'absolute',
|
|
127
|
+
top: '100%',
|
|
128
|
+
left: 0,
|
|
129
|
+
marginTop: '0.25rem',
|
|
130
|
+
minWidth: '8rem',
|
|
131
|
+
backgroundColor: '#ffffff',
|
|
132
|
+
border: '1px solid #e5e7eb',
|
|
133
|
+
borderRadius: '0.375rem',
|
|
134
|
+
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
|
135
|
+
zIndex: 50,
|
|
136
|
+
overflow: 'hidden',
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
{styles.map((style) => (
|
|
140
|
+
<button
|
|
141
|
+
key={style}
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={() => copyToClipboard(style)}
|
|
144
|
+
style={{
|
|
145
|
+
display: 'block',
|
|
146
|
+
width: '100%',
|
|
147
|
+
padding: '0.5rem 1rem',
|
|
148
|
+
fontSize: '0.875rem',
|
|
149
|
+
textAlign: 'left',
|
|
150
|
+
color: copied === style ? '#059669' : '#374151',
|
|
151
|
+
backgroundColor: copied === style ? '#ecfdf5' : 'transparent',
|
|
152
|
+
border: 'none',
|
|
153
|
+
cursor: 'pointer',
|
|
154
|
+
}}
|
|
155
|
+
onMouseEnter={(e) => {
|
|
156
|
+
if (copied !== style) {
|
|
157
|
+
e.target.style.backgroundColor = '#f3f4f6'
|
|
158
|
+
}
|
|
159
|
+
}}
|
|
160
|
+
onMouseLeave={(e) => {
|
|
161
|
+
if (copied !== style) {
|
|
162
|
+
e.target.style.backgroundColor = 'transparent'
|
|
163
|
+
}
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
{copied === style ? '✓ Copied!' : styleLabels[style] || style.toUpperCase()}
|
|
167
|
+
</button>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export default CiteButton
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DoiLink Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a DOI as a clickable link.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/scholar/components/DoiLink
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* DoiLink - Display a DOI with automatic link
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} props
|
|
15
|
+
* @param {string} props.doi - DOI value (with or without URL prefix)
|
|
16
|
+
* @param {string} [props.format='full'] - Display format: 'full', 'short', or 'link'
|
|
17
|
+
* @param {boolean} [props.showIcon=false] - Show external link icon
|
|
18
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <DoiLink doi="10.1234/example" />
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // Short format (just the DOI number)
|
|
25
|
+
* <DoiLink doi="10.1234/example" format="short" />
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // Link format (full URL)
|
|
29
|
+
* <DoiLink doi="10.1234/example" format="link" />
|
|
30
|
+
*/
|
|
31
|
+
export function DoiLink({
|
|
32
|
+
doi,
|
|
33
|
+
format = 'full',
|
|
34
|
+
showIcon = false,
|
|
35
|
+
className,
|
|
36
|
+
...props
|
|
37
|
+
}) {
|
|
38
|
+
if (!doi) return null
|
|
39
|
+
|
|
40
|
+
// Normalize DOI - remove URL prefix if present
|
|
41
|
+
const normalizedDoi = doi.replace(/^https?:\/\/doi\.org\//, '')
|
|
42
|
+
const doiUrl = `https://doi.org/${normalizedDoi}`
|
|
43
|
+
|
|
44
|
+
// Determine display text
|
|
45
|
+
let displayText
|
|
46
|
+
switch (format) {
|
|
47
|
+
case 'short':
|
|
48
|
+
displayText = normalizedDoi
|
|
49
|
+
break
|
|
50
|
+
case 'link':
|
|
51
|
+
displayText = doiUrl
|
|
52
|
+
break
|
|
53
|
+
case 'full':
|
|
54
|
+
default:
|
|
55
|
+
displayText = `DOI: ${normalizedDoi}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<a
|
|
60
|
+
href={doiUrl}
|
|
61
|
+
target="_blank"
|
|
62
|
+
rel="noopener noreferrer"
|
|
63
|
+
className={className}
|
|
64
|
+
style={{
|
|
65
|
+
color: '#2563eb',
|
|
66
|
+
textDecoration: 'none',
|
|
67
|
+
display: 'inline-flex',
|
|
68
|
+
alignItems: 'center',
|
|
69
|
+
gap: '0.25rem',
|
|
70
|
+
}}
|
|
71
|
+
{...props}
|
|
72
|
+
>
|
|
73
|
+
{displayText}
|
|
74
|
+
{showIcon && (
|
|
75
|
+
<svg
|
|
76
|
+
style={{
|
|
77
|
+
width: '0.875rem',
|
|
78
|
+
height: '0.875rem',
|
|
79
|
+
opacity: 0.7,
|
|
80
|
+
}}
|
|
81
|
+
fill="none"
|
|
82
|
+
stroke="currentColor"
|
|
83
|
+
viewBox="0 0 24 24"
|
|
84
|
+
>
|
|
85
|
+
<path
|
|
86
|
+
strokeLinecap="round"
|
|
87
|
+
strokeLinejoin="round"
|
|
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
|
+
)}
|
|
93
|
+
</a>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default DoiLink
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Components
|
|
3
|
+
*
|
|
4
|
+
* Reusable components for displaying academic content.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/scholar/components
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { CiteButton } from './CiteButton.jsx'
|
|
10
|
+
export { BibtexBlock } from './BibtexBlock.jsx'
|
|
11
|
+
export { AuthorList } from './AuthorList.jsx'
|
|
12
|
+
export { DoiLink } from './DoiLink.jsx'
|
package/src/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @uniweb/scholar - Academic content utilities for Uniweb
|
|
3
|
+
*
|
|
4
|
+
* Provides math rendering, bibliographic references, and inline citations
|
|
5
|
+
* for academic and scientific content.
|
|
6
|
+
*
|
|
7
|
+
* @module @uniweb/scholar
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Math components
|
|
11
|
+
export { Math, Equation, EquationRef, loadKatex } from './math/index.js'
|
|
12
|
+
|
|
13
|
+
// Bibliography utilities
|
|
14
|
+
export {
|
|
15
|
+
formatReference,
|
|
16
|
+
formatAuthors,
|
|
17
|
+
parseBibtex,
|
|
18
|
+
exportBibtex,
|
|
19
|
+
} from './bibliography/index.js'
|
|
20
|
+
|
|
21
|
+
// Citation system
|
|
22
|
+
export {
|
|
23
|
+
CitationProvider,
|
|
24
|
+
Citation,
|
|
25
|
+
Bibliography,
|
|
26
|
+
useCitation,
|
|
27
|
+
} from './citations/index.js'
|
|
28
|
+
|
|
29
|
+
// UI components
|
|
30
|
+
export {
|
|
31
|
+
CiteButton,
|
|
32
|
+
BibtexBlock,
|
|
33
|
+
AuthorList,
|
|
34
|
+
DoiLink,
|
|
35
|
+
} from './components/index.js'
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Equation Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a numbered equation with an optional label for cross-referencing.
|
|
5
|
+
* Equations are numbered sequentially within a CitationProvider context.
|
|
6
|
+
*
|
|
7
|
+
* @module @uniweb/scholar/math/Equation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useEffect, useState, useContext, useId } from 'react'
|
|
11
|
+
import { loadKatex } from './katex-loader.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Context for tracking equation numbers
|
|
15
|
+
* Exported for use by EquationRef
|
|
16
|
+
*/
|
|
17
|
+
export const EquationContext = React.createContext(null)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Provider for equation numbering
|
|
21
|
+
* Wrap your content with this to enable automatic equation numbering.
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} props
|
|
24
|
+
* @param {React.ReactNode} props.children
|
|
25
|
+
* @param {number} [props.startNumber=1] - Starting equation number
|
|
26
|
+
*/
|
|
27
|
+
export function EquationProvider({ children, startNumber = 1 }) {
|
|
28
|
+
const [equations, setEquations] = useState(new Map())
|
|
29
|
+
const counterRef = React.useRef(startNumber)
|
|
30
|
+
|
|
31
|
+
const register = React.useCallback((id) => {
|
|
32
|
+
setEquations((prev) => {
|
|
33
|
+
if (prev.has(id)) return prev
|
|
34
|
+
const next = new Map(prev)
|
|
35
|
+
next.set(id, counterRef.current++)
|
|
36
|
+
return next
|
|
37
|
+
})
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
const getNumber = React.useCallback(
|
|
41
|
+
(id) => equations.get(id),
|
|
42
|
+
[equations]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<EquationContext.Provider value={{ register, getNumber, equations }}>
|
|
47
|
+
{children}
|
|
48
|
+
</EquationContext.Provider>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Hook to access equation context
|
|
54
|
+
*/
|
|
55
|
+
export function useEquations() {
|
|
56
|
+
return useContext(EquationContext)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Equation - Numbered display equation with optional label
|
|
61
|
+
*
|
|
62
|
+
* @param {Object} props
|
|
63
|
+
* @param {string} props.children - LaTeX expression
|
|
64
|
+
* @param {string} [props.id] - Unique identifier for cross-referencing
|
|
65
|
+
* @param {string} [props.label] - Custom label (defaults to equation number)
|
|
66
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* <EquationProvider>
|
|
70
|
+
* <Equation id="einstein">E = mc^2</Equation>
|
|
71
|
+
* <p>As shown in <EquationRef id="einstein" />, energy equals...</p>
|
|
72
|
+
* </EquationProvider>
|
|
73
|
+
*/
|
|
74
|
+
export function Equation({ children, id, label, className, ...props }) {
|
|
75
|
+
const [html, setHtml] = useState(null)
|
|
76
|
+
const generatedId = useId()
|
|
77
|
+
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
|
+
|
|
83
|
+
// Register equation for numbering
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (ctx) {
|
|
86
|
+
ctx.register(equationId)
|
|
87
|
+
}
|
|
88
|
+
}, [ctx, equationId])
|
|
89
|
+
|
|
90
|
+
// Get equation number
|
|
91
|
+
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})` : '')
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div
|
|
127
|
+
id={equationId}
|
|
128
|
+
className={className}
|
|
129
|
+
style={{
|
|
130
|
+
display: 'flex',
|
|
131
|
+
alignItems: 'center',
|
|
132
|
+
justifyContent: 'center',
|
|
133
|
+
margin: '1.5rem 0',
|
|
134
|
+
position: 'relative',
|
|
135
|
+
}}
|
|
136
|
+
{...props}
|
|
137
|
+
>
|
|
138
|
+
{/* Equation content */}
|
|
139
|
+
<div style={{ flex: 1, textAlign: 'center' }}>
|
|
140
|
+
{html ? (
|
|
141
|
+
<span dangerouslySetInnerHTML={{ __html: html }} />
|
|
142
|
+
) : (
|
|
143
|
+
<code>{latex}</code>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{/* Equation number */}
|
|
148
|
+
{displayLabel && (
|
|
149
|
+
<span
|
|
150
|
+
style={{
|
|
151
|
+
position: 'absolute',
|
|
152
|
+
right: 0,
|
|
153
|
+
fontStyle: 'normal',
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
{displayLabel}
|
|
157
|
+
</span>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default Equation
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EquationRef Component
|
|
3
|
+
*
|
|
4
|
+
* Creates a reference link to a numbered equation.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/scholar/math/EquationRef
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useContext } from 'react'
|
|
10
|
+
import { EquationContext } from './Equation.jsx'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* EquationRef - Reference to a numbered equation
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} props
|
|
16
|
+
* @param {string} props.id - ID of the equation to reference
|
|
17
|
+
* @param {string} [props.format='parentheses'] - Format: 'parentheses', 'plain', or 'equation'
|
|
18
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <EquationRef id="einstein" /> // "(1)"
|
|
22
|
+
* <EquationRef id="einstein" format="plain" /> // "1"
|
|
23
|
+
* <EquationRef id="einstein" format="equation" /> // "Equation 1"
|
|
24
|
+
*/
|
|
25
|
+
export function EquationRef({ id, format = 'parentheses', className, ...props }) {
|
|
26
|
+
const ctx = useContext(EquationContext)
|
|
27
|
+
const number = ctx?.getNumber(id)
|
|
28
|
+
|
|
29
|
+
if (!number) {
|
|
30
|
+
// Equation not found or not yet registered
|
|
31
|
+
return (
|
|
32
|
+
<span className={className} style={{ color: '#cc0000' }} {...props}>
|
|
33
|
+
??
|
|
34
|
+
</span>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let display
|
|
39
|
+
switch (format) {
|
|
40
|
+
case 'plain':
|
|
41
|
+
display = String(number)
|
|
42
|
+
break
|
|
43
|
+
case 'equation':
|
|
44
|
+
display = `Equation ${number}`
|
|
45
|
+
break
|
|
46
|
+
case 'parentheses':
|
|
47
|
+
default:
|
|
48
|
+
display = `(${number})`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<a
|
|
53
|
+
href={`#${id}`}
|
|
54
|
+
className={className}
|
|
55
|
+
style={{ textDecoration: 'none' }}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
{display}
|
|
59
|
+
</a>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default EquationRef
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Math Component
|
|
3
|
+
*
|
|
4
|
+
* Renders LaTeX math expressions using KaTeX.
|
|
5
|
+
* Supports both inline and display (block) modes.
|
|
6
|
+
*
|
|
7
|
+
* @module @uniweb/scholar/math/Math
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useEffect, useState } from 'react'
|
|
11
|
+
import { loadKatex } from './katex-loader.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Math - Render LaTeX math expressions
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} props
|
|
17
|
+
* @param {string} props.children - LaTeX expression to render
|
|
18
|
+
* @param {boolean} [props.display=false] - Display mode (block) vs inline
|
|
19
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // Inline math
|
|
23
|
+
* <Math>E = mc^2</Math>
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // Display (block) math
|
|
27
|
+
* <Math display>
|
|
28
|
+
* \int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
|
|
29
|
+
* </Math>
|
|
30
|
+
*/
|
|
31
|
+
export function Math({ children, display = false, className, ...props }) {
|
|
32
|
+
const [html, setHtml] = useState(null)
|
|
33
|
+
const [error, setError] = useState(null)
|
|
34
|
+
|
|
35
|
+
// Extract LaTeX string from children
|
|
36
|
+
const latex = typeof children === 'string' ? children.trim() : ''
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!latex) return
|
|
40
|
+
|
|
41
|
+
let cancelled = false
|
|
42
|
+
|
|
43
|
+
async function render() {
|
|
44
|
+
const katex = await loadKatex()
|
|
45
|
+
if (cancelled || !katex) return
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const rendered = katex.renderToString(latex, {
|
|
49
|
+
displayMode: display,
|
|
50
|
+
throwOnError: false,
|
|
51
|
+
errorColor: '#cc0000',
|
|
52
|
+
})
|
|
53
|
+
if (!cancelled) {
|
|
54
|
+
setHtml(rendered)
|
|
55
|
+
setError(null)
|
|
56
|
+
}
|
|
57
|
+
} catch (e) {
|
|
58
|
+
if (!cancelled) {
|
|
59
|
+
setError(e.message)
|
|
60
|
+
setHtml(null)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
render()
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
cancelled = true
|
|
69
|
+
}
|
|
70
|
+
}, [latex, display])
|
|
71
|
+
|
|
72
|
+
// Error state
|
|
73
|
+
if (error) {
|
|
74
|
+
return (
|
|
75
|
+
<span
|
|
76
|
+
className={className}
|
|
77
|
+
style={{ color: '#cc0000', fontFamily: 'monospace' }}
|
|
78
|
+
title={error}
|
|
79
|
+
{...props}
|
|
80
|
+
>
|
|
81
|
+
{latex}
|
|
82
|
+
</span>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Loading state - show raw LaTeX in code format
|
|
87
|
+
if (!html) {
|
|
88
|
+
return (
|
|
89
|
+
<code className={className} {...props}>
|
|
90
|
+
{latex}
|
|
91
|
+
</code>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Rendered math
|
|
96
|
+
if (display) {
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
className={className}
|
|
100
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
101
|
+
{...props}
|
|
102
|
+
/>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<span
|
|
108
|
+
className={className}
|
|
109
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
110
|
+
{...props}
|
|
111
|
+
/>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default Math
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Math Module
|
|
3
|
+
*
|
|
4
|
+
* Components for rendering LaTeX math expressions using KaTeX.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/scholar/math
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { Math } from './Math.jsx'
|
|
10
|
+
export { Equation, EquationProvider, EquationContext, useEquations } from './Equation.jsx'
|
|
11
|
+
export { EquationRef } from './EquationRef.jsx'
|
|
12
|
+
export { loadKatex, isKatexLoaded, renderLatex } from './katex-loader.js'
|