@glossarist/concept-browser 0.1.0 → 0.1.2
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.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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/build-edges.js
CHANGED
|
@@ -89,6 +89,11 @@ function buildEdgesForDataset(datasetDir, registerId) {
|
|
|
89
89
|
// Main
|
|
90
90
|
console.log('Building edge indexes...\n');
|
|
91
91
|
|
|
92
|
+
if (!existsSync(DATA_DIR)) {
|
|
93
|
+
console.log('No data directory found. Nothing to do.');
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
92
97
|
const datasets = readdirSync(DATA_DIR).filter(f => {
|
|
93
98
|
try {
|
|
94
99
|
return existsSync(join(DATA_DIR, f, 'manifest.json'));
|
|
@@ -221,6 +221,42 @@ function conceptJsonToTurtle(concept) {
|
|
|
221
221
|
return lines.join('\n');
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
function conceptJsonToSkosJsonLd(concept) {
|
|
225
|
+
const uri = concept['@id'] || '';
|
|
226
|
+
const id = concept['gl:identifier'] || '';
|
|
227
|
+
|
|
228
|
+
const doc = {
|
|
229
|
+
'@context': {
|
|
230
|
+
skos: 'http://www.w3.org/2004/02/skos/core#',
|
|
231
|
+
dcterms: 'http://purl.org/dc/terms/',
|
|
232
|
+
'@language': { '@container': '@language' },
|
|
233
|
+
},
|
|
234
|
+
'@id': uri,
|
|
235
|
+
'@type': 'skos:Concept',
|
|
236
|
+
'skos:notation': id,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const prefLabels = {}, altLabels = {}, definitions = {}, scopeNotes = {};
|
|
240
|
+
for (const [lang, lc] of Object.entries(concept['gl:localizedConcept'] || {})) {
|
|
241
|
+
const descs = lc['gl:designation'] || [];
|
|
242
|
+
const pref = descs.find(d => d['gl:normativeStatus'] === 'preferred' && d['gl:term']);
|
|
243
|
+
const alt = descs.find(d => d['gl:normativeStatus'] !== 'preferred' && d['gl:term']);
|
|
244
|
+
if (pref) prefLabels[lang] = pref['gl:term'];
|
|
245
|
+
if (alt) altLabels[lang] = alt['gl:term'];
|
|
246
|
+
const def = (lc['gl:definition'] || [])[0];
|
|
247
|
+
if (def?.['gl:content']) definitions[lang] = def['gl:content'];
|
|
248
|
+
const note = (lc['gl:notes'] || [])[0];
|
|
249
|
+
if (note?.['gl:content']) scopeNotes[lang] = note['gl:content'];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (Object.keys(prefLabels).length) doc['skos:prefLabel'] = prefLabels;
|
|
253
|
+
if (Object.keys(altLabels).length) doc['skos:altLabel'] = altLabels;
|
|
254
|
+
if (Object.keys(definitions).length) doc['skos:definition'] = definitions;
|
|
255
|
+
if (Object.keys(scopeNotes).length) doc['skos:scopeNote'] = scopeNotes;
|
|
256
|
+
|
|
257
|
+
return JSON.stringify(doc, null, 2);
|
|
258
|
+
}
|
|
259
|
+
|
|
224
260
|
function processDataset(dir, register, opts) {
|
|
225
261
|
const files = fs.readdirSync(dir).filter(f => f.endsWith('.yaml')).sort((a, b) => naturalSort(a.replace('.yaml', ''), b.replace('.yaml', '')));
|
|
226
262
|
|
|
@@ -230,7 +266,7 @@ function processDataset(dir, register, opts) {
|
|
|
230
266
|
const concepts = [];
|
|
231
267
|
const langTermCounts = {};
|
|
232
268
|
const langDefCounts = {};
|
|
233
|
-
const availableFormats = ['ttl', 'yaml'];
|
|
269
|
+
const availableFormats = ['ttl', 'jsonld', 'yaml'];
|
|
234
270
|
|
|
235
271
|
for (let i = 0; i < files.length; i++) {
|
|
236
272
|
const file = files[i];
|
|
@@ -246,6 +282,10 @@ function processDataset(dir, register, opts) {
|
|
|
246
282
|
const ttlContent = conceptJsonToTurtle(jsonld);
|
|
247
283
|
fs.writeFileSync(path.join(conceptsDir, `${termid}.ttl`), ttlContent);
|
|
248
284
|
|
|
285
|
+
// Generate SKOS JSON-LD format
|
|
286
|
+
const skosJsonLd = conceptJsonToSkosJsonLd(jsonld);
|
|
287
|
+
fs.writeFileSync(path.join(conceptsDir, `${termid}.jsonld`), skosJsonLd);
|
|
288
|
+
|
|
249
289
|
// Copy source YAML
|
|
250
290
|
fs.copyFileSync(path.join(dir, file), path.join(conceptsDir, `${termid}.yaml`));
|
|
251
291
|
|
|
@@ -569,14 +609,14 @@ const processedPages = processPages(config);
|
|
|
569
609
|
|
|
570
610
|
// Generate site-config.json from site config
|
|
571
611
|
const siteBranding = { ...config.branding };
|
|
572
|
-
// Rewrite logo paths to
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
612
|
+
// Rewrite logo paths to destination filenames and strip build-time fields
|
|
613
|
+
for (const key of ['logo', 'footerLogo']) {
|
|
614
|
+
const suffix = key === 'logo' ? 'logo.svg' : 'footer-logo.svg';
|
|
615
|
+
if (siteBranding[key]) {
|
|
616
|
+
siteBranding[key] = { ...siteBranding[key], path: `/logos/${config.id}-${suffix}` };
|
|
617
|
+
delete siteBranding[key].localPath;
|
|
618
|
+
delete siteBranding[key].remoteUrl;
|
|
619
|
+
}
|
|
580
620
|
}
|
|
581
621
|
|
|
582
622
|
writeJson(path.join(PUBLIC, 'site-config.json'), {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { FORMAT_REGISTRY, conceptToTurtle, conceptToSkosJsonLd } from '../utils/concept-formats';
|
|
3
|
+
import type { ConceptDocument } from '../adapters/types';
|
|
4
|
+
|
|
5
|
+
function makeConcept(overrides: Partial<ConceptDocument> = {}): ConceptDocument {
|
|
6
|
+
return {
|
|
7
|
+
'@context': 'https://glossarist.org/ns/context.jsonld',
|
|
8
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
9
|
+
'@type': 'gl:Concept',
|
|
10
|
+
'gl:identifier': '1',
|
|
11
|
+
'gl:localizedConcept': {
|
|
12
|
+
eng: {
|
|
13
|
+
'@id': 'https://glossarist.org/test/concept/1/eng',
|
|
14
|
+
'@type': 'gl:LocalizedConcept',
|
|
15
|
+
'gl:languageCode': 'eng',
|
|
16
|
+
'gl:designation': [
|
|
17
|
+
{ '@type': 'gl:Expression', 'gl:normativeStatus': 'preferred', 'gl:term': 'test term' },
|
|
18
|
+
{ '@type': 'gl:Expression', 'gl:normativeStatus': 'admitted', 'gl:term': 'alt term' },
|
|
19
|
+
],
|
|
20
|
+
'gl:definition': [{ '@type': 'gl:DetailedDefinition', 'gl:content': 'a definition' }],
|
|
21
|
+
'gl:notes': [{ '@type': 'gl:DetailedDefinition', 'gl:content': 'a note' }],
|
|
22
|
+
},
|
|
23
|
+
deu: {
|
|
24
|
+
'@id': 'https://glossarist.org/test/concept/1/deu',
|
|
25
|
+
'@type': 'gl:LocalizedConcept',
|
|
26
|
+
'gl:languageCode': 'deu',
|
|
27
|
+
'gl:designation': [
|
|
28
|
+
{ '@type': 'gl:Expression', 'gl:normativeStatus': 'preferred', 'gl:term': 'Testbegriff' },
|
|
29
|
+
],
|
|
30
|
+
'gl:definition': [{ '@type': 'gl:DetailedDefinition', 'gl:content': 'eine Definition' }],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('FORMAT_REGISTRY', () => {
|
|
38
|
+
it('has ttl, jsonld, yaml entries', () => {
|
|
39
|
+
expect(FORMAT_REGISTRY.ttl).toBeDefined();
|
|
40
|
+
expect(FORMAT_REGISTRY.jsonld).toBeDefined();
|
|
41
|
+
expect(FORMAT_REGISTRY.yaml).toBeDefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('each entry has extension, label, mediaType', () => {
|
|
45
|
+
for (const [, desc] of Object.entries(FORMAT_REGISTRY)) {
|
|
46
|
+
expect(desc.extension).toBeTruthy();
|
|
47
|
+
expect(desc.label).toBeTruthy();
|
|
48
|
+
expect(desc.mediaType).toBeTruthy();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('conceptToTurtle', () => {
|
|
54
|
+
it('generates valid Turtle with SKOS predicates', () => {
|
|
55
|
+
const ttl = conceptToTurtle(makeConcept());
|
|
56
|
+
expect(ttl).toContain('@prefix skos:');
|
|
57
|
+
expect(ttl).toContain('a skos:Concept');
|
|
58
|
+
expect(ttl).toContain('skos:prefLabel "test term"@eng');
|
|
59
|
+
expect(ttl).toContain('skos:altLabel "alt term"@eng');
|
|
60
|
+
expect(ttl).toContain('skos:prefLabel "Testbegriff"@deu');
|
|
61
|
+
expect(ttl).toContain('skos:definition "a definition"@eng');
|
|
62
|
+
expect(ttl).toContain('skos:scopeNote "a note"@eng');
|
|
63
|
+
expect(ttl).toContain('skos:notation "1"');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('escapes special characters in Turtle', () => {
|
|
67
|
+
const concept = makeConcept();
|
|
68
|
+
concept['gl:localizedConcept']!.eng!['gl:definition'] = [
|
|
69
|
+
{ '@type': 'gl:DetailedDefinition', 'gl:content': 'has "quotes" and \\backslash' },
|
|
70
|
+
];
|
|
71
|
+
const ttl = conceptToTurtle(concept);
|
|
72
|
+
expect(ttl).toContain('\\"quotes\\"');
|
|
73
|
+
expect(ttl).toContain('\\\\backslash');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('handles empty concept gracefully', () => {
|
|
77
|
+
const ttl = conceptToTurtle({} as ConceptDocument);
|
|
78
|
+
expect(ttl).toContain('a skos:Concept');
|
|
79
|
+
expect(ttl).toContain('skos:notation ""');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('conceptToSkosJsonLd', () => {
|
|
84
|
+
it('generates SKOS JSON-LD with language maps', () => {
|
|
85
|
+
const jsonld = conceptToSkosJsonLd(makeConcept());
|
|
86
|
+
const parsed = JSON.parse(jsonld);
|
|
87
|
+
|
|
88
|
+
expect(parsed['@type']).toBe('skos:Concept');
|
|
89
|
+
expect(parsed['@id']).toBe('https://glossarist.org/test/concept/1');
|
|
90
|
+
expect(parsed['skos:notation']).toBe('1');
|
|
91
|
+
expect(parsed['skos:prefLabel']).toEqual({ eng: 'test term', deu: 'Testbegriff' });
|
|
92
|
+
expect(parsed['skos:altLabel']).toEqual({ eng: 'alt term' });
|
|
93
|
+
expect(parsed['skos:definition']).toEqual({ eng: 'a definition', deu: 'eine Definition' });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('uses @language container in context', () => {
|
|
97
|
+
const jsonld = conceptToSkosJsonLd(makeConcept());
|
|
98
|
+
const parsed = JSON.parse(jsonld);
|
|
99
|
+
expect(parsed['@context']['@language']).toEqual({ '@container': '@language' });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('omits empty language maps', () => {
|
|
103
|
+
const concept = makeConcept();
|
|
104
|
+
concept['gl:localizedConcept'] = {};
|
|
105
|
+
const parsed = JSON.parse(conceptToSkosJsonLd(concept));
|
|
106
|
+
expect(parsed['skos:prefLabel']).toBeUndefined();
|
|
107
|
+
expect(parsed['skos:definition']).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Format registry and converters for per-concept format downloads.
|
|
3
|
-
*
|
|
4
|
-
* Open/closed: add a new format by adding an entry to FORMAT_REGISTRY
|
|
5
|
-
* and a converter function. No changes to components needed.
|
|
6
|
-
*/
|
|
1
|
+
import type { ConceptDocument, LocalizedConcept } from '../adapters/types';
|
|
7
2
|
|
|
8
3
|
export interface FormatDescriptor {
|
|
9
4
|
extension: string;
|
|
@@ -13,33 +8,8 @@ export interface FormatDescriptor {
|
|
|
13
8
|
|
|
14
9
|
export const FORMAT_REGISTRY: Record<string, FormatDescriptor> = {
|
|
15
10
|
ttl: { extension: 'ttl', label: 'Turtle RDF', mediaType: 'text/turtle' },
|
|
16
|
-
jsonld: { extension: 'jsonld', label: 'JSON-LD
|
|
11
|
+
jsonld: { extension: 'jsonld', label: 'JSON-LD', mediaType: 'application/ld+json' },
|
|
17
12
|
yaml: { extension: 'yaml', label: 'YAML', mediaType: 'text/yaml' },
|
|
18
|
-
tbx: { extension: 'tbx', label: 'TBX', mediaType: 'application/xml' },
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type ConceptDesignation = {
|
|
22
|
-
'@type'?: string;
|
|
23
|
-
'gl:normativeStatus'?: string;
|
|
24
|
-
'gl:term'?: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export type ConceptDefinition = {
|
|
28
|
-
'gl:content'?: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export type ConceptLocalized = {
|
|
32
|
-
'gl:languageCode'?: string;
|
|
33
|
-
'gl:designation'?: ConceptDesignation[];
|
|
34
|
-
'gl:definition'?: ConceptDefinition[];
|
|
35
|
-
'gl:notes'?: ConceptDefinition[];
|
|
36
|
-
'gl:source'?: any[];
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export type ConceptDocument = {
|
|
40
|
-
'@id'?: string;
|
|
41
|
-
'gl:identifier'?: string;
|
|
42
|
-
'gl:localizedConcept'?: Record<string, ConceptLocalized>;
|
|
43
13
|
};
|
|
44
14
|
|
|
45
15
|
function getLocalizedData(concept: ConceptDocument) {
|
|
@@ -77,9 +47,6 @@ function escapeTurtle(s: string): string {
|
|
|
77
47
|
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
78
48
|
}
|
|
79
49
|
|
|
80
|
-
/**
|
|
81
|
-
* Convert a concept document to SKOS Turtle.
|
|
82
|
-
*/
|
|
83
50
|
export function conceptToTurtle(concept: ConceptDocument): string {
|
|
84
51
|
const uri = concept['@id'] || '';
|
|
85
52
|
const id = concept['gl:identifier'] || '';
|
|
@@ -88,7 +55,6 @@ export function conceptToTurtle(concept: ConceptDocument): string {
|
|
|
88
55
|
const lines: string[] = [
|
|
89
56
|
'@prefix skos: <http://www.w3.org/2004/02/skos/core#> .',
|
|
90
57
|
'@prefix dcterms: <http://purl.org/dc/terms/> .',
|
|
91
|
-
'@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .',
|
|
92
58
|
'',
|
|
93
59
|
];
|
|
94
60
|
|
|
@@ -117,9 +83,6 @@ export function conceptToTurtle(concept: ConceptDocument): string {
|
|
|
117
83
|
return lines.join('\n');
|
|
118
84
|
}
|
|
119
85
|
|
|
120
|
-
/**
|
|
121
|
-
* Convert a concept document to SKOS JSON-LD.
|
|
122
|
-
*/
|
|
123
86
|
export function conceptToSkosJsonLd(concept: ConceptDocument): string {
|
|
124
87
|
const uri = concept['@id'] || '';
|
|
125
88
|
const id = concept['gl:identifier'] || '';
|
package/tailwind.config.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { dirname, resolve } from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
|
|
1
6
|
/** @type {import('tailwindcss').Config} */
|
|
2
7
|
export default {
|
|
3
8
|
darkMode: 'class',
|
|
4
9
|
content: [
|
|
5
|
-
"
|
|
6
|
-
"
|
|
10
|
+
resolve(__dirname, "index.html"),
|
|
11
|
+
resolve(__dirname, "src/**/*.{vue,js,ts,jsx,tsx}"),
|
|
7
12
|
],
|
|
8
13
|
theme: {
|
|
9
14
|
extend: {
|