@larsgw/formica 0.2.0 → 0.3.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.
- package/CHANGELOG.md +66 -0
- package/lib/bin/process-resources-index.js +10 -4
- package/lib/bin/process-resources.js +214 -142
- package/lib/bin/validate-resources-text.js +0 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +2 -1
- package/lib/resources/parse-text.js +26 -10
- package/lib/taxon-names/index.d.ts +2 -0
- package/lib/taxon-names/index.js +79 -0
- package/package.json +4 -1
- package/src/bin/process-resources-index.ts +11 -4
- package/src/bin/process-resources.ts +154 -99
- package/src/index.ts +1 -0
- package/src/module.d.ts +70 -50
- package/src/resources/parse-text.ts +28 -11
- package/src/taxon-names/index.ts +79 -0
package/src/module.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
type Schema = Record<string, FieldSpecification>
|
|
2
2
|
type FieldSpecification = {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
required: boolean,
|
|
4
|
+
multiple: boolean | FieldSpecificationCallbackMultiple
|
|
5
|
+
format?: string[] | RegExp | FieldSpecificationCallbackFormat
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
type Value = string[] | string
|
|
@@ -11,12 +11,12 @@ type FieldSpecificationCallbackMultiple = (entry: Record<string, Value>) => bool
|
|
|
11
11
|
type FieldSpecificationCallbackFormat = (value: SingleValue) => boolean;
|
|
12
12
|
|
|
13
13
|
interface FieldError {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
field: string,
|
|
15
|
+
error: string
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
interface WorkError extends FieldError {
|
|
19
|
-
|
|
19
|
+
entity: WorkId
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
type Rank = string
|
|
@@ -28,59 +28,59 @@ type ResourceId = string
|
|
|
28
28
|
type WorkId = string
|
|
29
29
|
|
|
30
30
|
interface WorkingTaxon {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
31
|
+
scientificNameID?: TaxonId,
|
|
32
|
+
scientificName?: string,
|
|
33
|
+
scientificNameAuthorship?: string,
|
|
34
|
+
genericName?: string,
|
|
35
|
+
infragenericEpithet?: string,
|
|
36
|
+
specificEpithet?: string,
|
|
37
|
+
intraspecificEpithet?: string,
|
|
38
|
+
|
|
39
|
+
taxonRank?: Rank,
|
|
40
|
+
taxonRemarks?: string,
|
|
41
|
+
collectionCode?: ResourceId,
|
|
42
|
+
|
|
43
|
+
taxonomicStatus?: TaxonStatus,
|
|
44
|
+
acceptedNameUsageID?: TaxonId,
|
|
45
|
+
acceptedNameUsage?: string,
|
|
46
|
+
|
|
47
|
+
parentNameUsageID?: TaxonId,
|
|
48
|
+
parentNameUsage?: string,
|
|
49
|
+
kingdom?: string,
|
|
50
|
+
phylum?: string,
|
|
51
|
+
class?: string,
|
|
52
|
+
order?: string,
|
|
53
|
+
family?: string,
|
|
54
|
+
subfamily?: string,
|
|
55
|
+
genus?: string,
|
|
56
|
+
subgenus?: string,
|
|
57
|
+
higherClassification?: string,
|
|
58
|
+
|
|
59
|
+
// Non-standard
|
|
60
|
+
scientificNameOnly?: string,
|
|
61
|
+
incorrect?: WorkingTaxon
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
interface Taxon extends WorkingTaxon {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
scientificNameID: TaxonId,
|
|
66
|
+
scientificName: string,
|
|
67
|
+
taxonRank: Rank,
|
|
68
|
+
collectionCode: ResourceId,
|
|
69
|
+
taxonomicStatus: string
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
interface ResourceMetadata {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
levels: Rank[],
|
|
74
|
+
scope: string[],
|
|
75
|
+
catalog?: object
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
interface Resource {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
id: string,
|
|
80
|
+
file: string,
|
|
81
|
+
workId: string,
|
|
82
|
+
metadata: ResourceMetadata,
|
|
83
|
+
taxa: Record<TaxonId, Taxon>
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
interface ResourceHistory {
|
|
@@ -103,3 +103,23 @@ declare enum ResourceDiffType {
|
|
|
103
103
|
Modified = '~',
|
|
104
104
|
Unchanged = '='
|
|
105
105
|
}
|
|
106
|
+
|
|
107
|
+
interface AmendedTaxon extends Taxon {
|
|
108
|
+
colTaxonID?: string,
|
|
109
|
+
colAcceptedTaxonID?: string,
|
|
110
|
+
gbifTaxonID?: string,
|
|
111
|
+
gbifAcceptedTaxonID?: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface AmendedResource extends Resource {
|
|
115
|
+
taxa: Record<TaxonId, AmendedTaxon>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface TaxonMatch {
|
|
119
|
+
source: number,
|
|
120
|
+
id: string,
|
|
121
|
+
currentId?: string,
|
|
122
|
+
classificationPath: string[]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type GroupedNameMatches = Record<string, Record<string, Record<TaxonId, TaxonMatch>>>
|
|
@@ -31,6 +31,16 @@ const RANKS: Rank[] = [
|
|
|
31
31
|
'stirps' // not ICZN
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
+
const MAIN_RANKS: Rank[] = [
|
|
35
|
+
'kingdom',
|
|
36
|
+
'phylum',
|
|
37
|
+
'class',
|
|
38
|
+
'order',
|
|
39
|
+
'family',
|
|
40
|
+
'genus',
|
|
41
|
+
'species'
|
|
42
|
+
]
|
|
43
|
+
|
|
34
44
|
const DWC_RANKS: DwcRank[] = [
|
|
35
45
|
'kingdom',
|
|
36
46
|
'phylum',
|
|
@@ -360,11 +370,7 @@ function parseHeader (header: string): ResourceMetadata {
|
|
|
360
370
|
return metadata
|
|
361
371
|
}
|
|
362
372
|
|
|
363
|
-
function
|
|
364
|
-
const [header, _, ...rest] = resource.split(/(\n---\n+)/)
|
|
365
|
-
const config = parseHeader(header)
|
|
366
|
-
const content = rest.join('')
|
|
367
|
-
|
|
373
|
+
function validateResource (config: ResourceMetadata, content: string) {
|
|
368
374
|
// Check for too much indentation
|
|
369
375
|
const longerIndent = new RegExp(`^( ){${config.levels.length - 1}}(?! [+=>] ) `, 'm')
|
|
370
376
|
const longerIndentMatch = content.match(longerIndent)
|
|
@@ -377,15 +383,25 @@ ${content.slice(offset).split('\n', 1)}
|
|
|
377
383
|
}
|
|
378
384
|
|
|
379
385
|
// Check for missing leaf taxa
|
|
380
|
-
const
|
|
381
|
-
const
|
|
382
|
-
if (
|
|
383
|
-
const
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
+
const leafTaxonRank = config.levels.filter(rank => MAIN_RANKS.includes(rank)).pop() as string
|
|
387
|
+
const leafTaxonParentIndent = config.levels.indexOf(leafTaxonRank) - 1
|
|
388
|
+
if (leafTaxonRank && leafTaxonParentIndent >= 0) {
|
|
389
|
+
const missingLeafTaxa = new RegExp(`^((?: ){0,${leafTaxonParentIndent}})(?![+=> ] ).*\\n(\\1( )+[+=>].*\\n)*(?!\\1 )`, 'm')
|
|
390
|
+
const missingLeafTaxaMatch = content.match(missingLeafTaxa)
|
|
391
|
+
if (missingLeafTaxaMatch !== null) {
|
|
392
|
+
const offset = missingLeafTaxaMatch.index
|
|
393
|
+
const line = (content.slice(0, offset).match(/\n/g) || []).length + 1
|
|
394
|
+
throw new SyntaxError(`Missing leaf taxon at ${line}:0
|
|
386
395
|
${content.slice(offset).split('\n', 1)}
|
|
387
396
|
^`)
|
|
397
|
+
}
|
|
388
398
|
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function parseResource (resource: string): [ResourceMetadata, string] {
|
|
402
|
+
const [header, _, ...rest] = resource.split(/(\n---\n+)/)
|
|
403
|
+
const config = parseHeader(header)
|
|
404
|
+
const content = rest.join('')
|
|
389
405
|
|
|
390
406
|
return [config, content]
|
|
391
407
|
}
|
|
@@ -499,6 +515,7 @@ export function parseFile (file: string, id: WorkId, old?: ResourceHistory): Res
|
|
|
499
515
|
const oldResources = old ? splitResources(old.txt) : []
|
|
500
516
|
return splitResources(file).map((resource, index) => {
|
|
501
517
|
const [config, content] = parseResource(resource)
|
|
518
|
+
validateResource(config, content)
|
|
502
519
|
const template: Resource = {
|
|
503
520
|
id: `${id}:${index + 1}`,
|
|
504
521
|
file: `${id}-${index + 1}`,
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
}
|