@larsgw/formica 0.6.8 → 0.7.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 +19 -0
- package/lib/resources/diff-resource.d.ts +1 -1
- package/lib/resources/diff-resource.js +8 -3
- package/lib/resources/parse-text.js +39 -28
- package/package.json +1 -1
- package/src/module.d.ts +2 -1
- package/src/resources/diff-resource.ts +16 -6
- package/src/resources/parse-text.ts +38 -24
- package/test/resources.js +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
# [0.7.0](https://github.com/identification-resources/formica/compare/v0.6.8...v0.7.0) (2025-03-14)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **resources:** improve diff regarding indet lines ([2615437](https://github.com/identification-resources/formica/commit/26154374f50d680c5f3e187cd6a1a5eadbac6b5d))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **resources:** change syntax of "indet" line ([d53363e](https://github.com/identification-resources/formica/commit/d53363e65bfa77a2eb247999316621f9ca0bff59))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### BREAKING CHANGES
|
|
15
|
+
|
|
16
|
+
* **resources:** "indet" lines now have to be prefixed with "[indet]"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
1
20
|
## [0.6.8](https://github.com/identification-resources/formica/compare/v0.6.7...v0.6.8) (2025-03-13)
|
|
2
21
|
|
|
3
22
|
|
|
@@ -4,4 +4,4 @@ export declare enum ResourceDiffType {
|
|
|
4
4
|
Modified = "~",
|
|
5
5
|
Unchanged = "="
|
|
6
6
|
}
|
|
7
|
-
export declare function createDiff(a: string, b: string, tokenize?: ResourceDiffTokenizer):
|
|
7
|
+
export declare function createDiff(a: string, b: string, tokenize?: ResourceDiffTokenizer): ResourceDiff;
|
|
@@ -68,6 +68,7 @@ function gitTokenize(text) {
|
|
|
68
68
|
return text.match(/\S+|\n|[\r\t\f\v \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/g);
|
|
69
69
|
}
|
|
70
70
|
function createDiff(a, b, tokenize) {
|
|
71
|
+
var _a, _b;
|
|
71
72
|
if (tokenize === void 0) { tokenize = gitTokenize; }
|
|
72
73
|
var X = tokenize(a.trimEnd());
|
|
73
74
|
var Y = tokenize(b.trimEnd());
|
|
@@ -106,7 +107,7 @@ function createDiff(a, b, tokenize) {
|
|
|
106
107
|
// Start of line
|
|
107
108
|
if (line === null) {
|
|
108
109
|
deletedNewlines = 0;
|
|
109
|
-
line = {
|
|
110
|
+
line = { type: change.type };
|
|
110
111
|
}
|
|
111
112
|
// End of line (could be same token)
|
|
112
113
|
if (change.text === '\n') {
|
|
@@ -132,7 +133,8 @@ function createDiff(a, b, tokenize) {
|
|
|
132
133
|
// Add placeholders for deleted lines
|
|
133
134
|
while (deletedNewlines--) {
|
|
134
135
|
lines.push({
|
|
135
|
-
text:
|
|
136
|
+
text: undefined,
|
|
137
|
+
original: '',
|
|
136
138
|
type: ResourceDiffType.Deleted
|
|
137
139
|
});
|
|
138
140
|
}
|
|
@@ -144,7 +146,10 @@ function createDiff(a, b, tokenize) {
|
|
|
144
146
|
line.type = ResourceDiffType.Modified;
|
|
145
147
|
}
|
|
146
148
|
if (change.type !== ResourceDiffType.Deleted) {
|
|
147
|
-
line.text
|
|
149
|
+
line.text = ((_a = line.text) !== null && _a !== void 0 ? _a : '') + change.text;
|
|
150
|
+
}
|
|
151
|
+
if (change.type !== ResourceDiffType.Added) {
|
|
152
|
+
line.original = ((_b = line.original) !== null && _b !== void 0 ? _b : '') + change.text;
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
return lines;
|
|
@@ -74,13 +74,6 @@ var TAXONOMIC_STATUS = {
|
|
|
74
74
|
'+': 'heterotypic synonym',
|
|
75
75
|
'=': 'synonym'
|
|
76
76
|
};
|
|
77
|
-
var INDET_SUFFIXES = new Set([
|
|
78
|
-
'sp.',
|
|
79
|
-
'spec.',
|
|
80
|
-
'indet.',
|
|
81
|
-
'sp. indet.',
|
|
82
|
-
'spec. indet.'
|
|
83
|
-
]);
|
|
84
77
|
var RANK_LABELS = {
|
|
85
78
|
'subspecies': 'subsp.',
|
|
86
79
|
'variety': 'var.',
|
|
@@ -281,7 +274,7 @@ function parseName(name, rank, parent) {
|
|
|
281
274
|
item.scientificNameAuthorship = capitalizeAuthors(citation);
|
|
282
275
|
item.taxonRemarks = notes;
|
|
283
276
|
item.taxonRank = rank;
|
|
284
|
-
if (/[^\p{L}0-9\u{00D7}\- ]/u.test(taxon)
|
|
277
|
+
if (/[^\p{L}0-9\u{00D7}\- ]/u.test(taxon)) {
|
|
285
278
|
throw new Error("Taxon name contains unexpected characters: \"".concat(taxon, "\""));
|
|
286
279
|
}
|
|
287
280
|
// Validate names and recompose binomial and trinomial names
|
|
@@ -457,6 +450,16 @@ function parseResource(resource) {
|
|
|
457
450
|
var content = rest.join('');
|
|
458
451
|
return [config, content];
|
|
459
452
|
}
|
|
453
|
+
function getIndentation(line) {
|
|
454
|
+
return line.match(/^ */)[0].length;
|
|
455
|
+
}
|
|
456
|
+
function isIndetLine(line, indent) {
|
|
457
|
+
if (indent === undefined) {
|
|
458
|
+
indent = getIndentation(line);
|
|
459
|
+
}
|
|
460
|
+
line = line.slice(indent);
|
|
461
|
+
return line.startsWith('[indet]');
|
|
462
|
+
}
|
|
460
463
|
function parseResourceContent(content, resource, oldIds) {
|
|
461
464
|
var idBase = "".concat(resource.id, ":");
|
|
462
465
|
var data = resource.taxa;
|
|
@@ -465,20 +468,19 @@ function parseResourceContent(content, resource, oldIds) {
|
|
|
465
468
|
var groupIndent = 0;
|
|
466
469
|
var previousId = '';
|
|
467
470
|
var newIdOffset = Math.max.apply(Math, oldIds);
|
|
468
|
-
var
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return "continue";
|
|
471
|
+
for (var _i = 0, content_1 = content; _i < content_1.length; _i++) {
|
|
472
|
+
var line = content_1[_i];
|
|
473
|
+
if (line.type === diff_resource_1.ResourceDiffType.Deleted) {
|
|
474
|
+
// Increase id counter for removed line unless it was an "indet line"
|
|
475
|
+
if (!isIndetLine(line.original)) {
|
|
476
|
+
id++;
|
|
477
|
+
}
|
|
478
|
+
continue;
|
|
477
479
|
}
|
|
478
|
-
var lineIndent = line.
|
|
480
|
+
var lineIndent = getIndentation(line.text);
|
|
479
481
|
if (lineIndent > groupIndent) {
|
|
480
482
|
// Do not count synonyms as parents (unless this is correcting a typo in the synonym)
|
|
481
|
-
if (data[previousId] && data[previousId].taxonomicStatus === 'accepted' || /^( {2})+> /.test(line)) {
|
|
483
|
+
if (data[previousId] && data[previousId].taxonomicStatus === 'accepted' || /^( {2})+> /.test(line.text)) {
|
|
482
484
|
parents.push(previousId);
|
|
483
485
|
}
|
|
484
486
|
else {
|
|
@@ -500,17 +502,26 @@ function parseResourceContent(content, resource, oldIds) {
|
|
|
500
502
|
parents = parents.slice(0, lineIndent / 2);
|
|
501
503
|
groupIndent = lineIndent;
|
|
502
504
|
}
|
|
505
|
+
// Do not process "indet" lines further, as they only serve to indicate
|
|
506
|
+
// that subtaxa are explicitely omitted
|
|
507
|
+
if (isIndetLine(line.text, lineIndent)) {
|
|
508
|
+
// If the line was previously not and ndet line, increase the id counter
|
|
509
|
+
if (line.type === diff_resource_1.ResourceDiffType.Modified && !isIndetLine(line.original)) {
|
|
510
|
+
id++;
|
|
511
|
+
}
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
503
514
|
var parentId = parents.reduce(function (grandparent, parent) { return parent || grandparent; }, null);
|
|
504
515
|
var parent_1 = parentId === null ? {} : data[parentId];
|
|
505
|
-
var name_1 = line.slice(groupIndent);
|
|
516
|
+
var name_1 = line.text.slice(groupIndent);
|
|
506
517
|
var rank = resource.metadata.levels[groupIndent / 2];
|
|
507
518
|
var item = parseName(name_1, rank, parent_1);
|
|
508
519
|
var isSynonym = item.taxonomicStatus !== 'accepted';
|
|
509
520
|
// Add higher classification info
|
|
510
521
|
var itemAsObject = item;
|
|
511
522
|
var parentAsObject = parent_1;
|
|
512
|
-
for (var
|
|
513
|
-
var rank_1 = DWC_RANKS_1[
|
|
523
|
+
for (var _a = 0, DWC_RANKS_1 = DWC_RANKS; _a < DWC_RANKS_1.length; _a++) {
|
|
524
|
+
var rank_1 = DWC_RANKS_1[_a];
|
|
514
525
|
itemAsObject[rank_1] = undefined;
|
|
515
526
|
if (parentAsObject[rank_1]) {
|
|
516
527
|
itemAsObject[rank_1] = parentAsObject[rank_1];
|
|
@@ -542,10 +553,14 @@ function parseResourceContent(content, resource, oldIds) {
|
|
|
542
553
|
parentAsObject[key] = itemAsObject[key];
|
|
543
554
|
}
|
|
544
555
|
}
|
|
545
|
-
|
|
556
|
+
continue;
|
|
546
557
|
}
|
|
547
558
|
// Set identifiers
|
|
548
|
-
if (type === diff_resource_1.ResourceDiffType.Added) {
|
|
559
|
+
if (line.type === diff_resource_1.ResourceDiffType.Added) {
|
|
560
|
+
newIdOffset++;
|
|
561
|
+
item.scientificNameID = idBase + newIdOffset.toString();
|
|
562
|
+
}
|
|
563
|
+
else if (line.type === diff_resource_1.ResourceDiffType.Modified && isIndetLine(line.original)) {
|
|
549
564
|
newIdOffset++;
|
|
550
565
|
item.scientificNameID = idBase + newIdOffset.toString();
|
|
551
566
|
}
|
|
@@ -560,10 +575,6 @@ function parseResourceContent(content, resource, oldIds) {
|
|
|
560
575
|
item.acceptedNameUsage = isSynonym ? parent_1.scientificName : undefined;
|
|
561
576
|
item.collectionCode = idBase.slice(0, -1);
|
|
562
577
|
data[item.scientificNameID] = item;
|
|
563
|
-
};
|
|
564
|
-
for (var _i = 0, content_1 = content; _i < content_1.length; _i++) {
|
|
565
|
-
var _a = content_1[_i], line = _a.text, type = _a.type;
|
|
566
|
-
_loop_1(line, type);
|
|
567
578
|
}
|
|
568
579
|
return resource;
|
|
569
580
|
}
|
package/package.json
CHANGED
package/src/module.d.ts
CHANGED
|
@@ -5,7 +5,12 @@ export enum ResourceDiffType {
|
|
|
5
5
|
Unchanged = '='
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
interface DiffPart {
|
|
9
|
+
text: string,
|
|
10
|
+
type: ResourceDiffType
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function LCS (X: string[], Y: string[]): DiffPart[] {
|
|
9
14
|
const m = X.length
|
|
10
15
|
const n = Y.length
|
|
11
16
|
|
|
@@ -57,7 +62,7 @@ function gitTokenize (text: string): string[] {
|
|
|
57
62
|
return text.match(/\S+|\n|[\r\t\f\v \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/g) as string[]
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
export function createDiff (a: string, b: string, tokenize: ResourceDiffTokenizer = gitTokenize):
|
|
65
|
+
export function createDiff (a: string, b: string, tokenize: ResourceDiffTokenizer = gitTokenize): ResourceDiff {
|
|
61
66
|
const X = tokenize(a.trimEnd())
|
|
62
67
|
const Y = tokenize(b.trimEnd())
|
|
63
68
|
|
|
@@ -94,7 +99,7 @@ export function createDiff (a: string, b: string, tokenize: ResourceDiffTokenize
|
|
|
94
99
|
|
|
95
100
|
// Convert word diff to line diff
|
|
96
101
|
const lines: ResourceDiff = []
|
|
97
|
-
let line = null
|
|
102
|
+
let line: ResourceDiffPart|null = null
|
|
98
103
|
let deletedNewlines = 0
|
|
99
104
|
let nextLineNew = false
|
|
100
105
|
|
|
@@ -102,7 +107,7 @@ export function createDiff (a: string, b: string, tokenize: ResourceDiffTokenize
|
|
|
102
107
|
// Start of line
|
|
103
108
|
if (line === null) {
|
|
104
109
|
deletedNewlines = 0
|
|
105
|
-
line = {
|
|
110
|
+
line = { type: change.type }
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
// End of line (could be same token)
|
|
@@ -132,7 +137,8 @@ export function createDiff (a: string, b: string, tokenize: ResourceDiffTokenize
|
|
|
132
137
|
// Add placeholders for deleted lines
|
|
133
138
|
while (deletedNewlines--) {
|
|
134
139
|
lines.push({
|
|
135
|
-
text:
|
|
140
|
+
text: undefined,
|
|
141
|
+
original: '',
|
|
136
142
|
type: ResourceDiffType.Deleted
|
|
137
143
|
})
|
|
138
144
|
}
|
|
@@ -147,7 +153,11 @@ export function createDiff (a: string, b: string, tokenize: ResourceDiffTokenize
|
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
if (change.type !== ResourceDiffType.Deleted) {
|
|
150
|
-
line.text
|
|
156
|
+
line.text = (line.text ?? '') + change.text
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (change.type !== ResourceDiffType.Added) {
|
|
160
|
+
line.original = (line.original ?? '') + change.text
|
|
151
161
|
}
|
|
152
162
|
}
|
|
153
163
|
|
|
@@ -66,14 +66,6 @@ const TAXONOMIC_STATUS: Record<string, TaxonStatus> = {
|
|
|
66
66
|
'=': 'synonym'
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const INDET_SUFFIXES = new Set([
|
|
70
|
-
'sp.',
|
|
71
|
-
'spec.',
|
|
72
|
-
'indet.',
|
|
73
|
-
'sp. indet.',
|
|
74
|
-
'spec. indet.'
|
|
75
|
-
])
|
|
76
|
-
|
|
77
69
|
const RANK_LABELS: Record<Rank, string> = {
|
|
78
70
|
'subspecies': 'subsp.',
|
|
79
71
|
'variety': 'var.',
|
|
@@ -301,7 +293,7 @@ function parseName (name: string, rank: Rank, parent: WorkingTaxon): WorkingTaxo
|
|
|
301
293
|
item.taxonRemarks = notes
|
|
302
294
|
item.taxonRank = rank
|
|
303
295
|
|
|
304
|
-
if (/[^\p{L}0-9\u{00D7}\- ]/u.test(taxon)
|
|
296
|
+
if (/[^\p{L}0-9\u{00D7}\- ]/u.test(taxon)) {
|
|
305
297
|
throw new Error(`Taxon name contains unexpected characters: "${taxon}"`)
|
|
306
298
|
}
|
|
307
299
|
|
|
@@ -488,6 +480,20 @@ function parseResource (resource: string): [ResourceMetadata, string] {
|
|
|
488
480
|
return [config, content]
|
|
489
481
|
}
|
|
490
482
|
|
|
483
|
+
function getIndentation (line: string): number {
|
|
484
|
+
return (line.match(/^ */) as string[])[0].length
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function isIndetLine (line: string, indent?: number): boolean {
|
|
488
|
+
if (indent === undefined) {
|
|
489
|
+
indent = getIndentation(line)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
line = line.slice(indent)
|
|
493
|
+
|
|
494
|
+
return line.startsWith('[indet]')
|
|
495
|
+
}
|
|
496
|
+
|
|
491
497
|
function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds: number[]): Resource {
|
|
492
498
|
const idBase = `${resource.id}:`
|
|
493
499
|
|
|
@@ -498,23 +504,19 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
498
504
|
let previousId = ''
|
|
499
505
|
let newIdOffset = Math.max(...oldIds)
|
|
500
506
|
|
|
501
|
-
for (const
|
|
502
|
-
if (type === ResourceDiffType.Deleted) {
|
|
503
|
-
id
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
// Do not process "indet" lines further, as they only serve to indicate
|
|
508
|
-
// that subtaxa are explicitely omitted
|
|
509
|
-
if (Array.from(INDET_SUFFIXES).some(suffix => line.endsWith(' ' + suffix))) {
|
|
507
|
+
for (const line of content) {
|
|
508
|
+
if (line.type === ResourceDiffType.Deleted) {
|
|
509
|
+
// Increase id counter for removed line unless it was an "indet line"
|
|
510
|
+
if (!isIndetLine(line.original as string)) {
|
|
511
|
+
id++
|
|
512
|
+
}
|
|
510
513
|
continue
|
|
511
514
|
}
|
|
512
515
|
|
|
513
|
-
const lineIndent = (line.
|
|
514
|
-
|
|
516
|
+
const lineIndent = getIndentation(line.text as string)
|
|
515
517
|
if (lineIndent > groupIndent) {
|
|
516
518
|
// Do not count synonyms as parents (unless this is correcting a typo in the synonym)
|
|
517
|
-
if (data[previousId] && data[previousId].taxonomicStatus === 'accepted' || /^( {2})+> /.test(line)) {
|
|
519
|
+
if (data[previousId] && data[previousId].taxonomicStatus === 'accepted' || /^( {2})+> /.test(line.text as string)) {
|
|
518
520
|
parents.push(previousId)
|
|
519
521
|
} else {
|
|
520
522
|
parents.push(null)
|
|
@@ -533,10 +535,19 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
533
535
|
groupIndent = lineIndent
|
|
534
536
|
}
|
|
535
537
|
|
|
538
|
+
// Do not process "indet" lines further, as they only serve to indicate
|
|
539
|
+
// that subtaxa are explicitely omitted
|
|
540
|
+
if (isIndetLine(line.text as string, lineIndent)) {
|
|
541
|
+
// If the line was previously not and ndet line, increase the id counter
|
|
542
|
+
if (line.type === ResourceDiffType.Modified && !isIndetLine(line.original as string)) {
|
|
543
|
+
id++
|
|
544
|
+
}
|
|
545
|
+
continue
|
|
546
|
+
}
|
|
547
|
+
|
|
536
548
|
const parentId = parents.reduce((grandparent, parent) => parent || grandparent, null)
|
|
537
549
|
const parent = parentId === null ? {} as WorkingTaxon : data[parentId]
|
|
538
|
-
|
|
539
|
-
const name = line.slice(groupIndent)
|
|
550
|
+
const name = (line.text as string).slice(groupIndent)
|
|
540
551
|
const rank = resource.metadata.levels[groupIndent / 2]
|
|
541
552
|
const item = parseName(name, rank, parent)
|
|
542
553
|
const isSynonym = item.taxonomicStatus !== 'accepted'
|
|
@@ -581,7 +592,10 @@ function parseResourceContent (content: ResourceDiff, resource: Resource, oldIds
|
|
|
581
592
|
}
|
|
582
593
|
|
|
583
594
|
// Set identifiers
|
|
584
|
-
if (type === ResourceDiffType.Added) {
|
|
595
|
+
if (line.type === ResourceDiffType.Added) {
|
|
596
|
+
newIdOffset++
|
|
597
|
+
item.scientificNameID = idBase + newIdOffset.toString()
|
|
598
|
+
} else if (line.type === ResourceDiffType.Modified && isIndetLine(line.original as string)) {
|
|
585
599
|
newIdOffset++
|
|
586
600
|
item.scientificNameID = idBase + newIdOffset.toString()
|
|
587
601
|
} else {
|
package/test/resources.js
CHANGED
|
@@ -26,9 +26,9 @@ levels: [family, genus, species]
|
|
|
26
26
|
|
|
27
27
|
Dolichurus haemorrhous A. Costa, 1886
|
|
28
28
|
Dolichurus A. Costa, 1886
|
|
29
|
-
|
|
29
|
+
[indet]
|
|
30
30
|
Sphecidae A. Costa, 1886
|
|
31
|
-
|
|
31
|
+
[indet]
|
|
32
32
|
`, 'T1')
|
|
33
33
|
assert.deepStrictEqual(Object.values(resource.taxa).map(taxon => taxon.scientificNameAuthorship), Array(3).fill('A. Costa, 1886'))
|
|
34
34
|
})
|
|
@@ -86,7 +86,7 @@ levels: [genus, species]
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
88
|
Drymus
|
|
89
|
-
|
|
89
|
+
[indet]
|
|
90
90
|
`, 'T1')
|
|
91
91
|
assert.strictEqual(resource.taxa['T1:1:1'].scientificName, 'Drymus')
|
|
92
92
|
})
|
|
@@ -156,7 +156,7 @@ levels: [genus, species]
|
|
|
156
156
|
---
|
|
157
157
|
|
|
158
158
|
x Triticosecale
|
|
159
|
-
|
|
159
|
+
[indet]
|
|
160
160
|
x Festulpia
|
|
161
161
|
Festuca_rubra x Vulpia_bromoides
|
|
162
162
|
`, 'T1')
|