@glossarist/concept-browser 0.7.35 → 0.7.41

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.
Files changed (38) hide show
  1. package/package.json +2 -2
  2. package/scripts/build-edges.js +16 -8
  3. package/scripts/generate-data.mjs +284 -86
  4. package/src/__tests__/citation-display.test.ts +165 -3
  5. package/src/__tests__/cite-ref.test.ts +112 -0
  6. package/src/__tests__/concept-detail-interaction.test.ts +1 -1
  7. package/src/__tests__/{math.test.ts → content-renderer.test.ts} +113 -29
  8. package/src/__tests__/escape.test.ts +76 -0
  9. package/src/__tests__/graph-data-source.test.ts +155 -0
  10. package/src/__tests__/model-bridge-bridges.test.ts +150 -0
  11. package/src/__tests__/model-bridge-citation.test.ts +163 -0
  12. package/src/__tests__/reference-resolver-cite.test.ts +122 -0
  13. package/src/__tests__/reference-resolver.test.ts +12 -7
  14. package/src/__tests__/resolve-view.test.ts +1 -1
  15. package/src/__tests__/sidebar-nav-highlighting.test.ts +178 -0
  16. package/src/__tests__/source-refs.test.ts +9 -6
  17. package/src/__tests__/test-helpers.ts +20 -0
  18. package/src/__tests__/uri-router.test.ts +39 -12
  19. package/src/adapters/DatasetAdapter.ts +12 -0
  20. package/src/adapters/GraphDataSource.ts +3 -3
  21. package/src/adapters/ReferenceResolver.ts +85 -55
  22. package/src/adapters/UriRouter.ts +82 -10
  23. package/src/adapters/factory.ts +34 -10
  24. package/src/adapters/model-bridge.ts +121 -71
  25. package/src/adapters/types.ts +3 -0
  26. package/src/components/AppSidebar.vue +7 -4
  27. package/src/components/CitationDisplay.vue +86 -30
  28. package/src/components/ConceptDetail.vue +56 -20
  29. package/src/components/LanguageDetail.vue +6 -6
  30. package/src/composables/use-concept-content.ts +8 -8
  31. package/src/composables/use-concept-edges.ts +2 -1
  32. package/src/composables/use-render-options.ts +1 -1
  33. package/src/graph/GraphEngine.ts +3 -3
  34. package/src/stores/vocabulary.ts +2 -2
  35. package/src/style.css +29 -0
  36. package/src/utils/content-renderer.ts +312 -0
  37. package/src/utils/markdown-lite.ts +2 -2
  38. package/src/utils/math.ts +0 -189
@@ -1,34 +1,106 @@
1
1
  import type { Manifest } from './types';
2
2
 
