@larsgw/formica 0.9.2 → 0.10.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.
Files changed (50) hide show
  1. package/lib/bin/process-resources-index.js +0 -0
  2. package/lib/bin/process-resources.d.ts +6 -1
  3. package/lib/bin/process-resources.js +116 -25
  4. package/lib/bin/validate-catalog.js +0 -0
  5. package/lib/bin/validate-resources-text.js +3 -1
  6. package/lib/resources/parse-text.js +6 -3
  7. package/lib/taxon-names/index.js +1 -1
  8. package/package.json +15 -13
  9. package/.gitattributes +0 -1
  10. package/.github/workflows/ci.yml +0 -27
  11. package/CHANGELOG.md +0 -390
  12. package/eslint.config.js +0 -34
  13. package/lib/bin/SHEETS.js +0 -0
  14. package/lib/bin/clean-links.d.ts +0 -2
  15. package/lib/bin/clean-links.js +0 -170
  16. package/lib/bin/download-place-shapes.js +0 -188
  17. package/lib/bin/index-place-shapes.js +0 -115
  18. package/lib/bin/process-resources-problems.js +0 -177
  19. package/lib/bin/validate-linked-data.js +0 -0
  20. package/lib/resources/content/clavis.js +0 -10
  21. package/lib/resources/content/index.js +0 -0
  22. package/lib/resources/content/sdd.js +0 -151
  23. package/lib/resources/sdd.js +0 -78
  24. package/src/bin/generate-linked-data.ts +0 -762
  25. package/src/bin/process-resources-index.ts +0 -100
  26. package/src/bin/process-resources.ts +0 -510
  27. package/src/bin/util.ts +0 -74
  28. package/src/bin/validate-catalog.ts +0 -122
  29. package/src/bin/validate-resources-text.ts +0 -25
  30. package/src/catalog/entities.ts +0 -62
  31. package/src/catalog/entity.ts +0 -116
  32. package/src/catalog/index.ts +0 -33
  33. package/src/catalog/tables/author.ts +0 -15
  34. package/src/catalog/tables/index.ts +0 -14
  35. package/src/catalog/tables/place.ts +0 -14
  36. package/src/catalog/tables/publisher.ts +0 -15
  37. package/src/catalog/tables/taxon.ts +0 -17
  38. package/src/catalog/tables/work.ts +0 -65
  39. package/src/catalog/value.ts +0 -51
  40. package/src/csv.ts +0 -33
  41. package/src/index.ts +0 -4
  42. package/src/module.d.ts +0 -148
  43. package/src/resources/diff-resource.ts +0 -226
  44. package/src/resources/index.ts +0 -4
  45. package/src/resources/parse-name.ts +0 -392
  46. package/src/resources/parse-text.ts +0 -408
  47. package/src/resources/resource.ts +0 -10
  48. package/src/taxon-names/index.ts +0 -79
  49. package/test/resources.js +0 -374
  50. package/tsconfig.json +0 -15
