@larsgw/formica 0.8.5 → 0.8.7
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 +24 -0
- package/lib/bin/generate-linked-data.js +200 -323
- package/lib/bin/process-resources-index.js +59 -130
- package/lib/bin/process-resources.js +303 -524
- package/lib/bin/util.js +21 -56
- package/lib/bin/validate-catalog.js +26 -70
- package/lib/bin/validate-resources-text.js +17 -61
- package/lib/catalog/entities.js +28 -84
- package/lib/catalog/entity.js +36 -50
- package/lib/catalog/index.js +11 -12
- package/lib/catalog/tables/author.js +7 -24
- package/lib/catalog/tables/index.js +5 -5
- package/lib/catalog/tables/place.js +7 -24
- package/lib/catalog/tables/publisher.js +7 -24
- package/lib/catalog/tables/taxon.js +7 -24
- package/lib/catalog/tables/work.js +13 -30
- package/lib/catalog/value.js +11 -11
- package/lib/csv.js +7 -8
- package/lib/resources/diff-resource.js +52 -55
- package/lib/resources/parse-name.d.ts +4 -0
- package/lib/resources/parse-name.js +56 -54
- package/lib/resources/parse-text.js +121 -116
- package/lib/resources/resource.js +7 -25
- package/lib/taxon-names/index.js +15 -20
- package/package.json +1 -1
- package/src/resources/diff-resource.ts +19 -15
- package/src/resources/parse-name.ts +24 -19
- package/src/resources/parse-text.ts +35 -6
- package/test/resources.js +182 -89
- package/tsconfig.json +4 -1
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export class RecoverableSyntaxError<Result> extends SyntaxError {
|
|
2
|
+
result: Result
|
|
3
|
+
|
|
4
|
+
constructor (message: string, result: Result) {
|
|
5
|
+
super(message)
|
|
6
|
+
this.result = result
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
export const RANKS: Rank[] = [
|
|
2
11
|
'phylum',
|
|
3
12
|
'subphylum',
|
|
@@ -138,10 +147,8 @@ function getSynonymRank (name: string, rank: Rank): Rank {
|
|
|
138
147
|
const rankPrefix = rest.match(/^(?: |^)(st|r|ab|f|var|ssp|subsp)\. /)
|
|
139
148
|
if (rankPrefix) {
|
|
140
149
|
return RANK_LABELS_REVERSE[rankPrefix[1]] as string
|
|
141
|
-
} else if (SUBGENUS_PATTERN.test(name)) {
|
|
142
|
-
return 'subgenus'
|
|
143
150
|
} else if (!BINAME_PATTERN.test(name)) {
|
|
144
|
-
return rank
|
|
151
|
+
return SUBGENUS_PATTERN.test(name) ? 'subgenus' : rank
|
|
145
152
|
} else if (/^ (?!sensu)[a-z0-9-]+($| )/.test(rest)) {
|
|
146
153
|
return 'subspecies'
|
|
147
154
|
} else {
|
|
@@ -280,29 +287,32 @@ export function parseName (name: string, rank: Rank, parent: WorkingTaxon): Work
|
|
|
280
287
|
item.scientificNameAuthorship = capitalizeAuthors(citation)
|
|
281
288
|
item.taxonRemarks = notes
|
|
282
289
|
item.taxonRank = rank
|
|
290
|
+
item.genericName = undefined
|
|
291
|
+
item.infragenericEpithet = undefined
|
|
292
|
+
item.specificEpithet = undefined
|
|
293
|
+
item.infraspecificEpithet = undefined
|
|
283
294
|
|
|
284
|
-
// @ts-expect-error TS1501: This regular expression flag is only available when targeting 'es6' or later.
|
|
285
295
|
if (/[^\p{L}0-9\u{00D7}\- ]/u.test(taxon)) {
|
|
286
|
-
throw new
|
|
296
|
+
throw new RecoverableSyntaxError(`Taxon name contains unexpected characters: "${taxon}"`, item)
|
|
287
297
|
}
|
|
288
298
|
|
|
289
299
|
// Validate names and recompose binomial and trinomial names
|
|
290
300
|
if (compareRanks('genus', rank) > 0) {
|
|
291
301
|
item.scientificName = capitalize(taxon)
|
|
292
302
|
if (taxon[0].toUpperCase() !== taxon[0]) {
|
|
293
|
-
throw new
|
|
303
|
+
throw new RecoverableSyntaxError(`Taxon name (${rank}) should be capitalized: "${taxon}"`, item)
|
|
294
304
|
}
|
|
295
305
|
} else if (rank === 'genus') {
|
|
296
306
|
item.scientificName = capitalizeGenericName(taxon)
|
|
297
307
|
if (taxon[0].toUpperCase() !== taxon[0] || (taxon[0] === HYBRID_SIGN && taxon[1].toUpperCase() !== taxon[1])) {
|
|
298
|
-
throw new
|
|
308
|
+
throw new RecoverableSyntaxError(`Generic epithet should be capitalized: "${taxon}"`, item)
|
|
299
309
|
}
|
|
300
310
|
} else if (compareRanks('group', rank) > 0) {
|
|
301
311
|
item.genericName = parentContext.genus
|
|
302
312
|
item.infragenericEpithet = parentContext.subgenus
|
|
303
313
|
item.scientificName = capitalize(taxon)
|
|
304
314
|
if (taxon[0].toUpperCase() !== taxon[0]) {
|
|
305
|
-
throw new
|
|
315
|
+
throw new RecoverableSyntaxError(`Infrageneric epithet should be capitalized: "${taxon}"`, item)
|
|
306
316
|
}
|
|
307
317
|
} else if (rank === 'group') {
|
|
308
318
|
item.genericName = parentContext.genus
|
|
@@ -310,8 +320,7 @@ export function parseName (name: string, rank: Rank, parent: WorkingTaxon): Work
|
|
|
310
320
|
const specificEpithet = taxon.toLowerCase().replace(/(-group)?$/, '')
|
|
311
321
|
item.scientificName = `${item.genericName} ${specificEpithet}-group`
|
|
312
322
|
if (taxon.toLowerCase() !== taxon) {
|
|
313
|
-
|
|
314
|
-
throw new Error(`Group name should be lowercase: "${taxon}"`)
|
|
323
|
+
throw new RecoverableSyntaxError(`Group name should be lowercase: "${taxon}"`, item)
|
|
315
324
|
}
|
|
316
325
|
} else if (rank === 'subgroup') {
|
|
317
326
|
item.genericName = parentContext.genus
|
|
@@ -319,8 +328,7 @@ export function parseName (name: string, rank: Rank, parent: WorkingTaxon): Work
|
|
|
319
328
|
const specificEpithet = taxon.toLowerCase().replace(/(-subgroup)?$/, '')
|
|
320
329
|
item.scientificName = `${item.genericName} ${specificEpithet}-subgroup`
|
|
321
330
|
if (taxon.toLowerCase() !== taxon) {
|
|
322
|
-
|
|
323
|
-
throw new Error(`Subgroup name should be lowercase: "${taxon}"`)
|
|
331
|
+
throw new RecoverableSyntaxError(`Subgroup name should be lowercase: "${taxon}"`, item)
|
|
324
332
|
}
|
|
325
333
|
} else if (compareRanks('species', rank) > 0) {
|
|
326
334
|
item.genericName = parentContext.genus
|
|
@@ -328,15 +336,13 @@ export function parseName (name: string, rank: Rank, parent: WorkingTaxon): Work
|
|
|
328
336
|
const specificEpithet = taxon.toLowerCase()
|
|
329
337
|
item.scientificName = `${item.genericName} ${specificEpithet}`
|
|
330
338
|
if (specificEpithet !== taxon) {
|
|
331
|
-
|
|
332
|
-
throw new Error(`Subgroup name should be lowercase: "${taxon}"`)
|
|
339
|
+
throw new RecoverableSyntaxError(`Taxon name should be lowercase: "${taxon}"`, item)
|
|
333
340
|
}
|
|
334
341
|
} else if (rank === 'species') {
|
|
335
342
|
item.genericName = parentContext.genus
|
|
336
343
|
item.infragenericEpithet = parentContext.subgenus
|
|
337
344
|
if (taxon.toLowerCase() !== taxon && !/^[A-Z][a-z]+ [a-z]+\xD7[A-Z][a-z]+ [a-z]+$/.test(taxon)) {
|
|
338
|
-
|
|
339
|
-
throw new Error(`Specific epithet should be lowercase: "${taxon}"`)
|
|
345
|
+
throw new RecoverableSyntaxError(`Specific epithet should be lowercase: "${taxon}"`, item)
|
|
340
346
|
}
|
|
341
347
|
item.specificEpithet = taxon
|
|
342
348
|
item.scientificName = `${item.genericName} ${item.specificEpithet}`
|
|
@@ -359,8 +365,7 @@ export function parseName (name: string, rank: Rank, parent: WorkingTaxon): Work
|
|
|
359
365
|
item.scientificName = nameParts.join(' ')
|
|
360
366
|
|
|
361
367
|
if (item.infraspecificEpithet !== taxon) {
|
|
362
|
-
|
|
363
|
-
throw new Error(`Infraspecific epithet should be lowercase: "${taxon}"`)
|
|
368
|
+
throw new RecoverableSyntaxError(`Infraspecific epithet should be lowercase: "${taxon}"`, item)
|
|
364
369
|
}
|
|
365
370
|
}
|
|
366
371
|
|
|
@@ -371,4 +376,4 @@ export function parseName (name: string, rank: Rank, parent: WorkingTaxon): Work
|
|
|
371
376
|
}
|
|
372
377
|
|
|
373
378
|
return item
|
|
374
|
-
}
|
|
379
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as yaml from 'js-yaml'
|
|
2
2
|
import { WorkResource } from './resource'
|
|
3
3
|
import { createDiff, ResourceDiffType } from './diff-resource'
|
|
4
|
-
import { parseName, RANKS } from './parse-name'
|
|
4
|
+
import { parseName, RANKS, RecoverableSyntaxError } from './parse-name'
|
|
5
5
|
|
|
6
6
|
const MAIN_RANKS: Rank[] = [
|
|
7
7
|
'kingdom',
|
|
@@ -144,7 +144,7 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
144
144
|
let lineNumber = offsetLine
|
|
145
145
|
|
|
146
146
|
const parents: Array<TaxonId | null> = []
|
|
147
|
-
const previous = { id: '', indent: 0, group: { isLeaf: false, indent: 0 } }
|
|
147
|
+
const previous = { id: '', indent: 0, group: { isLeaf: false, indent: 0 }, errors: <SyntaxError[]>[] }
|
|
148
148
|
|
|
149
149
|
for (const line of content) {
|
|
150
150
|
const hasOriginalId = line.type !== ResourceDiffType.Added && !/^\s*(\[indet\]|> )/.test(line.original ?? line.text as string)
|
|
@@ -170,7 +170,6 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
170
170
|
continue
|
|
171
171
|
} else if (lineIndent <= previous.group.indent && (data[previous.id] && !previous.group.isLeaf)) {
|
|
172
172
|
errors.push(makeParseError('Missing leaf taxon', lineNumber - 1))
|
|
173
|
-
continue
|
|
174
173
|
}
|
|
175
174
|
|
|
176
175
|
// Update parentage
|
|
@@ -198,20 +197,28 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
198
197
|
// Do not process "indet" lines further, as they only serve to indicate
|
|
199
198
|
// that subtaxa are explicitely omitted
|
|
200
199
|
if (name.startsWith('[indet]')) {
|
|
200
|
+
errors.push(...previous.errors)
|
|
201
|
+
previous.errors.length = 0
|
|
201
202
|
previous.group.isLeaf = lineIndent / INDENT >= leafTaxonIndex
|
|
202
203
|
continue
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
const parentId = parents.reduce((grandparent, parent) => parent ?? grandparent, null)
|
|
206
207
|
const parent = parentId === null ? {} as WorkingTaxon : data[parentId]
|
|
207
|
-
const rank = resource.metadata.levels[parents.length]
|
|
208
208
|
let item
|
|
209
|
+
const itemErrors = []
|
|
209
210
|
|
|
210
211
|
try {
|
|
212
|
+
const rank = resource.metadata.levels[parents.length]
|
|
211
213
|
item = parseName(name, rank, parent)
|
|
212
214
|
} catch (error) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
if (error instanceof RecoverableSyntaxError) {
|
|
216
|
+
itemErrors.push(makeParseError(error.message, lineNumber))
|
|
217
|
+
item = error.result
|
|
218
|
+
} else {
|
|
219
|
+
errors.push(makeParseError(error.message, lineNumber))
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
215
222
|
}
|
|
216
223
|
|
|
217
224
|
// Add higher classification info
|
|
@@ -236,12 +243,31 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
236
243
|
|
|
237
244
|
// Amend "parent" with corrections, exit
|
|
238
245
|
if (item.taxonomicStatus === 'incorrect') {
|
|
246
|
+
if (parent.taxonomicStatus !== 'accepted') {
|
|
247
|
+
// Remove corrected synonym from parentage
|
|
248
|
+
parents[parents.length - 1] = null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (parent.incorrect) {
|
|
252
|
+
errors.push(makeParseError('Cannot apply a correction to a previous correction', lineNumber))
|
|
253
|
+
continue
|
|
254
|
+
} else if (parentId === null) {
|
|
255
|
+
errors.push(makeParseError('Cannot apply a correction to nothing', lineNumber))
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
|
|
239
259
|
parent.incorrect = { ...parent }
|
|
240
260
|
for (const key in item) {
|
|
241
261
|
if (key !== 'taxonomicStatus' && key !== 'verbatimIdentification') {
|
|
242
262
|
parentAsObject[key] = itemAsObject[key]
|
|
243
263
|
}
|
|
244
264
|
}
|
|
265
|
+
|
|
266
|
+
// If "parent" is corrected, its errors can be dropped
|
|
267
|
+
previous.errors.length = 0
|
|
268
|
+
// ...but errors associated with the corrected name are added immediately
|
|
269
|
+
errors.push(...itemErrors)
|
|
270
|
+
|
|
245
271
|
continue
|
|
246
272
|
}
|
|
247
273
|
|
|
@@ -267,6 +293,8 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
267
293
|
data[item.scientificNameID] = item
|
|
268
294
|
|
|
269
295
|
// Update loop state
|
|
296
|
+
errors.push(...previous.errors)
|
|
297
|
+
previous.errors = itemErrors
|
|
270
298
|
previous.id = item.scientificNameID
|
|
271
299
|
if (item.taxonomicStatus === 'accepted') {
|
|
272
300
|
previous.group.indent = previous.indent
|
|
@@ -274,6 +302,7 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
274
302
|
}
|
|
275
303
|
}
|
|
276
304
|
|
|
305
|
+
errors.push(...previous.errors)
|
|
277
306
|
if (errors.length) {
|
|
278
307
|
throw mergeParserErrors(errors)
|
|
279
308
|
}
|
package/test/resources.js
CHANGED
|
@@ -3,8 +3,8 @@ const assert = require('assert')
|
|
|
3
3
|
|
|
4
4
|
const { catalog, resources } = require('../lib')
|
|
5
5
|
|
|
6
|
-
suite('catalog',
|
|
7
|
-
|
|
6
|
+
suite('catalog', () => {
|
|
7
|
+
test('reports missing required fields', () => {
|
|
8
8
|
const errors = catalog.loadData(`id
|
|
9
9
|
B1`, 'catalog').validate()
|
|
10
10
|
assert.deepStrictEqual(errors, [
|
|
@@ -18,8 +18,8 @@ B1`, 'catalog').validate()
|
|
|
18
18
|
})
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
-
suite('resources',
|
|
22
|
-
|
|
21
|
+
suite('resources', () => {
|
|
22
|
+
test('parses author with initials', () => {
|
|
23
23
|
const [resource] = resources.parseTextFile(`---
|
|
24
24
|
levels: [family, genus, species]
|
|
25
25
|
---
|
|
@@ -33,32 +33,7 @@ Sphecidae A. Costa, 1886
|
|
|
33
33
|
assert.deepStrictEqual(Object.values(resource.taxa).map(taxon => taxon.scientificNameAuthorship), Array(3).fill('A. Costa, 1886'))
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
const [resource] = resources.parseTextFile(`---
|
|
38
|
-
levels: [species]
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
Clytochrysus lapidarius (Panzer, 1804)
|
|
42
|
-
= Crabo chrysostomus Lepeletier & Brullé, 1835
|
|
43
|
-
> Crabro chrysostomus Lepeletier & Brullé, 1835
|
|
44
|
-
`, 'T1')
|
|
45
|
-
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Clytochrysus lapidarius (Panzer, 1804)')
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
await test('errors for missing leaf taxa', (t) => {
|
|
49
|
-
assert.throws(() => {
|
|
50
|
-
resources.parseTextFile(`---
|
|
51
|
-
levels: [family, genus, species]
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
Cydnidae
|
|
55
|
-
Cydnidae
|
|
56
|
-
Legnotus
|
|
57
|
-
limbosus`, 'T1')
|
|
58
|
-
})
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
await test('parses synonyms starting with intraspecific ranks', (t) => {
|
|
36
|
+
test('parses synonyms starting with intraspecific ranks', () => {
|
|
62
37
|
const [resource] = resources.parseTextFile(`---
|
|
63
38
|
levels: [species]
|
|
64
39
|
---
|
|
@@ -69,7 +44,7 @@ Lygaeus equestris (Linnaeus, 1758)
|
|
|
69
44
|
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Lygaeus equestris f. lactans Horváth, 1899')
|
|
70
45
|
})
|
|
71
46
|
|
|
72
|
-
|
|
47
|
+
test('parses accepted taxa starting with intraspecific ranks', () => {
|
|
73
48
|
const [resource] = resources.parseTextFile(`---
|
|
74
49
|
levels: [species, form]
|
|
75
50
|
---
|
|
@@ -80,18 +55,7 @@ Lygaeus equestris (Linnaeus, 1758)
|
|
|
80
55
|
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Lygaeus equestris f. lactans Horváth, 1899')
|
|
81
56
|
})
|
|
82
57
|
|
|
83
|
-
|
|
84
|
-
const [resource] = resources.parseTextFile(`---
|
|
85
|
-
levels: [genus, species]
|
|
86
|
-
---
|
|
87
|
-
|
|
88
|
-
Drymus
|
|
89
|
-
[indet]
|
|
90
|
-
`, 'T1')
|
|
91
|
-
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Drymus')
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
await test('parses names containing non-ASCII characters', (t) => {
|
|
58
|
+
test('parses names containing non-ASCII characters', () => {
|
|
95
59
|
const [resource] = resources.parseTextFile(`---
|
|
96
60
|
levels: [species]
|
|
97
61
|
---
|
|
@@ -101,7 +65,7 @@ Nematus fåhraei Thomson
|
|
|
101
65
|
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Nematus fåhraei Thomson')
|
|
102
66
|
})
|
|
103
67
|
|
|
104
|
-
|
|
68
|
+
test('parses synonyms in different genera', () => {
|
|
105
69
|
const [resource] = resources.parseTextFile(`---
|
|
106
70
|
levels: [species]
|
|
107
71
|
---
|
|
@@ -113,7 +77,7 @@ Katamenes arbustorum subsp. burlinii
|
|
|
113
77
|
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Eumenes arbustorum var. burlinii')
|
|
114
78
|
})
|
|
115
79
|
|
|
116
|
-
|
|
80
|
+
test('parses species without generic names', () => {
|
|
117
81
|
const [resource] = resources.parseTextFile(`---
|
|
118
82
|
levels: [genus, subgenus, species]
|
|
119
83
|
---
|
|
@@ -125,7 +89,7 @@ Microdynerus Thomson, 1874
|
|
|
125
89
|
assert.strictEqual(resource.taxa['T1:1:3'].scientificName, 'Microdynerus microdynerus (Dalla Torre, 1889)')
|
|
126
90
|
})
|
|
127
91
|
|
|
128
|
-
|
|
92
|
+
test('parses hybrids', () => {
|
|
129
93
|
const [resource] = resources.parseTextFile(`---
|
|
130
94
|
levels: [species]
|
|
131
95
|
---
|
|
@@ -137,20 +101,7 @@ Tilia x vulgaris
|
|
|
137
101
|
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Tilia ×vulgaris')
|
|
138
102
|
})
|
|
139
103
|
|
|
140
|
-
|
|
141
|
-
const [resource] = resources.parseTextFile(`---
|
|
142
|
-
levels: [genus, species]
|
|
143
|
-
---
|
|
144
|
-
|
|
145
|
-
Bogdania Kerzhner, 1964
|
|
146
|
-
> Bogdiana Kerzhner, 1964
|
|
147
|
-
myrmica Kerzhner, 1964
|
|
148
|
-
`, 'T1')
|
|
149
|
-
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Bogdiana Kerzhner, 1964')
|
|
150
|
-
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Bogdiana myrmica Kerzhner, 1964')
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
await test('parses cross-genus hybrids', (t) => {
|
|
104
|
+
test('parses cross-genus hybrids', () => {
|
|
154
105
|
const [resource] = resources.parseTextFile(`---
|
|
155
106
|
levels: [genus, species]
|
|
156
107
|
---
|
|
@@ -165,7 +116,7 @@ x Festulpia
|
|
|
165
116
|
assert.strictEqual(resource.taxa['T1:1:3'].scientificName, '×Festulpia Festuca rubra×Vulpia bromoides')
|
|
166
117
|
})
|
|
167
118
|
|
|
168
|
-
|
|
119
|
+
test('parses cross-genus hybrids without parent context', () => {
|
|
169
120
|
const [resource] = resources.parseTextFile(`---
|
|
170
121
|
levels: [species]
|
|
171
122
|
---
|
|
@@ -176,7 +127,7 @@ x Festulpia Festuca_rubra x Vulpia_bromoides
|
|
|
176
127
|
assert.strictEqual(resource.taxa['T1:1:1'].verbatimIdentification, '× Festulpia Festuca rubra × Vulpia bromoides')
|
|
177
128
|
})
|
|
178
129
|
|
|
179
|
-
|
|
130
|
+
test('handles skips in ranks', () => {
|
|
180
131
|
const [resource] = resources.parseTextFile(`---
|
|
181
132
|
levels: [family, genus, species]
|
|
182
133
|
---
|
|
@@ -189,8 +140,59 @@ Apidae
|
|
|
189
140
|
assert.strictEqual(resource.taxa['T1:1:2'].taxonRank, 'species')
|
|
190
141
|
})
|
|
191
142
|
|
|
192
|
-
|
|
143
|
+
test('parses genera with subgenus-rank synonyms', () => {
|
|
193
144
|
const [resource] = resources.parseTextFile(`---
|
|
145
|
+
levels: [genus]
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
Ectemnius Dahlbom
|
|
149
|
+
= Crabro (Ectemnius) Dahlbom
|
|
150
|
+
`, 'T1')
|
|
151
|
+
assert.strictEqual(resource.taxa['T1:1:2'].taxonRank, 'subgenus')
|
|
152
|
+
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Ectemnius Dahlbom')
|
|
153
|
+
assert.strictEqual(resource.taxa['T1:1:2'].genericName, 'Crabro')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('parses genera with like-name subgenera', () => {
|
|
157
|
+
const [resource] = resources.parseTextFile(`---
|
|
158
|
+
levels: [genus, subgenus]
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
Polistes Latreille, 1802
|
|
162
|
+
Polistes Latreille, 1802
|
|
163
|
+
`, 'T1')
|
|
164
|
+
assert.strictEqual(resource.taxa['T1:1:2'].taxonRank, 'subgenus')
|
|
165
|
+
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Polistes Latreille, 1802')
|
|
166
|
+
assert.strictEqual(resource.taxa['T1:1:2'].genericName, 'Polistes')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
suite('leaf taxa checks', () => {
|
|
170
|
+
test('errors for missing leaf taxa', () => {
|
|
171
|
+
assert.throws(() => {
|
|
172
|
+
resources.parseTextFile(`---
|
|
173
|
+
levels: [family, genus, species]
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
Cydnidae
|
|
177
|
+
Cydnidae
|
|
178
|
+
Legnotus
|
|
179
|
+
limbosus`, 'T1')
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('does not validate "indet." lines', () => {
|
|
184
|
+
const [resource] = resources.parseTextFile(`---
|
|
185
|
+
levels: [genus, species]
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
Drymus
|
|
189
|
+
[indet]
|
|
190
|
+
`, 'T1')
|
|
191
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Drymus')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('synonyms do not break recognition of missing leaf taxa (1)', () => {
|
|
195
|
+
const [resource] = resources.parseTextFile(`---
|
|
194
196
|
levels: [genus, subgenus, species]
|
|
195
197
|
---
|
|
196
198
|
|
|
@@ -199,12 +201,12 @@ Bombus
|
|
|
199
201
|
Bombus
|
|
200
202
|
pascuorum
|
|
201
203
|
`, 'T1')
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
assert.strictEqual(resource.taxa['T1:1:4'].scientificName, 'Bombus pascuorum')
|
|
205
|
+
})
|
|
204
206
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
207
|
+
test('synonyms do not break recognition of missing leaf taxa (2)', () => {
|
|
208
|
+
assert.throws(() => {
|
|
209
|
+
resources.parseTextFile(`---
|
|
208
210
|
levels: [genus, subgenus, species]
|
|
209
211
|
---
|
|
210
212
|
|
|
@@ -214,12 +216,12 @@ Bombus
|
|
|
214
216
|
Bombus
|
|
215
217
|
pascuorum
|
|
216
218
|
`, 'T1')
|
|
219
|
+
})
|
|
217
220
|
})
|
|
218
|
-
})
|
|
219
221
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
222
|
+
test('synonyms do not break recognition of missing leaf taxa (3)', () => {
|
|
223
|
+
assert.throws(() => {
|
|
224
|
+
resources.parseTextFile(`---
|
|
223
225
|
levels: [family, genus, species]
|
|
224
226
|
---
|
|
225
227
|
|
|
@@ -229,32 +231,123 @@ Cydnidae
|
|
|
229
231
|
Legnotus
|
|
230
232
|
limbosus
|
|
231
233
|
`, 'T1')
|
|
234
|
+
})
|
|
232
235
|
})
|
|
233
236
|
})
|
|
234
237
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
+
suite('corrections', () => {
|
|
239
|
+
test('outputs corrected generic names', () => {
|
|
240
|
+
const [resource] = resources.parseTextFile(`---
|
|
241
|
+
levels: [genus, species]
|
|
238
242
|
---
|
|
239
243
|
|
|
240
|
-
|
|
241
|
-
|
|
244
|
+
Bogdania Kerzhner, 1964
|
|
245
|
+
> Bogdiana Kerzhner, 1964
|
|
246
|
+
myrmica Kerzhner, 1964
|
|
242
247
|
`, 'T1')
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
})
|
|
248
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Bogdiana Kerzhner, 1964')
|
|
249
|
+
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Bogdiana myrmica Kerzhner, 1964')
|
|
250
|
+
})
|
|
247
251
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
levels: [
|
|
252
|
+
test('does not validate name with correction', () => {
|
|
253
|
+
const [resource] = resources.parseTextFile(`---
|
|
254
|
+
levels: [species]
|
|
251
255
|
---
|
|
252
256
|
|
|
253
|
-
|
|
254
|
-
|
|
257
|
+
Clytochrysus lapidarius (Panzer, 1804)
|
|
258
|
+
= Crabo chrysostomus Lepeletier & Brullé, 1835
|
|
259
|
+
> Crabro chrysostomus Lepeletier & Brullé, 1835
|
|
255
260
|
`, 'T1')
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
261
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Clytochrysus lapidarius (Panzer, 1804)')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('parses invalid but corrected name', () => {
|
|
265
|
+
const [resource] = resources.parseTextFile(`---
|
|
266
|
+
levels: [species]
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
Crabro Kiesenwetteri A. Morawitz. 1866
|
|
270
|
+
> Crabro kiesenwetteri A. Morawitz. 1866
|
|
271
|
+
`, 'T1')
|
|
272
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Crabro kiesenwetteri A. Morawitz. 1866')
|
|
273
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificNameAuthorship, 'A. Morawitz. 1866')
|
|
274
|
+
assert.strictEqual(resource.taxa['T1:1:1'].verbatimIdentification, 'Crabro Kiesenwetteri A. Morawitz. 1866')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('parses children of invalid but corrected name', () => {
|
|
278
|
+
const [resource] = resources.parseTextFile(`---
|
|
279
|
+
levels: [genus, species]
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
Pirus L.
|
|
283
|
+
> Pyrus L.
|
|
284
|
+
aucuparia Gaertn.
|
|
285
|
+
Pirus domestica Sm.
|
|
286
|
+
Pirus aria Ehrh.
|
|
287
|
+
> Pyrus aria Ehrh.
|
|
288
|
+
`, 'T1')
|
|
289
|
+
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Pyrus aucuparia Gaertn.')
|
|
290
|
+
assert.strictEqual(resource.taxa['T1:1:3'].scientificName, 'Pyrus domestica Sm.')
|
|
291
|
+
assert.strictEqual(resource.taxa['T1:1:4'].scientificName, 'Pyrus aria Ehrh.')
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
test('does not keep parts of invalid but corrected name', () => {
|
|
295
|
+
const [resource] = resources.parseTextFile(`---
|
|
296
|
+
levels: [species]
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
Scolia 5-punctata FABRICIUS, 1781
|
|
300
|
+
> Scolia quinquepunctata FABRICIUS, 1781
|
|
301
|
+
`, 'T1')
|
|
302
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Scolia quinquepunctata Fabricius, 1781')
|
|
303
|
+
assert.strictEqual(resource.taxa['T1:1:1'].verbatimIdentification, 'Scolia 5-punctata FABRICIUS, 1781')
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
test('make correct diff when last line changes', () => {
|
|
307
|
+
const newText = `---
|
|
308
|
+
levels: [species]
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
Bittacus Hageni Brauer
|
|
312
|
+
> Bittacus hageni Brauer
|
|
313
|
+
`
|
|
314
|
+
const oldText = `---
|
|
315
|
+
levels: [genus, species]
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
Bittacus hageni Brauer
|
|
319
|
+
`
|
|
320
|
+
|
|
321
|
+
const [resource] = resources.parseTextFile(newText, 'T1', { txt: oldText, dwc: [[null, ['T:1:1'], ['T:1:2']]] })
|
|
322
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Bittacus hageni Brauer')
|
|
323
|
+
assert.strictEqual(resource.taxa['T1:1:1'].verbatimIdentification, 'Bittacus Hageni Brauer')
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
test('corrections of synonyms are correctly applied', () => {
|
|
327
|
+
const [resource] = resources.parseTextFile(`---
|
|
328
|
+
levels: [genus, subgenus, species]
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
Lasius F.
|
|
332
|
+
= Domisthorpea Mor. & Drnt., 1915
|
|
333
|
+
> Donisthorpea Mor. & Drnt., 1915
|
|
334
|
+
fuliginosus Latr.
|
|
335
|
+
`, 'T1')
|
|
336
|
+
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Donisthorpea Mor. & Drnt., 1915')
|
|
337
|
+
assert.strictEqual(resource.taxa['T1:1:3'].scientificName, 'Lasius fuliginosus Latr.')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
test('corrections are correctly applied', () => {
|
|
341
|
+
const [resource] = resources.parseTextFile(`---
|
|
342
|
+
levels: [genus, species]
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
Neopachygaster Austin, 1901
|
|
346
|
+
> Neopachygaster Austen, 1901
|
|
347
|
+
meromelas (Dufour, 1841)
|
|
348
|
+
`, 'T1')
|
|
349
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Neopachygaster Austen, 1901')
|
|
350
|
+
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Neopachygaster meromelas (Dufour, 1841)')
|
|
351
|
+
})
|
|
259
352
|
})
|
|
260
353
|
})
|
package/tsconfig.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"outDir": "./lib",
|
|
4
|
-
"target": "
|
|
4
|
+
"target": "es6",
|
|
5
5
|
|
|
6
6
|
"noImplicitAny": true,
|
|
7
7
|
"strictNullChecks": true,
|
|
8
8
|
"noImplicitThis": true,
|
|
9
|
+
|
|
10
|
+
"module": "commonjs",
|
|
11
|
+
"moduleResolution": "node10",
|
|
9
12
|
"esModuleInterop": true
|
|
10
13
|
},
|
|
11
14
|
"include": ["./src/**/*"]
|