@glossarist/concept-browser 0.3.4 → 0.4.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 (81) hide show
  1. package/README.md +3 -2
  2. package/cli/index.mjs +2 -1
  3. package/env.d.ts +5 -0
  4. package/package.json +4 -3
  5. package/scripts/build-edges.js +78 -10
  6. package/scripts/generate-data.mjs +152 -20
  7. package/scripts/generate-ontology-data.mjs +184 -0
  8. package/scripts/generate-ontology-schema.mjs +315 -0
  9. package/src/__tests__/about-view.test.ts +98 -0
  10. package/src/__tests__/app-footer.test.ts +38 -0
  11. package/src/__tests__/app-header.test.ts +130 -0
  12. package/src/__tests__/app-sidebar.test.ts +159 -0
  13. package/src/__tests__/asciidoc-lite.test.ts +1 -1
  14. package/src/__tests__/concept-card.test.ts +115 -0
  15. package/src/__tests__/concept-detail-interaction.test.ts +273 -0
  16. package/src/__tests__/concept-formats.test.ts +32 -30
  17. package/src/__tests__/concept-timeline.test.ts +200 -0
  18. package/src/__tests__/concept-view.test.ts +88 -0
  19. package/src/__tests__/contributors-view.test.ts +103 -0
  20. package/src/__tests__/dataset-adapter.test.ts +172 -23
  21. package/src/__tests__/dataset-view.test.ts +232 -0
  22. package/src/__tests__/designation-registry.test.ts +161 -0
  23. package/src/__tests__/format-downloads.test.ts +98 -0
  24. package/src/__tests__/graph-view.test.ts +69 -0
  25. package/src/__tests__/graph.test.ts +62 -0
  26. package/src/__tests__/home-interaction.test.ts +157 -0
  27. package/src/__tests__/language-detail.test.ts +203 -0
  28. package/src/__tests__/nav-icon.test.ts +48 -0
  29. package/src/__tests__/news-view.test.ts +87 -0
  30. package/src/__tests__/ontology-registry.test.ts +109 -0
  31. package/src/__tests__/page-view.test.ts +83 -0
  32. package/src/__tests__/relationship-categories.test.ts +62 -0
  33. package/src/__tests__/resolve-view.test.ts +77 -0
  34. package/src/__tests__/router.test.ts +65 -0
  35. package/src/__tests__/search-bar.test.ts +219 -0
  36. package/src/__tests__/search-view.test.ts +41 -0
  37. package/src/__tests__/stats-view.test.ts +77 -0
  38. package/src/__tests__/test-helpers.ts +171 -0
  39. package/src/__tests__/ui-store.test.ts +100 -0
  40. package/src/__tests__/v-math.test.ts +8 -7
  41. package/src/adapters/DatasetAdapter.ts +188 -63
  42. package/src/adapters/model-bridge.ts +277 -0
  43. package/src/adapters/ontology-registry.ts +75 -0
  44. package/src/adapters/ontology-schema.ts +100 -0
  45. package/src/adapters/types.ts +53 -78
  46. package/src/components/AppSidebar.vue +1 -1
  47. package/src/components/CitationDisplay.vue +35 -0
  48. package/src/components/ConceptDetail.vue +349 -146
  49. package/src/components/ConceptRdfView.vue +397 -0
  50. package/src/components/ConceptTimeline.vue +57 -60
  51. package/src/components/GraphPanel.vue +96 -31
  52. package/src/components/LanguageDetail.vue +46 -61
  53. package/src/components/NavIcon.vue +1 -0
  54. package/src/components/NonVerbalRepDisplay.vue +38 -0
  55. package/src/components/RelationshipList.vue +99 -0
  56. package/src/composables/use-render-options.ts +1 -4
  57. package/src/config/use-site-config.ts +3 -0
  58. package/src/data/ontology-schema.json +1551 -0
  59. package/src/data/taxonomies.json +543 -0
  60. package/src/graph/GraphEngine.ts +7 -4
  61. package/src/router/index.ts +6 -1
  62. package/src/shims/empty.ts +1 -0
  63. package/src/shims/node-crypto.ts +6 -0
  64. package/src/shims/node-path.ts +10 -0
  65. package/src/stores/vocabulary.ts +82 -32
  66. package/src/style.css +74 -20
  67. package/src/utils/asciidoc-lite.ts +17 -19
  68. package/src/utils/concept-formats.ts +22 -20
  69. package/src/utils/concept-helpers.ts +54 -0
  70. package/src/utils/designation-registry.ts +124 -0
  71. package/src/utils/escape.ts +7 -0
  72. package/src/utils/markdown-lite.ts +1 -3
  73. package/src/utils/math.ts +2 -11
  74. package/src/utils/plurimath.ts +2 -7
  75. package/src/utils/relationship-categories.ts +84 -0
  76. package/src/views/ConceptView.vue +22 -1
  77. package/src/views/DatasetView.vue +7 -2
  78. package/src/views/OntologySchemaView.vue +302 -0
  79. package/src/views/PageView.vue +28 -17
  80. package/src/views/StatsView.vue +34 -12
  81. package/vite.config.ts +8 -0
