@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
|
@@ -49,37 +49,7 @@ exports.parseFileHeader = parseFileHeader;
|
|
|
49
49
|
var yaml = __importStar(require("js-yaml"));
|
|
50
50
|
var resource_1 = require("./resource");
|
|
51
51
|
var diff_resource_1 = require("./diff-resource");
|
|
52
|
-
var
|
|
53
|
-
'phylum',
|
|
54
|
-
'subphylum',
|
|
55
|
-
'class',
|
|
56
|
-
'infraclass',
|
|
57
|
-
'superorder',
|
|
58
|
-
'order',
|
|
59
|
-
'suborder',
|
|
60
|
-
'infraorder',
|
|
61
|
-
'superfamily',
|
|
62
|
-
'family',
|
|
63
|
-
'subfamily',
|
|
64
|
-
'tribe',
|
|
65
|
-
'subtribe',
|
|
66
|
-
'genus',
|
|
67
|
-
'subgenus',
|
|
68
|
-
'section', // not ICZN
|
|
69
|
-
'subsection', // not ICZN
|
|
70
|
-
'series', // not ICZN
|
|
71
|
-
'group',
|
|
72
|
-
'subgroup', // ...
|
|
73
|
-
'aggregate', // not ICZN
|
|
74
|
-
'complex', // not ICZN
|
|
75
|
-
'species',
|
|
76
|
-
'subspecies',
|
|
77
|
-
'variety',
|
|
78
|
-
'form',
|
|
79
|
-
'aberration', // not ICZN
|
|
80
|
-
'race', // not ICZN
|
|
81
|
-
'stirps' // not ICZN
|
|
82
|
-
];
|
|
52
|
+
var parse_name_1 = require("./parse-name");
|
|
83
53
|
var MAIN_RANKS = [
|
|
84
54
|
'kingdom',
|
|
85
55
|
'phylum',
|
|
@@ -105,285 +75,14 @@ var FLAGS = [
|
|
|
105
75
|
'MISSING_SYNONYMS',
|
|
106
76
|
'MISSING_AUTHORSHIP'
|
|
107
77
|
];
|
|
108
|
-
var
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
var RANK_LABELS = {
|
|
114
|
-
'subspecies': 'subsp.',
|
|
115
|
-
'variety': 'var.',
|
|
116
|
-
'form': 'f.',
|
|
117
|
-
'aberration': 'ab.',
|
|
118
|
-
'race': 'r.',
|
|
119
|
-
'stirps': 'st.'
|
|
120
|
-
};
|
|
121
|
-
var RANK_LABELS_REVERSE = {
|
|
122
|
-
'st': 'stirps',
|
|
123
|
-
'r': 'race',
|
|
124
|
-
'ab': 'aberration',
|
|
125
|
-
'f': 'form',
|
|
126
|
-
'var': 'variety',
|
|
127
|
-
'ssp': 'subspecies',
|
|
128
|
-
'subsp': 'subspecies'
|
|
129
|
-
};
|
|
130
|
-
var HYBRID_SIGN = '\u00D7';
|
|
131
|
-
/**
|
|
132
|
-
* 1. Any number of
|
|
133
|
-
* - capitalized words
|
|
134
|
-
* - "&"
|
|
135
|
-
* - " in "
|
|
136
|
-
* - " ex "
|
|
137
|
-
* - lowercase name particles
|
|
138
|
-
* 2. Followed by a capitalized word
|
|
139
|
-
* 3. Optionally, followed by "et al."
|
|
140
|
-
*/
|
|
141
|
-
var LOWERCASE_NAME_PARTICLES = ['y', 'der', 'den', 'de', 'van', 'von'].join('|');
|
|
142
|
-
var SIMPLE_AUTHOR_PATTERN = '(?:(?:\\p{Lu}\\S*|&|in|ex|' + LOWERCASE_NAME_PARTICLES + ')\\s*)*\\p{Lu}\\S+(?:\\s+et\\s+al\\.)?';
|
|
143
|
-
var NAME_PATTERN = new RegExp('^' +
|
|
144
|
-
// $1 main name part
|
|
145
|
-
'(\\S+)' +
|
|
146
|
-
// $2 optional author citation
|
|
147
|
-
'(?: ' +
|
|
148
|
-
// but not auct(t)., etc.
|
|
149
|
-
'(?!auctt?\\.|(?:syn|comb|sp|spec|nom|gen|subgen)\\. n(?:ov)?\\.|s(?:ens[.u]|\\.)|in part|partim)' +
|
|
150
|
-
'(' +
|
|
151
|
-
// $2.1 anything in parentheses, followed by optional revising author(s)
|
|
152
|
-
'\\(.+?\\)(?:\\s+' + SIMPLE_AUTHOR_PATTERN + ')?' +
|
|
153
|
-
'|' +
|
|
154
|
-
// $2.2 anything followed by a year
|
|
155
|
-
'.+?\\d{4}\\)?' +
|
|
156
|
-
'|' +
|
|
157
|
-
// $2.3 author(s)
|
|
158
|
-
SIMPLE_AUTHOR_PATTERN +
|
|
159
|
-
'))?' +
|
|
160
|
-
// $3 optional notes
|
|
161
|
-
'(?:,? (.+))?' +
|
|
162
|
-
'$', 'u');
|
|
163
|
-
/**
|
|
164
|
-
* Structure
|
|
165
|
-
* $1 genus+subgenus (+ trailing space): (?:([A-Z]\S+) (?:\(([A-Z]\S+?)\) )?)?
|
|
166
|
-
* $1.1 genus: ((?:x )?[A-Z]\S+)
|
|
167
|
-
* $1.2 subgenus: (?:\(([A-Z]\S+?)\) )?
|
|
168
|
-
* $2 species: (x [a-z-]+|[a-z-][^\s.]+(?: x [a-z-]+)?|[A-Z][a-z]+_[a-z-]+ x [A-Z][a-z]+_[a-z-]+)
|
|
169
|
-
* $2a: x [a-z-]+
|
|
170
|
-
* $2b hybrid: [a-z-][^\s.]+(?: x [a-z-]+)?
|
|
171
|
-
* $2c intergeneric hybrid: [A-Z][a-z]+_[a-z-]+ x [A-Z][a-z]+_[a-z-]+
|
|
172
|
-
*/
|
|
173
|
-
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-]+)(?= |$)/;
|
|
174
|
-
function compareRanks(a, b) {
|
|
175
|
-
return RANKS.indexOf(a) - RANKS.indexOf(b);
|
|
176
|
-
}
|
|
177
|
-
function capitalize(name) {
|
|
178
|
-
return name[0].toUpperCase() + name.slice(1).toLowerCase();
|
|
179
|
-
}
|
|
180
|
-
function capitalizeGenericName(name) {
|
|
181
|
-
if (name[0] === HYBRID_SIGN) {
|
|
182
|
-
return HYBRID_SIGN + capitalize(name.slice(1));
|
|
183
|
-
}
|
|
184
|
-
return capitalize(name);
|
|
185
|
-
}
|
|
186
|
-
function isUpperCase(name) {
|
|
187
|
-
return name === name.toUpperCase();
|
|
78
|
+
var RESOURCE_DELIMITER = '\n\n===\n\n';
|
|
79
|
+
var INDENT = 2;
|
|
80
|
+
function makeParseError(message, line, column) {
|
|
81
|
+
if (column === void 0) { column = 1; }
|
|
82
|
+
return new SyntaxError("[".concat(line, ":").concat(column, "] ").concat(message));
|
|
188
83
|
}
|
|
189
|
-
function
|
|
190
|
-
|
|
191
|
-
var rankPrefix = rest.match(/^(?: |^)(st|r|ab|f|var|ssp|subsp)\. /);
|
|
192
|
-
if (rankPrefix) {
|
|
193
|
-
return RANK_LABELS_REVERSE[rankPrefix[1]];
|
|
194
|
-
}
|
|
195
|
-
else if (!BINAME_PATTERN.test(name)) {
|
|
196
|
-
return rank;
|
|
197
|
-
}
|
|
198
|
-
else if (/^ (?!sensu)[a-z0-9-]+($| )/.test(rest)) {
|
|
199
|
-
return 'subspecies';
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
return 'species';
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
function capitalizeAuthors(authors) {
|
|
206
|
-
return authors
|
|
207
|
-
.replace(/[^\x00-\x40\x5B-\x60\x7B-\x7F]+/g, // eslint-disable-line no-control-regex
|
|
208
|
-
function (// eslint-disable-line no-control-regex
|
|
209
|
-
name) { return isUpperCase(name) ? capitalize(name) : name; })
|
|
210
|
-
.replace(/ Y /g, ' y ');
|
|
211
|
-
}
|
|
212
|
-
function parseName(name, rank, parent) {
|
|
213
|
-
var item = {};
|
|
214
|
-
// Synonyms have the accepted name usage as 'parent'.
|
|
215
|
-
var isSynonym = /^[+=>] /.test(name);
|
|
216
|
-
if (isSynonym) {
|
|
217
|
-
item.taxonomicStatus = TAXONOMIC_STATUS[name[0]];
|
|
218
|
-
name = name.replace(/^[+=>] (\? ?)?/, '');
|
|
219
|
-
rank = getSynonymRank(name, parent.taxonRank);
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
item.taxonomicStatus = 'accepted';
|
|
223
|
-
}
|
|
224
|
-
// Clusters
|
|
225
|
-
if (/^\[(_|\d+)\] /.test(name)) {
|
|
226
|
-
name = name.replace(/^\[(_|\d+)\] /, '');
|
|
227
|
-
}
|
|
228
|
-
// Set verbatim identification after subsequent syntax is removed.
|
|
229
|
-
item.verbatimIdentification = name.replace(/(?<=^| )x(?=$| )/g, HYBRID_SIGN).replace(/_/g, ' ');
|
|
230
|
-
// Parent context is used for parsing and formatting binomial names.
|
|
231
|
-
// For formatting, it needs to match external databases (i.e. be correct).
|
|
232
|
-
// For parsing, it needs to match the current file. If relevant parents
|
|
233
|
-
// (i.e. genus, species) had mistakes that were corrected, the uncorrected
|
|
234
|
-
// genus and species names need to be used.
|
|
235
|
-
var parentContext = {
|
|
236
|
-
genus: parent.genus,
|
|
237
|
-
subgenus: parent.subgenus,
|
|
238
|
-
specificEpithet: parent.specificEpithet,
|
|
239
|
-
incorrect: {
|
|
240
|
-
genus: parent.incorrect && parent.incorrect.genus,
|
|
241
|
-
specificEpithet: parent.incorrect && parent.incorrect.specificEpithet
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
// Both contexts should be amended in the two cases where binomial names
|
|
245
|
-
// are fully used: (1) synonyms and (2) multinomial taxa without parents to
|
|
246
|
-
// provide parts of the name (e.g. bare species without a genus parent, or
|
|
247
|
-
// even subspecies without a species or genus parent).
|
|
248
|
-
if (isSynonym || !parentContext.genus || (compareRanks('species', rank) < 0 && !parentContext.specificEpithet)) {
|
|
249
|
-
var _a = name.match(BINAME_PATTERN) || [], genus = _a[1], subgenus = _a[2], species = _a[3];
|
|
250
|
-
if (genus) {
|
|
251
|
-
parentContext.incorrect.genus = genus;
|
|
252
|
-
parentContext.genus = capitalizeGenericName(genus.replace(/(^| )x /, HYBRID_SIGN));
|
|
253
|
-
}
|
|
254
|
-
if (subgenus) {
|
|
255
|
-
parentContext.subgenus = capitalize(subgenus);
|
|
256
|
-
}
|
|
257
|
-
else if (genus) {
|
|
258
|
-
// If a genus is given but no subgenus, remove any existing subgenus
|
|
259
|
-
// from the parent context.
|
|
260
|
-
delete parentContext.subgenus;
|
|
261
|
-
}
|
|
262
|
-
if (species && compareRanks('species', rank) < 0) {
|
|
263
|
-
parentContext.incorrect.specificEpithet = species;
|
|
264
|
-
parentContext.specificEpithet = species.replace(/(^| )x /, HYBRID_SIGN);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
// In taxa of group, species or lower, the name should just contain the
|
|
268
|
-
// (infra)specific epithet and the author information & remarks when processing
|
|
269
|
-
// further.
|
|
270
|
-
if (compareRanks('group', rank) <= 0) {
|
|
271
|
-
// Remove genus
|
|
272
|
-
var genus = parentContext.incorrect.genus || parentContext.genus || '';
|
|
273
|
-
if (name[0] === genus[0] && name.toLowerCase().startsWith(genus.toLowerCase() + ' ')) {
|
|
274
|
-
name = name.slice(genus.length + 1);
|
|
275
|
-
}
|
|
276
|
-
// Remove subgenus
|
|
277
|
-
name = name.replace(/^\(.*?\) /, '');
|
|
278
|
-
// Infraspecific taxa
|
|
279
|
-
if (compareRanks('species', rank) < 0) {
|
|
280
|
-
// Remove specific epithet
|
|
281
|
-
var species = parentContext.incorrect.specificEpithet || parentContext.specificEpithet || '';
|
|
282
|
-
if (name.startsWith(species + ' ')) {
|
|
283
|
-
name = name.slice(species.length + 1);
|
|
284
|
-
}
|
|
285
|
-
// Remove rank abbreviations
|
|
286
|
-
name = name.replace(/^(st|r|ab|f|var|ssp|subsp)\. /, '');
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
// Hybrids
|
|
290
|
-
if (rank === 'genus' && name.startsWith('x ')) {
|
|
291
|
-
name = HYBRID_SIGN + name.slice(2);
|
|
292
|
-
}
|
|
293
|
-
if (rank === 'species' && /(^| )x /.test(name)) {
|
|
294
|
-
name = name.replace(/(^| )x /, HYBRID_SIGN);
|
|
295
|
-
}
|
|
296
|
-
// Divide the name into the main scientific name (only the epithet for taxa
|
|
297
|
-
// lower than genus), the authorship information, and optionally remarks
|
|
298
|
-
var nameParts = name.match(NAME_PATTERN);
|
|
299
|
-
if (!nameParts) {
|
|
300
|
-
throw new Error("Taxon \"".concat(name, "\" could not be parsed"));
|
|
301
|
-
}
|
|
302
|
-
// To encode old names with spaces (e.g. "Orsillus pini canariensis Lindberg, 1953")
|
|
303
|
-
// underscores are used, which are replaced here. This is also used for undescribed
|
|
304
|
-
// species (e.g. "Leiobunum species A") and intergeneric hybrids (e.g. "×Festulpia
|
|
305
|
-
// Festuca rubra × Vulpia bromoides")
|
|
306
|
-
if (nameParts[1].includes('_')) {
|
|
307
|
-
nameParts[1] = nameParts[1].replace(/_/g, ' ');
|
|
308
|
-
}
|
|
309
|
-
var _ = nameParts[0], taxon = nameParts[1], _b = nameParts[2], citation = _b === void 0 ? '' : _b, notes = nameParts[3];
|
|
310
|
-
item.scientificNameAuthorship = capitalizeAuthors(citation);
|
|
311
|
-
item.taxonRemarks = notes;
|
|
312
|
-
item.taxonRank = rank;
|
|
313
|
-
// @ts-expect-error TS1501: This regular expression flag is only available when targeting 'es6' or later.
|
|
314
|
-
if (/[^\p{L}0-9\u{00D7}\- ]/u.test(taxon)) {
|
|
315
|
-
throw new Error("Taxon name contains unexpected characters: \"".concat(taxon, "\""));
|
|
316
|
-
}
|
|
317
|
-
// Validate names and recompose binomial and trinomial names
|
|
318
|
-
if (rank === 'genus') {
|
|
319
|
-
item.scientificName = capitalizeGenericName(taxon);
|
|
320
|
-
if (taxon[0].toUpperCase() !== taxon[0] || (taxon[0] === HYBRID_SIGN && taxon[1].toUpperCase() !== taxon[1])) {
|
|
321
|
-
throw new Error("Generic epithet should be capitalized: \"".concat(taxon, "\""));
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
else if (compareRanks('group', rank) > 0) {
|
|
325
|
-
item.scientificName = capitalize(taxon);
|
|
326
|
-
if (taxon[0].toUpperCase() !== taxon[0]) {
|
|
327
|
-
throw new Error("Taxon name (".concat(rank, ") should be capitalized: \"").concat(taxon, "\""));
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
else if (rank === 'group') {
|
|
331
|
-
item.genericName = parentContext.genus;
|
|
332
|
-
item.infragenericEpithet = parentContext.subgenus;
|
|
333
|
-
var specificEpithet = taxon.toLowerCase().replace(/(-group)?$/, '');
|
|
334
|
-
item.scientificName = "".concat(item.genericName, " ").concat(specificEpithet, "-group");
|
|
335
|
-
if (taxon.toLowerCase() !== taxon) {
|
|
336
|
-
console.log(item, taxon);
|
|
337
|
-
throw new Error("Group name should be lowercase: \"".concat(taxon, "\""));
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
else if (rank === 'subgroup') {
|
|
341
|
-
item.genericName = parentContext.genus;
|
|
342
|
-
item.infragenericEpithet = parentContext.subgenus;
|
|
343
|
-
var specificEpithet = taxon.toLowerCase().replace(/(-subgroup)?$/, '');
|
|
344
|
-
item.scientificName = "".concat(item.genericName, " ").concat(specificEpithet, "-subgroup");
|
|
345
|
-
if (taxon.toLowerCase() !== taxon) {
|
|
346
|
-
console.log(item, taxon);
|
|
347
|
-
throw new Error("Subgroup name should be lowercase: \"".concat(taxon, "\""));
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
else if (rank === 'species') {
|
|
351
|
-
item.genericName = parentContext.genus;
|
|
352
|
-
item.infragenericEpithet = parentContext.subgenus;
|
|
353
|
-
if (taxon.toLowerCase() !== taxon && !/^[A-Z][a-z]+ [a-z]+\xD7[A-Z][a-z]+ [a-z]+$/.test(taxon)) {
|
|
354
|
-
console.log(item, taxon);
|
|
355
|
-
throw new Error("Specific epithet should be lowercase: \"".concat(taxon, "\""));
|
|
356
|
-
}
|
|
357
|
-
item.specificEpithet = taxon;
|
|
358
|
-
item.scientificName = "".concat(item.genericName, " ").concat(item.specificEpithet);
|
|
359
|
-
}
|
|
360
|
-
else if (compareRanks('species', rank) < 0) {
|
|
361
|
-
item.genericName = parentContext.genus;
|
|
362
|
-
item.infragenericEpithet = parentContext.subgenus;
|
|
363
|
-
item.specificEpithet = parentContext.specificEpithet;
|
|
364
|
-
item.infraspecificEpithet = taxon.toLowerCase();
|
|
365
|
-
// If possible, names below species should have abbreviations for ranks,
|
|
366
|
-
// like "subsp."
|
|
367
|
-
var nameParts_1 = [
|
|
368
|
-
item.genericName,
|
|
369
|
-
item.specificEpithet,
|
|
370
|
-
item.infraspecificEpithet
|
|
371
|
-
];
|
|
372
|
-
if (item.taxonRank in RANK_LABELS) {
|
|
373
|
-
nameParts_1.splice(2, 0, RANK_LABELS[item.taxonRank]);
|
|
374
|
-
}
|
|
375
|
-
item.scientificName = nameParts_1.join(' ');
|
|
376
|
-
if (item.infraspecificEpithet !== taxon) {
|
|
377
|
-
console.log(item, taxon);
|
|
378
|
-
throw new Error("Infraspecific epithet should be lowercase: \"".concat(taxon, "\""));
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
// Re-add authorship information
|
|
382
|
-
item.scientificNameOnly = item.scientificName;
|
|
383
|
-
if (item.scientificNameAuthorship) {
|
|
384
|
-
item.scientificName += " ".concat(item.scientificNameAuthorship);
|
|
385
|
-
}
|
|
386
|
-
return item;
|
|
84
|
+
function mergeParserErrors(errors) {
|
|
85
|
+
return new SyntaxError(errors.map(function (error) { return error.message; }).join('\n'));
|
|
387
86
|
}
|
|
388
87
|
function parseHeader(header) {
|
|
389
88
|
var config = yaml.load(header);
|
|
@@ -409,7 +108,7 @@ function parseHeader(header) {
|
|
|
409
108
|
throw new SyntaxError('Resource contains no taxa');
|
|
410
109
|
}
|
|
411
110
|
// Invalid taxon ranks
|
|
412
|
-
var invalidTaxonRanks = levels.filter(function (rank) { return !RANKS.includes(rank); });
|
|
111
|
+
var invalidTaxonRanks = levels.filter(function (rank) { return !parse_name_1.RANKS.includes(rank); });
|
|
413
112
|
if (invalidTaxonRanks.length) {
|
|
414
113
|
throw new SyntaxError("\"levels\" contains invalid values: ".concat(invalidTaxonRanks.join(', ')));
|
|
415
114
|
}
|
|
@@ -459,66 +158,61 @@ function parseHeader(header) {
|
|
|
459
158
|
}
|
|
460
159
|
return metadata;
|
|
461
160
|
}
|
|
462
|
-
function
|
|
463
|
-
|
|
464
|
-
var
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
var offset = longerIndentMatch.index;
|
|
468
|
-
var line = (content.slice(0, offset).match(/\n/g) || []).length + 1;
|
|
469
|
-
throw new SyntaxError("Too much indentation at ".concat(line, ":0\n").concat(content.slice(offset).split('\n', 1), "\n^"));
|
|
161
|
+
function parseResource(resource) {
|
|
162
|
+
var _a = resource.content.split(/(\n---\n+)/), header = _a[0], _ = _a[1], rest = _a.slice(2);
|
|
163
|
+
var config;
|
|
164
|
+
try {
|
|
165
|
+
config = parseHeader(header);
|
|
470
166
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
var leafTaxonParentIndent = config.levels.indexOf(leafTaxonRank) - 1;
|
|
474
|
-
if (leafTaxonRank && leafTaxonParentIndent >= 0) {
|
|
475
|
-
var missingLeafTaxa = new RegExp("^((?: ){0,".concat(leafTaxonParentIndent, "})(?![+=> ] ).*\\n(\\1( )+[+=>].*\\n)*(?!\\1 )"), 'm');
|
|
476
|
-
var missingLeafTaxaMatch = content.match(missingLeafTaxa);
|
|
477
|
-
if (missingLeafTaxaMatch !== null) {
|
|
478
|
-
var offset = missingLeafTaxaMatch.index;
|
|
479
|
-
var line = (content.slice(0, offset).match(/\n/g) || []).length + 1;
|
|
480
|
-
throw new SyntaxError("Missing leaf taxon at ".concat(line, ":0\n").concat(content.slice(offset).split('\n', 1), "\n^"));
|
|
481
|
-
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
throw makeParseError(error.message, resource.offsetLine + 1);
|
|
482
169
|
}
|
|
483
|
-
}
|
|
484
|
-
function parseResource(resource) {
|
|
485
|
-
var _a = resource.split(/(\n---\n+)/), header = _a[0], _ = _a[1], rest = _a.slice(2);
|
|
486
|
-
var config = parseHeader(header);
|
|
487
170
|
var content = rest.join('');
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
function getIndentation(line) {
|
|
491
|
-
return line.match(/^ */)[0].length;
|
|
171
|
+
var offsetLine = resource.offsetLine + (header + _).split('\n').length - 1;
|
|
172
|
+
return [config, { content: content, offsetLine: offsetLine }];
|
|
492
173
|
}
|
|
493
|
-
function
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
line = line.slice(indent);
|
|
498
|
-
return line.startsWith('[indet]');
|
|
499
|
-
}
|
|
500
|
-
function parseResourceContent(content, resource, oldIds) {
|
|
501
|
-
var idBase = "".concat(resource.id, ":");
|
|
174
|
+
function parseResourceContent(content, resource, oldIds, offsetLine) {
|
|
175
|
+
var _a, _b;
|
|
176
|
+
var leafTaxonIndex = resource.metadata.levels.reduce(function (last, rank, i) { return MAIN_RANKS.includes(rank) ? i : last; }, 0);
|
|
502
177
|
var data = resource.taxa;
|
|
178
|
+
var errors = [];
|
|
503
179
|
var id = 0;
|
|
180
|
+
var newId = Math.max.apply(Math, oldIds);
|
|
181
|
+
var lineNumber = offsetLine;
|
|
504
182
|
var parents = [];
|
|
505
|
-
var
|
|
506
|
-
var previousId = '';
|
|
507
|
-
var newIdOffset = Math.max.apply(Math, oldIds);
|
|
183
|
+
var previous = { id: '', indent: 0, group: { isLeaf: false, indent: 0 } };
|
|
508
184
|
for (var _i = 0, content_1 = content; _i < content_1.length; _i++) {
|
|
509
185
|
var line = content_1[_i];
|
|
186
|
+
var hasOriginalId = line.type !== diff_resource_1.ResourceDiffType.Added && !/^\s*(\[indet\]|> )/.test((_a = line.original) !== null && _a !== void 0 ? _a : line.text);
|
|
187
|
+
if (hasOriginalId) {
|
|
188
|
+
id++;
|
|
189
|
+
}
|
|
510
190
|
if (line.type === diff_resource_1.ResourceDiffType.Deleted) {
|
|
511
|
-
// Increase id counter for removed line unless it was an "indet line"
|
|
512
|
-
if (!isIndetLine(line.original)) {
|
|
513
|
-
id++;
|
|
514
|
-
}
|
|
515
191
|
continue;
|
|
516
192
|
}
|
|
517
|
-
|
|
518
|
-
|
|
193
|
+
else {
|
|
194
|
+
lineNumber++;
|
|
195
|
+
}
|
|
196
|
+
var _c = line.text.match(/^(\s*)(.*)/).slice(1), indentation = _c[0], name_1 = _c[1];
|
|
197
|
+
var lineIndent = indentation.length;
|
|
198
|
+
// Validate line
|
|
199
|
+
if (lineIndent % INDENT === 1) {
|
|
200
|
+
errors.push(makeParseError('Too much or little indentation', lineNumber));
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
else if (lineIndent / INDENT >= resource.metadata.levels.length && !/^[+=>] /.test(name_1)) {
|
|
204
|
+
errors.push(makeParseError('Too much indentation', lineNumber));
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
else if (lineIndent <= previous.group.indent && (data[previous.id] && !previous.group.isLeaf)) {
|
|
208
|
+
errors.push(makeParseError('Missing leaf taxon', lineNumber - 1));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
// Update parentage
|
|
212
|
+
if (lineIndent > previous.indent) {
|
|
519
213
|
// Do not count synonyms as parents (unless this is correcting a typo in the synonym)
|
|
520
|
-
if (data[
|
|
521
|
-
parents.push(
|
|
214
|
+
if (data[previous.id] && data[previous.id].taxonomicStatus === 'accepted' || name_1.startsWith('> ')) {
|
|
215
|
+
parents.push(previous.id);
|
|
522
216
|
}
|
|
523
217
|
else {
|
|
524
218
|
parents.push(null);
|
|
@@ -527,38 +221,36 @@ function parseResourceContent(content, resource, oldIds) {
|
|
|
527
221
|
// e.g. if a certain genus has only species
|
|
528
222
|
// whereas other genera in the same key also
|
|
529
223
|
// have subgenera
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
for (var i = 0; i < gap; i++) {
|
|
533
|
-
parents.push(null);
|
|
534
|
-
}
|
|
224
|
+
for (var i = previous.indent + INDENT; i < lineIndent; i += INDENT) {
|
|
225
|
+
parents.push(null);
|
|
535
226
|
}
|
|
536
|
-
groupIndent = lineIndent;
|
|
537
227
|
}
|
|
538
|
-
else if (lineIndent <
|
|
539
|
-
parents
|
|
540
|
-
groupIndent = lineIndent;
|
|
228
|
+
else if (lineIndent < previous.indent) {
|
|
229
|
+
parents.splice(lineIndent / INDENT);
|
|
541
230
|
}
|
|
231
|
+
previous.indent = lineIndent;
|
|
542
232
|
// Do not process "indet" lines further, as they only serve to indicate
|
|
543
233
|
// that subtaxa are explicitely omitted
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
if (line.type === diff_resource_1.ResourceDiffType.Modified && !isIndetLine(line.original)) {
|
|
547
|
-
id++;
|
|
548
|
-
}
|
|
234
|
+
if (name_1.startsWith('[indet]')) {
|
|
235
|
+
previous.group.isLeaf = lineIndent / INDENT >= leafTaxonIndex;
|
|
549
236
|
continue;
|
|
550
237
|
}
|
|
551
|
-
var parentId = parents.reduce(function (grandparent, parent) { return parent
|
|
238
|
+
var parentId = parents.reduce(function (grandparent, parent) { return parent !== null && parent !== void 0 ? parent : grandparent; }, null);
|
|
552
239
|
var parent_1 = parentId === null ? {} : data[parentId];
|
|
553
|
-
var
|
|
554
|
-
var
|
|
555
|
-
|
|
556
|
-
|
|
240
|
+
var rank = resource.metadata.levels[parents.length];
|
|
241
|
+
var item = void 0;
|
|
242
|
+
try {
|
|
243
|
+
item = (0, parse_name_1.parseName)(name_1, rank, parent_1);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
errors.push(makeParseError(error.message, lineNumber));
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
557
249
|
// Add higher classification info
|
|
558
250
|
var itemAsObject = item;
|
|
559
251
|
var parentAsObject = parent_1;
|
|
560
|
-
for (var
|
|
561
|
-
var rank_1 = DWC_RANKS_1[
|
|
252
|
+
for (var _d = 0, DWC_RANKS_1 = DWC_RANKS; _d < DWC_RANKS_1.length; _d++) {
|
|
253
|
+
var rank_1 = DWC_RANKS_1[_d];
|
|
562
254
|
itemAsObject[rank_1] = undefined;
|
|
563
255
|
if (parentAsObject[rank_1]) {
|
|
564
256
|
itemAsObject[rank_1] = parentAsObject[rank_1];
|
|
@@ -573,16 +265,7 @@ function parseResourceContent(content, resource, oldIds) {
|
|
|
573
265
|
if (item.infragenericEpithet && !item.subgenus) {
|
|
574
266
|
item.subgenus = item.infragenericEpithet;
|
|
575
267
|
}
|
|
576
|
-
|
|
577
|
-
item.higherClassification = parent_1.higherClassification;
|
|
578
|
-
}
|
|
579
|
-
else if (parent_1.higherClassification) {
|
|
580
|
-
item.higherClassification = parent_1.higherClassification + " | ".concat(parent_1.scientificNameOnly);
|
|
581
|
-
}
|
|
582
|
-
else if (parentId) {
|
|
583
|
-
item.higherClassification = parent_1.scientificNameOnly;
|
|
584
|
-
}
|
|
585
|
-
// Amend "parent" with corrections
|
|
268
|
+
// Amend "parent" with corrections, exit
|
|
586
269
|
if (item.taxonomicStatus === 'incorrect') {
|
|
587
270
|
parent_1.incorrect = __assign({}, parent_1);
|
|
588
271
|
for (var key in item) {
|
|
@@ -592,37 +275,54 @@ function parseResourceContent(content, resource, oldIds) {
|
|
|
592
275
|
}
|
|
593
276
|
continue;
|
|
594
277
|
}
|
|
595
|
-
//
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
item.
|
|
278
|
+
// Add more classification info
|
|
279
|
+
var isSynonym = item.taxonomicStatus !== 'accepted';
|
|
280
|
+
if (isSynonym) {
|
|
281
|
+
item.higherClassification = parent_1.higherClassification;
|
|
599
282
|
}
|
|
600
|
-
else if (
|
|
601
|
-
|
|
602
|
-
item.scientificNameID = idBase + newIdOffset.toString();
|
|
283
|
+
else if (parent_1.higherClassification) {
|
|
284
|
+
item.higherClassification = parent_1.higherClassification + " | ".concat(parent_1.scientificNameOnly);
|
|
603
285
|
}
|
|
604
|
-
else {
|
|
605
|
-
|
|
606
|
-
item.scientificNameID = idBase + (oldIds[id - 1] || id).toString();
|
|
286
|
+
else if (parentId) {
|
|
287
|
+
item.higherClassification = parent_1.scientificNameOnly;
|
|
607
288
|
}
|
|
608
|
-
|
|
289
|
+
// Set identifiers
|
|
290
|
+
item.scientificNameID = "".concat(resource.id, ":").concat(hasOriginalId ? ((_b = oldIds[id - 1]) !== null && _b !== void 0 ? _b : id) : ++newId);
|
|
609
291
|
item.parentNameUsageID = isSynonym ? undefined : parent_1.scientificNameID;
|
|
610
292
|
item.parentNameUsage = isSynonym ? undefined : parent_1.scientificName;
|
|
611
293
|
item.acceptedNameUsageID = isSynonym ? parent_1.scientificNameID : undefined;
|
|
612
294
|
item.acceptedNameUsage = isSynonym ? parent_1.scientificName : undefined;
|
|
613
|
-
item.collectionCode =
|
|
295
|
+
item.collectionCode = resource.id;
|
|
614
296
|
data[item.scientificNameID] = item;
|
|
297
|
+
// Update loop state
|
|
298
|
+
previous.id = item.scientificNameID;
|
|
299
|
+
if (item.taxonomicStatus === 'accepted') {
|
|
300
|
+
previous.group.indent = previous.indent;
|
|
301
|
+
previous.group.isLeaf = lineIndent / INDENT >= leafTaxonIndex;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (errors.length) {
|
|
305
|
+
throw mergeParserErrors(errors);
|
|
615
306
|
}
|
|
616
307
|
return resource;
|
|
617
308
|
}
|
|
618
309
|
function splitResources(file) {
|
|
619
|
-
|
|
310
|
+
var resources = [];
|
|
311
|
+
var offsetLine = 0;
|
|
312
|
+
for (var _i = 0, _a = file.split(RESOURCE_DELIMITER); _i < _a.length; _i++) {
|
|
313
|
+
var content = _a[_i];
|
|
314
|
+
resources.push({ content: content, offsetLine: offsetLine });
|
|
315
|
+
offsetLine += (content + RESOURCE_DELIMITER).split('\n').length - 1;
|
|
316
|
+
}
|
|
317
|
+
return resources;
|
|
620
318
|
}
|
|
621
319
|
function parseFile(file, id, old) {
|
|
622
320
|
var oldResources = old ? splitResources(old.txt) : [];
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
321
|
+
var newResources = splitResources(file);
|
|
322
|
+
var resources = [];
|
|
323
|
+
var errors = [];
|
|
324
|
+
for (var index = 0; index < newResources.length; index++) {
|
|
325
|
+
var _a = parseResource(newResources[index]), config = _a[0], content = _a[1];
|
|
626
326
|
var template = {
|
|
627
327
|
id: "".concat(id, ":").concat(index + 1),
|
|
628
328
|
file: "".concat(id, "-").concat(index + 1),
|
|
@@ -630,14 +330,14 @@ function parseFile(file, id, old) {
|
|
|
630
330
|
metadata: config,
|
|
631
331
|
taxa: {}
|
|
632
332
|
};
|
|
633
|
-
var diff;
|
|
333
|
+
var diff = void 0;
|
|
634
334
|
if (oldResources[index]) {
|
|
635
|
-
diff = (0, diff_resource_1.createDiff)(content, parseResource(oldResources[index])[1]);
|
|
335
|
+
diff = (0, diff_resource_1.createDiff)(content.content, parseResource(oldResources[index])[1].content);
|
|
636
336
|
// Ignore empty lines
|
|
637
337
|
diff = diff.filter(function (line) { return line.text !== ''; });
|
|
638
338
|
}
|
|
639
339
|
else {
|
|
640
|
-
diff = (0, diff_resource_1.createDiff)(content, content);
|
|
340
|
+
diff = (0, diff_resource_1.createDiff)(content.content, content.content);
|
|
641
341
|
}
|
|
642
342
|
var oldIds = [];
|
|
643
343
|
if (old) {
|
|
@@ -646,8 +346,17 @@ function parseFile(file, id, old) {
|
|
|
646
346
|
oldIds.push(parseInt(row[0].split(':')[2]));
|
|
647
347
|
}
|
|
648
348
|
}
|
|
649
|
-
|
|
650
|
-
|
|
349
|
+
try {
|
|
350
|
+
resources.push(parseResourceContent(diff, template, oldIds, content.offsetLine));
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
errors.push(error);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (errors.length) {
|
|
357
|
+
throw mergeParserErrors(errors);
|
|
358
|
+
}
|
|
359
|
+
return resources;
|
|
651
360
|
}
|
|
652
361
|
function parseFileHeader(file) {
|
|
653
362
|
return splitResources(file).map(function (resource) { return parseResource(resource)[0]; });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@larsgw/formica",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "SDK and tools for data from the Library of Identification Resources",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"loir-validate-resources": "./lib/bin/validate-resources-text.js",
|
|
10
10
|
"loir-resources-process": "./lib/bin/process-resources.js",
|
|
11
11
|
"loir-resources-index": "./lib/bin/process-resources-index.js",
|
|
12
|
+
"loir-generate-linked-data": "./lib/bin/generate-linked-data.js",
|
|
12
13
|
"loir-catalog-clean-links": "./lib/bin/clean-links.js"
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|