glossarist 0.3.3 → 0.3.5

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "glossarist",
3
- "version": "0.3.3",
4
- "description": "JavaScript SDK for Glossarist GCR packages \u2014 read, write, validate, and manage terminology concepts",
3
+ "version": "0.3.5",
4
+ "description": "JavaScript SDK for Glossarist GCR packages read, write, validate, and manage terminology concepts",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "types": "src/index.d.ts",
@@ -5,7 +5,6 @@ const _items = Symbol('items');
5
5
  export class ConceptCollection {
6
6
  constructor(concepts = []) {
7
7
  this[_items] = Array.from(concepts);
8
- return new Proxy(this, _handler);
9
8
  }
10
9
 
11
10
  get length() { return this[_items].length; }
@@ -23,10 +22,35 @@ export class ConceptCollection {
23
22
  splice(...args) { return this[_items].splice(...args); }
24
23
  set(index, item) { this[_items][index] = item; }
25
24
 
25
+ toArray() { return [...this[_items]]; }
26
+
26
27
  byId(id) {
27
28
  return this[_items].find(c => c.id === id || c.termid === id);
28
29
  }
29
30
 
31
+ /**
32
+ * Find a concept by id and version.
33
+ *
34
+ * The version is matched against a top-level `version` field on
35
+ * the concept instance (e.g. `concept.version`). The deployment
36
+ * is responsible for setting this field on bibliographic records;
37
+ * the data model does not enforce its presence or type.
38
+ *
39
+ * If `version` is null, the method falls back to `byId(id)`. This
40
+ * supports the "version-agnostic" lookup for datasets where
41
+ * version is not tracked.
42
+ *
43
+ * @param {string} id
44
+ * @param {string | null} version
45
+ * @returns {Concept | null}
46
+ */
47
+ byIdAnd(id, version) {
48
+ if (version == null) return this.byId(id);
49
+ return this[_items].find(c =>
50
+ (c.id === id || c.termid === id) && c.version === version
51
+ ) ?? null;
52
+ }
53
+
30
54
  byPrefix(prefix) {
31
55
  return new ConceptCollection(this[_items].filter(c => c.id.startsWith(prefix)));
32
56
  }
@@ -62,14 +86,22 @@ export class ConceptCollection {
62
86
  for (const t of lc.terms) {
63
87
  if ((t.designation ?? '').toLowerCase().includes(q)) return true;
64
88
  }
65
- for (const d of lc.definitions) {
66
- if ((d.content ?? '').toLowerCase().includes(q)) return true;
67
- }
89
+ if (this._searchDefs(lc.definitions, q)) return true;
90
+ if (this._searchDefs(lc.notes, q)) return true;
91
+ if (this._searchDefs(lc.examples, q)) return true;
92
+ if (this._searchDefs(lc.annotations, q)) return true;
68
93
  }
69
94
  return false;
70
95
  }));
71
96
  }
72
97
 