@@ -2,14 +2,16 @@ import { defineStore } from 'pinia';
2
2
  import { ref, computed, toRaw } from 'vue';
3
3
  import { getFactory } from '../adapters/factory';
4
4
  import type { DatasetAdapter } from '../adapters/DatasetAdapter';
5
- import type { Manifest, ConceptDocument, SearchHit, GraphEdge } from '../adapters/types';
5
+ import type { Manifest, SearchHit, GraphEdge } from '../adapters/types';
6
+ import type { Concept } from 'glossarist';
7
+ import { conceptUri } from '../adapters/model-bridge';
6
8
  import { GraphEngine } from '../graph';
7
9
 
8
10
  export const useVocabularyStore = defineStore('vocabulary', () => {
9
11
  // State
10
12
  const datasets = ref<Map<string, DatasetAdapter>>(new Map());
11
13
  const manifests = ref<Map<string, Manifest>>(new Map());
12
- const currentConcept = ref<ConceptDocument | null>(null);
14
+ const currentConcept = ref<Concept | null>(null);
13
15
  const currentRegisterId = ref<string>('');
14
16
  const currentConceptId = ref<string>('');
15
17
  const loading = ref(false);
@@ -58,8 +60,8 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
58
60
  }
59
61
  }
60
62
  initialized.value = true;
61
- } catch (e: any) {
62
- error.value = `Failed to discover datasets: ${e.message}`;
63
+ } catch (e: unknown) {
64
+ error.value = `Failed to discover datasets: ${e instanceof Error ? e.message : String(e)}`;
63
65
  } finally {
64
66
  loading.value = false;
65
67
  }
@@ -81,8 +83,8 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
81
83
 
82
84
  // Seed graph nodes lazily — don't block UI for large datasets
83
85
  seedGraphNodes(registerId, adapter);
84
- } catch (e: any) {
85
- error.value = `Failed to load dataset ${registerId}: ${e.message}`;
86
+ } catch (e: unknown) {
87
+ error.value = `Failed to load dataset ${registerId}: ${e instanceof Error ? e.message : String(e)}`;
86
88
  throw e;
87
89
  }
88
90
  }
@@ -97,7 +99,7 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
97
99
  uri: factory.router.buildUri(registerId, entry.id),
98
100
  register: registerId,
99
101
  conceptId: entry.id,
100
- designations: entry.eng ? { eng: entry.eng } : {},
102
+ designations: entry.designations,
101
103
  status: entry.status,
102
104
  loaded: false,
103
105
  });
@@ -121,7 +123,7 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
121
123
  uri: factory.router.buildUri(registerId, entry.id),
122
124
  register: registerId,
123
125
  conceptId: entry.id,
124
- designations: entry.eng ? { eng: entry.eng } : {},
126
+ designations: entry.designations,
125
127
  status: entry.status,
126
128
  loaded: false,
127
129
  });
@@ -143,11 +145,12 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
143
145
 
