@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 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.0",
4
- "description": "Academic content utilities for Uniweb - math rendering, bibliographic references, and inline citations",
3
+ "version": "0.2.2",
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",
@@ -41,9 +40,23 @@
41
40
  "node": ">=20.19"
42
41
  },
43
42
  "dependencies": {
44
- "katex": "^0.16.0"
43
+ "@citestyle/bibtex": "^1.0.0",
44
+ "@citestyle/core": "^1.0.0",
45
+ "@citestyle/registry": "^1.0.0",
46
+ "@citestyle/styles": "^1.0.0"
45
47
  },
46
48
  "peerDependencies": {
47
49
  "react": "^18.0.0 || ^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": "^2.0.0"
57
+ },
58
+ "scripts": {
59
+ "test": "vitest run",
60
+ "test:watch": "vitest"
48
61
  }
49
62
  }
@@ -1,39 +1,58 @@
1
1
  /**
2
2
  * Bibliography Formatters
3
3
  *
4
+ * Delegates to @citestyle/* compiled styles for reference formatting.
5
+ * Returns structured output: { html, text, parts, links }.
6
+ *
4
7
  * @module @uniweb/scholar/bibliography/formatters
5
8
  */
6
9
 
7
- export { formatApa } from './apa.js'
8
- export { formatMla } from './mla.js'
9
- export { formatChicago } from './chicago.js'
10
- export { formatIeee } from './ieee.js'
10
+ import * as apa from '@citestyle/styles/apa'
11
+ import * as mla from '@citestyle/styles/mla'
12
+ import * as chicagoAD from '@citestyle/styles/chicago-author-date'
13
+ import * as ieee from '@citestyle/styles/ieee'
14
+ import * as harvard from '@citestyle/styles/harvard'
15
+ import * as vancouver from '@citestyle/styles/vancouver'
16
+ import { toCslJson } from '../normalize.js'
11
17
 
12
- import { formatApa } from './apa.js'
13
- import { formatMla } from './mla.js'
14
- import { formatChicago } from './chicago.js'
15
- import { formatIeee } from './ieee.js'
18
+ /**
19
+ * Compiled styles keyed by short name.
20
+ * Each value is a compiled style module with bibliography(), citation(), meta.
21
+ */
22
+ export const compiledStyles = {
23
+ apa,
24
+ mla,
25
+ chicago: chicagoAD,
26
+ ieee,
27
+ harvard,
28
+ vancouver,
29
+ }
16
30
 
17
31
  /**
18
- * Available citation style formatters
32
+ * Human-readable labels for each style.
19
33
  */
