@glossarist/concept-browser 0.5.0 → 0.5.1

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 CHANGED
@@ -291,6 +291,38 @@ BASE_PATH=/vocab/ npm run build
291
291
 
292
292
  This sets the Vite `base` config so all asset paths are prefixed correctly.
293
293
 
294
+ **How it works:**
295
+
296
+ The `BASE_PATH` environment variable controls subpath deployment through two mechanisms:
297
+
298
+ 1. **Build time (Node.js scripts):** `generate-data.mjs` uses `BASE_PATH` to prefix logo paths in the generated `site-config.json`. The favicon generation in `cli/index.mjs` also prefixes favicon link hrefs. These are build-time rewrites because they produce static JSON/HTML that can't use Vite's runtime resolution.
299
+
300
+ 2. **Runtime (browser):** The Vue app uses `import.meta.env.BASE_URL` (a Vite compile-time constant derived from `base`) to prefix all `fetch()` paths. This includes:
301
+ - `datasets.json` (dataset registry)
302
+ - `site-config.json` (branding, features)
303
+ - `data/{id}/...` (concept data via `DatasetAdapter`)
304
+ - `pages/*.json` (content pages)
305
+ - `news.json` (news feed)
306
+ - `data/{id}/bibliography.json` and `data/{id}/images/*` (render-time resources)
307
+
308
+ The Vite `base` config normalizes trailing slashes automatically, so `BASE_PATH=/vocab` and `BASE_PATH=/vocab/` both work. The value is passed through to `import.meta.env.BASE_URL` which always ends with `/`.
309
+
310
+ **CLI usage:**
311
+
312
+ When using the `concept-browser` CLI from a deployment repo (not the package source):
313
+
314
+ ```bash
315
+ git clone --branch fix/subpath-base-url --depth 1 https://github.com/glossarist/concept-browser.git /tmp/cb
316
+ cd /tmp/cb && npm install --ignore-scripts
317
+ npm install --prefix /tmp/cb sharp 2>/dev/null || true
318
+
319
+ # Build from the deployment repo's working directory
320
+ cd /path/to/deployment-repo
321
+ node /tmp/cb/cli/index.mjs build
322
+ ```
323
+
324
+ The CLI looks for `site-config.yml` in the CWD first, then falls back to the package's own `site-config.yml`.
325
+
294
326
  ### Other hosting platforms
295
327
 
296
328
  The build produces static files in `dist/` with an SPA `404.html` fallback. Deploy `dist/` to any static host:
package/cli/index.mjs CHANGED
@@ -141,6 +141,10 @@ Environment:
141
141
 
142
142
  // Pass favicon tags to Vite via env
143
143
  if (faviconHtml) {
144
+ const basePath = process.env.BASE_PATH?.replace(/\/+$/, '') || '';
145
+ if (basePath) {
146
+ faviconHtml = faviconHtml.replace(/(href|content)="\/([^"]+)"/g, `$1="${basePath}/$2"`);
147
+ }
144
148
  process.env.FAVICON_HTML = faviconHtml;
145
149
  }
146
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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": {
@@ -1063,10 +1063,11 @@ const processedPages = processPages(config);
1063
1063
  // Generate site-config.json from site config
1064
1064
  const siteBranding = { ...config.branding };
1065
1065
  // Rewrite logo paths to destination filenames and strip build-time fields
