@glossarist/concept-browser 0.7.32 → 0.7.33
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 +24 -41
- package/scripts/extract-source-refs.js +32 -0
- package/scripts/generate-data.mjs +2 -0
- package/src/__tests__/citation-display.test.ts +143 -0
- package/src/__tests__/concept-helpers.test.ts +129 -0
- package/src/__tests__/designation-relationship.test.ts +31 -0
- package/src/__tests__/extract-source-refs.test.ts +136 -0
- package/src/__tests__/factory-lazy.test.ts +6 -6
- package/src/__tests__/load-source-refs.test.ts +128 -0
- package/src/__tests__/slugify.test.ts +28 -0
- package/src/__tests__/source-refs.test.ts +11 -0
- package/src/adapters/DatasetAdapter.ts +51 -19
- package/src/adapters/ReferenceResolver.ts +5 -0
- package/src/adapters/factory.ts +36 -58
- package/src/adapters/model-bridge.ts +32 -24
- package/src/adapters/ontology-registry.ts +21 -4
- package/src/adapters/types.ts +2 -0
- package/src/components/CitationDisplay.vue +123 -14
- package/src/components/ConceptDetail.vue +25 -197
- package/src/components/DesignationList.vue +7 -11
- package/src/composables/use-concept-content.ts +160 -0
- package/src/composables/use-concept-edges.ts +181 -0
- package/src/data/taxonomies.json +13 -1
- package/src/graph/GraphEngine.ts +15 -0
- package/src/utils/concept-helpers.ts +4 -7
- package/src/utils/designation-registry.ts +15 -37
- package/src/utils/index.ts +1 -0
- package/src/utils/slugify.ts +3 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { getFactory, resetFactory } from '../adapters/factory';
|
|
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('AdapterFactory — bibliography resolution from registry', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
resetFactory();
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('registers bibliography from registry ref/refAliases', async () => {
|
|
18
|
+
const factory = getFactory();
|
|
19
|
+
const mockFetch = vi.fn((url: string) => {
|
|
20
|
+
if (url.endsWith('datasets.json')) {
|
|
21
|
+
return Promise.resolve({
|
|
22
|
+
ok: true,
|
|
23
|
+
json: () => Promise.resolve([
|
|
24
|
+
{
|
|
25
|
+
id: 'vim-2012',
|
|
26
|
+
manifestUrl: '/data/vim-2012/manifest.json',
|
|
27
|
+
summary: {
|
|
28
|
+
title: 'VIM 2012',
|
|
29
|
+
description: 'Vocabulary of metrology',
|
|
30
|
+
conceptCount: 144,
|
|
31
|
+
languages: ['eng', 'fra'],
|
|
32
|
+
owner: 'OIML',
|
|
33
|
+
tags: [],
|
|
34
|
+
},
|
|
35
|
+
datasetUri: 'urn:oiml:pub:v:2:2012',
|
|
36
|
+
uriBase: 'https://glossarist.org',
|
|
37
|
+
uriAliases: ['urn:oiml:pub:v:2:2012*'],
|
|
38
|
+
ref: 'OIML V 2-200:2012',
|
|
39
|
+
refAliases: ['VIM'],
|
|
40
|
+
},
|
|
41
|
+
]),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return Promise.resolve({ ok: false, status: 404 } as Response);
|
|
45
|
+
});
|
|
46
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
47
|
+
|
|
48
|
+
await factory.discoverDatasets('/datasets.json');
|
|
49
|
+
|
|
50
|
+
// No source-refs.json fetch — bibliography comes from registry config
|
|
51
|
+
expect(mockFetch).not.toHaveBeenCalledWith(expect.stringContaining('source-refs'));
|
|
52
|
+
|
|
53
|
+
// ref resolves via bibliography config
|
|
54
|
+
const result = factory.resolveCitation('OIML V 2-200:2012', '2.2', 'viml-2022');
|
|
55
|
+
expect(result).toEqual({
|
|
56
|
+
type: 'internal',
|
|
57
|
+
registerId: 'vim-2012',
|
|
58
|
+
conceptId: '2.2',
|
|
59
|
+
crossDataset: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// refAlias resolves too
|
|
63
|
+
const alias = factory.resolveCitation('VIM', '2.2', 'viml-2022');
|
|
64
|
+
expect(asInternal(alias)?.registerId).toBe('vim-2012');
|
|
65
|
+
|
|
66
|
+
// URN resolves directly via URI pattern matching (no bibliography entry needed)
|
|
67
|
+
const urn = factory.resolveCitation('urn:oiml:pub:v:2:2012', '2.2', 'viml-2022');
|
|
68
|
+
expect(asInternal(urn)?.registerId).toBe('vim-2012');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('resolves without ref/refAliases when only datasetUri is present', async () => {
|
|
72
|
+
const factory = getFactory();
|
|
73
|
+
const mockFetch = vi.fn((url: string) => {
|
|
74
|
+
if (url.endsWith('datasets.json')) {
|
|
75
|
+
return Promise.resolve({
|
|
76
|
+
ok: true,
|
|
77
|
+
json: () => Promise.resolve([
|
|
78
|
+
{
|
|
79
|
+
id: 'ds1',
|
|
80
|
+
manifestUrl: '/data/ds1/manifest.json',
|
|
81
|
+
summary: {
|
|
82
|
+
title: 'DS1',
|
|
83
|
+
description: 'Test',
|
|
84
|
+
conceptCount: 10,
|
|
85
|
+
languages: ['eng'],
|
|
86
|
+
owner: 'Test',
|
|
87
|
+
tags: [],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
]),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return Promise.resolve({ ok: false, status: 404 } as Response);
|
|
94
|
+
});
|
|
95
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
96
|
+
|
|
97
|
+
const adapters = await factory.discoverDatasets('/datasets.json');
|
|
98
|
+
expect(adapters).toHaveLength(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('skips registry entries without datasetUri for bibliography', async () => {
|
|
102
|
+
const factory = getFactory();
|
|
103
|
+
const mockFetch = vi.fn((url: string) => {
|
|
104
|
+
if (url.endsWith('datasets.json')) {
|
|
105
|
+
return Promise.resolve({
|
|
106
|
+
ok: true,
|
|
107
|
+
json: () => Promise.resolve([
|
|
108
|
+
{
|
|
109
|
+
id: 'ds1',
|
|
110
|
+
manifestUrl: '/data/ds1/manifest.json',
|
|
111
|
+
summary: { title: 'DS1', description: 'Test', conceptCount: 10, languages: ['eng'], owner: 'Test', tags: [] },
|
|
112
|
+
ref: 'Some Ref',
|
|
113
|
+
// No datasetUri — bibliography entry skipped
|
|
114
|
+
},
|
|
115
|
+
]),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return Promise.resolve({ ok: false, status: 404 } as Response);
|
|
119
|
+
});
|
|
120
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
121
|
+
|
|
122
|
+
await factory.discoverDatasets('/datasets.json');
|
|
123
|
+
|
|
124
|
+
// ref without URN can't resolve — no datasetUri to route to
|
|
125
|
+
const result = factory.resolveCitation('Some Ref', '1.1');
|
|
126
|
+
expect(result).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { slugify } from '../utils/slugify';
|
|
3
|
+
|
|
4
|
+
describe('slugify', () => {
|
|
5
|
+
it('lowercases text', () => {
|
|
6
|
+
expect(slugify('Hello World')).toBe('hello-world');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('strips slashes and joins surrounding text', () => {
|
|
10
|
+
expect(slugify('foo bar/baz')).toBe('foo-barbaz');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('strips non-word characters', () => {
|
|
14
|
+
expect(slugify('géométrie (2D)')).toBe('gomtrie-2d');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('collapses multiple spaces into single hyphen', () => {
|
|
18
|
+
expect(slugify('a b')).toBe('a-b');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('handles empty string', () => {
|
|
22
|
+
expect(slugify('')).toBe('');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('preserves hyphens in input', () => {
|
|
26
|
+
expect(slugify('iso-19107')).toBe('iso-19107');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -19,6 +19,17 @@ describe('Source reference resolution (citation linking)', () => {
|
|
|
19
19
|
resolver.registerDataset('vim-2007', ['urn:oiml:pub:v:2:2007*']);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
describe('hasSourceRef', () => {
|
|
23
|
+
it('returns true for registered source ref', () => {
|
|
24
|
+
resolver.registerSourceRef('VIM', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
25
|
+
expect(resolver.hasSourceRef('VIM')).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns false for unregistered source ref', () => {
|
|
29
|
+
expect(resolver.hasSourceRef('Unknown')).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
22
33
|
describe('registerSourceRef', () => {
|
|
23
34
|
it('resolves citation when source ref matches exactly', () => {
|
|
24
35
|
resolver.registerSourceRef('OIML V 2-200:2012', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
@@ -9,15 +9,47 @@ import type {
|
|
|
9
9
|
SectionNode,
|
|
10
10
|
DatasetSummary,
|
|
11
11
|
} from './types';
|
|
12
|
-
import type { Concept, LocalizedConcept, Designation } from 'glossarist';
|
|
12
|
+
import type { Concept, LocalizedConcept, Designation, RelatedConcept } from 'glossarist';
|
|
13
13
|
import { conceptFromJson, conceptUri } from './model-bridge';
|
|
14
14
|
import { UriRouter } from './UriRouter';
|
|
15
|
+
import { slugify } from '../utils/slugify';
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
// ── Wire-format types for JSON responses ────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
interface IndexJson {
|
|
20
|
+
registerId: string;
|
|
21
|
+
schemaVersion: string;
|
|
22
|
+
conceptCount: number;
|
|
23
|
+
chunkSize: number;
|
|
24
|
+
chunks: { file: string; count: number }[];
|
|
25
|
+
concepts: IndexConceptJson[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface IndexConceptJson {
|
|
29
|
+
id: string;
|
|
30
|
+
designations?: Record<string, string>;
|
|
31
|
+
eng?: string;
|
|
32
|
+
status: string;
|
|
33
|
+
groups?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface DomainNodeJson {
|
|
37
|
+
uri?: string;
|
|
38
|
+
id?: string;
|
|
39
|
+
registerId?: string;
|
|
40
|
+
label?: string;
|
|
41
|
+
names?: Record<string, string>;
|
|
42
|
+
conceptCount?: number;
|
|
43
|
+
children?: DomainNodeJson[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface SectionJson {
|
|
47
|
+
id: string;
|
|
48
|
+
names?: Record<string, string>;
|
|
49
|
+
children?: SectionJson[];
|
|
18
50
|
}
|
|
19
51
|
|
|
20
|
-
function resolveRefTarget(rc:
|
|
52
|
+
function resolveRefTarget(rc: RelatedConcept, uriBase: string, registerId: string, urnMap?: ReadonlyMap<string, string>): string {
|
|
21
53
|
if (!rc.ref) return '';
|
|
22
54
|
const ref = rc.ref;
|
|
23
55
|
if (ref.id) {
|
|
@@ -99,13 +131,13 @@ export class DatasetAdapter {
|
|
|
99
131
|
const data = await resp.json();
|
|
100
132
|
|
|
101
133
|
// Handle both old format (with eng/status fields) and new format (with designations map)
|
|
102
|
-
this.index = this.normalizeIndex(data);
|
|
134
|
+
this.index = this.normalizeIndex(data as IndexJson);
|
|
103
135
|
this.buildSummaryIndex();
|
|
104
136
|
return this.index;
|
|
105
137
|
}
|
|
106
138
|
|
|
107
|
-
private normalizeIndex(data:
|
|
108
|
-
const concepts: ConceptSummary[] = (data.concepts || []).map((c
|
|
139
|
+
private normalizeIndex(data: IndexJson): ConceptIndex {
|
|
140
|
+
const concepts: ConceptSummary[] = (data.concepts || []).map((c) => ({
|
|
109
141
|
id: c.id,
|
|
110
142
|
designations: c.designations || {},
|
|
111
143
|
eng: c.eng || c.designations?.eng || Object.values(c.designations || {})[0] || '',
|
|
@@ -144,7 +176,7 @@ export class DatasetAdapter {
|
|
|
144
176
|
const resp = await fetch(`${this.baseUrl}/index.json`);
|
|
145
177
|
if (!resp.ok) throw new Error(`Failed to load index for ${this.registerId}`);
|
|
146
178
|
const data = await resp.json();
|
|
147
|
-
this.index = this.normalizeIndex(data);
|
|
179
|
+
this.index = this.normalizeIndex(data as IndexJson);
|
|
148
180
|
this.buildSummaryIndex();
|
|
149
181
|
return this.index;
|
|
150
182
|
}
|
|
@@ -412,34 +444,34 @@ export class DatasetAdapter {
|
|
|
412
444
|
const resp = await fetch(`${this.baseUrl}/domain-nodes.json`);
|
|
413
445
|
if (!resp.ok) return [];
|
|
414
446
|
const data = await resp.json();
|
|
415
|
-
return (data.domainNodes || []).map((dn:
|
|
447
|
+
return (data.domainNodes || []).map((dn: DomainNodeJson) => this.mapDomainNode(dn));
|
|
416
448
|
}
|
|
417
449
|
|
|
418
|
-
private mapDomainNode(dn:
|
|
450
|
+
private mapDomainNode(dn: DomainNodeJson): GraphNode {
|
|
419
451
|
const node: GraphNode = {
|
|
420
|
-
uri: dn.uri,
|
|
421
|
-
register: dn.registerId,
|
|
452
|
+
uri: dn.uri ?? '',
|
|
453
|
+
register: dn.registerId ?? '',
|
|
422
454
|
conceptId: dn.uri?.split('/domain/')[1] || dn.id || '',
|
|
423
|
-
designations: dn.names || { eng: dn.label },
|
|
455
|
+
designations: dn.names || (dn.label ? { eng: dn.label } : {}),
|
|
424
456
|
status: 'domain',
|
|
425
457
|
loaded: true,
|
|
426
458
|
nodeType: 'domain' as const,
|
|
427
459
|
conceptCount: dn.conceptCount || 0,
|
|
428
460
|
};
|
|
429
461
|
if (dn.children && dn.children.length > 0) {
|
|
430
|
-
node.children = dn.children.map((c
|
|
462
|
+
node.children = dn.children.map((c) => this.mapSectionNode(c));
|
|
431
463
|
}
|
|
432
464
|
return node;
|
|
433
465
|
}
|
|
434
466
|
|
|
435
|
-
private mapSectionNode(dn:
|
|
467
|
+
private mapSectionNode(dn: DomainNodeJson): SectionNode {
|
|
436
468
|
const node: SectionNode = {
|
|
437
|
-
id: dn.id,
|
|
438
|
-
names: dn.names || { eng: dn.label },
|
|
469
|
+
id: dn.id ?? '',
|
|
470
|
+
names: dn.names || (dn.label ? { eng: dn.label } : {}),
|
|
439
471
|
conceptCount: dn.conceptCount || 0,
|
|
440
472
|
};
|
|
441
473
|
if (dn.children && dn.children.length > 0) {
|
|
442
|
-
node.children = dn.children.map((c
|
|
474
|
+
node.children = dn.children.map((c) => this.mapSectionNode(c));
|
|
443
475
|
}
|
|
444
476
|
return node;
|
|
445
477
|
}
|
|
@@ -450,7 +482,7 @@ export class DatasetAdapter {
|
|
|
450
482
|
return nodes.map(s => this.mapManifestSection(s));
|
|
451
483
|
}
|
|
452
484
|
|
|
453
|
-
private mapManifestSection(s:
|
|
485
|
+
private mapManifestSection(s: SectionJson): SectionNode {
|
|
454
486
|
const node: SectionNode = { id: s.id, names: s.names || {}, conceptCount: 0 };
|
|
455
487
|
if (s.children && s.children.length > 0) {
|
|
456
488
|
node.children = s.children.map(c => this.mapManifestSection(c));
|
|
@@ -40,6 +40,10 @@ export class ReferenceResolver {
|
|
|
40
40
|
this.sourceRefs.set(sourceRef, { datasetId, uriPrefix });
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
hasSourceRef(sourceRef: string): boolean {
|
|
44
|
+
return this.sourceRefs.has(sourceRef);
|
|
45
|
+
}
|
|
46
|
+
|
|
43
47
|
loadRouting(entries: RoutingEntry[]): void {
|
|
44
48
|
this.routing = entries;
|
|
45
49
|
}
|
|
@@ -90,6 +94,7 @@ export class ReferenceResolver {
|
|
|
90
94
|
resolveCitation(source: string, referenceFrom: string, sourceDatasetId?: string): Resolution | null {
|
|
91
95
|
const entry = this.sourceRefs.get(source);
|
|
92
96
|
if (!entry) {
|
|
97
|
+
// URN-based source strings resolve directly via dataset URI patterns
|
|
93
98
|
if (!source.startsWith('urn:')) return null;
|
|
94
99
|
return this.tryResolveCitationUri(source, referenceFrom, sourceDatasetId);
|
|
95
100
|
}
|
package/src/adapters/factory.ts
CHANGED
|
@@ -46,21 +46,25 @@ export class AdapterFactory {
|
|
|
46
46
|
await Promise.all(needManifest.map(a => a.loadManifest().catch(() => {})));
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Register all datasets' URI patterns eagerly so cross-dataset refs resolve
|
|
50
49
|
for (const adapter of adapters) {
|
|
51
50
|
if (adapter.manifest) {
|
|
52
|
-
this.
|
|
51
|
+
this.registerDataset(adapter.registerId, adapter.manifest);
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
|
-
for (const adapter of this.adapters.values()) {
|
|
56
|
-
adapter.setUrnMap(this.urnMap);
|
|
57
|
-
}
|
|
58
54
|
|
|
59
|
-
//
|
|
60
|
-
|
|
55
|
+
// Register bibliography from registry config (ref/refAliases → URN)
|
|
56
|
+
// This is the single source of truth — no separate source-refs file needed.
|
|
57
|
+
for (const reg of registry) {
|
|
58
|
+
if (!reg.datasetUri) continue;
|
|
59
|
+
if (reg.ref) {
|
|
60
|
+
this.resolver.registerSourceRef(reg.ref, reg.id, reg.datasetUri);
|
|
61
|
+
}
|
|
62
|
+
for (const alias of reg.refAliases ?? []) {
|
|
63
|
+
this.resolver.registerSourceRef(alias, reg.id, reg.datasetUri);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
61
66
|
|
|
62
67
|
return adapters;
|
|
63
|
-
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
getAdapter(registerId: string): DatasetAdapter | undefined {
|
|
@@ -71,8 +75,9 @@ export class AdapterFactory {
|
|
|
71
75
|
return [...this.adapters.values()];
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
private registerDataset(registerId: string, manifest: Manifest): void {
|
|
79
|
+
this.router.registerDataset(registerId, `${import.meta.env.BASE_URL}data/${registerId}`, manifest);
|
|
74
80
|
|
|
75
|
-
private registerUriPatterns(registerId: string, manifest: Manifest): void {
|
|
76
81
|
const uriPatterns = [
|
|
77
82
|
manifest.datasetUri,
|
|
78
83
|
...(manifest.uriAliases ?? []),
|
|
@@ -80,18 +85,15 @@ export class AdapterFactory {
|
|
|
80
85
|
].filter(Boolean) as string[];
|
|
81
86
|
this.resolver.registerDataset(registerId, uriPatterns);
|
|
82
87
|
|
|
83
|
-
if (manifest.ref) {
|
|
84
|
-
this.resolver.registerSourceRef(manifest.ref, registerId, manifest.datasetUri);
|
|
85
|
-
}
|
|
86
|
-
for (const alias of manifest.refAliases ?? []) {
|
|
87
|
-
this.resolver.registerSourceRef(alias, registerId, manifest.datasetUri);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
88
|
if (manifest.datasetUri) this.urnMap.set(manifest.datasetUri, registerId);
|
|
91
89
|
for (const alias of manifest.uriAliases ?? []) {
|
|
92
90
|
const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
|
|
93
91
|
if (base.startsWith('urn:')) this.urnMap.set(base, registerId);
|
|
94
92
|
}
|
|
93
|
+
|
|
94
|
+
for (const adapter of this.adapters.values()) {
|
|
95
|
+
adapter.setUrnMap(this.urnMap);
|
|
96
|
+
}
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
async loadDataset(registerId: string): Promise<DatasetAdapter> {
|
|
@@ -101,33 +103,7 @@ export class AdapterFactory {
|
|
|
101
103
|
const manifest = await adapter.loadManifest();
|
|
102
104
|
await adapter.loadIndex();
|
|
103
105
|
|
|
104
|
-
this.
|
|
105
|
-
|
|
106
|
-
const uriPatterns = [
|
|
107
|
-
manifest.datasetUri,
|
|
108
|
-
...(manifest.uriAliases ?? []),
|
|
109
|
-
manifest.uriBase ? `${manifest.uriBase}/${registerId}/*` : undefined,
|
|
110
|
-
].filter(Boolean) as string[];
|
|
111
|
-
this.resolver.registerDataset(registerId, uriPatterns);
|
|
112
|
-
|
|
113
|
-
if (manifest.ref) {
|
|
114
|
-
this.resolver.registerSourceRef(manifest.ref, registerId, manifest.datasetUri);
|
|
115
|
-
}
|
|
116
|
-
for (const alias of manifest.refAliases ?? []) {
|
|
117
|
-
this.resolver.registerSourceRef(alias, registerId, manifest.datasetUri);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Build URN→datasetId map from manifest
|
|
121
|
-
if (manifest.datasetUri) this.urnMap.set(manifest.datasetUri, registerId);
|
|
122
|
-
for (const alias of manifest.uriAliases ?? []) {
|
|
123
|
-
const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
|
|
124
|
-
if (base.startsWith('urn:')) this.urnMap.set(base, registerId);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Distribute the updated URN map to all loaded adapters
|
|
128
|
-
for (const adapter of this.adapters.values()) {
|
|
129
|
-
adapter.setUrnMap(this.urnMap);
|
|
130
|
-
}
|
|
106
|
+
this.registerDataset(registerId, manifest);
|
|
131
107
|
|
|
132
108
|
return adapter;
|
|
133
109
|
}
|
|
@@ -140,6 +116,23 @@ export class AdapterFactory {
|
|
|
140
116
|
return this.resolver.resolveReference(uri, sourceDatasetId);
|
|
141
117
|
}
|
|
142
118
|
|
|
119
|
+
resolveRelatedRef(ref: { source: string | null; id: string | null } | null, sourceDatasetId?: string): { registerId: string; conceptId: string } | null {
|
|
120
|
+
if (!ref?.source || !ref?.id) return null;
|
|
121
|
+
const uri = `${ref.source}/${ref.id}`;
|
|
122
|
+
const resolution = this.resolve(uri, sourceDatasetId);
|
|
123
|
+
if (resolution.type === 'internal') {
|
|
124
|
+
return { registerId: resolution.registerId, conceptId: resolution.conceptId.replace(/^\//, '') };
|
|
125
|
+
}
|
|
126
|
+
if (ref.source.startsWith('urn:')) {
|
|
127
|
+
const directUri = ref.source + ref.id;
|
|
128
|
+
const directRes = this.resolve(directUri, sourceDatasetId);
|
|
129
|
+
if (directRes.type === 'internal') {
|
|
130
|
+
return { registerId: directRes.registerId, conceptId: directRes.conceptId.replace(/^\//, '') };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
143
136
|
resolveCitation(source: string, referenceFrom: string, sourceDatasetId?: string): Resolution | null {
|
|
144
137
|
return this.resolver.resolveCitation(source, referenceFrom, sourceDatasetId);
|
|
145
138
|
}
|
|
@@ -157,21 +150,6 @@ export class AdapterFactory {
|
|
|
157
150
|
this.crossRefIndex = this.crossRefIndex || {};
|
|
158
151
|
return this.crossRefIndex;
|
|
159
152
|
}
|
|
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
|
-
}
|
|
175
153
|
}
|
|
176
154
|
|
|
177
155
|
let _instance: AdapterFactory | null = null;
|
|
@@ -179,10 +179,10 @@ export function getAnnotations(lc: LocalizedConcept): DetailedDefinition[] {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
// Designation relationship targets: RelatedConcept.target (string)
|
|
182
|
-
const designationTargets = new WeakMap<
|
|
182
|
+
const designationTargets = new WeakMap<object, string>();
|
|
183
183
|
|
|
184
|
-
export function getDesignationTarget(rc:
|
|
185
|
-
return designationTargets.get(rc) ?? null;
|
|
184
|
+
export function getDesignationTarget(rc: { type?: string | null; content?: string | null; target?: string | null; ref?: any }): string | null {
|
|
185
|
+
return designationTargets.get(rc) ?? rc.target ?? null;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
// ConceptRef text: human-readable label alongside source/id
|
|
@@ -213,23 +213,28 @@ function attachAnnotations(concept: Concept, localizations: Record<string, unkno
|
|
|
213
213
|
// Designation-level relationship targets and ref text
|
|
214
214
|
const rawTerms = rawObj.terms;
|
|
215
215
|
if (Array.isArray(rawTerms)) {
|
|
216
|
-
for (
|
|
217
|
-
|
|
218
|
-
const
|
|
216
|
+
for (const rawTerm of rawTerms) {
|
|
217
|
+
if (!rawTerm || typeof rawTerm !== 'object') continue;
|
|
218
|
+
const rawT = rawTerm as Record<string, unknown>;
|
|
219
|
+
const rawDesignation = rawT.designation as string | undefined;
|
|
220
|
+
if (!rawDesignation) continue;
|
|
221
|
+
const designation = lc.terms.find(d => d.designation === rawDesignation);
|
|
222
|
+
if (!designation) continue;
|
|
223
|
+
const rawRelated = rawT.related;
|
|
219
224
|
if (!Array.isArray(rawRelated)) continue;
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
225
|
+
for (const rawRel of rawRelated) {
|
|
226
|
+
if (!rawRel || typeof rawRel !== 'object') continue;
|
|
227
|
+
const rel = rawRel as Record<string, unknown>;
|
|
228
|
+
const relType = rel.type as string | undefined;
|
|
229
|
+
const rc = designation.related.find(r => r.type === relType);
|
|
230
|
+
if (!rc) continue;
|
|
231
|
+
if (rel.target && typeof rel.target === 'string') {
|
|
232
|
+
designationTargets.set(rc as object, rel.target);
|
|
233
|
+
}
|
|
234
|
+
if ('ref' in rc && rc.ref) {
|
|
235
|
+
const rawRef = rel.ref as Record<string, unknown> | undefined;
|
|
236
|
+
if (rawRef?.text && typeof rawRef.text === 'string') {
|
|
237
|
+
refTexts.set(rc.ref, rawRef.text);
|
|
233
238
|
}
|
|
234
239
|
}
|
|
235
240
|
}
|
|
@@ -239,11 +244,14 @@ function attachAnnotations(concept: Concept, localizations: Record<string, unkno
|
|
|
239
244
|
// Localization-level ref text
|
|
240
245
|
const rawRelated = rawObj.related;
|
|
241
246
|
if (Array.isArray(rawRelated)) {
|
|
242
|
-
for (
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
+
for (const rawRel of rawRelated) {
|
|
248
|
+
if (!rawRel || typeof rawRel !== 'object') continue;
|
|
249
|
+
const rel = rawRel as Record<string, unknown>;
|
|
250
|
+
const relType = rel.type as string | undefined;
|
|
251
|
+
const rc = relType ? lc.related.find(r => r.type === relType) : undefined;
|
|
252
|
+
if (!rc || !rc.ref) continue;
|
|
253
|
+
const rawRef = rel.ref as Record<string, unknown> | undefined;
|
|
254
|
+
if (rawRef?.text && typeof rawRef.text === 'string') {
|
|
247
255
|
refTexts.set(rc.ref, rawRef.text);
|
|
248
256
|
}
|
|
249
257
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ontology Registry — taxonomy-driven labels and
|
|
2
|
+
* Ontology Registry — taxonomy-driven labels, definitions, and colors for the browser.
|
|
3
3
|
*
|
|
4
4
|
* All enumeration labels, definitions, and colors come from the SKOS taxonomy
|
|
5
5
|
* data extracted at build time from concept-model/ontologies/taxonomies/*.ttl.
|
|
@@ -29,10 +29,19 @@ export interface Taxonomy {
|
|
|
29
29
|
schemeDefinition: string | null;
|
|
30
30
|
categories?: Record<string, TaxonomyCategory>;
|
|
31
31
|
concepts: Record<string, TaxonomyConcept>;
|
|
32
|
+
colors?: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TaxonomyDisplay {
|
|
36
|
+
label: string;
|
|
37
|
+
color: string;
|
|
38
|
+
definition?: string;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
type TaxonomyKey = keyof typeof taxonomyData;
|
|
35
42
|
|
|
43
|
+
const DEFAULT_COLOR = 'badge-gray';
|
|
44
|
+
|
|
36
45
|
export class OntologyRegistry {
|
|
37
46
|
private data: Record<string, Taxonomy>;
|
|
38
47
|
|
|
@@ -94,9 +103,17 @@ export class OntologyRegistry {
|
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
getColor(taxonomy: TaxonomyKey, id: string): string | null {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
return this.data[taxonomy]?.colors?.[id] ?? null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getDisplay(taxonomy: TaxonomyKey, id: string | null | undefined, colorFallback?: string): TaxonomyDisplay {
|
|
110
|
+
if (!id) return { label: '', color: colorFallback ?? DEFAULT_COLOR };
|
|
111
|
+
const concept = this.getConcept(taxonomy, id);
|
|
112
|
+
return {
|
|
113
|
+
label: concept?.prefLabel ?? id,
|
|
114
|
+
color: this.data[taxonomy]?.colors?.[id] ?? colorFallback ?? DEFAULT_COLOR,
|
|
115
|
+
definition: concept?.definition ?? undefined,
|
|
116
|
+
};
|
|
100
117
|
}
|
|
101
118
|
}
|
|
102
119
|
|
package/src/adapters/types.ts
CHANGED