@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.
- package/package.json +2 -2
- package/scripts/build-edges.js +16 -8
- package/scripts/generate-data.mjs +284 -86
- package/src/__tests__/citation-display.test.ts +165 -3
- package/src/__tests__/cite-ref.test.ts +112 -0
- package/src/__tests__/concept-detail-interaction.test.ts +1 -5
- package/src/__tests__/{math.test.ts → content-renderer.test.ts} +113 -29
- package/src/__tests__/escape.test.ts +76 -0
- package/src/__tests__/graph-data-source.test.ts +155 -0
- package/src/__tests__/model-bridge-bridges.test.ts +150 -0
- package/src/__tests__/model-bridge-citation.test.ts +163 -0
- package/src/__tests__/reference-resolver-cite.test.ts +122 -0
- package/src/__tests__/reference-resolver.test.ts +12 -7
- package/src/__tests__/resolve-view.test.ts +1 -1
- package/src/__tests__/sidebar-nav-highlighting.test.ts +178 -0
- package/src/__tests__/source-refs.test.ts +9 -6
- package/src/__tests__/test-helpers.ts +20 -0
- package/src/__tests__/uri-router.test.ts +39 -12
- package/src/adapters/DatasetAdapter.ts +35 -143
- package/src/adapters/GraphDataSource.ts +178 -0
- package/src/adapters/ReferenceResolver.ts +101 -47
- package/src/adapters/UriRouter.ts +82 -10
- package/src/adapters/factory.ts +35 -28
- package/src/adapters/model-bridge.ts +121 -71
- package/src/adapters/types.ts +3 -0
- package/src/components/AppSidebar.vue +7 -4
- package/src/components/CitationDisplay.vue +86 -30
- package/src/components/ConceptDetail.vue +24 -126
- package/src/components/LanguageDetail.vue +6 -6
- package/src/composables/use-concept-content.ts +8 -8
- package/src/composables/use-ontology-nav.ts +129 -130
- package/src/composables/use-render-options.ts +1 -1
- package/src/graph/GraphEngine.ts +65 -0
- package/src/stores/vocabulary.ts +12 -73
- package/src/utils/content-renderer.ts +312 -0
- package/src/utils/markdown-lite.ts +2 -2
- package/src/utils/math.ts +0 -189
|
@@ -73,18 +73,18 @@ interface JsonLdRef {
|
|
|
73
73
|
'gl:id'?: string;
|
|
74
74
|
'gl:version'?: string;
|
|
75
75
|
'gl:text'?: string;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
source?: string;
|
|
77
|
+
id?: string;
|
|
78
|
+
version?: string;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
interface JsonLdLocality {
|
|
82
82
|
'gl:localityType'?: string;
|
|
83
83
|
'gl:referenceFrom'?: string;
|
|
84
84
|
'gl:referenceTo'?: string;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
type?: string;
|
|
86
|
+
reference_from?: string;
|
|
87
|
+
reference_to?: string;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
interface JsonLdOrigin {
|
|
@@ -97,6 +97,7 @@ interface JsonLdOrigin {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
interface JsonLdSource {
|
|
100
|
+
'gl:id'?: string;
|
|
100
101
|
'gl:sourceType'?: string;
|
|
101
102
|
'gl:sourceStatus'?: string;
|
|
102
103
|
'gl:modification'?: string;
|
|
@@ -109,6 +110,8 @@ interface JsonLdRelated {
|
|
|
109
110
|
'@id'?: string;
|
|
110
111
|
'gl:term'?: string;
|
|
111
112
|
'gl:target'?: string;
|
|
113
|
+
'gl:sourceId'?: string;
|
|
114
|
+
'gl:citation'?: JsonLdOrigin;
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
interface JsonLdDesignation {
|
|
@@ -192,10 +195,24 @@ export function getRefText(ref: ConceptRef): string | null {
|
|
|
192
195
|
return refTexts.get(ref) ?? null;
|
|
193
196
|
}
|
|
194
197
|
|
|
198
|
+
// RelatedConcept.sourceId: links a citation reference back to its source entry
|
|
199
|
+
const relatedSourceIds = new WeakMap<object, string>();
|
|
200
|
+
|
|
201
|
+
export function getRelatedSourceId(rc: object): string | null {
|
|
202
|
+
return relatedSourceIds.get(rc) ?? null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// RelatedConcept.citation: embedded citation data for cite-ref references
|
|
206
|
+
const relatedCitations = new WeakMap<object, Record<string, unknown>>();
|
|
207
|
+
|
|
208
|
+
export function getRelatedCitation(rc: object): Record<string, unknown> | null {
|
|
209
|
+
return relatedCitations.get(rc) ?? null;
|
|
210
|
+
}
|
|
211
|
+
|
|
195
212
|
// Relationship types whose target is a designation string, not a concept ref.
|
|
196
213
|
const DESIGNATION_REL_TYPES = new Set(['abbreviated_form_for', 'short_form_for']);
|
|
197
214
|
|
|
198
|
-
function
|
|
215
|
+
function attachBridges(concept: Concept, localizations: Record<string, unknown>): void {
|
|
199
216
|
for (const lang of concept.languages) {
|
|
200
217
|
const lc = concept.localization(lang);
|
|
201
218
|
const raw = localizations[lang];
|
|
@@ -210,7 +227,7 @@ function attachAnnotations(concept: Concept, localizations: Record<string, unkno
|
|
|
210
227
|
));
|
|
211
228
|
}
|
|
212
229
|
|
|
213
|
-
// Designation-level relationship targets
|
|
230
|
+
// Designation-level relationship targets, ref text, sourceId, citation
|
|
214
231
|
const rawTerms = rawObj.terms;
|
|
215
232
|
if (Array.isArray(rawTerms)) {
|
|
216
233
|
for (const rawTerm of rawTerms) {
|
|
@@ -222,40 +239,56 @@ function attachAnnotations(concept: Concept, localizations: Record<string, unkno
|
|
|
222
239
|
if (!designation) continue;
|
|
223
240
|
const rawRelated = rawT.related;
|
|
224
241
|
if (!Array.isArray(rawRelated)) continue;
|
|
225
|
-
|
|
226
|
-
if (!rawRel || typeof rawRel !== 'object') continue;
|
|
227
|
-
const rel = rawRel as Record<string, unknown>;
|
|
228
|
-
const relType = rel.type as string | undefined;
|
|
229
|
-
const rc = designation.related.find(r => r.type === relType);
|
|
230
|
-
if (!rc) continue;
|
|
231
|
-
if (rel.target && typeof rel.target === 'string') {
|
|
232
|
-
designationTargets.set(rc as object, rel.target);
|
|
233
|
-
}
|
|
234
|
-
if ('ref' in rc && rc.ref) {
|
|
235
|
-
const rawRef = rel.ref as Record<string, unknown> | undefined;
|
|
236
|
-
if (rawRef?.text && typeof rawRef.text === 'string') {
|
|
237
|
-
refTexts.set(rc.ref, rawRef.text);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
242
|
+
attachRelatedBridges(designation.related, rawRelated);
|
|
241
243
|
}
|
|
242
244
|
}
|
|
243
245
|
|
|
244
|
-
// Localization-level
|
|
246
|
+
// Localization-level related concepts
|
|
245
247
|
const rawRelated = rawObj.related;
|
|
246
248
|
if (Array.isArray(rawRelated)) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
249
|
+
attachRelatedBridges(lc.related, rawRelated);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Attach bridged fields (ref text, sourceId, citation) to RelatedConcept instances
|
|
256
|
+
* from the raw deserialized data. Called after Concept.fromJSON creates the model
|
|
257
|
+
* instances, since RelatedConcept.fromJSON only reads type/content/ref.
|
|
258
|
+
*/
|
|
259
|
+
function attachRelatedBridges(
|
|
260
|
+
modelRelated: Array<{ type?: string | null; content?: string | null; ref?: any; related?: any[] }>,
|
|
261
|
+
rawRelated: unknown[],
|
|
262
|
+
): void {
|
|
263
|
+
for (const rawRel of rawRelated) {
|
|
264
|
+
if (!rawRel || typeof rawRel !== 'object') continue;
|
|
265
|
+
const rel = rawRel as Record<string, unknown>;
|
|
266
|
+
const relType = rel.type as string | undefined;
|
|
267
|
+
const rc = relType ? modelRelated.find(r => r.type === relType) : undefined;
|
|
268
|
+
if (!rc) continue;
|
|
269
|
+
|
|
270
|
+
// Ref text
|
|
271
|
+
if (rc.ref) {
|
|
272
|
+
const rawRef = rel.ref as Record<string, unknown> | undefined;
|
|
273
|
+
if (rawRef?.text && typeof rawRef.text === 'string') {
|
|
274
|
+
refTexts.set(rc.ref, rawRef.text);
|
|
257
275
|
}
|
|
258
276
|
}
|
|
277
|
+
|
|
278
|
+
// Designation target
|
|
279
|
+
if (rel.target && typeof rel.target === 'string') {
|
|
280
|
+
designationTargets.set(rc, rel.target);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Source ID — links citation reference back to the ConceptSource entry
|
|
284
|
+
if (rel.sourceId && typeof rel.sourceId === 'string') {
|
|
285
|
+
relatedSourceIds.set(rc, rel.sourceId);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Citation — embedded origin data for cite-ref references
|
|
289
|
+
if (rel.citation && typeof rel.citation === 'object') {
|
|
290
|
+
relatedCitations.set(rc, rel.citation as Record<string, unknown>);
|
|
291
|
+
}
|
|
259
292
|
}
|
|
260
293
|
}
|
|
261
294
|
|
|
@@ -335,8 +368,37 @@ function mapDesignationFromJsonLd(d: JsonLdDesignation): Record<string, unknown>
|
|
|
335
368
|
return result;
|
|
336
369
|
}
|
|
337
370
|
|
|
371
|
+
function mapRefFromJsonLd(rawRef: JsonLdRef | string | undefined): Record<string, unknown> | null {
|
|
372
|
+
if (!rawRef) return null;
|
|
373
|
+
if (typeof rawRef === 'string') return { source: rawRef };
|
|
374
|
+
const refObj: Record<string, unknown> = {};
|
|
375
|
+
// gl:-prefixed keys take precedence over unprefixed keys
|
|
376
|
+
refObj.source = rawRef['gl:source'] ?? rawRef.source;
|
|
377
|
+
refObj.id = rawRef['gl:id'] ?? rawRef.id;
|
|
378
|
+
refObj.version = rawRef['gl:version'] ?? rawRef.version;
|
|
379
|
+
if (rawRef['gl:text']) refObj.text = rawRef['gl:text'];
|
|
380
|
+
return (refObj.source ?? refObj.id ?? refObj.version ?? refObj.text) != null
|
|
381
|
+
? refObj : null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Map JSON-LD locality to glossarist's snake_case format.
|
|
386
|
+
* Always uses snake_case (reference_from/reference_to) for consistency
|
|
387
|
+
* with the glossarist model.
|
|
388
|
+
*/
|
|
389
|
+
function mapLocalityFromJsonLd(rawLoc: JsonLdLocality | undefined): Record<string, unknown> | null {
|
|
390
|
+
if (!rawLoc) return null;
|
|
391
|
+
const locObj: Record<string, unknown> = {};
|
|
392
|
+
locObj.type = rawLoc['gl:localityType'] ?? rawLoc.type;
|
|
393
|
+
locObj.reference_from = rawLoc['gl:referenceFrom'] ?? rawLoc.reference_from;
|
|
394
|
+
locObj.reference_to = rawLoc['gl:referenceTo'] ?? rawLoc.reference_to;
|
|
395
|
+
return (locObj.type ?? locObj.reference_from ?? locObj.reference_to) != null
|
|
396
|
+
? locObj : null;
|
|
397
|
+
}
|
|
398
|
+
|
|
338
399
|
function mapSourceFromJsonLd(s: JsonLdSource): Record<string, unknown> {
|
|
339
400
|
const result: Record<string, unknown> = {};
|
|
401
|
+
if (s['gl:id']) result.id = s['gl:id'];
|
|
340
402
|
if (s['gl:sourceType']) result.type = s['gl:sourceType'];
|
|
341
403
|
if (s['gl:sourceStatus']) result.status = s['gl:sourceStatus'];
|
|
342
404
|
if (s['gl:modification']) result.modification = s['gl:modification'];
|
|
@@ -344,32 +406,10 @@ function mapSourceFromJsonLd(s: JsonLdSource): Record<string, unknown> {
|
|
|
344
406
|
if (s['gl:origin']) {
|
|
345
407
|
const origin: Record<string, unknown> = {};
|
|
346
408
|
const o = s['gl:origin'];
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
} else {
|
|
352
|
-
const refObj: Record<string, unknown> = {};
|
|
353
|
-
if (rawRef['gl:source']) refObj.source = rawRef['gl:source'];
|
|
354
|
-
if (rawRef['gl:id']) refObj.id = rawRef['gl:id'];
|
|
355
|
-
if (rawRef['gl:version']) refObj.version = rawRef['gl:version'];
|
|
356
|
-
if (rawRef['source']) refObj.source = rawRef['source'];
|
|
357
|
-
if (rawRef['id']) refObj.id = rawRef['id'];
|
|
358
|
-
if (rawRef['version']) refObj.version = rawRef['version'];
|
|
359
|
-
if (Object.keys(refObj).length > 0) origin.ref = refObj;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (o['gl:locality']) {
|
|
363
|
-
const loc: Record<string, unknown> = {};
|
|
364
|
-
const rawLoc = o['gl:locality'];
|
|
365
|
-
if (rawLoc['gl:localityType']) loc.type = rawLoc['gl:localityType'];
|
|
366
|
-
if (rawLoc['gl:referenceFrom']) loc.reference_from = rawLoc['gl:referenceFrom'];
|
|
367
|
-
if (rawLoc['gl:referenceTo']) loc.reference_to = rawLoc['gl:referenceTo'];
|
|
368
|
-
if (rawLoc['type']) loc.type = rawLoc['type'];
|
|
369
|
-
if (rawLoc['reference_from']) loc.reference_from = rawLoc['reference_from'];
|
|
370
|
-
if (rawLoc['reference_to']) loc.reference_to = rawLoc['reference_to'];
|
|
371
|
-
origin.locality = loc;
|
|
372
|
-
}
|
|
409
|
+
const ref = mapRefFromJsonLd(o['gl:ref']);
|
|
410
|
+
if (ref) origin.ref = ref;
|
|
411
|
+
const loc = mapLocalityFromJsonLd(o['gl:locality']);
|
|
412
|
+
if (loc) origin.locality = loc;
|
|
373
413
|
if (o['gl:link']) origin.link = o['gl:link'];
|
|
374
414
|
if (o['gl:id']) origin.id = o['gl:id'];
|
|
375
415
|
if (o['gl:version']) origin.version = o['gl:version'];
|
|
@@ -388,14 +428,8 @@ function mapRelatedFromJsonLd(r: JsonLdRelated): Record<string, unknown> {
|
|
|
388
428
|
}
|
|
389
429
|
|
|
390
430
|
if (r['gl:ref']) {
|
|
391
|
-
const ref = r['gl:ref'];
|
|
392
|
-
|
|
393
|
-
if (ref['gl:source']) refObj.source = ref['gl:source'];
|
|
394
|
-
if (ref['gl:id']) refObj.id = ref['gl:id'];
|
|
395
|
-
if (ref['source']) refObj.source = ref['source'];
|
|
396
|
-
if (ref['id']) refObj.id = ref['id'];
|
|
397
|
-
if (ref['gl:text']) refObj.text = ref['gl:text'];
|
|
398
|
-
if (Object.keys(refObj).length > 0) result.ref = refObj;
|
|
431
|
+
const ref = mapRefFromJsonLd(r['gl:ref']);
|
|
432
|
+
if (ref) result.ref = ref;
|
|
399
433
|
}
|
|
400
434
|
|
|
401
435
|
if (!result.ref && r['@id']) {
|
|
@@ -406,6 +440,22 @@ function mapRelatedFromJsonLd(r: JsonLdRelated): Record<string, unknown> {
|
|
|
406
440
|
: { source: uri, id: null };
|
|
407
441
|
}
|
|
408
442
|
if (r['gl:term']) result.content = r['gl:term'];
|
|
443
|
+
|
|
444
|
+
// Bridged fields — stored in raw dict, extracted by attachBridges()
|
|
445
|
+
if (r['gl:sourceId']) result.sourceId = r['gl:sourceId'];
|
|
446
|
+
if (r['gl:citation']) {
|
|
447
|
+
const c = r['gl:citation'];
|
|
448
|
+
const citation: Record<string, unknown> = {};
|
|
449
|
+
if (c['gl:ref']) {
|
|
450
|
+
const cr = mapRefFromJsonLd(c['gl:ref']);
|
|
451
|
+
if (cr) citation.ref = cr;
|
|
452
|
+
}
|
|
453
|
+
const loc = mapLocalityFromJsonLd(c['gl:locality']);
|
|
454
|
+
if (loc) citation.locality = loc;
|
|
455
|
+
if (c['gl:link']) citation.link = c['gl:link'];
|
|
456
|
+
if (Object.keys(citation).length > 0) result.citation = citation;
|
|
457
|
+
}
|
|
458
|
+
|
|
409
459
|
return result;
|
|
410
460
|
}
|
|
411
461
|
|
|
@@ -493,7 +543,7 @@ function conceptFromJsonLd(doc: JsonLdConcept): Concept {
|
|
|
493
543
|
status: null,
|
|
494
544
|
});
|
|
495
545
|
|
|
496
|
-
|
|
546
|
+
attachBridges(concept, localizations);
|
|
497
547
|
return concept;
|
|
498
548
|
}
|
|
499
549
|
|
|
@@ -505,7 +555,7 @@ export function conceptFromJson(doc: Record<string, unknown>): Concept {
|
|
|
505
555
|
}
|
|
506
556
|
const concept = Concept.fromJSON(doc);
|
|
507
557
|
const locs = (doc as Record<string, unknown>).localizations as Record<string, unknown> | undefined;
|
|
508
|
-
if (locs)
|
|
558
|
+
if (locs) attachBridges(concept, locs);
|
|
509
559
|
return concept;
|
|
510
560
|
}
|
|
511
561
|
|
package/src/adapters/types.ts
CHANGED
|
@@ -32,6 +32,9 @@ export type {
|
|
|
32
32
|
export { RELATIONSHIP_TYPES, DATE_TYPES } from 'glossarist';
|
|
33
33
|
export { GRAMMAR_GENDERS, GRAMMAR_NUMBERS, GRAMMAR_PARTS_OF_SPEECH } from 'glossarist/models';
|
|
34
34
|
|
|
35
|
+
// Re-export citation classification from ReferenceResolver (single definition site)
|
|
36
|
+
export type { CitationClassification, CiteResolution } from './ReferenceResolver';
|
|
37
|
+
|
|
35
38
|
// ── Dataset metadata ──────────────────────────────────────────────────────
|
|
36
39
|
|
|
37
40
|
export interface ManifestSection {
|
|
@@ -114,8 +114,8 @@ const showDatasetNav = computed(() => !!currentManifest.value || !!siteConfig.va
|
|
|
114
114
|
const provenance = computed(() => {
|
|
115
115
|
const manifest = currentManifest.value;
|
|
116
116
|
return {
|
|
117
|
-
owner: manifest?.owner ||
|
|
118
|
-
ownerUrl:
|
|
117
|
+
owner: manifest?.owner || siteConfig.value?.branding?.ownerName,
|
|
118
|
+
ownerUrl: siteConfig.value?.branding?.ownerUrl,
|
|
119
119
|
ref: manifest?.ref,
|
|
120
120
|
status: manifest?.status,
|
|
121
121
|
lastUpdated: manifest?.lastUpdated,
|
|
@@ -149,14 +149,17 @@ function isActive(page: { route: string; datasetScoped?: boolean }): boolean {
|
|
|
149
149
|
const target = pageRoute(page);
|
|
150
150
|
if (route.path === target) return true;
|
|
151
151
|
if (page.datasetScoped) return route.name === page.route;
|
|
152
|
+
// Non-dataset-scoped page: only match if we're NOT inside a dataset route
|
|
153
|
+
const inDataset = 'registerId' in route.params;
|
|
154
|
+
if (inDataset) return false;
|
|
152
155
|
return route.name === page.route || route.name === `${page.route}-global`;
|
|
153
156
|
}
|
|
154
157
|
|
|
155
|
-
function navTitle(page: { route: string }): string {
|
|
158
|
+
function navTitle(page: { route: string; title?: string }): string {
|
|
156
159
|
const route = page.route || 'home';
|
|
157
160
|
const key = `nav.${route}`;
|
|
158
161
|
const translated = t(key);
|
|
159
|
-
return translated === key ? (page
|
|
162
|
+
return translated === key ? (page.title ?? route) : translated;
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
const expandedSectionNodes = ref<Set<string>>(new Set());
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Citation } from 'glossarist';
|
|
3
|
+
import type { CitationClassification, CiteResolution } from '../adapters/types';
|
|
3
4
|
import { computed, ref } from 'vue';
|
|
4
5
|
import { getFactory } from '../adapters/factory';
|
|
5
6
|
import { useRouter } from 'vue-router';
|
|
@@ -14,22 +15,26 @@ const router = useRouter();
|
|
|
14
15
|
const store = useVocabularyStore();
|
|
15
16
|
const factory = getFactory();
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!ref?.source || !locality?.referenceFrom) return null;
|
|
18
|
+
// ── Single source of truth for citation resolution ────────────────────────
|
|
19
|
+
// Both classification and navigation target come from the same resolveCite()
|
|
20
|
+
// call, so they can never disagree.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const citeResolution = computed<CiteResolution>(() =>
|
|
23
|
+
factory.resolver.resolveCite(props.citation, props.registerId),
|
|
24
|
+
);
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
const classification = computed<CitationClassification>(() =>
|
|
27
|
+
citeResolution.value.classification,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const resolvedTarget = computed(() => citeResolution.value.resolved);
|
|
27
31
|
|
|
28
|
-
const resolvedTarget = computed(() => resolveCitation());
|
|
29
32
|
const isCrossDataset = computed(() =>
|
|
30
33
|
resolvedTarget.value != null && resolvedTarget.value.registerId !== props.registerId,
|
|
31
34
|
);
|
|
32
35
|
|
|
36
|
+
// ── Navigation ────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
33
38
|
async function navigateToCitation() {
|
|
34
39
|
if (!resolvedTarget.value) return;
|
|
35
40
|
const { registerId, conceptId } = resolvedTarget.value;
|
|
@@ -37,7 +42,37 @@ async function navigateToCitation() {
|
|
|
37
42
|
router.push({ name: 'concept', params: { registerId, conceptId } });
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
//
|
|
45
|
+
// ── Wrapper element determined by classification ──────────────────────────
|
|
46
|
+
// Internal → button (navigates to concept)
|
|
47
|
+
// Self-contained with link → anchor (external link)
|
|
48
|
+
// Everything else → span (plain text)
|
|
49
|
+
|
|
50
|
+
const sourceTag = computed(() => {
|
|
51
|
+
if (classification.value === 'internal-citation') return 'button';
|
|
52
|
+
if (classification.value === 'self-contained-citation' && props.citation.link) return 'a';
|
|
53
|
+
return 'span';
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const sourceAttrs = computed(() => {
|
|
57
|
+
if (sourceTag.value === 'button') {
|
|
58
|
+
return { class: 'concept-link font-medium inline-flex items-center gap-0.5' };
|
|
59
|
+
}
|
|
60
|
+
if (sourceTag.value === 'a') {
|
|
61
|
+
return { href: props.citation.link!, target: '_blank', rel: 'noopener', class: 'concept-link font-medium' };
|
|
62
|
+
}
|
|
63
|
+
return { class: 'font-medium' };
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const sourceEvents = computed(() => {
|
|
67
|
+
if (sourceTag.value !== 'button') return {};
|
|
68
|
+
return {
|
|
69
|
+
onClick: navigateToCitation,
|
|
70
|
+
onMouseenter: schedulePreview,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ── Hover preview ─────────────────────────────────────────────────────────
|
|
75
|
+
|
|
41
76
|
const triggerEl = ref<HTMLElement | null>(null);
|
|
42
77
|
const preview = ref<{ designation: string; definition: string } | null>(null);
|
|
43
78
|
const previewVisible = ref(false);
|
|
@@ -90,39 +125,60 @@ function hidePreview() {
|
|
|
90
125
|
if (previewTimer) { clearTimeout(previewTimer); previewTimer = null; }
|
|
91
126
|
previewVisible.value = false;
|
|
92
127
|
}
|
|
128
|
+
|
|
129
|
+
// ── Locality formatting ───────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
function formatLocality(loc: NonNullable<Citation['locality']>): string {
|
|
132
|
+
const parts: string[] = [];
|
|
133
|
+
const lType = (loc as unknown as Record<string, unknown>).type as string | null;
|
|
134
|
+
const from = (loc as unknown as Record<string, unknown>).referenceFrom ?? (loc as unknown as Record<string, unknown>).reference_from;
|
|
135
|
+
const to = (loc as unknown as Record<string, unknown>).referenceTo ?? (loc as unknown as Record<string, unknown>).reference_to;
|
|
136
|
+
if (lType) parts.push(`, ${lType}`);
|
|
137
|
+
if (from) parts.push(to ? ` ${from}–${to}` : ` ${from}`);
|
|
138
|
+
return parts.join('');
|
|
139
|
+
}
|
|
93
140
|
</script>
|
|
94
141
|
|
|
95
142
|
<template>
|
|
96
143
|
<span class="inline" @mouseleave="hidePreview">
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
144
|
+
<!-- Source reference: dynamic wrapper (button/a/span) -->
|
|
145
|
+
<template v-if="citation.ref?.source">
|
|
146
|
+
<component
|
|
147
|
+
:is="sourceTag"
|
|
148
|
+
v-bind="sourceAttrs"
|
|
149
|
+
v-on="sourceEvents"
|
|
103
150
|
>
|
|
104
151
|
{{ citation.ref.source }}
|
|
105
152
|
<span v-if="isCrossDataset" class="text-[10px] opacity-60 leading-none">↗</span>
|
|
106
|
-
</
|
|
107
|
-
<span v-else-if="citation.ref.source" class="font-medium">{{ citation.ref.source }}</span>
|
|
153
|
+
</component>
|
|
108
154
|
<span v-if="citation.ref.id"> {{ citation.ref.id }}</span>
|
|
109
155
|
<span v-if="citation.ref.version" class="text-ink-400"> ({{ citation.ref.version }})</span>
|
|
110
156
|
</template>
|
|
157
|
+
|
|
158
|
+
<!-- Locality: same formatting across all classifications -->
|
|
111
159
|
<template v-if="citation.locality">
|
|
112
|
-
<button
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
160
|
+
<button
|
|
161
|
+
v-if="classification === 'internal-citation'"
|
|
162
|
+
@click="navigateToCitation"
|
|
163
|
+
@mouseenter="schedulePreview"
|
|
164
|
+
class="concept-link"
|
|
165
|
+
>
|
|
166
|
+
{{ formatLocality(citation.locality) }}
|
|
117
167
|
</button>
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{{ citation.locality.referenceTo ? ` ${citation.locality.referenceFrom}–${citation.locality.referenceTo}` : ` ${citation.locality.referenceFrom}` }}
|
|
122
|
-
</span>
|
|
123
|
-
</template>
|
|
168
|
+
<span v-else class="text-ink-400">
|
|
169
|
+
{{ formatLocality(citation.locality) }}
|
|
170
|
+
</span>
|
|
124
171
|
</template>
|
|
125
|
-
|
|
172
|
+
|
|
173
|
+
<!-- External link badge (only for non-self-contained citations that have a link) -->
|
|
174
|
+
<a
|
|
175
|
+
v-if="citation.link && classification !== 'self-contained-citation'"
|
|
176
|
+
:href="citation.link"
|
|
177
|
+
target="_blank"
|
|
178
|
+
rel="noopener"
|
|
179
|
+
class="concept-link ml-1"
|
|
180
|
+
>[link]</a>
|
|
181
|
+
|
|
126
182
|
<span v-if="citation.original" class="text-xs text-ink-300 ml-1">(orig: {{ citation.original }})</span>
|
|
127
183
|
<span v-if="resolvedTarget" class="text-[9px] text-ink-300 ml-1">→ {{ resolvedTarget.registerId }}/{{ resolvedTarget.conceptId }}</span>
|
|
128
184
|
|