@glossarist/concept-browser 0.7.118 → 0.7.120

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.118",
3
+ "version": "0.7.120",
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": {
@@ -42,7 +42,7 @@
42
42
  "astro": "^7.0.7",
43
43
  "d3": "^7.9.0",
44
44
  "favicons": "^7.2.0",
45
- "glossarist": "^0.4.52",
45
+ "glossarist": "^0.4.54",
46
46
  "js-yaml": "^4.1.0",
47
47
  "jszip": "^3.10.1",
48
48
  "n3": "^1.26.0",
@@ -157,6 +157,16 @@ function extractRelated(concept, registerId, uriBase, urnMap) {
157
157
  const edges = [];
158
158
  const sourceUri = concept["@id"];
159
159
  for (const r of concept["gl:related"] || []) {
160
+ if (r["gl:target"]) {
161
+ edges.push({
162
+ source: sourceUri,
163
+ target: r["gl:target"],
164
+ type: r["gl:relationshipType"] || "references",
165
+ label: r["gl:term"] || void 0,
166
+ register: r["gl:target"].match(/\/([^/]+)\/concept\//)?.[1] || registerId
167
+ });
168
+ continue;
169
+ }
160
170
  const ref = r["gl:ref"];
161
171
  if (!ref) continue;
162
172
  const source = ref["gl:source"] || ref["source"];
@@ -382,6 +392,9 @@ function main() {
382
392
  const base = alias.endsWith("*") ? alias.slice(0, -1) : alias;
383
393
  if (base) urnMap.set(base, ds);
384
394
  }
395
+ if (manifest.ref) {
396
+ urnMap.set(manifest.ref, ds);
397
+ }
385
398
  } catch {
386
399
  }
387
400
  }
@@ -173,6 +173,19 @@ function extractRelated(concept, registerId, uriBase, urnMap) {
173
173
  const edges = [];
174
174
  const sourceUri = concept['@id'];
175
175
  for (const r of concept['gl:related'] || []) {
176
+ // Prefer gl:target — already resolved at generate-data build time
177
+ // via refPrefixMap (maps dataset ref labels to dataset IDs).
178
+ if (r['gl:target']) {
179
+ edges.push({
180
+ source: sourceUri,
181
+ target: r['gl:target'],
182
+ type: r['gl:relationshipType'] || 'references',
183
+ label: r['gl:term'] || undefined,
184
+ register: r['gl:target'].match(/\/([^/]+)\/concept\//)?.[1] || registerId,
185
+ });
186
+ continue;
187
+ }
188
+ // Fallback: resolve from gl:ref using urnMap
176
189
  const ref = r['gl:ref'];
177
190
  if (!ref) continue;
178
191
  const source = ref['gl:source'] || ref['source'];
@@ -458,6 +471,12 @@ for (const ds of datasets) {
458
471
  const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
459
472
  if (base) urnMap.set(base, ds);
460
473
  }
474
+ // Map the dataset's ref label (e.g. "CIE S 017:2020") to its dataset ID.
475
+ // This enables cross-dataset resolution for gl:ref entries that use
476
+ // source labels instead of URIs.
477
+ if (manifest.ref) {
478
+ urnMap.set(manifest.ref, ds);
479
+ }
461
480
  } catch {}
462
481
  }
463
482
  console.log(`URI resolution map: ${[...urnMap.entries()].map(([k,v]) => `${k}→${v}`).join(', ')}\n`);
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { conceptFromJson } from '../../adapters/model-bridge';
3
+
4
+ function makeJsonLdWithRelated(target: string | null) {
5
+ const related = target
6
+ ? {
7
+ 'gl:relationshipType': 'superseded_by',
8
+ 'gl:target': target,
9
+ 'gl:ref': { 'gl:source': 'CIE S 017:2020', 'gl:id': '17-21-097' },
10
+ }
11
+ : {
12
+ 'gl:relationshipType': 'superseded_by',
13
+ 'gl:ref': { 'gl:source': 'CIE S 017:2020', 'gl:id': '17-21-097' },
14
+ };
15
+ return {
16
+ '@type': 'skos:Concept',
17
+ '@id': 'https://example.com/cie-2011/concept/17-50',
18
+ 'gl:identifier': '17-50',
19
+ 'gl:localizedConcept': {
20
+ eng: {
21
+ 'gl:languageCode': 'eng',
22
+ 'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'test term', 'gl:normativeStatus': 'preferred' }],
23
+ 'gl:definition': [{ 'gl:content': 'test definition' }],
24
+ },
25
+ },
26
+ 'gl:related': [related],
27
+ };
28
+ }
29
+
30
+ describe('cross-dataset related concept resolution — gl:target flows through model-bridge natively', () => {
31
+ it('concept.relatedConcepts entries with gl:target have native target field', () => {
32
+ const json = makeJsonLdWithRelated('https://example.com/cie-2020/concept/17-21-097');
33
+ const concept = conceptFromJson(json as any);
34
+ expect(concept.relatedConcepts).toHaveLength(1);
35
+ expect(concept.relatedConcepts[0].type).toBe('superseded_by');
36
+ expect(concept.relatedConcepts[0].target).toBe('https://example.com/cie-2020/concept/17-21-097');
37
+ });
38
+
39
+ it('concept.relatedConcepts entries without gl:target have null target', () => {
40
+ const json = makeJsonLdWithRelated(null);
41
+ const concept = conceptFromJson(json as any);
42
+ expect(concept.relatedConcepts).toHaveLength(1);
43
+ expect(concept.relatedConcepts[0].target).toBeNull();
44
+ });
45
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { RelatedConcept } from 'glossarist';
3
+ import { resolveRefTarget } from '../../adapters/GraphDataSource';
4
+
5
+ describe('GraphDataSource.resolveRefTarget — prefers native target field', () => {
6
+ it('returns rc.target when set (cross-dataset resolved URI)', () => {
7
+ const rc = new RelatedConcept({
8
+ type: 'superseded_by',
9
+ target: 'https://example.com/cie-2020/concept/17-21-097',
10
+ ref: { source: 'CIE S 017:2020', id: '17-21-097' },
11
+ });
12
+ const target = resolveRefTarget(rc, 'https://example.com/cie-eilv', 'cie-2011', undefined);
13
+ expect(target).toBe('https://example.com/cie-2020/concept/17-21-097');
14
+ });
15
+
16
+ it('falls back to ref resolution when target is null', () => {
17
+ const rc = new RelatedConcept({
18
+ type: 'supersedes',
19
+ ref: { source: 'cie-2020', id: '42' },
20
+ });
21
+ const urnMap = new Map([['cie-2020', 'cie-2020']]);
22
+ const target = resolveRefTarget(rc, 'https://example.com', 'cie-2011', urnMap);
23
+ expect(target).toBe('https://example.com/cie-2020/concept/42');
24
+ });
25
+
26
+ it('returns empty string when neither target nor ref is set', () => {
27
+ const rc = new RelatedConcept({ type: 'see' });
28
+ const target = resolveRefTarget(rc, 'https://example.com', 'cie-2011', undefined);
29
+ expect(target).toBe('');
30
+ });
31
+ });
@@ -16,7 +16,13 @@ interface DomainNodeJson {
16
16
  children?: DomainNodeJson[];
17
17
  }
18
18
 
19
- function resolveRefTarget(rc: RelatedConcept, uriBase: string, registerId: string, urnMap?: ReadonlyMap<string, string>): string {
19
+ export function resolveRefTarget(rc: RelatedConcept, uriBase: string, registerId: string, urnMap?: ReadonlyMap<string, string>): string {
20
+ // Prefer the native target field — already resolved at build time
21
+ // via refPrefixMap in generate-data.ts. This is the author-provided
22
+ // canonical URL for cross-dataset relations.
23
+ if (rc.target) return rc.target;
24
+
25
+ // Fallback: resolve from ref (for data generated before gl:target existed)
20
26
  if (!rc.ref) return '';
21
27
  const ref = rc.ref;
22
28
  if (ref.id) {
@@ -170,7 +170,8 @@ function attachBridges(concept: Concept, localizations: Record<string, unknown>)
170
170
  /**
171
171
  * Attach bridged fields (ref text, sourceId, citation) to RelatedConcept instances
172
172
  * from the raw deserialized data. Called after Concept.fromJSON creates the model
173
- * instances, since RelatedConcept.fromJSON only reads type/content/ref.
173
+ * instances, since RelatedConcept.fromJSON does not read sourceId/citation/text.
174
+ * (It does read type/content/ref/target natively.)
174
175
  */
175
176
  function attachRelatedBridges(
176
177
  modelRelated: ReadonlyArray<{ type?: string | null; content?: Record<string, string> | string | null; ref?: any; related?: ReadonlyArray<any> }>,
@@ -367,6 +368,13 @@ function mapRelatedFromJsonLd(r: JsonLdRelated): Record<string, unknown> {
367
368
  result.type = r[GL.RELATIONSHIP_TYPE];
368
369
  }
369
370
 
371
+ // gl:target is the resolved concept URI for cross-dataset relations
372
+ // (e.g., superseded_by → cie-2020 concept). Without this, the UI
373
+ // renders related concepts as plain text with no clickable link.
374
+ if (r[GL.TARGET]) {
375
+ result.target = r[GL.TARGET];
376
+ }
377
+
370
378
  if (r[GL.REF]) {
371
379
  const ref = mapRefFromJsonLd(r[GL.REF]);
372
380
  if (ref) result.ref = ref;