@glossarist/concept-browser 0.7.34 → 0.7.37

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.
Files changed (37) hide show
  1. package/package.json +2 -2
  2. package/scripts/build-edges.js +16 -8
  3. package/scripts/generate-data.mjs +284 -86
  4. package/src/__tests__/citation-display.test.ts +165 -3
  5. package/src/__tests__/cite-ref.test.ts +112 -0
  6. package/src/__tests__/concept-detail-interaction.test.ts +1 -5
  7. package/src/__tests__/{math.test.ts → content-renderer.test.ts} +113 -29
  8. package/src/__tests__/escape.test.ts +76 -0
  9. package/src/__tests__/graph-data-source.test.ts +155 -0
  10. package/src/__tests__/model-bridge-bridges.test.ts +150 -0
  11. package/src/__tests__/model-bridge-citation.test.ts +163 -0
  12. package/src/__tests__/reference-resolver-cite.test.ts +122 -0
  13. package/src/__tests__/reference-resolver.test.ts +12 -7
  14. package/src/__tests__/resolve-view.test.ts +1 -1
  15. package/src/__tests__/sidebar-nav-highlighting.test.ts +178 -0
  16. package/src/__tests__/source-refs.test.ts +9 -6
  17. package/src/__tests__/test-helpers.ts +20 -0
  18. package/src/__tests__/uri-router.test.ts +39 -12
  19. package/src/adapters/DatasetAdapter.ts +35 -143
  20. package/src/adapters/GraphDataSource.ts +178 -0
  21. package/src/adapters/ReferenceResolver.ts +101 -47
  22. package/src/adapters/UriRouter.ts +82 -10
  23. package/src/adapters/factory.ts +35 -28
  24. package/src/adapters/model-bridge.ts +121 -71
  25. package/src/adapters/types.ts +3 -0
  26. package/src/components/AppSidebar.vue +7 -4
  27. package/src/components/CitationDisplay.vue +86 -30
  28. package/src/components/ConceptDetail.vue +24 -126
  29. package/src/components/LanguageDetail.vue +6 -6
  30. package/src/composables/use-concept-content.ts +8 -8
  31. package/src/composables/use-ontology-nav.ts +129 -130
  32. package/src/composables/use-render-options.ts +1 -1
  33. package/src/graph/GraphEngine.ts +65 -0
  34. package/src/stores/vocabulary.ts +12 -73
  35. package/src/utils/content-renderer.ts +312 -0
  36. package/src/utils/markdown-lite.ts +2 -2
  37. package/src/utils/math.ts +0 -189
@@ -1,21 +1,21 @@
1
1
  <script setup lang="ts">
2
- import type { Concept, LocalizedConcept, Designation, ConceptSource } from 'glossarist';
2
+ import type { Concept, LocalizedConcept, Designation } from 'glossarist';
3
3
  import type { Manifest, GraphEdge } from '../adapters/types';
4
4
  import { computed, ref, nextTick, watch } from 'vue';
5
- import { langName, langLabel, sortLanguages } from '../utils/lang';
6
- import { renderMath, cleanContent } from '../utils/math';
7
- import type { RenderOptions } from '../utils/math';
5
+ import { langName } from '../utils/lang';
6
+ import { renderContent } from '../utils/content-renderer';
7
+ import type { RenderOptions } from '../utils/content-renderer';
8
8
  import { escapeAttr } from '../utils/escape';
9
9
  import { entryStatusColor, conceptStatusColor, conceptStatusLabel, conceptStatusDefinition, entryStatusLabel, entryStatusDefinition, getPreferredTerm } from '../utils/concept-helpers';
10
10
  import { sourceTypeInfo, sourceStatusInfo } from '../utils/designation-registry';
11
- import { conceptUri, getAnnotations } from '../adapters/model-bridge';
11
+ import { conceptUri } from '../adapters/model-bridge';
12
12
  import { useRouter } from 'vue-router';
13
13
  import { useVocabularyStore } from '../stores/vocabulary';
14
14
  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 { useConceptEdges } from '../composables/use-concept-edges';
18
- import { useConceptContent, type LangContent } from '../composables/use-concept-content';
18
+ import { useConceptContent } from '../composables/use-concept-content';
19
19
  import { relationshipLabel, INVERSE_RELATIONSHIPS } from '../utils/relationship-categories';
