# Your LLM Writes \(x\), Your Markdown Parser Wants $x$

> Source: <https://dev.to/duz52/your-llm-writes-x-your-markdown-parser-wants-x-3n3l>
> Published: 2026-07-31 07:54:51+00:00

**TL;DR** — LLMs love `\(...\)`

and `\[...\]`

. Most Markdown math plugins only speak `$...$`

and `$$...$$`

. I tried to bridge that with a regex, failed in interesting ways, and ended up teaching the tokenizer instead. Two packages came out of it: [ micromark-extension-math-extended](https://www.npmjs.com/package/micromark-extension-math-extended) and

`remark-math-extended`

Sometimes the hard part of rendering math isn't the equation. It's agreeing on where the equation *starts and stops*.

I was piping Markdown from some OpenAI models into a rendering pipeline, and the output kept looking like this:

```
The lift coefficient is \(C_L\).

\[
L = \frac{1}{2} \rho v^2 S C_L
\]
```

Nothing wrong here. This is textbook TeX:

`\( ... \)`

→ inline math`\[ ... \]`

→ display mathBut every Markdown math package in my toolchain wanted dollars:

``` bash
The lift coefficient is $C_L$.

$$
L = \frac{1}{2} \rho v^2 S C_L
$$
```

Two characters of difference. Weeks of my life. Let's go.

The obvious move is to preprocess the model output before it hits the parser:

``` php
\(x\)  ->  $x$
\[x\]  ->  $$x$$
```

A regex handles the happy path. Real Markdown is not the happy path.

Your converter now has to *not* touch delimiters that live inside:

That last one bites hard:

```
\begin{cases}
x \\[1em]
y
\end{cases}
```

`\\[1em]`

is a TeX line break with optional spacing. That `[`

is **not** the start of a display equation. Your regex does not know this. Your regex has never known anything.

And then there's the genuinely dangerous case:

```
\[
not closed

# Everything after this
```

If the model forgets the closing `\]`

— which happens constantly when you're streaming — a naive converter swallows the rest of the document into one enormous equation.

At the point where you've handled all of that, congratulations: you didn't write a preprocessor. You wrote a second Markdown parser, and now you maintain two.

So instead of rewriting the input *before* parsing, I added the TeX-style delimiters directly to the micromark tokenizer. Parse it properly once, and all of the above stops being your problem.

That turned into two packages.

`micromark-extension-math-extended`

The low-level one, for projects using micromark directly.

```
npm install micromark-extension-math-extended
js
import {micromark} from 'micromark'
import {math, mathHtml} from 'micromark-extension-math-extended'

const markdown = String.raw`
The lift coefficient is \(C_L\).

\[
L = \frac{1}{2} \rho v^2 S C_L
\]
`

const html = micromark(markdown, {
  extensions: [math()],
  htmlExtensions: [mathHtml()]
})

console.log(html)
```

All three forms work, and they keep the meaning you'd expect:

| Syntax | Renders as |
|---|---|
`$C_L$` |
inline |
`\(C_L\)` |
inline |
`$$C_L$$` |
display |
`\[C_L\]` |
display |

`remark-math-extended`

The higher-level one, for remark / unified. Drop-in replacement for `remark-math`

:

```
npm install remark-math-extended
python
import rehypeKatex from 'rehype-katex'
import rehypeStringify from 'rehype-stringify'
import remarkMath from 'remark-math-extended'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import {unified} from 'unified'

const markdown = String.raw`
The lift coefficient is \(C_L\).

\[
L = \frac{1}{2} \rho v^2 S C_L
\]
`

const file = await unified()
  .use(remarkParse)
  .use(remarkMath)
  .use(remarkRehype)
  .use(rehypeKatex)
  .use(rehypeStringify)
  .process(markdown)

console.log(String(file))
```

This is the part I care about most, so it gets its own heading.

**Rule: \[ must have a matching \].** When it doesn't, the parser falls back to ordinary Markdown instead of eating the remainder of your document.

If you're rendering token-by-token from an LLM, every single frame is technically malformed input. A response that's half-arrived shouldn't make your UI explode into one giant KaTeX block and then unexplode a second later.

The tokenizer also tells a real nested opener apart from legitimate TeX like `\\[1em]`

.

These two behaviors are the entire reason I went parser-level instead of regex-level.

One intentional incompatibility: in standard CommonMark, a backslash escapes punctuation, so `\(`

and `\[`

mean *literal* `(`

and `[`

. Turning on TeX-style delimiters changes that.

If you need the original behavior back:

```
math({backslashDelimiters: false})
```

or with remark:

```
unified().use(remarkMath, {
  backslashDelimiters: false
})
```

Dollar-delimited math keeps working either way.

`remark-math-extended`

reuses the existing `mdast-util-math`

tree and serializer. The math value and its inline/display meaning survive the round trip, but serialization normalizes delimiters back to dollars:

``` php
\(x\)  ->  $x$
\[x\]  ->  $$x$$
```

Fine for most rendering pipelines. Not fine if you need byte-for-byte delimiter preservation. Open an issue if that's you — I'd like to know how common it is.

If you work with LLM-generated Markdown, scientific writing, or anything that mixes Markdown and TeX: **what edge cases have you hit?** I'm collecting them. The `\\[1em]`

one took me embarrassingly long to find, and I'm sure it's not the last.

If these saved you from maintaining yet another delimiter-conversion parser, you can [buy me a coffee](https://buymeacoffee.com/duz52) ☕
