glossarist 0.1.1 → 0.1.3

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.1.1",
4
- "description": "JavaScript library for reading Glossarist GCR packages and v2 concept data",
3
+ "version": "0.1.3",
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",
@@ -24,14 +24,24 @@
24
24
  "types": "./src/concept-reader.d.ts",
25
25
  "import": "./src/concept-reader.js",
26
26
  "default": "./src/concept-reader.js"
27
+ },
28
+ "./models": {
29
+ "types": "./src/models/index.d.ts",
30
+ "import": "./src/models/index.js",
31
+ "default": "./src/models/index.js"
32
+ },
33
+ "./validators": {
34
+ "types": "./src/validators/index.d.ts",
35
+ "import": "./src/validators/index.js",
36
+ "default": "./src/validators/index.js"
27
37
  }
28
38
  },
29
39
  "scripts": {
30
40
  "lint": "eslint src/ test/",
31
41
  "pretest": "node test/fixtures/build-fixtures.js",
32
- "test": "node --test test/*.test.js",
33
- "test:verbose": "node --test --test-reporter spec test/*.test.js",
34
- "test:coverage": "node --test --experimental-test-coverage test/*.test.js",
42
+ "test": "find test -name '*.test.js' | sort | xargs node --test",
43
+ "test:verbose": "find test -name '*.test.js' | sort | xargs node --test --test-reporter spec",
44
+ "test:coverage": "find test -name '*.test.js' | sort | xargs node --test --experimental-test-coverage",
35
45
  "prepublishOnly": "npm test"
36
46
  },
