@glossarist/concept-browser 0.7.76 → 0.7.78
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/astro.config.mjs +1 -0
- package/package.json +1 -1
- package/scripts/bridge-to-astro.mjs +6 -1
- package/scripts/build-edges.js +84 -1
- package/scripts/generate-data.mjs +119 -27
- package/src/__tests__/hyperedge-styling.test.ts +57 -0
- package/src/__tests__/partitive-hyperedge.test.ts +199 -0
- package/src/__tests__/sphere-math.test.ts +56 -0
- package/src/adapters/GraphDataSource.ts +10 -3
- package/src/adapters/UriRouter.ts +5 -0
- package/src/adapters/model-bridge.ts +1 -1
- package/src/adapters/types.ts +26 -0
- package/src/components/AppSidebar.vue +1 -1
- package/src/components/NavIcon.vue +1 -0
- package/src/components/PartitiveHyperedgeList.vue +112 -0
- package/src/components/RelationSphere.vue +2 -36
- package/src/composables/use-concept-edges.ts +77 -5
- package/src/config/use-site-config.ts +3 -0
- package/src/i18n/locales/eng.yml +3 -0
- package/src/pages/[...path].astro +1 -0
- package/src/router/index.ts +6 -0
- package/src/utils/hyperedge-styling.ts +48 -0
- package/src/utils/sphere-math.ts +54 -0
- package/src/views/DatasetView.vue +4 -0
- package/src/views/SourcesView.vue +134 -0
- package/src/views/StatsView.vue +120 -61
package/astro.config.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.78",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
function buildConceptUri(uriBase, registerId, conceptId) {
|
|
2
|
+
return `${uriBase}/${registerId}/concept/${conceptId}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Data bridge: converts public/data/*.json into Astro content collections.
|
|
3
8
|
*
|
|
@@ -77,7 +82,7 @@ for (const reg of registry) {
|
|
|
77
82
|
const conceptEntry = {
|
|
78
83
|
registerId: id,
|
|
79
84
|
conceptId: concept.id,
|
|
80
|
-
uri:
|
|
85
|
+
uri: buildConceptUri(entry.uriBase || '', id, concept.id),
|
|
81
86
|
status: concept.status || 'valid',
|
|
82
87
|
designations: concept.designations || {},
|
|
83
88
|
eng: concept.eng || concept.designations?.eng || '',
|
package/scripts/build-edges.js
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
function buildConceptUri(uriBase, registerId, conceptId) {
|
|
2
|
+
return `${uriBase}/${registerId}/concept/${conceptId}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const VALID_MARKERS = new Set(['double', 'dashed']);
|
|
6
|
+
|
|
7
|
+
function validateHyperedgeMarkers(markers) {
|
|
8
|
+
const out = [];
|
|
9
|
+
for (const m of markers) {
|
|
10
|
+
if (!VALID_MARKERS.has(m)) {
|
|
11
|
+
throw new Error(`Invalid partitive hyperedge marker: "${m}". Allowed: double, dashed`);
|
|
12
|
+
}
|
|
13
|
+
out.push(m);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeHyperedgeContent(content) {
|
|
19
|
+
if (content == null) return undefined;
|
|
20
|
+
if (typeof content === 'string') return { default: content };
|
|
21
|
+
return content;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
1
25
|
/**
|
|
2
26
|
* Pre-computes cross-reference and domain edges for each dataset.
|
|
3
27
|
* Reads all concept JSON files, extracts structured references and
|
|
@@ -108,7 +132,7 @@ function extractRelated(concept, registerId, uriBase, urnMap) {
|
|
|
108
132
|
const id = ref['gl:id'] || ref['id'];
|
|
109
133
|
if (!source || !id) continue;
|
|
110
134
|
const reg = urnMap.get(source) || source;
|
|
111
|
-
const target =
|
|
135
|
+
const target = buildConceptUri(uriBase, reg, id);
|
|
112
136
|
if (target === sourceUri) continue;
|
|
113
137
|
edges.push({
|
|
114
138
|
source: sourceUri,
|
|
@@ -121,6 +145,52 @@ function extractRelated(concept, registerId, uriBase, urnMap) {
|
|
|
121
145
|
return edges;
|
|
122
146
|
}
|
|
123
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Extracts partitive hyperedges from a concept's gl:partitiveHyperedges
|
|
150
|
+
* array. Returns hyperedge objects (NOT binary GraphEdges) so the UI
|
|
151
|
+
* can render them with their full set-based semantics: comprehensive +
|
|
152
|
+
* parts together, enumeration completeness (open vs closed), and
|
|
153
|
+
* diagram plurality markers (double / dashed).
|
|
154
|
+
*
|
|
155
|
+
* Hyperedges are written to a separate hyperedges.json file, keeping
|
|
156
|
+
* edges.json binary-only.
|
|
157
|
+
*/
|
|
158
|
+
function extractPartitiveHyperedges(concept, registerId, uriBase, urnMap) {
|
|
159
|
+
const hyperedges = [];
|
|
160
|
+
const sourceUri = concept['@id'];
|
|
161
|
+
for (const he of concept['gl:partitiveHyperedges'] || []) {
|
|
162
|
+
const compRef = he['gl:comprehensive'];
|
|
163
|
+
if (!compRef) continue;
|
|
164
|
+
const comprehensive = resolveConceptUri(compRef, uriBase, urnMap);
|
|
165
|
+
if (!comprehensive) continue;
|
|
166
|
+
|
|
167
|
+
const parts = (he['gl:hasPart'] || [])
|
|
168
|
+
.map(p => resolveConceptUri(p, uriBase, urnMap))
|
|
169
|
+
.filter(p => p && p !== sourceUri);
|
|
170
|
+
|
|
171
|
+
if (parts.length === 0) continue;
|
|
172
|
+
|
|
173
|
+
hyperedges.push({
|
|
174
|
+
source: sourceUri,
|
|
175
|
+
comprehensive,
|
|
176
|
+
parts,
|
|
177
|
+
enumeration: he['gl:enumeration'] || 'closed',
|
|
178
|
+
markers: validateHyperedgeMarkers(he['gl:hasPluralityMarker'] || []),
|
|
179
|
+
label: normalizeHyperedgeContent(he['gl:content']),
|
|
180
|
+
register: registerId,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return hyperedges;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function resolveConceptUri(ref, uriBase, urnMap) {
|
|
187
|
+
const source = ref['gl:source'] || ref['source'];
|
|
188
|
+
const id = ref['gl:id'] || ref['id'];
|
|
189
|
+
if (!source || !id) return null;
|
|
190
|
+
const reg = urnMap.get(source) || source;
|
|
191
|
+
return buildConceptUri(uriBase, reg, id);
|
|
192
|
+
}
|
|
193
|
+
|
|
124
194
|
const EXTRACTORS = [extractReferences, extractRelated, extractDomains];
|
|
125
195
|
|
|
126
196
|
function extractAllEdges(concept, registerId, uriBase, urnMap) {
|
|
@@ -140,6 +210,7 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
140
210
|
console.log(` Processing ${files.length} concepts...`);
|
|
141
211
|
|
|
142
212
|
const allEdges = [];
|
|
213
|
+
const allHyperedges = [];
|
|
143
214
|
const allSourceRefs = [];
|
|
144
215
|
const domainConceptCount = new Map();
|
|
145
216
|
let processed = 0;
|
|
@@ -149,6 +220,7 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
149
220
|
const data = JSON.parse(readFileSync(join(conceptsDir, file), 'utf-8'));
|
|
150
221
|
const edges = extractAllEdges(data, registerId, uriBase, urnMap);
|
|
151
222
|
allEdges.push(...edges);
|
|
223
|
+
allHyperedges.push(...extractPartitiveHyperedges(data, registerId, uriBase, urnMap));
|
|
152
224
|
allSourceRefs.push(...extractSourceRefs(data, registerId));
|
|
153
225
|
|
|
154
226
|
for (const edge of edges) {
|
|
@@ -186,6 +258,17 @@ function buildEdgesForDataset(datasetDir, registerId, uriBase, urnMap, manifest)
|
|
|
186
258
|
writeFileSync(outputPath, JSON.stringify(output));
|
|
187
259
|
console.log(` Written ${deduped.length} edges to edges.json (${(JSON.stringify(output).length / 1024).toFixed(1)} KB)`);
|
|
188
260
|
|
|
261
|
+
// Write hyperedges.json (parallel to edges.json) for one-to-many
|
|
262
|
+
// partitive decompositions. See TODO.hyperedge/00-design-overview.md.
|
|
263
|
+
const hyperedgesOutput = {
|
|
264
|
+
registerId,
|
|
265
|
+
hyperedgeCount: allHyperedges.length,
|
|
266
|
+
hyperedges: allHyperedges,
|
|
267
|
+
};
|
|
268
|
+
const hyperedgesPath = join(datasetDir, 'hyperedges.json');
|
|
269
|
+
writeFileSync(hyperedgesPath, JSON.stringify(hyperedgesOutput));
|
|
270
|
+
console.log(` Written ${allHyperedges.length} hyperedges to hyperedges.json (${(JSON.stringify(hyperedgesOutput).length / 1024).toFixed(1)} KB)`);
|
|
271
|
+
|
|
189
272
|
// Build domain-nodes.json from manifest sections (authoritative source)
|
|
190
273
|
const manifestSections = manifest.sections;
|
|
191
274
|
if (manifestSections && manifestSections.length > 0) {
|
|
@@ -14,6 +14,11 @@ import { buildVersionHistoryTurtle } from './lib/version-turtle.mjs';
|
|
|
14
14
|
import { buildBibliographyTurtle } from './lib/bibliography-turtle.mjs';
|
|
15
15
|
import { ttlLit } from './lib/turtle-escape.mjs';
|
|
16
16
|
import { firstNonEmpty } from './lib/first-non-empty.mjs';
|
|
17
|
+
function buildConceptUri(uriBase, registerId, conceptId) {
|
|
18
|
+
return `${uriBase}/${registerId}/concept/${conceptId}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
17
22
|
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
18
23
|
const ROOT = process.cwd();
|
|
19
24
|
const PUBLIC = path.join(ROOT, 'public');
|
|
@@ -60,6 +65,7 @@ function loadConceptFile(filePath) {
|
|
|
60
65
|
|
|
61
66
|
// Managed concept-level fields
|
|
62
67
|
if (mc.related) result._related = mc.related;
|
|
68
|
+
if (mc.partitive_hyperedges) result._partitiveHyperedges = mc.partitive_hyperedges;
|
|
63
69
|
if (mc.data.domains) result._domains = mc.data.domains;
|
|
64
70
|
if (mc.dates) result._dates = mc.dates;
|
|
65
71
|
if (mc.sources) result._sources = mc.sources;
|
|
@@ -98,14 +104,14 @@ async function writeDatasetRdf(register, manifest, concepts, refMaps, opts) {
|
|
|
98
104
|
const datasetIri = `${uriBase}/${register}/`;
|
|
99
105
|
const topConceptUris = concepts
|
|
100
106
|
.slice(0, 32)
|
|
101
|
-
.map(c =>
|
|
107
|
+
.map(c => buildConceptUri(uriBase, register, c.id));
|
|
102
108
|
|
|
103
109
|
const sections = (manifest.sections ?? []).map(section => {
|
|
104
110
|
const sectionId = section.id ?? section.slug ?? section.title;
|
|
105
111
|
return {
|
|
106
112
|
collectionIri: `${uriBase}/${register}/section/${sectionId}`,
|
|
107
113
|
title: section.title ?? section.name ?? sectionId,
|
|
108
|
-
memberUris: (section.members ?? []).map(id =>
|
|
114
|
+
memberUris: (section.members ?? []).map(id => buildConceptUri(uriBase, register, id)),
|
|
109
115
|
};
|
|
110
116
|
});
|
|
111
117
|
|
|
@@ -242,15 +248,16 @@ function defsToJsonLd(defs) {
|
|
|
242
248
|
.filter(d => d['gl:content']);
|
|
243
249
|
}
|
|
244
250
|
|
|
245
|
-
function refToJsonLd(ref) {
|
|
251
|
+
function refToJsonLd(ref, typeName = 'gl:Ref') {
|
|
246
252
|
if (!ref) return undefined;
|
|
247
|
-
const refObj = { '@type':
|
|
253
|
+
const refObj = { '@type': typeName };
|
|
248
254
|
if (typeof ref === 'string') {
|
|
249
255
|
refObj['gl:source'] = ref;
|
|
250
256
|
} else {
|
|
251
257
|
if (ref.source) refObj['gl:source'] = ref.source;
|
|
252
258
|
if (ref.id) refObj['gl:id'] = ref.id;
|
|
253
259
|
if (ref.version) refObj['gl:version'] = ref.version;
|
|
260
|
+
if (ref.text) refObj['gl:text'] = ref.text;
|
|
254
261
|
}
|
|
255
262
|
return refObj;
|
|
256
263
|
}
|
|
@@ -366,12 +373,12 @@ function buildPatternIndex(datasets, registerCache) {
|
|
|
366
373
|
|
|
367
374
|
function resolveRefUri(term, refMaps) {
|
|
368
375
|
const resolved = refMaps.patternIndex.resolve(term);
|
|
369
|
-
if (resolved) return
|
|
376
|
+
if (resolved) return buildConceptUri(refMaps.uriBase, resolved.datasetId, resolved.conceptId);
|
|
370
377
|
|
|
371
378
|
const ievMatch = term.match(/^IEV:(\d+[-\d]+)$/);
|
|
372
379
|
if (ievMatch) {
|
|
373
380
|
const dsId = refMaps.refPrefixMap['IEV'];
|
|
374
|
-
if (dsId) return
|
|
381
|
+
if (dsId) return buildConceptUri(refMaps.uriBase, dsId, ievMatch[1]);
|
|
375
382
|
}
|
|
376
383
|
return null;
|
|
377
384
|
}
|
|
@@ -405,7 +412,7 @@ function resolveIevRef(display, term, refPrefixMap, uriBase) {
|
|
|
405
412
|
if (!display.startsWith('IEV:')) return null;
|
|
406
413
|
const datasetId = refPrefixMap['IEV'];
|
|
407
414
|
if (!datasetId) return null;
|
|
408
|
-
return { id:
|
|
415
|
+
return { id: buildConceptUri(uriBase, datasetId, display.slice(4)), term };
|
|
409
416
|
}
|
|
410
417
|
|
|
411
418
|
/**
|
|
@@ -414,7 +421,7 @@ function resolveIevRef(display, term, refPrefixMap, uriBase) {
|
|
|
414
421
|
function resolvePatternRef(identifier, display, refPrefixMap, patternIndex, uriBase) {
|
|
415
422
|
const r = patternIndex.resolve(identifier);
|
|
416
423
|
if (!r) return null;
|
|
417
|
-
return { id:
|
|
424
|
+
return { id: buildConceptUri(uriBase, r.datasetId, r.conceptId), term: display };
|
|
418
425
|
}
|
|
419
426
|
|
|
420
427
|
/**
|
|
@@ -444,7 +451,7 @@ function handleCiteRef(parsed, allSources) {
|
|
|
444
451
|
function handleNumeric(parsed, register, uriBase) {
|
|
445
452
|
if (!register) return null;
|
|
446
453
|
const term = parsed.label ?? parsed.id;
|
|
447
|
-
return { id:
|
|
454
|
+
return { id: buildConceptUri(uriBase, register, parsed.id), term };
|
|
448
455
|
}
|
|
449
456
|
|
|
450
457
|
/**
|
|
@@ -459,7 +466,7 @@ function handleDesignation(parsed, refMaps) {
|
|
|
459
466
|
const display = parsed.label ?? designation;
|
|
460
467
|
const conceptId = refMaps.designationLookup?.get(designation.toLowerCase());
|
|
461
468
|
return {
|
|
462
|
-
id:
|
|
469
|
+
id: buildConceptUri(refMaps.uriBase, register, conceptId ?? designation),
|
|
463
470
|
term: display,
|
|
464
471
|
};
|
|
465
472
|
}
|
|
@@ -485,7 +492,7 @@ function handleUnresolved(body, refMaps) {
|
|
|
485
492
|
// Same-dataset: {{conceptId, displayTerm}} where conceptId is numeric/X.Y
|
|
486
493
|
const register = refMaps.register;
|
|
487
494
|
if (register && (/^\d/.test(identifier) || /^[A-Z]\.\d/.test(identifier))) {
|
|
488
|
-
return { id:
|
|
495
|
+
return { id: buildConceptUri(refMaps.uriBase, register, identifier), term: display };
|
|
489
496
|
}
|
|
490
497
|
return null;
|
|
491
498
|
}
|
|
@@ -542,7 +549,7 @@ function extractInlineRefs(localizedData, refMaps, conceptSources = []) {
|
|
|
542
549
|
const term = parsed.label ?? uri;
|
|
543
550
|
const pattern = refMaps.patternIndex.resolve(uri);
|
|
544
551
|
if (pattern) {
|
|
545
|
-
ref = { id:
|
|
552
|
+
ref = { id: buildConceptUri(refMaps.uriBase, pattern.datasetId, pattern.conceptId), term };
|
|
546
553
|
} else {
|
|
547
554
|
ref = { id: uri, term };
|
|
548
555
|
}
|
|
@@ -571,7 +578,7 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
|
|
|
571
578
|
const base = refMaps.uriBase;
|
|
572
579
|
const doc = {
|
|
573
580
|
'@context': 'https://glossarist.org/ns/context.jsonld',
|
|
574
|
-
'@id':
|
|
581
|
+
'@id': buildConceptUri(base, register, termid),
|
|
575
582
|
'@type': 'gl:Concept',
|
|
576
583
|
'gl:identifier': termid,
|
|
577
584
|
};
|
|
@@ -672,6 +679,28 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
|
|
|
672
679
|
});
|
|
673
680
|
}
|
|
674
681
|
|
|
682
|
+
if (conceptYaml._partitiveHyperedges?.length > 0) {
|
|
683
|
+
doc['gl:partitiveHyperedges'] = conceptYaml._partitiveHyperedges.map(he => {
|
|
684
|
+
const out = { '@type': 'gl:PartitiveHyperedge' };
|
|
685
|
+
if (he.comprehensive) {
|
|
686
|
+
out['gl:comprehensive'] = refToJsonLd(he.comprehensive, 'gl:ConceptRef');
|
|
687
|
+
}
|
|
688
|
+
if (Array.isArray(he.parts) && he.parts.length > 0) {
|
|
689
|
+
out['gl:hasPart'] = he.parts.map(p => refToJsonLd(p, 'gl:ConceptRef'));
|
|
690
|
+
}
|
|
691
|
+
if (he.enumeration) {
|
|
692
|
+
out['gl:enumeration'] = he.enumeration;
|
|
693
|
+
}
|
|
694
|
+
if (Array.isArray(he.markers) && he.markers.length > 0) {
|
|
695
|
+
out['gl:hasPluralityMarker'] = [...he.markers];
|
|
696
|
+
}
|
|
697
|
+
if (he.content) {
|
|
698
|
+
out['gl:content'] = typeof he.content === 'string' ? { default: he.content } : he.content;
|
|
699
|
+
}
|
|
700
|
+
return out;
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
675
704
|
return doc;
|
|
676
705
|
}
|
|
677
706
|
|
|
@@ -920,6 +949,61 @@ async function processDataset(dir, register, opts) {
|
|
|
920
949
|
|
|
921
950
|
const dsRefMaps = { ...refMaps, register, designationLookup };
|
|
922
951
|
|
|
952
|
+
const stats = {
|
|
953
|
+
sourceMap: new Map(),
|
|
954
|
+
relTypeCounts: {},
|
|
955
|
+
partitiveHyperedges: { count: 0, byEnumeration: { closed: 0, open: 0 }, byMarker: { double: 0, dashed: 0, none: 0 } },
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
const STATS_PROCESSORS = [
|
|
959
|
+
function collectSources(cy, termid, s) {
|
|
960
|
+
const refs = new Set();
|
|
961
|
+
const allSources = [
|
|
962
|
+
...(cy._sources || []),
|
|
963
|
+
...opts.languages.flatMap(l => cy[l]?.sources || []),
|
|
964
|
+
];
|
|
965
|
+
for (const src of allSources) {
|
|
966
|
+
const ref = src.origin?.ref?.source || src.origin?.ref?.id || 'Unknown';
|
|
967
|
+
refs.add(ref);
|
|
968
|
+
if (!s.sourceMap.has(ref)) s.sourceMap.set(ref, { ref, types: new Set(), conceptIds: [] });
|
|
969
|
+
if (src.type) s.sourceMap.get(ref).types.add(src.type);
|
|
970
|
+
}
|
|
971
|
+
for (const ref of refs) s.sourceMap.get(ref)?.conceptIds.push(termid);
|
|
972
|
+
},
|
|
973
|
+
function countRelationships(cy, _termid, s) {
|
|
974
|
+
for (const r of cy._related || []) {
|
|
975
|
+
const type = r.type || 'unknown';
|
|
976
|
+
s.relTypeCounts[type] = (s.relTypeCounts[type] || 0) + 1;
|
|
977
|
+
}
|
|
978
|
+
},
|
|
979
|
+
function countHyperedges(cy, _termid, s) {
|
|
980
|
+
for (const he of cy._partitiveHyperedges || []) {
|
|
981
|
+
s.partitiveHyperedges.count += 1;
|
|
982
|
+
const e = he.enumeration || 'closed';
|
|
983
|
+
s.partitiveHyperedges.byEnumeration[e] = (s.partitiveHyperedges.byEnumeration[e] || 0) + 1;
|
|
984
|
+
const markers = he.markers || [];
|
|
985
|
+
if (markers.length === 0) {
|
|
986
|
+
s.partitiveHyperedges.byMarker.none += 1;
|
|
987
|
+
} else {
|
|
988
|
+
for (const m of markers) {
|
|
989
|
+
s.partitiveHyperedges.byMarker[m] = (s.partitiveHyperedges.byMarker[m] || 0) + 1;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
},
|
|
994
|
+
function countLanguages(cy, _termid, _s) {
|
|
995
|
+
for (const lang of opts.languages) {
|
|
996
|
+
const lc = cy[lang];
|
|
997
|
+
if (lc) {
|
|
998
|
+
if (lc.terms?.length > 0) langTermCounts[lang] = (langTermCounts[lang] || 0) + 1;
|
|
999
|
+
if (lc.definition && (Array.isArray(lc.definition) ? lc.definition.some(d => d.content) : lc.definition)) {
|
|
1000
|
+
langDefCounts[lang] = (langDefCounts[lang] || 0) + 1;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
];
|
|
1006
|
+
|
|
923
1007
|
for (let i = 0; i < files.length; i++) {
|
|
924
1008
|
const file = files[i];
|
|
925
1009
|
try {
|
|
@@ -930,21 +1014,17 @@ async function processDataset(dir, register, opts) {
|
|
|
930
1014
|
const jsonld = yamlToJsonLd(conceptYaml, register, dsRefMaps);
|
|
931
1015
|
writeJson(path.join(conceptsDir, `${termid}.json`), jsonld);
|
|
932
1016
|
|
|
933
|
-
// Generate Turtle format
|
|
934
1017
|
const ttlContent = conceptJsonToTurtle(jsonld);
|
|
935
1018
|
fs.writeFileSync(path.join(conceptsDir, `${termid}.ttl`), ttlContent);
|
|
936
1019
|
|
|
937
|
-
// Generate SKOS JSON-LD format
|
|
938
1020
|
const skosJsonLd = conceptJsonToSkosJsonLd(jsonld);
|
|
939
1021
|
fs.writeFileSync(path.join(conceptsDir, `${termid}.jsonld`), skosJsonLd);
|
|
940
1022
|
|
|
941
|
-
// Generate TBX-XML format
|
|
942
1023
|
const tbxContent = conceptJsonToTbx(jsonld);
|
|
943
1024
|
if (tbxContent) {
|
|
944
1025
|
fs.writeFileSync(path.join(conceptsDir, `${termid}.tbx`), tbxContent);
|
|
945
1026
|
}
|
|
946
1027
|
|
|
947
|
-
// Copy source YAML
|
|
948
1028
|
fs.copyFileSync(path.join(dir, file), path.join(conceptsDir, `${termid}.yaml`));
|
|
949
1029
|
|
|
950
1030
|
concepts.push({
|
|
@@ -954,16 +1034,8 @@ async function processDataset(dir, register, opts) {
|
|
|
954
1034
|
status: conceptYaml.eng?.entry_status || 'valid',
|
|
955
1035
|
});
|
|
956
1036
|
|
|
957
|
-
for (const
|
|
958
|
-
|
|
959
|
-
if (lc) {
|
|
960
|
-
if (lc.terms && lc.terms.length > 0) {
|
|
961
|
-
langTermCounts[lang] = (langTermCounts[lang] || 0) + 1;
|
|
962
|
-
}
|
|
963
|
-
if (lc.definition && (Array.isArray(lc.definition) ? lc.definition.some(d => d.content) : lc.definition)) {
|
|
964
|
-
langDefCounts[lang] = (langDefCounts[lang] || 0) + 1;
|
|
965
|
-
}
|
|
966
|
-
}
|
|
1037
|
+
for (const processor of STATS_PROCESSORS) {
|
|
1038
|
+
processor(conceptYaml, termid, stats);
|
|
967
1039
|
}
|
|
968
1040
|
} catch (e) {
|
|
969
1041
|
console.warn(` Skipping ${file}: ${e.message}`);
|
|
@@ -1072,6 +1144,24 @@ async function processDataset(dir, register, opts) {
|
|
|
1072
1144
|
}
|
|
1073
1145
|
}
|
|
1074
1146
|
|
|
1147
|
+
const sourceStats = Array.from(stats.sourceMap.values())
|
|
1148
|
+
.map(s => ({
|
|
1149
|
+
ref: s.ref,
|
|
1150
|
+
types: Array.from(s.types),
|
|
1151
|
+
conceptCount: s.conceptIds.length,
|
|
1152
|
+
conceptIds: s.conceptIds,
|
|
1153
|
+
}))
|
|
1154
|
+
.sort((a, b) => b.conceptCount - a.conceptCount);
|
|
1155
|
+
const totalRelationships = Object.values(stats.relTypeCounts).reduce((a, b) => a + b, 0);
|
|
1156
|
+
|
|
1157
|
+
writeJson(path.join(DATA, register, 'stats.json'), {
|
|
1158
|
+
sourceCount: sourceStats.length,
|
|
1159
|
+
sources: sourceStats,
|
|
1160
|
+
relationshipCount: totalRelationships,
|
|
1161
|
+
relationshipTypes: stats.relTypeCounts,
|
|
1162
|
+
partitiveHyperedges: stats.partitiveHyperedges,
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1075
1165
|
const manifest = {
|
|
1076
1166
|
id: register,
|
|
1077
1167
|
datasetUri: opts.datasetUri,
|
|
@@ -1082,6 +1172,8 @@ async function processDataset(dir, register, opts) {
|
|
|
1082
1172
|
baseUrl: `/data/${register}`,
|
|
1083
1173
|
languages: opts.languages,
|
|
1084
1174
|
conceptCount: concepts.length,
|
|
1175
|
+
sourceCount: sourceStats.length,
|
|
1176
|
+
relationshipCount: totalRelationships,
|
|
1085
1177
|
conceptUrlTemplate: '{baseUrl}/concepts/{conceptId}.json',
|
|
1086
1178
|
indexUrl: '{baseUrl}/index.json',
|
|
1087
1179
|
contextUrl: 'https://glossarist.org/ns/context.jsonld',
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { hyperedgeStyle, markerColor, enumerationLabel } from '../utils/hyperedge-styling';
|
|
3
|
+
|
|
4
|
+
describe('hyperedge-styling', () => {
|
|
5
|
+
describe('hyperedgeStyle', () => {
|
|
6
|
+
it('returns full opacity for closed enumeration', () => {
|
|
7
|
+
expect(hyperedgeStyle('closed', [], false).opacity).toBe(1.0);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('returns reduced opacity for open enumeration', () => {
|
|
11
|
+
expect(hyperedgeStyle('open', [], false).opacity).toBe(0.6);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('uses teal badge class for closed with no markers', () => {
|
|
15
|
+
expect(hyperedgeStyle('closed', [], false).badgeClass).toBe('badge-teal');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('uses blue badge class for double marker', () => {
|
|
19
|
+
expect(hyperedgeStyle('closed', ['double'], false).badgeClass).toBe('badge-blue');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('uses yellow badge class for dashed marker', () => {
|
|
23
|
+
expect(hyperedgeStyle('closed', ['dashed'], false).badgeClass).toBe('badge-yellow');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns different colors in dark mode', () => {
|
|
27
|
+
const light = hyperedgeStyle('closed', [], false);
|
|
28
|
+
const dark = hyperedgeStyle('closed', [], true);
|
|
29
|
+
expect(light.color).not.toBe(dark.color);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('markerColor', () => {
|
|
34
|
+
it('returns blue for double marker', () => {
|
|
35
|
+
expect(markerColor('double', false)).toBe('#3b82f6');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns amber for dashed marker', () => {
|
|
39
|
+
expect(markerColor('dashed', false)).toBe('#f59e0b');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns dark-mode variants', () => {
|
|
43
|
+
expect(markerColor('double', true)).toBe('#60a5fa');
|
|
44
|
+
expect(markerColor('dashed', true)).toBe('#fbbf24');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('enumerationLabel', () => {
|
|
49
|
+
it('labels closed as "Closed (complete)"', () => {
|
|
50
|
+
expect(enumerationLabel('closed')).toBe('Closed (complete)');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('labels open as "Open (partial)"', () => {
|
|
54
|
+
expect(enumerationLabel('open')).toBe('Open (partial)');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|