@glossarist/concept-browser 0.3.4 → 0.4.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 +3 -2
- package/cli/index.mjs +2 -1
- package/env.d.ts +5 -0
- package/package.json +4 -3
- package/scripts/build-edges.js +78 -10
- package/scripts/generate-data.mjs +152 -20
- package/scripts/generate-ontology-data.mjs +184 -0
- package/scripts/generate-ontology-schema.mjs +315 -0
- package/src/__tests__/about-view.test.ts +98 -0
- package/src/__tests__/app-footer.test.ts +38 -0
- package/src/__tests__/app-header.test.ts +130 -0
- package/src/__tests__/app-sidebar.test.ts +159 -0
- package/src/__tests__/asciidoc-lite.test.ts +1 -1
- package/src/__tests__/concept-card.test.ts +115 -0
- package/src/__tests__/concept-detail-interaction.test.ts +273 -0
- package/src/__tests__/concept-formats.test.ts +32 -30
- package/src/__tests__/concept-timeline.test.ts +200 -0
- package/src/__tests__/concept-view.test.ts +88 -0
- package/src/__tests__/contributors-view.test.ts +103 -0
- package/src/__tests__/dataset-adapter.test.ts +172 -23
- package/src/__tests__/dataset-view.test.ts +232 -0
- package/src/__tests__/designation-registry.test.ts +161 -0
- package/src/__tests__/format-downloads.test.ts +98 -0
- package/src/__tests__/graph-view.test.ts +69 -0
- package/src/__tests__/graph.test.ts +62 -0
- package/src/__tests__/home-interaction.test.ts +157 -0
- package/src/__tests__/language-detail.test.ts +203 -0
- package/src/__tests__/nav-icon.test.ts +48 -0
- package/src/__tests__/news-view.test.ts +87 -0
- package/src/__tests__/ontology-registry.test.ts +109 -0
- package/src/__tests__/page-view.test.ts +83 -0
- package/src/__tests__/relationship-categories.test.ts +62 -0
- package/src/__tests__/resolve-view.test.ts +77 -0
- package/src/__tests__/router.test.ts +65 -0
- package/src/__tests__/search-bar.test.ts +219 -0
- package/src/__tests__/search-view.test.ts +41 -0
- package/src/__tests__/stats-view.test.ts +77 -0
- package/src/__tests__/test-helpers.ts +171 -0
- package/src/__tests__/ui-store.test.ts +100 -0
- package/src/__tests__/v-math.test.ts +8 -7
- package/src/adapters/DatasetAdapter.ts +188 -63
- package/src/adapters/model-bridge.ts +277 -0
- package/src/adapters/ontology-registry.ts +75 -0
- package/src/adapters/ontology-schema.ts +100 -0
- package/src/adapters/types.ts +53 -78
- package/src/components/AppSidebar.vue +1 -1
- package/src/components/CitationDisplay.vue +35 -0
- package/src/components/ConceptDetail.vue +349 -146
- package/src/components/ConceptRdfView.vue +397 -0
- package/src/components/ConceptTimeline.vue +57 -60
- package/src/components/GraphPanel.vue +96 -31
- package/src/components/LanguageDetail.vue +46 -61
- package/src/components/NavIcon.vue +1 -0
- package/src/components/NonVerbalRepDisplay.vue +38 -0
- package/src/components/RelationshipList.vue +99 -0
- package/src/composables/use-render-options.ts +1 -4
- package/src/config/use-site-config.ts +3 -0
- package/src/data/ontology-schema.json +1551 -0
- package/src/data/taxonomies.json +543 -0
- package/src/graph/GraphEngine.ts +7 -4
- package/src/router/index.ts +6 -1
- package/src/shims/empty.ts +1 -0
- package/src/shims/node-crypto.ts +6 -0
- package/src/shims/node-path.ts +10 -0
- package/src/stores/vocabulary.ts +82 -32
- package/src/style.css +74 -20
- package/src/utils/asciidoc-lite.ts +17 -19
- package/src/utils/concept-formats.ts +22 -20
- package/src/utils/concept-helpers.ts +54 -0
- package/src/utils/designation-registry.ts +124 -0
- package/src/utils/escape.ts +7 -0
- package/src/utils/markdown-lite.ts +1 -3
- package/src/utils/math.ts +2 -11
- package/src/utils/plurimath.ts +2 -7
- package/src/utils/relationship-categories.ts +84 -0
- package/src/views/ConceptView.vue +22 -1
- package/src/views/DatasetView.vue +7 -2
- package/src/views/OntologySchemaView.vue +302 -0
- package/src/views/PageView.vue +28 -17
- package/src/views/StatsView.vue +34 -12
- package/vite.config.ts +8 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Designation, Abbreviation } from 'glossarist';
|
|
2
|
+
import type { GrammarInfo, Pronunciation } from 'glossarist/models';
|
|
3
|
+
import { ontology } from '../adapters/ontology-registry';
|
|
4
|
+
|
|
5
|
+
export interface DesignationTypeInfo {
|
|
6
|
+
label: string;
|
|
7
|
+
color: string;
|
|
8
|
+
definition?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const TYPE_COLORS: Record<string, string> = {
|
|
12
|
+
expression: 'bg-sky-50 text-sky-700',
|
|
13
|
+
abbreviation: 'bg-amber-50 text-amber-700',
|
|
14
|
+
symbol: 'bg-violet-50 text-violet-700',
|
|
15
|
+
letter_symbol: 'bg-violet-50 text-violet-700',
|
|
16
|
+
graphical_symbol: 'bg-violet-50 text-violet-700',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function designationTypeInfo(designation: Designation): DesignationTypeInfo {
|
|
20
|
+
const type = designation.type;
|
|
21
|
+
const concept = ontology.getConcept('designationType', type);
|
|
22
|
+
return {
|
|
23
|
+
label: concept?.prefLabel ?? type,
|
|
24
|
+
color: TYPE_COLORS[type] ?? 'bg-gray-50 text-gray-700',
|
|
25
|
+
definition: concept?.definition ?? undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function normativeStatusInfo(status: string | null): { label: string; color: string; definition?: string } {
|
|
30
|
+
if (!status) return { label: '', color: 'bg-gray-50 text-gray-700' };
|
|
31
|
+
|
|
32
|
+
const colors: Record<string, string> = {
|
|
33
|
+
preferred: 'bg-emerald-50 text-emerald-700',
|
|
34
|
+
admitted: 'bg-amber-50 text-amber-700',
|
|
35
|
+
deprecated: 'bg-red-50 text-red-700',
|
|
36
|
+
superseded: 'bg-red-50 text-red-700',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const concept = ontology.getConcept('normativeStatus', status);
|
|
40
|
+
return {
|
|
41
|
+
label: concept?.prefLabel ?? status,
|
|
42
|
+
color: colors[status] ?? 'bg-gray-50 text-gray-700',
|
|
43
|
+
definition: concept?.definition ?? undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function sourceStatusInfo(status: string | null): { label: string; color: string; definition?: string } {
|
|
48
|
+
if (!status) return { label: '', color: 'badge-gray' };
|
|
49
|
+
|
|
50
|
+
const concept = ontology.getConcept('sourceStatus', status);
|
|
51
|
+
return {
|
|
52
|
+
label: concept?.prefLabel ?? status,
|
|
53
|
+
color: 'badge-gray',
|
|
54
|
+
definition: concept?.definition ?? undefined,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function sourceTypeInfo(type: string | null): { label: string; color: string; definition?: string } {
|
|
59
|
+
if (!type) return { label: '', color: 'badge-gray' };
|
|
60
|
+
|
|
61
|
+
const colors: Record<string, string> = {
|
|
62
|
+
authoritative: 'badge-purple',
|
|
63
|
+
lineage: 'badge-blue',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const concept = ontology.getConcept('sourceType', type);
|
|
67
|
+
return {
|
|
68
|
+
label: concept?.prefLabel ?? type,
|
|
69
|
+
color: colors[type] ?? 'badge-gray',
|
|
70
|
+
definition: concept?.definition ?? undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function termTypeInfo(termType: string | null): { label: string; category: string; definition?: string } {
|
|
75
|
+
if (!termType) return { label: '', category: '' };
|
|
76
|
+
const concept = ontology.getConcept('termType', termType);
|
|
77
|
+
return {
|
|
78
|
+
label: concept?.prefLabel ?? termType,
|
|
79
|
+
category: concept?.broader ?? '',
|
|
80
|
+
definition: concept?.definition ?? undefined,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function abbreviationDetails(designation: Designation): string[] {
|
|
85
|
+
if (designation.type !== 'abbreviation') return [];
|
|
86
|
+
const abbr = designation as Abbreviation;
|
|
87
|
+
const parts: string[] = [];
|
|
88
|
+
if (abbr.acronym) parts.push('acronym');
|
|
89
|
+
if (abbr.initialism) parts.push('initialism');
|
|
90
|
+
if (abbr.truncation) parts.push('truncation');
|
|
91
|
+
return parts;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function grammarBadges(gi: GrammarInfo): { label: string; definition?: string }[] {
|
|
95
|
+
const badges: { label: string; definition?: string }[] = [];
|
|
96
|
+
if (gi.gender) {
|
|
97
|
+
const concept = ontology.getConcept('grammarGender', gi.gender);
|
|
98
|
+
badges.push({ label: concept?.prefLabel ?? gi.gender, definition: concept?.definition ?? undefined });
|
|
99
|
+
}
|
|
100
|
+
if (gi.number) {
|
|
101
|
+
const concept = ontology.getConcept('grammarNumber', gi.number);
|
|
102
|
+
badges.push({ label: concept?.prefLabel ?? gi.number, definition: concept?.definition ?? undefined });
|
|
103
|
+
}
|
|
104
|
+
if (gi.partOfSpeech) badges.push({ label: gi.partOfSpeech });
|
|
105
|
+
for (const pos of ['noun', 'verb', 'adj', 'adverb', 'preposition', 'participle'] as const) {
|
|
106
|
+
if (gi[pos]) badges.push({ label: pos });
|
|
107
|
+
}
|
|
108
|
+
return badges;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function pronunciationLabel(p: Pronunciation): string {
|
|
112
|
+
const parts = [p.content];
|
|
113
|
+
if (p.system) parts.push(`(${p.system})`);
|
|
114
|
+
return parts.filter(Boolean).join(' ');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function pronunciationTooltip(p: Pronunciation): string {
|
|
118
|
+
const parts: string[] = [];
|
|
119
|
+
if (p.language) parts.push(`Language: ${p.language}`);
|
|
120
|
+
if (p.script) parts.push(`Script: ${p.script}`);
|
|
121
|
+
if (p.country) parts.push(`Country: ${p.country}`);
|
|
122
|
+
if (p.system) parts.push(`System: ${p.system}`);
|
|
123
|
+
return parts.join(', ');
|
|
124
|
+
}
|
|
@@ -104,6 +104,4 @@ export function renderMarkdown(input: string): string {
|
|
|
104
104
|
return blocks.join('\n');
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
109
|
-
}
|
|
107
|
+
import { escapeHtml } from './escape';
|
package/src/utils/math.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escapeHtml, escapeAttr } from './escape';
|
|
2
|
+
|
|
1
3
|
export type XrefResolver = (uri: string, term: string) => string;
|
|
2
4
|
export type BibResolver = (refId: string, title: string) => string;
|
|
3
5
|
export type FigResolver = (figId: string) => string;
|
|
@@ -8,10 +10,6 @@ export interface RenderOptions {
|
|
|
8
10
|
figResolver?: FigResolver;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
function escapeAttr(s: string): string {
|
|
12
|
-
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
13
|
function replaceBracketed(text: string, prefix: string, handler: (content: string, bold: boolean) => string): string {
|
|
16
14
|
let result = '';
|
|
17
15
|
let i = 0;
|
|
@@ -84,13 +82,6 @@ function convertLists(text: string): string {
|
|
|
84
82
|
return result;
|
|
85
83
|
}
|
|
86
84
|
|
|
87
|
-
function escapeHtml(text: string): string {
|
|
88
|
-
return text
|
|
89
|
-
.replace(/&/g, '&')
|
|
90
|
-
.replace(/</g, '<')
|
|
91
|
-
.replace(/>/g, '>');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
85
|
export function renderMath(text: string, xrefResolverOrOpts?: XrefResolver | RenderOptions): string {
|
|
95
86
|
if (!text) return '';
|
|
96
87
|
let result = text;
|
package/src/utils/plurimath.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escapeHtml } from './escape';
|
|
2
|
+
|
|
1
3
|
type PlurimathCtor = new (data: string, format: string) => {
|
|
2
4
|
toAsciimath(): string;
|
|
3
5
|
toLatex(): string;
|
|
@@ -30,13 +32,6 @@ export function renderToMathML(expr: string, format: string): string | null {
|
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
function escapeHtml(text: string): string {
|
|
34
|
-
return text
|
|
35
|
-
.replace(/&/g, '&')
|
|
36
|
-
.replace(/</g, '<')
|
|
37
|
-
.replace(/>/g, '>');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
35
|
export function mathToHtml(expr: string, format: string, bold: boolean): string {
|
|
41
36
|
const mathml = renderToMathML(expr, format);
|
|
42
37
|
if (mathml) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ontology } from '../adapters/ontology-registry';
|
|
2
|
+
|
|
3
|
+
export interface RelationshipCategory {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
types: string[];
|
|
7
|
+
color: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const RELATIONSHIP_CATEGORIES: RelationshipCategory[] = [
|
|
11
|
+
{
|
|
12
|
+
id: 'hierarchical',
|
|
13
|
+
label: 'Hierarchy',
|
|
14
|
+
types: ['broader', 'narrower', 'broader_generic', 'narrower_generic',
|
|
15
|
+
'broader_partitive', 'narrower_partitive', 'broader_instantial', 'narrower_instantial'],
|
|
16
|
+
color: 'text-blue-600 bg-blue-50',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'mapping',
|
|
20
|
+
label: 'Equivalence',
|
|
21
|
+
types: ['equivalent', 'close_match', 'broad_match', 'narrow_match', 'related_match'],
|
|
22
|
+
color: 'text-emerald-600 bg-emerald-50',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'associative',
|
|
26
|
+
label: 'Associative',
|
|
27
|
+
types: ['see', 'related_concept', 'related_concept_broader', 'related_concept_narrower', 'references'],
|
|
28
|
+
color: 'text-violet-600 bg-violet-50',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'lifecycle',
|
|
32
|
+
label: 'Lifecycle',
|
|
33
|
+
types: ['deprecates', 'supersedes', 'superseded_by', 'replaces'],
|
|
34
|
+
color: 'text-red-600 bg-red-50',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'comparative',
|
|
38
|
+
label: 'Comparison',
|
|
39
|
+
types: ['compare', 'contrast'],
|
|
40
|
+
color: 'text-amber-600 bg-amber-50',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'spatiotemporal',
|
|
44
|
+
label: 'Spatiotemporal',
|
|
45
|
+
types: ['sequentially_related', 'spatially_related', 'temporally_related'],
|
|
46
|
+
color: 'text-teal-600 bg-teal-50',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'lexical',
|
|
50
|
+
label: 'Lexical',
|
|
51
|
+
types: ['homograph', 'false_friend'],
|
|
52
|
+
color: 'text-pink-600 bg-pink-50',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'designation',
|
|
56
|
+
label: 'Designation',
|
|
57
|
+
types: ['abbreviated_form_for', 'short_form_for'],
|
|
58
|
+
color: 'text-gray-600 bg-gray-50',
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const CATEGORY_MAP = new Map<string, RelationshipCategory>();
|
|
63
|
+
for (const cat of RELATIONSHIP_CATEGORIES) {
|
|
64
|
+
for (const t of cat.types) {
|
|
65
|
+
CATEGORY_MAP.set(t, cat);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function categorizeRelationship(type: string): RelationshipCategory {
|
|
70
|
+
return CATEGORY_MAP.get(type) ?? { id: 'other', label: 'Other', types: [type], color: 'text-gray-600 bg-gray-50' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function relationshipLabel(type: string): string {
|
|
74
|
+
// Check the ontology taxonomy first (for glossarist-specific types)
|
|
75
|
+
const concept = ontology.getConcept('relationshipType', type);
|
|
76
|
+
if (concept?.prefLabel) return concept.prefLabel;
|
|
77
|
+
|
|
78
|
+
// Fallback: humanize the type string
|
|
79
|
+
return type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function relationshipDefinition(type: string): string | null {
|
|
83
|
+
return ontology.getDefinition('relationshipType', type);
|
|
84
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, watch, ref } from 'vue';
|
|
2
|
+
import { computed, watch, ref, onMounted, onUnmounted } from 'vue';
|
|
3
|
+
import { useRouter } from 'vue-router';
|
|
3
4
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
4
5
|
import ConceptDetail from '../components/ConceptDetail.vue';
|
|
5
6
|
|
|
@@ -9,6 +10,7 @@ const props = defineProps<{
|
|
|
9
10
|
}>();
|
|
10
11
|
|
|
11
12
|
const store = useVocabularyStore();
|
|
13
|
+
const router = useRouter();
|
|
12
14
|
const conceptLoading = ref(false);
|
|
13
15
|
const localError = ref<string | null>(null);
|
|
14
16
|
|
|
@@ -55,6 +57,25 @@ async function loadAdjacent() {
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
watch(() => props.conceptId, () => { loadAdjacent(); });
|
|
60
|
+
|
|
61
|
+
function goAdjacent(id: string) {
|
|
62
|
+
router.push({ name: 'concept', params: { registerId: props.registerId, conceptId: id } });
|
|
63
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function onKeydown(e: KeyboardEvent) {
|
|
67
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
|
68
|
+
if (e.key === 'ArrowLeft' && adjacent.value.prev) {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
goAdjacent(adjacent.value.prev);
|
|
71
|
+
} else if (e.key === 'ArrowRight' && adjacent.value.next) {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
goAdjacent(adjacent.value.next);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
onMounted(() => window.addEventListener('keydown', onKeydown));
|
|
78
|
+
onUnmounted(() => window.removeEventListener('keydown', onKeydown));
|
|
58
79
|
</script>
|
|
59
80
|
|
|
60
81
|
<template>
|
|
@@ -44,6 +44,11 @@ function onGlobalKeydown(e: KeyboardEvent) {
|
|
|
44
44
|
e.preventDefault();
|
|
45
45
|
filterInput.value?.focus();
|
|
46
46
|
}
|
|
47
|
+
if (e.key === 'ArrowRight' && document.activeElement?.tagName !== 'INPUT' && page.value < totalPages.value) {
|
|
48
|
+
goToPage(page.value + 1);
|
|
49
|
+
} else if (e.key === 'ArrowLeft' && document.activeElement?.tagName !== 'INPUT' && page.value > 1) {
|
|
50
|
+
goToPage(page.value - 1);
|
|
51
|
+
}
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
onMounted(() => window.addEventListener('keydown', onGlobalKeydown));
|
|
@@ -62,7 +67,7 @@ watch(filter, async (q) => {
|
|
|
62
67
|
|
|
63
68
|
// Dense array: only loaded (non-undefined) entries
|
|
64
69
|
const loadedConcepts = computed(() => {
|
|
65
|
-
const arr = adapter.value?.getConcepts()
|
|
70
|
+
const arr = adapter.value?.getConcepts();
|
|
66
71
|
if (!arr) return [];
|
|
67
72
|
return arr.filter((c): c is import('../adapters/types').ConceptSummary => c != null);
|
|
68
73
|
});
|
|
@@ -93,7 +98,7 @@ const paged = computed(() => {
|
|
|
93
98
|
}
|
|
94
99
|
// When not filtering, slice directly from the pre-allocated index (may contain undefined)
|
|
95
100
|
const start = (page.value - 1) * perPage;
|
|
96
|
-
const arr = adapter.value?.getConcepts()
|
|
101
|
+
const arr = adapter.value?.getConcepts();
|
|
97
102
|
if (!arr) return [];
|
|
98
103
|
return arr.slice(start, start + perPage).filter((c): c is import('../adapters/types').ConceptSummary => c != null);
|
|
99
104
|
});
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue';
|
|
3
|
+
import {
|
|
4
|
+
getClass,
|
|
5
|
+
getAllClasses,
|
|
6
|
+
getClassTree,
|
|
7
|
+
getAllPropertiesForClass,
|
|
8
|
+
getStats,
|
|
9
|
+
type OwlClass,
|
|
10
|
+
} from '../adapters/ontology-schema';
|
|
11
|
+
import { ontology, type TaxonomyConcept } from '../adapters/ontology-registry';
|
|
12
|
+
|
|
13
|
+
const stats = getStats();
|
|
14
|
+
const allClasses = getAllClasses();
|
|
15
|
+
const treeRoots = getClassTree();
|
|
16
|
+
|
|
17
|
+
const taxonomyKeys = [
|
|
18
|
+
'conceptStatus', 'entryStatus', 'normativeStatus', 'sourceType', 'sourceStatus',
|
|
19
|
+
'relationshipType', 'designationType', 'termType', 'grammarGender', 'grammarNumber',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const taxonomyLabels: Record<string, string> = {
|
|
23
|
+
conceptStatus: 'Concept Status',
|
|
24
|
+
entryStatus: 'Entry Status',
|
|
25
|
+
normativeStatus: 'Normative Status',
|
|
26
|
+
sourceType: 'Source Type',
|
|
27
|
+
sourceStatus: 'Source Status',
|
|
28
|
+
relationshipType: 'Relationship Type',
|
|
29
|
+
designationType: 'Designation Type',
|
|
30
|
+
termType: 'Term Type',
|
|
31
|
+
grammarGender: 'Grammar Gender',
|
|
32
|
+
grammarNumber: 'Grammar Number',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const activeClassId = ref('gloss:Concept');
|
|
36
|
+
const showTaxonomies = ref(false);
|
|
37
|
+
const activeTaxonomy = ref<string | null>(null);
|
|
38
|
+
|
|
39
|
+
const activeClass = computed(() => getClass(activeClassId.value));
|
|
40
|
+
const activeProperties = computed(() => getAllPropertiesForClass(activeClassId.value));
|
|
41
|
+
|
|
42
|
+
function activeTaxonomyData() {
|
|
43
|
+
if (!activeTaxonomy.value) return null;
|
|
44
|
+
const key = activeTaxonomy.value as Parameters<typeof ontology.getAll>[0];
|
|
45
|
+
const all = ontology.getAll(key);
|
|
46
|
+
const top = all.filter(c => !c.broader);
|
|
47
|
+
return { scheme: ontology.getScheme(key), concepts: all, top };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function childClasses(parentId: string): OwlClass[] {
|
|
51
|
+
const cls = getClass(parentId);
|
|
52
|
+
if (!cls) return [];
|
|
53
|
+
return cls.children.map(id => getClass(id)).filter((c): c is OwlClass => !!c);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasChildren(cls: OwlClass): boolean {
|
|
57
|
+
return cls.children.length > 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const expandedClasses = ref(new Set<string>(['gloss:Concept', 'gloss:Designation']));
|
|
61
|
+
|
|
62
|
+
function toggleExpand(cls: OwlClass) {
|
|
63
|
+
const s = new Set(expandedClasses.value);
|
|
64
|
+
if (s.has(cls.compact)) s.delete(cls.compact);
|
|
65
|
+
else s.add(cls.compact);
|
|
66
|
+
expandedClasses.value = s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const allNavItems = computed(() => {
|
|
70
|
+
const items: { id: string; label: string; depth: number }[] = [];
|
|
71
|
+
function walk(classes: OwlClass[], depth: number) {
|
|
72
|
+
for (const cls of classes) {
|
|
73
|
+
items.push({ id: cls.compact, label: cls.label, depth });
|
|
74
|
+
if (expandedClasses.value.has(cls.compact)) {
|
|
75
|
+
walk(childClasses(cls.compact), depth + 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
walk(treeRoots, 0);
|
|
80
|
+
return items;
|
|
81
|
+
});
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
86
|
+
<!-- Header -->
|
|
87
|
+
<div class="mb-8">
|
|
88
|
+
<h1 class="text-2xl sm:text-3xl font-semibold text-ink-800">Glossarist Ontology</h1>
|
|
89
|
+
<p class="text-sm text-ink-400 mt-1">
|
|
90
|
+
OWL ontology for terminology management (ISO 10241-1, 30042, 12620, 25964/SKOS)
|
|
91
|
+
</p>
|
|
92
|
+
<div class="max-w-2xl mt-3 text-sm text-ink-500 leading-relaxed space-y-2">
|
|
93
|
+
<p>The Glossarist ontology defines the RDF/OWL vocabulary for describing structured terminology data. It models <strong>concepts</strong> with multilingual <strong>localizations</strong> (definitions, notes, examples) and typed <strong>designations</strong> (expressions, abbreviations, symbols) using the SKOS-XL pattern for reified lexical labels.</p>
|
|
94
|
+
<p>It aligns with <strong>SKOS</strong> (concepts and relationships), <strong>SKOS-XL</strong> (designations as labels), <strong>ISO 25964</strong> (hierarchical relationship subtypes — generic, partitive, instantial), <strong>PROV-O</strong> (source provenance), and <strong>Dublin Core Terms</strong> (language, citation). Enumeration values use SKOS ConceptSchemes (10 taxonomies).</p>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="flex flex-wrap gap-2 mt-4">
|
|
97
|
+
<span class="badge badge-blue text-[10px]">{{ stats.classCount }} classes</span>
|
|
98
|
+
<span class="badge text-[10px] bg-emerald-50 text-emerald-700">{{ stats.objectPropertyCount }} object properties</span>
|
|
99
|
+
<span class="badge text-[10px] bg-amber-50 text-amber-700">{{ stats.datatypePropertyCount }} datatype properties</span>
|
|
100
|
+
<span class="badge text-[10px] bg-purple-50 text-purple-700">{{ taxonomyKeys.length }} SKOS taxonomies</span>
|
|
101
|
+
</div>
|
|
102
|
+
<code class="block text-xs text-ink-400 mt-2">https://www.glossarist.org/ontologies/glossarist</code>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<!-- Sticky mobile chips -->
|
|
106
|
+
<div class="lg:hidden sticky top-14 z-10 bg-surface -mx-4 px-4 py-2 border-b border-ink-100/60 mb-4">
|
|
107
|
+
<div class="flex gap-2 overflow-x-auto scrollbar-none">
|
|
108
|
+
<button v-for="item in allNavItems" :key="item.id"
|
|
109
|
+
@click="activeClassId = item.id; activeTaxonomy = null"
|
|
110
|
+
class="flex-shrink-0 px-3 py-2.5 rounded-lg text-xs font-medium whitespace-nowrap transition-colors min-h-[44px] flex items-center gap-1.5"
|
|
111
|
+
:class="activeClassId === item.id && !activeTaxonomy
|
|
112
|
+
? 'bg-ink-800 text-white'
|
|
113
|
+
: 'bg-surface-raised border border-ink-100 text-ink-600 hover:bg-ink-50'"
|
|
114
|
+
>
|
|
115
|
+
<span v-if="item.depth > 0" class="text-ink-300">{{ ' '.repeat(item.depth * 2) }}</span>
|
|
116
|
+
{{ item.label }}
|
|
117
|
+
</button>
|
|
118
|
+
<button @click="activeTaxonomy = activeTaxonomy ? null : taxonomyKeys[0]; showTaxonomies = true"
|
|
119
|
+
class="flex-shrink-0 px-3 py-2.5 rounded-lg text-xs font-medium whitespace-nowrap transition-colors min-h-[44px] flex items-center gap-1.5"
|
|
120
|
+
:class="activeTaxonomy
|
|
121
|
+
? 'bg-ink-800 text-white'
|
|
122
|
+
: 'bg-surface-raised border border-ink-100 text-ink-600 hover:bg-ink-50'"
|
|
123
|
+
>Taxonomies</button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<!-- Two-column layout -->
|
|
128
|
+
<div class="lg:grid lg:grid-cols-[220px_1fr] lg:gap-6">
|
|
129
|
+
<!-- Left: tree sidebar (desktop only) -->
|
|
130
|
+
<nav class="hidden lg:block border-r border-ink-100/60 pr-4">
|
|
131
|
+
<div class="section-label mb-2">Classes</div>
|
|
132
|
+
|
|
133
|
+
<!-- Tree roots with recursive children -->
|
|
134
|
+
<template v-for="root in treeRoots" :key="root.compact">
|
|
135
|
+
<div>
|
|
136
|
+
<button @click="activeClassId = root.compact; activeTaxonomy = null; toggleExpand(root)"
|
|
137
|
+
class="w-full flex items-center gap-1.5 px-2 py-2 rounded-lg text-sm transition-colors"
|
|
138
|
+
:class="activeClassId === root.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-600 hover:bg-ink-50'"
|
|
139
|
+
>
|
|
140
|
+
<span v-if="hasChildren(root)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(root.compact) ? '▾' : '▸' }}</span>
|
|
141
|
+
<span v-else class="w-3"></span>
|
|
142
|
+
<span class="flex-1 text-left">{{ root.label }}</span>
|
|
143
|
+
</button>
|
|
144
|
+
|
|
145
|
+
<!-- Children -->
|
|
146
|
+
<div v-if="expandedClasses.has(root.compact) && hasChildren(root)" class="ml-4">
|
|
147
|
+
<template v-for="child in childClasses(root.compact)" :key="child.compact">
|
|
148
|
+
<button @click="activeClassId = child.compact; activeTaxonomy = null; toggleExpand(child)"
|
|
149
|
+
class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
|
|
150
|
+
:class="activeClassId === child.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-500 hover:bg-ink-50'"
|
|
151
|
+
>
|
|
152
|
+
<span v-if="hasChildren(child)" class="text-[10px] text-ink-300 w-3">{{ expandedClasses.has(child.compact) ? '▾' : '▸' }}</span>
|
|
153
|
+
<span v-else class="w-3 text-ink-200">·</span>
|
|
154
|
+
<span class="flex-1 text-left">{{ child.label }}</span>
|
|
155
|
+
</button>
|
|
156
|
+
<!-- Grandchildren -->
|
|
157
|
+
<div v-if="expandedClasses.has(child.compact) && hasChildren(child)" class="ml-4">
|
|
158
|
+
<button v-for="gc in childClasses(child.compact)" :key="gc.compact"
|
|
159
|
+
@click="activeClassId = gc.compact; activeTaxonomy = null"
|
|
160
|
+
class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
|
|
161
|
+
:class="activeClassId === gc.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
|
|
162
|
+
>
|
|
163
|
+
<span class="w-3 text-ink-200">·</span>
|
|
164
|
+
<span class="flex-1 text-left">{{ gc.label }}</span>
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
</template>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</template>
|
|
171
|
+
|
|
172
|
+
<!-- Remaining classes not in tree (supporting) -->
|
|
173
|
+
<div class="mt-4 pt-3 border-t border-ink-100/40">
|
|
174
|
+
<div class="text-[10px] uppercase tracking-wide text-ink-300 mb-1.5">Supporting</div>
|
|
175
|
+
<button v-for="cls in allClasses.filter(c => c.children.length === 0 && !c.subClassOf?.startsWith('gloss:') && c.compact !== 'gloss:Concept' && c.compact !== 'gloss:ConceptCollection')" :key="cls.compact"
|
|
176
|
+
@click="activeClassId = cls.compact; activeTaxonomy = null"
|
|
177
|
+
class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
|
|
178
|
+
:class="activeClassId === cls.compact && !activeTaxonomy ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
|
|
179
|
+
>
|
|
180
|
+
<span class="w-3 text-ink-200">·</span>
|
|
181
|
+
<span class="flex-1 text-left">{{ cls.label }}</span>
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<!-- Taxonomies -->
|
|
186
|
+
<div class="mt-4 pt-3 border-t border-ink-100/40">
|
|
187
|
+
<div class="text-[10px] uppercase tracking-wide text-ink-300 mb-1.5">SKOS Taxonomies</div>
|
|
188
|
+
<button v-for="tk in taxonomyKeys" :key="tk"
|
|
189
|
+
@click="activeTaxonomy = tk; showTaxonomies = true"
|
|
190
|
+
class="w-full flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-xs transition-colors"
|
|
191
|
+
:class="activeTaxonomy === tk ? 'bg-ink-800/8 text-blue-700 font-medium' : 'text-ink-400 hover:bg-ink-50'"
|
|
192
|
+
>
|
|
193
|
+
<span class="w-3 text-ink-200">·</span>
|
|
194
|
+
<span class="flex-1 text-left">{{ taxonomyLabels[tk] }}</span>
|
|
195
|
+
</button>
|
|
196
|
+
</div>
|
|
197
|
+
</nav>
|
|
198
|
+
|
|
199
|
+
<!-- Right: detail panel -->
|
|
200
|
+
<div class="mt-4 lg:mt-0">
|
|
201
|
+
<!-- Class detail -->
|
|
202
|
+
<template v-if="!activeTaxonomy && activeClass">
|
|
203
|
+
<div class="pb-4 border-b border-ink-100/60 mb-4">
|
|
204
|
+
<h2 class="text-lg font-semibold text-ink-800">{{ activeClass.label }}</h2>
|
|
205
|
+
<code class="block text-xs text-ink-400 mt-1">{{ activeClass.iri }}</code>
|
|
206
|
+
<div v-if="activeClass.subClassOf" class="flex items-center gap-2 mt-2 text-sm">
|
|
207
|
+
<span class="text-ink-400 text-xs">subClassOf</span>
|
|
208
|
+
<code class="text-xs text-ink-600 bg-ink-50 px-2 py-0.5 rounded">{{ activeClass.subClassOf }}</code>
|
|
209
|
+
<template v-if="activeClass.ancestors.length > 1">
|
|
210
|
+
<span class="text-ink-300 text-xs">→</span>
|
|
211
|
+
<code class="text-xs text-ink-500">{{ activeClass.ancestors.slice(1).join(' → ') }}</code>
|
|
212
|
+
</template>
|
|
213
|
+
</div>
|
|
214
|
+
<p v-if="activeClass.comment" class="text-sm text-ink-500 mt-3 leading-relaxed">{{ activeClass.comment }}</p>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<!-- Object Properties -->
|
|
218
|
+
<div v-if="activeProperties.object.length" class="mb-6">
|
|
219
|
+
<h3 class="text-xs uppercase tracking-wide text-ink-300 font-medium mb-2">
|
|
220
|
+
Object Properties ({{ activeProperties.object.length }})
|
|
221
|
+
</h3>
|
|
222
|
+
<table class="w-full text-sm border-collapse">
|
|
223
|
+
<thead>
|
|
224
|
+
<tr class="border-b border-ink-100/60">
|
|
225
|
+
<th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Property</th>
|
|
226
|
+
<th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Range</th>
|
|
227
|
+
<th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Description</th>
|
|
228
|
+
</tr>
|
|
229
|
+
</thead>
|
|
230
|
+
<tbody>
|
|
231
|
+
<tr v-for="p in activeProperties.object" :key="p.compact" class="border-b border-ink-100/30">
|
|
232
|
+
<td class="py-2 px-3 align-top">
|
|
233
|
+
<code class="text-xs text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded">{{ p.compact }}</code>
|
|
234
|
+
<div v-if="p.inverseOf" class="text-[10px] text-ink-300 mt-0.5">↔ {{ p.inverseOf }}</div>
|
|
235
|
+
</td>
|
|
236
|
+
<td class="py-2 px-3 align-top">
|
|
237
|
+
<code class="text-xs text-ink-500">{{ p.range || p.rangeUnion?.join(' | ') || '—' }}</code>
|
|
238
|
+
</td>
|
|
239
|
+
<td class="py-2 px-3 text-xs text-ink-400 align-top">{{ p.comment || '' }}</td>
|
|
240
|
+
</tr>
|
|
241
|
+
</tbody>
|
|
242
|
+
</table>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- Datatype Properties -->
|
|
246
|
+
<div v-if="activeProperties.datatype.length">
|
|
247
|
+
<h3 class="text-xs uppercase tracking-wide text-ink-300 font-medium mb-2">
|
|
248
|
+
Datatype Properties ({{ activeProperties.datatype.length }})
|
|
249
|
+
</h3>
|
|
250
|
+
<table class="w-full text-sm border-collapse">
|
|
251
|
+
<thead>
|
|
252
|
+
<tr class="border-b border-ink-100/60">
|
|
253
|
+
<th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Property</th>
|
|
254
|
+
<th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Datatype</th>
|
|
255
|
+
<th class="text-left text-[11px] font-medium text-ink-400 uppercase tracking-wide py-2 px-3">Description</th>
|
|
256
|
+
</tr>
|
|
257
|
+
</thead>
|
|
258
|
+
<tbody>
|
|
259
|
+
<tr v-for="p in activeProperties.datatype" :key="p.compact" class="border-b border-ink-100/30">
|
|
260
|
+
<td class="py-2 px-3 align-top">
|
|
261
|
+
<code class="text-xs text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded">{{ p.compact }}</code>
|
|
262
|
+
</td>
|
|
263
|
+
<td class="py-2 px-3 align-top">
|
|
264
|
+
<code class="text-xs text-ink-500">{{ p.range || '—' }}</code>
|
|
265
|
+
</td>
|
|
266
|
+
<td class="py-2 px-3 text-xs text-ink-400 align-top">{{ p.comment || '' }}</td>
|
|
267
|
+
</tr>
|
|
268
|
+
</tbody>
|
|
269
|
+
</table>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div v-if="!activeProperties.object.length && !activeProperties.datatype.length" class="text-sm text-ink-300 italic">
|
|
273
|
+
No properties defined directly on this class.
|
|
274
|
+
</div>
|
|
275
|
+
</template>
|
|
276
|
+
|
|
277
|
+
<!-- Taxonomy detail -->
|
|
278
|
+
<template v-if="activeTaxonomy && activeTaxonomyData()">
|
|
279
|
+
<div class="pb-4 border-b border-ink-100/60 mb-4">
|
|
280
|
+
<h2 class="text-lg font-semibold text-ink-800">{{ taxonomyLabels[activeTaxonomy] }}</h2>
|
|
281
|
+
<code class="block text-xs text-ink-400 mt-1">{{ activeTaxonomyData()!.scheme }}</code>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div class="space-y-2">
|
|
285
|
+
<div v-for="concept in activeTaxonomyData()!.concepts" :key="concept.id"
|
|
286
|
+
class="border border-ink-100/60 rounded-lg p-3">
|
|
287
|
+
<div class="flex items-center gap-2">
|
|
288
|
+
<code class="text-xs font-semibold text-ink-700">{{ concept.id }}</code>
|
|
289
|
+
<span class="text-sm text-ink-600">{{ concept.prefLabel }}</span>
|
|
290
|
+
<span v-if="concept.altLabel" class="text-xs text-ink-400">({{ concept.altLabel }})</span>
|
|
291
|
+
</div>
|
|
292
|
+
<p v-if="concept.definition" class="text-xs text-ink-400 mt-1 leading-relaxed">{{ concept.definition }}</p>
|
|
293
|
+
<div v-if="concept.broader" class="text-[10px] text-ink-300 mt-1">
|
|
294
|
+
broader: <code class="text-ink-400">{{ concept.broader }}</code>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</template>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</template>
|