@glossarist/concept-browser 0.7.33 → 0.7.35
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/__tests__/concept-detail-interaction.test.ts +0 -4
- package/src/adapters/DatasetAdapter.ts +23 -143
- package/src/adapters/GraphDataSource.ts +178 -0
- package/src/adapters/ReferenceResolver.ts +24 -0
- package/src/adapters/factory.ts +1 -18
- package/src/components/ConceptDetail.vue +20 -124
- package/src/composables/use-concept-edges.ts +2 -2
- package/src/composables/use-ontology-nav.ts +129 -130
- package/src/graph/GraphEngine.ts +68 -3
- package/src/stores/vocabulary.ts +14 -75
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.35",
|
|
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": {
|
|
@@ -242,10 +242,6 @@ describe('ConceptDetail interactions', () => {
|
|
|
242
242
|
// Register the URI pattern via factory so it resolves as internal
|
|
243
243
|
const { getFactory } = await import('../adapters/factory');
|
|
244
244
|
const factory = getFactory();
|
|
245
|
-
factory.router.registerDataset('test', '/data/test', {
|
|
246
|
-
...makeManifest(),
|
|
247
|
-
uriBase: 'https://glossarist.org',
|
|
248
|
-
});
|
|
249
245
|
factory.resolver.registerDataset('test', ['https://glossarist.org/test/concept/*']);
|
|
250
246
|
|
|
251
247
|
const wrapper = mountDetail(json);
|
|
@@ -9,10 +9,9 @@ import type {
|
|
|
9
9
|
SectionNode,
|
|
10
10
|
DatasetSummary,
|
|
11
11
|
} from './types';
|
|
12
|
-
import type { Concept
|
|
13
|
-
import { conceptFromJson
|
|
14
|
-
import {
|
|
15
|
-
import { slugify } from '../utils/slugify';
|
|
12
|
+
import type { Concept } from 'glossarist';
|
|
13
|
+
import { conceptFromJson } from './model-bridge';
|
|
14
|
+
import { GraphDataSource } from './GraphDataSource';
|
|
16
15
|
|
|
17
16
|
// ── Wire-format types for JSON responses ────────────────────────────────────
|
|
18
17
|
|
|
@@ -33,36 +32,6 @@ interface IndexConceptJson {
|
|
|
33
32
|
groups?: string[];
|
|
34
33
|
}
|
|
35
34
|
|
|
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[];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function resolveRefTarget(rc: RelatedConcept, uriBase: string, registerId: string, urnMap?: ReadonlyMap<string, string>): string {
|
|
53
|
-
if (!rc.ref) return '';
|
|
54
|
-
const ref = rc.ref;
|
|
55
|
-
if (ref.id) {
|
|
56
|
-
let reg = registerId;
|
|
57
|
-
if (ref.source && !ref.source.startsWith('http')) {
|
|
58
|
-
reg = urnMap?.get(ref.source) ?? ref.source;
|
|
59
|
-
}
|
|
60
|
-
return `${uriBase}/${reg}/concept/${ref.id}`;
|
|
61
|
-
}
|
|
62
|
-
if (ref.source && ref.source.startsWith('http')) return ref.source;
|
|
63
|
-
return ref.source || '';
|
|
64
|
-
}
|
|
65
|
-
|
|
66
35
|
export class DatasetAdapter {
|
|
67
36
|
private positionIndex = new Map<string, number>();
|
|
68
37
|
private _urnMap: ReadonlyMap<string, string> = new Map();
|
|
@@ -376,131 +345,42 @@ export class DatasetAdapter {
|
|
|
376
345
|
this._urnMap = map;
|
|
377
346
|
}
|
|
378
347
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const uriBase = this.manifest?.uriBase || 'https://glossarist.org';
|
|
382
|
-
const sourceUri = concept.uri || `${uriBase}/${this.registerId}/concept/${concept.id}`;
|
|
383
|
-
|
|
384
|
-
// Managed concept level relationships
|
|
385
|
-
for (const rc of concept.relatedConcepts) {
|
|
386
|
-
const target = resolveRefTarget(rc, uriBase, this.registerId, this._urnMap);
|
|
387
|
-
if (target && target !== sourceUri) {
|
|
388
|
-
const parsed = UriRouter.parseUri(target);
|
|
389
|
-
edges.push({
|
|
390
|
-
source: sourceUri,
|
|
391
|
-
target,
|
|
392
|
-
type: rc.type || 'references',
|
|
393
|
-
label: rc.content || undefined,
|
|
394
|
-
register: parsed?.registerId ?? this.registerId,
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Per-localization references (from inline extraction in generate-data)
|
|
400
|
-
for (const lang of concept.languages) {
|
|
401
|
-
const lc = concept.localization(lang);
|
|
402
|
-
if (!lc) continue;
|
|
403
|
-
for (const rc of lc.related) {
|
|
404
|
-
const target = resolveRefTarget(rc, uriBase, this.registerId, this._urnMap);
|
|
405
|
-
if (target && target !== sourceUri) {
|
|
406
|
-
const parsed = UriRouter.parseUri(target);
|
|
407
|
-
edges.push({
|
|
408
|
-
source: sourceUri,
|
|
409
|
-
target,
|
|
410
|
-
type: rc.type || 'references',
|
|
411
|
-
label: rc.content || undefined,
|
|
412
|
-
register: parsed?.registerId ?? this.registerId,
|
|
413
|
-
lang,
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
return edges;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
extractDomainEdges(concept: Concept): GraphEdge[] {
|
|
423
|
-
const edges: GraphEdge[] = [];
|
|
424
|
-
const uriBase = this.manifest?.uriBase || 'https://glossarist.org';
|
|
425
|
-
const sourceUri = concept.uri || `${uriBase}/${this.registerId}/concept/${concept.id}`;
|
|
426
|
-
|
|
427
|
-
for (const lang of concept.languages) {
|
|
428
|
-
const lc = concept.localization(lang);
|
|
429
|
-
if (lc?.domain) {
|
|
430
|
-
edges.push({
|
|
431
|
-
source: sourceUri,
|
|
432
|
-
target: `${uriBase}/${this.registerId}/domain/${slugify(lc.domain)}`,
|
|
433
|
-
type: 'domain',
|
|
434
|
-
label: lc.domain,
|
|
435
|
-
register: this.registerId,
|
|
436
|
-
lang,
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
return edges;
|
|
348
|
+
get dataUrl(): string {
|
|
349
|
+
return this.baseUrl;
|
|
441
350
|
}
|
|
442
351
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if (!resp.ok) return [];
|
|
446
|
-
const data = await resp.json();
|
|
447
|
-
return (data.domainNodes || []).map((dn: DomainNodeJson) => this.mapDomainNode(dn));
|
|
352
|
+
get urnMap(): ReadonlyMap<string, string> {
|
|
353
|
+
return this._urnMap;
|
|
448
354
|
}
|
|
449
355
|
|
|
450
|
-
private
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
conceptId: dn.uri?.split('/domain/')[1] || dn.id || '',
|
|
455
|
-
designations: dn.names || (dn.label ? { eng: dn.label } : {}),
|
|
456
|
-
status: 'domain',
|
|
457
|
-
loaded: true,
|
|
458
|
-
nodeType: 'domain' as const,
|
|
459
|
-
conceptCount: dn.conceptCount || 0,
|
|
460
|
-
};
|
|
461
|
-
if (dn.children && dn.children.length > 0) {
|
|
462
|
-
node.children = dn.children.map((c) => this.mapSectionNode(c));
|
|
463
|
-
}
|
|
464
|
-
return node;
|
|
356
|
+
private _graphDataSource: GraphDataSource | null = null;
|
|
357
|
+
get graphDataSource(): GraphDataSource {
|
|
358
|
+
if (!this._graphDataSource) this._graphDataSource = new GraphDataSource(this);
|
|
359
|
+
return this._graphDataSource;
|
|
465
360
|
}
|
|
466
361
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
id: dn.id ?? '',
|
|
470
|
-
names: dn.names || (dn.label ? { eng: dn.label } : {}),
|
|
471
|
-
conceptCount: dn.conceptCount || 0,
|
|
472
|
-
};
|
|
473
|
-
if (dn.children && dn.children.length > 0) {
|
|
474
|
-
node.children = dn.children.map((c) => this.mapSectionNode(c));
|
|
475
|
-
}
|
|
476
|
-
return node;
|
|
362
|
+
extractEdges(concept: Concept): GraphEdge[] {
|
|
363
|
+
return this.graphDataSource.extractEdges(concept);
|
|
477
364
|
}
|
|
478
365
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (!nodes || nodes.length === 0) return [];
|
|
482
|
-
return nodes.map(s => this.mapManifestSection(s));
|
|
366
|
+
extractDomainEdges(concept: Concept): GraphEdge[] {
|
|
367
|
+
return this.graphDataSource.extractDomainEdges(concept);
|
|
483
368
|
}
|
|
484
369
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (s.children && s.children.length > 0) {
|
|
488
|
-
node.children = s.children.map(c => this.mapManifestSection(c));
|
|
489
|
-
}
|
|
490
|
-
return node;
|
|
370
|
+
async loadDomainNodes(): Promise<GraphNode[]> {
|
|
371
|
+
return this.graphDataSource.loadDomainNodes();
|
|
491
372
|
}
|
|
492
373
|
|
|
493
374
|
async loadEdgeIndex(): Promise<GraphEdge[]> {
|
|
494
|
-
|
|
495
|
-
if (!resp.ok) return [];
|
|
496
|
-
const data = await resp.json();
|
|
497
|
-
return data.edges ?? [];
|
|
375
|
+
return this.graphDataSource.loadEdgeIndex();
|
|
498
376
|
}
|
|
499
377
|
|
|
500
378
|
async loadGraphNodes(): Promise<{ uriPrefix: string; nodes: [string, Record<string, string>, string][] }> {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
379
|
+
return this.graphDataSource.loadGraphNodes();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
getSectionTree(): SectionNode[] {
|
|
383
|
+
return this.graphDataSource.getSectionTree();
|
|
504
384
|
}
|
|
505
385
|
|
|
506
386
|
getLanguages(): string[] {
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { GraphEdge, GraphNode, SectionNode } from './types';
|
|
2
|
+
import type { Concept, RelatedConcept } from 'glossarist';
|
|
3
|
+
import type { DatasetAdapter } from './DatasetAdapter';
|
|
4
|
+
import { ReferenceResolver } from './ReferenceResolver';
|
|
5
|
+
import { slugify } from '../utils/slugify';
|
|
6
|
+
|
|
7
|
+
interface DomainNodeJson {
|
|
8
|
+
uri?: string;
|
|
9
|
+
id?: string;
|
|
10
|
+
registerId?: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
names?: Record<string, string>;
|
|
13
|
+
conceptCount?: number;
|
|
14
|
+
children?: DomainNodeJson[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SectionJson {
|
|
18
|
+
id: string;
|
|
19
|
+
names?: Record<string, string>;
|
|
20
|
+
children?: SectionJson[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveRefTarget(rc: RelatedConcept, uriBase: string, registerId: string, urnMap?: ReadonlyMap<string, string>): string {
|
|
24
|
+
if (!rc.ref) return '';
|
|
25
|
+
const ref = rc.ref;
|
|
26
|
+
if (ref.id) {
|
|
27
|
+
let reg = registerId;
|
|
28
|
+
if (ref.source && !ref.source.startsWith('http')) {
|
|
29
|
+
reg = urnMap?.get(ref.source) ?? ref.source;
|
|
30
|
+
}
|
|
31
|
+
return `${uriBase}/${reg}/concept/${ref.id}`;
|
|
32
|
+
}
|
|
33
|
+
if (ref.source && ref.source.startsWith('http')) return ref.source;
|
|
34
|
+
return ref.source || '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class GraphDataSource {
|
|
38
|
+
constructor(private adapter: DatasetAdapter) {}
|
|
39
|
+
|
|
40
|
+
private get baseUrl(): string {
|
|
41
|
+
return this.adapter.dataUrl;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private get registerId(): string {
|
|
45
|
+
return this.adapter.registerId;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private get uriBase(): string {
|
|
49
|
+
return this.adapter.manifest?.uriBase || 'https://glossarist.org';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private get urnMap(): ReadonlyMap<string, string> {
|
|
53
|
+
return this.adapter.urnMap;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async loadEdgeIndex(): Promise<GraphEdge[]> {
|
|
57
|
+
const resp = await fetch(`${this.baseUrl}/edges.json`);
|
|
58
|
+
if (!resp.ok) return [];
|
|
59
|
+
const data = await resp.json();
|
|
60
|
+
return data.edges ?? [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async loadGraphNodes(): Promise<{ uriPrefix: string; nodes: [string, Record<string, string>, string][] }> {
|
|
64
|
+
const resp = await fetch(`${this.baseUrl}/graph-nodes.json`);
|
|
65
|
+
if (!resp.ok) return { uriPrefix: '', nodes: [] };
|
|
66
|
+
return await resp.json();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async loadDomainNodes(): Promise<GraphNode[]> {
|
|
70
|
+
const resp = await fetch(`${this.baseUrl}/domain-nodes.json`);
|
|
71
|
+
if (!resp.ok) return [];
|
|
72
|
+
const data = await resp.json();
|
|
73
|
+
return (data.domainNodes || []).map((dn: DomainNodeJson) => this.mapDomainNode(dn));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
extractEdges(concept: Concept): GraphEdge[] {
|
|
77
|
+
const edges: GraphEdge[] = [];
|
|
78
|
+
const sourceUri = concept.uri || `${this.uriBase}/${this.registerId}/concept/${concept.id}`;
|
|
79
|
+
|
|
80
|
+
for (const rc of concept.relatedConcepts) {
|
|
81
|
+
const target = resolveRefTarget(rc, this.uriBase, this.registerId, this.urnMap);
|
|
82
|
+
if (target && target !== sourceUri) {
|
|
83
|
+
const parsed = ReferenceResolver.parseUri(target);
|
|
84
|
+
edges.push({
|
|
85
|
+
source: sourceUri,
|
|
86
|
+
target,
|
|
87
|
+
type: rc.type || 'references',
|
|
88
|
+
label: rc.content || undefined,
|
|
89
|
+
register: parsed?.registerId ?? this.registerId,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const lang of concept.languages) {
|
|
95
|
+
const lc = concept.localization(lang);
|
|
96
|
+
if (!lc) continue;
|
|
97
|
+
for (const rc of lc.related) {
|
|
98
|
+
const target = resolveRefTarget(rc, this.uriBase, this.registerId, this.urnMap);
|
|
99
|
+
if (target && target !== sourceUri) {
|
|
100
|
+
const parsed = ReferenceResolver.parseUri(target);
|
|
101
|
+
edges.push({
|
|
102
|
+
source: sourceUri,
|
|
103
|
+
target,
|
|
104
|
+
type: rc.type || 'references',
|
|
105
|
+
label: rc.content || undefined,
|
|
106
|
+
register: parsed?.registerId ?? this.registerId,
|
|
107
|
+
lang,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return edges;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
extractDomainEdges(concept: Concept): GraphEdge[] {
|
|
117
|
+
const edges: GraphEdge[] = [];
|
|
118
|
+
const sourceUri = concept.uri || `${this.uriBase}/${this.registerId}/concept/${concept.id}`;
|
|
119
|
+
|
|
120
|
+
for (const lang of concept.languages) {
|
|
121
|
+
const lc = concept.localization(lang);
|
|
122
|
+
if (lc?.domain) {
|
|
123
|
+
edges.push({
|
|
124
|
+
source: sourceUri,
|
|
125
|
+
target: `${this.uriBase}/${this.registerId}/domain/${slugify(lc.domain)}`,
|
|
126
|
+
type: 'domain',
|
|
127
|
+
label: lc.domain,
|
|
128
|
+
register: this.registerId,
|
|
129
|
+
lang,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return edges;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getSectionTree(): SectionNode[] {
|
|
137
|
+
const nodes = this.adapter.manifest?.sections;
|
|
138
|
+
if (!nodes || nodes.length === 0) return [];
|
|
139
|
+
return nodes.map(s => this.mapManifestSection(s));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private mapDomainNode(dn: DomainNodeJson): GraphNode {
|
|
143
|
+
const node: GraphNode = {
|
|
144
|
+
uri: dn.uri ?? '',
|
|
145
|
+
register: dn.registerId ?? '',
|
|
146
|
+
conceptId: dn.uri?.split('/domain/')[1] || dn.id || '',
|
|
147
|
+
designations: dn.names || (dn.label ? { eng: dn.label } : {}),
|
|
148
|
+
status: 'domain',
|
|
149
|
+
loaded: true,
|
|
150
|
+
nodeType: 'domain' as const,
|
|
151
|
+
conceptCount: dn.conceptCount || 0,
|
|
152
|
+
};
|
|
153
|
+
if (dn.children && dn.children.length > 0) {
|
|
154
|
+
node.children = dn.children.map((c) => this.mapSectionNode(c));
|
|
155
|
+
}
|
|
156
|
+
return node;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private mapSectionNode(dn: DomainNodeJson): SectionNode {
|
|
160
|
+
const node: SectionNode = {
|
|
161
|
+
id: dn.id ?? '',
|
|
162
|
+
names: dn.names || (dn.label ? { eng: dn.label } : {}),
|
|
163
|
+
conceptCount: dn.conceptCount || 0,
|
|
164
|
+
};
|
|
165
|
+
if (dn.children && dn.children.length > 0) {
|
|
166
|
+
node.children = dn.children.map((c) => this.mapSectionNode(c));
|
|
167
|
+
}
|
|
168
|
+
return node;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private mapManifestSection(s: SectionJson): SectionNode {
|
|
172
|
+
const node: SectionNode = { id: s.id, names: s.names || {}, conceptCount: 0 };
|
|
173
|
+
if (s.children && s.children.length > 0) {
|
|
174
|
+
node.children = s.children.map(c => this.mapManifestSection(c));
|
|
175
|
+
}
|
|
176
|
+
return node;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -32,6 +32,13 @@ export class ReferenceResolver {
|
|
|
32
32
|
private routing: RoutingEntry[] = [];
|
|
33
33
|
private sourceRefs = new Map<string, { datasetId: string; uriPrefix: string }>();
|
|
34
34
|
|
|
35
|
+
private static readonly URI_REGISTER_RE = /\/([^/]+)\/concept\/([^/]+)$/;
|
|
36
|
+
|
|
37
|
+
static parseUri(uri: string): { registerId: string; conceptId: string } | null {
|
|
38
|
+
const m = uri.match(ReferenceResolver.URI_REGISTER_RE);
|
|
39
|
+
return m ? { registerId: m[1], conceptId: m[2] } : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
registerDataset(id: string, uriPatterns: string[]): void {
|
|
36
43
|
this.datasets.push({ id, uriPatterns });
|
|
37
44
|
}
|
|
@@ -115,6 +122,23 @@ export class ReferenceResolver {
|
|
|
115
122
|
return null;
|
|
116
123
|
}
|
|
117
124
|
|
|
125
|
+
resolveRelatedRef(ref: { source: string | null; id: string | null } | null, sourceDatasetId?: string): { registerId: string; conceptId: string } | null {
|
|
126
|
+
if (!ref?.source || !ref?.id) return null;
|
|
127
|
+
const uri = `${ref.source}/${ref.id}`;
|
|
128
|
+
const resolution = this.resolveReference(uri, sourceDatasetId);
|
|
129
|
+
if (resolution.type === 'internal') {
|
|
130
|
+
return { registerId: resolution.registerId, conceptId: resolution.conceptId.replace(/^\//, '') };
|
|
131
|
+
}
|
|
132
|
+
if (ref.source.startsWith('urn:')) {
|
|
133
|
+
const directUri = ref.source + ref.id;
|
|
134
|
+
const directRes = this.resolveReference(directUri, sourceDatasetId);
|
|
135
|
+
if (directRes.type === 'internal') {
|
|
136
|
+
return { registerId: directRes.registerId, conceptId: directRes.conceptId.replace(/^\//, '') };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
118
142
|
private extractConceptIdFromRouting(uri: string, pattern: string): string | null {
|
|
119
143
|
for (const ds of this.datasets) {
|
|
120
144
|
for (const dsPattern of ds.uriPatterns) {
|
package/src/adapters/factory.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { DatasetRegistry, Manifest, Resolution } from './types';
|
|
2
2
|
import type { RoutingEntry as ConfigRoutingEntry } from '../config/types';
|
|
3
3
|
import { DatasetAdapter } from './DatasetAdapter';
|
|
4
|
-
import { UriRouter } from './UriRouter';
|
|
5
4
|
import { ReferenceResolver } from './ReferenceResolver';
|
|
6
5
|
|
|
7
6
|
export class AdapterFactory {
|
|
8
7
|
private adapters = new Map<string, DatasetAdapter>();
|
|
9
8
|
private urnMap = new Map<string, string>();
|
|
10
|
-
readonly router = new UriRouter();
|
|
11
9
|
readonly resolver: ReferenceResolver;
|
|
12
10
|
private crossRefIndex: Record<string, string[]> | null = null;
|
|
13
11
|
|
|
@@ -76,8 +74,6 @@ export class AdapterFactory {
|
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
private registerDataset(registerId: string, manifest: Manifest): void {
|
|
79
|
-
this.router.registerDataset(registerId, `${import.meta.env.BASE_URL}data/${registerId}`, manifest);
|
|
80
|
-
|
|
81
77
|
const uriPatterns = [
|
|
82
78
|
manifest.datasetUri,
|
|
83
79
|
...(manifest.uriAliases ?? []),
|
|
@@ -117,20 +113,7 @@ export class AdapterFactory {
|
|
|
117
113
|
}
|
|
118
114
|
|
|
119
115
|
resolveRelatedRef(ref: { source: string | null; id: string | null } | null, sourceDatasetId?: string): { registerId: string; conceptId: string } | null {
|
|
120
|
-
|
|
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;
|
|
116
|
+
return this.resolver.resolveRelatedRef(ref, sourceDatasetId);
|
|
134
117
|
}
|
|
135
118
|
|
|
136
119
|
resolveCitation(source: string, referenceFrom: string, sourceDatasetId?: string): Resolution | null {
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Concept, LocalizedConcept, Designation
|
|
2
|
+
import type { Concept, LocalizedConcept, Designation } from 'glossarist';
|
|
3
3
|
import type { Manifest, GraphEdge } from '../adapters/types';
|
|
4
4
|
import { computed, ref, nextTick, watch } from 'vue';
|
|
5
|
-
import { langName
|
|
6
|
-
import { renderMath
|
|
5
|
+
import { langName } from '../utils/lang';
|
|
6
|
+
import { renderMath } from '../utils/math';
|
|
7
7
|
import type { RenderOptions } from '../utils/math';
|
|
8
8
|
import { escapeAttr } from '../utils/escape';
|
|
9
9
|
import { entryStatusColor, conceptStatusColor, conceptStatusLabel, conceptStatusDefinition, entryStatusLabel, entryStatusDefinition, getPreferredTerm } from '../utils/concept-helpers';
|
|
10
10
|
import { sourceTypeInfo, sourceStatusInfo } from '../utils/designation-registry';
|
|
11
|
-
import { conceptUri
|
|
11
|
+
import { conceptUri } from '../adapters/model-bridge';
|
|
12
12
|
import { useRouter } from 'vue-router';
|
|
13
13
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
14
14
|
import { useDsStyle } from '../utils/dataset-style';
|
|
15
15
|
import { getFactory } from '../adapters/factory';
|
|
16
16
|
import { useRenderOptions } from '../composables/use-render-options';
|
|
17
17
|
import { useConceptEdges } from '../composables/use-concept-edges';
|
|
18
|
-
import { useConceptContent
|
|
18
|
+
import { useConceptContent } from '../composables/use-concept-content';
|
|
19
19
|
import { relationshipLabel, INVERSE_RELATIONSHIPS } from '../utils/relationship-categories';
|
|
20
20
|
import { slugify } from '../utils/slugify';
|
|
21
21
|
import { useSiteConfig } from '../config/use-site-config';
|
|
@@ -72,7 +72,7 @@ const {
|
|
|
72
72
|
relatedLabel,
|
|
73
73
|
navigateEdge,
|
|
74
74
|
navigateRelated,
|
|
75
|
-
} = useConceptEdges(conceptComputed, registerIdComputed, manifestComputed, edgesComputed);
|
|
75
|
+
} = useConceptEdges(conceptComputed, registerIdComputed, manifestComputed, edgesComputed, router);
|
|
76
76
|
|
|
77
77
|
const uriCopied = ref(false);
|
|
78
78
|
function copyUri() {
|
|
@@ -83,21 +83,6 @@ function copyUri() {
|
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
const languages = computed(() => {
|
|
87
|
-
const sorted = sortLanguages(props.concept.languages, props.manifest.languageOrder);
|
|
88
|
-
// Put current UI locale first
|
|
89
|
-
const current = locale.value;
|
|
90
|
-
const idx = sorted.indexOf(current);
|
|
91
|
-
if (idx > 0) {
|
|
92
|
-
sorted.splice(idx, 1);
|
|
93
|
-
sorted.unshift(current);
|
|
94
|
-
}
|
|
95
|
-
return sorted;
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Collapsible language sections — expand all with content, collapse those without
|
|
99
|
-
const collapsedLangs = ref(new Set<string>());
|
|
100
|
-
|
|
101
86
|
const engConcept = computed((): LocalizedConcept | null => {
|
|
102
87
|
return props.concept.localization('eng') ?? null;
|
|
103
88
|
});
|
|
@@ -115,12 +100,10 @@ const conceptSources = computed(() => props.concept.sources);
|
|
|
115
100
|
|
|
116
101
|
const conceptTags = computed(() => props.concept.tags ?? []);
|
|
117
102
|
|
|
118
|
-
// Cross-reference resolver: generates clickable links for inline refs
|
|
119
|
-
|
|
120
103
|
const factory = getFactory();
|
|
121
104
|
const { ensureBibLoaded, bibResolver, figResolver } = useRenderOptions(() => props.registerId);
|
|
122
105
|
|
|
123
|
-
const renderOpts
|
|
106
|
+
const renderOpts = computed<RenderOptions>(() => ({
|
|
124
107
|
xrefResolver: (uri, term) => {
|
|
125
108
|
const resolution = factory.resolve(uri, props.registerId);
|
|
126
109
|
if (resolution.type === 'internal') {
|
|
@@ -139,11 +122,10 @@ const renderOpts: RenderOptions = {
|
|
|
139
122
|
},
|
|
140
123
|
bibResolver,
|
|
141
124
|
figResolver,
|
|
142
|
-
};
|
|
125
|
+
}));
|
|
143
126
|
|
|
144
127
|
watch(() => props.registerId, () => { ensureBibLoaded(); }, { immediate: true });
|
|
145
128
|
|
|
146
|
-
// Handle clicks on cross-reference links via event delegation
|
|
147
129
|
function handleContentClick(e: MouseEvent) {
|
|
148
130
|
const target = (e.target as HTMLElement).closest('.xref-link') as HTMLElement | null;
|
|
149
131
|
if (!target) return;
|
|
@@ -155,86 +137,18 @@ function handleContentClick(e: MouseEvent) {
|
|
|
155
137
|
}
|
|
156
138
|
}
|
|
157
139
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const examples = lc.examples.map(e => e.content).filter(Boolean);
|
|
171
|
-
|
|
172
|
-
result.push({
|
|
173
|
-
lang,
|
|
174
|
-
lc,
|
|
175
|
-
renderedTerm: renderMath(getPreferredTerm(lc, '')),
|
|
176
|
-
definition,
|
|
177
|
-
renderedDefinition: renderMath(definition, renderOpts),
|
|
178
|
-
annotations,
|
|
179
|
-
renderedAnnotations: annotations.map((a: string) => renderMath(a, renderOpts)),
|
|
180
|
-
notes,
|
|
181
|
-
renderedNotes: notes.map(n => renderMath(n, renderOpts)),
|
|
182
|
-
examples,
|
|
183
|
-
renderedExamples: examples.map(e => renderMath(e, renderOpts)),
|
|
184
|
-
sources: lc.sources,
|
|
185
|
-
designations: lc.terms,
|
|
186
|
-
renderedDesignations: new Map(lc.terms.map(d => [d.designation, renderMath(d.designation)])),
|
|
187
|
-
entryStatus: lc.entryStatus ?? '',
|
|
188
|
-
classification: lc.classification,
|
|
189
|
-
reviewType: lc.reviewType,
|
|
190
|
-
release: lc.release,
|
|
191
|
-
lineageSourceSimilarity: lc.lineageSourceSimilarity,
|
|
192
|
-
lcScript: lc.script,
|
|
193
|
-
lcSystem: lc.system,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
return result;
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
const langContentMap = computed(() => {
|
|
200
|
-
const map = new Map<string, LangContent>();
|
|
201
|
-
for (const lc of allLangContent.value) map.set(lc.lang, lc);
|
|
202
|
-
return map;
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
function hasContent(lc: LangContent): boolean {
|
|
206
|
-
return !!(lc.definition || lc.annotations.length || lc.notes.length || lc.examples.length || lc.sources.length);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function initCollapsed() {
|
|
210
|
-
const mainLangs = siteConfig.value?.defaults?.mainLanguages || [];
|
|
211
|
-
const mainSet = new Set(mainLangs.length ? mainLangs : ['eng']);
|
|
212
|
-
const collapsed = new Set<string>();
|
|
213
|
-
for (const lc of allLangContent.value) {
|
|
214
|
-
if (!hasContent(lc) && !mainSet.has(lc.lang)) {
|
|
215
|
-
collapsed.add(lc.lang);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
collapsedLangs.value = collapsed;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
watch(languages, () => { initCollapsed(); }, { immediate: true });
|
|
222
|
-
|
|
223
|
-
const allCollapsed = computed(() => collapsedLangs.value.size === allLangContent.value.length);
|
|
224
|
-
|
|
225
|
-
function toggleLang(lang: string) {
|
|
226
|
-
const s = new Set(collapsedLangs.value);
|
|
227
|
-
if (s.has(lang)) s.delete(lang); else s.add(lang);
|
|
228
|
-
collapsedLangs.value = s;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function toggleAll() {
|
|
232
|
-
if (allCollapsed.value) {
|
|
233
|
-
collapsedLangs.value = new Set();
|
|
234
|
-
} else {
|
|
235
|
-
collapsedLangs.value = new Set(allLangContent.value.map(lc => lc.lang));
|
|
236
|
-
}
|
|
237
|
-
}
|
|
140
|
+
const {
|
|
141
|
+
languages,
|
|
142
|
+
allLangContent,
|
|
143
|
+
langContentMap,
|
|
144
|
+
hasContent,
|
|
145
|
+
collapsedLangs,
|
|
146
|
+
allCollapsed,
|
|
147
|
+
toggleLang,
|
|
148
|
+
toggleAll,
|
|
149
|
+
plainTruncate,
|
|
150
|
+
orderedDesignations,
|
|
151
|
+
} = useConceptContent(conceptComputed, manifestComputed, renderOpts);
|
|
238
152
|
|
|
239
153
|
function scrollToLang(lang: string) {
|
|
240
154
|
if (collapsedLangs.value.has(lang)) {
|
|
@@ -263,14 +177,6 @@ function getDesignationsForLang(lang: string): Designation[] {
|
|
|
263
177
|
return lc?.terms ?? [];
|
|
264
178
|
}
|
|
265
179
|
|
|
266
|
-
function orderedDesignations(lang: string): Designation[] {
|
|
267
|
-
const desigs = getDesignationsForLang(lang);
|
|
268
|
-
const preferred = desigs.filter(d => d.normativeStatus === 'preferred');
|
|
269
|
-
const admitted = desigs.filter(d => d.normativeStatus === 'admitted' || d.normativeStatus === 'deprecated');
|
|
270
|
-
const rest = desigs.filter(d => d.normativeStatus !== 'preferred' && d.normativeStatus !== 'admitted' && d.normativeStatus !== 'deprecated');
|
|
271
|
-
return [...preferred, ...admitted, ...rest];
|
|
272
|
-
}
|
|
273
|
-
|
|
274
180
|
function hasDefinition(lang: string): boolean {
|
|
275
181
|
const lc = props.concept.localization(lang);
|
|
276
182
|
if (!lc) return false;
|
|
@@ -282,16 +188,9 @@ function goAdjacent(id: string) {
|
|
|
282
188
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
283
189
|
}
|
|
284
190
|
|
|
285
|
-
function plainTruncate(html: string, max: number = 120): string {
|
|
286
|
-
const text = cleanContent(html).replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
287
|
-
return text.length <= max ? text : text.slice(0, max).trimEnd() + '…';
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Domain rendering: merge ConceptReference domains and per-localization domain strings
|
|
291
191
|
const conceptDomains = computed(() => {
|
|
292
192
|
const domainMap = new Map<string, { slug: string; label: string; langs: string[]; conceptId?: string }>();
|
|
293
193
|
|
|
294
|
-
// Managed concept level ConceptReference domains (authoritative)
|
|
295
194
|
for (const ref of conceptRefDomains.value) {
|
|
296
195
|
const id = ref.conceptId ?? '';
|
|
297
196
|
const label = id || ref.urn || '';
|
|
@@ -301,7 +200,6 @@ const conceptDomains = computed(() => {
|
|
|
301
200
|
}
|
|
302
201
|
}
|
|
303
202
|
|
|
304
|
-
// Per-localization domain strings
|
|
305
203
|
for (const lang of props.concept.languages) {
|
|
306
204
|
const lc = props.concept.localization(lang);
|
|
307
205
|
const domain = lc?.domain;
|
|
@@ -318,7 +216,6 @@ const conceptDomains = computed(() => {
|
|
|
318
216
|
return [...domainMap.values()].sort((a, b) => b.langs.length - a.langs.length);
|
|
319
217
|
});
|
|
320
218
|
|
|
321
|
-
// Non-verbal reps: aggregate across all localizations
|
|
322
219
|
const nonVerbalReps = computed(() => {
|
|
323
220
|
const reps: typeof import('glossarist').NonVerbRep.prototype[] = [];
|
|
324
221
|
for (const lang of props.concept.languages) {
|
|
@@ -329,7 +226,6 @@ const nonVerbalReps = computed(() => {
|
|
|
329
226
|
}
|
|
330
227
|
return reps;
|
|
331
228
|
});
|
|
332
|
-
|
|
333
229
|
</script>
|
|
334
230
|
|
|
335
231
|
<template>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computed, type ComputedRef } from 'vue';
|
|
2
|
+
import type { Router } from 'vue-router';
|
|
2
3
|
import type { Concept, RelatedConcept } from 'glossarist';
|
|
3
4
|
import type { Manifest, GraphEdge } from '../adapters/types';
|
|
4
5
|
import { getFactory } from '../adapters/factory';
|
|
@@ -24,6 +25,7 @@ export function useConceptEdges(
|
|
|
24
25
|
registerId: ComputedRef<string>,
|
|
25
26
|
manifest: ComputedRef<Manifest>,
|
|
26
27
|
edges: ComputedRef<GraphEdge[]>,
|
|
28
|
+
router: Router,
|
|
27
29
|
) {
|
|
28
30
|
const factory = getFactory();
|
|
29
31
|
const store = useVocabularyStore();
|
|
@@ -145,7 +147,6 @@ export function useConceptEdges(
|
|
|
145
147
|
async function navigateEdge(edge: GraphEdge) {
|
|
146
148
|
const uri = edge.source === conceptUriValue.value ? edge.target : edge.source;
|
|
147
149
|
const resolution = factory.resolve(uri);
|
|
148
|
-
const router = (await import('vue-router')).useRouter();
|
|
149
150
|
|
|
150
151
|
if (resolution.type === 'internal') {
|
|
151
152
|
router.push({ name: 'concept', params: { registerId: resolution.registerId, conceptId: resolution.conceptId } });
|
|
@@ -159,7 +160,6 @@ export function useConceptEdges(
|
|
|
159
160
|
async function navigateRelated(ref: { source: string | null; id: string | null }) {
|
|
160
161
|
const target = resolveRelatedRef(ref);
|
|
161
162
|
if (!target) return;
|
|
162
|
-
const router = (await import('vue-router')).useRouter();
|
|
163
163
|
router.push({ name: 'concept', params: { registerId: target.registerId, conceptId: target.conceptId } });
|
|
164
164
|
}
|
|
165
165
|
|
|
@@ -26,51 +26,6 @@ export function compactToSlug(compact: string): string {
|
|
|
26
26
|
return compact.replace(/:/g, '-');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const expandedClasses = ref(new Set<string>(['gloss:Designation']));
|
|
30
|
-
|
|
31
|
-
const collapsedSections = ref(new Set<string>([
|
|
32
|
-
'objectProperty',
|
|
33
|
-
'datatypeProperty',
|
|
34
|
-
'shape',
|
|
35
|
-
'taxonomy',
|
|
36
|
-
'namedIndividual',
|
|
37
|
-
'annotationProperty',
|
|
38
|
-
]));
|
|
39
|
-
|
|
40
|
-
const searchQuery = ref('');
|
|
41
|
-
|
|
42
|
-
function toggleExpand(cls: OwlClass) {
|
|
43
|
-
const s = new Set(expandedClasses.value);
|
|
44
|
-
if (s.has(cls.compact)) s.delete(cls.compact);
|
|
45
|
-
else s.add(cls.compact);
|
|
46
|
-
expandedClasses.value = s;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function toggleSection(key: string) {
|
|
50
|
-
const s = new Set(collapsedSections.value);
|
|
51
|
-
if (s.has(key)) s.delete(key);
|
|
52
|
-
else s.add(key);
|
|
53
|
-
collapsedSections.value = s;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function expandAllSections() {
|
|
57
|
-
collapsedSections.value = new Set();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function collapseAllSections() {
|
|
61
|
-
collapsedSections.value = new Set(['objectProperty', 'datatypeProperty', 'shape', 'taxonomy', 'class', 'namedIndividual', 'annotationProperty']);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function childClasses(parentId: string): OwlClass[] {
|
|
65
|
-
const cls = getClass(parentId);
|
|
66
|
-
if (!cls) return [];
|
|
67
|
-
return cls.children.map(id => getClass(id)).filter((c): c is OwlClass => !!c);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function hasChildren(cls: OwlClass): boolean {
|
|
71
|
-
return cls.children.length > 0;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
29
|
const treeRoots = getClassTree();
|
|
75
30
|
const allShapes = getAllShapes();
|
|
76
31
|
const objectProperties = getObjectProperties();
|
|
@@ -97,28 +52,6 @@ const taxonomyLabels: Record<string, string> = {
|
|
|
97
52
|
grammarNumber: 'Grammar Number',
|
|
98
53
|
};
|
|
99
54
|
|
|
100
|
-
interface IndividualGroup {
|
|
101
|
-
key: string;
|
|
102
|
-
label: string;
|
|
103
|
-
concepts: { id: string; prefLabel: string }[];
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const groupedIndividuals = computed<IndividualGroup[]>(() => {
|
|
107
|
-
return taxonomyKeys.map(key => {
|
|
108
|
-
const tax = (taxonomyData as Record<string, any>)[key];
|
|
109
|
-
if (!tax) return { key, label: taxonomyLabels[key] || key, concepts: [] };
|
|
110
|
-
const concepts = Object.values(tax.concepts as Record<string, any>).map((c: any) => ({
|
|
111
|
-
id: c.id,
|
|
112
|
-
prefLabel: c.prefLabel,
|
|
113
|
-
}));
|
|
114
|
-
return { key, label: tax.schemeLabel || taxonomyLabels[key] || key, concepts };
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const totalIndividuals = computed(() =>
|
|
119
|
-
groupedIndividuals.value.reduce((sum, g) => sum + g.concepts.length, 0),
|
|
120
|
-
);
|
|
121
|
-
|
|
122
55
|
const valuesToTaxonomy: Record<string, string> = {
|
|
123
56
|
'gloss:status': 'conceptStatus',
|
|
124
57
|
'gloss:entstatus': 'entryStatus',
|
|
@@ -132,6 +65,20 @@ const valuesToTaxonomy: Record<string, string> = {
|
|
|
132
65
|
'gloss:number': 'grammarNumber',
|
|
133
66
|
};
|
|
134
67
|
|
|
68
|
+
function childClasses(parentId: string): OwlClass[] {
|
|
69
|
+
const cls = getClass(parentId);
|
|
70
|
+
if (!cls) return [];
|
|
71
|
+
return cls.children.map(id => getClass(id)).filter((c): c is OwlClass => !!c);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function hasChildren(cls: OwlClass): boolean {
|
|
75
|
+
return cls.children.length > 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function matchesSearch(text: string, query: string): boolean {
|
|
79
|
+
return text.toLowerCase().includes(query.toLowerCase());
|
|
80
|
+
}
|
|
81
|
+
|
|
135
82
|
function taxonomyKeyForValuesFrom(valuesFrom: string | null): string | null {
|
|
136
83
|
if (!valuesFrom) return null;
|
|
137
84
|
return valuesToTaxonomy[valuesFrom] ?? null;
|
|
@@ -145,80 +92,126 @@ function getShapesForTaxonomy(taxonomyKey: string): OwlShape[] {
|
|
|
145
92
|
);
|
|
146
93
|
}
|
|
147
94
|
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
95
|
+
export function useOntologyNav() {
|
|
96
|
+
const expandedClasses = ref(new Set<string>(['gloss:Designation']));
|
|
97
|
+
|
|
98
|
+
const collapsedSections = ref(new Set<string>([
|
|
99
|
+
'objectProperty',
|
|
100
|
+
'datatypeProperty',
|
|
101
|
+
'shape',
|
|
102
|
+
'taxonomy',
|
|
103
|
+
'namedIndividual',
|
|
104
|
+
'annotationProperty',
|
|
105
|
+
]));
|
|
106
|
+
|
|
107
|
+
const searchQuery = ref('');
|
|
108
|
+
|
|
109
|
+
function toggleExpand(cls: OwlClass) {
|
|
110
|
+
const s = new Set(expandedClasses.value);
|
|
111
|
+
if (s.has(cls.compact)) s.delete(cls.compact);
|
|
112
|
+
else s.add(cls.compact);
|
|
113
|
+
expandedClasses.value = s;
|
|
157
114
|
}
|
|
158
|
-
walk(treeRoots, 0);
|
|
159
|
-
return items;
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
function matchesSearch(text: string, query: string): boolean {
|
|
163
|
-
return text.toLowerCase().includes(query.toLowerCase());
|
|
164
|
-
}
|
|
165
115
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const matchedObjectProps: OwlProperty[] = [];
|
|
172
|
-
const matchedDatatypeProps: OwlProperty[] = [];
|
|
173
|
-
const matchedShapes: OwlShape[] = [];
|
|
174
|
-
const matchedIndividuals: { group: string; id: string; prefLabel: string }[] = [];
|
|
175
|
-
const matchedAnnotationProps: AnnotationProperty[] = [];
|
|
176
|
-
|
|
177
|
-
// Walk all classes (tree + leaves)
|
|
178
|
-
function walkAll(classes: OwlClass[]) {
|
|
179
|
-
for (const cls of classes) {
|
|
180
|
-
if (matchesSearch(cls.label, q) || matchesSearch(cls.compact, q)) {
|
|
181
|
-
matchedClasses.push(cls);
|
|
182
|
-
}
|
|
183
|
-
walkAll(childClasses(cls.compact));
|
|
184
|
-
}
|
|
116
|
+
function toggleSection(key: string) {
|
|
117
|
+
const s = new Set(collapsedSections.value);
|
|
118
|
+
if (s.has(key)) s.delete(key);
|
|
119
|
+
else s.add(key);
|
|
120
|
+
collapsedSections.value = s;
|
|
185
121
|
}
|
|
186
|
-
walkAll(treeRoots);
|
|
187
122
|
|
|
188
|
-
|
|
189
|
-
|
|
123
|
+
function expandAllSections() {
|
|
124
|
+
collapsedSections.value = new Set();
|
|
190
125
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
for (const s of allShapes) {
|
|
195
|
-
if (matchesSearch(s.label, q) || matchesSearch(s.compact, q)) matchedShapes.push(s);
|
|
126
|
+
|
|
127
|
+
function collapseAllSections() {
|
|
128
|
+
collapsedSections.value = new Set(['objectProperty', 'datatypeProperty', 'shape', 'taxonomy', 'class', 'namedIndividual', 'annotationProperty']);
|
|
196
129
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
130
|
+
|
|
131
|
+
const allNavItems = computed(() => {
|
|
132
|
+
const items: { id: string; label: string; depth: number }[] = [];
|
|
133
|
+
function walk(classes: OwlClass[], depth: number) {
|
|
134
|
+
for (const cls of classes) {
|
|
135
|
+
items.push({ id: cls.compact, label: cls.label, depth });
|
|
136
|
+
if (expandedClasses.value.has(cls.compact)) {
|
|
137
|
+
walk(childClasses(cls.compact), depth + 1);
|
|
138
|
+
}
|
|
201
139
|
}
|
|
202
140
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
141
|
+
walk(treeRoots, 0);
|
|
142
|
+
return items;
|
|
143
|
+
});
|
|
207
144
|
|
|
208
|
-
const
|
|
145
|
+
const searchResults = computed(() => {
|
|
146
|
+
const q = searchQuery.value.trim();
|
|
147
|
+
if (!q) return null;
|
|
148
|
+
|
|
149
|
+
const matchedClasses: OwlClass[] = [];
|
|
150
|
+
const matchedObjectProps: OwlProperty[] = [];
|
|
151
|
+
const matchedDatatypeProps: OwlProperty[] = [];
|
|
152
|
+
const matchedShapes: OwlShape[] = [];
|
|
153
|
+
const matchedIndividuals: { group: string; id: string; prefLabel: string }[] = [];
|
|
154
|
+
const matchedAnnotationProps: AnnotationProperty[] = [];
|
|
155
|
+
|
|
156
|
+
function walkAll(classes: OwlClass[]) {
|
|
157
|
+
for (const cls of classes) {
|
|
158
|
+
if (matchesSearch(cls.label, q) || matchesSearch(cls.compact, q)) {
|
|
159
|
+
matchedClasses.push(cls);
|
|
160
|
+
}
|
|
161
|
+
walkAll(childClasses(cls.compact));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
walkAll(treeRoots);
|
|
209
165
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
166
|
+
for (const p of objectProperties) {
|
|
167
|
+
if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedObjectProps.push(p);
|
|
168
|
+
}
|
|
169
|
+
for (const p of datatypeProperties) {
|
|
170
|
+
if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedDatatypeProps.push(p);
|
|
171
|
+
}
|
|
172
|
+
for (const s of allShapes) {
|
|
173
|
+
if (matchesSearch(s.label, q) || matchesSearch(s.compact, q)) matchedShapes.push(s);
|
|
174
|
+
}
|
|
175
|
+
for (const g of groupedIndividuals.value) {
|
|
176
|
+
for (const c of g.concepts) {
|
|
177
|
+
if (matchesSearch(c.prefLabel, q) || matchesSearch(c.id, q)) {
|
|
178
|
+
matchedIndividuals.push({ group: g.key, id: c.id, prefLabel: c.prefLabel });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const ap of annotationProperties) {
|
|
183
|
+
if (matchesSearch(ap.label, q) || matchesSearch(ap.compact, q)) matchedAnnotationProps.push(ap);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const total = matchedClasses.length + matchedObjectProps.length + matchedDatatypeProps.length + matchedShapes.length + matchedIndividuals.length + matchedAnnotationProps.length;
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
total,
|
|
190
|
+
classes: matchedClasses,
|
|
191
|
+
objectProperties: matchedObjectProps,
|
|
192
|
+
datatypeProperties: matchedDatatypeProps,
|
|
193
|
+
shapes: matchedShapes,
|
|
194
|
+
individuals: matchedIndividuals,
|
|
195
|
+
annotationProperties: matchedAnnotationProps,
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const groupedIndividuals = computed<IndividualGroup[]>(() => {
|
|
200
|
+
return taxonomyKeys.map(key => {
|
|
201
|
+
const tax = (taxonomyData as Record<string, any>)[key];
|
|
202
|
+
if (!tax) return { key, label: taxonomyLabels[key] || key, concepts: [] };
|
|
203
|
+
const concepts = Object.values(tax.concepts as Record<string, any>).map((c: any) => ({
|
|
204
|
+
id: c.id,
|
|
205
|
+
prefLabel: c.prefLabel,
|
|
206
|
+
}));
|
|
207
|
+
return { key, label: tax.schemeLabel || taxonomyLabels[key] || key, concepts };
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const totalIndividuals = computed(() =>
|
|
212
|
+
groupedIndividuals.value.reduce((sum, g) => sum + g.concepts.length, 0),
|
|
213
|
+
);
|
|
220
214
|
|
|
221
|
-
export function useOntologyNav() {
|
|
222
215
|
return {
|
|
223
216
|
expandedClasses,
|
|
224
217
|
collapsedSections,
|
|
@@ -248,3 +241,9 @@ export function useOntologyNav() {
|
|
|
248
241
|
ENTITY_TYPE_META,
|
|
249
242
|
};
|
|
250
243
|
}
|
|
244
|
+
|
|
245
|
+
interface IndividualGroup {
|
|
246
|
+
key: string;
|
|
247
|
+
label: string;
|
|
248
|
+
concepts: { id: string; prefLabel: string }[];
|
|
249
|
+
}
|
package/src/graph/GraphEngine.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GraphNode, GraphEdge } from '../adapters/types';
|
|
2
|
-
import {
|
|
2
|
+
import { ReferenceResolver } from '../adapters/ReferenceResolver';
|
|
3
3
|
|
|
4
4
|
function hasDesignations(node: GraphNode): boolean {
|
|
5
5
|
const d = node.designations;
|
|
@@ -33,9 +33,9 @@ export class GraphEngine {
|
|
|
33
33
|
if (this.edgeKeys.has(key)) return;
|
|
34
34
|
this.edgeKeys.add(key);
|
|
35
35
|
|
|
36
|
-
const parsed =
|
|
36
|
+
const parsed = ReferenceResolver.parseUri(edge.target);
|
|
37
37
|
if (!this.nodes.has(edge.source)) {
|
|
38
|
-
const sourceParsed =
|
|
38
|
+
const sourceParsed = ReferenceResolver.parseUri(edge.source);
|
|
39
39
|
this.nodes.set(edge.source, {
|
|
40
40
|
uri: edge.source,
|
|
41
41
|
register: sourceParsed?.registerId ?? edge.register,
|
|
@@ -161,6 +161,71 @@ export class GraphEngine {
|
|
|
161
161
|
return [...this.nodes.values()];
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
// ── Bulk seeding: accept domain-level data, construct nodes internally ──────
|
|
165
|
+
|
|
166
|
+
addGraphNodes(uriPrefix: string, registerId: string, nodes: [string, Record<string, string>, string][]): void {
|
|
167
|
+
for (const [id, designations, status] of nodes) {
|
|
168
|
+
this.addNode({
|
|
169
|
+
uri: uriPrefix + id,
|
|
170
|
+
register: registerId,
|
|
171
|
+
conceptId: id,
|
|
172
|
+
designations: designations || {},
|
|
173
|
+
status: status || 'unknown',
|
|
174
|
+
loaded: false,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
addEdges(edges: GraphEdge[]): void {
|
|
180
|
+
for (const edge of edges) {
|
|
181
|
+
this.addEdge(edge);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
addDomainNodes(nodes: GraphNode[]): void {
|
|
186
|
+
for (const node of nodes) {
|
|
187
|
+
this.addNode(node);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
seedConceptNode(uri: string, registerId: string, conceptId: string, designations: Record<string, string>, status: string): void {
|
|
192
|
+
this.addNode({
|
|
193
|
+
uri,
|
|
194
|
+
register: registerId,
|
|
195
|
+
conceptId,
|
|
196
|
+
designations,
|
|
197
|
+
status,
|
|
198
|
+
loaded: true,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
addDomainEdgesWithNodes(edges: GraphEdge[], registerId: string): void {
|
|
203
|
+
for (const edge of edges) {
|
|
204
|
+
// Create domain target node before addEdge so addEdge finds it and skips stub creation
|
|
205
|
+
if (!this.nodes.has(edge.target)) {
|
|
206
|
+
this.nodes.set(edge.target, {
|
|
207
|
+
uri: edge.target,
|
|
208
|
+
register: registerId,
|
|
209
|
+
conceptId: '',
|
|
210
|
+
designations: edge.label ? { eng: edge.label } : {},
|
|
211
|
+
status: 'domain',
|
|
212
|
+
loaded: true,
|
|
213
|
+
nodeType: 'domain',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
this.addEdge(edge);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
getRelated(uri: string): { outgoing: GraphEdge[]; incoming: GraphEdge[] } {
|
|
221
|
+
return {
|
|
222
|
+
outgoing: this.getUniqueEdges(uri, 'outgoing', 'target')
|
|
223
|
+
.filter(e => e.type !== 'domain' && e.type !== 'section'),
|
|
224
|
+
incoming: this.getUniqueEdges(uri, 'incoming', 'source')
|
|
225
|
+
.filter(e => e.type !== 'domain' && e.type !== 'section'),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
164
229
|
get nodeCount(): number {
|
|
165
230
|
return this.nodes.size;
|
|
166
231
|
}
|
package/src/stores/vocabulary.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Manifest, SearchHit, GraphEdge } from '../adapters/types';
|
|
|
6
6
|
import type { Concept } from 'glossarist';
|
|
7
7
|
import { conceptUri } from '../adapters/model-bridge';
|
|
8
8
|
import { GraphEngine } from '../graph';
|
|
9
|
-
import {
|
|
9
|
+
import { ReferenceResolver } from '../adapters/ReferenceResolver';
|
|
10
10
|
import { deduplicateSearchHits } from '../utils/search';
|
|
11
11
|
|
|
12
12
|
export const useVocabularyStore = defineStore('vocabulary', () => {
|
|
@@ -107,31 +107,17 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
|
|
|
107
107
|
adapter.loadDomainNodes(),
|
|
108
108
|
]);
|
|
109
109
|
|
|
110
|
-
if (nodeResult.status === 'fulfilled') {
|
|
111
|
-
|
|
112
|
-
for (const [id, designations, status] of nodes) {
|
|
113
|
-
engine.addNode({
|
|
114
|
-
uri: uriPrefix + id,
|
|
115
|
-
register: adapter.registerId,
|
|
116
|
-
conceptId: id,
|
|
117
|
-
designations: designations || {},
|
|
118
|
-
status: status || 'unknown',
|
|
119
|
-
loaded: false,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
110
|
+
if (nodeResult.status === 'fulfilled' && nodeResult.value.uriPrefix) {
|
|
111
|
+
engine.addGraphNodes(nodeResult.value.uriPrefix, adapter.registerId, nodeResult.value.nodes);
|
|
122
112
|
}
|
|
123
113
|
|
|
124
114
|
if (edgeResult.status === 'fulfilled' && Array.isArray(edgeResult.value)) {
|
|
125
|
-
|
|
126
|
-
engine.addEdge(edge);
|
|
127
|
-
}
|
|
115
|
+
engine.addEdges(edgeResult.value);
|
|
128
116
|
edgeStatus.value[adapter.registerId] = { loaded: true, count: edgeResult.value.length };
|
|
129
117
|
}
|
|
130
118
|
|
|
131
119
|
if (domainResult.status === 'fulfilled') {
|
|
132
|
-
|
|
133
|
-
engine.addNode(dn);
|
|
134
|
-
}
|
|
120
|
+
engine.addDomainNodes(domainResult.value);
|
|
135
121
|
}
|
|
136
122
|
} catch {
|
|
137
123
|
// Individual adapter failures are non-critical for graph view
|
|
@@ -149,24 +135,11 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
|
|
|
149
135
|
adapter.loadGraphNodes(),
|
|
150
136
|
]);
|
|
151
137
|
const engine = graph.value;
|
|
152
|
-
|
|
153
|
-
engine.addNode(dn);
|
|
154
|
-
}
|
|
138
|
+
engine.addDomainNodes(domainNodes);
|
|
155
139
|
if (graphNodes.uriPrefix) {
|
|
156
|
-
|
|
157
|
-
engine.addNode({
|
|
158
|
-
uri: graphNodes.uriPrefix + id,
|
|
159
|
-
register: adapter.registerId,
|
|
160
|
-
conceptId: id,
|
|
161
|
-
designations: designations || {},
|
|
162
|
-
status: status || 'unknown',
|
|
163
|
-
loaded: false,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
for (const edge of edges) {
|
|
168
|
-
engine.addEdge(edge);
|
|
140
|
+
engine.addGraphNodes(graphNodes.uriPrefix, adapter.registerId, graphNodes.nodes);
|
|
169
141
|
}
|
|
142
|
+
engine.addEdges(edges);
|
|
170
143
|
edgeStatus.value[adapter.registerId] = { loaded: true, count: edges.length };
|
|
171
144
|
return edges;
|
|
172
145
|
} catch {
|
|
@@ -186,7 +159,7 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
|
|
|
186
159
|
if (adapter && loadedEdges.length > 0) {
|
|
187
160
|
const targetRegisters = new Set<string>();
|
|
188
161
|
for (const edge of loadedEdges) {
|
|
189
|
-
const parsed =
|
|
162
|
+
const parsed = ReferenceResolver.parseUri(edge.target);
|
|
190
163
|
if (parsed?.registerId && parsed.registerId !== registerId) {
|
|
191
164
|
targetRegisters.add(parsed.registerId);
|
|
192
165
|
}
|
|
@@ -197,17 +170,7 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
|
|
|
197
170
|
try {
|
|
198
171
|
const gn = await targetAdapter.loadGraphNodes();
|
|
199
172
|
if (gn.uriPrefix) {
|
|
200
|
-
|
|
201
|
-
for (const [id, designations, status] of gn.nodes) {
|
|
202
|
-
engine.addNode({
|
|
203
|
-
uri: gn.uriPrefix + id,
|
|
204
|
-
register: targetId,
|
|
205
|
-
conceptId: id,
|
|
206
|
-
designations: designations || {},
|
|
207
|
-
status: status || 'unknown',
|
|
208
|
-
loaded: false,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
173
|
+
graph.value.addGraphNodes(gn.uriPrefix, targetId, gn.nodes);
|
|
211
174
|
}
|
|
212
175
|
} catch { /* non-critical */ }
|
|
213
176
|
}));
|
|
@@ -261,36 +224,12 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
|
|
|
261
224
|
}
|
|
262
225
|
|
|
263
226
|
const engine = graph.value;
|
|
264
|
-
engine.
|
|
265
|
-
|
|
266
|
-
register: registerId,
|
|
267
|
-
conceptId,
|
|
268
|
-
designations,
|
|
269
|
-
status: indexEntry?.status ?? 'unknown',
|
|
270
|
-
loaded: true,
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
for (const edge of domainEdges) {
|
|
274
|
-
engine.addEdge(edge);
|
|
275
|
-
const existing = engine.getNode(edge.target);
|
|
276
|
-
if (!existing || !existing.loaded) {
|
|
277
|
-
engine.addNode({
|
|
278
|
-
uri: edge.target,
|
|
279
|
-
register: registerId,
|
|
280
|
-
conceptId: '',
|
|
281
|
-
designations: edge.label ? { eng: edge.label } : {},
|
|
282
|
-
status: 'domain',
|
|
283
|
-
loaded: true,
|
|
284
|
-
nodeType: 'domain',
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
227
|
+
engine.seedConceptNode(uri, registerId, conceptId, designations, indexEntry?.status ?? 'unknown');
|
|
228
|
+
engine.addDomainEdgesWithNodes(domainEdges, registerId);
|
|
288
229
|
|
|
289
230
|
touchGraph();
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
...engine.getIncomingEdges(uri),
|
|
293
|
-
];
|
|
231
|
+
const related = engine.getRelated(uri);
|
|
232
|
+
conceptEdges.value = [...related.outgoing, ...related.incoming];
|
|
294
233
|
} catch (e: unknown) {
|
|
295
234
|
error.value = `Failed to load concept ${conceptId}: ${e instanceof Error ? e.message : String(e)}`;
|
|
296
235
|
currentConcept.value = null;
|