@glossarist/concept-browser 0.7.31 → 0.7.33

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,160 @@
1
+ import { computed, ref, watch, type ComputedRef } from 'vue';
2
+ import type { Concept, LocalizedConcept, ConceptSource, Designation } from 'glossarist';
3
+ import type { Manifest } from '../adapters/types';
4
+ import type { RenderOptions } from '../utils/math';
5
+ import { renderMath, cleanContent } from '../utils/math';
6
+ import { getAnnotations } from '../adapters/model-bridge';
7
+ import { getPreferredTerm, entryStatusColor, entryStatusLabel, entryStatusDefinition } from '../utils/concept-helpers';
8
+ import { sortLanguages } from '../utils/lang';
9
+ import { useSiteConfig } from '../config/use-site-config';
10
+ import { useI18n } from '../i18n';
11
+
12
+ export interface LangContent {
13
+ lang: string;
14
+ lc: LocalizedConcept;
15
+ renderedTerm: string;
16
+ definition: string;
17
+ renderedDefinition: string;
18
+ annotations: string[];
19
+ renderedAnnotations: string[];
20
+ notes: string[];
21
+ renderedNotes: string[];
22
+ examples: string[];
23
+ renderedExamples: string[];
24
+ sources: ConceptSource[];
25
+ designations: Designation[];
26
+ renderedDesignations: Map<string, string>;
27
+ entryStatus: string;
28
+ classification: string | null;
29
+ reviewType: string | null;
30
+ release: string | null;
31
+ lineageSourceSimilarity: number | null;
32
+ lcScript: string | null;
33
+ lcSystem: string | null;
34
+ }
35
+
36
+ export function useConceptContent(
37
+ concept: ComputedRef<Concept>,
38
+ manifest: ComputedRef<Manifest>,
39
+ renderOpts: ComputedRef<RenderOptions>,
40
+ ) {
41
+ const { locale } = useI18n();
42
+ const { config: siteConfig } = useSiteConfig();
43
+
44
+ const languages = computed(() => {
45
+ const sorted = sortLanguages(concept.value.languages, manifest.value.languageOrder);
46
+ const current = locale.value;
47
+ const idx = sorted.indexOf(current);
48
+ if (idx > 0) {
49
+ sorted.splice(idx, 1);
50
+ sorted.unshift(current);
51
+ }
52
+ return sorted;
53
+ });
54
+
55
+ const allLangContent = computed(() => {
56
+ const result: LangContent[] = [];
57
+ for (const lang of languages.value) {
58
+ const lc = concept.value.localization(lang);
59
+ if (!lc) continue;
60
+
61
+ const definition = lc.definitions
62
+ .map(d => d.content).filter(Boolean).join('\n\n');
63
+ const annotations = getAnnotations(lc).map(a => a.content).filter(Boolean);
64
+ const notes = lc.notes.map(n => n.content).filter(Boolean);
65
+ const examples = lc.examples.map(e => e.content).filter(Boolean);
66
+ const opts = renderOpts.value;
67
+
68
+ result.push({
69
+ lang,
70
+ lc,
71
+ renderedTerm: renderMath(getPreferredTerm(lc, '')),
72
+ definition,
73
+ renderedDefinition: renderMath(definition, opts),
74
+ annotations,
75
+ renderedAnnotations: annotations.map((a: string) => renderMath(a, opts)),
76
+ notes,
77
+ renderedNotes: notes.map(n => renderMath(n, opts)),
78
+ examples,
79
+ renderedExamples: examples.map(e => renderMath(e, opts)),
80
+ sources: lc.sources,
81
+ designations: lc.terms,
82
+ renderedDesignations: new Map(lc.terms.map(d => [d.designation, renderMath(d.designation)])),
83
+ entryStatus: lc.entryStatus ?? '',
84
+ classification: lc.classification,
85
+ reviewType: lc.reviewType,
86
+ release: lc.release,
87
+ lineageSourceSimilarity: lc.lineageSourceSimilarity,
88
+ lcScript: lc.script,
89
+ lcSystem: lc.system,
90
+ });
91
+ }
92
+ return result;
93
+ });
94
+
95
+ const langContentMap = computed(() => {
96
+ const map = new Map<string, LangContent>();
97
+ for (const lc of allLangContent.value) map.set(lc.lang, lc);
98
+ return map;
99
+ });
100
+
101
+ function hasContent(lc: LangContent): boolean {
102
+ return !!(lc.definition || lc.annotations.length || lc.notes.length || lc.examples.length || lc.sources.length);
103
+ }
104
+
105
+ const collapsedLangs = ref(new Set<string>());
106
+
107
+ function initCollapsed() {
108
+ const mainLangs = siteConfig.value?.defaults?.mainLanguages || [];
109
+ const mainSet = new Set(mainLangs.length ? mainLangs : ['eng']);
110
+ const collapsed = new Set<string>();
111
+ for (const lc of allLangContent.value) {
112
+ if (!hasContent(lc) && !mainSet.has(lc.lang)) {
113
+ collapsed.add(lc.lang);
114
+ }
115
+ }
116
+ collapsedLangs.value = collapsed;
117
+ }
118
+
119
+ watch(languages, () => { initCollapsed(); }, { immediate: true });
120
+
121
+ const allCollapsed = computed(() => collapsedLangs.value.size === allLangContent.value.length);
122
+
123
+ function toggleLang(lang: string) {
124
+ const s = new Set(collapsedLangs.value);
125
+ if (s.has(lang)) s.delete(lang); else s.add(lang);
126
+ collapsedLangs.value = s;
127
+ }
128
+
129
+ function toggleAll() {
130
+ collapsedLangs.value = allCollapsed.value
131
+ ? new Set()
132
+ : new Set(allLangContent.value.map(lc => lc.lang));
133
+ }
134
+
135
+ function plainTruncate(html: string, max: number = 120): string {
136
+ const text = cleanContent(html).replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
137
+ return text.length <= max ? text : text.slice(0, max).trimEnd() + '…';
138
+ }
139
+
140
+ function orderedDesignations(lang: string): Designation[] {
141
+ const desigs = langContentMap.value.get(lang)?.designations ?? [];
142
+ const preferred = desigs.filter(d => d.normativeStatus === 'preferred');
143
+ const admitted = desigs.filter(d => d.normativeStatus === 'admitted' || d.normativeStatus === 'deprecated');
144
+ const rest = desigs.filter(d => d.normativeStatus !== 'preferred' && d.normativeStatus !== 'admitted' && d.normativeStatus !== 'deprecated');
145
+ return [...preferred, ...admitted, ...rest];
146
+ }
147
+
148
+ return {
149
+ languages,
150
+ allLangContent,
151
+ langContentMap,
152
+ hasContent,
153
+ collapsedLangs,
154
+ allCollapsed,
155
+ toggleLang,
156
+ toggleAll,
157
+ plainTruncate,
158
+ orderedDesignations,
159
+ };
160
+ }
@@ -0,0 +1,181 @@
1
+ import { computed, type ComputedRef } from 'vue';
2
+ import type { Concept, RelatedConcept } from 'glossarist';
3
+ import type { Manifest, GraphEdge } from '../adapters/types';
4
+ import { getFactory } from '../adapters/factory';
5
+ import { conceptUri } from '../adapters/model-bridge';
6
+ import { useVocabularyStore } from '../stores/vocabulary';
7
+ import { useDsStyle } from '../utils/dataset-style';
8
+ import { categorizeRelationship, relationshipLabel, INVERSE_RELATIONSHIPS } from '../utils/relationship-categories';
9
+ import { langLabel } from '../utils/lang';
10
+ import { escapeAttr } from '../utils/escape';
11
+ import { useI18n } from '../i18n';
12
+
13
+ export interface EdgeDisplay {
14
+ uri: string;
15
+ conceptId: string;
16
+ designation: string;
17
+ tooltip: string;
18
+ isLocal: boolean;
19
+ badge: { id: string; title: string } | null;
20
+ }
21
+
22
+ export function useConceptEdges(
23
+ concept: ComputedRef<Concept>,
24
+ registerId: ComputedRef<string>,
25
+ manifest: ComputedRef<Manifest>,
26
+ edges: ComputedRef<GraphEdge[]>,
27
+ ) {
28
+ const factory = getFactory();
29
+ const store = useVocabularyStore();
30
+ const { getColor } = useDsStyle();
31
+ const { locale } = useI18n();
32
+
33
+ const conceptUriValue = computed(() =>
34
+ conceptUri(concept.value, registerId.value, manifest.value.uriBase)
35
+ );
36
+
37
+ const outgoingEdges = computed(() =>
38
+ store.graph.getUniqueEdges(conceptUriValue.value, 'outgoing', 'target')
39
+ .filter(e => e.type !== 'domain' && e.type !== 'section')
40
+ );
41
+
42
+ const incomingEdges = computed(() =>
43
+ store.graph.getUniqueEdges(conceptUriValue.value, 'incoming', 'source')
44
+ .filter(e => e.type !== 'domain' && e.type !== 'section')
45
+ );
46
+
47
+ const edgeDisplayCache = computed(() => {
48
+ const cache = new Map<string, EdgeDisplay>();
49
+ for (const e of edges.value) {
50
+ const uri = e.source === conceptUriValue.value ? e.target : e.source;
51
+ if (cache.has(uri)) continue;
52
+ const resolution = factory.resolve(uri, registerId.value);
53
+ const isLocal = resolution.type === 'internal' && resolution.registerId === registerId.value;
54
+ const conceptId = uri.match(/\/concept\/([^/]+)$/)?.[1] ?? uri.split('/').pop() ?? uri;
55
+ const node = store.graph.getNode(uri);
56
+ const designation = node
57
+ ? (node.designations[locale.value] || node.designations.eng || Object.values(node.designations)[0] || '')
58
+ : '';
59
+ const tooltipLines: string[] = [uri];
60
+ if (node) {
61
+ for (const [lang, des] of Object.entries(node.designations)) {
62
+ tooltipLines.push(`${langLabel(lang)}: ${des}`);
63
+ }
64
+ }
65
+ let badge: { id: string; title: string } | null = null;
66
+ if (resolution.type === 'internal' && resolution.registerId !== registerId.value) {
67
+ const m = store.manifests.get(resolution.registerId);
68
+ badge = { id: resolution.registerId, title: m?.shortname || m?.title || resolution.registerId };
69
+ } else if (resolution.type === 'site') {
70
+ badge = { id: '', title: resolution.label };
71
+ } else if (resolution.type === 'url') {
72
+ badge = { id: '', title: resolution.label };
73
+ }
74
+ cache.set(uri, { uri, conceptId, designation, tooltip: tooltipLines.join('\n'), isLocal, badge });
75
+ }
76
+ return cache;
77
+ });
78
+
79
+ function getEdgeDisplay(uri: string): EdgeDisplay {
80
+ return edgeDisplayCache.value.get(uri) ?? { uri, conceptId: uri, designation: '', tooltip: uri, isLocal: false, badge: null };
81
+ }
82
+
83
+ function edgeBadgeColor(type: string, direction: 'out' | 'in'): string {
84
+ if (type === 'supersedes' || type === 'superseded_by') {
85
+ return direction === 'out' ? 'text-orange-700 bg-orange-50' : 'text-red-700 bg-red-50';
86
+ }
87
+ return categorizeRelationship(type).color;
88
+ }
89
+
90
+ function inverseEdgeType(type: string): string {
91
+ return INVERSE_RELATIONSHIPS[type] || type;
92
+ }
93
+
94
+ // Concept-level related concepts (managed concept cross-references)
95
+ const conceptRelated = computed(() => {
96
+ const direct = concept.value.relatedConcepts?.filter(rc => !INVERSE_RELATIONSHIPS[rc.type]) ?? [];
97
+ const derived = incomingEdges.value
98
+ .filter(e => INVERSE_RELATIONSHIPS[e.type])
99
+ .map(e => {
100
+ const parsed = factory.resolve(e.source, registerId.value);
101
+ const sourceUrn = parsed.type === 'internal'
102
+ ? store.manifests.get(parsed.registerId)?.datasetUri
103
+ : null;
104
+ const conceptId = e.source.match(/\/concept\/([^/]+)$/)?.[1];
105
+ return {
106
+ type: INVERSE_RELATIONSHIPS[e.type],
107
+ ref: sourceUrn && conceptId ? { source: sourceUrn, id: conceptId } : null,
108
+ content: '',
109
+ };
110
+ });
111
+ return [...direct, ...derived];
112
+ });
113
+
114
+ function resolveRelatedRef(ref: { source: string | null; id: string | null } | null) {
115
+ return factory.resolveRelatedRef(ref, registerId.value);
116
+ }
117
+
118
+ const resolvedRefs = computed(() => {
119
+ const map = new Map<string, { target: { registerId: string; conceptId: string } | null }>();
120
+ for (const cr of conceptRelated.value) {
121
+ const key = `${cr.ref?.source ?? ''}:${cr.ref?.id ?? ''}`;
122
+ if (map.has(key)) continue;
123
+ map.set(key, { target: resolveRelatedRef(cr.ref) });
124
+ }
125
+ return map;
126
+ });
127
+
128
+ function getResolvedRef(ref: { source: string | null; id: string | null } | null) {
129
+ if (!ref) return { target: null };
130
+ const key = `${ref.source ?? ''}:${ref.id ?? ''}`;
131
+ return resolvedRefs.value.get(key) ?? { target: resolveRelatedRef(ref) };
132
+ }
133
+
134
+ function relatedLabel(dr: { content?: string | null; ref?: { source: string | null; id: string | null } | null }): string {
135
+ if (dr.content) return dr.content;
136
+ const resolved = dr.ref ? getResolvedRef(dr.ref).target : null;
137
+ if (resolved) {
138
+ const m = store.manifests.get(resolved.registerId);
139
+ const dsLabel = m?.shortname || m?.title || resolved.registerId;
140
+ return `${resolved.conceptId} (${dsLabel})`;
141
+ }
142
+ return dr.ref ? `${dr.ref.id || ''} (${dr.ref.source || ''})`.trim() : '';
143
+ }
144
+
145
+ async function navigateEdge(edge: GraphEdge) {
146
+ const uri = edge.source === conceptUriValue.value ? edge.target : edge.source;
147
+ const resolution = factory.resolve(uri);
148
+ const router = (await import('vue-router')).useRouter();
149
+
150
+ if (resolution.type === 'internal') {
151
+ router.push({ name: 'concept', params: { registerId: resolution.registerId, conceptId: resolution.conceptId } });
152
+ } else if (resolution.type === 'site') {
153
+ window.open(`${resolution.baseUrl}/resolve/${encodeURIComponent(uri)}`, '_blank', 'noopener');
154
+ } else if (resolution.type === 'url') {
155
+ window.open(resolution.url, '_blank', 'noopener');
156
+ }
157
+ }
158
+
159
+ async function navigateRelated(ref: { source: string | null; id: string | null }) {
160
+ const target = resolveRelatedRef(ref);
161
+ if (!target) return;
162
+ const router = (await import('vue-router')).useRouter();
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
+ }