Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
import { KATEX_OPTIONS } from '@/lib/utils/katex-options'

interface TransformationPlaygroundProps {
transformations: Transformation[] | undefined
Expand Down Expand Up @@ -128,7 +129,7 @@ export function TransformationPlayground({ transformations, selectedTransformati
<div className="prose prose-sm max-w-none dark:prose-invert">
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
rehypePlugins={[[rehypeKatex, KATEX_OPTIONS]]}
components={{
table: ({ children }) => (
<div className="my-4 overflow-x-auto">
Expand Down
33 changes: 32 additions & 1 deletion frontend/src/components/ui/markdown-editor.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest'
import { describe, it, expect, vi, afterEach } from 'vitest'
import { render } from '@testing-library/react'
import MarkdownPreview from '@uiw/react-markdown-preview'

Expand Down Expand Up @@ -58,6 +58,37 @@ describe('MarkdownEditor preview sanitization', () => {
expect(container.querySelector('.katex-mathml math')).not.toBeNull()
})

describe('KATEX_OPTIONS unknownSymbol suppression', () => {
// KaTeX's default strict mode warns on every Unicode character outside
// its symbol table. AI-generated content routinely has one inside a
// legitimate single-dollar math span (this project's prompts steer
// models to emit inline math as $...$) - e.g. an en-dash in a price
// range. That specific warning is intentionally suppressed; every other
// strict warning is not, so a future edit that widens the ignore list
// (or removes it) shows up here instead of silently regressing.
afterEach(() => {
vi.restoreAllMocks()
})

it('does not warn on the em/en-dash-in-price-range pattern that motivated this fix', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
renderPreview('Price range: $5 – $10')
const unknownSymbolWarnings = warn.mock.calls.filter(([msg]) =>
typeof msg === 'string' && msg.includes('[unknownSymbol]')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The bracket-wrapped error code filter ([unknownSymbol], [commentAtEnd]) may not match KaTeX's actual console.warn format, which uses KaTeX <code>: <message> without brackets. If so, the first test passes trivially (no filter match even when warnings exist) and the second fails. Suggest checking the actual format by inspecting KaTeX's Settings.reportError or running the tests once. If format lacks brackets, the test would need updated assertion strings.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/ui/markdown-editor.test.tsx, line 77:

<comment>The bracket-wrapped error code filter (`[unknownSymbol]`, `[commentAtEnd]`) may not match KaTeX's actual console.warn format, which uses `KaTeX <code>: <message>` without brackets. If so, the first test passes trivially (no filter match even when warnings exist) and the second fails. Suggest checking the actual format by inspecting KaTeX's Settings.reportError or running the tests once. If format lacks brackets, the test would need updated assertion strings.</comment>

<file context>
@@ -58,6 +58,37 @@ describe('MarkdownEditor preview sanitization', () => {
+      const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
+      renderPreview('Price range: $5 – $10')
+      const unknownSymbolWarnings = warn.mock.calls.filter(([msg]) =>
+        typeof msg === 'string' && msg.includes('[unknownSymbol]')
+      )
+      expect(unknownSymbolWarnings).toHaveLength(0)
</file context>

)
expect(unknownSymbolWarnings).toHaveLength(0)
})

it('still warns on a different strict violation (comment with no terminating newline)', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
renderPreview('Still checked: $x^2 %comment$')
const commentWarnings = warn.mock.calls.filter(([msg]) =>
typeof msg === 'string' && msg.includes('[commentAtEnd]')
)
expect(commentWarnings.length).toBeGreaterThan(0)
})
})

it('still syntax-highlights fenced code blocks', () => {
const { container } = renderPreview('```python\ndef hello():\n return 42\n```')
expect(container.querySelectorAll('span[class*="token"]').length).toBeGreaterThan(0)
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/ui/markdown-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import rehypeKatex from 'rehype-katex'
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'
import type { PluggableList } from 'unified'

import { KATEX_OPTIONS } from '@/lib/utils/katex-options'

const MDEditor = dynamic(
() => import('@uiw/react-md-editor').then((mod) => mod.default),
{ ssr: false }
Expand Down Expand Up @@ -46,7 +48,7 @@ const SANITIZE_SCHEMA = {

export const PREVIEW_OPTIONS = {
remarkPlugins: [remarkMath] as PluggableList,
rehypePlugins: [[rehypeSanitize, SANITIZE_SCHEMA], rehypeKatex] as PluggableList,
rehypePlugins: [[rehypeSanitize, SANITIZE_SCHEMA], [rehypeKatex, KATEX_OPTIONS]] as PluggableList,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The Unicode-warning fix has no regression test for the behavior it changes, so future edits can reintroduce KaTeX warnings while the current math-rendering test still passes. A preview test for a case such as $5 – $10$ should assert that the expected KaTeX warning is absent while a non-unknownSymbol strict warning remains covered.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/ui/markdown-editor.tsx, line 51:

<comment>The Unicode-warning fix has no regression test for the behavior it changes, so future edits can reintroduce KaTeX warnings while the current math-rendering test still passes. A preview test for a case such as `$5 – $10$` should assert that the expected KaTeX warning is absent while a non-`unknownSymbol` strict warning remains covered.</comment>

<file context>
@@ -46,7 +48,7 @@ const SANITIZE_SCHEMA = {
 export const PREVIEW_OPTIONS = {
   remarkPlugins: [remarkMath] as PluggableList,
-  rehypePlugins: [[rehypeSanitize, SANITIZE_SCHEMA], rehypeKatex] as PluggableList,
+  rehypePlugins: [[rehypeSanitize, SANITIZE_SCHEMA], [rehypeKatex, KATEX_OPTIONS]] as PluggableList,
 }
 
</file context>

}

export interface MarkdownEditorProps {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/ui/markdown-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'

import { useThemeStore } from '@/lib/stores/theme-store'
import { KATEX_OPTIONS } from '@/lib/utils/katex-options'
import { oneDark as darkTheme } from 'react-syntax-highlighter/dist/esm/styles/prism'
import { oneLight as lightTheme } from 'react-syntax-highlighter/dist/esm/styles/prism'
// PrismLight with an explicit language set instead of the full Prism build:
Expand Down Expand Up @@ -63,7 +64,7 @@ export function MarkdownRenderer({ children, components = {}}: { children: React
<div className="prose prose-sm prose-neutral dark:prose-invert max-w-none break-words prose-headings:font-semibold prose-a:text-blue-600 prose-a:break-all prose-code:before:content-none prose-code:after:content-none prose-pre:p-0 prose-pre:bg-transparent prose-p:mb-4 prose-p:leading-7 prose-li:mb-2">
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
rehypePlugins={[[rehypeKatex, KATEX_OPTIONS]]}
components={{...{
p: ({ children }) => <p className="mb-4">{children}</p>,
h1: ({ children }) => <h1 className="mb-4 mt-6">{children}</h1>,
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/lib/utils/katex-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { KatexOptions } from 'katex'

// AI-generated content often contains Unicode punctuation (en/em dashes,
// smart quotes) inside single-dollar math spans that models emit for
// legitimate inline math (see prompts/*/system.jinja "MATH FORMATTING"
// sections). KaTeX's default strict mode logs a console warning for each
// character outside its symbol table. It still renders - falling back to
// text-mode handling for that character (see katex's Parser: unknown
// codepoints get `mode: 'text'` instead of proper math-mode metrics) - so
// this is not universally cosmetic: an unrecognized symbol can come out
// with approximated spacing/metrics rather than a "real" glyph. Acceptable
// for stray punctuation like a dash; don't extend this ignore to justify
// dumping arbitrary Unicode into math mode. Every other strict check (e.g.
// deprecated commands) is untouched.
export const KATEX_OPTIONS: KatexOptions = {
strict: (errorCode) => (errorCode === 'unknownSymbol' ? 'ignore' : 'warn'),
}
Loading