@glossarist/concept-browser 0.7.32 → 0.7.34

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.
@@ -0,0 +1,181 @@
1
+ import { computed, type ComputedRef } from 'vue';
2
+ import type { Router } from 'vue-router';
3
+ import type { Concept, RelatedConcept } from 'glossarist';
4
+ import type { Manifest, GraphEdge } from '../adapters/types';
5
+ import { getFactory } from '../adapters/factory';
6
+ import { conceptUri } from '../adapters/model-bridge';
7
+ import { useVocabularyStore } from '../stores/vocabulary';
8
+ import { useDsStyle } from '../utils/dataset-style';
9
+ import { categorizeRelationship, relationshipLabel, INVERSE_RELATIONSHIPS } from '../utils/relationship-categories';
10
+ import { langLabel } from '../utils/lang';
11
+ import { escapeAttr } from '../utils/escape';
12
+ import { useI18n } from '../i18n';
13
+
14
+ export interface EdgeDisplay {
15
+ uri: string;
16
+ conceptId: string;
17
+ designation: string;
18
+ tooltip: string;
19
+ isLocal: boolean;
20
+ badge: { id: string; title: string } | null;
21
+ }
22
+
23
+ export function useConceptEdges(
24
+ concept: ComputedRef<Concept>,
25
+ registerId: ComputedRef<string>,
26
+ manifest: ComputedRef<Manifest>,
27
+ edges: ComputedRef<GraphEdge[]>,
28
+ router: Router,
29
+ ) {
30
+ const factory = getFactory();
31
+ const store = useVocabularyStore();
32
+ const { getColor } = useDsStyle();
33
+ const { locale } = useI18n();
34
+
35
+ const conceptUriValue = computed(() =>
36
+ conceptUri(concept.value, registerId.value, manifest.value.uriBase)
37
+ );
38
+
39
+ const outgoingEdges = computed(() =>
40
+ store.graph.getUniqueEdges(conceptUriValue.value, 'outgoing', 'target')
41
+ .filter(e => e.type !== 'domain' && e.type !== 'section')
42
+ );
43
+
44
+ const incomingEdges = computed(() =>
45
+ store.graph.getUniqueEdges(conceptUriValue.value, 'incoming', 'source')
46
+ .filter(e => e.type !== 'domain' && e.type !== 'section')
47
+ );
48
+
49
+ const edgeDisplayCache = computed(() => {
50
+ const cache = new Map<string, EdgeDisplay>();
51
+ for (const e of edges.value) {
52
+ const uri = e.source === conceptUriValue.value ? e.target : e.source;
53
+ if (cache.has(uri)) continue;
54
+ const resolution = factory.resolve(uri, registerId.value);
55
+ const isLocal = resolution.type === 'internal' && resolution.registerId === registerId.value;
56
+ const conceptId = uri.match(/\/concept\/([^/]+)$/)?.[1] ?? uri.split('/').pop() ?? uri;
57
+ const node = store.graph.getNode(uri);
58
+ const designation = node
59
+ ? (node.designations[locale.value] || node.designations.eng || Object.values(node.designations)[0] || '')
60
+ : '';
61
+ const tooltipLines: string[] = [uri];
62
+ if (node) {
63
+ for (const [lang, des] of Object.entries(node.designations)) {
64
+ tooltipLines.push(`${langLabel(lang)}: ${des}`);
65
+ }
66
+ }
67
+ let badge: { id: string; title: string } | null = null;
68
+ if (resolution.type === 'internal' && resolution.registerId !== registerId.value) {
69
+ const m = store.manifests.get(resolution.registerId);
70
+ badge = { id: resolution.registerId, title: m?.shortname || m?.title || resolution.registerId };
71
+ } else if (resolution.type === 'site') {
72
+ badge = { id: '', title: resolution.label };
73
+ } else if (resolution.type === 'url') {
74
+ badge = { id: '', title: resolution.label };
75
+ }
76
+ cache.set(uri, { uri, conceptId, designation, tooltip: tooltipLines.join('\n'), isLocal, badge });
77
+ }
78
+ return cache;
79
+ });
80
+
81
+ function getEdgeDisplay(uri: string): EdgeDisplay {
82
+ return edgeDisplayCache.value.get(uri) ?? { uri, conceptId: uri, designation: '', tooltip: uri, isLocal: false, badge: null };
83
+ }
84
+
85
+ function edgeBadgeColor(type: string, direction: 'out' | 'in'): string {
86
+ if (type === 'supersedes' || type === 'superseded_by') {
87
+ return direction === 'out' ? 'text-orange-700 bg-orange-50' : 'text-red-700 bg-red-50';
88
+ }
89
+ return categorizeRelationship(type).color;
90
+ }
91
+
92
+ function inverseEdgeType(type: string): string {
93
+ return INVERSE_RELATIONSHIPS[type] || type;
94
+ }
95
+
96
+ // Concept-level related concepts (managed concept cross-references)
97
+ const conceptRelated = computed(() => {
98
+ const direct = concept.value.relatedConcepts?.filter(rc => !INVERSE_RELATIONSHIPS[rc.type]) ?? [];
99
+ const derived = incomingEdges.value
100
+ .filter(e => INVERSE_RELATIONSHIPS[e.type])
101
+ .map(e => {
102
+ const parsed = factory.resolve(e.source, registerId.value);
103
+ const sourceUrn = parsed.type === 'internal'
104
+ ? store.manifests.get(parsed.registerId)?.datasetUri
105
+ : null;
106
+ const conceptId = e.source.match(/\/concept\/([^/]+)$/)?.[1];
107
+ return {
108
+ type: INVERSE_RELATIONSHIPS[e.type],
109
+ ref: sourceUrn && conceptId ? { source: sourceUrn, id: conceptId } : null,
110
+ content: '',
111
+ };
112
+ });
113
+ return [...direct, ...derived];
114
+ });
115
+
116
+ function resolveRelatedRef(ref: { source: string | null; id: string | null } | null) {
117
+ return factory.resolveRelatedRef(ref, registerId.value);
118
+ }
119
+
120
+ const resolvedRefs = computed(() => {
121
+ const map = new Map<string, { target: { registerId: string; conceptId: string } | null }>();
122
+ for (const cr of conceptRelated.value) {
123
+ const key = `${cr.ref?.source ?? ''}:${cr.ref?.id ?? ''}`;
124
+ if (map.has(key)) continue;
125
+ map.set(key, { target: resolveRelatedRef(cr.ref) });
126
+ }
127
+ return map;
128
+ });
129
+
130
+ function getResolvedRef(ref: { source: string | null; id: string | null } | null) {
131
+ if (!ref) return { target: null };
132
+ const key = `${ref.source ?? ''}:${ref.id ?? ''}`;
133
+ return resolvedRefs.value.get(key) ?? { target: resolveRelatedRef(ref) };
134
+ }
135
+
136
+ function relatedLabel(dr: { content?: string | null; ref?: { source: string | null; id: string | null } | null }): string {
137
+ if (dr.content) return dr.content;
138
+ const resolved = dr.ref ? getResolvedRef(dr.ref).target : null;
139
+ if (resolved) {
140
+ const m = store.manifests.get(resolved.registerId);
141
+ const dsLabel = m?.shortname || m?.title || resolved.registerId;
142
+ return `${resolved.conceptId} (${dsLabel})`;
143
+ }
144
+ return dr.ref ? `${dr.ref.id || ''} (${dr.ref.source || ''})`.trim() : '';
145
+ }
146
+
147
+ async function navigateEdge(edge: GraphEdge) {
148
+ const uri = edge.source === conceptUriValue.value ? edge.target : edge.source;
149
+ const resolution = factory.resolve(uri);
150
+
151
+ if (resolution.type === 'internal') {
152
+ router.push({ name: 'concept', params: { registerId: resolution.registerId, conceptId: resolution.conceptId } });
153
+ } else if (resolution.type === 'site') {
154
+ window.open(`${resolution.baseUrl}/resolve/${encodeURIComponent(uri)}`, '_blank', 'noopener');
155
+ } else if (resolution.type === 'url') {
156
+ window.open(resolution.url, '_blank', 'noopener');
157
+ }
158
+ }
159
+
160
+ async function navigateRelated(ref: { source: string | null; id: string | null }) {
161
+ const target = resolveRelatedRef(ref);
162
+ if (!target) return;
163
+ router.push({ name: 'concept', params: { registerId: target.registerId, conceptId: target.conceptId } });
164
+ }
165
+
166
+ return {
167
+ conceptUriValue,
168
+ outgoingEdges,
169
+ incomingEdges,
170
+ edgeDisplayCache,
171
+ getEdgeDisplay,
172
+ edgeBadgeColor,
173
+ inverseEdgeType,
174
+ conceptRelated,
175
+ resolveRelatedRef,
176
+ getResolvedRef,
177
+ relatedLabel,
178
+ navigateEdge,
179
+ navigateRelated,
180
+ };
181
+ }
@@ -221,6 +221,18 @@
221
221
  "prefLabel": "not equal",
