@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,7 +1,19 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import yaml from 'js-yaml';
4
- import { naturalSort, Register, parseMention, parseMentions, InvalidMentionError } from 'glossarist';
4
+ import {
5
+ naturalSort,
6
+ Register,
7
+ parseMention,
8
+ parseMentions,
9
+ InvalidMentionError,
10
+ parseConceptYaml,
11
+ conceptToTurtle,
12
+ conceptsToTbx,
13
+ conceptsToCsv,
14
+ emitOutputSet,
15
+ type Concept,
16
+ } from 'glossarist';
5
17
  import { loadSiteConfig } from './load-site-config';
6
18
  import { getGroups } from './lib/concept-groups';
7
19
  import { consumeDatasetEntities } from './lib/build/non-verbal-consumer';
@@ -23,7 +35,7 @@ import type {
23
35
  YamlNewsFrontmatter,
24
36
  YamlContentPage,
25
37
  } from './lib/yaml-types';
26
- import { buildConceptUri } from './lib/concept-uri';
38
+ import { buildConceptUri, buildConceptUriPrefix } from './lib/concept-uri';
27
39
  import {
28
40
  loadConceptFile,
29
41
  readYaml,
@@ -41,15 +53,6 @@ import {
41
53
  refsToJsonLd,
42
54
  citationToJsonLd,
43
55
  } from './lib/jsonld-emitter';
44
- import {
45
- escapeTurtle,
46
- escapeXml,
47
- conceptJsonToTurtle,
48
- conceptJsonToSkosJsonLd,
49
- conceptJsonToTbx,
50
- conceptsToCsv,
51
- conceptsToSkosJsonLdGraph,
52
- } from './lib/concept-formats';
53
56
 
54
57
  // MECE partitive multiplicity: 2 independent axes (ISO 704:2022).
55
58
  // presence × count
@@ -691,7 +694,7 @@ async function processDataset(dir, register, opts) {
691
694
  for (const f of fs.readdirSync(conceptsDir)) fs.unlinkSync(path.join(conceptsDir, f));
692
695
  }
693
696
  const concepts = [];
694
- const jsonlds = [];
697
+ const conceptInstances: Concept[] = [];
695
698
  const langTermCounts = {};
696
699
  const langDefCounts = {};
697
700
  const availableFormats = ['ttl', 'jsonld', 'yaml', 'tbx', 'csv'];
@@ -805,17 +808,34 @@ async function processDataset(dir, register, opts) {
805
808
  const termid = String(conceptYaml.termid);
806
809
  const jsonld = yamlToJsonLd(conceptYaml, register, dsRefMaps);
807
810
  writeJson(path.join(conceptsDir, `${termid}.json`), jsonld);
808
- jsonlds.push(jsonld);
809
-
810
- const ttlContent = conceptJsonToTurtle(jsonld);
811
- fs.writeFileSync(path.join(conceptsDir, `${termid}.ttl`), ttlContent);
812
-
813
- const skosJsonLd = conceptJsonToSkosJsonLd(jsonld);
814
- fs.writeFileSync(path.join(conceptsDir, `${termid}.jsonld`), skosJsonLd);
815
-
816
- const tbxContent = conceptJsonToTbx(jsonld);
817
- if (tbxContent) {
818
- fs.writeFileSync(path.join(conceptsDir, `${termid}.tbx`), tbxContent);
811
+ // Canonical interchange exports — routed through glossarist's
812
+ // output API (one model walk; predicates generated from the
813
+ // concept-model context, wire shape from schemas/v3). The hand-rolled
814
+ // gl:-shaped emitters in scripts/lib/concept-formats.ts are retired.
815
+ // Note: TBX now includes <term> entries — the retired emitter
816
+ // computed termEntries but never wrote them into the document.
817
+ let conceptInstance: Concept | null = null;
818
+ try {
819
+ conceptInstance = parseConceptYaml(fs.readFileSync(path.join(dir, file), 'utf8'), termid);
820
+ } catch (e) {
821
+ console.warn(` ${termid}: canonical parse failed (${e.message}); skipping glossarist exports`);
822
+ }
823
+ if (conceptInstance) {
824
+ conceptInstances.push(conceptInstance);
825
+ fs.writeFileSync(
826
+ path.join(conceptsDir, `${termid}.ttl`),
827
+ conceptToTurtle(conceptInstance, { uriBase: refMaps.uriBase, registerId: register }),
828
+ );
829
+ const conceptJsonLd = await emitOutputSet([conceptInstance], {
830
+ registerId: register,
831
+ uriBase: refMaps.uriBase,
832
+ formats: ['jsonld'],
833
+ });
834
+ fs.writeFileSync(path.join(conceptsDir, `${termid}.jsonld`), conceptJsonLd.jsonld!);
835
+ const tbxContent = conceptsToTbx([conceptInstance], { registerId: register });
836
+ if (tbxContent) {
837
+ fs.writeFileSync(path.join(conceptsDir, `${termid}.tbx`), tbxContent);
838
+ }
819
839
  }
820
840
 
821
841
  fs.copyFileSync(path.join(dir, file), path.join(conceptsDir, `${termid}.yaml`));
@@ -835,9 +855,15 @@ async function processDataset(dir, register, opts) {
835
855
  }
836
856
  }
837
857
 
838
- // Aggregate distributions: single-file exports of the whole register
839
- fs.writeFileSync(path.join(DATA, register, `${register}.csv`), conceptsToCsv(jsonlds, { languageOrder: opts.languageOrder || opts.languages }));
840
- fs.writeFileSync(path.join(DATA, register, `${register}.jsonld`), conceptsToSkosJsonLdGraph(jsonlds));
858
+ // Aggregate distributions: single-file exports of the whole register,
859
+ // emitted by glossarist's output-set API from the canonical model walk.
860
+ const aggregate = await emitOutputSet(conceptInstances, {
861
+ registerId: register,
862
+ uriBase: refMaps.uriBase,
863
+ languageOrder: opts.languageOrder || opts.languages,
864
+ });
865
+ fs.writeFileSync(path.join(DATA, register, `${register}.csv`), aggregate.csv!);
866
+ fs.writeFileSync(path.join(DATA, register, `${register}.jsonld`), aggregate.jsonld!);
841
867
 
842
868
  const CHUNK_SIZE = 500;
843
869
  const chunks = [];
@@ -885,7 +911,7 @@ async function processDataset(dir, register, opts) {
885
911
  fs.writeFileSync(
886
912
  path.join(DATA, register, 'graph-nodes.json'),
887
913
  JSON.stringify({
888
- uriPrefix: `${refMaps.uriBase}/${register}/concept/`,
914
+ uriPrefix: buildConceptUriPrefix(refMaps.uriBase, register),
889
915
  registerId: register,
890
916
  nodes: graphNodeEntries,
891
917
  }),
@@ -15,3 +15,14 @@
15
15
  export function buildConceptUri(uriBase: string, registerId: string, conceptId: string | number): string {
16
16
  return `${uriBase}/dataset/${registerId}/concept/${conceptId}`;
17
17
  }
18
+
19
+ /** Prefix of every concept URI of a register (for keying node maps and
20
+ * matching edge endpoints against a register's URI space). */
21
+ export function buildConceptUriPrefix(uriBase: string, registerId: string): string {
22
+ return `${uriBase}/dataset/${registerId}/concept/`;
23
+ }
24
+
25
+ /** Prefix of any URI belonging to a register (for the cross-ref index). */
26
+ export function buildDatasetUriPrefix(uriBase: string, registerId: string): string {
27
+ return `${uriBase}/dataset/${registerId}/`;
28
+ }
@@ -1,8 +1,14 @@
1
+ // CSV aggregate export — now routed through glossarist's canonical
2
+ // output API (glossarist@0.4.58, glossarist-js#132). These specs pin the
3
+ // production contract that shipped in concept-browser#214: column order,
4
+ // preferred/alt split, RFC 4180 quoting, BOM + CRLF, languageOrder,
5
+ // and no-drop rows for concepts without localizations.
6
+
1
7
  import { describe, it, expect } from 'vitest';
2
- import { conceptsToCsv, conceptsToSkosJsonLdGraph, CSV_COLUMNS } from '../../../scripts/lib/concept-formats';
8
+ import { Concept, CSV_COLUMNS, conceptsToCsv } from 'glossarist';
3
9
 
4
10
  function parseCsv(csv: string): string[][] {
5
- const body = csv.replace(/^\uFEFF/, '');
11
+ const body = csv.replace(/^/, '');
6
12
  const rows: string[][] = [];
7
13
  let row: string[] = [];
8
14
  let field = '';
@@ -29,114 +35,72 @@ function parseCsv(csv: string): string[][] {
29
35
  return rows;
30
36
  }
31
37
 
32
- const baseConcept = {
33
- '@id': 'https://example.com/vocab/concept/1-1-010',
34
- 'gl:identifier': '1-1-010',
35
- 'gl:status': 'valid',
36
- 'gl:domain': [{ 'gl:conceptId': 'section-1' }],
37
- 'gl:source': [{
38
- 'gl:sourceType': 'authoritative',
39
- 'gl:origin': { 'gl:ref': { 'gl:source': 'IALA Dictionary' }, 'gl:link': 'https://example.com/source' },
40
- }],
41
- 'gl:localizedConcept': {
38
+ const baseConcept = () => new Concept({
39
+ id: '1-1-010',
40
+ termid: '1-1-010',
41
+ status: 'valid',
42
+ uri: 'https://example.com/vocab/concept/1-1-010',
43
+ domains: [{ conceptId: 'section-1' }],
44
+ sources: [{ type: 'authoritative', origin: { ref: { source: 'IALA Dictionary' }, link: 'https://example.com/source' } }],
45
+ localizations: {
42
46
  eng: {
43
- 'gl:designation': [
44
- { 'gl:normativeStatus': 'preferred', 'gl:term': 'Buoyage and beaconage' },
45
- { 'gl:normativeStatus': 'admitted', 'gl:term': 'Sea-marking (G.B.)' },
47
+ terms: [
48
+ { designation: 'Buoyage and beaconage', normative_status: 'preferred' },
49
+ { designation: 'Sea-marking (G.B.)', normative_status: 'admitted' },
46
50
  ],
47
- 'gl:definition': [{ 'gl:content': 'Ensemble of beacons, buoys, seamarks and small lights.' }],
48
- 'gl:notes': [{ 'gl:content': 'Note one.' }, { 'gl:content': 'Note two.' }],
51
+ definition: [{ content: 'Ensemble of beacons, buoys, seamarks and small lights.' }],
52
+ notes: [{ content: 'Note one.' }, { content: 'Note two.' }],
49
53
  },
50
54
  fra: {
51
- 'gl:designation': [{ 'gl:normativeStatus': 'preferred', 'gl:term': 'Balisage' }],
52
- 'gl:definition': [{ 'gl:content': 'Ensemble de balises.' }],
55
+ terms: [{ designation: 'Balisage', normative_status: 'preferred' }],
56
+ definition: [{ content: 'Ensemble de balises.' }],
53
57
  },
54
58
  },
55
- };
59
+ });
56
60
 
57
- describe('conceptsToCsv', () => {
61
+ describe('conceptsToCsv (glossarist output API)', () => {
58
62
  it('emits UTF-8 BOM, CRLF line endings, and the model-driven header', () => {
59
- const csv = conceptsToCsv([baseConcept]);
60
- expect(csv.startsWith('\uFEFF')).toBe(true);
63
+ const csv = conceptsToCsv([baseConcept()]);
64
+ expect(csv.startsWith('')).toBe(true);
61
65
  expect(csv.endsWith('\r\n')).toBe(true);
62
- expect(csv).toContain('\r\n');
63
66
  const rows = parseCsv(csv);
64
- expect(rows[0]).toEqual([...CSV_COLUMNS]);
67
+ expect(rows[0]!.join(',')).toBe(CSV_COLUMNS.join(','));
65
68
  });
66
69
 
67
- it('emits one row per concept and language', () => {
68
- const rows = parseCsv(conceptsToCsv([baseConcept]));
70
+ it('one row per concept × language; preferred designation first', () => {
71
+ const rows = parseCsv(conceptsToCsv([baseConcept()]));
69
72
  expect(rows).toHaveLength(3);
70
- expect(rows[1][CSV_COLUMNS.indexOf('termid')]).toBe('1-1-010');
71
- expect(rows[1][CSV_COLUMNS.indexOf('language')]).toBe('eng');
72
- expect(rows[2][CSV_COLUMNS.indexOf('language')]).toBe('fra');
73
- });
74
-
75
- it('honors languageOrder from the deployment config', () => {
76
- const rows = parseCsv(conceptsToCsv([baseConcept], { languageOrder: ['fra', 'eng'] }));
77
- expect(rows[1][CSV_COLUMNS.indexOf('language')]).toBe('fra');
78
- expect(rows[2][CSV_COLUMNS.indexOf('language')]).toBe('eng');
79
- });
80
-
81
- it('keeps languages missing from languageOrder at the end', () => {
82
- const rows = parseCsv(conceptsToCsv([baseConcept], { languageOrder: ['deu', 'eng'] }));
83
- expect(rows.slice(1).map(r => r[CSV_COLUMNS.indexOf('language')])).toEqual(['eng', 'fra']);
84
- });
85
-
86
- it('separates the preferred term from alt terms', () => {
87
- const rows = parseCsv(conceptsToCsv([baseConcept]));
88
- const eng = rows[1];
89
- expect(eng[CSV_COLUMNS.indexOf('term')]).toBe('Buoyage and beaconage');
90
- expect(eng[CSV_COLUMNS.indexOf('alt_terms')]).toBe('Sea-marking (G.B.)');
73
+ const eng = rows[1]!;
74
+ expect(eng[0]).toBe('1-1-010');
75
+ expect(eng[4]).toBe('eng');
76
+ expect(eng[5]).toBe('Buoyage and beaconage');
77
+ expect(eng[6]).toBe('Sea-marking (G.B.)');
91
78
  });
92
79
 
93
- it('joins multi-entry notes with a newline inside a quoted cell', () => {
94
- const rows = parseCsv(conceptsToCsv([baseConcept]));
95
- expect(rows[1][CSV_COLUMNS.indexOf('notes')]).toBe('Note one.\nNote two.');
80
+ it('definition/notes columns carry the full content', () => {
81
+ const rows = parseCsv(conceptsToCsv([baseConcept()]));
82
+ expect(rows[1]![7]).toBe('Ensemble of beacons, buoys, seamarks and small lights.');
83
+ expect(rows[1]![8]).toBe('Note one.\nNote two.');
84
+ expect(rows[2]![7]).toBe('Ensemble de balises.');
96
85
  });
97
86
 
98
- it('escapes quotes and commas per RFC 4180', () => {
99
- const concept = JSON.parse(JSON.stringify(baseConcept));
100
- concept['gl:localizedConcept'].eng['gl:definition'] = [{ 'gl:content': 'Says "hello", loudly' }];
101
- const csv = conceptsToCsv([concept]);
102
- expect(csv).toContain('"Says ""hello"", loudly"');
103
- expect(parseCsv(csv)[1][CSV_COLUMNS.indexOf('definition')]).toBe('Says "hello", loudly');
87
+ it('sources and source_links from origin ref/link', () => {
88
+ const rows = parseCsv(conceptsToCsv([baseConcept()]));
89
+ expect(rows[1]![10]).toBe('IALA Dictionary');
90
+ expect(rows[1]![11]).toBe('https://example.com/source');
104
91
  });
105
92
 
106
- it('falls back to managed sources when the localization has none', () => {
107
- const rows = parseCsv(conceptsToCsv([baseConcept]));
108
- for (const row of rows.slice(1)) {
109
- expect(row[CSV_COLUMNS.indexOf('sources')]).toBe('IALA Dictionary');
110
- expect(row[CSV_COLUMNS.indexOf('source_links')]).toBe('https://example.com/source');
111
- }
93
+ it('languageOrder reorders rows; unlisted languages append after', () => {
94
+ const rows = parseCsv(conceptsToCsv([baseConcept()], { languageOrder: ['fra', 'deu'] }));
95
+ expect(rows[1]![4]).toBe('fra');
96
+ expect(rows[2]![4]).toBe('eng');
112
97
  });
113
98
 
114
- it('still emits a row for concepts with no localizations', () => {
115
- const bare = {
116
- '@id': 'https://example.com/vocab/concept/9-9-999',
117
- 'gl:identifier': '9-9-999',
118
- 'gl:status': 'retired',
119
- };
99
+ it('concept without localizations still gets a row (no silent drops)', () => {
100
+ const bare = new Concept({ id: '222-02-02', termid: '222-02-02' });
120
101
  const rows = parseCsv(conceptsToCsv([bare]));
121
102
  expect(rows).toHaveLength(2);
122
- expect(rows[1][CSV_COLUMNS.indexOf('termid')]).toBe('9-9-999');
123
- expect(rows[1][CSV_COLUMNS.indexOf('language')]).toBe('');
124
- expect(rows[1][CSV_COLUMNS.indexOf('term')]).toBe('');
125
- });
126
-
127
- it('carries section domains and status columns', () => {
128
- const rows = parseCsv(conceptsToCsv([baseConcept]));
129
- expect(rows[1][CSV_COLUMNS.indexOf('section')]).toBe('section-1');
130
- expect(rows[1][CSV_COLUMNS.indexOf('status')]).toBe('valid');
131
- });
132
- });
133
-
134
- describe('conceptsToSkosJsonLdGraph', () => {
135
- it('wraps concepts in a shared @context and @graph', () => {
136
- const parsed = JSON.parse(conceptsToSkosJsonLdGraph([baseConcept]));
137
- expect(parsed['@context']['skos']).toBe('http://www.w3.org/2004/02/skos/core#');
138
- expect(parsed['@graph']).toHaveLength(1);
139
- expect(parsed['@graph'][0]['@id']).toBe(baseConcept['@id']);
140
- expect(parsed['@graph'][0]['skos:prefLabel']['eng']).toBe('Buoyage and beaconage');
103
+ expect(rows[1]![0]).toBe('222-02-02');
104
+ expect(rows[1]![4]).toBe('');
141
105
  });
142
106
  });