20
- export const formatters = {
21
- apa: formatApa,
22
- mla: formatMla,
23
- chicago: formatChicago,
24
- ieee: formatIeee,
34
+ const styleLabels = {
35
+ apa: 'APA (7th ed.)',
36
+ mla: 'MLA (9th ed.)',
37
+ chicago: 'Chicago (Author-Date)',
38
+ ieee: 'IEEE',
39
+ harvard: 'Harvard',
40
+ vancouver: 'Vancouver',
25
41
  }
26
42
 
27
43
  /**
28
- * Format a reference in a specific style
44
+ * Format a reference in a specific citation style.
45
+ *
46
+ * Accepts Scholar-shaped publication objects OR CSL-JSON items.
47
+ * Returns structured output from @citestyle — { html, text, parts, links }.
29
48
  *
30
- * @param {Object} publication - Publication data
49
+ * @param {Object} publication - Publication data (Scholar or CSL-JSON)
31
50
  * @param {Object} [options]
32
- * @param {string} [options.style='apa'] - Citation style (apa, mla, chicago, ieee)
33
- * @returns {string} Formatted reference string
51
+ * @param {string} [options.style='apa'] - Citation style name
52
+ * @returns {{ html: string, text: string, parts: Object, links: Object }}
34
53
  *
35
54
  * @example
36
- * const ref = formatReference({
55
+ * const entry = formatReference({
37
56
  * type: 'article',
38
57
  * authors: [{ family: 'Smith', given: 'John' }],
39
58
  * year: 2024,
@@ -43,14 +62,34 @@ export const formatters = {
43
62
  * pages: '1-10',
44
63
  * doi: '10.1234/example'
45
64
  * }, { style: 'apa' })
65
+ *
66
+ * entry.text // "Smith, J. (2024). A Study of Something. ..."
67
+ * entry.html // "<span class=\"csl-entry\">Smith, J. (2024)..."
46
68
  */
47
69
  export function formatReference(publication, options = {}) {
48
70
  const { style = 'apa' } = options
49
71
 
50
- const formatter = formatters[style.toLowerCase()]
51
- if (!formatter) {
52
- throw new Error(`Unknown citation style: ${style}. Available: ${Object.keys(formatters).join(', ')}`)
72
+ const compiled = compiledStyles[style.toLowerCase()]
73
+ if (!compiled) {
74
+ throw new Error(
75
+ `Unknown citation style: ${style}. Available: ${Object.keys(compiledStyles).join(', ')}`
76
+ )
53
77
  }
54
78
 
55
- return formatter(publication, options)
79
+ const item = toCslJson(publication)
80
+ return compiled.bibliography(item, {})
81
+ }
82
+
83
+ /**
84
+ * Get metadata for all available citation styles.
85
+ * Useful for building UI style pickers.
86
+ *
87
+ * @returns {Array<{ id: string, label: string, class: string }>}
88
+ */
89
+ export function getAvailableStyles() {
90
+ return Object.entries(compiledStyles).map(([id, style]) => ({
91
+ id,
92
+ label: styleLabels[id] || style.meta?.title || id,
93
+ class: style.meta?.class || 'in-text',
94
+ }))
56
95
  }
@@ -1,42 +1,21 @@
1
1
  /**
2
2
  * Bibliography Module
3
3
  *
4
- * Utilities for formatting bibliographic references in various citation styles.
4
+ * Reference formatting powered by @citestyle/* compiled styles,
5
+ * BibTeX parsing and export, and data normalization utilities.
5
6
  *
6
7
  * @module @uniweb/scholar/bibliography
7
8
  */
8
9
 
9
- // Formatters
10
+ // Formatters — citestyle-powered
10
11
  export {
11
12
  formatReference,
12
- formatApa,
13
- formatMla,
14
- formatChicago,
15
- formatIeee,
16
- formatters,
13
+ getAvailableStyles,
14
+ compiledStyles,
17
15
  } from './formatters/index.js'
18
16
 
19
- // Parsers
17
+ // Parsers — delegated to @citestyle/bibtex
20
18
  export { parseBibtex, exportBibtex } from './parsers/index.js'
21
19
 
22
- // Utilities
23
- export {
24
- formatAuthors,
25
- parseAuthorName,
26
- parseAuthorString,
27
- getInitials,
28
- formatApaAuthors,
29
- formatMlaAuthors,
30
- formatChicagoAuthors,
31
- formatIeeeAuthors,
32
- } from './utils/authors.js'
33
-
34
- export {
35
- formatDate,
36
- parseDate,
37
- getYear,
38
- formatApaDate,
39
- formatMlaDate,
40
- formatChicagoDate,
41
- formatIeeeDate,
42
- } from './utils/dates.js'
20
+ // Normalization — Scholar ↔ CSL-JSON bridge
21
+ export { toCslJson, fromCslJson, mapType, unmapType } from './normalize.js'
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Data Normalization Layer
3
+ *
4
+ * Bidirectional bridge between Scholar's publication objects (BibTeX-style
5
+ * field names) and CSL-JSON items (the format @citestyle/* expects).
6
+ *
7
+ * @module @uniweb/scholar/bibliography/normalize
8
+ */
9
+
10
+ // Scholar type → CSL-JSON type
11
+ const TYPE_TO_CSL = {
12
+ article: 'article-journal',
13
+ journal: 'article-journal',
14
+ book: 'book',
15
+ inbook: 'chapter',
16
+ incollection: 'chapter',
17
+ chapter: 'chapter',
18
+ inproceedings: 'paper-conference',
19
+ conference: 'paper-conference',
20
+ webpage: 'webpage',
21
+ website: 'webpage',
22
+ online: 'webpage',
23
+ thesis: 'thesis',
24
+ phdthesis: 'thesis',
25
+ mastersthesis: 'thesis',
26
+ report: 'report',
27
+ techreport: 'report',
28
+ misc: 'article', // sensible default
29
+ }
30
+
31
+ // CSL-JSON type → Scholar type (reverse mapping, picks first match)
32
+ const TYPE_FROM_CSL = {
33
+ 'article-journal': 'article',
34
+ book: 'book',
35
+ chapter: 'incollection',
36
+ 'paper-conference': 'inproceedings',
37
+ webpage: 'webpage',
38
+ thesis: 'thesis',
39
+ report: 'report',
40
+ article: 'article',
41
+ }
42
+
43
+ /**
44
+ * Map a Scholar entry type to a CSL-JSON type.
45
+ *
46
+ * @param {string} scholarType
47
+ * @returns {string}
48
+ */
49
+ export function mapType(scholarType) {
50
+ if (!scholarType) return 'article-journal'
51
+ return TYPE_TO_CSL[scholarType.toLowerCase()] || 'article-journal'
52
+ }
53
+
54
+ /**
55
+ * Map a CSL-JSON type back to a Scholar entry type.
56
+ *
57
+ * @param {string} cslType
58
+ * @returns {string}
59
+ */
60
+ export function unmapType(cslType) {
61
+ if (!cslType) return 'article'
62
+ return TYPE_FROM_CSL[cslType] || cslType
63
+ }
64
+
65
+ /**
66
+ * Convert a Scholar publication object to a CSL-JSON item.
67
+ *
68
+ * Scholar uses BibTeX-style field names (`authors`, `journal`, `year`).
69
+ * Citestyle expects CSL-JSON (`author`, `container-title`, `issued`).
70
+ *
71
+ * @param {Object} pub - Scholar publication object
72
+ * @returns {Object} CSL-JSON item
73
+ */
74
+ export function toCslJson(pub) {
75
+ if (!pub) return {}
76
+
77
+ // If it already looks like CSL-JSON (has `issued` or `container-title`),
78
+ // pass through with minimal normalization
79
+ if (pub.issued || pub['container-title']) {
80
+ return { id: pub.id || pub.citationKey || 'unknown', ...pub }
81
+ }
82
+
83
+ const item = {
84
+ id: pub.id || pub.citationKey || 'unknown',
85
+ type: mapType(pub.type),
86
+ }
87
+
88
+ // Authors — Scholar uses `authors` (array), CSL-JSON uses `author`
89
+ if (pub.authors?.length) {
90
+ item.author = pub.authors.map(normalizeName)
91
+ } else if (pub.author) {
92
+ // Already CSL-JSON shape
93
+ item.author = Array.isArray(pub.author)
94
+ ? pub.author.map(normalizeName)
95
+ : undefined
96
+ }
97
+
98
+ // Editors
99
+ if (pub.editors?.length) {
100
+ item.editor = pub.editors.map(normalizeName)
101
+ }
102
+
103
+ // Title
104
+ if (pub.title) item.title = pub.title
105
+
106
+ // Container title — Scholar uses `journal` or `bookTitle`
107
+ const container = pub.journal || pub.bookTitle || pub.containerTitle
108
+ if (container) item['container-title'] = container
109
+
110
+ // Date — Scholar uses `year` (number), CSL-JSON uses `issued`
111
+ if (pub.year) {
112
+ item.issued = { 'date-parts': [[Number(pub.year)]] }
113
+ } else if (pub.date) {
114
+ // Try to parse an ISO date string
115
+ const parts = String(pub.date).split('-').map(Number)
116
+ item.issued = { 'date-parts': [parts.filter((n) => !isNaN(n))] }
117
+ }
118
+
119
+ // Standard fields
120
+ if (pub.volume) item.volume = String(pub.volume)
121
+ if (pub.issue) item.issue = String(pub.issue)
122
+ if (pub.pages) item.page = pub.pages.replace('–', '-')
123
+ if (pub.page) item.page = pub.page
124
+ if (pub.doi) item.DOI = pub.doi.replace(/^https?:\/\/doi\.org\//, '')
125
+ if (pub.DOI) item.DOI = pub.DOI
126
+ if (pub.url) item.URL = pub.url
127
+ if (pub.URL) item.URL = pub.URL
128
+ if (pub.publisher) item.publisher = pub.publisher
129
+ if (pub.isbn) item.ISBN = pub.isbn
130
+ if (pub.issn) item.ISSN = pub.issn
131
+ if (pub.abstract) item.abstract = pub.abstract
132
+ if (pub.edition) item.edition = pub.edition
133
+
134
+ // Thesis-specific
135
+ if (pub.school || pub.institution) {
136
+ item.publisher = pub.school || pub.institution
137
+ }
138
+
139
+ return item
140
+ }
141
+
142
+ /**
143
+ * Convert a CSL-JSON item to a Scholar publication object.
144
+ *
145
+ * Used for backward compatibility when components expect Scholar-shaped
146
+ * objects (`pub.authors`, `pub.journal`, `pub.year`).
147
+ *
148
+ * @param {Object} item - CSL-JSON item
149
+ * @returns {Object} Scholar publication object
150
+ */
151
+ export function fromCslJson(item) {
152
+ if (!item) return {}
153
+
154
+ const pub = {
155
+ id: item.id,
156
+ type: unmapType(item.type),
157
+ }
158
+
159
+ // Authors
160
+ if (item.author?.length) {
161
+ pub.authors = item.author.map((a) => ({
162
+ family: a.family || '',
163
+ given: a.given || '',
164
+ ...(a.suffix ? { suffix: a.suffix } : {}),
165
+ }))
166
+ }
167
+
168
+ // Editors
169
+ if (item.editor?.length) {
170
+ pub.editors = item.editor.map((a) => ({
171
+ family: a.family || '',
172
+ given: a.given || '',
173
+ }))
174
+ }
175
+
176
+ // Title
177
+ if (item.title) pub.title = item.title
178
+
179
+ // Container title → journal or bookTitle
180
+ if (item['container-title']) {
181
+ if (pub.type === 'article' || pub.type === 'journal') {
182
+ pub.journal = item['container-title']
183
+ } else {
184
+ pub.bookTitle = item['container-title']
185
+ }
186
+ }
187
+
188
+ // Date
189
+ if (item.issued?.['date-parts']?.[0]) {
190
+ pub.year = item.issued['date-parts'][0][0]
191
+ }
192
+
193
+ // Standard fields
194
+ if (item.volume) pub.volume = item.volume
195
+ if (item.issue) pub.issue = item.issue
196
+ if (item.page) pub.pages = item.page
197
+ if (item.DOI) pub.doi = item.DOI
198
+ if (item.URL) pub.url = item.URL
199
+ if (item.publisher) pub.publisher = item.publisher
200
+ if (item.ISBN) pub.isbn = item.ISBN
201
+ if (item.ISSN) pub.issn = item.ISSN
202
+ if (item.abstract) pub.abstract = item.abstract
203
+ if (item.edition) pub.edition = item.edition
204
+
205
+ return pub
206
+ }
207
+
208
+ /**
209
+ * Normalize a name object — ensures {family, given} shape.
210
+ * Handles strings, objects with various key patterns.
211
+ *
212
+ * @param {Object|string} name
213
+ * @returns {{ family: string, given: string, suffix?: string }}
214
+ */
215
+ function normalizeName(name) {
216
+ if (typeof name === 'string') {
217
+ // "Last, First" or "First Last"
218
+ if (name.includes(',')) {
219
+ const [family, given] = name.split(',').map((s) => s.trim())
220
+ return { family, given }
221
+ }
222
+ const parts = name.trim().split(/\s+/)
223
+ return {
224
+ family: parts[parts.length - 1],
225
+ given: parts.slice(0, -1).join(' '),
226
+ }
227
+ }
228
+
229
+ return {
230
+ family: name.family || name.lastName || '',
231
+ given: name.given || name.firstName || '',
232
+ ...(name.suffix ? { suffix: name.suffix } : {}),
233
+ }
234
+ }
@@ -1,7 +1,41 @@
1
1
  /**
2
2
  * Bibliography Parsers
3
3
  *
4
+ * Delegates to @citestyle/bibtex for BibTeX parsing and export.
5
+ * Returns CSL-JSON items directly — no scholar-shaped wrapper.
6
+ *
4
7
  * @module @uniweb/scholar/bibliography/parsers
5
8
  */
6
9
 
7
- export { parseBibtex, exportBibtex } from './bibtex.js'
10
+ import {
11
+ parseBibtex as csParseBibtex,
12
+ exportBibtex as csExportBibtex,
13
+ } from '@citestyle/bibtex'
14
+ import { toCslJson } from '../normalize.js'
15
+
16
+ /**
17
+ * Parse a BibTeX string into an array of CSL-JSON items.
18
+ *
19
+ * @param {string} bibtex - BibTeX source string
20
+ * @returns {Array<Object>} Array of CSL-JSON items
21
+ */
22
+ export const parseBibtex = csParseBibtex
23
+
24
+ /**
25
+ * Export a single publication (Scholar or CSL-JSON) to BibTeX format.
26
+ *
27
+ * Wraps @citestyle/bibtex's exportBibtex, which expects an array of
28
+ * CSL-JSON items. This accepts a single Scholar-shaped pub for convenience.
29
+ *
30
+ * @param {Object} publication - Publication data (Scholar or CSL-JSON)
31
+ * @returns {string} BibTeX string
32
+ */
33
+ export function exportBibtex(publication) {
34
+ if (!publication) return ''
35
+
36
+ // If it's a string, it's already BibTeX — pass through
37
+ if (typeof publication === 'string') return publication
38
+
39
+ const item = toCslJson(publication)
40
+ return csExportBibtex([item])
41
+ }