@citestyle/core 0.0.2 → 1.0.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.
- package/README.md +85 -51
- package/package.json +17 -3
- package/src/index.js +1 -0
- package/src/validate.js +110 -0
- package/types.d.ts +2 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @citestyle/core
|
|
2
2
|
|
|
3
|
-
Shared runtime
|
|
3
|
+
Shared runtime library for the Citestyle ecosystem. Every compiled CSL style imports from this package — it provides the formatting functions that styles call at runtime (name formatting, date formatting, text-case transforms, number formatting, page ranges, and HTML output).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**You probably don't need to install this directly.** It's automatically pulled in as a dependency of compiled styles and `@citestyle/registry`. This documentation is for contributors, advanced use cases, and anyone curious about what's under the hood.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -10,110 +10,144 @@ Most consumers do not need to use this package directly -- compiled styles impor
|
|
|
10
10
|
npm install @citestyle/core
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
~6-8KB. Zero dependencies. Works in Node.js and browsers.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## What it does
|
|
16
|
+
|
|
17
|
+
When the Citestyle compiler transforms a `.csl` file into JavaScript, the generated code calls functions from this package instead of inlining the logic. This keeps each compiled style small (~3-5KB) while sharing the heavier formatting logic (~6-8KB) across all styles.
|
|
18
|
+
|
|
19
|
+
For example, a compiled APA style might generate:
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
// Generated code in apa.js — calls into @citestyle/core
|
|
23
|
+
import { formatNames, formatDate, applyTextCase, toHtml } from '@citestyle/core'
|
|
24
|
+
|
|
25
|
+
// The style bakes in the *configuration*, core provides the *implementation*
|
|
26
|
+
formatNames(item.author, { and: 'symbol', initializeWith: '. ', nameAsSortOrder: 'first' })
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
### Name formatting
|
|
16
32
|
|
|
17
33
|
#### `formatNames(names, config?)`
|
|
18
34
|
|
|
19
|
-
Format
|
|
35
|
+
Format CSL name objects according to style-specific rules. Handles et-al truncation, particles (von, de la), sort order, initialization, and all five CSL `disambiguate-add-givenname` rules.
|
|
20
36
|
|
|
21
|
-
```
|
|
37
|
+
```javascript
|
|
22
38
|
import { formatNames } from '@citestyle/core'
|
|
23
39
|
|
|
24
40
|
formatNames(
|
|
25
41
|
[{ family: 'Smith', given: 'John' }, { family: 'Doe', given: 'Jane' }],
|
|
26
42
|
{ and: 'text', delimiter: ', ', initializeWith: '. ' }
|
|
27
43
|
)
|
|
28
|
-
// "J. Smith and J. Doe"
|
|
44
|
+
// → "J. Smith and J. Doe"
|
|
45
|
+
|
|
46
|
+
formatNames(
|
|
47
|
+
[{ family: 'de la Cruz', given: 'Maria', 'non-dropping-particle': 'de la' }],
|
|
48
|
+
{ nameAsSortOrder: 'all', sortSeparator: ', ' }
|
|
49
|
+
)
|
|
50
|
+
// → "de la Cruz, Maria"
|
|
29
51
|
```
|
|
30
52
|
|
|
31
|
-
Config options
|
|
53
|
+
**Config options**: `and`, `delimiter`, `delimiterPrecedesLast`, `etAlMin`, `etAlUseFirst`, `etAlUseLast`, `initialize`, `initializeWith`, `nameAsSortOrder`, `sortSeparator`, `nameParts`, `form`.
|
|
32
54
|
|
|
33
|
-
### Date
|
|
55
|
+
### Date formatting
|
|
34
56
|
|
|
35
57
|
#### `formatDate(date, config?)`
|
|
36
58
|
|
|
37
|
-
Format
|
|
59
|
+
Format CSL date objects with configurable parts, localized month terms, ranges, and seasons.
|
|
38
60
|
|
|
39
|
-
```
|
|
61
|
+
```javascript
|
|
40
62
|
import { formatDate } from '@citestyle/core'
|
|
41
63
|
|
|
42
64
|
formatDate(
|
|
43
65
|
{ 'date-parts': [[2024, 3, 15]] },
|
|
44
|
-
{ dateParts: [{ name: 'year' }, { name: 'month', prefix: ', ' }, { name: 'day', prefix: ' ' }] }
|
|
66
|
+
{ dateParts: [{ name: 'year' }, { name: 'month', form: 'long', prefix: ', ' }, { name: 'day', prefix: ' ' }] }
|
|
45
67
|
)
|
|
46
|
-
// "2024, March 15"
|
|
68
|
+
// → "2024, March 15"
|
|
47
69
|
```
|
|
48
70
|
|
|
49
|
-
### Text-
|
|
71
|
+
### Text-case transforms
|
|
50
72
|
|
|
51
|
-
All transforms respect `<span class="nocase">` protection in CSL-JSON values.
|
|
73
|
+
All transforms respect `<span class="nocase">` protection in CSL-JSON values — brand names like "iPhone" and chemical terms like "pH" are preserved.
|
|
52
74
|
|
|
53
75
|
#### `applyTextCase(str, textCase)`
|
|
54
76
|
|
|
55
|
-
Unified entry point for all CSL text-case values
|
|
77
|
+
Unified entry point for all CSL text-case values.
|
|
56
78
|
|
|
57
|
-
```
|
|
79
|
+
```javascript
|
|
58
80
|
import { applyTextCase } from '@citestyle/core'
|
|
59
81
|
|
|
60
|
-
applyTextCase('the art of war', 'title')
|
|
61
|
-
applyTextCase('A STUDY OF DNA', 'sentence')
|
|
82
|
+
applyTextCase('the art of war', 'title') // "The Art of War"
|
|
83
|
+
applyTextCase('A STUDY OF DNA', 'sentence') // "A study of DNA"
|
|
84
|
+
applyTextCase('proceedings of the IEEE', 'title') // "Proceedings of the IEEE"
|
|
85
|
+
applyTextCase('<span class="nocase">iPhone</span> sales', 'uppercase')
|
|
86
|
+
// "IPHONE SALES" → no: "iPhone SALES"
|
|
62
87
|
```
|
|
63
88
|
|
|
64
|
-
|
|
89
|
+
**Supported values**: `'lowercase'`, `'uppercase'`, `'capitalize-first'`, `'capitalize-all'`, `'title'`, `'sentence'`.
|
|
65
90
|
|
|
66
|
-
|
|
91
|
+
#### Individual transforms
|
|
67
92
|
|
|
68
|
-
|
|
93
|
+
- **`titleCase(str)`** — Title case with stop-word handling. Preserves acronyms and nocase spans.
|
|
94
|
+
- **`sentenceCase(str)`** — Lowercase all but first word. Preserves all-caps acronyms (DNA, USA) in mixed-case input.
|
|
95
|
+
- **`capitalize(str)`** — Capitalize the first character.
|
|
96
|
+
- **`stripNocaseSpans(str)`** — Remove `<span class="nocase">` tags without changing case. Used when a value passes through without any text-case transform.
|
|
69
97
|
|
|
70
|
-
|
|
98
|
+
### Number formatting
|
|
71
99
|
|
|
72
|
-
|
|
100
|
+
```javascript
|
|
101
|
+
import { ordinal, longOrdinal, roman } from '@citestyle/core'
|
|
73
102
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
103
|
+
ordinal(3) // "3rd"
|
|
104
|
+
ordinal(1, 'fr') // "1re" (locale-aware)
|
|
105
|
+
longOrdinal(2) // "second"
|
|
106
|
+
roman(14) // "xiv"
|
|
107
|
+
```
|
|
79
108
|
|
|
80
|
-
###
|
|
109
|
+
### Page ranges
|
|
81
110
|
|
|
82
|
-
#### `
|
|
111
|
+
#### `pageRange(pages, format?)`
|
|
83
112
|
|
|
84
|
-
Format
|
|
113
|
+
Format page ranges per CSL `page-range-format` rules.
|
|
85
114
|
|
|
86
|
-
|
|
115
|
+
```javascript
|
|
116
|
+
import { pageRange } from '@citestyle/core'
|
|
87
117
|
|
|
88
|
-
|
|
118
|
+
pageRange('321-328', 'chicago') // "321–28" (en-dash, Chicago-style abbreviation)
|
|
119
|
+
pageRange('321-328', 'minimal') // "321–8" (en-dash, minimal abbreviation)
|
|
120
|
+
pageRange('321-328', 'expanded') // "321–328" (en-dash, no abbreviation)
|
|
121
|
+
```
|
|
89
122
|
|
|
90
|
-
|
|
123
|
+
### HTML output
|
|
91
124
|
|
|
92
|
-
|
|
125
|
+
#### `toHtml(str)`
|
|
93
126
|
|
|
94
|
-
|
|
127
|
+
Convert internal formatting tokens to semantic HTML. Also auto-links DOIs and URLs. This is the final output step — call it on the raw output from a compiled style function.
|
|
95
128
|
|
|
96
|
-
#### `
|
|
129
|
+
#### `stripFormatting(str)`
|
|
97
130
|
|
|
98
|
-
|
|
131
|
+
Remove all formatting tokens for clean plain text output.
|
|
99
132
|
|
|
100
|
-
|
|
101
|
-
import { pageRange } from '@citestyle/core'
|
|
133
|
+
#### `escapeHtml(str)`
|
|
102
134
|
|
|
103
|
-
|
|
104
|
-
pageRange('321-328', 'minimal') // "321\u20138" (321-8 with en-dash)
|
|
105
|
-
```
|
|
135
|
+
Escape `&`, `<`, `>`, `"`, `'` for safe HTML interpolation. Used internally by compiled styles for all variable output.
|
|
106
136
|
|
|
107
|
-
###
|
|
137
|
+
### Validation
|
|
108
138
|
|
|
109
|
-
#### `
|
|
139
|
+
#### `validateItem(item)`
|
|
110
140
|
|
|
111
|
-
|
|
141
|
+
Validate a CSL-JSON item and return warnings for common mistakes.
|
|
112
142
|
|
|
113
|
-
|
|
143
|
+
```javascript
|
|
144
|
+
import { validateItem } from '@citestyle/core'
|
|
114
145
|
|
|
115
|
-
|
|
146
|
+
validateItem({ id: '1', type: 'article-journal', title: 'My Paper', doi: '10.1234/x' })
|
|
147
|
+
// { valid: true, warnings: ['Use uppercase "DOI" instead of "doi"'] }
|
|
116
148
|
|
|
117
|
-
|
|
149
|
+
validateItem({ title: 'Missing required fields' })
|
|
150
|
+
// { valid: false, warnings: ['Missing required field: id', 'Missing required field: type'] }
|
|
151
|
+
```
|
|
118
152
|
|
|
119
|
-
|
|
153
|
+
Checks for: missing `id`/`type`, invalid type values, malformed name fields (should be arrays of objects with `family` or `literal`), malformed date fields, and common field-name mistakes (`doi` → `DOI`, `url` → `URL`, `isbn` → `ISBN`).
|
package/package.json
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@citestyle/core",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Shared runtime helpers for compiled CSL styles (~6-8KB)",
|
|
5
|
+
"author": "Uniweb",
|
|
6
|
+
"homepage": "https://github.com/uniweb/csl/tree/main/packages/core",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/uniweb/csl.git",
|
|
10
|
+
"directory": "packages/core"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"csl",
|
|
14
|
+
"citation",
|
|
15
|
+
"formatting",
|
|
16
|
+
"names",
|
|
17
|
+
"dates",
|
|
18
|
+
"text-case"
|
|
19
|
+
],
|
|
5
20
|
"type": "module",
|
|
6
21
|
"exports": {
|
|
7
22
|
".": {
|
|
@@ -23,7 +38,6 @@
|
|
|
23
38
|
},
|
|
24
39
|
"license": "MIT",
|
|
25
40
|
"scripts": {
|
|
26
|
-
"test": "vitest run"
|
|
27
|
-
"lint": "echo 'TODO: configure linter'"
|
|
41
|
+
"test": "vitest run"
|
|
28
42
|
}
|
|
29
43
|
}
|
package/src/index.js
CHANGED
|
@@ -12,3 +12,4 @@ export { titleCase, sentenceCase, capitalize, applyTextCase, stripNocaseSpans }
|
|
|
12
12
|
export { ordinal, longOrdinal, roman } from './numbers.js'
|
|
13
13
|
export { pageRange } from './pages.js'
|
|
14
14
|
export { escapeHtml, stripFormatting, toHtml } from './html.js'
|
|
15
|
+
export { validateItem } from './validate.js'
|
package/src/validate.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSL-JSON item validation.
|
|
3
|
+
*
|
|
4
|
+
* Checks that a CSL-JSON item has required fields and correct shapes.
|
|
5
|
+
* Returns an array of warning strings (empty = valid).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const VALID_TYPES = new Set([
|
|
9
|
+
'article', 'article-journal', 'article-magazine', 'article-newspaper',
|
|
10
|
+
'bill', 'book', 'broadcast', 'chapter', 'dataset', 'entry',
|
|
11
|
+
'entry-dictionary', 'entry-encyclopedia', 'figure', 'graphic',
|
|
12
|
+
'interview', 'legal_case', 'legislation', 'manuscript', 'map',
|
|
13
|
+
'motion_picture', 'musical_score', 'pamphlet', 'paper-conference',
|
|
14
|
+
'patent', 'personal_communication', 'post', 'post-weblog',
|
|
15
|
+
'regulation', 'report', 'review', 'review-book', 'software',
|
|
16
|
+
'song', 'speech', 'standard', 'thesis', 'treaty', 'webpage',
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
const NAME_FIELDS = [
|
|
20
|
+
'author', 'editor', 'translator', 'container-author', 'collection-editor',
|
|
21
|
+
'composer', 'director', 'interviewer', 'reviewed-author', 'recipient',
|
|
22
|
+
'illustrator', 'original-author',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const DATE_FIELDS = [
|
|
26
|
+
'issued', 'accessed', 'original-date', 'submitted', 'event-date',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validate a CSL-JSON item.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} item - CSL-JSON item to validate
|
|
33
|
+
* @returns {{ valid: boolean, warnings: string[] }}
|
|
34
|
+
*/
|
|
35
|
+
export function validateItem(item) {
|
|
36
|
+
const warnings = []
|
|
37
|
+
|
|
38
|
+
if (!item || typeof item !== 'object') {
|
|
39
|
+
return { valid: false, warnings: ['Item must be an object'] }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Required: id
|
|
43
|
+
if (item.id == null || item.id === '') {
|
|
44
|
+
warnings.push('Missing required field "id"')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Required: type
|
|
48
|
+
if (!item.type) {
|
|
49
|
+
warnings.push('Missing required field "type"')
|
|
50
|
+
} else if (!VALID_TYPES.has(item.type)) {
|
|
51
|
+
warnings.push(`Unknown type "${item.type}"`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate name fields are arrays of objects
|
|
55
|
+
for (const field of NAME_FIELDS) {
|
|
56
|
+
if (item[field] != null) {
|
|
57
|
+
if (!Array.isArray(item[field])) {
|
|
58
|
+
warnings.push(`"${field}" must be an array of name objects`)
|
|
59
|
+
} else {
|
|
60
|
+
for (let i = 0; i < item[field].length; i++) {
|
|
61
|
+
const name = item[field][i]
|
|
62
|
+
if (!name || typeof name !== 'object') {
|
|
63
|
+
warnings.push(`${field}[${i}] must be a name object`)
|
|
64
|
+
} else if (!name.family && !name.literal) {
|
|
65
|
+
warnings.push(`${field}[${i}] must have "family" or "literal"`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Validate date fields
|
|
73
|
+
for (const field of DATE_FIELDS) {
|
|
74
|
+
if (item[field] != null) {
|
|
75
|
+
const date = item[field]
|
|
76
|
+
if (typeof date !== 'object' || Array.isArray(date)) {
|
|
77
|
+
warnings.push(`"${field}" must be a date object`)
|
|
78
|
+
} else if (!date['date-parts'] && !date.literal && !date.raw) {
|
|
79
|
+
warnings.push(`"${field}" must have "date-parts", "literal", or "raw"`)
|
|
80
|
+
} else if (date['date-parts']) {
|
|
81
|
+
if (!Array.isArray(date['date-parts'])) {
|
|
82
|
+
warnings.push(`${field}.date-parts must be an array`)
|
|
83
|
+
} else {
|
|
84
|
+
for (let i = 0; i < date['date-parts'].length; i++) {
|
|
85
|
+
const dp = date['date-parts'][i]
|
|
86
|
+
if (!Array.isArray(dp) || dp.length === 0) {
|
|
87
|
+
warnings.push(`${field}.date-parts[${i}] must be a non-empty array [year, month?, day?]`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Helpful warnings for common mistakes
|
|
96
|
+
if (item.doi && !item.DOI) {
|
|
97
|
+
warnings.push('"doi" should be "DOI" (uppercase)')
|
|
98
|
+
}
|
|
99
|
+
if (item.url && !item.URL) {
|
|
100
|
+
warnings.push('"url" should be "URL" (uppercase)')
|
|
101
|
+
}
|
|
102
|
+
if (item.isbn && !item.ISBN) {
|
|
103
|
+
warnings.push('"isbn" should be "ISBN" (uppercase)')
|
|
104
|
+
}
|
|
105
|
+
if (item.issn && !item.ISSN) {
|
|
106
|
+
warnings.push('"issn" should be "ISSN" (uppercase)')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { valid: warnings.length === 0, warnings }
|
|
110
|
+
}
|