@glossarist/concept-browser 0.7.126 → 0.7.128

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.126",
3
+ "version": "0.7.128",
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": {
@@ -1179,9 +1179,10 @@ function extractInlineRefs(localizedData, refMaps2, conceptSources = []) {
1179
1179
  if (seg.kind === "concept" || seg.kind === "cite") {
1180
1180
  const target = seg.target;
1181
1181
  if (target.type === "dataset_qualified" && target.dataset && target.id) {
1182
- const dsId = refMaps2.refPrefixMap[target.dataset] || target.dataset;
1183
- const ref = { id: buildConceptUri(refMaps2.uriBase, dsId, target.id), term: seg.label ?? `${target.dataset}:${target.id}` };
1184
- refs.push(ref);
1182
+ const dsId = refMaps2.refPrefixMap[target.dataset];
1183
+ if (dsId) {
1184
+ refs.push({ id: buildConceptUri(refMaps2.uriBase, dsId, target.id), term: seg.label ?? `${target.dataset}:${target.id}` });
1185
+ }
1185
1186
  } else if (target.type === "urn" && target.urn) {
1186
1187
  const pattern = refMaps2.patternIndex.resolve(target.urn);
1187
1188
  if (pattern) {
@@ -409,9 +409,15 @@ function extractInlineRefs(localizedData, refMaps, conceptSources = []) {
409
409
  if (seg.kind === 'concept' || seg.kind === 'cite') {
410
410
  const target = seg.target;
411
411
  if (target.type === 'dataset_qualified' && target.dataset && target.id) {
412
- const dsId = refMaps.refPrefixMap[target.dataset] || target.dataset;
413
- const ref = { id: buildConceptUri(refMaps.uriBase, dsId, target.id), term: seg.label ?? `${target.dataset}:${target.id}` };
414
- refs.push(ref);
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.
417
+ const dsId = refMaps.refPrefixMap[target.dataset];
418
+ if (dsId) {
419
+ refs.push({ id: buildConceptUri(refMaps.uriBase, dsId, target.id), term: seg.label ?? `${target.dataset}:${target.id}` });
420
+ }
415
421
  } else if (target.type === 'urn' && target.urn) {
416
422
  const pattern = refMaps.patternIndex.resolve(target.urn);
417
423
  if (pattern) {
@@ -71,8 +71,22 @@ function checkFile(file, dataDir) {
71
71
  }
72
72
  }
73
73
  const sourceIds = /* @__PURE__ */ new Set();
74
- for (const s of json["gl:sources"] ?? []) {
74
+ const sourceRefKeys = /* @__PURE__ */ new Set();
75
+ const allSourceArrays = [
76
+ ...json["gl:sources"] ?? []
77
+ ];
78
+ for (const lc of Object.values(json["gl:localizedConcept"] ?? {})) {
79
+ const lcSources = lc["gl:source"] ?? lc["gl:sources"];
80
+ if (Array.isArray(lcSources)) allSourceArrays.push(...lcSources);
81
+ }
82
+ for (const s of allSourceArrays) {
75
83
  if (s["gl:id"]) sourceIds.add(s["gl:id"]);
84
+ const origin = s["gl:origin"] ?? {};
85
+ const ref = origin["gl:ref"] ?? {};
86
+ if (ref["gl:source"] && ref["gl:id"]) {
87
+ sourceRefKeys.add(`${ref["gl:source"]}:${ref["gl:id"]}`);
88
+ sourceIds.add(ref["gl:id"]);
89
+ }
76
90
  }
77
91
  const textChunks = [];
78
92
  for (const lc of Object.values(json["gl:localizedConcept"] ?? {})) {
@@ -149,9 +163,11 @@ function checkFile(file, dataDir) {
149
163
  if (citeMatch) {
150
164
  const rest = citeMatch[1];
151
165
  const commaIdx = rest.indexOf(",");
152
- const id = commaIdx > 0 ? rest.slice(0, commaIdx).trim() : rest.trim();
153
- if (!sourceIds.has(id)) {
154
- broken.push({ file: relFile, line: 0, field: "{{cite:...}}", target: id, reason: `source "${id}" not found in this concept's sources[]` });
166
+ const rawId = commaIdx > 0 ? rest.slice(0, commaIdx).trim() : rest.trim();
167
+ const lastColon = rawId.lastIndexOf(":");
168
+ const idPart = lastColon > 0 ? rawId.slice(lastColon + 1) : rawId;
169
+ if (!sourceIds.has(rawId) && !sourceIds.has(idPart) && !sourceRefKeys.has(rawId)) {
170
+ broken.push({ file: relFile, line: 0, field: "{{cite:...}}", target: rawId, reason: `source "${rawId}" not found in this concept's sources[]` });
155
171
  }
156
172
  continue;
157
173
  }
@@ -112,8 +112,23 @@ function checkFile(file: string, dataDir: string): BrokenLink[] {
112
112
  }
113
113
 
114
114
  const sourceIds = new Set<string>();
115
- for (const s of json['gl:sources'] ?? []) {
115
+ const sourceRefKeys = new Set<string>();
116
+ // Collect from concept-level sources AND localization-level sources
117
+ const allSourceArrays = [
118
+ ...(json['gl:sources'] ?? []),
119
+ ];
120
+ for (const lc of Object.values(json['gl:localizedConcept'] ?? {})) {
121
+ const lcSources = (lc as any)['gl:source'] ?? (lc as any)['gl:sources'];
122
+ if (Array.isArray(lcSources)) allSourceArrays.push(...lcSources);
123
+ }
124
+ for (const s of allSourceArrays) {
116
125
  if (s['gl:id']) sourceIds.add(s['gl:id']);
126
+ const origin = s['gl:origin'] ?? {};
127
+ const ref = origin['gl:ref'] ?? {};
128
+ if (ref['gl:source'] && ref['gl:id']) {
129
+ sourceRefKeys.add(`${ref['gl:source']}:${ref['gl:id']}`);
130
+ sourceIds.add(ref['gl:id']);
131
+ }
117
132
  }
118
133
 
119
134
  const textChunks: string[] = [];
@@ -190,14 +205,20 @@ function checkFile(file: string, dataDir: string): BrokenLink[] {
190
205
  continue;
191
206
  }
192
207
 
193
- // cite:id — source must exist in this concept's sources[]
208
+ // cite:id — source must exist in this concept's sources[].
209
+ // Handles DATASET:ID format: {{cite:IEV:702-02-07}} matches a source
210
+ // with id "702-02-07" or origin.ref { source: "IEV", id: "702-02-07" }.
194
211
  const citeMatch = body.match(/^cite:(.+)$/i);
195
212
  if (citeMatch) {
196
213
  const rest = citeMatch[1];
197
214
  const commaIdx = rest.indexOf(',');
198
- const id = (commaIdx > 0 ? rest.slice(0, commaIdx).trim() : rest.trim());
199
- if (!sourceIds.has(id)) {
200
- broken.push({ file: relFile, line: 0, field: '{{cite:...}}', target: id, reason: `source "${id}" not found in this concept's sources[]` });
215
+ const rawId = (commaIdx > 0 ? rest.slice(0, commaIdx).trim() : rest.trim());
216
+
217
+ // Check: rawId (e.g. "IEV:702-02-07"), or just the ID part ("702-02-07")
218
+ const lastColon = rawId.lastIndexOf(':');
219
+ const idPart = lastColon > 0 ? rawId.slice(lastColon + 1) : rawId;
220
+ if (!sourceIds.has(rawId) && !sourceIds.has(idPart) && !sourceRefKeys.has(rawId)) {
221
+ broken.push({ file: relFile, line: 0, field: '{{cite:...}}', target: rawId, reason: `source "${rawId}" not found in this concept's sources[]` });
201
222
  }
202
223
  continue;
203
224
  }
@@ -0,0 +1,80 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { conceptFromJson } from '../../adapters/model-bridge';
3
+
4
+ /**
5
+ * Regression test: sources stored at the localization level
6
+ * (gl:localizedConcept.eng.gl:source) must be accessible via
7
+ * concept.localization(lang).sources so the citeResolver can find them.
8
+ *
9
+ * Without this, {{cite:sourceId}} mentions render as plain text
10
+ * instead of links — the exact bug reported on cie-eilv concept 17-21-001.
11
+ */
12
+
13
+ function makeConceptWithLocalizationSources() {
14
+ return {
15
+ '@type': 'skos:Concept',
16
+ '@id': 'https://example.com/dataset/cie-2020/concept/17-21-001',
17
+ 'gl:identifier': '17-21-001',
18
+ 'gl:localizedConcept': {
19
+ eng: {
20
+ 'gl:languageCode': 'eng',
21
+ 'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'quantity', 'gl:normativeStatus': 'preferred' }],
22
+ 'gl:definition': [{ 'gl:content': 'property of a phenomenon' }],
23
+ 'gl:source': [
24
+ {
25
+ '@type': 'gl:ConceptSource',
26
+ 'gl:id': '845-01-01',
27
+ 'gl:sourceType': 'authoritative',
28
+ 'gl:origin': {
29
+ '@type': 'gl:Citation',
30
+ 'gl:ref': { '@type': 'gl:Ref', 'gl:source': 'IEV', 'gl:id': '845-01-01' },
31
+ 'gl:link': 'https://www.electropedia.org/iev/iev.nsf/display?openform&ievref=845-01-01',
32
+ },
33
+ },
34
+ {
35
+ '@type': 'gl:ConceptSource',
36
+ 'gl:sourceType': 'authoritative',
37
+ 'gl:origin': {
38
+ '@type': 'gl:Citation',
39
+ 'gl:ref': { '@type': 'gl:Ref', 'gl:source': 'CIE S 017:2011', 'gl:id': '17-370' },
40
+ },
41
+ },
42
+ ],
43
+ },
44
+ },
45
+ };
46
+ }
47
+
48
+ describe('localization-level sources accessible for citeResolver', () => {
49
+ it('concept.localization("eng").sources has the localization sources', () => {
50
+ const concept = conceptFromJson(makeConceptWithLocalizationSources() as any);
51
+ const lc = concept.localization('eng');
52
+ expect(lc).toBeTruthy();
53
+ expect(lc!.sources).toBeDefined();
54
+ expect(lc!.sources!.length).toBeGreaterThanOrEqual(1);
55
+ });
56
+
57
+ it('source with gl:id is findable by id', () => {
58
+ const concept = conceptFromJson(makeConceptWithLocalizationSources() as any);
59
+ const lc = concept.localization('eng');
60
+ const source = lc!.sources!.find(s => s.id === '845-01-01');
61
+ expect(source).toBeDefined();
62
+ });
63
+
64
+ it('source origin link is preserved', () => {
65
+ const concept = conceptFromJson(makeConceptWithLocalizationSources() as any);
66
+ const lc = concept.localization('eng');
67
+ const source = lc!.sources!.find(s => s.id === '845-01-01');
68
+ expect(source).toBeDefined();
69
+ expect(source!.origin?.link).toBeTruthy();
70
+ });
71
+
72
+ it('merging concept + localization sources gives complete set', () => {
73
+ const concept = conceptFromJson(makeConceptWithLocalizationSources() as any);
74
+ const conceptSources = concept.sources ?? [];
75
+ const lcSources = concept.localization('eng')?.sources ?? [];
76
+ const merged = [...conceptSources, ...lcSources];
77
+ expect(merged.length).toBeGreaterThanOrEqual(2);
78
+ expect(merged.find(s => s.id === '845-01-01')).toBeDefined();
79
+ });
80
+ });
@@ -119,7 +119,17 @@ const conceptTags = computed(() => props.concept?.tags ?? []);
119
119
  const factory = getFactory();
120
120
  const { ready: bibReady, ensureBibLoaded, bibResolver, citeResolver, nonVerbalRefResolver } = useRenderOptions(
121
121
  () => props.registerId,
122
- () => props.concept?.sources,
122
+ () => {
123
+ // Collect sources from ALL localizations (shown separately per language,
124
+ // but the citeResolver needs access to all of them since content from
125
+ // every language is rendered on the same page).
126
+ const sources: any[] = [];
127
+ for (const lang of props.concept?.languages ?? []) {
128
+ const lc = props.concept?.localization(lang);
129
+ if (lc?.sources) sources.push(...lc.sources);
130
+ }
131
+ return sources;
132
+ },
123
133
  );
124
134
 
125
135
  const renderOpts = computed<RenderOptions>(() => {