@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,408 +0,0 @@
1
- import * as yaml from 'js-yaml'
2
- import { WorkResource } from './resource'
3
- import { createDiff, ResourceDiffType } from './diff-resource'
4
- import { parseName, RANKS, RecoverableSyntaxError } from './parse-name'
5
-
6
- const MAIN_RANKS: Rank[] = [
7
- 'kingdom',
8
- 'phylum',
9
- 'class',
10
- 'order',
11
- 'family',
12
- 'genus',
13
- 'species'
14
- ]
15
-
16
- const DWC_RANKS: DwcRank[] = [
17
- 'kingdom',
18
- 'phylum',
19
- 'class',
20
- 'order',
21
- 'family',
22
- 'subfamily',
23
- 'genus',
24
- 'subgenus'
25
- ]
26
-
27
- const FLAGS: ResourceFlag[] = [
28
- 'MISSING_TAXA',
29
- 'MISSING_PARENT_TAXA',
30
- 'MISSING_SYNONYMS',
31
- 'MISSING_AUTHORSHIP'
32
- ]
33
-
34
- const RESOURCE_DELIMITER = '\n\n===\n\n'
35
- const INDENT = 2
36
-
37
- function makeParseError (message: string, line: number, column: number = 1): SyntaxError {
38
- return new SyntaxError(`[${line}:${column}] ${message}`)
39
- }
40
-
41
- function mergeParserErrors (errors: SyntaxError[]): SyntaxError {
42
- return new SyntaxError(errors.map(error => error.message).join('\n'))
43
- }
44
-
45
- function parseHeader (header: string): ResourceMetadata {
46
- const config = yaml.load(header)
47
-
48
- if (typeof config !== 'object' || Array.isArray(config) || config === null) {
49
- throw new SyntaxError('yaml header should be an object')
50
- }
51
-
52
- // Invalid configuration
53
- let levels
54
- if (!('levels' in config)) {
55
- levels = []
56
- } else if (!Array.isArray(config.levels)) {
57
- throw new SyntaxError('"levels" should be an array')
58
- } else {
59
- levels = config.levels
60
- }
61
-
62
- if ('scope' in config) {
63
- throw new SyntaxError('"scope" data should go in "catalog"')
64
- }
65
-
66
- // No taxon ranks
67
- if (levels.length === 0) {
68
- throw new SyntaxError('Resource contains no taxa')
69
- }
70
-
71
- // Invalid taxon ranks
72
- const invalidTaxonRanks = levels.filter(rank => !RANKS.includes(rank))
73
- if (invalidTaxonRanks.length) {
74
- throw new SyntaxError(`"levels" contains invalid values: ${invalidTaxonRanks.join(', ')}`)
75
- }
76
-
77
- const metadata: ResourceMetadata = { levels }
78
-
79
- if ('catalog' in config && typeof config.catalog === 'object' && config.catalog !== null) {
80
- const catalog: Record<string, string> = {}
81
- if ('id' in config.catalog) {
82
- throw new SyntaxError('"catalog" should not contain id')
83
- }
84
- for (const key in config.catalog) {
85
- const value = config.catalog[key as keyof object]
86
- if (typeof value === 'number') {
87
- catalog[key] = (value as number).toString()
88
- } else if (typeof value === 'string') {
89
- catalog[key] = value
90
- } else {
91
- throw new SyntaxError(`"catalog" should contain only strings ("${key}")`)
92
- }
93
- }
94
- const work = new WorkResource(catalog)
95
- const errors = work.validate().filter(({ error }) => error !== 'Value(s) required but missing')
96
- if (errors.length > 0) {
97
- throw new SyntaxError(`"catalog" contains errors: ${errors.map(({ field, error }) => `[${field}] ${error}`).join('; ')}`)
98
- }
99
- metadata.catalog = {}
100
- for (const key in work.fields) {
101
- metadata.catalog[key] = work.get(key) as Value
102
- }
103
- }
104
-
105
- if ('flags' in config) {
106
- if (!Array.isArray(config.flags)) {
107
- throw new SyntaxError('"flags" should be an array if present')
108
- }
109
-
110
- const invalidFlags = config.flags.filter(flag => !FLAGS.includes(flag))
111
- if (invalidFlags.length) {
112
- throw new SyntaxError(`"flags" contains invalid values: ${invalidFlags.join(', ')}`)
113
- }
114
-
115
- metadata.flags = config.flags
116
- }
117
-
118
- return metadata
119
- }
120
-
121
- function parseResource (resource: FilePart): [ResourceMetadata, FilePart] {
122
- const [header, _, ...rest] = resource.content.split(/(\n---\n+)/)
123
- let config
124
-
125
- try {
126
- config = parseHeader(header)
127
- } catch (error) {
128
- throw makeParseError(error.message, resource.offsetLine + 1)
129
- }
130
-
131
- const content = rest.join('')
132
- const offsetLine = resource.offsetLine + (header + _).split('\n').length - 1
133
-
134
- return [config, { content, offsetLine }]
135
- }
136
-
137
- function getTaxonChildren (parent: TaxonId|undefined, taxa: Record<TaxonId, WorkingTaxon>): WorkingTaxon[] {
138
- const children = []
139
- for (const id in taxa) {
140
- if (taxa[id].parentNameUsageID === parent) {
141
- children.push(taxa[id])
142
- }
143
- }
144
- return children
145
- }
146
-
147
- function processClusters (taxa: Record<TaxonId, WorkingTaxon>) {
148
- for (const id in taxa) {
149
- const taxon = taxa[id]
150
- if (taxon.taxonomicStatus !== 'accepted' || !taxon.cluster) {
151
- continue
152
- }
153
-
154
- const dynamicProperties = taxon.dynamicProperties ? JSON.parse(taxon.dynamicProperties) : {}
155
-
156
- if (taxon.cluster === '_') {
157
- dynamicProperties.identifiable = false
158
- } else {
159
- const siblings = getTaxonChildren(taxon.parentNameUsageID, taxa).filter(sibling => sibling.scientificNameID !== id)
160
- dynamicProperties.indistinguishableFrom = siblings.filter(sibling => sibling.cluster === taxon.cluster).map(sibling => sibling.scientificNameID)
161
- }
162
-
163
- taxon.dynamicProperties = JSON.stringify(dynamicProperties)
164
- }
165
- }
166
-
167
- function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds: number[], offsetLine: number): Resource {
168
- const leafTaxonIndex = resource.metadata.levels.reduce((last, rank, i) => MAIN_RANKS.includes(rank) ? i : last, 0)
169
- const data = resource.taxa as Record<TaxonId, WorkingTaxon>
170
- const errors = []
171
-
172
- let id = 0
173
- let newId = Math.max(...oldIds)
174
- let lineNumber = offsetLine
175
-
176
- const parents: Array<TaxonId | null> = []
177
- const previous = { id: '', indent: 0, group: { isLeaf: false, indent: 0 }, errors: <SyntaxError[]>[] }
178
-
179
- for (const line of content) {
180
- const hasOriginalId = line.type !== ResourceDiffType.Added && !/^\s*(\[indet\]|> )/.test(line.original ?? line.text as string)
181
- if (hasOriginalId) {
182
- id++
183
- }
184
-
185
- if (line.type === ResourceDiffType.Deleted) {
186
- continue
187
- } else {
188
- lineNumber++
189
- }
190
-
191
- const [indentation, name] = (line.text as string).match(/^(\s*)(.*)/)!.slice(1)
192
- const lineIndent = indentation.length
193
-
194
- // Validate line
195
- if (lineIndent % INDENT === 1) {
196
- errors.push(makeParseError('Too much or little indentation', lineNumber))
197
- continue
198
- } else if (lineIndent / INDENT >= resource.metadata.levels.length && !/^[+=>] /.test(name)) {
199
- errors.push(makeParseError('Too much indentation', lineNumber))
200
- continue
201
- } else if (lineIndent <= previous.group.indent && (data[previous.id] && !previous.group.isLeaf)) {
202
- errors.push(makeParseError('Missing leaf taxon', lineNumber - 1))
203
- }
204
-
205
- // Update parentage
206
- if (lineIndent > previous.indent) {
207
- // Do not count synonyms as parents (unless this is correcting a typo in the synonym)
208
- if (data[previous.id] && data[previous.id].taxonomicStatus === 'accepted' || name.startsWith('> ')) {
209
- parents.push(previous.id)
210
- } else {
211
- parents.push(null)
212
- }
213
-
214
- // Handle skips in indentation levels,
215
- // e.g. if a certain genus has only species
216
- // whereas other genera in the same key also
217
- // have subgenera
218
- for (let i = previous.indent + INDENT; i < lineIndent; i += INDENT) {
219
- parents.push(null)
220
- }
221
- } else if (lineIndent < previous.indent) {
222
- parents.splice(lineIndent / INDENT)
223
- }
224
-
225
- previous.indent = lineIndent
226
-
227
- // Do not process "indet" lines further, as they only serve to indicate
228
- // that subtaxa are explicitely omitted
229
- if (name.startsWith('[indet]')) {
230
- errors.push(...previous.errors)
231
- previous.errors.length = 0
232
- previous.group.isLeaf = lineIndent / INDENT >= leafTaxonIndex
233
- continue
234
- }
235
-
236
- const parentId = parents.reduce((grandparent, parent) => parent ?? grandparent, null)
237
- const parent = parentId === null ? {} as WorkingTaxon : data[parentId]
238
- let item
239
- const itemErrors = []
240
-
241
- try {
242
- const rank = resource.metadata.levels[parents.length]
243
- item = parseName(name, rank, parent)
244
- } catch (error) {
245
- if (error instanceof RecoverableSyntaxError) {
246
- itemErrors.push(makeParseError(error.message, lineNumber))
247
- item = error.result
248
- } else {
249
- errors.push(makeParseError(error.message, lineNumber))
250
- continue
251
- }
252
- }
253
-
254
- // Add higher classification info
255
- const itemAsObject = item as { [index: string]: unknown }
256
- const parentAsObject = parent as { [index: string]: unknown }
257
- for (const rank of DWC_RANKS) {
258
- itemAsObject[rank] = undefined
259
- if (parentAsObject[rank]) {
260
- itemAsObject[rank] = parentAsObject[rank]
261
- }
262
- if (item.taxonRank === rank) {
263
- itemAsObject[rank] = item.scientificNameOnly
264
- }
265
- }
266
-
267
- if (item.genericName && !item.genus) {
268
- item.genus = item.genericName
269
- }
270
- if (item.infragenericEpithet && !item.subgenus) {
271
- item.subgenus = item.infragenericEpithet
272
- }
273
-
274
- // Amend "parent" with corrections, exit
275
- if (item.taxonomicStatus === 'incorrect') {
276
- if (parent.taxonomicStatus !== 'accepted') {
277
- // Remove corrected synonym from parentage
278
- parents[parents.length - 1] = null
279
- }
280
-
281
- if (parent.incorrect) {
282
- errors.push(makeParseError('Cannot apply a correction to a previous correction', lineNumber))
283
- continue
284
- } else if (parentId === null) {
285
- errors.push(makeParseError('Cannot apply a correction to nothing', lineNumber))
286
- continue
287
- }
288
-
289
- parent.incorrect = { ...parent }
290
- for (const key in item) {
291
- if (key !== 'taxonomicStatus' && key !== 'verbatimIdentification') {
292
- parentAsObject[key] = itemAsObject[key]
293
- }
294
- }
295
-
296
- // If "parent" is corrected, its errors can be dropped
297
- previous.errors.length = 0
298
- // ...but errors associated with the corrected name are added immediately
299
- errors.push(...itemErrors)
300
-
301
- continue
302
- }
303
-
304
- // Add more classification info
305
- const isSynonym = item.taxonomicStatus !== 'accepted'
306
- if (isSynonym) {
307
- item.higherClassification = parent.higherClassification
308
- } else if (parent.higherClassification) {
309
- item.higherClassification = parent.higherClassification + ` | ${parent.scientificNameOnly}`
310
- } else if (parentId) {
311
- item.higherClassification = parent.scientificNameOnly
312
- }
313
-
314
- // Set identifiers
315
- item.scientificNameID = `${resource.id}:${hasOriginalId ? (oldIds[id - 1] ?? id) : ++newId}`
316
-
317
- item.parentNameUsageID = isSynonym ? undefined : parent.scientificNameID
318
- item.parentNameUsage = isSynonym ? undefined : parent.scientificName
319
- item.acceptedNameUsageID = isSynonym ? parent.scientificNameID : undefined
320
- item.acceptedNameUsage = isSynonym ? parent.scientificName : undefined
321
- item.collectionCode = resource.id
322
-
323
- data[item.scientificNameID] = item
324
-
325
- // Update loop state
326
- errors.push(...previous.errors)
327
- previous.errors = itemErrors
328
- previous.id = item.scientificNameID
329
- if (item.taxonomicStatus === 'accepted') {
330
- previous.group.indent = previous.indent
331
- previous.group.isLeaf = lineIndent / INDENT >= leafTaxonIndex
332
- }
333
- }
334
-
335
- errors.push(...previous.errors)
336
- if (errors.length) {
337
- throw mergeParserErrors(errors)
338
- }
339
-
340
- processClusters(data)
341
-
342
- return resource
343
- }
344
-
345
- interface FilePart {
346
- content: string
347
- offsetLine: number
348
- }
349
-
350
- function splitResources (file: string): FilePart[] {
351
- const resources: FilePart[] = []
352
- let offsetLine = 0
353
- for (const content of file.split(RESOURCE_DELIMITER)) {
354
- resources.push({ content, offsetLine })
355
- offsetLine += (content + RESOURCE_DELIMITER).split('\n').length - 1
356
- }
357
- return resources
358
- }
359
-
360
- export function parseFile (file: string, id: WorkId, old?: ResourceHistory): Resource[] {
361
- const oldResources = old ? splitResources(old.txt) : []
362
- const newResources = splitResources(file)
363
- const resources: Resource[] = []
364
- const errors = []
365
-
366
- for (let index = 0; index < newResources.length; index++) {
367
- const [config, content] = parseResource(newResources[index])
368
- const template: Resource = {
369
- id: `${id}:${index + 1}`,
370
- file: `${id}-${index + 1}`,
371
- workId: id,
372
- metadata: config,
373
- taxa: {}
374
- }
375
-
376
- let diff
377
- if (oldResources[index]) {
378
- diff = createDiff(content.content, parseResource(oldResources[index])[1].content)
379
- // Ignore empty lines
380
- diff = diff.filter(line => line.text !== '')
381
- } else {
382
- diff = createDiff(content.content, content.content)
383
- }
384
-
385
- const oldIds = []
386
- if (old) {
387
- for (const row of old.dwc[index].slice(1)) {
388
- oldIds.push(parseInt(row[0].split(':')[2]))
389
- }
390
- }
391
-
392
- try {
393
- resources.push(parseResourceContent(diff, template, oldIds, content.offsetLine))
394
- } catch (error) {
395
- errors.push(error)
396
- }
397
- }
398
-
399
- if (errors.length) {
400
- throw mergeParserErrors(errors)
401
- }
402
-
403
- return resources
404
- }
405
-
406
- export function parseFileHeader (file: string): ResourceMetadata[] {
407
- return splitResources(file).map(resource => parseResource(resource)[0])
408
- }
@@ -1,10 +0,0 @@
1
- import { Work } from '../catalog/tables/work'
2
-
3
- export class WorkResource extends Work {
4
- constructor (values: Record<string, string>) {
5
- super(values)
6
-
7
- this.schema.version_of.format = /^B[1-9]\d*:[1-9]\d*$/
8
- this.schema.duplicate_of.format = /^B[1-9]\d*:[1-9]\d*$/
9
- }
10
- }
@@ -1,79 +0,0 @@
1
- const MINIMUM_PREFIX_LENGTH = 3
2
- const VALID_COMMON_PREFIXES = new Set([
3
- 'Plantae|Tracheophyta',
4
- 'Fungi',
5
- 'Fungi|Ascomycota',
6
- 'Fungi|Basidiomycota',
7
- 'Fungi|Zygomycota'
8
- ])
9
-
10
- function getCommonPrefix (a: string[], b: string[]): string[] {
11
- for (let i = 0; i < Math.max(a.length, b.length); i++) {
12
- if (a[i] !== b[i]) {
13
- return a.slice(0, i)
14
- }
15
- }
16
- return a.slice()
17
- }
18
-
19
- function isValidPrefix (a: string[], b: string[]): boolean {
20
- const prefix = getCommonPrefix(a, b)
21
- return VALID_COMMON_PREFIXES.has(prefix.join('|')) || prefix.length >= MINIMUM_PREFIX_LENGTH
22
- }
23
-
24
- export function groupNameMatches (results: Record<TaxonId, TaxonMatch[]>): GroupedNameMatches {
25
- const prefixes: Record<string, [string[], Record<TaxonId, TaxonMatch>][]> = {}
26
-
27
- for (const scientificNameID in results) {
28
- for (const result of results[scientificNameID]) {
29
- if (!prefixes[result.source]) {
30
- prefixes[result.source] = []
31
- }
32
-
33
- let prefix = prefixes[result.source].find(prefix => isValidPrefix(prefix[0], result.classificationPath))
34
-
35
- if (!prefix) {
36
- prefix = [result.classificationPath, {}]
37
- prefixes[result.source].push(prefix)
38
- } else {
39
- prefix[0] = getCommonPrefix(prefix[0], result.classificationPath)
40
- }
41
-
42
- if (scientificNameID in prefix[1]) {
43
- continue
44
- }
45
-
46
- prefix[1][scientificNameID] = result
47
- }
48
- }
49
-
50
- const groupedNameMatches: GroupedNameMatches = {}
51
- for (const source in prefixes) {
52
- groupedNameMatches[source] = prefixes[source]
53
- .sort((a, b) => Object.keys(b[1]).length - Object.keys(a[1]).length)
54
- .reduce((map: Record<string, Record<TaxonId, TaxonMatch>>, [prefix, taxa]) => {
55
- map[prefix.join('|')] = taxa
56
- return map
57
- }, {})
58
- }
59
-
60
- return groupedNameMatches
61
- }
62
-
63
- export function amendResource (resource: AmendedResource, source: string, matches: Record<TaxonId, TaxonMatch>) {
64
- for (const id in matches) {
65
- const match = matches[id]
66
-
67
- if (source === '1') {
68
- resource.taxa[id].colTaxonID = match.id
69
- if (match.currentId) {
70
- resource.taxa[id].colAcceptedTaxonID = match.currentId
71
- }
72
- } else if (source === '11') {
73
- resource.taxa[id].gbifTaxonID = match.id
74
- if (match.currentId) {
75
- resource.taxa[id].gbifAcceptedTaxonID = match.currentId
76
- }
77
- }
78
- }
79
- }