98
+ _searchDefs(arr, q) {
99
+ for (const d of arr) {
100
+ if ((d.content ?? '').toLowerCase().includes(q)) return true;
101
+ }
102
+ return false;
103
+ }
104
+
73
105
  allLanguages() {
74
106
  const set = new Set();
75
107
  for (const c of this[_items]) {
@@ -82,27 +114,3 @@ export class ConceptCollection {
82
114
  slice(...args) { return new ConceptCollection(this[_items].slice(...args)); }
83
115
  concat(...args) { return new ConceptCollection(this[_items].concat(...args)); }
84
116
  }
85
-
86
- const _handler = {
87
- get(target, prop, receiver) {
88
- if (typeof prop === 'string' && /^\d+$/.test(prop)) {
89
- return target[_items][Number(prop)];
90
- }
91
- if (prop === 'length') return target[_items].length;
92
- const value = Reflect.get(target, prop, receiver);
93
- return typeof value === 'function' ? value.bind(target) : value;
94
- },
95
- set(target, prop, value) {
96
- if (typeof prop === 'string' && /^\d+$/.test(prop)) {
97
- target[_items][Number(prop)] = value;
98
- return true;
99
- }
100
- return Reflect.set(target, prop, value);
101
- },
102
- has(target, prop) {
103
- if (typeof prop === 'string' && /^\d+$/.test(prop)) {
104
- return Number(prop) in target[_items];
105
- }
106
- return Reflect.has(target, prop);
107
- },
108
- };
@@ -1,6 +1,5 @@
1
1
  import yaml from 'js-yaml';
2
2
  import { Concept } from './models/concept.js';
3
- import { ConceptRef } from './models/concept-ref.js';
4
3
  import { RelatedConcept } from './models/related-concept.js';
5
4
  import { InvalidInputError, YamlParseError } from './errors.js';
6
5
 
@@ -88,17 +87,13 @@ function _normalizeRelated(arr) {
88
87
  if (!arr || !Array.isArray(arr)) return [];
89
88
  return arr.map(r => {
90
89
  if (r instanceof RelatedConcept) return r;
91
- const data = { ...r };
92
- if (data.ref) {
93
- if (typeof data.ref !== 'object' || data.ref === null) {
94
- throw new InvalidInputError(
95
- `RelatedConcept.ref must be an object { source, id }, got: ${typeof data.ref}`,
96
- 'object',
97
- );
98
- }
99
- data.ref = new ConceptRef(data.ref);
90
+ if (r.ref != null && typeof r.ref !== 'object') {
91
+ throw new InvalidInputError(
92
+ `RelatedConcept.ref must be an object { source, id }, got: ${typeof r.ref}`,
93
+ 'object',
94
+ );
100
95
  }
101
- return new RelatedConcept(data);
96
+ return new RelatedConcept(r);
102
97
  });
103
98
  }
104
99
 
package/src/gcr-reader.js CHANGED
@@ -367,12 +367,42 @@ export class GcrPackage {
367
367
  return map;
368
368
  }
369
369
 
370
- /** @private @param {string} filePath @returns {Promise<string | null>} */
371
- async _readText(filePath) {
370
+ /**
371
+ * Read a text file from the package.
372
+ * @param {string} filePath
373
+ * @returns {Promise<string | null>} null if the file doesn't exist
374
+ */
375
+ async readText(filePath) {
372
376
  const entry = this._zip.file(filePath);
373
377
  if (!entry) return null;
374
378
  return entry.async('text');
375
379
  }
380
+
381
+ /**
382
+ * List all entry paths in the package.
383
+ * @returns {Array<{ path: string, dir: boolean }>}
384
+ */
385
+ entryPaths() {
386
+ const entries = [];
387
+ this._zip.forEach((relativePath, entry) => {
388
+ entries.push({ path: relativePath, dir: !!entry.dir });
389
+ });
390
+ return entries;
391
+ }
392
+
393
+ /**
394
+ * Check whether a specific entry exists in the package.
395
+ * @param {string} filePath
396
+ * @returns {boolean}
397
+ */
398
+ hasEntry(filePath) {
399
+ return this._zip.file(filePath) != null;
400
+ }
401
+
402
+ /** @private @deprecated Use readText instead */
403
+ async _readText(filePath) {
404
+ return this.readText(filePath);
405
+ }
376
406
  }
377
407
 
378
408
  // --- Concept YAML parsing ---
package/src/index.d.ts CHANGED
@@ -34,6 +34,16 @@ export { conceptUuid, localizedConceptUuid, uuidV5 } from './uuid';
34
34
  // Reference resolution
35
35
  export { ReferenceResolver, Reference, referenceResolver } from './reference-resolver';
36
36
 
37
+ export type MentionParseResult = {
38
+ kind: 'cite-ref' | 'numeric' | 'unresolved';
39
+ key?: string;
40
+ label?: string | null;
41
+ id?: string;
42
+ raw: string;
43
+ };
44
+
45
+ export function parseMention(raw: string): MentionParseResult;
46
+
37
47
  // V1 support
38
48
  export { V1Reader, migrateV1ToV2 } from './v1-reader';
39
49
 
package/src/index.js CHANGED
@@ -8,6 +8,8 @@ export { ManagedConceptCollection } from './managed-concept-collection.js';
8
8
  export { validateConcept, validateRegister, validateGcrPackage, createConceptValidator, ValidationError, ValidationRule, ValidationResult, RegisterValidator, GcrValidator } from './validators/index.js';
9
9
  export { conceptUuid, localizedConceptUuid, uuidV5 } from './uuid.js';
10
10
  export { ReferenceResolver, Reference, referenceResolver } from './reference-resolver.js';
11
+ export { parseMention } from './reference-mention.js';
12
+ export { ReferenceClassifier } from './render-classification.js';
11
13
  export { V1Reader, migrateV1ToV2 } from './v1-reader.js';
12
14
  export { GlossaristError, InvalidInputError, YamlParseError } from './errors.js';
13
15
 
@@ -32,8 +34,8 @@ export {
32
34
  REGISTER_STATUSES, ORDERING_METHODS,
33
35
  Concept, LocalizedConcept,
34
36
  Designation, Expression, Abbreviation, Symbol, GraphicalSymbol,
35
- Citation, ConceptRef, ConceptSource, RelatedConcept, ConceptReference, ConceptDate,
37
+ Citation, ConceptRef, ConceptSource, RelatedConcept, DesignationRelationship, ConceptReference, ConceptDate,
36
38
  DetailedDefinition, NonVerbRep,
37
39
  GcrMetadata, GcrStatistics,
38
- RELATIONSHIP_TYPES, DATE_TYPES,
40
+ RELATIONSHIP_TYPES, DESIGNATION_RELATIONSHIP_TYPES, DATE_TYPES,
39
41
  } from './models/index.js';
@@ -5,17 +5,24 @@ export class ConceptRef extends GlossaristModel {
5
5
  super();
6
6
  this.source = data.source ?? null;
7
7
  this.id = data.id ?? null;
8
+ this.text = data.text ?? null;
8
9
  }
9
10
 
10
11
  toString() {
11
- if (this.source && this.id) return `${this.source} ${this.id}`;
12
- return this.source ?? this.id ?? '';
12
+ const parts = [];
13
+ if (this.source) parts.push(this.source);
14
+ if (this.id) parts.push(this.id);
15
+ const base = parts.join(' ');
16
+ if (this.text && base) return `${base} (${this.text})`;
17
+ if (this.text) return this.text;
18
+ return base;
13
19
  }
14
20
 
15
21
  toJSON() {
16
22
  const obj = {};
17
23
  if (this.source != null) obj.source = this.source;
18
24
  if (this.id != null) obj.id = this.id;
25
+ if (this.text != null) obj.text = this.text;
19
26
  return obj;
20
27
  }
21
28
 
@@ -4,6 +4,7 @@ import { Citation } from './citation.js';
4
4
  export class ConceptSource extends GlossaristModel {
5
5
  constructor(data = {}) {
6
6
  super();
7
+ this.id = data.id ?? null;
7
8
  this.status = data.status ?? null;
8
9
  this.type = data.type ?? null;
9
10
  this.origin = data.origin
@@ -14,6 +15,7 @@ export class ConceptSource extends GlossaristModel {
14
15
 
15
16
  toJSON() {
16
17
  const obj = {};
18
+ if (this.id != null) obj.id = this.id;
17
19
  if (this.status != null) obj.status = this.status;
18
20
  if (this.type != null) obj.type = this.type;
19
21
  if (this.origin != null) obj.origin = this.origin.toJSON();
@@ -30,6 +30,11 @@ export class Concept extends GlossaristModel {
30
30
  return Object.keys(this._rawLocalizations);
31
31
  }
32
32
 
33
+ get languageCodes() {
34
+ return this.languages;
35
+ }
36
+
37
+ /** @deprecated Use localization(lang) for model access, or toJSON().localizations for raw data */
33
38
  get localizations() {
34
39
  return this._rawLocalizations;
35
40
  }
@@ -68,6 +73,45 @@ export class Concept extends GlossaristModel {
68
73
  return lang in this._rawLocalizations;
69
74
  }
70
75
 
76
+ /**
77
+ * Find a source by its local id within this concept.
78
+ *
79
+ * The lookup walks concept-level, localization-level, and
80
+ * designation-level sources in that order, returning the first
81
+ * source whose `id` matches. Sources without an `id` are
82
+ * skipped. The id is local to the concept; uniqueness is
83
+ * enforced by the validator (see CiteRefIntegrityRule).
84
+ *
85
+ * @param {string} id
86
+ * @returns {ConceptSource | null}
87
+ */
88
+ findSourceById(id) {
89
+ if (typeof id !== 'string' || id.length === 0) return null;
90
+
91
+ // 1. Concept-level sources.
92
+ for (const source of this.sources) {
93
+ if (source.id === id) return source;
94
+ }
95
+
96
+ // 2. Localization-level sources.
97
+ for (const lang of this.languages) {
98
+ const lc = this.localization(lang);
99
+ if (!lc) continue;
100
+ for (const source of lc.sources) {
101
+ if (source.id === id) return source;
102
+ }
103
+
104
+ // 3. Designation-level sources.
105
+ for (const designation of lc.terms) {
106
+ for (const source of designation.sources) {
107
+ if (source.id === id) return source;
108
+ }
109
+ }
110
+ }
111
+
112
+ return null;
113
+ }
114
+
71
115
  toJSON() {
72
116
  const obj = { id: this.id };
73
117
  if (this.term != null) obj.term = this.term;
@@ -0,0 +1,27 @@
1
+ import { GlossaristModel } from './base.js';
2
+
3
+ export const DESIGNATION_RELATIONSHIP_TYPES = Object.freeze([
4
+ // TBX (ISO 30042) / ISO 12620 term-level relationships
5
+ 'abbreviated_form_for', 'short_form_for',
6
+ ]);
7
+
8
+ export class DesignationRelationship extends GlossaristModel {
9
+ constructor(data = {}) {
10
+ super();
11
+ this.type = data.type ?? null;
12
+ this.content = data.content ?? null;
13
+ this.target = data.target ?? null;
14
+ }
15
+
16
+ toJSON() {
17
+ const obj = {};
18
+ if (this.type != null) obj.type = this.type;
19
+ if (this.content != null) obj.content = this.content;
20
+ if (this.target != null) obj.target = this.target;
21
+ return obj;
22
+ }
23
+
24
+ static fromJSON(data) {
25
+ return new DesignationRelationship(data);
26
+ }
27
+ }
@@ -3,6 +3,7 @@ import { ConceptSource } from './concept-source.js';
3
3
  import { Pronunciation } from './pronunciation.js';
4
4
  import { GrammarInfo } from './grammar-info.js';
5
5
  import { RelatedConcept } from './related-concept.js';
6
+ import { DesignationRelationship, DESIGNATION_RELATIONSHIP_TYPES } from './designation-relationship.js';
6
7
 
7
8
  export class Designation extends GlossaristModel {
8
9
  static _registry = new Map();
@@ -38,7 +39,11 @@ export class Designation extends GlossaristModel {
38
39
  s => s instanceof ConceptSource ? s : new ConceptSource(s)
39
40
  );
40
41
  this.related = (data.related ?? []).map(
41
- r => r instanceof RelatedConcept ? r : new RelatedConcept(r)
42
+ r => (r instanceof DesignationRelationship || r instanceof RelatedConcept)
43
+ ? r
44
+ : DESIGNATION_RELATIONSHIP_TYPES.includes(r.type)
45
+ ? DesignationRelationship.fromJSON(r)
46
+ : RelatedConcept.fromJSON(r)
42
47
  );
43
48
  }
44
49
 
@@ -88,6 +88,7 @@ export class LocalizedConcept extends GlossaristModel {
88
88
  readonly definitions: DetailedDefinition[];
89
89
  readonly definition: DetailedDefinition[];
90
90
  readonly notes: DetailedDefinition[];
91
+ readonly annotations: DetailedDefinition[];
91
92
  readonly examples: DetailedDefinition[];
92
93
  readonly sources: ConceptSource[];
93
94
  readonly dates: ConceptDate[];
@@ -113,7 +114,7 @@ export class Designation extends GlossaristModel {
113
114
  readonly termType: string | null;
114
115
  readonly pronunciations: Pronunciation[];
115
116
  readonly sources: ConceptSource[];
116
- readonly related: RelatedConcept[];
117
+ readonly related: (RelatedConcept | DesignationRelationship)[];
117
118
  static register(type: string, cls: typeof Designation): void;
118
119
  static fromData(data: Record<string, unknown>): Designation;
119
120
  static fromJSON(data: Record<string, unknown>): Designation;
@@ -194,6 +195,7 @@ export namespace Citation {
194
195
  export class ConceptRef extends GlossaristModel {
195
196
  readonly source: string | null;
196
197
  readonly id: string | null;
198
+ readonly text: string | null;
197
199
  toString(): string;
198
200
  static fromJSON(data: Record<string, unknown>): ConceptRef;
199
201
  }
@@ -212,6 +214,14 @@ export class RelatedConcept extends GlossaristModel {
212
214
  readonly ref: ConceptRef | null;
213
215
  }
214
216
 
217
+ export const DESIGNATION_RELATIONSHIP_TYPES: readonly string[];
218
+ export class DesignationRelationship extends GlossaristModel {
219
+ readonly type: string | null;
220
+ readonly content: string | null;
221
+ readonly target: string | null;
222
+ static fromJSON(data: Record<string, unknown>): DesignationRelationship;
223
+ }
224
+
215
225
  export class ConceptReference extends GlossaristModel {
216
226
  readonly conceptId: string | null;
217
227
  readonly refType: string | null;
@@ -8,6 +8,7 @@ export { Citation } from './citation.js';
8
8
  export { ConceptRef } from './concept-ref.js';
9
9
  export { ConceptSource } from './concept-source.js';
10
10
  export { RelatedConcept, RELATIONSHIP_TYPES } from './related-concept.js';
11
+ export { DesignationRelationship, DESIGNATION_RELATIONSHIP_TYPES } from './designation-relationship.js';
11
12
  export { ConceptReference } from './concept-reference.js';
12
13
  export { ConceptDate, DATE_TYPES } from './concept-date.js';
13
14
  export { DetailedDefinition } from './detailed-definition.js';
@@ -6,6 +6,10 @@ import { ConceptDate } from './concept-date.js';
6
6
  import { NonVerbRep } from './non-verb-rep.js';
7
7
  import { RelatedConcept } from './related-concept.js';
8
8
 
9
+ function wrapAs(Cls) {
10
+ return item => item instanceof Cls ? item : new Cls(item);
11
+ }
12
+
9
13
  export class LocalizedConcept extends GlossaristModel {
10
14
  constructor(data = {}) {
11
15
  super();
@@ -30,6 +34,7 @@ export class LocalizedConcept extends GlossaristModel {
30
34
  this._rawDefinition = data.definition ?? [];
31
35
  this._rawSources = data.sources ?? [];
32
36
  this._rawNotes = data.notes ?? [];
37
+ this._rawAnnotations = data.annotations ?? [];
33
38
  this._rawExamples = data.examples ?? [];
34
39
  this._rawDates = data.dates ?? [];
35
40
  this._rawNonVerbal = data.non_verbal_rep ?? data.non_verb ?? [];
@@ -39,6 +44,7 @@ export class LocalizedConcept extends GlossaristModel {
39
44
  this._definitions = null;
40
45
  this._sources = null;
41
46
  this._notes = null;
47
+ this._annotations = null;
42
48
  this._examples = null;
43
49
  this._dates = null;
44
50
  this._nonVerbal = null;
@@ -46,19 +52,11 @@ export class LocalizedConcept extends GlossaristModel {
46
52
  }
47
53
 
48
54
  get terms() {
49
- if (this._terms === null) {
50
- this._terms = this._rawTerms.map(t => Designation.fromData(t));
51
- }
52
- return this._terms;
55
+ return this._lazy('_terms', '_rawTerms', t => Designation.fromData(t));
53
56
  }
54
57
 
55
58
  get definitions() {
56
- if (this._definitions === null) {
57
- this._definitions = this._rawDefinition.map(
58
- d => d instanceof DetailedDefinition ? d : new DetailedDefinition(d)
59
- );
60
- }
61
- return this._definitions;
59
+ return this._lazy('_definitions', '_rawDefinition', wrapAs(DetailedDefinition));
62
60
  }
63
61
 
64
62
  get definition() {
@@ -66,57 +64,31 @@ export class LocalizedConcept extends GlossaristModel {
66
64
  }
67
65
 
68
66
  get sources() {
69
- if (this._sources === null) {
70
- this._sources = this._rawSources.map(
71
- s => s instanceof ConceptSource ? s : new ConceptSource(s)
72
- );
73
- }
74
- return this._sources;
67
+ return this._lazy('_sources', '_rawSources', wrapAs(ConceptSource));
75
68
  }
76
69
 
77
70
  get notes() {
78
- if (this._notes === null) {
79
- this._notes = this._rawNotes.map(
80
- n => n instanceof DetailedDefinition ? n : new DetailedDefinition(n)
81
- );
82
- }
83
- return this._notes;
71
+ return this._lazy('_notes', '_rawNotes', wrapAs(DetailedDefinition));
72
+ }
73
+
74
+ get annotations() {
75
+ return this._lazy('_annotations', '_rawAnnotations', wrapAs(DetailedDefinition));
84
76
  }
85
77
 
86
78
  get examples() {
87
- if (this._examples === null) {
88
- this._examples = this._rawExamples.map(
89
- e => e instanceof DetailedDefinition ? e : new DetailedDefinition(e)
90
- );
91
- }
92
- return this._examples;
79
+ return this._lazy('_examples', '_rawExamples', wrapAs(DetailedDefinition));
93
80
  }
94
81
 
95
82
  get dates() {
96
- if (this._dates === null) {
97
- this._dates = this._rawDates.map(
98
- d => d instanceof ConceptDate ? d : new ConceptDate(d)
99
- );
100
- }
101
- return this._dates;
83
+ return this._lazy('_dates', '_rawDates', wrapAs(ConceptDate));
102
84
  }
103
85
 
104
86
  get nonVerbalRep() {
105
- if (this._nonVerbal === null) {
106
- this._nonVerbal = this._rawNonVerbal.map(
107
- n => n instanceof NonVerbRep ? n : new NonVerbRep(n)
108
- );
109
- }
110
- return this._nonVerbal;
87
+ return this._lazy('_nonVerbal', '_rawNonVerbal', wrapAs(NonVerbRep));
111
88
  }
112
89
 
113
90
  get related() {
114
- if (this._related === null) {
115
- this._related = this._rawRelated.map(
116
- r => r instanceof RelatedConcept ? r : new RelatedConcept(r)
117
- );
118
- }
119
- return this._related;
91
+ return this._lazy('_related', '_rawRelated', wrapAs(RelatedConcept));
120
92
  }
121
93
 
122
94
  get primaryDesignation() {
@@ -146,47 +118,31 @@ export class LocalizedConcept extends GlossaristModel {
146
118
  if (this.reviewDecision) obj.review_decision = this.reviewDecision;
147
119
  if (this.reviewDecisionNotes) obj.review_decision_notes = this.reviewDecisionNotes;
148
120
 
149
- const terms = this._terms ?? this._rawTerms;
150
- if (terms.length > 0) {
151
- obj.terms = terms.map(t => (typeof t.toJSON === 'function') ? t.toJSON() : t);
152
- }
121
+ this._serialize(obj, 'terms', '_terms', '_rawTerms');
122
+ this._serialize(obj, 'definition', '_definitions', '_rawDefinition');
123
+ this._serialize(obj, 'notes', '_notes', '_rawNotes');
124
+ this._serialize(obj, 'annotations', '_annotations', '_rawAnnotations');
125
+ this._serialize(obj, 'examples', '_examples', '_rawExamples');
126
+ this._serialize(obj, 'sources', '_sources', '_rawSources');
127
+ this._serialize(obj, 'dates', '_dates', '_rawDates');
128
+ this._serialize(obj, 'non_verbal_rep', '_nonVerbal', '_rawNonVerbal');
129
+ this._serialize(obj, 'related', '_related', '_rawRelated');
153
130
 
154
- const defs = this._definitions ?? (this._rawDefinition.length > 0 ? this._rawDefinition : []);
155
- if (defs.length > 0) {
156
- obj.definition = defs.map(d => (typeof d.toJSON === 'function') ? d.toJSON() : d);
157
- }
158
-
159
- const notes = this._notes ?? (this._rawNotes.length > 0 ? this._rawNotes : []);
160
- if (notes.length > 0) {
161
- obj.notes = notes.map(n => (typeof n.toJSON === 'function') ? n.toJSON() : n);
162
- }
163
-
164
- const examples = this._examples ?? (this._rawExamples.length > 0 ? this._rawExamples : []);
165
- if (examples.length > 0) {
166
- obj.examples = examples.map(e => (typeof e.toJSON === 'function') ? e.toJSON() : e);
167
- }
168
-
169
- const sources = this._sources ?? (this._rawSources.length > 0 ? this._rawSources : []);
170
- if (sources.length > 0) {
171
- obj.sources = sources.map(s => (typeof s.toJSON === 'function') ? s.toJSON() : s);
172
- }
173
-
174
- const dates = this._dates ?? (this._rawDates.length > 0 ? this._rawDates : []);
175
- if (dates.length > 0) {
176
- obj.dates = dates.map(d => (typeof d.toJSON === 'function') ? d.toJSON() : d);
177
- }
131
+ return obj;
132
+ }
178
133
 
179
- const nonVerbal = this._nonVerbal ?? (this._rawNonVerbal.length > 0 ? this._rawNonVerbal : []);
180
- if (nonVerbal.length > 0) {
181
- obj.non_verbal_rep = nonVerbal.map(n => (typeof n.toJSON === 'function') ? n.toJSON() : n);
134
+ _lazy(cacheKey, rawKey, wrapFn) {
135
+ if (this[cacheKey] === null) {
136
+ this[cacheKey] = this[rawKey].map(wrapFn);
182
137
  }
138
+ return this[cacheKey];
139
+ }
183
140
 
184
- const related = this._related ?? (this._rawRelated.length > 0 ? this._rawRelated : []);
185
- if (related.length > 0) {
186
- obj.related = related.map(r => (typeof r.toJSON === 'function') ? r.toJSON() : r);
141
+ _serialize(obj, jsonKey, cacheKey, rawKey) {
142
+ const items = this[cacheKey] ?? (this[rawKey].length > 0 ? this[rawKey] : []);
143
+ if (items.length > 0) {
144
+ obj[jsonKey] = items.map(i => (typeof i.toJSON === 'function') ? i.toJSON() : i);
187
145
  }
188
-
189
- return obj;
190
146
  }
191
147
 
192
148
  static fromJSON(data) {
@@ -31,8 +31,6 @@ export const RELATIONSHIP_TYPES = Object.freeze([
31
31
  'sequentially_related_concept', 'spatially_related_concept', 'temporally_related_concept',
32
32
  // Lexical (ISO 12620 / TBX)
33
33
  'homograph', 'false_friend',
34
- // Designation-level
35
- 'abbreviated_form_for', 'short_form_for',
36
34
  ]);
37
35
 
38
36
  export class RelatedConcept extends GlossaristModel {