@glossarist/concept-browser 0.7.128 → 0.7.130

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.128",
3
+ "version": "0.7.130",
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": {
@@ -43,7 +43,7 @@
43
43
  "astro": "^7.0.7",
44
44
  "d3": "^7.9.0",
45
45
  "favicons": "^7.2.0",
46
- "glossarist": "^0.4.55",
46
+ "glossarist": "^0.4.56",
47
47
  "js-yaml": "^4.1.0",
48
48
  "jszip": "^3.10.1",
49
49
  "n3": "^1.26.0",
@@ -1163,6 +1163,7 @@ function extractInlineRefs(localizedData, refMaps2, conceptSources = []) {
1163
1163
  }
1164
1164
  for (const m of fullText.matchAll(/\{\{([^{}]+?)\}\}/g)) {
1165
1165
  const body = m[1];
1166
+ if (/^(bib|link|image):/i.test(body)) continue;
1166
1167
  let segments;
1167
1168
  try {
1168
1169
  segments = parseMentions(`{{${body}}}`);
@@ -1188,6 +1189,8 @@ function extractInlineRefs(localizedData, refMaps2, conceptSources = []) {
1188
1189
  if (pattern) {
1189
1190
  refs.push({ id: buildConceptUri(refMaps2.uriBase, pattern.datasetId, pattern.conceptId), term: seg.label ?? target.urn });
1190
1191
  }
1192
+ } else if (target.type === "entity_id" && target.id) {
1193
+ refs.push({ id: buildConceptUri(refMaps2.uriBase, refMaps2.register, target.id), term: seg.label ?? target.id });
1191
1194
  }
1192
1195
  }
1193
1196
  }
@@ -375,54 +375,46 @@ function extractInlineRefs(localizedData, refMaps, conceptSources = []) {
375
375
  if (pattern) { refs.push(pattern); }
376
376
  }
377
377
 
