glossarist 0.1.7 → 0.2.1
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 +1 -1
- package/src/concept-collection.js +2 -3
- package/src/concept-reader.d.ts +1 -1
- package/src/concept-reader.js +1 -1
- package/src/gcr-reader.d.ts +7 -51
- package/src/gcr-reader.js +53 -37
- package/src/gcr-writer.d.ts +2 -2
- package/src/gcr-writer.js +24 -1
- package/src/index.d.ts +5 -2
- package/src/index.js +4 -2
- package/src/models/gcr-metadata.js +68 -0
- package/src/models/gcr-statistics.js +51 -0
- package/src/models/index.d.ts +39 -0
- package/src/models/index.js +2 -0
- package/src/sort.js +25 -0
- package/src/validators/gcr-validator.js +74 -0
- package/src/validators/index.js +7 -0
- package/src/validators/validation-result.js +34 -0
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { naturalSort } from './
|
|
1
|
+
import { naturalSort } from './sort.js';
|
|
2
2
|
|
|
3
3
|
const _items = Symbol('items');
|
|
4
4
|
|
|
@@ -37,8 +37,7 @@ export class ConceptCollection {
|
|
|
37
37
|
|
|
38
38
|
byStatus(status) {
|
|
39
39
|
return new ConceptCollection(this[_items].filter(c => {
|
|
40
|
-
|
|
41
|
-
return langs.length > 0 && c.localization(langs[0])?.entryStatus === status;
|
|
40
|
+
return c.languages.some(lang => c.localization(lang)?.entryStatus === status);
|
|
42
41
|
}));
|
|
43
42
|
}
|
|
44
43
|
|
package/src/concept-reader.d.ts
CHANGED
package/src/concept-reader.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
import { conceptParser } from './concept-parser.js';
|
|
5
|
-
import { naturalSort } from './
|
|
5
|
+
import { naturalSort } from './sort.js';
|
|
6
6
|
import { InvalidInputError } from './errors.js';
|
|
7
7
|
|
|
8
8
|
function assertDir(dir, fnName) {
|
package/src/gcr-reader.d.ts
CHANGED
|
@@ -1,52 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export interface Term {
|
|
3
|
-
type: string;
|
|
4
|
-
designation: string;
|
|
5
|
-
normative_status?: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/** A definition content block. */
|
|
9
|
-
export interface Definition {
|
|
10
|
-
content: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** A bibliographic source reference. */
|
|
14
|
-
export interface Source {
|
|
15
|
-
type: string;
|
|
16
|
-
origin?: { ref: string };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Localized concept data for a single language. */
|
|
20
|
-
export interface Localization {
|
|
21
|
-
terms: Term[];
|
|
22
|
-
definition?: Definition[];
|
|
23
|
-
notes?: { content: string }[];
|
|
24
|
-
examples?: { content: string }[];
|
|
25
|
-
sources?: Source[];
|
|
26
|
-
entry_status?: string;
|
|
27
|
-
normative_status?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** A normalized glossarist concept. */
|
|
31
|
-
export interface Concept {
|
|
32
|
-
termid: string;
|
|
33
|
-
term: string | null;
|
|
34
|
-
localizations: Record<string, Localization>;
|
|
35
|
-
raw: Record<string, unknown>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** GCR package metadata from metadata.yaml. */
|
|
39
|
-
export interface GcrMetadata {
|
|
40
|
-
shortname: string;
|
|
41
|
-
version?: string;
|
|
42
|
-
title?: string;
|
|
43
|
-
concept_count?: number;
|
|
44
|
-
languages?: string[];
|
|
45
|
-
schema_version?: string;
|
|
46
|
-
glossarist_version?: string;
|
|
47
|
-
created_at?: string;
|
|
48
|
-
statistics?: Record<string, unknown>;
|
|
49
|
-
}
|
|
1
|
+
import { Concept, GcrMetadata } from './models/index';
|
|
50
2
|
|
|
51
3
|
/**
|
|
52
4
|
* Load a GCR package from a ZIP archive.
|
|
@@ -56,7 +8,7 @@ export function loadGcr(input: Buffer | ArrayBuffer | Uint8Array | Blob | string
|
|
|
56
8
|
|
|
57
9
|
/** A loaded GCR package (ZIP archive of glossarist concept data). */
|
|
58
10
|
export class GcrPackage {
|
|
59
|
-
/** Read and parse metadata.yaml. */
|
|
11
|
+
/** Read and parse metadata.yaml as a GcrMetadata instance. */
|
|
60
12
|
metadata(): Promise<GcrMetadata | null>;
|
|
61
13
|
/** Read and parse optional register.yaml. */
|
|
62
14
|
register(): Promise<Record<string, unknown> | null>;
|
|
@@ -88,6 +40,10 @@ export class GcrPackage {
|
|
|
88
40
|
|
|
89
41
|
// Dataset assets (bibliography, images)
|
|
90
42
|
|
|
43
|
+
/** List all dataset asset entries found in this package. */
|
|
44
|
+
datasetAssetEntries(): Promise<Array<{ path: string; type: 'file' | 'directory'; asset: { path: string; type: string } }>>;
|
|
45
|
+
/** Read a file-type dataset asset by its registered path. */
|
|
46
|
+
readDatasetFileAsset(assetPath: string): Promise<string | null>;
|
|
91
47
|
/** Read bibliography.yaml from the package as raw YAML string. */
|
|
92
48
|
bibliography(): Promise<string | null>;
|
|
93
49
|
/** Check whether the images/ directory is present and non-empty. */
|
|
@@ -102,7 +58,7 @@ export class GcrPackage {
|
|
|
102
58
|
allImageFiles(): Promise<Map<string, Uint8Array>>;
|
|
103
59
|
}
|
|
104
60
|
|
|
105
|
-
/** Parse raw concept YAML (canonical or managed format) into a
|
|
61
|
+
/** Parse raw concept YAML (canonical or managed format) into a Concept. */
|
|
106
62
|
export function parseConceptYaml(raw: string, context?: string): Concept;
|
|
107
63
|
|
|
108
64
|
/** Natural sort comparator for concept IDs like "3.1.1.1", "551-12-39". */
|
package/src/gcr-reader.js
CHANGED
|
@@ -3,11 +3,13 @@ import yaml from 'js-yaml';
|
|
|
3
3
|
import { conceptParser } from './concept-parser.js';
|
|
4
4
|
import { InvalidInputError } from './errors.js';
|
|
5
5
|
import { COMPILED_FORMATS, parseCompiledPath, compiledPath } from './compiled-format.js';
|
|
6
|
+
import { DATASET_ASSETS, findFileAsset, findDirectoryAssetPath } from './dataset-asset.js';
|
|
7
|
+
import { GcrMetadata } from './models/gcr-metadata.js';
|
|
8
|
+
import { naturalSort } from './sort.js';
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
export { naturalSort } from './sort.js';
|
|
8
11
|
|
|
9
|
-
const
|
|
10
|
-
const DIGIT_RE = /^\d+$/;
|
|
12
|
+
const BASE64_RE = /^[A-Za-z0-9+/]{100,}={0,2}$/;
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* @typedef {Object} Term
|
|
@@ -81,12 +83,12 @@ export class GcrPackage {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
/**
|
|
84
|
-
* Read and parse metadata.yaml from the package.
|
|
86
|
+
* Read and parse metadata.yaml from the package as a GcrMetadata instance.
|
|
85
87
|
* @returns {Promise<GcrMetadata | null>}
|
|
86
88
|
*/
|
|
87
89
|
async metadata() {
|
|
88
90
|
const raw = await this._readText('metadata.yaml');
|
|
89
|
-
return raw ?
|
|
91
|
+
return raw ? GcrMetadata.fromYaml(raw) : null;
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
/**
|
|
@@ -249,22 +251,58 @@ export class GcrPackage {
|
|
|
249
251
|
|
|
250
252
|
// --- Dataset assets (bibliography, images, etc.) ---
|
|
251
253
|
|
|
254
|
+
/**
|
|
255
|
+
* List all dataset asset entries found in this package.
|
|
256
|
+
* Each entry has { path, type, asset } where asset is the registry descriptor.
|
|
257
|
+
* @returns {Promise<Array<{ path: string, type: 'file' | 'directory', asset: { path: string, type: string } }>>}
|
|
258
|
+
*/
|
|
259
|
+
async datasetAssetEntries() {
|
|
260
|
+
const entries = [];
|
|
261
|
+
this._zip.forEach((relativePath, zipEntry) => {
|
|
262
|
+
if (zipEntry.dir) return;
|
|
263
|
+
const fileAsset = findFileAsset(relativePath);
|
|
264
|
+
if (fileAsset) {
|
|
265
|
+
entries.push({ path: relativePath, type: 'file', asset: fileAsset });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const dirAsset = findDirectoryAssetPath(relativePath);
|
|
269
|
+
if (dirAsset) {
|
|
270
|
+
entries.push({ path: relativePath, type: 'directory', asset: dirAsset });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return entries;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Read a file-type dataset asset by its registered path (e.g. 'bibliography.yaml').
|
|
278
|
+
* @param {string} assetPath - registered file asset path
|
|
279
|
+
* @returns {Promise<string | null>}
|
|
280
|
+
*/
|
|
281
|
+
async readDatasetFileAsset(assetPath) {
|
|
282
|
+
return this._readText(assetPath);
|
|
283
|
+
}
|
|
284
|
+
|
|
252
285
|
/**
|
|
253
286
|
* Read bibliography.yaml from the package as a string (raw YAML).
|
|
254
287
|
* @returns {Promise<string | null>}
|
|
255
288
|
*/
|
|
256
289
|
async bibliography() {
|
|
257
|
-
|
|
290
|
+
const fileAsset = DATASET_ASSETS.find((a) => a.type === 'file' && a.path === 'bibliography.yaml');
|
|
291
|
+
return fileAsset ? this._readText(fileAsset.path) : null;
|
|
258
292
|
}
|
|
259
293
|
|
|
260
294
|
/**
|
|
261
295
|
* Check whether the images/ directory is present and non-empty.
|
|
296
|
+
* Uses the dataset-asset registry to find the images directory.
|
|
262
297
|
* @returns {Promise<boolean>}
|
|
263
298
|
*/
|
|
264
299
|
async hasImages() {
|
|
300
|
+
const asset = DATASET_ASSETS.find((a) => a.type === 'directory' && a.path === 'images');
|
|
301
|
+
if (!asset) return false;
|
|
302
|
+
const prefix = `${asset.path}/`;
|
|
265
303
|
let found = false;
|
|
266
304
|
this._zip.forEach((relativePath, entry) => {
|
|
267
|
-
if (!found && !entry.dir && relativePath.startsWith(
|
|
305
|
+
if (!found && !entry.dir && relativePath.startsWith(prefix)) {
|
|
268
306
|
found = true;
|
|
269
307
|
}
|
|
270
308
|
});
|
|
@@ -273,12 +311,16 @@ export class GcrPackage {
|
|
|
273
311
|
|
|
274
312
|
/**
|
|
275
313
|
* List all image file paths (relative to ZIP root).
|
|
314
|
+
* Uses the dataset-asset registry to find the images directory.
|
|
276
315
|
* @returns {Promise<string[]>}
|
|
277
316
|
*/
|
|
278
317
|
async imageFileNames() {
|
|
318
|
+
const asset = DATASET_ASSETS.find((a) => a.type === 'directory' && a.path === 'images');
|
|
319
|
+
if (!asset) return [];
|
|
320
|
+
const prefix = `${asset.path}/`;
|
|
279
321
|
const names = [];
|
|
280
322
|
this._zip.forEach((relativePath, entry) => {
|
|
281
|
-
if (!entry.dir && relativePath.startsWith(
|
|
323
|
+
if (!entry.dir && relativePath.startsWith(prefix)) {
|
|
282
324
|
names.push(relativePath);
|
|
283
325
|
}
|
|
284
326
|
});
|
|
@@ -291,7 +333,9 @@ export class GcrPackage {
|
|
|
291
333
|
* @returns {Promise<Uint8Array | null>}
|
|
292
334
|
*/
|
|
293
335
|
async imageFile(path) {
|
|
294
|
-
const
|
|
336
|
+
const asset = DATASET_ASSETS.find((a) => a.type === 'directory' && a.path === 'images');
|
|
337
|
+
if (!asset) return null;
|
|
338
|
+
const fullPath = path.startsWith(`${asset.path}/`) ? path : `${asset.path}/${path}`;
|
|
295
339
|
const entry = this._zip.file(fullPath);
|
|
296
340
|
if (!entry) return null;
|
|
297
341
|
return entry.async('uint8array');
|
|
@@ -350,34 +394,6 @@ export function parseConceptYaml(raw, context) {
|
|
|
350
394
|
return conceptParser.parse(raw, context);
|
|
351
395
|
}
|
|
352
396
|
|
|
353
|
-
// --- Helpers ---
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Natural sort comparator for concept IDs like "3.1.1.1", "551-12-39".
|
|
357
|
-
* @param {string} a
|
|
358
|
-
* @param {string} b
|
|
359
|
-
* @returns {number}
|
|
360
|
-
*
|
|
361
|
-
* @example
|
|
362
|
-
* ['3.1.10', '3.1.2', '3.1.1'].sort(naturalSort); // ['3.1.1', '3.1.2', '3.1.10']
|
|
363
|
-
*/
|
|
364
|
-
export function naturalSort(a, b) {
|
|
365
|
-
const pa = a.match(NATURAL_SORT_RE) || [];
|
|
366
|
-
const pb = b.match(NATURAL_SORT_RE) || [];
|
|
367
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
368
|
-
const na = pa[i] || '';
|
|
369
|
-
const nb = pb[i] || '';
|
|
370
|
-
if (DIGIT_RE.test(na) && DIGIT_RE.test(nb)) {
|
|
371
|
-
const diff = parseInt(na, 10) - parseInt(nb, 10);
|
|
372
|
-
if (diff !== 0) return diff;
|
|
373
|
-
} else {
|
|
374
|
-
const cmp = na.localeCompare(nb);
|
|
375
|
-
if (cmp !== 0) return cmp;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return 0;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
397
|
/**
|
|
382
398
|
* @typedef {Object} GcrMetadata
|
|
383
399
|
* @property {string} shortname - dataset short name
|
package/src/gcr-writer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Concept } from './models/index';
|
|
1
|
+
import { Concept, GcrMetadata } from './models/index';
|
|
2
2
|
|
|
3
3
|
/** Compiled formats map: format name → id → content string. */
|
|
4
4
|
export type CompiledFormatsMap = Record<string, Record<string, string> | Map<string, string>>;
|
|
@@ -9,7 +9,7 @@ export type ImagesMap = Record<string, Uint8Array | string | ArrayBuffer> | Map<
|
|
|
9
9
|
export class GcrWriter {
|
|
10
10
|
static createBuffer(options: {
|
|
11
11
|
concepts: Concept[];
|
|
12
|
-
metadata?: Record<string, unknown>;
|
|
12
|
+
metadata?: GcrMetadata | Record<string, unknown>;
|
|
13
13
|
register?: Record<string, unknown>;
|
|
14
14
|
uuidFn?: () => string;
|
|
15
15
|
format?: 'canonical' | 'managed' | 'auto';
|
package/src/gcr-writer.js
CHANGED
|
@@ -2,6 +2,8 @@ import JSZip from 'jszip';
|
|
|
2
2
|
import { conceptSerializer } from './concept-serializer.js';
|
|
3
3
|
import { InvalidInputError } from './errors.js';
|
|
4
4
|
import { compiledPath, isKnownFormat } from './compiled-format.js';
|
|
5
|
+
import { GcrMetadata } from './models/gcr-metadata.js';
|
|
6
|
+
import { GcrStatistics } from './models/gcr-statistics.js';
|
|
5
7
|
|
|
6
8
|
export class GcrWriter {
|
|
7
9
|
static async createBuffer(options) {
|
|
@@ -15,7 +17,8 @@ export class GcrWriter {
|
|
|
15
17
|
const zip = new JSZip();
|
|
16
18
|
|
|
17
19
|
if (options.metadata) {
|
|
18
|
-
|
|
20
|
+
const meta = GcrWriter._normalizeMetadata(options.metadata, options.concepts);
|
|
21
|
+
zip.file('metadata.yaml', conceptSerializer.toRegisterYaml(meta));
|
|
19
22
|
}
|
|
20
23
|
if (options.register) {
|
|
21
24
|
zip.file('register.yaml', conceptSerializer.toRegisterYaml(options.register));
|
|
@@ -45,6 +48,26 @@ export class GcrWriter {
|
|
|
45
48
|
return zip.generateAsync({ type: 'uint8array' });
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
static _normalizeMetadata(metadata, concepts) {
|
|
52
|
+
if (metadata instanceof GcrMetadata) {
|
|
53
|
+
const meta = metadata.clone();
|
|
54
|
+
if (!meta.statistics && concepts.length > 0) {
|
|
55
|
+
meta.statistics = GcrStatistics.fromConcepts(concepts);
|
|
56
|
+
}
|
|
57
|
+
if (!meta.conceptCount) meta.conceptCount = concepts.length;
|
|
58
|
+
return meta.toJSON();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const data = { ...metadata };
|
|
62
|
+
if (!data.statistics && concepts.length > 0) {
|
|
63
|
+
data.statistics = GcrStatistics.fromConcepts(concepts).toJSON();
|
|
64
|
+
}
|
|
65
|
+
if (!data.concept_count && concepts.length > 0) {
|
|
66
|
+
data.concept_count = concepts.length;
|
|
67
|
+
}
|
|
68
|
+
return data;
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
static _writeCompiledFormats(zip, compiledFormats) {
|
|
49
72
|
for (const [format, entries] of Object.entries(compiledFormats)) {
|
|
50
73
|
if (!isKnownFormat(format)) {
|
package/src/index.d.ts
CHANGED
|
@@ -5,12 +5,12 @@ export {
|
|
|
5
5
|
Designation, Expression, Abbreviation, Symbol, GraphicalSymbol,
|
|
6
6
|
Citation, ConceptSource, RelatedConcept, ConceptDate,
|
|
7
7
|
DetailedDefinition, NonVerbRep,
|
|
8
|
+
GcrMetadata, GcrStatistics,
|
|
8
9
|
RELATIONSHIP_TYPES, DATE_TYPES,
|
|
9
10
|
} from './models/index';
|
|
10
11
|
|
|
11
12
|
// GCR reader
|
|
12
13
|
export { loadGcr, GcrPackage, parseConceptYaml, naturalSort } from './gcr-reader';
|
|
13
|
-
export type { GcrMetadata } from './gcr-reader';
|
|
14
14
|
|
|
15
15
|
// GCR writer
|
|
16
16
|
export { createGcr, GcrWriter } from './gcr-writer';
|
|
@@ -26,7 +26,7 @@ export { ConceptCollection } from './concept-collection';
|
|
|
26
26
|
export { ManagedConceptCollection } from './managed-concept-collection';
|
|
27
27
|
|
|
28
28
|
// Validators
|
|
29
|
-
export { validateConcept, validateRegister, createConceptValidator, ValidationError, ValidationRule, RegisterValidator } from './validators/index';
|
|
29
|
+
export { validateConcept, validateRegister, validateGcrPackage, createConceptValidator, ValidationError, ValidationRule, ValidationResult, RegisterValidator, GcrValidator } from './validators/index';
|
|
30
30
|
|
|
31
31
|
// UUID
|
|
32
32
|
export { conceptUuid, localizedConceptUuid, uuidV5 } from './uuid';
|
|
@@ -52,3 +52,6 @@ export function parseCompiledPath(zipPath: string): { format: string; id: string
|
|
|
52
52
|
export const DATASET_ASSETS: readonly { path: string; type: string }[];
|
|
53
53
|
export const FILE_ASSETS: readonly { path: string; type: string }[];
|
|
54
54
|
export const DIRECTORY_ASSETS: readonly { path: string; type: string }[];
|
|
55
|
+
export function findFileAsset(path: string): { path: string; type: string } | undefined;
|
|
56
|
+
export function findDirectoryAssetPath(zipPath: string): { path: string; type: string } | undefined;
|
|
57
|
+
export function isDatasetAssetPath(zipPath: string): boolean;
|
package/src/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { naturalSort } from './sort.js';
|
|
2
|
+
export { loadGcr, GcrPackage, parseConceptYaml } from './gcr-reader.js';
|
|
2
3
|
export { readConcepts, readConcept, listConceptIds, readRegister } from './concept-reader.js';
|
|
3
4
|
export { writeConcept, writeConcepts } from './concept-writer.js';
|
|
4
5
|
export { createGcr, GcrWriter } from './gcr-writer.js';
|
|
5
6
|
export { ConceptCollection } from './concept-collection.js';
|
|
6
7
|
export { ManagedConceptCollection } from './managed-concept-collection.js';
|
|
7
|
-
export { validateConcept, validateRegister, createConceptValidator, ValidationError, ValidationRule, RegisterValidator } from './validators/index.js';
|
|
8
|
+
export { validateConcept, validateRegister, validateGcrPackage, createConceptValidator, ValidationError, ValidationRule, ValidationResult, RegisterValidator, GcrValidator } from './validators/index.js';
|
|
8
9
|
export { conceptUuid, localizedConceptUuid, uuidV5 } from './uuid.js';
|
|
9
10
|
export { ReferenceResolver, Reference, referenceResolver } from './reference-resolver.js';
|
|
10
11
|
export { V1Reader, migrateV1ToV2 } from './v1-reader.js';
|
|
@@ -31,5 +32,6 @@ export {
|
|
|
31
32
|
Designation, Expression, Abbreviation, Symbol, GraphicalSymbol,
|
|
32
33
|
Citation, ConceptSource, RelatedConcept, ConceptDate,
|
|
33
34
|
DetailedDefinition, NonVerbRep,
|
|
35
|
+
GcrMetadata, GcrStatistics,
|
|
34
36
|
RELATIONSHIP_TYPES, DATE_TYPES,
|
|
35
37
|
} from './models/index.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
import { GlossaristModel } from './base.js';
|
|
3
|
+
import { GcrStatistics } from './gcr-statistics.js';
|
|
4
|
+
|
|
5
|
+
export class GcrMetadata extends GlossaristModel {
|
|
6
|
+
constructor(data = {}) {
|
|
7
|
+
super();
|
|
8
|
+
this.shortname = data.shortname ?? null;
|
|
9
|
+
this.version = data.version ?? null;
|
|
10
|
+
this.title = data.title ?? null;
|
|
11
|
+
this.description = data.description ?? null;
|
|
12
|
+
this.owner = data.owner ?? null;
|
|
13
|
+
this.tags = data.tags ?? [];
|
|
14
|
+
this.conceptCount = data.concept_count ?? data.conceptCount ?? 0;
|
|
15
|
+
this.languages = data.languages ?? [];
|
|
16
|
+
this.createdAt = data.created_at ?? data.createdAt ?? null;
|
|
17
|
+
this.glossaristVersion = data.glossarist_version ?? data.glossaristVersion ?? null;
|
|
18
|
+
this.schemaVersion = data.schema_version ?? data.schemaVersion ?? '1';
|
|
19
|
+
this.homepage = data.homepage ?? null;
|
|
20
|
+
this.repository = data.repository ?? null;
|
|
21
|
+
this.license = data.license ?? null;
|
|
22
|
+
this.uriPrefix = data.uri_prefix ?? data.uriPrefix ?? null;
|
|
23
|
+
this.conceptUriTemplate = data.concept_uri_template ?? data.conceptUriTemplate ?? null;
|
|
24
|
+
this.compiledFormats = data.compiled_formats ?? data.compiledFormats ?? [];
|
|
25
|
+
this.statistics = data.statistics
|
|
26
|
+
? (data.statistics instanceof GcrStatistics ? data.statistics : new GcrStatistics(data.statistics))
|
|
27
|
+
: null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get concept_count() { return this.conceptCount; }
|
|
31
|
+
get created_at() { return this.createdAt; }
|
|
32
|
+
get glossarist_version() { return this.glossaristVersion; }
|
|
33
|
+
get schema_version() { return this.schemaVersion; }
|
|
34
|
+
get uri_prefix() { return this.uriPrefix; }
|
|
35
|
+
get concept_uri_template() { return this.conceptUriTemplate; }
|
|
36
|
+
get compiled_formats() { return this.compiledFormats; }
|
|
37
|
+
|
|
38
|
+
toJSON() {
|
|
39
|
+
const obj = {};
|
|
40
|
+
if (this.shortname != null) obj.shortname = this.shortname;
|
|
41
|
+
if (this.version != null) obj.version = this.version;
|
|
42
|
+
if (this.title != null) obj.title = this.title;
|
|
43
|
+
if (this.description != null) obj.description = this.description;
|
|
44
|
+
if (this.owner != null) obj.owner = this.owner;
|
|
45
|
+
if (this.tags.length > 0) obj.tags = this.tags;
|
|
46
|
+
if (this.conceptCount > 0) obj.concept_count = this.conceptCount;
|
|
47
|
+
if (this.languages.length > 0) obj.languages = this.languages;
|
|
48
|
+
if (this.createdAt != null) obj.created_at = this.createdAt;
|
|
49
|
+
if (this.glossaristVersion != null) obj.glossarist_version = this.glossaristVersion;
|
|
50
|
+
if (this.schemaVersion != null) obj.schema_version = this.schemaVersion;
|
|
51
|
+
if (this.homepage != null) obj.homepage = this.homepage;
|
|
52
|
+
if (this.repository != null) obj.repository = this.repository;
|
|
53
|
+
if (this.license != null) obj.license = this.license;
|
|
54
|
+
if (this.uriPrefix != null) obj.uri_prefix = this.uriPrefix;
|
|
55
|
+
if (this.conceptUriTemplate != null) obj.concept_uri_template = this.conceptUriTemplate;
|
|
56
|
+
if (this.compiledFormats.length > 0) obj.compiled_formats = this.compiledFormats;
|
|
57
|
+
if (this.statistics != null) obj.statistics = this.statistics.toJSON();
|
|
58
|
+
return obj;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static fromJSON(data) {
|
|
62
|
+
return new GcrMetadata(data);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static fromYaml(yamlString) {
|
|
66
|
+
return new GcrMetadata(yaml.load(yamlString));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { GlossaristModel } from './base.js';
|
|
2
|
+
|
|
3
|
+
export class GcrStatistics extends GlossaristModel {
|
|
4
|
+
constructor(data = {}) {
|
|
5
|
+
super();
|
|
6
|
+
this.totalConcepts = data.total_concepts ?? data.totalConcepts ?? 0;
|
|
7
|
+
this.conceptsWithDefinitions = data.concepts_with_definitions ?? data.conceptsWithDefinitions ?? 0;
|
|
8
|
+
this.conceptsByStatus = data.concepts_by_status ?? data.conceptsByStatus ?? {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get total_concepts() { return this.totalConcepts; }
|
|
12
|
+
get concepts_with_definitions() { return this.conceptsWithDefinitions; }
|
|
13
|
+
get concepts_by_status() { return this.conceptsByStatus; }
|
|
14
|
+
|
|
15
|
+
toJSON() {
|
|
16
|
+
const obj = { total_concepts: this.totalConcepts };
|
|
17
|
+
if (this.conceptsWithDefinitions > 0) {
|
|
18
|
+
obj.concepts_with_definitions = this.conceptsWithDefinitions;
|
|
19
|
+
}
|
|
20
|
+
if (Object.keys(this.conceptsByStatus).length > 0) {
|
|
21
|
+
obj.concepts_by_status = this.conceptsByStatus;
|
|
22
|
+
}
|
|
23
|
+
return obj;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static fromJSON(data) {
|
|
27
|
+
return new GcrStatistics(data);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static fromConcepts(concepts) {
|
|
31
|
+
const langs = new Set();
|
|
32
|
+
let withDefs = 0;
|
|
33
|
+
const byStatus = {};
|
|
34
|
+
|
|
35
|
+
for (const concept of concepts) {
|
|
36
|
+
for (const lang of concept.languages) {
|
|
37
|
+
langs.add(lang);
|
|
38
|
+
const lc = concept.localization(lang);
|
|
39
|
+
if (lc && lc.definitions.length > 0) withDefs++;
|
|
40
|
+
const status = lc?.entryStatus ?? 'unknown';
|
|
41
|
+
byStatus[status] = (byStatus[status] ?? 0) + 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return new GcrStatistics({
|
|
46
|
+
total_concepts: concepts.length,
|
|
47
|
+
concepts_with_definitions: withDefs,
|
|
48
|
+
concepts_by_status: byStatus,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/models/index.d.ts
CHANGED
|
@@ -106,3 +106,42 @@ export class NonVerbRep extends GlossaristModel {
|
|
|
106
106
|
readonly formula: string | null;
|
|
107
107
|
readonly sources: Citation[];
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
export class GcrStatistics extends GlossaristModel {
|
|
111
|
+
readonly totalConcepts: number;
|
|
112
|
+
readonly conceptsWithDefinitions: number;
|
|
113
|
+
readonly conceptsByStatus: Record<string, number>;
|
|
114
|
+
readonly total_concepts: number;
|
|
115
|
+
readonly concepts_with_definitions: number;
|
|
116
|
+
readonly concepts_by_status: Record<string, number>;
|
|
117
|
+
static fromConcepts(concepts: Concept[]): GcrStatistics;
|
|
118
|
+
static fromJSON(data: Record<string, unknown>): GcrStatistics;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class GcrMetadata extends GlossaristModel {
|
|
122
|
+
readonly shortname: string | null;
|
|
123
|
+
readonly version: string | null;
|
|
124
|
+
readonly title: string | null;
|
|
125
|
+
readonly description: string | null;
|
|
126
|
+
readonly owner: string | null;
|
|
127
|
+
readonly tags: string[];
|
|
128
|
+
readonly conceptCount: number;
|
|
129
|
+
readonly languages: string[];
|
|
130
|
+
readonly createdAt: string | null;
|
|
131
|
+
readonly glossaristVersion: string | null;
|
|
132
|
+
readonly schemaVersion: string;
|
|
133
|
+
readonly homepage: string | null;
|
|
134
|
+
readonly repository: string | null;
|
|
135
|
+
readonly license: string | null;
|
|
136
|
+
readonly uriPrefix: string | null;
|
|
137
|
+
readonly conceptUriTemplate: string | null;
|
|
138
|
+
readonly compiledFormats: string[];
|
|
139
|
+
readonly statistics: GcrStatistics | null;
|
|
140
|
+
readonly concept_count: number;
|
|
141
|
+
readonly created_at: string | null;
|
|
142
|
+
readonly glossarist_version: string | null;
|
|
143
|
+
readonly schema_version: string;
|
|
144
|
+
readonly compiled_formats: string[];
|
|
145
|
+
static fromYaml(yamlString: string): GcrMetadata;
|
|
146
|
+
static fromJSON(data: Record<string, unknown>): GcrMetadata;
|
|
147
|
+
}
|
package/src/models/index.js
CHANGED
|
@@ -8,3 +8,5 @@ export { RelatedConcept, RELATIONSHIP_TYPES } from './related-concept.js';
|
|
|
8
8
|
export { ConceptDate, DATE_TYPES } from './concept-date.js';
|
|
9
9
|
export { DetailedDefinition } from './detailed-definition.js';
|
|
10
10
|
export { NonVerbRep } from './non-verb-rep.js';
|
|
11
|
+
export { GcrMetadata } from './gcr-metadata.js';
|
|
12
|
+
export { GcrStatistics } from './gcr-statistics.js';;
|
package/src/sort.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const NATURAL_SORT_RE = /(\d+|\D+)/g;
|
|
2
|
+
const DIGIT_RE = /^\d+$/;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Natural sort comparator for concept IDs like "3.1.1.1", "551-12-39".
|
|
6
|
+
* @param {string} a
|
|
7
|
+
* @param {string} b
|
|
8
|
+
* @returns {number}
|
|
9
|
+
*/
|
|
10
|
+
export function naturalSort(a, b) {
|
|
11
|
+
const pa = a.match(NATURAL_SORT_RE) || [];
|
|
12
|
+
const pb = b.match(NATURAL_SORT_RE) || [];
|
|
13
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
14
|
+
const na = pa[i] || '';
|
|
15
|
+
const nb = pb[i] || '';
|
|
16
|
+
if (DIGIT_RE.test(na) && DIGIT_RE.test(nb)) {
|
|
17
|
+
const diff = parseInt(na, 10) - parseInt(nb, 10);
|
|
18
|
+
if (diff !== 0) return diff;
|
|
19
|
+
} else {
|
|
20
|
+
const cmp = na.localeCompare(nb);
|
|
21
|
+
if (cmp !== 0) return cmp;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
import { DATASET_ASSETS } from '../dataset-asset.js';
|
|
3
|
+
import { ValidationResult } from './validation-result.js';
|
|
4
|
+
|
|
5
|
+
export class GcrValidator {
|
|
6
|
+
async validate(pkg) {
|
|
7
|
+
const result = new ValidationResult();
|
|
8
|
+
await this._validateMetadata(pkg, result);
|
|
9
|
+
await this._validateConcepts(pkg, result);
|
|
10
|
+
await this._validateAssets(pkg, result);
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async _validateMetadata(pkg, result) {
|
|
15
|
+
const raw = await pkg._readText('metadata.yaml');
|
|
16
|
+
if (!raw) {
|
|
17
|
+
result.addError('metadata.yaml is missing');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let meta;
|
|
22
|
+
try {
|
|
23
|
+
meta = yaml.load(raw);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
result.addError(`metadata.yaml: invalid YAML: ${e.message}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!meta.shortname) result.addError('metadata.yaml missing shortname');
|
|
30
|
+
if (!meta.version) result.addError('metadata.yaml missing version');
|
|
31
|
+
if (meta.concept_count == null) result.addError('metadata.yaml missing concept_count');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async _validateConcepts(pkg, result) {
|
|
35
|
+
const ids = await pkg.conceptIds();
|
|
36
|
+
if (ids.length === 0) {
|
|
37
|
+
result.addError('No concept files found in concepts/');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async _validateAssets(pkg, result) {
|
|
42
|
+
for (const asset of DATASET_ASSETS) {
|
|
43
|
+
if (asset.type === 'file') {
|
|
44
|
+
await this._validateFileAsset(pkg, asset.path, result);
|
|
45
|
+
} else if (asset.type === 'directory') {
|
|
46
|
+
await this._validateDirectoryAsset(pkg, asset.path, result);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async _validateFileAsset(pkg, path, result) {
|
|
52
|
+
const raw = await pkg._readText(path);
|
|
53
|
+
if (!raw) return;
|
|
54
|
+
try {
|
|
55
|
+
yaml.load(raw);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
result.addError(`${path}: invalid YAML at line ${e.mark?.line ?? '?'}: ${e.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async _validateDirectoryAsset(pkg, dirPath, result) {
|
|
62
|
+
let hasFiles = false;
|
|
63
|
+
let hasEntries = false;
|
|
64
|
+
pkg._zip.forEach((relativePath, entry) => {
|
|
65
|
+
if (relativePath.startsWith(`${dirPath}/`)) {
|
|
66
|
+
hasEntries = true;
|
|
67
|
+
if (!entry.dir) hasFiles = true;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (hasEntries && !hasFiles) {
|
|
71
|
+
result.addWarning(`${dirPath}/ directory exists but is empty`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/validators/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export { ValidationError } from './validation-error.js';
|
|
2
2
|
export { ValidationRule } from './validation-rule.js';
|
|
3
|
+
export { ValidationResult } from './validation-result.js';
|
|
3
4
|
export { ConceptValidator, LanguageCodeRule, DesignationTypeRule, EntryStatusRule } from './concept-validator.js';
|
|
4
5
|
export { RegisterValidator } from './register-validator.js';
|
|
6
|
+
export { GcrValidator } from './gcr-validator.js';
|
|
5
7
|
|
|
6
8
|
import { ConceptValidator, LanguageCodeRule, DesignationTypeRule, EntryStatusRule } from './concept-validator.js';
|
|
7
9
|
import { RegisterValidator } from './register-validator.js';
|
|
10
|
+
import { GcrValidator } from './gcr-validator.js';
|
|
8
11
|
|
|
9
12
|
const _default = new ConceptValidator()
|
|
10
13
|
.addRule(new LanguageCodeRule())
|
|
@@ -22,3 +25,7 @@ export function createConceptValidator() {
|
|
|
22
25
|
export function validateRegister(register) {
|
|
23
26
|
return new RegisterValidator().validate(register);
|
|
24
27
|
}
|
|
28
|
+
|
|
29
|
+
export async function validateGcrPackage(pkg) {
|
|
30
|
+
return new GcrValidator().validate(pkg);
|
|
31
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class ValidationResult {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.errors = [];
|
|
4
|
+
this.warnings = [];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
get valid() {
|
|
8
|
+
return this.errors.length === 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
addError(message) {
|
|
12
|
+
this.errors.push(message);
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
addWarning(message) {
|
|
17
|
+
this.warnings.push(message);
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
merge(other) {
|
|
22
|
+
for (const e of other.errors) this.errors.push(e);
|
|
23
|
+
for (const w of other.warnings) this.warnings.push(w);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
toJSON() {
|
|
28
|
+
return {
|
|
29
|
+
valid: this.valid,
|
|
30
|
+
errors: [...this.errors],
|
|
31
|
+
warnings: [...this.warnings],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|