@glossarist/concept-browser 0.2.12 → 0.3.1

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.2.12",
3
+ "version": "0.3.1",
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,6 +3,7 @@ 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';
6
7
 
7
8
  const __dirname = path.dirname(new URL(import.meta.url).pathname);
8
9
  const ROOT = process.cwd();
@@ -55,7 +56,7 @@ function termToDesignation(term) {
55
56
  : term.type === 'abbreviation' ? 'gl:Abbreviation'
56
57
  : 'gl:Designation',
57
58
  'gl:normativeStatus': term.normative_status || 'preferred',
58
- 'gl:term': term.designation,
59
+ 'gl:term': preRenderMath(term.designation),
59
60
  };
60
61
  if (term.gender) doc['gl:gender'] = term.gender;
61
62
  if (term.plurality) doc['gl:plurality'] = term.plurality;
@@ -68,7 +69,7 @@ function defsToJsonLd(defs) {
68
69
  return defs
69
70
  .map(d => ({
70
71
  '@type': 'gl:DetailedDefinition',
71
- 'gl:content': d.content || '',
72
+ 'gl:content': preRenderMath(d.content || ''),
72
73
  }))
73
74
  .filter(d => d['gl:content']);
74
75
  }
@@ -255,7 +256,7 @@ function getPrimaryDesignation(conceptYaml) {
255
256
  if (lc && lc.terms && lc.terms.length > 0) {
256
257
  const preferredExpr = lc.terms.find(t => t.normative_status === 'preferred' && t.type === 'expression');
257
258
  const preferred = preferredExpr || lc.terms.find(t => t.normative_status === 'preferred') || lc.terms[0];
258
- descs[lang] = preferred.designation;
259
+ descs[lang] = preRenderMath(preferred.designation);
259
260
  }
260
261
  }
261
262
  return descs;
@@ -531,11 +532,17 @@ function processDataset(dir, register, opts) {
531
532
  status: c.status,
532
533
  }));
533
534
 
535
+ // Strip HTML from index summary for text display
536
+ const plainSummary = summary.map(c => ({
537
+ ...c,
538
+ eng: c.eng.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim(),
539
+ }));
540
+
534
541
  const graphNodeEntries = concepts.map(c => {
535
542
  let term = '', lang = '';
536
543
  if (c.designations.eng) { term = c.designations.eng; lang = 'eng'; }
537
544
  else { for (const [l, t] of Object.entries(c.designations)) { if (t) { term = t; lang = l; break; } } }
538
- return [c.id, term, lang, c.status];
545
+ return [c.id, term.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim(), lang, c.status];
539
546
  });
540
547
  fs.mkdirSync(path.join(DATA, register), { recursive: true });
541
548
  fs.writeFileSync(
@@ -553,7 +560,7 @@ function processDataset(dir, register, opts) {
553
560
  conceptCount: concepts.length,
554
561
  chunkSize: CHUNK_SIZE,
555
562
  chunks,
556
- concepts: summary,
563
+ concepts: plainSummary,
557
564
  });
558
565
 
