@uniweb/scholar 0.2.1 → 0.2.3

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 CHANGED
@@ -12,28 +12,21 @@ pnpm add @uniweb/scholar
12
12
 
13
13
  ## Features
14
14
 
15
- - **Math Rendering** - LaTeX equations with KaTeX (lazy-loaded)
15
+ - **Numbered Equations** - Cross-referenceable display equations
16
16
  - **Bibliography** - Format references in APA, MLA, Chicago, and IEEE styles
17
17
  - **Citations** - Track inline citations with automatic bibliography generation
18
18
  - **BibTeX** - Parse and export BibTeX format
19
19
  - **UI Components** - Ready-to-use citation buttons, author lists, and DOI links
20
20
 
21
+ > **Plain LaTeX math** (inline `$x$`, display `$$x$$`, fenced ```` ```math ````) is handled by Uniweb's content pipeline — the markdown parser compiles LaTeX to MathML Core at build time and the browser renders it natively. No runtime math library needed; no `<Math>` component required.
22
+
21
23
  ## Quick Start
22
24
 
23
- ### Math Equations
25
+ ### Numbered Equations
24
26
 
25
27
  ```jsx
26
- import { Math, Equation, EquationRef, EquationProvider } from '@uniweb/scholar/math'
27
-
28
- // Inline math
29
- <p>The equation <Math>E = mc^2</Math> shows mass-energy equivalence.</p>
28
+ import { Equation, EquationRef, EquationProvider } from '@uniweb/scholar/math'
30
29
 
31
- // Display (block) math
32
- <Math display>
33
- \int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
34
- </Math>
35
-
36
- // Numbered equations with cross-references
37
30
  <EquationProvider>
38
31
  <Equation id="einstein">E = mc^2</Equation>
39
32
  <p>As shown in <EquationRef id="einstein" />, energy equals mass times the speed of light squared.</p>
@@ -97,29 +90,49 @@ const bibtex = `
97
90
 
98
91
  ### Math Module
99
92
 
100
- #### `<Math>`
93
+ Scholar does not ship a math renderer. Plain LaTeX in markdown is compiled to MathML by Uniweb's content pipeline and rendered natively by the browser — see the framework docs for the `$x$`, `$$x$$`, and ```math fence syntax.
101
94
 
102
- Renders inline or display LaTeX math.
95
+ `<Equation>` and `<EquationRef>` cover the distinct feature of **numbered cross-referenceable equations**. They consume pre-compiled MathML from `content.math` and are fully SSR-safe.
103
96
 
104
- | Prop | Type | Default | Description |
105
- |------|------|---------|-------------|
106
- | `children` | `string` | - | LaTeX expression |
107
- | `display` | `boolean` | `false` | Display mode (block) vs inline |
108
- | `className` | `string` | - | CSS classes |
97
+ Label an equation in markdown with a `:<id>` suffix on the fence:
98
+
99
+ ````markdown
100
+ ```math:einstein
101
+ E = mc^2
102
+ ```
103
+ ````
104
+
105
+ Then in a foundation's section component:
106
+
107
+ ```jsx
108
+ import { Equation, EquationProvider, EquationRef } from '@uniweb/scholar/math'
109
+
110
+ function MyBlock({ content }) {
111
+ return (
112
+ <EquationProvider>
113
+ {content.math?.filter(m => m.id).map(m => (
114
+ <Equation key={m.id} id={m.id} mathml={m.mathml} />
115
+ ))}
116
+ <p>As shown in <EquationRef id="einstein" />, energy equals mc².</p>
117
+ </EquationProvider>
118
+ )
119
+ }
120
+ ```
109
121
 
110
122
  #### `<Equation>`
111
123
 
112
- Renders a numbered equation with optional label.
124
+ Numbered display equation rendered from pre-compiled MathML.
113
125
 
114
126
  | Prop | Type | Default | Description |
115
127
  |------|------|---------|-------------|
116
- | `children` | `string` | - | LaTeX expression |
117
- | `id` | `string` | auto | Unique ID for cross-referencing |
118
- | `label` | `string` | - | Custom label (defaults to number) |
128
+ | `id` | `string` | auto | Identifier for cross-referencing |
129
+ | `mathml` | `string` | - | Pre-compiled MathML from `content.math[i].mathml` |
130
+ | `label` | `string` | - | Override the auto-number with a custom label |
131
+ | `className` | `string` | - | CSS classes on the wrapper |
119
132
 
120
133
  #### `<EquationRef>`
121
134
 
122
- Creates a reference link to a numbered equation.
135
+ Link to a numbered equation within the same `<EquationProvider>`.
123
136
 
124
137
  | Prop | Type | Default | Description |
125
138
  |------|------|---------|-------------|
@@ -128,7 +141,7 @@ Creates a reference link to a numbered equation.
128
141
 
129
142
  #### `<EquationProvider>`
130
143
 
131
- Context provider for equation numbering. Wrap content containing equations.
144
+ Context provider for equation numbering. Wrap content containing equations and their references.
132
145
 
133
146
  ### Citations Module
134
147
 
@@ -280,17 +293,14 @@ import { DoiLink } from '@uniweb/scholar/components'
280
293
 
281
294
  ## Bundle Size
282
295
 
283
- The package uses lazy loading to minimize initial bundle size:
296
+ Scholar ships no math renderer — MathML is pre-compiled upstream and rendered natively by the browser. Approximate subpath sizes:
284
297
 
285
298
  | Module | Size |
286
299
  |--------|------|
287
300
  | bibliography (formatters, parsers) | ~15KB |
288
- | math (without KaTeX) | ~5KB |
289
- | KaTeX (lazy-loaded on first use) | ~90KB |
290
- | citations | ~10KB |
291
- | components | ~8KB |
292
-
293
- KaTeX CSS is automatically injected when math rendering is first used.
301
+ | math (numbered equations, refs) | ~5KB |
302
+ | citations | ~10KB |
303
+ | components | ~8KB |
294
304
 
295
305
  ## License
296
306
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uniweb/scholar",
3
- "version": "0.2.1",
4
- "description": "Academic content utilities for Uniweb - math rendering, bibliographic references, and inline citations",
3
+ "version": "0.2.3",
4
+ "description": "Academic content utilities for Uniweb - numbered equations with cross-references, bibliographic references, and inline citations",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.js",
@@ -18,9 +18,8 @@
18
18
  "uniweb",
19
19
  "scholar",
20
20
  "academic",
21
- "math",
22
- "katex",
23
- "latex",
21
+ "equations",
22
+ "mathml",
24
23
  "bibliography",
25
24
  "citations",
26
25
  "bibtex",
@@ -44,10 +43,20 @@
44
43
  "@citestyle/bibtex": "^1.0.0",
45
44
  "@citestyle/core": "^1.0.0",
46
45
  "@citestyle/registry": "^1.0.0",
47
- "@citestyle/styles": "^1.0.0",
48
- "katex": "^0.16.0"
46
+ "@citestyle/styles": "^1.0.0"
49
47
  },
