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