@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 +1 -1
- package/scripts/generate-data.js +4 -3
- package/scripts/generate-data.ts +9 -3
- package/scripts/validate-links.js +20 -4
- package/scripts/validate-links.ts +26 -5
- package/src/__tests__/components/localization-sources.test.ts +80 -0
- package/src/components/ConceptDetail.vue +11 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
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": {
|
package/scripts/generate-data.js
CHANGED
|
@@ -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]
|
|
1183
|
-
|
|
1184
|
-
|
|
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) {
|
package/scripts/generate-data.ts
CHANGED
|
@@ -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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
() =>
|
|
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>(() => {
|