@aleph-alpha/chat-kit 3.1.2 → 3.1.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": "@aleph-alpha/chat-kit",
3
- "version": "3.1.2",
3
+ "version": "3.1.3",
4
4
  "description": "Chat kit package for building chat interfaces with Vue",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -67,8 +67,11 @@
67
67
  "dependencies": {
68
68
  "@aleph-alpha/ui-library": "^2.8.3",
69
69
  "dompurify": "^3.3.2",
70
+ "katex": "^0.17.0",
71
+ "rehype-katex": "^7.0.1",
70
72
  "rehype-stringify": "^10.0.1",
71
73
  "remark-gfm": "^4.0.1",
74
+ "remark-math": "^6.0.0",
72
75
  "remark-parse": "^11.0.0",
73
76
  "remark-rehype": "^11.1.2",
74
77
  "unified": "^11.0.5"
@@ -95,9 +98,10 @@
95
98
  "unocss-preset-animations": "^1.3.0",
96
99
  "unocss-preset-shadcn": "^1.0.1",
97
100
  "vite": "^7.3.2",
98
- "vitest": "4.0.15",
101
+ "vitest": "3.2.6",
99
102
  "vue": "^3.5.27",
100
- "vue-tsc": "^3.2.4"
103
+ "vue-tsc": "^3.2.4",
104
+ "@aleph-alpha/tsconfig-frontend": "0.7.0"
101
105
  },
