@larsgw/formica 0.8.3 → 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.
@@ -13,7 +13,7 @@ jobs:
13
13
 
14
14
  strategy:
15
15
  matrix:
16
- node-version: [18.x, 20.x]
16
+ node-version: [20.x, 22.x]
17
17
 
18
18
  steps:
19
19
  - uses: actions/checkout@v3
package/CHANGELOG.md CHANGED
@@ -1,3 +1,37 @@
1
+ ## [0.8.5](https://github.com/identification-resources/formica/compare/v0.8.4...v0.8.5) (2025-10-28)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **catalog:** add mapping for typo 'male' in linked data ([a83d34e](https://github.com/identification-resources/formica/commit/a83d34e7aa57a78e410e2ea55adbf75e522d9028))
7
+ * **catalog:** fix dcmitype: for algorithms, checklists ([68e353d](https://github.com/identification-resources/formica/commit/68e353d71d632e092e3e79254e6b4f575c004f1c))
8
+ * **catalog:** fix typos in linked data generation ([9ad951f](https://github.com/identification-resources/formica/commit/9ad951f5c19978509c6aba7fd81470270707da99))
9
+ * **resources:** add scientific name for complexes, aggregates ([843afc3](https://github.com/identification-resources/formica/commit/843afc39df1952dda4bb6339ee47f2619432bb74))
10
+ * **resources:** omit generic error stack trace ([c831bc2](https://github.com/identification-resources/formica/commit/c831bc2a952ef17a9e57b012fe0568e2acc0c6ae))
11
+
12
+
13
+ ### Features
14
+
15
+ * **resources:** handle subgenus synonyms of genera ([4e61336](https://github.com/identification-resources/formica/commit/4e61336e36054ffd6df57ebad5479e7ceebcb235)), closes [#16](https://github.com/identification-resources/formica/issues/16)
16
+ * **resources:** rework parsing of resources ([cff7730](https://github.com/identification-resources/formica/commit/cff773000c5b9e3f990c619cdd45a76a18ee39a9)), closes [#15](https://github.com/identification-resources/formica/issues/15) [#7](https://github.com/identification-resources/formica/issues/7)
17
+
18
+
19
+
20
+ ## [0.8.4](https://github.com/identification-resources/formica/compare/v0.8.3...v0.8.4) (2025-09-19)
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * **catalog:** clearer error message when validating ([b1c34f0](https://github.com/identification-resources/formica/commit/b1c34f00ed9a262251d2d24b3e6c46fd17730405))
26
+ * **resources:** optimize diffing algorithm ([0c86c49](https://github.com/identification-resources/formica/commit/0c86c49474754dced74c8423f3e6b7ed2fd2a78f)), closes [#6](https://github.com/identification-resources/formica/issues/6)
27
+
28
+
29
+ ### Features
30
+
31
+ * **catalog:** map additional scope value ([80c6d26](https://github.com/identification-resources/formica/commit/80c6d269a7d5b03600991fb353280e81afcefb0e))
32
+
33
+
34
+
1
35
  ## [0.8.3](https://github.com/identification-resources/formica/compare/v0.8.2...v0.8.3) (2025-07-15)
2
36
 
3
37
 
@@ -115,6 +115,7 @@ var SCOPES = {
115
115
  'nymphs': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
116
116
  'nypmhs': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
117
117
  'nymphs (instar V)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
118
+ 'nymphs (instar IV)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
118
119
  'eggs': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/embryo'],
119
120
  // plant life stage
120
121
  'flowering plants': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/adult'],
@@ -125,6 +126,7 @@ var SCOPES = {
125
126
  // sex
126
127
  'females': ['dwciri:sex', 'http://rs.gbif.org/vocabulary/gbif/sex/female'],
127
128
  'males': ['dwciri:sex', 'http://rs.gbif.org/vocabulary/gbif/sex/male'],
129
+ 'male': ['dwciri:sex', 'http://rs.gbif.org/vocabulary/gbif/sex/male'],
128
130
  // caste
129
131
  'queens': ['dwc:caste', 'queen'],
130
132
  'workers': ['dwc:caste', 'worker'],
@@ -277,7 +279,7 @@ function makeLinkedDataForTaxon(taxon) {
277
279
  var node = {
278
280
  '@id': "".concat(PREFIX, "taxon/").concat(taxon.get('id')),
279
281
  '@type': 'dwc:Taxon',
280
- 'dwc:scientificName': taxon.get('display_name'),
282
+ 'dwc:scientificName': taxon.get('name'),
281
283
  };
282
284
  if (taxon.has('rank')) {
283
285
  node['dwc:taxonRank'] = makeTaxonRankUri(taxon.get('rank'));
@@ -371,7 +373,7 @@ function makeLinkedDataForResource(work, files, resourceId) {
371
373
  }
372
374
  var node = __assign(__assign({}, makeLinkedDataForWork(resource, files)), { '@id': "".concat(PREFIX, "resource/").concat(resourceId), '@type': 'bibo:DocumentPart' });
373
375
  var types = (_b = (_a = resource.get('key_type')) !== null && _a !== void 0 ? _a : work.get('key_type')) !== null && _b !== void 0 ? _b : [];
374
- if (types.includes('matrix')) {
376
+ if (types.includes('matrix') || types.includes('algorithm')) {
375
377
  node['dcterms:type'] = { '@id': 'http://purl.org/dc/dcmitype/Software' };
376
378
  }
377
379
  else if (types.includes('key') || types.includes('reference') || types.includes('supplement')) {
@@ -380,6 +382,9 @@ function makeLinkedDataForResource(work, files, resourceId) {
380
382
  else if (types.includes('gallery') || types.includes('collection')) {
381
383
  node['dcterms:type'] = { '@id': 'http://purl.org/dc/dcmitype/Collection' };
382
384
  }
385
+ else if (types.includes('checklist')) {
386
+ node['dcterms:type'] = { '@id': 'http://purl.org/dc/dcmitype/Dataset' };
387
+ }
383
388
  if (types.includes('key') || types.includes('matrix')) {
384
389
  node['ac:subtype'] = { '@id': 'http://rs.tdwg.org/acsubtype/values/IdentificationKey' };
385
390
  }
@@ -636,10 +641,10 @@ function makeLinkedDataForWorks(files) {
636
641
  node['dcterms:hasPart'] = [];
637
642
  }
638
643
  node['dcterms:hasPart'].push(resource);
639
- if (!Array.isArray(resource['dcterms:isPartof'])) {
640
- resource['dcterms:isPartof'] = [];
644
+ if (!Array.isArray(resource['dcterms:isPartOf'])) {
645
+ resource['dcterms:isPartOf'] = [];
641
646
  }
642
- resource['dcterms:isPartof'].push({ '@id': node['@id'] });
647
+ resource['dcterms:isPartOf'].push({ '@id': node['@id'] });
643
648
  }
644
649
  };
645
650
  for (var _b = 0, _c = works.entities; _b < _c.length; _b++) {
@@ -411,7 +411,7 @@ var ResourceProcessor = /** @class */ (function () {
411
411
  return [2 /*return*/, resources.parseTextFile(file, id, old)];
412
412
  case 10:
413
413
  error_1 = _g.sent();
414
- console.log(error_1);
414
+ console.log(error_1.message);
415
415
  return [4 /*yield*/, (0, util_1.prompt)("".concat(id, ": generating Darwin Core failed, retry? "))
416
416
  // Clear cache to re-import
417
417
  ];
@@ -84,17 +84,14 @@ function validateFile(arg) {
84
84
  case 1:
85
85
  file = _a.sent();
86
86
  sheet = path.basename(filePath, '.csv');
87
- return [2 /*return*/, {
88
- filePath: filePath,
89
- errors: index_1.catalog.loadData(file, sheet).validate()
90
- }];
87
+ return [2 /*return*/, index_1.catalog.loadData(file, sheet).validate()];
91
88
  }
92
89
  });
93
90
  });
94
91
  }
95
92
  function main(args) {
96
93
  return __awaiter(this, void 0, void 0, function () {
97
- var exitStatus, results, _i, results_1, result;
94
+ var exitStatus, results, i, result;
98
95
  return __generator(this, function (_a) {
99
96
  switch (_a.label) {
100
97
  case 0:
@@ -102,16 +99,17 @@ function main(args) {
102
99
  return [4 /*yield*/, Promise.allSettled(args.map(validateFile))];
103
100
  case 1:
104
101
  results = _a.sent();
105
- for (_i = 0, results_1 = results; _i < results_1.length; _i++) {
106
- result = results_1[_i];
102
+ for (i = 0; i < results.length; i++) {
103
+ result = results[i];
107
104
  if (result.status === 'rejected') {
105
+ console.error("".concat(args[i], ":"));
108
106
  console.error(result.reason);
109
107
  console.error();
110
108
  exitStatus = 1;
111
109
  }
112
- else if (result.value.errors.length > 0) {
113
- console.error("".concat(result.value.filePath, ":"));
114
- console.table(result.value.errors);
110
+ else if (result.value.length > 0) {
111
+ console.error("".concat(args[i], ":"));
112
+ console.table(result.value);
115
113
  console.error();
116
114
  exitStatus = 1;
117
115
  }
@@ -94,7 +94,7 @@ function main(args) {
94
94
  index_1.resources.parseTextFile(file, id);
95
95
  }
96
96
  catch (error) {
97
- console.error(filePath + '\n ' + error.message + '\n');
97
+ console.error(filePath + '\n' + error.message.replace(/^/gm, ' ') + '\n');
98
98
  exitStatus = 1;
99
99
  }
100
100
  _a.label = 3;
@@ -4,4 +4,4 @@ export declare enum ResourceDiffType {
4
4
  Modified = "~",
5
5
  Unchanged = "="
6
6
  }
7
- export declare function createDiff(a: string, b: string, tokenize?: ResourceDiffTokenizer): ResourceDiff;
7
+ export declare function createDiff(a: string, b: string): ResourceDiff;
@@ -18,18 +18,32 @@ var ResourceDiffType;
18
18
  ResourceDiffType["Modified"] = "~";
19
19
  ResourceDiffType["Unchanged"] = "=";
20
20
  })(ResourceDiffType || (exports.ResourceDiffType = ResourceDiffType = {}));
21
+ var Matrix = /** @class */ (function () {
22
+ function Matrix(m, n) {
23
+ this.values = Array(m * n).fill(0);
24
+ this.m = m;
25
+ this.n = n;
26
+ }
27
+ Matrix.prototype.getValue = function (i, j) {
28
+ return this.values[(i * this.n) + j];
29
+ };
30
+ Matrix.prototype.setValue = function (i, j, value) {
31
+ this.values[(i * this.n) + j] = value;
32
+ };
33
+ return Matrix;
34
+ }());
21
35
  function LCS(X, Y) {
22
36
  var m = X.length;
23
37
  var n = Y.length;
24
38
  // Build matrix
25
- var C = Array(m + 1).fill(0).map(function () { return Array(n + 1).fill(0); });
39
+ var C = new Matrix(m + 1, n + 1);
26
40
  for (var i_1 = 0; i_1 < m; i_1++) {
27
41
  for (var j_1 = 0; j_1 < n; j_1++) {
28
42
  if (X[i_1] === Y[j_1]) {
29
- C[i_1 + 1][j_1 + 1] = C[i_1][j_1] + 1;
43
+ C.setValue(i_1 + 1, j_1 + 1, C.getValue(i_1, j_1) + 1);
30
44
  }
31
45
  else {
32
- C[i_1 + 1][j_1 + 1] = Math.max(C[i_1][j_1 + 1], C[i_1 + 1][j_1]);
46
+ C.setValue(i_1 + 1, j_1 + 1, Math.max(C.getValue(i_1, j_1 + 1), C.getValue(i_1 + 1, j_1)));
33
47
  }
34
48
  }
35
49
  }
@@ -46,7 +60,7 @@ function LCS(X, Y) {
46
60
  i--;
47
61
  j--;
48
62
  }
49
- else if (i !== 0 && (j === 0 || C[i - 1][j] > C[i][j - 1])) {
63
+ else if (i !== 0 && (j === 0 || C.getValue(i - 1, j) > C.getValue(i, j - 1))) {
50
64
  diff.unshift({
51
65
  text: X[i - 1],
52
66
  type: ResourceDiffType.Added
@@ -63,17 +77,16 @@ function LCS(X, Y) {
63
77
  }
64
78
  return diff;
65
79
  }
66
- function gitTokenize(text) {
80
+ function tokenizeWords(text) {
67
81
  if (text.length === 0) {
68
82
  return [];
69
83
  }
70
84
  return text.match(/\S+|\n|[\r\t\f\v \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/g);
71
85
  }
72
- function createDiff(a, b, tokenize) {
73
- var _a, _b;
74
- if (tokenize === void 0) { tokenize = gitTokenize; }
75
- var X = tokenize(a.trimEnd());
76
- var Y = tokenize(b.trimEnd());
86
+ function tokenizeLines(text) {
87
+ return text.split('\n');
88
+ }
89
+ function diffTokens(X, Y) {
77
90
  // Remove common prefix
78
91
  var prefix = [];
79
92
  while (X.length && X[0] === Y[0]) {
@@ -94,12 +107,11 @@ function createDiff(a, b, tokenize) {
94
107
  X.pop();
95
108
  Y.pop();
96
109
  }
97
- // Generate word from remains, combine with prefix and suffix, add trailing
98
- // newline
99
- var changes = __spreadArray(__spreadArray(__spreadArray(__spreadArray([], prefix, true), LCS(X, Y), true), suffix, true), [
100
- { text: '\n', type: ResourceDiffType.Unchanged }
101
- ], false);
102
- // Convert word diff to line diff
110
+ // Generate diff from remains, combine with prefix and suffix
111
+ return __spreadArray(__spreadArray(__spreadArray([], prefix, true), LCS(X, Y), true), suffix, true);
112
+ }
113
+ function convertWordDiff(changes) {
114
+ var _a, _b;
103
115
  var lines = [];
104
116
  var line = null;
105
117
  var deletedNewlines = 0;
@@ -156,3 +168,36 @@ function createDiff(a, b, tokenize) {
156
168
  }
157
169
  return lines;
158
170
  }
171
+ function getWordTokensFromLines(lines) {
172
+ return lines.flatMap(function (change) { return tokenizeWords(change.text).concat('\n'); });
173
+ }
174
+ function createDiff(a, b) {
175
+ var lines = diffTokens(tokenizeLines(a.trimEnd()), tokenizeLines(b.trimEnd()));
176
+ var changes = [];
177
+ var diffPart = { added: [], deleted: [] };
178
+ for (var i = 0; i < lines.length; i++) {
179
+ if (lines[i].type === ResourceDiffType.Added) {
180
+ diffPart.added.push(lines[i]);
181
+ continue;
182
+ }
183
+ else if (lines[i].type === ResourceDiffType.Deleted) {
184
+ diffPart.deleted.push(lines[i]);
185
+ continue;
186
+ }
187
+ if (diffPart.added.length && diffPart.deleted.length) {
188
+ changes.push.apply(changes, convertWordDiff(diffTokens(getWordTokensFromLines(diffPart.added), getWordTokensFromLines(diffPart.deleted))));
189
+ diffPart.added.length = 0;
190
+ diffPart.deleted.length = 0;
191
+ }
192
+ else if (diffPart.added.length) {
193
+ changes.push.apply(changes, diffPart.added);
194
+ diffPart.added.length = 0;
195
+ }
196
+ else if (diffPart.deleted.length) {
197
+ changes.push.apply(changes, diffPart.deleted.map(function (change) { return ({ text: undefined, original: change.text, type: change.type }); }));
198
+ diffPart.deleted.length = 0;
199
+ }
200
+ changes.push(lines[i]);
201
+ }
202
+ return changes;
203
+ }
@@ -0,0 +1,2 @@
1
+ export declare const RANKS: Rank[];
2
+ export declare function parseName(name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxon;
@@ -0,0 +1,352 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RANKS = void 0;
4
+ exports.parseName = parseName;
5
+ exports.RANKS = [
6
+ 'phylum',
7
+ 'subphylum',
8
+ 'class',
9
+ 'infraclass',
10
+ 'superorder',
11
+ 'order',
12
+ 'suborder',
13
+ 'infraorder',
14
+ 'superfamily',
15
+ 'family',
16
+ 'subfamily',
17
+ 'tribe',
18
+ 'subtribe',
19
+ 'genus',
20
+ 'subgenus',
21
+ 'section', // not ICZN
22
+ 'subsection', // not ICZN
23
+ 'series', // not ICZN
24
+ 'group',
25
+ 'subgroup', // ...
26
+ 'aggregate', // not ICZN
27
+ 'complex', // not ICZN
28
+ 'species',
29
+ 'subspecies',
30
+ 'variety',
31
+ 'form',
32
+ 'aberration', // not ICZN
33
+ 'race', // not ICZN
34
+ 'stirps' // not ICZN
35
+ ];
36
+ var TAXONOMIC_STATUS = {
37
+ '>': 'incorrect',
38
+ '+': 'heterotypic synonym',
39
+ '=': 'synonym'
40
+ };
41
+ var RANK_LABELS = {
42
+ 'subspecies': 'subsp.',
43
+ 'variety': 'var.',
44
+ 'form': 'f.',
45
+ 'aberration': 'ab.',
46
+ 'race': 'r.',
47
+ 'stirps': 'st.'
48
+ };
49
+ var RANK_LABELS_REVERSE = {
50
+ 'st': 'stirps',
51
+ 'r': 'race',
52
+ 'ab': 'aberration',
53
+ 'f': 'form',
54
+ 'var': 'variety',
55
+ 'ssp': 'subspecies',
56
+ 'subsp': 'subspecies'
57
+ };
58
+ var HYBRID_SIGN = '\u00D7';
59
+ /**
60
+ * 1. Any number of
61
+ * - capitalized words
62
+ * - "&"
63
+ * - " in "
64
+ * - " ex "
65
+ * - lowercase name particles
66
+ * 2. Followed by a capitalized word
67
+ * 3. Optionally, followed by "et al."
68
+ */
69
+ var LOWERCASE_NAME_PARTICLES = ['y', 'der', 'den', 'de', 'van', 'von'].join('|');
70
+ var SIMPLE_AUTHOR_PATTERN = '(?:(?:\\p{Lu}\\S*|&|in|ex|' + LOWERCASE_NAME_PARTICLES + ')\\s*)*\\p{Lu}\\S+(?:\\s+et\\s+al\\.)?';
71
+ var NAME_PATTERN = new RegExp('^' +
72
+ // $1 main name part
73
+ '(\\S+)' +
74
+ // $2 optional author citation
75
+ '(?: ' +
76
+ // but not auct(t)., etc.
77
+ '(?!auctt?\\.|(?:syn|comb|sp|spec|nom|gen|subgen)\\. n(?:ov)?\\.|s(?:ens[.u]|\\.)|in part|partim)' +
78
+ '(' +
79
+ // $2.1 anything in parentheses, followed by optional revising author(s)
80
+ '\\(.+?\\)(?:\\s+' + SIMPLE_AUTHOR_PATTERN + ')?' +
81
+ '|' +
82
+ // $2.2 anything followed by a year
83
+ '.+?\\d{4}\\)?' +
84
+ '|' +
85
+ // $2.3 author(s)
86
+ SIMPLE_AUTHOR_PATTERN +
87
+ '))?' +
88
+ // $3 optional notes
89
+ '(?:,? (.+))?' +
90
+ '$', 'u');
91
+ /**
92
+ * Structure
93
+ * $1 genus: ((?:x )?[A-Z]\S+)
94
+ * $2 subgenus: (?:\(([A-Z]\S+?)\) )?
95
+ */
96
+ var SUBGENUS_PATTERN = /^([A-Z]\S+) (?:\(([A-Z]\S+?)\))(?= |$)/;
97
+ /**
98
+ * Structure
99
+ * $1 genus+subgenus (+ trailing space): (?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?
100
+ * $1.1 genus: ((?:x )?[A-Z]\S+)
101
+ * $1.2 subgenus: (?:\(([A-Z]\S+?)\) )?
102
+ * $2 species: (x [a-z-]+|[a-z-][^\s.]+(?: x [a-z-]+)?|[A-Z][a-z]+_[a-z-]+ x [A-Z][a-z]+_[a-z-]+)
103
+ * $2a: x [a-z-]+
104
+ * $2b hybrid: [a-z-][^\s.]+(?: x [a-z-]+)?
105
+ * $2c intergeneric hybrid: [A-Z][a-z]+_[a-z-]+ x [A-Z][a-z]+_[a-z-]+
106
+ */
107
+ var 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-]+)(?= |$)/;
108
+ function compareRanks(a, b) {
109
+ return exports.RANKS.indexOf(a) - exports.RANKS.indexOf(b);
110
+ }
111
+ function capitalize(name) {
112
+ return name[0].toUpperCase() + name.slice(1).toLowerCase();
113
+ }
114
+ function capitalizeGenericName(name) {
115
+ if (name[0] === HYBRID_SIGN) {
116
+ return HYBRID_SIGN + capitalize(name.slice(1));
117
+ }
118
+ return capitalize(name);
119
+ }
120
+ function isUpperCase(name) {
121
+ return name === name.toUpperCase();
122
+ }
123
+ function getSynonymRank(name, rank) {
124
+ var rest = name.replace(BINAME_PATTERN, '');
125
+ var rankPrefix = rest.match(/^(?: |^)(st|r|ab|f|var|ssp|subsp)\. /);
126
+ if (rankPrefix) {
127
+ return RANK_LABELS_REVERSE[rankPrefix[1]];
128
+ }
129
+ else if (SUBGENUS_PATTERN.test(name)) {
130
+ return 'subgenus';
131
+ }
132
+ else if (!BINAME_PATTERN.test(name)) {
133
+ return rank;
134
+ }
135
+ else if (/^ (?!sensu)[a-z0-9-]+($| )/.test(rest)) {
136
+ return 'subspecies';
137
+ }
138
+ else {
139
+ return 'species';
140
+ }
141
+ }
142
+ function capitalizeAuthors(authors) {
143
+ return authors
144
+ .replace(/[^\x00-\x40\x5B-\x60\x7B-\x7F]+/g, // eslint-disable-line no-control-regex
145
+ function (// eslint-disable-line no-control-regex
146
+ name) { return isUpperCase(name) ? capitalize(name) : name; })
147
+ .replace(/ Y /g, ' y ');
148
+ }
149
+ function parseName(name, rank, parent) {
150
+ var _a, _b;
151
+ var item = {};
152
+ // Synonyms have the accepted name usage as 'parent'.
153
+ var isSynonym = /^[+=>] /.test(name);
154
+ if (isSynonym) {
155
+ item.taxonomicStatus = TAXONOMIC_STATUS[name[0]];
156
+ name = name.replace(/^[+=>] (\? ?)?/, '');
157
+ rank = getSynonymRank(name, parent.taxonRank);
158
+ }
159
+ else {
160
+ item.taxonomicStatus = 'accepted';
161
+ }
162
+ // Clusters
163
+ if (/^\[(_|\d+)\] /.test(name)) {
164
+ name = name.replace(/^\[(_|\d+)\] /, '');
165
+ }
166
+ // Set verbatim identification after subsequent syntax is removed.
167
+ item.verbatimIdentification = name.replace(/(?<=^| )x(?=$| )/g, HYBRID_SIGN).replace(/_/g, ' ');
168
+ // Parent context is used for parsing and formatting binomial names.
169
+ // For formatting, it needs to match external databases (i.e. be correct).
170
+ // For parsing, it needs to match the current file. If relevant parents
171
+ // (i.e. genus, species) had mistakes that were corrected, the uncorrected
172
+ // genus and species names need to be used.
173
+ var parentContext = {
174
+ genus: parent.genus,
175
+ subgenus: parent.subgenus,
176
+ specificEpithet: parent.specificEpithet,
177
+ incorrect: {
178
+ genus: parent.incorrect && parent.incorrect.genus,
179
+ specificEpithet: parent.incorrect && parent.incorrect.specificEpithet
180
+ }
181
+ };
182
+ // Both contexts should be amended in the two cases where binomial names
183
+ // are fully used: (1) synonyms and (2) multinomial taxa without parents to
184
+ // provide parts of the name (e.g. bare species without a genus parent, or
185
+ // even subspecies without a species or genus parent).
186
+ if (isSynonym || !parentContext.genus || (compareRanks('species', rank) < 0 && !parentContext.specificEpithet)) {
187
+ var _c = (_b = (_a = name.match(BINAME_PATTERN)) !== null && _a !== void 0 ? _a : name.match(SUBGENUS_PATTERN)) !== null && _b !== void 0 ? _b : [], genus = _c[1], subgenus = _c[2], species = _c[3];
188
+ if (genus) {
189
+ parentContext.incorrect.genus = genus;
190
+ parentContext.genus = capitalizeGenericName(genus.replace(/(^| )x /, HYBRID_SIGN));
191
+ }
192
+ if (subgenus) {
193
+ parentContext.subgenus = capitalize(subgenus);
194
+ }
195
+ else if (genus) {
196
+ // If a genus is given but no subgenus, remove any existing subgenus
197
+ // from the parent context.
198
+ delete parentContext.subgenus;
199
+ }
200
+ if (species && compareRanks('species', rank) < 0) {
201
+ parentContext.incorrect.specificEpithet = species;
202
+ parentContext.specificEpithet = species.replace(/(^| )x /, HYBRID_SIGN);
203
+ }
204
+ }
205
+ // In taxa of group, species or lower, the name should just contain the
206
+ // (infra)specific epithet and the author information & remarks when processing
207
+ // further.
208
+ if (compareRanks('group', rank) <= 0) {
209
+ // Remove genus
210
+ var genus = parentContext.incorrect.genus || parentContext.genus || '';
211
+ if (name[0] === genus[0] && name.toLowerCase().startsWith(genus.toLowerCase() + ' ')) {
212
+ name = name.slice(genus.length + 1);
213
+ }
214
+ // Remove subgenus
215
+ name = name.replace(/^\(.*?\) /, '');
216
+ // Infraspecific taxa
217
+ if (compareRanks('species', rank) < 0) {
218
+ // Remove specific epithet
219
+ var species = parentContext.incorrect.specificEpithet || parentContext.specificEpithet || '';
220
+ if (name.startsWith(species + ' ')) {
221
+ name = name.slice(species.length + 1);
222
+ }
223
+ // Remove rank abbreviations
224
+ name = name.replace(/^(st|r|ab|f|var|ssp|subsp)\. /, '');
225
+ }
226
+ }
227
+ else if (compareRanks('genus', rank) <= 0) {
228
+ // Remove genus
229
+ var genus = parentContext.incorrect.genus || parentContext.genus || '';
230
+ if (name[0] === genus[0] && name.toLowerCase().startsWith(genus.toLowerCase() + ' (')) {
231
+ name = name.slice(genus.length + 1);
232
+ }
233
+ // Remove subgenus parentheses
234
+ name = name.replace(/^\((.*?)\)/, '$1');
235
+ }
236
+ // Hybrids
237
+ if (rank === 'genus' && name.startsWith('x ')) {
238
+ name = HYBRID_SIGN + name.slice(2);
239
+ }
240
+ if (rank === 'species' && /(^| )x /.test(name)) {
241
+ name = name.replace(/(^| )x /, HYBRID_SIGN);
242
+ }
243
+ // Divide the name into the main scientific name (only the epithet for taxa
244
+ // lower than genus), the authorship information, and optionally remarks
245
+ var nameParts = name.match(NAME_PATTERN);
246
+ if (!nameParts) {
247
+ throw new Error("Taxon \"".concat(name, "\" could not be parsed"));
248
+ }
249
+ // To encode old names with spaces (e.g. "Orsillus pini canariensis Lindberg, 1953")
250
+ // underscores are used, which are replaced here. This is also used for undescribed
251
+ // species (e.g. "Leiobunum species A") and intergeneric hybrids (e.g. "×Festulpia
252
+ // Festuca rubra × Vulpia bromoides")
253
+ if (nameParts[1].includes('_')) {
254
+ nameParts[1] = nameParts[1].replace(/_/g, ' ');
255
+ }
256
+ var _ = nameParts[0], taxon = nameParts[1], _d = nameParts[2], citation = _d === void 0 ? '' : _d, notes = nameParts[3];
257
+ item.scientificNameAuthorship = capitalizeAuthors(citation);
258
+ item.taxonRemarks = notes;
259
+ item.taxonRank = rank;
260
+ // @ts-expect-error TS1501: This regular expression flag is only available when targeting 'es6' or later.
261
+ if (/[^\p{L}0-9\u{00D7}\- ]/u.test(taxon)) {
262
+ throw new Error("Taxon name contains unexpected characters: \"".concat(taxon, "\""));
263
+ }
264
+ // Validate names and recompose binomial and trinomial names
265
+ if (compareRanks('genus', rank) > 0) {
266
+ item.scientificName = capitalize(taxon);
267
+ if (taxon[0].toUpperCase() !== taxon[0]) {
268
+ throw new Error("Taxon name (".concat(rank, ") should be capitalized: \"").concat(taxon, "\""));
269
+ }
270
+ }
271
+ else if (rank === 'genus') {
272
+ item.scientificName = capitalizeGenericName(taxon);
273
+ if (taxon[0].toUpperCase() !== taxon[0] || (taxon[0] === HYBRID_SIGN && taxon[1].toUpperCase() !== taxon[1])) {
274
+ throw new Error("Generic epithet should be capitalized: \"".concat(taxon, "\""));
275
+ }
276
+ }
277
+ else if (compareRanks('group', rank) > 0) {
278
+ item.genericName = parentContext.genus;
279
+ item.infragenericEpithet = parentContext.subgenus;
280
+ item.scientificName = capitalize(taxon);
281
+ if (taxon[0].toUpperCase() !== taxon[0]) {
282
+ throw new Error("Infrageneric epithet should be capitalized: \"".concat(taxon, "\""));
283
+ }
284
+ }
285
+ else if (rank === 'group') {
286
+ item.genericName = parentContext.genus;
287
+ item.infragenericEpithet = parentContext.subgenus;
288
+ var specificEpithet = taxon.toLowerCase().replace(/(-group)?$/, '');
289
+ item.scientificName = "".concat(item.genericName, " ").concat(specificEpithet, "-group");
290
+ if (taxon.toLowerCase() !== taxon) {
291
+ console.log(item, taxon);
292
+ throw new Error("Group name should be lowercase: \"".concat(taxon, "\""));
293
+ }
294
+ }
295
+ else if (rank === 'subgroup') {
296
+ item.genericName = parentContext.genus;
297
+ item.infragenericEpithet = parentContext.subgenus;
298
+ var specificEpithet = taxon.toLowerCase().replace(/(-subgroup)?$/, '');
299
+ item.scientificName = "".concat(item.genericName, " ").concat(specificEpithet, "-subgroup");
300
+ if (taxon.toLowerCase() !== taxon) {
301
+ console.log(item, taxon);
302
+ throw new Error("Subgroup name should be lowercase: \"".concat(taxon, "\""));
303
+ }
304
+ }
305
+ else if (compareRanks('species', rank) > 0) {
306
+ item.genericName = parentContext.genus;
307
+ item.infragenericEpithet = parentContext.subgenus;
308
+ var specificEpithet = taxon.toLowerCase();
309
+ item.scientificName = "".concat(item.genericName, " ").concat(specificEpithet);
310
+ if (specificEpithet !== taxon) {
311
+ console.log(item, taxon);
312
+ throw new Error("Subgroup name should be lowercase: \"".concat(taxon, "\""));
313
+ }
314
+ }
315
+ else if (rank === 'species') {
316
+ item.genericName = parentContext.genus;
317
+ item.infragenericEpithet = parentContext.subgenus;
318
+ if (taxon.toLowerCase() !== taxon && !/^[A-Z][a-z]+ [a-z]+\xD7[A-Z][a-z]+ [a-z]+$/.test(taxon)) {
319
+ console.log(item, taxon);
320
+ throw new Error("Specific epithet should be lowercase: \"".concat(taxon, "\""));
321
+ }
322
+ item.specificEpithet = taxon;
323
+ item.scientificName = "".concat(item.genericName, " ").concat(item.specificEpithet);
324
+ }
325
+ else if (compareRanks('species', rank) < 0) {
326
+ item.genericName = parentContext.genus;
327
+ item.infragenericEpithet = parentContext.subgenus;
328
+ item.specificEpithet = parentContext.specificEpithet;
329
+ item.infraspecificEpithet = taxon.toLowerCase();
330
+ // If possible, names below species should have abbreviations for ranks,
331
+ // like "subsp."
332
+ var nameParts_1 = [
333
+ item.genericName,
334
+ item.specificEpithet,
335
+ item.infraspecificEpithet
336
+ ];
337
+ if (item.taxonRank in RANK_LABELS) {
338
+ nameParts_1.splice(2, 0, RANK_LABELS[item.taxonRank]);
339
+ }
340
+ item.scientificName = nameParts_1.join(' ');
341
+ if (item.infraspecificEpithet !== taxon) {
342
+ console.log(item, taxon);
343
+ throw new Error("Infraspecific epithet should be lowercase: \"".concat(taxon, "\""));
344
+ }
345
+ }
346
+ // Re-add authorship information
347
+ item.scientificNameOnly = item.scientificName;
348
+ if (item.scientificNameAuthorship) {
349
+ item.scientificName += " ".concat(item.scientificNameAuthorship);
350
+ }
351
+ return item;
352
+ }