@glossarist/concept-browser 0.7.62 → 0.7.64
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 +8 -4
- package/scripts/generate-data.mjs +6 -4
- package/scripts/lib/agents-turtle.mjs +2 -1
- package/scripts/lib/bibliography-turtle.mjs +3 -2
- package/src/__tests__/dataset-adapter.test.ts +1 -0
- package/src/__tests__/relation-sphere-uri.test.ts +45 -0
- package/src/__tests__/scripts/agents-turtle.test.ts +5 -5
- package/src/__tests__/scripts/bibliography-turtle.test.ts +5 -5
- package/src/__tests__/scripts/build-integration.test.ts +4 -4
- package/src/__tests__/test-helpers.ts +2 -1
- package/src/adapters/GraphDataSource.ts +3 -1
- package/src/components/RelationSphere.vue +11 -17
- package/src/stores/vocabulary.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.64",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/scripts/build-edges.js
CHANGED
|
@@ -49,7 +49,8 @@ function extractReferences(concept, registerId) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function extractDomains(concept, registerId, uriBase) {
|
|
52
|
-
|
|
52
|
+
if (!uriBase) throw new Error('build-edges: uriBase is required — set uriBase in site-config.yml');
|
|
53
|
+
const base = uriBase;
|
|
53
54
|
const edges = [];
|
|
54
55
|
const sourceUri = concept['@id'];
|
|
55
56
|
const seen = new Set();
|
|
@@ -188,7 +189,8 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
188
189
|
// Build domain-nodes.json from manifest sections (authoritative source)
|
|
189
190
|
const manifestSections = manifest.sections;
|
|
190
191
|
if (manifestSections && manifestSections.length > 0) {
|
|
191
|
-
const uriBase = manifest.uriBase
|
|
192
|
+
const uriBase = manifest.uriBase;
|
|
193
|
+
if (!uriBase) throw new Error('build-edges: manifest.uriBase is required');
|
|
192
194
|
|
|
193
195
|
function buildSectionNode(section, idx) {
|
|
194
196
|
const sectionId = `section-${section.id}`;
|
|
@@ -293,7 +295,8 @@ for (const ds of datasets) {
|
|
|
293
295
|
if (!manifest) continue;
|
|
294
296
|
try {
|
|
295
297
|
console.log(`${manifest.title} (${ds}):`);
|
|
296
|
-
const uriBase = manifest.uriBase
|
|
298
|
+
const uriBase = manifest.uriBase;
|
|
299
|
+
if (!uriBase) throw new Error('build-edges: manifest.uriBase is required');
|
|
297
300
|
const result = buildEdgesForDataset(join(DATA_DIR, ds), ds, uriBase, urnMap, manifest);
|
|
298
301
|
allDatasetEdges.set(ds, result.edges);
|
|
299
302
|
allSourceRefs.push(...result.sourceRefs);
|
|
@@ -347,7 +350,8 @@ if (auditUnmatched.size > 0) {
|
|
|
347
350
|
// edges.json contains edges targeting that dataset's URIs.
|
|
348
351
|
const datasetUriPrefixes = new Map();
|
|
349
352
|
for (const [ds, manifest] of manifestCache) {
|
|
350
|
-
const uriBase = manifest.uriBase
|
|
353
|
+
const uriBase = manifest.uriBase;
|
|
354
|
+
if (!uriBase) throw new Error('build-edges: manifest.uriBase is required');
|
|
351
355
|
datasetUriPrefixes.set(ds, `${uriBase}/${ds}/`);
|
|
352
356
|
}
|
|
353
357
|
|
|
@@ -92,7 +92,8 @@ function writeJson(filePath, data) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
async function writeDatasetRdf(register, manifest, concepts, refMaps, opts) {
|
|
95
|
-
const uriBase = refMaps?.uriBase
|
|
95
|
+
const uriBase = refMaps?.uriBase;
|
|
96
|
+
if (!uriBase) throw new Error('generate-data: uriBase is required — set uriBase in site-config.yml');
|
|
96
97
|
const datasetIri = `${uriBase}/${register}/`;
|
|
97
98
|
const topConceptUris = concepts
|
|
98
99
|
.slice(0, 32)
|
|
@@ -375,7 +376,8 @@ function buildRefMaps(config, registerCache) {
|
|
|
375
376
|
const xref = config.crossReferences || {};
|
|
376
377
|
if (xref.refPrefixMap) Object.assign(refPrefixMap, xref.refPrefixMap);
|
|
377
378
|
|
|
378
|
-
const uriBase = config.uriBase
|
|
379
|
+
const uriBase = config.uriBase;
|
|
380
|
+
if (!uriBase) throw new Error('site-config.yml: uriBase is required');
|
|
379
381
|
return { patternIndex, refPrefixMap, uriBase, register: null };
|
|
380
382
|
}
|
|
381
383
|
|
|
@@ -1617,7 +1619,7 @@ console.log('Emitted vocabulary graph: data/_vocab.ttl');
|
|
|
1617
1619
|
|
|
1618
1620
|
const contributors = config.contributors ?? [];
|
|
1619
1621
|
if (contributors.length > 0) {
|
|
1620
|
-
fs.writeFileSync(path.join(DATA, 'agents.ttl'), await buildAgentsTurtle(contributors));
|
|
1622
|
+
fs.writeFileSync(path.join(DATA, 'agents.ttl'), await buildAgentsTurtle(contributors, refMaps.uriBase + '/agent'));
|
|
1621
1623
|
console.log(`Emitted agents graph: data/agents.ttl (${contributors.length} contributors)`);
|
|
1622
1624
|
}
|
|
1623
1625
|
|
|
@@ -1626,7 +1628,7 @@ for (const ds of registry) {
|
|
|
1626
1628
|
const bibPath = path.join(DATA, ds.id, 'bibliography.json');
|
|
1627
1629
|
if (fs.existsSync(bibPath)) {
|
|
1628
1630
|
const bibJson = JSON.parse(fs.readFileSync(bibPath, 'utf8'));
|
|
1629
|
-
const bibTtl = await buildBibliographyTurtle(ds.id, bibJson);
|
|
1631
|
+
const bibTtl = await buildBibliographyTurtle(ds.id, bibJson, refMaps.uriBase);
|
|
1630
1632
|
fs.writeFileSync(path.join(DATA, ds.id, 'bib.ttl'), bibTtl);
|
|
1631
1633
|
}
|
|
1632
1634
|
}
|
|
@@ -19,7 +19,8 @@ function writerPrefixes() {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export async function buildAgentsTurtle(contributors, agentBase
|
|
22
|
+
export async function buildAgentsTurtle(contributors, agentBase) {
|
|
23
|
+
if (!agentBase) throw new Error('buildAgentsTurtle requires agentBase');
|
|
23
24
|
const agents = agentsFromContributors(contributors ?? [], agentBase);
|
|
24
25
|
const quads = collectQuads(agentsToQuads(agents));
|
|
25
26
|
return writeTurtle(quads, { prefixes: writerPrefixes() });
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
writeTurtle,
|
|
11
11
|
} from 'glossarist/rdf';
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// No hardcoded default base URI — callers MUST pass it.
|
|
14
14
|
|
|
15
15
|
function writerPrefixes() {
|
|
16
16
|
return {
|
|
@@ -22,7 +22,8 @@ function writerPrefixes() {
|
|
|
22
22
|
|
|
23
23
|
export { normalizeBibliographyData };
|
|
24
24
|
|
|
25
|
-
export async function buildBibliographyTurtle(register, bibliographyJson, baseUri
|
|
25
|
+
export async function buildBibliographyTurtle(register, bibliographyJson, baseUri) {
|
|
26
|
+
if (!baseUri) throw new Error('buildBibliographyTurtle requires baseUri');
|
|
26
27
|
const entries = normalizeBibliographyData(bibliographyJson);
|
|
27
28
|
const quads = collectQuads(bibliographyToQuads({ registerId: register, entries, baseUri }));
|
|
28
29
|
return writeTurtle(quads, { prefixes: writerPrefixes() });
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { UriRouter } from '../adapters/UriRouter';
|
|
3
|
+
|
|
4
|
+
describe('UriRouter.parseUri — deployment-agnostic concept URI parsing', () => {
|
|
5
|
+
// The old parseNeighborUri in RelationSphere.vue hardcoded specific
|
|
6
|
+
// hostnames. UriRouter.parseUri must work for ANY deployment.
|
|
7
|
+
const cases: Array<[string, string, string]> = [
|
|
8
|
+
// [input URI, expected registerId, expected conceptId]
|
|
9
|
+
['https://www.glossarist.org/iala-vocab/iala-2022/concept/1-1-000', 'iala-2022', '1-1-000'],
|
|
10
|
+
['https://glossarist.org/iala-2022/concept/1-1-000', 'iala-2022', '1-1-000'],
|
|
11
|
+
['https://glossarist.org/iso/concept/3.1.1', 'iso', '3.1.1'],
|
|
12
|
+
['https://oimlsmart.github.io/vocab/viml-2022/concept/1.1', 'viml-2022', '1.1'],
|
|
13
|
+
['https://www.oimlsmart.org/vocab/viml-2022/concept/1.1', 'viml-2022', '1.1'],
|
|
14
|
+
['https://metanorma.github.io/iala-vocab/iala-2023/concept/2-1-245', 'iala-2023', '2-1-245'],
|
|
15
|
+
['https://isotc204.geolexica.org/isotc204-2022/concept/3.5.8.8', 'isotc204-2022', '3.5.8.8'],
|
|
16
|
+
['https://vocab.example.org/dataset-x/concept/foo', 'dataset-x', 'foo'],
|
|
17
|
+
['https://custom.host/path/to/register/concept/abc-123', 'register', 'abc-123'],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
for (const [uri, expectedReg, expectedId] of cases) {
|
|
21
|
+
it(`parses ${uri}`, () => {
|
|
22
|
+
const parsed = UriRouter.parseUri(uri);
|
|
23
|
+
expect(parsed).toEqual({ registerId: expectedReg, conceptId: expectedId });
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
it('returns null for URIs without /concept/', () => {
|
|
28
|
+
expect(UriRouter.parseUri('https://example.org/foo/bar')).toBeNull();
|
|
29
|
+
expect(UriRouter.parseUri('urn:iso:std:iso:14812:2025:3.5.8.8')).toBeNull();
|
|
30
|
+
expect(UriRouter.parseUri('')).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('does NOT hardcode any specific hostname', () => {
|
|
34
|
+
// Every deployment domain should work. If the regex hardcodes
|
|
35
|
+
// a specific host, this test would fail for others.
|
|
36
|
+
const diverse = [
|
|
37
|
+
'https://a.io/r/concept/1',
|
|
38
|
+
'https://b.com/x/y/z/concept/2',
|
|
39
|
+
'https://1.2.3.4/reg/concept/3',
|
|
40
|
+
];
|
|
41
|
+
for (const uri of diverse) {
|
|
42
|
+
expect(UriRouter.parseUri(uri)).not.toBeNull();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -16,13 +16,13 @@ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
|
16
16
|
|
|
17
17
|
describe('buildAgentsTurtle (mjs)', () => {
|
|
18
18
|
it('parses without errors', async () => {
|
|
19
|
-
const ttl = await buildAgentsTurtle([{ name: 'Ada Lovelace' }]);
|
|
19
|
+
const ttl = await buildAgentsTurtle([{ name: 'Ada Lovelace' }], 'https://glossarist.org/agent');
|
|
20
20
|
const store = parse(ttl);
|
|
21
21
|
expect(store.size).toBeGreaterThan(0);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it('types each person as foaf:Person, prov:Person, prov:Agent', async () => {
|
|
25
|
-
const ttl = await buildAgentsTurtle([{ name: 'Ada Lovelace' }]);
|
|
25
|
+
const ttl = await buildAgentsTurtle([{ name: 'Ada Lovelace' }], 'https://glossarist.org/agent');
|
|
26
26
|
const store = parse(ttl);
|
|
27
27
|
const types = store.getObjects('https://glossarist.org/agent/ada-lovelace', RDF_TYPE, null).map(q => q.value);
|
|
28
28
|
expect(types).toContain(`${FOAF}Person`);
|
|
@@ -33,7 +33,7 @@ describe('buildAgentsTurtle (mjs)', () => {
|
|
|
33
33
|
it('records name, mailto mbox, role, and seeAlso', async () => {
|
|
34
34
|
const ttl = await buildAgentsTurtle([
|
|
35
35
|
{ name: 'Ada Lovelace', email: 'ada@example.org', role: 'Editor', url: 'https://example.org/ada' },
|
|
36
|
-
]);
|
|
36
|
+
], 'https://glossarist.org/agent');
|
|
37
37
|
const store = parse(ttl);
|
|
38
38
|
const iri = 'https://glossarist.org/agent/ada-lovelace';
|
|
39
39
|
expect(store.getObjects(iri, `${FOAF}name`, null).map(q => q.value)).toContain('Ada Lovelace');
|
|
@@ -46,7 +46,7 @@ describe('buildAgentsTurtle (mjs)', () => {
|
|
|
46
46
|
const ttl = await buildAgentsTurtle([
|
|
47
47
|
{ name: 'Alice', organization: 'ISO' },
|
|
48
48
|
{ name: 'Bob', organization: 'ISO' },
|
|
49
|
-
]);
|
|
49
|
+
], 'https://glossarist.org/agent');
|
|
50
50
|
const store = parse(ttl);
|
|
51
51
|
const types = store.getObjects('https://glossarist.org/org/iso', RDF_TYPE, null).map(q => q.value);
|
|
52
52
|
expect(types).toContain(`${FOAF}Organization`);
|
|
@@ -54,7 +54,7 @@ describe('buildAgentsTurtle (mjs)', () => {
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it('emits only prefix declarations for an empty input', async () => {
|
|
57
|
-
const ttl = await buildAgentsTurtle([]);
|
|
57
|
+
const ttl = await buildAgentsTurtle([], 'https://glossarist.org/agent');
|
|
58
58
|
expect(ttl).toContain('@prefix foaf:');
|
|
59
59
|
expect(ttl).not.toContain('<https://glossarist.org/agent/');
|
|
60
60
|
});
|
|
@@ -15,13 +15,13 @@ function parse(turtle: string): Store {
|
|
|
15
15
|
|
|
16
16
|
describe('buildBibliographyTurtle (mjs)', () => {
|
|
17
17
|
it('parses without errors', async () => {
|
|
18
|
-
const ttl = await buildBibliographyTurtle('iso-geodetic', { iso704: { reference: 'ISO 704' } });
|
|
18
|
+
const ttl = await buildBibliographyTurtle('iso-geodetic', { iso704: { reference: 'ISO 704' } }, 'https://glossarist.org');
|
|
19
19
|
const store = parse(ttl);
|
|
20
20
|
expect(store.size).toBeGreaterThan(0);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it('types each entry as dcterms:BibliographicResource', async () => {
|
|
24
|
-
const ttl = await buildBibliographyTurtle('iso-geodetic', { iso704: { reference: 'ISO 704' } });
|
|
24
|
+
const ttl = await buildBibliographyTurtle('iso-geodetic', { iso704: { reference: 'ISO 704' } }, 'https://glossarist.org');
|
|
25
25
|
const store = parse(ttl);
|
|
26
26
|
const types = store.getObjects('https://glossarist.org/iso-geodetic/bib/iso704', RDF_TYPE, null).map(q => q.value);
|
|
27
27
|
expect(types).toContain(`${DCTERMS}BibliographicResource`);
|
|
@@ -30,7 +30,7 @@ describe('buildBibliographyTurtle (mjs)', () => {
|
|
|
30
30
|
it('emits identifier, bibliographicCitation, and title', async () => {
|
|
31
31
|
const ttl = await buildBibliographyTurtle('iso-geodetic', {
|
|
32
32
|
iso704: { reference: 'ISO 704', title: 'Terminology work — Principles and methods' },
|
|
33
|
-
});
|
|
33
|
+
}, 'https://glossarist.org');
|
|
34
34
|
const store = parse(ttl);
|
|
35
35
|
const iri = 'https://glossarist.org/iso-geodetic/bib/iso704';
|
|
36
36
|
expect(store.getObjects(iri, `${DCTERMS}identifier`, null).map(q => q.value)).toContain('iso704');
|
|
@@ -41,7 +41,7 @@ describe('buildBibliographyTurtle (mjs)', () => {
|
|
|
41
41
|
it('emits foaf:page when link is provided', async () => {
|
|
42
42
|
const ttl = await buildBibliographyTurtle('iso-geodetic', {
|
|
43
43
|
ref: { reference: 'X', link: 'https://example.org/x' },
|
|
44
|
-
});
|
|
44
|
+
}, 'https://glossarist.org');
|
|
45
45
|
const store = parse(ttl);
|
|
46
46
|
const iri = 'https://glossarist.org/iso-geodetic/bib/ref';
|
|
47
47
|
expect(store.getObjects(iri, `${FOAF}page`, null).map(q => q.value)).toContain('https://example.org/x');
|
|
@@ -51,7 +51,7 @@ describe('buildBibliographyTurtle (mjs)', () => {
|
|
|
51
51
|
const ttl = await buildBibliographyTurtle('iso-geodetic', {
|
|
52
52
|
a: { reference: 'A' },
|
|
53
53
|
b: { reference: 'B' },
|
|
54
|
-
});
|
|
54
|
+
}, 'https://glossarist.org');
|
|
55
55
|
const store = parse(ttl);
|
|
56
56
|
expect(store.getObjects('https://glossarist.org/iso-geodetic/bib/a', `${DCTERMS}bibliographicCitation`, null).map(q => q.value)).toContain('A');
|
|
57
57
|
expect(store.getObjects('https://glossarist.org/iso-geodetic/bib/b', `${DCTERMS}bibliographicCitation`, null).map(q => q.value)).toContain('B');
|
|
@@ -61,7 +61,7 @@ describe('WS F Layer 8 — build pipeline integration', () => {
|
|
|
61
61
|
it('agents-turtle.mjs produces parseable foaf:Person', () => {
|
|
62
62
|
const out = runInline(`
|
|
63
63
|
import { buildAgentsTurtle } from './scripts/lib/agents-turtle.mjs';
|
|
64
|
-
console.log(await buildAgentsTurtle([{ name: 'Ada Lovelace', role: 'Editor', organization: 'Royal Society' }]));
|
|
64
|
+
console.log(await buildAgentsTurtle([{ name: 'Ada Lovelace', role: 'Editor', organization: 'Royal Society' }], 'https://glossarist.org/agent'));
|
|
65
65
|
`);
|
|
66
66
|
const store = parseTurtle(out);
|
|
67
67
|
const persons = [...store].filter(q =>
|
|
@@ -74,7 +74,7 @@ describe('WS F Layer 8 — build pipeline integration', () => {
|
|
|
74
74
|
it('bibliography-turtle.mjs produces parseable dcterms:BibliographicResource', () => {
|
|
75
75
|
const out = runInline(`
|
|
76
76
|
import { buildBibliographyTurtle } from './scripts/lib/bibliography-turtle.mjs';
|
|
77
|
-
console.log(await buildBibliographyTurtle('test', { iso704: { reference: 'ISO 704' } }));
|
|
77
|
+
console.log(await buildBibliographyTurtle('test', { iso704: { reference: 'ISO 704' } }, 'https://glossarist.org'));
|
|
78
78
|
`);
|
|
79
79
|
const store = parseTurtle(out);
|
|
80
80
|
const bibs = [...store].filter(q =>
|
|
@@ -122,8 +122,8 @@ describe('WS F Layer 8 — build pipeline integration', () => {
|
|
|
122
122
|
await buildVocabularyTurtle(),
|
|
123
123
|
await buildDatasetTurtle({ datasetIri: 'https://glossarist.org/test/', registerId: 'test', title: 't', modified: '2026-01-01', languages: ['eng'], distributions: [], topConceptUris: [], sections: [] }),
|
|
124
124
|
await buildActivityTurtle({ runId: 'r', startedAt: '2026-01-01T00:00:00Z', endedAt: '2026-01-01T00:05:00Z', toolId: 'cb', toolVersion: '0', datasetRegisters: [], conceptCount: 0 }),
|
|
125
|
-
await buildAgentsTurtle([{ name: 'Ada' }]),
|
|
126
|
-
await buildBibliographyTurtle('test', { x: { reference: 'X' } }),
|
|
125
|
+
await buildAgentsTurtle([{ name: 'Ada' }], 'https://glossarist.org/agent'),
|
|
126
|
+
await buildBibliographyTurtle('test', { x: { reference: 'X' } }, 'https://glossarist.org'),
|
|
127
127
|
await buildVersionHistoryTurtle({ registerId: 't', datasetIri: 'https://glossarist.org/t/', versions: [{ version: '1', generatedAt: '2026-01-01' }] }),
|
|
128
128
|
];
|
|
129
129
|
console.log(parts.join('\\n'));
|
|
@@ -38,6 +38,7 @@ export function makeManifest(overrides: Partial<Manifest> = {}): Manifest {
|
|
|
38
38
|
|
|
39
39
|
export interface AdapterStubOptions {
|
|
40
40
|
concepts?: ConceptSummary[];
|
|
41
|
+
manifest?: Manifest | null;
|
|
41
42
|
search?: () => SearchHit[];
|
|
42
43
|
fetchConcept?: () => Promise<any>;
|
|
43
44
|
getAdjacentConcepts?: () => { prev: string | null; next: string | null };
|
|
@@ -55,7 +56,7 @@ export interface AdapterStubOptions {
|
|
|
55
56
|
export function makeAdapterStub(options: AdapterStubOptions = {}): any {
|
|
56
57
|
return {
|
|
57
58
|
index: options.concepts ?? [],
|
|
58
|
-
manifest:
|
|
59
|
+
manifest: options.manifest ?? makeManifest(),
|
|
59
60
|
registerId: 'test',
|
|
60
61
|
getConceptCount: () => (options.concepts ?? []).length,
|
|
61
62
|
getConcepts: () => options.concepts ?? [],
|
|
@@ -42,7 +42,9 @@ export class GraphDataSource {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
private get uriBase(): string {
|
|
45
|
-
|
|
45
|
+
const uriBase = this.adapter.manifest?.uriBase;
|
|
46
|
+
if (!uriBase) throw new Error('GraphDataSource: manifest.uriBase is required');
|
|
47
|
+
return uriBase;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
private get urnMap(): ReadonlyMap<string, string> {
|
|
@@ -18,6 +18,7 @@ import { useDsStyle } from '../utils/dataset-style';
|
|
|
18
18
|
import { getFactory } from '../adapters/factory';
|
|
19
19
|
import { SPHERE_RELATION_CATEGORIES as RELATION_CATEGORIES, sphereCategoryForType as categoryForType, categorizeRelationForSphere as categorizeRelation, colorForTypeInMode as colorForTypeRaw, relationLabel as relationTypeLabel } from '../utils/relation-sphere-styling';
|
|
20
20
|
import { easeInOutCubic, slerp, fibonacciSpherePosition, project, cardEdge, type Vec3 } from '../composables/useSphereProjection';
|
|
21
|
+
import { UriRouter } from '../adapters/UriRouter';
|
|
21
22
|
import { getPreferredTerm } from '../utils/concept-helpers';
|
|
22
23
|
import { renderContent } from '../utils/content-renderer';
|
|
23
24
|
import { useI18n } from '../i18n';
|
|
@@ -112,7 +113,8 @@ let navDuration = 2200; /* mutable — shorter for expand changes than for conc
|
|
|
112
113
|
async function loadNeighborTerms() {
|
|
113
114
|
const factory = getFactory();
|
|
114
115
|
const lang = sphereLang.value;
|
|
115
|
-
const uriBase = props.manifest.uriBase
|
|
116
|
+
const uriBase = props.manifest.uriBase;
|
|
117
|
+
if (!uriBase) throw new Error('RelationSphere: manifest.uriBase is required');
|
|
116
118
|
const neighborRegisters = new Set<string>();
|
|
117
119
|
|
|
118
120
|
/* Phase 1: collect which neighbor datasets we need to load */
|
|
@@ -226,14 +228,6 @@ function updateNodeTerm(n: SNode) {
|
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
/* ── Build internal graph from concept + edges (BFS) ────── */
|
|
229
|
-
function parseNeighborUri(uri: string, fallbackReg: string): { register: string; conceptId: string } | null {
|
|
230
|
-
const m = uri.match(/(?:glossarist\.org|oimlsmart\.github\.io\/vocab)\/([^/]+)\/concept\/([^/?#]+)/);
|
|
231
|
-
if (m) return { register: m[1], conceptId: m[2] };
|
|
232
|
-
/* URN or other shape — use fallback register, last path segment as id */
|
|
233
|
-
const parts = uri.split('/').filter(Boolean);
|
|
234
|
-
if (parts.length === 0) return null;
|
|
235
|
-
return { register: fallbackReg, conceptId: parts[parts.length - 1] };
|
|
236
|
-
}
|
|
237
231
|
|
|
238
232
|
function hashSeed(s: string): number {
|
|
239
233
|
let h = 0;
|
|
@@ -280,12 +274,12 @@ function buildGraph() {
|
|
|
280
274
|
|
|
281
275
|
/* First pass: collect depth-1 neighbors (deduped) so we know N for even
|
|
282
276
|
ring placement. */
|
|
283
|
-
const depth1List: Array<{ uri: string; parsed: {
|
|
277
|
+
const depth1List: Array<{ uri: string; parsed: { registerId: string; conceptId: string }; isOutgoing: boolean; edge: GraphEdge }> = [];
|
|
284
278
|
for (const edge of props.edges) {
|
|
285
279
|
const isOutgoing = edge.source === focusUri || edge.source === focusId;
|
|
286
280
|
const otherUri = isOutgoing ? edge.target : edge.source;
|
|
287
281
|
if (!otherUri || visited.has(otherUri)) continue;
|
|
288
|
-
const parsed =
|
|
282
|
+
const parsed = UriRouter.parseUri(otherUri);
|
|
289
283
|
if (!parsed || parsed.conceptId === focusId) continue;
|
|
290
284
|
visited.set(otherUri, 1);
|
|
291
285
|
depth1List.push({ uri: otherUri, parsed, isOutgoing, edge });
|
|
@@ -298,8 +292,8 @@ function buildGraph() {
|
|
|
298
292
|
nodeMap.set(parsed.conceptId, {
|
|
299
293
|
id: parsed.conceptId,
|
|
300
294
|
term: edge.label || parsed.conceptId,
|
|
301
|
-
ref: parsed.
|
|
302
|
-
register: parsed.
|
|
295
|
+
ref: parsed.registerId,
|
|
296
|
+
register: parsed.registerId,
|
|
303
297
|
conceptId: parsed.conceptId,
|
|
304
298
|
depth: 1,
|
|
305
299
|
x: pos.x, y: pos.y, z: pos.z,
|
|
@@ -331,13 +325,13 @@ function buildGraph() {
|
|
|
331
325
|
const inEdges = store.graph.getIncomingEdges(uri);
|
|
332
326
|
/* Collect this node's children first so we can place them as an even
|
|
333
327
|
sub-ring around their parent. */
|
|
334
|
-
const children: Array<{ uri: string; parsed: {
|
|
328
|
+
const children: Array<{ uri: string; parsed: { registerId: string; conceptId: string }; isOutgoing: boolean; edge: GraphEdge }> = [];
|
|
335
329
|
for (const edge of [...outEdges, ...inEdges]) {
|
|
336
330
|
if (nodeMap.size + children.length >= MAX_NODES) break;
|
|
337
331
|
const isOutgoing = edge.source === uri;
|
|
338
332
|
const otherUri = isOutgoing ? edge.target : edge.source;
|
|
339
333
|
if (!otherUri || visited.has(otherUri)) continue;
|
|
340
|
-
const parsed =
|
|
334
|
+
const parsed = UriRouter.parseUri(otherUri);
|
|
341
335
|
if (!parsed || parsed.conceptId === focusId || nodeMap.has(parsed.conceptId)) continue;
|
|
342
336
|
visited.set(otherUri, depth + 1);
|
|
343
337
|
children.push({ uri: otherUri, parsed, isOutgoing, edge });
|
|
@@ -347,8 +341,8 @@ function buildGraph() {
|
|
|
347
341
|
nodeMap.set(parsed.conceptId, {
|
|
348
342
|
id: parsed.conceptId,
|
|
349
343
|
term: parsed.conceptId,
|
|
350
|
-
ref: parsed.
|
|
351
|
-
register: parsed.
|
|
344
|
+
ref: parsed.registerId,
|
|
345
|
+
register: parsed.registerId,
|
|
352
346
|
conceptId: parsed.conceptId,
|
|
353
347
|
depth: depth + 1,
|
|
354
348
|
x: pos.x, y: pos.y, z: pos.z,
|
package/src/stores/vocabulary.ts
CHANGED
|
@@ -207,7 +207,8 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
|
|
|
207
207
|
// We do NOT call adapter.extractEdges(concept) here because that would duplicate
|
|
208
208
|
// edges already present in edges.json, causing double-rendering of relations.
|
|
209
209
|
const domainEdges = adapter.extractDomainEdges(concept);
|
|
210
|
-
const uriBase = adapter.manifest?.uriBase
|
|
210
|
+
const uriBase = adapter.manifest?.uriBase;
|
|
211
|
+
if (!uriBase) throw new Error('vocabulary store: manifest.uriBase is required — set uriBase in site-config.yml');
|
|
211
212
|
const uri = conceptUri(concept, registerId, uriBase);
|
|
212
213
|
|
|
213
214
|
const designations: Record<string, string> = {};
|