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