Skip to content

Commit a38265b

Browse files
authored
feat: add support for math notation using katex (#280)
Closes #263
1 parent 869decd commit a38265b

File tree

7 files changed

+125
-4
lines changed

7 files changed

+125
-4
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A minimal web-UI for talking to [Ollama](https://github.com/jmorganca/ollama/) s
99
- Large prompt fields
1010
- Support for reasoning models
1111
- Markdown rendering with syntax highlighting
12+
- KaTeX math notation
1213
- Code editor features
1314
- Customizable system prompts & advanced Ollama parameters
1415
- Copy code snippets, messages or entire sessions

package-lock.json

+36
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@
4343
"eslint-config-prettier": "^9.1.0",
4444
"eslint-plugin-svelte": "^2.45.1",
4545
"highlight.js": "^11.9.0",
46+
"katex": "^0.16.21",
4647
"lucide-svelte": "^0.372.0",
4748
"markdown-it": "^14.1.0",
49+
"markdown-it-texmath": "^1.0.0",
4850
"ollama": "^0.5.9",
4951
"openai": "^4.66.1",
5052
"postcss": "^8.4.32",

src/lib/components/Markdown.svelte

+36-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,43 @@
11
<script lang="ts">
22
import hljs from 'highlight.js';
3+
import katex from 'katex';
4+
import texmath from 'markdown-it-texmath';
35
import MarkdownIt from 'markdown-it/lib/index.mjs';
46
import { mount, onMount } from 'svelte';
57
68
import 'highlight.js/styles/github.min.css';
9+
import 'katex/dist/katex.min.css';
710
811
import ButtonCopy from './ButtonCopy.svelte';
912
1013
export let markdown: string;
1114
const CODE_SNIPPET_ID = 'code-snippet';
1215
16+
function normalizeMarkdown(content: string) {
17+
// Replace multiple newlines with double newlines
18+
content = content.replace(/\n{2,}/g, '\n\n');
19+
20+
// First, normalize display math blocks
21+
content = content.replace(/\n\\\[/g, '\n\n\\[');
22+
content = content.replace(/\\]\n/g, '\\]\n\n');
23+
24+
// Split on all math delimiters: \[...\], \(...\), and $...$
25+
// Using [\s\S] instead of . to match across lines
26+
const parts = content.split(/(\$[^$]+\$|\\[([^)]+\\]|\\[\s\S]+?\\])/g);
27+
content = parts
28+
.map((part) => {
29+
// If this part is any kind of math block, leave it unchanged
30+
if (part.startsWith('$') || part.startsWith('\\[') || part.startsWith('\\(')) {
31+
return part;
32+
}
33+
// Otherwise, wrap any \boxed commands in inline math
34+
return part.replace(/\\boxed\{((?:[^{}]|\{[^{}]*\})*)\}/g, '\\(\\boxed{$1}\\)');
35+
})
36+
.join('');
37+
38+
return content;
39+
}
40+
1341
function renderCodeSnippet(code: string) {
1442
return `<pre id="${CODE_SNIPPET_ID}"><code class="hljs">${code}</code></pre>`;
1543
}
@@ -30,6 +58,12 @@
3058
}
3159
});
3260
61+
// Math notation parsing with Katex, with multiple delimiters
62+
md.use(texmath, {
63+
engine: katex,
64+
delimiters: ['dollars', 'brackets', 'doxygen', 'gitlab', 'julia', 'kramdown', 'beg_end']
65+
});
66+
3367
onMount(() => {
3468
const preElements = document.querySelectorAll(`pre#${CODE_SNIPPET_ID}`);
3569
@@ -50,7 +84,8 @@
5084
getting formatted on auto-formatting.
5185
-->
5286
{#if markdown}
53-
{@html md.render(markdown)} <!-- eslint-disable-line svelte/no-at-html-tags -->
87+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
88+
{@html md.render(normalizeMarkdown(markdown))}
5489
{/if}
5590
</div>
5691

src/routes/motd/motd.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
`2025-03-23`
1+
`2025-03-24`
22

33
### Message of the day
44

55
# Welcome to Hollama: a simple web interface for [Ollama](https://ollama.ai)
66

77
#### What's new?
88

9+
- **KaTeX math notation** is now supported in model responses.
910
- **Reasoning responses** (i.e. [`deepseek-r1`](https://ollama.com/library/deepseek-r1)) are now displayed in a dedicated UI component.
10-
- **Multiple-server support** allows you to connect to one or more Ollama (and/or OpenAI) servers at the same time.
11-
- **Bonjour le monde!** UI is now available in French.
1211

1312
#### Previously, in Hollama
1413

14+
- **Multiple-server support** allows you to connect to one or more Ollama (and/or OpenAI) servers at the same time.
1515
- **Models list can be filtered** by keyword for each server.
1616
- **Servers can be labeled** to help you identify them in the models list.
1717
- **Hallo Welt!** UI is now available in German.

src/types/markdown-it-texmath.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare module 'markdown-it-texmath';

tests/session-interaction.test.ts

+46
Original file line numberDiff line numberDiff line change
@@ -607,4 +607,50 @@ test.describe('Session interaction', () => {
607607
'Here is how you can test your code effectively:\n\n1. Write unit tests\n2. Use integration tests\n3. Implement end-to-end testing'
608608
);
609609
});
610+
611+
test('renders math notation correctly using KaTeX', async ({ page }) => {
612+
// Mock a session with LaTeX math content
613+
await page.evaluate(() =>
614+
window.localStorage.setItem(
615+
'hollama-sessions',
616+
JSON.stringify([
617+
{
618+
id: 'math123',
619+
model: 'llama3',
620+
messages: [
621+
{
622+
role: 'user',
623+
content:
624+
'What is the formula for Pythagoras theorem in math notation using latex/katex\n'
625+
},
626+
{
627+
role: 'assistant',
628+
content:
629+
"The formula for Pythagoras' theorem, written in LaTeX (and rendered by KaTeX), is:\n\n\\[\\boxed{a^2 + b^2 = c^2}\\]\n\nwhere:\n- \\(a\\) and \\(b\\) are the lengths of the legs of a right triangle,\n- \\(c\\) is the length of the hypotenuse.",
630+
reasoning: ''
631+
}
632+
],
633+
context: [],
634+
updatedAt: new Date().toISOString()
635+
}
636+
])
637+
)
638+
);
639+
640+
// Navigate to the session with math content
641+
await page.goto('/sessions/math123');
642+
643+
// Wait for KaTeX to render
644+
await page.waitForSelector('.katex');
645+
646+
// Check that math notation is rendered correctly
647+
const eqnElements = await page.locator('eqn').count();
648+
const eqElements = await page.locator('eq').count();
649+
const katexSpans = await page.locator('span.katex').count();
650+
651+
// Assert that we have 3 instances of each
652+
expect(eqnElements).toBe(1);
653+
expect(eqElements).toBe(3);
654+
expect(katexSpans).toBe(4);
655+
});
610656
});

0 commit comments

Comments
 (0)