@glossarist/concept-browser 0.7.135 → 0.7.136

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.
@@ -1,326 +0,0 @@
1
- /**
2
- * Concept format emitters — Turtle, SKOS JSON-LD, TBX, aggregate CSV.
3
- *
4
- * Extracted from generate-data.ts. Each function takes a JSON-LD concept
5
- * object and serializes it to a different interchange format.
6
- */
7
-
8
- import { ttlLit } from './turtle-escape';
9
-
10
- export function escapeTurtle(s: string): string {
11
- return ttlLit(s).slice(1, -1);
12
- }
13
-
14
- export function escapeXml(s: any): string {
15
- const str = Array.isArray(s) ? s.join(', ') : String(s ?? '');
16
- return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
17
- }
18
-
19
- export function conceptJsonToTurtle(concept: Record<string, any>): string {
20
- const uri = concept['@id'] || '';
21
- const id = concept['gl:identifier'] || '';
22
- const lines = [
23
- '@prefix skos: <http://www.w3.org/2004/02/skos/core#> .',
24
- '@prefix dcterms: <http://purl.org/dc/terms/> .',
25
- '',
26
- ];
27
-
28
- const props = [' a skos:Concept'];
29
- props.push(` skos:notation "${escapeTurtle(id)}"`);
30
-
31
- for (const [lang, lc] of Object.entries(concept['gl:localizedConcept'] || {}) as [string, any][]) {
32
- if (lc['gl:designation']) {
33
- for (const d of lc['gl:designation']) {
34
- const term = d['gl:term'];
35
- if (!term) continue;
36
- const pred = d['gl:normativeStatus'] === 'preferred' ? 'skos:prefLabel' : 'skos:altLabel';
37
- props.push(` ${pred} "${escapeTurtle(term)}"@${lang}`);
38
- }
39
- }
40
- if (lc['gl:definition']) {
41
- for (const d of lc['gl:definition']) {
42
- if (d['gl:content']) props.push(` skos:definition "${escapeTurtle(d['gl:content'])}"@${lang}`);
43
- }
44
- }
45
- if (lc['gl:notes']) {
46
- for (const d of lc['gl:notes']) {
47
- if (d['gl:content']) props.push(` skos:scopeNote "${escapeTurtle(d['gl:content'])}"@${lang}`);
48
- }
49
- }
50
- }
51
-
52
- lines.push(`<${uri}>`);
53
- lines.push(props.join(' ;\n'));
54
- lines.push(' .');
55
- return lines.join('\n');
56
- }
57
-
58
- const SKOS_JSON_LD_CONTEXT = {
59
- skos: 'http://www.w3.org/2004/02/skos/core#',
60
- dcterms: 'http://purl.org/dc/terms/',
61
- '@language': { '@container': '@language' },
62
- };
63
-
64
- function buildSkosJsonLdObject(concept: Record<string, any>): Record<string, any> {
65
- const uri = concept['@id'] || '';
66
- const id = concept['gl:identifier'] || '';
67
-
68
- const doc: Record<string, any> = {
69
- '@id': uri,
70
- '@type': 'skos:Concept',
71
- 'skos:notation': id,
72
- };
73
-
74
- const prefLabels: Record<string, string> = {};
75
- const altLabels: Record<string, string> = {};
76
- const definitions: Record<string, string> = {};
77
- const scopeNotes: Record<string, string> = {};
78
- for (const [lang, lc] of Object.entries(concept['gl:localizedConcept'] || {}) as [string, any][]) {
79
- const descs = lc['gl:designation'] || [];
80
- const pref = descs.find((d: any) => d['gl:normativeStatus'] === 'preferred' && d['gl:term']);
81
- const alt = descs.find((d: any) => d['gl:normativeStatus'] !== 'preferred' && d['gl:term']);
82
- if (pref) prefLabels[lang] = pref['gl:term'];
83
- if (alt) altLabels[lang] = alt['gl:term'];
84
- const def = (lc['gl:definition'] || [])[0];
85
- if (def?.['gl:content']) definitions[lang] = def['gl:content'];
86
- const note = (lc['gl:notes'] || [])[0];
87
- if (note?.['gl:content']) scopeNotes[lang] = note['gl:content'];
88
- }
89
-
90
- if (Object.keys(prefLabels).length) doc['skos:prefLabel'] = prefLabels;
91
- if (Object.keys(altLabels).length) doc['skos:altLabel'] = altLabels;
92
- if (Object.keys(definitions).length) doc['skos:definition'] = definitions;
93
- if (Object.keys(scopeNotes).length) doc['skos:scopeNote'] = scopeNotes;
94
-
95
- return doc;
96
- }
97
-
98
- export function conceptJsonToSkosJsonLd(concept: Record<string, any>): string {
99
- return JSON.stringify({
100
- '@context': SKOS_JSON_LD_CONTEXT,
101
- ...buildSkosJsonLdObject(concept),
102
- });
103
- }
104
-
105
- export function conceptsToSkosJsonLdGraph(concepts: Record<string, any>[]): string {
106
- return JSON.stringify(
107
- {
108
- '@context': SKOS_JSON_LD_CONTEXT,
109
- '@graph': concepts.map(buildSkosJsonLdObject),
110
- },
111
- null,
112
- 2,
113
- );
114
- }
115
-
116
- export const CSV_COLUMNS = [
117
- 'termid',
118
- 'uri',
119
- 'status',
120
- 'section',
121
- 'language',
122
- 'term',
123
- 'alt_terms',
124
- 'definition',
125
- 'notes',
126
- 'examples',
127
- 'sources',
128
- 'source_links',
129
- ] as const;
130
-
131
- function csvEscape(value: string): string {
132
- if (/[",\r\n]/.test(value)) {
133
- return `"${value.replace(/"/g, '""')}"`;
134
- }
135
- return value;
136
- }
137
-
138
- function joinLines(items: string[]): string {
139
- return items.filter(Boolean).join('\n');
140
- }
141
-
142
- function localizedLanguages(localized: Record<string, any>, languageOrder?: string[]): string[] {
143
- const present = Object.keys(localized);
144
- if (!languageOrder?.length) return present;
145
- const ordered = languageOrder.filter(lang => present.includes(lang));
146
- return ordered.concat(present.filter(lang => !ordered.includes(lang)));
147
- }
148
-
149
- function sourceRefString(src: Record<string, any>): string {
150
- const ref = src?.['gl:origin']?.['gl:ref'];
151
- if (!ref) return '';
152
- return [ref['gl:source'], ref['gl:id']].filter(Boolean).join(': ');
153
- }
154
-
155
- function sourceLinkString(src: Record<string, any>): string {
156
- return src?.['gl:origin']?.['gl:link'] || '';
157
- }
158
-
159
- /**
160
- * Aggregate CSV export: one row per (concept × language). Columns follow the
161
- * Glossarist concept-model fields. UTF-8 BOM + CRLF so Excel opens the file
162
- * with correct encoding without an import wizard.
163
- */
164
- export function conceptsToCsv(
165
- concepts: Record<string, any>[],
166
- opts: { languageOrder?: string[] } = {},
167
- ): string {
168
- const rows: string[] = [CSV_COLUMNS.join(',')];
169
-
170
- for (const concept of concepts) {
171
- const localized = concept['gl:localizedConcept'] || {};
172
- const domains = (concept['gl:domain'] || [])
173
- .map((d: any) => d['gl:conceptId'])
174
- .filter(Boolean)
175
- .join('; ');
176
- const managedSources = concept['gl:source'] || [];
177
- const shared = {
178
- termid: concept['gl:identifier'] || '',
179
- uri: concept['@id'] || '',
180
- status: concept['gl:status'] || '',
181
- section: domains,
182
- };
183
-
184
- const languages = localizedLanguages(localized, opts.languageOrder);
185
- if (languages.length === 0) {
186
- rows.push([shared.termid, shared.uri, shared.status, shared.section, '', '', '', '', '', '', '', ''].map(csvEscape).join(','));
187
- continue;
188
- }
189
-
190
- for (const lang of languages) {
191
- const lc = localized[lang] || {};
192
- const designations = (lc['gl:designation'] || []).filter((d: any) => d['gl:term']);
193
- const preferred = designations.find((d: any) => d['gl:normativeStatus'] === 'preferred') || designations[0];
194
- const altTerms = designations.filter((d: any) => d !== preferred).map((d: any) => d['gl:term']);
195
- const localizedSources = lc['gl:source']?.length ? lc['gl:source'] : managedSources;
196
-
197
- rows.push([
198
- shared.termid,
199
- shared.uri,
200
- shared.status,
201
- shared.section,
202
- lang,
203
- preferred?.['gl:term'] || '',
204
- altTerms.join('; '),
205
- joinLines((lc['gl:definition'] || []).map((d: any) => d['gl:content'])),
206
- joinLines((lc['gl:notes'] || []).map((d: any) => d['gl:content'])),
207
- joinLines((lc['gl:examples'] || []).map((d: any) => d['gl:content'])),
208
- localizedSources.map(sourceRefString).filter(Boolean).join('; '),
209
- localizedSources.map(sourceLinkString).filter(Boolean).join('; '),
210
- ].map(csvEscape).join(','));
211
- }
212
- }
213
-
214
- return '\uFEFF' + rows.join('\r\n') + '\r\n';
215
- }
216
-
217
- export function conceptJsonToTbx(concept: Record<string, any>): string {
218
- const id = concept['gl:identifier'] || '';
219
- const uri = concept['@id'] || '';
220
- const localized = concept['gl:localizedConcept'] || {};
221
-
222
- const langSections: { lang: string; termEntries: string[]; blocks: string }[] = [];
223
- for (const [lang, lc] of Object.entries(localized) as [string, any][]) {
224
- const descs = lc['gl:designation'] || [];
225
- const definitions = (lc['gl:definition'] || []).filter((d: any) => d['gl:content']);
226
- const notes = (lc['gl:notes'] || []).filter((d: any) => d['gl:content']);
227
- const examples = (lc['gl:examples'] || []).filter((d: any) => d['gl:content']);
228
- const sources = lc['gl:source'] || [];
229
- const entryStatus = lc['gl:entryStatus'] || '';
230
-
231
- if (!descs.length && !definitions.length) continue;
232
-
233
- const termEntries: string[] = [];
234
- for (const d of descs) {
235
- const term = d['gl:term'];
236
- if (!term) continue;
237
- const status = d['gl:normativeStatus'] || '';
238
- const type = d['@type'] || '';
239
- let gramGrp = '';
240
- if (d['gl:grammarInfo'] && d['gl:grammarInfo'].length > 0) {
241
- const gi = d['gl:grammarInfo'][0];
242
- if (gi['gl:gender']) gramGrp = `\n <grammaticalGender>${escapeXml(gi['gl:gender'])}</grammaticalGender>`;
243
- if (gi['gl:number']) gramGrp += `\n <grammaticalNumber>${escapeXml(gi['gl:number'])}</grammaticalNumber>`;
244
- for (const pos of ['noun', 'verb', 'adj', 'adverb', 'preposition', 'participle']) {
245
- if (gi[`gl:${pos}`]) gramGrp += `\n <partOfSpeech>${pos}</partOfSpeech>`;
246
- }
247
- }
248
- let posBlock = '';
249
- if (type.includes('Abbreviation')) posBlock = '\n <partOfSpeech>abbreviation</partOfSpeech>';
250
- if (type.includes('Symbol')) posBlock = '\n <partOfSpeech>symbol</partOfSpeech>';
251
-
252
- termEntries.push(` <termEntry>
253
- <langSet xml:lang="${lang}">
254
- <tig>
255
- <term>${escapeXml(term)}</term>${gramGrp}${posBlock}
256
- </tig>
257
- </langSet>
258
- </termEntry>`);
259
- }
260
-
261
- let defBlock = '';
262
- if (definitions.length) {
263
- const defParts = definitions.map((d: any) => ` <p>${escapeXml(d['gl:content'])}</p>`).join('\n');
264
- defBlock = `\n <descrip type="definition">\n${defParts}\n </descrip>`;
265
- }
266
-
267
- let noteBlock = '';
268
- for (let i = 0; i < notes.length; i++) {
269
- noteBlock += `\n <note type="note">${escapeXml(notes[i]['gl:content'])}</note>`;
270
- }
271
- for (let i = 0; i < examples.length; i++) {
272
- noteBlock += `\n <note type="example">${escapeXml(examples[i]['gl:content'])}</note>`;
273
- }
274
-
275
- let sourceBlock = '';
276
- for (const src of sources) {
277
- const origin = src['gl:origin'] || {};
278
- const parts: string[] = [];
279
- const ref = origin['gl:ref'];
280
- if (ref) {
281
- const refParts: string[] = [];
282
- if (ref['gl:source']) refParts.push(ref['gl:source']);
283
- if (ref['gl:id']) refParts.push(ref['gl:id']);
284
- parts.push(refParts.join(' ') || '');
285
- }
286
- if (origin['gl:locality']) {
287
- const loc = origin['gl:locality'];
288
- if (loc['gl:referenceFrom']) parts.push(loc['gl:localityType'] ? `${loc['gl:localityType']} ${loc['gl:referenceFrom']}` : loc['gl:referenceFrom']);
289
- }
290
- if (parts.filter(Boolean).length) {
291
- sourceBlock += `\n <ref>${escapeXml(parts.filter(Boolean).join(', '))}</ref>`;
292
- }
293
- }
294
-
295
- let statusBlock = '';
296
- if (entryStatus) {
297
- statusBlock += `\n <descrip type="entryStatus">${escapeXml(entryStatus)}</descrip>`;
298
- }
299
-
300
- const termEntriesBlock = termEntries.length ? '\n' + termEntries.join('\n') : '';
301
- langSections.push({ lang, termEntries, blocks: [defBlock, noteBlock, sourceBlock, statusBlock].filter(b => b).join('') });
302
- }
303
-
304
- if (!langSections.length) return '';
305
-
306
- const bodyEntries = langSections.map(ls => {
307
- return ` <languageSection xml:lang="${ls.lang}">${ls.blocks}\n </languageSection>`;
308
- }).join('\n');
309
-
310
- return `<?xml version="1.0" encoding="UTF-8"?>
311
- <tbx style="dca" type="TBX-Basic" xml:lang="en" xmlns="urn:iso:std:iso:30042:ed-2">
312
- <tbxHeader>
313
- <fileDesc>
314
- <source>${escapeXml(uri)}</source>
315
- </fileDesc>
316
- </tbxHeader>
317
- <text>
318
- <body>
319
- <conceptEntry id="${escapeXml(id)}">
320
- ${bodyEntries}
321
- </conceptEntry>
322
- </body>
323
- </text>
324
- </tbx>
325
- `;
326
- }
@@ -1,111 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { FORMAT_REGISTRY, conceptToTurtle, conceptToSkosJsonLd } from '../utils/concept-formats';
3
- import { Concept } from 'glossarist';
4
-
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: {
10
- eng: {
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' },
16
- ],
17
- definition: [{ content: 'a definition' }],
18
- notes: [{ content: 'a note' }],
19
- },
20
- deu: {
21
- language_code: 'deu',
22
- entry_status: 'valid',
23
- terms: [
24
- { type: 'expression', designation: 'Testbegriff', normative_status: 'preferred' },
25
- ],
26
- definition: [{ content: 'eine Definition' }],
27
- },
28
- },
29
- ...overrides,
30
- });
31
- }
32
-
33
- describe('FORMAT_REGISTRY', () => {
34
- it('has ttl, jsonld, yaml entries', () => {
35
- expect(FORMAT_REGISTRY.ttl).toBeDefined();
36
- expect(FORMAT_REGISTRY.jsonld).toBeDefined();
37
- expect(FORMAT_REGISTRY.yaml).toBeDefined();
38
- });
39
-
40
- it('each entry has extension, label, mediaType', () => {
41
- for (const [, desc] of Object.entries(FORMAT_REGISTRY)) {
42
- expect(desc.extension).toBeTruthy();
43
- expect(desc.label).toBeTruthy();
44
- expect(desc.mediaType).toBeTruthy();
45
- }
46
- });
47
- });
48
-
49
- describe('conceptToTurtle', () => {
50
- it('generates valid Turtle with SKOS predicates', () => {
51
- const ttl = conceptToTurtle(makeConcept());
52
- expect(ttl).toContain('@prefix skos:');
53
- expect(ttl).toContain('a skos:Concept');
54
- expect(ttl).toContain('skos:prefLabel "test term"@eng');
55
- expect(ttl).toContain('skos:altLabel "alt term"@eng');
56
- expect(ttl).toContain('skos:prefLabel "Testbegriff"@deu');
57
- expect(ttl).toContain('skos:definition "a definition"@eng');
58
- expect(ttl).toContain('skos:scopeNote "a note"@eng');
59
- expect(ttl).toContain('skos:notation "1"');
60
- });
61
-
62
- it('escapes special characters in Turtle', () => {
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
- });
74
- const ttl = conceptToTurtle(concept);
75
- expect(ttl).toContain('\\"quotes\\"');
76
- expect(ttl).toContain('\\\\backslash');
77
- });
78
-
79
- it('handles empty concept gracefully', () => {
80
- const ttl = conceptToTurtle(Concept.fromJSON({}));
81
- expect(ttl).toContain('a skos:Concept');
82
- expect(ttl).toContain('skos:notation ""');
83
- });
84
- });
85
-
86
- describe('conceptToSkosJsonLd', () => {
87
- it('generates SKOS JSON-LD with language maps', () => {
88
- const jsonld = conceptToSkosJsonLd(makeConcept());
89
- const parsed = JSON.parse(jsonld);
90
-
91
- expect(parsed['@type']).toBe('skos:Concept');
92
- expect(parsed['@id']).toBe('https://glossarist.org/test/concept/1');
93
- expect(parsed['skos:notation']).toBe('1');
94
- expect(parsed['skos:prefLabel']).toEqual({ eng: 'test term', deu: 'Testbegriff' });
95
- expect(parsed['skos:altLabel']).toEqual({ eng: 'alt term' });
96
- expect(parsed['skos:definition']).toEqual({ eng: 'a definition', deu: 'eine Definition' });
97
- });
98
-
99
- it('uses @language container in context', () => {
100
- const jsonld = conceptToSkosJsonLd(makeConcept());
101
- const parsed = JSON.parse(jsonld);
102
- expect(parsed['@context']['@language']).toEqual({ '@container': '@language' });
103
- });
104
-
105
- it('omits empty language maps', () => {
106
- const concept = Concept.fromJSON({ id: '1', uri: 'https://glossarist.org/test/concept/1' });
107
- const parsed = JSON.parse(conceptToSkosJsonLd(concept));
108
- expect(parsed['skos:prefLabel']).toBeUndefined();
109
- expect(parsed['skos:definition']).toBeUndefined();
110
- });
111
- });
@@ -1,123 +0,0 @@
1
- import type { Concept } from 'glossarist';
2
-
3
- export interface FormatDescriptor {
4
- extension: string;
5
- label: string;
6
- mediaType: string;
7
- }
8
-
9
- export const FORMAT_REGISTRY: Record<string, FormatDescriptor> = {
10
- ttl: { extension: 'ttl', label: 'Turtle RDF', mediaType: 'text/turtle' },
11
- jsonld: { extension: 'jsonld', label: 'JSON-LD', mediaType: 'application/ld+json' },
12
- tbx: { extension: 'tbx', label: 'TBX-XML', mediaType: 'application/xml' },
13
- yaml: { extension: 'yaml', label: 'YAML', mediaType: 'text/yaml' },
14
- };
15
-
16
- function getLocalizedData(concept: Concept) {
17
- const result: Record<string, {
18
- prefLabels: string[];
19
- altLabels: string[];
20
- definitions: string[];
21
- notes: string[];
22
- }> = {};
23
-
24
- for (const lang of concept.languages) {
25
- const lc = concept.localization(lang);
26
- if (!lc) continue;
27
-
28
- const prefLabels = lc.terms
29
- .filter(d => d.normativeStatus === 'preferred' && d.designation)
30
- .map(d => d.designation);
31
- const altLabels = lc.terms
32
- .filter(d => d.normativeStatus !== 'preferred' && d.designation)
33
- .map(d => d.designation);
34
- const definitions = lc.definitions
35
- .map(d => d.content || '')
36
- .filter(Boolean);
37
- const notes = lc.notes
38
- .map(d => d.content || '')
39
- .filter(Boolean);
40
-
41
- if (prefLabels.length || definitions.length) {
42
- result[lang] = { prefLabels, altLabels, definitions, notes };
43
- }
44
- }
45
-
46
- return result;
47
- }
48
-
49
- function escapeTurtle(s: string): string {
50
- return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
51
- }
52
-
53
- export function conceptToTurtle(concept: Concept): string {
54
- const uri = concept.uri || '';
55
- const id = concept.id;
56
- const data = getLocalizedData(concept);
57
-
58
- const lines: string[] = [
59
- '@prefix skos: <http://www.w3.org/2004/02/skos/core#> .',
60
- '@prefix dcterms: <http://purl.org/dc/terms/> .',
61
- '',
62
- ];
63
-
64
- const props: string[] = ['a skos:Concept'];
65
- props.push(` skos:notation "${escapeTurtle(id)}"`);
66
-
67
- for (const [lang, d] of Object.entries(data)) {
68
- for (const label of d.prefLabels) {
69
- props.push(` skos:prefLabel "${escapeTurtle(label)}"@${lang}`);
70
- }
71
- for (const label of d.altLabels) {
72
- props.push(` skos:altLabel "${escapeTurtle(label)}"@${lang}`);
73
- }
74
- for (const def of d.definitions) {
75
- props.push(` skos:definition "${escapeTurtle(def)}"@${lang}`);
76
- }
77
- for (const note of d.notes) {
78
- props.push(` skos:scopeNote "${escapeTurtle(note)}"@${lang}`);
79
- }
80
- }
81
-
82
- lines.push(`<${uri}>`);
83
- lines.push(props.join(' ;\n'));
84
- lines.push(' .');
85
-
86
- return lines.join('\n');
87
- }
88
-
89
- export function conceptToSkosJsonLd(concept: Concept): string {
90
- const uri = concept.uri || '';
91
- const id = concept.id;
92
- const data = getLocalizedData(concept);
93
-
94
- const doc: Record<string, any> = {
95
- '@context': {
96
- skos: 'http://www.w3.org/2004/02/skos/core#',
97
- dcterms: 'http://purl.org/dc/terms/',
98
- '@language': { '@container': '@language' },
99
- },
100
- '@id': uri,
101
- '@type': 'skos:Concept',
102
- 'skos:notation': id,
103
- };
104
-
105
- const prefLabels: Record<string, string> = {};
106
- const altLabels: Record<string, string> = {};
107
- const definitions: Record<string, string> = {};
108
- const scopeNotes: Record<string, string> = {};
109
-
110
- for (const [lang, d] of Object.entries(data)) {
111
- if (d.prefLabels[0]) prefLabels[lang] = d.prefLabels[0];
112
- if (d.altLabels[0]) altLabels[lang] = d.altLabels[0];
113
- if (d.definitions[0]) definitions[lang] = d.definitions[0];
114
- if (d.notes[0]) scopeNotes[lang] = d.notes[0];
115
- }
116
-
117
- if (Object.keys(prefLabels).length) doc['skos:prefLabel'] = prefLabels;
118
- if (Object.keys(altLabels).length) doc['skos:altLabel'] = altLabels;
119
- if (Object.keys(definitions).length) doc['skos:definition'] = definitions;
120
- if (Object.keys(scopeNotes).length) doc['skos:scopeNote'] = scopeNotes;
121
-
122
- return JSON.stringify(doc, null, 2);
123
- }