@glossarist/concept-browser 0.3.2 → 0.3.3
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/scripts/generate-data.mjs +3 -4
- package/src/__tests__/math.test.ts +62 -0
- package/src/components/ConceptDetail.vue +1 -1
- package/src/components/LanguageDetail.vue +1 -1
- package/src/directives/v-math.ts +33 -0
- package/src/main.ts +2 -0
- package/src/utils/math.ts +62 -10
- package/src/utils/plurimath.ts +46 -0
- package/src/views/HomeView.vue +74 -55
- package/scripts/math-prerender.mjs +0 -80
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -3,7 +3,6 @@ import path from 'path';
|
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
import { naturalSort } from 'glossarist';
|
|
5
5
|
import { loadSiteConfig } from './load-site-config.mjs';
|
|
6
|
-
import { preRenderMath } from './math-prerender.mjs';
|
|
7
6
|
|
|
8
7
|
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
9
8
|
const ROOT = process.cwd();
|
|
@@ -56,7 +55,7 @@ function termToDesignation(term) {
|
|
|
56
55
|
: term.type === 'abbreviation' ? 'gl:Abbreviation'
|
|
57
56
|
: 'gl:Designation',
|
|
58
57
|
'gl:normativeStatus': term.normative_status || 'preferred',
|
|
59
|
-
'gl:term':
|
|
58
|
+
'gl:term': term.designation,
|
|
60
59
|
};
|
|
61
60
|
if (term.gender) doc['gl:gender'] = term.gender;
|
|
62
61
|
if (term.plurality) doc['gl:plurality'] = term.plurality;
|
|
@@ -69,7 +68,7 @@ function defsToJsonLd(defs) {
|
|
|
69
68
|
return defs
|
|
70
69
|
.map(d => ({
|
|
71
70
|
'@type': 'gl:DetailedDefinition',
|
|
72
|
-
'gl:content':
|
|
71
|
+
'gl:content': d.content || '',
|
|
73
72
|
}))
|
|
74
73
|
.filter(d => d['gl:content']);
|
|
75
74
|
}
|
|
@@ -256,7 +255,7 @@ function getPrimaryDesignation(conceptYaml) {
|
|
|
256
255
|
if (lc && lc.terms && lc.terms.length > 0) {
|
|
257
256
|
const preferredExpr = lc.terms.find(t => t.normative_status === 'preferred' && t.type === 'expression');
|
|
258
257
|
const preferred = preferredExpr || lc.terms.find(t => t.normative_status === 'preferred') || lc.terms[0];
|
|
259
|
-
descs[lang] =
|
|
258
|
+
descs[lang] = preferred.designation;
|
|
260
259
|
}
|
|
261
260
|
}
|
|
262
261
|
return descs;
|
|
@@ -100,6 +100,52 @@ describe('renderMath', () => {
|
|
|
100
100
|
expect(renderMath(null as any)).toBe('');
|
|
101
101
|
expect(renderMath(undefined as any)).toBe('');
|
|
102
102
|
});
|
|
103
|
+
|
|
104
|
+
// stem:[...] placeholder tests
|
|
105
|
+
it('outputs math-pending placeholder for stem:[expr]', () => {
|
|
106
|
+
const result = renderMath('value stem:[x^2] here');
|
|
107
|
+
expect(result).toContain('class="math-pending"');
|
|
108
|
+
expect(result).toContain('data-expr="x^2"');
|
|
109
|
+
expect(result).toContain('data-format="asciimath"');
|
|
110
|
+
expect(result).toContain('x^2');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('outputs math-pending with math-bold for *stem:[expr]', () => {
|
|
114
|
+
const result = renderMath('*stem:[alpha]');
|
|
115
|
+
expect(result).toContain('class="math-pending math-bold"');
|
|
116
|
+
expect(result).toContain('data-expr="alpha"');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('outputs math-pending placeholder for latexmath:[expr]', () => {
|
|
120
|
+
const result = renderMath('equation latexmath:[\\frac{a}{b}] end');
|
|
121
|
+
expect(result).toContain('class="math-pending"');
|
|
122
|
+
expect(result).toContain('data-expr="\\frac{a}{b}"');
|
|
123
|
+
expect(result).toContain('data-format="latex"');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('handles multiple stem: expressions in one string', () => {
|
|
127
|
+
const result = renderMath('stem:[m] out of stem:[n] redundancy');
|
|
128
|
+
const matches = result.match(/class="math-pending"/g);
|
|
129
|
+
expect(matches).toHaveLength(2);
|
|
130
|
+
expect(result).toContain('data-expr="m"');
|
|
131
|
+
expect(result).toContain('data-expr="n"');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('handles nested brackets in stem:', () => {
|
|
135
|
+
const result = renderMath('stem:[a_[i]]');
|
|
136
|
+
expect(result).toContain('data-expr="a_[i]"');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('handles stem: in designation text', () => {
|
|
140
|
+
const result = renderMath('stem:[n]-ary digit');
|
|
141
|
+
expect(result).toContain('data-expr="n"');
|
|
142
|
+
expect(result).toContain('-ary digit');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('escapes special HTML in stem: expressions', () => {
|
|
146
|
+
const result = renderMath('stem:[a<b]');
|
|
147
|
+
expect(result).toContain('data-expr="a<b"');
|
|
148
|
+
});
|
|
103
149
|
});
|
|
104
150
|
|
|
105
151
|
describe('cleanContent', () => {
|
|
@@ -135,4 +181,20 @@ describe('cleanContent', () => {
|
|
|
135
181
|
expect(cleanContent('')).toBe('');
|
|
136
182
|
expect(cleanContent(null as any)).toBe('');
|
|
137
183
|
});
|
|
184
|
+
|
|
185
|
+
it('strips stem: notation to plain expression', () => {
|
|
186
|
+
expect(cleanContent('stem:[x^2]')).toBe('x^2');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('strips *stem: bold notation', () => {
|
|
190
|
+
expect(cleanContent('*stem:[alpha]')).toBe('alpha');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('strips latexmath: notation', () => {
|
|
194
|
+
expect(cleanContent('latexmath:[\\frac{a}{b}]')).toBe('\\frac{a}{b}');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('strips stem: in running text', () => {
|
|
198
|
+
expect(cleanContent('value stem:[m] out of stem:[n]')).toBe('value m out of n');
|
|
199
|
+
});
|
|
138
200
|
});
|
|
@@ -318,7 +318,7 @@ function plainTruncate(html: string, max: number = 120): string {
|
|
|
318
318
|
</script>
|
|
319
319
|
|
|
320
320
|
<template>
|
|
321
|
-
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
321
|
+
<div v-math class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
322
322
|
<!-- Header -->
|
|
323
323
|
<div class="mb-6">
|
|
324
324
|
<!-- Breadcrumb + nav row -->
|
|
@@ -110,7 +110,7 @@ function handleContentClick(e: MouseEvent) {
|
|
|
110
110
|
</script>
|
|
111
111
|
|
|
112
112
|
<template>
|
|
113
|
-
<div class="space-y-5" @click="handleContentClick">
|
|
113
|
+
<div v-math class="space-y-5" @click="handleContentClick">
|
|
114
114
|
<!-- Language selector -->
|
|
115
115
|
<div class="flex flex-wrap gap-1.5">
|
|
116
116
|
<button
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Directive } from 'vue';
|
|
2
|
+
import { loadPlurimath, mathToHtml } from '../utils/plurimath';
|
|
3
|
+
|
|
4
|
+
let loaded = false;
|
|
5
|
+
|
|
6
|
+
function upgrade(el: HTMLElement) {
|
|
7
|
+
const pending = el.querySelectorAll('.math-pending');
|
|
8
|
+
if (!pending.length) return;
|
|
9
|
+
|
|
10
|
+
if (!loaded) {
|
|
11
|
+
loadPlurimath().then(() => {
|
|
12
|
+
loaded = true;
|
|
13
|
+
upgrade(el);
|
|
14
|
+
});
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pending.forEach((span) => {
|
|
19
|
+
const expr = (span as HTMLElement).dataset.expr;
|
|
20
|
+
const format = (span as HTMLElement).dataset.format || 'asciimath';
|
|
21
|
+
const bold = span.classList.contains('math-bold');
|
|
22
|
+
if (!expr) return;
|
|
23
|
+
const html = mathToHtml(expr, format, bold);
|
|
24
|
+
const wrapper = document.createElement('span');
|
|
25
|
+
wrapper.innerHTML = html;
|
|
26
|
+
span.replaceWith(wrapper.firstElementChild || wrapper);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const vMath: Directive<HTMLElement> = {
|
|
31
|
+
updated(el) { upgrade(el); },
|
|
32
|
+
mounted(el) { upgrade(el); },
|
|
33
|
+
};
|
package/src/main.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { createApp } from 'vue';
|
|
|
2
2
|
import { createPinia } from 'pinia';
|
|
3
3
|
import App from './App.vue';
|
|
4
4
|
import router from './router';
|
|
5
|
+
import { vMath } from './directives/v-math';
|
|
5
6
|
import './style.css';
|
|
6
7
|
|
|
7
8
|
const app = createApp(App);
|
|
8
9
|
app.use(createPinia());
|
|
9
10
|
app.use(router);
|
|
11
|
+
app.directive('math', vMath);
|
|
10
12
|
app.mount('#app');
|
package/src/utils/math.ts
CHANGED
|
@@ -8,6 +8,53 @@ export interface RenderOptions {
|
|
|
8
8
|
figResolver?: FigResolver;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function escapeAttr(s: string): string {
|
|
12
|
+
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function replaceBracketed(text: string, prefix: string, handler: (content: string, bold: boolean) => string): string {
|
|
16
|
+
let result = '';
|
|
17
|
+
let i = 0;
|
|
18
|
+
const boldPrefix = '*' + prefix;
|
|
19
|
+
while (i < text.length) {
|
|
20
|
+
if (text.startsWith(boldPrefix + '[', i)) {
|
|
21
|
+
i += boldPrefix.length + 1;
|
|
22
|
+
let j = i;
|
|
23
|
+
let d = 1;
|
|
24
|
+
while (j < text.length && d > 0) {
|
|
25
|
+
if (text[j] === '[') d++;
|
|
26
|
+
else if (text[j] === ']') d--;
|
|
27
|
+
j++;
|
|
28
|
+
}
|
|
29
|
+
const content = text.slice(i, j - 1);
|
|
30
|
+
let end = j;
|
|
31
|
+
if (end < text.length && text[end] === '*') end++;
|
|
32
|
+
result += handler(content, true);
|
|
33
|
+
i = end;
|
|
34
|
+
} else if (text.startsWith(prefix + '[', i)) {
|
|
35
|
+
i += prefix.length + 1;
|
|
36
|
+
let j = i;
|
|
37
|
+
let d = 1;
|
|
38
|
+
while (j < text.length && d > 0) {
|
|
39
|
+
if (text[j] === '[') d++;
|
|
40
|
+
else if (text[j] === ']') d--;
|
|
41
|
+
j++;
|
|
42
|
+
}
|
|
43
|
+
const content = text.slice(i, j - 1);
|
|
44
|
+
result += handler(content, false);
|
|
45
|
+
i = j;
|
|
46
|
+
} else {
|
|
47
|
+
result += text[i];
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function mathPlaceholder(expr: string, format: string, bold: boolean): string {
|
|
55
|
+
return `<span class="math-pending${bold ? ' math-bold' : ''}" data-expr="${escapeAttr(expr)}" data-format="${format}">${escapeAttr(expr)}</span>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
11
58
|
function convertLists(text: string): string {
|
|
12
59
|
let result = text.replace(/(?:^|\n)((?:[ \t]*\* [^\n]+)(?:\n[ \t]*\* [^\n]+)*)/g, (_, block) => {
|
|
13
60
|
if (/^\*stem:\[/.test(block.trimStart())) return _;
|
|
@@ -37,6 +84,13 @@ function convertLists(text: string): string {
|
|
|
37
84
|
return result;
|
|
38
85
|
}
|
|
39
86
|
|
|
87
|
+
function escapeHtml(text: string): string {
|
|
88
|
+
return text
|
|
89
|
+
.replace(/&/g, '&')
|
|
90
|
+
.replace(/</g, '<')
|
|
91
|
+
.replace(/>/g, '>');
|
|
92
|
+
}
|
|
93
|
+
|
|
40
94
|
export function renderMath(text: string, xrefResolverOrOpts?: XrefResolver | RenderOptions): string {
|
|
41
95
|
if (!text) return '';
|
|
42
96
|
let result = text;
|
|
@@ -45,7 +99,10 @@ export function renderMath(text: string, xrefResolverOrOpts?: XrefResolver | Ren
|
|
|
45
99
|
? { xrefResolver: xrefResolverOrOpts }
|
|
46
100
|
: (xrefResolverOrOpts ?? {});
|
|
47
101
|
|
|
48
|
-
// Math
|
|
102
|
+
// Math expressions: output placeholders for v-math directive to upgrade
|
|
103
|
+
result = replaceBracketed(result, 'stem:', (expr, bold) => mathPlaceholder(expr, 'asciimath', bold));
|
|
104
|
+
result = replaceBracketed(result, 'latexmath:', (expr, bold) => mathPlaceholder(expr, 'latex', bold));
|
|
105
|
+
|
|
49
106
|
result = convertLists(result);
|
|
50
107
|
result = result.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
51
108
|
result = result.replace(/~([^~]+)~/g, '<sub>$1</sub>');
|
|
@@ -85,17 +142,10 @@ export function renderMath(text: string, xrefResolverOrOpts?: XrefResolver | Ren
|
|
|
85
142
|
return result;
|
|
86
143
|
}
|
|
87
144
|
|
|
88
|
-
function escapeHtml(text: string): string {
|
|
89
|
-
return text
|
|
90
|
-
.replace(/&/g, '&')
|
|
91
|
-
.replace(/</g, '<')
|
|
92
|
-
.replace(/>/g, '>');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
145
|
export function cleanContent(text: string): string {
|
|
96
146
|
if (!text) return '';
|
|
97
147
|
let result = text
|
|
98
|
-
.replace(/<[^>]+>/g, '')
|
|
148
|
+
.replace(/<[^>]+>/g, '')
|
|
99
149
|
.replace(/\*([^*]+)\*/g, '$1')
|
|
100
150
|
.replace(/~([^~]+)~/g, '_$1')
|
|
101
151
|
.replace(/\n[ \t]*\* /g, '; ')
|
|
@@ -103,6 +153,8 @@ export function cleanContent(text: string): string {
|
|
|
103
153
|
.replace(/<<(fig_[^>]+)>>/g, '$1')
|
|
104
154
|
.replace(/\{\{urn:[^,}]+,([^,}]+)(?:,[^}]+)?\}\}/g, '$1')
|
|
105
155
|
.replace(/\{urn:[^,}]+,([^,}]+)(?:,[^}]+)?\}/g, '$1')
|
|
106
|
-
.replace(/\{\{([^,}]+)(?:,\s*[^}]+)?\}\}/g, '$1')
|
|
156
|
+
.replace(/\{\{([^,}]+)(?:,\s*[^}]+)?\}\}/g, '$1')
|
|
157
|
+
.replace(/(?:\*?)stem:\[([^\]]*)\]/g, '$1')
|
|
158
|
+
.replace(/(?:\*?)latexmath:\[([^\]]*)\]/g, '$1');
|
|
107
159
|
return result;
|
|
108
160
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
type PlurimathCtor = new (data: string, format: string) => {
|
|
2
|
+
toAsciimath(): string;
|
|
3
|
+
toLatex(): string;
|
|
4
|
+
toMathml(): string;
|
|
5
|
+
toHtml(): string;
|
|
6
|
+
toOmml(): string;
|
|
7
|
+
toDisplay(lang: string): string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
let Plurimath: PlurimathCtor | null = null;
|
|
11
|
+
let loading: Promise<PlurimathCtor> | null = null;
|
|
12
|
+
|
|
13
|
+
export async function loadPlurimath(): Promise<PlurimathCtor> {
|
|
14
|
+
if (Plurimath) return Plurimath;
|
|
15
|
+
if (loading) return loading;
|
|
16
|
+
loading = import('@plurimath/plurimath').then(m => {
|
|
17
|
+
Plurimath = m.default as PlurimathCtor;
|
|
18
|
+
return Plurimath;
|
|
19
|
+
});
|
|
20
|
+
return loading;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderToMathML(expr: string, format: string): string | null {
|
|
24
|
+
if (!Plurimath) return null;
|
|
25
|
+
try {
|
|
26
|
+
const p = new Plurimath(expr, format);
|
|
27
|
+
return p.toMathml().replace('display="block"', 'display="inline"').trim();
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function escapeHtml(text: string): string {
|
|
34
|
+
return text
|
|
35
|
+
.replace(/&/g, '&')
|
|
36
|
+
.replace(/</g, '<')
|
|
37
|
+
.replace(/>/g, '>');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function mathToHtml(expr: string, format: string, bold: boolean): string {
|
|
41
|
+
const mathml = renderToMathML(expr, format);
|
|
42
|
+
if (mathml) {
|
|
43
|
+
return `<span class="math-inline${bold ? ' math-bold' : ''}">${mathml}</span>`;
|
|
44
|
+
}
|
|
45
|
+
return `<code class="math-fallback">${escapeHtml(expr)}</code>`;
|
|
46
|
+
}
|
package/src/views/HomeView.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, ref
|
|
2
|
+
import { computed, ref } from 'vue';
|
|
3
3
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
4
4
|
import { useRouter } from 'vue-router';
|
|
5
5
|
import { useDsStyle } from '../utils/dataset-style';
|
|
@@ -8,20 +8,9 @@ import { useSiteConfig } from '../config/use-site-config';
|
|
|
8
8
|
const store = useVocabularyStore();
|
|
9
9
|
const router = useRouter();
|
|
10
10
|
const { getStyle } = useDsStyle();
|
|
11
|
-
const { config: siteConfig
|
|
11
|
+
const { config: siteConfig } = useSiteConfig();
|
|
12
12
|
const exploring = ref(false);
|
|
13
13
|
|
|
14
|
-
onMounted(async () => {
|
|
15
|
-
await loadConfig();
|
|
16
|
-
if (siteConfig.value?.defaultDataset) {
|
|
17
|
-
const targetId = siteConfig.value.defaultDataset;
|
|
18
|
-
if (!store.initialized) await store.discoverDatasets();
|
|
19
|
-
if (store.datasets.has(targetId)) {
|
|
20
|
-
router.replace({ name: 'dataset', params: { registerId: targetId } });
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
|
|
25
14
|
async function exploreRandom() {
|
|
26
15
|
exploring.value = true;
|
|
27
16
|
try {
|
|
@@ -115,58 +104,88 @@ function goToGraph() { router.push({ name: 'graph' }); }
|
|
|
115
104
|
</div>
|
|
116
105
|
</div>
|
|
117
106
|
|
|
118
|
-
<!-- Dataset
|
|
119
|
-
<
|
|
120
|
-
<div class="section-label mb-0">Available Datasets</div>
|
|
121
|
-
<span class="text-xs text-ink-300">Click to browse</span>
|
|
122
|
-
</div>
|
|
123
|
-
<div :class="[
|
|
124
|
-
filteredDatasets.length === 1 ? 'max-w-md' : '',
|
|
125
|
-
filteredDatasets.length === 2 ? 'max-w-3xl' : '',
|
|
126
|
-
'grid gap-4',
|
|
127
|
-
filteredDatasets.length === 1 ? 'grid-cols-1' : 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'
|
|
128
|
-
]">
|
|
107
|
+
<!-- Dataset section -->
|
|
108
|
+
<template v-if="filteredDatasets.length === 1">
|
|
129
109
|
<button
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
class="card-hover p-6 text-left group animate-entrance"
|
|
134
|
-
:style="{ borderLeft: `3px solid ${getStyle(ds.id).color}`, animationDelay: `${idx * 60}ms` }"
|
|
110
|
+
@click="goToDataset(filteredDatasets[0].id)"
|
|
111
|
+
class="card-hover p-8 text-left group animate-entrance max-w-md"
|
|
112
|
+
:style="{ borderLeft: `3px solid ${getStyle(filteredDatasets[0].id).color}` }"
|
|
135
113
|
>
|
|
136
|
-
<div class="flex items-
|
|
137
|
-
<span class="w-
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
</h2>
|
|
142
|
-
</div>
|
|
114
|
+
<div class="flex items-center gap-3 mb-3">
|
|
115
|
+
<span class="w-3 h-3 rounded-full flex-shrink-0" :style="{ backgroundColor: getStyle(filteredDatasets[0].id).color }"></span>
|
|
116
|
+
<h2 class="font-serif text-xl text-ink-800 group-hover:text-ink-900 transition-colors">
|
|
117
|
+
{{ filteredDatasets[0].manifest.title }}
|
|
118
|
+
</h2>
|
|
143
119
|
</div>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
{{ ds.manifest.description }}
|
|
120
|
+
<p v-if="filteredDatasets[0].manifest.description" class="text-sm text-ink-400 mb-4 line-clamp-2 leading-relaxed pl-6">
|
|
121
|
+
{{ filteredDatasets[0].manifest.description }}
|
|
147
122
|
</p>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<span :style="{ color: getStyle(ds.id).color }" class="text-sm font-semibold tabular-nums">{{ ds.manifest.conceptCount.toLocaleString() }}</span>
|
|
123
|
+
<div class="flex items-center gap-3 pl-6 mb-4">
|
|
124
|
+
<span :style="{ color: getStyle(filteredDatasets[0].id).color }" class="text-sm font-semibold tabular-nums">{{ filteredDatasets[0].manifest.conceptCount.toLocaleString() }}</span>
|
|
151
125
|
<span class="text-xs text-ink-300">concepts</span>
|
|
152
126
|
<span class="text-ink-200 text-xs">·</span>
|
|
153
|
-
<span class="text-sm text-ink-500 tabular-nums">{{
|
|
127
|
+
<span class="text-sm text-ink-500 tabular-nums">{{ filteredDatasets[0].manifest.languages.length }}</span>
|
|
154
128
|
<span class="text-xs text-ink-300">languages</span>
|
|
155
129
|
</div>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
130
|
+
<div class="pl-6">
|
|
131
|
+
<span class="btn-primary inline-flex items-center gap-2">
|
|
132
|
+
Browse concepts
|
|
133
|
+
<svg class="w-4 h-4 group-hover:translate-x-0.5 transition-transform" 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>
|
|
160
134
|
</span>
|
|
161
135
|
</div>
|
|
162
|
-
|
|
163
|
-
<div class="flex items-center justify-between pl-[22px]">
|
|
164
|
-
<span class="text-[11px] text-ink-300">{{ ds.manifest.owner }}</span>
|
|
165
|
-
<svg class="w-4 h-4 text-ink-200 group-hover:text-ink-400 group-hover:translate-x-0.5 transition-all" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
166
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
167
|
-
</svg>
|
|
168
|
-
</div>
|
|
169
136
|
</button>
|
|
170
|
-
</
|
|
137
|
+
</template>
|
|
138
|
+
<template v-else-if="filteredDatasets.length > 1">
|
|
139
|
+
<div class="flex items-center justify-between mb-5">
|
|
140
|
+
<div class="section-label mb-0">Available Datasets</div>
|
|
141
|
+
<span class="text-xs text-ink-300">Click to browse</span>
|
|
142
|
+
</div>
|
|
143
|
+
<div :class="[
|
|
144
|
+
filteredDatasets.length === 2 ? 'max-w-3xl' : '',
|
|
145
|
+
'grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3'
|
|
146
|
+
]">
|
|
147
|
+
<button
|
|
148
|
+
v-for="(ds, idx) in filteredDatasets"
|
|
149
|
+
:key="ds.id"
|
|
150
|
+
@click="goToDataset(ds.id)"
|
|
151
|
+
class="card-hover p-6 text-left group animate-entrance"
|
|
152
|
+
:style="{ borderLeft: `3px solid ${getStyle(ds.id).color}`, animationDelay: `${idx * 60}ms` }"
|
|
153
|
+
>
|
|
154
|
+
<div class="flex items-start gap-3 mb-4">
|
|
155
|
+
<span class="w-2.5 h-2.5 rounded-full mt-1.5 flex-shrink-0" :style="{ backgroundColor: getStyle(ds.id).color }"></span>
|
|
156
|
+
<div class="min-w-0">
|
|
157
|
+
<h2 class="font-serif text-xl text-ink-800 leading-snug group-hover:text-ink-900 transition-colors">
|
|
158
|
+
{{ ds.manifest.title }}
|
|
159
|
+
</h2>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<p class="text-sm text-ink-400 mb-5 line-clamp-2 leading-relaxed pl-[22px]">
|
|
164
|
+
{{ ds.manifest.description }}
|
|
165
|
+
</p>
|
|
166
|
+
|
|
167
|
+
<div class="flex items-center gap-3 pl-[22px] mb-3">
|
|
168
|
+
<span :style="{ color: getStyle(ds.id).color }" class="text-sm font-semibold tabular-nums">{{ ds.manifest.conceptCount.toLocaleString() }}</span>
|
|
169
|
+
<span class="text-xs text-ink-300">concepts</span>
|
|
170
|
+
<span class="text-ink-200 text-xs">·</span>
|
|
171
|
+
<span class="text-sm text-ink-500 tabular-nums">{{ ds.manifest.languages.length }}</span>
|
|
172
|
+
<span class="text-xs text-ink-300">languages</span>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="flex flex-wrap gap-1.5 pl-[22px] mb-3">
|
|
176
|
+
<span v-for="tag in (ds.manifest.tags ?? []).slice(0, 3)" :key="tag" class="badge text-[10px]" :style="{ backgroundColor: getStyle(ds.id).light, color: getStyle(ds.id).dark }">
|
|
177
|
+
{{ tag }}
|
|
178
|
+
</span>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div class="flex items-center justify-between pl-[22px]">
|
|
182
|
+
<span class="text-[11px] text-ink-300">{{ ds.manifest.owner }}</span>
|
|
183
|
+
<svg class="w-4 h-4 text-ink-200 group-hover:text-ink-400 group-hover:translate-x-0.5 transition-all" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
184
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
185
|
+
</svg>
|
|
186
|
+
</div>
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
</template>
|
|
171
190
|
</div>
|
|
172
191
|
</template>
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import Plurimath from '@plurimath/plurimath';
|
|
2
|
-
|
|
3
|
-
function renderToMathML(math, format) {
|
|
4
|
-
try {
|
|
5
|
-
const p = new Plurimath(math, format);
|
|
6
|
-
return p.toMathml().replace('display="block"', 'display="inline"').trim();
|
|
7
|
-
} catch {
|
|
8
|
-
return null;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function escapeHtml(text) {
|
|
13
|
-
return text
|
|
14
|
-
.replace(/&/g, '&')
|
|
15
|
-
.replace(/</g, '<')
|
|
16
|
-
.replace(/>/g, '>');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function replaceBracketed(text, prefix, render) {
|
|
20
|
-
let result = '';
|
|
21
|
-
let i = 0;
|
|
22
|
-
const boldPrefix = '*' + prefix;
|
|
23
|
-
while (i < text.length) {
|
|
24
|
-
if (text.startsWith(boldPrefix + '[', i)) {
|
|
25
|
-
i += boldPrefix.length + 1;
|
|
26
|
-
let j = i;
|
|
27
|
-
let d = 1;
|
|
28
|
-
while (j < text.length && d > 0) {
|
|
29
|
-
if (text[j] === '[') d++;
|
|
30
|
-
else if (text[j] === ']') d--;
|
|
31
|
-
j++;
|
|
32
|
-
}
|
|
33
|
-
const content = text.slice(i, j - 1);
|
|
34
|
-
let end = j;
|
|
35
|
-
if (end < text.length && text[end] === '*') end++;
|
|
36
|
-
result += render(content, true);
|
|
37
|
-
i = end;
|
|
38
|
-
} else if (text.startsWith(prefix + '[', i)) {
|
|
39
|
-
i += prefix.length + 1;
|
|
40
|
-
let j = i;
|
|
41
|
-
let d = 1;
|
|
42
|
-
while (j < text.length && d > 0) {
|
|
43
|
-
if (text[j] === '[') d++;
|
|
44
|
-
else if (text[j] === ']') d--;
|
|
45
|
-
j++;
|
|
46
|
-
}
|
|
47
|
-
const content = text.slice(i, j - 1);
|
|
48
|
-
result += render(content, false);
|
|
49
|
-
i = j;
|
|
50
|
-
} else {
|
|
51
|
-
result += text[i];
|
|
52
|
-
i++;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return result;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function renderStem(math, bold) {
|
|
59
|
-
const mathml = renderToMathML(math, 'asciimath');
|
|
60
|
-
if (mathml) {
|
|
61
|
-
return `<span class="math-inline${bold ? ' math-bold' : ''}">${mathml}</span>`;
|
|
62
|
-
}
|
|
63
|
-
return `<code class="math-fallback">${escapeHtml(math)}</code>`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function renderLatexmath(math, bold) {
|
|
67
|
-
const mathml = renderToMathML(math, 'latex');
|
|
68
|
-
if (mathml) {
|
|
69
|
-
return `<span class="math-inline${bold ? ' math-bold' : ''}">${mathml}</span>`;
|
|
70
|
-
}
|
|
71
|
-
return `<code class="math-fallback">${escapeHtml(math)}</code>`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function preRenderMath(text) {
|
|
75
|
-
if (!text) return '';
|
|
76
|
-
let result = text;
|
|
77
|
-
result = replaceBracketed(result, 'stem:', renderStem);
|
|
78
|
-
result = replaceBracketed(result, 'latexmath:', renderLatexmath);
|
|
79
|
-
return result;
|
|
80
|
-
}
|