@glossarist/concept-browser 0.7.32 → 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.
- package/package.json +1 -1
- package/scripts/build-edges.js +24 -41
- package/scripts/extract-source-refs.js +32 -0
- package/scripts/generate-data.mjs +2 -0
- package/src/__tests__/citation-display.test.ts +143 -0
- package/src/__tests__/concept-helpers.test.ts +129 -0
- package/src/__tests__/designation-relationship.test.ts +31 -0
- package/src/__tests__/extract-source-refs.test.ts +136 -0
- package/src/__tests__/factory-lazy.test.ts +6 -6
- package/src/__tests__/load-source-refs.test.ts +128 -0
- package/src/__tests__/slugify.test.ts +28 -0
- package/src/__tests__/source-refs.test.ts +11 -0
- package/src/adapters/DatasetAdapter.ts +51 -19
- package/src/adapters/ReferenceResolver.ts +5 -0
- package/src/adapters/factory.ts +36 -58
- package/src/adapters/model-bridge.ts +32 -24
- package/src/adapters/ontology-registry.ts +21 -4
- package/src/adapters/types.ts +2 -0
- package/src/components/CitationDisplay.vue +123 -14
- package/src/components/ConceptDetail.vue +25 -197
- package/src/components/DesignationList.vue +7 -11
- package/src/composables/use-concept-content.ts +160 -0
- package/src/composables/use-concept-edges.ts +181 -0
- package/src/data/taxonomies.json +13 -1
- package/src/graph/GraphEngine.ts +15 -0
- package/src/utils/concept-helpers.ts +4 -7
- package/src/utils/designation-registry.ts +15 -37
- package/src/utils/index.ts +1 -0
- package/src/utils/slugify.ts +3 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Citation } from 'glossarist';
|
|
3
|
-
import { computed } from 'vue';
|
|
3
|
+
import { computed, ref } from 'vue';
|
|
4
4
|
import { getFactory } from '../adapters/factory';
|
|
5
5
|
import { useRouter } from 'vue-router';
|
|
6
6
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
@@ -14,16 +14,6 @@ const router = useRouter();
|
|
|
14
14
|
const store = useVocabularyStore();
|
|
15
15
|
const factory = getFactory();
|
|
16
16
|
|
|
17
|
-
function formatRef(c: Citation): string {
|
|
18
|
-
const ref = c.ref;
|
|
19
|
-
if (!ref) return '';
|
|
20
|
-
const parts: string[] = [];
|
|
21
|
-
if (ref.source) parts.push(ref.source);
|
|
22
|
-
if (ref.id) parts.push(ref.id);
|
|
23
|
-
if (ref.version) parts.push(`(${ref.version})`);
|
|
24
|
-
return parts.join(' ');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
17
|
function resolveCitation(): { registerId: string; conceptId: string } | null {
|
|
28
18
|
const ref = props.citation.ref;
|
|
29
19
|
const locality = props.citation.locality;
|
|
@@ -36,6 +26,9 @@ function resolveCitation(): { registerId: string; conceptId: string } | null {
|
|
|
36
26
|
}
|
|
37
27
|
|
|
38
28
|
const resolvedTarget = computed(() => resolveCitation());
|
|
29
|
+
const isCrossDataset = computed(() =>
|
|
30
|
+
resolvedTarget.value != null && resolvedTarget.value.registerId !== props.registerId,
|
|
31
|
+
);
|
|
39
32
|
|
|
40
33
|
async function navigateToCitation() {
|
|
41
34
|
if (!resolvedTarget.value) return;
|
|
@@ -43,18 +36,80 @@ async function navigateToCitation() {
|
|
|
43
36
|
await store.viewConcept(registerId, conceptId);
|
|
44
37
|
router.push({ name: 'concept', params: { registerId, conceptId } });
|
|
45
38
|
}
|
|
39
|
+
|
|
40
|
+
// --- Hover preview ---
|
|
41
|
+
const triggerEl = ref<HTMLElement | null>(null);
|
|
42
|
+
const preview = ref<{ designation: string; definition: string } | null>(null);
|
|
43
|
+
const previewVisible = ref(false);
|
|
44
|
+
let previewTimer: ReturnType<typeof setTimeout> | null = null;
|
|
45
|
+
|
|
46
|
+
const previewStyle = computed(() => {
|
|
47
|
+
if (!triggerEl.value) return {};
|
|
48
|
+
const rect = triggerEl.value.getBoundingClientRect();
|
|
49
|
+
return {
|
|
50
|
+
top: `${rect.bottom + 6}px`,
|
|
51
|
+
left: `${Math.max(8, Math.min(rect.left, window.innerWidth - 336))}px`,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
async function loadPreview() {
|
|
56
|
+
if (!resolvedTarget.value || preview.value) return;
|
|
57
|
+
const { registerId, conceptId } = resolvedTarget.value;
|
|
58
|
+
const adapter = factory.getAdapter(registerId);
|
|
59
|
+
if (!adapter) return;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const entry = adapter.getIndexEntry(conceptId);
|
|
63
|
+
if (entry && !preview.value) {
|
|
64
|
+
preview.value = {
|
|
65
|
+
designation: entry.designations?.eng || entry.eng || conceptId,
|
|
66
|
+
definition: '',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const concept = await adapter.fetchConcept(conceptId);
|
|
71
|
+
const lc = concept.localization('eng');
|
|
72
|
+
const def = lc?.definitions?.[0]?.content || concept.definition('eng');
|
|
73
|
+
const term = lc?.terms?.[0]?.designation || concept.primaryDesignation('eng');
|
|
74
|
+
preview.value = {
|
|
75
|
+
designation: term || entry?.designations?.eng || conceptId,
|
|
76
|
+
definition: typeof def === 'string' ? def.slice(0, 200) : '',
|
|
77
|
+
};
|
|
78
|
+
} catch {
|
|
79
|
+
// Concept not available — preview stays with index data or empty
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function schedulePreview(e: MouseEvent) {
|
|
84
|
+
triggerEl.value = e.currentTarget as HTMLElement;
|
|
85
|
+
previewVisible.value = true;
|
|
86
|
+
previewTimer = setTimeout(loadPreview, 400);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function hidePreview() {
|
|
90
|
+
if (previewTimer) { clearTimeout(previewTimer); previewTimer = null; }
|
|
91
|
+
previewVisible.value = false;
|
|
92
|
+
}
|
|
46
93
|
</script>
|
|
47
94
|
|
|
48
95
|
<template>
|
|
49
|
-
<span class="inline">
|
|
96
|
+
<span class="inline" @mouseleave="hidePreview">
|
|
50
97
|
<template v-if="citation.ref">
|
|
51
|
-
<button
|
|
98
|
+
<button
|
|
99
|
+
v-if="resolvedTarget"
|
|
100
|
+
@click="navigateToCitation"
|
|
101
|
+
@mouseenter="schedulePreview"
|
|
102
|
+
class="concept-link font-medium inline-flex items-center gap-0.5"
|
|
103
|
+
>
|
|
104
|
+
{{ citation.ref.source }}
|
|
105
|
+
<span v-if="isCrossDataset" class="text-[10px] opacity-60 leading-none">↗</span>
|
|
106
|
+
</button>
|
|
52
107
|
<span v-else-if="citation.ref.source" class="font-medium">{{ citation.ref.source }}</span>
|
|
53
108
|
<span v-if="citation.ref.id"> {{ citation.ref.id }}</span>
|
|
54
109
|
<span v-if="citation.ref.version" class="text-ink-400"> ({{ citation.ref.version }})</span>
|
|
55
110
|
</template>
|
|
56
111
|
<template v-if="citation.locality">
|
|
57
|
-
<button v-if="resolvedTarget" @click="navigateToCitation" class="concept-link">
|
|
112
|
+
<button v-if="resolvedTarget" @click="navigateToCitation" @mouseenter="schedulePreview" class="concept-link">
|
|
58
113
|
<span v-if="citation.locality.type" class="text-ink-400">, {{ citation.locality.type }}</span>
|
|
59
114
|
<span v-if="citation.locality.referenceFrom" class="text-ink-400">
|
|
60
115
|
{{ citation.locality.referenceTo ? ` ${citation.locality.referenceFrom}–${citation.locality.referenceTo}` : ` ${citation.locality.referenceFrom}` }}
|
|
@@ -70,5 +125,59 @@ async function navigateToCitation() {
|
|
|
70
125
|
<a v-if="citation.link" :href="citation.link" target="_blank" rel="noopener" class="concept-link ml-1">[link]</a>
|
|
71
126
|
<span v-if="citation.original" class="text-xs text-ink-300 ml-1">(orig: {{ citation.original }})</span>
|
|
72
127
|
<span v-if="resolvedTarget" class="text-[9px] text-ink-300 ml-1">→ {{ resolvedTarget.registerId }}/{{ resolvedTarget.conceptId }}</span>
|
|
128
|
+
|
|
129
|
+
<!-- Hover preview tooltip -->
|
|
130
|
+
<Teleport to="body">
|
|
131
|
+
<div
|
|
132
|
+
v-if="previewVisible && preview"
|
|
133
|
+
class="citation-preview"
|
|
134
|
+
@mouseenter="previewVisible = true"
|
|
135
|
+
@mouseleave="hidePreview"
|
|
136
|
+
:style="previewStyle"
|
|
137
|
+
>
|
|
138
|
+
<div class="citation-preview-title">{{ preview.designation }}</div>
|
|
139
|
+
<div v-if="preview.definition" class="citation-preview-def">{{ preview.definition }}</div>
|
|
140
|
+
<div v-if="resolvedTarget" class="citation-preview-dataset">{{ resolvedTarget.registerId }}</div>
|
|
141
|
+
</div>
|
|
142
|
+
</Teleport>
|
|
73
143
|
</span>
|
|
74
144
|
</template>
|
|
145
|
+
|
|
146
|
+
<style scoped>
|
|
147
|
+
.citation-preview {
|
|
148
|
+
position: fixed;
|
|
149
|
+
z-index: 50;
|
|
150
|
+
max-width: 320px;
|
|
151
|
+
padding: 8px 12px;
|
|
152
|
+
border-radius: 6px;
|
|
153
|
+
font-size: 13px;
|
|
154
|
+
line-height: 1.4;
|
|
155
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
156
|
+
pointer-events: auto;
|
|
157
|
+
background: var(--color-bg, #fff);
|
|
158
|
+
color: var(--color-ink, #1a1a1a);
|
|
159
|
+
border: 1px solid var(--color-border, #e5e5e5);
|
|
160
|
+
}
|
|
161
|
+
:global(.dark) .citation-preview {
|
|
162
|
+
background: var(--color-bg, #1a1a2e);
|
|
163
|
+
color: var(--color-ink, #e5e5e5);
|
|
164
|
+
border-color: var(--color-border, #333);
|
|
165
|
+
}
|
|
166
|
+
.citation-preview-title {
|
|
167
|
+
font-weight: 600;
|
|
168
|
+
margin-bottom: 4px;
|
|
169
|
+
}
|
|
170
|
+
.citation-preview-def {
|
|
171
|
+
color: var(--color-ink-600, #555);
|
|
172
|
+
font-size: 12px;
|
|
173
|
+
}
|
|
174
|
+
:global(.dark) .citation-preview-def {
|
|
175
|
+
color: var(--color-ink-400, #999);
|
|
176
|
+
}
|
|
177
|
+
.citation-preview-dataset {
|
|
178
|
+
margin-top: 4px;
|
|
179
|
+
font-size: 11px;
|
|
180
|
+
color: var(--brand-primary);
|
|
181
|
+
opacity: 0.8;
|
|
182
|
+
}
|
|
183
|
+
</style>
|
|
@@ -14,7 +14,10 @@ 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
|
-
import {
|
|
17
|
+
import { useConceptEdges } from '../composables/use-concept-edges';
|
|
18
|
+
import { useConceptContent, type LangContent } from '../composables/use-concept-content';
|
|
19
|
+
import { relationshipLabel, INVERSE_RELATIONSHIPS } from '../utils/relationship-categories';
|
|
20
|
+
import { slugify } from '../utils/slugify';
|
|
18
21
|
import { useSiteConfig } from '../config/use-site-config';
|
|
19
22
|
import ConceptTimeline from './ConceptTimeline.vue';
|
|
20
23
|
import ConceptRdfView from './ConceptRdfView.vue';
|
|
@@ -38,7 +41,6 @@ const router = useRouter();
|
|
|
38
41
|
const store = useVocabularyStore();
|
|
39
42
|
const { getColor } = useDsStyle();
|
|
40
43
|
const { config: siteConfig, localizedDatasetField } = useSiteConfig();
|
|
41
|
-
const factory = getFactory();
|
|
42
44
|
|
|
43
45
|
const activeTab = ref<'rdf' | 'definition' | 'history'>('definition');
|
|
44
46
|
const activeHistoryLang = ref('eng');
|
|
@@ -53,6 +55,25 @@ const conceptPosition = computed(() => {
|
|
|
53
55
|
return { index: idx + 1, total: adapter.getConceptCount() };
|
|
54
56
|
});
|
|
55
57
|
|
|
58
|
+
const conceptComputed = computed(() => props.concept);
|
|
59
|
+
const registerIdComputed = computed(() => props.registerId);
|
|
60
|
+
const manifestComputed = computed(() => props.manifest);
|
|
61
|
+
const edgesComputed = computed(() => props.edges);
|
|
62
|
+
|
|
63
|
+
const {
|
|
64
|
+
conceptUriValue,
|
|
65
|
+
outgoingEdges,
|
|
66
|
+
incomingEdges,
|
|
67
|
+
getEdgeDisplay,
|
|
68
|
+
edgeBadgeColor,
|
|
69
|
+
inverseEdgeType,
|
|
70
|
+
conceptRelated,
|
|
71
|
+
getResolvedRef,
|
|
72
|
+
relatedLabel,
|
|
73
|
+
navigateEdge,
|
|
74
|
+
navigateRelated,
|
|
75
|
+
} = useConceptEdges(conceptComputed, registerIdComputed, manifestComputed, edgesComputed);
|
|
76
|
+
|
|
56
77
|
const uriCopied = ref(false);
|
|
57
78
|
function copyUri() {
|
|
58
79
|
const uri = conceptUri(props.concept, props.registerId, props.manifest.uriBase);
|
|
@@ -84,79 +105,19 @@ const engConcept = computed((): LocalizedConcept | null => {
|
|
|
84
105
|
const primaryTerm = computed(() => getPreferredTerm(engConcept.value, conceptId.value));
|
|
85
106
|
const renderedPrimaryTerm = computed(() => renderMath(primaryTerm.value));
|
|
86
107
|
|
|
87
|
-
// Managed concept status from Concept.status (7 values from concept-status.ttl)
|
|
88
108
|
const managedStatus = computed(() => props.concept.status);
|
|
89
109
|
|
|
90
|
-
// ConceptReference domains from managed concept level
|
|
91
110
|
const conceptRefDomains = computed(() => props.concept.domains);
|
|
92
111
|
|
|
93
|
-
// Managed concept dates
|
|
94
112
|
const conceptDates = computed(() => props.concept.dates);
|
|
95
113
|
|
|
96
|
-
// Managed concept sources (distinct from localized sources)
|
|
97
114
|
const conceptSources = computed(() => props.concept.sources);
|
|
98
115
|
|
|
99
|
-
// Managed concept tags
|
|
100
116
|
const conceptTags = computed(() => props.concept.tags ?? []);
|
|
101
117
|
|
|
102
|
-
// Managed concept related (concept-level cross-references)
|
|
103
|
-
// Derives inverse relationships from incoming edges (e.g. supersedes → superseded_by).
|
|
104
|
-
const conceptRelated = computed(() => {
|
|
105
|
-
const direct = props.concept.relatedConcepts?.filter(rc => !INVERSE_RELATIONSHIPS[rc.type]) ?? [];
|
|
106
|
-
const derived = incomingEdges.value
|
|
107
|
-
.filter(e => INVERSE_RELATIONSHIPS[e.type])
|
|
108
|
-
.map(e => {
|
|
109
|
-
const parsed = factory.resolve(e.source, props.registerId);
|
|
110
|
-
const sourceUrn = parsed.type === 'internal'
|
|
111
|
-
? store.manifests.get(parsed.registerId)?.datasetUri
|
|
112
|
-
: null;
|
|
113
|
-
const conceptId = e.source.match(/\/concept\/([^/]+)$/)?.[1];
|
|
114
|
-
return {
|
|
115
|
-
type: INVERSE_RELATIONSHIPS[e.type],
|
|
116
|
-
ref: sourceUrn && conceptId ? { source: sourceUrn, id: conceptId } : null,
|
|
117
|
-
content: '',
|
|
118
|
-
};
|
|
119
|
-
});
|
|
120
|
-
return [...direct, ...derived];
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
function resolveRelatedRef(ref: { source: string | null; id: string | null } | null): { registerId: string; conceptId: string } | null {
|
|
124
|
-
if (!ref?.source || !ref?.id) return null;
|
|
125
|
-
const uri = `${ref.source}/${ref.id}`;
|
|
126
|
-
const resolution = factory.resolve(uri, props.registerId);
|
|
127
|
-
if (resolution.type === 'internal') {
|
|
128
|
-
const conceptId = resolution.conceptId.replace(/^\//, '');
|
|
129
|
-
return { registerId: resolution.registerId, conceptId };
|
|
130
|
-
}
|
|
131
|
-
if (ref.source.startsWith('urn:')) {
|
|
132
|
-
const directUri = ref.source + ref.id;
|
|
133
|
-
const directRes = factory.resolve(directUri, props.registerId);
|
|
134
|
-
if (directRes.type === 'internal') {
|
|
135
|
-
return { registerId: directRes.registerId, conceptId: directRes.conceptId.replace(/^\//, '') };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function navigateRelated(ref: { source: string | null; id: string | null }) {
|
|
142
|
-
const target = resolveRelatedRef(ref);
|
|
143
|
-
if (!target) return;
|
|
144
|
-
router.push({ name: 'concept', params: { registerId: target.registerId, conceptId: target.conceptId } });
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function relatedLabel(dr: { content?: string | null; ref?: { source: string | null; id: string | null } | null }): string {
|
|
148
|
-
if (dr.content) return dr.content;
|
|
149
|
-
const resolved = dr.ref ? getResolvedRef(dr.ref).target : null;
|
|
150
|
-
if (resolved) {
|
|
151
|
-
const m = store.manifests.get(resolved.registerId);
|
|
152
|
-
const dsLabel = m?.shortname || m?.title || resolved.registerId;
|
|
153
|
-
return `${resolved.conceptId} (${dsLabel})`;
|
|
154
|
-
}
|
|
155
|
-
return dr.ref ? `${dr.ref.id || ''} (${dr.ref.source || ''})`.trim() : '';
|
|
156
|
-
}
|
|
157
|
-
|
|
158
118
|
// Cross-reference resolver: generates clickable links for inline refs
|
|
159
119
|
|
|
120
|
+
const factory = getFactory();
|
|
160
121
|
const { ensureBibLoaded, bibResolver, figResolver } = useRenderOptions(() => props.registerId);
|
|
161
122
|
|
|
162
123
|
const renderOpts: RenderOptions = {
|
|
@@ -194,30 +155,7 @@ function handleContentClick(e: MouseEvent) {
|
|
|
194
155
|
}
|
|
195
156
|
}
|
|
196
157
|
|
|
197
|
-
//
|
|
198
|
-
interface LangContent {
|
|
199
|
-
lang: string;
|
|
200
|
-
lc: LocalizedConcept;
|
|
201
|
-
renderedTerm: string;
|
|
202
|
-
definition: string;
|
|
203
|
-
renderedDefinition: string;
|
|
204
|
-
annotations: string[];
|
|
205
|
-
renderedAnnotations: string[];
|
|
206
|
-
notes: string[];
|
|
207
|
-
renderedNotes: string[];
|
|
208
|
-
examples: string[];
|
|
209
|
-
renderedExamples: string[];
|
|
210
|
-
sources: ConceptSource[];
|
|
211
|
-
designations: Designation[];
|
|
212
|
-
renderedDesignations: Map<string, string>;
|
|
213
|
-
entryStatus: string;
|
|
214
|
-
classification: string | null;
|
|
215
|
-
reviewType: string | null;
|
|
216
|
-
release: string | null;
|
|
217
|
-
lineageSourceSimilarity: number | null;
|
|
218
|
-
lcScript: string | null;
|
|
219
|
-
lcSystem: string | null;
|
|
220
|
-
}
|
|
158
|
+
// LangContent type is imported from the composable
|
|
221
159
|
|
|
222
160
|
const allLangContent = computed(() => {
|
|
223
161
|
const result: LangContent[] = [];
|
|
@@ -310,112 +248,6 @@ function scrollToLang(lang: string) {
|
|
|
310
248
|
});
|
|
311
249
|
}
|
|
312
250
|
|
|
313
|
-
const conceptUriValue = computed(() =>
|
|
314
|
-
conceptUri(props.concept, props.registerId, props.manifest.uriBase)
|
|
315
|
-
);
|
|
316
|
-
|
|
317
|
-
const outgoingEdges = computed(() => dedupeEdges(props.edges.filter(e => e.source === conceptUriValue.value && e.type !== 'domain' && e.type !== 'section'), 'target'));
|
|
318
|
-
const incomingEdges = computed(() => dedupeEdges(props.edges.filter(e => e.target === conceptUriValue.value && e.type !== 'domain' && e.type !== 'section'), 'source'));
|
|
319
|
-
|
|
320
|
-
function dedupeEdges(edges: GraphEdge[], direction: 'source' | 'target'): GraphEdge[] {
|
|
321
|
-
const seen = new Set<string>();
|
|
322
|
-
return edges.filter(e => {
|
|
323
|
-
const key = `${e[direction]}\0${e.type}`;
|
|
324
|
-
if (seen.has(key)) return false;
|
|
325
|
-
seen.add(key);
|
|
326
|
-
return true;
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function inverseEdgeType(type: string): string {
|
|
331
|
-
return INVERSE_RELATIONSHIPS[type] || type;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function edgeBadgeColor(type: string, direction: 'out' | 'in'): string {
|
|
335
|
-
if (type === 'supersedes' || type === 'superseded_by') {
|
|
336
|
-
return direction === 'out' ? 'text-orange-700 bg-orange-50' : 'text-red-700 bg-red-50';
|
|
337
|
-
}
|
|
338
|
-
return categorizeRelationship(type).color;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
interface EdgeDisplay {
|
|
342
|
-
uri: string;
|
|
343
|
-
conceptId: string;
|
|
344
|
-
designation: string;
|
|
345
|
-
tooltip: string;
|
|
346
|
-
isLocal: boolean;
|
|
347
|
-
badge: { id: string; title: string } | null;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const edgeDisplayCache = computed(() => {
|
|
351
|
-
const cache = new Map<string, EdgeDisplay>();
|
|
352
|
-
for (const e of props.edges) {
|
|
353
|
-
const uri = e.source === conceptUriValue.value ? e.target : e.source;
|
|
354
|
-
if (cache.has(uri)) continue;
|
|
355
|
-
const resolution = factory.resolve(uri, props.registerId);
|
|
356
|
-
const isLocal = resolution.type === 'internal' && resolution.registerId === props.registerId;
|
|
357
|
-
const conceptId = uri.match(/\/concept\/([^/]+)$/)?.[1] ?? uri.split('/').pop() ?? uri;
|
|
358
|
-
const node = store.graph.getNode(uri);
|
|
359
|
-
const designation = node
|
|
360
|
-
? (node.designations[locale.value] || node.designations.eng || Object.values(node.designations)[0] || '')
|
|
361
|
-
: '';
|
|
362
|
-
const tooltipLines: string[] = [uri];
|
|
363
|
-
if (node) {
|
|
364
|
-
for (const [lang, des] of Object.entries(node.designations)) {
|
|
365
|
-
tooltipLines.push(`${langLabel(lang)}: ${des}`);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
let badge: { id: string; title: string } | null = null;
|
|
369
|
-
if (resolution.type === 'internal' && resolution.registerId !== props.registerId) {
|
|
370
|
-
const m = store.manifests.get(resolution.registerId);
|
|
371
|
-
badge = { id: resolution.registerId, title: m?.shortname || m?.title || resolution.registerId };
|
|
372
|
-
} else if (resolution.type === 'site') {
|
|
373
|
-
badge = { id: '', title: resolution.label };
|
|
374
|
-
} else if (resolution.type === 'url') {
|
|
375
|
-
badge = { id: '', title: resolution.label };
|
|
376
|
-
}
|
|
377
|
-
cache.set(uri, { uri, conceptId, designation, tooltip: tooltipLines.join('\n'), isLocal, badge });
|
|
378
|
-
}
|
|
379
|
-
return cache;
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
function getEdgeDisplay(uri: string): EdgeDisplay {
|
|
383
|
-
return edgeDisplayCache.value.get(uri) ?? { uri, conceptId: uri, designation: "", tooltip: uri, isLocal: false, badge: null };
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
interface ResolvedRef {
|
|
387
|
-
target: { registerId: string; conceptId: string } | null;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const resolvedRefs = computed(() => {
|
|
391
|
-
const map = new Map<string, ResolvedRef>();
|
|
392
|
-
for (const cr of conceptRelated.value) {
|
|
393
|
-
const key = `${cr.ref?.source ?? ''}:${cr.ref?.id ?? ''}`;
|
|
394
|
-
if (map.has(key)) continue;
|
|
395
|
-
map.set(key, { target: resolveRelatedRef(cr.ref) });
|
|
396
|
-
}
|
|
397
|
-
return map;
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
function getResolvedRef(ref: { source: string | null; id: string | null } | null): ResolvedRef {
|
|
401
|
-
if (!ref) return { target: null };
|
|
402
|
-
const key = `${ref.source ?? ''}:${ref.id ?? ''}`;
|
|
403
|
-
return resolvedRefs.value.get(key) ?? { target: resolveRelatedRef(ref) };
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
async function navigateEdge(edge: GraphEdge) {
|
|
407
|
-
const uri = edge.source === conceptUriValue.value ? edge.target : edge.source;
|
|
408
|
-
const resolution = factory.resolve(uri);
|
|
409
|
-
|
|
410
|
-
if (resolution.type === 'internal') {
|
|
411
|
-
router.push({ name: 'concept', params: { registerId: resolution.registerId, conceptId: resolution.conceptId } });
|
|
412
|
-
} else if (resolution.type === 'site') {
|
|
413
|
-
window.open(`${resolution.baseUrl}/resolve/${encodeURIComponent(uri)}`, '_blank', 'noopener');
|
|
414
|
-
} else if (resolution.type === 'url') {
|
|
415
|
-
window.open(resolution.url, '_blank', 'noopener');
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
251
|
function navigateDomain(domain: { slug: string; conceptId?: string }) {
|
|
420
252
|
const sectionId = domain.conceptId || domain.slug;
|
|
421
253
|
router.push({ name: 'dataset', params: { registerId: props.manifest.id }, query: { section: sectionId } });
|
|
@@ -455,10 +287,6 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
455
287
|
return text.length <= max ? text : text.slice(0, max).trimEnd() + '…';
|
|
456
288
|
}
|
|
457
289
|
|
|
458
|
-
function slugify(text: string): string {
|
|
459
|
-
return text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/[\s/]+/g, '-');
|
|
460
|
-
}
|
|
461
|
-
|
|
462
290
|
// Domain rendering: merge ConceptReference domains and per-localization domain strings
|
|
463
291
|
const conceptDomains = computed(() => {
|
|
464
292
|
const domainMap = new Map<string, { slug: string; label: string; langs: string[]; conceptId?: string }>();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Designation, Expression, ConceptSource
|
|
2
|
+
import type { Designation, Expression, ConceptSource } from 'glossarist';
|
|
3
3
|
import { designationTypeInfo, normativeStatusInfo, abbreviationDetails, termTypeInfo, grammarBadges, pronunciationLabel, pronunciationTooltip, sourceTypeInfo } from '../utils/designation-registry';
|
|
4
4
|
import { relationshipLabel } from '../utils/relationship-categories';
|
|
5
5
|
import { langName } from '../utils/lang';
|
|
@@ -17,11 +17,7 @@ const emit = defineEmits<{
|
|
|
17
17
|
(e: 'navigate-related', ref: { source: string | null; id: string | null }): void;
|
|
18
18
|
}>();
|
|
19
19
|
|
|
20
|
-
function
|
|
21
|
-
return dr && typeof dr === 'object' && 'ref' in dr ? dr as RelatedConcept : null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function resolvedLabel(dr: { content: string | null; ref: { source: string | null; id: string | null } | null }): string {
|
|
20
|
+
function resolvedLabel(dr: { content: string | null; ref?: { source: string | null; id: string | null } | null }): string {
|
|
25
21
|
if (dr.content) return dr.content;
|
|
26
22
|
if (dr.ref?.source && dr.ref?.id) return `${dr.ref.source}/${dr.ref.id}`;
|
|
27
23
|
return '(ref)';
|
|
@@ -69,12 +65,12 @@ function resolvedLabel(dr: { content: string | null; ref: { source: string | nul
|
|
|
69
65
|
</div>
|
|
70
66
|
<div v-if="d.related?.length" class="mt-0.5 space-y-0.5">
|
|
71
67
|
<div v-for="(dr, dri) in d.related" :key="'dr'+dri" class="text-xs text-ink-400 flex items-center gap-1.5">
|
|
72
|
-
<span class="badge text-[9px] bg-gray-50 text-gray-600">{{ relationshipLabel(dr.type
|
|
73
|
-
<template v-if="getDesignationTarget(dr
|
|
74
|
-
<span class="italic">{{ getDesignationTarget(dr
|
|
68
|
+
<span class="badge text-[9px] bg-gray-50 text-gray-600">{{ relationshipLabel(dr.type ?? '') }}</span>
|
|
69
|
+
<template v-if="getDesignationTarget(dr)">
|
|
70
|
+
<span class="italic">{{ getDesignationTarget(dr) }}</span>
|
|
75
71
|
</template>
|
|
76
|
-
<button v-else-if="
|
|
77
|
-
<span v-else>{{ resolvedLabel(dr
|
|
72
|
+
<button v-else-if="'ref' in dr && dr.ref" @click="emit('navigate-related', dr.ref)" class="concept-link">{{ resolvedLabel(dr) }}</button>
|
|
73
|
+
<span v-else>{{ resolvedLabel(dr) }}</span>
|
|
78
74
|
</div>
|
|
79
75
|
</div>
|
|
80
76
|
</div>
|
|
@@ -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
|
+
}
|