@larsgw/formica 0.6.7 → 0.7.0
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/CHANGELOG.md +30 -0
- package/lib/bin/process-resources-index.js +23 -11
- package/lib/resources/diff-resource.d.ts +1 -1
- package/lib/resources/diff-resource.js +8 -3
- package/lib/resources/parse-text.js +76 -43
- package/package.json +1 -1
- package/src/bin/process-resources-index.ts +26 -10
- package/src/module.d.ts +2 -1
- package/src/resources/diff-resource.ts +16 -6
- package/src/resources/parse-text.ts +78 -39
- package/test/resources.js +29 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
# [0.7.0](https://github.com/identification-resources/formica/compare/v0.6.8...v0.7.0) (2025-03-14)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **resources:** improve diff regarding indet lines ([2615437](https://github.com/identification-resources/formica/commit/26154374f50d680c5f3e187cd6a1a5eadbac6b5d))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **resources:** change syntax of "indet" line ([d53363e](https://github.com/identification-resources/formica/commit/d53363e65bfa77a2eb247999316621f9ca0bff59))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### BREAKING CHANGES
|
|
15
|
+
|
|
16
|
+
* **resources:** "indet" lines now have to be prefixed with "[indet]"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## [0.6.8](https://github.com/identification-resources/formica/compare/v0.6.7...v0.6.8) (2025-03-13)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* **resources:** add support for intergeneric hybrids without parents ([3cb0c0c](https://github.com/identification-resources/formica/commit/3cb0c0cd60fc5a4ba88add623fbadc3b90d8530c))
|
|
26
|
+
* **resources:** create index of CoL identifiers ([3792903](https://github.com/identification-resources/formica/commit/37929035374bc0c67ed2403e775e06a904bdbd4d))
|
|
27
|
+
* **resources:** support intergeneric hybrids ([0a6696c](https://github.com/identification-resources/formica/commit/0a6696c5fd0603a0366c80c2298b6544636ed0c6))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
1
31
|
## [0.6.7](https://github.com/identification-resources/formica/compare/v0.6.6...v0.6.7) (2025-02-26)
|
|
2
32
|
|
|
3
33
|
|
|
@@ -52,26 +52,27 @@ var fs_1 = require("fs");
|
|
|
52
52
|
var path = require("path");
|
|
53
53
|
var index_1 = require("../index");
|
|
54
54
|
var util_1 = require("./util");
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
function alphabeticSort(a, b) {
|
|
56
|
+
return a > b ? 1 : a < b ? -1 : 0;
|
|
57
|
+
}
|
|
58
|
+
function sortObject(object, sorter) {
|
|
57
59
|
var sorted = {};
|
|
58
|
-
for (var _i = 0, _a = Object.keys(object).sort(util_1.numericSort); _i < _a.length; _i++) {
|
|
60
|
+
for (var _i = 0, _a = Object.keys(object).sort(sorter !== null && sorter !== void 0 ? sorter : util_1.numericSort); _i < _a.length; _i++) {
|
|
59
61
|
var key = _a[_i];
|
|
60
62
|
sorted[key] = object[key];
|
|
61
63
|
}
|
|
62
64
|
return sorted;
|
|
63
65
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
gbifIndex[gbifId] = [];
|
|
66
|
+
function addTaxon(index, id, taxon) {
|
|
67
|
+
if (!(id in index)) {
|
|
68
|
+
index[id] = [];
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
index[id].push(taxon[0]);
|
|
71
|
+
index[id].sort(util_1.numericSort);
|
|
71
72
|
}
|
|
72
73
|
function main(args) {
|
|
73
74
|
return __awaiter(this, void 0, void 0, function () {
|
|
74
|
-
var REPO_ROOT, files, gbifIndex, resourceIndex;
|
|
75
|
+
var REPO_ROOT, files, gbifIndex, colIndex, resourceIndex;
|
|
75
76
|
return __generator(this, function (_a) {
|
|
76
77
|
switch (_a.label) {
|
|
77
78
|
case 0:
|
|
@@ -80,6 +81,7 @@ function main(args) {
|
|
|
80
81
|
case 1:
|
|
81
82
|
files = _a.sent();
|
|
82
83
|
gbifIndex = {};
|
|
84
|
+
colIndex = {};
|
|
83
85
|
resourceIndex = {};
|
|
84
86
|
return [4 /*yield*/, Promise.all(files.map(function (fileName) {
|
|
85
87
|
return __awaiter(this, void 0, void 0, function () {
|
|
@@ -96,7 +98,7 @@ function main(args) {
|
|
|
96
98
|
file = _a.sent();
|
|
97
99
|
return [2 /*return*/, Promise.all(index_1.resources.parseTextFileHeader(file).map(function (resource, index) {
|
|
98
100
|
return __awaiter(this, void 0, void 0, function () {
|
|
99
|
-
var amendedResource, dwcFile, _a, header, dwc, _b, _c, gbifColumn, gbifAcceptedColumn, _i, dwc_1, taxon, gbifId;
|
|
101
|
+
var amendedResource, dwcFile, _a, header, dwc, _b, _c, gbifColumn, gbifAcceptedColumn, colColumn, colAcceptedColumn, _i, dwc_1, taxon, gbifId, colId;
|
|
100
102
|
return __generator(this, function (_d) {
|
|
101
103
|
switch (_d.label) {
|
|
102
104
|
case 0:
|
|
@@ -111,6 +113,8 @@ function main(args) {
|
|
|
111
113
|
_a = _c.apply(_b, [_d.sent()]), header = _a[0], dwc = _a.slice(1);
|
|
112
114
|
gbifColumn = header.indexOf('gbifTaxonID');
|
|
113
115
|
gbifAcceptedColumn = header.indexOf('gbifAcceptedTaxonID');
|
|
116
|
+
colColumn = header.indexOf('colTaxonID');
|
|
117
|
+
colAcceptedColumn = header.indexOf('colAcceptedTaxonID');
|
|
114
118
|
for (_i = 0, dwc_1 = dwc; _i < dwc_1.length; _i++) {
|
|
115
119
|
taxon = dwc_1[_i];
|
|
116
120
|
gbifId = taxon[gbifColumn];
|
|
@@ -120,6 +124,13 @@ function main(args) {
|
|
|
120
124
|
addTaxon(gbifIndex, taxon[gbifAcceptedColumn], taxon);
|
|
121
125
|
}
|
|
122
126
|
}
|
|
127
|
+
colId = taxon[colColumn];
|
|
128
|
+
if (colId) {
|
|
129
|
+
addTaxon(colIndex, colId, taxon);
|
|
130
|
+
if (taxon[colAcceptedColumn] !== taxon[colColumn]) {
|
|
131
|
+
addTaxon(colIndex, taxon[colAcceptedColumn], taxon);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
123
134
|
amendedResource.taxonCount += 1;
|
|
124
135
|
}
|
|
125
136
|
resourceIndex[amendedResource.id] = amendedResource;
|
|
@@ -136,6 +147,7 @@ function main(args) {
|
|
|
136
147
|
_a.sent();
|
|
137
148
|
return [4 /*yield*/, Promise.all([
|
|
138
149
|
fs_1.promises.writeFile(path.join(REPO_ROOT, 'gbif.index.json'), JSON.stringify(sortObject(gbifIndex), null, 2)),
|
|
150
|
+
fs_1.promises.writeFile(path.join(REPO_ROOT, 'col.index.json'), JSON.stringify(sortObject(colIndex, alphabeticSort), null, 2)),
|
|
139
151
|
fs_1.promises.writeFile(path.join(REPO_ROOT, 'index.json'), JSON.stringify(sortObject(resourceIndex), null, 2))
|
|
140
152
|
])];
|
|
141
153
|
case 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):
|
|
7
|
+
export declare function createDiff(a: string, b: string, tokenize?: ResourceDiffTokenizer): ResourceDiff;
|
|
@@ -68,6 +68,7 @@ function gitTokenize(text) {
|
|
|
68
68
|
return text.match(/\S+|\n|[\r\t\f\v \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/g);
|
|
69
69
|
}
|
|
70
70
|
function createDiff(a, b, tokenize) {
|
|
71
|
+
var _a, _b;
|
|
71
72
|
if (tokenize === void 0) { tokenize = gitTokenize; }
|
|
72
73
|
var X = tokenize(a.trimEnd());
|
|
73
74
|
var Y = tokenize(b.trimEnd());
|
|
@@ -106,7 +107,7 @@ function createDiff(a, b, tokenize) {
|
|
|
106
107
|
// Start of line
|
|
107
108
|
if (line === null) {
|
|
108
109
|
deletedNewlines = 0;
|
|
109
|
-
line = {
|
|
110
|
+
line = { type: change.type };
|
|
110
111
|
}
|
|
111
112
|
// End of line (could be same token)
|
|
112
113
|
if (change.text === '\n') {
|
|
@@ -132,7 +133,8 @@ function createDiff(a, b, tokenize) {
|
|
|
132
133
|
// Add placeholders for deleted lines
|
|
133
134
|
while (deletedNewlines--) {
|
|
134
135
|
lines.push({
|
|
135
|
-
text:
|
|
136
|
+
text: undefined,
|
|
137
|
+
original: '',
|
|
136
138
|
type: ResourceDiffType.Deleted
|
|
137
139
|
});
|
|
138
140
|
}
|
|
@@ -144,7 +146,10 @@ function createDiff(a, b, tokenize) {
|
|
|
144
146
|
line.type = ResourceDiffType.Modified;
|
|
145
147
|
}
|
|
146
148
|
if (change.type !== ResourceDiffType.Deleted) {
|
|
147
|
-
line.text
|
|
149
|
+
line.text = ((_a = line.text) !== null && _a !== void 0 ? _a : '') + change.text;
|
|
150
|
+
}
|
|
151
|
+
if (change.type !== ResourceDiffType.Added) {
|
|
152
|
+
line.original = ((_b = line.original) !== null && _b !== void 0 ? _b : '') + change.text;
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
return lines;
|
|
@@ -74,13 +74,6 @@ var TAXONOMIC_STATUS = {
|
|
|
74
74
|
'+': 'heterotypic synonym',
|
|
75
75
|
'=': 'synonym'
|
|
76
76
|
};
|
|
77
|
-
var INDET_SUFFIXES = new Set([
|
|
78
|
-
'sp.',
|
|
79
|
-
'spec.',
|
|
80
|
-
'indet.',
|
|
81
|
-
'sp. indet.',
|
|
82
|
-
'spec. indet.'
|
|
83
|
-
]);
|
|
84
77
|
var RANK_LABELS = {
|
|
85
78
|
'subspecies': 'subsp.',
|
|
86
79
|
'variety': 'var.',
|
|
@@ -98,6 +91,7 @@ var RANK_LABELS_REVERSE = {
|
|
|
98
91
|
'ssp': 'subspecies',
|
|
99
92
|
'subsp': 'subspecies'
|
|
100
93
|
};
|
|
94
|
+
var HYBRID_SIGN = '\u00D7';
|
|
101
95
|
/**
|
|
102
96
|
* 1. Any number of
|
|
103
97
|
* - capitalized words
|
|
@@ -133,22 +127,30 @@ var NAME_PATTERN = new RegExp('^' +
|
|
|
133
127
|
/**
|
|
134
128
|
* Structure
|
|
135
129
|
* $1 genus+subgenus (+ trailing space): (?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?
|
|
136
|
-
* $1.1 genus: ([A-Z]\S+)
|
|
130
|
+
* $1.1 genus: ((?:x )?[A-Z]\S+)
|
|
137
131
|
* $1.2 subgenus: (?:\(([A-Z]\S+?)\) )?
|
|
138
|
-
* $2 species: (
|
|
132
|
+
* $2 species: (x [a-z]+|[a-z][^\s.]+(?: x [a-z]+)?|[A-Z][a-z]+_[a-z]+ x [A-Z][a-z]+_[a-z]+)
|
|
133
|
+
* $2a: x [a-z]+
|
|
134
|
+
* $2b hybrid: [a-z][^\s.]+(?: x [a-z]+)?
|
|
135
|
+
* $2c intergeneric hybrid: [A-Z][a-z]+_[a-z]+ x [A-Z][a-z]+_[a-z]+
|
|
139
136
|
*/
|
|
140
|
-
var BINAME_PATTERN = /^(?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?(
|
|
137
|
+
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]+)(?= |$)/;
|
|
141
138
|
function compareRanks(a, b) {
|
|
142
139
|
return RANKS.indexOf(a) - RANKS.indexOf(b);
|
|
143
140
|
}
|
|
144
141
|
function capitalize(name) {
|
|
145
142
|
return name[0].toUpperCase() + name.slice(1).toLowerCase();
|
|
146
143
|
}
|
|
144
|
+
function capitalizeGenericName(name) {
|
|
145
|
+
if (name[0] === HYBRID_SIGN) {
|
|
146
|
+
return HYBRID_SIGN + capitalize(name.slice(1));
|
|
147
|
+
}
|
|
148
|
+
return capitalize(name);
|
|
149
|
+
}
|
|
147
150
|
function isUpperCase(name) {
|
|
148
151
|
return name === name.toUpperCase();
|
|
149
152
|
}
|
|
150
153
|
function getSynonymRank(name, rank) {
|
|
151
|
-
var BINAME_PATTERN = /^([A-Z]\S+ (\([A-Z]\S+\) )?)?(x )?[a-z0-9-]+(?= |$)/;
|
|
152
154
|
var rest = name.replace(BINAME_PATTERN, '');
|
|
153
155
|
var rankPrefix = rest.match(/^(?: |^)(st|r|ab|f|var|ssp|subsp)\. /);
|
|
154
156
|
if (rankPrefix) {
|
|
@@ -188,7 +190,7 @@ function parseName(name, rank, parent) {
|
|
|
188
190
|
name = name.replace(/^\[(_|\d+)\] /, '');
|
|
189
191
|
}
|
|
190
192
|
// Set verbatim identification after subsequent syntax is removed.
|
|
191
|
-
item.verbatimIdentification = name;
|
|
193
|
+
item.verbatimIdentification = name.replace(/(?<=^| )x(?=$| )/g, HYBRID_SIGN).replace(/_/g, ' ');
|
|
192
194
|
// Parent context is used for parsing and formatting binomial names.
|
|
193
195
|
// For formatting, it needs to match external databases (i.e. be correct).
|
|
194
196
|
// For parsing, it needs to match the current file. If relevant parents
|
|
@@ -210,7 +212,8 @@ function parseName(name, rank, parent) {
|
|
|
210
212
|
if (isSynonym || !parentContext.genus || (compareRanks('species', rank) < 0 && !parentContext.specificEpithet)) {
|
|
211
213
|
var _a = name.match(BINAME_PATTERN) || [], genus = _a[1], subgenus = _a[2], species = _a[3];
|
|
212
214
|
if (genus) {
|
|
213
|
-
parentContext.
|
|
215
|
+
parentContext.incorrect.genus = genus;
|
|
216
|
+
parentContext.genus = capitalizeGenericName(genus.replace(/(^| )x /, HYBRID_SIGN));
|
|
214
217
|
}
|
|
215
218
|
if (subgenus) {
|
|
216
219
|
parentContext.subgenus = capitalize(subgenus);
|
|
@@ -220,8 +223,9 @@ function parseName(name, rank, parent) {
|
|
|
220
223
|
// from the parent context.
|
|
221
224
|
delete parentContext.subgenus;
|
|
222
225
|
}
|
|
223
|
-
if (species) {
|
|
224
|
-
parentContext.
|
|
226
|
+
if (species && compareRanks('species', rank) < 0) {
|
|
227
|
+
parentContext.incorrect.specificEpithet = species;
|
|
228
|
+
parentContext.specificEpithet = species.replace(/(^| )x /, HYBRID_SIGN);
|
|
225
229
|
}
|
|
226
230
|
}
|
|
227
231
|
// In taxa of group, species or lower, the name should just contain the
|
|
@@ -247,8 +251,11 @@ function parseName(name, rank, parent) {
|
|
|
247
251
|
}
|
|
248
252
|
}
|
|
249
253
|
// Hybrids
|
|
254
|
+
if (rank === 'genus' && name.startsWith('x ')) {
|
|
255
|
+
name = HYBRID_SIGN + name.slice(2);
|
|
256
|
+
}
|
|
250
257
|
if (rank === 'species' && /(^| )x /.test(name)) {
|
|
251
|
-
name = name.replace(/(^| )x /,
|
|
258
|
+
name = name.replace(/(^| )x /, HYBRID_SIGN);
|
|
252
259
|
}
|
|
253
260
|
// Divide the name into the main scientific name (only the epithet for taxa
|
|
254
261
|
// lower than genus), the authorship information, and optionally remarks
|
|
@@ -257,7 +264,9 @@ function parseName(name, rank, parent) {
|
|
|
257
264
|
throw new Error("Taxon \"".concat(name, "\" could not be parsed"));
|
|
258
265
|
}
|
|
259
266
|
// To encode old names with spaces (e.g. "Orsillus pini canariensis Lindberg, 1953")
|
|
260
|
-
// underscores are used, which are replaced here.
|
|
267
|
+
// underscores are used, which are replaced here. This is also used for undescribed
|
|
268
|
+
// species (e.g. "Leiobunum species A") and intergeneric hybrids (e.g. "×Festulpia
|
|
269
|
+
// Festuca rubra × Vulpia bromoides")
|
|
261
270
|
if (nameParts[1].includes('_')) {
|
|
262
271
|
nameParts[1] = nameParts[1].replace(/_/g, ' ');
|
|
263
272
|
}
|
|
@@ -265,13 +274,19 @@ function parseName(name, rank, parent) {
|
|
|
265
274
|
item.scientificNameAuthorship = capitalizeAuthors(citation);
|
|
266
275
|
item.taxonRemarks = notes;
|
|
267
276
|
item.taxonRank = rank;
|
|
268
|
-
if (/[^\p{L}0-9\u{00D7}\- ]/u.test(taxon)
|
|
277
|
+
if (/[^\p{L}0-9\u{00D7}\- ]/u.test(taxon)) {
|
|
269
278
|
throw new Error("Taxon name contains unexpected characters: \"".concat(taxon, "\""));
|
|
270
279
|
}
|
|
271
280
|
// Validate names and recompose binomial and trinomial names
|
|
272
|
-
if (
|
|
281
|
+
if (rank === 'genus') {
|
|
282
|
+
item.scientificName = capitalizeGenericName(taxon);
|
|
283
|
+
if (taxon[0].toUpperCase() !== taxon[0] || (taxon[0] === HYBRID_SIGN && taxon[1].toUpperCase() !== taxon[1])) {
|
|
284
|
+
throw new Error("Generic epithet should be capitalized: \"".concat(taxon, "\""));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else if (compareRanks('group', rank) > 0) {
|
|
273
288
|
item.scientificName = capitalize(taxon);
|
|
274
|
-
if (
|
|
289
|
+
if (taxon[0].toUpperCase() !== taxon[0]) {
|
|
275
290
|
throw new Error("Taxon name (".concat(rank, ") should be capitalized: \"").concat(taxon, "\""));
|
|
276
291
|
}
|
|
277
292
|
}
|
|
@@ -298,12 +313,12 @@ function parseName(name, rank, parent) {
|
|
|
298
313
|
else if (rank === 'species') {
|
|
299
314
|
item.genericName = parentContext.genus;
|
|
300
315
|
item.infragenericEpithet = parentContext.subgenus;
|
|
301
|
-
|
|
302
|
-
item.scientificName = "".concat(item.genericName, " ").concat(item.specificEpithet);
|
|
303
|
-
if (item.specificEpithet !== taxon) {
|
|
316
|
+
if (taxon.toLowerCase() !== taxon && !/^[A-Z][a-z]+ [a-z]+\xD7[A-Z][a-z]+ [a-z]+$/.test(taxon)) {
|
|
304
317
|
console.log(item, taxon);
|
|
305
318
|
throw new Error("Specific epithet should be lowercase: \"".concat(taxon, "\""));
|
|
306
319
|
}
|
|
320
|
+
item.specificEpithet = taxon;
|
|
321
|
+
item.scientificName = "".concat(item.genericName, " ").concat(item.specificEpithet);
|
|
307
322
|
}
|
|
308
323
|
else if (compareRanks('species', rank) < 0) {
|
|
309
324
|
item.genericName = parentContext.genus;
|
|
@@ -435,6 +450,16 @@ function parseResource(resource) {
|
|
|
435
450
|
var content = rest.join('');
|
|
436
451
|
return [config, content];
|
|
437
452
|
}
|
|
453
|
+
function getIndentation(line) {
|
|
454
|
+
return line.match(/^ */)[0].length;
|
|
455
|
+
}
|
|
456
|
+
function isIndetLine(line, indent) {
|
|
457
|
+
if (indent === undefined) {
|
|
458
|
+
indent = getIndentation(line);
|
|
459
|
+
}
|
|
460
|
+
line = line.slice(indent);
|
|
461
|
+
return line.startsWith('[indet]');
|
|
462
|
+
}
|
|
438
463
|
function parseResourceContent(content, resource, oldIds) {
|
|
439
464
|
var idBase = "".concat(resource.id, ":");
|
|
440
465
|
var data = resource.taxa;
|
|
@@ -443,20 +468,19 @@ function parseResourceContent(content, resource, oldIds) {
|
|
|
443
468
|
var groupIndent = 0;
|
|
444
469
|
var previousId = '';
|
|
445
470
|
var newIdOffset = Math.max.apply(Math, oldIds);
|
|
446
|
-
var
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
return "continue";
|
|
471
|
+
for (var _i = 0, content_1 = content; _i < content_1.length; _i++) {
|
|
472
|
+
var line = content_1[_i];
|
|
473
|
+
if (line.type === diff_resource_1.ResourceDiffType.Deleted) {
|
|
474
|
+
// Increase id counter for removed line unless it was an "indet line"
|
|
475
|
+
if (!isIndetLine(line.original)) {
|
|
476
|
+
id++;
|
|
477
|
+
}
|
|
478
|
+
continue;
|
|
455
479
|
}
|
|
456
|
-
var lineIndent = line.
|
|
480
|
+
var lineIndent = getIndentation(line.text);
|
|
457
481
|
if (lineIndent > groupIndent) {
|
|
458
482
|
// Do not count synonyms as parents (unless this is correcting a typo in the synonym)
|
|
459
|
-
if (data[previousId] && data[previousId].taxonomicStatus === 'accepted' || /^( {2})+> /.test(line)) {
|
|
483
|
+
if (data[previousId] && data[previousId].taxonomicStatus === 'accepted' || /^( {2})+> /.test(line.text)) {
|
|
460
484
|
parents.push(previousId);
|
|
461
485
|
}
|
|
462
486
|
else {
|
|
@@ -478,17 +502,26 @@ function parseResourceContent(content, resource, oldIds) {
|
|
|
478
502
|
parents = parents.slice(0, lineIndent / 2);
|
|
479
503
|
groupIndent = lineIndent;
|
|
480
504
|
}
|
|
505
|
+
// Do not process "indet" lines further, as they only serve to indicate
|
|
506
|
+
// that subtaxa are explicitely omitted
|
|
507
|
+
if (isIndetLine(line.text, lineIndent)) {
|
|
508
|
+
// If the line was previously not and ndet line, increase the id counter
|
|
509
|
+
if (line.type === diff_resource_1.ResourceDiffType.Modified && !isIndetLine(line.original)) {
|
|
510
|
+
id++;
|
|
511
|
+
}
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
481
514
|
var parentId = parents.reduce(function (grandparent, parent) { return parent || grandparent; }, null);
|
|
482
515
|
var parent_1 = parentId === null ? {} : data[parentId];
|
|
483
|
-
var name_1 = line.slice(groupIndent);
|
|
516
|
+
var name_1 = line.text.slice(groupIndent);
|
|
484
517
|
var rank = resource.metadata.levels[groupIndent / 2];
|
|
485
518
|
var item = parseName(name_1, rank, parent_1);
|
|
486
519
|
var isSynonym = item.taxonomicStatus !== 'accepted';
|
|
487
520
|
// Add higher classification info
|
|
488
521
|
var itemAsObject = item;
|
|
489
522
|
var parentAsObject = parent_1;
|
|
490
|
-
for (var
|
|
491
|
-
var rank_1 = DWC_RANKS_1[
|
|
523
|
+
for (var _a = 0, DWC_RANKS_1 = DWC_RANKS; _a < DWC_RANKS_1.length; _a++) {
|
|
524
|
+
var rank_1 = DWC_RANKS_1[_a];
|
|
492
525
|
itemAsObject[rank_1] = undefined;
|
|
493
526
|
if (parentAsObject[rank_1]) {
|
|
494
527
|
itemAsObject[rank_1] = parentAsObject[rank_1];
|
|
@@ -520,10 +553,14 @@ function parseResourceContent(content, resource, oldIds) {
|
|
|
520
553
|
parentAsObject[key] = itemAsObject[key];
|
|
521
554
|
}
|
|
522
555
|
}
|
|
523
|
-
|
|
556
|
+
continue;
|
|
524
557
|
}
|
|
525
558
|
// Set identifiers
|
|
526
|
-
if (type === diff_resource_1.ResourceDiffType.Added) {
|
|
559
|
+
if (line.type === diff_resource_1.ResourceDiffType.Added) {
|
|
560
|
+
newIdOffset++;
|
|
561
|
+
item.scientificNameID = idBase + newIdOffset.toString();
|
|
562
|
+
}
|
|
563
|
+
else if (line.type === diff_resource_1.ResourceDiffType.Modified && isIndetLine(line.original)) {
|
|
527
564
|
newIdOffset++;
|
|
528
565
|
item.scientificNameID = idBase + newIdOffset.toString();
|
|
529
566
|
}
|
|
@@ -538,10 +575,6 @@ function parseResourceContent(content, resource, oldIds) {
|
|
|
538
575
|
item.acceptedNameUsage = isSynonym ? parent_1.scientificName : undefined;
|
|
539
576
|
item.collectionCode = idBase.slice(0, -1);
|
|
540
577
|
data[item.scientificNameID] = item;
|
|
541
|
-
};
|
|
542
|
-
for (var _i = 0, content_1 = content; _i < content_1.length; _i++) {
|
|
543
|
-
var _a = content_1[_i], line = _a.text, type = _a.type;
|
|
544
|
-
_loop_1(line, type);
|
|
545
578
|
}
|
|
546
579
|
return resource;
|
|
547
580
|
}
|
package/package.json
CHANGED
|
@@ -11,22 +11,25 @@ interface AmendedResourceMetadata extends ResourceMetadata {
|
|
|
11
11
|
taxonCount: number
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
type SortObjectCallback = (a: string, b: string) => number
|
|
15
|
+
function alphabeticSort (a: string, b: string): number {
|
|
16
|
+
return a > b ? 1 : a < b ? -1 : 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sortObject (object: Record<string, unknown>, sorter?: SortObjectCallback): Record<string, unknown> {
|
|
20
|
+
const sorted: Record<string, unknown> = {}
|
|
21
|
+
for (const key of Object.keys(object).sort(sorter ?? numericSort)) {
|
|
18
22
|
sorted[key] = object[key]
|
|
19
23
|
}
|
|
20
24
|
return sorted
|
|
21
25
|
}
|
|
22
|
-
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
23
26
|
|
|
24
|
-
function addTaxon (
|
|
25
|
-
if (!(
|
|
26
|
-
|
|
27
|
+
function addTaxon (index: Record<string, TaxonId[]>, id: string, taxon: string[]) {
|
|
28
|
+
if (!(id in index)) {
|
|
29
|
+
index[id] = []
|
|
27
30
|
}
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
index[id].push(taxon[0])
|
|
32
|
+
index[id].sort(numericSort)
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
async function main (args: string[]): Promise<void> {
|
|
@@ -35,6 +38,7 @@ async function main (args: string[]): Promise<void> {
|
|
|
35
38
|
const files = await fs.readdir(path.join(REPO_ROOT, 'txt'))
|
|
36
39
|
|
|
37
40
|
const gbifIndex: Record<string, TaxonId[]> = {}
|
|
41
|
+
const colIndex: Record<string, TaxonId[]> = {}
|
|
38
42
|
const resourceIndex: Record<TaxonId, AmendedResourceMetadata> = {}
|
|
39
43
|
|
|
40
44
|
await Promise.all(files.map(async function (fileName) {
|
|
@@ -57,6 +61,8 @@ async function main (args: string[]): Promise<void> {
|
|
|
57
61
|
const [header, ...dwc] = csv.parseCsv(await fs.readFile(dwcFile, 'utf-8'))
|
|
58
62
|
const gbifColumn = header.indexOf('gbifTaxonID')
|
|
59
63
|
const gbifAcceptedColumn = header.indexOf('gbifAcceptedTaxonID')
|
|
64
|
+
const colColumn = header.indexOf('colTaxonID')
|
|
65
|
+
const colAcceptedColumn = header.indexOf('colAcceptedTaxonID')
|
|
60
66
|
for (const taxon of dwc) {
|
|
61
67
|
const gbifId = taxon[gbifColumn]
|
|
62
68
|
if (gbifId) {
|
|
@@ -65,6 +71,15 @@ async function main (args: string[]): Promise<void> {
|
|
|
65
71
|
addTaxon(gbifIndex, taxon[gbifAcceptedColumn], taxon)
|
|
66
72
|
}
|
|
67
73
|
}
|
|
74
|
+
|
|
75
|
+
const colId = taxon[colColumn]
|
|
76
|
+
if (colId) {
|
|
77
|
+
addTaxon(colIndex, colId, taxon)
|
|
78
|
+
if (taxon[colAcceptedColumn] !== taxon[colColumn]) {
|
|
79
|
+
addTaxon(colIndex, taxon[colAcceptedColumn], taxon)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
amendedResource.taxonCount += 1
|
|
69
84
|
}
|
|
70
85
|
|
|
@@ -74,6 +89,7 @@ async function main (args: string[]): Promise<void> {
|
|
|
74
89
|
|
|
75
90
|
await Promise.all([
|
|
76
91
|
fs.writeFile(path.join(REPO_ROOT, 'gbif.index.json'), JSON.stringify(sortObject(gbifIndex), null, 2)),
|
|
92
|
+
fs.writeFile(path.join(REPO_ROOT, 'col.index.json'), JSON.stringify(sortObject(colIndex, alphabeticSort), null, 2)),
|
|
77
93
|
fs.writeFile(path.join(REPO_ROOT, 'index.json'), JSON.stringify(sortObject(resourceIndex), null, 2))
|
|
78
94
|
])
|
|
79
95
|
}
|
package/src/module.d.ts
CHANGED
|
@@ -5,7 +5,12 @@ export enum ResourceDiffType {
|
|
|
5
5
|
Unchanged = '='
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
interface DiffPart {
|
|
9
|
+
text: string,
|
|
10
|
+
type: ResourceDiffType
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function LCS (X: string[], Y: string[]): DiffPart[] {
|
|
9
14
|
const m = X.length
|
|
10
15
|
const n = Y.length
|
|
11
16
|
|
|
@@ -57,7 +62,7 @@ function gitTokenize (text: string): string[] {
|
|
|
57
62
|
return text.match(/\S+|\n|[\r\t\f\v \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/g) as string[]
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
export function createDiff (a: string, b: string, tokenize: ResourceDiffTokenizer = gitTokenize):
|
|
65
|
+
export function createDiff (a: string, b: string, tokenize: ResourceDiffTokenizer = gitTokenize): ResourceDiff {
|
|
61
66
|
const X = tokenize(a.trimEnd())
|
|
62
67
|
const Y = tokenize(b.trimEnd())
|
|
63
68
|
|
|
@@ -94,7 +99,7 @@ export function createDiff (a: string, b: string, tokenize: ResourceDiffTokenize
|
|
|
94
99
|
|
|
95
100
|
// Convert word diff to line diff
|
|
96
101
|
const lines: ResourceDiff = []
|
|
97
|
-
let line = null
|
|
102
|
+
let line: ResourceDiffPart|null = null
|
|
98
103
|
let deletedNewlines = 0
|
|
99
104
|
let nextLineNew = false
|
|
100
105
|
|
|
@@ -102,7 +107,7 @@ export function createDiff (a: string, b: string, tokenize: ResourceDiffTokenize
|
|
|
102
107
|
// Start of line
|
|
103
108
|
if (line === null) {
|
|
104
109
|
deletedNewlines = 0
|
|
105
|
-
line = {
|
|
110
|
+
line = { type: change.type }
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
// End of line (could be same token)
|
|
@@ -132,7 +137,8 @@ export function createDiff (a: string, b: string, tokenize: ResourceDiffTokenize
|
|
|
132
137
|
// Add placeholders for deleted lines
|
|
133
138
|
while (deletedNewlines--) {
|
|
134
139
|
lines.push({
|
|
135
|
-
text:
|
|
140
|
+
text: undefined,
|
|
141
|
+
original: '',
|
|
136
142
|
type: ResourceDiffType.Deleted
|
|
137
143
|
})
|
|
138
144
|
}
|
|
@@ -147,7 +153,11 @@ export function createDiff (a: string, b: string, tokenize: ResourceDiffTokenize
|
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
if (change.type !== ResourceDiffType.Deleted) {
|
|
150
|
-
line.text
|
|
156
|
+
line.text = (line.text ?? '') + change.text
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (change.type !== ResourceDiffType.Added) {
|
|
160
|
+
line.original = (line.original ?? '') + change.text
|
|
151
161
|
}
|
|
152
162
|
}
|
|
153
163
|
|
|
@@ -66,14 +66,6 @@ const TAXONOMIC_STATUS: Record<string, TaxonStatus> = {
|
|
|
66
66
|
'=': 'synonym'
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const INDET_SUFFIXES = new Set([
|
|
70
|
-
'sp.',
|
|
71
|
-
'spec.',
|
|
72
|
-
'indet.',
|
|
73
|
-
'sp. indet.',
|
|
74
|
-
'spec. indet.'
|
|
75
|
-
])
|
|
76
|
-
|
|
77
69
|
const RANK_LABELS: Record<Rank, string> = {
|
|
78
70
|
'subspecies': 'subsp.',
|
|
79
71
|
'variety': 'var.',
|
|
@@ -93,6 +85,8 @@ const RANK_LABELS_REVERSE: Record<string, Rank> = {
|
|
|
93
85
|
'subsp': 'subspecies'
|
|
94
86
|
}
|
|
95
87
|
|
|
88
|
+
const HYBRID_SIGN = '\u00D7'
|
|
89
|
+
|
|
96
90
|
/**
|
|
97
91
|
* 1. Any number of
|
|
98
92
|
* - capitalized words
|
|
@@ -133,11 +127,14 @@ const NAME_PATTERN = new RegExp(
|
|
|
133
127
|
/**
|
|
134
128
|
* Structure
|
|
135
129
|
* $1 genus+subgenus (+ trailing space): (?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?
|
|
136
|
-
* $1.1 genus: ([A-Z]\S+)
|
|
130
|
+
* $1.1 genus: ((?:x )?[A-Z]\S+)
|
|
137
131
|
* $1.2 subgenus: (?:\(([A-Z]\S+?)\) )?
|
|
138
|
-
* $2 species: (
|
|
132
|
+
* $2 species: (x [a-z]+|[a-z][^\s.]+(?: x [a-z]+)?|[A-Z][a-z]+_[a-z]+ x [A-Z][a-z]+_[a-z]+)
|
|
133
|
+
* $2a: x [a-z]+
|
|
134
|
+
* $2b hybrid: [a-z][^\s.]+(?: x [a-z]+)?
|
|
135
|
+
* $2c intergeneric hybrid: [A-Z][a-z]+_[a-z]+ x [A-Z][a-z]+_[a-z]+
|
|
139
136
|
*/
|
|
140
|
-
const BINAME_PATTERN = /^(?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?(
|
|
137
|
+
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]+)(?= |$)/
|
|
141
138
|
|
|
142
139
|
function compareRanks (a: Rank, b: Rank): number {
|
|
143
140
|
return RANKS.indexOf(a) - RANKS.indexOf(b)
|
|
@@ -147,12 +144,19 @@ function capitalize (name: string): string {
|
|
|
147
144
|
return name[0].toUpperCase() + name.slice(1).toLowerCase()
|
|
148
145
|
}
|
|
149
146
|
|
|
147
|
+
function capitalizeGenericName (name: string): string {
|
|
148
|
+
if (name[0] === HYBRID_SIGN) {
|
|
149
|
+
return HYBRID_SIGN + capitalize(name.slice(1))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return capitalize(name)
|
|
153
|
+
}
|
|
154
|
+
|
|
150
155
|
function isUpperCase (name: string): boolean {
|
|
151
156
|
return name === name.toUpperCase()
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
function getSynonymRank (name: string, rank: Rank): Rank {
|
|
155
|
-
const BINAME_PATTERN = /^([A-Z]\S+ (\([A-Z]\S+\) )?)?(x )?[a-z0-9-]+(?= |$)/
|
|
156
160
|
const rest = name.replace(BINAME_PATTERN, '')
|
|
157
161
|
const rankPrefix = rest.match(/^(?: |^)(st|r|ab|f|var|ssp|subsp)\. /)
|
|
158
162
|
if (rankPrefix) {
|
|
@@ -194,7 +198,7 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
194
198
|
}
|
|
195
199
|
|
|
196
200
|
// Set verbatim identification after subsequent syntax is removed.
|
|
197
|
-
item.verbatimIdentification = name
|
|
201
|
+
item.verbatimIdentification = name.replace(/(?<=^| )x(?=$| )/g, HYBRID_SIGN).replace(/_/g, ' ')
|
|
198
202
|
|
|
199
203
|
// Parent context is used for parsing and formatting binomial names.
|
|
200
204
|
// For formatting, it needs to match external databases (i.e. be correct).
|
|
@@ -218,7 +222,8 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
218
222
|
if (isSynonym || !parentContext.genus || (compareRanks('species', rank) < 0 && !parentContext.specificEpithet)) {
|
|
219
223
|
const [, genus, subgenus, species] = name.match(BINAME_PATTERN) || []
|
|
220
224
|
if (genus) {
|
|
221
|
-
parentContext.
|
|
225
|
+
parentContext.incorrect.genus = genus
|
|
226
|
+
parentContext.genus = capitalizeGenericName(genus.replace(/(^| )x /, HYBRID_SIGN))
|
|
222
227
|
}
|
|
223
228
|
if (subgenus) {
|
|
224
229
|
parentContext.subgenus = capitalize(subgenus)
|
|
@@ -227,8 +232,9 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
227
232
|
// from the parent context.
|
|
228
233
|
delete parentContext.subgenus
|
|
229
234
|
}
|
|
230
|
-
if (species) {
|
|
231
|
-
parentContext.
|
|
235
|
+
if (species && compareRanks('species', rank) < 0) {
|
|
236
|
+
parentContext.incorrect.specificEpithet = species
|
|
237
|
+
parentContext.specificEpithet = species.replace(/(^| )x /, HYBRID_SIGN)
|
|
232
238
|
}
|
|
233
239
|
}
|
|
234
240
|
|
|
@@ -259,8 +265,12 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
259
265
|
}
|
|
260
266
|
|
|
261
267
|
// Hybrids
|
|
268
|
+
if (rank === 'genus' && name.startsWith('x ')) {
|
|
269
|
+
name = HYBRID_SIGN + name.slice(2)
|
|
270
|
+
}
|
|
271
|
+
|
|
262
272
|
if (rank === 'species' && /(^| )x /.test(name)) {
|
|
263
|
-
name = name.replace(/(^| )x /,
|
|
273
|
+
name = name.replace(/(^| )x /, HYBRID_SIGN)
|
|
264
274
|
}
|
|
265
275
|
|
|
266
276
|
// Divide the name into the main scientific name (only the epithet for taxa
|
|
@@ -271,7 +281,9 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
271
281
|
}
|
|
272
282
|
|
|
273
283
|
// To encode old names with spaces (e.g. "Orsillus pini canariensis Lindberg, 1953")
|
|
274
|
-
// underscores are used, which are replaced here.
|
|
284
|
+
// underscores are used, which are replaced here. This is also used for undescribed
|
|
285
|
+
// species (e.g. "Leiobunum species A") and intergeneric hybrids (e.g. "×Festulpia
|
|
286
|
+
// Festuca rubra × Vulpia bromoides")
|
|
275
287
|
if (nameParts[1].includes('_')) {
|
|
276
288
|
nameParts[1] = nameParts[1].replace(/_/g, ' ')
|
|
277
289
|
}
|
|
@@ -281,14 +293,19 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
281
293
|
item.taxonRemarks = notes
|
|
282
294
|
item.taxonRank = rank
|
|
283
295
|
|
|
284
|
-
if (/[^\p{L}0-9\u{00D7}\- ]/u.test(taxon)
|
|
296
|
+
if (/[^\p{L}0-9\u{00D7}\- ]/u.test(taxon)) {
|
|
285
297
|
throw new Error(`Taxon name contains unexpected characters: "${taxon}"`)
|
|
286
298
|
}
|
|
287
299
|
|
|
288
300
|
// Validate names and recompose binomial and trinomial names
|
|
289
|
-
if (
|
|
301
|
+
if (rank === 'genus') {
|
|
302
|
+
item.scientificName = capitalizeGenericName(taxon)
|
|
303
|
+
if (taxon[0].toUpperCase() !== taxon[0] || (taxon[0] === HYBRID_SIGN && taxon[1].toUpperCase() !== taxon[1])) {
|
|
304
|
+
throw new Error(`Generic epithet should be capitalized: "${taxon}"`)
|
|
305
|
+
}
|
|
306
|
+
} else if (compareRanks('group', rank) > 0) {
|
|
290
307
|
item.scientificName = capitalize(taxon)
|
|
291
|
-
if (
|
|
308
|
+
if (taxon[0].toUpperCase() !== taxon[0]) {
|
|
292
309
|
throw new Error(`Taxon name (${rank}) should be capitalized: "${taxon}"`)
|
|
293
310
|
}
|
|
294
311
|
} else if (rank === 'group') {
|
|
@@ -312,12 +329,12 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
312
329
|
} else if (rank === 'species') {
|
|
313
330
|
item.genericName = parentContext.genus
|
|
314
331
|
item.infragenericEpithet = parentContext.subgenus
|
|
315
|
-
|
|
316
|
-
item.scientificName = `${item.genericName} ${item.specificEpithet}`
|
|
317
|
-
if (item.specificEpithet !== taxon) {
|
|
332
|
+
if (taxon.toLowerCase() !== taxon && !/^[A-Z][a-z]+ [a-z]+\xD7[A-Z][a-z]+ [a-z]+$/.test(taxon)) {
|
|
318
333
|
console.log(item, taxon)
|
|
319
334
|
throw new Error(`Specific epithet should be lowercase: "${taxon}"`)
|
|
320
335
|
}
|
|
336
|
+
item.specificEpithet = taxon
|
|
337
|
+
item.scientificName = `${item.genericName} ${item.specificEpithet}`
|
|
321
338
|
} else if (compareRanks('species', rank) < 0) {
|
|
322
339
|
item.genericName = parentContext.genus
|
|
323
340
|
item.infragenericEpithet = parentContext.subgenus
|
|
@@ -463,6 +480,20 @@ function parseResource (resource: string): [ResourceMetadata, string] {
|
|
|
463
480
|
return [config, content]
|
|
464
481
|
}
|
|
465
482
|
|
|
483
|
+
function getIndentation (line: string): number {
|
|
484
|
+
return (line.match(/^ */) as string[])[0].length
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function isIndetLine (line: string, indent?: number): boolean {
|
|
488
|
+
if (indent === undefined) {
|
|
489
|
+
indent = getIndentation(line)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
line = line.slice(indent)
|
|
493
|
+
|
|
494
|
+
return line.startsWith('[indet]')
|
|
495
|
+
}
|
|
496
|
+
|
|
466
497
|
function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds: number[]): Resource {
|
|
467
498
|
const idBase = `${resource.id}:`
|
|
468
499
|
|
|
@@ -473,23 +504,19 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
473
504
|
let previousId = ''
|
|
474
505
|
let newIdOffset = Math.max(...oldIds)
|
|
475
506
|
|
|
476
|
-
for (const
|
|
477
|
-
if (type === ResourceDiffType.Deleted) {
|
|
478
|
-
id
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
// Do not process "indet" lines further, as they only serve to indicate
|
|
483
|
-
// that subtaxa are explicitely omitted
|
|
484
|
-
if (Array.from(INDET_SUFFIXES).some(suffix => line.endsWith(' ' + suffix))) {
|
|
507
|
+
for (const line of content) {
|
|
508
|
+
if (line.type === ResourceDiffType.Deleted) {
|
|
509
|
+
// Increase id counter for removed line unless it was an "indet line"
|
|
510
|
+
if (!isIndetLine(line.original as string)) {
|
|
511
|
+
id++
|
|
512
|
+
}
|
|
485
513
|
continue
|
|
486
514
|
}
|
|
487
515
|
|
|
488
|
-
const lineIndent = (line.
|
|
489
|
-
|
|
516
|
+
const lineIndent = getIndentation(line.text as string)
|
|
490
517
|
if (lineIndent > groupIndent) {
|
|
491
518
|
// Do not count synonyms as parents (unless this is correcting a typo in the synonym)
|
|
492
|
-
if (data[previousId] && data[previousId].taxonomicStatus === 'accepted' || /^( {2})+> /.test(line)) {
|
|
519
|
+
if (data[previousId] && data[previousId].taxonomicStatus === 'accepted' || /^( {2})+> /.test(line.text as string)) {
|
|
493
520
|
parents.push(previousId)
|
|
494
521
|
} else {
|
|
495
522
|
parents.push(null)
|
|
@@ -508,10 +535,19 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
508
535
|
groupIndent = lineIndent
|
|
509
536
|
}
|
|
510
537
|
|
|
538
|
+
// Do not process "indet" lines further, as they only serve to indicate
|
|
539
|
+
// that subtaxa are explicitely omitted
|
|
540
|
+
if (isIndetLine(line.text as string, lineIndent)) {
|
|
541
|
+
// If the line was previously not and ndet line, increase the id counter
|
|
542
|
+
if (line.type === ResourceDiffType.Modified && !isIndetLine(line.original as string)) {
|
|
543
|
+
id++
|
|
544
|
+
}
|
|
545
|
+
continue
|
|
546
|
+
}
|
|
547
|
+
|
|
511
548
|
const parentId = parents.reduce((grandparent, parent) => parent || grandparent, null)
|
|
512
549
|
const parent = parentId === null ? {} as WorkingTaxon : data[parentId]
|
|
513
|
-
|
|
514
|
-
const name = line.slice(groupIndent)
|
|
550
|
+
const name = (line.text as string).slice(groupIndent)
|
|
515
551
|
const rank = resource.metadata.levels[groupIndent / 2]
|
|
516
552
|
const item = parseName(name, rank, parent)
|
|
517
553
|
const isSynonym = item.taxonomicStatus !== 'accepted'
|
|
@@ -556,7 +592,10 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
556
592
|
}
|
|
557
593
|
|
|
558
594
|
// Set identifiers
|
|
559
|
-
if (type === ResourceDiffType.Added) {
|
|
595
|
+
if (line.type === ResourceDiffType.Added) {
|
|
596
|
+
newIdOffset++
|
|
597
|
+
item.scientificNameID = idBase + newIdOffset.toString()
|
|
598
|
+
} else if (line.type === ResourceDiffType.Modified && isIndetLine(line.original as string)) {
|
|
560
599
|
newIdOffset++
|
|
561
600
|
item.scientificNameID = idBase + newIdOffset.toString()
|
|
562
601
|
} else {
|
package/test/resources.js
CHANGED
|
@@ -26,9 +26,9 @@ levels: [family, genus, species]
|
|
|
26
26
|
|
|
27
27
|
Dolichurus haemorrhous A. Costa, 1886
|
|
28
28
|
Dolichurus A. Costa, 1886
|
|
29
|
-
|
|
29
|
+
[indet]
|
|
30
30
|
Sphecidae A. Costa, 1886
|
|
31
|
-
|
|
31
|
+
[indet]
|
|
32
32
|
`, 'T1')
|
|
33
33
|
assert.deepStrictEqual(Object.values(resource.taxa).map(taxon => taxon.scientificNameAuthorship), Array(3).fill('A. Costa, 1886'))
|
|
34
34
|
})
|
|
@@ -86,7 +86,7 @@ levels: [genus, species]
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
88
|
Drymus
|
|
89
|
-
|
|
89
|
+
[indet]
|
|
90
90
|
`, 'T1')
|
|
91
91
|
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Drymus')
|
|
92
92
|
})
|
|
@@ -149,4 +149,30 @@ Bogdania Kerzhner, 1964
|
|
|
149
149
|
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Bogdiana Kerzhner, 1964')
|
|
150
150
|
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, 'Bogdiana myrmica Kerzhner, 1964')
|
|
151
151
|
})
|
|
152
|
+
|
|
153
|
+
await t.test('parses cross-genus hybrids', (t) => {
|
|
154
|
+
const [resource] = resources.parseTextFile(`---
|
|
155
|
+
levels: [genus, species]
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
x Triticosecale
|
|
159
|
+
[indet]
|
|
160
|
+
x Festulpia
|
|
161
|
+
Festuca_rubra x Vulpia_bromoides
|
|
162
|
+
`, 'T1')
|
|
163
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, '×Triticosecale')
|
|
164
|
+
assert.strictEqual(resource.taxa['T1:1:2'].scientificName, '×Festulpia')
|
|
165
|
+
assert.strictEqual(resource.taxa['T1:1:3'].scientificName, '×Festulpia Festuca rubra×Vulpia bromoides')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
await t.test('parses cross-genus hybrids without parent context', (t) => {
|
|
169
|
+
const [resource] = resources.parseTextFile(`---
|
|
170
|
+
levels: [species]
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
x Festulpia Festuca_rubra x Vulpia_bromoides
|
|
174
|
+
`, 'T1')
|
|
175
|
+
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, '×Festulpia Festuca rubra×Vulpia bromoides')
|
|
176
|
+
assert.strictEqual(resource.taxa['T1:1:1'].verbatimIdentification, '× Festulpia Festuca rubra × Vulpia bromoides')
|
|
177
|
+
})
|
|
152
178
|
})
|