@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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # @citestyle/core
2
2
 
3
- Shared runtime helpers imported by all compiled CSL styles. Keeps individual style modules small (~3-5KB each) by centralizing formatting logic here (~6-8KB shared).
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
- Most consumers do not need to use this package directly -- compiled styles import from it automatically. This reference is for contributors and advanced use cases.
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
- ## API Reference
13
+ ~6-8KB. Zero dependencies. Works in Node.js and browsers.
14
14
 
15
- ### Name Formatting
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 a list of CSL name objects according to CSL name options.
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
- ```js
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: `and`, `delimiter`, `delimiterPrecedesLast`, `etAlMin`, `etAlUseFirst`, `etAlUseLast`, `initialize`, `initializeWith`, `nameAsSortOrder`, `sortSeparator`, `nameParts`, `form`.
53
+ **Config options**: `and`, `delimiter`, `delimiterPrecedesLast`, `etAlMin`, `etAlUseFirst`, `etAlUseLast`, `initialize`, `initializeWith`, `nameAsSortOrder`, `sortSeparator`, `nameParts`, `form`.
32
54
 
33
- ### Date Formatting
55
+ ### Date formatting
34
56
 
35
57
  #### `formatDate(date, config?)`
36
58
 
37
- Format a CSL date object with configurable parts, month terms, and range support.
59
+ Format CSL date objects with configurable parts, localized month terms, ranges, and seasons.
38
60
 
39
- ```js
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-Case Transforms
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: `'lowercase'`, `'uppercase'`, `'capitalize-first'`, `'capitalize-all'`, `'title'`, `'sentence'`.
77
+ Unified entry point for all CSL text-case values.
56
78
 
57
- ```js
79
+ ```javascript
58
80
  import { applyTextCase } from '@citestyle/core'
59
81
 
60
- applyTextCase('the art of war', 'title') // "The Art of War"
61
- applyTextCase('A STUDY OF DNA', 'sentence') // "A study of DNA"
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
- #### `titleCase(str)`
89
+ **Supported values**: `'lowercase'`, `'uppercase'`, `'capitalize-first'`, `'capitalize-all'`, `'title'`, `'sentence'`.
65
90
 
66
- Title-case with stop word handling. Preserves acronyms and intentional casing.
91
+ #### Individual transforms
67
92
 
68
- #### `sentenceCase(str)`
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
- Capitalize first word only. Preserves all-caps acronyms (DNA, USA) in mixed-case input.
98
+ ### Number formatting
71
99
 
72
- #### `capitalize(str)`
100
+ ```javascript
101
+ import { ordinal, longOrdinal, roman } from '@citestyle/core'
73
102
 
74
- Capitalize the first character of the string.
75
-
76
- #### `stripNocaseSpans(str)`
77
-
78
- Remove `<span class="nocase">` tags without applying any case transform.
103
+ ordinal(3) // "3rd"
104
+ ordinal(1, 'fr') // "1re" (locale-aware)
105
+ longOrdinal(2) // "second"
106
+ roman(14) // "xiv"
107
+ ```
79
108
 
80
- ### Number Formatting
109
+ ### Page ranges
81
110
 
82
- #### `ordinal(n, locale?)`
111
+ #### `pageRange(pages, format?)`
83
112
 
84
- Format as ordinal: `ordinal(3)` returns `"3rd"`. Supports locale-specific suffixes.
113
+ Format page ranges per CSL `page-range-format` rules.
85
114
 
86
- #### `longOrdinal(n, locale?)`
115
+ ```javascript
116
+ import { pageRange } from '@citestyle/core'
87
117
 
88
- Format as long ordinal: `longOrdinal(2)` returns `"second"`. Falls back to numeric ordinal for n > 10.
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
- #### `roman(n)`
123
+ ### HTML output
91
124
 
92
- Format as lowercase Roman numeral: `roman(14)` returns `"xiv"`.
125
+ #### `toHtml(str)`
93
126
 
94
- ### Page Ranges
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
- #### `pageRange(pages, format?)`
129
+ #### `stripFormatting(str)`
97
130
 
98
- Format page ranges per CSL `page-range-format`: `'expanded'`, `'chicago'`, `'minimal'`, `'minimal-two'`.
131
+ Remove all formatting tokens for clean plain text output.
99
132
 
100
- ```js
101
- import { pageRange } from '@citestyle/core'
133
+ #### `escapeHtml(str)`
102
134
 
103
- pageRange('321-328', 'chicago') // "321\u201328" (321-28 with en-dash)
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
- ### HTML Output
137
+ ### Validation
108
138
 
109
- #### `escapeHtml(str)`
139
+ #### `validateItem(item)`
110
140
 
111
- Escape `&`, `<`, `>`, `"`, `'` for safe HTML output.
141
+ Validate a CSL-JSON item and return warnings for common mistakes.
112
142
 
113
- #### `toHtml(str)`
143
+ ```javascript
144
+ import { validateItem } from '@citestyle/core'
114
145
 
115
- Convert PUA formatting tokens to HTML tags. Also auto-links DOIs and URLs. This is the final output step for HTML rendering.
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
- #### `stripFormatting(str)`
149
+ validateItem({ title: 'Missing required fields' })
150
+ // { valid: false, warnings: ['Missing required field: id', 'Missing required field: type'] }
151
+ ```
118
152
 
119
- Remove all PUA formatting tokens, returning clean plain text.
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.2",
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'
@@ -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
+ }
package/types.d.ts CHANGED
@@ -13,6 +13,8 @@ export {
13
13
  escapeHtml,
14
14
  stripFormatting,
15
15
  toHtml,
16
+ validateItem,
17
+ ValidationResult,
16
18
  NameFormatConfig,
17
19
  DateFormatConfig,
18
20
  } from '@citestyle/types'