@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.
Files changed (65) hide show
  1. package/.eslintrc.js +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +19 -0
  4. package/lib/bin/process-resources-index.d.ts +1 -0
  5. package/lib/bin/process-resources-index.js +142 -0
  6. package/lib/bin/process-resources.d.ts +1 -0
  7. package/lib/bin/process-resources.js +594 -0
  8. package/lib/bin/util.d.ts +16 -0
  9. package/lib/bin/util.js +125 -0
  10. package/lib/bin/validate-catalog.d.ts +2 -0
  11. package/lib/bin/validate-catalog.js +92 -0
  12. package/lib/bin/validate-resources-text.d.ts +2 -0
  13. package/lib/bin/validate-resources-text.js +78 -0
  14. package/lib/catalog/entities.d.ts +12 -0
  15. package/lib/catalog/entities.js +111 -0
  16. package/lib/catalog/entity.d.ts +13 -0
  17. package/lib/catalog/entity.js +103 -0
  18. package/lib/catalog/index.d.ts +4 -0
  19. package/lib/catalog/index.js +33 -0
  20. package/lib/catalog/tables/author.d.ts +4 -0
  21. package/lib/catalog/tables/author.js +33 -0
  22. package/lib/catalog/tables/index.d.ts +2 -0
  23. package/lib/catalog/tables/index.js +13 -0
  24. package/lib/catalog/tables/place.d.ts +4 -0
  25. package/lib/catalog/tables/place.js +32 -0
  26. package/lib/catalog/tables/publisher.d.ts +4 -0
  27. package/lib/catalog/tables/publisher.js +33 -0
  28. package/lib/catalog/tables/work.d.ts +5 -0
  29. package/lib/catalog/tables/work.js +82 -0
  30. package/lib/catalog/value.d.ts +19 -0
  31. package/lib/catalog/value.js +49 -0
  32. package/lib/csv.d.ts +2 -0
  33. package/lib/csv.js +37 -0
  34. package/lib/index.d.ts +3 -0
  35. package/lib/index.js +6 -0
  36. package/lib/resources/diff-resource.d.ts +7 -0
  37. package/lib/resources/diff-resource.js +152 -0
  38. package/lib/resources/index.d.ts +1 -0
  39. package/lib/resources/index.js +6 -0
  40. package/lib/resources/parse-text.d.ts +2 -0
  41. package/lib/resources/parse-text.js +499 -0
  42. package/lib/types.d.ts +62 -0
  43. package/lib/types.js +0 -0
  44. package/package.json +42 -0
  45. package/src/bin/process-resources-index.ts +73 -0
  46. package/src/bin/process-resources.ts +406 -0
  47. package/src/bin/util.ts +74 -0
  48. package/src/bin/validate-catalog.ts +37 -0
  49. package/src/bin/validate-resources-text.ts +25 -0
  50. package/src/catalog/entities.ts +62 -0
  51. package/src/catalog/entity.ts +113 -0
  52. package/src/catalog/index.ts +32 -0
  53. package/src/catalog/tables/author.ts +13 -0
  54. package/src/catalog/tables/index.ts +12 -0
  55. package/src/catalog/tables/place.ts +12 -0
  56. package/src/catalog/tables/publisher.ts +13 -0
  57. package/src/catalog/tables/work.ts +62 -0
  58. package/src/catalog/value.ts +48 -0
  59. package/src/csv.ts +33 -0
  60. package/src/index.ts +3 -0
  61. package/src/module.d.ts +105 -0
  62. package/src/resources/diff-resource.ts +155 -0
  63. package/src/resources/index.ts +4 -0
  64. package/src/resources/parse-text.ts +519 -0
  65. 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
@@ -0,0 +1,3 @@
1
+ export * as catalog from './catalog/index'
2
+ export * as resources from './resources/index'
3
+ export * as csv from './csv'
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export {
2
+ parseFile as parseTextFile,
3
+ parseFileHeader as parseTextFileHeader
4
+ } from './parse-text'