144
146
  await Promise.allSettled(adapters.map(async (adapter) => {
145
147
  try {
146
- const [nodeResult, edgeResult] = await Promise.allSettled([
148
+ const [nodeResult, edgeResult, domainResult] = await Promise.allSettled([
147
149
  adapter.loadGraphNodes(),
148
150
  !edgeStatus.value[adapter.registerId]?.loaded
149
151
  ? adapter.loadEdgeIndex()
150
152
  : Promise.resolve([] as GraphEdge[]),
153
+ adapter.loadDomainNodes(),
151
154
  ]);
152
155
 
153
156
  if (nodeResult.status === 'fulfilled') {
@@ -170,6 +173,12 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
170
173
  }
171
174
  edgeStatus.value[adapter.registerId] = { loaded: true, count: edgeResult.value.length };
172
175
  }
176
+
177
+ if (domainResult.status === 'fulfilled') {
178
+ for (const dn of domainResult.value) {
179
+ engine.addNode(dn);
180
+ }
181
+ }
173
182
  } catch {
174
183
  // Individual adapter failures are non-critical for graph view
175
184
  }
@@ -180,9 +189,14 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
180
189
 
181
190
  async function loadEdges(adapter: DatasetAdapter) {
182
191
  try {
183
- const edges = await adapter.loadEdgeIndex();
192
+ const [edges, domainNodes] = await Promise.all([
193
+ adapter.loadEdgeIndex(),
194
+ adapter.loadDomainNodes(),
195
+ ]);
196
+ for (const dn of domainNodes) {
197
+ graph.value.addNode(dn);
198
+ }
184
199
  for (const edge of edges) {
185
- // Mark source node as having edges
186
200
  graph.value.addEdge(edge);
187
201
  }
188
202
  edgeStatus.value[adapter.registerId] = { loaded: true, count: edges.length };
@@ -205,26 +219,31 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
205
219
 
206
220
  // Extract and register edges for this specific concept
207
221
  const edges = adapter.extractEdges(concept);
208
- const uri = concept['@id'];
222
+ const domainEdges = adapter.extractDomainEdges(concept);
223
+ const uriBase = adapter.manifest?.uriBase || 'https://glossarist.org';
224
+ const uri = conceptUri(concept, registerId, uriBase);
209
225
 
210
226
  // Update graph node with full data
227
+ const designations: Record<string, string> = {};
228
+ const indexEntry = adapter.getIndexEntry(conceptId);
229
+ if (indexEntry) {
230
+ for (const [lang, term] of Object.entries(indexEntry.designations)) {
231
+ if (term) designations[lang] = term;
232
+ }
233
+ }
234
+ for (const lang of concept.languages) {
235
+ const lc = concept.localization(lang);
236
+ if (lc?.primaryDesignation) {
237
+ designations[lang] = lc.primaryDesignation;
238
+ }
239
+ }
240
+
211
241
  graph.value.addNode({
212
242
  uri,
213
243
  register: registerId,
214
244
  conceptId,
215
- designations: (() => {
216
- const d: Record<string, string> = {};
217
- const entry = adapter.getIndexEntry(conceptId);
218
- if (entry?.eng) d.eng = entry.eng;
219
- for (const [lang, lc] of Object.entries(concept['gl:localizedConcept'] || {})) {
220
- const preferred = lc['gl:designation']?.find(
221
- (dd: any) => dd['gl:normativeStatus'] === 'preferred'
222
- );
223
- if (preferred?.['gl:term']) d[lang] = preferred['gl:term'];
224
- }
225
- return d;
226
- })(),
227
- status: adapter.getIndexEntry(conceptId)?.status ?? 'unknown',
245
+ designations,
246
+ status: indexEntry?.status ?? 'unknown',
228
247
  loaded: true,
229
248
  });
230
249
 
@@ -232,10 +251,26 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
232
251
  graph.value.addEdge(edge);
233
252
  }
234
253
 
254
+ for (const edge of domainEdges) {
255
+ graph.value.addEdge(edge);
256
+ const existing = graph.value.getNode(edge.target);
257
+ if (!existing || !existing.loaded) {
258
+ graph.value.addNode({
259
+ uri: edge.target,
260
+ register: registerId,
261
+ conceptId: '',
262
+ designations: edge.label ? { eng: edge.label } : {},
263
+ status: 'domain',
264
+ loaded: true,
265
+ nodeType: 'domain',
266
+ });
267
+ }
268
+ }
269
+
235
270
  touchGraph();
236
271
  conceptEdges.value = graph.value.getEdges(uri);
237
- } catch (e: any) {
238
- error.value = `Failed to load concept ${conceptId}: ${e.message}`;
272
+ } catch (e: unknown) {
273
+ error.value = `Failed to load concept ${conceptId}: ${e instanceof Error ? e.message : String(e)}`;
239
274
  currentConcept.value = null;
240
275
  throw e;
241
276
  }
@@ -260,22 +295,37 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
260
295
  }
261
296
  }
262
297
 
263
- async function searchAcrossDatasets(query: string, lang: string = 'eng'): Promise<SearchHit[]> {
264
- const hits: SearchHit[] = [];
298
+ async function searchAcrossDatasets(query: string): Promise<SearchHit[]> {
299
+ const allHits: SearchHit[] = [];
265
300
  for (const adapter of datasets.value.values()) {
266
301
  if (adapter.index || adapter.manifest) {
267
302
  await adapter.ensureAllChunksLoaded();
268
- hits.push(...adapter.search(query, lang));
303
+ allHits.push(...adapter.search(query));
304
+ }
305
+ }
306
+
307
+ // Deduplicate: keep the best hit per (registerId, conceptId)
308
+ const best = new Map<string, SearchHit>();
309
+ for (const hit of allHits) {
310
+ const key = `${hit.registerId}:${hit.conceptId}`;
311
+ const existing = best.get(key);
312
+ if (!existing) {
313
+ best.set(key, hit);
314
+ } else {
315
+ // Prefer designation match over id match, then prefer shorter language code (eng first)
316
+ if (hit.matchField === 'designation' && existing.matchField === 'id') {
317
+ best.set(key, hit);
318
+ }
269
319
  }
270
320
  }
271
- return hits;
321
+ return [...best.values()];
272
322
  }
273
323
 
274
324
  async function getRandomConcept(): Promise<{ registerId: string; conceptId: string } | null> {
275
325
  const loaded = [...datasets.value.values()].filter(a => a.index);
276
326
  if (!loaded.length) return null;
277
327
  const adapter = loaded[Math.floor(Math.random() * loaded.length)];
278
- const concepts = adapter.getConcepts() as (import('../adapters/types').ConceptSummary | undefined)[];
328
+ const concepts = adapter.getConcepts();
279
329
  const dense = concepts.filter((c): c is import('../adapters/types').ConceptSummary => c != null);
280
330
  if (!dense.length) return null;
281
331
  const pick = dense[Math.floor(Math.random() * dense.length)];
package/src/style.css CHANGED
@@ -290,50 +290,78 @@
290
290
  }
291
291
 
292
292
  /* ===== Dark mode ===== */
293
+
294
+ /*
295
+ Color philosophy:
296
+ Light mode uses an "ink" navy-indigo palette (50=lightest → 900=darkest).
297
+ Dark mode inverts the scale: the darkest light colors become the brightest
298
+ dark colors, maintaining WCAG AA contrast on dark surfaces.
299
+
300
+ Surface hierarchy (light → dark):
301
+ surface (#faf9f6) → surface-alt (#f3f2ee) → surface-raised (#fff) — cards, inputs
302
+ Dark surface hierarchy:
303
+ surface (#0f1020) → surface-alt (#161728) → surface-raised (#1e1f34)
304
+ Raised cards sit one step lighter than the base surface.
305
+ */
306
+
293
307
  .dark body {
294
308
  background-color: #0f1020;
295
309
  color: #dddde6;
296
310
  }
297
311
 
298
- /* Surfaces */
312
+ /* ── Surfaces ── */
299
313
  .dark .bg-surface { background-color: #0f1020 !important; }
300
314
  .dark .bg-surface-alt { background-color: #161728 !important; }
301
- .dark .bg-surface-raised { background-color: #1a1b2e !important; }
302
-
303
- /* Text */
315
+ .dark .bg-surface-raised { background-color: #1e1f34 !important; }
316
+
317
+ /* ── Text (inverted ink scale for dark) ──
318
+ ink-800/700 → primary headings (brightest)
319
+ ink-600 → body text (readable, ≥4.5:1 on raised surfaces)
320
+ ink-500/400 → secondary/hint
321
+ ink-300/200 → muted labels
322
+ ink-100 → borders/decorative only
323
+ */
304
324
  .dark .text-ink { color: #dddde6 !important; }
325
+ .dark .text-ink-900 { color: #eeeef4 !important; }
305
326
  .dark .text-ink-800 { color: #dddde6 !important; }
306
- .dark .text-ink-700 { color: #b8b9cc !important; }
307
- .dark .text-ink-600 { color: #8d8faa !important; }
308
- .dark .text-ink-500 { color: #8d8faa !important; }
309
- .dark .text-ink-400 { color: #636588 !important; }
327
+ .dark .text-ink-700 { color: #c8c9d8 !important; }
328
+ .dark .text-ink-600 { color: #b8b9cc !important; }
329
+ .dark .text-ink-500 { color: #9d9fbb !important; }
330
+ .dark .text-ink-400 { color: #7a7c9a !important; }
310
331
  .dark .text-ink-300 { color: #636588 !important; }
311
332
  .dark .text-ink-200 { color: #484a6e !important; }
312
333
  .dark .text-ink-100 { color: #484a6e !important; }
313
334
 
314
- /* Backgrounds */
335
+ /* ── Backgrounds ── */
315
336
  .dark .bg-ink-50 { background-color: #161728 !important; }
337
+ .dark .bg-ink-50\/30 { background-color: rgba(22, 23, 40, 0.3) !important; }
338
+ .dark .bg-ink-50\/60 { background-color: rgba(22, 23, 40, 0.6) !important; }
316
339
  .dark .bg-ink-100 { background-color: #1e1f34 !important; }
340
+ .dark .bg-ink-200 { background-color: #22243c !important; }
341
+ .dark .bg-ink-800 { background-color: #0a0b18 !important; }
342
+ .dark .bg-ink-800\/8 { background-color: rgba(10, 11, 24, 0.08) !important; }
317
343
 
318
- /* Borders */
344
+ /* ── Borders ── */
319
345
  .dark .border-ink-100 { border-color: #2c2e4a !important; }
346
+ .dark .border-ink-100\/30 { border-color: rgba(44, 46, 74, 0.3) !important; }
320
347
  .dark .border-ink-100\/60 { border-color: rgba(44, 46, 74, 0.6) !important; }
321
348
  .dark .border-ink-100\/80 { border-color: rgba(44, 46, 74, 0.8) !important; }
322
349
  .dark .border-ink-200 { border-color: #36385a !important; }
350
+ .dark .border-ink-800 { border-color: #2c2e4a !important; }
323
351
 
324
- /* Focus rings */
352
+ /* ── Focus rings ── */
325
353
  .dark .focus\:ring-ink-200:focus { --tw-ring-color: #36385a !important; }
326
354
  .dark .focus\:ring-ink-200\/30:focus { --tw-ring-color: rgba(54, 56, 90, 0.3) !important; }
327
- .dark .focus\:border-ink-400:focus { border-color: #636588 !important; }
355
+ .dark .focus\:border-ink-400:focus { border-color: #7a7c9a !important; }
328
356
 
329
- /* Cards */
357
+ /* ── Cards ── */
330
358
  .dark .card {
331
359
  background-color: #1e1f34 !important;
332
360
  border-color: rgba(44, 46, 74, 0.6) !important;
333
361
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2) !important;
334
362
  }
335
363
 
336
- /* Inputs */
364
+ /* ── Inputs ── */
337
365
  .dark input {
338
366
  background-color: #161728 !important;
339
367
  border-color: #2c2e4a !important;
@@ -341,16 +369,42 @@
341
369
  }
342
370
  .dark input::placeholder { color: #484a6e !important; }
343
371
 
344
- /* Skeleton */
372
+ /* ── Skeleton ── */
345
373
  .dark .skeleton {
346
374
  background: linear-gradient(90deg, #1e1f34 25%, #2c2e4a 50%, #1e1f34 75%) !important;
347
375
  }
348
376
 
349
- /* Hover states */
350
- .dark .hover\:bg-ink-50:hover { background-color: #161728 !important; }
377
+ /* ── Hover states ── */
378
+ .dark .hover\:bg-ink-50:hover { background-color: #1e1f34 !important; }
379
+ .dark .hover\:bg-ink-50\/30:hover { background-color: rgba(30, 31, 52, 0.3) !important; }
380
+ .dark .hover\:bg-ink-50\/50:hover { background-color: rgba(30, 31, 52, 0.5) !important; }
351
381
  .dark .hover\:bg-surface-alt:hover { background-color: #161728 !important; }
352
- .dark .hover\:text-ink-700:hover { color: #b8b9cc !important; }
353
- .dark .hover\:text-ink-800:hover { color: #dddde6 !important; }
382
+ .dark .hover\:text-ink-700:hover { color: #dddde6 !important; }
383
+ .dark .hover\:text-ink-800:hover { color: #eeeef4 !important; }
354
384
 
355
- /* Placeholder text */
385
+ /* ── Placeholder text ── */
356
386
  .dark .placeholder\:text-ink-300::placeholder { color: #484a6e !important; }
387
+
388
+ /* ── Semantic colors (badges, accent text) ── */
389
+ .dark .bg-blue-50 { background-color: rgba(59, 130, 246, 0.15) !important; }
390
+ .dark .text-blue-600 { color: #93bbfd !important; }
391
+ .dark .text-blue-700 { color: #7aa8fb !important; }
392
+ .dark .border-blue-100 { border-color: rgba(59, 130, 246, 0.25) !important; }
393
+ .dark .border-blue-500 { border-color: #3b82f6 !important; }
394
+ .dark .bg-emerald-50 { background-color: rgba(16, 185, 129, 0.15) !important; }
395
+ .dark .text-emerald-500 { color: #6ee7b7 !important; }
396
+ .dark .text-emerald-700 { color: #6ee7b7 !important; }
397
+ .dark .border-emerald-100 { border-color: rgba(16, 185, 129, 0.25) !important; }
398
+ .dark .bg-amber-50 { background-color: rgba(245, 158, 11, 0.15) !important; }
399
+ .dark .text-amber-600 { color: #fbbf24 !important; }
400
+ .dark .text-amber-700 { color: #fbbf24 !important; }
401
+ .dark .bg-purple-50 { background-color: rgba(139, 92, 246, 0.15) !important; }
402
+ .dark .text-purple-700 { color: #c4b5fd !important; }
403
+ .dark .bg-red-50 { background-color: rgba(239, 68, 68, 0.15) !important; }
404
+ .dark .text-red-600 { color: #fca5a5 !important; }
405
+
406
+ /* Scrollbar hide utility */
407
+ @layer utilities {
408
+ .scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; }
409
+ .scrollbar-none::-webkit-scrollbar { display: none; }
410
+ }
@@ -2,10 +2,21 @@
2
2
  * Lightweight AsciiDoc-to-HTML converter for news posts.
3
3
  * Handles: paragraphs, headings, bold, italic, monospace, links, lists, source blocks.
4
4
  */
5
+
6
+ import { escapeHtml, escapeAttr } from './escape';
7
+
5
8
  export function renderAsciiDocLite(text: string): string {
6
9
  if (!text) return '';
7
10
 
8
11
  const output: string[] = [];
12
+ let paragraphBuf: string[] = [];
13
+
14
+ function flushParagraph() {
15
+ if (paragraphBuf.length > 0) {
16
+ output.push(`<p>${paragraphBuf.join(' ')}</p>`);
17
+ paragraphBuf = [];
18
+ }
19
+ }
9
20
  const lines = text.split('\n');
10
21
  let i = 0;
11
22
  let inSourceBlock = false;
@@ -22,7 +33,7 @@ export function renderAsciiDocLite(text: string): string {
22
33
  sourceLines = [];
23
34
  inSourceBlock = false;
24
35
  } else {
25
- flushParagraph(output);
36
+ flushParagraph();
26
37
  inSourceBlock = true;
27
38
  }
28
39
  i++;
@@ -37,7 +48,7 @@ export function renderAsciiDocLite(text: string): string {
37
48
 
38
49
  // Empty line — paragraph break
39
50
  if (!trimmed) {
40
- flushParagraph(output);
51
+ flushParagraph();
41
52
  i++;
42
53
  continue;
43
54
  }
@@ -45,7 +56,7 @@ export function renderAsciiDocLite(text: string): string {
45
56
  // Headings
46
57
  const headingMatch = trimmed.match(/^(={1,5})\s+(.+)$/);
47
58
  if (headingMatch) {
48
- flushParagraph(output);
59
+ flushParagraph();
49
60
  const level = headingMatch[1].length + 1;
50
61
  output.push(`<h${level}>${inlineFormat(headingMatch[2])}</h${level}>`);
51
62
  i++;
@@ -54,7 +65,7 @@ export function renderAsciiDocLite(text: string): string {
54
65
 
55
66
  // Unordered list item
56
67
  if (trimmed.match(/^\*+\s+/)) {
57
- flushParagraph(output);
68
+ flushParagraph();
58
69
  const items: string[] = [];
59
70
  while (i < lines.length && lines[i].trim().match(/^\*+\s+/)) {
60
71
  const itemLine = lines[i].trim();
@@ -69,7 +80,7 @@ export function renderAsciiDocLite(text: string): string {
69
80
 
70
81
  // Ordered list item
71
82
  if (trimmed.match(/^\.\s+/)) {
72
- flushParagraph(output);
83
+ flushParagraph();
73
84
  const items: string[] = [];
74
85
  while (i < lines.length && lines[i].trim().match(/^\.\s+/)) {
75
86
  items.push(`<li>${inlineFormat(lines[i].trim().replace(/^\.\s+/, ''))}</li>`);
@@ -84,20 +95,11 @@ export function renderAsciiDocLite(text: string): string {
84
95
  i++;
85
96
  }
86
97
 
87
- flushParagraph(output);
98
+ flushParagraph();
88
99
 
89
100
  return output.join('\n');
90
101
  }
91
102
 
92
- let paragraphBuf: string[] = [];
93
-
94
- function flushParagraph(output: string[]) {
95
- if (paragraphBuf.length > 0) {
96
- output.push(`<p>${paragraphBuf.join(' ')}</p>`);
97
- paragraphBuf = [];
98
- }
99
- }
100
-
101
103
  function inlineFormat(text: string): string {
102
104
  // AsciiDoc link: https://example.com[text]
103
105
  text = text.replace(/(https?:\/\/[^\s\[]+)\[([^\]]*)\]/g, (_, url, label) =>
@@ -120,7 +122,3 @@ function inlineFormat(text: string): string {
120
122
 
121
123
  return text;
122
124
  }
123
-
124
- function escapeHtml(s: string): string {
125
- return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
126
- }
@@ -1,4 +1,4 @@
1
- import type { ConceptDocument, LocalizedConcept } from '../adapters/types';
1
+ import type { Concept } from 'glossarist';
2
2
 
3
3
  export interface FormatDescriptor {
4
4
  extension: string;
@@ -13,7 +13,7 @@ export const FORMAT_REGISTRY: Record<string, FormatDescriptor> = {
13
13
  yaml: { extension: 'yaml', label: 'YAML', mediaType: 'text/yaml' },
14
14
  };
15
15
 
16
- function getLocalizedData(concept: ConceptDocument) {
16
+ function getLocalizedData(concept: Concept) {
17
17
  const result: Record<string, {
18
18
  prefLabels: string[];
19
19
  altLabels: string[];
@@ -21,19 +21,21 @@ function getLocalizedData(concept: ConceptDocument) {
21
21
  notes: string[];
22
22
  }> = {};
23
23
 
24
- for (const [lang, lc] of Object.entries(concept['gl:localizedConcept'] || {})) {
25
- const descs = lc['gl:designation'] || [];
26
- const prefLabels = descs
27
- .filter(d => d['gl:normativeStatus'] === 'preferred' && d['gl:term'])
28
- .map(d => d['gl:term']!);
29
- const altLabels = descs
30
- .filter(d => d['gl:normativeStatus'] !== 'preferred' && d['gl:term'])
31
- .map(d => d['gl:term']!);
32
- const definitions = (lc['gl:definition'] || [])
33
- .map(d => d['gl:content'] || '')
24
+ for (const lang of concept.languages) {
25
+ const lc = concept.localization(lang);
26
+ if (!lc) continue;
27
+
28
+ const prefLabels = lc.terms
29
+ .filter(d => d.normativeStatus === 'preferred' && d.designation)
30
+ .map(d => d.designation);
31
+ const altLabels = lc.terms
32
+ .filter(d => d.normativeStatus !== 'preferred' && d.designation)
33
+ .map(d => d.designation);
34
+ const definitions = lc.definitions
35
+ .map(d => d.content || '')
34
36
  .filter(Boolean);
35
- const notes = (lc['gl:notes'] || [])
36
- .map(d => d['gl:content'] || '')
37
+ const notes = lc.notes
38
+ .map(d => d.content || '')
37
39
  .filter(Boolean);
38
40
 
39
41
  if (prefLabels.length || definitions.length) {
@@ -48,9 +50,9 @@ function escapeTurtle(s: string): string {
48
50
  return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
49
51
  }
50
52
 
51
- export function conceptToTurtle(concept: ConceptDocument): string {
52
- const uri = concept['@id'] || '';
53
- const id = concept['gl:identifier'] || '';
53
+ export function conceptToTurtle(concept: Concept): string {
54
+ const uri = concept.uri || '';
55
+ const id = concept.id;
54
56
  const data = getLocalizedData(concept);
55
57
 
56
58
  const lines: string[] = [
@@ -84,9 +86,9 @@ export function conceptToTurtle(concept: ConceptDocument): string {
84
86
  return lines.join('\n');
85
87
  }
86
88
 
87
- export function conceptToSkosJsonLd(concept: ConceptDocument): string {
88
- const uri = concept['@id'] || '';
89
- const id = concept['gl:identifier'] || '';
89
+ export function conceptToSkosJsonLd(concept: Concept): string {
90
+ const uri = concept.uri || '';
91
+ const id = concept.id;
90
92
  const data = getLocalizedData(concept);
91
93
 
92
94
  const doc: Record<string, any> = {
@@ -0,0 +1,54 @@
1
+ import type { LocalizedConcept } from 'glossarist';
2
+ import { ontology } from '../adapters/ontology-registry';
3
+
4
+ export function entryStatusColor(status: string): string {
5
+ const colors: Record<string, string> = {
6
+ valid: 'badge-green',
7
+ not_valid: 'bg-red-50 text-red-700',
8
+ superseded: 'bg-red-50 text-red-700',
9
+ retired: 'badge-gray',
10
+ withdrawn: 'bg-red-100 text-red-800',
11
+ draft: 'badge-yellow',
12
+ Standard: 'badge-green',
13
+ };
14
+ return colors[status] ?? 'badge-gray';
15
+ }
16
+
17
+ export function conceptStatusColor(status: string | null): string {
18
+ if (!status) return 'badge-gray';
19
+ const colors: Record<string, string> = {
20
+ draft: 'badge-yellow',
21
+ submitted: 'badge-blue',
22
+ valid: 'badge-green',
23
+ not_valid: 'bg-red-50 text-red-700',
24
+ invalid: 'bg-red-50 text-red-700',
25
+ superseded: 'bg-red-50 text-red-700',
26
+ retired: 'badge-gray',
27
+ };
28
+ return colors[status] ?? 'badge-gray';
29
+ }
30
+
31
+ export function conceptStatusLabel(status: string | null): string {
32
+ if (!status) return '';
33
+ return ontology.getLabel('conceptStatus', status) || status;
34
+ }
35
+
36
+ export function conceptStatusDefinition(status: string | null): string | null {
37
+ if (!status) return null;
38
+ return ontology.getDefinition('conceptStatus', status);
39
+ }
40
+
41
+ export function entryStatusLabel(status: string | null): string {
42
+ if (!status) return '';
43
+ return ontology.getLabel('entryStatus', status) || status;
44
+ }
45
+
46
+ export function entryStatusDefinition(status: string | null): string | null {
47
+ if (!status) return null;
48
+ return ontology.getDefinition('entryStatus', status);
49
+ }
50
+
51
+ export function getPreferredTerm(lc: LocalizedConcept | null | undefined, fallback = '—'): string {
52
+ if (!lc) return fallback;
53
+ return lc.primaryDesignation ?? lc.terms[0]?.designation ?? fallback;
54
+ }