@larsgw/formica 0.9.2 → 0.10.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.
Files changed (50) hide show
  1. package/lib/bin/process-resources-index.js +0 -0
  2. package/lib/bin/process-resources.d.ts +6 -1
  3. package/lib/bin/process-resources.js +116 -25
  4. package/lib/bin/validate-catalog.js +0 -0
  5. package/lib/bin/validate-resources-text.js +3 -1
  6. package/lib/resources/parse-text.js +6 -3
  7. package/lib/taxon-names/index.js +1 -1
  8. package/package.json +15 -13
  9. package/.gitattributes +0 -1
  10. package/.github/workflows/ci.yml +0 -27
  11. package/CHANGELOG.md +0 -390
  12. package/eslint.config.js +0 -34
  13. package/lib/bin/SHEETS.js +0 -0
  14. package/lib/bin/clean-links.d.ts +0 -2
  15. package/lib/bin/clean-links.js +0 -170
  16. package/lib/bin/download-place-shapes.js +0 -188
  17. package/lib/bin/index-place-shapes.js +0 -115
  18. package/lib/bin/process-resources-problems.js +0 -177
  19. package/lib/bin/validate-linked-data.js +0 -0
  20. package/lib/resources/content/clavis.js +0 -10
  21. package/lib/resources/content/index.js +0 -0
  22. package/lib/resources/content/sdd.js +0 -151
  23. package/lib/resources/sdd.js +0 -78
  24. package/src/bin/generate-linked-data.ts +0 -762
  25. package/src/bin/process-resources-index.ts +0 -100
  26. package/src/bin/process-resources.ts +0 -510
  27. package/src/bin/util.ts +0 -74
  28. package/src/bin/validate-catalog.ts +0 -122
  29. package/src/bin/validate-resources-text.ts +0 -25
  30. package/src/catalog/entities.ts +0 -62
  31. package/src/catalog/entity.ts +0 -116
  32. package/src/catalog/index.ts +0 -33
  33. package/src/catalog/tables/author.ts +0 -15
  34. package/src/catalog/tables/index.ts +0 -14
  35. package/src/catalog/tables/place.ts +0 -14
  36. package/src/catalog/tables/publisher.ts +0 -15
  37. package/src/catalog/tables/taxon.ts +0 -17
  38. package/src/catalog/tables/work.ts +0 -65
  39. package/src/catalog/value.ts +0 -51
  40. package/src/csv.ts +0 -33
  41. package/src/index.ts +0 -4
  42. package/src/module.d.ts +0 -148
  43. package/src/resources/diff-resource.ts +0 -226
  44. package/src/resources/index.ts +0 -4
  45. package/src/resources/parse-name.ts +0 -392
  46. package/src/resources/parse-text.ts +0 -408
  47. package/src/resources/resource.ts +0 -10
  48. package/src/taxon-names/index.ts +0 -79
  49. package/test/resources.js +0 -374
  50. package/tsconfig.json +0 -15
