@glossarist/concept-browser 0.7.134 → 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.
- package/README.md +20 -0
- package/data/concept-model/SOURCE.json +2 -3
- package/data/concept-model/glossarist.context.jsonld +15 -0
- package/data/concept-model/glossarist.ttl +86 -1
- package/data/concept-model/shapes/glossarist.shacl.ttl +169 -1
- package/package.json +2 -2
- package/scripts/__tests__/doctor.test.ts +45 -5
- package/scripts/doctor.js +64 -34
- package/scripts/doctor.ts +83 -35
- package/scripts/fetch-datasets.js +1 -0
- package/scripts/generate-data.js +73 -231
- package/scripts/generate-data.ts +94 -41
- package/scripts/lib/concept-uri.ts +11 -0
- package/scripts/load-site-config.js +1 -0
- package/scripts/load-site-config.ts +1 -0
- package/scripts/process-about-pages.js +1 -0
- package/src/__tests__/scripts/concept-csv.test.ts +106 -0
- package/src/composables/use-format-registry.ts +1 -0
- package/src/config/types.ts +1 -0
- package/src/data/ontology-schema.json +374 -8
- package/scripts/lib/concept-formats.ts +0 -206
- package/src/__tests__/concept-formats.test.ts +0 -111
- package/src/utils/concept-formats.ts +0 -123
package/scripts/generate-data.ts
CHANGED
|
@@ -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 {
|
|
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,13 +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
|
-
} from './lib/concept-formats';
|
|
51
56
|
|
|
52
57
|
// MECE partitive multiplicity: 2 independent axes (ISO 704:2022).
|
|
53
58
|
// presence × count
|
|
@@ -689,9 +694,10 @@ async function processDataset(dir, register, opts) {
|
|
|
689
694
|
for (const f of fs.readdirSync(conceptsDir)) fs.unlinkSync(path.join(conceptsDir, f));
|
|
690
695
|
}
|
|
691
696
|
const concepts = [];
|
|
697
|
+
const conceptInstances: Concept[] = [];
|
|
692
698
|
const langTermCounts = {};
|
|
693
699
|
const langDefCounts = {};
|
|
694
|
-
const availableFormats = ['ttl', 'jsonld', 'yaml', 'tbx'];
|
|
700
|
+
const availableFormats = ['ttl', 'jsonld', 'yaml', 'tbx', 'csv'];
|
|
695
701
|
|
|
696
702
|
// Pre-scan: build designation → concept ID lookup for same-dataset designation refs
|
|
697
703
|
const designationLookup = new Map();
|
|
@@ -802,16 +808,34 @@ async function processDataset(dir, register, opts) {
|
|
|
802
808
|
const termid = String(conceptYaml.termid);
|
|
803
809
|
const jsonld = yamlToJsonLd(conceptYaml, register, dsRefMaps);
|
|
804
810
|
writeJson(path.join(conceptsDir, `${termid}.json`), jsonld);
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
+
}
|
|
815
839
|
}
|
|
816
840
|
|
|
817
841
|
fs.copyFileSync(path.join(dir, file), path.join(conceptsDir, `${termid}.yaml`));
|
|
@@ -831,6 +855,16 @@ async function processDataset(dir, register, opts) {
|
|
|
831
855
|
}
|
|
832
856
|
}
|
|
833
857
|
|
|
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!);
|
|
867
|
+
|
|
834
868
|
const CHUNK_SIZE = 500;
|
|
835
869
|
const chunks = [];
|
|
836
870
|
for (let i = 0; i < concepts.length; i += CHUNK_SIZE) {
|
|
@@ -877,7 +911,7 @@ async function processDataset(dir, register, opts) {
|
|
|
877
911
|
fs.writeFileSync(
|
|
878
912
|
path.join(DATA, register, 'graph-nodes.json'),
|
|
879
913
|
JSON.stringify({
|
|
880
|
-
uriPrefix:
|
|
914
|
+
uriPrefix: buildConceptUriPrefix(refMaps.uriBase, register),
|
|
881
915
|
registerId: register,
|
|
882
916
|
nodes: graphNodeEntries,
|
|
883
917
|
}),
|
|
@@ -908,29 +942,37 @@ async function processDataset(dir, register, opts) {
|
|
|
908
942
|
};
|
|
909
943
|
}
|
|
910
944
|
|
|
911
|
-
//
|
|
945
|
+
// Aggregate bulk exports offered on the dataset page: single-file
|
|
946
|
+
// distributions generated by this build plus any files shipped in the
|
|
947
|
+
// dataset's compiled/ directory. site-config `bulk_exports` restricts
|
|
948
|
+
// which formats are offered; absent/empty config offers all available.
|
|
949
|
+
const BULK_FORMAT_BY_EXT = {
|
|
950
|
+
'.ttl': 'turtle',
|
|
951
|
+
'.jsonld': 'jsonld',
|
|
952
|
+
'.xml': 'tbx',
|
|
953
|
+
'.jsonl': 'jsonl',
|
|
954
|
+
'.yaml': 'yaml',
|
|
955
|
+
'.csv': 'csv',
|
|
956
|
+
};
|
|
957
|
+
const bulkFormats = [];
|
|
958
|
+
const addBulkFormat = file => {
|
|
959
|
+
const dest = path.join(DATA, register, file);
|
|
960
|
+
if (!fs.existsSync(dest) || bulkFormats.some(f => f.file === file)) return;
|
|
961
|
+
const formatName = BULK_FORMAT_BY_EXT[path.extname(file)] || path.extname(file).slice(1);
|
|
962
|
+
bulkFormats.push({ file, format: formatName, size: fs.statSync(dest).size });
|
|
963
|
+
};
|
|
964
|
+
for (const file of [`${register}.csv`, `${register}.jsonld`]) addBulkFormat(file);
|
|
965
|
+
|
|
912
966
|
const sourceRoot = path.dirname(dir);
|
|
913
967
|
const compiledDir = path.join(sourceRoot, 'compiled');
|
|
914
|
-
const bulkFormats = [];
|
|
915
968
|
if (fs.existsSync(compiledDir)) {
|
|
969
|
+
let copied = 0;
|
|
916
970
|
for (const file of fs.readdirSync(compiledDir)) {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const ext = path.extname(file);
|
|
921
|
-
const formatMap = {
|
|
922
|
-
'.ttl': 'turtle',
|
|
923
|
-
'.jsonld': 'jsonld',
|
|
924
|
-
'.xml': 'tbx',
|
|
925
|
-
'.jsonl': 'jsonl',
|
|
926
|
-
'.yaml': 'yaml',
|
|
927
|
-
};
|
|
928
|
-
const formatName = formatMap[ext] || ext.slice(1);
|
|
929
|
-
bulkFormats.push({ file, format: formatName, size: fs.statSync(src).size });
|
|
930
|
-
}
|
|
931
|
-
if (bulkFormats.length) {
|
|
932
|
-
console.log(` Copied ${bulkFormats.length} bulk format files`);
|
|
971
|
+
fs.copyFileSync(path.join(compiledDir, file), path.join(DATA, register, file));
|
|
972
|
+
addBulkFormat(file);
|
|
973
|
+
copied += 1;
|
|
933
974
|
}
|
|
975
|
+
if (copied) console.log(` Copied ${copied} bulk format files from compiled/`);
|
|
934
976
|
}
|
|
935
977
|
|
|
936
978
|
const sourceStats = Array.from(stats.sourceMap.values())
|
|
@@ -977,7 +1019,6 @@ async function processDataset(dir, register, opts) {
|
|
|
977
1019
|
year: opts.year ?? undefined,
|
|
978
1020
|
languageStats: langStats,
|
|
979
1021
|
availableFormats,
|
|
980
|
-
bulkFormats,
|
|
981
1022
|
hasBibliography: opts.hasBibliography,
|
|
982
1023
|
hasImages: opts.hasImages,
|
|
983
1024
|
};
|
|
@@ -987,10 +1028,16 @@ async function processDataset(dir, register, opts) {
|
|
|
987
1028
|
if (opts.status) manifest.editionStatus = opts.status;
|
|
988
1029
|
if (opts.ordering) manifest.ordering = opts.ordering;
|
|
989
1030
|
if (opts.sections && opts.sections.length > 0) manifest.sections = opts.sections;
|
|
990
|
-
writeJson(path.join(DATA, register, 'manifest.json'), manifest);
|
|
991
1031
|
|
|
992
1032
|
// Dataset-level RDF (WS J2/J5): dcat:Dataset + skos:ConceptScheme + skos:Collection per section.
|
|
1033
|
+
// Runs before the manifest write so the ttl distribution is on disk when
|
|
1034
|
+
// bulkFormats is assembled.
|
|
993
1035
|
await writeDatasetRdf(register, manifest, concepts, refMaps, opts);
|
|
1036
|
+
addBulkFormat(`${register}.ttl`);
|
|
1037
|
+
manifest.bulkFormats = opts.bulkExports?.length
|
|
1038
|
+
? bulkFormats.filter(f => opts.bulkExports.includes(f.format))
|
|
1039
|
+
: bulkFormats;
|
|
1040
|
+
writeJson(path.join(DATA, register, 'manifest.json'), manifest);
|
|
994
1041
|
|
|
995
1042
|
// Copy bibliography.yaml → bibliography.json
|
|
996
1043
|
// ALWAYS emit bibliography.json — empty stub `{ bibliography: [] }` when no
|
|
@@ -1058,6 +1105,11 @@ for (const ds of config.datasets) {
|
|
|
1058
1105
|
|
|
1059
1106
|
const refMaps = buildRefMaps(config, registerCache);
|
|
1060
1107
|
|
|
1108
|
+
// site-config `bulk_exports`: which aggregate formats the dataset pages
|
|
1109
|
+
// offer for download. 'ttl' is accepted as an alias of the canonical
|
|
1110
|
+
// format id 'turtle'. Absent/empty → offer all available formats.
|
|
1111
|
+
const bulkExports = (config.bulkExports || []).map(f => (f === 'ttl' ? 'turtle' : f));
|
|
1112
|
+
|
|
1061
1113
|
for (let i = 0; i < config.datasets.length; i++) {
|
|
1062
1114
|
const ds = config.datasets[i];
|
|
1063
1115
|
|
|
@@ -1110,6 +1162,7 @@ for (let i = 0; i < config.datasets.length; i++) {
|
|
|
1110
1162
|
sections: reg?.sections ? reg.sections.map(s => s.toJSON()) : [],
|
|
1111
1163
|
hasBibliography: fs.existsSync(path.join(datasetDir(ds), 'bibliography.yaml')),
|
|
1112
1164
|
hasImages: fs.existsSync(path.join(datasetDir(ds), 'images')),
|
|
1165
|
+
bulkExports,
|
|
1113
1166
|
});
|
|
1114
1167
|
const resolvedOwner = firstNonEmpty(reg?.owner, ds.owner) ?? '';
|
|
1115
1168
|
const resolvedTags = firstNonEmpty(reg?.tags, ds.tags) ?? [];
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { Concept, CSV_COLUMNS, conceptsToCsv } from 'glossarist';
|
|
9
|
+
|
|
10
|
+
function parseCsv(csv: string): string[][] {
|
|
11
|
+
const body = csv.replace(/^/, '');
|
|
12
|
+
const rows: string[][] = [];
|
|
13
|
+
let row: string[] = [];
|
|
14
|
+
let field = '';
|
|
15
|
+
let quoted = false;
|
|
16
|
+
for (let i = 0; i < body.length; i++) {
|
|
17
|
+
const ch = body[i];
|
|
18
|
+
if (quoted) {
|
|
19
|
+
if (ch === '"') {
|
|
20
|
+
if (body[i + 1] === '"') { field += '"'; i++; }
|
|
21
|
+
else quoted = false;
|
|
22
|
+
} else {
|
|
23
|
+
field += ch;
|
|
24
|
+
}
|
|
25
|
+
} else if (ch === '"') {
|
|
26
|
+
quoted = true;
|
|
27
|
+
} else if (ch === ',') {
|
|
28
|
+
row.push(field); field = '';
|
|
29
|
+
} else if (ch === '\r' && body[i + 1] === '\n') {
|
|
30
|
+
row.push(field); rows.push(row); row = []; field = ''; i++;
|
|
31
|
+
} else {
|
|
32
|
+
field += ch;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return rows;
|
|
36
|
+
}
|
|
37
|
+
|
|
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: {
|
|
46
|
+
eng: {
|
|
47
|
+
terms: [
|
|
48
|
+
{ designation: 'Buoyage and beaconage', normative_status: 'preferred' },
|
|
49
|
+
{ designation: 'Sea-marking (G.B.)', normative_status: 'admitted' },
|
|
50
|
+
],
|
|
51
|
+
definition: [{ content: 'Ensemble of beacons, buoys, seamarks and small lights.' }],
|
|
52
|
+
notes: [{ content: 'Note one.' }, { content: 'Note two.' }],
|
|
53
|
+
},
|
|
54
|
+
fra: {
|
|
55
|
+
terms: [{ designation: 'Balisage', normative_status: 'preferred' }],
|
|
56
|
+
definition: [{ content: 'Ensemble de balises.' }],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('conceptsToCsv (glossarist output API)', () => {
|
|
62
|
+
it('emits UTF-8 BOM, CRLF line endings, and the model-driven header', () => {
|
|
63
|
+
const csv = conceptsToCsv([baseConcept()]);
|
|
64
|
+
expect(csv.startsWith('')).toBe(true);
|
|
65
|
+
expect(csv.endsWith('\r\n')).toBe(true);
|
|
66
|
+
const rows = parseCsv(csv);
|
|
67
|
+
expect(rows[0]!.join(',')).toBe(CSV_COLUMNS.join(','));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('one row per concept × language; preferred designation first', () => {
|
|
71
|
+
const rows = parseCsv(conceptsToCsv([baseConcept()]));
|
|
72
|
+
expect(rows).toHaveLength(3);
|
|
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.)');
|
|
78
|
+
});
|
|
79
|
+
|
|
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.');
|
|
85
|
+
});
|
|
86
|
+
|
|
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');
|
|
91
|
+
});
|
|
92
|
+
|
|
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');
|
|
97
|
+
});
|
|
98
|
+
|
|
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' });
|
|
101
|
+
const rows = parseCsv(conceptsToCsv([bare]));
|
|
102
|
+
expect(rows).toHaveLength(2);
|
|
103
|
+
expect(rows[1]![0]).toBe('222-02-02');
|
|
104
|
+
expect(rows[1]![4]).toBe('');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -40,3 +40,4 @@ registerFormat({ id: 'jsonld', extension: 'jsonld', mediaType: 'application/ld+
|
|
|
40
40
|
registerFormat({ id: 'yaml', extension: 'yaml', mediaType: 'application/yaml', label: 'YAML', available: 'per-concept', serialize: 'build' });
|
|
41
41
|
registerFormat({ id: 'tbx', extension: 'tbx.xml', mediaType: 'application/x-tbx', label: 'TBX', available: 'aggregate', serialize: 'build' });
|
|
42
42
|
registerFormat({ id: 'jsonl', extension: 'jsonl', mediaType: 'application/jsonl+json', label: 'JSON-Lines', available: 'aggregate', serialize: 'build' });
|
|
43
|
+
registerFormat({ id: 'csv', extension: 'csv', mediaType: 'text/csv', label: 'CSV', available: 'aggregate', serialize: 'build' });
|