@glossarist/concept-browser 0.7.46 → 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 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://metanorma.github.io/oiml-viml/) — OIML International Vocabulary of Legal Metrology
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.46",
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": {
@@ -11,6 +11,9 @@
11
11
  "build": "vue-tsc --noEmit && vite build",
12
12
  "postbuild": "node scripts/generate-404.js",
13
13
  "preview": "vite preview",
14
+ "check:scripts": "node scripts/check-syntax.mjs",
15
+ "pretest": "node scripts/check-syntax.mjs",
16
+ "prebuild": "node scripts/check-syntax.mjs",
14
17
  "fetch-datasets": "node scripts/fetch-datasets.mjs",
15
18
  "generate-data": "node scripts/generate-data.mjs",
16
19
  "generate-ontology": "node scripts/generate-ontology-data.mjs && node scripts/generate-ontology-schema.mjs",
@@ -25,7 +28,7 @@
25
28
  "autoprefixer": "^10.4.21",
26
29
  "d3": "^7.9.0",
27
30
  "favicons": "^7.2.0",
28
- "glossarist": "^0.4.0",
31
+ "glossarist": "^0.4.2",
29
32
  "js-yaml": "^4.1.0",
30
33
  "jszip": "^3.10.1",
31
34
  "pinia": "^2.3.1",
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { readdirSync, statSync } from "node:fs";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+
7
+ const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
8
+
9
+ const TARGET_DIRS = ["scripts", "cli"];
10
+ const EXTENSIONS = new Set([".mjs", ".js", ".cjs"]);
11
+ const SKIP_DIRS = new Set(["__tests__", "node_modules"]);
12
+
13
+ function walk(dir) {
14
+ const out = [];
15
+ for (const entry of readdirSync(dir)) {
16
+ if (SKIP_DIRS.has(entry)) continue;
17
+ const full = path.join(dir, entry);
18
+ const st = statSync(full);
19
+ if (st.isDirectory()) {
20
+ out.push(...walk(full));
21
+ } else if (EXTENSIONS.has(path.extname(entry))) {
22
+ out.push(full);
23
+ }
24
+ }
25
+ return out;
26
+ }
27
+
28
+ export function checkAllScripts({ root = ROOT } = {}) {
29
+ const files = [];
30
+ for (const sub of TARGET_DIRS) {
31
+ const dir = path.join(root, sub);
32
+ try {
33
+ statSync(dir);
34
+ } catch {
35
+ continue;
36
+ }
37
+ files.push(...walk(dir));
38
+ }
39
+ files.sort();
40
+
41
+ const failures = [];
42
+ for (const file of files) {
43
+ const result = spawnSync(process.execPath, ["--check", file], {
44
+ encoding: "utf8",
45
+ });
46
+ if (result.status !== 0) {
47
+ failures.push({
48
+ file: path.relative(root, file),
49
+ stderr: result.stderr || "",
50
+ });
51
+ }
52
+ }
53
+
54
+ return { files, failures };
55
+ }
56
+
57
+ function main() {
58
+ const { files, failures } = checkAllScripts();
59
+ if (failures.length === 0) {
60
+ process.stdout.write(`syntax OK: ${files.length} file(s) checked\n`);
61
+ return;
62
+ }
63
+ process.stderr.write(
64
+ `syntax check failed: ${failures.length} of ${files.length} file(s)\n\n`,
65
+ );
66
+ for (const { file, stderr } of failures) {
67
+ process.stderr.write(`--- ${file} ---\n${stderr}\n`);
68
+ }
69
+ process.exit(1);
70
+ }
71
+
72
+ const isDirectEntry = process.argv[1] &&
73
+ path.resolve(process.argv[1]) ===
74
+ path.resolve(new URL(import.meta.url).pathname);
75
+
76
+ if (isDirectEntry) {
77
+ main();
78
+ }
@@ -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', () => {
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { spawnSync } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const repoRoot = path.resolve(
7
+ path.dirname(fileURLToPath(import.meta.url)),
8
+ '../..',
9
+ );
10
+
11
+ describe('scripts syntax gate', () => {
12
+ it('`npm run check:scripts` exits 0 (every .mjs/.js/.cjs in scripts/ and cli/ parses)', () => {
13
+ const result = spawnSync(
14
+ process.execPath,
15
+ [path.join(repoRoot, 'scripts', 'check-syntax.mjs')],
16
+ { encoding: 'utf8' },
17
+ );
18
+
19
+ if (result.status !== 0) {
20
+ throw new Error(
21
+ `check-syntax.mjs failed (exit ${result.status}):\n${result.stdout || ''}${result.stderr || ''}`,
22
+ );
23
+ }
24
+
25
+ expect(result.status).toBe(0);
26
+ expect(result.stdout).toMatch(/syntax OK:/);
27
+ });
28
+ });
@@ -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(d => ({
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(n => ({ content: n['gl:content'] ?? '' }));
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(a => ({ content: a['gl:content'] ?? '' }));
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(e => ({ content: e['gl:content'] ?? '' }));
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"> &middot; </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.examples.length"> &middot; </template>
384
- <template v-if="lc.examples.length">{{ lc.examples.length }} example{{ lc.examples.length > 1 ? 's' : '' }}</template>
384
+ <template v-if="(lc.annotations.length || lc.notes.length) && totalExampleCount(lc)"> &middot; </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="(_, i) in lc.notes" :key="i" class="text-ink-600 text-sm leading-relaxed">
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="lc.renderedNotes[i]"></div>
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="(_, i) in lc.examples" :key="i" class="text-ink-600 text-sm leading-relaxed">
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="lc.renderedExamples[i]"></div>
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: string[];
21
- renderedNotes: string[];
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
  }