@glossarist/concept-browser 0.3.7 → 0.4.0
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/README.md +3 -2
- package/cli/index.mjs +2 -1
- package/env.d.ts +5 -0
- package/package.json +4 -3
- package/scripts/build-edges.js +78 -10
- package/scripts/generate-data.mjs +152 -20
- package/scripts/generate-ontology-data.mjs +184 -0
- package/scripts/generate-ontology-schema.mjs +315 -0
- package/src/__tests__/concept-card.test.ts +1 -1
- package/src/__tests__/concept-detail-interaction.test.ts +40 -18
- package/src/__tests__/concept-formats.test.ts +32 -30
- package/src/__tests__/concept-timeline.test.ts +108 -83
- package/src/__tests__/concept-view.test.ts +15 -2
- package/src/__tests__/dataset-adapter.test.ts +172 -23
- package/src/__tests__/dataset-view.test.ts +6 -5
- package/src/__tests__/designation-registry.test.ts +161 -0
- package/src/__tests__/graph.test.ts +62 -0
- package/src/__tests__/language-detail.test.ts +117 -60
- package/src/__tests__/ontology-registry.test.ts +109 -0
- package/src/__tests__/relationship-categories.test.ts +62 -0
- package/src/__tests__/test-helpers.ts +11 -8
- package/src/adapters/DatasetAdapter.ts +171 -48
- package/src/adapters/model-bridge.ts +277 -0
- package/src/adapters/ontology-registry.ts +75 -0
- package/src/adapters/ontology-schema.ts +100 -0
- package/src/adapters/types.ts +52 -77
- package/src/components/AppSidebar.vue +1 -1
- package/src/components/CitationDisplay.vue +35 -0
- package/src/components/ConceptDetail.vue +334 -93
- package/src/components/ConceptRdfView.vue +397 -0
- package/src/components/ConceptTimeline.vue +56 -52
- package/src/components/GraphPanel.vue +96 -31
- package/src/components/LanguageDetail.vue +45 -37
- package/src/components/NavIcon.vue +1 -0
- package/src/components/NonVerbalRepDisplay.vue +38 -0
- package/src/components/RelationshipList.vue +99 -0
- package/src/config/use-site-config.ts +3 -0
- package/src/data/ontology-schema.json +1551 -0
- package/src/data/taxonomies.json +543 -0
- package/src/graph/GraphEngine.ts +7 -4
- package/src/router/index.ts +5 -0
- package/src/shims/empty.ts +1 -0
- package/src/shims/node-crypto.ts +6 -0
- package/src/shims/node-path.ts +10 -0
- package/src/stores/vocabulary.ts +75 -25
- package/src/style.css +74 -20
- package/src/utils/concept-formats.ts +22 -20
- package/src/utils/concept-helpers.ts +43 -23
- package/src/utils/designation-registry.ts +124 -0
- package/src/utils/relationship-categories.ts +84 -0
- package/src/views/OntologySchemaView.vue +302 -0
- package/src/views/PageView.vue +28 -17
- package/src/views/StatsView.vue +34 -12
- package/vite.config.ts +8 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Parse the glossarist OWL ontology (TTL) into a structured JSON schema
|
|
4
|
+
* for the Ontospy-style browser view.
|
|
5
|
+
*
|
|
6
|
+
* Reads: ../concept-model/ontologies/glossarist.ttl
|
|
7
|
+
* Writes: src/data/ontology-schema.json
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
11
|
+
import { resolve, dirname, join } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const ROOT = resolve(__dirname, '..');
|
|
16
|
+
const ONTOLOGY_TTL = resolve(ROOT, '..', 'concept-model', 'ontologies', 'glossarist.ttl');
|
|
17
|
+
const OUTPUT = resolve(ROOT, 'src', 'data', 'ontology-schema.json');
|
|
18
|
+
|
|
19
|
+
const KNOWN_PREFIXES = {
|
|
20
|
+
gloss: 'https://www.glossarist.org/ontologies/',
|
|
21
|
+
owl: 'http://www.w3.org/2002/07/owl#',
|
|
22
|
+
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
23
|
+
rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
|
|
24
|
+
skos: 'http://www.w3.org/2004/02/skos/core#',
|
|
25
|
+
xl: 'http://www.w3.org/2008/05/skos-xl#',
|
|
26
|
+
'iso-thes': 'http://purl.org/iso25964/skos-thes#',
|
|
27
|
+
dcterms: 'http://purl.org/dc/terms/',
|
|
28
|
+
prov: 'http://www.w3.org/ns/prov#',
|
|
29
|
+
xsd: 'http://www.w3.org/2001/XMLSchema#',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function expandPrefixed(term) {
|
|
33
|
+
for (const [prefix, uri] of Object.entries(KNOWN_PREFIXES)) {
|
|
34
|
+
if (term.startsWith(prefix + ':')) {
|
|
35
|
+
return uri + term.slice(prefix.length + 1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return term;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function compactIri(iri) {
|
|
42
|
+
for (const [prefix, uri] of Object.entries(KNOWN_PREFIXES)) {
|
|
43
|
+
if (iri.startsWith(uri)) {
|
|
44
|
+
return prefix + ':' + iri.slice(uri.length);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return iri;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Minimal TTL subject-block splitter. Handles nested [] and () and quoted strings.
|
|
52
|
+
*/
|
|
53
|
+
function splitSubjectBlocks(text) {
|
|
54
|
+
const blocks = [];
|
|
55
|
+
let depth = 0;
|
|
56
|
+
let start = -1;
|
|
57
|
+
let inTripleQuote = false;
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < text.length; i++) {
|
|
60
|
+
const ch = text[i];
|
|
61
|
+
|
|
62
|
+
if (inTripleQuote) {
|
|
63
|
+
if (ch === '"' && text.slice(i, i + 3) === '"""') {
|
|
64
|
+
inTripleQuote = false;
|
|
65
|
+
i += 2;
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (ch === '"' && text.slice(i, i + 3) === '"""') {
|
|
71
|
+
inTripleQuote = true;
|
|
72
|
+
i += 2;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (ch === '"') {
|
|
77
|
+
i++;
|
|
78
|
+
while (i < text.length && text[i] !== '"') {
|
|
79
|
+
if (text[i] === '\\') i++;
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (ch === '[' || ch === '(') depth++;
|
|
86
|
+
if (ch === ']' || ch === ')') depth--;
|
|
87
|
+
|
|
88
|
+
if (depth === 0 && ch === '.') {
|
|
89
|
+
if (start >= 0) {
|
|
90
|
+
blocks.push(text.slice(start, i));
|
|
91
|
+
start = -1;
|
|
92
|
+
}
|
|
93
|
+
} else if (start < 0 && /\S/.test(ch)) {
|
|
94
|
+
start = i;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return blocks;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function extractLiteral(block, predicate) {
|
|
102
|
+
const tripleQuoted = new RegExp(predicate + '\\s+"""([^]*?)"""@en');
|
|
103
|
+
let m = block.match(tripleQuoted);
|
|
104
|
+
if (m) return m[1].replace(/\s+/g, ' ').trim();
|
|
105
|
+
|
|
106
|
+
const singleQuoted = new RegExp(predicate + '\\s+"([^"]*?)"@en');
|
|
107
|
+
m = block.match(singleQuoted);
|
|
108
|
+
if (m) return m[1];
|
|
109
|
+
|
|
110
|
+
// Without @en
|
|
111
|
+
const plain = new RegExp(predicate + '\\s+"""([^]*?)"""');
|
|
112
|
+
m = block.match(plain);
|
|
113
|
+
if (m) return m[1].replace(/\s+/g, ' ').trim();
|
|
114
|
+
|
|
115
|
+
const plainSingle = new RegExp(predicate + '\\s+"([^"]*?)"');
|
|
116
|
+
m = block.match(plainSingle);
|
|
117
|
+
return m ? m[1] : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function extractResource(block, predicate) {
|
|
121
|
+
const re = new RegExp(predicate + '\\s+([^\\s,;]+)');
|
|
122
|
+
const m = block.match(re);
|
|
123
|
+
if (!m) return null;
|
|
124
|
+
let val = m[1].replace(/[;.]+$/, '');
|
|
125
|
+
if (val === 'a') return null;
|
|
126
|
+
return val;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractAllResources(block, predicate) {
|
|
130
|
+
const results = [];
|
|
131
|
+
const re = new RegExp(predicate + '\\s+', 'g');
|
|
132
|
+
let match;
|
|
133
|
+
while ((match = re.exec(block)) !== null) {
|
|
134
|
+
const rest = block.slice(match.index + match[0].length).trimStart();
|
|
135
|
+
// Read comma-separated resources until ; or .
|
|
136
|
+
const tokens = rest.split(/[\s;.\n]+/)[0];
|
|
137
|
+
if (tokens && tokens !== 'a') {
|
|
138
|
+
results.push(tokens.replace(/[;,]+$/, ''));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return [...new Set(results)];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseOntology(ttlText) {
|
|
145
|
+
const rawLines = ttlText.split('\n');
|
|
146
|
+
// Remove comment lines but keep content
|
|
147
|
+
const cleaned = rawLines.map(l => l.replace(/#[^\n]*/g, '')).join('\n');
|
|
148
|
+
|
|
149
|
+
const blocks = splitSubjectBlocks(cleaned);
|
|
150
|
+
|
|
151
|
+
const classes = [];
|
|
152
|
+
const properties = [];
|
|
153
|
+
|
|
154
|
+
for (const block of blocks) {
|
|
155
|
+
const trimmed = block.trim();
|
|
156
|
+
if (!trimmed) continue;
|
|
157
|
+
|
|
158
|
+
// Parse subject
|
|
159
|
+
const subjectMatch = trimmed.match(/^([^\s]+)/);
|
|
160
|
+
if (!subjectMatch) continue;
|
|
161
|
+
const subject = subjectMatch[1];
|
|
162
|
+
|
|
163
|
+
// Skip ontology declaration, prefix declarations
|
|
164
|
+
if (subject === '@prefix' || subject.startsWith('@')) continue;
|
|
165
|
+
if (subject.includes('glossarist>') && !subject.startsWith('gloss:')) continue;
|
|
166
|
+
|
|
167
|
+
// Determine type
|
|
168
|
+
const typeMatch = trimmed.match(/\ba\s+(.+?)(?:\s*[;.\n]|$)/);
|
|
169
|
+
if (!typeMatch) continue;
|
|
170
|
+
const typeStr = typeMatch[1];
|
|
171
|
+
|
|
172
|
+
const isClass = /\bowl:Class\b/.test(typeStr);
|
|
173
|
+
const isObjectProperty = /\bowl:ObjectProperty\b/.test(typeStr);
|
|
174
|
+
const isDatatypeProperty = /\bowl:DatatypeProperty\b/.test(typeStr);
|
|
175
|
+
|
|
176
|
+
if (!isClass && !isObjectProperty && !isDatatypeProperty) continue;
|
|
177
|
+
|
|
178
|
+
const label = extractLiteral(trimmed, 'rdfs:label');
|
|
179
|
+
const comment = extractLiteral(trimmed, 'rdfs:comment');
|
|
180
|
+
const iri = expandPrefixed(subject);
|
|
181
|
+
const compact = compactIri(iri);
|
|
182
|
+
|
|
183
|
+
if (isClass) {
|
|
184
|
+
const subClassOf = extractResource(trimmed, 'rdfs:subClassOf');
|
|
185
|
+
const disjointWith = extractResource(trimmed, 'owl:disjointWith');
|
|
186
|
+
|
|
187
|
+
classes.push({
|
|
188
|
+
iri,
|
|
189
|
+
compact,
|
|
190
|
+
label: label || subject.replace('gloss:', ''),
|
|
191
|
+
comment,
|
|
192
|
+
subClassOf: subClassOf ? compactIri(expandPrefixed(subClassOf)) : null,
|
|
193
|
+
disjointWith: disjointWith ? compactIri(expandPrefixed(disjointWith)) : null,
|
|
194
|
+
});
|
|
195
|
+
} else {
|
|
196
|
+
const domain = extractResource(trimmed, 'rdfs:domain');
|
|
197
|
+
const range = extractResource(trimmed, 'rdfs:range');
|
|
198
|
+
const inverseOf = extractResource(trimmed, 'owl:inverseOf');
|
|
199
|
+
|
|
200
|
+
// For unionOf domains/ranges, detect the bracket pattern
|
|
201
|
+
// rdfs:domain [ a owl:Class ; owl:unionOf ( gloss:A gloss:B ) ] ;
|
|
202
|
+
let domainUnion = null;
|
|
203
|
+
let rangeUnion = null;
|
|
204
|
+
|
|
205
|
+
const unionDomainMatch = trimmed.match(/rdfs:domain\s+\[\s*a\s+owl:Class\s*;\s*owl:unionOf\s*\(([^)]+)\)\s*\]/);
|
|
206
|
+
if (unionDomainMatch) {
|
|
207
|
+
domainUnion = unionDomainMatch[1].trim().split(/\s+/).map(t => compactIri(expandPrefixed(t)));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const unionRangeMatch = trimmed.match(/rdfs:range\s+\[\s*a\s+owl:Class\s*;\s*owl:unionOf\s*\(([^)]+)\)\s*\]/);
|
|
211
|
+
if (unionRangeMatch) {
|
|
212
|
+
rangeUnion = unionRangeMatch[1].trim().split(/\s+/).map(t => compactIri(expandPrefixed(t)));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
properties.push({
|
|
216
|
+
iri,
|
|
217
|
+
compact,
|
|
218
|
+
label: label || subject.replace('gloss:', ''),
|
|
219
|
+
comment,
|
|
220
|
+
type: isObjectProperty ? 'object' : 'datatype',
|
|
221
|
+
domain: domain ? compactIri(expandPrefixed(domain)) : null,
|
|
222
|
+
domainUnion: domainUnion,
|
|
223
|
+
range: range ? compactIri(expandPrefixed(range)) : null,
|
|
224
|
+
rangeUnion: rangeUnion,
|
|
225
|
+
inverseOf: inverseOf ? compactIri(expandPrefixed(inverseOf)) : null,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { classes, properties };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildClassHierarchy(classes) {
|
|
234
|
+
const map = new Map();
|
|
235
|
+
for (const c of classes) {
|
|
236
|
+
map.set(c.compact, c);
|
|
237
|
+
c.children = [];
|
|
238
|
+
c.ancestors = [];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Build children
|
|
242
|
+
for (const c of classes) {
|
|
243
|
+
if (c.subClassOf && map.has(c.subClassOf)) {
|
|
244
|
+
map.get(c.subClassOf).children.push(c.compact);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Build ancestor chains
|
|
249
|
+
for (const c of classes) {
|
|
250
|
+
const chain = [];
|
|
251
|
+
let current = c.subClassOf;
|
|
252
|
+
while (current && map.has(current)) {
|
|
253
|
+
chain.push(current);
|
|
254
|
+
current = map.get(current).subClassOf;
|
|
255
|
+
}
|
|
256
|
+
// Add non-glossarist ancestors
|
|
257
|
+
if (current) chain.push(current);
|
|
258
|
+
c.ancestors = chain;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Find roots (no subClassOf or subClassOf points outside our ontology)
|
|
262
|
+
const roots = classes
|
|
263
|
+
.filter(c => !c.subClassOf || !map.has(c.subClassOf))
|
|
264
|
+
.map(c => c.compact);
|
|
265
|
+
|
|
266
|
+
return { roots, map: Object.fromEntries(map) };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function groupPropertiesByDomain(properties) {
|
|
270
|
+
const groups = {};
|
|
271
|
+
for (const p of properties) {
|
|
272
|
+
const domains = p.domainUnion || (p.domain ? [p.domain] : ['(unspecified)']);
|
|
273
|
+
for (const d of domains) {
|
|
274
|
+
if (!groups[d]) groups[d] = { object: [], datatype: [] };
|
|
275
|
+
groups[d][p.type].push(p.compact);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return groups;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function main() {
|
|
282
|
+
if (!existsSync(ONTOLOGY_TTL)) {
|
|
283
|
+
console.error(`Ontology file not found: ${ONTOLOGY_TTL}`);
|
|
284
|
+
console.error('Ensure concept-model is available at ../concept-model/');
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const ttlText = readFileSync(ONTOLOGY_TTL, 'utf-8');
|
|
289
|
+
const { classes, properties } = parseOntology(ttlText);
|
|
290
|
+
|
|
291
|
+
const hierarchy = buildClassHierarchy(classes);
|
|
292
|
+
const propsByDomain = groupPropertiesByDomain(properties);
|
|
293
|
+
|
|
294
|
+
const output = {
|
|
295
|
+
ontologyIri: 'https://www.glossarist.org/ontologies/glossarist',
|
|
296
|
+
ontologyLabel: 'Glossarist Ontology',
|
|
297
|
+
classes: hierarchy.map,
|
|
298
|
+
classHierarchyRoots: hierarchy.roots,
|
|
299
|
+
properties: Object.fromEntries(properties.map(p => [p.compact, p])),
|
|
300
|
+
propertiesByDomain: propsByDomain,
|
|
301
|
+
stats: {
|
|
302
|
+
classCount: classes.length,
|
|
303
|
+
objectPropertyCount: properties.filter(p => p.type === 'object').length,
|
|
304
|
+
datatypePropertyCount: properties.filter(p => p.type === 'datatype').length,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
mkdirSync(dirname(OUTPUT), { recursive: true });
|
|
309
|
+
writeFileSync(OUTPUT, JSON.stringify(output, null, 2) + '\n');
|
|
310
|
+
|
|
311
|
+
console.log(`Parsed ${output.stats.classCount} classes, ${output.stats.objectPropertyCount} object properties, ${output.stats.datatypePropertyCount} datatype properties`);
|
|
312
|
+
console.log(`Wrote ${OUTPUT}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
main();
|
|
@@ -31,7 +31,7 @@ function makeManifest(): Manifest {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function makeEntry(overrides: Partial<ConceptSummary> = {}): ConceptSummary {
|
|
34
|
-
return { id: '3.1.1.1', eng: 'test term', status: 'valid', ...overrides };
|
|
34
|
+
return { id: '3.1.1.1', designations: { eng: 'test term' }, eng: 'test term', status: 'valid', ...overrides };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
async function createTestRouter() {
|
|
@@ -4,7 +4,8 @@ import { createPinia, setActivePinia } from 'pinia';
|
|
|
4
4
|
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
5
5
|
import ConceptDetail from '../components/ConceptDetail.vue';
|
|
6
6
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
7
|
-
import type { Manifest
|
|
7
|
+
import type { Manifest } from '../adapters/types';
|
|
8
|
+
import { conceptFromJson } from '../adapters/model-bridge';
|
|
8
9
|
// Prevent the 2.7MB Opal runtime from loading in tests
|
|
9
10
|
vi.mock('../utils/plurimath', () => ({
|
|
10
11
|
loadPlurimath: () => new Promise(() => {}),
|
|
@@ -39,7 +40,7 @@ function makeManifest(): Manifest {
|
|
|
39
40
|
};
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
function
|
|
43
|
+
function makeConceptJson(overrides: Record<string, any> = {}) {
|
|
43
44
|
return {
|
|
44
45
|
'@context': 'https://glossarist.org/context',
|
|
45
46
|
'@id': 'https://glossarist.org/test/concept/1',
|
|
@@ -79,6 +80,7 @@ function makeConcept(): ConceptDocument {
|
|
|
79
80
|
],
|
|
80
81
|
},
|
|
81
82
|
},
|
|
83
|
+
...overrides,
|
|
82
84
|
};
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -109,7 +111,8 @@ describe('ConceptDetail interactions', () => {
|
|
|
109
111
|
store.datasets.set('test', { index: [], getConceptCount: () => 0, getConcepts: () => [], getConceptPosition: () => -1, getIndexEntry: () => undefined } as any);
|
|
110
112
|
});
|
|
111
113
|
|
|
112
|
-
function mountDetail(
|
|
114
|
+
function mountDetail(conceptJson: Record<string, any> = makeConceptJson()) {
|
|
115
|
+
const concept = conceptFromJson(conceptJson);
|
|
113
116
|
return mount(ConceptDetail, {
|
|
114
117
|
global: {
|
|
115
118
|
plugins: [pinia, router],
|
|
@@ -130,49 +133,64 @@ describe('ConceptDetail interactions', () => {
|
|
|
130
133
|
expect(wrapper.find('h1').html()).toContain('test term');
|
|
131
134
|
});
|
|
132
135
|
|
|
136
|
+
async function switchToDefinition(wrapper: ReturnType<typeof mountDetail>) {
|
|
137
|
+
const tabs = wrapper.findAll('button[role="tab"]');
|
|
138
|
+
const defTab = tabs.find(t => t.text().includes('Definition'));
|
|
139
|
+
if (defTab) {
|
|
140
|
+
await defTab.trigger('click');
|
|
141
|
+
await flushPromises();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
133
145
|
it('renders concept ID badge', () => {
|
|
134
146
|
const wrapper = mountDetail();
|
|
135
147
|
expect(wrapper.text()).toContain('1');
|
|
136
148
|
});
|
|
137
149
|
|
|
138
|
-
it('renders language sections for eng and fra', () => {
|
|
150
|
+
it('renders language sections for eng and fra', async () => {
|
|
139
151
|
const wrapper = mountDetail();
|
|
152
|
+
await switchToDefinition(wrapper);
|
|
140
153
|
expect(wrapper.text()).toContain('English');
|
|
141
154
|
expect(wrapper.text()).toContain('French');
|
|
142
155
|
});
|
|
143
156
|
|
|
144
|
-
it('renders italic text in definition', () => {
|
|
157
|
+
it('renders italic text in definition', async () => {
|
|
145
158
|
const wrapper = mountDetail();
|
|
159
|
+
await switchToDefinition(wrapper);
|
|
146
160
|
expect(wrapper.html()).toContain('<em>italic</em>');
|
|
147
161
|
});
|
|
148
162
|
|
|
149
|
-
it('renders stem: notation as math-pending placeholder', () => {
|
|
163
|
+
it('renders stem: notation as math-pending placeholder', async () => {
|
|
150
164
|
const wrapper = mountDetail();
|
|
165
|
+
await switchToDefinition(wrapper);
|
|
151
166
|
expect(wrapper.html()).toContain('math-pending');
|
|
152
167
|
expect(wrapper.html()).toContain('data-expr="x"');
|
|
153
168
|
});
|
|
154
169
|
|
|
155
|
-
it('renders notes section', () => {
|
|
170
|
+
it('renders notes section', async () => {
|
|
156
171
|
const wrapper = mountDetail();
|
|
172
|
+
await switchToDefinition(wrapper);
|
|
157
173
|
expect(wrapper.text()).toContain('Note 1');
|
|
158
174
|
expect(wrapper.text()).toContain('a note');
|
|
159
175
|
});
|
|
160
176
|
|
|
161
|
-
it('renders examples section', () => {
|
|
177
|
+
it('renders examples section', async () => {
|
|
162
178
|
const wrapper = mountDetail();
|
|
179
|
+
await switchToDefinition(wrapper);
|
|
163
180
|
expect(wrapper.text()).toContain('Example 1');
|
|
164
181
|
expect(wrapper.text()).toContain('an example');
|
|
165
182
|
});
|
|
166
183
|
|
|
167
|
-
it('renders designation types as badges', () => {
|
|
184
|
+
it('renders designation types as badges', async () => {
|
|
168
185
|
const wrapper = mountDetail();
|
|
169
|
-
|
|
186
|
+
await switchToDefinition(wrapper);
|
|
187
|
+
expect(wrapper.text()).toContain('symbol');
|
|
170
188
|
});
|
|
171
189
|
|
|
172
190
|
it('collapses non-eng languages when 6+ languages present', async () => {
|
|
173
|
-
const
|
|
191
|
+
const json = makeConceptJson() as Record<string, any>;
|
|
174
192
|
for (const lang of ['deu', 'spa', 'kor', 'jpn']) {
|
|
175
|
-
|
|
193
|
+
json['gl:localizedConcept'][lang] = {
|
|
176
194
|
'@id': `https://glossarist.org/test/concept/1/${lang}`,
|
|
177
195
|
'@type': 'gl:LocalizedConcept',
|
|
178
196
|
'gl:languageCode': lang,
|
|
@@ -184,13 +202,15 @@ describe('ConceptDetail interactions', () => {
|
|
|
184
202
|
],
|
|
185
203
|
};
|
|
186
204
|
}
|
|
187
|
-
const wrapper = mountDetail(
|
|
205
|
+
const wrapper = mountDetail(json);
|
|
206
|
+
await switchToDefinition(wrapper);
|
|
188
207
|
await flushPromises();
|
|
189
208
|
expect(wrapper.text()).toContain('6 languages');
|
|
190
209
|
});
|
|
191
210
|
|
|
192
211
|
it('toggles language section on click', async () => {
|
|
193
212
|
const wrapper = mountDetail();
|
|
213
|
+
await switchToDefinition(wrapper);
|
|
194
214
|
const buttons = wrapper.findAll('button');
|
|
195
215
|
const fraButton = buttons.find(b => b.text().includes('French'));
|
|
196
216
|
expect(fraButton).toBeDefined();
|
|
@@ -201,6 +221,7 @@ describe('ConceptDetail interactions', () => {
|
|
|
201
221
|
|
|
202
222
|
it('switches between definition and history tabs', async () => {
|
|
203
223
|
const wrapper = mountDetail();
|
|
224
|
+
await switchToDefinition(wrapper);
|
|
204
225
|
expect(wrapper.text()).toContain('a definition with');
|
|
205
226
|
|
|
206
227
|
const tabs = wrapper.findAll('button[role="tab"]');
|
|
@@ -213,9 +234,8 @@ describe('ConceptDetail interactions', () => {
|
|
|
213
234
|
});
|
|
214
235
|
|
|
215
236
|
it('renders cross-reference link and navigates on click', async () => {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
eng['gl:definition'] = [
|
|
237
|
+
const json = makeConceptJson();
|
|
238
|
+
json['gl:localizedConcept'].eng['gl:definition'] = [
|
|
219
239
|
{ '@type': 'gl:DetailedDefinition', 'gl:content': 'see {{urn:iso:std:iso:14812:3.1.1.1,entity}} here' },
|
|
220
240
|
];
|
|
221
241
|
|
|
@@ -228,7 +248,8 @@ describe('ConceptDetail interactions', () => {
|
|
|
228
248
|
});
|
|
229
249
|
factory.resolver.registerDataset('test', ['https://glossarist.org/test/concept/*']);
|
|
230
250
|
|
|
231
|
-
const wrapper = mountDetail(
|
|
251
|
+
const wrapper = mountDetail(json);
|
|
252
|
+
await switchToDefinition(wrapper);
|
|
232
253
|
await flushPromises();
|
|
233
254
|
|
|
234
255
|
const xref = wrapper.find('.xref-link');
|
|
@@ -244,8 +265,9 @@ describe('ConceptDetail interactions', () => {
|
|
|
244
265
|
expect(wrapper.text()).toContain('valid');
|
|
245
266
|
});
|
|
246
267
|
|
|
247
|
-
it('renders the language quick-jump sidebar with all languages', () => {
|
|
268
|
+
it('renders the language quick-jump sidebar with all languages', async () => {
|
|
248
269
|
const wrapper = mountDetail();
|
|
270
|
+
await switchToDefinition(wrapper);
|
|
249
271
|
expect(wrapper.text()).toContain('Languages (2)');
|
|
250
272
|
});
|
|
251
273
|
});
|
|
@@ -1,37 +1,33 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { FORMAT_REGISTRY, conceptToTurtle, conceptToSkosJsonLd } from '../utils/concept-formats';
|
|
3
|
-
import
|
|
3
|
+
import { Concept } from 'glossarist';
|
|
4
4
|
|
|
5
|
-
function makeConcept(overrides:
|
|
6
|
-
return {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'gl:identifier': '1',
|
|
11
|
-
'gl:localizedConcept': {
|
|
5
|
+
function makeConcept(overrides: Record<string, unknown> = {}): Concept {
|
|
6
|
+
return Concept.fromJSON({
|
|
7
|
+
id: '1',
|
|
8
|
+
uri: 'https://glossarist.org/test/concept/1',
|
|
9
|
+
localizations: {
|
|
12
10
|
eng: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{
|
|
18
|
-
{ '@type': 'gl:Expression', 'gl:normativeStatus': 'admitted', 'gl:term': 'alt term' },
|
|
11
|
+
language_code: 'eng',
|
|
12
|
+
entry_status: 'valid',
|
|
13
|
+
terms: [
|
|
14
|
+
{ type: 'expression', designation: 'test term', normative_status: 'preferred' },
|
|
15
|
+
{ type: 'expression', designation: 'alt term', normative_status: 'admitted' },
|
|
19
16
|
],
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
definition: [{ content: 'a definition' }],
|
|
18
|
+
notes: [{ content: 'a note' }],
|
|
22
19
|
},
|
|
23
20
|
deu: {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{ '@type': 'gl:Expression', 'gl:normativeStatus': 'preferred', 'gl:term': 'Testbegriff' },
|
|
21
|
+
language_code: 'deu',
|
|
22
|
+
entry_status: 'valid',
|
|
23
|
+
terms: [
|
|
24
|
+
{ type: 'expression', designation: 'Testbegriff', normative_status: 'preferred' },
|
|
29
25
|
],
|
|
30
|
-
|
|
26
|
+
definition: [{ content: 'eine Definition' }],
|
|
31
27
|
},
|
|
32
28
|
},
|
|
33
29
|
...overrides,
|
|
34
|
-
};
|
|
30
|
+
});
|
|
35
31
|
}
|
|
36
32
|
|
|
37
33
|
describe('FORMAT_REGISTRY', () => {
|
|
@@ -64,17 +60,24 @@ describe('conceptToTurtle', () => {
|
|
|
64
60
|
});
|
|
65
61
|
|
|
66
62
|
it('escapes special characters in Turtle', () => {
|
|
67
|
-
const concept =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
const concept = Concept.fromJSON({
|
|
64
|
+
id: '1',
|
|
65
|
+
uri: 'https://glossarist.org/test/concept/1',
|
|
66
|
+
localizations: {
|
|
67
|
+
eng: {
|
|
68
|
+
language_code: 'eng',
|
|
69
|
+
terms: [{ type: 'expression', designation: 'test', normative_status: 'preferred' }],
|
|
70
|
+
definition: [{ content: 'has "quotes" and \\backslash' }],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
71
74
|
const ttl = conceptToTurtle(concept);
|
|
72
75
|
expect(ttl).toContain('\\"quotes\\"');
|
|
73
76
|
expect(ttl).toContain('\\\\backslash');
|
|
74
77
|
});
|
|
75
78
|
|
|
76
79
|
it('handles empty concept gracefully', () => {
|
|
77
|
-
const ttl = conceptToTurtle({}
|
|
80
|
+
const ttl = conceptToTurtle(Concept.fromJSON({}));
|
|
78
81
|
expect(ttl).toContain('a skos:Concept');
|
|
79
82
|
expect(ttl).toContain('skos:notation ""');
|
|
80
83
|
});
|
|
@@ -100,8 +103,7 @@ describe('conceptToSkosJsonLd', () => {
|
|
|
100
103
|
});
|
|
101
104
|
|
|
102
105
|
it('omits empty language maps', () => {
|
|
103
|
-
const concept =
|
|
104
|
-
concept['gl:localizedConcept'] = {};
|
|
106
|
+
const concept = Concept.fromJSON({ id: '1', uri: 'https://glossarist.org/test/concept/1' });
|
|
105
107
|
const parsed = JSON.parse(conceptToSkosJsonLd(concept));
|
|
106
108
|
expect(parsed['skos:prefLabel']).toBeUndefined();
|
|
107
109
|
expect(parsed['skos:definition']).toBeUndefined();
|