@glossarist/concept-browser 0.7.32 → 0.7.34
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.34",
|
|
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": {
|
package/scripts/build-edges.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Usage: node scripts/build-edges.js
|
|
7
7
|
*/
|
|
8
|
+
import { extractSourceRefs } from './extract-source-refs.js';
|
|
8
9
|
import { readFileSync, writeFileSync, readdirSync, existsSync } from 'fs';
|
|
9
10
|
import { join, dirname } from 'path';
|
|
10
11
|
import { fileURLToPath } from 'url';
|
|
@@ -23,32 +24,6 @@ function slugify(text) {
|
|
|
23
24
|
|
|
24
25
|
// --- Extractors (open/closed: add new extractors to EXTRACTORS array) ---
|
|
25
26
|
|
|
26
|
-
function extractSourceRefs(concept, registerId) {
|
|
27
|
-
const refs = new Set();
|
|
28
|
-
|
|
29
|
-
// Managed concept-level sources
|
|
30
|
-
for (const src of concept['gl:source'] || []) {
|
|
31
|
-
const origin = src['gl:origin'];
|
|
32
|
-
if (origin) {
|
|
33
|
-
const ref = origin['gl:ref'];
|
|
34
|
-
if (ref?.['gl:source']) refs.add(ref['gl:source']);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Localized concept-level sources
|
|
39
|
-
for (const lc of Object.values(concept['gl:localizedConcept'] || {})) {
|
|
40
|
-
for (const src of lc['gl:source'] || []) {
|
|
41
|
-
const origin = src['gl:origin'];
|
|
42
|
-
if (origin) {
|
|
43
|
-
const ref = origin['gl:ref'];
|
|
44
|
-
if (ref?.['gl:source']) refs.add(ref['gl:source']);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return [...refs].map(source => ({ source, registerId }));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
27
|
function extractReferences(concept, registerId) {
|
|
53
28
|
const edges = [];
|
|
54
29
|
const sourceUri = concept['@id'];
|
|
@@ -323,34 +298,42 @@ for (const ds of datasets) {
|
|
|
323
298
|
console.log();
|
|
324
299
|
}
|
|
325
300
|
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
301
|
+
// Audit — report concept source strings not covered by manifest refs.
|
|
302
|
+
// Bibliography mapping (ref → URN) is declared in register.yaml and flows
|
|
303
|
+
// through manifest.json to datasets.json at generate time. No separate
|
|
304
|
+
// source-refs file needed — the registry IS the single source of truth.
|
|
330
305
|
|
|
331
|
-
//
|
|
306
|
+
// Build a lookup of all known source strings from manifests
|
|
307
|
+
const knownSourceStrings = new Set();
|
|
332
308
|
for (const [ds, manifest] of manifestCache) {
|
|
333
|
-
if (manifest.ref)
|
|
309
|
+
if (manifest.ref) knownSourceStrings.add(manifest.ref);
|
|
334
310
|
for (const alias of manifest.refAliases ?? []) {
|
|
335
|
-
|
|
311
|
+
knownSourceStrings.add(alias);
|
|
336
312
|
}
|
|
337
|
-
if (manifest.datasetUri)
|
|
313
|
+
if (manifest.datasetUri) knownSourceStrings.add(manifest.datasetUri);
|
|
338
314
|
for (const alias of manifest.uriAliases ?? []) {
|
|
339
315
|
const base = alias.endsWith('*') ? alias.slice(0, -1) : alias;
|
|
340
|
-
|
|
316
|
+
knownSourceStrings.add(base);
|
|
341
317
|
}
|
|
342
318
|
}
|
|
343
319
|
|
|
344
|
-
|
|
320
|
+
const auditUnmatched = new Map();
|
|
345
321
|
for (const { source, registerId } of allSourceRefs) {
|
|
346
|
-
if (
|
|
347
|
-
|
|
322
|
+
if (knownSourceStrings.has(source)) continue;
|
|
323
|
+
// URN source strings resolve via URI routing — not a bibliography concern
|
|
324
|
+
if (source.startsWith('urn:')) continue;
|
|
325
|
+
if (!auditUnmatched.has(source)) {
|
|
326
|
+
auditUnmatched.set(source, registerId);
|
|
348
327
|
}
|
|
349
328
|
}
|
|
350
329
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
330
|
+
if (auditUnmatched.size > 0) {
|
|
331
|
+
console.warn(`\n⚠ ${auditUnmatched.size} source string(s) in concept data have no matching dataset ref:`);
|
|
332
|
+
for (const [source, fromDataset] of auditUnmatched) {
|
|
333
|
+
console.warn(` "${source}" (from ${fromDataset})`);
|
|
334
|
+
}
|
|
335
|
+
console.warn('Add refAliases to the target dataset manifest, or fix source strings to use URNs.\n');
|
|
336
|
+
}
|
|
354
337
|
|
|
355
338
|
// Build cross-reference index: for each dataset, which other datasets'
|
|
356
339
|
// edges.json contains edges targeting that dataset's URIs.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source reference extraction for citation linking.
|
|
3
|
+
*
|
|
4
|
+
* Scans concept JSON-LD for source strings in gl:source fields.
|
|
5
|
+
* Used by build-edges.js to generate source-refs.json.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function extractSourceRefs(concept, registerId) {
|
|
9
|
+
const refs = new Set();
|
|
10
|
+
|
|
11
|
+
// Managed concept-level sources
|
|
12
|
+
for (const src of concept['gl:source'] || []) {
|
|
13
|
+
const origin = src['gl:origin'];
|
|
14
|
+
if (origin) {
|
|
15
|
+
const ref = origin['gl:ref'];
|
|
16
|
+
if (ref?.['gl:source']) refs.add(ref['gl:source']);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Localized concept-level sources
|
|
21
|
+
for (const lc of Object.values(concept['gl:localizedConcept'] || {})) {
|
|
22
|
+
for (const src of lc['gl:source'] || []) {
|
|
23
|
+
const origin = src['gl:origin'];
|
|
24
|
+
if (origin) {
|
|
25
|
+
const ref = origin['gl:ref'];
|
|
26
|
+
if (ref?.['gl:source']) refs.add(ref['gl:source']);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [...refs].map(source => ({ source, registerId }));
|
|
32
|
+
}
|
|
@@ -927,6 +927,8 @@ for (let i = 0; i < config.datasets.length; i++) {
|
|
|
927
927
|
datasetUri: ds.uri || reg?.urn || undefined,
|
|
928
928
|
uriBase: config.uriBase || undefined,
|
|
929
929
|
uriAliases: ds.uriAliases || reg?.urnAliases || undefined,
|
|
930
|
+
ref: ds.ref || reg?.ref || undefined,
|
|
931
|
+
refAliases: ds.refAliases || reg?.refAliases || undefined,
|
|
930
932
|
});
|
|
931
933
|
}
|
|
932
934
|
writeJson(path.join(PUBLIC, 'datasets.json'), registry);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
4
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
5
|
+
import CitationDisplay from '../components/CitationDisplay.vue';
|
|
6
|
+
import { getFactory, resetFactory } from '../adapters/factory';
|
|
7
|
+
import { ReferenceResolver } from '../adapters/ReferenceResolver';
|
|
8
|
+
|
|
9
|
+
// Minimal Citation type matching the glossarist Citation interface
|
|
10
|
+
function makeCitation(source: string, referenceFrom: string, type = 'clause') {
|
|
11
|
+
return {
|
|
12
|
+
ref: { source },
|
|
13
|
+
locality: { type, referenceFrom },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('CitationDisplay — source reference linking', () => {
|
|
18
|
+
let router: any;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
resetFactory();
|
|
22
|
+
const factory = getFactory();
|
|
23
|
+
// Register dataset patterns
|
|
24
|
+
factory.resolver.registerDataset('vim-2012', ['urn:oiml:pub:v:2:2012*']);
|
|
25
|
+
factory.resolver.registerDataset('viml-2022', ['urn:oiml:pub:v:1:2022*']);
|
|
26
|
+
// Register source refs
|
|
27
|
+
factory.resolver.registerSourceRef('OIML V 2-200:2012', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
28
|
+
factory.resolver.registerSourceRef('OIML V2-200:2012', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
29
|
+
factory.resolver.registerSourceRef('VIM', 'vim-2012', 'urn:oiml:pub:v:2:2012');
|
|
30
|
+
|
|
31
|
+
router = createRouter({
|
|
32
|
+
history: createMemoryHistory(),
|
|
33
|
+
routes: [
|
|
34
|
+
{ path: '/', component: { template: '<div/>' } },
|
|
35
|
+
{ name: 'concept', path: '/dataset/:registerId/concept/:conceptId', component: { template: '<div/>' } },
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
await router.push('/');
|
|
39
|
+
setActivePinia(createPinia());
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function mountCitation(citation: any, registerId?: string) {
|
|
43
|
+
return mount(CitationDisplay, {
|
|
44
|
+
props: { citation, registerId },
|
|
45
|
+
global: { plugins: [router] },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
it('renders source text as plain span when citation cannot be resolved', () => {
|
|
50
|
+
const wrapper = mountCitation(makeCitation('Unknown Source', '1.1'));
|
|
51
|
+
expect(wrapper.find('button.concept-link').exists()).toBe(false);
|
|
52
|
+
expect(wrapper.text()).toContain('Unknown Source');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('renders source text as clickable button when citation resolves', () => {
|
|
56
|
+
const wrapper = mountCitation(makeCitation('OIML V2-200:2012', '2.2'), 'viml-2022');
|
|
57
|
+
expect(wrapper.find('button.concept-link').exists()).toBe(true);
|
|
58
|
+
expect(wrapper.text()).toContain('OIML V2-200:2012');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('renders locality as clickable when citation resolves', () => {
|
|
62
|
+
const wrapper = mountCitation(makeCitation('OIML V2-200:2012', '2.2'), 'viml-2022');
|
|
63
|
+
expect(wrapper.text()).toContain('2.2');
|
|
64
|
+
// The clause type and referenceFrom should be inside a button
|
|
65
|
+
const buttons = wrapper.findAll('button.concept-link');
|
|
66
|
+
expect(buttons.length).toBeGreaterThanOrEqual(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('renders locality as plain text when citation does not resolve', () => {
|
|
70
|
+
const wrapper = mountCitation(makeCitation('Unknown', '1.1'));
|
|
71
|
+
expect(wrapper.text()).toContain('1.1');
|
|
72
|
+
expect(wrapper.find('button.concept-link').exists()).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('renders nothing when citation has no ref', () => {
|
|
76
|
+
const wrapper = mountCitation({ ref: null, locality: null });
|
|
77
|
+
expect(wrapper.find('button').exists()).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('renders ref.id when present alongside ref.source', () => {
|
|
81
|
+
const wrapper = mountCitation({
|
|
82
|
+
ref: { source: 'Unknown', id: 'ABC-123' },
|
|
83
|
+
locality: null,
|
|
84
|
+
});
|
|
85
|
+
expect(wrapper.text()).toContain('ABC-123');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('renders ref.version when present', () => {
|
|
89
|
+
const wrapper = mountCitation({
|
|
90
|
+
ref: { source: 'ISO 9000', version: '2015' },
|
|
91
|
+
locality: null,
|
|
92
|
+
});
|
|
93
|
+
expect(wrapper.text()).toContain('2015');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('shows target hint for resolved cross-dataset citation', () => {
|
|
97
|
+
const wrapper = mountCitation(makeCitation('VIM', '2.2'), 'viml-2022');
|
|
98
|
+
const hint = wrapper.text();
|
|
99
|
+
expect(hint).toContain('→');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('resolves via ref alias', () => {
|
|
103
|
+
const wrapper = mountCitation(makeCitation('VIM', '2.2'), 'viml-2022');
|
|
104
|
+
expect(wrapper.find('button.concept-link').exists()).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('renders citation with both source and locality resolved', () => {
|
|
108
|
+
const wrapper = mountCitation(makeCitation('OIML V 2-200:2012', '5.1'), 'viml-2022');
|
|
109
|
+
expect(wrapper.find('button.concept-link').exists()).toBe(true);
|
|
110
|
+
expect(wrapper.text()).toContain('OIML V 2-200:2012');
|
|
111
|
+
expect(wrapper.text()).toContain('5.1');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('shows cross-dataset arrow indicator for cross-dataset citations', () => {
|
|
115
|
+
const wrapper = mountCitation(makeCitation('VIM', '2.2'), 'viml-2022');
|
|
116
|
+
expect(wrapper.text()).toContain('↗');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('does not show cross-dataset arrow for same-dataset citations', () => {
|
|
120
|
+
// Same dataset: registerId matches resolved target
|
|
121
|
+
const wrapper = mountCitation(makeCitation('OIML V 2-200:2012', '2.2'), 'vim-2012');
|
|
122
|
+
expect(wrapper.text()).not.toContain('↗');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('does not show cross-dataset arrow for unresolved citations', () => {
|
|
126
|
+
const wrapper = mountCitation(makeCitation('Unknown', '1.1'));
|
|
127
|
+
expect(wrapper.text()).not.toContain('↗');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('renders hover preview elements for resolved citations', () => {
|
|
131
|
+
const wrapper = mountCitation(makeCitation('VIM', '2.2'), 'viml-2022');
|
|
132
|
+
// The component should have mouseenter handlers on the button
|
|
133
|
+
const buttons = wrapper.findAll('button.concept-link');
|
|
134
|
+
expect(buttons.length).toBeGreaterThanOrEqual(1);
|
|
135
|
+
// Teleport content is not rendered in test env, but the template structure is present
|
|
136
|
+
expect(wrapper.html()).toContain('Hover preview');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('does not render preview elements for unresolved citations', () => {
|
|
140
|
+
const wrapper = mountCitation(makeCitation('Unknown', '1.1'));
|
|
141
|
+
expect(wrapper.html()).not.toContain('citation-preview');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { LocalizedConcept, Expression } from 'glossarist';
|
|
3
|
+
import {
|
|
4
|
+
entryStatusColor,
|
|
5
|
+
conceptStatusColor,
|
|
6
|
+
conceptStatusLabel,
|
|
7
|
+
conceptStatusDefinition,
|
|
8
|
+
entryStatusLabel,
|
|
9
|
+
entryStatusDefinition,
|
|
10
|
+
getPreferredTerm,
|
|
11
|
+
} from '../utils/concept-helpers';
|
|
12
|
+
import { ontology } from '../adapters/ontology-registry';
|
|
13
|
+
|
|
14
|
+
describe('conceptStatusColor', () => {
|
|
15
|
+
it('returns correct color for each concept status', () => {
|
|
16
|
+
expect(conceptStatusColor('valid')).toBe('badge-green');
|
|
17
|
+
expect(conceptStatusColor('superseded')).toBe('bg-red-50 text-red-700');
|
|
18
|
+
expect(conceptStatusColor('draft')).toBe('badge-yellow');
|
|
19
|
+
expect(conceptStatusColor('retired')).toBe('badge-gray');
|
|
20
|
+
expect(conceptStatusColor('invalid')).toBe('bg-red-50 text-red-700');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns badge-gray for null', () => {
|
|
24
|
+
expect(conceptStatusColor(null)).toBe('badge-gray');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns badge-gray for unknown status', () => {
|
|
28
|
+
expect(conceptStatusColor('nonexistent')).toBe('badge-gray');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('conceptStatusLabel', () => {
|
|
33
|
+
it('returns label for each concept status', () => {
|
|
34
|
+
expect(conceptStatusLabel('valid')).toBe('valid');
|
|
35
|
+
expect(conceptStatusLabel('superseded')).toBe('superseded');
|
|
36
|
+
expect(conceptStatusLabel('retired')).toBe('retired');
|
|
37
|
+
expect(conceptStatusLabel('draft')).toBe('draft');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns empty string for null', () => {
|
|
41
|
+
expect(conceptStatusLabel(null)).toBe('');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns raw id for unknown status', () => {
|
|
45
|
+
expect(conceptStatusLabel('unknown_status')).toBe('unknown_status');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('conceptStatusDefinition', () => {
|
|
50
|
+
it('returns definition for known statuses', () => {
|
|
51
|
+
const def = conceptStatusDefinition('valid');
|
|
52
|
+
expect(def).toBeTruthy();
|
|
53
|
+
expect(typeof def).toBe('string');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns null for null', () => {
|
|
57
|
+
expect(conceptStatusDefinition(null)).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('entryStatusColor', () => {
|
|
62
|
+
it('returns correct color for each entry status', () => {
|
|
63
|
+
expect(entryStatusColor('valid')).toBe('badge-green');
|
|
64
|
+
expect(entryStatusColor('not_valid')).toBe('bg-red-50 text-red-700');
|
|
65
|
+
expect(entryStatusColor('draft')).toBe('badge-yellow');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns badge-gray for unknown status', () => {
|
|
69
|
+
expect(entryStatusColor('nonexistent')).toBe('badge-gray');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('entryStatusLabel', () => {
|
|
74
|
+
it('returns label for each entry status', () => {
|
|
75
|
+
expect(entryStatusLabel('valid')).toBe('valid');
|
|
76
|
+
expect(entryStatusLabel('not_valid')).toBe('not valid');
|
|
77
|
+
expect(entryStatusLabel('draft')).toBe('draft');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('returns empty string for null', () => {
|
|
81
|
+
expect(entryStatusLabel(null)).toBe('');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('entryStatusDefinition', () => {
|
|
86
|
+
it('returns definition for known statuses', () => {
|
|
87
|
+
const def = entryStatusDefinition('valid');
|
|
88
|
+
expect(def).toBeTruthy();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns null for null', () => {
|
|
92
|
+
expect(entryStatusDefinition(null)).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('getPreferredTerm', () => {
|
|
97
|
+
it('returns primary designation when set', () => {
|
|
98
|
+
const lc = LocalizedConcept.fromJSON({
|
|
99
|
+
language_code: 'eng',
|
|
100
|
+
terms: [
|
|
101
|
+
{ designation: 'preferred term', type: 'expression', normative_status: 'preferred' },
|
|
102
|
+
{ designation: 'admitted term', type: 'expression', normative_status: 'admitted' },
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
expect(getPreferredTerm(lc)).toBe('preferred term');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns first term when no primary designation', () => {
|
|
109
|
+
const lc = LocalizedConcept.fromJSON({
|
|
110
|
+
language_code: 'eng',
|
|
111
|
+
terms: [{ designation: 'only term', type: 'expression' }],
|
|
112
|
+
});
|
|
113
|
+
expect(getPreferredTerm(lc)).toBe('only term');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns fallback for null', () => {
|
|
117
|
+
expect(getPreferredTerm(null)).toBe('—');
|
|
118
|
+
expect(getPreferredTerm(null, 'N/A')).toBe('N/A');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('returns fallback for undefined', () => {
|
|
122
|
+
expect(getPreferredTerm(undefined)).toBe('—');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('returns fallback when no terms', () => {
|
|
126
|
+
const lc = LocalizedConcept.fromJSON({ language_code: 'eng' });
|
|
127
|
+
expect(getPreferredTerm(lc)).toBe('—');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -171,6 +171,37 @@ describe('ConceptRef text bridge', () => {
|
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
+
// ── Robustness: matching by designation string, not array index ────────────
|
|
175
|
+
|
|
176
|
+
describe('designation target matching robustness', () => {
|
|
177
|
+
it('matches designation targets correctly even when raw terms are reordered', () => {
|
|
178
|
+
const doc = {
|
|
179
|
+
id: '1',
|
|
180
|
+
localizations: {
|
|
181
|
+
eng: {
|
|
182
|
+
language_code: 'eng',
|
|
183
|
+
terms: [
|
|
184
|
+
{ designation: 'PDF', type: 'abbreviation', related: [{ type: 'abbreviated_form_for', target: 'Portable Document Format' }] },
|
|
185
|
+
{ designation: 'ISO', type: 'abbreviation', related: [{ type: 'abbreviated_form_for', target: 'International Organization for Standardization' }] },
|
|
186
|
+
],
|
|
187
|
+
definition: [{ content: 'test' }],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
// Reverse the raw terms to simulate reordering
|
|
192
|
+
const rawTerms = doc.localizations.eng.terms.slice().reverse();
|
|
193
|
+
doc.localizations.eng.terms = rawTerms;
|
|
194
|
+
|
|
195
|
+
const concept = conceptFromJson(doc);
|
|
196
|
+
const lc = concept.localization('eng')!;
|
|
197
|
+
// Terms are in model order, not raw order
|
|
198
|
+
const pdf = lc.terms.find(t => t.designation === 'PDF')!;
|
|
199
|
+
const iso = lc.terms.find(t => t.designation === 'ISO')!;
|
|
200
|
+
expect(getDesignationTarget(pdf.related[0])).toBe('Portable Document Format');
|
|
201
|
+
expect(getDesignationTarget(iso.related[0])).toBe('International Organization for Standardization');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
174
205
|
// ── Generate-data serialization ────────────────────────────────────────────
|
|
175
206
|
|
|
176
207
|
describe('generate-data designation serialization', () => {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// @ts-nocheck — ESM .js import without type declarations
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { extractSourceRefs } from '../../scripts/extract-source-refs.js';
|
|
4
|
+
|
|
5
|
+
function makeConcept(sources: any[], localizedSources?: any) {
|
|
6
|
+
return {
|
|
7
|
+
'@id': 'https://glossarist.org/ds1/concept/1.1',
|
|
8
|
+
'gl:source': sources || [],
|
|
9
|
+
'gl:localizedConcept': localizedSources || {},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function makeSource(source: string, refFrom: string, type = 'authoritative') {
|
|
14
|
+
return {
|
|
15
|
+
'gl:origin': {
|
|
16
|
+
'gl:ref': { 'gl:source': source },
|
|
17
|
+
'gl:locality': { 'gl:type': type, 'gl:referenceFrom': refFrom },
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('extractSourceRefs', () => {
|
|
23
|
+
it('extracts source from managed concept-level gl:source', () => {
|
|
24
|
+
const concept = makeConcept([makeSource('OIML V2-200:2012', '2.2')]);
|
|
25
|
+
const result = extractSourceRefs(concept, 'viml-2022');
|
|
26
|
+
expect(result).toEqual([{ source: 'OIML V2-200:2012', registerId: 'viml-2022' }]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('extracts source from localized concept gl:source', () => {
|
|
30
|
+
const concept = makeConcept([], {
|
|
31
|
+
eng: { 'gl:source': [makeSource('ISO/IEC 17000:2020', '3.1')] },
|
|
32
|
+
});
|
|
33
|
+
const result = extractSourceRefs(concept, 'viml-2022');
|
|
34
|
+
expect(result).toEqual([{ source: 'ISO/IEC 17000:2020', registerId: 'viml-2022' }]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('extracts from both managed and localized sources', () => {
|
|
38
|
+
const concept = makeConcept(
|
|
39
|
+
[makeSource('OIML V2-200:2012', '2.2')],
|
|
40
|
+
{
|
|
41
|
+
eng: { 'gl:source': [makeSource('ISO/IEC 17000:2020', '3.1')] },
|
|
42
|
+
fra: { 'gl:source': [makeSource('ISO/CEI 17000:2020', '3.1')] },
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
const result = extractSourceRefs(concept, 'viml-2022');
|
|
46
|
+
expect(result).toEqual([
|
|
47
|
+
{ source: 'OIML V2-200:2012', registerId: 'viml-2022' },
|
|
48
|
+
{ source: 'ISO/IEC 17000:2020', registerId: 'viml-2022' },
|
|
49
|
+
{ source: 'ISO/CEI 17000:2020', registerId: 'viml-2022' },
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('deduplicates identical source strings', () => {
|
|
54
|
+
const concept = makeConcept(
|
|
55
|
+
[makeSource('VIM', '2.2')],
|
|
56
|
+
{ eng: { 'gl:source': [makeSource('VIM', '2.3')] } },
|
|
57
|
+
);
|
|
58
|
+
const result = extractSourceRefs(concept, 'viml-2022');
|
|
59
|
+
expect(result).toEqual([{ source: 'VIM', registerId: 'viml-2022' }]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns empty array when no sources present', () => {
|
|
63
|
+
const concept = makeConcept();
|
|
64
|
+
const result = extractSourceRefs(concept, 'viml-2022');
|
|
65
|
+
expect(result).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns empty array for concept with no gl:source field', () => {
|
|
69
|
+
const concept = { '@id': 'https://glossarist.org/ds1/concept/1.1' };
|
|
70
|
+
const result = extractSourceRefs(concept, 'viml-2022');
|
|
71
|
+
expect(result).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('skips sources without gl:origin', () => {
|
|
75
|
+
const concept = makeConcept([{ 'gl:ref': { 'gl:source': 'VIM' } }]);
|
|
76
|
+
const result = extractSourceRefs(concept, 'ds1');
|
|
77
|
+
expect(result).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('skips origins without gl:ref', () => {
|
|
81
|
+
const concept = makeConcept([{ 'gl:origin': { 'gl:locality': {} } }]);
|
|
82
|
+
const result = extractSourceRefs(concept, 'ds1');
|
|
83
|
+
expect(result).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('skips refs without gl:source', () => {
|
|
87
|
+
const concept = makeConcept([{ 'gl:origin': { 'gl:ref': { 'gl:id': '2.2' } } }]);
|
|
88
|
+
const result = extractSourceRefs(concept, 'ds1');
|
|
89
|
+
expect(result).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('handles concept with multiple localized languages', () => {
|
|
93
|
+
const concept = makeConcept([], {
|
|
94
|
+
eng: { 'gl:source': [makeSource('OIML V 1:2022', '0.01')] },
|
|
95
|
+
fra: { 'gl:source': [makeSource('OIML V 1:2022', '0.01')] },
|
|
96
|
+
});
|
|
97
|
+
const result = extractSourceRefs(concept, 'viml-2022');
|
|
98
|
+
expect(result).toEqual([{ source: 'OIML V 1:2022', registerId: 'viml-2022' }]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('preserves registerId from the caller, not the concept data', () => {
|
|
102
|
+
const concept = makeConcept([makeSource('VIM', '2.2')]);
|
|
103
|
+
const result = extractSourceRefs(concept, 'my-dataset');
|
|
104
|
+
expect(result[0].registerId).toBe('my-dataset');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('handles source strings with special characters', () => {
|
|
108
|
+
const concept = makeConcept([makeSource('OIML V 2:1993', '3.6')]);
|
|
109
|
+
const result = extractSourceRefs(concept, 'vim-1993');
|
|
110
|
+
expect(result).toEqual([{ source: 'OIML V 2:1993', registerId: 'vim-1993' }]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('handles source strings with whitespace variations', () => {
|
|
114
|
+
const concept = makeConcept([makeSource('ISO/ CEI 17000:2004', '3.1')]);
|
|
115
|
+
const result = extractSourceRefs(concept, 'viml-2013');
|
|
116
|
+
expect(result).toEqual([{ source: 'ISO/ CEI 17000:2004', registerId: 'viml-2013' }]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('extracts from multiple sources in a single concept', () => {
|
|
120
|
+
const concept = makeConcept([
|
|
121
|
+
makeSource('OIML V2-200:2012', '2.2'),
|
|
122
|
+
makeSource('ISO/IEC 17000:2020', '3.1'),
|
|
123
|
+
]);
|
|
124
|
+
const result = extractSourceRefs(concept, 'viml-2022');
|
|
125
|
+
expect(result).toEqual([
|
|
126
|
+
{ source: 'OIML V2-200:2012', registerId: 'viml-2022' },
|
|
127
|
+
{ source: 'ISO/IEC 17000:2020', registerId: 'viml-2022' },
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('extracts URN-based source strings', () => {
|
|
132
|
+
const concept = makeConcept([makeSource('urn:oiml:pub:v:2:2012', '2.2')]);
|
|
133
|
+
const result = extractSourceRefs(concept, 'viml-2022');
|
|
134
|
+
expect(result).toEqual([{ source: 'urn:oiml:pub:v:2:2012', registerId: 'viml-2022' }]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -56,8 +56,8 @@ describe('AdapterFactory — lazy discovery', () => {
|
|
|
56
56
|
expect(adapters[0].manifest).not.toBeNull();
|
|
57
57
|
expect(adapters[0].manifest!.title).toBe('Dataset 1');
|
|
58
58
|
expect(adapters[0].manifest!.conceptCount).toBe(100);
|
|
59
|
-
// Should NOT fetch manifest.json — only datasets.json
|
|
60
|
-
expect(mockFetch).toHaveBeenCalledTimes(
|
|
59
|
+
// Should NOT fetch manifest.json — only datasets.json
|
|
60
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
61
61
|
expect(mockFetch).toHaveBeenCalledWith('/datasets.json');
|
|
62
62
|
});
|
|
63
63
|
|
|
@@ -89,8 +89,8 @@ describe('AdapterFactory — lazy discovery', () => {
|
|
|
89
89
|
|
|
90
90
|
expect(adapters.length).toBe(1);
|
|
91
91
|
expect(adapters[0].manifest!.title).toBe('Full Dataset');
|
|
92
|
-
// Should fetch datasets.json
|
|
93
|
-
expect(mockFetch).toHaveBeenCalledTimes(
|
|
92
|
+
// Should fetch datasets.json and manifest.json
|
|
93
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
it('loads full manifest in loadDataset after summary discovery', async () => {
|
|
@@ -140,10 +140,10 @@ describe('AdapterFactory — lazy discovery', () => {
|
|
|
140
140
|
return Promise.resolve({ ok: false, status: 404 } as Response);
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
// Discover with summary —
|
|
143
|
+
// Discover with summary — only datasets.json fetched
|
|
144
144
|
const adapters = await factory.discoverDatasets('/datasets.json');
|
|
145
145
|
expect(adapters[0].manifest!.title).toBe('Summary Title');
|
|
146
|
-
expect(mockFetch).toHaveBeenCalledTimes(
|
|
146
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
147
147
|
|
|
148
148
|
// Load full dataset — fetches full manifest + index
|
|
149
149
|
const loaded = await factory.loadDataset('ds1');
|