@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.
Files changed (37) hide show
  1. package/package.json +2 -2
  2. package/scripts/build-edges.js +16 -8
  3. package/scripts/generate-data.mjs +284 -86
  4. package/src/__tests__/citation-display.test.ts +165 -3
  5. package/src/__tests__/cite-ref.test.ts +112 -0
  6. package/src/__tests__/concept-detail-interaction.test.ts +1 -5
  7. package/src/__tests__/{math.test.ts → content-renderer.test.ts} +113 -29
  8. package/src/__tests__/escape.test.ts +76 -0
  9. package/src/__tests__/graph-data-source.test.ts +155 -0
  10. package/src/__tests__/model-bridge-bridges.test.ts +150 -0
  11. package/src/__tests__/model-bridge-citation.test.ts +163 -0
  12. package/src/__tests__/reference-resolver-cite.test.ts +122 -0
  13. package/src/__tests__/reference-resolver.test.ts +12 -7
  14. package/src/__tests__/resolve-view.test.ts +1 -1
  15. package/src/__tests__/sidebar-nav-highlighting.test.ts +178 -0
  16. package/src/__tests__/source-refs.test.ts +9 -6
  17. package/src/__tests__/test-helpers.ts +20 -0
  18. package/src/__tests__/uri-router.test.ts +39 -12
  19. package/src/adapters/DatasetAdapter.ts +35 -143
  20. package/src/adapters/GraphDataSource.ts +178 -0
  21. package/src/adapters/ReferenceResolver.ts +101 -47
  22. package/src/adapters/UriRouter.ts +82 -10
  23. package/src/adapters/factory.ts +35 -28
  24. package/src/adapters/model-bridge.ts +121 -71
  25. package/src/adapters/types.ts +3 -0
  26. package/src/components/AppSidebar.vue +7 -4
  27. package/src/components/CitationDisplay.vue +86 -30
  28. package/src/components/ConceptDetail.vue +24 -126
  29. package/src/components/LanguageDetail.vue +6 -6
  30. package/src/composables/use-concept-content.ts +8 -8
  31. package/src/composables/use-ontology-nav.ts +129 -130
  32. package/src/composables/use-render-options.ts +1 -1
  33. package/src/graph/GraphEngine.ts +65 -0
  34. package/src/stores/vocabulary.ts +12 -73
  35. package/src/utils/content-renderer.ts +312 -0
  36. package/src/utils/markdown-lite.ts +2 -2
  37. package/src/utils/math.ts +0 -189