222
222
  "definition": "The concept is not equal to the source."
223
223
  }
224
+ },
225
+ "colors": {
226
+ "identical": "badge-green",
227
+ "similar": "badge-blue",
228
+ "modified": "badge-yellow",
229
+ "restyle": "badge-yellow",
230
+ "context_added": "badge-blue",
231
+ "generalisation": "badge-purple",
232
+ "specialisation": "badge-purple",
233
+ "unspecified": "badge-gray",
234
+ "related": "badge-gray",
235
+ "not_equal": "bg-red-50 text-red-700"
224
236
  }
225
237
  },
226
238
  "relationshipType": {
@@ -940,4 +952,4 @@
940
952
  }
941
953
  }
942
954
  }
943
- }
955
+ }
@@ -98,6 +98,21 @@ export class GraphEngine {
98
98
  return result;
99
99
  }
100
100
 
101
+ getUniqueEdges(uri: string, direction: 'outgoing' | 'incoming' | 'both', dedupeBy: 'source' | 'target' = 'target'): GraphEdge[] {
102
+ const raw = direction === 'outgoing'
103
+ ? this.getEdges(uri)
104
+ : direction === 'incoming'
105
+ ? this.getIncomingEdges(uri)
106
+ : [...this.getEdges(uri), ...this.getIncomingEdges(uri)];
107
+ const seen = new Set<string>();
108
+ return raw.filter(e => {
109
+ const key = `${e[dedupeBy]}\0${e.type}`;
110
+ if (seen.has(key)) return false;
111
+ seen.add(key);
112
+ return true;
113
+ });
114
+ }
115
+
101
116
  getNeighbors(uri: string): { outgoing: string[]; incoming: string[] } {
102
117
  const outgoing: string[] = [];
103
118
  const adj = this.adjacency.get(uri);
@@ -2,17 +2,15 @@ import type { LocalizedConcept } from 'glossarist';
2
2
  import { ontology } from '../adapters/ontology-registry';
3
3
 
4
4
  export function entryStatusColor(status: string): string {
5
- return ontology.getColor('entryStatus', status) ?? 'badge-gray';
5
+ return ontology.getDisplay('entryStatus', status).color;
6
6
  }
7
7
 
8
8
  export function conceptStatusColor(status: string | null): string {
9
- if (!status) return 'badge-gray';
10
- return ontology.getColor('conceptStatus', status) ?? 'badge-gray';
9
+ return ontology.getDisplay('conceptStatus', status).color;
11
10
  }
12
11
 
13
12
  export function conceptStatusLabel(status: string | null): string {
14
- if (!status) return '';
15
- return ontology.getLabel('conceptStatus', status) || status;
13
+ return ontology.getDisplay('conceptStatus', status).label;
16
14
  }
17
15
 
18
16
  export function conceptStatusDefinition(status: string | null): string | null {
@@ -21,8 +19,7 @@ export function conceptStatusDefinition(status: string | null): string | null {
21
19
  }
22
20
 
23
21
  export function entryStatusLabel(status: string | null): string {
24
- if (!status) return '';
25
- return ontology.getLabel('entryStatus', status) || status;
22
+ return ontology.getDisplay('entryStatus', status).label;
26
23
  }
27
24
 
28
25
  export function entryStatusDefinition(status: string | null): string | null {
@@ -9,52 +9,28 @@ export interface DesignationTypeInfo {
9
9
  }
10
10
 
11
11
  export function designationTypeInfo(designation: Designation): DesignationTypeInfo {
12
- const type = designation.type;
13
- const concept = ontology.getConcept('designationType', type);
14
- return {
15
- label: concept?.prefLabel ?? type,
16
- color: ontology.getColor('designationType', type) ?? 'bg-gray-50 text-gray-700',
17
- definition: concept?.definition ?? undefined,
18
- };
12
+ return ontology.getDisplay('designationType', designation.type, 'bg-gray-50 text-gray-700');
19
13
  }
20
14
 
21
15
  export function normativeStatusInfo(status: string | null): { label: string; color: string; definition?: string } {
22
- if (!status) return { label: '', color: 'bg-gray-50 text-gray-700' };
23
- const concept = ontology.getConcept('normativeStatus', status);
24
- return {
25
- label: concept?.prefLabel ?? status,
26
- color: ontology.getColor('normativeStatus', status) ?? 'bg-gray-50 text-gray-700',
27
- definition: concept?.definition ?? undefined,
28
- };
16
+ return ontology.getDisplay('normativeStatus', status, 'bg-gray-50 text-gray-700');
29
17
  }
30
18
 
31
19
  export function sourceStatusInfo(status: string | null): { label: string; color: string; definition?: string } {
32
- if (!status) return { label: '', color: 'badge-gray' };
33
- const concept = ontology.getConcept('sourceStatus', status);
34
- return {
35
- label: concept?.prefLabel ?? status,
36
- color: 'badge-gray',
37
- definition: concept?.definition ?? undefined,
38
- };
20
+ return ontology.getDisplay('sourceStatus', status, 'badge-gray');
39
21
  }
40
22
 
41
23
  export function sourceTypeInfo(type: string | null): { label: string; color: string; definition?: string } {
42
- if (!type) return { label: '', color: 'badge-gray' };
43
- const concept = ontology.getConcept('sourceType', type);
44
- return {
45
- label: concept?.prefLabel ?? type,
46
- color: ontology.getColor('sourceType', type) ?? 'badge-gray',
47
- definition: concept?.definition ?? undefined,
48
- };
24
+ return ontology.getDisplay('sourceType', type, 'badge-gray');
49
25
  }
50
26
 
51
27
  export function termTypeInfo(termType: string | null): { label: string; category: string; definition?: string } {
52
28
  if (!termType) return { label: '', category: '' };
53
- const concept = ontology.getConcept('termType', termType);
29
+ const display = ontology.getDisplay('termType', termType);
54
30
  return {
55
- label: concept?.prefLabel ?? termType,
56
- category: concept?.broader ?? '',
57
- definition: concept?.definition ?? undefined,
31
+ label: display.label,
32
+ category: ontology.getBroader('termType', termType) ?? '',
33
+ definition: display.definition,
58
34
  };
59
35
  }
60
36
 
@@ -68,18 +44,20 @@ export function abbreviationDetails(designation: Designation): string[] {
68
44
  return parts;
69
45
  }
70
46
 
47
+ const GRAMMAR_BOOLEAN_POS = ['noun', 'verb', 'adj', 'adverb', 'preposition', 'participle'] as const;
48
+
71
49
  export function grammarBadges(gi: GrammarInfo): { label: string; definition?: string }[] {
72
50
  const badges: { label: string; definition?: string }[] = [];
73
51
  if (gi.gender) {
74
- const concept = ontology.getConcept('grammarGender', gi.gender);
75
- badges.push({ label: concept?.prefLabel ?? gi.gender, definition: concept?.definition ?? undefined });
52
+ const display = ontology.getDisplay('grammarGender', gi.gender);
53
+ badges.push({ label: display.label, definition: display.definition });
76
54
  }
77
55
  if (gi.number) {
78
- const concept = ontology.getConcept('grammarNumber', gi.number);
79
- badges.push({ label: concept?.prefLabel ?? gi.number, definition: concept?.definition ?? undefined });
56
+ const display = ontology.getDisplay('grammarNumber', gi.number);
57
+ badges.push({ label: display.label, definition: display.definition });
80
58
  }
81
59
  if (gi.partOfSpeech) badges.push({ label: gi.partOfSpeech });
82
- for (const pos of ['noun', 'verb', 'adj', 'adverb', 'preposition', 'participle'] as const) {
60
+ for (const pos of GRAMMAR_BOOLEAN_POS) {
83
61
  if (gi[pos]) badges.push({ label: pos });
84
62
  }
85
63
  return badges;
@@ -1,2 +1,3 @@
1
1
  export { langName, langLabel, DEFAULT_LANG } from './lang';
2
2
  export { deduplicateSearchHits } from './search';
3
+ export { slugify } from './slugify';
@@ -0,0 +1,3 @@
1
+ export function slugify(text: string): string {
2
+ return text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s/]+/g, '-');
3
+ }