559
566
  writeJson(path.join(DATA, register, 'index-meta.json'), {
@@ -0,0 +1,80 @@
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
+ }
@@ -1,22 +1,4 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
-
3
- // Mock @plurimath/plurimath for test environment
4
- vi.mock('@plurimath/plurimath', () => {
5
- return {
6
- default: class MockPlurimath {
7
- private data: string;
8
- private format: string;
9
- constructor(data: string, format: string) {
10
- this.data = data;
11
- this.format = format;
12
- }
13
- toMathml() {
14
- return `<math xmlns="http://www.w3.org/1998/Math/MathML"><mi>${this.data}</mi></math>`;
15
- }
16
- },
17
- };
18
- });
19
-
1
+ import { describe, it, expect } from 'vitest';
20
2
  import { renderMath, cleanContent } from '../utils/math';
21
3
 
22
4
  describe('renderMath', () => {
@@ -24,25 +6,24 @@ describe('renderMath', () => {
24
6
  expect(renderMath('hello world')).toBe('hello world');
25
7
  });
26
8
 
27
- it('renders stem:[x^2] to MathML span', () => {
28
- const result = renderMath('the value stem:[x^2]');
29
- expect(result).toContain('math-inline');
30
- expect(result).toContain('<math');
31
- expect(result).not.toContain('math-bold');
9
+ it('passes through pre-rendered MathML unchanged', () => {
10
+ const preRendered = 'value <span class="math-inline"><math><mi>x</mi></math></span> here';
11
+ expect(renderMath(preRendered)).toBe(preRendered);
32
12
  });
33
13
 
34
- it('renders *stem:[x]* (bold math) with math-bold class', () => {
35
- const result = renderMath('the value *stem:[x]*');
36
- expect(result).toContain('math-inline');
37
- expect(result).toContain('math-bold');
14
+ it('still converts italic in mixed pre-rendered MathML content', () => {
15
+ const preRendered = '<span class="math-inline"><math><mi>x</mi></math></span> and *italic*';
16
+ expect(renderMath(preRendered)).toBe(
17
+ '<span class="math-inline"><math><mi>x</mi></math></span> and <em>italic</em>',
18
+ );
38
19
  });
39
20
 
40
- it('renders latexmath:[...] with nested brackets', () => {
41
- const result = renderMath('coords latexmath:[[u_0, u_1] \\leq 1.0] here');
42
- expect(result).toContain('math-inline');
43
- expect(result).toContain('<math');
44
- expect(result).toContain('u_0');
45
- expect(result).not.toContain('latexmath:');
21
+ it('converts *text* to <em> (italic) for non-pre-rendered content', () => {
22
+ expect(renderMath('some *italic* text')).toBe('some <em>italic</em> text');
23
+ });
24
+
25
+ it('converts ~text~ to <sub> (subscript)', () => {
26
+ expect(renderMath('H~2~O')).toBe('H<sub>2</sub>O');
46
27
  });
47
28
 
48
29
  it('converts bullet lines to <ul><li>', () => {
@@ -52,20 +33,6 @@ describe('renderMath', () => {
52
33
  expect(result).toContain('<li>second item</li>');
53
34
  });
54
35
 
55
- it('does NOT convert *stem:[...] lines to list items', () => {
56
- const result = renderMath('*stem:[x]*');
57
- expect(result).not.toContain('<ul');
58
- expect(result).toContain('math-bold');
59
- });
60
-
61
- it('converts *text* to <em> (italic)', () => {
62
- expect(renderMath('some *italic* text')).toBe('some <em>italic</em> text');
63
- });
64
-
65
- it('converts ~text~ to <sub> (subscript)', () => {
66
- expect(renderMath('H~2~O')).toBe('H<sub>2</sub>O');
67
- });
68
-
69
36
  it('resolves URN inline refs via xrefResolver', () => {
70
37
  const resolver = (uri: string, term: string) => `[${term}→${uri}]`;
71
38
  const result = renderMath(
@@ -117,6 +84,14 @@ describe('renderMath', () => {
117
84
  expect(result).toBe('see some term');
118
85
  });
119
86
 
87
+ it('resolves cross-refs even in pre-rendered content', () => {
88
+ const resolver = (uri: string, term: string) => `[${term}→${uri}]`;
89
+ const preRendered = '<span class="math-inline"><math><mi>x</mi></math></span> and {{urn:iso:std:iso:14812:3.1.1.1,entity}}';
90
+ expect(renderMath(preRendered, resolver)).toBe(
91
+ '<span class="math-inline"><math><mi>x</mi></math></span> and [entity→urn:iso:std:iso:14812:3.1.1.1]',
92
+ );
93
+ });
94
+
120
95
  it('handles empty input', () => {
121
96
  expect(renderMath('')).toBe('');
122
97
  });
@@ -128,16 +103,9 @@ describe('renderMath', () => {
128
103
  });
129
104
 
130
105
  describe('cleanContent', () => {
131
- it('strips stem:[...] to raw math text', () => {
132
- expect(cleanContent('value stem:[x^2] here')).toBe('value x^2 here');
133
- });
134
-
135
- it('strips bold stem', () => {
136
- expect(cleanContent('value *stem:[x]* here')).toBe('value x here');
137
- });
138
-
139
- it('strips latexmath:[...] with nested brackets', () => {
140
- expect(cleanContent('coords latexmath:[[u_0, u_1] \\leq 1.0] end')).toBe('coords [u_0, u_1] \\leq 1.0 end');
106
+ it('strips pre-rendered HTML/MathML tags', () => {
107
+ expect(cleanContent('value <span class="math-inline"><math><mi>x</mi></math></span> here'))
108
+ .toBe('value x here');
141
109
  });
142
110
 
143
111
  it('strips *text* to plain text', () => {
package/src/style.css CHANGED
@@ -158,16 +158,14 @@
158
158
  font-family: var(--font-body);
159
159
  }
160
160
 
161
- /* Math (Plurimath MathML output) */
161
+ /* Math */
162
162
  .math-inline {
163
163
  display: inline;
164
164
  }
165
165
  .math-inline math {
166
166
  font-size: 1.05em;
167
167
  }
168
- .math-bold math mi,
169
- .math-bold math mn,
170
- .math-bold math mo {
168
+ .math-bold .math-inline math {
171
169
  font-weight: bold;
172
170
  }
173
171
  .math-fallback {
package/src/utils/math.ts CHANGED
@@ -1,17 +1,3 @@
1
- import Plurimath from '@plurimath/plurimath';
2
-
3
- type MathFormat = 'asciimath' | 'latex' | 'mathml' | 'html' | 'mahtml' | 'omml';
4
-
5
- function renderMathSpan(math: string, format: MathFormat, bold: boolean): string {
6
- try {
7
- const p = new Plurimath(math, format);
8
- const mathml = p.toMathml();
9
- return `<span class="math-inline${bold ? ' math-bold' : ''}">${mathml}</span>`;
10
- } catch {
11
- return `<code class="math-fallback">${escapeHtml(math)}</code>`;
12
- }
13
- }
14
-
15
1
  export type XrefResolver = (uri: string, term: string) => string;
16
2
  export type BibResolver = (refId: string, title: string) => string;
17
3
  export type FigResolver = (figId: string) => string;
@@ -22,12 +8,7 @@ export interface RenderOptions {
22
8
  figResolver?: FigResolver;
23
9
  }
24
10
 
25
- /**
26
- * Convert `* item` lines into <ul><li> blocks.
27
- * Also handles `1)` and `1.` numbered items into ordered lists.
28
- */
29
11
  function convertLists(text: string): string {
30
- // Unordered: * item (separated by \n or \n\n)
31
12
  let result = text.replace(/(?:^|\n)((?:[ \t]*\* [^\n]+)(?:\n[ \t]*\* [^\n]+)*)/g, (_, block) => {
32
13
  if (/^\*stem:\[/.test(block.trimStart())) return _;
33
14
  const items: string[] = [];
@@ -41,7 +22,6 @@ function convertLists(text: string): string {
41
22
  return `\n<ul class="concept-list">${lis}</ul>`;
42
23
  });
43
24
 
44
- // Ordered: 1) item or 1. item (numbered items)
45
25
  result = result.replace(/(?:^|\n)((?:[ \t]*\d+[).][ \t]+[^\n]+)(?:\n[ \t]*\d+[).][ \t]+[^\n]+)*)/g, (_, block) => {
46
26
  const items: string[] = [];
47
27
  const re = /[ \t]*\d+[).][ \t]+([^\n]+)/g;
@@ -57,63 +37,6 @@ function convertLists(text: string): string {
57
37
  return result;
58
38
  }
59
39
 
60
- /**
61
- * Replace `prefix:[content]` where content may contain nested brackets.
62
- * Handles `*prefix:[content]*` (bold) too.
63
- */
64
- function replaceBracketed(
65
- text: string,
66
- prefix: string,
67
- render: (math: string, bold: boolean) => string,
68
- ): string {
69
- let result = '';
70
- let i = 0;
71
- const boldPrefix = '*' + prefix;
72
- while (i < text.length) {
73
- // Check for bold variant: *prefix:[...]
74
- if (text.startsWith(boldPrefix + '[', i)) {
75
- const start = i;
76
- i += boldPrefix.length + 1; // skip *prefix:[
77
- const depth = 1;
78
- let j = i;
79
- let d = 1;
80
- while (j < text.length && d > 0) {
81
- if (text[j] === '[') d++;
82
- else if (text[j] === ']') d--;
83
- j++;
84
- }
85
- const content = text.slice(i, j - 1);
86
- // Check for closing *
87
- let end = j;
88
- if (end < text.length && text[end] === '*') end++;
89
- result += render(content, true);
90
- i = end;
91
- }
92
- // Check for normal variant: prefix:[...]
93
- else if (text.startsWith(prefix + '[', i)) {
94
- i += prefix.length + 1;
95
- let j = i;
96
- let d = 1;
97
- while (j < text.length && d > 0) {
98
- if (text[j] === '[') d++;
99
- else if (text[j] === ']') d--;
100
- j++;
101
- }
102
- const content = text.slice(i, j - 1);
103
- result += render(content, false);
104
- i = j;
105
- } else {
106
- result += text[i];
107
- i++;
108
- }
109
- }
110
- return result;
111
- }
112
-
113
- /**
114
- * Render stem:[...] math notation to KaTeX HTML.
115
- * Also handles cross-reference inline patterns (URN refs, bibliography, figures).
116
- */
117
40
  export function renderMath(text: string, xrefResolverOrOpts?: XrefResolver | RenderOptions): string {
118
41
  if (!text) return '';
119
42
  let result = text;
@@ -122,15 +45,11 @@ export function renderMath(text: string, xrefResolverOrOpts?: XrefResolver | Ren
122
45
  ? { xrefResolver: xrefResolverOrOpts }
123
46
  : (xrefResolverOrOpts ?? {});
124
47
 
125
- result = replaceBracketed(result, 'stem:', (math, bold) => renderMathSpan(math, 'asciimath', bold));
126
- result = replaceBracketed(result, 'latexmath:', (math, bold) => renderMathSpan(math, 'latex', bold));
127
-
48
+ // Math (stem/latexmath) is pre-rendered at build time. Only process text formatting.
128
49
  result = convertLists(result);
129
-
130
50
  result = result.replace(/\*([^*]+)\*/g, '<em>$1</em>');
131
51
  result = result.replace(/~([^~]+)~/g, '<sub>$1</sub>');
132
52
 
133
- // Handle AsciiDoc bibliography xrefs: <<ref_XX,title>>
134
53
  result = result.replace(/<<([^,>]+),([^>]+)>>/g, (_, refId, title) => {
135
54
  if (opts.bibResolver) {
136
55
  return opts.bibResolver(refId.trim(), title.trim());
@@ -138,7 +57,6 @@ export function renderMath(text: string, xrefResolverOrOpts?: XrefResolver | Ren
138
57
  return `<span class="bib-ref">${escapeHtml(title.trim())}</span>`;
139
58
  });
140
59
 
141
- // Handle AsciiDoc figure xrefs: <<fig_XX>>
142
60
  result = result.replace(/<<(fig_[^>]+)>>/g, (_, figId) => {
143
61
  if (opts.figResolver) {
144
62
  return opts.figResolver(figId.trim());
@@ -146,25 +64,22 @@ export function renderMath(text: string, xrefResolverOrOpts?: XrefResolver | Ren
146
64
  return `<span class="fig-ref">${escapeHtml(figId.trim())}</span>`;
147
65
  });
148
66
 
149
- // Handle URN inline refs: {{urn:...,term[,displayText]}} (double-braced)
150
67
  result = result.replace(/\{\{(urn:[^,}]+),([^,}]+)(?:,([^}]+))?\}\}/g, (_, uri, term, display) => {
151
- const text = (display || term).trim();
68
+ const t = (display || term).trim();
152
69
  if (opts.xrefResolver) {
153
- return opts.xrefResolver(uri, text);
70
+ return opts.xrefResolver(uri, t);
154
71
  }
155
- return text;
72
+ return t;
156
73
  });
157
74
 
158
- // Handle URN inline refs: {urn:...,term[,displayText]} (single-braced)
159
75
  result = result.replace(/\{(urn:[^,}]+),([^,}]+)(?:,([^}]+))?\}/g, (_, uri, term, display) => {
160
- const text = (display || term).trim();
76
+ const t = (display || term).trim();
161
77
  if (opts.xrefResolver) {
162
- return opts.xrefResolver(uri, text);
78
+ return opts.xrefResolver(uri, t);
163
79
  }
164
- return text;
80
+ return t;
165
81
  });
166
82
 
167
- // Handle any remaining {{...}} refs (fallback: show term before comma)
168
83
  result = result.replace(/\{\{([^,}]+)(?:,\s*[^}]+)?\}\}/g, '$1');
169
84
 
170
85
  return result;
@@ -177,12 +92,10 @@ function escapeHtml(text: string): string {
177
92
  .replace(/>/g, '&gt;');
178
93
  }
179
94
 
180
- /**
181
- * Clean content for plain text display (no math rendering).
182
- */
183
95
  export function cleanContent(text: string): string {
184
96
  if (!text) return '';
185
97
  let result = text
98
+ .replace(/<[^>]+>/g, '') // strip pre-rendered HTML/MathML
186
99
  .replace(/\*([^*]+)\*/g, '$1')
187
100
  .replace(/~([^~]+)~/g, '_$1')
188
101
  .replace(/\n[ \t]*\* /g, '; ')
@@ -191,7 +104,5 @@ export function cleanContent(text: string): string {
191
104
  .replace(/\{\{urn:[^,}]+,([^,}]+)(?:,[^}]+)?\}\}/g, '$1')
192
105
  .replace(/\{urn:[^,}]+,([^,}]+)(?:,[^}]+)?\}/g, '$1')
193
106
  .replace(/\{\{([^,}]+)(?:,\s*[^}]+)?\}\}/g, '$1');
194
- result = replaceBracketed(result, 'stem:', (math) => math);
195
- result = replaceBracketed(result, 'latexmath:', (math) => math);
196
107
  return result;
197
108
  }
package/vite.config.ts CHANGED
@@ -20,9 +20,6 @@ export default defineConfig({
20
20
  '@': resolve(__dirname, 'src'),
21
21
  },
22
22
  },
23
- optimizeDeps: {
24
- include: ['@plurimath/plurimath'],
25
- },
26
23
  test: {
27
24
  environment: 'happy-dom',
28
25
  globals: true,