20
20
  import { slugify } from '../utils/slugify';
21
21
  import { useSiteConfig } from '../config/use-site-config';
@@ -83,27 +83,12 @@ function copyUri() {
83
83
  });
84
84
  }
85
85
 
86
- const languages = computed(() => {
87
- const sorted = sortLanguages(props.concept.languages, props.manifest.languageOrder);
88
- // Put current UI locale first
89
- const current = locale.value;
90
- const idx = sorted.indexOf(current);
91
- if (idx > 0) {
92
- sorted.splice(idx, 1);
93
- sorted.unshift(current);
94
- }
95
- return sorted;
96
- });
97
-
98
- // Collapsible language sections — expand all with content, collapse those without
99
- const collapsedLangs = ref(new Set<string>());
100
-
101
86
  const engConcept = computed((): LocalizedConcept | null => {
102
87
  return props.concept.localization('eng') ?? null;
103
88
  });
104
89
 
105
90
  const primaryTerm = computed(() => getPreferredTerm(engConcept.value, conceptId.value));
106
- const renderedPrimaryTerm = computed(() => renderMath(primaryTerm.value));
91
+ const renderedPrimaryTerm = computed(() => renderContent(primaryTerm.value));
107
92
 
108
93
  const managedStatus = computed(() => props.concept.status);
109
94
 
@@ -115,12 +100,10 @@ const conceptSources = computed(() => props.concept.sources);
115
100
 
116
101
  const conceptTags = computed(() => props.concept.tags ?? []);
117
102
 
118
- // Cross-reference resolver: generates clickable links for inline refs
119
-
120
103
  const factory = getFactory();
121
104
  const { ensureBibLoaded, bibResolver, figResolver } = useRenderOptions(() => props.registerId);
122
105
 