3
+ // ── URI pattern matching ────────────────────────────────────────────────────
4
+
5
+ function matchUriPattern(uri: string, pattern: string): boolean {
6
+ if (!pattern.endsWith('*')) return uri === pattern;
7
+ return uri.startsWith(pattern.slice(0, -1));
8
+ }
9
+
10
+ function extractConceptId(uri: string, pattern: string): string | null {
11
+ if (!pattern.endsWith('*')) return null;
12
+ const base = pattern.slice(0, -1);
13
+ if (!uri.startsWith(base)) return null;
14
+ const remainder = uri.slice(base.length);
15
+
16
+ if (uri.startsWith('https://') || uri.startsWith('http://')) {
17
+ const match = remainder.match(/^\/?concept\/([^/?#]+)/);
18
+ return match ? match[1] : null;
19
+ }
20
+ if (uri.startsWith('urn:')) {
21
+ return remainder || null;
22
+ }
23
+ return null;
24
+ }
25
+
26
+ // ── Static parse regex ──────────────────────────────────────────────────────
27
+
3
28
  const URI_REGISTER_RE = /\/([^/]+)\/concept\/([^/]+)$/;
4
29
 
30
+ // ── UriRouter ───────────────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Single source of truth for URI routing.
34
+ *
35
+ * Maps URIs to {registerId, conceptId} pairs using dataset-registered URI
36
+ * patterns. Supports wildcard patterns, URN prefix mapping, and URI construction.
37
+ *
38
+ * ReferenceResolver delegates URI matching here and adds its own concerns
39
+ * (routing table, source refs, citation classification) on top.
40
+ */
5
41
  export class UriRouter {
6
- private registerMap = new Map<string, { baseUrl: string; manifest: Manifest | null; uriBase: string }>();
42
+ /** registerId { baseUrl, uriBase, uriPatterns } */
43
+ private registerMap = new Map<string, { baseUrl: string; uriBase: string; uriPatterns: string[] }>();
7
44
 
8
- registerDataset(registerId: string, baseUrl: string, manifest?: Manifest) {
9
- this.registerMap.set(registerId, {
10
- baseUrl,
11
- manifest: manifest ?? null,
12
- uriBase: manifest?.uriBase ?? '',
13
- });
45
+ /** URN prefix → registerId (extracted from uriPatterns at registration time) */
46
+ private urnMap = new Map<string, string>();
47
+
48
+ /**
49
+ * Register a dataset's URI patterns for routing.
50
+ * URN-prefixed patterns are also indexed for fast URN → registerId lookup.
51
+ */
52
+ registerDataset(registerId: string, baseUrl: string, uriBase: string, uriPatterns: string[]): void {
53
+ this.registerMap.set(registerId, { baseUrl, uriBase, uriPatterns });
54
+
55
+ for (const pattern of uriPatterns) {
56
+ const base = pattern.endsWith('*') ? pattern.slice(0, -1) : pattern;
57
+ if (base.startsWith('urn:')) {
58
+ // Store without trailing colon so prefix matching works naturally
59
+ const clean = base.endsWith(':') ? base.slice(0, -1) : base;
60
+ this.urnMap.set(clean, registerId);
61
+ }
62
+ }
14
63
  }
15
64
 
65
+ /**
66
+ * Resolve a URI to {registerId, conceptId} using registered patterns.
67
+ * Returns null if no registered dataset matches.
68
+ */
16
69
  resolveUri(uri: string): { registerId: string; conceptId: string } | null {
17
70
  for (const [registerId, info] of this.registerMap) {
18
- const prefix = `${info.uriBase}/${registerId}/concept/`;
19
- if (uri.startsWith(prefix)) {
20
- return { registerId, conceptId: uri.slice(prefix.length) };
71
+ for (const pattern of info.uriPatterns) {
72
+ if (matchUriPattern(uri, pattern)) {
73
+ const conceptId = extractConceptId(uri, pattern);
74
+ if (conceptId) return { registerId, conceptId };
75
+ }
21
76
  }
22
77
  }
23
78
  return null;
24
79
  }
25
80
 
81
+ /** Resolve a URN prefix to a registerId. Matches by longest prefix. Returns null if unknown. */
82
+ resolveUrn(urn: string): string | null {
83
+ // Try exact match first, then progressively shorter prefixes
84
+ for (let len = urn.length; len > 0; len--) {
85
+ const prefix = urn.slice(0, len);
86
+ const match = this.urnMap.get(prefix);
87
+ if (match) return match;
88
+ }
89
+ return null;
90
+ }
91
+
92
+ /** Get the uriBase for a register. Returns empty string if unknown. */
93
+ getUriBase(registerId: string): string {
94
+ return this.registerMap.get(registerId)?.uriBase ?? '';
95
+ }
96
+
26
97
  /** Extract registerId and conceptId from any glossarist URI (no registration needed). */
27
98
  static parseUri(uri: string): { registerId: string; conceptId: string } | null {
28
99
  const m = uri.match(URI_REGISTER_RE);
29
100
  return m ? { registerId: m[1], conceptId: m[2] } : null;
30
101
  }
31
102
 
103
+ /** Construct a canonical URI for a concept. */
32
104
  buildUri(registerId: string, conceptId: string): string {
33
105
  const info = this.registerMap.get(registerId);
34
106
  const uriBase = info?.uriBase ?? '';
@@ -2,15 +2,17 @@ import type { DatasetRegistry, Manifest, Resolution } from './types';
2
2
  import type { RoutingEntry as ConfigRoutingEntry } from '../config/types';
3
3
  import { DatasetAdapter } from './DatasetAdapter';
4
4
  import { ReferenceResolver } from './ReferenceResolver';
5
+ import { UriRouter } from './UriRouter';
5
6
 
6
7
  export class AdapterFactory {
7
8
  private adapters = new Map<string, DatasetAdapter>();
8
- private urnMap = new Map<string, string>();
9
- readonly resolver: ReferenceResolver;
10
9
  private crossRefIndex: Record<string, string[]> | null = null;
10
+ readonly uriRouter: UriRouter;
11
+ readonly resolver: ReferenceResolver;
11
12
 
12
13
  constructor() {
13
- this.resolver = new ReferenceResolver();
14
+ this.uriRouter = new UriRouter();
15
+ this.resolver = new ReferenceResolver(this.uriRouter);
14
16
  }
15
17
 
16
18
  async discoverDatasets(datasetsUrl: string): Promise<DatasetAdapter[]> {
@@ -79,16 +81,38 @@ export class AdapterFactory {
79
81
  ...(manifest.uriAliases ?? []),
80
82
  manifest.uriBase ? `${manifest.uriBase}/${registerId}/*` : undefined,
81
83
  ].filter(Boolean) as string[];
82
- this.resolver.registerDataset(registerId, uriPatterns);
83
84
 
84
- if (manifest.datasetUri) this.urnMap.set(manifest.datasetUri, registerId);
85
- for (const alias of manifest.uriAliases ?? []) {
86
- const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
87
- if (base.startsWith('urn:')) this.urnMap.set(base, registerId);
85
+ this.uriRouter.registerDataset(
86
+ registerId,
87
+ manifest.baseUrl,
88
+ manifest.uriBase,
89
+ uriPatterns,
90
+ );
91
+
92
+ // Propagate URN map to all adapters for ref-target resolution
93
+ const urnMap = new Map<string, string>();
94
+ for (const id of this.uriRouter.getRegisteredIds()) {
95
+ const uriBase = this.uriRouter.getUriBase(id);
96
+ if (!uriBase) continue;
97
+ // Reconstruct URN map from uriRouter registrations
98
+ for (const pattern of [manifest.datasetUri, ...(manifest.uriAliases ?? [])]) {
99
+ if (!pattern) continue;
100
+ const base = pattern.endsWith('*') ? pattern.slice(0, -1) : pattern;
101
+ if (base.startsWith('urn:')) urnMap.set(base, registerId);
102
+ }
103
+ }
104
+ // Include URNs from all previously registered datasets
105
+ for (const adapter of this.adapters.values()) {
106
+ const m = adapter.manifest;
107
+ if (!m) continue;
108
+ for (const pattern of [m.datasetUri, ...(m.uriAliases ?? [])]) {
109
+ if (!pattern) continue;
110
+ const base = pattern.endsWith('*') ? pattern.slice(0, -1) : pattern;
111
+ if (base.startsWith('urn:')) urnMap.set(base, adapter.registerId);
112
+ }
88
113
  }
89
-
90
114
  for (const adapter of this.adapters.values()) {
91
- adapter.setUrnMap(this.urnMap);
115
+ adapter.setUrnMap(urnMap);
92
116
  }
93
117
  }
94
118
 
@@ -73,18 +73,18 @@ interface JsonLdRef {
73
73
  'gl:id'?: string;
74
74
  'gl:version'?: string;
75
75
  'gl:text'?: string;
76
- 'source'?: string;
77
- 'id'?: string;
78
- 'version'?: string;
76
+ source?: string;
77
+ id?: string;
78
+ version?: string;
79
79
  }
80
80
 
81
81
  interface JsonLdLocality {
82
82
  'gl:localityType'?: string;
83
83
  'gl:referenceFrom'?: string;
84
84
  'gl:referenceTo'?: string;
85
- 'type'?: string;
86
- 'reference_from'?: string;
87
- 'reference_to'?: string;
85
+ type?: string;
86
+ reference_from?: string;
87
+ reference_to?: string;
88
88
  }
89
89
 
90
90
  interface JsonLdOrigin {
@@ -97,6 +97,7 @@ interface JsonLdOrigin {
97
97
  }
98
98
 
99
99
  interface JsonLdSource {
100
+ 'gl:id'?: string;
100
101
  'gl:sourceType'?: string;
101
102
  'gl:sourceStatus'?: string;
102
103
  'gl:modification'?: string;
@@ -109,6 +110,8 @@ interface JsonLdRelated {
109
110
  '@id'?: string;
110
111
  'gl:term'?: string;
111
112
  'gl:target'?: string;
113
+ 'gl:sourceId'?: string;
114
+ 'gl:citation'?: JsonLdOrigin;
112
115
  }
113
116
 
114
117
  interface JsonLdDesignation {
@@ -192,10 +195,24 @@ export function getRefText(ref: ConceptRef): string | null {
192
195
  return refTexts.get(ref) ?? null;
193
196
  }
194
197
 
198
+ // RelatedConcept.sourceId: links a citation reference back to its source entry
199
+ const relatedSourceIds = new WeakMap<object, string>();
200
+
201
+ export function getRelatedSourceId(rc: object): string | null {
202
+ return relatedSourceIds.get(rc) ?? null;
203
+ }
204
+
205
+ // RelatedConcept.citation: embedded citation data for cite-ref references
206
+ const relatedCitations = new WeakMap<object, Record<string, unknown>>();
207
+
208
+ export function getRelatedCitation(rc: object): Record<string, unknown> | null {
209
+ return relatedCitations.get(rc) ?? null;
210
+ }
211
+
195
212
  // Relationship types whose target is a designation string, not a concept ref.
196
213
  const DESIGNATION_REL_TYPES = new Set(['abbreviated_form_for', 'short_form_for']);
197
214
 
198
- function attachAnnotations(concept: Concept, localizations: Record<string, unknown>): void {
215
+ function attachBridges(concept: Concept, localizations: Record<string, unknown>): void {
199
216
  for (const lang of concept.languages) {
200
217
  const lc = concept.localization(lang);
201
218
  const raw = localizations[lang];
@@ -210,7 +227,7 @@ function attachAnnotations(concept: Concept, localizations: Record<string, unkno
210
227
  ));
211
228
  }
212
229
 
213
- // Designation-level relationship targets and ref text
230
+ // Designation-level relationship targets, ref text, sourceId, citation
214
231
  const rawTerms = rawObj.terms;
215
232
  if (Array.isArray(rawTerms)) {
216
233
  for (const rawTerm of rawTerms) {
@@ -222,40 +239,56 @@ function attachAnnotations(concept: Concept, localizations: Record<string, unkno
222
239
  if (!designation) continue;
223
240
  const rawRelated = rawT.related;
224
241
  if (!Array.isArray(rawRelated)) continue;
225
- for (const rawRel of rawRelated) {
226
- if (!rawRel || typeof rawRel !== 'object') continue;
227
- const rel = rawRel as Record<string, unknown>;
228
- const relType = rel.type as string | undefined;
229
- const rc = designation.related.find(r => r.type === relType);
230
- if (!rc) continue;
231
- if (rel.target && typeof rel.target === 'string') {
232
- designationTargets.set(rc as object, rel.target);
233
- }
234
- if ('ref' in rc && rc.ref) {
235
- const rawRef = rel.ref as Record<string, unknown> | undefined;
236
- if (rawRef?.text && typeof rawRef.text === 'string') {
237
- refTexts.set(rc.ref, rawRef.text);
238
- }
239
- }
240
- }
242
+ attachRelatedBridges(designation.related, rawRelated);
241
243
  }
242
244
  }
243
245
 
244
- // Localization-level ref text
246
+ // Localization-level related concepts
245
247
  const rawRelated = rawObj.related;
246
248
  if (Array.isArray(rawRelated)) {
247
- for (const rawRel of rawRelated) {
248
- if (!rawRel || typeof rawRel !== 'object') continue;
249
- const rel = rawRel as Record<string, unknown>;
250
- const relType = rel.type as string | undefined;
251
- const rc = relType ? lc.related.find(r => r.type === relType) : undefined;
252
- if (!rc || !rc.ref) continue;
253
- const rawRef = rel.ref as Record<string, unknown> | undefined;
254
- if (rawRef?.text && typeof rawRef.text === 'string') {
255
- refTexts.set(rc.ref, rawRef.text);
256
- }
249
+ attachRelatedBridges(lc.related, rawRelated);
250
+ }
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Attach bridged fields (ref text, sourceId, citation) to RelatedConcept instances
256
+ * from the raw deserialized data. Called after Concept.fromJSON creates the model
257
+ * instances, since RelatedConcept.fromJSON only reads type/content/ref.
258
+ */
259
+ function attachRelatedBridges(
260
+ modelRelated: Array<{ type?: string | null; content?: string | null; ref?: any; related?: any[] }>,
261
+ rawRelated: unknown[],
262
+ ): void {
263
+ for (const rawRel of rawRelated) {
264
+ if (!rawRel || typeof rawRel !== 'object') continue;
265
+ const rel = rawRel as Record<string, unknown>;
266
+ const relType = rel.type as string | undefined;
267
+ const rc = relType ? modelRelated.find(r => r.type === relType) : undefined;
268
+ if (!rc) continue;
269
+
270
+ // Ref text
271
+ if (rc.ref) {
272
+ const rawRef = rel.ref as Record<string, unknown> | undefined;
273
+ if (rawRef?.text && typeof rawRef.text === 'string') {
274
+ refTexts.set(rc.ref, rawRef.text);
257
275
  }
258
276
  }
277
+
278
+ // Designation target
279
+ if (rel.target && typeof rel.target === 'string') {
280
+ designationTargets.set(rc, rel.target);
281
+ }
282
+
283
+ // Source ID — links citation reference back to the ConceptSource entry
284
+ if (rel.sourceId && typeof rel.sourceId === 'string') {
285
+ relatedSourceIds.set(rc, rel.sourceId);
286
+ }
287
+
288
+ // Citation — embedded origin data for cite-ref references
289
+ if (rel.citation && typeof rel.citation === 'object') {
290
+ relatedCitations.set(rc, rel.citation as Record<string, unknown>);
291
+ }
259
292
  }
260
293
  }
261
294
 
@@ -335,8 +368,37 @@ function mapDesignationFromJsonLd(d: JsonLdDesignation): Record<string, unknown>
335
368
  return result;
336
369
  }
337
370
 
371
+ function mapRefFromJsonLd(rawRef: JsonLdRef | string | undefined): Record<string, unknown> | null {
372
+ if (!rawRef) return null;
373
+ if (typeof rawRef === 'string') return { source: rawRef };
374
+ const refObj: Record<string, unknown> = {};
375
+ // gl:-prefixed keys take precedence over unprefixed keys
376
+ refObj.source = rawRef['gl:source'] ?? rawRef.source;
377
+ refObj.id = rawRef['gl:id'] ?? rawRef.id;
378
+ refObj.version = rawRef['gl:version'] ?? rawRef.version;
379
+ if (rawRef['gl:text']) refObj.text = rawRef['gl:text'];
380
+ return (refObj.source ?? refObj.id ?? refObj.version ?? refObj.text) != null
381
+ ? refObj : null;
382
+ }
383
+
384
+ /**
385
+ * Map JSON-LD locality to glossarist's snake_case format.
386
+ * Always uses snake_case (reference_from/reference_to) for consistency
387
+ * with the glossarist model.
388
+ */
389
+ function mapLocalityFromJsonLd(rawLoc: JsonLdLocality | undefined): Record<string, unknown> | null {
390
+ if (!rawLoc) return null;
391
+ const locObj: Record<string, unknown> = {};
392
+ locObj.type = rawLoc['gl:localityType'] ?? rawLoc.type;
393
+ locObj.reference_from = rawLoc['gl:referenceFrom'] ?? rawLoc.reference_from;
394
+ locObj.reference_to = rawLoc['gl:referenceTo'] ?? rawLoc.reference_to;
395
+ return (locObj.type ?? locObj.reference_from ?? locObj.reference_to) != null
396
+ ? locObj : null;
397
+ }
398
+
338
399
  function mapSourceFromJsonLd(s: JsonLdSource): Record<string, unknown> {
339
400
  const result: Record<string, unknown> = {};
401
+ if (s['gl:id']) result.id = s['gl:id'];
340
402
  if (s['gl:sourceType']) result.type = s['gl:sourceType'];
341
403
  if (s['gl:sourceStatus']) result.status = s['gl:sourceStatus'];
342
404
  if (s['gl:modification']) result.modification = s['gl:modification'];
@@ -344,32 +406,10 @@ function mapSourceFromJsonLd(s: JsonLdSource): Record<string, unknown> {
344
406
  if (s['gl:origin']) {
345
407
  const origin: Record<string, unknown> = {};
346
408
  const o = s['gl:origin'];
347
- if (o['gl:ref']) {
348
- const rawRef = o['gl:ref'];
349
- if (typeof rawRef === 'string') {
350
- origin.ref = { source: rawRef };
351
- } else {
352
- const refObj: Record<string, unknown> = {};
353
- if (rawRef['gl:source']) refObj.source = rawRef['gl:source'];
354
- if (rawRef['gl:id']) refObj.id = rawRef['gl:id'];
355
- if (rawRef['gl:version']) refObj.version = rawRef['gl:version'];
356
- if (rawRef['source']) refObj.source = rawRef['source'];
357
- if (rawRef['id']) refObj.id = rawRef['id'];
358
- if (rawRef['version']) refObj.version = rawRef['version'];
359
- if (Object.keys(refObj).length > 0) origin.ref = refObj;
360
- }
361
- }
362
- if (o['gl:locality']) {
363
- const loc: Record<string, unknown> = {};
364
- const rawLoc = o['gl:locality'];
365
- if (rawLoc['gl:localityType']) loc.type = rawLoc['gl:localityType'];
366
- if (rawLoc['gl:referenceFrom']) loc.reference_from = rawLoc['gl:referenceFrom'];
367
- if (rawLoc['gl:referenceTo']) loc.reference_to = rawLoc['gl:referenceTo'];
368
- if (rawLoc['type']) loc.type = rawLoc['type'];
369
- if (rawLoc['reference_from']) loc.reference_from = rawLoc['reference_from'];
370
- if (rawLoc['reference_to']) loc.reference_to = rawLoc['reference_to'];
371
- origin.locality = loc;
372
- }
409
+ const ref = mapRefFromJsonLd(o['gl:ref']);
410
+ if (ref) origin.ref = ref;
411
+ const loc = mapLocalityFromJsonLd(o['gl:locality']);
412
+ if (loc) origin.locality = loc;
373
413
  if (o['gl:link']) origin.link = o['gl:link'];
374
414
  if (o['gl:id']) origin.id = o['gl:id'];
375
415
  if (o['gl:version']) origin.version = o['gl:version'];
@@ -388,14 +428,8 @@ function mapRelatedFromJsonLd(r: JsonLdRelated): Record<string, unknown> {
388
428
  }
389
429
 
390
430
  if (r['gl:ref']) {
391
- const ref = r['gl:ref'];
392
- const refObj: Record<string, unknown> = {};
393
- if (ref['gl:source']) refObj.source = ref['gl:source'];
394
- if (ref['gl:id']) refObj.id = ref['gl:id'];
395
- if (ref['source']) refObj.source = ref['source'];
396
- if (ref['id']) refObj.id = ref['id'];
397
- if (ref['gl:text']) refObj.text = ref['gl:text'];
398
- if (Object.keys(refObj).length > 0) result.ref = refObj;
431
+ const ref = mapRefFromJsonLd(r['gl:ref']);
432
+ if (ref) result.ref = ref;
399
433
  }
400
434
 
401
435
  if (!result.ref && r['@id']) {
@@ -406,6 +440,22 @@ function mapRelatedFromJsonLd(r: JsonLdRelated): Record<string, unknown> {
406
440
  : { source: uri, id: null };
407
441
  }
408
442
  if (r['gl:term']) result.content = r['gl:term'];
443
+
444
+ // Bridged fields — stored in raw dict, extracted by attachBridges()
445
+ if (r['gl:sourceId']) result.sourceId = r['gl:sourceId'];
446
+ if (r['gl:citation']) {
447
+ const c = r['gl:citation'];
448
+ const citation: Record<string, unknown> = {};
449
+ if (c['gl:ref']) {
450
+ const cr = mapRefFromJsonLd(c['gl:ref']);
451
+ if (cr) citation.ref = cr;
452
+ }
453
+ const loc = mapLocalityFromJsonLd(c['gl:locality']);
454
+ if (loc) citation.locality = loc;
455
+ if (c['gl:link']) citation.link = c['gl:link'];
456
+ if (Object.keys(citation).length > 0) result.citation = citation;
457
+ }
458
+
409
459
  return result;
410
460
  }
411
461
 
@@ -493,7 +543,7 @@ function conceptFromJsonLd(doc: JsonLdConcept): Concept {
493
543
  status: null,
494
544
  });
495
545
 
496
- attachAnnotations(concept, localizations);
546
+ attachBridges(concept, localizations);
497
547
  return concept;
498
548
  }
499
549
 
@@ -505,7 +555,7 @@ export function conceptFromJson(doc: Record<string, unknown>): Concept {
505
555
  }
506
556
  const concept = Concept.fromJSON(doc);
507
557
  const locs = (doc as Record<string, unknown>).localizations as Record<string, unknown> | undefined;
508
- if (locs) attachAnnotations(concept, locs);
558
+ if (locs) attachBridges(concept, locs);
509
559
  return concept;
510
560
  }
511
561
 
@@ -32,6 +32,9 @@ export type {
32
32
  export { RELATIONSHIP_TYPES, DATE_TYPES } from 'glossarist';
33
33
  export { GRAMMAR_GENDERS, GRAMMAR_NUMBERS, GRAMMAR_PARTS_OF_SPEECH } from 'glossarist/models';
34
34
 
35
+ // Re-export citation classification from ReferenceResolver (single definition site)
36
+ export type { CitationClassification, CiteResolution } from './ReferenceResolver';
37
+
35
38
  // ── Dataset metadata ──────────────────────────────────────────────────────
36
39
 
37
40
  export interface ManifestSection {
@@ -114,8 +114,8 @@ const showDatasetNav = computed(() => !!currentManifest.value || !!siteConfig.va
114
114
  const provenance = computed(() => {
115
115
  const manifest = currentManifest.value;
116
116
  return {
117
- owner: manifest?.owner || (siteConfig.value as any)?.branding?.ownerName,
118
- ownerUrl: (siteConfig.value as any)?.branding?.ownerUrl,
117
+ owner: manifest?.owner || siteConfig.value?.branding?.ownerName,
118
+ ownerUrl: siteConfig.value?.branding?.ownerUrl,
119
119
  ref: manifest?.ref,
120
120
  status: manifest?.status,
121
121
  lastUpdated: manifest?.lastUpdated,
@@ -149,14 +149,17 @@ function isActive(page: { route: string; datasetScoped?: boolean }): boolean {
149
149
  const target = pageRoute(page);
150
150
  if (route.path === target) return true;
151
151
  if (page.datasetScoped) return route.name === page.route;
152
+ // Non-dataset-scoped page: only match if we're NOT inside a dataset route
153
+ const inDataset = 'registerId' in route.params;
154
+ if (inDataset) return false;
152
155
  return route.name === page.route || route.name === `${page.route}-global`;
153
156
  }
154
157
 
155
- function navTitle(page: { route: string }): string {
158
+ function navTitle(page: { route: string; title?: string }): string {
156
159
  const route = page.route || 'home';
157
160
  const key = `nav.${route}`;
158
161
  const translated = t(key);
159
- return translated === key ? (page as any).title : translated;
162
+ return translated === key ? (page.title ?? route) : translated;
160
163
  }
161
164
 
162
165
  const expandedSectionNodes = ref<Set<string>>(new Set());