@glossarist/concept-browser 0.2.13 → 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 +2 -2
- package/scripts/generate-data.mjs +12 -5
- package/scripts/math-prerender.mjs +80 -0
- package/src/__tests__/math.test.ts +25 -38
- package/src/main.ts +0 -1
- package/src/style.css +5 -4
- package/src/utils/math.ts +2 -65
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.
|
|
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": {
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
"test:watch": "vitest"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
+
"@plurimath/plurimath": "^0.2.2",
|
|
22
23
|
"@vitejs/plugin-vue": "^5.2.3",
|
|
23
24
|
"autoprefixer": "^10.4.21",
|
|
24
25
|
"d3": "^7.9.0",
|
|
25
26
|
"glossarist": "^0.2.0",
|
|
26
27
|
"js-yaml": "^4.1.0",
|
|
27
|
-
"katex": "^0.16.45",
|
|
28
28
|
"pinia": "^2.3.1",
|
|
29
29
|
"postcss": "^8.5.3",
|
|
30
30
|
"tailwindcss": "^3.4.17",
|
|
@@ -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:
|
|
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, '&')
|
|
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
|
+
}
|
|
@@ -6,24 +6,24 @@ describe('renderMath', () => {
|
|
|
6
6
|
expect(renderMath('hello world')).toBe('hello world');
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
-
it('
|
|
10
|
-
const
|
|
11
|
-
expect(
|
|
12
|
-
expect(result).toContain('katex');
|
|
13
|
-
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);
|
|
14
12
|
});
|
|
15
13
|
|
|
16
|
-
it('
|
|
17
|
-
const
|
|
18
|
-
expect(
|
|
19
|
-
|
|
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
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('converts *text* to <em> (italic) for non-pre-rendered content', () => {
|
|
22
|
+
expect(renderMath('some *italic* text')).toBe('some <em>italic</em> text');
|
|
20
23
|
});
|
|
21
24
|
|
|
22
|
-
it('
|
|
23
|
-
|
|
24
|
-
expect(result).toContain('math-inline');
|
|
25
|
-
expect(result).toContain('katex');
|
|
26
|
-
expect(result).not.toContain('latexmath:');
|
|
25
|
+
it('converts ~text~ to <sub> (subscript)', () => {
|
|
26
|
+
expect(renderMath('H~2~O')).toBe('H<sub>2</sub>O');
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
it('converts bullet lines to <ul><li>', () => {
|
|
@@ -33,20 +33,6 @@ describe('renderMath', () => {
|
|
|
33
33
|
expect(result).toContain('<li>second item</li>');
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
it('does NOT convert *stem:[...] lines to list items', () => {
|
|
37
|
-
const result = renderMath('*stem:[x]*');
|
|
38
|
-
expect(result).not.toContain('<ul');
|
|
39
|
-
expect(result).toContain('math-bold');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('converts *text* to <em> (italic)', () => {
|
|
43
|
-
expect(renderMath('some *italic* text')).toBe('some <em>italic</em> text');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('converts ~text~ to <sub> (subscript)', () => {
|
|
47
|
-
expect(renderMath('H~2~O')).toBe('H<sub>2</sub>O');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
36
|
it('resolves URN inline refs via xrefResolver', () => {
|
|
51
37
|
const resolver = (uri: string, term: string) => `[${term}→${uri}]`;
|
|
52
38
|
const result = renderMath(
|
|
@@ -98,6 +84,14 @@ describe('renderMath', () => {
|
|
|
98
84
|
expect(result).toBe('see some term');
|
|
99
85
|
});
|
|
100
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
|
+
|
|
101
95
|
it('handles empty input', () => {
|
|
102
96
|
expect(renderMath('')).toBe('');
|
|
103
97
|
});
|
|
@@ -109,16 +103,9 @@ describe('renderMath', () => {
|
|
|
109
103
|
});
|
|
110
104
|
|
|
111
105
|
describe('cleanContent', () => {
|
|
112
|
-
it('strips
|
|
113
|
-
expect(cleanContent('value
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
it('strips bold stem', () => {
|
|
117
|
-
expect(cleanContent('value *stem:[x]* here')).toBe('value x here');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('strips latexmath:[...] with nested brackets', () => {
|
|
121
|
-
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');
|
|
122
109
|
});
|
|
123
110
|
|
|
124
111
|
it('strips *text* to plain text', () => {
|
package/src/main.ts
CHANGED
package/src/style.css
CHANGED
|
@@ -159,12 +159,13 @@
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
/* Math */
|
|
162
|
-
.math-inline
|
|
162
|
+
.math-inline {
|
|
163
|
+
display: inline;
|
|
164
|
+
}
|
|
165
|
+
.math-inline math {
|
|
163
166
|
font-size: 1.05em;
|
|
164
167
|
}
|
|
165
|
-
.math-bold .
|
|
166
|
-
.math-bold .katex .mbin,
|
|
167
|
-
.math-bold .katex .mrel {
|
|
168
|
+
.math-bold .math-inline math {
|
|
168
169
|
font-weight: bold;
|
|
169
170
|
}
|
|
170
171
|
.math-fallback {
|
package/src/utils/math.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import katex from 'katex';
|
|
2
|
-
|
|
3
1
|
export type XrefResolver = (uri: string, term: string) => string;
|
|
4
2
|
export type BibResolver = (refId: string, title: string) => string;
|
|
5
3
|
export type FigResolver = (figId: string) => string;
|
|
@@ -39,50 +37,6 @@ function convertLists(text: string): string {
|
|
|
39
37
|
return result;
|
|
40
38
|
}
|
|
41
39
|
|
|
42
|
-
function replaceBracketed(
|
|
43
|
-
text: string,
|
|
44
|
-
prefix: string,
|
|
45
|
-
render: (math: string, bold: boolean) => string,
|
|
46
|
-
): string {
|
|
47
|
-
let result = '';
|
|
48
|
-
let i = 0;
|
|
49
|
-
const boldPrefix = '*' + prefix;
|
|
50
|
-
while (i < text.length) {
|
|
51
|
-
if (text.startsWith(boldPrefix + '[', i)) {
|
|
52
|
-
i += boldPrefix.length + 1;
|
|
53
|
-
let j = i;
|
|
54
|
-
let d = 1;
|
|
55
|
-
while (j < text.length && d > 0) {
|
|
56
|
-
if (text[j] === '[') d++;
|
|
57
|
-
else if (text[j] === ']') d--;
|
|
58
|
-
j++;
|
|
59
|
-
}
|
|
60
|
-
const content = text.slice(i, j - 1);
|
|
61
|
-
let end = j;
|
|
62
|
-
if (end < text.length && text[end] === '*') end++;
|
|
63
|
-
result += render(content, true);
|
|
64
|
-
i = end;
|
|
65
|
-
}
|
|
66
|
-
else if (text.startsWith(prefix + '[', i)) {
|
|
67
|
-
i += prefix.length + 1;
|
|
68
|
-
let j = i;
|
|
69
|
-
let d = 1;
|
|
70
|
-
while (j < text.length && d > 0) {
|
|
71
|
-
if (text[j] === '[') d++;
|
|
72
|
-
else if (text[j] === ']') d--;
|
|
73
|
-
j++;
|
|
74
|
-
}
|
|
75
|
-
const content = text.slice(i, j - 1);
|
|
76
|
-
result += render(content, false);
|
|
77
|
-
i = j;
|
|
78
|
-
} else {
|
|
79
|
-
result += text[i];
|
|
80
|
-
i++;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return result;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
40
|
export function renderMath(text: string, xrefResolverOrOpts?: XrefResolver | RenderOptions): string {
|
|
87
41
|
if (!text) return '';
|
|
88
42
|
let result = text;
|
|
@@ -91,11 +45,8 @@ export function renderMath(text: string, xrefResolverOrOpts?: XrefResolver | Ren
|
|
|
91
45
|
? { xrefResolver: xrefResolverOrOpts }
|
|
92
46
|
: (xrefResolverOrOpts ?? {});
|
|
93
47
|
|
|
94
|
-
|
|
95
|
-
result = replaceBracketed(result, 'latexmath:', renderKatexSpan);
|
|
96
|
-
|
|
48
|
+
// Math (stem/latexmath) is pre-rendered at build time. Only process text formatting.
|
|
97
49
|
result = convertLists(result);
|
|
98
|
-
|
|
99
50
|
result = result.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
100
51
|
result = result.replace(/~([^~]+)~/g, '<sub>$1</sub>');
|
|
101
52
|
|
|
@@ -134,19 +85,6 @@ export function renderMath(text: string, xrefResolverOrOpts?: XrefResolver | Ren
|
|
|
134
85
|
return result;
|
|
135
86
|
}
|
|
136
87
|
|
|
137
|
-
function renderKatexSpan(math: string, bold: boolean): string {
|
|
138
|
-
try {
|
|
139
|
-
const html = katex.renderToString(math, {
|
|
140
|
-
throwOnError: false,
|
|
141
|
-
displayMode: false,
|
|
142
|
-
output: 'html',
|
|
143
|
-
});
|
|
144
|
-
return `<span class="math-inline${bold ? ' math-bold' : ''}">${html}</span>`;
|
|
145
|
-
} catch {
|
|
146
|
-
return `<code class="math-fallback">${escapeHtml(math)}</code>`;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
88
|
function escapeHtml(text: string): string {
|
|
151
89
|
return text
|
|
152
90
|
.replace(/&/g, '&')
|
|
@@ -157,6 +95,7 @@ function escapeHtml(text: string): string {
|
|
|
157
95
|
export function cleanContent(text: string): string {
|
|
158
96
|
if (!text) return '';
|
|
159
97
|
let result = text
|
|
98
|
+
.replace(/<[^>]+>/g, '') // strip pre-rendered HTML/MathML
|
|
160
99
|
.replace(/\*([^*]+)\*/g, '$1')
|
|
161
100
|
.replace(/~([^~]+)~/g, '_$1')
|
|
162
101
|
.replace(/\n[ \t]*\* /g, '; ')
|
|
@@ -165,7 +104,5 @@ export function cleanContent(text: string): string {
|
|
|
165
104
|
.replace(/\{\{urn:[^,}]+,([^,}]+)(?:,[^}]+)?\}\}/g, '$1')
|
|
166
105
|
.replace(/\{urn:[^,}]+,([^,}]+)(?:,[^}]+)?\}/g, '$1')
|
|
167
106
|
.replace(/\{\{([^,}]+)(?:,\s*[^}]+)?\}\}/g, '$1');
|
|
168
|
-
result = replaceBracketed(result, 'stem:', (math) => math);
|
|
169
|
-
result = replaceBracketed(result, 'latexmath:', (math) => math);
|
|
170
107
|
return result;
|
|
171
108
|
}
|