@glossarist/concept-browser 0.3.3 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/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 +92 -0
- package/src/__tests__/concept-card.test.ts +115 -0
- package/src/__tests__/concept-detail-interaction.test.ts +251 -0
- package/src/__tests__/concept-timeline.test.ts +175 -0
- package/src/__tests__/concept-view.test.ts +75 -0
- package/src/__tests__/contributors-view.test.ts +103 -0
- package/src/__tests__/dataset-view.test.ts +231 -0
- package/src/__tests__/format-downloads.test.ts +98 -0
- package/src/__tests__/graph-view.test.ts +69 -0
- package/src/__tests__/home-interaction.test.ts +157 -0
- package/src/__tests__/language-detail.test.ts +146 -0
- package/src/__tests__/markdown-lite.test.ts +88 -0
- package/src/__tests__/nav-icon.test.ts +48 -0
- package/src/__tests__/news-view.test.ts +87 -0
- package/src/__tests__/page-view.test.ts +83 -0
- package/src/__tests__/plurimath.test.ts +71 -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 +168 -0
- package/src/__tests__/ui-store.test.ts +100 -0
- package/src/__tests__/v-math.test.ts +79 -0
- package/src/adapters/DatasetAdapter.ts +17 -15
- package/src/adapters/types.ts +1 -1
- package/src/components/ConceptDetail.vue +16 -54
- package/src/components/ConceptTimeline.vue +1 -8
- package/src/components/LanguageDetail.vue +2 -25
- package/src/composables/use-render-options.ts +1 -4
- package/src/router/index.ts +1 -1
- package/src/stores/vocabulary.ts +7 -7
- package/src/utils/asciidoc-lite.ts +17 -19
- package/src/utils/concept-helpers.ts +34 -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/views/ConceptView.vue +22 -1
- package/src/views/DatasetView.vue +7 -2
|
@@ -44,13 +44,20 @@ export class DatasetAdapter {
|
|
|
44
44
|
const resp = await fetch(`${this.baseUrl}/index.json`);
|
|
45
45
|
if (!resp.ok) throw new Error(`Failed to load index for ${this.registerId}: ${resp.status}`);
|
|
46
46
|
this.index = (await resp.json()) as ConceptIndex;
|
|
47
|
+
this.buildSummaryIndex();
|
|
48
|
+
return this.index;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private buildSummaryIndex() {
|
|
47
52
|
this.summaryMap.clear();
|
|
48
53
|
this.positionIndex.clear();
|
|
49
|
-
for (let i = 0; i < this.index
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
for (let i = 0; i < this.index!.concepts.length; i++) {
|
|
55
|
+
const entry = this.index!.concepts[i];
|
|
56
|
+
if (entry) {
|
|
57
|
+
this.summaryMap.set(entry.id, entry);
|
|
58
|
+
this.positionIndex.set(entry.id, i);
|
|
59
|
+
}
|
|
52
60
|
}
|
|
53
|
-
return this.index;
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
private async loadIndexChunked(): Promise<ConceptIndex> {
|
|
@@ -62,12 +69,7 @@ export class DatasetAdapter {
|
|
|
62
69
|
const resp = await fetch(`${this.baseUrl}/index.json`);
|
|
63
70
|
if (!resp.ok) throw new Error(`Failed to load index for ${this.registerId}`);
|
|
64
71
|
this.index = (await resp.json()) as ConceptIndex;
|
|
65
|
-
this.
|
|
66
|
-
this.positionIndex.clear();
|
|
67
|
-
for (let i = 0; i < this.index.concepts.length; i++) {
|
|
68
|
-
this.summaryMap.set(this.index.concepts[i].id, this.index.concepts[i]);
|
|
69
|
-
this.positionIndex.set(this.index.concepts[i].id, i);
|
|
70
|
-
}
|
|
72
|
+
this.buildSummaryIndex();
|
|
71
73
|
return this.index;
|
|
72
74
|
}
|
|
73
75
|
|
|
@@ -121,7 +123,7 @@ export class DatasetAdapter {
|
|
|
121
123
|
eng: entry.designations?.eng || Object.values(entry.designations || {})[0] || '',
|
|
122
124
|
status: entry.status,
|
|
123
125
|
};
|
|
124
|
-
|
|
126
|
+
this.index!.concepts[startPos + i] = summary;
|
|
125
127
|
this.summaryMap.set(entry.id, summary);
|
|
126
128
|
this.positionIndex.set(entry.id, startPos + i);
|
|
127
129
|
}
|
|
@@ -153,7 +155,7 @@ export class DatasetAdapter {
|
|
|
153
155
|
|
|
154
156
|
isRangeLoaded(offset: number, limit: number): boolean {
|
|
155
157
|
if (!this.index?.concepts) return false;
|
|
156
|
-
const arr = this.index.concepts
|
|
158
|
+
const arr = this.index.concepts;
|
|
157
159
|
for (let i = offset; i < Math.min(offset + limit, arr.length); i++) {
|
|
158
160
|
if (arr[i] === undefined) return false;
|
|
159
161
|
}
|
|
@@ -175,7 +177,7 @@ export class DatasetAdapter {
|
|
|
175
177
|
return this.summaryMap.get(conceptId);
|
|
176
178
|
}
|
|
177
179
|
|
|
178
|
-
getConcepts(): ConceptSummary[] {
|
|
180
|
+
getConcepts(): (ConceptSummary | undefined)[] {
|
|
179
181
|
return this.index?.concepts ?? [];
|
|
180
182
|
}
|
|
181
183
|
|
|
@@ -188,7 +190,7 @@ export class DatasetAdapter {
|
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
getAdjacentConcepts(conceptId: string): { prev: string | null; next: string | null } {
|
|
191
|
-
const concepts = this.index?.concepts
|
|
193
|
+
const concepts = this.index?.concepts;
|
|
192
194
|
if (!concepts) return { prev: null, next: null };
|
|
193
195
|
const idx = this.getConceptPosition(conceptId);
|
|
194
196
|
if (idx === -1) return { prev: null, next: null };
|
|
@@ -208,7 +210,7 @@ export class DatasetAdapter {
|
|
|
208
210
|
search(query: string, lang: string = 'eng'): SearchHit[] {
|
|
209
211
|
const q = query.toLowerCase();
|
|
210
212
|
const hits: SearchHit[] = [];
|
|
211
|
-
const arr = this.index?.concepts
|
|
213
|
+
const arr = this.index?.concepts;
|
|
212
214
|
if (!arr) return hits;
|
|
213
215
|
|
|
214
216
|
for (const entry of arr) {
|
package/src/adapters/types.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { computed, ref, nextTick, watch } from 'vue';
|
|
|
5
5
|
import { langName, langLabel } from '../utils/lang';
|
|
6
6
|
import { renderMath, cleanContent } from '../utils/math';
|
|
7
7
|
import type { RenderOptions } from '../utils/math';
|
|
8
|
+
import { escapeAttr } from '../utils/escape';
|
|
9
|
+
import { entryStatusColor, designationTypeLabel, designationTypeColor, getPreferredTerm } from '../utils/concept-helpers';
|
|
8
10
|
import { useRouter } from 'vue-router';
|
|
9
11
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
10
12
|
import { useDsStyle } from '../utils/dataset-style';
|
|
@@ -66,27 +68,24 @@ const languages = computed(() => {
|
|
|
66
68
|
});
|
|
67
69
|
});
|
|
68
70
|
|
|
69
|
-
//
|
|
71
|
+
// Collapsible language sections — auto-collapse non-eng when 6+ languages
|
|
72
|
+
const collapsedLangs = ref(new Set<string>());
|
|
73
|
+
|
|
74
|
+
function initCollapsed(langs: string[]) {
|
|
75
|
+
if (langs.length >= 6) {
|
|
76
|
+
collapsedLangs.value = new Set(langs.filter(l => l !== 'eng'));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
watch(languages, (langs) => { initCollapsed(langs); }, { immediate: true });
|
|
71
81
|
|
|
72
82
|
const engConcept = computed((): LocalizedConcept | null => {
|
|
73
83
|
return props.concept['gl:localizedConcept']?.['eng'] ?? null;
|
|
74
84
|
});
|
|
75
85
|
|
|
76
|
-
const primaryTerm = computed(() =>
|
|
77
|
-
const eng = engConcept.value;
|
|
78
|
-
if (!eng?.['gl:designation']?.length) return conceptId.value;
|
|
79
|
-
const desigs = eng['gl:designation'];
|
|
80
|
-
const preferredExpr = desigs.find(d => d['gl:normativeStatus'] === 'preferred' && d['@type'] === 'gl:Expression');
|
|
81
|
-
if (preferredExpr) return preferredExpr['gl:term'];
|
|
82
|
-
const preferred = desigs.find(d => d['gl:normativeStatus'] === 'preferred');
|
|
83
|
-
return preferred?.['gl:term'] ?? desigs[0]?.['gl:term'] ?? conceptId.value;
|
|
84
|
-
});
|
|
86
|
+
const primaryTerm = computed(() => getPreferredTerm(engConcept.value, conceptId.value));
|
|
85
87
|
|
|
86
88
|
// Cross-reference resolver: generates clickable links for inline refs
|
|
87
|
-
function escapeAttr(s: string) {
|
|
88
|
-
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
89
|
-
}
|
|
90
89
|
|
|
91
90
|
const { ensureBibLoaded, bibResolver, figResolver } = useRenderOptions(() => props.registerId);
|
|
92
91
|
|
|
@@ -158,15 +157,6 @@ const allLangContent = computed(() => {
|
|
|
158
157
|
return result;
|
|
159
158
|
});
|
|
160
159
|
|
|
161
|
-
// Collapsible language sections — auto-collapse non-eng when 6+ languages
|
|
162
|
-
const collapsedLangs = ref(new Set<string>());
|
|
163
|
-
|
|
164
|
-
function initCollapsed(langs: string[]) {
|
|
165
|
-
if (langs.length >= 6) {
|
|
166
|
-
collapsedLangs.value = new Set(langs.filter(l => l !== 'eng'));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
160
|
function hasContent(lc: LangContent): boolean {
|
|
171
161
|
return !!(lc.definition || lc.notes.length || lc.examples.length || lc.sources.length);
|
|
172
162
|
}
|
|
@@ -256,12 +246,7 @@ async function navigateEdge(edge: GraphEdge) {
|
|
|
256
246
|
|
|
257
247
|
function getTermForLang(lang: string): string {
|
|
258
248
|
const lc = props.concept['gl:localizedConcept']?.[lang];
|
|
259
|
-
|
|
260
|
-
const desigs = lc['gl:designation'];
|
|
261
|
-
const preferredExpr = desigs.find(d => d['gl:normativeStatus'] === 'preferred' && d['@type'] === 'gl:Expression');
|
|
262
|
-
if (preferredExpr) return preferredExpr['gl:term'];
|
|
263
|
-
const preferred = desigs.find(d => d['gl:normativeStatus'] === 'preferred');
|
|
264
|
-
return preferred?.['gl:term'] ?? desigs[0]?.['gl:term'] ?? '\u2014';
|
|
249
|
+
return getPreferredTerm(lc);
|
|
265
250
|
}
|
|
266
251
|
|
|
267
252
|
function getDesignationsForLang(lang: string) {
|
|
@@ -283,32 +268,9 @@ function hasDefinition(lang: string): boolean {
|
|
|
283
268
|
return lc['gl:definition']?.some((d: any) => d['gl:content']) ?? false;
|
|
284
269
|
}
|
|
285
270
|
|
|
286
|
-
function designationTypeLabel(type: string): string {
|
|
287
|
-
const labels: Record<string, string> = {
|
|
288
|
-
'gl:Expression': 'Expression',
|
|
289
|
-
'gl:Symbol': 'Symbol',
|
|
290
|
-
'gl:Abbreviation': 'Abbreviation',
|
|
291
|
-
'gl:GraphicalSymbol': 'Graphical',
|
|
292
|
-
};
|
|
293
|
-
return labels[type] ?? type;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function designationTypeColor(type: string): string {
|
|
297
|
-
if (type === 'gl:Symbol') return 'badge-purple';
|
|
298
|
-
if (type === 'gl:Abbreviation') return 'badge-yellow';
|
|
299
|
-
return 'badge-blue';
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function entryStatusColor(status: string): string {
|
|
303
|
-
if (status === 'valid' || status === 'Standard') return 'badge-green';
|
|
304
|
-
if (status === 'superseded') return 'bg-red-50 text-red-700';
|
|
305
|
-
if (status === 'withdrawn') return 'bg-red-100 text-red-800';
|
|
306
|
-
if (status === 'draft') return 'badge-yellow';
|
|
307
|
-
return 'badge-gray';
|
|
308
|
-
}
|
|
309
|
-
|
|
310
271
|
function goAdjacent(id: string) {
|
|
311
272
|
router.push({ name: 'concept', params: { registerId: props.registerId, conceptId: id } });
|
|
273
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
312
274
|
}
|
|
313
275
|
|
|
314
276
|
function plainTruncate(html: string, max: number = 120): string {
|
|
@@ -339,7 +301,7 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
339
301
|
v-if="adjacent.prev"
|
|
340
302
|
@click="goAdjacent(adjacent.prev)"
|
|
341
303
|
class="p-1.5 rounded-md text-ink-300 hover:text-ink-600 hover:bg-ink-50 transition-colors"
|
|
342
|
-
title="Previous concept"
|
|
304
|
+
title="Previous concept (←)"
|
|
343
305
|
>
|
|
344
306
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
|
|
345
307
|
</button>
|
|
@@ -347,7 +309,7 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
347
309
|
v-if="adjacent.next"
|
|
348
310
|
@click="goAdjacent(adjacent.next)"
|
|
349
311
|
class="p-1.5 rounded-md text-ink-300 hover:text-ink-600 hover:bg-ink-50 transition-colors"
|
|
350
|
-
title="Next concept"
|
|
312
|
+
title="Next concept (→)"
|
|
351
313
|
>
|
|
352
314
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
|
353
315
|
</button>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { LocalizedConcept } from '../adapters/types';
|
|
3
3
|
import { computed } from 'vue';
|
|
4
4
|
import { langName, langLabel } from '../utils/lang';
|
|
5
|
+
import { entryStatusColor } from '../utils/concept-helpers';
|
|
5
6
|
|
|
6
7
|
const props = defineProps<{
|
|
7
8
|
localizedConcepts: Record<string, LocalizedConcept>;
|
|
@@ -216,14 +217,6 @@ function eventRingColor(type: string): string {
|
|
|
216
217
|
return colors[type] || 'ring-ink-100';
|
|
217
218
|
}
|
|
218
219
|
|
|
219
|
-
function entryStatusColor(status: string): string {
|
|
220
|
-
if (status === 'valid' || status === 'Standard') return 'badge-green';
|
|
221
|
-
if (status === 'superseded') return 'bg-red-50 text-red-700';
|
|
222
|
-
if (status === 'withdrawn') return 'bg-red-100 text-red-800';
|
|
223
|
-
if (status === 'draft') return 'badge-yellow';
|
|
224
|
-
return 'badge-gray';
|
|
225
|
-
}
|
|
226
|
-
|
|
227
220
|
function eventIconPath(type: string): string {
|
|
228
221
|
// Returns an SVG path for the event type icon
|
|
229
222
|
switch (type) {
|
|
@@ -4,6 +4,8 @@ import { computed } from 'vue';
|
|
|
4
4
|
import { langName, langLabel } from '../utils/lang';
|
|
5
5
|
import { renderMath } from '../utils/math';
|
|
6
6
|
import type { RenderOptions } from '../utils/math';
|
|
7
|
+
import { escapeAttr } from '../utils/escape';
|
|
8
|
+
import { entryStatusColor, designationTypeLabel, designationTypeColor } from '../utils/concept-helpers';
|
|
7
9
|
import { useRouter } from 'vue-router';
|
|
8
10
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
9
11
|
import { getFactory } from '../adapters/factory';
|
|
@@ -51,35 +53,10 @@ function normativeColor(status: string): string {
|
|
|
51
53
|
if (status === 'deprecated') return 'bg-red-50 text-red-700';
|
|
52
54
|
return 'bg-amber-50 text-amber-700';
|
|
53
55
|
}
|
|
54
|
-
function entryStatusColor(status: string): string {
|
|
55
|
-
if (status === 'valid' || status === 'Standard') return 'badge-green';
|
|
56
|
-
if (status === 'superseded') return 'bg-red-50 text-red-700';
|
|
57
|
-
if (status === 'withdrawn') return 'bg-red-100 text-red-800';
|
|
58
|
-
if (status === 'draft') return 'badge-yellow';
|
|
59
|
-
return 'badge-gray';
|
|
60
|
-
}
|
|
61
|
-
function designationTypeLabel(type: string): string {
|
|
62
|
-
const labels: Record<string, string> = {
|
|
63
|
-
'gl:Expression': 'Expression',
|
|
64
|
-
'gl:Symbol': 'Symbol',
|
|
65
|
-
'gl:Abbreviation': 'Abbreviation',
|
|
66
|
-
'gl:GraphicalSymbol': 'Graphical',
|
|
67
|
-
};
|
|
68
|
-
return labels[type] ?? type;
|
|
69
|
-
}
|
|
70
|
-
function designationTypeColor(type: string): string {
|
|
71
|
-
if (type === 'gl:Symbol') return 'badge-purple';
|
|
72
|
-
if (type === 'gl:Abbreviation') return 'badge-yellow';
|
|
73
|
-
return 'badge-blue';
|
|
74
|
-
}
|
|
75
56
|
|
|
76
57
|
const router = useRouter();
|
|
77
58
|
const store = useVocabularyStore();
|
|
78
59
|
|
|
79
|
-
function escapeAttr(s: string) {
|
|
80
|
-
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
60
|
const factory = getFactory();
|
|
84
61
|
|
|
85
62
|
const renderOpts: RenderOptions = {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ref, watch } from 'vue';
|
|
2
2
|
import type { RenderOptions, BibResolver, FigResolver } from '../utils/math';
|
|
3
3
|
import { getFactory } from '../adapters/factory';
|
|
4
|
+
import { escapeAttr } from '../utils/escape';
|
|
4
5
|
|
|
5
6
|
interface BibEntry {
|
|
6
7
|
reference: string;
|
|
@@ -52,7 +53,3 @@ export function useRenderOptions(registerId: () => string) {
|
|
|
52
53
|
|
|
53
54
|
return { bibData, ensureBibLoaded, bibResolver, figResolver };
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
-
function escapeAttr(s: string) {
|
|
57
|
-
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
58
|
-
}
|
package/src/router/index.ts
CHANGED
package/src/stores/vocabulary.ts
CHANGED
|
@@ -58,8 +58,8 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
initialized.value = true;
|
|
61
|
-
} catch (e:
|
|
62
|
-
error.value = `Failed to discover datasets: ${e.message}`;
|
|
61
|
+
} catch (e: unknown) {
|
|
62
|
+
error.value = `Failed to discover datasets: ${e instanceof Error ? e.message : String(e)}`;
|
|
63
63
|
} finally {
|
|
64
64
|
loading.value = false;
|
|
65
65
|
}
|
|
@@ -81,8 +81,8 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
|
|
|
81
81
|
|
|
82
82
|
// Seed graph nodes lazily — don't block UI for large datasets
|
|
83
83
|
seedGraphNodes(registerId, adapter);
|
|
84
|
-
} catch (e:
|
|
85
|
-
error.value = `Failed to load dataset ${registerId}: ${e.message}`;
|
|
84
|
+
} catch (e: unknown) {
|
|
85
|
+
error.value = `Failed to load dataset ${registerId}: ${e instanceof Error ? e.message : String(e)}`;
|
|
86
86
|
throw e;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -234,8 +234,8 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
|
|
|
234
234
|
|
|
235
235
|
touchGraph();
|
|
236
236
|
conceptEdges.value = graph.value.getEdges(uri);
|
|
237
|
-
} catch (e:
|
|
238
|
-
error.value = `Failed to load concept ${conceptId}: ${e.message}`;
|
|
237
|
+
} catch (e: unknown) {
|
|
238
|
+
error.value = `Failed to load concept ${conceptId}: ${e instanceof Error ? e.message : String(e)}`;
|
|
239
239
|
currentConcept.value = null;
|
|
240
240
|
throw e;
|
|
241
241
|
}
|
|
@@ -275,7 +275,7 @@ export const useVocabularyStore = defineStore('vocabulary', () => {
|
|
|
275
275
|
const loaded = [...datasets.value.values()].filter(a => a.index);
|
|
276
276
|
if (!loaded.length) return null;
|
|
277
277
|
const adapter = loaded[Math.floor(Math.random() * loaded.length)];
|
|
278
|
-
const concepts = adapter.getConcepts()
|
|
278
|
+
const concepts = adapter.getConcepts();
|
|
279
279
|
const dense = concepts.filter((c): c is import('../adapters/types').ConceptSummary => c != null);
|
|
280
280
|
if (!dense.length) return null;
|
|
281
281
|
const pick = dense[Math.floor(Math.random() * dense.length)];
|
|
@@ -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
|
-
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Designation, LocalizedConcept } from '../adapters/types';
|
|
2
|
+
|
|
3
|
+
export function entryStatusColor(status: string): string {
|
|
4
|
+
if (status === 'valid' || status === 'Standard') return 'badge-green';
|
|
5
|
+
if (status === 'superseded') return 'bg-red-50 text-red-700';
|
|
6
|
+
if (status === 'withdrawn') return 'bg-red-100 text-red-800';
|
|
7
|
+
if (status === 'draft') return 'badge-yellow';
|
|
8
|
+
return 'badge-gray';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function designationTypeLabel(type: string): string {
|
|
12
|
+
const labels: Record<string, string> = {
|
|
13
|
+
'gl:Expression': 'Expression',
|
|
14
|
+
'gl:Symbol': 'Symbol',
|
|
15
|
+
'gl:Abbreviation': 'Abbreviation',
|
|
16
|
+
'gl:GraphicalSymbol': 'Graphical',
|
|
17
|
+
};
|
|
18
|
+
return labels[type] ?? type;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function designationTypeColor(type: string): string {
|
|
22
|
+
if (type === 'gl:Symbol') return 'badge-purple';
|
|
23
|
+
if (type === 'gl:Abbreviation') return 'badge-yellow';
|
|
24
|
+
return 'badge-blue';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getPreferredTerm(lc: LocalizedConcept | null | undefined, fallback = '—'): string {
|
|
28
|
+
if (!lc?.['gl:designation']?.length) return fallback;
|
|
29
|
+
const desigs = lc['gl:designation'];
|
|
30
|
+
const preferredExpr = desigs.find(d => d['gl:normativeStatus'] === 'preferred' && d['@type'] === 'gl:Expression');
|
|
31
|
+
if (preferredExpr) return preferredExpr['gl:term'];
|
|
32
|
+
const preferred = desigs.find(d => d['gl:normativeStatus'] === 'preferred');
|
|
33
|
+
return preferred?.['gl:term'] ?? desigs[0]?.['gl:term'] ?? fallback;
|
|
34
|
+
}
|
|
@@ -104,6 +104,4 @@ export function renderMarkdown(input: string): string {
|
|
|
104
104
|
return blocks.join('\n');
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
109
|
-
}
|
|
107
|
+
import { escapeHtml } from './escape';
|
package/src/utils/math.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escapeHtml, escapeAttr } from './escape';
|
|
2
|
+
|
|
1
3
|
export type XrefResolver = (uri: string, term: string) => string;
|
|
2
4
|
export type BibResolver = (refId: string, title: string) => string;
|
|
3
5
|
export type FigResolver = (figId: string) => string;
|
|
@@ -8,10 +10,6 @@ export interface RenderOptions {
|
|
|
8
10
|
figResolver?: FigResolver;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
function escapeAttr(s: string): string {
|
|
12
|
-
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
13
|
function replaceBracketed(text: string, prefix: string, handler: (content: string, bold: boolean) => string): string {
|
|
16
14
|
let result = '';
|
|
17
15
|
let i = 0;
|
|
@@ -84,13 +82,6 @@ function convertLists(text: string): string {
|
|
|
84
82
|
return result;
|
|
85
83
|
}
|
|
86
84
|
|
|
87
|
-
function escapeHtml(text: string): string {
|
|
88
|
-
return text
|
|
89
|
-
.replace(/&/g, '&')
|
|
90
|
-
.replace(/</g, '<')
|
|
91
|
-
.replace(/>/g, '>');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
85
|
export function renderMath(text: string, xrefResolverOrOpts?: XrefResolver | RenderOptions): string {
|
|
95
86
|
if (!text) return '';
|
|
96
87
|
let result = text;
|
package/src/utils/plurimath.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escapeHtml } from './escape';
|
|
2
|
+
|
|
1
3
|
type PlurimathCtor = new (data: string, format: string) => {
|
|
2
4
|
toAsciimath(): string;
|
|
3
5
|
toLatex(): string;
|
|
@@ -30,13 +32,6 @@ export function renderToMathML(expr: string, format: string): string | null {
|
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
function escapeHtml(text: string): string {
|
|
34
|
-
return text
|
|
35
|
-
.replace(/&/g, '&')
|
|
36
|
-
.replace(/</g, '<')
|
|
37
|
-
.replace(/>/g, '>');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
35
|
export function mathToHtml(expr: string, format: string, bold: boolean): string {
|
|
41
36
|
const mathml = renderToMathML(expr, format);
|
|
42
37
|
if (mathml) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, watch, ref } from 'vue';
|
|
2
|
+
import { computed, watch, ref, onMounted, onUnmounted } from 'vue';
|
|
3
|
+
import { useRouter } from 'vue-router';
|
|
3
4
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
4
5
|
import ConceptDetail from '../components/ConceptDetail.vue';
|
|
5
6
|
|
|
@@ -9,6 +10,7 @@ const props = defineProps<{
|
|
|
9
10
|
}>();
|
|
10
11
|
|
|
11
12
|
const store = useVocabularyStore();
|
|
13
|
+
const router = useRouter();
|
|
12
14
|
const conceptLoading = ref(false);
|
|
13
15
|
const localError = ref<string | null>(null);
|
|
14
16
|
|
|
@@ -55,6 +57,25 @@ async function loadAdjacent() {
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
watch(() => props.conceptId, () => { loadAdjacent(); });
|
|
60
|
+
|
|
61
|
+
function goAdjacent(id: string) {
|
|
62
|
+
router.push({ name: 'concept', params: { registerId: props.registerId, conceptId: id } });
|
|
63
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function onKeydown(e: KeyboardEvent) {
|
|
67
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
|
68
|
+
if (e.key === 'ArrowLeft' && adjacent.value.prev) {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
goAdjacent(adjacent.value.prev);
|
|
71
|
+
} else if (e.key === 'ArrowRight' && adjacent.value.next) {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
goAdjacent(adjacent.value.next);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
onMounted(() => window.addEventListener('keydown', onKeydown));
|
|
78
|
+
onUnmounted(() => window.removeEventListener('keydown', onKeydown));
|
|
58
79
|
</script>
|
|
59
80
|
|
|
60
81
|
<template>
|
|
@@ -44,6 +44,11 @@ function onGlobalKeydown(e: KeyboardEvent) {
|
|
|
44
44
|
e.preventDefault();
|
|
45
45
|
filterInput.value?.focus();
|
|
46
46
|
}
|
|
47
|
+
if (e.key === 'ArrowRight' && document.activeElement?.tagName !== 'INPUT' && page.value < totalPages.value) {
|
|
48
|
+
goToPage(page.value + 1);
|
|
49
|
+
} else if (e.key === 'ArrowLeft' && document.activeElement?.tagName !== 'INPUT' && page.value > 1) {
|
|
50
|
+
goToPage(page.value - 1);
|
|
51
|
+
}
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
onMounted(() => window.addEventListener('keydown', onGlobalKeydown));
|
|
@@ -62,7 +67,7 @@ watch(filter, async (q) => {
|
|
|
62
67
|
|
|
63
68
|
// Dense array: only loaded (non-undefined) entries
|
|
64
69
|
const loadedConcepts = computed(() => {
|
|
65
|
-
const arr = adapter.value?.getConcepts()
|
|
70
|
+
const arr = adapter.value?.getConcepts();
|
|
66
71
|
if (!arr) return [];
|
|
67
72
|
return arr.filter((c): c is import('../adapters/types').ConceptSummary => c != null);
|
|
68
73
|
});
|
|
@@ -93,7 +98,7 @@ const paged = computed(() => {
|
|
|
93
98
|
}
|
|
94
99
|
// When not filtering, slice directly from the pre-allocated index (may contain undefined)
|
|
95
100
|
const start = (page.value - 1) * perPage;
|
|
96
|
-
const arr = adapter.value?.getConcepts()
|
|
101
|
+
const arr = adapter.value?.getConcepts();
|
|
97
102
|
if (!arr) return [];
|
|
98
103
|
return arr.slice(start, start + perPage).filter((c): c is import('../adapters/types').ConceptSummary => c != null);
|
|
99
104
|
});
|