@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 +6 -2
- package/src/bibliography/formatters/index.js +62 -23
- package/src/bibliography/index.js +8 -29
- package/src/bibliography/normalize.js +234 -0
- package/src/bibliography/parsers/index.js +35 -1
- package/src/citations/Bibliography.jsx +26 -54
- package/src/citations/Citation.jsx +35 -145
- package/src/citations/CitationProvider.jsx +83 -85
- package/src/components/AuthorList.jsx +65 -40
- package/src/components/BibtexBlock.jsx +1 -1
- package/src/components/CiteButton.jsx +78 -45
- package/src/components/DoiLink.jsx +27 -21
- package/src/index.js +9 -3
- package/src/bibliography/formatters/apa.js +0 -352
- package/src/bibliography/formatters/chicago.js +0 -339
- package/src/bibliography/formatters/ieee.js +0 -332
- package/src/bibliography/formatters/mla.js +0 -335
- package/src/bibliography/parsers/bibtex.js +0 -405
- package/src/bibliography/utils/authors.js +0 -277
- package/src/bibliography/utils/dates.js +0 -228
- package/src/bibliography/utils/index.js +0 -8
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CiteButton Component
|
|
3
3
|
*
|
|
4
|
-
* A dropdown button that lets users copy citations in various formats
|
|
4
|
+
* A dropdown button that lets users copy citations in various formats,
|
|
5
|
+
* including rich text (HTML with formatting preserved for paste into Word/Docs).
|
|
5
6
|
*
|
|
6
7
|
* @module @uniweb/scholar/components/CiteButton
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import React, { useState, useRef, useEffect } from 'react'
|
|
10
|
-
import { formatReference } from '../bibliography/formatters/index.js'
|
|
11
|
-
import { exportBibtex } from '../bibliography/parsers/
|
|
11
|
+
import { formatReference, getAvailableStyles } from '../bibliography/formatters/index.js'
|
|
12
|
+
import { exportBibtex } from '../bibliography/parsers/index.js'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* CiteButton - Copy citation dropdown
|
|
15
16
|
*
|
|
16
17
|
* @param {Object} props
|
|
17
|
-
* @param {Object} props.publication - Publication data
|
|
18
|
-
* @param {Array<string>} [props.styles
|
|
18
|
+
* @param {Object} props.publication - Publication data (Scholar or CSL-JSON)
|
|
19
|
+
* @param {Array<string>} [props.styles] - Style names to show (defaults to all + bibtex)
|
|
19
20
|
* @param {string} [props.label='Cite'] - Button label
|
|
20
21
|
* @param {string} [props.className] - Additional CSS classes
|
|
21
22
|
*
|
|
@@ -24,7 +25,7 @@ import { exportBibtex } from '../bibliography/parsers/bibtex.js'
|
|
|
24
25
|
*/
|
|
25
26
|
export function CiteButton({
|
|
26
27
|
publication,
|
|
27
|
-
styles
|
|
28
|
+
styles,
|
|
28
29
|
label = 'Cite',
|
|
29
30
|
className,
|
|
30
31
|
...props
|
|
@@ -33,6 +34,16 @@ export function CiteButton({
|
|
|
33
34
|
const [copied, setCopied] = useState(null)
|
|
34
35
|
const dropdownRef = useRef(null)
|
|
35
36
|
|
|
37
|
+
// Default: all available citation styles + bibtex
|
|
38
|
+
const availableStyles = getAvailableStyles()
|
|
39
|
+
const styleIds = styles || [...availableStyles.map((s) => s.id), 'bibtex']
|
|
40
|
+
|
|
41
|
+
// Build label map from available styles
|
|
42
|
+
const styleLabels = Object.fromEntries(
|
|
43
|
+
availableStyles.map((s) => [s.id, s.label])
|
|
44
|
+
)
|
|
45
|
+
styleLabels.bibtex = 'BibTeX'
|
|
46
|
+
|
|
36
47
|
// Close dropdown when clicking outside
|
|
37
48
|
useEffect(() => {
|
|
38
49
|
function handleClickOutside(event) {
|
|
@@ -47,17 +58,35 @@ export function CiteButton({
|
|
|
47
58
|
}
|
|
48
59
|
}, [isOpen])
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Copy citation to clipboard.
|
|
63
|
+
* For citation styles, copies both plain text and rich HTML.
|
|
64
|
+
*/
|
|
51
65
|
async function copyToClipboard(style) {
|
|
52
66
|
try {
|
|
53
|
-
let text
|
|
54
67
|
if (style === 'bibtex') {
|
|
55
|
-
text = exportBibtex(publication)
|
|
68
|
+
const text = exportBibtex(publication)
|
|
69
|
+
await navigator.clipboard.writeText(text)
|
|
56
70
|
} else {
|
|
57
|
-
|
|
71
|
+
const entry = formatReference(publication, { style })
|
|
72
|
+
|
|
73
|
+
// Try rich-text copy (HTML + plain text) for formatting in Word/Docs
|
|
74
|
+
if (typeof ClipboardItem !== 'undefined') {
|
|
75
|
+
try {
|
|
76
|
+
const item = new ClipboardItem({
|
|
77
|
+
'text/html': new Blob([entry.html], { type: 'text/html' }),
|
|
78
|
+
'text/plain': new Blob([entry.text], { type: 'text/plain' }),
|
|
79
|
+
})
|
|
80
|
+
await navigator.clipboard.write([item])
|
|
81
|
+
} catch {
|
|
82
|
+
// Fallback to plain text if rich copy fails
|
|
83
|
+
await navigator.clipboard.writeText(entry.text)
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
await navigator.clipboard.writeText(entry.text)
|
|
87
|
+
}
|
|
58
88
|
}
|
|
59
89
|
|
|
60
|
-
await navigator.clipboard.writeText(text)
|
|
61
90
|
setCopied(style)
|
|
62
91
|
setTimeout(() => setCopied(null), 2000)
|
|
63
92
|
} catch (err) {
|
|
@@ -65,14 +94,6 @@ export function CiteButton({
|
|
|
65
94
|
}
|
|
66
95
|
}
|
|
67
96
|
|
|
68
|
-
const styleLabels = {
|
|
69
|
-
apa: 'APA',
|
|
70
|
-
mla: 'MLA',
|
|
71
|
-
chicago: 'Chicago',
|
|
72
|
-
ieee: 'IEEE',
|
|
73
|
-
bibtex: 'BibTeX',
|
|
74
|
-
}
|
|
75
|
-
|
|
76
97
|
return (
|
|
77
98
|
<div
|
|
78
99
|
ref={dropdownRef}
|
|
@@ -88,21 +109,34 @@ export function CiteButton({
|
|
|
88
109
|
display: 'inline-flex',
|
|
89
110
|
alignItems: 'center',
|
|
90
111
|
gap: '0.25rem',
|
|
91
|
-
padding: '0.
|
|
92
|
-
fontSize: '0.
|
|
93
|
-
fontWeight: '
|
|
94
|
-
color: '#
|
|
95
|
-
backgroundColor: '
|
|
96
|
-
border: '
|
|
97
|
-
borderRadius: '0.
|
|
112
|
+
padding: '0.125rem 0.5rem',
|
|
113
|
+
fontSize: '0.8125rem',
|
|
114
|
+
fontWeight: '400',
|
|
115
|
+
color: 'var(--subtle, #6b7280)',
|
|
116
|
+
backgroundColor: 'transparent',
|
|
117
|
+
border: 'none',
|
|
118
|
+
borderRadius: '0.25rem',
|
|
98
119
|
cursor: 'pointer',
|
|
120
|
+
letterSpacing: '0.01em',
|
|
99
121
|
}}
|
|
122
|
+
onMouseEnter={(e) => { e.target.style.color = 'var(--heading, #111827)' }}
|
|
123
|
+
onMouseLeave={(e) => { if (!isOpen) e.target.style.color = 'var(--subtle, #6b7280)' }}
|
|
100
124
|
>
|
|
125
|
+
{/* Quote icon */}
|
|
126
|
+
<svg
|
|
127
|
+
style={{ width: '0.875rem', height: '0.875rem', opacity: 0.7, flexShrink: 0 }}
|
|
128
|
+
fill="none"
|
|
129
|
+
stroke="currentColor"
|
|
130
|
+
viewBox="0 0 24 24"
|
|
131
|
+
>
|
|
132
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
|
|
133
|
+
</svg>
|
|
101
134
|
{label}
|
|
102
135
|
<svg
|
|
103
136
|
style={{
|
|
104
|
-
width: '
|
|
105
|
-
height: '
|
|
137
|
+
width: '0.625rem',
|
|
138
|
+
height: '0.625rem',
|
|
139
|
+
opacity: 0.5,
|
|
106
140
|
transform: isOpen ? 'rotate(180deg)' : 'rotate(0)',
|
|
107
141
|
transition: 'transform 0.15s',
|
|
108
142
|
}}
|
|
@@ -110,12 +144,7 @@ export function CiteButton({
|
|
|
110
144
|
stroke="currentColor"
|
|
111
145
|
viewBox="0 0 24 24"
|
|
112
146
|
>
|
|
113
|
-
<path
|
|
114
|
-
strokeLinecap="round"
|
|
115
|
-
strokeLinejoin="round"
|
|
116
|
-
strokeWidth={2}
|
|
117
|
-
d="M19 9l-7 7-7-7"
|
|
118
|
-
/>
|
|
147
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
119
148
|
</svg>
|
|
120
149
|
</button>
|
|
121
150
|
|
|
@@ -127,16 +156,17 @@ export function CiteButton({
|
|
|
127
156
|
top: '100%',
|
|
128
157
|
left: 0,
|
|
129
158
|
marginTop: '0.25rem',
|
|
130
|
-
minWidth: '
|
|
131
|
-
backgroundColor: '#ffffff',
|
|
132
|
-
border: '1px solid #e5e7eb',
|
|
133
|
-
borderRadius: '0.
|
|
134
|
-
boxShadow: '0 4px
|
|
159
|
+
minWidth: '8.5rem',
|
|
160
|
+
backgroundColor: 'var(--section, #ffffff)',
|
|
161
|
+
border: '1px solid var(--border, #e5e7eb)',
|
|
162
|
+
borderRadius: '0.5rem',
|
|
163
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
|
|
135
164
|
zIndex: 50,
|
|
136
165
|
overflow: 'hidden',
|
|
166
|
+
padding: '0.25rem',
|
|
137
167
|
}}
|
|
138
168
|
>
|
|
139
|
-
{
|
|
169
|
+
{styleIds.map((style) => (
|
|
140
170
|
<button
|
|
141
171
|
key={style}
|
|
142
172
|
type="button"
|
|
@@ -144,17 +174,18 @@ export function CiteButton({
|
|
|
144
174
|
style={{
|
|
145
175
|
display: 'block',
|
|
146
176
|
width: '100%',
|
|
147
|
-
padding: '0.
|
|
148
|
-
fontSize: '0.
|
|
177
|
+
padding: '0.375rem 0.625rem',
|
|
178
|
+
fontSize: '0.8125rem',
|
|
149
179
|
textAlign: 'left',
|
|
150
|
-
color: copied === style ? '#059669' : '#374151',
|
|
180
|
+
color: copied === style ? '#059669' : 'var(--body, #374151)',
|
|
151
181
|
backgroundColor: copied === style ? '#ecfdf5' : 'transparent',
|
|
152
182
|
border: 'none',
|
|
183
|
+
borderRadius: '0.25rem',
|
|
153
184
|
cursor: 'pointer',
|
|
154
185
|
}}
|
|
155
186
|
onMouseEnter={(e) => {
|
|
156
187
|
if (copied !== style) {
|
|
157
|
-
e.target.style.backgroundColor = '#f3f4f6'
|
|
188
|
+
e.target.style.backgroundColor = 'var(--card, #f3f4f6)'
|
|
158
189
|
}
|
|
159
190
|
}}
|
|
160
191
|
onMouseLeave={(e) => {
|
|
@@ -163,7 +194,9 @@ export function CiteButton({
|
|
|
163
194
|
}
|
|
164
195
|
}}
|
|
165
196
|
>
|
|
166
|
-
{copied === style
|
|
197
|
+
{copied === style
|
|
198
|
+
? '✓ Copied!'
|
|
199
|
+
: styleLabels[style] || style.toUpperCase()}
|
|
167
200
|
</button>
|
|
168
201
|
))}
|
|
169
202
|
</div>
|
|
@@ -50,6 +50,12 @@ export function DoiLink({
|
|
|
50
50
|
case 'link':
|
|
51
51
|
displayText = doiUrl
|
|
52
52
|
break
|
|
53
|
+
case 'label':
|
|
54
|
+
displayText = 'DOI'
|
|
55
|
+
break
|
|
56
|
+
case 'icon':
|
|
57
|
+
displayText = null
|
|
58
|
+
break
|
|
53
59
|
case 'full':
|
|
54
60
|
default:
|
|
55
61
|
displayText = `DOI: ${normalizedDoi}`
|
|
@@ -61,35 +67,35 @@ export function DoiLink({
|
|
|
61
67
|
target="_blank"
|
|
62
68
|
rel="noopener noreferrer"
|
|
63
69
|
className={className}
|
|
70
|
+
title={`DOI: ${normalizedDoi}`}
|
|
64
71
|
style={{
|
|
65
|
-
color: '#
|
|
72
|
+
color: 'var(--subtle, #6b7280)',
|
|
66
73
|
textDecoration: 'none',
|
|
67
74
|
display: 'inline-flex',
|
|
68
75
|
alignItems: 'center',
|
|
69
76
|
gap: '0.25rem',
|
|
77
|
+
fontSize: '0.8125rem',
|
|
78
|
+
transition: 'color 0.15s',
|
|
70
79
|
}}
|
|
80
|
+
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--primary, #2563eb)' }}
|
|
81
|
+
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--subtle, #6b7280)' }}
|
|
71
82
|
{...props}
|
|
72
83
|
>
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
)}
|
|
84
|
+
{/* DOI icon — external link */}
|
|
85
|
+
<svg
|
|
86
|
+
style={{ width: '0.875rem', height: '0.875rem', flexShrink: 0 }}
|
|
87
|
+
fill="none"
|
|
88
|
+
stroke="currentColor"
|
|
89
|
+
viewBox="0 0 24 24"
|
|
90
|
+
>
|
|
91
|
+
<path
|
|
92
|
+
strokeLinecap="round"
|
|
93
|
+
strokeLinejoin="round"
|
|
94
|
+
strokeWidth={1.5}
|
|
95
|
+
d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
|
96
|
+
/>
|
|
97
|
+
</svg>
|
|
98
|
+
{displayText && <span>{displayText}</span>}
|
|
93
99
|
</a>
|
|
94
100
|
)
|
|
95
101
|
}
|
package/src/index.js
CHANGED
|
@@ -4,21 +4,27 @@
|
|
|
4
4
|
* Provides math rendering, bibliographic references, and inline citations
|
|
5
5
|
* for academic and scientific content.
|
|
6
6
|
*
|
|
7
|
+
* Bibliography formatting is powered by @citestyle/* compiled styles,
|
|
8
|
+
* giving access to 6 citation styles with structured HTML output,
|
|
9
|
+
* year-suffix disambiguation, and cite collapsing.
|
|
10
|
+
*
|
|
7
11
|
* @module @uniweb/scholar
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
14
|
// Math components
|
|
11
15
|
export { Math, Equation, EquationRef, loadKatex } from './math/index.js'
|
|
12
16
|
|
|
13
|
-
// Bibliography
|
|
17
|
+
// Bibliography — formatting, parsing, normalization
|
|
14
18
|
export {
|
|
15
19
|
formatReference,
|
|
16
|
-
|
|
20
|
+
getAvailableStyles,
|
|
17
21
|
parseBibtex,
|
|
18
22
|
exportBibtex,
|
|
23
|
+
toCslJson,
|
|
24
|
+
fromCslJson,
|
|
19
25
|
} from './bibliography/index.js'
|
|
20
26
|
|
|
21
|
-
// Citation system
|
|
27
|
+
// Citation system — registry-powered
|
|
22
28
|
export {
|
|
23
29
|
CitationProvider,
|
|
24
30
|
Citation,
|
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* APA 7th Edition Reference Formatter
|
|
3
|
-
*
|
|
4
|
-
* Formats references according to APA 7th edition guidelines.
|
|
5
|
-
*
|
|
6
|
-
* @module @uniweb/scholar/bibliography/formatters/apa
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { formatApaAuthors, parseAuthorString } from '../utils/authors.js'
|
|
10
|
-
import { formatApaDate, getYear } from '../utils/dates.js'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Format a journal article in APA style
|
|
14
|
-
*
|
|
15
|
-
* @param {Object} pub - Publication data
|
|
16
|
-
* @returns {string}
|
|
17
|
-
*/
|
|
18
|
-
function formatArticle(pub) {
|
|
19
|
-
const parts = []
|
|
20
|
-
|
|
21
|
-
// Authors
|
|
22
|
-
const authors = Array.isArray(pub.authors)
|
|
23
|
-
? pub.authors
|
|
24
|
-
: parseAuthorString(pub.authors || pub.author)
|
|
25
|
-
if (authors.length) {
|
|
26
|
-
parts.push(formatApaAuthors(authors))
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Year
|
|
30
|
-
const year = getYear(pub.year || pub.date)
|
|
31
|
-
parts.push(`(${year || 'n.d.'}).`)
|
|
32
|
-
|
|
33
|
-
// Title
|
|
34
|
-
if (pub.title) {
|
|
35
|
-
parts.push(`${pub.title}.`)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Journal (italicized in actual rendering)
|
|
39
|
-
if (pub.journal) {
|
|
40
|
-
let journalPart = pub.journal
|
|
41
|
-
if (pub.volume) {
|
|
42
|
-
journalPart += `, ${pub.volume}`
|
|
43
|
-
if (pub.issue) {
|
|
44
|
-
journalPart += `(${pub.issue})`
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
if (pub.pages) {
|
|
48
|
-
journalPart += `, ${pub.pages}`
|
|
49
|
-
}
|
|
50
|
-
parts.push(`${journalPart}.`)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// DOI
|
|
54
|
-
if (pub.doi) {
|
|
55
|
-
const doi = pub.doi.startsWith('http')
|
|
56
|
-
? pub.doi
|
|
57
|
-
: `https://doi.org/${pub.doi}`
|
|
58
|
-
parts.push(doi)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return parts.join(' ')
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Format a book in APA style
|
|
66
|
-
*
|
|
67
|
-
* @param {Object} pub - Publication data
|
|
68
|
-
* @returns {string}
|
|
69
|
-
*/
|
|
70
|
-
function formatBook(pub) {
|
|
71
|
-
const parts = []
|
|
72
|
-
|
|
73
|
-
// Authors or editors
|
|
74
|
-
const authors = Array.isArray(pub.authors)
|
|
75
|
-
? pub.authors
|
|
76
|
-
: parseAuthorString(pub.authors || pub.author)
|
|
77
|
-
const editors = Array.isArray(pub.editors)
|
|
78
|
-
? pub.editors
|
|
79
|
-
: parseAuthorString(pub.editors || pub.editor)
|
|
80
|
-
|
|
81
|
-
if (authors.length) {
|
|
82
|
-
parts.push(formatApaAuthors(authors))
|
|
83
|
-
} else if (editors.length) {
|
|
84
|
-
parts.push(`${formatApaAuthors(editors)} (Ed${editors.length > 1 ? 's' : ''}).`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Year
|
|
88
|
-
const year = getYear(pub.year || pub.date)
|
|
89
|
-
parts.push(`(${year || 'n.d.'}).`)
|
|
90
|
-
|
|
91
|
-
// Title (italicized in actual rendering)
|
|
92
|
-
if (pub.title) {
|
|
93
|
-
let title = pub.title
|
|
94
|
-
if (pub.edition && pub.edition !== '1') {
|
|
95
|
-
title += ` (${pub.edition} ed.)`
|
|
96
|
-
}
|
|
97
|
-
parts.push(`${title}.`)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Publisher
|
|
101
|
-
if (pub.publisher) {
|
|
102
|
-
parts.push(`${pub.publisher}.`)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// DOI or URL
|
|
106
|
-
if (pub.doi) {
|
|
107
|
-
const doi = pub.doi.startsWith('http')
|
|
108
|
-
? pub.doi
|
|
109
|
-
: `https://doi.org/${pub.doi}`
|
|
110
|
-
parts.push(doi)
|
|
111
|
-
} else if (pub.url) {
|
|
112
|
-
parts.push(pub.url)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return parts.join(' ')
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Format a book chapter in APA style
|
|
120
|
-
*
|
|
121
|
-
* @param {Object} pub - Publication data
|
|
122
|
-
* @returns {string}
|
|
123
|
-
*/
|
|
124
|
-
function formatInBook(pub) {
|
|
125
|
-
const parts = []
|
|
126
|
-
|
|
127
|
-
// Chapter authors
|
|
128
|
-
const authors = Array.isArray(pub.authors)
|
|
129
|
-
? pub.authors
|
|
130
|
-
: parseAuthorString(pub.authors || pub.author)
|
|
131
|
-
if (authors.length) {
|
|
132
|
-
parts.push(formatApaAuthors(authors))
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Year
|
|
136
|
-
const year = getYear(pub.year || pub.date)
|
|
137
|
-
parts.push(`(${year || 'n.d.'}).`)
|
|
138
|
-
|
|
139
|
-
// Chapter title
|
|
140
|
-
if (pub.title) {
|
|
141
|
-
parts.push(`${pub.title}.`)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Book editors and title
|
|
145
|
-
const editors = Array.isArray(pub.editors)
|
|
146
|
-
? pub.editors
|
|
147
|
-
: parseAuthorString(pub.editors || pub.editor)
|
|
148
|
-
if (editors.length || pub.bookTitle) {
|
|
149
|
-
let bookPart = 'In'
|
|
150
|
-
if (editors.length) {
|
|
151
|
-
bookPart += ` ${formatApaAuthors(editors)} (Ed${editors.length > 1 ? 's' : ''}),`
|
|
152
|
-
}
|
|
153
|
-
if (pub.bookTitle) {
|
|
154
|
-
bookPart += ` ${pub.bookTitle}`
|
|
155
|
-
}
|
|
156
|
-
if (pub.pages) {
|
|
157
|
-
bookPart += ` (pp. ${pub.pages})`
|
|
158
|
-
}
|
|
159
|
-
parts.push(`${bookPart}.`)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Publisher
|
|
163
|
-
if (pub.publisher) {
|
|
164
|
-
parts.push(`${pub.publisher}.`)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// DOI
|
|
168
|
-
if (pub.doi) {
|
|
169
|
-
const doi = pub.doi.startsWith('http')
|
|
170
|
-
? pub.doi
|
|
171
|
-
: `https://doi.org/${pub.doi}`
|
|
172
|
-
parts.push(doi)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return parts.join(' ')
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Format conference proceedings in APA style
|
|
180
|
-
*
|
|
181
|
-
* @param {Object} pub - Publication data
|
|
182
|
-
* @returns {string}
|
|
183
|
-
*/
|
|
184
|
-
function formatConference(pub) {
|
|
185
|
-
const parts = []
|
|
186
|
-
|
|
187
|
-
// Authors
|
|
188
|
-
const authors = Array.isArray(pub.authors)
|
|
189
|
-
? pub.authors
|
|
190
|
-
: parseAuthorString(pub.authors || pub.author)
|
|
191
|
-
if (authors.length) {
|
|
192
|
-
parts.push(formatApaAuthors(authors))
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Year
|
|
196
|
-
const year = getYear(pub.year || pub.date)
|
|
197
|
-
parts.push(`(${year || 'n.d.'}).`)
|
|
198
|
-
|
|
199
|
-
// Title
|
|
200
|
-
if (pub.title) {
|
|
201
|
-
parts.push(`${pub.title}.`)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Conference/Proceedings
|
|
205
|
-
if (pub.booktitle || pub.conference) {
|
|
206
|
-
let confPart = `In ${pub.booktitle || pub.conference}`
|
|
207
|
-
if (pub.pages) {
|
|
208
|
-
confPart += ` (pp. ${pub.pages})`
|
|
209
|
-
}
|
|
210
|
-
parts.push(`${confPart}.`)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Publisher or location
|
|
214
|
-
if (pub.publisher) {
|
|
215
|
-
parts.push(`${pub.publisher}.`)
|
|
216
|
-
} else if (pub.address || pub.location) {
|
|
217
|
-
parts.push(`${pub.address || pub.location}.`)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// DOI
|
|
221
|
-
if (pub.doi) {
|
|
222
|
-
const doi = pub.doi.startsWith('http')
|
|
223
|
-
? pub.doi
|
|
224
|
-
: `https://doi.org/${pub.doi}`
|
|
225
|
-
parts.push(doi)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return parts.join(' ')
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Format a website/webpage in APA style
|
|
233
|
-
*
|
|
234
|
-
* @param {Object} pub - Publication data
|
|
235
|
-
* @returns {string}
|
|
236
|
-
*/
|
|
237
|
-
function formatWebsite(pub) {
|
|
238
|
-
const parts = []
|
|
239
|
-
|
|
240
|
-
// Authors or organization
|
|
241
|
-
const authors = Array.isArray(pub.authors)
|
|
242
|
-
? pub.authors
|
|
243
|
-
: parseAuthorString(pub.authors || pub.author)
|
|
244
|
-
if (authors.length) {
|
|
245
|
-
parts.push(formatApaAuthors(authors))
|
|
246
|
-
} else if (pub.organization) {
|
|
247
|
-
parts.push(pub.organization)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Date
|
|
251
|
-
const date = formatApaDate(pub.date || pub.year)
|
|
252
|
-
parts.push(`(${date}).`)
|
|
253
|
-
|
|
254
|
-
// Title
|
|
255
|
-
if (pub.title) {
|
|
256
|
-
parts.push(`${pub.title}.`)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Site name
|
|
260
|
-
if (pub.siteName && pub.siteName !== pub.organization) {
|
|
261
|
-
parts.push(`${pub.siteName}.`)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// URL
|
|
265
|
-
if (pub.url) {
|
|
266
|
-
parts.push(pub.url)
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return parts.join(' ')
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Format a thesis/dissertation in APA style
|
|
274
|
-
*
|
|
275
|
-
* @param {Object} pub - Publication data
|
|
276
|
-
* @returns {string}
|
|
277
|
-
*/
|
|
278
|
-
function formatThesis(pub) {
|
|
279
|
-
const parts = []
|
|
280
|
-
|
|
281
|
-
// Author
|
|
282
|
-
const authors = Array.isArray(pub.authors)
|
|
283
|
-
? pub.authors
|
|
284
|
-
: parseAuthorString(pub.authors || pub.author)
|
|
285
|
-
if (authors.length) {
|
|
286
|
-
parts.push(formatApaAuthors(authors))
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Year
|
|
290
|
-
const year = getYear(pub.year || pub.date)
|
|
291
|
-
parts.push(`(${year || 'n.d.'}).`)
|
|
292
|
-
|
|
293
|
-
// Title (italicized)
|
|
294
|
-
if (pub.title) {
|
|
295
|
-
parts.push(`${pub.title}`)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Thesis type and institution
|
|
299
|
-
const type = pub.type || 'Doctoral dissertation'
|
|
300
|
-
if (pub.school || pub.institution) {
|
|
301
|
-
parts.push(`[${type}, ${pub.school || pub.institution}].`)
|
|
302
|
-
} else {
|
|
303
|
-
parts.push(`[${type}].`)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// URL or database
|
|
307
|
-
if (pub.url) {
|
|
308
|
-
parts.push(pub.url)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return parts.join(' ')
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Format a reference in APA 7th edition style
|
|
316
|
-
*
|
|
317
|
-
* @param {Object} pub - Publication data
|
|
318
|
-
* @param {Object} [options] - Formatting options
|
|
319
|
-
* @returns {string}
|
|
320
|
-
*/
|
|
321
|
-
export function formatApa(pub, options = {}) {
|
|
322
|
-
const type = (pub.type || pub.entryType || 'article').toLowerCase()
|
|
323
|
-
|
|
324
|
-
switch (type) {
|
|
325
|
-
case 'article':
|
|
326
|
-
case 'journal':
|
|
327
|
-
return formatArticle(pub)
|
|
328
|
-
case 'book':
|
|
329
|
-
return formatBook(pub)
|
|
330
|
-
case 'inbook':
|
|
331
|
-
case 'incollection':
|
|
332
|
-
case 'chapter':
|
|
333
|
-
return formatInBook(pub)
|
|
334
|
-
case 'inproceedings':
|
|
335
|
-
case 'conference':
|
|
336
|
-
case 'proceedings':
|
|
337
|
-
return formatConference(pub)
|
|
338
|
-
case 'webpage':
|
|
339
|
-
case 'website':
|
|
340
|
-
case 'online':
|
|
341
|
-
return formatWebsite(pub)
|
|
342
|
-
case 'thesis':
|
|
343
|
-
case 'phdthesis':
|
|
344
|
-
case 'mastersthesis':
|
|
345
|
-
return formatThesis(pub)
|
|
346
|
-
default:
|
|
347
|
-
// Generic format
|
|
348
|
-
return formatArticle(pub)
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export default formatApa
|