@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.
- package/README.md +3 -2
- package/cli/index.mjs +2 -1
- package/env.d.ts +5 -0
- package/package.json +4 -3
- package/scripts/build-edges.js +78 -10
- package/scripts/generate-data.mjs +152 -20
- package/scripts/generate-ontology-data.mjs +184 -0
- package/scripts/generate-ontology-schema.mjs +315 -0
- package/src/__tests__/about-view.test.ts +98 -0
- package/src/__tests__/app-footer.test.ts +38 -0
- package/src/__tests__/app-header.test.ts +130 -0
- package/src/__tests__/app-sidebar.test.ts +159 -0
- package/src/__tests__/asciidoc-lite.test.ts +1 -1
- package/src/__tests__/concept-card.test.ts +115 -0
- package/src/__tests__/concept-detail-interaction.test.ts +273 -0
- package/src/__tests__/concept-formats.test.ts +32 -30
- package/src/__tests__/concept-timeline.test.ts +200 -0
- package/src/__tests__/concept-view.test.ts +88 -0
- package/src/__tests__/contributors-view.test.ts +103 -0
- package/src/__tests__/dataset-adapter.test.ts +172 -23
- package/src/__tests__/dataset-view.test.ts +232 -0
- package/src/__tests__/designation-registry.test.ts +161 -0
- package/src/__tests__/format-downloads.test.ts +98 -0
- package/src/__tests__/graph-view.test.ts +69 -0
- package/src/__tests__/graph.test.ts +62 -0
- package/src/__tests__/home-interaction.test.ts +157 -0
- package/src/__tests__/language-detail.test.ts +203 -0
- package/src/__tests__/nav-icon.test.ts +48 -0
- package/src/__tests__/news-view.test.ts +87 -0
- package/src/__tests__/ontology-registry.test.ts +109 -0
- package/src/__tests__/page-view.test.ts +83 -0
- package/src/__tests__/relationship-categories.test.ts +62 -0
- package/src/__tests__/resolve-view.test.ts +77 -0
- package/src/__tests__/router.test.ts +65 -0
- package/src/__tests__/search-bar.test.ts +219 -0
- package/src/__tests__/search-view.test.ts +41 -0
- package/src/__tests__/stats-view.test.ts +77 -0
- package/src/__tests__/test-helpers.ts +171 -0
- package/src/__tests__/ui-store.test.ts +100 -0
- package/src/__tests__/v-math.test.ts +8 -7
- package/src/adapters/DatasetAdapter.ts +188 -63
- package/src/adapters/model-bridge.ts +277 -0
- package/src/adapters/ontology-registry.ts +75 -0
- package/src/adapters/ontology-schema.ts +100 -0
- package/src/adapters/types.ts +53 -78
- package/src/components/AppSidebar.vue +1 -1
- package/src/components/CitationDisplay.vue +35 -0
- package/src/components/ConceptDetail.vue +349 -146
- package/src/components/ConceptRdfView.vue +397 -0
- package/src/components/ConceptTimeline.vue +57 -60
- package/src/components/GraphPanel.vue +96 -31
- package/src/components/LanguageDetail.vue +46 -61
- package/src/components/NavIcon.vue +1 -0
- package/src/components/NonVerbalRepDisplay.vue +38 -0
- package/src/components/RelationshipList.vue +99 -0
- package/src/composables/use-render-options.ts +1 -4
- package/src/config/use-site-config.ts +3 -0
- package/src/data/ontology-schema.json +1551 -0
- package/src/data/taxonomies.json +543 -0
- package/src/graph/GraphEngine.ts +7 -4
- package/src/router/index.ts +6 -1
- package/src/shims/empty.ts +1 -0
- package/src/shims/node-crypto.ts +6 -0
- package/src/shims/node-path.ts +10 -0
- package/src/stores/vocabulary.ts +82 -32
- package/src/style.css +74 -20
- package/src/utils/asciidoc-lite.ts +17 -19
- package/src/utils/concept-formats.ts +22 -20
- package/src/utils/concept-helpers.ts +54 -0
- package/src/utils/designation-registry.ts +124 -0
- package/src/utils/escape.ts +7 -0
- package/src/utils/markdown-lite.ts +1 -3
- package/src/utils/math.ts +2 -11
- package/src/utils/plurimath.ts +2 -7
- package/src/utils/relationship-categories.ts +84 -0
- package/src/views/ConceptView.vue +22 -1
- package/src/views/DatasetView.vue +7 -2
- package/src/views/OntologySchemaView.vue +302 -0
- package/src/views/PageView.vue +28 -17
- package/src/views/StatsView.vue +34 -12
- package/vite.config.ts +8 -0
package/src/stores/vocabulary.ts
CHANGED
|
@@ -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,
|
|
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<
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
264
|
-
const
|
|
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
|
-
|
|
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
|
|
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()
|
|
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: #
|
|
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: #
|
|
307
|
-
.dark .text-ink-600 { color: #
|
|
308
|
-
.dark .text-ink-500 { color: #
|
|
309
|
-
.dark .text-ink-400 { color: #
|
|
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: #
|
|
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: #
|
|
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: #
|
|
353
|
-
.dark .hover\:text-ink-800:hover { color: #
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
126
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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:
|
|
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
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
.map(d => d
|
|
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 =
|
|
36
|
-
.map(d => d
|
|
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:
|
|
52
|
-
const uri = concept
|
|
53
|
-
const id = concept
|
|
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:
|
|
88
|
-
const uri = concept
|
|
89
|
-
const id = concept
|
|
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
|
+
}
|