@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.
@@ -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 Error(`Taxon name contains unexpected characters: "${taxon}"`)
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 Error(`Taxon name (${rank}) should be capitalized: "${taxon}"`)
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 Error(`Generic epithet should be capitalized: "${taxon}"`)
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 Error(`Infrageneric epithet should be capitalized: "${taxon}"`)
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
- console.log(item, taxon)
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
- console.log(item, taxon)
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
- console.log(item, taxon)
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
- console.log(item, taxon)
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
- console.log(item, taxon)
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
- errors.push(makeParseError(error.message, lineNumber))
214
- continue
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": "es5",
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/**/*"]