378
- // Double-brace mentions: use the STRICT parser from glossarist@0.4.55.
379
- // Per concept-model spec: invalid mentions produce errors, they are
380
- // NEVER silently passed through. The build FAILS on any invalid syntax.
378
+ // Double-brace mentions: use the strict parser from glossarist@0.4.56.
379
+ // Bare numeric IDs and designations are implicit same-dataset concept refs.
381
380
  for (const m of fullText.matchAll(/\{\{([^{}]+?)\}\}/g)) {
382
381
  const body = m[1];
383
382
 
384
- // Strict parse — throws InvalidMentionError on invalid syntax.
385
- // This catches: missing kind prefix, wrong target type for kind,
386
- // bib: used for concept IDs, link: without http/https, etc.
383
+ // Skip deployment-specific kinds (not concept cross-references)
384
+ if (/^(bib|link|image):/i.test(body)) continue;
385
+
387
386
  let segments;
388
387
  try {
389
388
  segments = parseMentions(`{{${body}}}`);
390
389
  } catch (e) {
391
390
  if (e instanceof InvalidMentionError) {
392
- // Report the error but don't crash — collect for batch reporting.
393
391
  console.warn(` Invalid mention: {{${body}}} — ${e.reason}`);
394
392
  continue;
395
393
  }
396
394
  throw e;
397
395
  }
398
396
 
399
- // Extract mention segments (skip text segments).
400
397
  for (const seg of segments) {
401
398
  if (seg.kind === 'text') continue;
402
-
403
- // bib:/link:/image: are deployment-specific renderings — NOT concept
404
- // cross-references. They must NOT appear in gl:references.
405
399
  if (seg.kind === 'bib' || seg.kind === 'link' || seg.kind === 'image') continue;
406
400
 
407
- // For concept/cite/fig/table/formula — resolve to URIs.
408
- // fig/table/formula resolve to entity refs (handled separately).
409
401
  if (seg.kind === 'concept' || seg.kind === 'cite') {
410
402
  const target = seg.target;
411
403
  if (target.type === 'dataset_qualified' && target.dataset && target.id) {
412
- // Only create a concept URI if the dataset is CO-DEPLOYED.
413
- // If the dataset doesn't exist in this deployment's refPrefixMap,
414
- // skip — the runtime resolver will handle it (unresolved text or
415
- // external link via routing). Creating a URI for a non-existent
416
- // dataset produces a 404.
404
+ // Cross-dataset: only if co-deployed
417
405
  const dsId = refMaps.refPrefixMap[target.dataset];
418
406
  if (dsId) {
419
407
  refs.push({ id: buildConceptUri(refMaps.uriBase, dsId, target.id), term: seg.label ?? `${target.dataset}:${target.id}` });
420
408
  }
421
409
  } else if (target.type === 'urn' && target.urn) {
410
+ // URN: resolve via pattern index
422
411
  const pattern = refMaps.patternIndex.resolve(target.urn);
423
412
  if (pattern) {
424
413
  refs.push({ id: buildConceptUri(refMaps.uriBase, pattern.datasetId, pattern.conceptId), term: seg.label ?? target.urn });
425
414
  }
415
+ } else if (target.type === 'entity_id' && target.id) {
416
+ // Same-dataset concept reference (bare numeric ID or designation)
417
+ refs.push({ id: buildConceptUri(refMaps.uriBase, refMaps.register, target.id), term: seg.label ?? target.id });
426
418
  }
427
419
  }
428
420
  }
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ /**
4
+ * Tests for citeResolver DATASET:ID matching logic.
5
+ *
6
+ * The citeResolver must parse {{cite:IEV:702-02-07}} and match it
7
+ * against sources that have:
8
+ * - id: "702-02-07" (the ID part)
9
+ * - origin.ref: { source: "IEV", id: "702-02-07" }
10
+ *
11
+ * Previously, it only matched s.id === key, so "IEV:702-02-07"
12
+ * never matched source.id "702-02-07".
13
+ */
14
+
15
+ interface SourceLike {
16
+ id: string | null;
17
+ origin?: {
18
+ ref?: { source?: string; id?: string } | null;
19
+ link?: string | null;
20
+ } | null;
21
+ }
22
+
23
+ function findSource(key: string, sources: SourceLike[]): SourceLike | undefined {
24
+ const lastColon = key.lastIndexOf(':');
25
+ const idPart = lastColon > 0 ? key.slice(lastColon + 1) : key;
26
+ const datasetPart = lastColon > 0 ? key.slice(0, lastColon) : '';
27
+
28
+ return sources.find(s => {
29
+ if (s.id === key || s.id === idPart) return true;
30
+ const ref = s.origin?.ref;
31
+ if (ref) {
32
+ if (datasetPart && ref.source === datasetPart && ref.id === idPart) return true;
33
+ if (ref.id === key || ref.id === idPart) return true;
34
+ }
35
+ return false;
36
+ });
37
+ }
38
+
39
+ describe('citeResolver DATASET:ID matching', () => {
40
+ const sources: SourceLike[] = [
41
+ { id: null, origin: { ref: { source: 'IEV', id: '845-01-01' }, link: 'https://electropedia.org/...' } },
42
+ { id: '845-01-01', origin: { ref: { source: 'IEV', id: '845-01-01' }, link: 'https://electropedia.org/...' } },
43
+ { id: '702-02-07', origin: { ref: { source: 'IEV', id: '702-02-07' }, link: 'https://electropedia.org/...' } },
44
+ { id: null, origin: { ref: { source: 'CIE S 017:2011', id: '17-370' } } },
45
+ ];
46
+
47
+ it('matches cite:IEV:845-01-01 via origin.ref', () => {
48
+ const s = findSource('IEV:845-01-01', sources);
49
+ expect(s).toBeDefined();
50
+ expect(s!.origin!.ref!.id).toBe('845-01-01');
51
+ });
52
+
53
+ it('matches cite:IEV:702-02-07 via source id', () => {
54
+ const s = findSource('IEV:702-02-07', sources);
55
+ expect(s).toBeDefined();
56
+ expect(s!.id).toBe('702-02-07');
57
+ });
58
+
59
+ it('matches cite:845-01-01 (bare ID without dataset prefix)', () => {
60
+ const s = findSource('845-01-01', sources);
61
+ expect(s).toBeDefined();
62
+ });
63
+
64
+ it('matches cite with CIE dataset prefix', () => {
65
+ const s = findSource('CIE S 017:2011:17-370', sources);
66
+ expect(s).toBeDefined();
67
+ expect(s!.origin!.ref!.id).toBe('17-370');
68
+ });
69
+
70
+ it('returns undefined for unknown source', () => {
71
+ const s = findSource('UNKNOWN:999', sources);
72
+ expect(s).toBeUndefined();
73
+ });
74
+ });
@@ -65,7 +65,24 @@ export function useRenderOptions(
65
65
  return `<span class="cite-ref">${escapeHtml(label ?? key)}</span>`;
66
66
  }
67
67
 
68
- const source = sources.find(s => s.id === key);
68
+ // Parse DATASET:ID format. {{cite:IEV:702-02-07}} → dataset="IEV", idPart="702-02-07"
69
+ // Match against: raw id, id part, or origin.ref { source: dataset, id: idPart }
70
+ const lastColon = key.lastIndexOf(':');
71
+ const idPart = lastColon > 0 ? key.slice(lastColon + 1) : key;
72
+ const datasetPart = lastColon > 0 ? key.slice(0, lastColon) : '';
73
+
74
+ const source = sources.find(s => {
75
+ // Match by source id (raw or id part)
76
+ if (s.id === key || s.id === idPart) return true;
77
+ // Match by origin.ref
78
+ const ref = s.origin?.ref;
79
+ if (ref) {
80
+ if (datasetPart && ref.source === datasetPart && ref.id === idPart) return true;
81
+ if (ref.id === key || ref.id === idPart) return true;
82
+ }
83
+ return false;
84
+ });
85
+
69
86
  if (!source?.origin) {
70
87
  return `<span class="cite-unresolved">${escapeHtml(label ?? key)}</span>`;
71
88
  }