@glossarist/concept-browser 0.1.0

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 (68) hide show
  1. package/README.md +319 -0
  2. package/cli/index.mjs +119 -0
  3. package/env.d.ts +7 -0
  4. package/index.html +16 -0
  5. package/package.json +78 -0
  6. package/postcss.config.js +6 -0
  7. package/scripts/build-edges.js +112 -0
  8. package/scripts/fetch-datasets.mjs +195 -0
  9. package/scripts/generate-404.js +15 -0
  10. package/scripts/generate-data.mjs +606 -0
  11. package/scripts/load-site-config.mjs +56 -0
  12. package/src/App.vue +98 -0
  13. package/src/__tests__/data-integration.test.ts +135 -0
  14. package/src/__tests__/data-integrity.test.ts +101 -0
  15. package/src/__tests__/dataset-adapter.test.ts +336 -0
  16. package/src/__tests__/dataset-style.test.ts +37 -0
  17. package/src/__tests__/graph.test.ts +187 -0
  18. package/src/__tests__/lang.test.ts +29 -0
  19. package/src/__tests__/math.test.ts +113 -0
  20. package/src/__tests__/reference-resolver.test.ts +122 -0
  21. package/src/__tests__/site-config.test.ts +52 -0
  22. package/src/__tests__/uri-router.test.ts +76 -0
  23. package/src/adapters/DatasetAdapter.ts +270 -0
  24. package/src/adapters/ReferenceResolver.ts +95 -0
  25. package/src/adapters/UriRouter.ts +41 -0
  26. package/src/adapters/factory.ts +78 -0
  27. package/src/adapters/types.ts +162 -0
  28. package/src/components/AppHeader.vue +99 -0
  29. package/src/components/AppSidebar.vue +133 -0
  30. package/src/components/ConceptCard.vue +65 -0
  31. package/src/components/ConceptDetail.vue +540 -0
  32. package/src/components/ConceptTimeline.vue +410 -0
  33. package/src/components/FormatDownloads.vue +46 -0
  34. package/src/components/GraphPanel.vue +499 -0
  35. package/src/components/LanguageDetail.vue +211 -0
  36. package/src/components/NavIcon.vue +20 -0
  37. package/src/components/SearchBar.vue +241 -0
  38. package/src/composables/use-dataset-loader.ts +27 -0
  39. package/src/config/types.ts +130 -0
  40. package/src/config/use-site-config.ts +144 -0
  41. package/src/graph/GraphEngine.ts +137 -0
  42. package/src/graph/index.ts +1 -0
  43. package/src/main.ts +11 -0
  44. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  45. package/src/router/index.ts +43 -0
  46. package/src/router/page-routes.ts +35 -0
  47. package/src/stores/ui.ts +59 -0
  48. package/src/stores/vocabulary.ts +309 -0
  49. package/src/style.css +314 -0
  50. package/src/utils/asciidoc-lite.ts +123 -0
  51. package/src/utils/concept-formats.ts +157 -0
  52. package/src/utils/dataset-style.ts +54 -0
  53. package/src/utils/index.ts +1 -0
  54. package/src/utils/lang.ts +32 -0
  55. package/src/utils/math.ts +100 -0
  56. package/src/views/AboutView.vue +122 -0
  57. package/src/views/ConceptView.vue +119 -0
  58. package/src/views/ContributorsView.vue +110 -0
  59. package/src/views/DatasetView.vue +249 -0
  60. package/src/views/GraphView.vue +65 -0
  61. package/src/views/HomeView.vue +168 -0
  62. package/src/views/NewsView.vue +146 -0
  63. package/src/views/ResolveView.vue +63 -0
  64. package/src/views/SearchView.vue +33 -0
  65. package/src/views/StatsView.vue +121 -0
  66. package/tailwind.config.js +43 -0
  67. package/tsconfig.json +24 -0
  68. package/vite.config.ts +27 -0
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { makeDsStyle, paletteColor } from '../utils/dataset-style';
3
+
4
+ describe('makeDsStyle', () => {
5
+ it('returns color, light, and dark variants', () => {
6
+ const style = makeDsStyle('#3366ff');
7
+ expect(style.color).toBe('#3366ff');
8
+ expect(style.light).toMatch(/^rgba\(51, 102, 255, 0\.1\)$/);
9
+ expect(style.dark).toMatch(/^rgba\(51, 102, 255, 0\.85\)$/);
10
+ });
11
+
12
+ it('light variant has alpha 0.1', () => {
13
+ const style = makeDsStyle('#d97706');
14
+ expect(style.light).toContain('0.1)');
15
+ });
16
+
17
+ it('dark variant has alpha 0.85', () => {
18
+ const style = makeDsStyle('#d97706');
19
+ expect(style.dark).toContain('0.85)');
20
+ });
21
+ });
22
+
23
+ describe('paletteColor', () => {
24
+ it('returns correct color for index 0', () => {
25
+ expect(paletteColor(0)).toBe('#3366ff');
26
+ });
27
+
28
+ it('returns correct color for index 3', () => {
29
+ expect(paletteColor(3)).toBe('#8b5cf6');
30
+ });
31
+
32
+ it('cycles through palette for indices beyond length', () => {
33
+ // Palette has 12 entries (indices 0-11), so index 12 wraps to 0
34
+ expect(paletteColor(12)).toBe(paletteColor(0));
35
+ expect(paletteColor(13)).toBe(paletteColor(1));
36
+ });
37
+ });
@@ -0,0 +1,187 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { GraphEngine } from '../graph/GraphEngine';
3
+ import type { GraphNode, GraphEdge } from '../adapters/types';
4
+
5
+ function makeNode(uri: string, conceptId: string, register = 'test', overrides?: Partial<GraphNode>): GraphNode {
6
+ return {
7
+ uri,
8
+ register,
9
+ conceptId,
10
+ designations: { eng: conceptId },
11
+ status: 'valid',
12
+ loaded: true,
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ function makeEdge(source: string, target: string, type = 'references', register = 'test'): GraphEdge {
18
+ return { source, target, type, register };
19
+ }
20
+
21
+ describe('GraphEngine', () => {
22
+ it('starts empty', () => {
23
+ const g = new GraphEngine();
24
+ expect(g.nodeCount).toBe(0);
25
+ expect(g.edgeCount).toBe(0);
26
+ expect(g.getAllNodes()).toEqual([]);
27
+ expect(g.getEdges()).toEqual([]);
28
+ });
29
+
30
+ describe('addNode', () => {
31
+ it('adds a node that can be retrieved', () => {
32
+ const g = new GraphEngine();
33
+ const node = makeNode('uri:a', 'a');
34
+ g.addNode(node);
35
+ expect(g.nodeCount).toBe(1);
36
+ expect(g.getNode('uri:a')).toEqual(node);
37
+ });
38
+
39
+ it('does not duplicate nodes with the same URI', () => {
40
+ const g = new GraphEngine();
41
+ g.addNode(makeNode('uri:a', 'a'));
42
+ g.addNode(makeNode('uri:a', 'a-updated'));
43
+ expect(g.nodeCount).toBe(1);
44
+ // Original node is kept (not overwritten)
45
+ expect(g.getNode('uri:a')?.conceptId).toBe('a');
46
+ });
47
+ });
48
+
49
+ describe('addEdge', () => {
50
+ it('creates stub nodes for unknown source/target', () => {
51
+ const g = new GraphEngine();
52
+ g.addEdge(makeEdge('uri:a', 'uri:b'));
53
+ expect(g.nodeCount).toBe(2);
54
+ expect(g.getNode('uri:a')?.loaded).toBe(false);
55
+ expect(g.getNode('uri:a')?.status).toBe('stub');
56
+ expect(g.getNode('uri:b')?.status).toBe('stub');
57
+ expect(g.edgeCount).toBe(1);
58
+ });
59
+
60
+ it('does not overwrite existing nodes with stubs', () => {
61
+ const g = new GraphEngine();
62
+ g.addNode(makeNode('uri:a', 'a', 'test', { loaded: true, status: 'valid' }));
63
+ g.addEdge(makeEdge('uri:a', 'uri:b'));
64
+ expect(g.getNode('uri:a')?.loaded).toBe(true);
65
+ expect(g.getNode('uri:a')?.status).toBe('valid');
66
+ });
67
+
68
+ it('upgrades stub node when loaded node is added', () => {
69
+ const g = new GraphEngine();
70
+ g.addEdge(makeEdge('uri:a', 'uri:b'));
71
+ expect(g.getNode('uri:a')?.loaded).toBe(false);
72
+ g.addNode(makeNode('uri:a', 'a', 'test', { loaded: true, status: 'valid' }));
73
+ expect(g.getNode('uri:a')?.loaded).toBe(true);
74
+ expect(g.getNode('uri:a')?.conceptId).toBe('a');
75
+ });
76
+
77
+ it('supports multiple edges between same pair', () => {
78
+ const g = new GraphEngine();
79
+ g.addEdge(makeEdge('uri:a', 'uri:b', 'references'));
80
+ g.addEdge(makeEdge('uri:a', 'uri:b', 'related'));
81
+ expect(g.edgeCount).toBe(2);
82
+ const edges = g.getEdges('uri:a');
83
+ expect(edges.length).toBe(2);
84
+ expect(edges.map(e => e.type).sort()).toEqual(['references', 'related']);
85
+ });
86
+
87
+ it('deduplicates identical edges', () => {
88
+ const g = new GraphEngine();
89
+ g.addEdge(makeEdge('uri:a', 'uri:b', 'references'));
90
+ g.addEdge(makeEdge('uri:a', 'uri:b', 'references'));
91
+ expect(g.edgeCount).toBe(1);
92
+ });
93
+
94
+ it('extracts register from URI for stub nodes', () => {
95
+ const g = new GraphEngine();
96
+ g.addEdge({
97
+ source: 'https://glossarist.org/isotc204/concept/3.1.1.1',
98
+ target: 'https://glossarist.org/iev/concept/102-01-10',
99
+ type: 'references',
100
+ register: 'isotc204',
101
+ });
102
+ const target = g.getNode('https://glossarist.org/iev/concept/102-01-10');
103
+ expect(target?.register).toBe('iev');
104
+ expect(target?.conceptId).toBe('102-01-10');
105
+ });
106
+ });
107
+
108
+ describe('getNeighbors', () => {
109
+ it('returns outgoing and incoming neighbors', () => {
110
+ const g = new GraphEngine();
111
+ g.addNode(makeNode('uri:a', 'a'));
112
+ g.addNode(makeNode('uri:b', 'b'));
113
+ g.addNode(makeNode('uri:c', 'c'));
114
+ g.addEdge(makeEdge('uri:a', 'uri:b'));
115
+ g.addEdge(makeEdge('uri:c', 'uri:a'));
116
+
117
+ const neighbors = g.getNeighbors('uri:a');
118
+ expect(neighbors.outgoing).toEqual(['uri:b']);
119
+ expect(neighbors.incoming).toEqual(['uri:c']);
120
+ });
121
+
122
+ it('returns empty arrays for unknown nodes', () => {
123
+ const g = new GraphEngine();
124
+ const n = g.getNeighbors('uri:unknown');
125
+ expect(n.outgoing).toEqual([]);
126
+ expect(n.incoming).toEqual([]);
127
+ });
128
+ });
129
+
130
+ describe('getIncomingEdges', () => {
131
+ it('returns edges pointing to a node', () => {
132
+ const g = new GraphEngine();
133
+ g.addEdge(makeEdge('uri:a', 'uri:c'));
134
+ g.addEdge(makeEdge('uri:b', 'uri:c'));
135
+ g.addEdge(makeEdge('uri:c', 'uri:d'));
136
+
137
+ const incoming = g.getIncomingEdges('uri:c');
138
+ expect(incoming.length).toBe(2);
139
+ expect(incoming.map(e => e.source).sort()).toEqual(['uri:a', 'uri:b']);
140
+ });
141
+ });
142
+
143
+ describe('getSubgraph', () => {
144
+ it('returns a BFS subgraph of given depth', () => {
145
+ const g = new GraphEngine();
146
+ g.addNode(makeNode('uri:a', 'a'));
147
+ g.addNode(makeNode('uri:b', 'b'));
148
+ g.addNode(makeNode('uri:c', 'c'));
149
+ g.addNode(makeNode('uri:d', 'd'));
150
+ g.addEdge(makeEdge('uri:a', 'uri:b'));
151
+ g.addEdge(makeEdge('uri:b', 'uri:c'));
152
+ g.addEdge(makeEdge('uri:c', 'uri:d'));
153
+
154
+ const sub = g.getSubgraph('uri:a', 1);
155
+ expect(sub.nodes.length).toBe(2); // a, b
156
+ // Edges touching collected nodes: a->b is outgoing, plus any incoming to a or b
157
+ // b->c is an outgoing edge of b, collected because b is a collected node
158
+ expect(sub.edges.length).toBeGreaterThanOrEqual(1);
159
+ expect(sub.edges.some(e => e.source === 'uri:a' && e.target === 'uri:b')).toBe(true);
160
+
161
+ const sub2 = g.getSubgraph('uri:a', 2);
162
+ expect(sub2.nodes.length).toBe(3); // a, b, c
163
+ });
164
+
165
+ it('does not loop infinitely on cycles', () => {
166
+ const g = new GraphEngine();
167
+ g.addNode(makeNode('uri:a', 'a'));
168
+ g.addNode(makeNode('uri:b', 'b'));
169
+ g.addEdge(makeEdge('uri:a', 'uri:b'));
170
+ g.addEdge(makeEdge('uri:b', 'uri:a'));
171
+
172
+ const sub = g.getSubgraph('uri:a', 5);
173
+ expect(sub.nodes.length).toBe(2);
174
+ });
175
+ });
176
+
177
+ describe('getAllNodes', () => {
178
+ it('returns all nodes', () => {
179
+ const g = new GraphEngine();
180
+ g.addNode(makeNode('uri:a', 'a'));
181
+ g.addNode(makeNode('uri:b', 'b'));
182
+ const all = g.getAllNodes();
183
+ expect(all.length).toBe(2);
184
+ expect(all.map(n => n.conceptId).sort()).toEqual(['a', 'b']);
185
+ });
186
+ });
187
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { langName, langLabel, DEFAULT_LANG } from '../utils/lang';
3
+
4
+ describe('langName', () => {
5
+ it('returns name for known codes', () => {
6
+ expect(langName('eng')).toBe('English');
7
+ expect(langName('fra')).toBe('French');
8
+ expect(langName('deu')).toBe('German');
9
+ expect(langName('zho')).toBe('Chinese');
10
+ });
11
+
12
+ it('returns code as-is for unknown codes', () => {
13
+ expect(langName('xyz')).toBe('xyz');
14
+ expect(langName('abc')).toBe('abc');
15
+ });
16
+ });
17
+
18
+ describe('langLabel', () => {
19
+ it('uppercases the code', () => {
20
+ expect(langLabel('eng')).toBe('ENG');
21
+ expect(langLabel('fra')).toBe('FRA');
22
+ });
23
+ });
24
+
25
+ describe('DEFAULT_LANG', () => {
26
+ it('is eng', () => {
27
+ expect(DEFAULT_LANG).toBe('eng');
28
+ });
29
+ });
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { renderMath, cleanContent } from '../utils/math';
3
+
4
+ describe('renderMath', () => {
5
+ it('passes through plain text unchanged', () => {
6
+ expect(renderMath('hello world')).toBe('hello world');
7
+ });
8
+
9
+ it('renders stem:[x^2] to KaTeX span', () => {
10
+ const result = renderMath('the value stem:[x^2]');
11
+ expect(result).toContain('math-inline');
12
+ expect(result).toContain('katex');
13
+ expect(result).not.toContain('math-bold');
14
+ });
15
+
16
+ it('renders *stem:[x]* (bold math) with math-bold class', () => {
17
+ const result = renderMath('the value *stem:[x]*');
18
+ expect(result).toContain('math-inline');
19
+ expect(result).toContain('math-bold');
20
+ });
21
+
22
+ it('converts bullet lines to <ul><li>', () => {
23
+ const result = renderMath('* first item\n\n* second item');
24
+ expect(result).toContain('<ul class="concept-list">');
25
+ expect(result).toContain('<li>first item</li>');
26
+ expect(result).toContain('<li>second item</li>');
27
+ });
28
+
29
+ it('does NOT convert *stem:[...] lines to list items', () => {
30
+ const result = renderMath('*stem:[x]*');
31
+ expect(result).not.toContain('<ul');
32
+ expect(result).toContain('math-bold');
33
+ });
34
+
35
+ it('converts *text* to <em> (italic)', () => {
36
+ expect(renderMath('some *italic* text')).toBe('some <em>italic</em> text');
37
+ });
38
+
39
+ it('converts ~text~ to <sub> (subscript)', () => {
40
+ expect(renderMath('H~2~O')).toBe('H<sub>2</sub>O');
41
+ });
42
+
43
+ it('resolves URN inline refs via xrefResolver', () => {
44
+ const resolver = (uri: string, term: string) => `[${term}→${uri}]`;
45
+ const result = renderMath(
46
+ 'a {{urn:iso:std:iso:14812:3.1.1.1,entity}} reference',
47
+ resolver,
48
+ );
49
+ expect(result).toBe('a [entity→urn:iso:std:iso:14812:3.1.1.1] reference');
50
+ });
51
+
52
+ it('resolves single-braced URN inline refs', () => {
53
+ const resolver = (uri: string, term: string) => `[${term}→${uri}]`;
54
+ const result = renderMath(
55
+ 'a {urn:iso:std:iso:14812:3.1.1.1,entity} reference',
56
+ resolver,
57
+ );
58
+ expect(result).toBe('a [entity→urn:iso:std:iso:14812:3.1.1.1] reference');
59
+ });
60
+
61
+ it('shows term without resolver', () => {
62
+ const result = renderMath('a {{urn:iso:std:iso:14812:3.1.1.1,entity}} ref');
63
+ expect(result).toBe('a entity ref');
64
+ });
65
+
66
+ it('strips remaining {{...}} to just the term', () => {
67
+ const result = renderMath('see {{some term, unknown ref}}');
68
+ expect(result).toBe('see some term');
69
+ });
70
+
71
+ it('handles empty input', () => {
72
+ expect(renderMath('')).toBe('');
73
+ });
74
+
75
+ it('handles null-ish input', () => {
76
+ expect(renderMath(null as any)).toBe('');
77
+ expect(renderMath(undefined as any)).toBe('');
78
+ });
79
+ });
80
+
81
+ describe('cleanContent', () => {
82
+ it('strips stem:[...] to raw math text', () => {
83
+ expect(cleanContent('value stem:[x^2] here')).toBe('value x^2 here');
84
+ });
85
+
86
+ it('strips bold stem', () => {
87
+ expect(cleanContent('value *stem:[x]* here')).toBe('value x here');
88
+ });
89
+
90
+ it('strips *text* to plain text', () => {
91
+ expect(cleanContent('some *italic* text')).toBe('some italic text');
92
+ });
93
+
94
+ it('converts ~text~ to _text', () => {
95
+ expect(cleanContent('H~2~O')).toBe('H_2O');
96
+ });
97
+
98
+ it('strips list markers', () => {
99
+ const result = cleanContent('items:\n* one\n* two');
100
+ expect(result).toContain('one');
101
+ expect(result).toContain('two');
102
+ expect(result).not.toContain('* ');
103
+ });
104
+
105
+ it('strips URN refs to just the term', () => {
106
+ expect(cleanContent('a {{urn:iso:std:iso:14812:3.1.1.1,entity}} ref')).toBe('a entity ref');
107
+ });
108
+
109
+ it('handles empty input', () => {
110
+ expect(cleanContent('')).toBe('');
111
+ expect(cleanContent(null as any)).toBe('');
112
+ });
113
+ });
@@ -0,0 +1,122 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { ReferenceResolver } from '../adapters/ReferenceResolver';
3
+
4
+ describe('ReferenceResolver', () => {
5
+ let resolver: ReferenceResolver;
6
+
7
+ beforeEach(() => {
8
+ resolver = new ReferenceResolver();
9
+ });
10
+
11
+ describe('resolveReference', () => {
12
+ it('resolves internal URI for provided dataset', () => {
13
+ resolver.registerDataset('isotc204', ['urn:iso:std:iso:14812:*', 'https://glossarist.org/isotc204/*']);
14
+ const result = resolver.resolveReference('https://glossarist.org/isotc204/concept/3.1.1.1');
15
+ expect(result).toEqual({
16
+ type: 'internal',
17
+ registerId: 'isotc204',
18
+ conceptId: '3.1.1.1',
19
+ crossDataset: false,
20
+ });
21
+ });
22
+
23
+ it('resolves URN to internal for provided dataset', () => {
24
+ resolver.registerDataset('isotc204', ['urn:iso:std:iso:14812:*']);
25
+ const result = resolver.resolveReference('urn:iso:std:iso:14812:3.1.1.1');
26
+ expect(result).toEqual({
27
+ type: 'internal',
28
+ registerId: 'isotc204',
29
+ conceptId: '3.1.1.1',
30
+ crossDataset: false,
31
+ });
32
+ });
33
+
34
+ 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:*']);
37
+ const result = resolver.resolveReference('urn:iec:std:iec:60050:103-01-02', 'isotc204');
38
+ expect(result).toEqual({
39
+ type: 'internal',
40
+ registerId: 'iev',
41
+ conceptId: '103-01-02',
42
+ crossDataset: true,
43
+ });
44
+ });
45
+
46
+ it('resolves to site via routing table', () => {
47
+ resolver.loadRouting([
48
+ {
49
+ uri: 'https://glossarist.org/iev/*',
50
+ type: 'site',
51
+ baseUrl: 'https://www.geolexica.org',
52
+ label: 'Geolexica Hub',
53
+ },
54
+ ]);
55
+ const result = resolver.resolveReference('https://glossarist.org/iev/concept/103-01-02');
56
+ expect(result).toEqual({
57
+ type: 'site',
58
+ baseUrl: 'https://www.geolexica.org',
59
+ conceptUri: 'https://glossarist.org/iev/concept/103-01-02',
60
+ label: 'Geolexica Hub',
61
+ });
62
+ });
63
+
64
+ it('resolves to url via routing table with {conceptId} substitution', () => {
65
+ resolver.loadRouting([
66
+ {
67
+ uri: 'urn:iec:std:iec:60050:*',
68
+ type: 'url',
69
+ url: 'https://electropedia.org/iev/iev.nsf/display?openform&ievref={conceptId}',
70
+ label: 'IEC Electropedia',
71
+ },
72
+ ]);
73
+ const result = resolver.resolveReference('urn:iec:std:iec:60050:103-01-02');
74
+ expect(result).toEqual({
75
+ type: 'url',
76
+ url: 'https://electropedia.org/iev/iev.nsf/display?openform&ievref=103-01-02',
77
+ label: 'IEC Electropedia',
78
+ });
79
+ });
80
+
81
+ it('resolves to url without conceptId placeholder', () => {
82
+ resolver.loadRouting([
83
+ {
84
+ uri: 'https://glossarist.org/legacy/*',
85
+ type: 'url',
86
+ url: 'https://example.com/obp/concepts',
87
+ label: 'Legacy concepts',
88
+ },
89
+ ]);
90
+ const result = resolver.resolveReference('https://glossarist.org/legacy/concept/123');
91
+ expect(result).toEqual({
92
+ type: 'url',
93
+ url: 'https://example.com/obp/concepts',
94
+ label: 'Legacy concepts',
95
+ });
96
+ });
97
+
98
+ it('prefers provided dataset over routing', () => {
99
+ resolver.registerDataset('iev', ['urn:iec:std:iec:60050:*']);
100
+ resolver.loadRouting([
101
+ {
102
+ uri: 'urn:iec:std:iec:60050:*',
103
+ type: 'url',
104
+ url: 'https://electropedia.org/{conceptId}',
105
+ label: 'External',
106
+ },
107
+ ]);
108
+ const result = resolver.resolveReference('urn:iec:std:iec:60050:103-01-02');
109
+ expect(result.type).toBe('internal');
110
+ });
111
+
112
+ it('returns unresolved for unknown URIs', () => {
113
+ const result = resolver.resolveReference('https://glossarist.org/unknown/concept/123');
114
+ expect(result).toEqual({ type: 'unresolved', uri: 'https://glossarist.org/unknown/concept/123' });
115
+ });
116
+
117
+ it('returns unresolved for non-matching URNs', () => {
118
+ const result = resolver.resolveReference('urn:other:scheme:123');
119
+ expect(result).toEqual({ type: 'unresolved', uri: 'urn:other:scheme:123' });
120
+ });
121
+ });
122
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { RuntimeSiteConfig } from '../config/use-site-config';
3
+
4
+ describe('RuntimeSiteConfig', () => {
5
+ it('accepts valid config structure', () => {
6
+ const config: RuntimeSiteConfig = {
7
+ id: 'geolexica',
8
+ domain: 'www.geolexica.org',
9
+ title: 'Geolexica',
10
+ datasets: ['iev', 'isotc211', 'isotc204', 'osgeo'],
11
+ branding: { primaryColor: '#2563eb' },
12
+ defaults: { language: 'eng' },
13
+ };
14
+ expect(config.datasets).toEqual(['iev', 'isotc211', 'isotc204', 'osgeo']);
15
+ expect(config.branding?.primaryColor).toBe('#2563eb');
16
+ });
17
+
18
+ it('supports single-dataset config', () => {
19
+ const config: RuntimeSiteConfig = {
20
+ id: 'isotc204',
21
+ domain: 'isotc204.geolexica.org',
22
+ title: 'ISO/TC 204 ITS Vocabulary',
23
+ datasets: ['isotc204'],
24
+ defaultDataset: 'isotc204',
25
+ branding: { primaryColor: '#d97706', ownerName: 'ISO/TC 204' },
26
+ defaults: { language: 'eng' },
27
+ };
28
+ expect(config.datasets).toEqual(['isotc204']);
29
+ expect(config.defaultDataset).toBe('isotc204');
30
+ });
31
+
32
+ it('supports optional fields', () => {
33
+ const config: RuntimeSiteConfig = {
34
+ id: 'osgeo',
35
+ domain: 'osgeo.geolexica.org',
36
+ title: 'OSGeo Lexicon',
37
+ subtitle: 'Open Source Geospatial Glossary',
38
+ datasets: ['osgeo'],
39
+ branding: {
40
+ primaryColor: '#059669',
41
+ ownerName: 'OSGeo',
42
+ ownerUrl: 'https://www.osgeo.org',
43
+ },
44
+ analytics: { googleAnalyticsId: 'UA-168998071-3' },
45
+ features: { graph: true, search: true },
46
+ social: { github: 'https://github.com/geolexica/osgeo-glossary' },
47
+ defaults: { language: 'eng' },
48
+ };
49
+ expect(config.subtitle).toBe('Open Source Geospatial Glossary');
50
+ expect(config.analytics?.googleAnalyticsId).toBe('UA-168998071-3');
51
+ });
52
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { UriRouter } from '../adapters/UriRouter';
3
+
4
+ describe('UriRouter', () => {
5
+ it('resolves URIs for registered datasets', () => {
6
+ const router = new UriRouter();
7
+ router.registerDataset('iev', '/data/iev');
8
+
9
+ const resolved = router.resolveUri('https://glossarist.org/iev/concept/103-01-02');
10
+ expect(resolved).toEqual({ registerId: 'iev', conceptId: '103-01-02' });
11
+ });
12
+
13
+ it('resolves URIs with multi-part concept IDs', () => {
14
+ const router = new UriRouter();
15
+ router.registerDataset('isotc204', '/data/isotc204');
16
+
17
+ const resolved = router.resolveUri('https://glossarist.org/isotc204/concept/3.1.1.1');
18
+ expect(resolved).toEqual({ registerId: 'isotc204', conceptId: '3.1.1.1' });
19
+ });
20
+
21
+ it('returns null for unknown register', () => {
22
+ const router = new UriRouter();
23
+ router.registerDataset('iev', '/data/iev');
24
+
25
+ expect(router.resolveUri('https://glossarist.org/unknown/concept/123')).toBeNull();
26
+ });
27
+
28
+ it('returns null for non-matching URI pattern', () => {
29
+ const router = new UriRouter();
30
+ router.registerDataset('iev', '/data/iev');
31
+
32
+ expect(router.resolveUri('https://example.com/other')).toBeNull();
33
+ });
34
+
35
+ it('builds URIs from register and concept ID', () => {
36
+ const router = new UriRouter();
37
+ expect(router.buildUri('iev', '103-01-02')).toBe('https://glossarist.org/iev/concept/103-01-02');
38
+ });
39
+
40
+ it('lists all registered IDs', () => {
41
+ const router = new UriRouter();
42
+ router.registerDataset('iev', '/data/iev');
43
+ router.registerDataset('isotc211', '/data/isotc211');
44
+
45
+ expect(router.getRegisteredIds()).toEqual(['iev', 'isotc211']);
46
+ });
47
+
48
+ it('resolves across multiple registers', () => {
49
+ const router = new UriRouter();
50
+ router.registerDataset('iev', '/data/iev');
51
+ router.registerDataset('isotc211', '/data/isotc211');
52
+ router.registerDataset('isotc204', '/data/isotc204');
53
+
54
+ expect(router.resolveUri('https://glossarist.org/iev/concept/102-01-01')?.registerId).toBe('iev');
55
+ expect(router.resolveUri('https://glossarist.org/isotc211/concept/10')?.registerId).toBe('isotc211');
56
+ expect(router.resolveUri('https://glossarist.org/isotc204/concept/3.1.1.1')?.registerId).toBe('isotc204');
57
+ });
58
+
59
+ describe('parseUri (static)', () => {
60
+ it('extracts register and concept from any glossarist URI', () => {
61
+ expect(UriRouter.parseUri('https://glossarist.org/iev/concept/103-01-02')).toEqual({
62
+ registerId: 'iev', conceptId: '103-01-02',
63
+ });
64
+ });
65
+
66
+ it('handles multi-part concept IDs', () => {
67
+ expect(UriRouter.parseUri('https://glossarist.org/isotc204/concept/3.1.1.1')).toEqual({
68
+ registerId: 'isotc204', conceptId: '3.1.1.1',
69
+ });
70
+ });
71
+
72
+ it('returns null for non-matching URIs', () => {
73
+ expect(UriRouter.parseUri('https://example.com/other')).toBeNull();
74
+ });
75
+ });
76
+ });