@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.
@@ -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
+ }