@glossarist/concept-browser 0.5.0 → 0.6.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 +32 -0
- package/cli/index.mjs +21 -0
- package/package.json +1 -1
- package/scripts/generate-data.mjs +43 -1
- package/scripts/generate-ontology-schema.mjs +312 -10
- package/src/App.vue +3 -0
- package/src/__tests__/concept-card.test.ts +16 -2
- package/src/__tests__/markdown-lite.test.ts +26 -0
- package/src/adapters/factory.ts +3 -2
- package/src/adapters/model-bridge.ts +2 -0
- package/src/adapters/ontology-schema.ts +89 -4
- package/src/adapters/types.ts +1 -0
- package/src/components/AppFooter.vue +3 -1
- package/src/components/AppHeader.vue +46 -4
- package/src/components/AppSidebar.vue +286 -46
- package/src/components/ConceptCard.vue +16 -4
- package/src/components/ConceptDetail.vue +42 -35
- package/src/components/ConceptRdfView.vue +3 -3
- package/src/components/ConceptTimeline.vue +2 -14
- package/src/components/GraphPanel.vue +19 -0
- package/src/components/LanguageDetail.vue +11 -8
- package/src/composables/use-ontology-nav.ts +183 -13
- package/src/composables/use-render-options.ts +2 -2
- package/src/config/types.ts +1 -0
- package/src/config/use-site-config.ts +3 -2
- package/src/data/ontology-schema.json +1721 -153
- package/src/i18n/index.ts +49 -0
- package/src/i18n/locales/eng.yml +66 -0
- package/src/i18n/locales/fra.yml +66 -0
- package/src/router/index.ts +10 -0
- package/src/shims/glossarist-tags.ts +10 -0
- package/src/stores/vocabulary.ts +1 -1
- package/src/style.css +12 -0
- package/src/utils/lang.ts +13 -0
- package/src/utils/markdown-lite.ts +15 -0
- package/src/views/AboutView.vue +1 -1
- package/src/views/ContributorsView.vue +1 -1
- package/src/views/DatasetView.vue +77 -6
- package/src/views/HomeView.vue +21 -17
- package/src/views/NewsView.vue +2 -2
- package/src/views/OntologySchemaView.vue +331 -14
- package/src/views/PageView.vue +27 -11
- package/src/views/ResolveView.vue +1 -1
- package/src/views/SearchView.vue +4 -2
- package/src/views/StatsView.vue +1 -1
- package/vite.config.ts +34 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { Concept, LocalizedConcept, Designation, Expression, ConceptSource } from 'glossarist';
|
|
3
3
|
import type { Manifest, GraphEdge } from '../adapters/types';
|
|
4
4
|
import { computed, ref, nextTick, watch } from 'vue';
|
|
5
|
-
import { langName, langLabel } from '../utils/lang';
|
|
5
|
+
import { langName, langLabel, sortLanguages } from '../utils/lang';
|
|
6
6
|
import { renderMath, cleanContent } from '../utils/math';
|
|
7
7
|
import type { RenderOptions } from '../utils/math';
|
|
8
8
|
import { escapeAttr } from '../utils/escape';
|
|
@@ -15,11 +15,15 @@ import { useDsStyle } from '../utils/dataset-style';
|
|
|
15
15
|
import { getFactory } from '../adapters/factory';
|
|
16
16
|
import { useRenderOptions } from '../composables/use-render-options';
|
|
17
17
|
import { categorizeRelationship, relationshipLabel, relationshipDefinition } from '../utils/relationship-categories';
|
|
18
|
+
import { useSiteConfig } from '../config/use-site-config';
|
|
18
19
|
import ConceptTimeline from './ConceptTimeline.vue';
|
|
19
20
|
import ConceptRdfView from './ConceptRdfView.vue';
|
|
20
21
|
import FormatDownloads from './FormatDownloads.vue';
|
|
21
22
|
import NonVerbalRepDisplay from './NonVerbalRepDisplay.vue';
|
|
22
23
|
import CitationDisplay from './CitationDisplay.vue';
|
|
24
|
+
import { useI18n } from '../i18n';
|
|
25
|
+
|
|
26
|
+
const { t } = useI18n();
|
|
23
27
|
|
|
24
28
|
const props = defineProps<{
|
|
25
29
|
concept: Concept;
|
|
@@ -32,6 +36,7 @@ const props = defineProps<{
|
|
|
32
36
|
const router = useRouter();
|
|
33
37
|
const store = useVocabularyStore();
|
|
34
38
|
const { getColor } = useDsStyle();
|
|
39
|
+
const { config: siteConfig } = useSiteConfig();
|
|
35
40
|
const factory = getFactory();
|
|
36
41
|
|
|
37
42
|
const activeTab = ref<'rdf' | 'definition' | 'history'>('definition');
|
|
@@ -57,35 +62,12 @@ function copyUri() {
|
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
const languages = computed(() => {
|
|
60
|
-
|
|
61
|
-
const keys = props.concept.languages;
|
|
62
|
-
if (!order) {
|
|
63
|
-
return [...keys].sort((a, b) => {
|
|
64
|
-
if (a === 'eng') return -1;
|
|
65
|
-
if (a === 'eng') return 1;
|
|
66
|
-
return a.localeCompare(b);
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
const orderIndex = new Map(order.map((lang, i) => [lang, i]));
|
|
70
|
-
return [...keys].sort((a, b) => {
|
|
71
|
-
const ai = orderIndex.get(a) ?? order.length;
|
|
72
|
-
const bi = orderIndex.get(b) ?? order.length;
|
|
73
|
-
if (ai !== bi) return ai - bi;
|
|
74
|
-
return a.localeCompare(b);
|
|
75
|
-
});
|
|
65
|
+
return sortLanguages(props.concept.languages, props.manifest.languageOrder);
|
|
76
66
|
});
|
|
77
67
|
|
|
78
|
-
// Collapsible language sections —
|
|
68
|
+
// Collapsible language sections — expand all with content, collapse those without
|
|
79
69
|
const collapsedLangs = ref(new Set<string>());
|
|
80
70
|
|
|
81
|
-
function initCollapsed(langs: string[]) {
|
|
82
|
-
if (langs.length >= 6) {
|
|
83
|
-
collapsedLangs.value = new Set(langs.filter(l => l !== 'eng'));
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
watch(languages, (langs) => { initCollapsed(langs); }, { immediate: true });
|
|
88
|
-
|
|
89
71
|
const engConcept = computed((): LocalizedConcept | null => {
|
|
90
72
|
return props.concept.localization('eng') ?? null;
|
|
91
73
|
});
|
|
@@ -104,6 +86,9 @@ const conceptDates = computed(() => props.concept.dates);
|
|
|
104
86
|
// Managed concept sources (distinct from localized sources)
|
|
105
87
|
const conceptSources = computed(() => props.concept.sources);
|
|
106
88
|
|
|
89
|
+
// Managed concept tags
|
|
90
|
+
const conceptTags = computed(() => props.concept.tags ?? []);
|
|
91
|
+
|
|
107
92
|
// Cross-reference resolver: generates clickable links for inline refs
|
|
108
93
|
|
|
109
94
|
const { ensureBibLoaded, bibResolver, figResolver } = useRenderOptions(() => props.registerId);
|
|
@@ -192,6 +177,20 @@ function hasContent(lc: LangContent): boolean {
|
|
|
192
177
|
return !!(lc.definition || lc.notes.length || lc.examples.length || lc.sources.length);
|
|
193
178
|
}
|
|
194
179
|
|
|
180
|
+
function initCollapsed() {
|
|
181
|
+
const mainLangs = siteConfig.value?.defaults?.mainLanguages || [];
|
|
182
|
+
const mainSet = new Set(mainLangs.length ? mainLangs : ['eng']);
|
|
183
|
+
const collapsed = new Set<string>();
|
|
184
|
+
for (const lc of allLangContent.value) {
|
|
185
|
+
if (!hasContent(lc) && !mainSet.has(lc.lang)) {
|
|
186
|
+
collapsed.add(lc.lang);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
collapsedLangs.value = collapsed;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
watch(languages, () => { initCollapsed(); }, { immediate: true });
|
|
193
|
+
|
|
195
194
|
const allCollapsed = computed(() => collapsedLangs.value.size === allLangContent.value.length);
|
|
196
195
|
|
|
197
196
|
function toggleLang(lang: string) {
|
|
@@ -567,7 +566,7 @@ const nonVerbalReps = computed(() => {
|
|
|
567
566
|
<!-- Notes -->
|
|
568
567
|
<div v-if="lc.notes.length" class="space-y-2">
|
|
569
568
|
<div v-for="(note, i) in lc.notes" :key="i" class="text-ink-600 text-sm leading-relaxed">
|
|
570
|
-
<span class="font-medium text-ink-400 text-xs uppercase tracking-wide">
|
|
569
|
+
<span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.note') }} {{ i + 1 }}</span>
|
|
571
570
|
<div class="mt-1" v-html="renderMath(note, renderOpts)"></div>
|
|
572
571
|
</div>
|
|
573
572
|
</div>
|
|
@@ -575,7 +574,7 @@ const nonVerbalReps = computed(() => {
|
|
|
575
574
|
<!-- Examples -->
|
|
576
575
|
<div v-if="lc.examples.length" class="space-y-2">
|
|
577
576
|
<div v-for="(ex, i) in lc.examples" :key="i" class="text-ink-600 text-sm leading-relaxed">
|
|
578
|
-
<span class="font-medium text-ink-400 text-xs uppercase tracking-wide">
|
|
577
|
+
<span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.example') }} {{ i + 1 }}</span>
|
|
579
578
|
<div class="mt-1" v-html="renderMath(ex, renderOpts)"></div>
|
|
580
579
|
</div>
|
|
581
580
|
</div>
|
|
@@ -639,9 +638,9 @@ const nonVerbalReps = computed(() => {
|
|
|
639
638
|
<div class="w-full lg:w-64 flex-shrink-0 space-y-4 mt-6 lg:mt-0">
|
|
640
639
|
<!-- Relations -->
|
|
641
640
|
<div v-if="outgoingEdges.length || incomingEdges.length" class="card p-5">
|
|
642
|
-
<div class="section-label">
|
|
641
|
+
<div class="section-label">{{ t('concept.relations') }}</div>
|
|
643
642
|
<div v-if="outgoingEdges.length" class="mt-3">
|
|
644
|
-
<div class="text-xs text-ink-300 mb-2">
|
|
643
|
+
<div class="text-xs text-ink-300 mb-2">{{ t('concept.outgoing') }} ({{ outgoingEdges.length }})</div>
|
|
645
644
|
<div class="space-y-1 max-h-64 overflow-y-auto">
|
|
646
645
|
<button
|
|
647
646
|
v-for="edge in outgoingEdges"
|
|
@@ -658,7 +657,7 @@ const nonVerbalReps = computed(() => {
|
|
|
658
657
|
</div>
|
|
659
658
|
</div>
|
|
660
659
|
<div v-if="incomingEdges.length" class="mt-3 pt-3 border-t border-ink-100/60">
|
|
661
|
-
<div class="text-xs text-ink-300 mb-2">
|
|
660
|
+
<div class="text-xs text-ink-300 mb-2">{{ t('concept.incoming') }} ({{ incomingEdges.length }})</div>
|
|
662
661
|
<div class="space-y-1 max-h-48 overflow-y-auto">
|
|
663
662
|
<button
|
|
664
663
|
v-for="edge in incomingEdges"
|
|
@@ -677,7 +676,7 @@ const nonVerbalReps = computed(() => {
|
|
|
677
676
|
|
|
678
677
|
<!-- Domains -->
|
|
679
678
|
<div v-if="conceptDomains.length" class="card p-5">
|
|
680
|
-
<div class="section-label">
|
|
679
|
+
<div class="section-label">{{ t('concept.domains') }}</div>
|
|
681
680
|
<div class="space-y-1 mt-3">
|
|
682
681
|
<div v-for="domain in conceptDomains" :key="domain.slug" class="flex items-center gap-1.5 text-sm">
|
|
683
682
|
<span class="w-2 h-1.5 rounded inline-block flex-shrink-0" style="background: #8b5cf6;"></span>
|
|
@@ -690,9 +689,17 @@ const nonVerbalReps = computed(() => {
|
|
|
690
689
|
</div>
|
|
691
690
|
</div>
|
|
692
691
|
|
|
692
|
+
<!-- Tags -->
|
|
693
|
+
<div v-if="conceptTags.length" class="card p-5">
|
|
694
|
+
<div class="section-label">{{ t('concept.tags') }}</div>
|
|
695
|
+
<div class="flex flex-wrap gap-1.5 mt-3">
|
|
696
|
+
<span v-for="tag in conceptTags" :key="tag" class="badge badge-gray text-[10px]">{{ tag }}</span>
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
|
|
693
700
|
<!-- Managed concept dates -->
|
|
694
701
|
<div v-if="conceptDates.length" class="card p-5">
|
|
695
|
-
<div class="section-label">
|
|
702
|
+
<div class="section-label">{{ t('concept.lifecycleDates') }}</div>
|
|
696
703
|
<dl class="mt-3 space-y-1.5 text-xs">
|
|
697
704
|
<div v-for="(d, i) in conceptDates" :key="i" class="flex gap-2">
|
|
698
705
|
<dt class="text-ink-300 min-w-[70px]">{{ d.type }}</dt>
|
|
@@ -703,7 +710,7 @@ const nonVerbalReps = computed(() => {
|
|
|
703
710
|
|
|
704
711
|
<!-- Managed concept sources -->
|
|
705
712
|
<div v-if="conceptSources.length" class="card p-5">
|
|
706
|
-
<div class="section-label">
|
|
713
|
+
<div class="section-label">{{ t('concept.conceptSources') }}</div>
|
|
707
714
|
<div class="space-y-2 mt-3">
|
|
708
715
|
<div v-for="(src, i) in conceptSources" :key="i" class="text-xs">
|
|
709
716
|
<div class="flex items-center gap-1.5 flex-wrap mb-0.5">
|
|
@@ -753,7 +760,7 @@ const nonVerbalReps = computed(() => {
|
|
|
753
760
|
|
|
754
761
|
<!-- Metadata -->
|
|
755
762
|
<div class="card p-5">
|
|
756
|
-
<div class="section-label">
|
|
763
|
+
<div class="section-label">{{ t('concept.metadata') }}</div>
|
|
757
764
|
<dl class="space-y-2 text-xs mt-3">
|
|
758
765
|
<div v-if="managedStatus">
|
|
759
766
|
<dt class="text-ink-300">Status</dt>
|
|
@@ -333,8 +333,8 @@ const rdfSource = computed(() => rdfFormat.value === 'turtle' ? turtleSource.val
|
|
|
333
333
|
</button>
|
|
334
334
|
</div>
|
|
335
335
|
<div class="flex gap-1.5 mt-2.5">
|
|
336
|
-
<
|
|
337
|
-
<
|
|
336
|
+
<router-link to="/ontology/class/gloss-Concept" class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium bg-blue-50 text-blue-700 border border-blue-100 hover:bg-blue-100 transition-colors">gloss:Concept</router-link>
|
|
337
|
+
<router-link to="/ontology/class/gloss-Concept" class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium bg-emerald-50 text-emerald-700 border border-emerald-100 hover:bg-emerald-100 transition-colors">skos:Concept</router-link>
|
|
338
338
|
</div>
|
|
339
339
|
</div>
|
|
340
340
|
</div>
|
|
@@ -356,7 +356,7 @@ const rdfSource = computed(() => rdfFormat.value === 'turtle' ? turtleSource.val
|
|
|
356
356
|
<div v-for="(section, si) in sections" :key="si" class="card p-5">
|
|
357
357
|
<div class="flex items-center gap-2 mb-3">
|
|
358
358
|
<div class="w-1 h-4 rounded-full" :class="section.classId === 'gloss:Concept' ? 'bg-blue-500' : section.classId === 'gloss:LocalizedConcept' ? 'bg-emerald-500' : 'bg-amber-500'"></div>
|
|
359
|
-
<
|
|
359
|
+
<router-link :to="`/ontology/class/${section.classId.replace(/:/g, '-')}`" class="text-xs font-semibold text-ink-700 hover:text-blue-600 transition-colors">{{ section.classId }}</router-link>
|
|
360
360
|
<span class="text-xs text-ink-400">·</span>
|
|
361
361
|
<span class="text-xs text-ink-500">{{ section.label }}</span>
|
|
362
362
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Concept, LocalizedConcept } from 'glossarist';
|
|
3
3
|
import { computed } from 'vue';
|
|
4
|
-
import { langName, langLabel } from '../utils/lang';
|
|
4
|
+
import { langName, langLabel, sortLanguages } from '../utils/lang';
|
|
5
5
|
import { entryStatusColor } from '../utils/concept-helpers';
|
|
6
6
|
|
|
7
7
|
const props = defineProps<{
|
|
@@ -141,19 +141,7 @@ const languagesWithHistory = computed(() => {
|
|
|
141
141
|
langs.push(lang);
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
-
|
|
145
|
-
if (order) {
|
|
146
|
-
const orderIndex = new Map(order.map((l, i) => [l, i]));
|
|
147
|
-
langs.sort((a, b) => {
|
|
148
|
-
const ai = orderIndex.get(a) ?? order.length;
|
|
149
|
-
const bi = orderIndex.get(b) ?? order.length;
|
|
150
|
-
if (ai !== bi) return ai - bi;
|
|
151
|
-
return a.localeCompare(b);
|
|
152
|
-
});
|
|
153
|
-
} else {
|
|
154
|
-
langs.sort();
|
|
155
|
-
}
|
|
156
|
-
return langs;
|
|
144
|
+
return sortLanguages(langs, props.languageOrder);
|
|
157
145
|
});
|
|
158
146
|
|
|
159
147
|
function formatDate(isoDate: string): string {
|
|
@@ -115,6 +115,7 @@ interface SimNode extends SimulationNodeDatum {
|
|
|
115
115
|
register: string;
|
|
116
116
|
conceptId: string;
|
|
117
117
|
designation: string;
|
|
118
|
+
altDesignations: string[];
|
|
118
119
|
hasDesignation: boolean;
|
|
119
120
|
loaded: boolean;
|
|
120
121
|
nodeType?: 'concept' | 'domain';
|
|
@@ -178,11 +179,15 @@ function buildSimulation(width: number, height: number) {
|
|
|
178
179
|
const simNodes: SimNode[] = renderNodes.map(n => {
|
|
179
180
|
const lang = uiStore.selectedLang;
|
|
180
181
|
const desig = n.designations[lang] || Object.values(n.designations)[0] || '';
|
|
182
|
+
const alts = Object.entries(n.designations)
|
|
183
|
+
.filter(([l, t]) => l !== lang && t && t !== desig)
|
|
184
|
+
.map(([, t]) => t);
|
|
181
185
|
return {
|
|
182
186
|
uri: n.uri,
|
|
183
187
|
register: n.register,
|
|
184
188
|
conceptId: n.conceptId,
|
|
185
189
|
designation: desig,
|
|
190
|
+
altDesignations: alts,
|
|
186
191
|
hasDesignation: !!n.designations[lang],
|
|
187
192
|
loaded: n.loaded,
|
|
188
193
|
nodeType: n.nodeType,
|
|
@@ -272,6 +277,20 @@ function buildSimulation(width: number, height: number) {
|
|
|
272
277
|
return '#636588';
|
|
273
278
|
});
|
|
274
279
|
|
|
280
|
+
conceptNodes.append('text')
|
|
281
|
+
.attr('dy', -9)
|
|
282
|
+
.attr('y', 7)
|
|
283
|
+
.attr('text-anchor', 'middle')
|
|
284
|
+
.attr('font-size', '6px')
|
|
285
|
+
.attr('font-family', '"DM Sans", system-ui, sans-serif')
|
|
286
|
+
.attr('fill', '#a0a1b5')
|
|
287
|
+
.attr('pointer-events', 'none')
|
|
288
|
+
.text(d => {
|
|
289
|
+
if (labelMode.value !== 'designation') return '';
|
|
290
|
+
const alts = d.altDesignations.slice(0, 2);
|
|
291
|
+
return alts.join(' · ');
|
|
292
|
+
});
|
|
293
|
+
|
|
275
294
|
const dragBehavior = drag<SVGGElement, SimNode>()
|
|
276
295
|
.on('start', (event: D3DragEvent<SVGGElement, SimNode, SimNode>, d) => {
|
|
277
296
|
if (!event.active) simulation?.alphaTarget(0.3).restart();
|
|
@@ -11,6 +11,9 @@ import { useRouter } from 'vue-router';
|
|
|
11
11
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
12
12
|
import { getFactory } from '../adapters/factory';
|
|
13
13
|
import CitationDisplay from './CitationDisplay.vue';
|
|
14
|
+
import { useI18n } from '../i18n';
|
|
15
|
+
|
|
16
|
+
const { t } = useI18n();
|
|
14
17
|
|
|
15
18
|
const props = defineProps<{
|
|
16
19
|
concept: Concept;
|
|
@@ -109,7 +112,7 @@ function handleContentClick(e: MouseEvent) {
|
|
|
109
112
|
|
|
110
113
|
<!-- Designations -->
|
|
111
114
|
<div v-if="designations.length > 0" class="card p-5">
|
|
112
|
-
<div class="section-label">
|
|
115
|
+
<div class="section-label">{{ t('concept.designations') }}</div>
|
|
113
116
|
<div class="space-y-2 mt-3">
|
|
114
117
|
<div v-for="(d, i) in designations" :key="i" class="flex items-center gap-2 flex-wrap">
|
|
115
118
|
<span class="font-medium text-ink-800 text-lg" v-html="renderMath(d.designation)"></span>
|
|
@@ -138,16 +141,16 @@ function handleContentClick(e: MouseEvent) {
|
|
|
138
141
|
|
|
139
142
|
<!-- Definition -->
|
|
140
143
|
<div v-if="definition" class="card p-5">
|
|
141
|
-
<div class="section-label">
|
|
144
|
+
<div class="section-label">{{ t('concept.definition') }}</div>
|
|
142
145
|
<div class="text-ink-800 leading-relaxed mt-3" v-html="renderMath(definition, renderOpts)"></div>
|
|
143
146
|
</div>
|
|
144
147
|
|
|
145
148
|
<!-- Notes -->
|
|
146
149
|
<div v-if="notes.length" class="card p-5">
|
|
147
|
-
<div class="section-label">
|
|
150
|
+
<div class="section-label">{{ t('concept.notes') }}</div>
|
|
148
151
|
<div class="space-y-3 mt-3">
|
|
149
152
|
<div v-for="(note, i) in notes" :key="i" class="text-ink-600 text-sm leading-relaxed">
|
|
150
|
-
<span class="font-medium text-ink-400 text-xs uppercase tracking-wide">
|
|
153
|
+
<span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.note') }} {{ i + 1 }}</span>
|
|
151
154
|
<div class="mt-1" v-html="renderMath(note, renderOpts)"></div>
|
|
152
155
|
</div>
|
|
153
156
|
</div>
|
|
@@ -155,10 +158,10 @@ function handleContentClick(e: MouseEvent) {
|
|
|
155
158
|
|
|
156
159
|
<!-- Examples -->
|
|
157
160
|
<div v-if="examples.length" class="card p-5">
|
|
158
|
-
<div class="section-label">
|
|
161
|
+
<div class="section-label">{{ t('concept.examples') }}</div>
|
|
159
162
|
<div class="space-y-3 mt-3">
|
|
160
163
|
<div v-for="(ex, i) in examples" :key="i" class="text-ink-600 text-sm leading-relaxed">
|
|
161
|
-
<span class="font-medium text-ink-400 text-xs uppercase tracking-wide">
|
|
164
|
+
<span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.example') }} {{ i + 1 }}</span>
|
|
162
165
|
<div class="mt-1" v-html="renderMath(ex, renderOpts)"></div>
|
|
163
166
|
</div>
|
|
164
167
|
</div>
|
|
@@ -187,7 +190,7 @@ function handleContentClick(e: MouseEvent) {
|
|
|
187
190
|
<span class="text-xs font-semibold text-ink-500 bg-ink-50 px-1.5 py-0.5 rounded">{{ langLabel(activeLang) }}</span>
|
|
188
191
|
</div>
|
|
189
192
|
<div>
|
|
190
|
-
<p class="text-sm text-ink-600 font-medium">
|
|
193
|
+
<p class="text-sm text-ink-600 font-medium">{{ t('lang.termOnlyIn') }} {{ langName(activeLang) }}</p>
|
|
191
194
|
<p class="text-xs text-ink-400 mt-1 leading-relaxed">
|
|
192
195
|
This concept has a registered designation in {{ langName(activeLang) }} but no definition or notes.
|
|
193
196
|
</p>
|
|
@@ -198,7 +201,7 @@ function handleContentClick(e: MouseEvent) {
|
|
|
198
201
|
|
|
199
202
|
<!-- No data for this language -->
|
|
200
203
|
<div v-else class="card p-5 text-center">
|
|
201
|
-
<p class="text-sm text-ink-400">
|
|
204
|
+
<p class="text-sm text-ink-400">{{ t('concept.noData') }}</p>
|
|
202
205
|
</div>
|
|
203
206
|
</div>
|
|
204
207
|
</template>
|
|
@@ -2,8 +2,21 @@ import { ref, computed } from 'vue';
|
|
|
2
2
|
import {
|
|
3
3
|
getClass,
|
|
4
4
|
getClassTree,
|
|
5
|
+
getAllShapes,
|
|
6
|
+
getObjectProperties,
|
|
7
|
+
getDatatypeProperties,
|
|
8
|
+
getAnnotationProperties,
|
|
9
|
+
getOntology,
|
|
10
|
+
getStats,
|
|
5
11
|
type OwlClass,
|
|
12
|
+
type OwlShape,
|
|
13
|
+
type OwlProperty,
|
|
14
|
+
type AnnotationProperty,
|
|
15
|
+
type OwlOntology,
|
|
16
|
+
ENTITY_TYPE_META,
|
|
17
|
+
type EntityType,
|
|
6
18
|
} from '../adapters/ontology-schema';
|
|
19
|
+
import taxonomyData from '../data/taxonomies.json';
|
|
7
20
|
|
|
8
21
|
export function slugToCompact(slug: string): string {
|
|
9
22
|
return slug.replace(/-/g, ':');
|
|
@@ -15,6 +28,57 @@ export function compactToSlug(compact: string): string {
|
|
|
15
28
|
|
|
16
29
|
const expandedClasses = ref(new Set<string>(['gloss:Designation']));
|
|
17
30
|
|
|
31
|
+
const collapsedSections = ref(new Set<string>([
|
|
32
|
+
'objectProperty',
|
|
33
|
+
'datatypeProperty',
|
|
34
|
+
'shape',
|
|
35
|
+
'taxonomy',
|
|
36
|
+
'namedIndividual',
|
|
37
|
+
'annotationProperty',
|
|
38
|
+
]));
|
|
39
|
+
|
|
40
|
+
const searchQuery = ref('');
|
|
41
|
+
|
|
42
|
+
function toggleExpand(cls: OwlClass) {
|
|
43
|
+
const s = new Set(expandedClasses.value);
|
|
44
|
+
if (s.has(cls.compact)) s.delete(cls.compact);
|
|
45
|
+
else s.add(cls.compact);
|
|
46
|
+
expandedClasses.value = s;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toggleSection(key: string) {
|
|
50
|
+
const s = new Set(collapsedSections.value);
|
|
51
|
+
if (s.has(key)) s.delete(key);
|
|
52
|
+
else s.add(key);
|
|
53
|
+
collapsedSections.value = s;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function expandAllSections() {
|
|
57
|
+
collapsedSections.value = new Set();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function collapseAllSections() {
|
|
61
|
+
collapsedSections.value = new Set(['objectProperty', 'datatypeProperty', 'shape', 'taxonomy', 'class', 'namedIndividual', 'annotationProperty']);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function childClasses(parentId: string): OwlClass[] {
|
|
65
|
+
const cls = getClass(parentId);
|
|
66
|
+
if (!cls) return [];
|
|
67
|
+
return cls.children.map(id => getClass(id)).filter((c): c is OwlClass => !!c);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function hasChildren(cls: OwlClass): boolean {
|
|
71
|
+
return cls.children.length > 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const treeRoots = getClassTree();
|
|
75
|
+
const allShapes = getAllShapes();
|
|
76
|
+
const objectProperties = getObjectProperties();
|
|
77
|
+
const datatypeProperties = getDatatypeProperties();
|
|
78
|
+
const annotationProperties = getAnnotationProperties();
|
|
79
|
+
const ontology = getOntology();
|
|
80
|
+
const stats = getStats();
|
|
81
|
+
|
|
18
82
|
const taxonomyKeys = [
|
|
19
83
|
'conceptStatus', 'entryStatus', 'normativeStatus', 'sourceType', 'sourceStatus',
|
|
20
84
|
'relationshipType', 'designationType', 'termType', 'grammarGender', 'grammarNumber',
|
|
@@ -33,24 +97,53 @@ const taxonomyLabels: Record<string, string> = {
|
|
|
33
97
|
grammarNumber: 'Grammar Number',
|
|
34
98
|
};
|
|
35
99
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
expandedClasses.value = s;
|
|
100
|
+
interface IndividualGroup {
|
|
101
|
+
key: string;
|
|
102
|
+
label: string;
|
|
103
|
+
concepts: { id: string; prefLabel: string }[];
|
|
41
104
|
}
|
|
42
105
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
106
|
+
const groupedIndividuals = computed<IndividualGroup[]>(() => {
|
|
107
|
+
return taxonomyKeys.map(key => {
|
|
108
|
+
const tax = (taxonomyData as Record<string, any>)[key];
|
|
109
|
+
if (!tax) return { key, label: taxonomyLabels[key] || key, concepts: [] };
|
|
110
|
+
const concepts = Object.values(tax.concepts as Record<string, any>).map((c: any) => ({
|
|
111
|
+
id: c.id,
|
|
112
|
+
prefLabel: c.prefLabel,
|
|
113
|
+
}));
|
|
114
|
+
return { key, label: tax.schemeLabel || taxonomyLabels[key] || key, concepts };
|
|
115
|
+
});
|
|
116
|
+
});
|
|
48
117
|
|
|
49
|
-
|
|
50
|
-
|
|
118
|
+
const totalIndividuals = computed(() =>
|
|
119
|
+
groupedIndividuals.value.reduce((sum, g) => sum + g.concepts.length, 0),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const valuesToTaxonomy: Record<string, string> = {
|
|
123
|
+
'gloss:status': 'conceptStatus',
|
|
124
|
+
'gloss:entstatus': 'entryStatus',
|
|
125
|
+
'gloss:norm': 'normativeStatus',
|
|
126
|
+
'gloss:sourceType': 'sourceType',
|
|
127
|
+
'gloss:sourceStatus': 'sourceStatus',
|
|
128
|
+
'gloss:rel': 'relationshipType',
|
|
129
|
+
'gloss:desigType': 'designationType',
|
|
130
|
+
'gloss:termType': 'termType',
|
|
131
|
+
'gloss:gender': 'grammarGender',
|
|
132
|
+
'gloss:number': 'grammarNumber',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function taxonomyKeyForValuesFrom(valuesFrom: string | null): string | null {
|
|
136
|
+
if (!valuesFrom) return null;
|
|
137
|
+
return valuesToTaxonomy[valuesFrom] ?? null;
|
|
51
138
|
}
|
|
52
139
|
|
|
53
|
-
|
|
140
|
+
function getShapesForTaxonomy(taxonomyKey: string): OwlShape[] {
|
|
141
|
+
const targetScheme = Object.entries(valuesToTaxonomy).find(([, v]) => v === taxonomyKey)?.[0];
|
|
142
|
+
if (!targetScheme) return [];
|
|
143
|
+
return allShapes.filter(s =>
|
|
144
|
+
s.constraints.some(c => c.valuesFrom === targetScheme),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
54
147
|
|
|
55
148
|
const allNavItems = computed(() => {
|
|
56
149
|
const items: { id: string; label: string; depth: number }[] = [];
|
|
@@ -66,15 +159,92 @@ const allNavItems = computed(() => {
|
|
|
66
159
|
return items;
|
|
67
160
|
});
|
|
68
161
|
|
|
162
|
+
function matchesSearch(text: string, query: string): boolean {
|
|
163
|
+
return text.toLowerCase().includes(query.toLowerCase());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const searchResults = computed(() => {
|
|
167
|
+
const q = searchQuery.value.trim();
|
|
168
|
+
if (!q) return null;
|
|
169
|
+
|
|
170
|
+
const matchedClasses: OwlClass[] = [];
|
|
171
|
+
const matchedObjectProps: OwlProperty[] = [];
|
|
172
|
+
const matchedDatatypeProps: OwlProperty[] = [];
|
|
173
|
+
const matchedShapes: OwlShape[] = [];
|
|
174
|
+
const matchedIndividuals: { group: string; id: string; prefLabel: string }[] = [];
|
|
175
|
+
const matchedAnnotationProps: AnnotationProperty[] = [];
|
|
176
|
+
|
|
177
|
+
// Walk all classes (tree + leaves)
|
|
178
|
+
function walkAll(classes: OwlClass[]) {
|
|
179
|
+
for (const cls of classes) {
|
|
180
|
+
if (matchesSearch(cls.label, q) || matchesSearch(cls.compact, q)) {
|
|
181
|
+
matchedClasses.push(cls);
|
|
182
|
+
}
|
|
183
|
+
walkAll(childClasses(cls.compact));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
walkAll(treeRoots);
|
|
187
|
+
|
|
188
|
+
for (const p of objectProperties) {
|
|
189
|
+
if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedObjectProps.push(p);
|
|
190
|
+
}
|
|
191
|
+
for (const p of datatypeProperties) {
|
|
192
|
+
if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedDatatypeProps.push(p);
|
|
193
|
+
}
|
|
194
|
+
for (const s of allShapes) {
|
|
195
|
+
if (matchesSearch(s.label, q) || matchesSearch(s.compact, q)) matchedShapes.push(s);
|
|
196
|
+
}
|
|
197
|
+
for (const g of groupedIndividuals.value) {
|
|
198
|
+
for (const c of g.concepts) {
|
|
199
|
+
if (matchesSearch(c.prefLabel, q) || matchesSearch(c.id, q)) {
|
|
200
|
+
matchedIndividuals.push({ group: g.key, id: c.id, prefLabel: c.prefLabel });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const ap of annotationProperties) {
|
|
205
|
+
if (matchesSearch(ap.label, q) || matchesSearch(ap.compact, q)) matchedAnnotationProps.push(ap);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const total = matchedClasses.length + matchedObjectProps.length + matchedDatatypeProps.length + matchedShapes.length + matchedIndividuals.length + matchedAnnotationProps.length;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
total,
|
|
212
|
+
classes: matchedClasses,
|
|
213
|
+
objectProperties: matchedObjectProps,
|
|
214
|
+
datatypeProperties: matchedDatatypeProps,
|
|
215
|
+
shapes: matchedShapes,
|
|
216
|
+
individuals: matchedIndividuals,
|
|
217
|
+
annotationProperties: matchedAnnotationProps,
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
|
|
69
221
|
export function useOntologyNav() {
|
|
70
222
|
return {
|
|
71
223
|
expandedClasses,
|
|
224
|
+
collapsedSections,
|
|
225
|
+
searchQuery,
|
|
72
226
|
taxonomyKeys,
|
|
73
227
|
taxonomyLabels,
|
|
74
228
|
treeRoots,
|
|
229
|
+
allShapes,
|
|
230
|
+
objectProperties,
|
|
231
|
+
datatypeProperties,
|
|
232
|
+
annotationProperties,
|
|
233
|
+
ontology,
|
|
234
|
+
stats,
|
|
235
|
+
groupedIndividuals,
|
|
236
|
+
totalIndividuals,
|
|
75
237
|
allNavItems,
|
|
238
|
+
searchResults,
|
|
239
|
+
valuesToTaxonomy,
|
|
240
|
+
taxonomyKeyForValuesFrom,
|
|
241
|
+
getShapesForTaxonomy,
|
|
76
242
|
toggleExpand,
|
|
243
|
+
toggleSection,
|
|
244
|
+
expandAllSections,
|
|
245
|
+
collapseAllSections,
|
|
77
246
|
childClasses,
|
|
78
247
|
hasChildren,
|
|
248
|
+
ENTITY_TYPE_META,
|
|
79
249
|
};
|
|
80
250
|
}
|
|
@@ -14,7 +14,7 @@ const bibCache = new Map<string, Record<string, BibEntry>>();
|
|
|
14
14
|
async function loadBibliography(registerId: string): Promise<Record<string, BibEntry> | null> {
|
|
15
15
|
if (bibCache.has(registerId)) return bibCache.get(registerId)!;
|
|
16
16
|
try {
|
|
17
|
-
const resp = await fetch(
|
|
17
|
+
const resp = await fetch(`${import.meta.env.BASE_URL}data/${registerId}/bibliography.json`);
|
|
18
18
|
if (!resp.ok) return null;
|
|
19
19
|
const data = await resp.json();
|
|
20
20
|
bibCache.set(registerId, data);
|
|
@@ -47,7 +47,7 @@ export function useRenderOptions(registerId: () => string) {
|
|
|
47
47
|
|
|
48
48
|
const figResolver: FigResolver = (figId) => {
|
|
49
49
|
const id = registerId();
|
|
50
|
-
const imgSrc =
|
|
50
|
+
const imgSrc = `${import.meta.env.BASE_URL}data/${id}/images/${figId}.png`;
|
|
51
51
|
return `<span class="fig-ref"><a href="${escapeAttr(imgSrc)}" target="_blank" rel="noopener">${escapeAttr(figId)}</a></span>`;
|
|
52
52
|
};
|
|
53
53
|
|
package/src/config/types.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface RuntimeSiteConfig {
|
|
|
10
10
|
description?: string;
|
|
11
11
|
datasets: string[];
|
|
12
12
|
defaultDataset?: string;
|
|
13
|
+
uiLanguages?: { code: string; label: string }[];
|
|
13
14
|
branding?: {
|
|
14
15
|
primaryColor?: string;
|
|
15
16
|
darkColor?: string;
|
|
@@ -27,7 +28,7 @@ export interface RuntimeSiteConfig {
|
|
|
27
28
|
social?: Record<string, string>;
|
|
28
29
|
nav?: { label: string; route: string }[];
|
|
29
30
|
footerNav?: { label: string; route: string }[];
|
|
30
|
-
defaults?: { language?: string; languageOrder?: string[] };
|
|
31
|
+
defaults?: { language?: string; languageOrder?: string[]; mainLanguages?: string[] };
|
|
31
32
|
email?: string;
|
|
32
33
|
pages?: PageConfig[];
|
|
33
34
|
contributors?: { name: string; role?: string; organization?: string; url?: string; email?: string }[];
|
|
@@ -94,7 +95,7 @@ function applyBranding(config: RuntimeSiteConfig) {
|
|
|
94
95
|
async function loadConfig(): Promise<RuntimeSiteConfig | null> {
|
|
95
96
|
if (loaded.value) return siteConfig.value;
|
|
96
97
|
try {
|
|
97
|
-
const resp = await fetch(
|
|
98
|
+
const resp = await fetch(`${import.meta.env.BASE_URL}site-config.json`);
|
|
98
99
|
if (resp.ok) {
|
|
99
100
|
siteConfig.value = await resp.json();
|
|
100
101
|
if (siteConfig.value) applyBranding(siteConfig.value);
|