@larsgw/formica 0.8.4 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,4 @@
1
1
  "use strict";
2
- var __assign = (this && this.__assign) || function () {
3
- __assign = Object.assign || function(t) {
4
- for (var s, i = 1, n = arguments.length; i < n; i++) {
5
- s = arguments[i];
6
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
- t[p] = s[p];
8
- }
9
- return t;
10
- };
11
- return __assign.apply(this, arguments);
12
- };
13
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
3
  if (k2 === undefined) k2 = k;
15
4
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -46,41 +35,11 @@ var __importStar = (this && this.__importStar) || (function () {
46
35
  Object.defineProperty(exports, "__esModule", { value: true });
47
36
  exports.parseFile = parseFile;
48
37
  exports.parseFileHeader = parseFileHeader;
49
- var yaml = __importStar(require("js-yaml"));
50
- var resource_1 = require("./resource");
51
- var diff_resource_1 = require("./diff-resource");
52
- var RANKS = [
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
- ];
83
- var MAIN_RANKS = [
38
+ const yaml = __importStar(require("js-yaml"));
39
+ const resource_1 = require("./resource");
40
+ const diff_resource_1 = require("./diff-resource");
41
+ const parse_name_1 = require("./parse-name");
42
+ const MAIN_RANKS = [
84
43
  'kingdom',
85
44
  'phylum',
86
45
  'class',
@@ -89,7 +48,7 @@ var MAIN_RANKS = [
89
48
  'genus',
90
49
  'species'
91
50
  ];
92
- var DWC_RANKS = [
51
+ const DWC_RANKS = [
93
52
  'kingdom',
94
53
  'phylum',
95
54
  'class',
@@ -99,299 +58,27 @@ var DWC_RANKS = [
99
58
  'genus',
100
59
  'subgenus'
101
60
  ];
102
- var FLAGS = [
61
+ const FLAGS = [
103
62
  'MISSING_TAXA',
104
63
  'MISSING_PARENT_TAXA',
105
64
  'MISSING_SYNONYMS',
106
65
  'MISSING_AUTHORSHIP'
107
66
  ];
108
- var TAXONOMIC_STATUS = {
109
- '>': 'incorrect',
110
- '+': 'heterotypic synonym',
111
- '=': 'synonym'
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();
67
+ const RESOURCE_DELIMITER = '\n\n===\n\n';
68
+ const INDENT = 2;
69
+ function makeParseError(message, line, column = 1) {
70
+ return new SyntaxError(`[${line}:${column}] ${message}`);
188
71
  }
189
- function getSynonymRank(name, rank) {
190
- var rest = name.replace(BINAME_PATTERN, '');
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;
72
+ function mergeParserErrors(errors) {
73
+ return new SyntaxError(errors.map(error => error.message).join('\n'));
387
74
  }
388
75
  function parseHeader(header) {
389
- var config = yaml.load(header);
76
+ const config = yaml.load(header);
390
77
  if (typeof config !== 'object' || Array.isArray(config) || config === null) {
391
78
  throw new SyntaxError('yaml header should be an object');
392
79
  }
393
80
  // Invalid configuration
394
- var levels;
81
+ let levels;
395
82
  if (!('levels' in config)) {
396
83
  levels = [];
397
84
  }
@@ -409,18 +96,18 @@ function parseHeader(header) {
409
96
  throw new SyntaxError('Resource contains no taxa');
410
97
  }
411
98
  // Invalid taxon ranks
412
- var invalidTaxonRanks = levels.filter(function (rank) { return !RANKS.includes(rank); });
99
+ const invalidTaxonRanks = levels.filter(rank => !parse_name_1.RANKS.includes(rank));
413
100
  if (invalidTaxonRanks.length) {
414
- throw new SyntaxError("\"levels\" contains invalid values: ".concat(invalidTaxonRanks.join(', ')));
101
+ throw new SyntaxError(`"levels" contains invalid values: ${invalidTaxonRanks.join(', ')}`);
415
102
  }
416
- var metadata = { levels: levels };
103
+ const metadata = { levels };
417
104
  if ('catalog' in config && typeof config.catalog === 'object' && config.catalog !== null) {
418
- var catalog = {};
105
+ const catalog = {};
419
106
  if ('id' in config.catalog) {
420
107
  throw new SyntaxError('"catalog" should not contain id');
421
108
  }
422
- for (var key in config.catalog) {
423
- var value = config.catalog[key];
109
+ for (const key in config.catalog) {
110
+ const value = config.catalog[key];
424
111
  if (typeof value === 'number') {
425
112
  catalog[key] = value.toString();
426
113
  }
@@ -428,22 +115,16 @@ function parseHeader(header) {
428
115
  catalog[key] = value;
429
116
  }
430
117
  else {
431
- throw new SyntaxError("\"catalog\" should contain only strings (\"".concat(key, "\")"));
118
+ throw new SyntaxError(`"catalog" should contain only strings ("${key}")`);
432
119
  }
433
120
  }
434
- var work = new resource_1.WorkResource(catalog);
435
- var errors = work.validate().filter(function (_a) {
436
- var error = _a.error;
437
- return error !== 'Value(s) required but missing';
438
- });
121
+ const work = new resource_1.WorkResource(catalog);
122
+ const errors = work.validate().filter(({ error }) => error !== 'Value(s) required but missing');
439
123
  if (errors.length > 0) {
440
- throw new SyntaxError("\"catalog\" contains errors: ".concat(errors.map(function (_a) {
441
- var field = _a.field, error = _a.error;
442
- return "[".concat(field, "] ").concat(error);
443
- }).join('; ')));
124
+ throw new SyntaxError(`"catalog" contains errors: ${errors.map(({ field, error }) => `[${field}] ${error}`).join('; ')}`);
444
125
  }
445
126
  metadata.catalog = {};
446
- for (var key in work.fields) {
127
+ for (const key in work.fields) {
447
128
  metadata.catalog[key] = work.get(key);
448
129
  }
449
130
  }
@@ -451,74 +132,67 @@ function parseHeader(header) {
451
132
  if (!Array.isArray(config.flags)) {
452
133
  throw new SyntaxError('"flags" should be an array if present');
453
134
  }
454
- var invalidFlags = config.flags.filter(function (flag) { return !FLAGS.includes(flag); });
135
+ const invalidFlags = config.flags.filter(flag => !FLAGS.includes(flag));
455
136
  if (invalidFlags.length) {
456
- throw new SyntaxError("\"flags\" contains invalid values: ".concat(invalidFlags.join(', ')));
137
+ throw new SyntaxError(`"flags" contains invalid values: ${invalidFlags.join(', ')}`);
457
138
  }
458
139
  metadata.flags = config.flags;
459
140
  }
460
141
  return metadata;
461
142
  }
462
- function validateResource(config, content) {
463
- // Check for too much indentation
464
- var longerIndent = new RegExp("^( ){".concat(config.levels.length - 1, "}(?! [+=>] | > ) "), 'm');
465
- var longerIndentMatch = content.match(longerIndent);
466
- if (longerIndentMatch !== null) {
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^"));
470
- }
471
- // Check for missing leaf taxa
472
- var leafTaxonRank = config.levels.filter(function (rank) { return MAIN_RANKS.includes(rank); }).pop();
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
- }
482
- }
483
- }
484
143
  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
- var content = rest.join('');
488
- return [config, content];
489
- }
490
- function getIndentation(line) {
491
- return line.match(/^ */)[0].length;
492
- }
493
- function isIndetLine(line, indent) {
494
- if (indent === undefined) {
495
- indent = getIndentation(line);
144
+ const [header, _, ...rest] = resource.content.split(/(\n---\n+)/);
145
+ let config;
146
+ try {
147
+ config = parseHeader(header);
148
+ }
149
+ catch (error) {
150
+ throw makeParseError(error.message, resource.offsetLine + 1);
496
151
  }
497
- line = line.slice(indent);
498
- return line.startsWith('[indet]');
152
+ const content = rest.join('');
153
+ const offsetLine = resource.offsetLine + (header + _).split('\n').length - 1;
154
+ return [config, { content, offsetLine }];
499
155
  }
500
- function parseResourceContent(content, resource, oldIds) {
501
- var idBase = "".concat(resource.id, ":");
502
- var data = resource.taxa;
503
- var id = 0;
504
- var parents = [];
505
- var groupIndent = 0;
506
- var previousId = '';
507
- var newIdOffset = Math.max.apply(Math, oldIds);
508
- for (var _i = 0, content_1 = content; _i < content_1.length; _i++) {
509
- var line = content_1[_i];
156
+ function parseResourceContent(content, resource, oldIds, offsetLine) {
157
+ var _a, _b;
158
+ const leafTaxonIndex = resource.metadata.levels.reduce((last, rank, i) => MAIN_RANKS.includes(rank) ? i : last, 0);
159
+ const data = resource.taxa;
160
+ const errors = [];
161
+ let id = 0;
162
+ let newId = Math.max(...oldIds);
163
+ let lineNumber = offsetLine;
164
+ const parents = [];
165
+ const previous = { id: '', indent: 0, group: { isLeaf: false, indent: 0 }, errors: [] };
166
+ for (const line of content) {
167
+ const hasOriginalId = line.type !== diff_resource_1.ResourceDiffType.Added && !/^\s*(\[indet\]|> )/.test((_a = line.original) !== null && _a !== void 0 ? _a : line.text);
168
+ if (hasOriginalId) {
169
+ id++;
170
+ }
510
171
  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
172
  continue;
516
173
  }
517
- var lineIndent = getIndentation(line.text);
518
- if (lineIndent > groupIndent) {
174
+ else {
175
+ lineNumber++;
176
+ }
177
+ const [indentation, name] = line.text.match(/^(\s*)(.*)/).slice(1);
178
+ const lineIndent = indentation.length;
179
+ // Validate line
180
+ if (lineIndent % INDENT === 1) {
181
+ errors.push(makeParseError('Too much or little indentation', lineNumber));
182
+ continue;
183
+ }
184
+ else if (lineIndent / INDENT >= resource.metadata.levels.length && !/^[+=>] /.test(name)) {
185
+ errors.push(makeParseError('Too much indentation', lineNumber));
186
+ continue;
187
+ }
188
+ else if (lineIndent <= previous.group.indent && (data[previous.id] && !previous.group.isLeaf)) {
189
+ errors.push(makeParseError('Missing leaf taxon', lineNumber - 1));
190
+ }
191
+ // Update parentage
192
+ if (lineIndent > previous.indent) {
519
193
  // Do not count synonyms as parents (unless this is correcting a typo in the synonym)
520
- if (data[previousId] && data[previousId].taxonomicStatus === 'accepted' || /^( {2})+> /.test(line.text)) {
521
- parents.push(previousId);
194
+ if (data[previous.id] && data[previous.id].taxonomicStatus === 'accepted' || name.startsWith('> ')) {
195
+ parents.push(previous.id);
522
196
  }
523
197
  else {
524
198
  parents.push(null);
@@ -527,44 +201,50 @@ function parseResourceContent(content, resource, oldIds) {
527
201
  // e.g. if a certain genus has only species
528
202
  // whereas other genera in the same key also
529
203
  // have subgenera
530
- if ((lineIndent - groupIndent) > 2) {
531
- var gap = (lineIndent - groupIndent - 2) / 2;
532
- for (var i = 0; i < gap; i++) {
533
- parents.push(null);
534
- }
204
+ for (let i = previous.indent + INDENT; i < lineIndent; i += INDENT) {
205
+ parents.push(null);
535
206
  }
536
- groupIndent = lineIndent;
537
207
  }
538
- else if (lineIndent < groupIndent) {
539
- parents = parents.slice(0, lineIndent / 2);
540
- groupIndent = lineIndent;
208
+ else if (lineIndent < previous.indent) {
209
+ parents.splice(lineIndent / INDENT);
541
210
  }
211
+ previous.indent = lineIndent;
542
212
  // Do not process "indet" lines further, as they only serve to indicate
543
213
  // that subtaxa are explicitely omitted
544
- if (isIndetLine(line.text, lineIndent)) {
545
- // If the line was previously not and ndet line, increase the id counter
546
- if (line.type === diff_resource_1.ResourceDiffType.Modified && !isIndetLine(line.original)) {
547
- id++;
548
- }
214
+ if (name.startsWith('[indet]')) {
215
+ errors.push(...previous.errors);
216
+ previous.errors.length = 0;
217
+ previous.group.isLeaf = lineIndent / INDENT >= leafTaxonIndex;
549
218
  continue;
550
219
  }
551
- var parentId = parents.reduce(function (grandparent, parent) { return parent || grandparent; }, null);
552
- var parent_1 = parentId === null ? {} : data[parentId];
553
- var name_1 = line.text.slice(groupIndent);
554
- var rank = resource.metadata.levels[groupIndent / 2];
555
- var item = parseName(name_1, rank, parent_1);
556
- var isSynonym = item.taxonomicStatus !== 'accepted';
220
+ const parentId = parents.reduce((grandparent, parent) => parent !== null && parent !== void 0 ? parent : grandparent, null);
221
+ const parent = parentId === null ? {} : data[parentId];
222
+ let item;
223
+ const itemErrors = [];
224
+ try {
225
+ const rank = resource.metadata.levels[parents.length];
226
+ item = (0, parse_name_1.parseName)(name, rank, parent);
227
+ }
228
+ catch (error) {
229
+ if (error instanceof parse_name_1.RecoverableSyntaxError) {
230
+ itemErrors.push(makeParseError(error.message, lineNumber));
231
+ item = error.result;
232
+ }
233
+ else {
234
+ errors.push(makeParseError(error.message, lineNumber));
235
+ continue;
236
+ }
237
+ }
557
238
  // Add higher classification info
558
- var itemAsObject = item;
559
- var parentAsObject = parent_1;
560
- for (var _a = 0, DWC_RANKS_1 = DWC_RANKS; _a < DWC_RANKS_1.length; _a++) {
561
- var rank_1 = DWC_RANKS_1[_a];
562
- itemAsObject[rank_1] = undefined;
563
- if (parentAsObject[rank_1]) {
564
- itemAsObject[rank_1] = parentAsObject[rank_1];
239
+ const itemAsObject = item;
240
+ const parentAsObject = parent;
241
+ for (const rank of DWC_RANKS) {
242
+ itemAsObject[rank] = undefined;
243
+ if (parentAsObject[rank]) {
244
+ itemAsObject[rank] = parentAsObject[rank];
565
245
  }
566
- if (item.taxonRank === rank_1) {
567
- itemAsObject[rank_1] = item.scientificNameOnly;
246
+ if (item.taxonRank === rank) {
247
+ itemAsObject[rank] = item.scientificNameOnly;
568
248
  }
569
249
  }
570
250
  if (item.genericName && !item.genus) {
@@ -573,82 +253,112 @@ function parseResourceContent(content, resource, oldIds) {
573
253
  if (item.infragenericEpithet && !item.subgenus) {
574
254
  item.subgenus = item.infragenericEpithet;
575
255
  }
576
- if (isSynonym) {
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
256
+ // Amend "parent" with corrections, exit
586
257
  if (item.taxonomicStatus === 'incorrect') {
587
- parent_1.incorrect = __assign({}, parent_1);
588
- for (var key in item) {
258
+ if (parent.incorrect) {
259
+ errors.push(makeParseError('Cannot apply a correction to a previous correction', lineNumber));
260
+ continue;
261
+ }
262
+ else if (parentId === null) {
263
+ errors.push(makeParseError('Cannot apply a correction to nothing', lineNumber));
264
+ continue;
265
+ }
266
+ parent.incorrect = Object.assign({}, parent);
267
+ for (const key in item) {
589
268
  if (key !== 'taxonomicStatus' && key !== 'verbatimIdentification') {
590
269
  parentAsObject[key] = itemAsObject[key];
591
270
  }
592
271
  }
272
+ // If "parent" is corrected, its errors can be dropped
273
+ previous.errors.length = 0;
274
+ // ...but errors associated with the corrected name are added immediately
275
+ errors.push(...itemErrors);
593
276
  continue;
594
277
  }
595
- // Set identifiers
596
- if (line.type === diff_resource_1.ResourceDiffType.Added) {
597
- newIdOffset++;
598
- item.scientificNameID = idBase + newIdOffset.toString();
278
+ // Add more classification info
279
+ const isSynonym = item.taxonomicStatus !== 'accepted';
280
+ if (isSynonym) {
281
+ item.higherClassification = parent.higherClassification;
599
282
  }
600
- else if (line.type === diff_resource_1.ResourceDiffType.Modified && isIndetLine(line.original)) {
601
- newIdOffset++;
602
- item.scientificNameID = idBase + newIdOffset.toString();
283
+ else if (parent.higherClassification) {
284
+ item.higherClassification = parent.higherClassification + ` | ${parent.scientificNameOnly}`;
603
285
  }
604
- else {
605
- id++;
606
- item.scientificNameID = idBase + (oldIds[id - 1] || id).toString();
286
+ else if (parentId) {
287
+ item.higherClassification = parent.scientificNameOnly;
607
288
  }
608
- previousId = item.scientificNameID;
609
- item.parentNameUsageID = isSynonym ? undefined : parent_1.scientificNameID;
610
- item.parentNameUsage = isSynonym ? undefined : parent_1.scientificName;
611
- item.acceptedNameUsageID = isSynonym ? parent_1.scientificNameID : undefined;
612
- item.acceptedNameUsage = isSynonym ? parent_1.scientificName : undefined;
613
- item.collectionCode = idBase.slice(0, -1);
289
+ // Set identifiers
290
+ item.scientificNameID = `${resource.id}:${hasOriginalId ? ((_b = oldIds[id - 1]) !== null && _b !== void 0 ? _b : id) : ++newId}`;
291
+ item.parentNameUsageID = isSynonym ? undefined : parent.scientificNameID;
292
+ item.parentNameUsage = isSynonym ? undefined : parent.scientificName;
293
+ item.acceptedNameUsageID = isSynonym ? parent.scientificNameID : undefined;
294
+ item.acceptedNameUsage = isSynonym ? parent.scientificName : undefined;
295
+ item.collectionCode = resource.id;
614
296
  data[item.scientificNameID] = item;
297
+ // Update loop state
298
+ errors.push(...previous.errors);
299
+ previous.errors = itemErrors;
300
+ previous.id = item.scientificNameID;
301
+ if (item.taxonomicStatus === 'accepted') {
302
+ previous.group.indent = previous.indent;
303
+ previous.group.isLeaf = lineIndent / INDENT >= leafTaxonIndex;
304
+ }
305
+ }
306
+ errors.push(...previous.errors);
307
+ if (errors.length) {
308
+ throw mergeParserErrors(errors);
615
309
  }
616
310
  return resource;
617
311
  }
618
312
  function splitResources(file) {
619
- return file.split('\n\n===\n\n');
313
+ const resources = [];
314
+ let offsetLine = 0;
315
+ for (const content of file.split(RESOURCE_DELIMITER)) {
316
+ resources.push({ content, offsetLine });
317
+ offsetLine += (content + RESOURCE_DELIMITER).split('\n').length - 1;
318
+ }
319
+ return resources;
620
320
  }
621
321
  function parseFile(file, id, old) {
622
- var oldResources = old ? splitResources(old.txt) : [];
623
- return splitResources(file).map(function (resource, index) {
624
- var _a = parseResource(resource), config = _a[0], content = _a[1];
625
- validateResource(config, content);
626
- var template = {
627
- id: "".concat(id, ":").concat(index + 1),
628
- file: "".concat(id, "-").concat(index + 1),
322
+ const oldResources = old ? splitResources(old.txt) : [];
323
+ const newResources = splitResources(file);
324
+ const resources = [];
325
+ const errors = [];
326
+ for (let index = 0; index < newResources.length; index++) {
327
+ const [config, content] = parseResource(newResources[index]);
328
+ const template = {
329
+ id: `${id}:${index + 1}`,
330
+ file: `${id}-${index + 1}`,
629
331
  workId: id,
630
332
  metadata: config,
631
333
  taxa: {}
632
334
  };
633
- var diff;
335
+ let diff;
634
336
  if (oldResources[index]) {
635
- diff = (0, diff_resource_1.createDiff)(content, parseResource(oldResources[index])[1]);
337
+ diff = (0, diff_resource_1.createDiff)(content.content, parseResource(oldResources[index])[1].content);
636
338
  // Ignore empty lines
637
- diff = diff.filter(function (line) { return line.text !== ''; });
339
+ diff = diff.filter(line => line.text !== '');
638
340
  }
639
341
  else {
640
- diff = (0, diff_resource_1.createDiff)(content, content);
342
+ diff = (0, diff_resource_1.createDiff)(content.content, content.content);
641
343
  }
642
- var oldIds = [];
344
+ const oldIds = [];
643
345
  if (old) {
644
- for (var _i = 0, _b = old.dwc[index].slice(1); _i < _b.length; _i++) {
645
- var row = _b[_i];
346
+ for (const row of old.dwc[index].slice(1)) {
646
347
  oldIds.push(parseInt(row[0].split(':')[2]));
647
348
  }
648
349
  }
649
- return parseResourceContent(diff, template, oldIds);
650
- });
350
+ try {
351
+ resources.push(parseResourceContent(diff, template, oldIds, content.offsetLine));
352
+ }
353
+ catch (error) {
354
+ errors.push(error);
355
+ }
356
+ }
357
+ if (errors.length) {
358
+ throw mergeParserErrors(errors);
359
+ }
360
+ return resources;
651
361
  }
652
362
  function parseFileHeader(file) {
653
- return splitResources(file).map(function (resource) { return parseResource(resource)[0]; });
363
+ return splitResources(file).map(resource => parseResource(resource)[0]);
654
364
  }