@glossarist/concept-browser 0.3.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.3.1",
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': preRenderMath(term.designation),
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': preRenderMath(d.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] = preRenderMath(preferred.designation);
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&lt;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, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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, '&amp;')
90
+ .replace(/</g, '&lt;')
91
+ .replace(/>/g, '&gt;');
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 (stem/latexmath) is pre-rendered at build time. Only process text formatting.
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, '&amp;')
91
- .replace(/</g, '&lt;')
92
- .replace(/>/g, '&gt;');
93
- }
94
-
95
145
  export function cleanContent(text: string): string {
96
146
  if (!text) return '';
97
147
  let result = text
98
- .replace(/<[^>]+>/g, '') // strip pre-rendered HTML/MathML
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, '&amp;')
36
+ .replace(/</g, '&lt;')
37
+ .replace(/>/g, '&gt;');
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
+ }
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref, onMounted } from 'vue';
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, loadConfig } = useSiteConfig();
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 cards -->
119
- <div class="flex items-center justify-between mb-5">
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
- v-for="(ds, idx) in filteredDatasets"
131
- :key="ds.id"
132
- @click="goToDataset(ds.id)"
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-start gap-3 mb-4">
137
- <span class="w-2.5 h-2.5 rounded-full mt-1.5 flex-shrink-0" :style="{ backgroundColor: getStyle(ds.id).color }"></span>
138
- <div class="min-w-0">
139
- <h2 class="font-serif text-xl text-ink-800 leading-snug group-hover:text-ink-900 transition-colors">
140
- {{ ds.manifest.title }}
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
- <p class="text-sm text-ink-400 mb-5 line-clamp-2 leading-relaxed pl-[22px]">
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
- <div class="flex items-center gap-3 pl-[22px] mb-3">
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">&middot;</span>
153
- <span class="text-sm text-ink-500 tabular-nums">{{ ds.manifest.languages.length }}</span>
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
- <div class="flex flex-wrap gap-1.5 pl-[22px] mb-3">
158
- <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 }">
159
- {{ tag }}
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
- </div>
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">&middot;</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, '&amp;')
15
- .replace(/</g, '&lt;')
16
- .replace(/>/g, '&gt;');
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
- }