102
106
  "scripts": {
103
107
  "build": "vite build && vue-tsc --project tsconfig.build.json --declaration --emitDeclarationOnly --outDir dist",
@@ -344,6 +344,56 @@ export const GfmFeatures: Story = {
344
344
  parameters: { docs: { source: { code: gfmFeaturesTemplateSource } } },
345
345
  };
346
346
 
347
+ const mathTemplateSource = `<script setup lang="ts">
348
+ import { CkMarkdownRenderer } from '@aleph-alpha/chat-kit';
349
+ const content = \`# Math Rendering
350
+
351
+ Inline math: $E = mc^2$
352
+
353
+ Display math:
354
+
355
+ $$
356
+ \\\\int_{-\\\\infty}^{\\\\infty} e^{-x^2} dx = \\\\sqrt{\\\\pi}
357
+ $$
358
+
359
+ The quadratic formula is $x = \\\\frac{-b \\\\pm \\\\sqrt{b^2 - 4ac}}{2a}$.
360
+ \`;
361
+ </script>
362
+
363
+ <template>
364
+ <CkMarkdownRenderer :content="content" />
365
+ </template>`;
366
+
367
+ /**
368
+ * Renders inline and display math expressions using KaTeX.
369
+ * Inline math uses single dollar signs ($...$) and display math uses
370
+ * double dollar signs ($$...$$).
371
+ */
372
+ export const MathExpressions: Story = {
373
+ args: {
374
+ content: [
375
+ '# Math Rendering',
376
+ '',
377
+ 'Inline math: $E = mc^2$',
378
+ '',
379
+ 'Display math:',
380
+ '',
381
+ '$$',
382
+ '\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}',
383
+ '$$',
384
+ '',
385
+ 'The quadratic formula is $x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}$.',
386
+ '',
387
+ 'A matrix:',
388
+ '',
389
+ '$$',
390
+ '\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}',
391
+ '$$',
392
+ ].join('\n'),
393
+ },
394
+ parameters: { docs: { source: { code: mathTemplateSource } } },
395
+ };
396
+
347
397
  function visitTextNodes(node: any, transform: (value: string) => string): void {
348
398
  if (node.type === 'text') {
349
399
  node.value = transform(node.value);
@@ -109,4 +109,35 @@ describe('useMarkdownParser', () => {
109
109
 
110
110
  expect(html.value).toBe('<p>Hello world</p>');
111
111
  });
112
+
113
+ it('renders inline math with single dollar signs', () => {
114
+ const source = ref('The formula $E = mc^2$ is famous.');
115
+ const { html } = useMarkdownParser(source, ref(undefined));
116
+
117
+ expect(html.value).toContain('class="katex"');
118
+ expect(html.value).toContain('E = mc^2');
119
+ });
120
+
121
+ it('renders display math with double dollar signs', () => {
122
+ const source = ref('$$\n\\int_0^1 x^2 dx\n$$');
123
+ const { html } = useMarkdownParser(source, ref(undefined));
124
+
125
+ expect(html.value).toContain('class="katex-display"');
126
+ });
127
+
128
+ it('preserves MathML elements through sanitization', () => {
129
+ const source = ref('$x^2$');
130
+ const { html } = useMarkdownParser(source, ref(undefined));
131
+
132
+ expect(html.value).toContain('<math');
133
+ expect(html.value).toContain('<semantics>');
134
+ });
135
+
136
+ it('does not treat a lone dollar sign as math', () => {
137
+ const source = ref('The price is $5.');
138
+ const { html } = useMarkdownParser(source, ref(undefined));
139
+
140
+ expect(html.value).not.toContain('class="katex"');
141
+ expect(html.value).toContain('$5');
142
+ });
112
143
  });
@@ -2,18 +2,82 @@ import { computed, type MaybeRefOrGetter, toValue } from 'vue';
2
2
  import { unified, type PluggableList } from 'unified';
3
3
  import remarkParse from 'remark-parse';
4
4
  import remarkGfm from 'remark-gfm';
5
+ import remarkMath from 'remark-math';
5
6
  import remarkRehype from 'remark-rehype';
7
+ import rehypeKatex from 'rehype-katex';
6
8
  import rehypeStringify from 'rehype-stringify';
7
- import DOMPurify from 'dompurify';
9
+ import DOMPurify, { type Config } from 'dompurify';
10
+ import 'katex/dist/katex.min.css';
8
11
 
9
12
  type SyncProcessor = { processSync: (value: string) => { toString(): string } };
10
13
 
14
+ const KATEX_MATHML_TAGS = [
15
+ 'math',
16
+ 'semantics',
17
+ 'annotation',
18
+ 'mrow',
19
+ 'mi',
20
+ 'mo',
21
+ 'mn',
22
+ 'ms',
23
+ 'mtext',
24
+ 'mspace',
25
+ 'msup',
26
+ 'msub',
27
+ 'msubsup',
28
+ 'mfrac',
29
+ 'mover',
30
+ 'munder',
31
+ 'munderover',
32
+ 'msqrt',
33
+ 'mroot',
34
+ 'mtable',
35
+ 'mtr',
36
+ 'mtd',
37
+ 'menclose',
38
+ 'mpadded',
39
+ 'mphantom',
40
+ ] as const;
41
+
42
+ const KATEX_MATHML_ATTRS = [
43
+ 'xmlns',
44
+ 'encoding',
45
+ 'mathvariant',
46
+ 'displaystyle',
47
+ 'scriptlevel',
48
+ 'fence',
49
+ 'separator',
50
+ 'stretchy',
51
+ 'symmetric',
52
+ 'lspace',
53
+ 'rspace',
54
+ 'movablelimits',
55
+ 'accent',
56
+ 'accentunder',
57
+ 'linethickness',
58
+ 'columnalign',
59
+ 'rowalign',
60
+ 'columnspan',
61
+ 'rowspan',
62
+ 'width',
63
+ 'height',
64
+ 'depth',
65
+ 'minsize',
66
+ 'maxsize',
67
+ 'aria-hidden',
68
+ ] as const;
69
+
70
+ const SANITIZE_CONFIG: Config = {
71
+ ADD_TAGS: [...KATEX_MATHML_TAGS],
72
+ ADD_ATTR: [...KATEX_MATHML_ATTRS],
73
+ };
74
+
11
75
  function buildProcessor(plugins: PluggableList): SyncProcessor {
12
- let proc = unified().use(remarkParse).use(remarkGfm);
76
+ let proc = unified().use(remarkParse).use(remarkGfm).use(remarkMath);
13
77
  for (const plugin of plugins) {
14
78
  proc = proc.use(plugin as any);
15
79
  }
16
- return proc.use(remarkRehype).use(rehypeStringify);
80
+ return proc.use(remarkRehype).use(rehypeKatex).use(rehypeStringify);
17
81
  }
18
82
 
19
83
  export function useMarkdownParser(
@@ -30,16 +94,16 @@ export function useMarkdownParser(
30
94
  return cachedProcessor;
31
95
  }
32
96
 
33
- const html = computed(() => {
97
+ const html = computed<string>(() => {
34
98
  const markdown = toValue(source);
35
99
  if (!markdown) return '';
36
100
  try {
37
101
  const raw = String(
38
102
  getProcessor(toValue(plugins) ?? []).processSync(markdown),
39
103
  );
40
- return DOMPurify.sanitize(raw);
104
+ return DOMPurify.sanitize(raw, SANITIZE_CONFIG) as string;
41
105
  } catch {
42
- return DOMPurify.sanitize(markdown);
106
+ return DOMPurify.sanitize(markdown, SANITIZE_CONFIG) as string;
43
107
  }
44
108
  });
45
109