37
47
  "keywords": [
@@ -0,0 +1,109 @@
1
+ import { naturalSort } from './gcr-reader.js';
2
+
3
+ const _items = Symbol('items');
4
+
5
+ export class ConceptCollection {
6
+ constructor(concepts = []) {
7
+ this[_items] = Array.from(concepts);
8
+ return new Proxy(this, _handler);
9
+ }
10
+
11
+ get length() { return this[_items].length; }
12
+ [Symbol.iterator]() { return this[_items][Symbol.iterator](); }
13
+
14
+ at(index) { return this[_items].at(index); }
15
+ indexOf(item) { return this[_items].indexOf(item); }
16
+ find(fn) { return this[_items].find(fn); }
17
+ findIndex(fn) { return this[_items].findIndex(fn); }
18
+ forEach(fn) { this[_items].forEach(fn); }
19
+ map(fn) { return this[_items].map(fn); }
20
+ reduce(fn, init) { return this[_items].reduce(fn, init); }
21
+ includes(item) { return this[_items].includes(item); }
22
+ push(...items) { return this[_items].push(...items); }
23
+ splice(...args) { return this[_items].splice(...args); }
24
+ set(index, item) { this[_items][index] = item; }
25
+
26
+ byId(id) {
27
+ return this[_items].find(c => c.id === id || c.termid === id);
28
+ }
29
+
30
+ byPrefix(prefix) {
31
+ return new ConceptCollection(this[_items].filter(c => c.id.startsWith(prefix)));
32
+ }
33
+
34
+ byLanguage(lang) {
35
+ return new ConceptCollection(this[_items].filter(c => c.hasLocalization(lang)));
36
+ }
37
+
38
+ byStatus(status) {
39
+ return new ConceptCollection(this[_items].filter(c => {
40
+ const langs = c.languages;
41
+ return langs.length > 0 && c.localization(langs[0])?.entryStatus === status;
42
+ }));
43
+ }
44
+
45
+ index() {
46
+ const map = new Map();
47
+ for (const c of this[_items]) map.set(c.id, c);
48
+ return map;
49
+ }
50
+
51
+ sorted() {
52
+ const copy = [...this[_items]];
53
+ copy.sort((a, b) => naturalSort(a.id, b.id));
54
+ return new ConceptCollection(copy);
55
+ }
56
+
57
+ search(query) {
58
+ const q = query.toLowerCase();
59
+ return new ConceptCollection(this[_items].filter(c => {
60
+ for (const lang of c.languages) {
61
+ const lc = c.localization(lang);
62
+ if (!lc) continue;
63
+ for (const t of lc.terms) {
64
+ if ((t.designation ?? '').toLowerCase().includes(q)) return true;
65
+ }
66
+ for (const d of lc.definitions) {
67
+ if ((d.content ?? '').toLowerCase().includes(q)) return true;
68
+ }
69
+ }
70
+ return false;
71
+ }));
72
+ }
73
+
74
+ allLanguages() {
75
+ const set = new Set();
76
+ for (const c of this[_items]) {
77
+ for (const lang of c.languages) set.add(lang);
78
+ }
79
+ return [...set].sort();
80
+ }
81
+
82
+ filter(fn) { return new ConceptCollection(this[_items].filter(fn)); }
83
+ slice(...args) { return new ConceptCollection(this[_items].slice(...args)); }
84
+ concat(...args) { return new ConceptCollection(this[_items].concat(...args)); }
85
+ }
86
+
87
+ const _handler = {
88
+ get(target, prop, receiver) {
89
+ if (typeof prop === 'string' && /^\d+$/.test(prop)) {
90
+ return target[_items][Number(prop)];
91
+ }
92
+ if (prop === 'length') return target[_items].length;
93
+ const value = Reflect.get(target, prop, receiver);
94
+ return typeof value === 'function' ? value.bind(target) : value;
95
+ },
96
+ set(target, prop, value) {
97
+ if (typeof prop === 'string' && /^\d+$/.test(prop)) {
98
+ target[_items][Number(prop)] = value;
99
+ return true;
100
+ }
101
+ return Reflect.set(target, prop, value);
102
+ },
103
+ has(target, prop) {
104
+ if (typeof prop === 'string' && /^\d+$/.test(prop)) {
105
+ return Number(prop) in target[_items];
106
+ }
107
+ return Reflect.has(target, prop);
108
+ },
109
+ };
@@ -0,0 +1,78 @@
1
+ import yaml from 'js-yaml';
2
+ import { Concept } from './models/concept.js';
3
+ import { InvalidInputError, YamlParseError } from './errors.js';
4
+
5
+ const STRUCTURAL_KEYS = new Set(['termid', 'term']);
6
+
7
+ export class ConceptParser {
8
+ parse(raw, context) {
9
+ const label = context ?? 'concept';
10
+
11
+ if (raw == null) {
12
+ throw new InvalidInputError(
13
+ `parseConceptYaml requires a non-empty YAML string (${label})`,
14
+ 'non-null string',
15
+ );
16
+ }
17
+ if (typeof raw !== 'string' || raw.trim() === '') {
18
+ throw new InvalidInputError(
19
+ `parseConceptYaml requires a non-empty YAML string (${label})`,
20
+ 'non-empty string',
21
+ );
22
+ }
23
+
24
+ let docs;
25
+ try {
26
+ docs = yaml.loadAll(raw, null, { schema: yaml.DEFAULT_SCHEMA });
27
+ } catch (err) {
28
+ throw new YamlParseError(label, err);
29
+ }
30
+
31
+ return this._detectFormat(docs, label) === 'managed'
32
+ ? this._parseManaged(docs)
33
+ : this._parseCanonical(docs[0]);
34
+ }
35
+
36
+ _detectFormat(docs, label) {
37
+ if (docs.length >= 1 && docs[0]?.data?.identifier !== undefined) return 'managed';
38
+ if (docs[0] == null) throw new YamlParseError(label, new Error('YAML document is empty'));
39
+ return 'canonical';
40
+ }
41
+
42
+ _parseCanonical(doc) {
43
+ const localizations = {};
44
+ for (const key of Object.keys(doc)) {
45
+ if (!STRUCTURAL_KEYS.has(key) && typeof doc[key] === 'object' && doc[key] !== null) {
46
+ localizations[key] = doc[key];
47
+ }
48
+ }
49
+ return new Concept({
50
+ id: String(doc.termid),
51
+ term: doc.term || null,
52
+ localizations,
53
+ raw: doc,
54
+ });
55
+ }
56
+
57
+ _parseManaged(docs) {
58
+ const mc = docs[0];
59
+ const localizations = {};
60
+
61
+ for (const doc of docs.slice(1)) {
62
+ if (!doc?.data?.language_code) continue;
63
+ const lang = doc.data.language_code;
64
+ const lcData = { ...doc.data };
65
+ delete lcData.language_code;
66
+ localizations[lang] = lcData;
67
+ }
68
+
69
+ return new Concept({
70
+ id: String(mc.data.identifier),
71
+ term: null,
72
+ localizations,
73
+ raw: mc,
74
+ });
75
+ }
76
+ }
77
+
78
+ export const conceptParser = new ConceptParser();
@@ -1,7 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import yaml from 'js-yaml';
4
- import { parseConceptYaml, naturalSort } from './gcr-reader.js';
4
+ import { conceptParser } from './concept-parser.js';
5
+ import { naturalSort } from './gcr-reader.js';
5
6
  import { InvalidInputError } from './errors.js';
6
7
 
7
8
  function assertDir(dir, fnName) {
@@ -13,7 +14,7 @@ function assertDir(dir, fnName) {
13
14
  /**
14
15
  * Read all v2 glossarist concept YAML files from a directory.
15
16
  * @param {string} dir - path to directory containing concept YAML files
16
- * @returns {import('./gcr-reader.js').Concept[]}
17
+ * @returns {import('./models/concept.js').Concept[]}
17
18
  * @throws {InvalidInputError} if dir is missing or empty
18
19
  *
19
20
  * @example
@@ -29,8 +30,8 @@ export function readConcepts(dir) {
29
30
  const concepts = [];
30
31
  for (const file of files) {
31
32
  const raw = fs.readFileSync(path.join(dir, file), 'utf8');
32
- const concept = parseConceptYaml(raw, file);
33
- if (concept && concept.termid) {
33
+ const concept = conceptParser.parse(raw, file);
34
+ if (concept && concept.id) {
34
35
  concepts.push(concept);
35
36
  }
36
37
  }
@@ -41,7 +42,7 @@ export function readConcepts(dir) {
41
42
  * Read a single concept file by ID from a directory.
42
43
  * @param {string} dir - path to directory containing concept YAML files
43
44
  * @param {string} id - concept identifier (filename without .yaml)
44
- * @returns {import('./gcr-reader.js').Concept | null}
45
+ * @returns {import('./models/concept.js').Concept | null}
45
46
  * @throws {InvalidInputError} if dir or id is missing or empty
46
47
  *
47
48
  * @example
@@ -56,7 +57,7 @@ export function readConcept(dir, id) {
56
57
  const filePath = path.join(dir, `${id}.yaml`);
57
58
  if (!fs.existsSync(filePath)) return null;
58
59
  const raw = fs.readFileSync(filePath, 'utf8');
59
- return parseConceptYaml(raw, `${id}.yaml`);
60
+ return conceptParser.parse(raw, `${id}.yaml`);
60
61
  }
61
62
 
62
63
  /**
@@ -0,0 +1,60 @@
1
+ import yaml from 'js-yaml';
2
+
3
+ const DUMP_OPTS = { lineWidth: -1, noRefs: true, sortKeys: false, skipInvalid: true };
4
+
5
+ export class ConceptSerializer {
6
+ toCanonicalYaml(concept) {
7
+ const doc = { termid: concept.id };
8
+ if (concept.term) doc.term = concept.term;
9
+
10
+ for (const lang of concept.languages) {
11
+ const lc = concept.localization(lang);
12
+ if (lc) {
13
+ const lcObj = lc.toJSON();
14
+ delete lcObj.language_code;
15
+ doc[lang] = lcObj;
16
+ }
17
+ }
18
+
19
+ return yaml.dump(doc, DUMP_OPTS);
20
+ }
21
+
22
+ toManagedYaml(concept, uuidFn) {
23
+ const genId = uuidFn ?? (() => crypto.randomUUID());
24
+ const localizedConcepts = {};
25
+ const langDocs = [];
26
+
27
+ for (const lang of concept.languages) {
28
+ const lc = concept.localization(lang);
29
+ if (!lc) continue;
30
+ const lcId = genId();
31
+ localizedConcepts[lang] = lcId;
32
+
33
+ const lcObj = lc.toJSON();
34
+ langDocs.push({ data: lcObj, id: lcId });
35
+ }
36
+
37
+ const mainDoc = {
38
+ data: { identifier: concept.id, localized_concepts: localizedConcepts },
39
+ id: genId(),
40
+ };
41
+
42
+ const parts = [
43
+ '---\n' + yaml.dump(mainDoc, DUMP_OPTS),
44
+ ...langDocs.map(d => '---\n' + yaml.dump(d, DUMP_OPTS)),
45
+ ];
46
+ return parts.join('');
47
+ }
48
+
49
+ toYaml(concept, uuidFn) {
50
+ return concept.term
51
+ ? this.toCanonicalYaml(concept)
52
+ : this.toManagedYaml(concept, uuidFn);
53
+ }
54
+
55
+ toRegisterYaml(data) {
56
+ return yaml.dump(data, DUMP_OPTS);
57
+ }
58
+ }
59
+
60
+ export const conceptSerializer = new ConceptSerializer();
@@ -0,0 +1,7 @@
1
+ import { Concept } from './models/index';
2
+
3
+ export function writeConcept(dir: string, concept: Concept, format?: 'canonical' | 'managed' | 'auto'): void;
4
+ export function writeConcepts(dir: string, concepts: Concept[], options?: {
5
+ register?: Record<string, unknown>;
6
+ format?: 'canonical' | 'managed' | 'auto';
7
+ }): void;
@@ -0,0 +1,42 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { conceptSerializer } from './concept-serializer.js';
4
+ import { InvalidInputError } from './errors.js';
5
+
6
+ function assertDir(dir, fnName) {
7
+ if (typeof dir !== 'string' || dir.trim() === '') {
8
+ throw new InvalidInputError(`${fnName} requires a directory path`, 'non-empty string');
9
+ }
10
+ }
11
+
12
+ export function writeConcept(dir, concept, format = 'auto') {
13
+ assertDir(dir, 'writeConcept');
14
+ if (!concept?.id) {
15
+ throw new InvalidInputError('writeConcept requires a Concept with an id', 'Concept');
16
+ }
17
+
18
+ const y = format === 'canonical'
19
+ ? conceptSerializer.toCanonicalYaml(concept)
20
+ : format === 'managed'
21
+ ? conceptSerializer.toManagedYaml(concept)
22
+ : conceptSerializer.toYaml(concept);
23
+
24
+ fs.writeFileSync(path.join(dir, `${concept.id}.yaml`), y, 'utf8');
25
+ }
26
+
27
+ export function writeConcepts(dir, concepts, options = {}) {
28
+ assertDir(dir, 'writeConcepts');
29
+ fs.mkdirSync(dir, { recursive: true });
30
+
31
+ for (const concept of concepts) {
32
+ writeConcept(dir, concept, options.format);
33
+ }
34
+
35
+ if (options.register) {
36
+ fs.writeFileSync(
37
+ path.join(dir, 'register.yaml'),
38
+ conceptSerializer.toRegisterYaml(options.register),
39
+ 'utf8',
40
+ );
41
+ }
42
+ }
package/src/gcr-reader.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import JSZip from 'jszip';
2
2
  import yaml from 'js-yaml';
3
- import { InvalidInputError, YamlParseError } from './errors.js';
4
-
5
- const STRUCTURAL_KEYS = new Set(['termid', 'term']);
3
+ import { conceptParser } from './concept-parser.js';
4
+ import { InvalidInputError } from './errors.js';
6
5
 
7
6
  const BASE64_RE = /^[A-Za-z0-9+/]{100,}={0,2}$/;
8
7
 
@@ -120,7 +119,7 @@ export class GcrPackage {
120
119
  async concept(id) {
121
120
  const raw = await this._readText(`concepts/${id}.yaml`);
122
121
  if (raw === null) return null;
123
- return parseConceptYaml(raw, id);
122
+ return conceptParser.parse(raw, id);
124
123
  }
125
124
 
126
125
  /**
@@ -163,13 +162,6 @@ export class GcrPackage {
163
162
  /**
164
163
  * Parse concept YAML (canonical or managed format) into a normalized object.
165
164
  *
166
- * Canonical format (single doc):
167
- * { termid: "3.1.1.1", eng: { terms: [...], definition: [...] }, ... }
168
- *
169
- * Managed concept format (multi-doc):
170
- * doc 0: { data: { identifier: "3.1.1.1", localized_concepts: { eng: "uuid" } }, id: "uuid" }
171
- * doc 1+: { data: { language_code: "eng", terms: [...], ... }, id: "uuid" }
172
- *
173
165
  * @param {string} raw - raw YAML string
174
166
  * @param {string} [context] - concept ID or filename for error messages
175
167
  * @returns {Concept}
@@ -181,73 +173,7 @@ export class GcrPackage {
181
173
  * console.log(concept.localizations.eng.terms[0].designation); // "test"
182
174
  */
183
175
  export function parseConceptYaml(raw, context) {
184
- const label = context ?? 'concept';
185
-
186
- if (raw == null) {
187
- throw new InvalidInputError(`parseConceptYaml requires a non-empty YAML string (${label})`, 'non-null string');
188
- }
189
- if (typeof raw !== 'string' || raw.trim() === '') {
190
- throw new InvalidInputError(`parseConceptYaml requires a non-empty YAML string (${label})`, 'non-empty string');
191
- }
192
-
193
- let docs;
194
- try {
195
- docs = yaml.loadAll(raw, null, { schema: yaml.DEFAULT_SCHEMA });
196
- } catch (err) {
197
- throw new YamlParseError(label, err);
198
- }
199
-
200
- if (docs.length === 1 && docs[0]?.termid !== undefined) {
201
- return normalizeCanonical(docs[0]);
202
- }
203
-
204
- if (docs.length >= 1 && docs[0]?.data?.identifier !== undefined) {
205
- return normalizeManaged(docs);
206
- }
207
-
208
- if (docs[0] == null) {
209
- throw new YamlParseError(label, new Error('YAML document is empty'));
210
- }
211
-
212
- return normalizeCanonical(docs[0]);
213
- }
214
-
215
- /** @private @param {Record<string, any>} doc @returns {Concept} */
216
- function normalizeCanonical(doc) {
217
- const localizations = {};
218
- for (const key of Object.keys(doc)) {
219
- if (!STRUCTURAL_KEYS.has(key) && typeof doc[key] === 'object' && doc[key] !== null) {
220
- localizations[key] = doc[key];
221
- }
222
- }
223
- return {
224
- termid: String(doc.termid),
225
- term: doc.term || null,
226
- localizations,
227
- raw: doc,
228
- };
229
- }
230
-
231
- /** @private @param {Record<string, any>[]} docs @returns {Concept} */
232
- function normalizeManaged(docs) {
233
- const mc = docs[0];
234
- const termid = String(mc.data.identifier);
235
- const localizations = {};
236
-
237
- for (const doc of docs.slice(1)) {
238
- if (!doc || !doc.data || !doc.data.language_code) continue;
239
- const lang = doc.data.language_code;
240
- const lcData = { ...doc.data };
241
- delete lcData.language_code;
242
- localizations[lang] = lcData;
243
- }
244
-
245
- return {
246
- termid,
247
- term: null,
248
- localizations,
249
- raw: mc,
250
- };
176
+ return conceptParser.parse(raw, context);
251
177
  }
252
178
 
253
179
  // --- Helpers ---
@@ -0,0 +1,13 @@
1
+ import { Concept } from './models/index';
2
+
3
+ export class GcrWriter {
4
+ static createBuffer(options: {
5
+ concepts: Concept[];
6
+ metadata?: Record<string, unknown>;
7
+ register?: Record<string, unknown>;
8
+ uuidFn?: () => string;
9
+ format?: 'canonical' | 'managed' | 'auto';
10
+ }): Promise<Uint8Array>;
11
+ }
12
+
13
+ export function createGcr(concepts: Concept[], metadata?: Record<string, unknown>): Promise<Uint8Array>;
@@ -0,0 +1,38 @@
1
+ import JSZip from 'jszip';
2
+ import { conceptSerializer } from './concept-serializer.js';
3
+ import { InvalidInputError } from './errors.js';
4
+
5
+ export class GcrWriter {
6
+ static async createBuffer(options) {
7
+ if (!options || !options.concepts || typeof options.concepts[Symbol.iterator] !== 'function') {
8
+ throw new InvalidInputError(
9
+ 'GcrWriter requires { concepts: Concept[] }',
10
+ 'object with concepts array',
11
+ );
12
+ }
13
+
14
+ const zip = new JSZip();
15
+
16
+ if (options.metadata) {
17
+ zip.file('metadata.yaml', conceptSerializer.toRegisterYaml(options.metadata));
18
+ }
19
+ if (options.register) {
20
+ zip.file('register.yaml', conceptSerializer.toRegisterYaml(options.register));
21
+ }
22
+
23
+ for (const concept of options.concepts) {
24
+ const y = options.format === 'canonical'
25
+ ? conceptSerializer.toCanonicalYaml(concept)
26
+ : options.format === 'managed'
27
+ ? conceptSerializer.toManagedYaml(concept, options.uuidFn)
28
+ : conceptSerializer.toYaml(concept, options.uuidFn);
29
+ zip.file(`concepts/${concept.id}.yaml`, y);
30
+ }
31
+
32
+ return zip.generateAsync({ type: 'uint8array' });
33
+ }
34
+ }
35
+
36
+ export async function createGcr(concepts, metadata) {
37
+ return GcrWriter.createBuffer({ concepts, metadata });
38
+ }
package/src/index.d.ts CHANGED
@@ -1,4 +1,41 @@
1
+ // Models
2
+ export {
3
+ GlossaristModel,
4
+ Concept, LocalizedConcept,
5
+ Designation, Expression, Abbreviation, Symbol, GraphicalSymbol,
6
+ Citation, ConceptSource, RelatedConcept, ConceptDate,
7
+ DetailedDefinition, NonVerbRep,
8
+ RELATIONSHIP_TYPES, DATE_TYPES,
9
+ } from './models/index';
10
+
11
+ // GCR reader
1
12
  export { loadGcr, GcrPackage, parseConceptYaml, naturalSort } from './gcr-reader';
2
- export type { Concept, Localization, Term, Definition, Source, GcrMetadata } from './gcr-reader';
13
+ export type { GcrMetadata } from './gcr-reader';
14
+
15
+ // GCR writer
16
+ export { createGcr, GcrWriter } from './gcr-writer';
17
+
18
+ // Concept reader
3
19
  export { readConcepts, readConcept, listConceptIds, readRegister } from './concept-reader';
20
+
21
+ // Concept writer
22
+ export { writeConcept, writeConcepts } from './concept-writer';
23
+
24
+ // Collections
25
+ export { ConceptCollection } from './concept-collection';
26
+ export { ManagedConceptCollection } from './managed-concept-collection';
27
+
28
+ // Validators
29
+ export { validateConcept, validateRegister, createConceptValidator, ValidationError, ValidationRule, RegisterValidator } from './validators/index';
30
+
31
+ // UUID
32
+ export { conceptUuid, localizedConceptUuid, uuidV5 } from './uuid';
33
+
34
+ // Reference resolution
35
+ export { ReferenceResolver, Reference, referenceResolver } from './reference-resolver';
36
+
37
+ // V1 support
38
+ export { V1Reader, migrateV1ToV2 } from './v1-reader';
39
+
40
+ // Errors
4
41
  export { GlossaristError, InvalidInputError, YamlParseError } from './errors';
package/src/index.js CHANGED
@@ -1,3 +1,20 @@
1
1
  export { loadGcr, GcrPackage, parseConceptYaml, naturalSort } from './gcr-reader.js';
2
2
  export { readConcepts, readConcept, listConceptIds, readRegister } from './concept-reader.js';
3
+ export { writeConcept, writeConcepts } from './concept-writer.js';
4
+ export { createGcr, GcrWriter } from './gcr-writer.js';
5
+ export { ConceptCollection } from './concept-collection.js';
6
+ export { ManagedConceptCollection } from './managed-concept-collection.js';
7
+ export { validateConcept, validateRegister, createConceptValidator, ValidationError, ValidationRule, RegisterValidator } from './validators/index.js';
8
+ export { conceptUuid, localizedConceptUuid, uuidV5 } from './uuid.js';
9
+ export { ReferenceResolver, Reference, referenceResolver } from './reference-resolver.js';
10
+ export { V1Reader, migrateV1ToV2 } from './v1-reader.js';
3
11
  export { GlossaristError, InvalidInputError, YamlParseError } from './errors.js';
12
+
13
+ export {
14
+ GlossaristModel,
15
+ Concept, LocalizedConcept,
16
+ Designation, Expression, Abbreviation, Symbol, GraphicalSymbol,
17
+ Citation, ConceptSource, RelatedConcept, ConceptDate,
18
+ DetailedDefinition, NonVerbRep,
19
+ RELATIONSHIP_TYPES, DATE_TYPES,
20
+ } from './models/index.js';
@@ -0,0 +1,61 @@
1
+ import { ConceptCollection } from './concept-collection.js';
2
+ import { readConcepts, readRegister } from './concept-reader.js';
3
+ import { writeConcepts } from './concept-writer.js';
4
+ import { loadGcr } from './gcr-reader.js';
5
+ import { createGcr } from './gcr-writer.js';
6
+
7
+ export class ManagedConceptCollection {
8
+ constructor() {
9
+ this._concepts = new ConceptCollection();
10
+ this._register = null;
11
+ }
12
+
13
+ get concepts() { return this._concepts; }
14
+ get register() { return this._register; }
15
+
16
+ loadFromDirectory(dir) {
17
+ this._concepts = new ConceptCollection(readConcepts(dir));
18
+ this._register = readRegister(dir);
19
+ return this;
20
+ }
21
+
22
+ async loadFromGcr(input) {
23
+ const pkg = await loadGcr(input);
24
+ this._concepts = new ConceptCollection(await pkg.allConcepts());
25
+ this._register = await pkg.register();
26
+ return this;
27
+ }
28
+
29
+ saveToDirectory(dir, options = {}) {
30
+ writeConcepts(dir, this._concepts, {
31
+ register: this._register ?? undefined,
32
+ format: options.format,
33
+ });
34
+ }
35
+
36
+ async saveToGcr(options = {}) {
37
+ return createGcr(this._concepts, options.metadata);
38
+ }
39
+
40
+ add(concept) {
41
+ const existing = this._concepts.byId(concept.id);
42
+ if (existing) {
43
+ const idx = this._concepts.indexOf(existing);
44
+ this._concepts.set(idx, concept);
45
+ } else {
46
+ this._concepts.push(concept);
47
+ }
48
+ return this;
49
+ }
50
+
51
+ remove(id) {
52
+ const idx = this._concepts.findIndex(c => c.id === id);
53
+ if (idx >= 0) this._concepts.splice(idx, 1);
54
+ return this;
55
+ }
56
+
57
+ setRegister(data) {
58
+ this._register = data;
59
+ return this;
60
+ }
61
+ }