@@ -1,762 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { existsSync as doesFileExist, promises as fs } from 'fs'
4
- import * as path from 'path'
5
- import * as util from 'util'
6
-
7
- import type { JsonLdDocument, NodeObject } from 'jsonld'
8
- import * as jsonld from 'jsonld'
9
- import * as N3 from 'n3'
10
-
11
- import { catalog } from '../index'
12
- import { WorkResource } from '../resources/resource'
13
- import { parseCsv } from '../csv'
14
-
15
- const SHEETS = ['catalog', 'authors', 'places', 'publishers', 'taxa'] as const
16
-
17
- interface Resource {
18
- metadata: ResourceMetadata,
19
- taxa: AmendedTaxon[],
20
- }
21
-
22
- interface Catalog {
23
- catalog: catalog.Entities,
24
- authors: catalog.Entities,
25
- places: catalog.Entities,
26
- publishers: catalog.Entities,
27
- taxa: catalog.Entities,
28
-
29
- resources: Record<ResourceId, Resource>,
30
- }
31
-
32
- const PREFIX = 'https://purl.org/identification-resources/'
33
- const HANDLE_PREFIX = 'https://hdl.handle.net/'
34
- const SCOPES: Record<string, [string, string]> = {
35
- // animal life stage
36
- 'adults': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/adult'],
37
- 'pupae': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/pupa'],
38
- 'juveniles': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/juvenile'],
39
- 'subimagos': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/juvenile'],
40
- 'larvae': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
41
- 'larvae (instar V)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
42
- 'larvae (instar IV)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
43
- 'larvae (instar III)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
44
- 'larvae (instar I)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
45
- 'nymphs': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
46
- 'nypmhs': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
47
- 'nymphs (instar V)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
48
- 'nymphs (instar IV)': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/larva'],
49
- 'eggs': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/embryo'],
50
-
51
- // plant life stage
52
- 'flowering plants': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/adult'],
53
- 'fruiting plants': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/adult'],
54
- 'without sporangia': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/juvenile'],
55
- 'with sporangia': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/adult'],
56
- 'teleomorphs': ['dwciri:lifeStage', 'http://rs.gbif.org/vocabulary/gbif/life_stage/adult'],
57
-
58
- // sex
59
- 'females': ['dwciri:sex', 'http://rs.gbif.org/vocabulary/gbif/sex/female'],
60
- 'males': ['dwciri:sex', 'http://rs.gbif.org/vocabulary/gbif/sex/male'],
61
- 'male': ['dwciri:sex', 'http://rs.gbif.org/vocabulary/gbif/sex/male'],
62
-
63
- // caste
64
- 'queens': ['dwc:caste', 'queen'],
65
- 'workers': ['dwc:caste', 'worker'],
66
- 'soldiers': ['dwc:caste', 'soldier'],
67
- 'alatae': ['dwc:caste', 'alate'],
68
- 'apterae': ['dwc:caste', 'aptera'],
69
- 'viviparae': ['dwc:caste', 'vivipara'],
70
-
71
- // evidence
72
- 'nests': ['ac:subjectPartLiteral', 'nest'],
73
- 'galls': ['ac:subjectPartLiteral', 'gall'],
74
- 'puparia': ['ac:subjectPartLiteral', 'puparium'],
75
- 'eggcases': ['ac:subjectPart', 'http://rs.tdwg.org/acpart/values/p0031'],
76
- 'bones': ['ac:subjectPartLiteral', 'skeleton'],
77
- 'bones (skulls)': ['ac:subjectPart', 'http://rs.tdwg.org/acpart/values/p0027'],
78
- 'bones (upper jaws)': ['ac:subjectPart', 'http://rs.tdwg.org/acpart/values/p0028'],
79
- 'bones (lower jaws)': ['ac:subjectPart', 'http://rs.tdwg.org/acpart/values/p0029'],
80
- }
81
- const PREFIXES = {
82
- 'ac': 'http://rs.tdwg.org/ac/terms/',
83
- 'bibo': 'http://purl.org/ontology/bibo/',
84
- 'dcterms': 'http://purl.org/dc/terms/',
85
- 'dwc': 'http://rs.tdwg.org/dwc/terms/',
86
- 'dwciri': 'http://rs.tdwg.org/dwc/iri/',
87
- 'foaf': 'http://xmlns.com/foaf/0.1/',
88
- 'owl': 'http://www.w3.org/2002/07/owl#',
89
- 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
90
- 'schema': 'https://schema.org/',
91
- 'xsd': 'http://www.w3.org/2001/XMLSchema#',
92
- }
93
- const DWC_FIELDS: Record<string, string> = {
94
- scientificName: 'dwc:scientificName',
95
- scientificNameAuthorship: 'dwc:scientificNameAuthorship',
96
- genericName: 'dwc:genericName',
97
- infragenericEpithet: 'dwc:infragenericEpithet',
98
- specificEpithet: 'dwc:specificEpithet',
99
- infraspecificEpithet: 'dwc:infraspecificEpithet',
100
- taxonRank: 'dwc:taxonRank',
101
- taxonRemarks: 'dwc:taxonRemarks',
102
- taxonomicStatus: 'dwc:taxonomicStatus',
103
- verbatimIdentification: 'dwc:verbatimIdentification',
104
- }
105
- const GBIF_RANKS = ['kingdom', 'phylum', 'class', 'order', 'family', 'genus', 'species', 'subspecies']
106
- const GBIF_VOCAB_RANKS = ['domain', 'kingdom', 'subkingdom', 'superphylum', 'phylum', 'subphylum', 'superclass', 'class', 'subclass', 'supercohort', 'cohort', 'subcohort', 'superorder', 'order', 'suborder', 'infraorder', 'superfamily', 'family', 'subfamily', 'tribe', 'subtribe', 'genus', 'subgenus', 'section', 'subsection', 'series', 'subseries', 'speciesAggregate', 'species', 'subspecificAggregate', 'subspecies', 'variety', 'subvariety', 'form', 'subform', 'cultivarGroup', 'cultivar', 'strain']
107
- const STATUSES: Record<string, string> = {
108
- 'accepted': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/accepted',
109
- 'misapplied': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/misapplied',
110
- 'proparte synonym': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/proParteSynonym',
111
- 'synonym': 'http://rs.gbif.org/vocabulary/gbif/taxonomicStatus/synonym',
112
- }
113
-
114
- function getCoveringTaxon (taxa: catalog.Entity[]): string|null {
115
- if (taxa.length === 1 && !taxa[0].has('gbif')) {
116
- return `${PREFIX}taxon/${taxa[0].get('id')}`
117
- }
118
-
119
- function getAncestors (taxon: catalog.Entity): string[] {
120
- const [...parents] = (taxon.get('parent_taxa') ?? []) as string[]
121
- if (taxon.has('gbif')) {
122
- parents.push(taxon.get('gbif') as string)
123
- }
124
- return parents
125
- }
126
-
127
- const ancestors = getAncestors(taxa[0])
128
- for (const taxon of taxa.slice(1)) {
129
- const otherAncestors = getAncestors(taxon)
130
-
131
- for (let i = 0; i < ancestors.length; i++) {
132
- if (ancestors[i] !== otherAncestors[i]) {
133
- ancestors.splice(i)
134
- }
135
- }
136
- }
137
-
138
- return ancestors.length ? makeGbifUri(ancestors[ancestors.length - 1])['@id'] as string : null
139
- }
140
-
141
- function mapEntities (names: string[], entities: catalog.Entity[]): catalog.Entity[] {
142
- const result = []
143
-
144
- for (const name of names) {
145
- const entity = entities.find(entity => {
146
- const values = entity.get('name')
147
- return Array.isArray(values) ? values.includes(name) : values === name
148
- })
149
-
150
- if (!entity) {
151
- console.error('Unmapped entity:', name)
152
- continue
153
- }
154
-
155
- result.push(entity as catalog.Entity)
156
- }
157
-
158
- return result
159
- }
160
-
161
- function makeWikidataUri (qid: string): NodeObject {
162
- return { '@id': `http://www.wikidata.org/entity/${qid}` }
163
- }
164
-
165
- function makeGbifUri (id: string): NodeObject {
166
- return { '@id': `https://gbif.org/species/${id}` }
167
- }
168
-
169
- function makeWorkUri (id: string): NodeObject {
170
- return { '@id': `${PREFIX}catalog/${id}` }
171
- }
172
-
173
- function makeScientificNameUri (id: string): NodeObject {
174
- const resource = id.split(':').slice(0, -1).join(':')
175
- return { '@id': `${PREFIX}resource/${resource}#${id}` }
176
- }
177
-
178
- function makeTaxonRankUri (rank: string): NodeObject|string {
179
- if (GBIF_VOCAB_RANKS.includes(rank)) {
180
- return { '@id': `http://rs.gbif.org/vocabulary/gbif/rank/${rank}` }
181
- } else {
182
- return rank
183
- }
184
- }
185
-
186
- function makeTaxonomicStatusUri (status: keyof typeof STATUSES): NodeObject {
187
- return { '@id': STATUSES[status] }
188
- }
189
-
190
- function makeLinkedDataForAuthor (author: catalog.Entity): NodeObject {
191
- const node: NodeObject = {
192
- '@id': `${PREFIX}author/${author.get('id')}`,
193
- '@type': 'foaf:Person',
194
- 'foaf:name': author.get('display_name')
195
- }
196
-
197
- if (author.has('qid')) {
198
- node['owl:sameAs'] = makeWikidataUri(author.get('qid') as string)
199
- }
200
-
201
- return node
202
- }
203
-
204
- function makeLinkedDataForPlace (place: catalog.Entity): NodeObject {
205
- const node: NodeObject = {
206
- '@id': `${PREFIX}place/${place.get('id')}`,
207
- '@type': 'dcterms:Location',
208
- 'dcterms:title': place.get('display_name'),
209
- }
210
-
211
- if (place.has('qid')) {
212
- node['owl:sameAs'] = makeWikidataUri(place.get('qid') as string)
213
- }
214
-
215
- return node
216
- }
217
-
218
- function makeLinkedDataForPublisher (publisher: catalog.Entity): NodeObject {
219
- const node: NodeObject = {
220
- '@id': `${PREFIX}publisher/${publisher.get('id')}`,
221
- '@type': 'foaf:Organization',
222
- 'foaf:name': publisher.get('display_name')
223
- }
224
-
225
- if (publisher.has('qid')) {
226
- node['owl:sameAs'] = makeWikidataUri(publisher.get('qid') as string)
227
- }
228
-
229
- return node
230
- }
231
-
232
- function makeLinkedDataForTaxon (taxon: catalog.Entity): NodeObject {
233
- const node: NodeObject = {
234
- '@id': `${PREFIX}taxon/${taxon.get('id')}`,
235
- '@type': 'dwc:Taxon',
236
- 'dwc:scientificName': taxon.get('name'),
237
- }
238
-
239
- if (taxon.has('rank')) {
240
- node['dwc:taxonRank'] = makeTaxonRankUri(taxon.get('rank') as string)
241
- }
242
-
243
- const ids = []
244
- if (taxon.has('qid')) {
245
- ids.push(makeWikidataUri(taxon.get('qid') as string))
246
- }
247
- if (taxon.has('gbif')) {
248
- ids.push(makeGbifUri(taxon.get('gbif') as string))
249
- }
250
- if (ids.length) {
251
- node['owl:sameAs'] = ids
252
- }
253
-
254
- return node
255
- }
256
-
257
- function makeLinkedDataForTaxa (files: Catalog): NodeObject[] {
258
- const nodes = []
259
- const gbifTaxa: Record<string, NodeObject> = {}
260
-
261
- for (const taxon of files.taxa.entities) {
262
- const node = makeLinkedDataForTaxon(taxon)
263
-
264
- const ancestors = taxon.get('ancestors_gbif') ?? []
265
- if (ancestors.length) {
266
- node['dwc:parentNameUsageID'] = makeGbifUri(ancestors[ancestors.length - 1])
267
- } else if (taxon.get('id') !== 'T141') {
268
- node['dwc:parentNameUsageID'] = { '@id': `${PREFIX}taxon/T141` }
269
- }
270
-
271
- for (let i = 0; i < ancestors.length; i++) {
272
- gbifTaxa[ancestors[i]] = {
273
- ...makeGbifUri(ancestors[i]),
274
- 'dwc:taxonRank': makeTaxonRankUri(GBIF_RANKS[i]),
275
- 'dwc:parentNameUsageID': i ? makeGbifUri(ancestors[i - 1]) : { '@id': `${PREFIX}taxon/T141` }
276
- }
277
- }
278
-
279
- if (taxon.has('children_gbif')) {
280
- const children = taxon.get('children_gbif') as string[]
281
- node['@reverse'] = { 'dwc:parentNameUsageID': children.map(makeGbifUri) as unknown as string }
282
-
283
- const childRank = GBIF_RANKS[ancestors.length]
284
- for (const child of children) {
285
- gbifTaxa[child] = {
286
- ...makeGbifUri(child),
287
- 'dwc:taxonRank': makeTaxonRankUri(childRank),
288
- }
289
- }
290
- }
291
-
292
- nodes.push(node)
293
- }
294
-
295
- nodes.push(...Object.values(gbifTaxa))
296
-
297
- return nodes
298
- }
299
-
300
- function makeLinkedDataForScientificName (name: AmendedTaxon): NodeObject {
301
- const node: NodeObject = {
302
- ...makeScientificNameUri(name.scientificNameID),
303
- '@type': 'dwc:Taxon',
304
- }
305
-
306
- for (const field in DWC_FIELDS) {
307
- const value = name[field as keyof AmendedTaxon]
308
- if (value) {
309
- node[DWC_FIELDS[field]] = value as string
310
- }
311
- }
312
-
313
- if (GBIF_VOCAB_RANKS.includes(name.taxonRank)) {
314
- node[DWC_FIELDS.taxonRank] = makeTaxonRankUri(name.taxonRank)
315
- }
316
-
317
- if (name.taxonomicStatus in STATUSES) {
318
- node[DWC_FIELDS.taxonomicStatus] = makeTaxonomicStatusUri(name.taxonomicStatus)
319
- } else {
320
- console.error('Unmapped taxonomic status:', name.taxonomicStatus)
321
- }
322
-
323
- if (name.acceptedNameUsageID) {
324
- node['dwc:acceptedNameUsageID'] = makeScientificNameUri(name.acceptedNameUsageID)
325
- }
326
-
327
- if (name.parentNameUsageID) {
328
- node['dwc:parentNameUsageID'] = makeScientificNameUri(name.parentNameUsageID)
329
- }
330
-
331
- const identifiers = []
332
- if (name.gbifTaxonID) {
333
- identifiers.push(makeGbifUri(name.gbifTaxonID))
334
- }
335
- if (name.colTaxonID) {
336
- identifiers.push({ '@id': `https://www.checklistbank.org/dataset/309120/taxon/${name.colTaxonID}` })
337
- }
338
-
339
- if (identifiers.length) {
340
- node['dwc:taxonID'] = identifiers
341
- }
342
-
343
- return node
344
- }
345
-
346
- function makeLinkedDataForResource (work: catalog.Entity, files: Catalog, resourceId?: ResourceId): NodeObject {
347
- const resource = new WorkResource({})
348
-
349
- if (!resourceId) {
350
- resourceId = `${work.get('id')}:0`
351
- }
352
-
353
- if (files.resources[resourceId] && files.resources[resourceId].metadata.catalog) {
354
- resource.fields = files.resources[resourceId].metadata.catalog as Record<string, Value>
355
- }
356
-
357
- if (!resource.has('language')) {
358
- resource.fields.language = work.get('language') as Value
359
- }
360
-
361
- const node: NodeObject = {
362
- ...makeLinkedDataForWork(resource, files),
363
- '@id': `${PREFIX}resource/${resourceId}`,
364
- '@type': 'bibo:DocumentPart',
365
- }
366
-
367
- const types = resource.get('key_type') ?? work.get('key_type') ?? []
368
-
369
- if (types.includes('matrix') || types.includes('algorithm')) {
370
- node['dcterms:type'] = { '@id': 'http://purl.org/dc/dcmitype/Software' }
371
- } else if (types.includes('key') || types.includes('reference') || types.includes('supplement')) {
372
- node['dcterms:type'] = { '@id': 'http://purl.org/dc/dcmitype/Text' }
373
- } else if (types.includes('gallery') || types.includes('collection')) {
374
- node['dcterms:type'] = { '@id': 'http://purl.org/dc/dcmitype/Collection' }
375
- } else if (types.includes('checklist')) {
376
- node['dcterms:type'] = { '@id': 'http://purl.org/dc/dcmitype/Dataset' }
377
- }
378
-
379
- if (types.includes('key') || types.includes('matrix')) {
380
- node['ac:subtype'] = { '@id': 'http://rs.tdwg.org/acsubtype/values/IdentificationKey' }
381
- }
382
-
383
- const taxonNames = resource.get('taxon') ?? work.get('taxon')
384
- if (taxonNames) {
385
- const taxa = mapEntities(taxonNames as string[], files.taxa.entities)
386
- const coveringTaxon = taxa.length ? getCoveringTaxon(taxa) : null
387
- if (coveringTaxon !== null) {
388
- node['ac:taxonCoverage'] = { '@id': coveringTaxon }
389
- }
390
-
391
- node['dwc:taxonID'] = taxa.map(taxon => ({ '@id': `${PREFIX}taxon/${taxon.get('id')}` }))
392
- }
393
-
394
- const scopes = resource.get('scope') ?? work.get('scope')
395
- if (scopes) {
396
- for (const scope of scopes as string[]) {
397
- if (!SCOPES[scope]) {
398
- console.error('Unmapped scope:', scope)
399
- continue
400
- }
401
-
402
- const [property, ...values] = SCOPES[scope]
403
-
404
- if (!Array.isArray(node[property])) {
405
- node[property] = []
406
- }
407
-
408
- for (const value of values) {
409
- if (value.startsWith('http')) {
410
- (node[property] as unknown[]).push({ '@id': value })
411
- } else {
412
- (node[property] as unknown[]).push(value)
413
- }
414
- }
415
- }
416
- }
417
-
418
- const region = resource.get('region') ?? work.get('region')
419
- if (region) {
420
- const places = mapEntities(region as string[], files.places.entities).map(place => ({ '@id': `${PREFIX}place/${place.get('id')}` }))
421
- node['dcterms:spatial'] = places
422
- }
423
-
424
- const tags: string[] = []
425
-
426
- const taxonScopes = resource.get('taxon_scope') ?? work.get('taxon_scope')
427
- if (taxonScopes) {
428
- tags.push(...taxonScopes as string[])
429
- } else if ((resource.get('complete') ?? work.get('complete')) === 'FALSE') {
430
- tags.push('not intended to be complete')
431
- }
432
-
433
- const targetTaxa = resource.get('target_taxa') ?? work.get('target_taxa')
434
- if (targetTaxa) {
435
- const first = (targetTaxa as string[]).slice(0, -1).map(rank => rank + ',')
436
- const last = targetTaxa[targetTaxa.length - 1]
437
- const list = first.length ? first.join('') + ' or ' + last : last
438
- tags.push(`for identification to ${list}`)
439
- }
440
-
441
- if (tags.length) {
442
- node['ac:tag'] = tags
443
- }
444
-
445
- if (files.resources[resourceId].taxa) {
446
- const leafs = new Set(files.resources[resourceId].taxa.map(taxon => taxon.scientificNameID))
447
- const taxa = []
448
- for (const taxon of files.resources[resourceId].taxa) {
449
- taxa.push(makeLinkedDataForScientificName(taxon))
450
-
451
- if (taxon.parentNameUsageID) {
452
- leafs.delete(taxon.parentNameUsageID)
453
- }
454
- }
455
-
456
- node['dcterms:subject'] = taxa
457
- node['ac:taxonCount'] = leafs.size
458
- }
459
-
460
- return node
461
- }
462
-
463
- function makeLinkedDataForWork (work: catalog.Entity, files: Catalog): NodeObject {
464
- const id = work.get('id') as string
465
- const node: NodeObject = makeWorkUri(id)
466
-
467
- const languages = work.get('language') as string[]
468
- node['dcterms:language'] = languages.map(language => ({ '@id': `http://id.loc.gov/vocabulary/iso639-1/${language}` }))
469
-
470
- if (work.has('title')) {
471
- const title = work.get('title') as string[]
472
- if (title.length > languages.length) {
473
- title.splice(0, title.length, title.join('; '))
474
- }
475
- node['dcterms:title'] = title.map((value, i) => ({ '@value': value, '@language': languages[i] }))
476
- }
477
-
478
- if (work.has('pages') && (work.get('pages') as string).includes('-')) {
479
- const containers = work.has('part_of') ? (work.get('part_of') as string[]).map(id => files.catalog.get(id)) : []
480
- if (containers.find(container => container.get('entry_type') === 'online')) {
481
- node['@type'] = 'bibo:BookSection'
482
- } else {
483
- node['@type'] = 'bibo:AcademicArticle'
484
- }
485
- } else if (work.get('entry_type') === 'online') {
486
- node['@type'] = 'bibo:Website'
487
- } else {
488
- node['@type'] = 'bibo:Book'
489
- }
490
-
491
- if (work.has('author')) {
492
- const authors = mapEntities(work.get('author') as string[], files.authors.entities).map(author => ({ '@id': `${PREFIX}author/${author.get('id')}` }))
493
- node['bibo:authorList'] = { '@list': authors }
494
- node['dcterms:creator'] = authors
495
- }
496
-
497
- if (work.has('url')) {
498
- const urls = work.get('url') as string[]
499
-
500
- const handle = urls.find(url => url.startsWith(HANDLE_PREFIX))
501
- if (handle) {
502
- node['bibo:handle'] = { '@id': handle.slice(HANDLE_PREFIX.length) }
503
- }
504
-
505
- node['schema:url'] = urls.map(url => ({ '@id': url }))
506
- }
507
-
508
- if (work.has('fulltext_url')) {
509
- const urls = work.get('fulltext_url') as string[]
510
-
511
- node['schema:encoding'] = urls.map(url => ({ '@type': 'schema:MediaObject', 'schema:contentUrl': { '@id': url } }))
512
- }
513
-
514
- if (work.has('archive_url')) {
515
- node['schema:archivedAt'] = (work.get('archive_url') as string[]).map(url => ({ '@id': url }))
516
- }
517
-
518
- if (work.has('date')) {
519
- const date = work.get('date') as string
520
- let dateType = 'rdfs:Literal'
521
- if (date.match(/^\d{4}-\d{2}-\d{2}$/)) {
522
- dateType = 'xsd:date'
523
- } else if (date.match(/^\d{4}-\d{2}$/)) {
524
- dateType = 'xsd:gYearMonth'
525
- } else if (date.match(/^\d{4}$/)) {
526
- dateType = 'xsd:gYear'
527
- }
528
-
529
- node['dcterms:issued'] = { '@value': work.get('date'), '@type': dateType }
530
- }
531
-
532
- if (work.has('publisher')) {
533
- const publishers = mapEntities(work.get('publisher') as string[], files.publishers.entities).map(publisher => ({ '@id': `${PREFIX}publisher/${publisher.get('id')}` }))
534
- node['dcterms:publisher'] = publishers
535
- }
536
-
537
- if (work.has('ISSN')) {
538
- node['bibo:issn'] = work.get('ISSN')
539
- }
540
-
541
- if (work.has('ISBN')) {
542
- const isbns = work.get('ISBN') as string[]
543
-
544
- for (const isbn of isbns) {
545
- if (isbn.length === 13) {
546
- node['bibo:isbn13'] = isbn
547
- } else if (isbn.length === 10) {
548
- node['bibo:isbn10'] = isbn
549
- } else {
550
- node['bibo:isbn'] = isbn
551
- }
552
- }
553
- }
554
-
555
- if (work.has('QID')) {
556
- node['bibo:uri'] = makeWikidataUri(work.get('QID') as string)
557
- }
558
-
559
- if (work.has('DOI')) {
560
- node['bibo:doi'] = work.get('DOI')
561
- }
562
-
563
- if (work.has('volume')) {
564
- node['bibo:volume'] = work.get('volume')
565
- }
566
-
567
- if (work.has('issue')) {
568
- node['bibo:issue'] = work.get('issue')
569
- }
570
-
571
- if (work.has('pages')) {
572
- const pages = work.get('pages') as string
573
- const range = pages.split('-')
574
-
575
- if (pages.match(/^\d+$/)) {
576
- node['bibo:numPages'] = parseInt(pages)
577
- } else if (range.length === 2) {
578
- node['bibo:pages'] = pages
579
-
580
- const [start, end] = range.map(part => parseInt(part))
581
- if (!isNaN(start)) {
582
- node['bibo:pageStart'] = start
583
- }
584
- if (!isNaN(end)) {
585
- node['bibo:pageEnd'] = end
586
- }
587
- if (!isNaN(start) && !isNaN(end)) {
588
- node['bibo:numPages'] = end - start + 1
589
- }
590
- } else {
591
- node['bibo:pages'] = pages
592
- }
593
- }
594
-
595
- if (work.has('edition')) {
596
- node['bibo:edition'] = work.get('edition')
597
- }
598
-
599
- if (work.has('license')) {
600
- const licenses = work.get('license') as string[]
601
- node['dcterms:rights'] = licenses.map(license => {
602
- if (license === '<public domain>' || license.match(/^<.+\?>$/)) {
603
- return license.slice(1, -1)
604
- } else {
605
- return { '@id': `https://spdx.org/licenses/${license}.html` }
606
- }
607
- })
608
- }
609
-
610
- return node
611
- }
612
-
613
- function makeLinkedDataForWorks (files: Catalog): NodeObject[] {
614
- const nodes: Record<string, NodeObject> = {}
615
- const works = files.catalog
616
-
617
- for (const work of works.entities) {
618
- nodes[work.get('id') as string] = makeLinkedDataForWork(work, files)
619
- }
620
-
621
- for (const work of works.entities) {
622
- const id = work.get('id') as string
623
- const node = nodes[id]
624
- const language = work.get('language') as string[]
625
-
626
- if (work.has('part_of')) {
627
- const containers = work.get('part_of') as string[]
628
-
629
- if ((work.get('key_type') as string[]).includes('supplement')) {
630
- node['bibo:annotates'] = containers.map(makeWorkUri)
631
- } else {
632
- node['dcterms:isPartOf'] = containers.map(makeWorkUri)
633
-
634
- for (const container of containers) {
635
- if (!Array.isArray(nodes[container]['dcterms:hasPart'])) {
636
- nodes[container]['dcterms:hasPart'] = []
637
- }
638
- (nodes[container]['dcterms:hasPart'] as NodeObject[]).push(makeWorkUri(id))
639
- }
640
- }
641
- }
642
-
643
- if (work.has('listed_in')) {
644
- const referers = work.get('listed_in') as string[]
645
- node['bibo:citedBy'] = node['dcterms:isReferencedBy'] = referers.map(makeWorkUri)
646
-
647
- for (const referer of referers) {
648
- nodes[referer]['bibo:cites'] = nodes[referer]['dcterms:references'] = makeWorkUri(id)
649
- }
650
- }
651
-
652
- if (work.has('version_of')) {
653
- const originals = work.get('version_of') as string[]
654
- const originalLanguages = originals.map(id => ((works.get(id) as catalog.Entity).get('language') as string[]).join())
655
- node['bibo:translationOf'] = originals.filter((_, i) => originalLanguages[i] !== language.join()).map(makeWorkUri)
656
- node['dcterms:isVersionOf'] = originals.filter(id => work.get('id') !== id).map(makeWorkUri)
657
- }
658
-
659
- let resourceIndex = 1
660
- let resourceId
661
- while ((resourceId = `${id}:${resourceIndex++}`) in files.resources) {
662
- const resource = makeLinkedDataForResource(work, files, resourceId)
663
-
664
- if (!Array.isArray(node['dcterms:hasPart'])) {
665
- node['dcterms:hasPart'] = []
666
- }
667
- (node['dcterms:hasPart'] as NodeObject[]).push(resource)
668
-
669
- if (!Array.isArray(resource['dcterms:isPartOf'])) {
670
- resource['dcterms:isPartOf'] = []
671
- }
672
- (resource['dcterms:isPartOf'] as NodeObject[]).push({ '@id': node['@id'] })
673
- }
674
- }
675
-
676
- return Object.values(nodes)
677
- }
678
-
679
- async function writeOutput (document: JsonLdDocument, format = 'jsonld'): Promise<void> {
680
- if (format === 'jsonld') {
681
- process.stdout.write(JSON.stringify(document, null, 2))
682
- return
683
- }
684
-
685
- const nquads = await jsonld.toRDF(document, { format: 'application/n-quads' }) as string
686
- if (format === 'nquads') {
687
- process.stdout.write(nquads)
688
- return
689
- }
690
-
691
- const parser = new N3.StreamParser()
692
- const writer = new N3.StreamWriter({ prefixes: PREFIXES })
693
- parser.write(nquads)
694
- parser.pipe(writer)
695
- writer.pipe(process.stdout)
696
- }
697
-
698
- async function main (): Promise<void> {
699
- const args = util.parseArgs({
700
- allowPositionals: true,
701
- options: {
702
- format: {
703
- type: 'string',
704
- short: 'f',
705
- default: 'jsonld',
706
- }
707
- },
708
- })
709
- const directory = path.resolve(args.positionals[0])
710
-
711
- const files = {} as Catalog
712
- for (const sheet of SHEETS) {
713
- const filePath = path.join(directory, `${sheet}.csv`)
714
- if (!doesFileExist(filePath)) {
715
- throw new Error(`File "${sheet}.csv" must be provided`)
716
- }
717
-
718
- const file = await fs.readFile(filePath, 'utf8')
719
- files[sheet] = catalog.loadData(file, sheet)
720
- }
721
-
722
- files.resources = {}
723
- const resources = JSON.parse(await fs.readFile(path.join(directory, 'resources', 'index.json'), 'utf8'))
724
- for (const id in resources) {
725
- const filePath = path.join(directory, 'resources', 'dwc', id.split(':').join('-') + '.csv')
726
- const file = await fs.readFile(filePath, 'utf8')
727
- const [header, ...rows] = parseCsv(file)
728
- const taxa = rows.map(row => row.reduce((object, value, index) => {
729
- object[header[index]] = value
730
- return object
731
- }, {} as Record<string, string>) as unknown as AmendedTaxon)
732
-
733
- files.resources[id] = { metadata: resources[id], taxa }
734
- }
735
-
736
- const graph: NodeObject[] = [
737
- ...makeLinkedDataForWorks(files),
738
- ...makeLinkedDataForTaxa(files),
739
- ]
740
-
741
- for (const entity of files.authors.entities) {
742
- graph.push(makeLinkedDataForAuthor(entity))
743
- }
744
- for (const entity of files.places.entities) {
745
- graph.push(makeLinkedDataForPlace(entity))
746
- }
747
- for (const entity of files.publishers.entities) {
748
- graph.push(makeLinkedDataForPublisher(entity))
749
- }
750
-
751
- const document: JsonLdDocument = {
752
- '@context': PREFIXES,
753
- '@graph': graph
754
- }
755
-
756
- writeOutput(document, args.values.format)
757
- }
758
-
759
- main().catch((error: Error) => {
760
- console.error(error)
761
- process.exit(1)
762
- })