1066
+ const basePathPrefix = process.env.BASE_PATH?.replace(/\/+$/, '') || '';
1066
1067
  for (const key of ['logo', 'footerLogo']) {
1067
1068
  const suffix = key === 'logo' ? 'logo.svg' : 'footer-logo.svg';
1068
1069
  if (siteBranding[key]) {
1069
- siteBranding[key] = { ...siteBranding[key], path: `/logos/${config.id}-${suffix}` };
1070
+ siteBranding[key] = { ...siteBranding[key], path: `${basePathPrefix}/logos/${config.id}-${suffix}` };
1070
1071
  delete siteBranding[key].localPath;
1071
1072
  delete siteBranding[key].remoteUrl;
1072
1073
  }
@@ -13,7 +13,9 @@ import { fileURLToPath } from 'url';
13
13
 
14
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
15
  const ROOT = resolve(__dirname, '..');
16
- const ONTOLOGY_TTL = resolve(ROOT, '..', 'concept-model', 'ontologies', 'glossarist.ttl');
16
+ const CONCEPT_MODEL = resolve(ROOT, '..', 'concept-model', 'ontologies');
17
+ const ONTOLOGY_TTL = resolve(CONCEPT_MODEL, 'glossarist.ttl');
18
+ const SHACL_TTL = resolve(CONCEPT_MODEL, 'shapes', 'glossarist.shacl.ttl');
17
19
  const OUTPUT = resolve(ROOT, 'src', 'data', 'ontology-schema.json');
18
20
 
19
21
  const KNOWN_PREFIXES = {
@@ -27,6 +29,8 @@ const KNOWN_PREFIXES = {
27
29
  dcterms: 'http://purl.org/dc/terms/',
28
30
  prov: 'http://www.w3.org/ns/prov#',
29
31
  xsd: 'http://www.w3.org/2001/XMLSchema#',
32
+ sh: 'http://www.w3.org/ns/shacl#',
33
+ vann: 'http://purl.org/vocab/vann/',
30
34
  };
31
35
 
32
36
  function expandPrefixed(term) {
@@ -49,12 +53,14 @@ function compactIri(iri) {
49
53
 
50
54
  /**
51
55
  * Minimal TTL subject-block splitter. Handles nested [] and () and quoted strings.
56
+ * Tracks <...> URI references to avoid splitting on dots inside URIs.
52
57
  */
53
58
  function splitSubjectBlocks(text) {
54
59
  const blocks = [];
55
60
  let depth = 0;
56
61
  let start = -1;
57
62
  let inTripleQuote = false;
63
+ let inUri = false;
58
64
 
59
65
  for (let i = 0; i < text.length; i++) {
60
66
  const ch = text[i];
@@ -82,10 +88,20 @@ function splitSubjectBlocks(text) {
82
88
  continue;
83
89
  }
84
90
 
91
+ if (inUri) {
92
+ if (ch === '>') inUri = false;
93
+ continue;
94
+ }
95
+ if (ch === '<') {
96
+ inUri = true;
97
+ if (start < 0) start = i;
98
+ continue;
99
+ }
100
+
85
101
  if (ch === '[' || ch === '(') depth++;
86
102
  if (ch === ']' || ch === ')') depth--;
87
103
 
88
- if (depth === 0 && ch === '.') {
104
+ if (!inUri && depth === 0 && ch === '.') {
89
105
  if (start >= 0) {
90
106
  blocks.push(text.slice(start, i));
91
107
  start = -1;
@@ -141,10 +157,81 @@ function extractAllResources(block, predicate) {
141
157
  return [...new Set(results)];
142
158
  }
143
159
 
160
+ /**
161
+ * Strip @prefix and @base lines. Strip comments (#...) but preserve # inside <...> URIs
162
+ * and inside quoted strings.
163
+ */
164
+ function preprocessTtl(ttlText) {
165
+ const lines = ttlText.split('\n');
166
+ const filtered = lines.filter(l => !l.trimStart().startsWith('@prefix') && !l.trimStart().startsWith('@base'));
167
+ const text = filtered.join('\n');
168
+
169
+ // Character-by-character comment stripping
170
+ let result = '';
171
+ let inTriple = false;
172
+ let inSingle = false;
173
+ let inUri = false;
174
+
175
+ for (let i = 0; i < text.length; i++) {
176
+ const ch = text[i];
177
+
178
+ if (inTriple) {
179
+ result += ch;
180
+ if (ch === '"' && text.slice(i, i + 3) === '"""') {
181
+ result += text.slice(i + 1, i + 3);
182
+ i += 2;
183
+ inTriple = false;
184
+ }
185
+ continue;
186
+ }
187
+
188
+ if (inSingle) {
189
+ result += ch;
190
+ if (ch === '\\') { result += text[i + 1]; i++; continue; }
191
+ if (ch === '"') inSingle = false;
192
+ continue;
193
+ }
194
+
195
+ if (inUri) {
196
+ result += ch;
197
+ if (ch === '>') inUri = false;
198
+ continue;
199
+ }
200
+
201
+ if (ch === '"' && text.slice(i, i + 3) === '"""') {
202
+ result += '"""';
203
+ inTriple = true;
204
+ i += 2;
205
+ continue;
206
+ }
207
+
208
+ if (ch === '"') {
209
+ result += ch;
210
+ inSingle = true;
211
+ continue;
212
+ }
213
+
214
+ if (ch === '<') {
215
+ result += ch;
216
+ inUri = true;
217
+ continue;
218
+ }
219
+
220
+ if (ch === '#') {
221
+ // Skip until end of line
222
+ while (i < text.length && text[i] !== '\n') i++;
223
+ if (i < text.length) result += '\n';
224
+ continue;
225
+ }
226
+
227
+ result += ch;
228
+ }
229
+
230
+ return result;
231
+ }
232
+
144
233
  function parseOntology(ttlText) {
145
- const rawLines = ttlText.split('\n');
146
- // Remove comment lines but keep content
147
- const cleaned = rawLines.map(l => l.replace(/#[^\n]*/g, '')).join('\n');
234
+ const cleaned = preprocessTtl(ttlText);
148
235
 
149
236
  const blocks = splitSubjectBlocks(cleaned);
150
237
 
@@ -160,8 +247,7 @@ function parseOntology(ttlText) {
160
247
  if (!subjectMatch) continue;
161
248
  const subject = subjectMatch[1];
162
249
 
163
- // Skip ontology declaration, prefix declarations
164
- if (subject === '@prefix' || subject.startsWith('@')) continue;
250
+ // Skip ontology declaration
165
251
  if (subject.includes('glossarist>') && !subject.startsWith('gloss:')) continue;
166
252
 
167
253
  // Determine type
@@ -278,6 +364,176 @@ function groupPropertiesByDomain(properties) {
278
364
  return groups;
279
365
  }
280
366
 
367
+ /**
368
+ * Extract individual sh:property [...] constraint blocks from a shape block.
369
+ * Returns an array of strings, each being the inner content of one [ ... ].
370
+ */
371
+ function extractPropertyBlocks(shapeBlock) {
372
+ const blocks = [];
373
+ const re = /sh:property\s+\[/g;
374
+ let match;
375
+ while ((match = re.exec(shapeBlock)) !== null) {
376
+ const start = match.index + match[0].length - 1; // position of '['
377
+ let depth = 1;
378
+ let i = start + 1;
379
+ while (i < shapeBlock.length && depth > 0) {
380
+ if (shapeBlock[i] === '[') depth++;
381
+ else if (shapeBlock[i] === ']') depth--;
382
+ i++;
383
+ }
384
+ blocks.push(shapeBlock.slice(start + 1, i - 1));
385
+ }
386
+ return blocks;
387
+ }
388
+
389
+ function parseConstraint(propBlock) {
390
+ const c = {};
391
+ const path = extractResource(propBlock, 'sh:path');
392
+ c.path = path ? compactIri(expandPrefixed(path)) : null;
393
+
394
+ const dt = extractResource(propBlock, 'sh:datatype');
395
+ c.datatype = dt ? compactIri(expandPrefixed(dt)) : null;
396
+
397
+ const cls = extractResource(propBlock, 'sh:class');
398
+ c.class = cls ? compactIri(expandPrefixed(cls)) : null;
399
+
400
+ const vf = extractResource(propBlock, 'sh:valuesFrom');
401
+ c.valuesFrom = vf ? compactIri(expandPrefixed(vf)) : null;
402
+
403
+ const nk = extractResource(propBlock, 'sh:nodeKind');
404
+ c.nodeKind = nk ? compactIri(expandPrefixed(nk)) : null;
405
+
406
+ const minMatch = propBlock.match(/sh:minCount\s+(\d+)/);
407
+ c.minCount = minMatch ? parseInt(minMatch[1], 10) : null;
408
+
409
+ const maxMatch = propBlock.match(/sh:maxCount\s+(\d+)/);
410
+ c.maxCount = maxMatch ? parseInt(maxMatch[1], 10) : null;
411
+
412
+ // sh:in ( "val1" "val2" ... )
413
+ const inMatch = propBlock.match(/sh:in\s*\(([^)]+)\)/);
414
+ if (inMatch) {
415
+ c.in = inMatch[1].match(/"([^"]+)"/g)?.map(s => s.replace(/"/g, '')) || null;
416
+ } else {
417
+ c.in = null;
418
+ }
419
+
420
+ return c;
421
+ }
422
+
423
+ function parseShaclShapes(ttlText) {
424
+ const cleaned = preprocessTtl(ttlText);
425
+ const blocks = splitSubjectBlocks(cleaned);
426
+
427
+ const shapes = [];
428
+ for (const block of blocks) {
429
+ const trimmed = block.trim();
430
+ if (!trimmed) continue;
431
+
432
+ const subjectMatch = trimmed.match(/^([^\s]+)/);
433
+ if (!subjectMatch) continue;
434
+ const subject = subjectMatch[1];
435
+
436
+ // Check if this is a sh:NodeShape
437
+ if (!/\ba\s+sh:NodeShape\b/.test(trimmed)) continue;
438
+
439
+ const iri = expandPrefixed(subject);
440
+ const compact = compactIri(iri);
441
+ const label = extractLiteral(trimmed, 'rdfs:label');
442
+ const comment = extractLiteral(trimmed, 'rdfs:comment');
443
+ const targetClass = extractResource(trimmed, 'sh:targetClass');
444
+
445
+ const propBlocks = extractPropertyBlocks(trimmed);
446
+ const constraints = propBlocks.map(parseConstraint).filter(c => c.path);
447
+
448
+ // sh:class at shape level (not inside sh:property blocks)
449
+ const shapeClassMatch = trimmed.match(/^\s*[^[]*?\ba\s+sh:NodeShape\s[^[]*?sh:class\s+([^\s,;]+)/);
450
+ const shapeClass = shapeClassMatch ? shapeClassMatch[1].replace(/[;.]+$/, '') : null;
451
+
452
+ shapes.push({
453
+ iri,
454
+ compact,
455
+ label: label || subject.replace('gloss:', '').replace('Shape', ''),
456
+ comment,
457
+ targetClass: targetClass ? compactIri(expandPrefixed(targetClass)) : null,
458
+ shapeClass: shapeClass ? compactIri(expandPrefixed(shapeClass)) : null,
459
+ constraints,
460
+ });
461
+ }
462
+
463
+ return shapes;
464
+ }
465
+
466
+ const IMPORT_LABELS = {
467
+ 'http://www.w3.org/2004/02/skos/core': 'SKOS',
468
+ 'http://www.w3.org/2004/02/skos/core#': 'SKOS',
469
+ 'http://www.w3.org/2008/05/skos-xl': 'SKOS-XL',
470
+ 'http://www.w3.org/2008/05/skos-xl#': 'SKOS-XL',
471
+ 'http://purl.org/iso25964/skos-thes': 'ISO 25964',
472
+ 'http://purl.org/iso25964/skos-thes#': 'ISO 25964',
473
+ 'http://www.w3.org/ns/prov#': 'PROV-O',
474
+ 'http://purl.org/dc/terms/': 'Dublin Core Terms',
475
+ };
476
+
477
+ function parseOntologyDeclaration(ttlText) {
478
+ const cleaned = preprocessTtl(ttlText);
479
+ const blocks = splitSubjectBlocks(cleaned);
480
+
481
+ for (const block of blocks) {
482
+ const trimmed = block.trim();
483
+ if (!trimmed) continue;
484
+
485
+ // Match the owl:Ontology declaration
486
+ if (!/\ba\s+owl:Ontology\b/.test(trimmed)) continue;
487
+
488
+ const subjectMatch = trimmed.match(/^<([^>]+)>/);
489
+ const iri = subjectMatch ? subjectMatch[1] : null;
490
+ const label = extractLiteral(trimmed, 'rdfs:label') || extractLiteral(trimmed, 'dcterms:title');
491
+ const comment = extractLiteral(trimmed, 'rdfs:comment') || extractLiteral(trimmed, 'dcterms:description');
492
+ const prefix = extractLiteral(trimmed, 'vann:preferredNamespacePrefix');
493
+ const nsUri = extractLiteral(trimmed, 'vann:preferredNamespaceUri');
494
+ const license = extractResource(trimmed, 'dcterms:license')?.replace(/[<>]/g, '') || null;
495
+ const created = extractLiteral(trimmed, 'dcterms:created');
496
+
497
+ const imports = [];
498
+ const importRe = /owl:imports\s+<([^>]+)>/g;
499
+ let im;
500
+ while ((im = importRe.exec(trimmed)) !== null) {
501
+ const iri = im[1];
502
+ imports.push({
503
+ iri,
504
+ label: IMPORT_LABELS[iri] || IMPORT_LABELS[iri.replace(/#?$/, '#')] || compactIri(iri),
505
+ });
506
+ }
507
+
508
+ return { iri, label, comment, prefix, namespaceUri: nsUri, imports, license, created };
509
+ }
510
+ return null;
511
+ }
512
+
513
+ function parseAnnotationProperties(ttlText) {
514
+ const cleaned = preprocessTtl(ttlText);
515
+ const blocks = splitSubjectBlocks(cleaned);
516
+
517
+ const props = [];
518
+ for (const block of blocks) {
519
+ const trimmed = block.trim();
520
+ if (!trimmed) continue;
521
+
522
+ if (!/\ba\s+owl:AnnotationProperty\b/.test(trimmed)) continue;
523
+
524
+ const subjectMatch = trimmed.match(/^([^\s]+)/);
525
+ if (!subjectMatch) continue;
526
+ const subject = subjectMatch[1];
527
+ const iri = expandPrefixed(subject);
528
+ const compact = compactIri(iri);
529
+ const label = extractLiteral(trimmed, 'rdfs:label');
530
+
531
+ props.push({ iri, compact, label: label || compact });
532
+ }
533
+
534
+ return props;
535
+ }
536
+
281
537
  function main() {
282
538
  if (!existsSync(ONTOLOGY_TTL)) {
283
539
  console.error(`Ontology file not found: ${ONTOLOGY_TTL}`);
@@ -291,24 +547,70 @@ function main() {
291
547
  const hierarchy = buildClassHierarchy(classes);
292
548
  const propsByDomain = groupPropertiesByDomain(properties);
293
549
 
550
+ let shapes = [];
551
+ if (existsSync(SHACL_TTL)) {
552
+ const shaclText = readFileSync(SHACL_TTL, 'utf-8');
553
+ shapes = parseShaclShapes(shaclText);
554
+ } else {
555
+ console.warn(`SHACL shapes file not found: ${SHACL_TTL}`);
556
+ }
557
+
558
+ const ontologyDecl = parseOntologyDeclaration(ttlText);
559
+ let annotationProps = parseAnnotationProperties(ttlText);
560
+
561
+ // Hardcode standard annotation properties used in the ontology (not declared as owl:AnnotationProperty)
562
+ if (annotationProps.length === 0) {
563
+ annotationProps = [
564
+ { iri: 'http://www.w3.org/2000/01/rdf-schema#label', compact: 'rdfs:label', label: 'label' },
565
+ { iri: 'http://www.w3.org/2000/01/rdf-schema#comment', compact: 'rdfs:comment', label: 'comment' },
566
+ { iri: 'http://www.w3.org/2000/01/rdf-schema#seeAlso', compact: 'rdfs:seeAlso', label: 'seeAlso' },
567
+ { iri: 'http://www.w3.org/2000/01/rdf-schema#isDefinedBy', compact: 'rdfs:isDefinedBy', label: 'isDefinedBy' },
568
+ { iri: 'http://purl.org/dc/terms/title', compact: 'dcterms:title', label: 'title' },
569
+ { iri: 'http://purl.org/dc/terms/description', compact: 'dcterms:description', label: 'description' },
570
+ { iri: 'http://purl.org/dc/terms/source', compact: 'dcterms:source', label: 'source' },
571
+ { iri: 'http://purl.org/dc/terms/license', compact: 'dcterms:license', label: 'license' },
572
+ { iri: 'http://purl.org/dc/terms/created', compact: 'dcterms:created', label: 'created' },
573
+ { iri: 'http://purl.org/vocab/vann/preferredNamespacePrefix', compact: 'vann:preferredNamespacePrefix', label: 'preferredNamespacePrefix' },
574
+ { iri: 'http://purl.org/vocab/vann/preferredNamespaceUri', compact: 'vann:preferredNamespaceUri', label: 'preferredNamespaceUri' },
575
+ ];
576
+ }
577
+
578
+ // Group shapes by targetClass
579
+ const shapesByTargetClass = {};
580
+ for (const s of shapes) {
581
+ const tc = s.targetClass || '(unspecified)';
582
+ if (!shapesByTargetClass[tc]) shapesByTargetClass[tc] = [];
583
+ shapesByTargetClass[tc].push(s.compact);
584
+ }
585
+
294
586
  const output = {
295
- ontologyIri: 'https://www.glossarist.org/ontologies/glossarist',
296
- ontologyLabel: 'Glossarist Ontology',
587
+ ontology: ontologyDecl,
588
+ ontologyIri: ontologyDecl?.iri || 'https://www.glossarist.org/ontologies/glossarist',
589
+ ontologyLabel: ontologyDecl?.label || 'Glossarist Ontology',
297
590
  classes: hierarchy.map,
298
591
  classHierarchyRoots: hierarchy.roots,
299
592
  properties: Object.fromEntries(properties.map(p => [p.compact, p])),
300
593
  propertiesByDomain: propsByDomain,
594
+ shapes: Object.fromEntries(shapes.map(s => [s.compact, s])),
595
+ shapesByTargetClass,
596
+ annotationProperties: annotationProps,
301
597
  stats: {
302
598
  classCount: classes.length,
303
599
  objectPropertyCount: properties.filter(p => p.type === 'object').length,
304
600
  datatypePropertyCount: properties.filter(p => p.type === 'datatype').length,
601
+ shapeCount: shapes.length,
602
+ annotationPropertyCount: annotationProps.length,
305
603
  },
306
604
  };
307
605
 
308
606
  mkdirSync(dirname(OUTPUT), { recursive: true });
309
607
  writeFileSync(OUTPUT, JSON.stringify(output, null, 2) + '\n');
310
608
 
311
- console.log(`Parsed ${output.stats.classCount} classes, ${output.stats.objectPropertyCount} object properties, ${output.stats.datatypePropertyCount} datatype properties`);
609
+ console.log(`Parsed ${output.stats.classCount} classes, ${output.stats.objectPropertyCount} object properties, ${output.stats.datatypePropertyCount} datatype properties, ${output.stats.shapeCount} SHACL shapes, ${output.stats.annotationPropertyCount} annotation properties`);
610
+ if (ontologyDecl) {
611
+ console.log(`Ontology: ${ontologyDecl.iri}`);
612
+ console.log(` imports: ${ontologyDecl.imports.map(i => i.label).join(', ')}`);
613
+ }
312
614
  console.log(`Wrote ${OUTPUT}`);
313
615
  }
314
616
 
@@ -108,8 +108,22 @@ describe('ConceptCard', () => {
108
108
  expect(router.currentRoute.value.params.conceptId).toBe('3.1.1.1');
109
109
  });
110
110
 
111
- it('shows language count from manifest', () => {
111
+ it('shows language count from designations', () => {
112
112
  const wrapper = mountCard();
113
- expect(wrapper.text()).toContain('2 lang');
113
+ expect(wrapper.text()).toContain('1 lang');
114
+ });
115
+
116
+ it('shows selected language designation as title', () => {
117
+ const wrapper = mountCard(makeEntry({
118
+ designations: { eng: 'test term', fra: 'terme test' },
119
+ eng: 'test term',
120
+ }), 'test');
121
+ expect(wrapper.text()).toContain('test term');
122
+
123
+ const wrapperFra = mount(ConceptCard, {
124
+ global: { plugins: [pinia, router] },
125
+ props: { entry: makeEntry({ designations: { eng: 'test term', fra: 'terme test' }, eng: 'test term' }), registerId: 'test', displayLang: 'fra' },
126
+ });
127
+ expect(wrapperFra.text()).toContain('terme test');
114
128
  });
115
129
  });
@@ -18,9 +18,10 @@ export class AdapterFactory {
18
18
  if (!resp.ok) throw new Error(`Failed to load dataset registry: ${resp.status}`);
19
19
  const registry = (await resp.json()) as DatasetRegistry[];
20
20
 
21
+ const base = import.meta.env.BASE_URL;
21
22
  const adapters: DatasetAdapter[] = [];
22
23
  for (const reg of registry) {
23
- const adapter = new DatasetAdapter(reg.id, `/data/${reg.id}`);
24
+ const adapter = new DatasetAdapter(reg.id, `${base}data/${reg.id}`);
24
25
  this.adapters.set(reg.id, adapter);
25
26
  adapters.push(adapter);
26
27
  }
@@ -45,7 +46,7 @@ export class AdapterFactory {
45
46
  const manifest = await adapter.loadManifest();
46
47
  await adapter.loadIndex();
47
48
 
48
- this.router.registerDataset(registerId, `/data/${registerId}`, manifest);
49
+ this.router.registerDataset(registerId, `${import.meta.env.BASE_URL}data/${registerId}`, manifest);
49
50
 
50
51
  const uriPatterns = [
51
52
  manifest.datasetUri,
@@ -238,6 +238,7 @@ function conceptFromJsonLd(doc: Record<string, any>): Concept {
238
238
  }
239
239
 
240
240
  const related = (doc['gl:references'] ?? []).map(mapRelatedFromJsonLd);
241
+ const tags = Array.isArray(doc['gl:tags']) ? [...doc['gl:tags']] : [];
241
242
 
242
243
  return Concept.fromJSON({
243
244
  id,
@@ -245,6 +246,7 @@ function conceptFromJsonLd(doc: Record<string, any>): Concept {
245
246
  uri: doc['@id'] ?? null,
246
247
  localizations,
247
248
  related,
249
+ tags,
248
250
  status: null,
249
251
  });
250
252
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Ontology schema loader — provides class/property definitions parsed from
2
+ * Ontology schema loader — provides class/property/shape definitions parsed from
3
3
  * the Glossarist OWL ontology for the Ontospy-style concept view.
4
4
  */
5
5
  import schemaData from '../data/ontology-schema.json';
@@ -28,14 +28,72 @@ export interface OwlProperty {
28
28
  inverseOf: string | null;
29
29
  }
30
30
 
31
+ export interface ShaclConstraint {
32
+ path: string | null;
33
+ datatype: string | null;
34
+ class: string | null;
35
+ valuesFrom: string | null;
36
+ nodeKind: string | null;
37
+ minCount: number | null;
38
+ maxCount: number | null;
39
+ in: string[] | null;
40
+ }
41
+
42
+ export interface OwlShape {
43
+ iri: string;
44
+ compact: string;
45
+ label: string;
46
+ comment: string | null;
47
+ targetClass: string | null;
48
+ shapeClass: string | null;
49
+ constraints: ShaclConstraint[];
50
+ }
51
+
52
+ export interface OwlOntology {
53
+ iri: string;
54
+ label: string;
55
+ comment: string | null;
56
+ prefix: string | null;
57
+ namespaceUri: string | null;
58
+ imports: { iri: string; label: string }[];
59
+ license: string | null;
60
+ created: string | null;
61
+ }
62
+
63
+ export interface AnnotationProperty {
64
+ iri: string;
65
+ compact: string;
66
+ label: string;
67
+ }
68
+
69
+ export type EntityType = 'class' | 'objectProperty' | 'datatypeProperty' | 'shape' | 'annotationProperty';
70
+
71
+ export const ENTITY_TYPE_META: Record<EntityType, { label: string; color: string }> = {
72
+ class: { label: 'Classes', color: 'blue' },
73
+ objectProperty: { label: 'Object Properties', color: 'emerald' },
74
+ datatypeProperty: { label: 'Datatype Properties', color: 'amber' },
75
+ shape: { label: 'SHACL Shapes', color: 'purple' },
76
+ annotationProperty: { label: 'Annotation Properties', color: 'pink' },
77
+ };
78
+
31
79
  interface OntologySchema {
80
+ ontology: OwlOntology | null;
32
81
  ontologyIri: string;
33
82
  ontologyLabel: string;
34
83
  classes: Record<string, OwlClass>;
35
84
  classHierarchyRoots: string[];
36
85
  properties: Record<string, OwlProperty>;
37
86
  propertiesByDomain: Record<string, { object: string[]; datatype: string[] }>;
38
- stats: { classCount: number; objectPropertyCount: number; datatypePropertyCount: number };
87
+ shapes: Record<string, OwlShape>;
88
+ shapesByTargetClass: Record<string, string[]>;
89
+ annotationProperties: AnnotationProperty[];
90
+ stats: {
91
+ classCount: number;
92
+ objectPropertyCount: number;
93
+ datatypePropertyCount: number;
94
+ shapeCount: number;
95
+ annotationPropertyCount: number;
96
+ };
39
97
  }
40
98
 
41
99
  const data = schemaData as unknown as OntologySchema;
@@ -48,6 +106,10 @@ export function getProperty(id: string): OwlProperty | null {
48
106
  return data.properties[id] ?? null;
49
107
  }
50
108
 
109
+ export function getShape(id: string): OwlShape | null {
110
+ return data.shapes[id] ?? null;
111
+ }
112
+
51
113
  export function getPropertiesForDomain(domain: string): { object: OwlProperty[]; datatype: OwlProperty[] } {
52
114
  const group = data.propertiesByDomain[domain];
53
115
  if (!group) return { object: [], datatype: [] };
@@ -57,7 +119,6 @@ export function getPropertiesForDomain(domain: string): { object: OwlProperty[];
57
119
  };
58
120
  }
59
121
 
60
- /** Get all properties applicable to a class, including inherited ones. */
61
122
  export function getAllPropertiesForClass(classId: string): { object: OwlProperty[]; datatype: OwlProperty[] } {
62
123
  const cls = data.classes[classId];
63
124
  if (!cls) return { object: [], datatype: [] };
@@ -80,7 +141,11 @@ export function getAllPropertiesForClass(classId: string): { object: OwlProperty
80
141
  return { object: objectProps, datatype: datatypeProps };
81
142
  }
82
143
 
83
- /** Get the full class hierarchy tree starting from roots. */
144
+ export function getShapesForClass(classId: string): OwlShape[] {
145
+ const shapeIds = data.shapesByTargetClass[classId] ?? [];
146
+ return shapeIds.map(id => data.shapes[id]).filter(Boolean);
147
+ }
148
+
84
149
  export function getClassTree(): OwlClass[] {
85
150
  return data.classHierarchyRoots
86
151
  .map(id => data.classes[id])
@@ -95,6 +160,26 @@ export function getAllProperties(): OwlProperty[] {
95
160
  return Object.values(data.properties);
96
161
  }
97
162
 
163
+ export function getObjectProperties(): OwlProperty[] {
164
+ return Object.values(data.properties).filter(p => p.type === 'object');
165
+ }
166
+
167
+ export function getDatatypeProperties(): OwlProperty[] {
168
+ return Object.values(data.properties).filter(p => p.type === 'datatype');
169
+ }
170
+
171
+ export function getAllShapes(): OwlShape[] {
172
+ return Object.values(data.shapes);
173
+ }
174
+
175
+ export function getAnnotationProperties(): AnnotationProperty[] {
176
+ return data.annotationProperties;
177
+ }
178
+
179
+ export function getOntology(): OwlOntology | null {
180
+ return data.ontology;
181
+ }
182
+
98
183
  export function getStats() {
99
184
  return data.stats;
100
185
  }
@@ -82,6 +82,7 @@ export interface ConceptEntry {
82
82
  id: string;
83
83
  designations: Record<string, string>;
84
84
  groups: string[];
85
+ tags: string[];
85
86
  status: string;
86
87
  }
87
88