@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/scripts/doctor.ts CHANGED
@@ -22,7 +22,9 @@ export const NODE_MIN_MAJOR = 18;
22
22
  /**
23
23
  * @typedef {'pass'|'fail'|'warn'} CheckStatus
24
24
  * @typedef {{ id: string, label: string, status: CheckStatus, detail?: string, hint?: string }} CheckResult
25
- * @typedef {{ cwd: string, pkgRoot: string, datasetsYml?: any, datasetsYmlError?: string }} DoctorContext
25
+ * @typedef {{ id: string, localPath?: string }} RegisteredDataset
26
+ * @typedef {{ registered: RegisteredDataset[], source: 'datasets.yml'|'site-config.yml'|null, error?: string }} DatasetRegistry
27
+ * @typedef {{ cwd: string, pkgRoot: string, datasetsYml?: any, datasetsYmlError?: string, registry: DatasetRegistry }} DoctorContext
26
28
  */
27
29
 
28
30
  /**
@@ -63,8 +65,12 @@ function nodeVersionCheck() {
63
65
  }
64
66
 
65
67
  function packageDepsCheck(ctx) {
66
- const nm = resolve(ctx.pkgRoot, 'node_modules');
67
- if (!existsSync(nm)) {
68
+ // When installed as a library, the CLI's deps may be hoisted into the
69
+ // consumer's node_modules or nested under the package's own — accept
70
+ // either. Checking only PKG_ROOT (the installed package dir) reported
71
+ // false "node_modules missing" in consumer deployments.
72
+ const roots = [resolve(ctx.cwd, 'node_modules'), resolve(ctx.pkgRoot, 'node_modules')];
73
+ if (!roots.some((nm) => existsSync(nm))) {
68
74
  return {
69
75
  id: 'package-deps',
70
76
  label: 'Dependencies installed',
@@ -74,7 +80,7 @@ function packageDepsCheck(ctx) {
74
80
  };
75
81
  }
76
82
  const required = ['vue', 'vue-router', 'pinia', 'vite', 'n3', 'glossarist', '@rdfjs/dataset'];
77
- const missing = required.filter((p) => !existsSync(join(nm, p)));
83
+ const missing = required.filter((p) => !roots.some((nm) => existsSync(join(nm, p))));
78
84
  if (missing.length) {
79
85
  return {
80
86
  id: 'package-deps',
@@ -87,67 +93,103 @@ function packageDepsCheck(ctx) {
87
93
  return { id: 'package-deps', label: 'Dependencies installed', status: 'pass' };
88
94
  }
89
95
 
90
- function datasetsYmlCheck(ctx) {
91
- const path = resolve(ctx.cwd, 'datasets.yml');
92
- if (!existsSync(path)) {
93
- return {
94
- id: 'datasets-yml',
95
- label: 'datasets.yml present',
96
- status: 'warn',
97
- detail: 'file not found at project root',
98
- hint: 'datasets.yml registers every dataset the browser serves',
99
- };
96
+ /**
97
+ * Dataset registrations come from either `datasets.yml` (source repos to
98
+ * fetch) or `site-config.yml` (deployment sites, entries carry
99
+ * `local_path` to in-repo YAML). Doctor only knew the former, so
100
+ * site-config deployments reported "no datasets registered".
101
+ * @returns {{ registered: Array<{id: string, localPath?: string}>, source: 'datasets.yml'|'site-config.yml'|null, error?: string }}
102
+ */
103
+ export function resolveDatasetRegistry(cwd, fileError = undefined) {
104
+ if (fileError) return { registered: [], source: 'datasets.yml', error: fileError };
105
+ const ymlPath = resolve(cwd, 'datasets.yml');
106
+ if (existsSync(ymlPath)) {
107
+ const list = normalizeRegistry(yaml.load(readFileSync(ymlPath, 'utf8')));
108
+ return { registered: list, source: 'datasets.yml' };
109
+ }
110
+ const sitePath = resolve(cwd, 'site-config.yml');
111
+ if (existsSync(sitePath)) {
112
+ try {
113
+ const list = normalizeRegistry(yaml.load(readFileSync(sitePath, 'utf8')));
114
+ return { registered: list, source: 'site-config.yml' };
115
+ } catch (err) {
116
+ return { registered: [], source: 'site-config.yml', error: err.message };
117
+ }
100
118
  }
101
- if (ctx.datasetsYmlError) {
119
+ return { registered: [], source: null };
120
+ }
121
+
122
+ function normalizeRegistry(doc) {
123
+ const datasets = doc?.datasets;
124
+ if (!Array.isArray(datasets)) return [];
125
+ return datasets
126
+ .filter((d) => d && typeof d === 'object' && d.id)
127
+ .map((d) => ({ id: String(d.id), localPath: d.local_path ?? d.localPath }));
128
+ }
129
+
130
+ function datasetsRegisteredCheck(ctx) {
131
+ if (ctx.registry.error) {
102
132
  return {
103
- id: 'datasets-yml',
104
- label: 'datasets.yml parses',
133
+ id: 'datasets-registered',
134
+ label: `${ctx.registry.source} parses`,
105
135
  status: 'fail',
106
- detail: ctx.datasetsYmlError,
136
+ detail: ctx.registry.error,
107
137
  hint: 'fix the YAML syntax error',
108
138
  };
109
139
  }
110
- const datasets = ctx.datasetsYml?.datasets;
111
- if (!Array.isArray(datasets) || datasets.length === 0) {
140
+ if (!ctx.registry.source) {
112
141
  return {
113
- id: 'datasets-yml',
114
- label: 'datasets.yml lists datasets',
142
+ id: 'datasets-registered',
143
+ label: 'Dataset registry present',
115
144
  status: 'warn',
116
- detail: '`datasets` key is empty or missing',
145
+ detail: 'neither datasets.yml nor site-config.yml found at project root',
146
+ hint: 'datasets.yml / site-config.yml register every dataset the browser serves',
147
+ };
148
+ }
149
+ if (ctx.registry.registered.length === 0) {
150
+ return {
151
+ id: 'datasets-registered',
152
+ label: 'Dataset registry lists datasets',
153
+ status: 'warn',
154
+ detail: `\`datasets\` key is empty or missing in ${ctx.registry.source}`,
117
155
  hint: 'add at least one dataset entry; see docs/adding-a-dataset.md',
118
156
  };
119
157
  }
120
158
  return {
121
- id: 'datasets-yml',
122
- label: `datasets.yml lists ${datasets.length} dataset(s)`,
159
+ id: 'datasets-registered',
160
+ label: `${ctx.registry.source} lists ${ctx.registry.registered.length} dataset(s)`,
123
161
  status: 'pass',
124
162
  };
125
163
  }
126
164
 
127
165
  function datasetsFetchedCheck(ctx) {
128
- const datasets = ctx.datasetsYml?.datasets ?? [];
129
- if (!datasets.length) {
166
+ const datasets = ctx.registry.registered;
167
+ if (!ctx.registry.source || !datasets.length) {
130
168
  return { id: 'datasets-fetched', label: 'Source datasets fetched', status: 'warn', detail: 'no datasets registered' };
131
169
  }
132
- const dotDatasets = resolve(ctx.cwd, '.datasets');
170
+ // site-config deployments keep sources in-repo at local_path; the
171
+ // datasets.yml flow fetches source repos into .datasets/{id}/concepts.
133
172
  const missing = datasets
134
- .filter((d) => !existsSync(join(dotDatasets, d.id, 'concepts')))
173
+ .filter((d) => {
174
+ if (d.localPath) return !existsSync(resolve(ctx.cwd, d.localPath));
175
+ return !existsSync(join(resolve(ctx.cwd, '.datasets'), d.id, 'concepts'));
176
+ })
135
177
  .map((d) => d.id);
136
178
  if (missing.length) {
137
179
  return {
138
180
  id: 'datasets-fetched',
139
181
  label: 'Source datasets fetched',
140
182
  status: 'fail',
141
- detail: `.datasets/ missing: ${missing.join(', ')}`,
142
- hint: 'run `npm run fetch-datasets`',
183
+ detail: `dataset sources missing: ${missing.join(', ')}`,
184
+ hint: 'run `npm run fetch-datasets` (or check local_path entries in site-config.yml)',
143
185
  };
144
186
  }
145
187
  return { id: 'datasets-fetched', label: 'Source datasets fetched', status: 'pass' };
146
188
  }
147
189
 
148
190
  function datasetsGeneratedCheck(ctx) {
149
- const datasets = ctx.datasetsYml?.datasets ?? [];
150
- if (!datasets.length) {
191
+ const datasets = ctx.registry.registered;
192
+ if (!ctx.registry.source || !datasets.length) {
151
193
  return { id: 'datasets-generated', label: 'Generated data present', status: 'warn', detail: 'no datasets registered' };
152
194
  }
153
195
  const publicData = resolve(ctx.cwd, 'public', 'data');
@@ -256,7 +298,7 @@ function jsonldContextCheck(ctx) {
256
298
  export const CHECKS = [
257
299
  safe('node-version', nodeVersionCheck),
258
300
  safe('package-deps', packageDepsCheck),
259
- safe('datasets-yml', datasetsYmlCheck),
301
+ safe('datasets-registered', datasetsRegisteredCheck),
260
302
  safe('datasets-fetched', datasetsFetchedCheck),
261
303
  safe('datasets-generated', datasetsGeneratedCheck),
262
304
  safe('public-datasets-json', publicDatasetsJsonCheck),
@@ -280,7 +322,13 @@ export async function runDoctor(cwd = process.cwd()) {
280
322
  }
281
323
  }
282
324
  /** @type {DoctorContext} */
283
- const ctx = { cwd, pkgRoot: PKG_ROOT, datasetsYml, datasetsYmlError };
325
+ const ctx = {
326
+ cwd,
327
+ pkgRoot: PKG_ROOT,
328
+ datasetsYml,
329
+ datasetsYmlError,
330
+ registry: resolveDatasetRegistry(cwd, datasetsYmlError),
331
+ };
284
332
 
285
333
  const results = [];
286
334
  for (const check of CHECKS) {
@@ -30,6 +30,7 @@ var SNAKE_TO_CAMEL = {
30
30
  footer_nav: "footerNav",
31
31
  footer_logo: "footerLogo",
32
32
  news_dir: "newsDir",
33
+ bulk_exports: "bulkExports",
33
34
  // Dataset-level
34
35
  gcr_package: "gcrPackage",
35
36
  source_repo: "sourceRepo",
@@ -2,7 +2,16 @@
2
2
  import fs2 from "fs";
3
3
  import path3 from "path";
4
4
  import yaml4 from "js-yaml";
5
- import { naturalSort, Register, parseMentions, InvalidMentionError } from "glossarist";
5
+ import {
6
+ naturalSort,
7
+ Register,
8
+ parseMentions,
9
+ InvalidMentionError,
10
+ parseConceptYaml,
11
+ conceptToTurtle,
12
+ conceptsToTbx,
13
+ emitOutputSet
14
+ } from "glossarist";
6
15
 
7
16
  // scripts/load-site-config.ts
8
17
  import { readFileSync, existsSync } from "fs";
@@ -26,6 +35,7 @@ var SNAKE_TO_CAMEL = {
26
35
  footer_nav: "footerNav",
27
36
  footer_logo: "footerLogo",
28
37
  news_dir: "newsDir",
38
+ bulk_exports: "bulkExports",
29
39
  // Dataset-level
30
40
  gcr_package: "gcrPackage",
31
41
  source_repo: "sourceRepo",
@@ -545,6 +555,9 @@ function normalizeBibliography(raw) {
545
555
  function buildConceptUri(uriBase, registerId, conceptId) {
546
556
  return `${uriBase}/dataset/${registerId}/concept/${conceptId}`;
547
557
  }
558
+ function buildConceptUriPrefix(uriBase, registerId) {
559
+ return `${uriBase}/dataset/${registerId}/concept/`;
560
+ }
548
561
 
549
562
  // scripts/lib/harmonize-concept.ts
550
563
  import fs from "fs";
@@ -781,207 +794,6 @@ function refsToJsonLd(refs, refMaps2) {
781
794
  }).filter((r) => r["@id"]);
782
795
  }
783
796
 
784
- // scripts/lib/turtle-escape.ts
785
- var STRING_ESCAPES = {
786
- "\\": "\\\\",
787
- '"': '\\"',
788
- "\n": "\\n",
789
- "\r": "\\r",
790
- " ": "\\t"
791
- };
792
- function ttlLit(s) {
793
- if (s == null) return '""';
794
- const escaped = String(s).replace(/[\\\"\n\r\t]/g, (ch) => STRING_ESCAPES[ch] ?? ch);
795
- return `"${escaped}"`;
796
- }
797
-
798
- // scripts/lib/concept-formats.ts
799
- function escapeTurtle(s) {
800
- return ttlLit(s).slice(1, -1);
801
- }
802
- function escapeXml(s) {
803
- const str = Array.isArray(s) ? s.join(", ") : String(s ?? "");
804
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
805
- }
806
- function conceptJsonToTurtle(concept) {
807
- const uri = concept["@id"] || "";
808
- const id = concept["gl:identifier"] || "";
809
- const lines = [
810
- "@prefix skos: <http://www.w3.org/2004/02/skos/core#> .",
811
- "@prefix dcterms: <http://purl.org/dc/terms/> .",
812
- ""
813
- ];
814
- const props = [" a skos:Concept"];
815
- props.push(` skos:notation "${escapeTurtle(id)}"`);
816
- for (const [lang, lc] of Object.entries(concept["gl:localizedConcept"] || {})) {
817
- if (lc["gl:designation"]) {
818
- for (const d of lc["gl:designation"]) {
819
- const term = d["gl:term"];
820
- if (!term) continue;
821
- const pred = d["gl:normativeStatus"] === "preferred" ? "skos:prefLabel" : "skos:altLabel";
822
- props.push(` ${pred} "${escapeTurtle(term)}"@${lang}`);
823
- }
824
- }
825
- if (lc["gl:definition"]) {
826
- for (const d of lc["gl:definition"]) {
827
- if (d["gl:content"]) props.push(` skos:definition "${escapeTurtle(d["gl:content"])}"@${lang}`);
828
- }
829
- }
830
- if (lc["gl:notes"]) {
831
- for (const d of lc["gl:notes"]) {
832
- if (d["gl:content"]) props.push(` skos:scopeNote "${escapeTurtle(d["gl:content"])}"@${lang}`);
833
- }
834
- }
835
- }
836
- lines.push(`<${uri}>`);
837
- lines.push(props.join(" ;\n"));
838
- lines.push(" .");
839
- return lines.join("\n");
840
- }
841
- function conceptJsonToSkosJsonLd(concept) {
842
- const uri = concept["@id"] || "";
843
- const id = concept["gl:identifier"] || "";
844
- const doc = {
845
- "@context": {
846
- skos: "http://www.w3.org/2004/02/skos/core#",
847
- dcterms: "http://purl.org/dc/terms/",
848
- "@language": { "@container": "@language" }
849
- },
850
- "@id": uri,
851
- "@type": "skos:Concept",
852
- "skos:notation": id
853
- };
854
- const prefLabels = {};
855
- const altLabels = {};
856
- const definitions = {};
857
- const scopeNotes = {};
858
- for (const [lang, lc] of Object.entries(concept["gl:localizedConcept"] || {})) {
859
- const descs = lc["gl:designation"] || [];
860
- const pref = descs.find((d) => d["gl:normativeStatus"] === "preferred" && d["gl:term"]);
861
- const alt = descs.find((d) => d["gl:normativeStatus"] !== "preferred" && d["gl:term"]);
862
- if (pref) prefLabels[lang] = pref["gl:term"];
863
- if (alt) altLabels[lang] = alt["gl:term"];
864
- const def = (lc["gl:definition"] || [])[0];
865
- if (def?.["gl:content"]) definitions[lang] = def["gl:content"];
866
- const note = (lc["gl:notes"] || [])[0];
867
- if (note?.["gl:content"]) scopeNotes[lang] = note["gl:content"];
868
- }
869
- if (Object.keys(prefLabels).length) doc["skos:prefLabel"] = prefLabels;
870
- if (Object.keys(altLabels).length) doc["skos:altLabel"] = altLabels;
871
- if (Object.keys(definitions).length) doc["skos:definition"] = definitions;
872
- if (Object.keys(scopeNotes).length) doc["skos:scopeNote"] = scopeNotes;
873
- return JSON.stringify(doc);
874
- }
875
- function conceptJsonToTbx(concept) {
876
- const id = concept["gl:identifier"] || "";
877
- const uri = concept["@id"] || "";
878
- const localized = concept["gl:localizedConcept"] || {};
879
- const langSections = [];
880
- for (const [lang, lc] of Object.entries(localized)) {
881
- const descs = lc["gl:designation"] || [];
882
- const definitions = (lc["gl:definition"] || []).filter((d) => d["gl:content"]);
883
- const notes = (lc["gl:notes"] || []).filter((d) => d["gl:content"]);
884
- const examples = (lc["gl:examples"] || []).filter((d) => d["gl:content"]);
885
- const sources = lc["gl:source"] || [];
886
- const entryStatus = lc["gl:entryStatus"] || "";
887
- if (!descs.length && !definitions.length) continue;
888
- const termEntries = [];
889
- for (const d of descs) {
890
- const term = d["gl:term"];
891
- if (!term) continue;
892
- const status = d["gl:normativeStatus"] || "";
893
- const type = d["@type"] || "";
894
- let gramGrp = "";
895
- if (d["gl:grammarInfo"] && d["gl:grammarInfo"].length > 0) {
896
- const gi = d["gl:grammarInfo"][0];
897
- if (gi["gl:gender"]) gramGrp = `
898
- <grammaticalGender>${escapeXml(gi["gl:gender"])}</grammaticalGender>`;
899
- if (gi["gl:number"]) gramGrp += `
900
- <grammaticalNumber>${escapeXml(gi["gl:number"])}</grammaticalNumber>`;
901
- for (const pos of ["noun", "verb", "adj", "adverb", "preposition", "participle"]) {
902
- if (gi[`gl:${pos}`]) gramGrp += `
903
- <partOfSpeech>${pos}</partOfSpeech>`;
904
- }
905
- }
906
- let posBlock = "";
907
- if (type.includes("Abbreviation")) posBlock = "\n <partOfSpeech>abbreviation</partOfSpeech>";
908
- if (type.includes("Symbol")) posBlock = "\n <partOfSpeech>symbol</partOfSpeech>";
909
- termEntries.push(` <termEntry>
910
- <langSet xml:lang="${lang}">
911
- <tig>
912
- <term>${escapeXml(term)}</term>${gramGrp}${posBlock}
913
- </tig>
914
- </langSet>
915
- </termEntry>`);
916
- }
917
- let defBlock = "";
918
- if (definitions.length) {
919
- const defParts = definitions.map((d) => ` <p>${escapeXml(d["gl:content"])}</p>`).join("\n");
920
- defBlock = `
921
- <descrip type="definition">
922
- ${defParts}
923
- </descrip>`;
924
- }
925
- let noteBlock = "";
926
- for (let i = 0; i < notes.length; i++) {
927
- noteBlock += `
928
- <note type="note">${escapeXml(notes[i]["gl:content"])}</note>`;
929
- }
930
- for (let i = 0; i < examples.length; i++) {
931
- noteBlock += `
932
- <note type="example">${escapeXml(examples[i]["gl:content"])}</note>`;
933
- }
934
- let sourceBlock = "";
935
- for (const src of sources) {
936
- const origin = src["gl:origin"] || {};
937
- const parts = [];
938
- const ref = origin["gl:ref"];
939
- if (ref) {
940
- const refParts = [];
941
- if (ref["gl:source"]) refParts.push(ref["gl:source"]);
942
- if (ref["gl:id"]) refParts.push(ref["gl:id"]);
943
- parts.push(refParts.join(" ") || "");
944
- }
945
- if (origin["gl:locality"]) {
946
- const loc = origin["gl:locality"];
947
- if (loc["gl:referenceFrom"]) parts.push(loc["gl:localityType"] ? `${loc["gl:localityType"]} ${loc["gl:referenceFrom"]}` : loc["gl:referenceFrom"]);
948
- }
949
- if (parts.filter(Boolean).length) {
950
- sourceBlock += `
951
- <ref>${escapeXml(parts.filter(Boolean).join(", "))}</ref>`;
952
- }
953
- }
954
- let statusBlock = "";
955
- if (entryStatus) {
956
- statusBlock += `
957
- <descrip type="entryStatus">${escapeXml(entryStatus)}</descrip>`;
958
- }
959
- const termEntriesBlock = termEntries.length ? "\n" + termEntries.join("\n") : "";
960
- langSections.push({ lang, termEntries, blocks: [defBlock, noteBlock, sourceBlock, statusBlock].filter((b) => b).join("") });
961
- }
962
- if (!langSections.length) return "";
963
- const bodyEntries = langSections.map((ls) => {
964
- return ` <languageSection xml:lang="${ls.lang}">${ls.blocks}
965
- </languageSection>`;
966
- }).join("\n");
967
- return `<?xml version="1.0" encoding="UTF-8"?>
968
- <tbx style="dca" type="TBX-Basic" xml:lang="en" xmlns="urn:iso:std:iso:30042:ed-2">
969
- <tbxHeader>
970
- <fileDesc>
971
- <source>${escapeXml(uri)}</source>
972
- </fileDesc>
973
- </tbxHeader>
974
- <text>
975
- <body>
976
- <conceptEntry id="${escapeXml(id)}">
977
- ${bodyEntries}
978
- </conceptEntry>
979
- </body>
980
- </text>
981
- </tbx>
982
- `;
983
- }
984
-
985
797
  // scripts/generate-data.ts
986
798
  var __dirname3 = path3.dirname(new URL(import.meta.url).pathname);
987
799
  var ROOT = process.cwd();
@@ -1417,9 +1229,10 @@ async function processDataset(dir, register, opts) {
1417
1229
  for (const f of fs2.readdirSync(conceptsDir)) fs2.unlinkSync(path3.join(conceptsDir, f));
1418
1230
  }
1419
1231
  const concepts = [];
1232
+ const conceptInstances = [];
1420
1233
  const langTermCounts = {};
1421
1234
  const langDefCounts = {};
1422
- const availableFormats = ["ttl", "jsonld", "yaml", "tbx"];
1235
+ const availableFormats = ["ttl", "jsonld", "yaml", "tbx", "csv"];
1423
1236
  const designationLookup = /* @__PURE__ */ new Map();
1424
1237
  for (const file of files) {
1425
1238
  try {
@@ -1521,13 +1334,28 @@ async function processDataset(dir, register, opts) {
1521
1334
  const termid = String(conceptYaml.termid);
1522
1335
  const jsonld = yamlToJsonLd(conceptYaml, register, dsRefMaps);
1523
1336
  writeJson(path3.join(conceptsDir, `${termid}.json`), jsonld);
1524
- const ttlContent = conceptJsonToTurtle(jsonld);
1525
- fs2.writeFileSync(path3.join(conceptsDir, `${termid}.ttl`), ttlContent);
1526
- const skosJsonLd = conceptJsonToSkosJsonLd(jsonld);
1527
- fs2.writeFileSync(path3.join(conceptsDir, `${termid}.jsonld`), skosJsonLd);
1528
- const tbxContent = conceptJsonToTbx(jsonld);
1529
- if (tbxContent) {
1530
- fs2.writeFileSync(path3.join(conceptsDir, `${termid}.tbx`), tbxContent);
1337
+ let conceptInstance = null;
1338
+ try {
1339
+ conceptInstance = parseConceptYaml(fs2.readFileSync(path3.join(dir, file), "utf8"), termid);
1340
+ } catch (e) {
1341
+ console.warn(` ${termid}: canonical parse failed (${e.message}); skipping glossarist exports`);
1342
+ }
1343
+ if (conceptInstance) {
1344
+ conceptInstances.push(conceptInstance);
1345
+ fs2.writeFileSync(
1346
+ path3.join(conceptsDir, `${termid}.ttl`),
1347
+ conceptToTurtle(conceptInstance, { uriBase: refMaps.uriBase, registerId: register })
1348
+ );
1349
+ const conceptJsonLd = await emitOutputSet([conceptInstance], {
1350
+ registerId: register,
1351
+ uriBase: refMaps.uriBase,
1352
+ formats: ["jsonld"]
1353
+ });
1354
+ fs2.writeFileSync(path3.join(conceptsDir, `${termid}.jsonld`), conceptJsonLd.jsonld);
1355
+ const tbxContent = conceptsToTbx([conceptInstance], { registerId: register });
1356
+ if (tbxContent) {
1357
+ fs2.writeFileSync(path3.join(conceptsDir, `${termid}.tbx`), tbxContent);
1358
+ }
1531
1359
  }
1532
1360
  fs2.copyFileSync(path3.join(dir, file), path3.join(conceptsDir, `${termid}.yaml`));
1533
1361
  concepts.push({
@@ -1543,6 +1371,13 @@ async function processDataset(dir, register, opts) {
1543
1371
  console.warn(` Skipping ${file}: ${e.message}`);
1544
1372
  }
1545
1373
  }
1374
+ const aggregate = await emitOutputSet(conceptInstances, {
1375
+ registerId: register,
1376
+ uriBase: refMaps.uriBase,
1377
+ languageOrder: opts.languageOrder || opts.languages
1378
+ });
1379
+ fs2.writeFileSync(path3.join(DATA, register, `${register}.csv`), aggregate.csv);
1380
+ fs2.writeFileSync(path3.join(DATA, register, `${register}.jsonld`), aggregate.jsonld);
1546
1381
  const CHUNK_SIZE = 500;
1547
1382
  const chunks = [];
1548
1383
  for (let i = 0; i < concepts.length; i += CHUNK_SIZE) {
@@ -1585,7 +1420,7 @@ async function processDataset(dir, register, opts) {
1585
1420
  fs2.writeFileSync(
1586
1421
  path3.join(DATA, register, "graph-nodes.json"),
1587
1422
  JSON.stringify({
1588
- uriPrefix: `${refMaps.uriBase}/${register}/concept/`,
1423
+ uriPrefix: buildConceptUriPrefix(refMaps.uriBase, register),
1589
1424
  registerId: register,
1590
1425
  nodes: graphNodeEntries
1591
1426
  })
@@ -1612,28 +1447,32 @@ async function processDataset(dir, register, opts) {
1612
1447
  definitions: langDefCounts[lang] || 0
1613
1448
  };
1614
1449
  }
1450
+ const BULK_FORMAT_BY_EXT = {
1451
+ ".ttl": "turtle",
1452
+ ".jsonld": "jsonld",
1453
+ ".xml": "tbx",
1454
+ ".jsonl": "jsonl",
1455
+ ".yaml": "yaml",
1456
+ ".csv": "csv"
1457
+ };
1458
+ const bulkFormats = [];
1459
+ const addBulkFormat = (file) => {
1460
+ const dest = path3.join(DATA, register, file);
1461
+ if (!fs2.existsSync(dest) || bulkFormats.some((f) => f.file === file)) return;
1462
+ const formatName = BULK_FORMAT_BY_EXT[path3.extname(file)] || path3.extname(file).slice(1);
1463
+ bulkFormats.push({ file, format: formatName, size: fs2.statSync(dest).size });
1464
+ };
1465
+ for (const file of [`${register}.csv`, `${register}.jsonld`]) addBulkFormat(file);
1615
1466
  const sourceRoot = path3.dirname(dir);
1616
1467
  const compiledDir = path3.join(sourceRoot, "compiled");
1617
- const bulkFormats = [];
1618
1468
  if (fs2.existsSync(compiledDir)) {
1469
+ let copied = 0;
1619
1470
  for (const file of fs2.readdirSync(compiledDir)) {
1620
- const src = path3.join(compiledDir, file);
1621
- const dest = path3.join(DATA, register, file);
1622
- fs2.copyFileSync(src, dest);
1623
- const ext = path3.extname(file);
1624
- const formatMap = {
1625
- ".ttl": "turtle",
1626
- ".jsonld": "jsonld",
1627
- ".xml": "tbx",
1628
- ".jsonl": "jsonl",
1629
- ".yaml": "yaml"
1630
- };
1631
- const formatName = formatMap[ext] || ext.slice(1);
1632
- bulkFormats.push({ file, format: formatName, size: fs2.statSync(src).size });
1633
- }
1634
- if (bulkFormats.length) {
1635
- console.log(` Copied ${bulkFormats.length} bulk format files`);
1471
+ fs2.copyFileSync(path3.join(compiledDir, file), path3.join(DATA, register, file));
1472
+ addBulkFormat(file);
1473
+ copied += 1;
1636
1474
  }
1475
+ if (copied) console.log(` Copied ${copied} bulk format files from compiled/`);
1637
1476
  }
1638
1477
  const sourceStats = Array.from(stats.sourceMap.values()).map((s) => ({
1639
1478
  ref: s.ref,
@@ -1675,7 +1514,6 @@ async function processDataset(dir, register, opts) {
1675
1514
  year: opts.year ?? void 0,
1676
1515
  languageStats: langStats,
1677
1516
  availableFormats,
1678
- bulkFormats,
1679
1517
  hasBibliography: opts.hasBibliography,
1680
1518
  hasImages: opts.hasImages
1681
1519
  };
@@ -1685,8 +1523,10 @@ async function processDataset(dir, register, opts) {
1685
1523
  if (opts.status) manifest.editionStatus = opts.status;
1686
1524
  if (opts.ordering) manifest.ordering = opts.ordering;
1687
1525
  if (opts.sections && opts.sections.length > 0) manifest.sections = opts.sections;
1688
- writeJson(path3.join(DATA, register, "manifest.json"), manifest);
1689
1526
  await writeDatasetRdf(register, manifest, concepts, refMaps, opts);
1527
+ addBulkFormat(`${register}.ttl`);
1528
+ manifest.bulkFormats = opts.bulkExports?.length ? bulkFormats.filter((f) => opts.bulkExports.includes(f.format)) : bulkFormats;
1529
+ writeJson(path3.join(DATA, register, "manifest.json"), manifest);
1690
1530
  const bibPath = path3.join(sourceRoot, "bibliography.yaml");
1691
1531
  if (fs2.existsSync(bibPath)) {
1692
1532
  const rawBib = readYaml(bibPath);
@@ -1736,6 +1576,7 @@ for (const ds of config.datasets) {
1736
1576
  }
1737
1577
  }
1738
1578
  var refMaps = buildRefMaps(config, registerCache);
1579
+ var bulkExports = (config.bulkExports || []).map((f) => f === "ttl" ? "turtle" : f);
1739
1580
  for (let i = 0; i < config.datasets.length; i++) {
1740
1581
  const ds = config.datasets[i];
1741
1582
  const dir = path3.join(datasetDir(ds), "concepts");
@@ -1769,7 +1610,8 @@ for (let i = 0; i < config.datasets.length; i++) {
1769
1610
  ordering: reg?.ordering || null,
1770
1611
  sections: reg?.sections ? reg.sections.map((s) => s.toJSON()) : [],
1771
1612
  hasBibliography: fs2.existsSync(path3.join(datasetDir(ds), "bibliography.yaml")),
1772
- hasImages: fs2.existsSync(path3.join(datasetDir(ds), "images"))
1613
+ hasImages: fs2.existsSync(path3.join(datasetDir(ds), "images")),
1614
+ bulkExports
1773
1615
  });
1774
1616
  const resolvedOwner = firstNonEmpty(reg?.owner, ds.owner) ?? "";
1775
1617
  const resolvedTags = firstNonEmpty(reg?.tags, ds.tags) ?? [];