@citestyle/ris 0.1.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/LICENSE +21 -0
- package/README.md +101 -0
- package/package.json +31 -0
- package/src/export.js +127 -0
- package/src/index.js +8 -0
- package/src/parse.js +213 -0
- package/types.d.ts +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Proximify
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# @citestyle/ris
|
|
2
|
+
|
|
3
|
+
RIS parser and serializer. Converts between RIS (Research Information Systems) tagged format and CSL-JSON, the standard input format for Citestyle.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @citestyle/ris
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Parse RIS to CSL-JSON
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { parseRis } from '@citestyle/ris'
|
|
17
|
+
|
|
18
|
+
const ris = `
|
|
19
|
+
TY - JOUR
|
|
20
|
+
AU - Smith, John
|
|
21
|
+
AU - Doe, Jane
|
|
22
|
+
TI - A Study of Climate Change
|
|
23
|
+
JF - Nature Climate Change
|
|
24
|
+
PY - 2024
|
|
25
|
+
VL - 14
|
|
26
|
+
SP - 100
|
|
27
|
+
EP - 108
|
|
28
|
+
DO - 10.1038/example
|
|
29
|
+
ER -
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
const items = parseRis(ris)
|
|
33
|
+
console.log(items[0].title) // "A Study of Climate Change"
|
|
34
|
+
console.log(items[0].author.length) // 2
|
|
35
|
+
console.log(items[0].page) // "100-108"
|
|
36
|
+
console.log(items[0].DOI) // "10.1038/example"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Export CSL-JSON to RIS
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
import { exportRis } from '@citestyle/ris'
|
|
43
|
+
|
|
44
|
+
const items = [{
|
|
45
|
+
id: '1',
|
|
46
|
+
type: 'article-journal',
|
|
47
|
+
title: 'A Study of Climate Change',
|
|
48
|
+
author: [
|
|
49
|
+
{ family: 'Smith', given: 'John' },
|
|
50
|
+
{ family: 'Doe', given: 'Jane' },
|
|
51
|
+
],
|
|
52
|
+
issued: { 'date-parts': [[2024]] },
|
|
53
|
+
'container-title': 'Nature Climate Change',
|
|
54
|
+
volume: '14',
|
|
55
|
+
page: '100-108',
|
|
56
|
+
DOI: '10.1038/example',
|
|
57
|
+
}]
|
|
58
|
+
|
|
59
|
+
const output = exportRis(items)
|
|
60
|
+
// TY - JOUR
|
|
61
|
+
// AU - Smith, John
|
|
62
|
+
// AU - Doe, Jane
|
|
63
|
+
// TI - A Study of Climate Change
|
|
64
|
+
// ...
|
|
65
|
+
// ER -
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Pipeline: RIS to formatted bibliography
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import { parseRis } from '@citestyle/ris'
|
|
72
|
+
import { createRegistry } from '@citestyle/registry'
|
|
73
|
+
import * as apa from '@citestyle/styles/apa'
|
|
74
|
+
|
|
75
|
+
const ris = readFileSync('export.ris', 'utf-8')
|
|
76
|
+
const items = parseRis(ris)
|
|
77
|
+
|
|
78
|
+
const registry = createRegistry(apa)
|
|
79
|
+
registry.addItems(items)
|
|
80
|
+
|
|
81
|
+
const bibliography = registry.getBibliography()
|
|
82
|
+
bibliography.forEach(entry => console.log(entry.html))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API
|
|
86
|
+
|
|
87
|
+
### `parseRis(str)`
|
|
88
|
+
|
|
89
|
+
Parse a RIS string into an array of CSL-JSON items.
|
|
90
|
+
|
|
91
|
+
**Features:**
|
|
92
|
+
- 30+ RIS type codes (JOUR, BOOK, CHAP, THES, CONF, RPRT, etc.)
|
|
93
|
+
- Repeatable tags (AU for multiple authors, KW for keywords)
|
|
94
|
+
- SP + EP page merging into a single page range
|
|
95
|
+
- Date parsing from PY and DA tags
|
|
96
|
+
- Editor (A2/ED), translator, and other contributor roles
|
|
97
|
+
- Common in exports from PubMed, Scopus, Web of Science, and Zotero
|
|
98
|
+
|
|
99
|
+
### `exportRis(items)`
|
|
100
|
+
|
|
101
|
+
Serialize an array of CSL-JSON items to a RIS string. Each item is delimited by `TY` (type) and `ER` (end of record) tags.
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@citestyle/ris",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "RIS ↔ CSL-JSON parser and serializer",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./types.d.ts",
|
|
9
|
+
"import": "./src/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"types.d.ts",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@citestyle/types": "0.0.2"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"vitest": "^3.1.1"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20.19.0"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "vitest run --passWithNoTests",
|
|
29
|
+
"lint": "echo 'TODO: configure linter'"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/export.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize CSL-JSON items as a RIS string.
|
|
3
|
+
*
|
|
4
|
+
* @param {object[]} items - CSL-JSON items
|
|
5
|
+
* @returns {string} RIS source
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── CSL type → RIS type mapping ────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const REVERSE_TYPE_MAP = {
|
|
11
|
+
'article-journal': 'JOUR',
|
|
12
|
+
'article-magazine': 'MGZN',
|
|
13
|
+
'article-newspaper': 'NEWS',
|
|
14
|
+
'article': 'GEN',
|
|
15
|
+
'book': 'BOOK',
|
|
16
|
+
'chapter': 'CHAP',
|
|
17
|
+
'dataset': 'DATA',
|
|
18
|
+
'graphic': 'ART',
|
|
19
|
+
'legal_case': 'CASE',
|
|
20
|
+
'legislation': 'STAT',
|
|
21
|
+
'manuscript': 'UNPB',
|
|
22
|
+
'map': 'MAP',
|
|
23
|
+
'motion_picture': 'MPCT',
|
|
24
|
+
'pamphlet': 'PAMP',
|
|
25
|
+
'paper-conference': 'CONF',
|
|
26
|
+
'patent': 'PAT',
|
|
27
|
+
'personal_communication': 'PCOMM',
|
|
28
|
+
'report': 'RPRT',
|
|
29
|
+
'software': 'COMP',
|
|
30
|
+
'song': 'MUSIC',
|
|
31
|
+
'thesis': 'THES',
|
|
32
|
+
'webpage': 'ELEC',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Name serialization ──────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
function serializeName(name) {
|
|
38
|
+
if (name.literal) return name.literal
|
|
39
|
+
const parts = []
|
|
40
|
+
const particle = name['non-dropping-particle'] || name['dropping-particle'] || ''
|
|
41
|
+
const family = (particle ? particle + ' ' : '') + (name.family || '')
|
|
42
|
+
parts.push(family)
|
|
43
|
+
if (name.given) parts.push(name.given)
|
|
44
|
+
if (name.suffix) parts.push(name.suffix)
|
|
45
|
+
return parts.join(', ')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export function exportRis(items) {
|
|
51
|
+
if (!items || !items.length) return ''
|
|
52
|
+
|
|
53
|
+
return items.map(item => {
|
|
54
|
+
const lines = []
|
|
55
|
+
const tag = (t, v) => { if (v != null && v !== '') lines.push(`${t} - ${v}`) }
|
|
56
|
+
|
|
57
|
+
// Type
|
|
58
|
+
tag('TY', REVERSE_TYPE_MAP[item.type] || 'GEN')
|
|
59
|
+
|
|
60
|
+
// ID
|
|
61
|
+
tag('ID', item.id)
|
|
62
|
+
|
|
63
|
+
// Authors
|
|
64
|
+
if (item.author) item.author.forEach(a => tag('AU', serializeName(a)))
|
|
65
|
+
|
|
66
|
+
// Editors
|
|
67
|
+
if (item.editor) item.editor.forEach(e => tag('A2', serializeName(e)))
|
|
68
|
+
|
|
69
|
+
// Title
|
|
70
|
+
tag('TI', item.title)
|
|
71
|
+
|
|
72
|
+
// Container title
|
|
73
|
+
if (item['container-title']) {
|
|
74
|
+
const ct = item.type === 'article-journal' ? 'JO' : 'T2'
|
|
75
|
+
tag(ct, item['container-title'])
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Abstract
|
|
79
|
+
tag('AB', item.abstract)
|
|
80
|
+
|
|
81
|
+
// Date
|
|
82
|
+
const dateParts = item.issued?.['date-parts']?.[0]
|
|
83
|
+
if (dateParts) {
|
|
84
|
+
const parts = [String(dateParts[0])]
|
|
85
|
+
if (dateParts[1]) parts.push(String(dateParts[1]).padStart(2, '0'))
|
|
86
|
+
if (dateParts[2]) parts.push(String(dateParts[2]).padStart(2, '0'))
|
|
87
|
+
tag('PY', parts.join('/'))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Volume, issue
|
|
91
|
+
tag('VL', item.volume)
|
|
92
|
+
tag('IS', item.issue)
|
|
93
|
+
|
|
94
|
+
// Pages
|
|
95
|
+
if (item.page) {
|
|
96
|
+
const pageParts = item.page.split(/[–-]/)
|
|
97
|
+
tag('SP', pageParts[0].trim())
|
|
98
|
+
if (pageParts[1]) tag('EP', pageParts[1].trim())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Identifiers and links
|
|
102
|
+
tag('DO', item.DOI)
|
|
103
|
+
tag('UR', item.URL)
|
|
104
|
+
tag('SN', item.ISSN || item.ISBN)
|
|
105
|
+
|
|
106
|
+
// Publisher
|
|
107
|
+
tag('PB', item.publisher)
|
|
108
|
+
tag('CY', item['publisher-place'])
|
|
109
|
+
|
|
110
|
+
// Other
|
|
111
|
+
tag('ET', item.edition)
|
|
112
|
+
tag('LA', item.language)
|
|
113
|
+
if (item['collection-title']) tag('T3', item['collection-title'])
|
|
114
|
+
|
|
115
|
+
// Keywords
|
|
116
|
+
if (item.keyword) {
|
|
117
|
+
item.keyword.split(/,\s*/).forEach(kw => tag('KW', kw.trim()))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Notes
|
|
121
|
+
tag('N1', item.note)
|
|
122
|
+
|
|
123
|
+
// End record
|
|
124
|
+
lines.push('ER - ')
|
|
125
|
+
return lines.join('\n')
|
|
126
|
+
}).join('\n') + '\n'
|
|
127
|
+
}
|
package/src/index.js
ADDED
package/src/parse.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a RIS string into an array of CSL-JSON items.
|
|
3
|
+
*
|
|
4
|
+
* RIS is a tagged format with TY (type) / ER (end) delimiters.
|
|
5
|
+
* Each tag is 2 uppercase letters followed by " - " and a value.
|
|
6
|
+
* Common in database exports (PubMed, Scopus, Web of Science).
|
|
7
|
+
*
|
|
8
|
+
* @param {string} ris - RIS source
|
|
9
|
+
* @returns {object[]} Array of CSL-JSON items
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ── RIS type → CSL type mapping ────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const TYPE_MAP = {
|
|
15
|
+
ABST: 'article',
|
|
16
|
+
ADVS: 'motion_picture',
|
|
17
|
+
ART: 'graphic',
|
|
18
|
+
BILL: 'bill',
|
|
19
|
+
BOOK: 'book',
|
|
20
|
+
CASE: 'legal_case',
|
|
21
|
+
CHAP: 'chapter',
|
|
22
|
+
COMP: 'software',
|
|
23
|
+
CONF: 'paper-conference',
|
|
24
|
+
CTLG: 'book',
|
|
25
|
+
DATA: 'dataset',
|
|
26
|
+
EDBOOK: 'book',
|
|
27
|
+
EJOUR: 'article-journal',
|
|
28
|
+
ELEC: 'webpage',
|
|
29
|
+
GEN: 'article',
|
|
30
|
+
HEAR: 'legal_case',
|
|
31
|
+
ICOMM: 'personal_communication',
|
|
32
|
+
INPR: 'article-journal',
|
|
33
|
+
JOUR: 'article-journal',
|
|
34
|
+
JFULL: 'article-journal',
|
|
35
|
+
MAP: 'map',
|
|
36
|
+
MGZN: 'article-magazine',
|
|
37
|
+
MPCT: 'motion_picture',
|
|
38
|
+
MUSIC: 'song',
|
|
39
|
+
NEWS: 'article-newspaper',
|
|
40
|
+
PAMP: 'pamphlet',
|
|
41
|
+
PAT: 'patent',
|
|
42
|
+
PCOMM: 'personal_communication',
|
|
43
|
+
RPRT: 'report',
|
|
44
|
+
SER: 'article',
|
|
45
|
+
SLIDE: 'graphic',
|
|
46
|
+
SOUND: 'song',
|
|
47
|
+
STAND: 'legislation',
|
|
48
|
+
STAT: 'legislation',
|
|
49
|
+
THES: 'thesis',
|
|
50
|
+
UNPB: 'manuscript',
|
|
51
|
+
VIDEO: 'motion_picture',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Name parsing ────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function parseName(str) {
|
|
57
|
+
if (!str) return null
|
|
58
|
+
str = str.trim()
|
|
59
|
+
|
|
60
|
+
// "Last, First Suffix" or "Last, First, Suffix"
|
|
61
|
+
const parts = str.split(',').map(s => s.trim())
|
|
62
|
+
if (parts.length >= 2) {
|
|
63
|
+
const result = { family: parts[0] }
|
|
64
|
+
// Check if third part is a suffix
|
|
65
|
+
if (parts.length >= 3) {
|
|
66
|
+
result.given = parts[1]
|
|
67
|
+
result.suffix = parts[2]
|
|
68
|
+
} else {
|
|
69
|
+
result.given = parts[1]
|
|
70
|
+
}
|
|
71
|
+
return result
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Single name (corporate author)
|
|
75
|
+
return { literal: str }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Date parsing ────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function parseDate(str) {
|
|
81
|
+
if (!str) return null
|
|
82
|
+
str = str.trim()
|
|
83
|
+
|
|
84
|
+
// Common formats: YYYY, YYYY/MM/DD, YYYY/MM, YYYY/MM/DD/other
|
|
85
|
+
const parts = str.split('/')
|
|
86
|
+
const year = parseInt(parts[0], 10)
|
|
87
|
+
if (isNaN(year)) return null
|
|
88
|
+
|
|
89
|
+
const dateParts = [year]
|
|
90
|
+
if (parts[1]) {
|
|
91
|
+
const month = parseInt(parts[1], 10)
|
|
92
|
+
if (!isNaN(month) && month >= 1 && month <= 12) {
|
|
93
|
+
dateParts.push(month)
|
|
94
|
+
if (parts[2]) {
|
|
95
|
+
const day = parseInt(parts[2], 10)
|
|
96
|
+
if (!isNaN(day) && day >= 1 && day <= 31) dateParts.push(day)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { 'date-parts': [dateParts] }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
export function parseRis(ris) {
|
|
106
|
+
if (!ris || typeof ris !== 'string') return []
|
|
107
|
+
|
|
108
|
+
const items = []
|
|
109
|
+
let current = null
|
|
110
|
+
|
|
111
|
+
const lines = ris.split(/\r?\n/)
|
|
112
|
+
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
// RIS tag format: XX - value (2 chars, 2 spaces, dash, space, value)
|
|
115
|
+
const match = line.match(/^([A-Z][A-Z0-9])\s{2}-\s?(.*)$/)
|
|
116
|
+
if (!match) continue
|
|
117
|
+
|
|
118
|
+
const [, tag, value] = match
|
|
119
|
+
const val = value.trim()
|
|
120
|
+
|
|
121
|
+
if (tag === 'TY') {
|
|
122
|
+
current = { type: TYPE_MAP[val] || 'article', _raw: {} }
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (tag === 'ER') {
|
|
127
|
+
if (current) {
|
|
128
|
+
items.push(finalizeItem(current))
|
|
129
|
+
current = null
|
|
130
|
+
}
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!current) continue
|
|
135
|
+
|
|
136
|
+
// Accumulate tags (some are repeatable)
|
|
137
|
+
if (!current._raw[tag]) current._raw[tag] = []
|
|
138
|
+
current._raw[tag].push(val)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Handle unterminated record
|
|
142
|
+
if (current) items.push(finalizeItem(current))
|
|
143
|
+
|
|
144
|
+
return items
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function finalizeItem(record) {
|
|
148
|
+
const raw = record._raw
|
|
149
|
+
const item = { type: record.type }
|
|
150
|
+
|
|
151
|
+
// ID
|
|
152
|
+
item.id = first(raw.ID) || first(raw.AN) || `ris-${Date.now()}`
|
|
153
|
+
|
|
154
|
+
// Authors (AU/A1 are primary authors)
|
|
155
|
+
const authors = [...(raw.AU || []), ...(raw.A1 || [])]
|
|
156
|
+
if (authors.length) item.author = authors.map(parseName).filter(Boolean)
|
|
157
|
+
|
|
158
|
+
// Editors (A2/ED)
|
|
159
|
+
const editors = [...(raw.A2 || []), ...(raw.ED || [])]
|
|
160
|
+
if (editors.length) item.editor = editors.map(parseName).filter(Boolean)
|
|
161
|
+
|
|
162
|
+
// Title
|
|
163
|
+
item.title = first(raw.TI) || first(raw.T1) || ''
|
|
164
|
+
|
|
165
|
+
// Container title
|
|
166
|
+
const container = first(raw.T2) || first(raw.JO) || first(raw.JF) || first(raw.JA) || first(raw.J2)
|
|
167
|
+
if (container) item['container-title'] = container
|
|
168
|
+
|
|
169
|
+
// Abstract
|
|
170
|
+
const abstract = first(raw.AB) || first(raw.N2)
|
|
171
|
+
if (abstract) item.abstract = abstract
|
|
172
|
+
|
|
173
|
+
// Date
|
|
174
|
+
const dateStr = first(raw.PY) || first(raw.Y1) || first(raw.DA)
|
|
175
|
+
if (dateStr) {
|
|
176
|
+
const d = parseDate(dateStr)
|
|
177
|
+
if (d) item.issued = d
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Simple fields
|
|
181
|
+
if (first(raw.VL)) item.volume = first(raw.VL)
|
|
182
|
+
if (first(raw.IS)) item.issue = first(raw.IS)
|
|
183
|
+
|
|
184
|
+
// Pages: SP (start) + EP (end)
|
|
185
|
+
const sp = first(raw.SP)
|
|
186
|
+
const ep = first(raw.EP)
|
|
187
|
+
if (sp && ep) item.page = `${sp}–${ep}`
|
|
188
|
+
else if (sp) item.page = sp
|
|
189
|
+
|
|
190
|
+
if (first(raw.DO)) item.DOI = first(raw.DO)
|
|
191
|
+
if (first(raw.UR)) item.URL = first(raw.UR)
|
|
192
|
+
if (first(raw.SN)) item.ISSN = first(raw.SN)
|
|
193
|
+
if (first(raw.PB)) item.publisher = first(raw.PB)
|
|
194
|
+
if (first(raw.CY)) item['publisher-place'] = first(raw.CY)
|
|
195
|
+
if (first(raw.ET)) item.edition = first(raw.ET)
|
|
196
|
+
if (first(raw.LA)) item.language = first(raw.LA)
|
|
197
|
+
|
|
198
|
+
// Keywords (repeatable)
|
|
199
|
+
if (raw.KW && raw.KW.length) item.keyword = raw.KW.join(', ')
|
|
200
|
+
|
|
201
|
+
// Notes
|
|
202
|
+
const notes = [...(raw.N1 || []), ...(raw.RN || [])]
|
|
203
|
+
if (notes.length) item.note = notes.join('; ')
|
|
204
|
+
|
|
205
|
+
// Series
|
|
206
|
+
if (first(raw.T3)) item['collection-title'] = first(raw.T3)
|
|
207
|
+
|
|
208
|
+
return item
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function first(arr) {
|
|
212
|
+
return arr && arr.length ? arr[0] : null
|
|
213
|
+
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseRis, exportRis } from '@citestyle/types'
|