123
- const renderOpts: RenderOptions = {
106
+ const renderOpts = computed<RenderOptions>(() => ({
124
107
  xrefResolver: (uri, term) => {
125
108
  const resolution = factory.resolve(uri, props.registerId);
126
109
  if (resolution.type === 'internal') {
@@ -135,15 +118,16 @@ const renderOpts: RenderOptions = {
135
118
  return escapeAttr(term);
136
119
  },
137
120
  conceptRefResolver: (conceptId, term) => {
138
- return `<a href="#" class="xref-link" data-register="${escapeAttr(props.registerId)}" data-concept="${escapeAttr(conceptId)}">${escapeAttr(term)}</a>`;
121
+ const adapter = factory.getAdapter(props.registerId);
122
+ const resolvedId = adapter?.lookupByDesignation(conceptId) ?? conceptId;
123
+ return `<a href="#" class="xref-link" data-register="${escapeAttr(props.registerId)}" data-concept="${escapeAttr(resolvedId)}">${escapeAttr(term)}</a>`;
139
124
  },
140
125
  bibResolver,
141
126
  figResolver,
142
- };
127
+ }));
143
128
 
144
129
  watch(() => props.registerId, () => { ensureBibLoaded(); }, { immediate: true });
145
130
 
146
- // Handle clicks on cross-reference links via event delegation
147
131
  function handleContentClick(e: MouseEvent) {
148
132
  const target = (e.target as HTMLElement).closest('.xref-link') as HTMLElement | null;
149
133
  if (!target) return;
@@ -155,86 +139,18 @@ function handleContentClick(e: MouseEvent) {
155
139
  }
156
140
  }
157
141
 
158
- // LangContent type is imported from the composable
159
-
160
- const allLangContent = computed(() => {
161
- const result: LangContent[] = [];
162
- for (const lang of languages.value) {
163
- const lc = props.concept.localization(lang);
164
- if (!lc) continue;
165
-
166
- const definition = lc.definitions
167
- .map(d => d.content).filter(Boolean).join('\n\n');
168
- const annotations = getAnnotations(lc).map(a => a.content).filter(Boolean);
169
- const notes = lc.notes.map(n => n.content).filter(Boolean);
170
- const examples = lc.examples.map(e => e.content).filter(Boolean);
171
-
172
- result.push({
173
- lang,
174
- lc,
175
- renderedTerm: renderMath(getPreferredTerm(lc, '')),
176
- definition,
177
- renderedDefinition: renderMath(definition, renderOpts),
178
- annotations,
179
- renderedAnnotations: annotations.map((a: string) => renderMath(a, renderOpts)),
180
- notes,
181
- renderedNotes: notes.map(n => renderMath(n, renderOpts)),
182
- examples,
183
- renderedExamples: examples.map(e => renderMath(e, renderOpts)),
184
- sources: lc.sources,
185
- designations: lc.terms,
186
- renderedDesignations: new Map(lc.terms.map(d => [d.designation, renderMath(d.designation)])),
187
- entryStatus: lc.entryStatus ?? '',
188
- classification: lc.classification,
189
- reviewType: lc.reviewType,
190
- release: lc.release,
191
- lineageSourceSimilarity: lc.lineageSourceSimilarity,
192
- lcScript: lc.script,
193
- lcSystem: lc.system,
194
- });
195
- }
196
- return result;
197
- });
198
-
199
- const langContentMap = computed(() => {
200
- const map = new Map<string, LangContent>();
201
- for (const lc of allLangContent.value) map.set(lc.lang, lc);
202
- return map;
203
- });
204
-
205
- function hasContent(lc: LangContent): boolean {
206
- return !!(lc.definition || lc.annotations.length || lc.notes.length || lc.examples.length || lc.sources.length);
207
- }
208
-
209
- function initCollapsed() {
210
- const mainLangs = siteConfig.value?.defaults?.mainLanguages || [];
211
- const mainSet = new Set(mainLangs.length ? mainLangs : ['eng']);
212
- const collapsed = new Set<string>();
213
- for (const lc of allLangContent.value) {
214
- if (!hasContent(lc) && !mainSet.has(lc.lang)) {
215
- collapsed.add(lc.lang);
216
- }
217
- }
218
- collapsedLangs.value = collapsed;
219
- }
220
-
221
- watch(languages, () => { initCollapsed(); }, { immediate: true });
222
-
223
- const allCollapsed = computed(() => collapsedLangs.value.size === allLangContent.value.length);
224
-
225
- function toggleLang(lang: string) {
226
- const s = new Set(collapsedLangs.value);
227
- if (s.has(lang)) s.delete(lang); else s.add(lang);
228
- collapsedLangs.value = s;
229
- }
230
-
231
- function toggleAll() {
232
- if (allCollapsed.value) {
233
- collapsedLangs.value = new Set();
234
- } else {
235
- collapsedLangs.value = new Set(allLangContent.value.map(lc => lc.lang));
236
- }
237
- }
142
+ const {
143
+ languages,
144
+ allLangContent,
145
+ langContentMap,
146
+ hasContent,
147
+ collapsedLangs,
148
+ allCollapsed,
149
+ toggleLang,
150
+ toggleAll,
151
+ plainTruncate,
152
+ orderedDesignations,
153
+ } = useConceptContent(conceptComputed, manifestComputed, renderOpts);
238
154
 
239
155
  function scrollToLang(lang: string) {
240
156
  if (collapsedLangs.value.has(lang)) {
@@ -263,14 +179,6 @@ function getDesignationsForLang(lang: string): Designation[] {
263
179
  return lc?.terms ?? [];
264
180
  }
265
181
 
266
- function orderedDesignations(lang: string): Designation[] {
267
- const desigs = getDesignationsForLang(lang);
268
- const preferred = desigs.filter(d => d.normativeStatus === 'preferred');
269
- const admitted = desigs.filter(d => d.normativeStatus === 'admitted' || d.normativeStatus === 'deprecated');
270
- const rest = desigs.filter(d => d.normativeStatus !== 'preferred' && d.normativeStatus !== 'admitted' && d.normativeStatus !== 'deprecated');
271
- return [...preferred, ...admitted, ...rest];
272
- }
273
-
274
182
  function hasDefinition(lang: string): boolean {
275
183
  const lc = props.concept.localization(lang);
276
184
  if (!lc) return false;
@@ -282,16 +190,9 @@ function goAdjacent(id: string) {
282
190
  window.scrollTo({ top: 0, behavior: 'smooth' });
283
191
  }
284
192
 
285
- function plainTruncate(html: string, max: number = 120): string {
286
- const text = cleanContent(html).replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
287
- return text.length <= max ? text : text.slice(0, max).trimEnd() + '…';
288
- }
289
-
290
- // Domain rendering: merge ConceptReference domains and per-localization domain strings
291
193
  const conceptDomains = computed(() => {
292
194
  const domainMap = new Map<string, { slug: string; label: string; langs: string[]; conceptId?: string }>();
293
195
 
294
- // Managed concept level ConceptReference domains (authoritative)
295
196
  for (const ref of conceptRefDomains.value) {
296
197
  const id = ref.conceptId ?? '';
297
198
  const label = id || ref.urn || '';
@@ -301,7 +202,6 @@ const conceptDomains = computed(() => {
301
202
  }
302
203
  }
303
204
 
304
- // Per-localization domain strings
305
205
  for (const lang of props.concept.languages) {
306
206
  const lc = props.concept.localization(lang);
307
207
  const domain = lc?.domain;
@@ -318,7 +218,6 @@ const conceptDomains = computed(() => {
318
218
  return [...domainMap.values()].sort((a, b) => b.langs.length - a.langs.length);
319
219
  });
320
220
 
321
- // Non-verbal reps: aggregate across all localizations
322
221
  const nonVerbalReps = computed(() => {
323
222
  const reps: typeof import('glossarist').NonVerbRep.prototype[] = [];
324
223
  for (const lang of props.concept.languages) {
@@ -329,7 +228,6 @@ const nonVerbalReps = computed(() => {
329
228
  }
330
229
  return reps;
331
230
  });
332
-
333
231
  </script>
334
232
 
335
233
  <template>
@@ -2,8 +2,8 @@
2
2
  import type { Concept, LocalizedConcept, Designation, Expression, Abbreviation as AbbreviationType } from 'glossarist';
3
3
  import { computed } from 'vue';
4
4
  import { langName, langLabel } from '../utils/lang';
5
- import { renderMath } from '../utils/math';
6
- import type { RenderOptions } from '../utils/math';
5
+ import { renderContent } from '../utils/content-renderer';
6
+ import type { RenderOptions } from '../utils/content-renderer';
7
7
  import { escapeAttr } from '../utils/escape';
8
8
  import { entryStatusColor } from '../utils/concept-helpers';
9
9
  import { designationTypeInfo, normativeStatusInfo, grammarBadges, pronunciationLabel, pronunciationTooltip } from '../utils/designation-registry';
@@ -115,7 +115,7 @@ function handleContentClick(e: MouseEvent) {
115
115
  <div class="section-label">{{ t('concept.designations') }}</div>
116
116
  <div class="space-y-2 mt-3">
117
117
  <div v-for="(d, i) in designations" :key="i" class="flex items-center gap-2 flex-wrap">
118
- <span class="font-medium text-ink-800 text-lg" v-html="renderMath(d.designation)"></span>
118
+ <span class="font-medium text-ink-800 text-lg" v-html="renderContent(d.designation)"></span>
119
119
  <span class="badge text-[10px]" :class="designationTypeInfo(d).color">{{ designationTypeInfo(d).label }}</span>
120
120
  <span class="badge text-[10px]" :class="normativeStatusInfo(d.normativeStatus).color">{{ normativeStatusInfo(d.normativeStatus).label }}</span>
121
121
  <template v-if="d.type === 'expression' && (d as Expression).grammarInfo?.length">
@@ -142,7 +142,7 @@ function handleContentClick(e: MouseEvent) {
142
142
  <!-- Definition -->
143
143
  <div v-if="definition" class="card p-5">
144
144
  <div class="section-label">{{ t('concept.definition') }}</div>
145
- <div class="text-ink-800 leading-relaxed mt-3" v-html="renderMath(definition, renderOpts)"></div>
145
+ <div class="text-ink-800 leading-relaxed mt-3" v-html="renderContent(definition, renderOpts)"></div>
146
146
  </div>
147
147
 
148
148
  <!-- Notes -->
@@ -151,7 +151,7 @@ function handleContentClick(e: MouseEvent) {
151
151
  <div class="space-y-3 mt-3">
152
152
  <div v-for="(note, i) in notes" :key="i" class="text-ink-600 text-sm leading-relaxed">
153
153
  <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.note') }} {{ i + 1 }}</span>
154
- <div class="mt-1" v-html="renderMath(note, renderOpts)"></div>
154
+ <div class="mt-1" v-html="renderContent(note, renderOpts)"></div>
155
155
  </div>
156
156
  </div>
157
157
  </div>
@@ -162,7 +162,7 @@ function handleContentClick(e: MouseEvent) {
162
162
  <div class="space-y-3 mt-3">
163
163
  <div v-for="(ex, i) in examples" :key="i" class="text-ink-600 text-sm leading-relaxed">
164
164
  <span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.example') }} {{ i + 1 }}</span>
165
- <div class="mt-1" v-html="renderMath(ex, renderOpts)"></div>
165
+ <div class="mt-1" v-html="renderContent(ex, renderOpts)"></div>
166
166
  </div>
167
167
  </div>
168
168
  </div>
@@ -1,8 +1,8 @@
1
1
  import { computed, ref, watch, type ComputedRef } from 'vue';
2
2
  import type { Concept, LocalizedConcept, ConceptSource, Designation } from 'glossarist';
3
3
  import type { Manifest } from '../adapters/types';
4
- import type { RenderOptions } from '../utils/math';
5
- import { renderMath, cleanContent } from '../utils/math';
4
+ import type { RenderOptions } from '../utils/content-renderer';
5
+ import { renderContent, cleanContent } from '../utils/content-renderer';
6
6
  import { getAnnotations } from '../adapters/model-bridge';
7
7
  import { getPreferredTerm, entryStatusColor, entryStatusLabel, entryStatusDefinition } from '../utils/concept-helpers';
8
8
  import { sortLanguages } from '../utils/lang';
@@ -68,18 +68,18 @@ export function useConceptContent(
68
68
  result.push({
69
69
  lang,
70
70
  lc,
71
- renderedTerm: renderMath(getPreferredTerm(lc, '')),
71
+ renderedTerm: renderContent(getPreferredTerm(lc, '')),
72
72
  definition,
73
- renderedDefinition: renderMath(definition, opts),
73
+ renderedDefinition: renderContent(definition, opts),
74
74
  annotations,
75
- renderedAnnotations: annotations.map((a: string) => renderMath(a, opts)),
75
+ renderedAnnotations: annotations.map((a: string) => renderContent(a, opts)),
76
76
  notes,
77
- renderedNotes: notes.map(n => renderMath(n, opts)),
77
+ renderedNotes: notes.map(n => renderContent(n, opts)),
78
78
  examples,
79
- renderedExamples: examples.map(e => renderMath(e, opts)),
79
+ renderedExamples: examples.map(e => renderContent(e, opts)),
80
80
  sources: lc.sources,
81
81
  designations: lc.terms,
82
- renderedDesignations: new Map(lc.terms.map(d => [d.designation, renderMath(d.designation)])),
82
+ renderedDesignations: new Map(lc.terms.map(d => [d.designation, renderContent(d.designation)])),
83
83
  entryStatus: lc.entryStatus ?? '',
84
84
  classification: lc.classification,
85
85
  reviewType: lc.reviewType,
@@ -26,51 +26,6 @@ export function compactToSlug(compact: string): string {
26
26
  return compact.replace(/:/g, '-');
27
27
  }
28
28
 
29
- const expandedClasses = ref(new Set<string>(['gloss:Designation']));
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
29
  const treeRoots = getClassTree();
75
30
  const allShapes = getAllShapes();
76
31
  const objectProperties = getObjectProperties();
@@ -97,28 +52,6 @@ const taxonomyLabels: Record<string, string> = {
97
52
  grammarNumber: 'Grammar Number',
98
53
  };
99
54
 
100
- interface IndividualGroup {
101
- key: string;
102
- label: string;
103
- concepts: { id: string; prefLabel: string }[];
104
- }
105
-
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
- });
117
-
118
- const totalIndividuals = computed(() =>
119
- groupedIndividuals.value.reduce((sum, g) => sum + g.concepts.length, 0),
120
- );
121
-
122
55
  const valuesToTaxonomy: Record<string, string> = {
123
56
  'gloss:status': 'conceptStatus',
124
57
  'gloss:entstatus': 'entryStatus',
@@ -132,6 +65,20 @@ const valuesToTaxonomy: Record<string, string> = {
132
65
  'gloss:number': 'grammarNumber',
133
66
  };
134
67
 
68
+ function childClasses(parentId: string): OwlClass[] {
69
+ const cls = getClass(parentId);
70
+ if (!cls) return [];
71
+ return cls.children.map(id => getClass(id)).filter((c): c is OwlClass => !!c);
72
+ }
73
+
74
+ function hasChildren(cls: OwlClass): boolean {
75
+ return cls.children.length > 0;
76
+ }
77
+
78
+ function matchesSearch(text: string, query: string): boolean {
79
+ return text.toLowerCase().includes(query.toLowerCase());
80
+ }
81
+
135
82
  function taxonomyKeyForValuesFrom(valuesFrom: string | null): string | null {
136
83
  if (!valuesFrom) return null;
137
84
  return valuesToTaxonomy[valuesFrom] ?? null;
@@ -145,80 +92,126 @@ function getShapesForTaxonomy(taxonomyKey: string): OwlShape[] {
145
92
  );
146
93
  }
147
94
 
148
- const allNavItems = computed(() => {
149
- const items: { id: string; label: string; depth: number }[] = [];
150
- function walk(classes: OwlClass[], depth: number) {
151
- for (const cls of classes) {
152
- items.push({ id: cls.compact, label: cls.label, depth });
153
- if (expandedClasses.value.has(cls.compact)) {
154
- walk(childClasses(cls.compact), depth + 1);
155
- }
156
- }
95
+ export function useOntologyNav() {
96
+ const expandedClasses = ref(new Set<string>(['gloss:Designation']));
97
+
98
+ const collapsedSections = ref(new Set<string>([
99
+ 'objectProperty',
100
+ 'datatypeProperty',
101
+ 'shape',
102
+ 'taxonomy',
103
+ 'namedIndividual',
104
+ 'annotationProperty',
105
+ ]));
106
+
107
+ const searchQuery = ref('');
108
+
109
+ function toggleExpand(cls: OwlClass) {
110
+ const s = new Set(expandedClasses.value);
111
+ if (s.has(cls.compact)) s.delete(cls.compact);
112
+ else s.add(cls.compact);
113
+ expandedClasses.value = s;
157
114
  }
158
- walk(treeRoots, 0);
159
- return items;
160
- });
161
-
162
- function matchesSearch(text: string, query: string): boolean {
163
- return text.toLowerCase().includes(query.toLowerCase());
164
- }
165
115
 
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
- }
116
+ function toggleSection(key: string) {
117
+ const s = new Set(collapsedSections.value);
118
+ if (s.has(key)) s.delete(key);
119
+ else s.add(key);
120
+ collapsedSections.value = s;
185
121
  }
186
- walkAll(treeRoots);
187
122
 
188
- for (const p of objectProperties) {
189
- if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedObjectProps.push(p);
123
+ function expandAllSections() {
124
+ collapsedSections.value = new Set();
190
125
  }
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);
126
+
127
+ function collapseAllSections() {
128
+ collapsedSections.value = new Set(['objectProperty', 'datatypeProperty', 'shape', 'taxonomy', 'class', 'namedIndividual', 'annotationProperty']);
196
129
  }
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 });
130
+
131
+ const allNavItems = computed(() => {
132
+ const items: { id: string; label: string; depth: number }[] = [];
133
+ function walk(classes: OwlClass[], depth: number) {
134
+ for (const cls of classes) {
135
+ items.push({ id: cls.compact, label: cls.label, depth });
136
+ if (expandedClasses.value.has(cls.compact)) {
137
+ walk(childClasses(cls.compact), depth + 1);
138
+ }
201
139
  }
202
140
  }
203
- }
204
- for (const ap of annotationProperties) {
205
- if (matchesSearch(ap.label, q) || matchesSearch(ap.compact, q)) matchedAnnotationProps.push(ap);
206
- }
141
+ walk(treeRoots, 0);
142
+ return items;
143
+ });
207
144
 
208
- const total = matchedClasses.length + matchedObjectProps.length + matchedDatatypeProps.length + matchedShapes.length + matchedIndividuals.length + matchedAnnotationProps.length;
145
+ const searchResults = computed(() => {
146
+ const q = searchQuery.value.trim();
147
+ if (!q) return null;
148
+
149
+ const matchedClasses: OwlClass[] = [];
150
+ const matchedObjectProps: OwlProperty[] = [];
151
+ const matchedDatatypeProps: OwlProperty[] = [];
152
+ const matchedShapes: OwlShape[] = [];
153
+ const matchedIndividuals: { group: string; id: string; prefLabel: string }[] = [];
154
+ const matchedAnnotationProps: AnnotationProperty[] = [];
155
+
156
+ function walkAll(classes: OwlClass[]) {
157
+ for (const cls of classes) {
158
+ if (matchesSearch(cls.label, q) || matchesSearch(cls.compact, q)) {
159
+ matchedClasses.push(cls);
160
+ }
161
+ walkAll(childClasses(cls.compact));
162
+ }
163
+ }
164
+ walkAll(treeRoots);
209
165
 
210
- return {
211
- total,
212
- classes: matchedClasses,
213
- objectProperties: matchedObjectProps,
214
- datatypeProperties: matchedDatatypeProps,
215
- shapes: matchedShapes,
216
- individuals: matchedIndividuals,
217
- annotationProperties: matchedAnnotationProps,
218
- };
219
- });
166
+ for (const p of objectProperties) {
167
+ if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedObjectProps.push(p);
168
+ }
169
+ for (const p of datatypeProperties) {
170
+ if (matchesSearch(p.label, q) || matchesSearch(p.compact, q)) matchedDatatypeProps.push(p);
171
+ }
172
+ for (const s of allShapes) {
173
+ if (matchesSearch(s.label, q) || matchesSearch(s.compact, q)) matchedShapes.push(s);
174
+ }
175
+ for (const g of groupedIndividuals.value) {
176
+ for (const c of g.concepts) {
177
+ if (matchesSearch(c.prefLabel, q) || matchesSearch(c.id, q)) {
178
+ matchedIndividuals.push({ group: g.key, id: c.id, prefLabel: c.prefLabel });
179
+ }
180
+ }
181
+ }
182
+ for (const ap of annotationProperties) {
183
+ if (matchesSearch(ap.label, q) || matchesSearch(ap.compact, q)) matchedAnnotationProps.push(ap);
184
+ }
185
+
186
+ const total = matchedClasses.length + matchedObjectProps.length + matchedDatatypeProps.length + matchedShapes.length + matchedIndividuals.length + matchedAnnotationProps.length;
187
+
188
+ return {
189
+ total,
190
+ classes: matchedClasses,
191
+ objectProperties: matchedObjectProps,
192
+ datatypeProperties: matchedDatatypeProps,
193
+ shapes: matchedShapes,
194
+ individuals: matchedIndividuals,
195
+ annotationProperties: matchedAnnotationProps,
196
+ };
197
+ });
198
+
199
+ const groupedIndividuals = computed<IndividualGroup[]>(() => {
200
+ return taxonomyKeys.map(key => {
201
+ const tax = (taxonomyData as Record<string, any>)[key];
202
+ if (!tax) return { key, label: taxonomyLabels[key] || key, concepts: [] };
203
+ const concepts = Object.values(tax.concepts as Record<string, any>).map((c: any) => ({
204
+ id: c.id,
205
+ prefLabel: c.prefLabel,
206
+ }));
207
+ return { key, label: tax.schemeLabel || taxonomyLabels[key] || key, concepts };
208
+ });
209
+ });
210
+
211
+ const totalIndividuals = computed(() =>
212
+ groupedIndividuals.value.reduce((sum, g) => sum + g.concepts.length, 0),
213
+ );
220
214
 
221
- export function useOntologyNav() {
222
215
  return {
223
216
  expandedClasses,
224
217
  collapsedSections,
@@ -248,3 +241,9 @@ export function useOntologyNav() {
248
241
  ENTITY_TYPE_META,
249
242
  };
250
243
  }
244
+
245
+ interface IndividualGroup {
246
+ key: string;
247
+ label: string;
248
+ concepts: { id: string; prefLabel: string }[];
249
+ }
@@ -1,5 +1,5 @@
1
1
  import { ref, watch } from 'vue';
2
- import type { RenderOptions, BibResolver, FigResolver } from '../utils/math';
2
+ import type { RenderOptions, BibResolver, FigResolver } from '../utils/content-renderer';
3
3
  import { getFactory } from '../adapters/factory';
4
4
  import { escapeAttr } from '../utils/escape';
5
5