@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.
- package/.github/workflows/ci.yml +1 -1
- package/CHANGELOG.md +34 -0
- package/lib/bin/generate-linked-data.js +10 -5
- package/lib/bin/process-resources.js +1 -1
- package/lib/bin/validate-catalog.js +8 -10
- package/lib/bin/validate-resources-text.js +1 -1
- package/lib/resources/diff-resource.d.ts +1 -1
- package/lib/resources/diff-resource.js +61 -16
- 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 +9 -5
- package/src/bin/process-resources.ts +1 -1
- package/src/bin/validate-catalog.ts +9 -9
- package/src/bin/validate-resources-text.ts +1 -1
- package/src/module.d.ts +4 -4
- package/src/resources/diff-resource.ts +70 -14
- package/src/resources/parse-name.ts +374 -0
- package/src/resources/parse-text.ts +126 -440
- package/test/resources.js +99 -17
package/.github/workflows/ci.yml
CHANGED
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('
|
|
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:
|
|
640
|
-
resource['dcterms:
|
|
644
|
+
if (!Array.isArray(resource['dcterms:isPartOf'])) {
|
|
645
|
+
resource['dcterms:isPartOf'] = [];
|
|
641
646
|
}
|
|
642
|
-
resource['dcterms:
|
|
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,
|
|
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 (
|
|
106
|
-
result =
|
|
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.
|
|
113
|
-
console.error("".concat(
|
|
114
|
-
console.table(result.value
|
|
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
|
|
97
|
+
console.error(filePath + '\n' + error.message.replace(/^/gm, ' ') + '\n');
|
|
98
98
|
exitStatus = 1;
|
|
99
99
|
}
|
|
100
100
|
_a.label = 3;
|
|
@@ -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 =
|
|
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
|
|
43
|
+
C.setValue(i_1 + 1, j_1 + 1, C.getValue(i_1, j_1) + 1);
|
|
30
44
|
}
|
|
31
45
|
else {
|
|
32
|
-
C
|
|
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
|
|
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
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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,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
|
+
}
|