@glossarist/concept-browser 0.3.4 → 0.4.0
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/README.md +3 -2
- package/cli/index.mjs +2 -1
- package/env.d.ts +5 -0
- package/package.json +4 -3
- package/scripts/build-edges.js +78 -10
- package/scripts/generate-data.mjs +152 -20
- package/scripts/generate-ontology-data.mjs +184 -0
- package/scripts/generate-ontology-schema.mjs +315 -0
- package/src/__tests__/about-view.test.ts +98 -0
- package/src/__tests__/app-footer.test.ts +38 -0
- package/src/__tests__/app-header.test.ts +130 -0
- package/src/__tests__/app-sidebar.test.ts +159 -0
- package/src/__tests__/asciidoc-lite.test.ts +1 -1
- package/src/__tests__/concept-card.test.ts +115 -0
- package/src/__tests__/concept-detail-interaction.test.ts +273 -0
- package/src/__tests__/concept-formats.test.ts +32 -30
- package/src/__tests__/concept-timeline.test.ts +200 -0
- package/src/__tests__/concept-view.test.ts +88 -0
- package/src/__tests__/contributors-view.test.ts +103 -0
- package/src/__tests__/dataset-adapter.test.ts +172 -23
- package/src/__tests__/dataset-view.test.ts +232 -0
- package/src/__tests__/designation-registry.test.ts +161 -0
- package/src/__tests__/format-downloads.test.ts +98 -0
- package/src/__tests__/graph-view.test.ts +69 -0
- package/src/__tests__/graph.test.ts +62 -0
- package/src/__tests__/home-interaction.test.ts +157 -0
- package/src/__tests__/language-detail.test.ts +203 -0
- package/src/__tests__/nav-icon.test.ts +48 -0
- package/src/__tests__/news-view.test.ts +87 -0
- package/src/__tests__/ontology-registry.test.ts +109 -0
- package/src/__tests__/page-view.test.ts +83 -0
- package/src/__tests__/relationship-categories.test.ts +62 -0
- package/src/__tests__/resolve-view.test.ts +77 -0
- package/src/__tests__/router.test.ts +65 -0
- package/src/__tests__/search-bar.test.ts +219 -0
- package/src/__tests__/search-view.test.ts +41 -0
- package/src/__tests__/stats-view.test.ts +77 -0
- package/src/__tests__/test-helpers.ts +171 -0
- package/src/__tests__/ui-store.test.ts +100 -0
- package/src/__tests__/v-math.test.ts +8 -7
- package/src/adapters/DatasetAdapter.ts +188 -63
- package/src/adapters/model-bridge.ts +277 -0
- package/src/adapters/ontology-registry.ts +75 -0
- package/src/adapters/ontology-schema.ts +100 -0
- package/src/adapters/types.ts +53 -78
- package/src/components/AppSidebar.vue +1 -1
- package/src/components/CitationDisplay.vue +35 -0
- package/src/components/ConceptDetail.vue +349 -146
- package/src/components/ConceptRdfView.vue +397 -0
- package/src/components/ConceptTimeline.vue +57 -60
- package/src/components/GraphPanel.vue +96 -31
- package/src/components/LanguageDetail.vue +46 -61
- package/src/components/NavIcon.vue +1 -0
- package/src/components/NonVerbalRepDisplay.vue +38 -0
- package/src/components/RelationshipList.vue +99 -0
- package/src/composables/use-render-options.ts +1 -4
- package/src/config/use-site-config.ts +3 -0
- package/src/data/ontology-schema.json +1551 -0
- package/src/data/taxonomies.json +543 -0
- package/src/graph/GraphEngine.ts +7 -4
- package/src/router/index.ts +6 -1
- package/src/shims/empty.ts +1 -0
- package/src/shims/node-crypto.ts +6 -0
- package/src/shims/node-path.ts +10 -0
- package/src/stores/vocabulary.ts +82 -32
- package/src/style.css +74 -20
- package/src/utils/asciidoc-lite.ts +17 -19
- package/src/utils/concept-formats.ts +22 -20
- package/src/utils/concept-helpers.ts +54 -0
- package/src/utils/designation-registry.ts +124 -0
- package/src/utils/escape.ts +7 -0
- package/src/utils/markdown-lite.ts +1 -3
- package/src/utils/math.ts +2 -11
- package/src/utils/plurimath.ts +2 -7
- package/src/utils/relationship-categories.ts +84 -0
- package/src/views/ConceptView.vue +22 -1
- package/src/views/DatasetView.vue +7 -2
- package/src/views/OntologySchemaView.vue +302 -0
- package/src/views/PageView.vue +28 -17
- package/src/views/StatsView.vue +34 -12
- package/vite.config.ts +8 -0
|
@@ -19,6 +19,8 @@ class MockPlurimath {
|
|
|
19
19
|
import { vMath } from '../directives/v-math';
|
|
20
20
|
import { loadPlurimath, mathToHtml } from '../utils/plurimath';
|
|
21
21
|
|
|
22
|
+
const directive = vMath as import('vue').ObjectDirective<HTMLElement>;
|
|
23
|
+
|
|
22
24
|
describe('v-math directive', () => {
|
|
23
25
|
let container: HTMLElement;
|
|
24
26
|
|
|
@@ -29,21 +31,20 @@ describe('v-math directive', () => {
|
|
|
29
31
|
|
|
30
32
|
it('does nothing when no math-pending elements exist', () => {
|
|
31
33
|
container.innerHTML = '<p>plain text</p>';
|
|
32
|
-
|
|
34
|
+
directive.mounted!(container, {} as any, {} as any, {} as any);
|
|
33
35
|
expect(container.innerHTML).toBe('<p>plain text</p>');
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
it('triggers loadPlurimath when math-pending elements exist', async () => {
|
|
37
39
|
container.innerHTML = '<span class="math-pending" data-expr="x^2" data-format="asciimath">x^2</span>';
|
|
38
|
-
|
|
40
|
+
directive.mounted!(container, {} as any, {} as any, {} as any);
|
|
39
41
|
expect(loadPlurimath).toHaveBeenCalled();
|
|
40
42
|
});
|
|
41
43
|
|
|
42
44
|
it('replaces math-pending elements after loading', async () => {
|
|
43
45
|
container.innerHTML = '<span class="math-pending" data-expr="x^2" data-format="asciimath">x^2</span>';
|
|
44
|
-
|
|
46
|
+
directive.mounted!(container, {} as any, {} as any, {} as any);
|
|
45
47
|
|
|
46
|
-
// Wait for loadPlurimath promise to resolve and upgrade to run
|
|
47
48
|
await vi.waitFor(() => {
|
|
48
49
|
expect(mathToHtml).toHaveBeenCalledWith('x^2', 'asciimath', false);
|
|
49
50
|
});
|
|
@@ -51,7 +52,7 @@ describe('v-math directive', () => {
|
|
|
51
52
|
|
|
52
53
|
it('handles bold math-pending elements', async () => {
|
|
53
54
|
container.innerHTML = '<span class="math-pending math-bold" data-expr="alpha" data-format="asciimath">alpha</span>';
|
|
54
|
-
|
|
55
|
+
directive.mounted!(container, {} as any, {} as any, {} as any);
|
|
55
56
|
|
|
56
57
|
await vi.waitFor(() => {
|
|
57
58
|
expect(mathToHtml).toHaveBeenCalledWith('alpha', 'asciimath', true);
|
|
@@ -60,7 +61,7 @@ describe('v-math directive', () => {
|
|
|
60
61
|
|
|
61
62
|
it('skips elements without data-expr', async () => {
|
|
62
63
|
container.innerHTML = '<span class="math-pending">no expr</span>';
|
|
63
|
-
|
|
64
|
+
directive.mounted!(container, {} as any, {} as any, {} as any);
|
|
64
65
|
|
|
65
66
|
await vi.waitFor(() => {
|
|
66
67
|
expect(mathToHtml).not.toHaveBeenCalled();
|
|
@@ -69,7 +70,7 @@ describe('v-math directive', () => {
|
|
|
69
70
|
|
|
70
71
|
it('uses default format asciimath when data-format is missing', async () => {
|
|
71
72
|
container.innerHTML = '<span class="math-pending" data-expr="x">x</span>';
|
|
72
|
-
|
|
73
|
+
directive.mounted!(container, {} as any, {} as any, {} as any);
|
|
73
74
|
|
|
74
75
|
await vi.waitFor(() => {
|
|
75
76
|
expect(mathToHtml).toHaveBeenCalledWith('x', 'asciimath', false);
|
|
@@ -3,12 +3,29 @@ import type {
|
|
|
3
3
|
ConceptIndex,
|
|
4
4
|
ConceptSummary,
|
|
5
5
|
ConceptEntry,
|
|
6
|
-
ConceptDocument,
|
|
7
6
|
SearchHit,
|
|
8
7
|
GraphEdge,
|
|
8
|
+
GraphNode,
|
|
9
9
|
} from './types';
|
|
10
|
+
import type { Concept, LocalizedConcept, Designation } from 'glossarist';
|
|
11
|
+
import { conceptFromJson, conceptUri } from './model-bridge';
|
|
10
12
|
import { UriRouter } from './UriRouter';
|
|
11
13
|
|
|
14
|
+
function slugify(text: string): string {
|
|
15
|
+
return text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s/]+/g, '-');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveRefTarget(rc: any, uriBase: string, registerId: string): string {
|
|
19
|
+
if (!rc.ref) return '';
|
|
20
|
+
const ref = rc.ref;
|
|
21
|
+
if (ref.id) {
|
|
22
|
+
const reg = (ref.source && !ref.source.startsWith('http')) ? ref.source : registerId;
|
|
23
|
+
return `${uriBase}/${reg}/concept/${ref.id}`;
|
|
24
|
+
}
|
|
25
|
+
if (ref.source && ref.source.startsWith('http')) return ref.source;
|
|
26
|
+
return ref.source || '';
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
export class DatasetAdapter {
|
|
13
30
|
private positionIndex = new Map<string, number>();
|
|
14
31
|
readonly registerId: string;
|
|
@@ -16,7 +33,7 @@ export class DatasetAdapter {
|
|
|
16
33
|
manifest: Manifest | null = null;
|
|
17
34
|
index: ConceptIndex | null = null;
|
|
18
35
|
|
|
19
|
-
private conceptCache = new Map<string,
|
|
36
|
+
private conceptCache = new Map<string, Concept>();
|
|
20
37
|
private summaryMap = new Map<string, ConceptSummary>();
|
|
21
38
|
private loadedChunks = new Set<number>();
|
|
22
39
|
private indexMeta: { conceptCount: number; chunkSize: number; chunks: { file: string; count: number }[] } | null = null;
|
|
@@ -43,14 +60,53 @@ export class DatasetAdapter {
|
|
|
43
60
|
|
|
44
61
|
const resp = await fetch(`${this.baseUrl}/index.json`);
|
|
45
62
|
if (!resp.ok) throw new Error(`Failed to load index for ${this.registerId}: ${resp.status}`);
|
|
46
|
-
|
|
63
|
+
const data = await resp.json();
|
|
64
|
+
|
|
65
|
+
// Handle both old format (with eng/status fields) and new format (with designations map)
|
|
66
|
+
this.index = this.normalizeIndex(data);
|
|
67
|
+
this.buildSummaryIndex();
|
|
68
|
+
return this.index;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private normalizeIndex(data: any): ConceptIndex {
|
|
72
|
+
const concepts: ConceptSummary[] = (data.concepts || []).map((c: any) => {
|
|
73
|
+
if (c.designations && typeof c.designations === 'object') {
|
|
74
|
+
return {
|
|
75
|
+
id: c.id,
|
|
76
|
+
designations: c.designations,
|
|
77
|
+
eng: c.eng || c.designations.eng || Object.values(c.designations)[0] || '',
|
|
78
|
+
status: c.status,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Legacy format: c.eng is a string, no designations map
|
|
82
|
+
return {
|
|
83
|
+
id: c.id,
|
|
84
|
+
designations: c.eng ? { eng: c.eng } : {},
|
|
85
|
+
eng: c.eng || '',
|
|
86
|
+
status: c.status,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
registerId: data.registerId,
|
|
92
|
+
schemaVersion: data.schemaVersion,
|
|
93
|
+
conceptCount: data.conceptCount,
|
|
94
|
+
chunkSize: data.chunkSize,
|
|
95
|
+
chunks: data.chunks || [],
|
|
96
|
+
concepts,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private buildSummaryIndex() {
|
|
47
101
|
this.summaryMap.clear();
|
|
48
102
|
this.positionIndex.clear();
|
|
49
|
-
for (let i = 0; i < this.index
|
|
50
|
-
|
|
51
|
-
|
|
103
|
+
for (let i = 0; i < this.index!.concepts.length; i++) {
|
|
104
|
+
const entry = this.index!.concepts[i];
|
|
105
|
+
if (entry) {
|
|
106
|
+
this.summaryMap.set(entry.id, entry);
|
|
107
|
+
this.positionIndex.set(entry.id, i);
|
|
108
|
+
}
|
|
52
109
|
}
|
|
53
|
-
return this.index;
|
|
54
110
|
}
|
|
55
111
|
|
|
56
112
|
private async loadIndexChunked(): Promise<ConceptIndex> {
|
|
@@ -61,13 +117,9 @@ export class DatasetAdapter {
|
|
|
61
117
|
} else {
|
|
62
118
|
const resp = await fetch(`${this.baseUrl}/index.json`);
|
|
63
119
|
if (!resp.ok) throw new Error(`Failed to load index for ${this.registerId}`);
|
|
64
|
-
|
|
65
|
-
this.
|
|
66
|
-
this.
|
|
67
|
-
for (let i = 0; i < this.index.concepts.length; i++) {
|
|
68
|
-
this.summaryMap.set(this.index.concepts[i].id, this.index.concepts[i]);
|
|
69
|
-
this.positionIndex.set(this.index.concepts[i].id, i);
|
|
70
|
-
}
|
|
120
|
+
const data = await resp.json();
|
|
121
|
+
this.index = this.normalizeIndex(data);
|
|
122
|
+
this.buildSummaryIndex();
|
|
71
123
|
return this.index;
|
|
72
124
|
}
|
|
73
125
|
|
|
@@ -77,7 +129,6 @@ export class DatasetAdapter {
|
|
|
77
129
|
chunks: meta.chunks,
|
|
78
130
|
};
|
|
79
131
|
|
|
80
|
-
// Pre-allocate array so positions match concept order — undefined = not loaded yet
|
|
81
132
|
this.index = {
|
|
82
133
|
registerId: meta.registerId,
|
|
83
134
|
schemaVersion: meta.schemaVersion,
|
|
@@ -88,7 +139,6 @@ export class DatasetAdapter {
|
|
|
88
139
|
};
|
|
89
140
|
|
|
90
141
|
await this.loadChunkAsSummaries(0);
|
|
91
|
-
|
|
92
142
|
return this.index;
|
|
93
143
|
}
|
|
94
144
|
|
|
@@ -116,12 +166,14 @@ export class DatasetAdapter {
|
|
|
116
166
|
|
|
117
167
|
for (let i = 0; i < entries.length; i++) {
|
|
118
168
|
const entry = entries[i];
|
|
169
|
+
const designations = entry.designations || (entry.groups ? {} : { eng: '' });
|
|
119
170
|
const summary: ConceptSummary = {
|
|
120
171
|
id: entry.id,
|
|
121
|
-
|
|
172
|
+
designations,
|
|
173
|
+
eng: designations.eng || Object.values(designations)[0] || '',
|
|
122
174
|
status: entry.status,
|
|
123
175
|
};
|
|
124
|
-
|
|
176
|
+
this.index!.concepts[startPos + i] = summary;
|
|
125
177
|
this.summaryMap.set(entry.id, summary);
|
|
126
178
|
this.positionIndex.set(entry.id, startPos + i);
|
|
127
179
|
}
|
|
@@ -144,7 +196,6 @@ export class DatasetAdapter {
|
|
|
144
196
|
if (!this.indexMeta) return;
|
|
145
197
|
const { chunks } = this.indexMeta;
|
|
146
198
|
const toLoad = chunks.map((_, i) => i).filter(i => !this.loadedChunks.has(i));
|
|
147
|
-
// Load in parallel batches of 5 to avoid overwhelming the browser
|
|
148
199
|
for (let i = 0; i < toLoad.length; i += 5) {
|
|
149
200
|
const batch = toLoad.slice(i, i + 5);
|
|
150
201
|
await Promise.all(batch.map(c => this.loadChunkAsSummaries(c)));
|
|
@@ -153,29 +204,30 @@ export class DatasetAdapter {
|
|
|
153
204
|
|
|
154
205
|
isRangeLoaded(offset: number, limit: number): boolean {
|
|
155
206
|
if (!this.index?.concepts) return false;
|
|
156
|
-
const arr = this.index.concepts
|
|
207
|
+
const arr = this.index.concepts;
|
|
157
208
|
for (let i = offset; i < Math.min(offset + limit, arr.length); i++) {
|
|
158
209
|
if (arr[i] === undefined) return false;
|
|
159
210
|
}
|
|
160
211
|
return true;
|
|
161
212
|
}
|
|
162
213
|
|
|
163
|
-
async fetchConcept(conceptId: string): Promise<
|
|
214
|
+
async fetchConcept(conceptId: string): Promise<Concept> {
|
|
164
215
|
const cached = this.conceptCache.get(conceptId);
|
|
165
216
|
if (cached) return cached;
|
|
166
217
|
|
|
167
218
|
const resp = await fetch(`${this.baseUrl}/concepts/${conceptId}.json`);
|
|
168
219
|
if (!resp.ok) throw new Error(`Concept ${conceptId} not found in ${this.registerId}`);
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
220
|
+
const json = await resp.json();
|
|
221
|
+
const concept = conceptFromJson(json);
|
|
222
|
+
this.conceptCache.set(conceptId, concept);
|
|
223
|
+
return concept;
|
|
172
224
|
}
|
|
173
225
|
|
|
174
226
|
getIndexEntry(conceptId: string): ConceptSummary | undefined {
|
|
175
227
|
return this.summaryMap.get(conceptId);
|
|
176
228
|
}
|
|
177
229
|
|
|
178
|
-
getConcepts(): ConceptSummary[] {
|
|
230
|
+
getConcepts(): (ConceptSummary | undefined)[] {
|
|
179
231
|
return this.index?.concepts ?? [];
|
|
180
232
|
}
|
|
181
233
|
|
|
@@ -188,16 +240,14 @@ export class DatasetAdapter {
|
|
|
188
240
|
}
|
|
189
241
|
|
|
190
242
|
getAdjacentConcepts(conceptId: string): { prev: string | null; next: string | null } {
|
|
191
|
-
const concepts = this.index?.concepts
|
|
243
|
+
const concepts = this.index?.concepts;
|
|
192
244
|
if (!concepts) return { prev: null, next: null };
|
|
193
245
|
const idx = this.getConceptPosition(conceptId);
|
|
194
246
|
if (idx === -1) return { prev: null, next: null };
|
|
195
|
-
// Scan backward for prev (skip undefined)
|
|
196
247
|
let prev: string | null = null;
|
|
197
248
|
for (let i = idx - 1; i >= 0; i--) {
|
|
198
249
|
if (concepts[i]) { prev = concepts[i]!.id; break; }
|
|
199
250
|
}
|
|
200
|
-
// Scan forward for next (skip undefined)
|
|
201
251
|
let next: string | null = null;
|
|
202
252
|
for (let i = idx + 1; i < concepts.length; i++) {
|
|
203
253
|
if (concepts[i]) { next = concepts[i]!.id; break; }
|
|
@@ -205,53 +255,92 @@ export class DatasetAdapter {
|
|
|
205
255
|
return { prev, next };
|
|
206
256
|
}
|
|
207
257
|
|
|
208
|
-
search(query: string
|
|
258
|
+
search(query: string): SearchHit[] {
|
|
209
259
|
const q = query.toLowerCase();
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
260
|
+
const arr = this.index?.concepts;
|
|
261
|
+
if (!arr) return [];
|
|
262
|
+
|
|
263
|
+
type ScoredHit = SearchHit & { _score: number };
|
|
264
|
+
const scored: ScoredHit[] = [];
|
|
213
265
|
|
|
214
266
|
for (const entry of arr) {
|
|
215
267
|
if (!entry) continue;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
if (!termMatch && idMatch) {
|
|
223
|
-
snippet = `ID: ${entry.id}`;
|
|
224
|
-
}
|
|
225
|
-
hits.push({
|
|
268
|
+
|
|
269
|
+
// ID search — exact match highest, then starts with, then contains
|
|
270
|
+
const idLow = entry.id.toLowerCase();
|
|
271
|
+
if (idLow.includes(q)) {
|
|
272
|
+
const score = idLow === q ? 4 : idLow.startsWith(q) ? 3 : 2;
|
|
273
|
+
scored.push({
|
|
226
274
|
conceptId: entry.id,
|
|
227
275
|
registerId: this.registerId,
|
|
228
|
-
designation:
|
|
229
|
-
language:
|
|
230
|
-
matchField,
|
|
231
|
-
snippet
|
|
276
|
+
designation: entry.eng || '',
|
|
277
|
+
language: '',
|
|
278
|
+
matchField: 'id',
|
|
279
|
+
snippet: `ID: ${entry.id}`,
|
|
280
|
+
_score: score,
|
|
232
281
|
});
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Multi-language designation search
|
|
286
|
+
for (const [language, term] of Object.entries(entry.designations)) {
|
|
287
|
+
if (!term) continue;
|
|
288
|
+
const tLow = term.toLowerCase();
|
|
289
|
+
if (tLow.includes(q)) {
|
|
290
|
+
const score = tLow === q ? 4 : tLow.startsWith(q) ? 3 : 1;
|
|
291
|
+
scored.push({
|
|
292
|
+
conceptId: entry.id,
|
|
293
|
+
registerId: this.registerId,
|
|
294
|
+
designation: term,
|
|
295
|
+
language,
|
|
296
|
+
matchField: 'designation',
|
|
297
|
+
_score: score,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
233
300
|
}
|
|
234
301
|
}
|
|
235
|
-
|
|
302
|
+
|
|
303
|
+
// Sort by score descending, then alphabetically
|
|
304
|
+
scored.sort((a, b) => b._score - a._score || a.designation.localeCompare(b.designation));
|
|
305
|
+
return scored;
|
|
236
306
|
}
|
|
237
307
|
|
|
238
|
-
extractEdges(concept:
|
|
308
|
+
extractEdges(concept: Concept): GraphEdge[] {
|
|
239
309
|
const edges: GraphEdge[] = [];
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
310
|
+
const uriBase = this.manifest?.uriBase || 'https://glossarist.org';
|
|
311
|
+
const sourceUri = concept.uri || `${uriBase}/${this.registerId}/concept/${concept.id}`;
|
|
312
|
+
|
|
313
|
+
// Managed concept level relationships
|
|
314
|
+
for (const rc of concept.relatedConcepts) {
|
|
315
|
+
const target = resolveRefTarget(rc, uriBase, this.registerId);
|
|
316
|
+
if (target && target !== sourceUri) {
|
|
317
|
+
const parsed = UriRouter.parseUri(target);
|
|
318
|
+
edges.push({
|
|
319
|
+
source: sourceUri,
|
|
320
|
+
target,
|
|
321
|
+
type: rc.type || 'references',
|
|
322
|
+
label: rc.content || undefined,
|
|
323
|
+
register: parsed?.registerId ?? this.registerId,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Per-localization references (from inline extraction in generate-data)
|
|
329
|
+
for (const lang of concept.languages) {
|
|
330
|
+
const lc = concept.localization(lang);
|
|
331
|
+
if (!lc) continue;
|
|
332
|
+
for (const rc of lc.related) {
|
|
333
|
+
const target = resolveRefTarget(rc, uriBase, this.registerId);
|
|
334
|
+
if (target && target !== sourceUri) {
|
|
335
|
+
const parsed = UriRouter.parseUri(target);
|
|
336
|
+
edges.push({
|
|
337
|
+
source: sourceUri,
|
|
338
|
+
target,
|
|
339
|
+
type: rc.type || 'references',
|
|
340
|
+
label: rc.content || undefined,
|
|
341
|
+
register: parsed?.registerId ?? this.registerId,
|
|
342
|
+
lang,
|
|
343
|
+
});
|
|
255
344
|
}
|
|
256
345
|
}
|
|
257
346
|
}
|
|
@@ -259,6 +348,42 @@ export class DatasetAdapter {
|
|
|
259
348
|
return edges;
|
|
260
349
|
}
|
|
261
350
|
|
|
351
|
+
extractDomainEdges(concept: Concept): GraphEdge[] {
|
|
352
|
+
const edges: GraphEdge[] = [];
|
|
353
|
+
const uriBase = this.manifest?.uriBase || 'https://glossarist.org';
|
|
354
|
+
const sourceUri = concept.uri || `${uriBase}/${this.registerId}/concept/${concept.id}`;
|
|
355
|
+
|
|
356
|
+
for (const lang of concept.languages) {
|
|
357
|
+
const lc = concept.localization(lang);
|
|
358
|
+
if (lc?.domain) {
|
|
359
|
+
edges.push({
|
|
360
|
+
source: sourceUri,
|
|
361
|
+
target: `${uriBase}/${this.registerId}/domain/${slugify(lc.domain)}`,
|
|
362
|
+
type: 'domain',
|
|
363
|
+
label: lc.domain,
|
|
364
|
+
register: this.registerId,
|
|
365
|
+
lang,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return edges;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async loadDomainNodes(): Promise<GraphNode[]> {
|
|
373
|
+
const resp = await fetch(`${this.baseUrl}/domain-nodes.json`);
|
|
374
|
+
if (!resp.ok) return [];
|
|
375
|
+
const data = await resp.json();
|
|
376
|
+
return (data.domainNodes || []).map((dn: any) => ({
|
|
377
|
+
uri: dn.uri,
|
|
378
|
+
register: dn.registerId,
|
|
379
|
+
conceptId: dn.uri.split('/domain/')[1] || '',
|
|
380
|
+
designations: { eng: dn.label },
|
|
381
|
+
status: 'domain',
|
|
382
|
+
loaded: true,
|
|
383
|
+
nodeType: 'domain' as const,
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
|
|
262
387
|
async loadEdgeIndex(): Promise<GraphEdge[]> {
|
|
263
388
|
const resp = await fetch(`${this.baseUrl}/edges.json`);
|
|
264
389
|
if (!resp.ok) return [];
|