diff --git a/examples/server/public/index.html.gz b/examples/server/public/index.html.gz index fc119561b..08322d9f4 100644 Binary files a/examples/server/public/index.html.gz and b/examples/server/public/index.html.gz differ diff --git a/examples/server/webui/package-lock.json b/examples/server/webui/package-lock.json index 38b578aef..1d0ab0236 100644 --- a/examples/server/webui/package-lock.json +++ b/examples/server/webui/package-lock.json @@ -12,6 +12,7 @@ "autoprefixer": "^10.4.20", "daisyui": "^4.12.14", "highlight.js": "^11.10.0", + "katex": "^0.16.15", "markdown-it": "^14.1.0", "postcss": "^8.4.49", "tailwindcss": "^3.4.15", diff --git a/examples/server/webui/package.json b/examples/server/webui/package.json index 7d53a7a28..48090c6de 100644 --- a/examples/server/webui/package.json +++ b/examples/server/webui/package.json @@ -18,6 +18,7 @@ "autoprefixer": "^10.4.20", "daisyui": "^4.12.14", "highlight.js": "^11.10.0", + "katex": "^0.16.15", "markdown-it": "^14.1.0", "postcss": "^8.4.49", "tailwindcss": "^3.4.15", diff --git a/examples/server/webui/public/demo-conversation.json b/examples/server/webui/public/demo-conversation.json index 84365019a..afed7142b 100644 --- a/examples/server/webui/public/demo-conversation.json +++ b/examples/server/webui/public/demo-conversation.json @@ -11,7 +11,7 @@ { "id": 1734087548327, "role": "assistant", - "content": "This is the formula:\n$\\frac{e^{x_i}}{\\sum_{j=1}^{n}e^{x_j}}$\n\nGiven an input vector \\(\\mathbf{x} = [x_1, x_2, \\ldots, x_n]\\)\n\n\\[\ny_i = \\frac{e^{x_i}}{\\sum_{j=1}^n e^{x_j}}\n\\]\n\nCode block latex:\n```latex\n\\frac{e^{x_i}}{\\sum_{j=1}^{n}e^{x_j}}\n```", + "content": "This is the formula:\n$\\frac{e^{x_i}}{\\sum_{j=1}^{n}e^{x_j}}$\n\nGiven an input vector \\(\\mathbf{x} = [x_1, x_2, \\ldots, x_n]\\)\n\n\\[\ny_i = \\frac{e^{x_i}}{\\sum_{j=1}^n e^{x_j}}\n\\]\n\nCode block latex:\n```latex\n\\frac{e^{x_i}}{\\sum_{j=1}^{n}e^{x_j}}\n```\n\nTest dollar sign: $1234 $4567\n\nInvalid latex syntax: $E = mc^$ and $$E = mc^$$", "timings": { "prompt_n": 1, "prompt_ms": 28.923, @@ -30,4 +30,4 @@ "content": "Code block js:\n```js\nconsole.log('hello world')\n```" } ] -} \ No newline at end of file +} diff --git a/examples/server/webui/src/katex-gpt.js b/examples/server/webui/src/katex-gpt.js new file mode 100644 index 000000000..7c7c5e22c --- /dev/null +++ b/examples/server/webui/src/katex-gpt.js @@ -0,0 +1,66 @@ +import katex from 'katex'; + +// Adapted from https://github.com/SchneeHertz/markdown-it-katex-gpt +// MIT license + +const defaultOptions = { + delimiters: [ + { left: '\\[', right: '\\]', display: true }, + { left: '\\(', right: '\\)', display: false }, + ], +}; + +export function renderLatexHTML(content, display = false) { + return katex.renderToString(content, { + throwOnError: false, + output: 'mathml', + displayMode: display, + }); +} + +function escapedBracketRule(options) { + return (state, silent) => { + const max = state.posMax; + const start = state.pos; + + for (const { left, right, display } of options.delimiters) { + + // Check if it starts with the left delimiter + if (!state.src.slice(start).startsWith(left)) continue; + + // Skip the length of the left delimiter + let pos = start + left.length; + + // Find the matching right delimiter + while (pos < max) { + if (state.src.slice(pos).startsWith(right)) { + break; + } + pos++; + } + + // No matching right delimiter found, skip to the next match + if (pos >= max) continue; + + // If not in silent mode, convert LaTeX formula to MathML + if (!silent) { + const content = state.src.slice(start + left.length, pos); + try { + const renderedContent = renderLatexHTML(content, display); + const token = state.push('html_inline', '', 0); + token.content = renderedContent; + } catch (e) { + console.error(e); + } + } + + // Update position, skip the length of the right delimiter + state.pos = pos + right.length; + return true; + } + } +} + +export default function (md, options = defaultOptions) { + md.inline.ruler.after('text', 'escaped_bracket', escapedBracketRule(options)); +} diff --git a/examples/server/webui/src/main.js b/examples/server/webui/src/main.js index 1b0653499..b670b0733 100644 --- a/examples/server/webui/src/main.js +++ b/examples/server/webui/src/main.js @@ -5,7 +5,8 @@ import TextLineStream from 'textlinestream'; // math formula rendering import 'katex/dist/katex.min.css'; -import markdownItKatex from '@vscode/markdown-it-katex'; +import markdownItKatexGpt, { renderLatexHTML } from './katex-gpt'; +import markdownItKatexNormal from '@vscode/markdown-it-katex'; // code highlighting import hljs from './highlight-config'; @@ -90,6 +91,9 @@ const VueMarkdown = defineComponent( const md = shallowRef(new MarkdownIt({ breaks: true, highlight: function (str, lang) { // Add highlight.js + if (lang === 'latex') { + return renderLatexHTML(str, true); + } if (lang && hljs.getLanguage(lang)) { try { return '
' +
@@ -100,7 +104,8 @@ const VueMarkdown = defineComponent(
return '' + md.value.utils.escapeHtml(str) + '
';
}
}));
- md.value.use(markdownItKatex, {
+ // support latex with double dollar sign and square brackets
+ md.value.use(markdownItKatexGpt, {
delimiters: [
{ left: '\\[', right: '\\]', display: true },
{ left: '\\(', right: '\\)', display: false },
@@ -108,6 +113,9 @@ const VueMarkdown = defineComponent(
],
throwOnError: false,
});
+ // support latex with single dollar sign
+ md.value.use(markdownItKatexNormal, { throwOnError: false });
+ // add copy button to code blocks
const origFenchRenderer = md.value.renderer.rules.fence;
md.value.renderer.rules.fence = (tokens, idx, ...args) => {
const content = tokens[idx].content;