@larsgw/formica 0.4.2 → 0.5.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/CHANGELOG.md +41 -0
- package/lib/bin/process-resources.d.ts +5 -1
- package/lib/bin/process-resources.js +143 -144
- package/lib/catalog/value.js +2 -2
- package/lib/csv.js +1 -1
- package/lib/resources/parse-text.js +80 -47
- package/package.json +1 -1
- package/src/bin/process-resources.ts +81 -60
- package/src/catalog/value.ts +2 -2
- package/src/csv.ts +1 -1
- package/src/module.d.ts +16 -4
- package/src/resources/parse-text.ts +76 -36
- package/test/resources.js +34 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as yaml from 'js-yaml'
|
|
2
|
+
import { Work } from '../catalog/tables/work'
|
|
2
3
|
import { createDiff, ResourceDiffType } from './diff-resource'
|
|
3
4
|
|
|
4
5
|
const RANKS: Rank[] = [
|
|
@@ -120,9 +121,9 @@ const NAME_PATTERN = new RegExp(
|
|
|
120
121
|
* $1 genus+subgenus (+ trailing space): (?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?
|
|
121
122
|
* $1.1 genus: ([A-Z]\S+)
|
|
122
123
|
* $1.2 subgenus: (?:\(([A-Z]\S+?)\) )?
|
|
123
|
-
* $2 species: ((?:x )?[a-
|
|
124
|
+
* $2 species: ((?:x )?[a-z][^\s.]+)
|
|
124
125
|
*/
|
|
125
|
-
const BINAME_PATTERN = /^(?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?((?:x )?[a-
|
|
126
|
+
const BINAME_PATTERN = /^(?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?((?:x )?[a-z][^\s.]+)(?= |$)/
|
|
126
127
|
|
|
127
128
|
function compareRanks (a: Rank, b: Rank): number {
|
|
128
129
|
return RANKS.indexOf(a) - RANKS.indexOf(b)
|
|
@@ -178,49 +179,67 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
178
179
|
name = name.replace(/^\[(_|\d+)\] /, '')
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
// Set verbatim identification after subsequent syntax is removed.
|
|
183
|
+
item.verbatimIdentification = name
|
|
184
|
+
|
|
181
185
|
// Parent context is used for parsing and formatting binomial names.
|
|
182
|
-
|
|
183
|
-
|
|
186
|
+
// For formatting, it needs to match external databases (i.e. be correct).
|
|
187
|
+
// For parsing, it needs to match the current file. If relevant parents
|
|
188
|
+
// (i.e. genus, species) had mistakes that were corrected, the uncorrected
|
|
189
|
+
// genus and species names need to be used.
|
|
190
|
+
const parentContext = {
|
|
191
|
+
genus: parent.genus,
|
|
192
|
+
subgenus: parent.subgenus,
|
|
193
|
+
specificEpithet: parent.specificEpithet,
|
|
194
|
+
incorrect: {
|
|
195
|
+
genus: parent.incorrect && parent.incorrect.genus,
|
|
196
|
+
specificEpithet: parent.incorrect && parent.incorrect.specificEpithet
|
|
197
|
+
}
|
|
198
|
+
}
|
|
184
199
|
|
|
185
|
-
//
|
|
186
|
-
// are
|
|
187
|
-
//
|
|
200
|
+
// Both contexts should be amended in the two cases where binomial names
|
|
201
|
+
// are fully used: (1) synonyms and (2) multinomial taxa without parents to
|
|
202
|
+
// provide parts of the name (e.g. bare species without a genus parent, or
|
|
203
|
+
// even subspecies without a species or genus parent).
|
|
188
204
|
if (isSynonym || !parentContext.genus || (compareRanks('species', rank) < 0 && !parentContext.specificEpithet)) {
|
|
189
205
|
const [, genus, subgenus, species] = name.match(BINAME_PATTERN) || []
|
|
190
206
|
if (genus) {
|
|
191
|
-
parentContext.genus = capitalize(genus)
|
|
192
|
-
if (parentContext.incorrect) parentContext.incorrect.genus = capitalize(genus)
|
|
207
|
+
parentContext.genus = parentContext.incorrect.genus = capitalize(genus)
|
|
193
208
|
}
|
|
194
209
|
if (subgenus) {
|
|
195
210
|
parentContext.subgenus = capitalize(subgenus)
|
|
196
|
-
if (parentContext.incorrect) parentContext.incorrect.subgenus = capitalize(subgenus)
|
|
197
211
|
} else if (genus) {
|
|
198
|
-
// If a genus is given but no subgenus, remove
|
|
212
|
+
// If a genus is given but no subgenus, remove any existing subgenus
|
|
213
|
+
// from the parent context.
|
|
199
214
|
delete parentContext.subgenus
|
|
200
|
-
if (parentContext.incorrect) delete parentContext.incorrect.subgenus
|
|
201
215
|
}
|
|
202
216
|
if (species) {
|
|
203
|
-
parentContext.specificEpithet = species
|
|
204
|
-
if (parentContext.incorrect) parentContext.incorrect.specificEpithet = species
|
|
217
|
+
parentContext.specificEpithet = parentContext.incorrect.specificEpithet = species
|
|
205
218
|
}
|
|
206
219
|
}
|
|
207
220
|
|
|
208
221
|
// In taxa of group, species or lower, the name should just contain the
|
|
209
|
-
// (
|
|
222
|
+
// (infra)specific epithet and the author information & remarks when processing
|
|
210
223
|
// further.
|
|
211
224
|
if (compareRanks('group', rank) <= 0) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
name = name.replace(genusPrefix, '')
|
|
225
|
+
// Remove genus
|
|
226
|
+
const genus = parentContext.incorrect.genus || parentContext.genus || ''
|
|
227
|
+
if (name[0] === genus[0] && name.toLowerCase().startsWith(genus.toLowerCase() + ' ')) {
|
|
228
|
+
name = name.slice(genus.length + 1)
|
|
217
229
|
}
|
|
218
230
|
|
|
231
|
+
// Remove subgenus
|
|
232
|
+
name = name.replace(/^\(.*?\) /, '')
|
|
233
|
+
|
|
234
|
+
// Infraspecific taxa
|
|
219
235
|
if (compareRanks('species', rank) < 0) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
236
|
+
// Remove specific epithet
|
|
237
|
+
const species = parentContext.incorrect.specificEpithet || parentContext.specificEpithet || ''
|
|
238
|
+
if (name.startsWith(species + ' ')) {
|
|
239
|
+
name = name.slice(species.length + 1)
|
|
223
240
|
}
|
|
241
|
+
|
|
242
|
+
// Remove rank abbreviations
|
|
224
243
|
name = name.replace(/^(st|r|ab|f|var|ssp|subsp)\. /, '')
|
|
225
244
|
}
|
|
226
245
|
}
|
|
@@ -236,6 +255,13 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
236
255
|
if (!nameParts) {
|
|
237
256
|
throw new Error(`Taxon "${name}" could not be parsed`)
|
|
238
257
|
}
|
|
258
|
+
|
|
259
|
+
// To encode old names with spaces (e.g. "Orsillus pini canariensis Lindberg, 1953")
|
|
260
|
+
// underscores are used, which are replaced here.
|
|
261
|
+
if (nameParts[1].includes('_')) {
|
|
262
|
+
nameParts[1] = nameParts[1].replace(/_/g, ' ')
|
|
263
|
+
}
|
|
264
|
+
|
|
239
265
|
const [_, taxon, citation = '', notes] = nameParts
|
|
240
266
|
item.scientificNameAuthorship = capitalizeAuthors(citation)
|
|
241
267
|
item.taxonRemarks = notes
|
|
@@ -282,23 +308,23 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
282
308
|
item.genericName = parentContext.genus
|
|
283
309
|
item.infragenericEpithet = parentContext.subgenus
|
|
284
310
|
item.specificEpithet = parentContext.specificEpithet
|
|
285
|
-
item.
|
|
311
|
+
item.infraspecificEpithet = taxon.toLowerCase()
|
|
286
312
|
|
|
287
313
|
// If possible, names below species should have abbreviations for ranks,
|
|
288
314
|
// like "subsp."
|
|
289
315
|
const nameParts = [
|
|
290
316
|
item.genericName,
|
|
291
317
|
item.specificEpithet,
|
|
292
|
-
item.
|
|
318
|
+
item.infraspecificEpithet
|
|
293
319
|
]
|
|
294
320
|
if (item.taxonRank in RANK_LABELS) {
|
|
295
321
|
nameParts.splice(2, 0, RANK_LABELS[item.taxonRank])
|
|
296
322
|
}
|
|
297
323
|
item.scientificName = nameParts.join(' ')
|
|
298
324
|
|
|
299
|
-
if (item.
|
|
325
|
+
if (item.infraspecificEpithet !== taxon) {
|
|
300
326
|
console.log(item, taxon)
|
|
301
|
-
throw new Error(`
|
|
327
|
+
throw new Error(`Infraspecific epithet should be lowercase: "${taxon}"`)
|
|
302
328
|
}
|
|
303
329
|
}
|
|
304
330
|
|
|
@@ -341,13 +367,8 @@ function parseHeader (header: string): ResourceMetadata {
|
|
|
341
367
|
levels = config.levels
|
|
342
368
|
}
|
|
343
369
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
scope = []
|
|
347
|
-
} else if (!Array.isArray(config.scope)) {
|
|
348
|
-
throw new SyntaxError('"scope" should be an array')
|
|
349
|
-
} else {
|
|
350
|
-
scope = config.scope
|
|
370
|
+
if ('scope' in config) {
|
|
371
|
+
throw new SyntaxError('"scope" data should go in "catalog"')
|
|
351
372
|
}
|
|
352
373
|
|
|
353
374
|
// No taxon ranks
|
|
@@ -361,10 +382,29 @@ function parseHeader (header: string): ResourceMetadata {
|
|
|
361
382
|
throw new SyntaxError(`"levels" contains invalid values: ${invalidTaxonRanks.join(', ')}`)
|
|
362
383
|
}
|
|
363
384
|
|
|
364
|
-
const metadata: ResourceMetadata = { levels
|
|
385
|
+
const metadata: ResourceMetadata = { levels }
|
|
365
386
|
|
|
366
387
|
if ('catalog' in config && typeof config.catalog === 'object' && config.catalog !== null) {
|
|
367
|
-
|
|
388
|
+
const catalog: Record<string, string> = {}
|
|
389
|
+
for (const key in config.catalog) {
|
|
390
|
+
const value = config.catalog[key as keyof object]
|
|
391
|
+
if (typeof value === 'number') {
|
|
392
|
+
catalog[key] = (value as number).toString()
|
|
393
|
+
} else if (typeof value === 'string') {
|
|
394
|
+
catalog[key] = value
|
|
395
|
+
} else {
|
|
396
|
+
throw new SyntaxError(`"catalog" should contain only strings ("${key}")`)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const work = new Work(catalog)
|
|
400
|
+
const errors = work.validate().filter(({ error }) => error !== 'Value(s) required but missing')
|
|
401
|
+
if (errors.length > 0) {
|
|
402
|
+
throw new SyntaxError(`"catalog" contains errors: ${errors.map(({ field, error }) => `[${field}] ${error}`).join('; ')}`)
|
|
403
|
+
}
|
|
404
|
+
metadata.catalog = {}
|
|
405
|
+
for (const key in work.fields) {
|
|
406
|
+
metadata.catalog[key] = work.get(key) as Value
|
|
407
|
+
}
|
|
368
408
|
}
|
|
369
409
|
|
|
370
410
|
return metadata
|
package/test/resources.js
CHANGED
|
@@ -90,4 +90,38 @@ Drymus
|
|
|
90
90
|
`, 'T1')
|
|
91
91
|
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Drymus')
|
|
92
92
|
})
|
|
93
|
+
|
|
94
|
+
await t.test('parses names containing non-ASCII characters', (t) => {
|
|
95
|
+
const [resource] = resources.parseTextFile(`---
|
|
96
|
+
levels: [species]
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
Nematus fåhraei Thomson
|
|
100
|
+
`, 'T1')
|
|
101
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Nematus fåhraei Thomson')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
await t.test('parses synonyms in different genera', (t) => {
|
|
105
|
+
const [resource] = resources.parseTextFile(`---
|
|
106
|
+
levels: [species]
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
Katamenes arbustorum subsp. burlinii
|
|
110
|
+
> arbustorum subsp. burlinii
|
|
111
|
+
= Eumenes arbustorum var. burlinii
|
|
112
|
+
`, 'T1')
|
|
113
|
+
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Eumenes arbustorum var. burlinii')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
await t.test('parses species without generic names', (t) => {
|
|
117
|
+
const [resource] = resources.parseTextFile(`---
|
|
118
|
+
levels: [genus, subgenus, species]
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
Microdynerus Thomson, 1874
|
|
122
|
+
Alastorynerus Blüthgen, 1938
|
|
123
|
+
microdynerus (Dalla Torre, 1889)
|
|
124
|
+
`, 'T1')
|
|
125
|
+
assert.strictEqual(resource.taxa['T1:1:3'].scientificName, 'Microdynerus microdynerus (Dalla Torre, 1889)')
|
|
126
|
+
})
|
|
93
127
|
})
|