@larsgw/formica 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/.eslintrc.js +16 -0
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/lib/bin/process-resources-index.d.ts +1 -0
- package/lib/bin/process-resources-index.js +142 -0
- package/lib/bin/process-resources.d.ts +1 -0
- package/lib/bin/process-resources.js +594 -0
- package/lib/bin/util.d.ts +16 -0
- package/lib/bin/util.js +125 -0
- package/lib/bin/validate-catalog.d.ts +2 -0
- package/lib/bin/validate-catalog.js +92 -0
- package/lib/bin/validate-resources-text.d.ts +2 -0
- package/lib/bin/validate-resources-text.js +78 -0
- package/lib/catalog/entities.d.ts +12 -0
- package/lib/catalog/entities.js +111 -0
- package/lib/catalog/entity.d.ts +13 -0
- package/lib/catalog/entity.js +103 -0
- package/lib/catalog/index.d.ts +4 -0
- package/lib/catalog/index.js +33 -0
- package/lib/catalog/tables/author.d.ts +4 -0
- package/lib/catalog/tables/author.js +33 -0
- package/lib/catalog/tables/index.d.ts +2 -0
- package/lib/catalog/tables/index.js +13 -0
- package/lib/catalog/tables/place.d.ts +4 -0
- package/lib/catalog/tables/place.js +32 -0
- package/lib/catalog/tables/publisher.d.ts +4 -0
- package/lib/catalog/tables/publisher.js +33 -0
- package/lib/catalog/tables/work.d.ts +5 -0
- package/lib/catalog/tables/work.js +82 -0
- package/lib/catalog/value.d.ts +19 -0
- package/lib/catalog/value.js +49 -0
- package/lib/csv.d.ts +2 -0
- package/lib/csv.js +37 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +6 -0
- package/lib/resources/diff-resource.d.ts +7 -0
- package/lib/resources/diff-resource.js +152 -0
- package/lib/resources/index.d.ts +1 -0
- package/lib/resources/index.js +6 -0
- package/lib/resources/parse-text.d.ts +2 -0
- package/lib/resources/parse-text.js +499 -0
- package/lib/types.d.ts +62 -0
- package/lib/types.js +0 -0
- package/package.json +42 -0
- package/src/bin/process-resources-index.ts +73 -0
- package/src/bin/process-resources.ts +406 -0
- package/src/bin/util.ts +74 -0
- package/src/bin/validate-catalog.ts +37 -0
- package/src/bin/validate-resources-text.ts +25 -0
- package/src/catalog/entities.ts +62 -0
- package/src/catalog/entity.ts +113 -0
- package/src/catalog/index.ts +32 -0
- package/src/catalog/tables/author.ts +13 -0
- package/src/catalog/tables/index.ts +12 -0
- package/src/catalog/tables/place.ts +12 -0
- package/src/catalog/tables/publisher.ts +13 -0
- package/src/catalog/tables/work.ts +62 -0
- package/src/catalog/value.ts +48 -0
- package/src/csv.ts +33 -0
- package/src/index.ts +3 -0
- package/src/module.d.ts +105 -0
- package/src/resources/diff-resource.ts +155 -0
- package/src/resources/index.ts +4 -0
- package/src/resources/parse-text.ts +519 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import * as yaml from 'js-yaml'
|
|
2
|
+
import { createDiff, ResourceDiffType } from './diff-resource'
|
|
3
|
+
|
|
4
|
+
const RANKS: Rank[] = [
|
|
5
|
+
'class',
|
|
6
|
+
'infraclass',
|
|
7
|
+
'superorder',
|
|
8
|
+
'order',
|
|
9
|
+
'suborder',
|
|
10
|
+
'infraorder',
|
|
11
|
+
'superfamily',
|
|
12
|
+
'family',
|
|
13
|
+
'subfamily',
|
|
14
|
+
'tribe',
|
|
15
|
+
'subtribe',
|
|
16
|
+
'genus',
|
|
17
|
+
'subgenus',
|
|
18
|
+
'section', // not ICZN
|
|
19
|
+
'subsection', // not ICZN
|
|
20
|
+
'series', // not ICZN
|
|
21
|
+
'group',
|
|
22
|
+
'subgroup', // ...
|
|
23
|
+
'aggregate', // not ICZN
|
|
24
|
+
'complex', // not ICZN
|
|
25
|
+
'species',
|
|
26
|
+
'subspecies',
|
|
27
|
+
'variety',
|
|
28
|
+
'form',
|
|
29
|
+
'aberration', // not ICZN
|
|
30
|
+
'race', // not ICZN
|
|
31
|
+
'stirps' // not ICZN
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const DWC_RANKS: DwcRank[] = [
|
|
35
|
+
'kingdom',
|
|
36
|
+
'phylum',
|
|
37
|
+
'class',
|
|
38
|
+
'order',
|
|
39
|
+
'family',
|
|
40
|
+
'subfamily',
|
|
41
|
+
'genus',
|
|
42
|
+
'subgenus'
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
const TAXONOMIC_STATUS: Record<string, TaxonStatus> = {
|
|
46
|
+
'>': 'incorrect',
|
|
47
|
+
'+': 'heterotypic synonym',
|
|
48
|
+
'=': 'synonym'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const INDET_SUFFIXES = new Set([
|
|
52
|
+
'sp.',
|
|
53
|
+
'spec.',
|
|
54
|
+
'indet.',
|
|
55
|
+
'sp. indet.',
|
|
56
|
+
'spec. indet.'
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
const RANK_LABELS: Record<Rank, string> = {
|
|
60
|
+
'subspecies': 'subsp.',
|
|
61
|
+
'variety': 'var.',
|
|
62
|
+
'form': 'f.',
|
|
63
|
+
'aberration': 'ab.',
|
|
64
|
+
'race': 'r.',
|
|
65
|
+
'stirps': 'st.'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const RANK_LABELS_REVERSE: Record<string, Rank> = {
|
|
69
|
+
'st': 'stirps',
|
|
70
|
+
'r': 'race',
|
|
71
|
+
'ab': 'aberration',
|
|
72
|
+
'f': 'form',
|
|
73
|
+
'var': 'variety',
|
|
74
|
+
'ssp': 'subspecies',
|
|
75
|
+
'subsp': 'subspecies'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const NAME_PATTERN = new RegExp(
|
|
79
|
+
'^' +
|
|
80
|
+
// $1 main name part
|
|
81
|
+
'(\\S+)' +
|
|
82
|
+
// $2 optional author citation
|
|
83
|
+
'(?: ' +
|
|
84
|
+
// but not auct(t)., etc.
|
|
85
|
+
'(?!auctt?\\.|(?:syn|comb|sp|spec)\\. n(?:ov)?\\.|s(?:ens[.u]|\\.))' +
|
|
86
|
+
'(' +
|
|
87
|
+
// $2.1 anything in parentheses
|
|
88
|
+
'\\(.+?\\)' +
|
|
89
|
+
'|' +
|
|
90
|
+
// $2.2 anything followed by a year
|
|
91
|
+
'.+?\\d{4}\\)?' +
|
|
92
|
+
'|' +
|
|
93
|
+
// $2.3 name(, name)* & name
|
|
94
|
+
'.+(?:, .+)* & \\S+' +
|
|
95
|
+
'|' +
|
|
96
|
+
// $2.4 name y name
|
|
97
|
+
'\\S+ [yY] \\S+' +
|
|
98
|
+
'|' +
|
|
99
|
+
// $2.5 name( in name)
|
|
100
|
+
'\\p{Lu}\\S*(?: in \\S+)?' +
|
|
101
|
+
'))?' +
|
|
102
|
+
// $3 optional notes
|
|
103
|
+
'(?:,? (.+))?' +
|
|
104
|
+
'$',
|
|
105
|
+
'u'
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Structure
|
|
110
|
+
* $1 genus+subgenus (+ trailing space): (?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?
|
|
111
|
+
* $1.1 genus: ([A-Z]\S+)
|
|
112
|
+
* $1.2 subgenus: (?:\(([A-Z]\S+?)\) )?
|
|
113
|
+
* $2 species: ([a-z]\S+)
|
|
114
|
+
*/
|
|
115
|
+
const BINAME_PATTERN = /^(?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?([a-z]\S+) ?/
|
|
116
|
+
|
|
117
|
+
function compareRanks (a: Rank, b: Rank): number {
|
|
118
|
+
return RANKS.indexOf(a) - RANKS.indexOf(b)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function capitalize (name: string): string {
|
|
122
|
+
return name[0].toUpperCase() + name.slice(1).toLowerCase()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isUpperCase (name: string): boolean {
|
|
126
|
+
return name === name.toUpperCase()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getSynonymRank (name: string, rank: Rank): Rank {
|
|
130
|
+
const BINAME_PATTERN = /^([A-Z]\S+ (\([A-Z]\S+\) )?)?(x )?[a-z0-9-]+(?= |$)/
|
|
131
|
+
if (!BINAME_PATTERN.test(name)) {
|
|
132
|
+
return rank
|
|
133
|
+
}
|
|
134
|
+
const rest = name.replace(BINAME_PATTERN, '')
|
|
135
|
+
const rankPrefix = rest.match(/^ (st|r|ab|f|var|ssp|subsp)\. /)
|
|
136
|
+
if (rankPrefix) {
|
|
137
|
+
return RANK_LABELS_REVERSE[rankPrefix[1]] as string
|
|
138
|
+
} else if (/^ (?!sensu)[a-z0-9-]+($| )/.test(rest)) {
|
|
139
|
+
return 'subspecies'
|
|
140
|
+
} else {
|
|
141
|
+
return 'species'
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function capitalizeAuthors (authors: string): string {
|
|
146
|
+
return authors
|
|
147
|
+
.replace(
|
|
148
|
+
/[^\x00-\x40\x5B-\x60\x7B-\x7F]+/g, // eslint-disable-line no-control-regex
|
|
149
|
+
name => isUpperCase(name) ? capitalize(name) : name
|
|
150
|
+
)
|
|
151
|
+
.replace(/ Y /g, ' y ')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxon {
|
|
155
|
+
const item = {} as WorkingTaxon
|
|
156
|
+
|
|
157
|
+
// Synonyms have the accepted name usage as 'parent'.
|
|
158
|
+
const isSynonym = /^[+=>] /.test(name)
|
|
159
|
+
if (isSynonym) {
|
|
160
|
+
item.taxonomicStatus = TAXONOMIC_STATUS[name[0]]
|
|
161
|
+
name = name.replace(/^[+=>] (\? ?)?/, '')
|
|
162
|
+
rank = getSynonymRank(name, parent.taxonRank as Rank)
|
|
163
|
+
} else {
|
|
164
|
+
item.taxonomicStatus = 'accepted'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Clusters
|
|
168
|
+
if (/^\[(_|\d+)\] /.test(name)) {
|
|
169
|
+
name = name.replace(/^\[(_|\d+)\] /, '')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Parent context is used for parsing and formatting binomial names.
|
|
173
|
+
const parentContext = { ...parent }
|
|
174
|
+
if (parent.incorrect) { parentContext.incorrect = { ...parent.incorrect } }
|
|
175
|
+
|
|
176
|
+
// The parent context should be amended in the two cases where binomial names
|
|
177
|
+
// are truly accepted: synonyms and species (and below) without parents (resp.
|
|
178
|
+
// genera and genera and species) to provide parts of the name.
|
|
179
|
+
if (isSynonym || !parentContext.genus || (compareRanks('species', rank) < 0 && !parentContext.specificEpithet)) {
|
|
180
|
+
const [, genus, subgenus, species] = name.match(BINAME_PATTERN) || []
|
|
181
|
+
if (genus) {
|
|
182
|
+
parentContext.genus = capitalize(genus)
|
|
183
|
+
if (parentContext.incorrect) parentContext.incorrect.genus = capitalize(genus)
|
|
184
|
+
}
|
|
185
|
+
if (subgenus) {
|
|
186
|
+
parentContext.subgenus = capitalize(subgenus)
|
|
187
|
+
if (parentContext.incorrect) parentContext.incorrect.subgenus = capitalize(subgenus)
|
|
188
|
+
} else if (genus) {
|
|
189
|
+
// If a genus is given but no subgenus, remove it from the parent context
|
|
190
|
+
delete parentContext.subgenus
|
|
191
|
+
if (parentContext.incorrect) delete parentContext.incorrect.subgenus
|
|
192
|
+
}
|
|
193
|
+
if (species) {
|
|
194
|
+
parentContext.specificEpithet = species
|
|
195
|
+
if (parentContext.incorrect) parentContext.incorrect.specificEpithet = species
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// In taxa of group, species or lower, the name should just contain the
|
|
200
|
+
// (inter)specific epithet and the author information & remarks when processing
|
|
201
|
+
// further.
|
|
202
|
+
if (compareRanks('group', rank) <= 0) {
|
|
203
|
+
const parseContext = parentContext.incorrect || parentContext
|
|
204
|
+
if (!parseContext.genus) { parseContext.genus = name.split(' ', 1)[0] }
|
|
205
|
+
const genusPrefix = new RegExp(`^${parentContext.genus} (\\(.*?\\) )?`, 'i')
|
|
206
|
+
if (name[0] === (parentContext.genus as string)[0]) {
|
|
207
|
+
name = name.replace(genusPrefix, '')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (compareRanks('species', rank) < 0) {
|
|
211
|
+
const speciesPrefix = parseContext.specificEpithet + ' '
|
|
212
|
+
if (name.startsWith(speciesPrefix)) {
|
|
213
|
+
name = name.slice(speciesPrefix.length).replace(/^(st|r|ab|f|var|ssp|subsp)\. /, '')
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Hybrids
|
|
219
|
+
if (rank === 'species' && /^x /.test(name)) {
|
|
220
|
+
name = '\u00D7' + name.slice(2)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Divide the name into the main scientific name (only the epithet for taxa
|
|
224
|
+
// lower than genus), the authorship information, and optionally remarks
|
|
225
|
+
const nameParts = name.match(NAME_PATTERN)
|
|
226
|
+
if (!nameParts) {
|
|
227
|
+
throw new Error(`Taxon "${name}" could not be parsed`)
|
|
228
|
+
}
|
|
229
|
+
const [_, taxon, citation = '', notes] = nameParts
|
|
230
|
+
item.scientificNameAuthorship = capitalizeAuthors(citation)
|
|
231
|
+
item.taxonRemarks = notes
|
|
232
|
+
item.taxonRank = rank
|
|
233
|
+
|
|
234
|
+
if (/[^\p{L}0-9\u{00D7}\- ]/u.test(taxon) && !INDET_SUFFIXES.has(taxon)) {
|
|
235
|
+
throw new Error(`Taxon name contains unexpected characters: "${taxon}"`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Validate names and recompose binomial and trinomial names
|
|
239
|
+
if (compareRanks('group', rank) > 0) {
|
|
240
|
+
item.scientificName = capitalize(taxon)
|
|
241
|
+
if (item.scientificName[0] !== taxon[0]) {
|
|
242
|
+
throw new Error(`Taxon name (${rank}) should be capitalized: "${taxon}"`)
|
|
243
|
+
}
|
|
244
|
+
} else if (rank === 'group') {
|
|
245
|
+
item.genericName = parentContext.genus
|
|
246
|
+
item.infragenericEpithet = parentContext.subgenus
|
|
247
|
+
const specificEpithet = taxon.toLowerCase().replace(/(-group)?$/, '')
|
|
248
|
+
item.scientificName = `${item.genericName} ${specificEpithet}-group`
|
|
249
|
+
if (taxon.toLowerCase() !== taxon) {
|
|
250
|
+
console.log(item, taxon)
|
|
251
|
+
throw new Error(`Group name should be lowercase: "${taxon}"`)
|
|
252
|
+
}
|
|
253
|
+
} else if (rank === 'subgroup') {
|
|
254
|
+
item.genericName = parentContext.genus
|
|
255
|
+
item.infragenericEpithet = parentContext.subgenus
|
|
256
|
+
const specificEpithet = taxon.toLowerCase().replace(/(-subgroup)?$/, '')
|
|
257
|
+
item.scientificName = `${item.genericName} ${specificEpithet}-subgroup`
|
|
258
|
+
if (taxon.toLowerCase() !== taxon) {
|
|
259
|
+
console.log(item, taxon)
|
|
260
|
+
throw new Error(`Subgroup name should be lowercase: "${taxon}"`)
|
|
261
|
+
}
|
|
262
|
+
} else if (rank === 'species') {
|
|
263
|
+
item.genericName = parentContext.genus
|
|
264
|
+
item.infragenericEpithet = parentContext.subgenus
|
|
265
|
+
item.specificEpithet = taxon.toLowerCase()
|
|
266
|
+
item.scientificName = `${item.genericName} ${item.specificEpithet}`
|
|
267
|
+
if (item.specificEpithet !== taxon) {
|
|
268
|
+
console.log(item, taxon)
|
|
269
|
+
throw new Error(`Specific epithet should be lowercase: "${taxon}"`)
|
|
270
|
+
}
|
|
271
|
+
} else if (compareRanks('species', rank) < 0) {
|
|
272
|
+
item.genericName = parentContext.genus
|
|
273
|
+
item.infragenericEpithet = parentContext.subgenus
|
|
274
|
+
item.specificEpithet = parentContext.specificEpithet
|
|
275
|
+
item.intraspecificEpithet = taxon.toLowerCase()
|
|
276
|
+
|
|
277
|
+
// If possible, names below species should have abbreviations for ranks,
|
|
278
|
+
// like "subsp."
|
|
279
|
+
const nameParts = [
|
|
280
|
+
item.genericName,
|
|
281
|
+
item.specificEpithet,
|
|
282
|
+
item.intraspecificEpithet
|
|
283
|
+
]
|
|
284
|
+
if (item.taxonRank in RANK_LABELS) {
|
|
285
|
+
nameParts.splice(2, 0, RANK_LABELS[item.taxonRank])
|
|
286
|
+
}
|
|
287
|
+
item.scientificName = nameParts.join(' ')
|
|
288
|
+
|
|
289
|
+
if (item.intraspecificEpithet !== taxon) {
|
|
290
|
+
console.log(item, taxon)
|
|
291
|
+
throw new Error(`Intraspecific epithet should be lowercase: "${taxon}"`)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Re-add authorship information
|
|
296
|
+
item.scientificNameOnly = item.scientificName
|
|
297
|
+
if (item.scientificNameAuthorship) {
|
|
298
|
+
item.scientificName += ` ${item.scientificNameAuthorship}`
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Amend "parent" with corrections
|
|
302
|
+
if (item.taxonomicStatus === 'incorrect') {
|
|
303
|
+
const itemAsObject = item as { [index: string]: unknown }
|
|
304
|
+
const parentAsObject = parent as { [index: string]: unknown }
|
|
305
|
+
|
|
306
|
+
parent.incorrect = { ...parent }
|
|
307
|
+
for (const key in item) {
|
|
308
|
+
if (key !== 'taxonomicStatus') {
|
|
309
|
+
parentAsObject[key] = itemAsObject[key]
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return item
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function parseHeader (header: string): ResourceMetadata {
|
|
318
|
+
const config = yaml.load(header)
|
|
319
|
+
|
|
320
|
+
if (typeof config !== 'object' || Array.isArray(config) || config === null) {
|
|
321
|
+
throw new SyntaxError('yaml header should be an object')
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Invalid configuration
|
|
325
|
+
let levels
|
|
326
|
+
if (!('levels' in config)) {
|
|
327
|
+
levels = []
|
|
328
|
+
} else if (!Array.isArray(config.levels)) {
|
|
329
|
+
throw new SyntaxError('"levels" should be an array')
|
|
330
|
+
} else {
|
|
331
|
+
levels = config.levels
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let scope
|
|
335
|
+
if (!('scope' in config)) {
|
|
336
|
+
scope = []
|
|
337
|
+
} else if (!Array.isArray(config.scope)) {
|
|
338
|
+
throw new SyntaxError('"scope" should be an array')
|
|
339
|
+
} else {
|
|
340
|
+
scope = config.scope
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// No taxon ranks
|
|
344
|
+
if (levels.length === 0) {
|
|
345
|
+
throw new SyntaxError('Resource contains no taxa')
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Invalid taxon ranks
|
|
349
|
+
const invalidTaxonRanks = levels.filter(rank => !RANKS.includes(rank))
|
|
350
|
+
if (invalidTaxonRanks.length) {
|
|
351
|
+
throw new SyntaxError(`"levels" contains invalid values: ${invalidTaxonRanks.join(', ')}`)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const metadata: ResourceMetadata = { levels, scope }
|
|
355
|
+
|
|
356
|
+
if ('catalog' in config && typeof config.catalog === 'object' && config.catalog !== null) {
|
|
357
|
+
metadata.catalog = config.catalog
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return metadata
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function parseResource (resource: string): [ResourceMetadata, string] {
|
|
364
|
+
const [header, _, ...rest] = resource.split(/(\n---\n+)/)
|
|
365
|
+
const config = parseHeader(header)
|
|
366
|
+
const content = rest.join('')
|
|
367
|
+
|
|
368
|
+
// Check for too much indentation
|
|
369
|
+
const longerIndent = new RegExp(`^( ){${config.levels.length - 1}}(?! [+=>] ) `, 'm')
|
|
370
|
+
const longerIndentMatch = content.match(longerIndent)
|
|
371
|
+
if (longerIndentMatch !== null) {
|
|
372
|
+
const offset = longerIndentMatch.index
|
|
373
|
+
const line = (content.slice(0, offset).match(/\n/g) || []).length + 1
|
|
374
|
+
throw new SyntaxError(`Too much indentation at ${line}:0
|
|
375
|
+
${content.slice(offset).split('\n', 1)}
|
|
376
|
+
^`)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return [config, content]
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds: number[]): Resource {
|
|
383
|
+
const idBase = `${resource.id}:`
|
|
384
|
+
|
|
385
|
+
const data = resource.taxa as Record<TaxonId, WorkingTaxon>
|
|
386
|
+
let id = 0
|
|
387
|
+
let parents: Array<TaxonId | null> = []
|
|
388
|
+
let groupIndent = 0
|
|
389
|
+
let previousId = ''
|
|
390
|
+
let newIdOffset = Math.max(...oldIds)
|
|
391
|
+
|
|
392
|
+
for (const { text: line, type } of content) {
|
|
393
|
+
if (type === ResourceDiffType.Deleted) {
|
|
394
|
+
id++
|
|
395
|
+
continue
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const lineIndent = (line.match(/^ */) as string[])[0].length
|
|
399
|
+
|
|
400
|
+
if (lineIndent > groupIndent) {
|
|
401
|
+
// Do not count synonyms as parents
|
|
402
|
+
if (data[previousId] && data[previousId].taxonomicStatus === 'accepted') {
|
|
403
|
+
parents.push(previousId)
|
|
404
|
+
} else {
|
|
405
|
+
parents.push(null)
|
|
406
|
+
}
|
|
407
|
+
// Handle skips in indentation levels,
|
|
408
|
+
// e.g. if a certain genus has only species
|
|
409
|
+
// whereas other genera in the same key also
|
|
410
|
+
// have subgenera
|
|
411
|
+
if ((lineIndent - groupIndent) > 2) {
|
|
412
|
+
const gap = (lineIndent - groupIndent - 2) / 2
|
|
413
|
+
for (let i = 0; i < gap; i++) { parents.push(null) }
|
|
414
|
+
}
|
|
415
|
+
groupIndent = lineIndent
|
|
416
|
+
} else if (lineIndent < groupIndent) {
|
|
417
|
+
parents = parents.slice(0, lineIndent / 2)
|
|
418
|
+
groupIndent = lineIndent
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const parentId = parents.reduce((grandparent, parent) => parent || grandparent, null)
|
|
422
|
+
const parent = parentId === null ? {} as WorkingTaxon : data[parentId]
|
|
423
|
+
|
|
424
|
+
const name = line.slice(groupIndent)
|
|
425
|
+
const rank = resource.metadata.levels[groupIndent / 2]
|
|
426
|
+
const item = parseName(name, rank, parent)
|
|
427
|
+
const isSynonym = item.taxonomicStatus !== 'accepted'
|
|
428
|
+
const isIndet = Array.from(INDET_SUFFIXES).some(suffix => name.endsWith(' ' + suffix))
|
|
429
|
+
|
|
430
|
+
if (item.taxonomicStatus === 'incorrect' || isIndet) {
|
|
431
|
+
continue
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (type === ResourceDiffType.Added) {
|
|
435
|
+
newIdOffset++
|
|
436
|
+
item.scientificNameID = idBase + newIdOffset.toString()
|
|
437
|
+
} else {
|
|
438
|
+
id++
|
|
439
|
+
item.scientificNameID = idBase + (oldIds[id - 1] || id).toString()
|
|
440
|
+
}
|
|
441
|
+
previousId = item.scientificNameID
|
|
442
|
+
|
|
443
|
+
item.parentNameUsageID = isSynonym ? undefined : parent.scientificNameID
|
|
444
|
+
item.parentNameUsage = isSynonym ? undefined : parent.scientificName
|
|
445
|
+
item.acceptedNameUsageID = isSynonym ? parent.scientificNameID : undefined
|
|
446
|
+
item.acceptedNameUsage = isSynonym ? parent.scientificName : undefined
|
|
447
|
+
item.collectionCode = idBase.slice(0, -1)
|
|
448
|
+
|
|
449
|
+
for (const rank of DWC_RANKS) {
|
|
450
|
+
const itemAsObject = item as { [index: string]: unknown }
|
|
451
|
+
const parentAsObject = parent as { [index: string]: unknown }
|
|
452
|
+
|
|
453
|
+
itemAsObject[rank] = undefined
|
|
454
|
+
if (parentAsObject[rank]) {
|
|
455
|
+
itemAsObject[rank] = parentAsObject[rank]
|
|
456
|
+
}
|
|
457
|
+
if (item.taxonRank === rank) {
|
|
458
|
+
itemAsObject[rank] = item.scientificNameOnly
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (item.genericName && !item.genus) {
|
|
463
|
+
item.genus = item.genericName
|
|
464
|
+
}
|
|
465
|
+
if (item.infragenericEpithet && !item.subgenus) {
|
|
466
|
+
item.subgenus = item.infragenericEpithet
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (isSynonym) {
|
|
470
|
+
item.higherClassification = parent.higherClassification
|
|
471
|
+
} else if (parent.higherClassification) {
|
|
472
|
+
item.higherClassification = parent.higherClassification + ` | ${parent.scientificNameOnly}`
|
|
473
|
+
} else if (parentId) {
|
|
474
|
+
item.higherClassification = parent.scientificNameOnly
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
data[item.scientificNameID] = item
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return resource
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function splitResources (file: string): string[] {
|
|
484
|
+
return file.split('\n\n===\n\n')
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function parseFile (file: string, id: WorkId, old?: ResourceHistory): Resource[] {
|
|
488
|
+
const oldResources = old ? splitResources(old.txt) : []
|
|
489
|
+
return splitResources(file).map((resource, index) => {
|
|
490
|
+
const [config, content] = parseResource(resource)
|
|
491
|
+
const template: Resource = {
|
|
492
|
+
id: `${id}:${index + 1}`,
|
|
493
|
+
file: `${id}-${index + 1}`,
|
|
494
|
+
workId: id,
|
|
495
|
+
metadata: config,
|
|
496
|
+
taxa: {}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let diff
|
|
500
|
+
if (oldResources[index]) {
|
|
501
|
+
diff = createDiff(content, parseResource(oldResources[index])[1])
|
|
502
|
+
} else {
|
|
503
|
+
diff = createDiff(content, content)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const oldIds = []
|
|
507
|
+
if (old) {
|
|
508
|
+
for (const row of old.dwc[index].slice(1)) {
|
|
509
|
+
oldIds.push(parseInt(row[0].split(':')[2]))
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return parseResourceContent(diff, template, oldIds)
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function parseFileHeader (file: string): ResourceMetadata[] {
|
|
518
|
+
return splitResources(file).map(resource => parseResource(resource)[0])
|
|
519
|
+
}
|