@glossarist/concept-browser 0.7.47 → 0.7.48
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/README.md +20 -1
- package/package.json +2 -2
- package/src/__tests__/concept-detail-interaction.test.ts +21 -0
- package/src/__tests__/dataset-adapter.test.ts +44 -0
- package/src/adapters/model-bridge.ts +12 -6
- package/src/components/ConceptDetail.vue +13 -6
- package/src/composables/use-concept-content.ts +39 -9
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A statically deployable single-page application for browsing terminology dataset
|
|
|
4
4
|
|
|
5
5
|
**Live sites:**
|
|
6
6
|
- [GeoLexica](https://www.geolexica.org) — IEC Electropedia + ISO/TC 211 + more
|
|
7
|
-
- [VIML](https://
|
|
7
|
+
- [VIML](https://www.oimlsmart.org/vocab/) — OIML International Vocabulary of Legal Metrology
|
|
8
8
|
- [OIML Terms](https://metanorma.github.io/oiml-terms/) — OIML G 18 terminology
|
|
9
9
|
|
|
10
10
|
---
|
|
@@ -346,6 +346,25 @@ The build produces static files in `dist/` with an SPA `404.html` fallback:
|
|
|
346
346
|
- **Vercel:** Framework Vite, build command `npx concept-browser build`, output directory `dist`
|
|
347
347
|
- **AWS S3 + CloudFront:** Upload `dist/`, error document `index.html`, configure CloudFront for SPA routing
|
|
348
348
|
|
|
349
|
+
### Known deployments
|
|
350
|
+
|
|
351
|
+
Sites currently powered by `@glossarist/concept-browser`. When cutting a release with breaking changes, bump `@glossarist/concept-browser` in each consumer repo and redeploy.
|
|
352
|
+
|
|
353
|
+
| Repo | Site |
|
|
354
|
+
|---|---|
|
|
355
|
+
| [`geolexica/geolexica.github.io`](https://github.com/geolexica/geolexica.github.io) | https://www.geolexica.org |
|
|
356
|
+
| [`geolexica/isotc204.geolexica.org`](https://github.com/geolexica/isotc204.geolexica.org) | https://isotc204.geolexica.org |
|
|
357
|
+
| [`geolexica/isotc211.geolexica.org`](https://github.com/geolexica/isotc211.geolexica.org) | https://isotc211.geolexica.org |
|
|
358
|
+
| [`geolexica/osgeo.geolexica.org`](https://github.com/geolexica/osgeo.geolexica.org) | https://osgeo.geolexica.org |
|
|
359
|
+
| [`oimlsmart/vocab`](https://github.com/oimlsmart/vocab) | https://www.oimlsmart.org/vocab/ (VIML)¹ |
|
|
360
|
+
| [`metanorma/oiml-terms`](https://github.com/metanorma/oiml-terms) | https://metanorma.github.io/oiml-terms/ |
|
|
361
|
+
| [`metanorma/iala-vocab`](https://github.com/metanorma/iala-vocab) | https://metanorma.github.io/iala-vocab/ |
|
|
362
|
+
| [`metanorma/iso-10303-2-vocab`](https://github.com/metanorma/iso-10303-2-vocab) | https://metanorma.github.io/iso-10303-2-vocab/ |
|
|
363
|
+
|
|
364
|
+
To add a new deployment here, open a PR against this README.
|
|
365
|
+
|
|
366
|
+
¹ The VIML deployment moved from `metanorma/oiml-viml` to [`oimlsmart/vocab`](https://github.com/oimlsmart/vocab) (GitHub redirects the old name automatically), and its live URL moved from `metanorma.github.io/oiml-viml/` to `www.oimlsmart.org/vocab/`. The old `metanorma.github.io/oiml-viml/` URL returns 404 (Pages doesn't follow repo renames across orgs).
|
|
367
|
+
|
|
349
368
|
---
|
|
350
369
|
|
|
351
370
|
## Architecture
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.48",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"autoprefixer": "^10.4.21",
|
|
29
29
|
"d3": "^7.9.0",
|
|
30
30
|
"favicons": "^7.2.0",
|
|
31
|
-
"glossarist": "^0.4.
|
|
31
|
+
"glossarist": "^0.4.2",
|
|
32
32
|
"js-yaml": "^4.1.0",
|
|
33
33
|
"jszip": "^3.10.1",
|
|
34
34
|
"pinia": "^2.3.1",
|
|
@@ -181,6 +181,27 @@ describe('ConceptDetail interactions', () => {
|
|
|
181
181
|
expect(wrapper.text()).toContain('an example');
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
+
it('renders scoped examples nested inside a note', async () => {
|
|
185
|
+
const json = makeConceptJson() as Record<string, any>;
|
|
186
|
+
json['gl:localizedConcept'].eng['gl:notes'] = [
|
|
187
|
+
{
|
|
188
|
+
'@type': 'gl:DetailedDefinition',
|
|
189
|
+
'gl:content': 'resistance depends on dimensions and material',
|
|
190
|
+
'gl:examples': [
|
|
191
|
+
{ '@type': 'gl:DetailedDefinition', 'gl:content': 'copper resistivity ≈ 1.68e-8 Ω·m at 20 °C' },
|
|
192
|
+
{ '@type': 'gl:DetailedDefinition', 'gl:content': '1 m of 1 mm² copper wire ≈ 0.017 Ω' },
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
const wrapper = mountDetail(json);
|
|
197
|
+
await switchToDefinition(wrapper);
|
|
198
|
+
expect(wrapper.text()).toContain('resistance depends');
|
|
199
|
+
expect(wrapper.text()).toContain('Example 1');
|
|
200
|
+
expect(wrapper.text()).toContain('Example 2');
|
|
201
|
+
expect(wrapper.text()).toContain('copper resistivity');
|
|
202
|
+
expect(wrapper.text()).toContain('0.017 Ω');
|
|
203
|
+
});
|
|
204
|
+
|
|
184
205
|
it('renders designation types as badges', async () => {
|
|
185
206
|
const wrapper = mountDetail();
|
|
186
207
|
await switchToDefinition(wrapper);
|
|
@@ -85,6 +85,50 @@ describe('DatasetAdapter', () => {
|
|
|
85
85
|
expect(adapter.getIndexEntry('103-01-02')?.eng).toBe('functional');
|
|
86
86
|
expect(adapter.getConceptCount()).toBe(3);
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
it('preserves the full multilingual designations map (regression for #25)', async () => {
|
|
90
|
+
const index = {
|
|
91
|
+
registerId: 'test',
|
|
92
|
+
schemaVersion: '1.0.0',
|
|
93
|
+
conceptCount: 2,
|
|
94
|
+
chunkSize: 500,
|
|
95
|
+
chunks: [],
|
|
96
|
+
concepts: [
|
|
97
|
+
{
|
|
98
|
+
id: '102-01-01',
|
|
99
|
+
designations: { eng: 'equality', fra: 'égalité', deu: 'Gleichheit' },
|
|
100
|
+
eng: 'equality',
|
|
101
|
+
status: 'Standard',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: '102-01-02',
|
|
105
|
+
designations: { eng: 'inequality', fra: 'inégalité' },
|
|
106
|
+
eng: 'inequality',
|
|
107
|
+
status: 'Standard',
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
mockFetch.mockReturnValue(mockJsonResponse(index));
|
|
112
|
+
|
|
113
|
+
await adapter.loadIndex();
|
|
114
|
+
|
|
115
|
+
const equality = adapter.getIndexEntry('102-01-01');
|
|
116
|
+
expect(equality?.designations).toEqual({
|
|
117
|
+
eng: 'equality',
|
|
118
|
+
fra: 'égalité',
|
|
119
|
+
deu: 'Gleichheit',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const inequality = adapter.getIndexEntry('102-01-02');
|
|
123
|
+
expect(inequality?.designations).toEqual({
|
|
124
|
+
eng: 'inequality',
|
|
125
|
+
fra: 'inégalité',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(adapter.lookupByDesignation('égalité')).toBe('102-01-01');
|
|
129
|
+
expect(adapter.lookupByDesignation('Gleichheit')).toBe('102-01-01');
|
|
130
|
+
expect(adapter.lookupByDesignation('inégalité')).toBe('102-01-02');
|
|
131
|
+
});
|
|
88
132
|
});
|
|
89
133
|
|
|
90
134
|
describe('fetchConcept', () => {
|
|
@@ -399,6 +399,14 @@ function mapLocalityFromJsonLd(rawLoc: JsonLdLocality | undefined): Record<strin
|
|
|
399
399
|
? locObj : null;
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
+
function mapDetailedDefinitionFromJsonLd(d: any): Record<string, unknown> {
|
|
403
|
+
const result: Record<string, unknown> = { content: d['gl:content'] ?? '' };
|
|
404
|
+
if (d['gl:examples']?.length) {
|
|
405
|
+
result.examples = d['gl:examples'].map(mapDetailedDefinitionFromJsonLd);
|
|
406
|
+
}
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
|
|
402
410
|
function mapSourceFromJsonLd(s: JsonLdSource): Record<string, unknown> {
|
|
403
411
|
const result: Record<string, unknown> = {};
|
|
404
412
|
if (s['gl:id']) result.id = s['gl:id'];
|
|
@@ -480,21 +488,19 @@ function mapLocalizedFromJsonLd(lc: JsonLdLocalizedConcept): Record<string, unkn
|
|
|
480
488
|
}
|
|
481
489
|
|
|
482
490
|
if (lc['gl:definition']?.length) {
|
|
483
|
-
data.definition = lc['gl:definition'].map(
|
|
484
|
-
content: d['gl:content'] ?? '',
|
|
485
|
-
}));
|
|
491
|
+
data.definition = lc['gl:definition'].map(mapDetailedDefinitionFromJsonLd);
|
|
486
492
|
}
|
|
487
493
|
|
|
488
494
|
if (lc['gl:notes']?.length) {
|
|
489
|
-
data.notes = lc['gl:notes'].map(
|
|
495
|
+
data.notes = lc['gl:notes'].map(mapDetailedDefinitionFromJsonLd);
|
|
490
496
|
}
|
|
491
497
|
|
|
492
498
|
if (lc['gl:annotations']?.length) {
|
|
493
|
-
data.annotations = lc['gl:annotations'].map(
|
|
499
|
+
data.annotations = lc['gl:annotations'].map(mapDetailedDefinitionFromJsonLd);
|
|
494
500
|
}
|
|
495
501
|
|
|
496
502
|
if (lc['gl:examples']?.length) {
|
|
497
|
-
data.examples = lc['gl:examples'].map(
|
|
503
|
+
data.examples = lc['gl:examples'].map(mapDetailedDefinitionFromJsonLd);
|
|
498
504
|
}
|
|
499
505
|
|
|
500
506
|
if (lc['gl:source']?.length) {
|
|
@@ -161,6 +161,7 @@ const {
|
|
|
161
161
|
toggleLang,
|
|
162
162
|
toggleAll,
|
|
163
163
|
plainTruncate,
|
|
164
|
+
totalExampleCount,
|
|
164
165
|
orderedDesignations,
|
|
165
166
|
} = useConceptContent(conceptComputed, manifestComputed, renderOpts);
|
|
166
167
|
|
|
@@ -380,8 +381,8 @@ const nonVerbalReps = computed(() => {
|
|
|
380
381
|
<template v-if="lc.annotations.length">{{ lc.annotations.length }} annotation{{ lc.annotations.length > 1 ? 's' : '' }}</template>
|
|
381
382
|
<template v-if="lc.annotations.length && lc.notes.length"> · </template>
|
|
382
383
|
<template v-if="lc.notes.length">{{ lc.notes.length }} note{{ lc.notes.length > 1 ? 's' : '' }}</template>
|
|
383
|
-
<template v-if="(lc.annotations.length || lc.notes.length) && lc
|
|
384
|
-
<template v-if="lc
|
|
384
|
+
<template v-if="(lc.annotations.length || lc.notes.length) && totalExampleCount(lc)"> · </template>
|
|
385
|
+
<template v-if="totalExampleCount(lc)">{{ totalExampleCount(lc) }} example{{ totalExampleCount(lc) > 1 ? 's' : '' }}</template>
|
|
385
386
|
</p>
|
|
386
387
|
</div>
|
|
387
388
|
|
|
@@ -411,17 +412,23 @@ const nonVerbalReps = computed(() => {
|
|
|
411
412
|
|
|
412
413
|
<!-- Notes -->
|
|
413
414
|
<div v-if="lc.notes.length" class="space-y-2">
|
|
414
|
-
<div v-for="(
|
|
415
|
+
<div v-for="(note, i) in lc.notes" :key="i" class="text-ink-600 text-sm leading-relaxed">
|
|
415
416
|
<span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.note') }} {{ i + 1 }}</span>
|
|
416
|
-
<div class="mt-1" v-html="
|
|
417
|
+
<div class="mt-1" v-html="note.renderedContent"></div>
|
|
418
|
+
<div v-if="note.examples.length" class="mt-2 ml-4 space-y-2 border-l-2 border-ink-200/70 pl-3">
|
|
419
|
+
<div v-for="(ex, j) in note.examples" :key="j" class="text-ink-500 leading-relaxed">
|
|
420
|
+
<span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.example') }} {{ j + 1 }}</span>
|
|
421
|
+
<div class="mt-1" v-html="ex.renderedContent"></div>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
417
424
|
</div>
|
|
418
425
|
</div>
|
|
419
426
|
|
|
420
427
|
<!-- Examples -->
|
|
421
428
|
<div v-if="lc.examples.length" class="space-y-2">
|
|
422
|
-
<div v-for="(
|
|
429
|
+
<div v-for="(ex, i) in lc.examples" :key="i" class="text-ink-600 text-sm leading-relaxed">
|
|
423
430
|
<span class="font-medium text-ink-400 text-xs uppercase tracking-wide">{{ t('concept.example') }} {{ i + 1 }}</span>
|
|
424
|
-
<div class="mt-1" v-html="
|
|
431
|
+
<div class="mt-1" v-html="ex.renderedContent"></div>
|
|
425
432
|
</div>
|
|
426
433
|
</div>
|
|
427
434
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { computed, ref, watch, type ComputedRef } from 'vue';
|
|
2
|
-
import type { Concept, LocalizedConcept, ConceptSource, Designation } from 'glossarist';
|
|
2
|
+
import type { Concept, LocalizedConcept, ConceptSource, Designation, DetailedDefinition } from 'glossarist';
|
|
3
3
|
import type { Manifest } from '../adapters/types';
|
|
4
4
|
import type { RenderOptions } from '../utils/content-renderer';
|
|
5
5
|
import { renderContent, cleanContent } from '../utils/content-renderer';
|
|
@@ -9,6 +9,17 @@ import { sortLanguages } from '../utils/lang';
|
|
|
9
9
|
import { useSiteConfig } from '../config/use-site-config';
|
|
10
10
|
import { useI18n } from '../i18n';
|
|
11
11
|
|
|
12
|
+
export interface ExampleEntry {
|
|
13
|
+
content: string;
|
|
14
|
+
renderedContent: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NoteEntry {
|
|
18
|
+
content: string;
|
|
19
|
+
renderedContent: string;
|
|
20
|
+
examples: ExampleEntry[];
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
export interface LangContent {
|
|
13
24
|
lang: string;
|
|
14
25
|
lc: LocalizedConcept;
|
|
@@ -17,10 +28,8 @@ export interface LangContent {
|
|
|
17
28
|
renderedDefinition: string;
|
|
18
29
|
annotations: string[];
|
|
19
30
|
renderedAnnotations: string[];
|
|
20
|
-
notes:
|
|
21
|
-
|
|
22
|
-
examples: string[];
|
|
23
|
-
renderedExamples: string[];
|
|
31
|
+
notes: NoteEntry[];
|
|
32
|
+
examples: ExampleEntry[];
|
|
24
33
|
sources: ConceptSource[];
|
|
25
34
|
designations: Designation[];
|
|
26
35
|
renderedDesignations: Map<string, string>;
|
|
@@ -61,10 +70,27 @@ export function useConceptContent(
|
|
|
61
70
|
const definition = lc.definitions
|
|
62
71
|
.map(d => d.content).filter(Boolean).join('\n\n');
|
|
63
72
|
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
73
|
const opts = renderOpts.value;
|
|
67
74
|
|
|
75
|
+
const buildExample = (e: { content?: string } | undefined): ExampleEntry | null => {
|
|
76
|
+
const content = e?.content ?? '';
|
|
77
|
+
return content ? { content, renderedContent: renderContent(content, opts) } : null;
|
|
78
|
+
};
|
|
79
|
+
const notes: NoteEntry[] = lc.notes
|
|
80
|
+
.map(n => {
|
|
81
|
+
const content = n.content ?? '';
|
|
82
|
+
if (!content) return null;
|
|
83
|
+
const nested = n.examples ?? [];
|
|
84
|
+
const examples = nested
|
|
85
|
+
.map(buildExample)
|
|
86
|
+
.filter((e): e is ExampleEntry => e !== null);
|
|
87
|
+
return { content, renderedContent: renderContent(content, opts), examples };
|
|
88
|
+
})
|
|
89
|
+
.filter((n): n is NoteEntry => n !== null);
|
|
90
|
+
const examples: ExampleEntry[] = lc.examples
|
|
91
|
+
.map(buildExample)
|
|
92
|
+
.filter((e): e is ExampleEntry => e !== null);
|
|
93
|
+
|
|
68
94
|
result.push({
|
|
69
95
|
lang,
|
|
70
96
|
lc,
|
|
@@ -74,9 +100,7 @@ export function useConceptContent(
|
|
|
74
100
|
annotations,
|
|
75
101
|
renderedAnnotations: annotations.map((a: string) => renderContent(a, opts)),
|
|
76
102
|
notes,
|
|
77
|
-
renderedNotes: notes.map(n => renderContent(n, opts)),
|
|
78
103
|
examples,
|
|
79
|
-
renderedExamples: examples.map(e => renderContent(e, opts)),
|
|
80
104
|
sources: lc.sources,
|
|
81
105
|
designations: lc.terms,
|
|
82
106
|
renderedDesignations: new Map(lc.terms.map(d => [d.designation, renderContent(d.designation)])),
|
|
@@ -137,6 +161,11 @@ export function useConceptContent(
|
|
|
137
161
|
return text.length <= max ? text : text.slice(0, max).trimEnd() + '…';
|
|
138
162
|
}
|
|
139
163
|
|
|
164
|
+
function totalExampleCount(lc: LangContent): number {
|
|
165
|
+
const nested = lc.notes.reduce((sum, n) => sum + n.examples.length, 0);
|
|
166
|
+
return lc.examples.length + nested;
|
|
167
|
+
}
|
|
168
|
+
|
|
140
169
|
function orderedDesignations(lang: string): Designation[] {
|
|
141
170
|
const desigs = langContentMap.value.get(lang)?.designations ?? [];
|
|
142
171
|
const preferred = desigs.filter(d => d.normativeStatus === 'preferred');
|
|
@@ -155,6 +184,7 @@ export function useConceptContent(
|
|
|
155
184
|
toggleLang,
|
|
156
185
|
toggleAll,
|
|
157
186
|
plainTruncate,
|
|
187
|
+
totalExampleCount,
|
|
158
188
|
orderedDesignations,
|
|
159
189
|
};
|
|
160
190
|
}
|