@larsgw/formica 0.8.5 → 0.8.6
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 +15 -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 +117 -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 +30 -6
- package/test/resources.js +62 -0
- 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,26 @@ 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.incorrect) {
|
|
247
|
+
errors.push(makeParseError('Cannot apply a correction to a previous correction', lineNumber))
|
|
248
|
+
continue
|
|
249
|
+
} else if (parentId === null) {
|
|
250
|
+
errors.push(makeParseError('Cannot apply a correction to nothing', lineNumber))
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
|
|
239
254
|
parent.incorrect = { ...parent }
|
|
240
255
|
for (const key in item) {
|
|
241
256
|
if (key !== 'taxonomicStatus' && key !== 'verbatimIdentification') {
|
|
242
257
|
parentAsObject[key] = itemAsObject[key]
|
|
243
258
|
}
|
|
244
259
|
}
|
|
260
|
+
|
|
261
|
+
// If "parent" is corrected, its errors can be dropped
|
|
262
|
+
previous.errors.length = 0
|
|
263
|
+
// ...but errors associated with the corrected name are added immediately
|
|
264
|
+
errors.push(...itemErrors)
|
|
265
|
+
|
|
245
266
|
continue
|
|
246
267
|
}
|
|
247
268
|
|
|
@@ -267,6 +288,8 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
267
288
|
data[item.scientificNameID] = item
|
|
268
289
|
|
|
269
290
|
// Update loop state
|
|
291
|
+
errors.push(...previous.errors)
|
|
292
|
+
previous.errors = itemErrors
|
|
270
293
|
previous.id = item.scientificNameID
|
|
271
294
|
if (item.taxonomicStatus === 'accepted') {
|
|
272
295
|
previous.group.indent = previous.indent
|
|
@@ -274,6 +297,7 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
274
297
|
}
|
|
275
298
|
}
|
|
276
299
|
|
|
300
|
+
errors.push(...previous.errors)
|
|
277
301
|
if (errors.length) {
|
|
278
302
|
throw mergeParserErrors(errors)
|
|
279
303
|
}
|
package/test/resources.js
CHANGED
|
@@ -257,4 +257,66 @@ Polistes Latreille, 1802
|
|
|
257
257
|
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Polistes Latreille, 1802')
|
|
258
258
|
assert.strictEqual(resource.taxa['T1:1:2'].genericName, 'Polistes')
|
|
259
259
|
})
|
|
260
|
+
|
|
261
|
+
await test('parses invalid but corrected name', (t) => {
|
|
262
|
+
const [resource] = resources.parseTextFile(`---
|
|
263
|
+
levels: [species]
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
Crabro Kiesenwetteri A. Morawitz. 1866
|
|
267
|
+
> Crabro kiesenwetteri A. Morawitz. 1866
|
|
268
|
+
`, 'T1')
|
|
269
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Crabro kiesenwetteri A. Morawitz. 1866')
|
|
270
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificNameAuthorship, 'A. Morawitz. 1866')
|
|
271
|
+
assert.strictEqual(resource.taxa['T1:1:1'].verbatimIdentification, 'Crabro Kiesenwetteri A. Morawitz. 1866')
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
await test('parses children of invalid but corrected name', (t) => {
|
|
275
|
+
const [resource] = resources.parseTextFile(`---
|
|
276
|
+
levels: [genus, species]
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
Pirus L.
|
|
280
|
+
> Pyrus L.
|
|
281
|
+
aucuparia Gaertn.
|
|
282
|
+
Pirus domestica Sm.
|
|
283
|
+
Pirus aria Ehrh.
|
|
284
|
+
> Pyrus aria Ehrh.
|
|
285
|
+
`, 'T1')
|
|
286
|
+
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Pyrus aucuparia Gaertn.')
|
|
287
|
+
assert.strictEqual(resource.taxa['T1:1:3'].scientificName, 'Pyrus domestica Sm.')
|
|
288
|
+
assert.strictEqual(resource.taxa['T1:1:4'].scientificName, 'Pyrus aria Ehrh.')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
await test('does not keep parts of invalid but corrected name', (t) => {
|
|
292
|
+
const [resource] = resources.parseTextFile(`---
|
|
293
|
+
levels: [species]
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
Scolia 5-punctata FABRICIUS, 1781
|
|
297
|
+
> Scolia quinquepunctata FABRICIUS, 1781
|
|
298
|
+
`, 'T1')
|
|
299
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Scolia quinquepunctata Fabricius, 1781')
|
|
300
|
+
assert.strictEqual(resource.taxa['T1:1:1'].verbatimIdentification, 'Scolia 5-punctata FABRICIUS, 1781')
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
await test('make correct diff when last line changes', (t) => {
|
|
304
|
+
const newText = `---
|
|
305
|
+
levels: [species]
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
Bittacus Hageni Brauer
|
|
309
|
+
> Bittacus hageni Brauer
|
|
310
|
+
`
|
|
311
|
+
const oldText = `---
|
|
312
|
+
levels: [genus, species]
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
Bittacus hageni Brauer
|
|
316
|
+
`
|
|
317
|
+
|
|
318
|
+
const [resource] = resources.parseTextFile(newText, 'T1', { txt: oldText, dwc: [[null, ['T:1:1'], ['T:1:2']]] })
|
|
319
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Bittacus hageni Brauer')
|
|
320
|
+
assert.strictEqual(resource.taxa['T1:1:1'].verbatimIdentification, 'Bittacus Hageni Brauer')
|
|
321
|
+
})
|
|
260
322
|
})
|
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/**/*"]
|