@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.
@@ -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'