@@ -1,392 +0,0 @@
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
-
10
- export const RANKS: Rank[] = [
11
- 'phylum',
12
- 'subphylum',
13
- 'class',
14
- 'subclass',
15
- 'infraclass',
16
- 'superorder',
17
- 'order',
18
- 'suborder',
19
- 'infraorder',
20
- 'superfamily',
21
- 'family',
22
- 'subfamily',
23
- 'tribe',
24
- 'subtribe',
25
- 'genus',
26
- 'subgenus',
27
- 'section', // not ICZN
28
- 'subsection', // not ICZN
29
- 'series', // not ICZN
30
- 'group',
31
- 'subgroup', // ...
32
- 'aggregate', // not ICZN
33
- 'complex', // not ICZN
34
- 'species',
35
- 'subspecies',
36
- 'variety',
37
- 'form',
38
- 'aberration', // not ICZN
39
- 'race', // not ICZN
40
- 'stirps' // not ICZN
41
- ]
42
-
43
- const TAXONOMIC_STATUS: Record<string, TaxonStatus> = {
44
- '>': 'incorrect',
45
- '+': 'proparte synonym',
46
- '=': 'synonym'
47
- }
48
-
49
- const RANK_LABELS: Record<Rank, string> = {
50
- 'subspecies': 'subsp.',
51
- 'variety': 'var.',
52
- 'form': 'f.',
53
- 'aberration': 'ab.',
54
- 'race': 'r.',
55
- 'stirps': 'st.'
56
- }
57
-
58
- const RANK_LABELS_REVERSE: Record<string, Rank> = {
59
- 'st': 'stirps',
60
- 'r': 'race',
61
- 'ab': 'aberration',
62
- 'f': 'form',
63
- 'var': 'variety',
64
- 'ssp': 'subspecies',
65
- 'subsp': 'subspecies'
66
- }
67
-
68
- const HYBRID_SIGN = '\u00D7'
69
-
70
- /**
71
- * 1. Any number of
72
- * - capitalized words
73
- * - "&"
74
- * - " in "
75
- * - " ex "
76
- * - lowercase name particles
77
- * 2. Followed by a capitalized word
78
- * 3. Optionally, followed by "et al."
79
- */
80
- const LOWERCASE_NAME_PARTICLES = ['y', 'der', 'den', 'de', 'van', 'von'].join('|')
81
- const SIMPLE_AUTHOR_PATTERN = '(?:(?:\\p{Lu}\\S*|&|in|ex|' + LOWERCASE_NAME_PARTICLES + ')\\s*)*\\p{Lu}\\S+(?:\\s+et\\s+al\\.)?'
82
-
83
- const NAME_PATTERN = new RegExp(
84
- '^' +
85
- // $1 main name part
86
- '(\\S+)' +
87
- // $2 optional author citation
88
- '(?: ' +
89
- // but not auct(t)., etc.
90
- '(?!auctt?\\.|(?:syn|comb|sp|spec|nom|gen|subgen)\\. n(?:ov)?\\.|s(?:ens[.u]|\\.)|in part|partim)' +
91
- '(' +
92
- // $2.1 anything in parentheses, followed by optional revising author(s)
93
- '\\(.+?\\)(?:\\s+' + SIMPLE_AUTHOR_PATTERN + ')?' +
94
- '|' +
95
- // $2.2 anything followed by a year
96
- '.+?\\d{4}\\)?' +
97
- '|' +
98
- // $2.3 author(s)
99
- SIMPLE_AUTHOR_PATTERN +
100
- '))?' +
101
- // $3 optional notes
102
- '(?:,? (.+))?' +
103
- '$',
104
- 'u'
105
- )
106
-
107
- /**
108
- * Structure
109
- * $1 genus: ((?:x )?[A-Z]\S+)
110
- * $2 subgenus: (?:\(([A-Z]\S+?)\) )?
111
- */
112
- const SUBGENUS_PATTERN = /^([A-Z]\S+) (?:\(([A-Z]\S+?)\))(?= |$)/
113
-
114
- /**
115
- * Structure
116
- * $1 genus+subgenus (+ trailing space): (?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?
117
- * $1.1 genus: ((?:x )?[A-Z]\S+)
118
- * $1.2 subgenus: (?:\(([A-Z]\S+?)\) )?
119
- * $2 species: (x [a-z-]+|[a-z-][^\s.]+(?: x [a-z-]+)?|[A-Z][a-z]+_[a-z-]+ x [A-Z][a-z]+_[a-z-]+)
120
- * $2a: x [a-z-]+
121
- * $2b hybrid: [a-z-][^\s.]+(?: x [a-z-]+)?
122
- * $2c intergeneric hybrid: [A-Z][a-z]+_[a-z-]+ x [A-Z][a-z]+_[a-z-]+
123
- */
124
- const BINAME_PATTERN = /^(?:((?:x )?[A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?(x [a-z-]+|[a-z-][^\s.]+(?: x [a-z-]+)?|[A-Z][a-z]+_[a-z-]+ x [A-Z][a-z]+_[a-z-]+)(?= |$)/
125
-
126
- const CLUSTER_PATTERN = /^\[(_|\d+)\] /
127
-
128
- function compareRanks (a: Rank, b: Rank): number {
129
- return RANKS.indexOf(a) - RANKS.indexOf(b)
130
- }
131
-
132
- function capitalize (name: string): string {
133
- return name[0].toUpperCase() + name.slice(1).toLowerCase()
134
- }
135
-
136
- function capitalizeGenericName (name: string): string {
137
- if (name[0] === HYBRID_SIGN) {
138
- return HYBRID_SIGN + capitalize(name.slice(1))
139
- }
140
-
141
- return capitalize(name)
142
- }
143
-
144
- function isUpperCase (name: string): boolean {
145
- return name === name.toUpperCase()
146
- }
147
-
148
- function getSynonymRank (name: string, rank: Rank): Rank {
149
- const rest = name.replace(BINAME_PATTERN, '')
150
- const rankPrefix = rest.match(/^(?: |^)(st|r|ab|f|var|ssp|subsp)\. /)
151
- if (rankPrefix) {
152
- return RANK_LABELS_REVERSE[rankPrefix[1]] as string
153
- } else if (!BINAME_PATTERN.test(name)) {
154
- return SUBGENUS_PATTERN.test(name) ? 'subgenus' : rank
155
- } else if (/^ (?!sensu)[a-z0-9-]+($| )/.test(rest)) {
156
- return 'subspecies'
157
- } else {
158
- return 'species'
159
- }
160
- }
161
-
162
- function capitalizeAuthors (authors: string): string {
163
- return authors
164
- .replace(
165
- /[^\x00-\x40\x5B-\x60\x7B-\x7F]+/g, // eslint-disable-line no-control-regex
166
- name => isUpperCase(name) ? capitalize(name) : name
167
- )
168
- .replace(/ Y /g, ' y ')
169
- }
170
-
171
- export function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxon {
172
- const item = {} as WorkingTaxon
173
-
174
- // Synonyms have the accepted name usage as 'parent'.
175
- const isSynonym = /^[+=>] /.test(name)
176
- if (isSynonym) {
177
- item.taxonomicStatus = TAXONOMIC_STATUS[name[0]]
178
- name = name.slice(2)
179
- if (/^\? ?/.test(name)) {
180
- item.dynamicProperties = '{"synonymUncertain":true}'
181
- name = name.replace(/^\? ?/, '')
182
- }
183
- rank = getSynonymRank(name, parent.taxonRank as Rank)
184
- } else {
185
- item.taxonomicStatus = 'accepted'
186
- }
187
-
188
- // Clusters
189
- if (CLUSTER_PATTERN.test(name)) {
190
- const [match, cluster] = name.match(CLUSTER_PATTERN) as string[]
191
- item.cluster = cluster
192
- name = name.slice(match.length)
193
- }
194
-
195
- // Set verbatim identification after subsequent syntax is removed.
196
- item.verbatimIdentification = name.replace(/(?<=^| )x(?=$| )/g, HYBRID_SIGN).replace(/_/g, ' ')
197
-
198
- // Parent context is used for parsing and formatting binomial names.
199
- // For formatting, it needs to match external databases (i.e. be correct).
200
- // For parsing, it needs to match the current file. If relevant parents
201
- // (i.e. genus, species) had mistakes that were corrected, the uncorrected
202
- // genus and species names need to be used.
203
- const parentContext = {
204
- genus: parent.genus,
205
- subgenus: parent.subgenus,
206
- specificEpithet: parent.specificEpithet,
207
- incorrect: {
208
- genus: parent.incorrect && parent.incorrect.genus,
209
- specificEpithet: parent.incorrect && parent.incorrect.specificEpithet
210
- }
211
- }
212
-
213
- // Both contexts should be amended in the two cases where binomial names
214
- // are fully used: (1) synonyms and (2) multinomial taxa without parents to
215
- // provide parts of the name (e.g. bare species without a genus parent, or
216
- // even subspecies without a species or genus parent).
217
- if (isSynonym || !parentContext.genus || (compareRanks('species', rank) < 0 && !parentContext.specificEpithet)) {
218
- const [, genus, subgenus, species] = name.match(BINAME_PATTERN) ?? name.match(SUBGENUS_PATTERN) ?? []
219
- if (genus) {
220
- parentContext.incorrect.genus = genus
221
- parentContext.genus = capitalizeGenericName(genus.replace(/(^| )x /, HYBRID_SIGN))
222
- }
223
- if (subgenus) {
224
- parentContext.subgenus = capitalize(subgenus)
225
- } else if (genus) {
226
- // If a genus is given but no subgenus, remove any existing subgenus
227
- // from the parent context.
228
- delete parentContext.subgenus
229
- }
230
- if (species && compareRanks('species', rank) < 0) {
231
- parentContext.incorrect.specificEpithet = species
232
- parentContext.specificEpithet = species.replace(/(^| )x /, HYBRID_SIGN)
233
- }
234
- }
235
-
236
- // In taxa of group, species or lower, the name should just contain the
237
- // (infra)specific epithet and the author information & remarks when processing
238
- // further.
239
- if (compareRanks('group', rank) <= 0) {
240
- // Remove genus
241
- const genus = parentContext.incorrect.genus || parentContext.genus || ''
242
- if (name[0] === genus[0] && name.toLowerCase().startsWith(genus.toLowerCase() + ' ')) {
243
- name = name.slice(genus.length + 1)
244
- }
245
-
246
- // Remove subgenus
247
- name = name.replace(/^\(.*?\) /, '')
248
-
249
- // Infraspecific taxa
250
- if (compareRanks('species', rank) < 0) {
251
- // Remove specific epithet
252
- const species = parentContext.incorrect.specificEpithet || parentContext.specificEpithet || ''
253
- if (name.startsWith(species + ' ')) {
254
- name = name.slice(species.length + 1)
255
- }
256
-
257
- // Remove rank abbreviations
258
- name = name.replace(/^(st|r|ab|f|var|ssp|subsp)\. /, '')
259
- }
260
- } else if (compareRanks('genus', rank) <= 0) {
261
- // Remove genus
262
- const genus = parentContext.incorrect.genus || parentContext.genus || ''
263
- if (name[0] === genus[0] && name.toLowerCase().startsWith(genus.toLowerCase() + ' (')) {
264
- name = name.slice(genus.length + 1)
265
- }
266
-
267
- // Remove subgenus parentheses
268
- name = name.replace(/^\((.*?)\)/, '$1')
269
- }
270
-
271
- // Hybrids
272
- if (rank === 'genus' && name.startsWith('x ')) {
273
- name = HYBRID_SIGN + name.slice(2)
274
- }
275
-
276
- if (rank === 'species' && /(^| )x /.test(name)) {
277
- name = name.replace(/(^| )x /, HYBRID_SIGN)
278
- }
279
-
280
- // Divide the name into the main scientific name (only the epithet for taxa
281
- // lower than genus), the authorship information, and optionally remarks
282
- const nameParts = name.match(NAME_PATTERN)
283
- if (!nameParts) {
284
- throw new Error(`Taxon "${name}" could not be parsed`)
285
- }
286
-
287
- // To encode old names with spaces (e.g. "Orsillus pini canariensis Lindberg, 1953")
288
- // underscores are used, which are replaced here. This is also used for undescribed
289
- // species (e.g. "Leiobunum species A") and intergeneric hybrids (e.g. "×Festulpia
290
- // Festuca rubra × Vulpia bromoides")
291
- if (nameParts[1].includes('_')) {
292
- nameParts[1] = nameParts[1].replace(/_/g, ' ')
293
- }
294
-
295
- const [_, taxon, citation = '', notes] = nameParts
296
- item.scientificNameAuthorship = capitalizeAuthors(citation)
297
- item.taxonRemarks = notes
298
- item.taxonRank = rank
299
- item.genericName = undefined
300
- item.infragenericEpithet = undefined
301
- item.specificEpithet = undefined
302
- item.infraspecificEpithet = undefined
303
-
304
- if (/[^\p{L}0-9\u{00D7}\- ]/u.test(taxon)) {
305
- throw new RecoverableSyntaxError(`Taxon name contains unexpected characters: "${taxon}"`, item)
306
- }
307
-
308
- // Validate names and recompose binomial and trinomial names
309
- if (compareRanks('genus', rank) > 0) {
310
- item.scientificName = capitalize(taxon)
311
- if (taxon[0].toUpperCase() !== taxon[0]) {
312
- throw new RecoverableSyntaxError(`Taxon name (${rank}) should be capitalized: "${taxon}"`, item)
313
- }
314
- } else if (rank === 'genus') {
315
- item.scientificName = capitalizeGenericName(taxon)
316
- if (taxon[0].toUpperCase() !== taxon[0] || (taxon[0] === HYBRID_SIGN && taxon[1].toUpperCase() !== taxon[1])) {
317
- throw new RecoverableSyntaxError(`Generic epithet should be capitalized: "${taxon}"`, item)
318
- }
319
- } else if (compareRanks('group', rank) > 0) {
320
- item.genericName = parentContext.genus
321
- item.infragenericEpithet = parentContext.subgenus
322
- item.scientificName = capitalize(taxon)
323
- if (taxon[0].toUpperCase() !== taxon[0]) {
324
- throw new RecoverableSyntaxError(`Infrageneric epithet should be capitalized: "${taxon}"`, item)
325
- }
326
- } else if (rank === 'group') {
327
- item.genericName = parentContext.genus
328
- item.infragenericEpithet = parentContext.subgenus
329
- const specificEpithet = taxon.toLowerCase().replace(/(-group)?$/, '')
330
- item.scientificName = `${item.genericName} ${specificEpithet}-group`
331
- if (taxon.toLowerCase() !== taxon) {
332
- throw new RecoverableSyntaxError(`Group name should be lowercase: "${taxon}"`, item)
333
- }
334
- } else if (rank === 'subgroup') {
335
- item.genericName = parentContext.genus
336
- item.infragenericEpithet = parentContext.subgenus
337
- const specificEpithet = taxon.toLowerCase().replace(/(-subgroup)?$/, '')
338
- item.scientificName = `${item.genericName} ${specificEpithet}-subgroup`
339
- if (taxon.toLowerCase() !== taxon) {
340
- throw new RecoverableSyntaxError(`Subgroup name should be lowercase: "${taxon}"`, item)
341
- }
342
- } else if (compareRanks('species', rank) > 0) {
343
- item.genericName = parentContext.genus
344
- item.infragenericEpithet = parentContext.subgenus
345
- const specificEpithet = taxon.toLowerCase()
346
- item.scientificName = `${item.genericName} ${specificEpithet}`
347
- if (specificEpithet !== taxon) {
348
- throw new RecoverableSyntaxError(`Taxon name should be lowercase: "${taxon}"`, item)
349
- }
350
- } else if (rank === 'species') {
351
- item.genericName = parentContext.genus
352
- item.infragenericEpithet = parentContext.subgenus
353
- if (taxon.toLowerCase() !== taxon && !/^[A-Z][a-z]+ [a-z]+\xD7[A-Z][a-z]+ [a-z]+$/.test(taxon)) {
354
- throw new RecoverableSyntaxError(`Specific epithet should be lowercase: "${taxon}"`, item)
355
- }
356
- item.specificEpithet = taxon
357
- item.scientificName = `${item.genericName} ${item.specificEpithet}`
358
- } else if (compareRanks('species', rank) < 0) {
359
- item.genericName = parentContext.genus
360
- item.infragenericEpithet = parentContext.subgenus
361
- item.specificEpithet = parentContext.specificEpithet
362
- item.infraspecificEpithet = taxon.toLowerCase()
363
-
364
- // If possible, names below species should have abbreviations for ranks,
365
- // like "subsp."
366
- const nameParts = [
367
- item.genericName,
368
- item.specificEpithet,
369
- item.infraspecificEpithet
370
- ]
371
- if (item.taxonRank in RANK_LABELS) {
372
- nameParts.splice(2, 0, RANK_LABELS[item.taxonRank])
373
- }
374
- item.scientificName = nameParts.join(' ')
375
-
376
- if (item.infraspecificEpithet !== taxon) {
377
- throw new RecoverableSyntaxError(`Infraspecific epithet should be lowercase: "${taxon}"`, item)
378
- }
379
- }
380
-
381
- // Re-add authorship information
382
- item.scientificNameOnly = item.scientificName
383
- if (item.scientificNameAuthorship) {
384
- item.scientificName += ` ${item.scientificNameAuthorship}`
385
- }
386
-
387
- if (isSynonym && /^auctt?\./.test(item.taxonRemarks)) {
388
- item.taxonomicStatus = 'misapplied'
389
- }
390
-
391
- return item
392
- }