@glossarist/concept-browser 0.1.0
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 +319 -0
- package/cli/index.mjs +119 -0
- package/env.d.ts +7 -0
- package/index.html +16 -0
- package/package.json +78 -0
- package/postcss.config.js +6 -0
- package/scripts/build-edges.js +112 -0
- package/scripts/fetch-datasets.mjs +195 -0
- package/scripts/generate-404.js +15 -0
- package/scripts/generate-data.mjs +606 -0
- package/scripts/load-site-config.mjs +56 -0
- package/src/App.vue +98 -0
- package/src/__tests__/data-integration.test.ts +135 -0
- package/src/__tests__/data-integrity.test.ts +101 -0
- package/src/__tests__/dataset-adapter.test.ts +336 -0
- package/src/__tests__/dataset-style.test.ts +37 -0
- package/src/__tests__/graph.test.ts +187 -0
- package/src/__tests__/lang.test.ts +29 -0
- package/src/__tests__/math.test.ts +113 -0
- package/src/__tests__/reference-resolver.test.ts +122 -0
- package/src/__tests__/site-config.test.ts +52 -0
- package/src/__tests__/uri-router.test.ts +76 -0
- package/src/adapters/DatasetAdapter.ts +270 -0
- package/src/adapters/ReferenceResolver.ts +95 -0
- package/src/adapters/UriRouter.ts +41 -0
- package/src/adapters/factory.ts +78 -0
- package/src/adapters/types.ts +162 -0
- package/src/components/AppHeader.vue +99 -0
- package/src/components/AppSidebar.vue +133 -0
- package/src/components/ConceptCard.vue +65 -0
- package/src/components/ConceptDetail.vue +540 -0
- package/src/components/ConceptTimeline.vue +410 -0
- package/src/components/FormatDownloads.vue +46 -0
- package/src/components/GraphPanel.vue +499 -0
- package/src/components/LanguageDetail.vue +211 -0
- package/src/components/NavIcon.vue +20 -0
- package/src/components/SearchBar.vue +241 -0
- package/src/composables/use-dataset-loader.ts +27 -0
- package/src/config/types.ts +130 -0
- package/src/config/use-site-config.ts +144 -0
- package/src/graph/GraphEngine.ts +137 -0
- package/src/graph/index.ts +1 -0
- package/src/main.ts +11 -0
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/router/index.ts +43 -0
- package/src/router/page-routes.ts +35 -0
- package/src/stores/ui.ts +59 -0
- package/src/stores/vocabulary.ts +309 -0
- package/src/style.css +314 -0
- package/src/utils/asciidoc-lite.ts +123 -0
- package/src/utils/concept-formats.ts +157 -0
- package/src/utils/dataset-style.ts +54 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/lang.ts +32 -0
- package/src/utils/math.ts +100 -0
- package/src/views/AboutView.vue +122 -0
- package/src/views/ConceptView.vue +119 -0
- package/src/views/ContributorsView.vue +110 -0
- package/src/views/DatasetView.vue +249 -0
- package/src/views/GraphView.vue +65 -0
- package/src/views/HomeView.vue +168 -0
- package/src/views/NewsView.vue +146 -0
- package/src/views/ResolveView.vue +63 -0
- package/src/views/SearchView.vue +33 -0
- package/src/views/StatsView.vue +121 -0
- package/tailwind.config.js +43 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +27 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { naturalSort } from 'glossarist';
|
|
5
|
+
import { loadSiteConfig } from './load-site-config.mjs';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
8
|
+
const ROOT = process.cwd();
|
|
9
|
+
const PUBLIC = path.join(ROOT, 'public');
|
|
10
|
+
const DATA = path.join(PUBLIC, 'data');
|
|
11
|
+
|
|
12
|
+
const DS_PALETTE = [
|
|
13
|
+
'#3366ff', '#0d9488', '#d97706', '#8b5cf6',
|
|
14
|
+
'#ec4899', '#059669', '#dc2626', '#6366f1',
|
|
15
|
+
'#0891b2', '#65a30d',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function readYaml(filePath) {
|
|
19
|
+
return yaml.load(fs.readFileSync(filePath, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function loadConceptFile(filePath) {
|
|
23
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
24
|
+
const docs = yaml.loadAll(content, null, { schema: yaml.DEFAULT_SCHEMA });
|
|
25
|
+
|
|
26
|
+
if (docs.length === 1 && docs[0].termid !== undefined) {
|
|
27
|
+
return docs[0];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (docs.length >= 1 && docs[0].data && docs[0].data.identifier !== undefined) {
|
|
31
|
+
const mc = docs[0];
|
|
32
|
+
const result = { termid: String(mc.data.identifier) };
|
|
33
|
+
for (const doc of docs.slice(1)) {
|
|
34
|
+
if (!doc || !doc.data || !doc.data.language_code) continue;
|
|
35
|
+
const lang = doc.data.language_code;
|
|
36
|
+
const lcData = { ...doc.data };
|
|
37
|
+
delete lcData.language_code;
|
|
38
|
+
result[lang] = lcData;
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return docs[0];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeJson(filePath, data) {
|
|
47
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
48
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function termToDesignation(term) {
|
|
52
|
+
const doc = {
|
|
53
|
+
'@type': term.type === 'expression' ? 'gl:Expression'
|
|
54
|
+
: term.type === 'symbol' ? 'gl:Symbol'
|
|
55
|
+
: term.type === 'abbreviation' ? 'gl:Abbreviation'
|
|
56
|
+
: 'gl:Designation',
|
|
57
|
+
'gl:normativeStatus': term.normative_status || 'preferred',
|
|
58
|
+
'gl:term': term.designation,
|
|
59
|
+
};
|
|
60
|
+
if (term.gender) doc['gl:gender'] = term.gender;
|
|
61
|
+
if (term.plurality) doc['gl:plurality'] = term.plurality;
|
|
62
|
+
if (term.international !== undefined) doc['gl:international'] = term.international;
|
|
63
|
+
return doc;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function defsToJsonLd(defs) {
|
|
67
|
+
if (!defs || !Array.isArray(defs)) return [];
|
|
68
|
+
return defs
|
|
69
|
+
.map(d => ({
|
|
70
|
+
'@type': 'gl:DetailedDefinition',
|
|
71
|
+
'gl:content': d.content || '',
|
|
72
|
+
}))
|
|
73
|
+
.filter(d => d['gl:content']);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sourcesToJsonLd(sources) {
|
|
77
|
+
if (!sources || !Array.isArray(sources)) return [];
|
|
78
|
+
return sources.map(s => {
|
|
79
|
+
const doc = { '@type': 'gl:ConceptSource' };
|
|
80
|
+
if (s.type) doc['gl:sourceType'] = s.type;
|
|
81
|
+
if (s.status) doc['gl:sourceStatus'] = s.status;
|
|
82
|
+
if (s.origin) {
|
|
83
|
+
const origin = { '@type': 'gl:Citation' };
|
|
84
|
+
if (s.origin.ref) origin['gl:ref'] = s.origin.ref;
|
|
85
|
+
if (s.origin.clause) origin['gl:clause'] = s.origin.clause;
|
|
86
|
+
if (s.origin.link) origin['gl:link'] = s.origin.link;
|
|
87
|
+
doc['gl:origin'] = origin;
|
|
88
|
+
}
|
|
89
|
+
return doc;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function refsToJsonLd(refs) {
|
|
94
|
+
if (!refs || !Array.isArray(refs)) return [];
|
|
95
|
+
return refs.map(r => ({
|
|
96
|
+
'@id': r.id,
|
|
97
|
+
'gl:term': r.term,
|
|
98
|
+
})).filter(r => r['@id']);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const LANG_CODES = ['eng', 'ara', 'deu', 'fra', 'spa', 'ita', 'jpn', 'kor', 'pol', 'por', 'srp', 'swe', 'zho', 'rus', 'fin', 'dan', 'nld', 'msa', 'nob', 'nno', 'zho'];
|
|
102
|
+
|
|
103
|
+
function yamlToJsonLd(conceptYaml, register) {
|
|
104
|
+
const termid = String(conceptYaml.termid);
|
|
105
|
+
const doc = {
|
|
106
|
+
'@context': 'https://glossarist.org/ns/context.jsonld',
|
|
107
|
+
'@id': `https://glossarist.org/${register}/concept/${termid}`,
|
|
108
|
+
'@type': 'gl:Concept',
|
|
109
|
+
'gl:identifier': termid,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const localizations = {};
|
|
113
|
+
for (const lang of LANG_CODES) {
|
|
114
|
+
const lc = conceptYaml[lang];
|
|
115
|
+
if (!lc) continue;
|
|
116
|
+
|
|
117
|
+
const lDoc = {
|
|
118
|
+
'@id': `https://glossarist.org/${register}/concept/${termid}/${lang}`,
|
|
119
|
+
'@type': 'gl:LocalizedConcept',
|
|
120
|
+
'gl:languageCode': lang,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (lc.entry_status) lDoc['gl:entryStatus'] = lc.entry_status;
|
|
124
|
+
if (lc.terms && lc.terms.length > 0) lDoc['gl:designation'] = lc.terms.map(termToDesignation);
|
|
125
|
+
if (lc.definition) lDoc['gl:definition'] = defsToJsonLd(lc.definition);
|
|
126
|
+
if (lc.notes && lc.notes.length > 0) lDoc['gl:notes'] = defsToJsonLd(lc.notes);
|
|
127
|
+
if (lc.examples && lc.examples.length > 0) lDoc['gl:examples'] = defsToJsonLd(lc.examples);
|
|
128
|
+
if (lc.sources && lc.sources.length > 0) lDoc['gl:source'] = sourcesToJsonLd(lc.sources);
|
|
129
|
+
if (lc.lineage_source_similarity !== undefined) lDoc['gl:lineageSourceSimilarity'] = lc.lineage_source_similarity;
|
|
130
|
+
if (lc.release) lDoc['gl:release'] = lc.release;
|
|
131
|
+
if (lc.review_date) lDoc['gl:reviewDate'] = lc.review_date;
|
|
132
|
+
if (lc.review_decision_date) lDoc['gl:reviewDecisionDate'] = lc.review_decision_date;
|
|
133
|
+
if (lc.review_decision_event) lDoc['gl:reviewDecisionEvent'] = lc.review_decision_event;
|
|
134
|
+
if (lc.review_status) lDoc['gl:reviewStatus'] = lc.review_status;
|
|
135
|
+
if (lc.review_decision) lDoc['gl:reviewDecision'] = lc.review_decision;
|
|
136
|
+
if (lc.review_decision_notes) lDoc['gl:reviewDecisionNotes'] = lc.review_decision_notes;
|
|
137
|
+
if (lc.dates && lc.dates.length > 0) {
|
|
138
|
+
lDoc['gl:dates'] = lc.dates.map(d => ({
|
|
139
|
+
'gl:dateType': d.type,
|
|
140
|
+
'gl:date': d.date,
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
if (lc.references && lc.references.length > 0) {
|
|
144
|
+
lDoc['gl:references'] = refsToJsonLd(lc.references);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
localizations[lang] = lDoc;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (Object.keys(localizations).length > 0) {
|
|
151
|
+
doc['gl:localizedConcept'] = localizations;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return doc;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getPrimaryDesignation(conceptYaml) {
|
|
158
|
+
const descs = {};
|
|
159
|
+
for (const lang of LANG_CODES) {
|
|
160
|
+
const lc = conceptYaml[lang];
|
|
161
|
+
if (lc && lc.terms && lc.terms.length > 0) {
|
|
162
|
+
const preferredExpr = lc.terms.find(t => t.normative_status === 'preferred' && t.type === 'expression');
|
|
163
|
+
const preferred = preferredExpr || lc.terms.find(t => t.normative_status === 'preferred') || lc.terms[0];
|
|
164
|
+
descs[lang] = preferred.designation;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return descs;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getGroups(conceptYaml) {
|
|
171
|
+
if (conceptYaml.eng && conceptYaml.eng.groups) return conceptYaml.eng.groups;
|
|
172
|
+
const termid = String(conceptYaml.termid);
|
|
173
|
+
if (/^\d{3}-/.test(termid)) return [termid.substring(0, 3)];
|
|
174
|
+
if (/^\d+\.\d+\.\d+/.test(termid)) {
|
|
175
|
+
const parts = termid.split('.');
|
|
176
|
+
return [`${parts[0]}.${parts[1]}.${parts[2]}`];
|
|
177
|
+
}
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function escapeTurtle(s) {
|
|
182
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function conceptJsonToTurtle(concept) {
|
|
186
|
+
const uri = concept['@id'] || '';
|
|
187
|
+
const id = concept['gl:identifier'] || '';
|
|
188
|
+
const lines = [
|
|
189
|
+
'@prefix skos: <http://www.w3.org/2004/02/skos/core#> .',
|
|
190
|
+
'@prefix dcterms: <http://purl.org/dc/terms/> .',
|
|
191
|
+
'',
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
const props = [' a skos:Concept'];
|
|
195
|
+
props.push(` skos:notation "${escapeTurtle(id)}"`);
|
|
196
|
+
|
|
197
|
+
for (const [lang, lc] of Object.entries(concept['gl:localizedConcept'] || {})) {
|
|
198
|
+
if (lc['gl:designation']) {
|
|
199
|
+
for (const d of lc['gl:designation']) {
|
|
200
|
+
const term = d['gl:term'];
|
|
201
|
+
if (!term) continue;
|
|
202
|
+
const pred = d['gl:normativeStatus'] === 'preferred' ? 'skos:prefLabel' : 'skos:altLabel';
|
|
203
|
+
props.push(` ${pred} "${escapeTurtle(term)}"@${lang}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (lc['gl:definition']) {
|
|
207
|
+
for (const d of lc['gl:definition']) {
|
|
208
|
+
if (d['gl:content']) props.push(` skos:definition "${escapeTurtle(d['gl:content'])}"@${lang}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (lc['gl:notes']) {
|
|
212
|
+
for (const d of lc['gl:notes']) {
|
|
213
|
+
if (d['gl:content']) props.push(` skos:scopeNote "${escapeTurtle(d['gl:content'])}"@${lang}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
lines.push(`<${uri}>`);
|
|
219
|
+
lines.push(props.join(' ;\n'));
|
|
220
|
+
lines.push(' .');
|
|
221
|
+
return lines.join('\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function processDataset(dir, register, opts) {
|
|
225
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.yaml')).sort((a, b) => naturalSort(a.replace('.yaml', ''), b.replace('.yaml', '')));
|
|
226
|
+
|
|
227
|
+
console.log(`Processing ${register}: ${files.length} files`);
|
|
228
|
+
|
|
229
|
+
const conceptsDir = path.join(DATA, register, 'concepts');
|
|
230
|
+
const concepts = [];
|
|
231
|
+
const langTermCounts = {};
|
|
232
|
+
const langDefCounts = {};
|
|
233
|
+
const availableFormats = ['ttl', 'yaml'];
|
|
234
|
+
|
|
235
|
+
for (let i = 0; i < files.length; i++) {
|
|
236
|
+
const file = files[i];
|
|
237
|
+
try {
|
|
238
|
+
const conceptYaml = loadConceptFile(path.join(dir, file));
|
|
239
|
+
if (!conceptYaml || !conceptYaml.termid) continue;
|
|
240
|
+
|
|
241
|
+
const termid = String(conceptYaml.termid);
|
|
242
|
+
const jsonld = yamlToJsonLd(conceptYaml, register);
|
|
243
|
+
writeJson(path.join(conceptsDir, `${termid}.json`), jsonld);
|
|
244
|
+
|
|
245
|
+
// Generate Turtle format
|
|
246
|
+
const ttlContent = conceptJsonToTurtle(jsonld);
|
|
247
|
+
fs.writeFileSync(path.join(conceptsDir, `${termid}.ttl`), ttlContent);
|
|
248
|
+
|
|
249
|
+
// Copy source YAML
|
|
250
|
+
fs.copyFileSync(path.join(dir, file), path.join(conceptsDir, `${termid}.yaml`));
|
|
251
|
+
|
|
252
|
+
concepts.push({
|
|
253
|
+
id: termid,
|
|
254
|
+
designations: getPrimaryDesignation(conceptYaml),
|
|
255
|
+
groups: getGroups(conceptYaml),
|
|
256
|
+
status: conceptYaml.eng?.entry_status || 'valid',
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
for (const lang of opts.languages) {
|
|
260
|
+
const lc = conceptYaml[lang];
|
|
261
|
+
if (lc) {
|
|
262
|
+
if (lc.terms && lc.terms.length > 0) {
|
|
263
|
+
langTermCounts[lang] = (langTermCounts[lang] || 0) + 1;
|
|
264
|
+
}
|
|
265
|
+
if (lc.definition && (Array.isArray(lc.definition) ? lc.definition.some(d => d.content) : lc.definition)) {
|
|
266
|
+
langDefCounts[lang] = (langDefCounts[lang] || 0) + 1;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch (e) {
|
|
271
|
+
console.warn(` Skipping ${file}: ${e.message}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const CHUNK_SIZE = 500;
|
|
276
|
+
const chunks = [];
|
|
277
|
+
for (let i = 0; i < concepts.length; i += CHUNK_SIZE) {
|
|
278
|
+
const chunk = concepts.slice(i, i + CHUNK_SIZE);
|
|
279
|
+
const chunkIndex = Math.floor(i / CHUNK_SIZE);
|
|
280
|
+
const chunkFile = `index-${String(chunkIndex).padStart(4, '0')}.json`;
|
|
281
|
+
writeJson(path.join(DATA, register, 'chunks', chunkFile), {
|
|
282
|
+
registerId: register,
|
|
283
|
+
chunkIndex,
|
|
284
|
+
concepts: chunk,
|
|
285
|
+
});
|
|
286
|
+
chunks.push({ file: chunkFile, count: chunk.length });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const summary = concepts.map(c => ({
|
|
290
|
+
id: c.id,
|
|
291
|
+
eng: c.designations.eng || Object.values(c.designations)[0] || '',
|
|
292
|
+
status: c.status,
|
|
293
|
+
}));
|
|
294
|
+
|
|
295
|
+
const graphNodeEntries = concepts.map(c => {
|
|
296
|
+
let term = '', lang = '';
|
|
297
|
+
if (c.designations.eng) { term = c.designations.eng; lang = 'eng'; }
|
|
298
|
+
else { for (const [l, t] of Object.entries(c.designations)) { if (t) { term = t; lang = l; break; } } }
|
|
299
|
+
return [c.id, term, lang, c.status];
|
|
300
|
+
});
|
|
301
|
+
fs.mkdirSync(path.join(DATA, register), { recursive: true });
|
|
302
|
+
fs.writeFileSync(
|
|
303
|
+
path.join(DATA, register, 'graph-nodes.json'),
|
|
304
|
+
JSON.stringify({
|
|
305
|
+
uriPrefix: `https://glossarist.org/${register}/concept/`,
|
|
306
|
+
registerId: register,
|
|
307
|
+
nodes: graphNodeEntries,
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
writeJson(path.join(DATA, register, 'index.json'), {
|
|
312
|
+
registerId: register,
|
|
313
|
+
schemaVersion: '1.0.0',
|
|
314
|
+
conceptCount: concepts.length,
|
|
315
|
+
chunkSize: CHUNK_SIZE,
|
|
316
|
+
chunks,
|
|
317
|
+
concepts: summary,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
writeJson(path.join(DATA, register, 'index-meta.json'), {
|
|
321
|
+
registerId: register,
|
|
322
|
+
schemaVersion: '1.0.0',
|
|
323
|
+
conceptCount: concepts.length,
|
|
324
|
+
chunkSize: CHUNK_SIZE,
|
|
325
|
+
chunks,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const langStats = {};
|
|
329
|
+
for (const lang of opts.languages) {
|
|
330
|
+
langStats[lang] = {
|
|
331
|
+
terms: langTermCounts[lang] || 0,
|
|
332
|
+
definitions: langDefCounts[lang] || 0,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const manifest = {
|
|
337
|
+
id: register,
|
|
338
|
+
datasetUri: opts.datasetUri,
|
|
339
|
+
uriAliases: opts.uriAliases,
|
|
340
|
+
title: opts.title,
|
|
341
|
+
description: opts.description,
|
|
342
|
+
owner: opts.owner,
|
|
343
|
+
baseUrl: `/data/${register}`,
|
|
344
|
+
languages: opts.languages,
|
|
345
|
+
conceptCount: concepts.length,
|
|
346
|
+
conceptUrlTemplate: '{baseUrl}/concepts/{conceptId}.json',
|
|
347
|
+
indexUrl: '{baseUrl}/index.json',
|
|
348
|
+
contextUrl: 'https://glossarist.org/ns/context.jsonld',
|
|
349
|
+
uriBase: 'https://glossarist.org',
|
|
350
|
+
status: 'valid',
|
|
351
|
+
schemaVersion: '1.0.0',
|
|
352
|
+
tags: opts.tags,
|
|
353
|
+
lastUpdated: new Date().toISOString().split('T')[0],
|
|
354
|
+
sourceRepo: opts.sourceRepo,
|
|
355
|
+
chunkSize: CHUNK_SIZE,
|
|
356
|
+
color: opts.color,
|
|
357
|
+
languageStats: langStats,
|
|
358
|
+
availableFormats,
|
|
359
|
+
};
|
|
360
|
+
if (opts.languageOrder) manifest.languageOrder = opts.languageOrder;
|
|
361
|
+
writeJson(path.join(DATA, register, 'manifest.json'), manifest);
|
|
362
|
+
|
|
363
|
+
console.log(` Generated ${concepts.length} concepts, manifest, ${chunks.length} index chunks`);
|
|
364
|
+
return concepts.length;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// --- Main ---
|
|
368
|
+
console.log('Generating Glossarist vocabulary browser data...\n');
|
|
369
|
+
|
|
370
|
+
const { config } = loadSiteConfig();
|
|
371
|
+
const counts = {};
|
|
372
|
+
const registry = [];
|
|
373
|
+
|
|
374
|
+
for (let i = 0; i < config.datasets.length; i++) {
|
|
375
|
+
const ds = config.datasets[i];
|
|
376
|
+
|
|
377
|
+
const dir = path.join(ROOT, '.datasets', ds.id, 'concepts');
|
|
378
|
+
if (!fs.existsSync(dir)) {
|
|
379
|
+
console.warn(`Skipping ${ds.id}: source directory not found (${dir})`);
|
|
380
|
+
console.warn(` Run: npm run fetch-datasets`);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const registerDir = path.join(ROOT, '.datasets', ds.id);
|
|
385
|
+
const registerYamlPath = path.join(registerDir, 'register.yaml');
|
|
386
|
+
let registerMeta = {};
|
|
387
|
+
if (fs.existsSync(registerYamlPath)) {
|
|
388
|
+
try { registerMeta = readYaml(registerYamlPath) || {}; } catch {}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const dsLanguages = ds.languages || (registerMeta.subregisters ? Object.keys(registerMeta.subregisters) : ['eng']);
|
|
392
|
+
|
|
393
|
+
counts[ds.id] = processDataset(dir, ds.id, {
|
|
394
|
+
title: ds.title || registerMeta.name || ds.id,
|
|
395
|
+
description: ds.description || registerMeta.description || '',
|
|
396
|
+
owner: ds.owner,
|
|
397
|
+
languages: dsLanguages,
|
|
398
|
+
sourceRepo: ds.sourceRepo,
|
|
399
|
+
languageOrder: ds.languageOrder,
|
|
400
|
+
tags: ds.tags,
|
|
401
|
+
color: ds.color || DS_PALETTE[i % DS_PALETTE.length],
|
|
402
|
+
datasetUri: ds.uri,
|
|
403
|
+
uriAliases: ds.uriAliases,
|
|
404
|
+
});
|
|
405
|
+
registry.push({ id: ds.id, manifestUrl: `/data/${ds.id}/manifest.json` });
|
|
406
|
+
}
|
|
407
|
+
writeJson(path.join(PUBLIC, 'datasets.json'), registry);
|
|
408
|
+
|
|
409
|
+
// Generate routing.json from site config
|
|
410
|
+
writeJson(path.join(PUBLIC, 'routing.json'), config.routing || []);
|
|
411
|
+
console.log('Generated routing.json');
|
|
412
|
+
|
|
413
|
+
// Copy/download logos
|
|
414
|
+
async function processLogo(logoConfig, filename) {
|
|
415
|
+
if (!logoConfig) return;
|
|
416
|
+
const destDir = path.join(PUBLIC, 'logos');
|
|
417
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
418
|
+
const destPath = path.join(destDir, filename);
|
|
419
|
+
|
|
420
|
+
// Local file in deployment repo
|
|
421
|
+
if (logoConfig.localPath) {
|
|
422
|
+
const src = path.resolve(process.cwd(), logoConfig.localPath);
|
|
423
|
+
if (fs.existsSync(src)) {
|
|
424
|
+
fs.copyFileSync(src, destPath);
|
|
425
|
+
console.log(` Copied logo: ${src} → ${destPath}`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
console.warn(` Logo not found at: ${src}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Remote URL
|
|
432
|
+
if (logoConfig.remoteUrl) {
|
|
433
|
+
try {
|
|
434
|
+
console.log(` Downloading logo: ${logoConfig.remoteUrl}`);
|
|
435
|
+
const resp = await fetch(logoConfig.remoteUrl);
|
|
436
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
437
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
438
|
+
fs.writeFileSync(destPath, buf);
|
|
439
|
+
console.log(` Saved logo: ${destPath}`);
|
|
440
|
+
} catch (e) {
|
|
441
|
+
console.warn(` Logo download failed: ${e.message}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
await processLogo(config.branding?.logo, `${config.id}-logo.svg`);
|
|
447
|
+
await processLogo(config.branding?.footerLogo, `${config.id}-footer-logo.svg`);
|
|
448
|
+
|
|
449
|
+
// === Page processors ===
|
|
450
|
+
|
|
451
|
+
function processNewsPage(config, page) {
|
|
452
|
+
const newsDir = page.source
|
|
453
|
+
? path.resolve(process.cwd(), page.source)
|
|
454
|
+
: config.newsDir
|
|
455
|
+
? path.resolve(process.cwd(), config.newsDir)
|
|
456
|
+
: null;
|
|
457
|
+
|
|
458
|
+
if (!newsDir || !fs.existsSync(newsDir)) {
|
|
459
|
+
if (newsDir) console.warn(`News directory not found: ${newsDir}`);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const index = [];
|
|
464
|
+
const newsOutDir = path.join(PUBLIC, 'news');
|
|
465
|
+
fs.mkdirSync(newsOutDir, { recursive: true });
|
|
466
|
+
const postFiles = fs.readdirSync(newsDir).filter(f => f.endsWith('.adoc') || f.endsWith('.md')).sort().reverse();
|
|
467
|
+
|
|
468
|
+
for (const file of postFiles) {
|
|
469
|
+
const content = fs.readFileSync(path.join(newsDir, file), 'utf8');
|
|
470
|
+
const frontmatter = {};
|
|
471
|
+
const bodyLines = [];
|
|
472
|
+
|
|
473
|
+
let inFm = false;
|
|
474
|
+
const lines = content.split('\n');
|
|
475
|
+
if (lines[0] === '---') {
|
|
476
|
+
inFm = true;
|
|
477
|
+
for (let i = 1; i < lines.length; i++) {
|
|
478
|
+
if (lines[i] === '---') { inFm = false; continue; }
|
|
479
|
+
if (inFm) {
|
|
480
|
+
const m = lines[i].match(/^(\w[\w\s]*):\s*(.*)/);
|
|
481
|
+
if (m) frontmatter[m[1].trim()] = m[2].trim().replace(/^["']|["']$/g, '');
|
|
482
|
+
} else {
|
|
483
|
+
bodyLines.push(lines[i]);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
bodyLines.push(...lines);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const slug = file.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.(adoc|md)$/, '');
|
|
491
|
+
const body = bodyLines.join('\n').trim();
|
|
492
|
+
|
|
493
|
+
const ext = path.extname(file);
|
|
494
|
+
const destFile = path.join(newsOutDir, `${slug}${ext}`);
|
|
495
|
+
fs.copyFileSync(path.join(newsDir, file), destFile);
|
|
496
|
+
|
|
497
|
+
index.push({
|
|
498
|
+
slug,
|
|
499
|
+
title: frontmatter.title || slug,
|
|
500
|
+
date: frontmatter.date || '',
|
|
501
|
+
categories: frontmatter.categories ? frontmatter.categories.split(',').map(s => s.trim()) : [],
|
|
502
|
+
file: `/news/${slug}${ext}`,
|
|
503
|
+
excerpt: body.split('\n').find(l => l.trim())?.slice(0, 200) || '',
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
writeJson(path.join(PUBLIC, 'news.json'), index);
|
|
508
|
+
console.log(`Generated news index: ${index.length} posts, ${postFiles.length} files copied to public/news/`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function processContributorsPage(config, page) {
|
|
512
|
+
const contributors = { register: config.id, contributors: [] };
|
|
513
|
+
|
|
514
|
+
for (const ds of config.datasets) {
|
|
515
|
+
const infoYamlPath = path.join(ROOT, '.datasets', ds.id, 'info.yaml');
|
|
516
|
+
if (!fs.existsSync(infoYamlPath)) continue;
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
const info = readYaml(infoYamlPath);
|
|
520
|
+
if (info.header) {
|
|
521
|
+
contributors.owner = info.header['register-owner'];
|
|
522
|
+
contributors.manager = info.header['register-manager'];
|
|
523
|
+
}
|
|
524
|
+
if (info.languages) {
|
|
525
|
+
for (const [lang, data] of Object.entries(info.languages)) {
|
|
526
|
+
contributors.contributors.push({
|
|
527
|
+
language: lang,
|
|
528
|
+
registerName: data['register-name'] || '',
|
|
529
|
+
organization: data['submitting-organisation-name'] || '',
|
|
530
|
+
contact: data['submitting-organisation-contact'] || '',
|
|
531
|
+
email: data['submitting-organisation-contact-email'] || '',
|
|
532
|
+
uri: data['uniform-resource-identifier-uri'] || '',
|
|
533
|
+
country: data['operating-language-country'] || '',
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
} catch (e) {
|
|
538
|
+
console.warn(` Skipping contributors for ${ds.id}: ${e.message}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (contributors.contributors.length > 0 || contributors.owner) {
|
|
543
|
+
writeJson(path.join(PUBLIC, 'contributors.json'), contributors);
|
|
544
|
+
console.log(`Generated contributors: ${contributors.contributors.length} languages`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const pageProcessors = {
|
|
549
|
+
news: processNewsPage,
|
|
550
|
+
contributors: processContributorsPage,
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
function synthesizePages(config) {
|
|
554
|
+
const pages = [];
|
|
555
|
+
if (config.newsDir) pages.push({ type: 'news', route: 'news', title: 'News', icon: 'newspaper' });
|
|
556
|
+
return pages;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function processPages(config) {
|
|
560
|
+
const pages = config.pages || synthesizePages(config);
|
|
561
|
+
for (const page of pages) {
|
|
562
|
+
const processor = pageProcessors[page.type];
|
|
563
|
+
if (processor) processor(config, page);
|
|
564
|
+
}
|
|
565
|
+
return pages;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const processedPages = processPages(config);
|
|
569
|
+
|
|
570
|
+
// Generate site-config.json from site config
|
|
571
|
+
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;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
writeJson(path.join(PUBLIC, 'site-config.json'), {
|
|
583
|
+
id: config.id,
|
|
584
|
+
domain: config.domain,
|
|
585
|
+
title: config.title,
|
|
586
|
+
subtitle: config.subtitle,
|
|
587
|
+
description: config.description,
|
|
588
|
+
datasets: config.datasets.map(d => d.id),
|
|
589
|
+
defaultDataset: config.datasets.length === 1 ? config.datasets[0].id : undefined,
|
|
590
|
+
branding: siteBranding,
|
|
591
|
+
analytics: config.analytics,
|
|
592
|
+
features: config.features,
|
|
593
|
+
social: config.social,
|
|
594
|
+
nav: config.nav,
|
|
595
|
+
footerNav: config.footerNav,
|
|
596
|
+
defaults: config.defaults,
|
|
597
|
+
email: config.email,
|
|
598
|
+
pages: processedPages.length > 0 ? processedPages : undefined,
|
|
599
|
+
});
|
|
600
|
+
console.log('Generated site-config.json');
|
|
601
|
+
|
|
602
|
+
const total = Object.values(counts).reduce((s, n) => s + n, 0);
|
|
603
|
+
console.log(`\nDone! Generated data for ${total} concepts across ${registry.length} datasets.`);
|
|
604
|
+
for (const [id, count] of Object.entries(counts)) {
|
|
605
|
+
console.log(` ${id}: ${count} concepts`);
|
|
606
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Site config discovery and loading.
|
|
5
|
+
*
|
|
6
|
+
* Resolution order:
|
|
7
|
+
* 1. SITE_CONFIG env var → file path directly
|
|
8
|
+
* 2. SITE_ID env var → configs/{SITE_ID}.yml
|
|
9
|
+
* 3. --site CLI flag → same as SITE_ID
|
|
10
|
+
* 4. Fallback → site-config.yml in project root
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, existsSync } from 'fs';
|
|
14
|
+
import { resolve, dirname } from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import yaml from 'js-yaml';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const projectRoot = resolve(__dirname, '..');
|
|
20
|
+
|
|
21
|
+
function findConfigFile(args = []) {
|
|
22
|
+
if (process.env.SITE_CONFIG) {
|
|
23
|
+
return resolve(process.env.SITE_CONFIG);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const siteId = process.env.SITE_ID || args.find(a => !a.startsWith('-')) || null;
|
|
27
|
+
if (siteId) {
|
|
28
|
+
// Check project configs/ dir first
|
|
29
|
+
const p = resolve(projectRoot, 'configs', `${siteId}.yml`);
|
|
30
|
+
if (existsSync(p)) return p;
|
|
31
|
+
// Check CWD (deployment repo may have configs locally)
|
|
32
|
+
for (const name of [`${siteId}.yml`, 'site-config.yml']) {
|
|
33
|
+
const cwdP = resolve(process.cwd(), name);
|
|
34
|
+
if (existsSync(cwdP)) return cwdP;
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Site config not found for '${siteId}'. Checked configs/${siteId}.yml, ${siteId}.yml, site-config.yml in CWD`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check CWD first (deployment repos), then project root
|
|
40
|
+
for (const dir of [process.cwd(), projectRoot]) {
|
|
41
|
+
const p = resolve(dir, 'site-config.yml');
|
|
42
|
+
if (existsSync(p)) return p;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
throw new Error('No site config found. Set SITE_CONFIG, SITE_ID, or create site-config.yml');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function loadSiteConfig(args = []) {
|
|
49
|
+
const configPath = findConfigFile(args);
|
|
50
|
+
const raw = yaml.load(readFileSync(configPath, 'utf-8'));
|
|
51
|
+
return { config: raw, configPath };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getProjectRoot() {
|
|
55
|
+
return projectRoot;
|
|
56
|
+
}
|