Ver código fonte

test(ui): strengthen TeX parser boundaries

fz 2 meses atrás
pai
commit
09a2e120f3

+ 7 - 2
packages/client/ui-primitives/src/markdown/remarkMathCompatibility.ts

@@ -1,3 +1,5 @@
+/** Extend upstream dollar-only math syntax with TeX delimiters while reusing its token vocabulary. */
+
 import { factorySpace } from 'micromark-factory-space'
 import type {} from 'micromark-extension-math'
 import { markdownLineEnding } from 'micromark-util-character'
@@ -11,8 +13,11 @@ 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
+  if (code !== codes.backslash) return true
+  const tail = this.events.at(-1)
+  /* v8 ignore next -- a previous code necessarily has a preceding event. */
+  if (tail === undefined) return false
+  return tail[1].type === types.characterEscape
 }
 
 const tokenizeBackslashMathText: Tokenizer = function (effects, ok, nok) {

+ 39 - 13
packages/client/ui-primitives/tests/markdown.spec.tsx

@@ -203,6 +203,12 @@ describe('MarkdownText', () => {
         math: 1,
         display: 0,
       },
+      {
+        source: String.raw`\\\(x\)`,
+        math: 1,
+        display: 0,
+        value: 'x',
+      },
       {
         source: '\\(\\frac{1}{5}\n+\\frac{1}{7}\\)',
         math: 1,
@@ -238,31 +244,51 @@ describe('MarkdownText', () => {
       rendered.unmount()
     }
 
-    const literal = render(<MarkdownText text={'\\\\(x\\)\n\n\\[x\n\n$$x$$ trailing'} />)
-    expect(literal.container.querySelectorAll('.katex')).toHaveLength(1)
+    const literal = render(<MarkdownText text={'\\\\(x\\)\n\n\\[x'} />)
+    expect(literal.container.querySelectorAll('.katex')).toHaveLength(0)
     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$$$',
-      '$$a$b\nc',
-      '  \\[\n  \\theta\n  \\]',
-      '\\(\\theta',
-      '\\[\n\\[',
-      '> \\[\nnot a quoted continuation\n\\]',
+    const cases = [
+      { source: '$$\n\\theta\n$$', math: 1, display: 1 },
+      { source: '$$$\\theta$$$', math: 1, display: 0 },
+      { source: '$$a$b\nc', math: 0, display: 0 },
+      { source: '  \\[\n  \\theta\n  \\]', math: 1, display: 1 },
+      { source: '\\(\\theta', math: 0, display: 0 },
+      { source: String.raw`\(a\\)`, math: 0, display: 0 },
+      { source: '\\[\n\\[', math: 0, display: 0 },
+      { source: '> \\[\nnot a quoted continuation\n\\]', math: 0, display: 0 },
     ]
 
-    for (const source of sources) {
-      const rendered = render(<MarkdownText text={source} />)
+    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()
     }
   })
 
+  it('lets display math interrupt an open paragraph', () => {
+    for (const source of ['Prose line\n\\[x\\]', 'Prose line\n$$x$$']) {
+      const rendered = render(<MarkdownText text={source} />)
+      expect(rendered.container.querySelectorAll('p')).toHaveLength(1)
+      expect(rendered.container.querySelectorAll('.katex-display')).toHaveLength(1)
+      rendered.unmount()
+    }
+  })
+
+  it('leaves a dollar block with trailing text to upstream inline math', () => {
+    const { container } = render(<MarkdownText text="$$x$$ trailing" />)
+
+    expect(container.querySelectorAll('.katex')).toHaveLength(1)
+    expect(container.querySelector('.katex-display')).toBeNull()
+    expect(container.querySelector('annotation')?.textContent).toBe('x')
+    expect(container.textContent).toContain('trailing')
+  })
+
   it('renders escaped dollars and even backslash pairs before closing fences', () => {
     const source = [
       String.raw`$$100\$$$`,