diff --git a/frontend/src/app/(dashboard)/transformations/components/TransformationPlayground.tsx b/frontend/src/app/(dashboard)/transformations/components/TransformationPlayground.tsx index 006e257af8..6dbd7b10c7 100644 --- a/frontend/src/app/(dashboard)/transformations/components/TransformationPlayground.tsx +++ b/frontend/src/app/(dashboard)/transformations/components/TransformationPlayground.tsx @@ -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 @@ -128,7 +129,7 @@ export function TransformationPlayground({ transformations, selectedTransformati
(
diff --git a/frontend/src/components/ui/markdown-editor.test.tsx b/frontend/src/components/ui/markdown-editor.test.tsx index 0015cea09b..05f4982235 100644 --- a/frontend/src/components/ui/markdown-editor.test.tsx +++ b/frontend/src/components/ui/markdown-editor.test.tsx @@ -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' @@ -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]') + ) + 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) diff --git a/frontend/src/components/ui/markdown-editor.tsx b/frontend/src/components/ui/markdown-editor.tsx index e6caeff4bf..471d4ac173 100644 --- a/frontend/src/components/ui/markdown-editor.tsx +++ b/frontend/src/components/ui/markdown-editor.tsx @@ -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 } @@ -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, } export interface MarkdownEditorProps { diff --git a/frontend/src/components/ui/markdown-renderer.tsx b/frontend/src/components/ui/markdown-renderer.tsx index 212e2b1089..c8320c9265 100644 --- a/frontend/src/components/ui/markdown-renderer.tsx +++ b/frontend/src/components/ui/markdown-renderer.tsx @@ -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: @@ -63,7 +64,7 @@ export function MarkdownRenderer({ children, components = {}}: { children: React

{children}

, h1: ({ children }) =>

{children}

, diff --git a/frontend/src/lib/utils/katex-options.ts b/frontend/src/lib/utils/katex-options.ts new file mode 100644 index 0000000000..d72e767516 --- /dev/null +++ b/frontend/src/lib/utils/katex-options.ts @@ -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'), +}