@glossarist/concept-browser 0.7.31 → 0.7.32
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/scripts/build-edges.js +63 -4
- package/src/__tests__/designation-relationship.test.ts +4 -4
- package/src/__tests__/factory-lazy.test.ts +6 -6
- package/src/__tests__/source-refs.test.ts +180 -0
- package/src/adapters/factory.ts +18 -0
- package/src/adapters/model-bridge.ts +9 -7
- package/src/components/DesignationList.vue +10 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.32",
|
|
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
|
@@ -23,6 +23,32 @@ function slugify(text) {
|
|
|
23
23
|
|
|
24
24
|
// --- Extractors (open/closed: add new extractors to EXTRACTORS array) ---
|
|
25
25
|
|
|
26
|
+
function extractSourceRefs(concept, registerId) {
|
|
27
|
+
const refs = new Set();
|
|
28
|
+
|
|
29
|
+
// Managed concept-level sources
|
|
30
|
+
for (const src of concept['gl:source'] || []) {
|
|
31
|
+
const origin = src['gl:origin'];
|
|
32
|
+
if (origin) {
|
|
33
|
+
const ref = origin['gl:ref'];
|
|
34
|
+
if (ref?.['gl:source']) refs.add(ref['gl:source']);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Localized concept-level sources
|
|
39
|
+
for (const lc of Object.values(concept['gl:localizedConcept'] || {})) {
|
|
40
|
+
for (const src of lc['gl:source'] || []) {
|
|
41
|
+
const origin = src['gl:origin'];
|
|
42
|
+
if (origin) {
|
|
43
|
+
const ref = origin['gl:ref'];
|
|
44
|
+
if (ref?.['gl:source']) refs.add(ref['gl:source']);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return [...refs].map(source => ({ source, registerId }));
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
function extractReferences(concept, registerId) {
|
|
27
53
|
const edges = [];
|
|
28
54
|
const sourceUri = concept['@id'];
|
|
@@ -129,13 +155,14 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
129
155
|
const conceptsDir = join(datasetDir, 'concepts');
|
|
130
156
|
if (!existsSync(conceptsDir)) {
|
|
131
157
|
console.log(` Skipping ${registerId}: no concepts directory`);
|
|
132
|
-
return [];
|
|
158
|
+
return { edges: [], sourceRefs: [] };
|
|
133
159
|
}
|
|
134
160
|
|
|
135
161
|
const files = readdirSync(conceptsDir).filter(f => f.endsWith('.json'));
|
|
136
162
|
console.log(` Processing ${files.length} concepts...`);
|
|
137
163
|
|
|
138
164
|
const allEdges = [];
|
|
165
|
+
const allSourceRefs = [];
|
|
139
166
|
const domainConceptCount = new Map();
|
|
140
167
|
let processed = 0;
|
|
141
168
|
|
|
@@ -144,6 +171,7 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
144
171
|
const data = JSON.parse(readFileSync(join(conceptsDir, file), 'utf-8'));
|
|
145
172
|
const edges = extractAllEdges(data, registerId, uriBase, urnMap);
|
|
146
173
|
allEdges.push(...edges);
|
|
174
|
+
allSourceRefs.push(...extractSourceRefs(data, registerId));
|
|
147
175
|
|
|
148
176
|
for (const edge of edges) {
|
|
149
177
|
if (edge.type === 'domain' || edge.type === 'section') {
|
|
@@ -241,7 +269,7 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
241
269
|
console.log(` Written ${domainNodes.length} edge-derived domain nodes to domain-nodes.json`);
|
|
242
270
|
}
|
|
243
271
|
|
|
244
|
-
return deduped;
|
|
272
|
+
return { edges: deduped, sourceRefs: allSourceRefs };
|
|
245
273
|
}
|
|
246
274
|
|
|
247
275
|
// Main
|
|
@@ -278,6 +306,7 @@ for (const ds of datasets) {
|
|
|
278
306
|
console.log(`URN resolution map: ${[...urnMap.entries()].map(([k,v]) => `${k}→${v}`).join(', ')}\n`);
|
|
279
307
|
|
|
280
308
|
const allDatasetEdges = new Map();
|
|
309
|
+
const allSourceRefs = [];
|
|
281
310
|
|
|
282
311
|
for (const ds of datasets) {
|
|
283
312
|
const manifest = manifestCache.get(ds);
|
|
@@ -285,14 +314,44 @@ for (const ds of datasets) {
|
|
|
285
314
|
try {
|
|
286
315
|
console.log(`${manifest.title} (${ds}):`);
|
|
287
316
|
const uriBase = manifest.uriBase || 'https://glossarist.org';
|
|
288
|
-
const
|
|
289
|
-
allDatasetEdges.set(ds, edges);
|
|
317
|
+
const result = buildEdgesForDataset(join(DATA_DIR, ds), ds, uriBase, urnMap, manifest);
|
|
318
|
+
allDatasetEdges.set(ds, result.edges);
|
|
319
|
+
allSourceRefs.push(...result.sourceRefs);
|
|
290
320
|
} catch (e) {
|
|
291
321
|
console.error(`Error reading manifest for ${ds}: ${e.message}`);
|
|
292
322
|
}
|
|
293
323
|
console.log();
|
|
294
324
|
}
|
|
295
325
|
|
|
326
|
+
// Build source-refs index: maps every source string to its dataset ID.
|
|
327
|
+
// Uses manifest ref/refAliases as authoritative keys, augmented by
|
|
328
|
+
// actual source strings found in concept data.
|
|
329
|
+
const sourceRefMap = {};
|
|
330
|
+
|
|
331
|
+
// Seed from manifests (authoritative)
|
|
332
|
+
for (const [ds, manifest] of manifestCache) {
|
|
333
|
+
if (manifest.ref) sourceRefMap[manifest.ref] = ds;
|
|
334
|
+
for (const alias of manifest.refAliases ?? []) {
|
|
335
|
+
sourceRefMap[alias] = ds;
|
|
336
|
+
}
|
|
337
|
+
if (manifest.datasetUri) sourceRefMap[manifest.datasetUri] = ds;
|
|
338
|
+
for (const alias of manifest.uriAliases ?? []) {
|
|
339
|
+
const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
|
|
340
|
+
sourceRefMap[base] = ds;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Augment with actual source strings from concepts
|
|
345
|
+
for (const { source, registerId } of allSourceRefs) {
|
|
346
|
+
if (!sourceRefMap[source]) {
|
|
347
|
+
sourceRefMap[source] = registerId;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const sourceRefPath = join(DATA_DIR, 'source-refs.json');
|
|
352
|
+
writeFileSync(sourceRefPath, JSON.stringify(sourceRefMap));
|
|
353
|
+
console.log(`Written source-refs.json (${Object.keys(sourceRefMap).length} source references across ${datasets.length} datasets)\n`);
|
|
354
|
+
|
|
296
355
|
// Build cross-reference index: for each dataset, which other datasets'
|
|
297
356
|
// edges.json contains edges targeting that dataset's URIs.
|
|
298
357
|
const datasetUriPrefixes = new Map();
|
|
@@ -42,7 +42,7 @@ describe('designation relationship bridge', () => {
|
|
|
42
42
|
|
|
43
43
|
const rc = term.related[0];
|
|
44
44
|
expect(rc.type).toBe('abbreviated_form_for');
|
|
45
|
-
expect(getDesignationTarget(rc)).toBe('Portable Document Format');
|
|
45
|
+
expect(getDesignationTarget(rc as any)).toBe('Portable Document Format');
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
it('returns null for concept-level relationships (no target)', () => {
|
|
@@ -50,7 +50,7 @@ describe('designation relationship bridge', () => {
|
|
|
50
50
|
const concept = conceptFromJson(doc);
|
|
51
51
|
const rc = concept.localization('eng')!.terms[0].related[0];
|
|
52
52
|
expect(rc.type).toBe('abbreviated_form_for');
|
|
53
|
-
expect(getDesignationTarget(rc)).toBeNull();
|
|
53
|
+
expect(getDesignationTarget(rc as any)).toBeNull();
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it('handles short_form_for designation target', () => {
|
|
@@ -58,7 +58,7 @@ describe('designation relationship bridge', () => {
|
|
|
58
58
|
const concept = conceptFromJson(doc);
|
|
59
59
|
const rc = concept.localization('eng')!.terms[0].related[0];
|
|
60
60
|
expect(rc.type).toBe('short_form_for');
|
|
61
|
-
expect(getDesignationTarget(rc)).toBe('kilogram');
|
|
61
|
+
expect(getDesignationTarget(rc as any)).toBe('kilogram');
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
it('preserves designation target from glossarist native format', () => {
|
|
@@ -78,7 +78,7 @@ describe('designation relationship bridge', () => {
|
|
|
78
78
|
};
|
|
79
79
|
const concept = conceptFromJson(doc);
|
|
80
80
|
const rc = concept.localization('eng')!.terms[0].related[0];
|
|
81
|
-
expect(getDesignationTarget(rc)).toBe('Portable Document Format');
|
|
81
|
+
expect(getDesignationTarget(rc as any)).toBe('Portable Document Format');
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
it('designation without related returns empty related array', () => {
|
|
@@ -56,8 +56,8 @@ describe('AdapterFactory — lazy discovery', () => {
|
|
|
56
56
|
expect(adapters[0].manifest).not.toBeNull();
|
|
57
57
|
expect(adapters[0].manifest!.title).toBe('Dataset 1');
|
|
58
58
|
expect(adapters[0].manifest!.conceptCount).toBe(100);
|
|
59
|
-
// Should NOT fetch manifest.json — only datasets.json
|
|
60
|
-
expect(mockFetch).toHaveBeenCalledTimes(
|
|
59
|
+
// Should NOT fetch manifest.json — only datasets.json + source-refs.json
|
|
60
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
61
61
|
expect(mockFetch).toHaveBeenCalledWith('/datasets.json');
|
|
62
62
|
});
|
|
63
63
|
|
|
@@ -89,8 +89,8 @@ describe('AdapterFactory — lazy discovery', () => {
|
|
|
89
89
|
|
|
90
90
|
expect(adapters.length).toBe(1);
|
|
91
91
|
expect(adapters[0].manifest!.title).toBe('Full Dataset');
|
|
92
|
-
// Should fetch
|
|
93
|
-
expect(mockFetch).toHaveBeenCalledTimes(
|
|
92
|
+
// Should fetch datasets.json, manifest.json, and source-refs.json
|
|
93
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
it('loads full manifest in loadDataset after summary discovery', async () => {
|
|
@@ -140,10 +140,10 @@ describe('AdapterFactory — lazy discovery', () => {
|
|
|
140
140
|
return Promise.resolve({ ok: false, status: 404 } as Response);
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
// Discover with summary — no manifest fetch
|
|
143
|
+
// Discover with summary — no manifest fetch (datasets.json + source-refs.json)
|
|
144
144
|
const adapters = await factory.discoverDatasets('/datasets.json');
|
|
145
145
|
expect(adapters[0].manifest!.title).toBe('Summary Title');
|
|
146
|
-
expect(mockFetch).toHaveBeenCalledTimes(
|
|
146
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
147
147
|
|
|
148
148
|
// Load full dataset — fetches full manifest + index
|
|
149
149
|
const loaded = await factory.loadDataset('ds1');
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { ReferenceResolver } from '../adapters/ReferenceResolver';
|
|
3
|
+
import type { Resolution } from '../adapters/types';
|
|
4
|
+
|
|
5
|
+
type InternalResolution = Extract<Resolution, { type: 'internal' }>;
|
|
6
|
+
|
|
7
|
+
function asInternal(r: Resolution | null): InternalResolution | null {
|
|
8
|
+
return r?.type === 'internal' ? (r as InternalResolution) : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('Source reference resolution (citation linking)', () => {
|
|
12
|
+
let resolver: ReferenceResolver;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
resolver = new ReferenceResolver();
|
|
16
|
+
// Register datasets with URI patterns
|
|
17
|
+
resolver.registerDataset('vim-2012', ['urn:oiml:pub:v:2:2012*']);
|
|
18
|
+
resolver.registerDataset('viml-2022', ['urn:oiml:pub:v:1:2022*']);
|
|
19
|
+
resolver.registerDataset('vim-2007', ['urn:oiml:pub:v:2:2007*']);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('registerSourceRef', () => {
|
|
23
|
+
it('resolves citation when source ref matches exactly', () => {
|
|
24
|
+
resolver.registerSourceRef('OIML V 2-200:2012', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
25
|
+
const result = resolver.resolveCitation('OIML V 2-200:2012', '2.2', 'viml-2022');
|
|
26
|
+
expect(result).toEqual({
|
|
27
|
+
type: 'internal',
|
|
28
|
+
registerId: 'vim-2012',
|
|
29
|
+
conceptId: '2.2',
|
|
30
|
+
crossDataset: true,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('resolves citation using variant source string', () => {
|
|
35
|
+
// Manifest has "OIML V 2-200:2012" but concepts use "OIML V2-200:2012"
|
|
36
|
+
resolver.registerSourceRef('OIML V 2-200:2012', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
37
|
+
resolver.registerSourceRef('OIML V2-200:2012', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
38
|
+
|
|
39
|
+
const result = resolver.resolveCitation('OIML V2-200:2012', '2.2', 'viml-2022');
|
|
40
|
+
expect(result).toEqual({
|
|
41
|
+
type: 'internal',
|
|
42
|
+
registerId: 'vim-2012',
|
|
43
|
+
conceptId: '2.2',
|
|
44
|
+
crossDataset: true,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('resolves citation using URN as source', () => {
|
|
49
|
+
resolver.registerSourceRef('urn:oiml:pub:v:2:2012', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
50
|
+
const result = resolver.resolveCitation('urn:oiml:pub:v:2:2012', '2.2', 'viml-2022');
|
|
51
|
+
expect(result).toEqual({
|
|
52
|
+
type: 'internal',
|
|
53
|
+
registerId: 'vim-2012',
|
|
54
|
+
conceptId: '2.2',
|
|
55
|
+
crossDataset: true,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('resolves citation using ref alias (e.g. "VIM")', () => {
|
|
60
|
+
resolver.registerSourceRef('VIM', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
61
|
+
const result = resolver.resolveCitation('VIM', '2.2', 'viml-2022');
|
|
62
|
+
expect(result).toEqual({
|
|
63
|
+
type: 'internal',
|
|
64
|
+
registerId: 'vim-2012',
|
|
65
|
+
conceptId: '2.2',
|
|
66
|
+
crossDataset: true,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns null when source ref is not registered', () => {
|
|
71
|
+
const result = resolver.resolveCitation('Unknown Source', '2.2', 'viml-2022');
|
|
72
|
+
expect(result).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns null when source ref matches but concept does not exist in dataset', () => {
|
|
76
|
+
resolver.registerSourceRef('OIML V 2-200:2012', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
77
|
+
// The resolver can only check if the dataset exists, not if the concept exists
|
|
78
|
+
// It will still return internal if the dataset is registered
|
|
79
|
+
const result = resolver.resolveCitation('OIML V 2-200:2012', '99.99', 'viml-2022');
|
|
80
|
+
expect(result?.type).toBe('internal');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('handles same-dataset citation (crossDataset=false)', () => {
|
|
84
|
+
resolver.registerSourceRef('OIML V 2-200:2012', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
85
|
+
const result = resolver.resolveCitation('OIML V 2-200:2012', '2.2', 'vim-2012');
|
|
86
|
+
expect(result).toEqual({
|
|
87
|
+
type: 'internal',
|
|
88
|
+
registerId: 'vim-2012',
|
|
89
|
+
conceptId: '2.2',
|
|
90
|
+
crossDataset: false,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('resolveCitation fallback to URN', () => {
|
|
96
|
+
it('resolves URN-based source without registerSourceRef', () => {
|
|
97
|
+
// When source starts with "urn:", tryResolveCitationUri handles it
|
|
98
|
+
const result = resolver.resolveCitation('urn:oiml:pub:v:2:2012', '2.2', 'viml-2022');
|
|
99
|
+
expect(result).toEqual({
|
|
100
|
+
type: 'internal',
|
|
101
|
+
registerId: 'vim-2012',
|
|
102
|
+
conceptId: '2.2',
|
|
103
|
+
crossDataset: true,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('multiple source strings for same dataset', () => {
|
|
109
|
+
it('registers multiple source refs pointing to the same dataset', () => {
|
|
110
|
+
resolver.registerSourceRef('OIML V 2-200:2012', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
111
|
+
resolver.registerSourceRef('OIML V2-200:2012', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
112
|
+
resolver.registerSourceRef('VIM', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
113
|
+
resolver.registerSourceRef('ISO Guide 99:2007', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
114
|
+
|
|
115
|
+
expect(asInternal(resolver.resolveCitation('OIML V 2-200:2012', '2.2'))?.registerId).toBe('vim-2012');
|
|
116
|
+
expect(asInternal(resolver.resolveCitation('OIML V2-200:2012', '2.2'))?.registerId).toBe('vim-2012');
|
|
117
|
+
expect(asInternal(resolver.resolveCitation('VIM', '2.2'))?.registerId).toBe('vim-2012');
|
|
118
|
+
expect(asInternal(resolver.resolveCitation('ISO Guide 99:2007', '2.2'))?.registerId).toBe('vim-2012');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('ISO source references', () => {
|
|
123
|
+
it('resolves ISO/IEC references when registered', () => {
|
|
124
|
+
resolver.registerDataset('iso-17000', ['urn:iso:std:iso:iec:17000*']);
|
|
125
|
+
resolver.registerSourceRef('ISO/IEC 17000:2020', 'iso-17000', 'urn:iso:std:iso:iec:17000');
|
|
126
|
+
|
|
127
|
+
const result = resolver.resolveCitation('ISO/IEC 17000:2020', '3.1', 'viml-2022');
|
|
128
|
+
expect(result).toEqual({
|
|
129
|
+
type: 'internal',
|
|
130
|
+
registerId: 'iso-17000',
|
|
131
|
+
conceptId: '3.1',
|
|
132
|
+
crossDataset: true,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('returns null for unresolvable ISO references', () => {
|
|
137
|
+
// ISO dataset not loaded — no registerSourceRef or dataset pattern match
|
|
138
|
+
const result = resolver.resolveCitation('ISO/IEC 17000:2020', '3.1', 'viml-2022');
|
|
139
|
+
expect(result).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('edge cases', () => {
|
|
144
|
+
it('returns internal with empty conceptId when referenceFrom is empty', () => {
|
|
145
|
+
resolver.registerSourceRef('OIML V 2-200:2012', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
146
|
+
const result = resolver.resolveCitation('OIML V 2-200:2012', '');
|
|
147
|
+
// Resolver still resolves but with empty conceptId — UI handles this
|
|
148
|
+
expect(result?.type).toBe('internal');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('handles concept IDs with dots', () => {
|
|
152
|
+
resolver.registerSourceRef('OIML V 2-200:2012', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
153
|
+
const result = resolver.resolveCitation('OIML V 2-200:2012', '5.12.3', 'viml-2022');
|
|
154
|
+
expect(result).toEqual({
|
|
155
|
+
type: 'internal',
|
|
156
|
+
registerId: 'vim-2012',
|
|
157
|
+
conceptId: '5.12.3',
|
|
158
|
+
crossDataset: true,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('handles source strings with special characters', () => {
|
|
163
|
+
resolver.registerDataset('vim-1993', ['urn:oiml:pub:v:2:1993*']);
|
|
164
|
+
resolver.registerSourceRef('OIML V 2:1993', 'vim-1993', 'urn:oiml:pub:v:2:1993');
|
|
165
|
+
const result = resolver.resolveCitation('OIML V 2:1993', '3.6');
|
|
166
|
+
expect(asInternal(result)?.registerId).toBe('vim-1993');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('does not confuse similar source strings', () => {
|
|
170
|
+
resolver.registerSourceRef('OIML V 2-200:2007', 'vim-2007', 'urn:oiml:pub:v:2:2007');
|
|
171
|
+
resolver.registerSourceRef('OIML V 2-200:2012', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
172
|
+
|
|
173
|
+
const result2007 = resolver.resolveCitation('OIML V 2-200:2007', '2.2');
|
|
174
|
+
const result2012 = resolver.resolveCitation('OIML V 2-200:2012', '2.2');
|
|
175
|
+
|
|
176
|
+
expect(asInternal(result2007)?.registerId).toBe('vim-2007');
|
|
177
|
+
expect(asInternal(result2012)?.registerId).toBe('vim-2012');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
package/src/adapters/factory.ts
CHANGED
|
@@ -56,6 +56,9 @@ export class AdapterFactory {
|
|
|
56
56
|
adapter.setUrnMap(this.urnMap);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Load source-refs index for citation resolution
|
|
60
|
+
await this.loadSourceRefs();
|
|
61
|
+
|
|
59
62
|
return adapters;
|
|
60
63
|
|
|
61
64
|
}
|
|
@@ -154,6 +157,21 @@ export class AdapterFactory {
|
|
|
154
157
|
this.crossRefIndex = this.crossRefIndex || {};
|
|
155
158
|
return this.crossRefIndex;
|
|
156
159
|
}
|
|
160
|
+
|
|
161
|
+
async loadSourceRefs(): Promise<void> {
|
|
162
|
+
try {
|
|
163
|
+
const resp = await fetch(`${import.meta.env.BASE_URL}data/source-refs.json`);
|
|
164
|
+
if (!resp.ok) return;
|
|
165
|
+
const refs: Record<string, string> = await resp.json();
|
|
166
|
+
for (const [source, datasetId] of Object.entries(refs)) {
|
|
167
|
+
const adapter = this.adapters.get(datasetId);
|
|
168
|
+
if (!adapter?.manifest) continue;
|
|
169
|
+
this.resolver.registerSourceRef(source, datasetId, adapter.manifest.datasetUri);
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
// source-refs.json is optional
|
|
173
|
+
}
|
|
174
|
+
}
|
|
157
175
|
}
|
|
158
176
|
|
|
159
177
|
let _instance: AdapterFactory | null = null;
|
|
@@ -221,13 +221,15 @@ function attachAnnotations(concept: Concept, localizations: Record<string, unkno
|
|
|
221
221
|
for (let j = 0; j < designation.related.length && j < rawRelated.length; j++) {
|
|
222
222
|
const rawRel = rawRelated[j] as Record<string, unknown>;
|
|
223
223
|
const rc = designation.related[j];
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
224
|
+
if ('type' in rc) {
|
|
225
|
+
if (rawRel.target && typeof rawRel.target === 'string') {
|
|
226
|
+
designationTargets.set(rc as RelatedConcept, rawRel.target);
|
|
227
|
+
}
|
|
228
|
+
if ('ref' in rc && rc.ref) {
|
|
229
|
+
const rawRef = rawRel.ref as Record<string, unknown> | undefined;
|
|
230
|
+
if (rawRef?.text && typeof rawRef.text === 'string') {
|
|
231
|
+
refTexts.set((rc as RelatedConcept).ref!, rawRef.text);
|
|
232
|
+
}
|
|
231
233
|
}
|
|
232
234
|
}
|
|
233
235
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Designation, Expression, ConceptSource } from 'glossarist';
|
|
2
|
+
import type { Designation, Expression, ConceptSource, RelatedConcept } from 'glossarist';
|
|
3
3
|
import { designationTypeInfo, normativeStatusInfo, abbreviationDetails, termTypeInfo, grammarBadges, pronunciationLabel, pronunciationTooltip, sourceTypeInfo } from '../utils/designation-registry';
|
|
4
4
|
import { relationshipLabel } from '../utils/relationship-categories';
|
|
5
5
|
import { langName } from '../utils/lang';
|
|
@@ -17,6 +17,10 @@ const emit = defineEmits<{
|
|
|
17
17
|
(e: 'navigate-related', ref: { source: string | null; id: string | null }): void;
|
|
18
18
|
}>();
|
|
19
19
|
|
|
20
|
+
function asRelated(dr: unknown): RelatedConcept | null {
|
|
21
|
+
return dr && typeof dr === 'object' && 'ref' in dr ? dr as RelatedConcept : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
function resolvedLabel(dr: { content: string | null; ref: { source: string | null; id: string | null } | null }): string {
|
|
21
25
|
if (dr.content) return dr.content;
|
|
22
26
|
if (dr.ref?.source && dr.ref?.id) return `${dr.ref.source}/${dr.ref.id}`;
|
|
@@ -65,12 +69,12 @@ function resolvedLabel(dr: { content: string | null; ref: { source: string | nul
|
|
|
65
69
|
</div>
|
|
66
70
|
<div v-if="d.related?.length" class="mt-0.5 space-y-0.5">
|
|
67
71
|
<div v-for="(dr, dri) in d.related" :key="'dr'+dri" class="text-xs text-ink-400 flex items-center gap-1.5">
|
|
68
|
-
<span class="badge text-[9px] bg-gray-50 text-gray-600">{{ relationshipLabel(dr.type) }}</span>
|
|
69
|
-
<template v-if="getDesignationTarget(dr)">
|
|
70
|
-
<span class="italic">{{ getDesignationTarget(dr) }}</span>
|
|
72
|
+
<span class="badge text-[9px] bg-gray-50 text-gray-600">{{ relationshipLabel(dr.type as string) }}</span>
|
|
73
|
+
<template v-if="getDesignationTarget(dr as any)">
|
|
74
|
+
<span class="italic">{{ getDesignationTarget(dr as any) }}</span>
|
|
71
75
|
</template>
|
|
72
|
-
<button v-else-if="dr
|
|
73
|
-
<span v-else>{{ resolvedLabel(dr) }}</span>
|
|
76
|
+
<button v-else-if="asRelated(dr)?.ref" @click="emit('navigate-related', asRelated(dr)!.ref!)" class="concept-link">{{ resolvedLabel(asRelated(dr)!) }}</button>
|
|
77
|
+
<span v-else>{{ resolvedLabel(dr as any) }}</span>
|
|
74
78
|
</div>
|
|
75
79
|
</div>
|
|
76
80
|
</div>
|