@uniweb/scholar 0.1.0 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/scholar",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Academic content utilities for Uniweb - math rendering, bibliographic references, and inline citations",
5
5
  "type": "module",
6
6
  "exports": {
@@ -41,9 +41,13 @@
41
41
  "node": ">=20.19"
42
42
  },
43
43
  "dependencies": {
44
+ "@citestyle/bibtex": "^1.0.0",
45
+ "@citestyle/core": "^1.0.0",
46
+ "@citestyle/registry": "^1.0.0",
47
+ "@citestyle/styles": "^1.0.0",
44
48
  "katex": "^0.16.0"
45
49
  },
46
50
  "peerDependencies": {
47
51
  "react": "^18.0.0 || ^19.0.0"
48
52
  }
49
- }
53
+ }
@@ -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
+ }
@@ -1,69 +1,49 @@
1
1
  /**
2
2
  * Bibliography Component
3
3
  *
4
- * Renders a formatted list of references that have been cited in the document.
4
+ * Renders a formatted list of references using @citestyle/registry.
5
+ * The registry handles sorting, year-suffix disambiguation, and
6
+ * subsequent-author-substitute (em-dash for repeated authors).
5
7
  *
6
8
  * @module @uniweb/scholar/citations/Bibliography
7
9
  */
8
10
 
9
11
  import React, { useMemo } from 'react'
10
12
  import { useCitation } from './CitationProvider.jsx'
11
- import { formatReference } from '../bibliography/formatters/index.js'
12
13
 
13
14
  /**
14
- * Bibliography - Renders the list of cited references
15
+ * Bibliography - Renders the list of references
15
16
  *
16
17
  * @param {Object} props
17
18
  * @param {string} [props.title='References'] - Section title
18
19
  * @param {boolean} [props.showTitle=true] - Whether to show the title
19
- * @param {boolean} [props.showAll=false] - Show all references, not just cited ones
20
20
  * @param {string} [props.className] - Additional CSS classes
21
21
  * @param {string} [props.itemClassName] - CSS classes for each item
22
22
  *
23
23
  * @example
24
24
  * <Bibliography title="Works Cited" />
25
- *
26
- * @example
27
- * // Show all references including uncited ones
28
- * <Bibliography showAll />
29
25
  */
30
26
  export function Bibliography({
31
27
  title = 'References',
32
28
  showTitle = true,
33
- showAll = false,
34
29
  className,
35
30
  itemClassName,
36
31
  ...props
37
32
  }) {
38
- const { getCitedReferences, references, style, cited } = useCitation()
39
-
40
- // Get references to display
41
- const refsToShow = useMemo(() => {
42
- if (showAll) {
43
- return references
44
- }
45
- return getCitedReferences()
46
- }, [showAll, references, getCitedReferences])
33
+ const { getBibliography, styleMeta } = useCitation()
47
34
 
48
- // Format references
49
- const formattedRefs = useMemo(() => {
50
- return refsToShow.map((ref) => {
51
- const number = cited.get(ref.id) || cited.get(ref.citationKey)
52
- return {
53
- id: ref.id || ref.citationKey,
54
- number,
55
- formatted: formatReference(ref, { style }),
56
- reference: ref,
57
- }
58
- })
59
- }, [refsToShow, style, cited])
35
+ // Get the fully formatted bibliography from the registry.
36
+ // The registry applies: sorting, subsequent-author-substitute,
37
+ // year-suffix disambiguation, and citation numbering.
38
+ const entries = useMemo(() => getBibliography(), [getBibliography])
60
39
 
61
- if (!formattedRefs.length) {
40
+ if (!entries.length) {
62
41
  return null
63
42
  }
64
43
 
65
- // Use numbered list for IEEE style
66
- const isNumeric = style === 'ieee'
44
+ // Use numbered list for numeric styles (IEEE, Vancouver, etc.)
45
+ const isNumeric = styleMeta?.class === 'note' ||
46
+ styleMeta?.collapse === 'citation-number'
67
47
 
68
48
  return (
69
49
  <section className={className} {...props}>
@@ -87,38 +67,30 @@ export function Bibliography({
87
67
  lineHeight: '1.8',
88
68
  }}
89
69
  >
90
- {formattedRefs.map((ref) => (
70
+ {entries.map((entry, i) => (
91
71
  <li
92
- key={ref.id}
93
- id={`ref-${ref.id}`}
72
+ key={entry.parts?.id || i}
73
+ id={`ref-${entry.parts?.id || i}`}
94
74
  className={itemClassName}
95
- style={{
96
- marginBottom: '0.75rem',
97
- }}
98
- >
99
- {ref.formatted}
100
- </li>
75
+ style={{ marginBottom: '0.75rem' }}
76
+ dangerouslySetInnerHTML={{ __html: entry.html }}
77
+ />
101
78
  ))}
102
79
  </ol>
103
80
  ) : (
104
- <div
105
- style={{
106
- lineHeight: '1.8',
107
- }}
108
- >
109
- {formattedRefs.map((ref) => (
110
- <p
111
- key={ref.id}
112
- id={`ref-${ref.id}`}
81
+ <div style={{ lineHeight: '1.8' }}>
82
+ {entries.map((entry, i) => (
83
+ <div
84
+ key={entry.parts?.id || i}
85
+ id={`ref-${entry.parts?.id || i}`}
113
86
  className={itemClassName}
114
87
  style={{
115
88
  marginBottom: '0.75rem',
116
89
  paddingLeft: '2rem',
117
90
  textIndent: '-2rem',
118
91
  }}
119
- >
120
- {ref.formatted}
121
- </p>
92
+ dangerouslySetInnerHTML={{ __html: entry.html }}
93
+ />
122
94
  ))}
123
95
  </div>
124
96
  )}