@@ -0,0 +1,155 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { GraphDataSource } from '../adapters/GraphDataSource';
3
+ import type { DatasetAdapter } from '../adapters/DatasetAdapter';
4
+ import type { Concept, RelatedConcept } from 'glossarist';
5
+ import { conceptFromJson } from '../adapters/model-bridge';
6
+
7
+ function makeMinimalConcept(overrides: Record<string, unknown> = {}): Concept {
8
+ return conceptFromJson({
9
+ '@type': 'skos:Concept',
10
+ '@id': 'https://glossarist.org/test/concept/1',
11
+ 'gl:identifier': '1',
12
+ 'gl:localizedConcept': {
13
+ eng: {
14
+ 'gl:languageCode': 'eng',
15
+ 'gl:entryStatus': 'valid',
16
+ 'gl:designation': [{ '@type': 'Expression', 'gl:term': 'test term' }],
17
+ 'gl:definition': [{ 'gl:content': 'test definition' }],
18
+ },
19
+ },
20
+ ...overrides,
21
+ });
22
+ }
23
+
24
+ function makeAdapterStub(manifest?: Record<string, unknown>): DatasetAdapter {
25
+ return {
26
+ registerId: 'test',
27
+ dataUrl: '/data/test',
28
+ manifest: manifest ? { uriBase: 'https://glossarist.org', ...manifest } as any : { uriBase: 'https://glossarist.org' } as any,
29
+ urnMap: new Map([['urn:iso:std:iso:10241', 'test']]),
30
+ } as unknown as DatasetAdapter;
31
+ }
32
+
33
+ describe('GraphDataSource', () => {
34
+ describe('resolveRefTarget', () => {
35
+ it('returns empty string when ref is null', async () => {
36
+ const { GraphDataSource } = await import('../adapters/GraphDataSource');
37
+ const ds = new GraphDataSource(makeAdapterStub());
38
+ const concept = makeMinimalConcept();
39
+ // concept.relatedConcepts is empty, so extractEdges returns []
40
+ const edges = ds.extractEdges(concept);
41
+ expect(edges).toEqual([]);
42
+ });
43
+ });
44
+
45
+ describe('extractEdges', () => {
46
+ it('extracts concept-level related concept edges', () => {
47
+ const ds = new GraphDataSource(makeAdapterStub());
48
+ const concept = makeMinimalConcept({
49
+ 'gl:related': [{
50
+ 'gl:relationshipType': 'references',
51
+ 'gl:ref': { 'gl:source': 'urn:iso:std:iso:10241', 'gl:id': '2.1' },
52
+ }],
53
+ });
54
+ const edges = ds.extractEdges(concept);
55
+ expect(edges.length).toBeGreaterThan(0);
56
+ expect(edges[0].type).toBe('references');
57
+ expect(edges[0].source).toContain('/concept/1');
58
+ });
59
+
60
+ it('extracts localization-level related concept edges', () => {
61
+ const ds = new GraphDataSource(makeAdapterStub());
62
+ const concept = makeMinimalConcept({
63
+ 'gl:localizedConcept': {
64
+ eng: {
65
+ 'gl:languageCode': 'eng',
66
+ 'gl:entryStatus': 'valid',
67
+ 'gl:designation': [{ '@type': 'Expression', 'gl:term': 'test term' }],
68
+ 'gl:references': [{
69
+ 'gl:relationshipType': 'related',
70
+ 'gl:ref': { 'gl:source': 'urn:iso:std:iso:10241', 'gl:id': '3.1' },
71
+ }],
72
+ },
73
+ },
74
+ });
75
+ const edges = ds.extractEdges(concept);
76
+ expect(edges.length).toBeGreaterThan(0);
77
+ const relatedEdge = edges.find(e => e.type === 'related');
78
+ expect(relatedEdge).toBeDefined();
79
+ expect(relatedEdge!.lang).toBe('eng');
80
+ });
81
+
82
+ it('skips self-referencing edges', () => {
83
+ const ds = new GraphDataSource(makeAdapterStub());
84
+ const concept = makeMinimalConcept({
85
+ 'gl:related': [{
86
+ 'gl:relationshipType': 'references',
87
+ 'gl:ref': { 'gl:source': 'https://glossarist.org', 'gl:id': '1' },
88
+ '@id': 'https://glossarist.org/test/concept/1',
89
+ }],
90
+ });
91
+ const edges = ds.extractEdges(concept);
92
+ // Self-reference should be filtered out
93
+ expect(edges).toEqual([]);
94
+ });
95
+ });
96
+
97
+ describe('extractDomainEdges', () => {
98
+ it('creates domain edges from localized domains', () => {
99
+ const ds = new GraphDataSource(makeAdapterStub());
100
+ const concept = makeMinimalConcept({
101
+ 'gl:localizedConcept': {
102
+ eng: {
103
+ 'gl:languageCode': 'eng',
104
+ 'gl:entryStatus': 'valid',
105
+ 'gl:designation': [{ '@type': 'Expression', 'gl:term': 'test term' }],
106
+ 'gl:domain': 'Metrology',
107
+ },
108
+ fra: {
109
+ 'gl:languageCode': 'fra',
110
+ 'gl:entryStatus': 'valid',
111
+ 'gl:designation': [{ '@type': 'Expression', 'gl:term': 'test terme' }],
112
+ 'gl:domain': 'Métrologie',
113
+ },
114
+ },
115
+ });
116
+ const edges = ds.extractDomainEdges(concept);
117
+ expect(edges.length).toBe(2);
118
+ expect(edges[0].type).toBe('domain');
119
+ expect(edges[0].label).toBe('Metrology');
120
+ expect(edges[0].lang).toBe('eng');
121
+ expect(edges[1].label).toBe('Métrologie');
122
+ expect(edges[1].lang).toBe('fra');
123
+ });
124
+
125
+ it('returns empty for concepts without domains', () => {
126
+ const ds = new GraphDataSource(makeAdapterStub());
127
+ const concept = makeMinimalConcept();
128
+ const edges = ds.extractDomainEdges(concept);
129
+ expect(edges).toEqual([]);
130
+ });
131
+ });
132
+
133
+ describe('getSectionTree', () => {
134
+ it('returns empty when manifest has no sections', () => {
135
+ const ds = new GraphDataSource(makeAdapterStub());
136
+ expect(ds.getSectionTree()).toEqual([]);
137
+ });
138
+
139
+ it('maps manifest sections to section tree', () => {
140
+ const ds = new GraphDataSource(makeAdapterStub({
141
+ sections: [
142
+ { id: '1', names: { eng: 'Section 1' }, children: [
143
+ { id: '1.1', names: { eng: 'Section 1.1' } },
144
+ ]},
145
+ ],
146
+ }));
147
+ const tree = ds.getSectionTree();
148
+ expect(tree.length).toBe(1);
149
+ expect(tree[0].id).toBe('1');
150
+ expect(tree[0].names.eng).toBe('Section 1');
151
+ expect(tree[0].children?.length).toBe(1);
152
+ expect(tree[0].children![0].id).toBe('1.1');
153
+ });
154
+ });
155
+ });
@@ -0,0 +1,150 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { conceptFromJson, getRelatedSourceId, getRelatedCitation } from '../adapters/model-bridge';
3
+
4
+ describe('model-bridge — mapRefFromJsonLd', () => {
5
+ it('maps string ref to { source }', () => {
6
+ const doc = {
7
+ '@type': 'gl:Concept',
8
+ '@id': 'https://example.com/reg/concept/1',
9
+ 'gl:identifier': '1',
10
+ 'gl:localizedConcept': {
11
+ eng: {
12
+ '@type': 'gl:LocalizedConcept',
13
+ 'gl:languageCode': 'eng',
14
+ 'gl:source': [{
15
+ '@type': 'gl:ConceptSource',
16
+ 'gl:sourceType': 'authoritative',
17
+ 'gl:origin': {
18
+ '@type': 'gl:Citation',
19
+ 'gl:ref': 'ISO 9000:2015',
20
+ },
21
+ }],
22
+ },
23
+ },
24
+ };
25
+
26
+ const concept = conceptFromJson(doc);
27
+ const sources = concept.localization('eng')!.sources;
28
+ expect(sources[0].origin!.ref!.source).toBe('ISO 9000:2015');
29
+ });
30
+
31
+ it('prefers gl:-prefixed keys over unprefixed keys', () => {
32
+ const doc = {
33
+ '@type': 'gl:Concept',
34
+ '@id': 'https://example.com/reg/concept/1',
35
+ 'gl:identifier': '1',
36
+ 'gl:localizedConcept': {
37
+ eng: {
38
+ '@type': 'gl:LocalizedConcept',
39
+ 'gl:languageCode': 'eng',
40
+ 'gl:source': [{
41
+ '@type': 'gl:ConceptSource',
42
+ 'gl:origin': {
43
+ '@type': 'gl:Citation',
44
+ 'gl:ref': {
45
+ 'gl:source': 'gl-source',
46
+ 'source': 'plain-source',
47
+ 'gl:id': 'gl-id',
48
+ 'id': 'plain-id',
49
+ },
50
+ },
51
+ }],
52
+ },
53
+ },
54
+ };
55
+
56
+ const concept = conceptFromJson(doc);
57
+ const sources = concept.localization('eng')!.sources;
58
+ expect(sources[0].origin!.ref!.source).toBe('gl-source');
59
+ expect(sources[0].origin!.ref!.id).toBe('gl-id');
60
+ });
61
+ });
62
+
63
+ describe('model-bridge — locality mapping round-trip', () => {
64
+ it('maps gl:referenceFrom to Locality instance with correct getters', () => {
65
+ const doc = {
66
+ '@type': 'gl:Concept',
67
+ '@id': 'https://example.com/reg/concept/1',
68
+ 'gl:identifier': '1',
69
+ 'gl:localizedConcept': {
70
+ eng: {
71
+ '@type': 'gl:LocalizedConcept',
72
+ 'gl:languageCode': 'eng',
73
+ 'gl:source': [{
74
+ '@type': 'gl:ConceptSource',
75
+ 'gl:origin': {
76
+ '@type': 'gl:Citation',
77
+ 'gl:ref': { 'gl:source': 'ISO 9000' },
78
+ 'gl:locality': {
79
+ 'gl:localityType': 'clause',
80
+ 'gl:referenceFrom': '3.1',
81
+ 'gl:referenceTo': '3.5',
82
+ },
83
+ },
84
+ }],
85
+ },
86
+ },
87
+ };
88
+
89
+ const concept = conceptFromJson(doc);
90
+ const origin = concept.localization('eng')!.sources[0].origin!;
91
+ const loc = origin.locality!;
92
+ // Locality model uses camelCase getters, toJSON() produces snake_case
93
+ expect(loc.type).toBe('clause');
94
+ expect(loc.referenceFrom).toBe('3.1');
95
+ expect(loc.referenceTo).toBe('3.5');
96
+ // Verify serialization is snake_case
97
+ const json = loc.toJSON();
98
+ expect(json.reference_from).toBe('3.1');
99
+ expect(json.reference_to).toBe('3.5');
100
+ });
101
+ });
102
+
103
+ describe('model-bridge — citation bridge round-trip', () => {
104
+ it('preserves sourceId and citation through JSON-LD round-trip', () => {
105
+ const doc = {
106
+ '@type': 'gl:Concept',
107
+ '@id': 'https://example.com/reg/concept/1',
108
+ 'gl:identifier': '1',
109
+ 'gl:localizedConcept': {
110
+ eng: {
111
+ '@type': 'gl:LocalizedConcept',
112
+ 'gl:languageCode': 'eng',
113
+ 'gl:references': [{
114
+ '@id': 'cite:vim-2.2',
115
+ 'gl:term': 'entity',
116
+ 'gl:sourceId': 'vim-2.2',
117
+ 'gl:citation': {
118
+ 'gl:ref': {
119
+ '@type': 'gl:Ref',
120
+ 'gl:source': 'OIML V2-200:2012',
121
+ 'gl:id': '2.2',
122
+ },
123
+ 'gl:locality': {
124
+ 'gl:localityType': 'definition',
125
+ 'gl:referenceFrom': '2.2',
126
+ },
127
+ 'gl:link': 'https://example.com/vim',
128
+ },
129
+ }],
130
+ },
131
+ },
132
+ };
133
+
134
+ const concept = conceptFromJson(doc);
135
+ const lc = concept.localization('eng')!;
136
+ const rc = lc.related[0];
137
+
138
+ // All fields preserved through the bridge
139
+ expect(rc.content).toBe('entity');
140
+ expect(getRelatedSourceId(rc)).toBe('vim-2.2');
141
+
142
+ const citation = getRelatedCitation(rc)!;
143
+ expect((citation.ref as any).source).toBe('OIML V2-200:2012');
144
+ expect((citation.ref as any).id).toBe('2.2');
145
+ // Citation locality is stored as raw dict (snake_case from mapLocalityFromJsonLd)
146
+ expect((citation.locality as any).type).toBe('definition');
147
+ expect((citation.locality as any).reference_from).toBe('2.2');
148
+ expect(citation.link).toBe('https://example.com/vim');
149
+ });
150
+ });
@@ -0,0 +1,163 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { conceptFromJson, getRelatedSourceId, getRelatedCitation } from '../adapters/model-bridge';
3
+
4
+ describe('model-bridge — source id mapping', () => {
5
+ it('maps gl:id from JSON-LD ConceptSource', () => {
6
+ const doc = {
7
+ '@type': 'gl:Concept',
8
+ '@id': 'https://example.com/reg/concept/1',
9
+ 'gl:identifier': '1',
10
+ 'gl:localizedConcept': {
11
+ eng: {
12
+ '@type': 'gl:LocalizedConcept',
13
+ 'gl:languageCode': 'eng',
14
+ 'gl:source': [{
15
+ '@type': 'gl:ConceptSource',
16
+ 'gl:id': 'iso-10303-2-def',
17
+ 'gl:sourceType': 'authoritative',
18
+ 'gl:origin': {
19
+ '@type': 'gl:Citation',
20
+ 'gl:ref': {
21
+ '@type': 'gl:Ref',
22
+ 'gl:source': 'ISO 10303-2',
23
+ 'gl:id': '3.1.1',
24
+ },
25
+ },
26
+ }],
27
+ },
28
+ },
29
+ };
30
+
31
+ const concept = conceptFromJson(doc);
32
+ const sources = concept.localization('eng')!.sources;
33
+ expect(sources).toHaveLength(1);
34
+ expect((sources[0] as any).id).toBe('iso-10303-2-def');
35
+ expect(sources[0].origin!.ref!.source).toBe('ISO 10303-2');
36
+ expect(sources[0].origin!.ref!.id).toBe('3.1.1');
37
+ });
38
+ });
39
+
40
+ describe('model-bridge — reference citation mapping via WeakMap bridges', () => {
41
+ it('maps gl:sourceId from JSON-LD reference', () => {
42
+ const doc = {
43
+ '@type': 'gl:Concept',
44
+ '@id': 'https://example.com/reg/concept/1',
45
+ 'gl:identifier': '1',
46
+ 'gl:localizedConcept': {
47
+ eng: {
48
+ '@type': 'gl:LocalizedConcept',
49
+ 'gl:languageCode': 'eng',
50
+ 'gl:references': [{
51
+ '@id': 'cite:iso-10303-2-def',
52
+ 'gl:term': 'entity data type',
53
+ 'gl:sourceId': 'iso-10303-2-def',
54
+ }],
55
+ },
56
+ },
57
+ };
58
+
59
+ const concept = conceptFromJson(doc);
60
+ const lc = concept.localization('eng')!;
61
+ const related = lc.related;
62
+ expect(related).toHaveLength(1);
63
+ expect(related[0].content).toBe('entity data type');
64
+ expect(getRelatedSourceId(related[0])).toBe('iso-10303-2-def');
65
+ });
66
+
67
+ it('maps gl:citation from JSON-LD reference', () => {
68
+ const doc = {
69
+ '@type': 'gl:Concept',
70
+ '@id': 'https://example.com/reg/concept/1',
71
+ 'gl:identifier': '1',
72
+ 'gl:localizedConcept': {
73
+ eng: {
74
+ '@type': 'gl:LocalizedConcept',
75
+ 'gl:languageCode': 'eng',
76
+ 'gl:references': [{
77
+ '@id': 'cite:vim-def',
78
+ 'gl:term': 'entity',
79
+ 'gl:sourceId': 'vim-def',
80
+ 'gl:citation': {
81
+ 'gl:ref': {
82
+ '@type': 'gl:Ref',
83
+ 'gl:source': 'OIML V2-200:2012',
84
+ 'gl:id': '2.2',
85
+ },
86
+ 'gl:locality': {
87
+ 'gl:localityType': 'clause',
88
+ 'gl:referenceFrom': '2.2',
89
+ },
90
+ },
91
+ }],
92
+ },
93
+ },
94
+ };
95
+
96
+ const concept = conceptFromJson(doc);
97
+ const lc = concept.localization('eng')!;
98
+ const related = lc.related;
99
+ expect(related).toHaveLength(1);
100
+
101
+ const citation = getRelatedCitation(related[0]);
102
+ expect(citation).toBeDefined();
103
+ expect((citation!.ref as any).source).toBe('OIML V2-200:2012');
104
+ expect((citation!.ref as any).id).toBe('2.2');
105
+ expect((citation!.locality as any).type).toBe('clause');
106
+ expect((citation!.locality as any).reference_from).toBe('2.2');
107
+ });
108
+
109
+ it('maps gl:citation with link', () => {
110
+ const doc = {
111
+ '@type': 'gl:Concept',
112
+ '@id': 'https://example.com/reg/concept/1',
113
+ 'gl:identifier': '1',
114
+ 'gl:localizedConcept': {
115
+ eng: {
116
+ '@type': 'gl:LocalizedConcept',
117
+ 'gl:languageCode': 'eng',
118
+ 'gl:references': [{
119
+ '@id': 'cite:iso-9000',
120
+ 'gl:term': 'ISO 9000',
121
+ 'gl:sourceId': 'iso-9000',
122
+ 'gl:citation': {
123
+ 'gl:ref': {
124
+ '@type': 'gl:Ref',
125
+ 'gl:source': 'ISO 9000:2015',
126
+ },
127
+ 'gl:link': 'https://iso.org/standard/62085.html',
128
+ },
129
+ }],
130
+ },
131
+ },
132
+ };
133
+
134
+ const concept = conceptFromJson(doc);
135
+ const lc = concept.localization('eng')!;
136
+ const related = lc.related;
137
+ const citation = getRelatedCitation(related[0]);
138
+ expect(citation!.link).toBe('https://iso.org/standard/62085.html');
139
+ });
140
+
141
+ it('returns null for RelatedConcept without bridged fields', () => {
142
+ const doc = {
143
+ '@type': 'gl:Concept',
144
+ '@id': 'https://example.com/reg/concept/1',
145
+ 'gl:identifier': '1',
146
+ 'gl:localizedConcept': {
147
+ eng: {
148
+ '@type': 'gl:LocalizedConcept',
149
+ 'gl:languageCode': 'eng',
150
+ 'gl:references': [{
151
+ '@id': 'https://example.com/reg/concept/2',
152
+ 'gl:term': 'other concept',
153
+ }],
154
+ },
155
+ },
156
+ };
157
+
158
+ const concept = conceptFromJson(doc);
159
+ const lc = concept.localization('eng')!;
160
+ expect(getRelatedSourceId(lc.related[0])).toBeNull();
161
+ expect(getRelatedCitation(lc.related[0])).toBeNull();
162
+ });
163
+ });
@@ -0,0 +1,122 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { createResolverPair } from './test-helpers';
3
+
4
+ describe('ReferenceResolver — resolveCite edge cases', () => {
5
+ let resolver: ReturnType<typeof createResolverPair>['resolver'];
6
+ let uriRouter: ReturnType<typeof createResolverPair>['uriRouter'];
7
+
8
+ beforeEach(() => {
9
+ const pair = createResolverPair();
10
+ resolver = pair.resolver;
11
+ uriRouter = pair.uriRouter;
12
+ uriRouter.registerDataset('vim-2012', '', '', ['urn:oiml:pub:v:2:2012*']);
13
+ resolver.registerSourceRef('VIM', 'vim-2012', 'urn:oiml:pub:v:2:2012');
14
+ });
15
+
16
+ it('handles citation with camelCase locality (backward compat)', () => {
17
+ const result = resolver.resolveCite({
18
+ ref: { source: 'VIM' },
19
+ locality: { referenceFrom: '2.2' } as any,
20
+ });
21
+ expect(result.classification).toBe('internal-citation');
22
+ expect(result.resolved).toEqual({ registerId: 'vim-2012', conceptId: '2.2' });
23
+ });
24
+
25
+ it('handles citation with snake_case locality', () => {
26
+ const result = resolver.resolveCite({
27
+ ref: { source: 'VIM' },
28
+ locality: { reference_from: '2.2' },
29
+ });
30
+ expect(result.classification).toBe('internal-citation');
31
+ expect(result.resolved).toEqual({ registerId: 'vim-2012', conceptId: '2.2' });
32
+ });
33
+
34
+ it('resolves source without locality to empty conceptId (document-level)', () => {
35
+ // When no locality is provided, the resolver resolves to the source document itself
36
+ const result = resolver.resolveCite({
37
+ ref: { source: 'VIM' },
38
+ locality: null,
39
+ });
40
+ expect(result.classification).toBe('internal-citation');
41
+ expect(result.resolved).not.toBeNull();
42
+ expect(result.resolved!.registerId).toBe('vim-2012');
43
+ });
44
+
45
+ it('handles citation with null ref source', () => {
46
+ const result = resolver.resolveCite({
47
+ ref: { source: null, id: '3.1' },
48
+ locality: null,
49
+ });
50
+ expect(result.classification).toBe('unresolved-citation');
51
+ });
52
+
53
+ it('handles undefined citation', () => {
54
+ const result = resolver.resolveCite(undefined as any);
55
+ expect(result.classification).toBe('unresolved-citation');
56
+ expect(result.resolved).toBeNull();
57
+ });
58
+
59
+ it('classification and resolved always agree — internal implies non-null resolved', () => {
60
+ // If classification is internal-citation, resolved must be non-null
61
+ const internal = resolver.resolveCite({
62
+ ref: { source: 'VIM' },
63
+ locality: { reference_from: '2.2' },
64
+ });
65
+ if (internal.classification === 'internal-citation') {
66
+ expect(internal.resolved).not.toBeNull();
67
+ }
68
+ });
69
+
70
+ it('classification and resolved always agree — non-internal implies null resolved', () => {
71
+ const external = resolver.resolveCite({
72
+ ref: { source: 'Unknown' },
73
+ locality: null,
74
+ });
75
+ if (external.classification !== 'internal-citation') {
76
+ expect(external.resolved).toBeNull();
77
+ }
78
+ });
79
+
80
+ it('resolves with sourceDatasetId for cross-dataset detection', () => {
81
+ const result = resolver.resolveCite(
82
+ { ref: { source: 'VIM' }, locality: { reference_from: '2.2' } },
83
+ 'vim-2012',
84
+ );
85
+ // Same dataset — still resolves internally
86
+ expect(result.classification).toBe('internal-citation');
87
+ expect(result.resolved!.registerId).toBe('vim-2012');
88
+ });
89
+ });
90
+
91
+ describe('ReferenceResolver — URI pattern matching', () => {
92
+ it('matches URN patterns with wildcard', () => {
93
+ const { resolver, uriRouter } = createResolverPair();
94
+ uriRouter.registerDataset('iso-10303', '', '', ['urn:iso:std:iso:10303:*']);
95
+
96
+ const result = resolver.resolveReference('urn:iso:std:iso:10303:3.1.1.1');
97
+ expect(result.type).toBe('internal');
98
+ if (result.type === 'internal') {
99
+ expect(result.registerId).toBe('iso-10303');
100
+ expect(result.conceptId).toBe('3.1.1.1');
101
+ }
102
+ });
103
+
104
+ it('matches HTTPS patterns with wildcard', () => {
105
+ const { resolver, uriRouter } = createResolverPair();
106
+ uriRouter.registerDataset('iev', '', '', ['https://glossarist.org/iev/*']);
107
+
108
+ const result = resolver.resolveReference('https://glossarist.org/iev/concept/103-01-02');
109
+ expect(result.type).toBe('internal');
110
+ if (result.type === 'internal') {
111
+ expect(result.registerId).toBe('iev');
112
+ expect(result.conceptId).toBe('103-01-02');
113
+ }
114
+ });
115
+
116
+ it('returns unresolved for unknown URIs', () => {
117
+ const { resolver } = createResolverPair();
118
+ const result = resolver.resolveReference('https://unknown.example.com/concept/1');
119
+ expect(result.type).toBe('unresolved');
120
+ });
121
+
122
+ });
@@ -1,16 +1,21 @@
1
1
  import { describe, it, expect, beforeEach } from 'vitest';
