@larsgw/formica 0.1.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/.eslintrc.js +16 -0
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/lib/bin/process-resources-index.d.ts +1 -0
- package/lib/bin/process-resources-index.js +142 -0
- package/lib/bin/process-resources.d.ts +1 -0
- package/lib/bin/process-resources.js +594 -0
- package/lib/bin/util.d.ts +16 -0
- package/lib/bin/util.js +125 -0
- package/lib/bin/validate-catalog.d.ts +2 -0
- package/lib/bin/validate-catalog.js +92 -0
- package/lib/bin/validate-resources-text.d.ts +2 -0
- package/lib/bin/validate-resources-text.js +78 -0
- package/lib/catalog/entities.d.ts +12 -0
- package/lib/catalog/entities.js +111 -0
- package/lib/catalog/entity.d.ts +13 -0
- package/lib/catalog/entity.js +103 -0
- package/lib/catalog/index.d.ts +4 -0
- package/lib/catalog/index.js +33 -0
- package/lib/catalog/tables/author.d.ts +4 -0
- package/lib/catalog/tables/author.js +33 -0
- package/lib/catalog/tables/index.d.ts +2 -0
- package/lib/catalog/tables/index.js +13 -0
- package/lib/catalog/tables/place.d.ts +4 -0
- package/lib/catalog/tables/place.js +32 -0
- package/lib/catalog/tables/publisher.d.ts +4 -0
- package/lib/catalog/tables/publisher.js +33 -0
- package/lib/catalog/tables/work.d.ts +5 -0
- package/lib/catalog/tables/work.js +82 -0
- package/lib/catalog/value.d.ts +19 -0
- package/lib/catalog/value.js +49 -0
- package/lib/csv.d.ts +2 -0
- package/lib/csv.js +37 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +6 -0
- package/lib/resources/diff-resource.d.ts +7 -0
- package/lib/resources/diff-resource.js +152 -0
- package/lib/resources/index.d.ts +1 -0
- package/lib/resources/index.js +6 -0
- package/lib/resources/parse-text.d.ts +2 -0
- package/lib/resources/parse-text.js +499 -0
- package/lib/types.d.ts +62 -0
- package/lib/types.js +0 -0
- package/package.json +42 -0
- package/src/bin/process-resources-index.ts +73 -0
- package/src/bin/process-resources.ts +406 -0
- package/src/bin/util.ts +74 -0
- package/src/bin/validate-catalog.ts +37 -0
- package/src/bin/validate-resources-text.ts +25 -0
- package/src/catalog/entities.ts +62 -0
- package/src/catalog/entity.ts +113 -0
- package/src/catalog/index.ts +32 -0
- package/src/catalog/tables/author.ts +13 -0
- package/src/catalog/tables/index.ts +12 -0
- package/src/catalog/tables/place.ts +12 -0
- package/src/catalog/tables/publisher.ts +13 -0
- package/src/catalog/tables/work.ts +62 -0
- package/src/catalog/value.ts +48 -0
- package/src/csv.ts +33 -0
- package/src/index.ts +3 -0
- package/src/module.d.ts +105 -0
- package/src/resources/diff-resource.ts +155 -0
- package/src/resources/index.ts +4 -0
- package/src/resources/parse-text.ts +519 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { parseValue } from './value'
|
|
2
|
+
|
|
3
|
+
export class Entity {
|
|
4
|
+
fields: Record<string, Value>;
|
|
5
|
+
schema: Schema;
|
|
6
|
+
derivedFields: Record<string, Value>;
|
|
7
|
+
|
|
8
|
+
constructor (values: Record<string, string>, schema: Schema) {
|
|
9
|
+
this.fields = {}
|
|
10
|
+
for (const key in values) {
|
|
11
|
+
const value = parseValue(values[key], !!schema[key].multiple)
|
|
12
|
+
if (value !== null) {
|
|
13
|
+
this.fields[key] = value
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
this.schema = schema
|
|
18
|
+
this.derivedFields = {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
22
|
+
deriveFields () {
|
|
23
|
+
}
|
|
24
|
+
/* eslint-enable @typescript-eslint/no-empty-function */
|
|
25
|
+
|
|
26
|
+
get (key: string): Value | undefined {
|
|
27
|
+
if (key in this.fields) {
|
|
28
|
+
return this.fields[key]
|
|
29
|
+
} else if (key in this.derivedFields) {
|
|
30
|
+
return this.derivedFields[key]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
has (key: string): boolean {
|
|
35
|
+
return key in this.fields || key in this.derivedFields
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
validate (): FieldError[] {
|
|
39
|
+
const errors = []
|
|
40
|
+
for (const field in this.fields) {
|
|
41
|
+
for (const error of this._validateField(field)) {
|
|
42
|
+
errors.push({ field, error })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return errors
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_validateField (field: string) {
|
|
49
|
+
const errors = []
|
|
50
|
+
|
|
51
|
+
if (!(field in this.fields)) {
|
|
52
|
+
if (this.schema[field].required) {
|
|
53
|
+
errors.push('Value(s) required but missing')
|
|
54
|
+
}
|
|
55
|
+
return errors
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const valueCountError = this._validateValueCount(field, this.fields[field])
|
|
59
|
+
if (valueCountError !== undefined) {
|
|
60
|
+
errors.push(valueCountError)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let values = this.fields[field]
|
|
64
|
+
if (!Array.isArray(values)) {
|
|
65
|
+
values = [values]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const value of values) {
|
|
69
|
+
const result = this._validateSingleValue(field, value)
|
|
70
|
+
if (result !== undefined) {
|
|
71
|
+
errors.push(result)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return errors
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_validateValueCount (field: string, value: Value): string | undefined {
|
|
79
|
+
const multipleConfig = this.schema[field].multiple
|
|
80
|
+
const allowMultiple = typeof multipleConfig === 'function'
|
|
81
|
+
? multipleConfig.call(null, this.fields)
|
|
82
|
+
: multipleConfig
|
|
83
|
+
|
|
84
|
+
if (allowMultiple === false) {
|
|
85
|
+
let multiple = false
|
|
86
|
+
|
|
87
|
+
if (Array.isArray(value) && value.length > 2) {
|
|
88
|
+
multiple = true
|
|
89
|
+
} else if (typeof value === 'string' && value.includes('; ')) {
|
|
90
|
+
multiple = true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (multiple) {
|
|
94
|
+
return 'Multiple values but only one expected'
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_validateSingleValue (field: string, value: SingleValue): string | undefined {
|
|
102
|
+
const config = this.schema[field].format
|
|
103
|
+
if (config === undefined) {
|
|
104
|
+
return undefined
|
|
105
|
+
} else if (Array.isArray(config) && (value === null || !config.includes(value))) {
|
|
106
|
+
return `The value "${value}" is not included: ${config.join(', ')}`
|
|
107
|
+
} else if (config instanceof RegExp && !config.test(value)) {
|
|
108
|
+
return `The value "${value}" does not conform to pattern: ${config.source}`
|
|
109
|
+
} else if (typeof config === 'function' && !config.call(null, value)) {
|
|
110
|
+
return `The value "${value}" does not conform to pattern: ${config.name}`
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Entities } from './entities'
|
|
2
|
+
import { Entity } from './entity'
|
|
3
|
+
import { TYPE_INFO } from './tables/index'
|
|
4
|
+
import { parseCsv } from '../csv'
|
|
5
|
+
|
|
6
|
+
function getTypeInfo (type: string): [typeof Entity, string] {
|
|
7
|
+
switch (type) {
|
|
8
|
+
case 'authors': return TYPE_INFO.authors
|
|
9
|
+
case 'places': return TYPE_INFO.places
|
|
10
|
+
case 'publishers': return TYPE_INFO.publishers
|
|
11
|
+
case 'catalog': return TYPE_INFO.catalog
|
|
12
|
+
default: throw new TypeError(`Unknown type "${type}"`)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { Entities, Entity }
|
|
17
|
+
|
|
18
|
+
export function loadData (file: string, type: string): Entities {
|
|
19
|
+
const [subClass, indexField] = getTypeInfo(type)
|
|
20
|
+
const entities = []
|
|
21
|
+
const [header, ...rows] = parseCsv(file)
|
|
22
|
+
|
|
23
|
+
for (const row of rows) {
|
|
24
|
+
const data: Record<string, string> = {}
|
|
25
|
+
for (let index = 0; index < header.length && index < row.length; index++) {
|
|
26
|
+
data[header[index]] = row[index]
|
|
27
|
+
}
|
|
28
|
+
entities.push(new subClass(data, {}))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return new Entities(entities, indexField)
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FORMATS } from '../value'
|
|
2
|
+
import { Entity } from '../entity'
|
|
3
|
+
|
|
4
|
+
export class Author extends Entity {
|
|
5
|
+
constructor (values: Record<string, string>) {
|
|
6
|
+
super(values, {
|
|
7
|
+
name: { required: true, multiple: false },
|
|
8
|
+
qid: { required: false, multiple: false, format: FORMATS.QID },
|
|
9
|
+
main_full_name: { required: false, multiple: false },
|
|
10
|
+
full_names: { required: false, multiple: true }
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Entity } from '../entity'
|
|
2
|
+
import { Author } from './author'
|
|
3
|
+
import { Publisher } from './publisher'
|
|
4
|
+
import { Place } from './place'
|
|
5
|
+
import { Work } from './work'
|
|
6
|
+
|
|
7
|
+
export const TYPE_INFO: Record<string, [typeof Entity, string]> = {
|
|
8
|
+
authors: [Author, 'name'],
|
|
9
|
+
publishers: [Publisher, 'name'],
|
|
10
|
+
places: [Place, 'name'],
|
|
11
|
+
catalog: [Work, 'id']
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FORMATS } from '../value'
|
|
2
|
+
import { Entity } from '../entity'
|
|
3
|
+
|
|
4
|
+
export class Place extends Entity {
|
|
5
|
+
constructor (values: Record<string, string>) {
|
|
6
|
+
super(values, {
|
|
7
|
+
name: { required: true, multiple: false },
|
|
8
|
+
qid: { required: false, multiple: false, format: FORMATS.QID },
|
|
9
|
+
display_name: { required: false, multiple: false }
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FORMATS } from '../value'
|
|
2
|
+
import { Entity } from '../entity'
|
|
3
|
+
|
|
4
|
+
export class Publisher extends Entity {
|
|
5
|
+
constructor (values: Record<string, string>) {
|
|
6
|
+
super(values, {
|
|
7
|
+
name: { required: true, multiple: false },
|
|
8
|
+
qid: { required: false, multiple: false, format: FORMATS.QID },
|
|
9
|
+
full_name: { required: false, multiple: false },
|
|
10
|
+
long_name: { required: false, multiple: true }
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { FORMATS, CHECK } from '../value'
|
|
2
|
+
import { Entity } from '../entity'
|
|
3
|
+
|
|
4
|
+
export class Work extends Entity {
|
|
5
|
+
constructor (values: Record<string, string>) {
|
|
6
|
+
super(values, {
|
|
7
|
+
id: { required: true, multiple: false, format: FORMATS.ID },
|
|
8
|
+
title: { required: true, multiple: CHECK.MULTILANG },
|
|
9
|
+
author: { required: false, multiple: true },
|
|
10
|
+
url: { required: false, multiple: true, format: FORMATS.URL },
|
|
11
|
+
fulltext_url: { required: false, multiple: true, format: FORMATS.URL },
|
|
12
|
+
archive_url: { required: false, multiple: true, format: FORMATS.URL },
|
|
13
|
+
entry_type: { required: true, multiple: false, format: FORMATS.ENTRY_TYPE },
|
|
14
|
+
date: { required: false, multiple: false, format: FORMATS.EDTF_0 },
|
|
15
|
+
publisher: { required: false, multiple: true },
|
|
16
|
+
series: { required: false, multiple: false },
|
|
17
|
+
ISSN: { required: false, multiple: false, format: FORMATS.ISSN_L },
|
|
18
|
+
ISBN: { required: false, multiple: CHECK.ISBN, format: FORMATS.ISBN },
|
|
19
|
+
DOI: { required: false, multiple: false, format: FORMATS.DOI },
|
|
20
|
+
QID: { required: false, multiple: false, format: FORMATS.QID },
|
|
21
|
+
volume: { required: false, multiple: false },
|
|
22
|
+
issue: { required: false, multiple: false },
|
|
23
|
+
pages: { required: false, multiple: false },
|
|
24
|
+
edition: { required: false, multiple: false },
|
|
25
|
+
language: { required: true, multiple: true, format: FORMATS.LANGUAGE },
|
|
26
|
+
license: { required: false, multiple: true, format: FORMATS.LICENSE },
|
|
27
|
+
key_type: { required: true, multiple: true, format: FORMATS.KEY_TYPE },
|
|
28
|
+
taxon: { required: true, multiple: true },
|
|
29
|
+
scope: { required: false, multiple: true },
|
|
30
|
+
region: { required: true, multiple: true },
|
|
31
|
+
complete: { required: false, multiple: false, format: FORMATS.COMPLETE },
|
|
32
|
+
target_taxa: { required: false, multiple: true },
|
|
33
|
+
listed_in: { required: false, multiple: true, format: FORMATS.ID },
|
|
34
|
+
part_of: { required: false, multiple: true, format: FORMATS.ID },
|
|
35
|
+
version_of: { required: false, multiple: true, format: FORMATS.ID }
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
deriveFields () {
|
|
40
|
+
if (typeof this.fields.date === 'string') {
|
|
41
|
+
const year = parseInt(this.fields.date.split('-')[0])
|
|
42
|
+
this.derivedFields.year = year.toString()
|
|
43
|
+
this.derivedFields.decade = (year - (year % 10)).toString()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof this.fields.license === 'string' && !this.fields.license.endsWith('?>')) {
|
|
47
|
+
this.derivedFields.access = 'Open license'
|
|
48
|
+
} else {
|
|
49
|
+
const info = this.fields.url
|
|
50
|
+
const content = this.fields.fulltext_url
|
|
51
|
+
const archive = this.fields.archive_url
|
|
52
|
+
|
|
53
|
+
if (typeof content === 'string') {
|
|
54
|
+
this.derivedFields.access = 'Full text available, no license'
|
|
55
|
+
} else if (typeof archive === 'string' && (!(typeof info === 'string') || !archive.endsWith(info))) {
|
|
56
|
+
this.derivedFields.access = 'Archived full text available, no license'
|
|
57
|
+
} else {
|
|
58
|
+
this.derivedFields.access = 'No full text available'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import spdxLicenseList = require('spdx-license-list')
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import ietfTagListFactory = require('ietf-language-tag-regex')
|
|
5
|
+
|
|
6
|
+
const ietfTagList = ietfTagListFactory()
|
|
7
|
+
|
|
8
|
+
export const FORMATS = {
|
|
9
|
+
ENTRY_TYPE: ['print', 'online', 'cd'],
|
|
10
|
+
KEY_TYPE: ['key', 'matrix', 'reference', 'gallery', 'checklist', 'supplement', 'collection'],
|
|
11
|
+
COMPLETE: ['TRUE', 'FALSE'],
|
|
12
|
+
|
|
13
|
+
ID: /^B\d+$/,
|
|
14
|
+
EDTF_0: /^(\d{4}(-\d{2}(-\d{2}(T\d{2}:\d{2}:\d{2}(Z|[-+]\d{2}(:\d{2})?))?)?)?|\d{4}(-\d{2}(-\d{2})?)?\/(\d{4}(-\d{2}(-\d{2})?)?|\.\.))$/,
|
|
15
|
+
ISSN_L: /^[0-9]{4}-[0-9]{3}[0-9X]$/,
|
|
16
|
+
ISBN: /^(\d{13}|\d{9}[0-9X])$/,
|
|
17
|
+
DOI: /^10\./,
|
|
18
|
+
QID: /^Q[1-9][0-9]*$/,
|
|
19
|
+
URL: /^(ftp|http|https):\/\/((?:[a-z0-9][a-z0-9-_]*?[a-z0-9]?\.)+(?:xn--)?[a-z0-9]+)(:\d*)?((?:\/(?:%\d\d|[!$&'()*+,\-.0-9";=@A-Z_a-z~])*)*)(\?(?:%\d\d|[!$&'()*+,\-./0-9:;=?@A-Z_a-z~])*)?(#(?:%\d\d|[!$&'()*+,\-./0-9:;=?@A-Z_a-z~])*)?/i,
|
|
20
|
+
|
|
21
|
+
LICENSE (value: SingleValue) { return /^<(public domain|.+\?)>$/.test(value) || !!spdxLicenseList[value] },
|
|
22
|
+
LANGUAGE (value: SingleValue) { return ietfTagList.test(value) }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const CHECK = {
|
|
26
|
+
MULTILANG (entry: Record<string, Value>) {
|
|
27
|
+
return entry.language.length > 1
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
ISBN (entry: Record<string, Value>) {
|
|
31
|
+
if (entry.ISBN.length < 2) { return false }
|
|
32
|
+
const a = entry.ISBN[0].length === 10
|
|
33
|
+
const b = entry.ISBN[0].length === 13
|
|
34
|
+
const c = entry.ISBN[1].length === 10
|
|
35
|
+
const d = entry.ISBN[1].length === 13
|
|
36
|
+
return (a && d) || (b && c)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseValue (value: string, multiple: boolean): Value | null {
|
|
41
|
+
if (value === '') {
|
|
42
|
+
return null
|
|
43
|
+
} else if (multiple) {
|
|
44
|
+
return value.split('; ')
|
|
45
|
+
} else {
|
|
46
|
+
return value
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/csv.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function parseCsv (file: string): string[][] {
|
|
2
|
+
const values = file
|
|
3
|
+
.trim()
|
|
4
|
+
.match(/("([^"]|"")*?"|[^,\n]+|(?!$))(,|\n|$)/g)
|
|
5
|
+
|
|
6
|
+
if (values === null) {
|
|
7
|
+
throw new TypeError('Failed to parse csv')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return values.reduce((rows, value) => {
|
|
11
|
+
const last: string[] = rows[rows.length - 1]
|
|
12
|
+
if (value.endsWith('\n')) {
|
|
13
|
+
rows.push([])
|
|
14
|
+
}
|
|
15
|
+
value = value.replace(/[,\n]$/, '')
|
|
16
|
+
last.push(value.startsWith('"') ? value.replace(/""/g, '"').slice(1, -1) : value)
|
|
17
|
+
return rows
|
|
18
|
+
}, [[]])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatCsv (table: string[][], delimiter = ',') {
|
|
22
|
+
return table
|
|
23
|
+
.map((row: string[]) => {
|
|
24
|
+
return row.map(value => {
|
|
25
|
+
if (/["\n]/.test(value) || value.includes(delimiter)) {
|
|
26
|
+
return `"${value.replace(/"/, '"""')}"`
|
|
27
|
+
} else {
|
|
28
|
+
return value
|
|
29
|
+
}
|
|
30
|
+
}).join(delimiter)
|
|
31
|
+
})
|
|
32
|
+
.join('\n') + '\n'
|
|
33
|
+
}
|
package/src/index.ts
ADDED
package/src/module.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
type Schema = Record<string, FieldSpecification>
|
|
2
|
+
type FieldSpecification = {
|
|
3
|
+
required: boolean,
|
|
4
|
+
multiple: boolean | FieldSpecificationCallbackMultiple
|
|
5
|
+
format?: string[] | RegExp | FieldSpecificationCallbackFormat
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type Value = string[] | string
|
|
9
|
+
type SingleValue = string
|
|
10
|
+
type FieldSpecificationCallbackMultiple = (entry: Record<string, Value>) => boolean;
|
|
11
|
+
type FieldSpecificationCallbackFormat = (value: SingleValue) => boolean;
|
|
12
|
+
|
|
13
|
+
interface FieldError {
|
|
14
|
+
field: string,
|
|
15
|
+
error: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface WorkError extends FieldError {
|
|
19
|
+
entity: WorkId
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Rank = string
|
|
23
|
+
type DwcRank = Rank
|
|
24
|
+
type TaxonStatus = string
|
|
25
|
+
|
|
26
|
+
type TaxonId = string
|
|
27
|
+
type ResourceId = string
|
|
28
|
+
type WorkId = string
|
|
29
|
+
|
|
30
|
+
interface WorkingTaxon {
|
|
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
|
+
}
|
|
63
|
+
|
|
64
|
+
interface Taxon extends WorkingTaxon {
|
|
65
|
+
scientificNameID: TaxonId,
|
|
66
|
+
scientificName: string,
|
|
67
|
+
taxonRank: Rank,
|
|
68
|
+
collectionCode: ResourceId,
|
|
69
|
+
taxonomicStatus: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ResourceMetadata {
|
|
73
|
+
levels: Rank[],
|
|
74
|
+
scope: string[],
|
|
75
|
+
catalog?: object
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface Resource {
|
|
79
|
+
id: string,
|
|
80
|
+
file: string,
|
|
81
|
+
workId: string,
|
|
82
|
+
metadata: ResourceMetadata,
|
|
83
|
+
taxa: Record<TaxonId, Taxon>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface ResourceHistory {
|
|
87
|
+
txt: string,
|
|
88
|
+
dwc: Array<string[][]>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type ResourceDiff = ResourceDiffPart[]
|
|
92
|
+
|
|
93
|
+
interface ResourceDiffPart {
|
|
94
|
+
text: string,
|
|
95
|
+
type: ResourceDiffType
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type ResourceDiffTokenizer = (text: string) => string[]
|
|
99
|
+
|
|
100
|
+
declare enum ResourceDiffType {
|
|
101
|
+
Added = '+',
|
|
102
|
+
Deleted = '-',
|
|
103
|
+
Modified = '~',
|
|
104
|
+
Unchanged = '='
|
|
105
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
export enum ResourceDiffType {
|
|
2
|
+
Added = '+',
|
|
3
|
+
Deleted = '-',
|
|
4
|
+
Modified = '~',
|
|
5
|
+
Unchanged = '='
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function LCS (X: string[], Y: string[]): ResourceDiffPart[] {
|
|
9
|
+
const m = X.length
|
|
10
|
+
const n = Y.length
|
|
11
|
+
|
|
12
|
+
// Build matrix
|
|
13
|
+
const C = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0))
|
|
14
|
+
for (let i = 0; i < m; i++) {
|
|
15
|
+
for (let j = 0; j < n; j++) {
|
|
16
|
+
if (X[i] === Y[j]) {
|
|
17
|
+
C[i + 1][j + 1] = C[i][j] + 1
|
|
18
|
+
} else {
|
|
19
|
+
C[i + 1][j + 1] = Math.max(C[i][j + 1], C[i + 1][j])
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Backtrace
|
|
25
|
+
const diff = []
|
|
26
|
+
let i = m
|
|
27
|
+
let j = n
|
|
28
|
+
while (i + j !== 0) {
|
|
29
|
+
if (X[i - 1] === Y[j - 1]) {
|
|
30
|
+
diff.unshift({
|
|
31
|
+
text: X[i - 1],
|
|
32
|
+
type: ResourceDiffType.Unchanged
|
|
33
|
+
})
|
|
34
|
+
i--, j--
|
|
35
|
+
} else if (i !== 0 && (j === 0 || C[i - 1][j] > C[i][j - 1])) {
|
|
36
|
+
diff.unshift({
|
|
37
|
+
text: X[i - 1],
|
|
38
|
+
type: ResourceDiffType.Added
|
|
39
|
+
})
|
|
40
|
+
i--
|
|
41
|
+
} else {
|
|
42
|
+
diff.unshift({
|
|
43
|
+
text: Y[j - 1],
|
|
44
|
+
type: ResourceDiffType.Deleted
|
|
45
|
+
})
|
|
46
|
+
j--
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return diff
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function gitTokenize (text: string): string[] {
|
|
54
|
+
if (text.length === 0) {
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
return text.match(/\S+|\n|[\r\t\f\v \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/g) as string[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createDiff (a: string, b: string, tokenize: ResourceDiffTokenizer = gitTokenize): ResourceDiffPart[] {
|
|
61
|
+
const X = tokenize(a.trimEnd())
|
|
62
|
+
const Y = tokenize(b.trimEnd())
|
|
63
|
+
|
|
64
|
+
// Remove common prefix
|
|
65
|
+
const prefix = []
|
|
66
|
+
while (X.length && X[0] === Y[0]) {
|
|
67
|
+
prefix.push({
|
|
68
|
+
text: X[0],
|
|
69
|
+
type: ResourceDiffType.Unchanged
|
|
70
|
+
})
|
|
71
|
+
X.shift()
|
|
72
|
+
Y.shift()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Remove common suffix
|
|
76
|
+
const suffix = []
|
|
77
|
+
while (X.length && X[X.length - 1] === Y[Y.length - 1]) {
|
|
78
|
+
suffix.unshift({
|
|
79
|
+
text: X[X.length - 1],
|
|
80
|
+
type: ResourceDiffType.Unchanged
|
|
81
|
+
})
|
|
82
|
+
X.pop()
|
|
83
|
+
Y.pop()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Generate word from remains, combine with prefix and suffix, add trailing
|
|
87
|
+
// newline
|
|
88
|
+
const changes = [
|
|
89
|
+
...prefix,
|
|
90
|
+
...LCS(X, Y),
|
|
91
|
+
...suffix,
|
|
92
|
+
{ text: '\n', type: ResourceDiffType.Unchanged }
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
// Convert word diff to line diff
|
|
96
|
+
const lines: ResourceDiff = []
|
|
97
|
+
let line = null
|
|
98
|
+
let deletedNewlines = 0
|
|
99
|
+
let nextLineNew = false
|
|
100
|
+
|
|
101
|
+
for (const change of changes) {
|
|
102
|
+
// Start of line
|
|
103
|
+
if (line === null) {
|
|
104
|
+
deletedNewlines = 0
|
|
105
|
+
line = { text: '', type: change.type }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// End of line (could be same token)
|
|
109
|
+
if (change.text === '\n') {
|
|
110
|
+
if (change.type === ResourceDiffType.Deleted) {
|
|
111
|
+
// Keep track of deleted newlines. These indicate the merging of
|
|
112
|
+
// lines. To keep the taxon identifiers consistent, it is
|
|
113
|
+
// important that deleted taxa are tracked.
|
|
114
|
+
deletedNewlines++
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// If there is a new newline, there is a new... line. If the current
|
|
119
|
+
// line is not seen as new, the next one should be marked as new.
|
|
120
|
+
if (nextLineNew) {
|
|
121
|
+
line.type = ResourceDiffType.Added
|
|
122
|
+
nextLineNew = false
|
|
123
|
+
}
|
|
124
|
+
if (change.type === ResourceDiffType.Added && line.type !== ResourceDiffType.Added) {
|
|
125
|
+
nextLineNew = true
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Line ended
|
|
129
|
+
lines.push(line)
|
|
130
|
+
line = null
|
|
131
|
+
|
|
132
|
+
// Add placeholders for deleted lines
|
|
133
|
+
while (deletedNewlines--) {
|
|
134
|
+
lines.push({
|
|
135
|
+
text: '',
|
|
136
|
+
type: ResourceDiffType.Deleted
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Added/Deleted/Unchanged on the same line -> line is modified but did
|
|
144
|
+
// already exist.
|
|
145
|
+
if (change.type !== line.type) {
|
|
146
|
+
line.type = ResourceDiffType.Modified
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (change.type !== ResourceDiffType.Deleted) {
|
|
150
|
+
line.text += change.text
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return lines
|
|
155
|
+
}
|