@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.0",
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": {
@@ -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 downloaded filenames
573
- if (siteBranding.logo?.remoteUrl) {
574
- siteBranding.logo = { ...siteBranding.logo, path: `/logos/${config.id}-logo.svg` };
575
- delete siteBranding.logo.remoteUrl;
576
- }
577
- if (siteBranding.footerLogo?.remoteUrl) {
578
- siteBranding.footerLogo = { ...siteBranding.footerLogo, path: `/logos/${config.id}-footer-logo.svg` };
579
- delete siteBranding.footerLogo.remoteUrl;
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 (SKOS)', mediaType: 'application/ld+json' },
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'] || '';
@@ -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
- "./index.html",
6
- "./src/**/*.{vue,js,ts,jsx,tsx}",
10
+ resolve(__dirname, "index.html"),
11
+ resolve(__dirname, "src/**/*.{vue,js,ts,jsx,tsx}"),
7
12
  ],
8
13
  theme: {
9
14
  extend: {