test(web): cover TeX math rendering
This commit is contained in:
@@ -11,6 +11,7 @@ interface RemarkProcessor {
|
||||
}
|
||||
|
||||
const previousBackslash: Previous = function (code) {
|
||||
/* v8 ignore next -- micromark calls previous after an event has been emitted. */
|
||||
return code !== codes.backslash || this.events.at(-1)?.[1].type === types.characterEscape
|
||||
}
|
||||
|
||||
@@ -20,6 +21,7 @@ const tokenizeBackslashMathText: Tokenizer = function (effects, ok, nok) {
|
||||
return start
|
||||
|
||||
function start(code: number | null): State | undefined {
|
||||
/* v8 ignore next -- the text construct is dispatched only for a backslash. */
|
||||
if (code !== codes.backslash) return nok(code)
|
||||
effects.enter('mathText')
|
||||
effects.enter('mathTextSequence')
|
||||
@@ -72,6 +74,7 @@ const tokenizeBackslashMathText: Tokenizer = function (effects, ok, nok) {
|
||||
return slash
|
||||
|
||||
function slash(code: number | null): State | undefined {
|
||||
/* v8 ignore next -- this partial construct is attempted only at a backslash. */
|
||||
if (code !== codes.backslash) return closeNok(code)
|
||||
closeEffects.enter('mathTextSequence')
|
||||
closeEffects.consume(code)
|
||||
@@ -98,6 +101,7 @@ function createMathFlow(marker: number, openMarker: number, closeMarker: number,
|
||||
return start
|
||||
|
||||
function start(code: number | null): State | undefined {
|
||||
/* v8 ignore next -- the flow construct is dispatched only for its marker. */
|
||||
if (code !== marker) return nok(code)
|
||||
effects.enter('mathFlow')
|
||||
effects.enter('mathFlowFence')
|
||||
@@ -124,6 +128,7 @@ function createMathFlow(marker: number, openMarker: number, closeMarker: number,
|
||||
return effects.attempt({ partial: true, tokenize: tokenizeClosingFence }, closed, markerValueStart)(code)
|
||||
}
|
||||
if (markdownLineEnding(code)) {
|
||||
/* v8 ignore next -- micromark gives same-line dollar flow to remark-math before this continuation branch. */
|
||||
return multiline
|
||||
? effects.attempt(nonLazyContinuation, afterContinuation, nok)(code)
|
||||
: nok(code)
|
||||
@@ -218,7 +223,9 @@ const tokenizeNonLazyContinuation: Tokenizer = function (effects, ok, nok) {
|
||||
return start
|
||||
|
||||
function start(code: number | null): State | undefined {
|
||||
/* v8 ignore next -- continuation constructs are attempted only after a line ending. */
|
||||
if (code === codes.eof) return ok(code)
|
||||
/* v8 ignore next -- continuation constructs are attempted only after a line ending. */
|
||||
if (!markdownLineEnding(code)) return nok(code)
|
||||
effects.enter(types.lineEnding)
|
||||
effects.consume(code)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// @vitest-environment jsdom
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import type { Extension } from 'micromark-util-types'
|
||||
import { JsonBlock, MarkdownText, MessageText } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { remarkMathCompatibility } from '../src/markdown/remarkMathCompatibility.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -171,6 +173,91 @@ describe('MarkdownText', () => {
|
||||
expect(container.querySelector('a')).toBeNull()
|
||||
})
|
||||
|
||||
it('renders common TeX delimiters and same-line tagged display blocks after the reply settles', () => {
|
||||
const source = [
|
||||
'Inline dollar $\\theta$ and backslash \\(\\frac{1}{5}\\).',
|
||||
'',
|
||||
'\\[\\frac{\\pi}{4} < \\theta < \\frac{\\pi}{2}\\]',
|
||||
'',
|
||||
'$$\\theta \\in \\left(\\frac{\\pi}{4}, \\frac{\\pi}{2}\\right). \\tag{1}$$',
|
||||
'',
|
||||
'| Symbol | Value |',
|
||||
'| --- | --- |',
|
||||
'| $\\theta$ | \\(\\frac{1}{5}\\) |',
|
||||
].join('\n')
|
||||
const { container } = render(<MarkdownText text={source} />)
|
||||
|
||||
expect(container.querySelectorAll('.katex')).toHaveLength(6)
|
||||
expect(container.querySelectorAll('.katex-display')).toHaveLength(2)
|
||||
expect(container.querySelector('.katex-display annotation')?.textContent).toContain('\\frac{\\pi}{4}')
|
||||
expect([...container.querySelectorAll('.katex-display')].at(-1)?.querySelector('annotation')?.textContent)
|
||||
.toContain('\\tag{1}')
|
||||
expect(container.querySelector('.katex-error')).toBeNull()
|
||||
expect(container.querySelector('table .katex')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('keeps backslash delimiters correct across Markdown boundaries and malformed candidates', () => {
|
||||
const cases = [
|
||||
{
|
||||
source: '\\(\\alpha \\, \\beta\\)',
|
||||
math: 1,
|
||||
display: 0,
|
||||
},
|
||||
{
|
||||
source: '\\(\\frac{1}{5}\n+\\frac{1}{7}\\)',
|
||||
math: 1,
|
||||
display: 0,
|
||||
},
|
||||
{
|
||||
source: '> \\[\n> \\frac{1}{5}\n> \\]',
|
||||
math: 1,
|
||||
display: 1,
|
||||
},
|
||||
{
|
||||
source: '- \\[\n \\frac{1}{5}\n \\]',
|
||||
math: 1,
|
||||
display: 1,
|
||||
},
|
||||
]
|
||||
|
||||
for (const item of cases) {
|
||||
const rendered = render(<MarkdownText text={item.source} />)
|
||||
expect(rendered.container.querySelectorAll('.katex')).toHaveLength(item.math)
|
||||
expect(rendered.container.querySelectorAll('.katex-display')).toHaveLength(item.display)
|
||||
expect(rendered.container.querySelector('.katex-error')).toBeNull()
|
||||
rendered.unmount()
|
||||
}
|
||||
|
||||
const literal = render(<MarkdownText text={'\\\\(x\\)\n\n\\[x\n\n$$x$$ trailing'} />)
|
||||
expect(literal.container.querySelectorAll('.katex')).toHaveLength(1)
|
||||
expect(literal.container.querySelector('.katex-display')).toBeNull()
|
||||
expect(literal.container.textContent).toContain('[x')
|
||||
expect(literal.container.textContent).toContain('xxx trailing')
|
||||
})
|
||||
|
||||
it('keeps ordinary dollar blocks and incomplete delimiter candidates parseable', () => {
|
||||
const sources = [
|
||||
'$$\n\\theta\n$$',
|
||||
'$$$\\theta$$$',
|
||||
' \\[\n \\theta\n \\]',
|
||||
'\\(\\theta',
|
||||
'> \\[\nnot a quoted continuation\n\\]',
|
||||
]
|
||||
|
||||
for (const source of sources) {
|
||||
const rendered = render(<MarkdownText text={source} />)
|
||||
expect(rendered.container.querySelector('.katex-error')).toBeNull()
|
||||
rendered.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
it('registers the compatibility extension on a bare remark processor', () => {
|
||||
const data: { micromarkExtensions?: Extension[] } = {}
|
||||
remarkMathCompatibility.call({ data: () => data })
|
||||
|
||||
expect(data.micromarkExtensions).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('defers TeX rendering while streaming so incomplete formulas never flash KaTeX errors', () => {
|
||||
const partial = '$$\n\\frac{\\partial \\mathbf{u}}{\\partial'
|
||||
const complete = '$$\n\\frac{\\partial \\mathbf{u}}{\\partial t}\n$$'
|
||||
|
||||
Reference in New Issue
Block a user