@glossarist/concept-browser 0.7.14 → 0.7.15
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.15",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/scripts/build-edges.js
CHANGED
|
@@ -43,7 +43,8 @@ function extractReferences(concept, registerId) {
|
|
|
43
43
|
return edges;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function extractDomains(concept, registerId) {
|
|
46
|
+
function extractDomains(concept, registerId, uriBase) {
|
|
47
|
+
const base = uriBase || 'https://glossarist.org';
|
|
47
48
|
const edges = [];
|
|
48
49
|
const sourceUri = concept['@id'];
|
|
49
50
|
const lcs = concept['gl:localizedConcept'] || {};
|
|
@@ -55,7 +56,7 @@ function extractDomains(concept, registerId) {
|
|
|
55
56
|
seen.add(domain);
|
|
56
57
|
edges.push({
|
|
57
58
|
source: sourceUri,
|
|
58
|
-
target:
|
|
59
|
+
target: `${base}/${registerId}/domain/${slugify(domain)}`,
|
|
59
60
|
type: 'domain',
|
|
60
61
|
label: domain,
|
|
61
62
|
register: registerId,
|
|
@@ -65,15 +66,38 @@ function extractDomains(concept, registerId) {
|
|
|
65
66
|
return edges;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
function extractRelated(concept, registerId, uriBase, urnMap) {
|
|
70
|
+
const edges = [];
|
|
71
|
+
const sourceUri = concept['@id'];
|
|
72
|
+
for (const r of concept['gl:related'] || []) {
|
|
73
|
+
const ref = r['gl:ref'];
|
|
74
|
+
if (!ref) continue;
|
|
75
|
+
const source = ref['gl:source'] || ref['source'];
|
|
76
|
+
const id = ref['gl:id'] || ref['id'];
|
|
77
|
+
if (!source || !id) continue;
|
|
78
|
+
const reg = urnMap.get(source) || source;
|
|
79
|
+
const target = `${uriBase}/${reg}/concept/${id}`;
|
|
80
|
+
if (target === sourceUri) continue;
|
|
81
|
+
edges.push({
|
|
82
|
+
source: sourceUri,
|
|
83
|
+
target,
|
|
84
|
+
type: r['gl:relationshipType'] || 'references',
|
|
85
|
+
label: r['gl:term'] || undefined,
|
|
86
|
+
register: reg,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return edges;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const EXTRACTORS = [extractReferences, extractRelated, extractDomains];
|
|
69
93
|
|
|
70
|
-
function extractAllEdges(concept, registerId) {
|
|
71
|
-
return EXTRACTORS.flatMap(fn => fn(concept, registerId));
|
|
94
|
+
function extractAllEdges(concept, registerId, uriBase, urnMap) {
|
|
95
|
+
return EXTRACTORS.flatMap(fn => fn(concept, registerId, uriBase, urnMap));
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
// --- Build ---
|
|
75
99
|
|
|
76
|
-
function buildEdgesForDataset(datasetDir, registerId) {
|
|
100
|
+
function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap) {
|
|
77
101
|
const conceptsDir = join(datasetDir, 'concepts');
|
|
78
102
|
if (!existsSync(conceptsDir)) {
|
|
79
103
|
console.log(` Skipping ${registerId}: no concepts directory`);
|
|
@@ -90,7 +114,7 @@ function buildEdgesForDataset(datasetDir, registerId) {
|
|
|
90
114
|
for (const file of files) {
|
|
91
115
|
try {
|
|
92
116
|
const data = JSON.parse(readFileSync(join(conceptsDir, file), 'utf-8'));
|
|
93
|
-
const edges = extractAllEdges(data, registerId);
|
|
117
|
+
const edges = extractAllEdges(data, registerId, uriBase, urnMap);
|
|
94
118
|
allEdges.push(...edges);
|
|
95
119
|
|
|
96
120
|
for (const edge of edges) {
|
|
@@ -170,12 +194,30 @@ const datasets = readdirSync(DATA_DIR).filter(f => {
|
|
|
170
194
|
}
|
|
171
195
|
});
|
|
172
196
|
|
|
197
|
+
// Build URN→datasetId map from all manifests
|
|
198
|
+
const urnMap = new Map();
|
|
199
|
+
const manifestCache = new Map();
|
|
173
200
|
for (const ds of datasets) {
|
|
174
201
|
const manifestPath = join(DATA_DIR, ds, 'manifest.json');
|
|
175
202
|
try {
|
|
176
203
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
204
|
+
manifestCache.set(ds, manifest);
|
|
205
|
+
if (manifest.datasetUri) urnMap.set(manifest.datasetUri, ds);
|
|
206
|
+
for (const alias of manifest.uriAliases ?? []) {
|
|
207
|
+
const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
|
|
208
|
+
if (base.startsWith('urn:')) urnMap.set(base, ds);
|
|
209
|
+
}
|
|
210
|
+
} catch {}
|
|
211
|
+
}
|
|
212
|
+
console.log(`URN resolution map: ${[...urnMap.entries()].map(([k,v]) => `${k}→${v}`).join(', ')}\n`);
|
|
213
|
+
|
|
214
|
+
for (const ds of datasets) {
|
|
215
|
+
const manifest = manifestCache.get(ds);
|
|
216
|
+
if (!manifest) continue;
|
|
217
|
+
try {
|
|
177
218
|
console.log(`${manifest.title} (${ds}):`);
|
|
178
|
-
|
|
219
|
+
const uriBase = manifest.uriBase || 'https://glossarist.org';
|
|
220
|
+
buildEdgesForDataset(join(DATA_DIR, ds), ds, uriBase, urnMap);
|
|
179
221
|
} catch (e) {
|
|
180
222
|
console.error(`Error reading manifest for ${ds}: ${e.message}`);
|
|
181
223
|
}
|
|
@@ -851,6 +851,18 @@ for (let i = 0; i < config.datasets.length; i++) {
|
|
|
851
851
|
}
|
|
852
852
|
writeJson(path.join(PUBLIC, 'datasets.json'), registry);
|
|
853
853
|
|
|
854
|
+
// Clean stale dataset directories not referenced in config
|
|
855
|
+
const configuredIds = new Set(config.datasets.map(d => d.id));
|
|
856
|
+
if (fs.existsSync(DATA)) {
|
|
857
|
+
for (const entry of fs.readdirSync(DATA)) {
|
|
858
|
+
if (!configuredIds.has(entry)) {
|
|
859
|
+
const stalePath = path.join(DATA, entry);
|
|
860
|
+
fs.rmSync(stalePath, { recursive: true, force: true });
|
|
861
|
+
console.log(` Removed stale data directory: ${entry}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
854
866
|
// Generate routing.json from site config
|
|
855
867
|
writeJson(path.join(PUBLIC, 'routing.json'), config.routing || []);
|
|
856
868
|
console.log('Generated routing.json');
|
|
@@ -386,6 +386,37 @@ describe('DatasetAdapter', () => {
|
|
|
386
386
|
expect(edges[0].target).toBe('https://glossarist.org/isotc204/concept/3.1.1.6');
|
|
387
387
|
expect(edges[0].label).toBe('entity');
|
|
388
388
|
});
|
|
389
|
+
|
|
390
|
+
it('resolves URN-based ref.source to dataset ID via urnMap', () => {
|
|
391
|
+
const urnMap = new Map([
|
|
392
|
+
['urn:oiml:pub:v:1:2000', 'viml-2000'],
|
|
393
|
+
]);
|
|
394
|
+
adapter.setUrnMap(urnMap);
|
|
395
|
+
|
|
396
|
+
const concept = conceptFromJson({
|
|
397
|
+
'@id': 'https://metanorma.github.io/oiml-viml/viml-2022/concept/2.23',
|
|
398
|
+
'@type': 'gl:Concept',
|
|
399
|
+
'gl:localizedConcept': {
|
|
400
|
+
eng: {
|
|
401
|
+
'gl:languageCode': 'eng',
|
|
402
|
+
'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'measurement' }],
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
'gl:related': [
|
|
406
|
+
{
|
|
407
|
+
'gl:relationshipType': 'supersedes',
|
|
408
|
+
'gl:ref': { 'gl:source': 'urn:oiml:pub:v:1:2000', 'gl:id': '2.23' },
|
|
409
|
+
'gl:term': 'measurement',
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
adapter.manifest = { uriBase: 'https://metanorma.github.io/oiml-viml' } as any;
|
|
415
|
+
const edges = adapter.extractEdges(concept);
|
|
416
|
+
expect(edges.length).toBe(1);
|
|
417
|
+
expect(edges[0].target).toBe('https://metanorma.github.io/oiml-viml/viml-2000/concept/2.23');
|
|
418
|
+
expect(edges[0].type).toBe('supersedes');
|
|
419
|
+
});
|
|
389
420
|
});
|
|
390
421
|
|
|
391
422
|
describe('extractDomainEdges', () => {
|
|
@@ -15,11 +15,14 @@ function slugify(text: string): string {
|
|
|
15
15
|
return text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s/]+/g, '-');
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function resolveRefTarget(rc: any, uriBase: string, registerId: string): string {
|
|
18
|
+
function resolveRefTarget(rc: any, uriBase: string, registerId: string, urnMap?: ReadonlyMap<string, string>): string {
|
|
19
19
|
if (!rc.ref) return '';
|
|
20
20
|
const ref = rc.ref;
|
|
21
21
|
if (ref.id) {
|
|
22
|
-
|
|
22
|
+
let reg = registerId;
|
|
23
|
+
if (ref.source && !ref.source.startsWith('http')) {
|
|
24
|
+
reg = urnMap?.get(ref.source) ?? ref.source;
|
|
25
|
+
}
|
|
23
26
|
return `${uriBase}/${reg}/concept/${ref.id}`;
|
|
24
27
|
}
|
|
25
28
|
if (ref.source && ref.source.startsWith('http')) return ref.source;
|
|
@@ -28,6 +31,7 @@ function resolveRefTarget(rc: any, uriBase: string, registerId: string): string
|
|
|
28
31
|
|
|
29
32
|
export class DatasetAdapter {
|
|
30
33
|
private positionIndex = new Map<string, number>();
|
|
34
|
+
private _urnMap: ReadonlyMap<string, string> = new Map();
|
|
31
35
|
readonly registerId: string;
|
|
32
36
|
private baseUrl: string;
|
|
33
37
|
manifest: Manifest | null = null;
|
|
@@ -294,6 +298,10 @@ export class DatasetAdapter {
|
|
|
294
298
|
return scored;
|
|
295
299
|
}
|
|
296
300
|
|
|
301
|
+
setUrnMap(map: ReadonlyMap<string, string>): void {
|
|
302
|
+
this._urnMap = map;
|
|
303
|
+
}
|
|
304
|
+
|
|
297
305
|
extractEdges(concept: Concept): GraphEdge[] {
|
|
298
306
|
const edges: GraphEdge[] = [];
|
|
299
307
|
const uriBase = this.manifest?.uriBase || 'https://glossarist.org';
|
|
@@ -301,7 +309,7 @@ export class DatasetAdapter {
|
|
|
301
309
|
|
|
302
310
|
// Managed concept level relationships
|
|
303
311
|
for (const rc of concept.relatedConcepts) {
|
|
304
|
-
const target = resolveRefTarget(rc, uriBase, this.registerId);
|
|
312
|
+
const target = resolveRefTarget(rc, uriBase, this.registerId, this._urnMap);
|
|
305
313
|
if (target && target !== sourceUri) {
|
|
306
314
|
const parsed = UriRouter.parseUri(target);
|
|
307
315
|
edges.push({
|
|
@@ -319,7 +327,7 @@ export class DatasetAdapter {
|
|
|
319
327
|
const lc = concept.localization(lang);
|
|
320
328
|
if (!lc) continue;
|
|
321
329
|
for (const rc of lc.related) {
|
|
322
|
-
const target = resolveRefTarget(rc, uriBase, this.registerId);
|
|
330
|
+
const target = resolveRefTarget(rc, uriBase, this.registerId, this._urnMap);
|
|
323
331
|
if (target && target !== sourceUri) {
|
|
324
332
|
const parsed = UriRouter.parseUri(target);
|
|
325
333
|
edges.push({
|
package/src/adapters/factory.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { ReferenceResolver } from './ReferenceResolver';
|
|
|
6
6
|
|
|
7
7
|
export class AdapterFactory {
|
|
8
8
|
private adapters = new Map<string, DatasetAdapter>();
|
|
9
|
+
private urnMap = new Map<string, string>();
|
|
9
10
|
readonly router = new UriRouter();
|
|
10
11
|
readonly resolver: ReferenceResolver;
|
|
11
12
|
|
|
@@ -62,6 +63,18 @@ export class AdapterFactory {
|
|
|
62
63
|
this.resolver.registerSourceRef(alias, registerId, manifest.datasetUri);
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
// Build URN→datasetId map from manifest
|
|
67
|
+
if (manifest.datasetUri) this.urnMap.set(manifest.datasetUri, registerId);
|
|
68
|
+
for (const alias of manifest.uriAliases ?? []) {
|
|
69
|
+
const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
|
|
70
|
+
if (base.startsWith('urn:')) this.urnMap.set(base, registerId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Distribute the updated URN map to all loaded adapters
|
|
74
|
+
for (const adapter of this.adapters.values()) {
|
|
75
|
+
adapter.setUrnMap(this.urnMap);
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
return adapter;
|
|
66
79
|
}
|
|
67
80
|
|
|
@@ -156,8 +156,22 @@ function mapSourceFromJsonLd(s: any): Record<string, unknown> {
|
|
|
156
156
|
|
|
157
157
|
function mapRelatedFromJsonLd(r: any): Record<string, unknown> {
|
|
158
158
|
const result: Record<string, unknown> = { type: 'references' };
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
|
|
160
|
+
if (r['gl:relationshipType']) {
|
|
161
|
+
result.type = r['gl:relationshipType'];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (r['gl:ref']) {
|
|
165
|
+
const ref = r['gl:ref'];
|
|
166
|
+
const refObj: Record<string, unknown> = {};
|
|
167
|
+
if (ref['gl:source']) refObj.source = ref['gl:source'];
|
|
168
|
+
if (ref['gl:id']) refObj.id = ref['gl:id'];
|
|
169
|
+
if (ref['source']) refObj.source = ref['source'];
|
|
170
|
+
if (ref['id']) refObj.id = ref['id'];
|
|
171
|
+
if (Object.keys(refObj).length > 0) result.ref = refObj;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!result.ref && r['@id']) {
|
|
161
175
|
const uri = r['@id'] as string;
|
|
162
176
|
const idMatch = uri.match(/\/concept\/([^/]+)$/);
|
|
163
177
|
result.ref = idMatch
|
|
@@ -237,7 +251,7 @@ function conceptFromJsonLd(doc: Record<string, any>): Concept {
|
|
|
237
251
|
}
|
|
238
252
|
}
|
|
239
253
|
|
|
240
|
-
const related = (doc['gl:
|
|
254
|
+
const related = (doc['gl:related'] ?? []).map(mapRelatedFromJsonLd);
|
|
241
255
|
const tags = Array.isArray(doc['gl:tags']) ? [...doc['gl:tags']] : [];
|
|
242
256
|
|
|
243
257
|
return Concept.fromJSON({
|