@larsgw/formica 0.6.4 → 0.6.6

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 CHANGED
@@ -1,3 +1,23 @@
1
+ ## [0.6.6](https://github.com/identification-resources/formica/compare/v0.6.5...v0.6.6) (2025-02-16)
2
+
3
+
4
+ ### Features
5
+
6
+ * **resources:** allow other resources in version_of ([85947b0](https://github.com/identification-resources/formica/commit/85947b0dcfbdb4bfd6537c628b34087a6e6326f7))
7
+
8
+
9
+
10
+ ## [0.6.5](https://github.com/identification-resources/formica/compare/v0.6.4...v0.6.5) (2024-12-20)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **resources:** ignore empty lines in diff mode ([3fb027e](https://github.com/identification-resources/formica/commit/3fb027e8968196910fff2e574e700ac0abee996c))
16
+ * **resources:** improve (ICBN) author parsing ([d3e3faf](https://github.com/identification-resources/formica/commit/d3e3faf411d1a7dccd528a5ee78bd0823a5883b4)), closes [#13](https://github.com/identification-resources/formica/issues/13) [#14](https://github.com/identification-resources/formica/issues/14)
17
+ * **resources:** support "et al." in author name ([82993bf](https://github.com/identification-resources/formica/commit/82993bffac5e7f3ed00d00c28c836f347b264a43))
18
+
19
+
20
+
1
21
  ## [0.6.4](https://github.com/identification-resources/formica/compare/v0.6.3...v0.6.4) (2024-12-16)
2
22
 
3
23
 
@@ -454,8 +454,8 @@ var ResourceProcessor = /** @class */ (function () {
454
454
  }
455
455
  return name_2 === a.matchedName ? -1 : name_2 === b.matchedName ? 1 : 0;
456
456
  });
457
- for (var _o = 0, matches_1 = matches; _o < matches_1.length; _o++) {
458
- var match = matches_1[_o];
457
+ for (var _o = 0, _p = matches; _o < _p.length; _o++) {
458
+ var match = _p[_o];
459
459
  var source = match.dataSourceId;
460
460
  var currentRank = match.classificationRanks.split('|').pop();
461
461
  if (match.scoreDetails.cardinalityScore === 0) {
@@ -466,8 +466,8 @@ var ResourceProcessor = /** @class */ (function () {
466
466
  // GBIF species like "Nomada spec"
467
467
  continue;
468
468
  }
469
- for (var _p = 0, _q = taxonNames[name_2]; _p < _q.length; _p++) {
470
- var loirId = _q[_p];
469
+ for (var _q = 0, _r = taxonNames[name_2]; _q < _r.length; _q++) {
470
+ var loirId = _r[_q];
471
471
  var taxon = resource.taxa[loirId];
472
472
  if (source === 11 && !GBIF_RANKS.includes(taxon.taxonRank)) {
473
473
  // Exclude GBIF matches for ranks that are not in GBIF
@@ -13,7 +13,7 @@ var __assign = (this && this.__assign) || function () {
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.parseFileHeader = exports.parseFile = void 0;
15
15
  var yaml = require("js-yaml");
16
- var work_1 = require("../catalog/tables/work");
16
+ var resource_1 = require("./resource");
17
17
  var diff_resource_1 = require("./diff-resource");
18
18
  var RANKS = [
19
19
  'phylum',
@@ -93,6 +93,18 @@ var RANK_LABELS_REVERSE = {
93
93
  'ssp': 'subspecies',
94
94
  'subsp': 'subspecies'
95
95
  };
96
+ /**
97
+ * 1. Any number of
98
+ * - capitalized words
99
+ * - "&"
100
+ * - " in "
101
+ * - " ex "
102
+ * - lowercase name particles
103
+ * 2. Followed by a capitalized word
104
+ * 3. Optionally, followed by "et al."
105
+ */
106
+ var LOWERCASE_NAME_PARTICLES = ['y', 'der', 'den', 'de', 'van'].join('|');
107
+ var SIMPLE_AUTHOR_PATTERN = '(?:(?:\\p{Lu}\\S*|&|in|ex|' + LOWERCASE_NAME_PARTICLES + ')\\s*)*\\p{Lu}\\S+(?:\\s+et\\s+al\\.)?';
96
108
  var NAME_PATTERN = new RegExp('^' +
97
109
  // $1 main name part
98
110
  '(\\S+)' +
@@ -101,20 +113,14 @@ var NAME_PATTERN = new RegExp('^' +
101
113
  // but not auct(t)., etc.
102
114
  '(?!auctt?\\.|(?:syn|comb|sp|spec)\\. n(?:ov)?\\.|s(?:ens[.u]|\\.))' +
103
115
  '(' +
104
- // $2.1 anything in parentheses
105
- '\\(.+?\\)' +
116
+ // $2.1 anything in parentheses, followed by optional revising author(s)
117
+ '\\(.+?\\)(?:\\s+' + SIMPLE_AUTHOR_PATTERN + ')?' +
106
118
  '|' +
107
119
  // $2.2 anything followed by a year
108
120
  '.+?\\d{4}\\)?' +
109
121
  '|' +
110
- // $2.3 name(, name)* & name
111
- '.+(?:, .+)* & \\S+' +
112
- '|' +
113
- // $2.4 name y name
114
- '\\S+ [yY] \\S+' +
115
- '|' +
116
- // $2.5 name( in name)
117
- '\\p{Lu}\\S*(?: in \\S+)?' +
122
+ // $2.3 author(s)
123
+ SIMPLE_AUTHOR_PATTERN +
118
124
  '))?' +
119
125
  // $3 optional notes
120
126
  '(?:,? (.+))?' +
@@ -364,6 +370,9 @@ function parseHeader(header) {
364
370
  var metadata = { levels: levels };
365
371
  if ('catalog' in config && typeof config.catalog === 'object' && config.catalog !== null) {
366
372
  var catalog = {};
373
+ if ('id' in config.catalog) {
374
+ throw new SyntaxError('"catalog" should not contain id');
375
+ }
367
376
  for (var key in config.catalog) {
368
377
  var value = config.catalog[key];
369
378
  if (typeof value === 'number') {
@@ -376,7 +385,7 @@ function parseHeader(header) {
376
385
  throw new SyntaxError("\"catalog\" should contain only strings (\"".concat(key, "\")"));
377
386
  }
378
387
  }
379
- var work = new work_1.Work(catalog);
388
+ var work = new resource_1.WorkResource(catalog);
380
389
  var errors = work.validate().filter(function (_a) {
381
390
  var error = _a.error;
382
391
  return error !== 'Value(s) required but missing';
@@ -541,6 +550,8 @@ function parseFile(file, id, old) {
541
550
  var diff;
542
551
  if (oldResources[index]) {
543
552
  diff = (0, diff_resource_1.createDiff)(content, parseResource(oldResources[index])[1]);
553
+ // Ignore empty lines
554
+ diff = diff.filter(function (line) { return line.text !== ''; });
544
555
  }
545
556
  else {
546
557
  diff = (0, diff_resource_1.createDiff)(content, content);
@@ -0,0 +1,4 @@
1
+ import { Work } from '../catalog/tables/work';
2
+ export declare class WorkResource extends Work {
3
+ constructor(values: Record<string, string>);
4
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ var __extends = (this && this.__extends) || (function () {
3
+ var extendStatics = function (d, b) {
4
+ extendStatics = Object.setPrototypeOf ||
5
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
7
+ return extendStatics(d, b);
8
+ };
9
+ return function (d, b) {
10
+ if (typeof b !== "function" && b !== null)
11
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
12
+ extendStatics(d, b);
13
+ function __() { this.constructor = d; }
14
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
15
+ };
16
+ })();
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.WorkResource = void 0;
19
+ var work_1 = require("../catalog/tables/work");
20
+ var WorkResource = /** @class */ (function (_super) {
21
+ __extends(WorkResource, _super);
22
+ function WorkResource(values) {
23
+ var _this = _super.call(this, values) || this;
24
+ _this.schema.version_of.format = /^B[1-9]\d*:[1-9]\d*$/;
25
+ _this.schema.duplicate_of.format = /^B[1-9]\d*:[1-9]\d*$/;
26
+ return _this;
27
+ }
28
+ return WorkResource;
29
+ }(work_1.Work));
30
+ exports.WorkResource = WorkResource;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larsgw/formica",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "SDK and tools for data from the Library of Identification Resources",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -16,7 +16,7 @@
16
16
  "lint": "eslint src",
17
17
  "build": "tsc -d",
18
18
  "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
19
- "preversion": "npm run lint",
19
+ "preversion": "npm run lint && npm test",
20
20
  "version": "npm run changelog && git add CHANGELOG.md",
21
21
  "prepublishOnly": "npm run build"
22
22
  },
@@ -287,6 +287,22 @@ class ResourceProcessor {
287
287
 
288
288
  const result = await runGnverifier(Array.from(names).join('\n'))
289
289
  for (const results of result.trim().split('\n')) {
290
+ interface MatchScoreDetails {
291
+ cardinalityScore: number;
292
+ }
293
+
294
+ interface Match {
295
+ currentRecordId: string;
296
+ dataSourceId: number;
297
+ matchedName: string;
298
+ recordId: string;
299
+ sortScore: number;
300
+ isSynonym: boolean;
301
+ classificationPath: string;
302
+ classificationRanks: string;
303
+ scoreDetails: MatchScoreDetails;
304
+ }
305
+
290
306
  const { name, results: matches } = JSON.parse(results)
291
307
 
292
308
  if (!matches) {
@@ -294,7 +310,7 @@ class ResourceProcessor {
294
310
  }
295
311
 
296
312
  // Fix author scoring for some species, see https://github.com/gnames/gnverifier/issues/129
297
- matches.sort((a: Record<string, any>, b: Record<string, any>) => {
313
+ matches.sort((a: Match, b: Match) => {
298
314
  if (a.sortScore !== b.sortScore) {
299
315
  return b.sortScore - a.sortScore
300
316
  }
@@ -302,7 +318,7 @@ class ResourceProcessor {
302
318
  return name === a.matchedName ? -1 : name === b.matchedName ? 1 : 0
303
319
  })
304
320
 
305
- for (const match of matches) {
321
+ for (const match of matches as Match[]) {
306
322
  const source = match.dataSourceId
307
323
  const currentRank = match.classificationRanks.split('|').pop()
308
324
 
@@ -1,5 +1,5 @@
1
1
  import * as yaml from 'js-yaml'
2
- import { Work } from '../catalog/tables/work'
2
+ import { WorkResource } from './resource'
3
3
  import { createDiff, ResourceDiffType } from './diff-resource'
4
4
 
5
5
  const RANKS: Rank[] = [
@@ -87,6 +87,19 @@ const RANK_LABELS_REVERSE: Record<string, Rank> = {
87
87
  'subsp': 'subspecies'
88
88
  }
89
89
 
90
+ /**
91
+ * 1. Any number of
92
+ * - capitalized words
93
+ * - "&"
94
+ * - " in "
95
+ * - " ex "
96
+ * - lowercase name particles
97
+ * 2. Followed by a capitalized word
98
+ * 3. Optionally, followed by "et al."
99
+ */
100
+ const LOWERCASE_NAME_PARTICLES = ['y', 'der', 'den', 'de', 'van'].join('|')
101
+ const SIMPLE_AUTHOR_PATTERN = '(?:(?:\\p{Lu}\\S*|&|in|ex|' + LOWERCASE_NAME_PARTICLES + ')\\s*)*\\p{Lu}\\S+(?:\\s+et\\s+al\\.)?'
102
+
90
103
  const NAME_PATTERN = new RegExp(
91
104
  '^' +
92
105
  // $1 main name part
@@ -96,20 +109,14 @@ const NAME_PATTERN = new RegExp(
96
109
  // but not auct(t)., etc.
97
110
  '(?!auctt?\\.|(?:syn|comb|sp|spec)\\. n(?:ov)?\\.|s(?:ens[.u]|\\.))' +
98
111
  '(' +
99
- // $2.1 anything in parentheses
100
- '\\(.+?\\)' +
112
+ // $2.1 anything in parentheses, followed by optional revising author(s)
113
+ '\\(.+?\\)(?:\\s+' + SIMPLE_AUTHOR_PATTERN + ')?' +
101
114
  '|' +
102
115
  // $2.2 anything followed by a year
103
116
  '.+?\\d{4}\\)?' +
104
117
  '|' +
105
- // $2.3 name(, name)* & name
106
- '.+(?:, .+)* & \\S+' +
107
- '|' +
108
- // $2.4 name y name
109
- '\\S+ [yY] \\S+' +
110
- '|' +
111
- // $2.5 name( in name)
112
- '\\p{Lu}\\S*(?: in \\S+)?' +
118
+ // $2.3 author(s)
119
+ SIMPLE_AUTHOR_PATTERN +
113
120
  '))?' +
114
121
  // $3 optional notes
115
122
  '(?:,? (.+))?' +
@@ -387,6 +394,9 @@ function parseHeader (header: string): ResourceMetadata {
387
394
 
388
395
  if ('catalog' in config && typeof config.catalog === 'object' && config.catalog !== null) {
389
396
  const catalog: Record<string, string> = {}
397
+ if ('id' in config.catalog) {
398
+ throw new SyntaxError('"catalog" should not contain id')
399
+ }
390
400
  for (const key in config.catalog) {
391
401
  const value = config.catalog[key as keyof object]
392
402
  if (typeof value === 'number') {
@@ -397,7 +407,7 @@ function parseHeader (header: string): ResourceMetadata {
397
407
  throw new SyntaxError(`"catalog" should contain only strings ("${key}")`)
398
408
  }
399
409
  }
400
- const work = new Work(catalog)
410
+ const work = new WorkResource(catalog)
401
411
  const errors = work.validate().filter(({ error }) => error !== 'Value(s) required but missing')
402
412
  if (errors.length > 0) {
403
413
  throw new SyntaxError(`"catalog" contains errors: ${errors.map(({ field, error }) => `[${field}] ${error}`).join('; ')}`)
@@ -573,6 +583,8 @@ export function parseFile (file: string, id: WorkId, old?: ResourceHistory): Res
573
583
  let diff
574
584
  if (oldResources[index]) {
575
585
  diff = createDiff(content, parseResource(oldResources[index])[1])
586
+ // Ignore empty lines
587
+ diff = diff.filter(line => line.text !== '')
576
588
  } else {
577
589
  diff = createDiff(content, content)
578
590
  }
@@ -0,0 +1,10 @@
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
+ }