50
48
  "peerDependencies": {
51
- "react": "^18.0.0 || ^19.0.0"
49
+ "react": "^19.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "@testing-library/react": "^16.0.0",
53
+ "jsdom": "^25.0.0",
54
+ "react": "^19.0.0",
55
+ "react-dom": "^19.0.0",
56
+ "vitest": "^4.1.7"
57
+ },
58
+ "scripts": {
59
+ "test": "vitest run",
60
+ "test:watch": "vitest"
52
61
  }
53
62
  }
package/src/index.js CHANGED
@@ -11,8 +11,12 @@
11
11
  * @module @uniweb/scholar
12
12
  */
13
13
 
14
- // Math components
15
- export { Math, Equation, EquationRef, loadKatex } from './math/index.js'
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'
16
20
 
17
21
  // Bibliography — formatting, parsing, normalization
18
22
  export {
@@ -1,31 +1,49 @@
1
1
  /**
2
2
  * Equation Component
3
3
  *
4
- * Renders a numbered equation with an optional label for cross-referencing.
5
- * Equations are numbered sequentially within a CitationProvider context.
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, { useEffect, useState, useContext, useId } from '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
- * Wrap your content with this to enable automatic equation numbering.
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 to access equation context
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 - Numbered display equation with optional label
79
+ * Equation — numbered display equation rendered from pre-compiled MathML.
61
80
  *
62
81
  * @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>
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({ children, id, label, className, ...props }) {
75
- const [html, setHtml] = useState(null)
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
- {/* 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 */}
114
+ <div
115
+ style={{ flex: 1, textAlign: 'center' }}
116
+ dangerouslySetInnerHTML={{ __html: mathml || '' }}
117
+ />
148
118
  {displayLabel && (
149
119
  <span
150
120
  style={{
package/src/math/index.js CHANGED
@@ -1,12 +1,27 @@
1
1
  /**
2
2
  * Math Module
3
3
  *
4
- * Components for rendering LaTeX math expressions using KaTeX.
4
+ * Numbered equations and cross-references, rendered from pre-compiled
5
+ * MathML produced by Uniweb's content pipeline.
6
+ *
7
+ * Plain inline and display math (`$x$`, `$$x$$`, fenced ```math) is
8
+ * handled entirely by the content pipeline — authors write LaTeX in
9
+ * markdown and the browser renders real MathML natively. No component
10
+ * wrapper needed. The former `<Math>` component (KaTeX-based) was
11
+ * removed in an earlier release.
12
+ *
13
+ * `<Equation>` and `<EquationRef>` remain for the distinct feature of
14
+ * numbered cross-referenceable equations. They consume `content.math`
15
+ * entries (which carry pre-compiled MathML and an optional id); scholar
16
+ * no longer ships a math renderer.
5
17
  *
6
18
  * @module @uniweb/scholar/math
7
19
  */
8
20
 
9
- export { Math } from './Math.jsx'
10
- export { Equation, EquationProvider, EquationContext, useEquations } from './Equation.jsx'
21
+ export {
22
+ Equation,
23
+ EquationProvider,
24
+ EquationContext,
25
+ useEquations,
26
+ } from './Equation.jsx'
11
27
  export { EquationRef } from './EquationRef.jsx'
12
- export { loadKatex, isKatexLoaded, renderLatex } from './katex-loader.js'
package/src/math/Math.jsx DELETED
@@ -1,115 +0,0 @@
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
@@ -1,105 +0,0 @@
1
- /**
2
- * KaTeX Lazy Loader
3
- *
4
- * Provides lazy loading of KaTeX with module-level caching.
5
- * KaTeX is only loaded when math rendering is actually needed.
6
- *
7
- * @module @uniweb/scholar/math/katex-loader
8
- */
9
-
10
- // Module-level state for singleton pattern
11
- let katexInstance = null
12
- let katexLoadPromise = null
13
- let cssInjected = false
14
-
15
- /**
16
- * CDN URL for KaTeX CSS (matches the katex version in package.json)
17
- */
18
- const KATEX_CSS_URL = 'https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css'
19
-
20
- /**
21
- * Inject KaTeX CSS into the document head
22
- * Only injects once per page load
23
- */
24
- function injectKatexCSS() {
25
- if (cssInjected || typeof document === 'undefined') return
26
-
27
- const styleId = 'uniweb-katex-css'
28
-
29
- // Check if already injected
30
- if (document.getElementById(styleId)) {
31
- cssInjected = true
32
- return
33
- }
34
-
35
- const link = document.createElement('link')
36
- link.id = styleId
37
- link.rel = 'stylesheet'
38
- link.href = KATEX_CSS_URL
39
- link.crossOrigin = 'anonymous'
40
- document.head.appendChild(link)
41
-
42
- cssInjected = true
43
- }
44
-
45
- /**
46
- * Load KaTeX lazily
47
- *
48
- * Returns cached instance if already loaded, otherwise loads
49
- * KaTeX dynamically and injects its CSS.
50
- *
51
- * @returns {Promise<Object>} The KaTeX module
52
- */
53
- export async function loadKatex() {
54
- if (katexInstance) return katexInstance
55
- if (katexLoadPromise) return katexLoadPromise
56
-
57
- katexLoadPromise = (async () => {
58
- try {
59
- const katex = await import('katex')
60
- katexInstance = katex.default || katex
61
-
62
- // Inject CSS when KaTeX is loaded
63
- injectKatexCSS()
64
-
65
- return katexInstance
66
- } catch (error) {
67
- console.warn('[scholar/math] Failed to load KaTeX:', error)
68
- katexLoadPromise = null
69
- return null
70
- }
71
- })()
72
-
73
- return katexLoadPromise
74
- }
75
-
76
- /**
77
- * Check if KaTeX is already loaded
78
- *
79
- * @returns {boolean} True if KaTeX is loaded and ready
80
- */
81
- export function isKatexLoaded() {
82
- return katexInstance !== null
83
- }
84
-
85
- /**
86
- * Render LaTeX to HTML string
87
- *
88
- * @param {string} latex - LaTeX string to render
89
- * @param {Object} options - KaTeX options
90
- * @returns {Promise<string|null>} Rendered HTML or null on error
91
- */
92
- export async function renderLatex(latex, options = {}) {
93
- const katex = await loadKatex()
94
- if (!katex) return null
95
-
96
- try {
97
- return katex.renderToString(latex, {
98
- throwOnError: false,
99
- ...options,
100
- })
101
- } catch (error) {
102
- console.warn('[scholar/math] LaTeX render error:', error.message)
103
- return null
104
- }
105
- }