@larsgw/formica 0.10.2 → 0.10.4

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/README.md CHANGED
@@ -20,6 +20,8 @@ Run locally:
20
20
  - `loir-validate-resources [./B1234.txt]`:
21
21
  Validate the `.txt` files containing information on the taxonomic scope of keys
22
22
  ([documentation](https://github.com/identification-resources/catalog/blob/main/docs/resources-txt.md)).
23
+ - `loir-validate-integrity .`:
24
+ Check if all values are mapped as expected.
23
25
  - `loir-resources-process [./resources]`:
24
26
  Interactive tool to convert the aforementioned `.txt` files to Darwin Core archives
25
27
  ([documentation](https://github.com/identification-resources/catalog/blob/main/docs/resources-dwc.md)),
@@ -69,6 +69,9 @@ const SCOPES = {
69
69
  'nypmhs': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
70
70
  'nymphs (instar V)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
71
71
  'nymphs (instar IV)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
72
+ 'nymphs (instar III)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
73
+ 'nymphs (instar II)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
74
+ 'nymphs (instar I)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
72
75
  'eggs': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/embryo'],
73
76
  // plant life stage
74
77
  'flowering plants': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/adult'],
@@ -87,6 +90,9 @@ const SCOPES = {
87
90
  'alatae': ['dwc:caste', 'alate'],
88
91
  'apterae': ['dwc:caste', 'aptera'],
89
92
  'viviparae': ['dwc:caste', 'vivipara'],
93
+ 'brachypterae': ['dwc:caste', 'brachypterae'],
94
+ 'macropterae': ['dwc:caste', 'macropterae'],
95
+ 'micropterae': ['dwc:caste', 'micropterae'],
90
96
  // evidence
91
97
  'nests': ['ac:subjectPartLiteral', 'nest'],
92
98
  'galls': ['ac:subjectPartLiteral', 'gall'],
@@ -96,6 +102,7 @@ const SCOPES = {
96
102
  'bones (skulls)': ['ac:subjectPart', 'http://rs.tdwg.org/acpart/values/p0027'],
97
103
  'bones (upper jaws)': ['ac:subjectPart', 'http://rs.tdwg.org/acpart/values/p0028'],
98
104
  'bones (lower jaws)': ['ac:subjectPart', 'http://rs.tdwg.org/acpart/values/p0029'],
105
+ 'spermathecae': ['ac:subjectPartLiteral', 'spermathecae'],
99
106
  };
100
107
  const PREFIXES = {
101
108
  'ac': 'http://rs.tdwg.org/ac/terms/',
@@ -129,15 +136,18 @@ const STATUSES = {
129
136
  'proparte synonym': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/proParteSynonym',
130
137
  'synonym': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/synonym',
131
138
  };
132
- function getCoveringTaxon(taxa) {
133
- if (taxa.length === 1 && !taxa[0].has('gbif')) {
139
+ function getCoveringTaxon(taxa, allTaxa) {
140
+ if (taxa.length === 1 && !taxa[0].has('col')) {
134
141
  return `${PREFIX}taxon/${taxa[0].get('id')}`;
135
142
  }
136
143
  function getAncestors(taxon) {
137
144
  var _a;
138
- const [...parents] = ((_a = taxon.get('parent_taxa')) !== null && _a !== void 0 ? _a : []);
139
- if (taxon.has('gbif')) {
140
- parents.push(taxon.get('gbif'));
145
+ const [...parents] = ((_a = taxon.get('ancestors_col')) !== null && _a !== void 0 ? _a : []);
146
+ if (taxon.has('accepted_col')) {
147
+ parents.push(taxon.get('accepted_col'));
148
+ }
149
+ else if (taxon.has('col')) {
150
+ parents.push(taxon.get('col'));
141
151
  }
142
152
  return parents;
143
153
  }
@@ -150,7 +160,15 @@ function getCoveringTaxon(taxa) {
150
160
  }
151
161
  }
152
162
  }
153
- return ancestors.length ? makeGbifUri(ancestors[ancestors.length - 1])['@id'] : null;
163
+ if (!ancestors.length) {
164
+ return null;
165
+ }
166
+ const ancestor = ancestors[ancestors.length - 1];
167
+ const ancestorEntity = allTaxa.find(taxon => taxon.get('col') === ancestor);
168
+ if (ancestorEntity) {
169
+ return makeTaxonUri(ancestorEntity.get('id'))['@id'];
170
+ }
171
+ return makeColUri(ancestor)['@id'];
154
172
  }
155
173
  function mapEntities(names, entities) {
156
174
  const result = [];
@@ -171,11 +189,17 @@ function makeWikidataUri(qid) {
171
189
  return { '@id': `http://www.wikidata.org/entity/${qid}` };
172
190
  }
173
191
  function makeGbifUri(id) {
174
- return { '@id': `https://gbif.org/species/${id}` };
192
+ return { '@id': `https://www.gbif.org/species/${id}` };
193
+ }
194
+ function makeColUri(id) {
195
+ return { '@id': `https://www.catalogueoflife.org/data/taxon/${id}` };
175
196
  }
176
197
  function makeWorkUri(id) {
177
198
  return { '@id': `${PREFIX}catalog/${id}` };
178
199
  }
200
+ function makeTaxonUri(id) {
201
+ return { '@id': `${PREFIX}taxon/${id}` };
202
+ }
179
203
  function makeScientificNameUri(id) {
180
204
  const resource = id.split(':').slice(0, -1).join(':');
181
205
  return { '@id': `${PREFIX}resource/${resource}#${id}` };
@@ -226,7 +250,7 @@ function makeLinkedDataForPublisher(publisher) {
226
250
  }
227
251
  function makeLinkedDataForTaxon(taxon) {
228
252
  const node = {
229
- '@id': `${PREFIX}taxon/${taxon.get('id')}`,
253
+ '@id': makeTaxonUri(taxon.get('id'))['@id'],
230
254
  '@type': 'dwc:Taxon',
231
255
  'dwc:scientificName': taxon.get('name'),
232
256
  };
@@ -237,6 +261,9 @@ function makeLinkedDataForTaxon(taxon) {
237
261
  if (taxon.has('qid')) {
238
262
  ids.push(makeWikidataUri(taxon.get('qid')));
239
263
  }
264
+ if (taxon.has('col')) {
265
+ ids.push(makeColUri(taxon.get('col')));
266
+ }
240
267
  if (taxon.has('gbif')) {
241
268
  ids.push(makeGbifUri(taxon.get('gbif')));
242
269
  }
@@ -247,32 +274,89 @@ function makeLinkedDataForTaxon(taxon) {
247
274
  }
248
275
  function makeLinkedDataForTaxa(files) {
249
276
  var _a;
250
- const nodes = [];
251
- const gbifTaxa = {};
277
+ const nodes = {};
278
+ const parents = {};
279
+ const colIndex = files.taxa.entities.reduce((index, taxon) => {
280
+ if (taxon.has('col')) {
281
+ index[taxon.get('col')] = taxon.get('id');
282
+ }
283
+ return index;
284
+ }, {});
285
+ const getUriFromCol = (col) => {
286
+ if (col == null) {
287
+ // No parent -> Biota
288
+ return makeTaxonUri('T141');
289
+ }
290
+ else if (col in colIndex) {
291
+ return makeTaxonUri(colIndex[col]);
292
+ }
293
+ else {
294
+ return makeColUri(col);
295
+ }
296
+ };
297
+ const getUriFromGbif = (gbif) => {
298
+ if (gbif == null) {
299
+ // No parent -> Biota
300
+ return makeTaxonUri('T141');
301
+ }
302
+ else {
303
+ return makeGbifUri(gbif);
304
+ }
305
+ };
306
+ const addParent = (child, parent) => {
307
+ if (!parents[child]) {
308
+ parents[child] = new Set();
309
+ }
310
+ parents[child].add(parent);
311
+ };
252
312
  for (const taxon of files.taxa.entities) {
253
313
  const node = makeLinkedDataForTaxon(taxon);
254
- const ancestors = (_a = taxon.get('ancestors_gbif')) !== null && _a !== void 0 ? _a : [];
255
- if (ancestors.length) {
256
- node['dwc:parentNameUsageID'] = makeGbifUri(ancestors[ancestors.length - 1]);
314
+ const ancestors = (_a = taxon.get('ancestors_col')) !== null && _a !== void 0 ? _a : [];
315
+ if (taxon.get('id') !== 'T141') {
316
+ node['dwc:parentNameUsageID'] = getUriFromCol(ancestors.length ? ancestors[ancestors.length - 1] : null);
257
317
  }
258
- else if (taxon.get('id') !== 'T141') {
259
- node['dwc:parentNameUsageID'] = { '@id': `${PREFIX}taxon/T141` };
318
+ if (taxon.has('accepted_col')) {
319
+ node['dwc:acceptedNameUsageID'] = getUriFromCol(taxon.get('accepted_col'));
260
320
  }
261
321
  for (let i = 0; i < ancestors.length; i++) {
262
- gbifTaxa[ancestors[i]] = Object.assign(Object.assign({}, makeGbifUri(ancestors[i])), { 'dwc:taxonRank': makeTaxonRankUri(GBIF_RANKS[i]), 'dwc:parentNameUsageID': i ? makeGbifUri(ancestors[i - 1]) : { '@id': `${PREFIX}taxon/T141` } });
322
+ const parent = getUriFromCol(i ? ancestors[i - 1] : null);
323
+ addParent(getUriFromCol(ancestors[i])['@id'], parent['@id']);
324
+ }
325
+ if (taxon.has('children_col')) {
326
+ const children = taxon.get('children_col');
327
+ for (const child of children) {
328
+ addParent(getUriFromCol(child)['@id'], node['@id']);
329
+ }
330
+ }
331
+ // GBIF
332
+ if (taxon.has('ancestors_gbif')) {
333
+ const ancestors = taxon.get('ancestors_gbif');
334
+ for (let i = 0; i < ancestors.length; i++) {
335
+ const ancestor = makeGbifUri(ancestors[i])['@id'];
336
+ addParent(ancestor, getUriFromGbif(ancestors[i - 1])['@id']);
337
+ nodes[ancestor] = { '@id': ancestor, 'dwc:taxonRank': makeTaxonRankUri(GBIF_RANKS[i]) };
338
+ }
263
339
  }
264
340
  if (taxon.has('children_gbif')) {
265
341
  const children = taxon.get('children_gbif');
266
- node['@reverse'] = { 'dwc:parentNameUsageID': children.map(makeGbifUri) };
267
342
  const childRank = GBIF_RANKS[ancestors.length];
268
343
  for (const child of children) {
269
- gbifTaxa[child] = Object.assign(Object.assign({}, makeGbifUri(child)), { 'dwc:taxonRank': makeTaxonRankUri(childRank) });
344
+ const childUri = makeGbifUri(child)['@id'];
345
+ addParent(childUri, node['@id']);
346
+ nodes[childUri] = { '@id': childUri, 'dwc:taxonRank': makeTaxonRankUri(childRank) };
270
347
  }
271
348
  }
272
- nodes.push(node);
349
+ nodes[node['@id']] = node;
350
+ }
351
+ for (const child in parents) {
352
+ if (!nodes[child]) {
353
+ nodes[child] = { '@id': child };
354
+ }
355
+ for (const parent of parents[child]) {
356
+ nodes[child]['dwc:parentNameUsageID'] = { '@id': parent };
357
+ }
273
358
  }
274
- nodes.push(...Object.values(gbifTaxa));
275
- return nodes;
359
+ return Object.values(nodes);
276
360
  }
277
361
  function makeLinkedDataForScientificName(name) {
278
362
  const node = Object.assign(Object.assign({}, makeScientificNameUri(name.scientificNameID)), { '@type': 'dwc:Taxon' });
@@ -298,12 +382,12 @@ function makeLinkedDataForScientificName(name) {
298
382
  node['dwc:parentNameUsageID'] = makeScientificNameUri(name.parentNameUsageID);
299
383
  }
300
384
  const identifiers = [];
385
+ if (name.colTaxonID) {
386
+ identifiers.push(makeColUri(name.colTaxonID));
387
+ }
301
388
  if (name.gbifTaxonID) {
302
389
  identifiers.push(makeGbifUri(name.gbifTaxonID));
303
390
  }
304
- if (name.colTaxonID) {
305
- identifiers.push({ '@id': `https://www.checklistbank.org/dataset/309120/taxon/${name.colTaxonID}` });
306
- }
307
391
  if (identifiers.length) {
308
392
  node['dwc:taxonID'] = identifiers;
309
393
  }
@@ -341,7 +425,7 @@ function makeLinkedDataForResource(work, files, resourceId) {
341
425
  const taxonNames = (_c = resource.get('taxon')) !== null && _c !== void 0 ? _c : work.get('taxon');
342
426
  if (taxonNames) {
343
427
  const taxa = mapEntities(taxonNames, files.taxa.entities);
344
- const coveringTaxon = taxa.length ? getCoveringTaxon(taxa) : null;
428
+ const coveringTaxon = taxa.length ? getCoveringTaxon(taxa, files.taxa.entities) : null;
345
429
  if (coveringTaxon !== null) {
346
430
  node['ac:taxonCoverage'] = { '@id': coveringTaxon };
347
431
  }
@@ -408,6 +492,7 @@ function makeLinkedDataForResource(work, files, resourceId) {
408
492
  function makeLinkedDataForWork(work, files) {
409
493
  const id = work.get('id');
410
494
  const node = makeWorkUri(id);
495
+ node['@reverse'] = {};
411
496
  const languages = work.get('language');
412
497
  node['dcterms:language'] = languages.map(language => ({ '@id': `http://id.loc.gov/vocabulary/iso639-1/${language}` }));
413
498
  if (work.has('title')) {
@@ -536,46 +621,60 @@ function makeLinkedDataForWork(work, files) {
536
621
  }
537
622
  });
538
623
  }
624
+ if (work.has('part_of')) {
625
+ const containers = work.get('part_of');
626
+ if (work.get('key_type').includes('supplement')) {
627
+ node['bibo:annotates'] = containers.map(makeWorkUri);
628
+ }
629
+ else {
630
+ node['dcterms:isPartOf'] = containers.map(makeWorkUri);
631
+ for (const container of containers) {
632
+ if (!Array.isArray(node['@reverse']['dcterms:hasPart'])) {
633
+ node['@reverse']['dcterms:hasPart'] = [];
634
+ }
635
+ node['@reverse']['dcterms:hasPart'].push(makeWorkUri(container));
636
+ }
637
+ }
638
+ }
639
+ if (work.has('listed_in')) {
640
+ const referers = work.get('listed_in');
641
+ node['bibo:citedBy'] = node['dcterms:isReferencedBy'] = referers.map(makeWorkUri);
642
+ for (const referer of referers) {
643
+ if (!Array.isArray(node['@reverse']['bibo:cites'])) {
644
+ node['@reverse']['bibo:cites'] = [];
645
+ }
646
+ node['@reverse']['bibo:cites'].push(makeWorkUri(referer));
647
+ if (!Array.isArray(node['@reverse']['dcterms:references'])) {
648
+ node['@reverse']['dcterms:references'] = [];
649
+ }
650
+ node['@reverse']['dcterms:references'].push(makeWorkUri(referer));
651
+ }
652
+ }
653
+ if (work.has('version_of')) {
654
+ const originals = work.get('version_of');
655
+ const originalLanguages = originals.map(id => {
656
+ var _a;
657
+ const [workId, resourceId] = id.split(':');
658
+ const original = files.catalog.get(workId);
659
+ const language = original.get('language').join();
660
+ if (resourceId) {
661
+ const resourceLanguage = (_a = files.resources[id].metadata.catalog) === null || _a === void 0 ? void 0 : _a.language;
662
+ if (resourceLanguage) {
663
+ return resourceLanguage.join(',');
664
+ }
665
+ }
666
+ return language;
667
+ });
668
+ node['bibo:translationOf'] = originals.filter((_, i) => originalLanguages[i] !== languages.join()).map(makeWorkUri);
669
+ node['dcterms:isVersionOf'] = originals.filter(id => work.get('id') !== id).map(makeWorkUri);
670
+ }
539
671
  return node;
540
672
  }
541
- function makeLinkedDataForWorks(files) {
542
- const nodes = {};
673
+ function* makeLinkedDataForWorks(files) {
543
674
  const works = files.catalog;
544
- for (const work of works.entities) {
545
- nodes[work.get('id')] = makeLinkedDataForWork(work, files);
546
- }
547
675
  for (const work of works.entities) {
548
676
  const id = work.get('id');
549
- const node = nodes[id];
550
- const language = work.get('language');
551
- if (work.has('part_of')) {
552
- const containers = work.get('part_of');
553
- if (work.get('key_type').includes('supplement')) {
554
- node['bibo:annotates'] = containers.map(makeWorkUri);
555
- }
556
- else {
557
- node['dcterms:isPartOf'] = containers.map(makeWorkUri);
558
- for (const container of containers) {
559
- if (!Array.isArray(nodes[container]['dcterms:hasPart'])) {
560
- nodes[container]['dcterms:hasPart'] = [];
561
- }
562
- nodes[container]['dcterms:hasPart'].push(makeWorkUri(id));
563
- }
564
- }
565
- }
566
- if (work.has('listed_in')) {
567
- const referers = work.get('listed_in');
568
- node['bibo:citedBy'] = node['dcterms:isReferencedBy'] = referers.map(makeWorkUri);
569
- for (const referer of referers) {
570
- nodes[referer]['bibo:cites'] = nodes[referer]['dcterms:references'] = makeWorkUri(id);
571
- }
572
- }
573
- if (work.has('version_of')) {
574
- const originals = work.get('version_of');
575
- const originalLanguages = originals.map(id => works.get(id).get('language').join());
576
- node['bibo:translationOf'] = originals.filter((_, i) => originalLanguages[i] !== language.join()).map(makeWorkUri);
577
- node['dcterms:isVersionOf'] = originals.filter(id => work.get('id') !== id).map(makeWorkUri);
578
- }
677
+ const node = makeLinkedDataForWork(work, files);
579
678
  let resourceIndex = 1;
580
679
  let resourceId;
581
680
  while ((resourceId = `${id}:${resourceIndex++}`) in files.resources) {
@@ -589,26 +688,52 @@ function makeLinkedDataForWorks(files) {
589
688
  }
590
689
  resource['dcterms:isPartOf'].push({ '@id': node['@id'] });
591
690
  }
691
+ yield node;
592
692
  }
593
- return Object.values(nodes);
594
693
  }
595
- function writeOutput(document_1) {
596
- return __awaiter(this, arguments, void 0, function* (document, format = 'jsonld') {
597
- if (format === 'jsonld') {
598
- process.stdout.write(JSON.stringify(document, null, 2));
599
- return;
600
- }
601
- const nquads = yield jsonld.toRDF(document, { format: 'application/n-quads' });
602
- if (format === 'nquads') {
603
- process.stdout.write(nquads);
604
- return;
605
- }
606
- const parser = new N3.StreamParser();
607
- const writer = new N3.StreamWriter({ prefixes: PREFIXES });
608
- parser.write(nquads);
609
- parser.pipe(writer);
610
- writer.pipe(process.stdout);
611
- });
694
+ class Writer {
695
+ constructor(format) {
696
+ this.format = format;
697
+ this.issuer = new jsonld.util.IdentifierIssuer('_:b');
698
+ this.n3parser = new N3.StreamParser();
699
+ this.n3writer = new N3.StreamWriter({ prefixes: PREFIXES });
700
+ if (this.format === 'jsonld') {
701
+ const [start] = JSON.stringify({ '@context': PREFIXES, '@graph': [] }, null, 2).split(/(?<="@graph": \[)/);
702
+ process.stdout.write(start + '\n');
703
+ }
704
+ else if (this.format === 'nquads') {
705
+ // no preparation required
706
+ }
707
+ else {
708
+ this.n3parser.pipe(this.n3writer);
709
+ this.n3writer.pipe(process.stdout);
710
+ }
711
+ }
712
+ write(graph) {
713
+ return __awaiter(this, void 0, void 0, function* () {
714
+ if (this.format === 'jsonld') {
715
+ for (const entity of graph) {
716
+ process.stdout.write(JSON.stringify(entity, null, 2).replace(/^/gm, ' ') + ',\n');
717
+ }
718
+ return;
719
+ }
720
+ const document = {
721
+ '@context': PREFIXES,
722
+ '@graph': graph
723
+ };
724
+ const nquads = (yield jsonld.toRDF(document, { format: 'application/n-quads', issuer: this.issuer }));
725
+ if (this.format === 'nquads') {
726
+ process.stdout.write(nquads);
727
+ return;
728
+ }
729
+ this.n3parser.write(nquads);
730
+ });
731
+ }
732
+ end() {
733
+ if (this.format === 'jsonld') {
734
+ process.stdout.write(' null\n ]\n}\n');
735
+ }
736
+ }
612
737
  }
613
738
  function main() {
614
739
  return __awaiter(this, void 0, void 0, function* () {
@@ -644,8 +769,11 @@ function main() {
644
769
  }, {}));
645
770
  files.resources[id] = { metadata: resources[id], taxa };
646
771
  }
772
+ const writer = new Writer(args.values.format);
773
+ for (const entity of makeLinkedDataForWorks(files)) {
774
+ yield writer.write([entity]);
775
+ }
647
776
  const graph = [
648
- ...makeLinkedDataForWorks(files),
649
777
  ...makeLinkedDataForTaxa(files),
650
778
  ];
651
779
  for (const entity of files.authors.entities) {
@@ -657,11 +785,8 @@ function main() {
657
785
  for (const entity of files.publishers.entities) {
658
786
  graph.push(makeLinkedDataForPublisher(entity));
659
787
  }
660
- const document = {
661
- '@context': PREFIXES,
662
- '@graph': graph
663
- };
664
- writeOutput(document, args.values.format);
788
+ yield writer.write(graph);
789
+ writer.end();
665
790
  });
666
791
  }
667
792
  main().catch((error) => {
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
37
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
38
+ return new (P || (P = Promise))(function (resolve, reject) {
39
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
40
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
41
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
42
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
43
+ });
44
+ };
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ const fs_1 = require("fs");
47
+ const path = __importStar(require("path"));
48
+ const index_1 = require("../index");
49
+ function loadCatalogFile(file, base) {
50
+ return __awaiter(this, void 0, void 0, function* () {
51
+ const contents = yield fs_1.promises.readFile(path.join(base, `${file}.csv`), 'utf8');
52
+ return index_1.catalog.loadData(contents, file);
53
+ });
54
+ }
55
+ function loadResourcesIndex(base) {
56
+ return __awaiter(this, void 0, void 0, function* () {
57
+ const contents = yield fs_1.promises.readFile(path.join(base, 'resources', 'index.json'), 'utf8');
58
+ return JSON.parse(contents);
59
+ });
60
+ }
61
+ function getAll(entity, field) {
62
+ const value = entity.get(field);
63
+ if (Array.isArray(value)) {
64
+ return value;
65
+ }
66
+ else if (value) {
67
+ return [value];
68
+ }
69
+ else {
70
+ return [];
71
+ }
72
+ }
73
+ function checkIntegrity(catalog, resources, field, sheet) {
74
+ const errors = [];
75
+ const index = new Set();
76
+ for (const entity of sheet) {
77
+ for (const value of getAll(entity, 'name')) {
78
+ index.add(value);
79
+ }
80
+ }
81
+ for (const entity of catalog) {
82
+ const id = entity.get('id');
83
+ for (const value of getAll(entity, field)) {
84
+ if (!index.has(value)) {
85
+ errors.push({
86
+ entity: id,
87
+ field: field,
88
+ error: `Value "${value}" is not mapped`
89
+ });
90
+ }
91
+ }
92
+ }
93
+ for (const id in resources) {
94
+ const resource = resources[id];
95
+ if (!resource.catalog || !Object.prototype.hasOwnProperty.call(resource.catalog, field)) {
96
+ continue;
97
+ }
98
+ const value = resource.catalog[field];
99
+ const values = Array.isArray(value) ? value : [value];
100
+ for (const value of values) {
101
+ if (!index.has(value)) {
102
+ errors.push({
103
+ entity: id,
104
+ field: field,
105
+ error: `Value "${value}" is not mapped`
106
+ });
107
+ }
108
+ }
109
+ }
110
+ return errors;
111
+ }
112
+ function main(args) {
113
+ return __awaiter(this, void 0, void 0, function* () {
114
+ const base = path.resolve(args[0]);
115
+ const [catalog, authors, places, publishers, taxa, resources] = yield Promise.all([
116
+ loadCatalogFile('catalog', base),
117
+ loadCatalogFile('authors', base),
118
+ loadCatalogFile('places', base),
119
+ loadCatalogFile('publishers', base),
120
+ loadCatalogFile('taxa', base),
121
+ loadResourcesIndex(base)
122
+ ]);
123
+ const errors = [
124
+ ...checkIntegrity(catalog, resources, 'author', authors),
125
+ ...checkIntegrity(catalog, resources, 'publisher', publishers),
126
+ ...checkIntegrity(catalog, resources, 'taxon', taxa),
127
+ ...checkIntegrity(catalog, resources, 'region', places)
128
+ ];
129
+ if (errors.length) {
130
+ console.table(errors);
131
+ process.exit(1);
132
+ }
133
+ });
134
+ }
135
+ main(process.argv.slice(2));
@@ -233,7 +233,17 @@ function parseName(name, rank, parent) {
233
233
  name = name.slice(species.length + 1);
234
234
  }
235
235
  // Remove rank abbreviations
236
- name = name.replace(/^(st|r|ab|f|var|ssp|subsp)\. /, '');
236
+ for (const abbreviation in RANK_LABELS_REVERSE) {
237
+ const prefix = abbreviation + '. ';
238
+ if (name.startsWith(prefix)) {
239
+ const abbreviationRank = RANK_LABELS_REVERSE[abbreviation];
240
+ if (rank !== abbreviationRank) {
241
+ throw new RecoverableSyntaxError(`Infraspecific rank label has an unexpected rank: "${abbreviation}." meaning ${abbreviationRank}, expected ${rank}`, item);
242
+ }
243
+ name = name.slice(prefix.length);
244
+ break;
245
+ }
246
+ }
237
247
  }
238
248
  }
239
249
  else if (compareRanks('genus', rank) <= 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larsgw/formica",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
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",
@@ -10,6 +10,7 @@
10
10
  "bin": {
11
11
  "loir-validate-catalog": "lib/bin/validate-catalog.js",
12
12
  "loir-validate-resources": "lib/bin/validate-resources-text.js",
13
+ "loir-validate-integrity": "lib/bin/validate-integrity.js",
13
14
  "loir-resources-process": "lib/bin/process-resources.js",
14
15
  "loir-resources-index": "lib/bin/process-resources-index.js",
15
16
  "loir-resources-check-diff": "lib/bin/check-resources-diff.js",
@@ -52,6 +53,7 @@
52
53
  "@typescript-eslint/parser": "^8.32.0",
53
54
  "conventional-changelog-cli": "^5.0.0",
54
55
  "eslint": "^10.6.0",
56
+ "patch-package": "^8.0.1",
55
57
  "typescript": "^6.0.3"
56
58
  }
57
59
  }