2
- import { ReferenceResolver } from '../adapters/ReferenceResolver';
2
+ import { createResolverPair } from './test-helpers';
3
+ import type { ReferenceResolver } from '../adapters/ReferenceResolver';
4
+ import type { UriRouter } from '../adapters/UriRouter';
3
5
 
4
6
  describe('ReferenceResolver', () => {
5
7
  let resolver: ReferenceResolver;
8
+ let uriRouter: UriRouter;
6
9
 
7
10
  beforeEach(() => {
8
- resolver = new ReferenceResolver();
11
+ const pair = createResolverPair();
12
+ resolver = pair.resolver;
13
+ uriRouter = pair.uriRouter;
9
14
  });
10
15
 
11
16
  describe('resolveReference', () => {
12
17
  it('resolves internal URI for provided dataset', () => {
13
- resolver.registerDataset('isotc204', ['urn:iso:std:iso:14812:*', 'https://glossarist.org/isotc204/*']);
18
+ uriRouter.registerDataset('isotc204', '', '', ['urn:iso:std:iso:14812:*', 'https://glossarist.org/isotc204/*']);
14
19
  const result = resolver.resolveReference('https://glossarist.org/isotc204/concept/3.1.1.1');
15
20
  expect(result).toEqual({
16
21
  type: 'internal',
@@ -21,7 +26,7 @@ describe('ReferenceResolver', () => {
21
26
  });
22
27
 
23
28
  it('resolves URN to internal for provided dataset', () => {
24
- resolver.registerDataset('isotc204', ['urn:iso:std:iso:14812:*']);
29
+ uriRouter.registerDataset('isotc204', '', '', ['urn:iso:std:iso:14812:*']);
25
30
  const result = resolver.resolveReference('urn:iso:std:iso:14812:3.1.1.1');
26
31
  expect(result).toEqual({
27
32
  type: 'internal',
@@ -32,8 +37,8 @@ describe('ReferenceResolver', () => {
32
37
  });
33
38
 
34
39
  it('sets crossDataset=true when source dataset differs', () => {
35
- resolver.registerDataset('iev', ['urn:iec:std:iec:60050:*']);
36
- resolver.registerDataset('isotc204', ['urn:iso:std:iso:14812:*']);
40
+ uriRouter.registerDataset('iev', '', '', ['urn:iec:std:iec:60050:*']);
41
+ uriRouter.registerDataset('isotc204', '', '', ['urn:iso:std:iso:14812:*']);
37
42
  const result = resolver.resolveReference('urn:iec:std:iec:60050:103-01-02', 'isotc204');
38
43
  expect(result).toEqual({
39
44
  type: 'internal',
@@ -96,7 +101,7 @@ describe('ReferenceResolver', () => {
96
101
  });
97
102
 
98
103
  it('prefers provided dataset over routing', () => {
99
- resolver.registerDataset('iev', ['urn:iec:std:iec:60050:*']);
104
+ uriRouter.registerDataset('iev', '', '', ['urn:iec:std:iec:60050:*']);
100
105
  resolver.loadRouting([
101
106
  {
102
107
  uri: 'urn:iec:std:iec:60050:*',
@@ -68,7 +68,7 @@ describe('ResolveView', () => {
68
68
 
69
69
  it('resolves the URI via the factory resolver', async () => {
70
70
  const factory = getFactory();
71
- factory.resolver.registerDataset('test', ['https://glossarist.org/test/*']);
71
+ factory.uriRouter.registerDataset('test', '', '', ['https://glossarist.org/test/*']);
72
72
  const resolution = factory.resolve(TEST_URI);
73
73
  expect(resolution.type).toBe('internal');
74
74
  expect(resolution).toHaveProperty('registerId', 'test');