{"slug": "your-llm-writes-x-your-markdown-parser-wants-x", "title": "Your LLM Writes \\(x\\), Your Markdown Parser Wants $x$", "summary": "A developer built two open-source packages, micromark-extension-math-extended and remark-math-extended, to let Markdown parsers handle TeX-style math delimiters (\\(...\\) and \\[...\\]) commonly emitted by LLMs. The project adds these delimiters directly to the micromark tokenizer, avoiding fragile regex preprocessing and gracefully handling malformed input during streaming.", "body_md": "**TL;DR** — LLMs love `\\(...\\)`\n\nand `\\[...\\]`\n\n. Most Markdown math plugins only speak `$...$`\n\nand `$$...$$`\n\n. 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\n\n`remark-math-extended`\n\nSometimes the hard part of rendering math isn't the equation. It's agreeing on where the equation *starts and stops*.\n\nI was piping Markdown from some OpenAI models into a rendering pipeline, and the output kept looking like this:\n\n```\nThe lift coefficient is \\(C_L\\).\n\n\\[\nL = \\frac{1}{2} \\rho v^2 S C_L\n\\]\n```\n\nNothing wrong here. This is textbook TeX:\n\n`\\( ... \\)`\n\n→ inline math`\\[ ... \\]`\n\n→ display mathBut every Markdown math package in my toolchain wanted dollars:\n\n``` bash\nThe lift coefficient is $C_L$.\n\n$$\nL = \\frac{1}{2} \\rho v^2 S C_L\n$$\n```\n\nTwo characters of difference. Weeks of my life. Let's go.\n\nThe obvious move is to preprocess the model output before it hits the parser:\n\n``` php\n\\(x\\)  ->  $x$\n\\[x\\]  ->  $$x$$\n```\n\nA regex handles the happy path. Real Markdown is not the happy path.\n\nYour converter now has to *not* touch delimiters that live inside:\n\nThat last one bites hard:\n\n```\n\\begin{cases}\nx \\\\[1em]\ny\n\\end{cases}\n```\n\n`\\\\[1em]`\n\nis a TeX line break with optional spacing. That `[`\n\nis **not** the start of a display equation. Your regex does not know this. Your regex has never known anything.\n\nAnd then there's the genuinely dangerous case:\n\n```\n\\[\nnot closed\n\n# Everything after this\n```\n\nIf the model forgets the closing `\\]`\n\n— which happens constantly when you're streaming — a naive converter swallows the rest of the document into one enormous equation.\n\nAt 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.\n\nSo 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.\n\nThat turned into two packages.\n\n`micromark-extension-math-extended`\n\nThe low-level one, for projects using micromark directly.\n\n```\nnpm install micromark-extension-math-extended\njs\nimport {micromark} from 'micromark'\nimport {math, mathHtml} from 'micromark-extension-math-extended'\n\nconst markdown = String.raw`\nThe lift coefficient is \\(C_L\\).\n\n\\[\nL = \\frac{1}{2} \\rho v^2 S C_L\n\\]\n`\n\nconst html = micromark(markdown, {\n  extensions: [math()],\n  htmlExtensions: [mathHtml()]\n})\n\nconsole.log(html)\n```\n\nAll three forms work, and they keep the meaning you'd expect:\n\n| Syntax | Renders as |\n|---|---|\n`$C_L$` |\ninline |\n`\\(C_L\\)` |\ninline |\n`$$C_L$$` |\ndisplay |\n`\\[C_L\\]` |\ndisplay |\n\n`remark-math-extended`\n\nThe higher-level one, for remark / unified. Drop-in replacement for `remark-math`\n\n:\n\n```\nnpm install remark-math-extended\npython\nimport rehypeKatex from 'rehype-katex'\nimport rehypeStringify from 'rehype-stringify'\nimport remarkMath from 'remark-math-extended'\nimport remarkParse from 'remark-parse'\nimport remarkRehype from 'remark-rehype'\nimport {unified} from 'unified'\n\nconst markdown = String.raw`\nThe lift coefficient is \\(C_L\\).\n\n\\[\nL = \\frac{1}{2} \\rho v^2 S C_L\n\\]\n`\n\nconst file = await unified()\n  .use(remarkParse)\n  .use(remarkMath)\n  .use(remarkRehype)\n  .use(rehypeKatex)\n  .use(rehypeStringify)\n  .process(markdown)\n\nconsole.log(String(file))\n```\n\nThis is the part I care about most, so it gets its own heading.\n\n**Rule: \\[ must have a matching \\].** When it doesn't, the parser falls back to ordinary Markdown instead of eating the remainder of your document.\n\nIf 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.\n\nThe tokenizer also tells a real nested opener apart from legitimate TeX like `\\\\[1em]`\n\n.\n\nThese two behaviors are the entire reason I went parser-level instead of regex-level.\n\nOne intentional incompatibility: in standard CommonMark, a backslash escapes punctuation, so `\\(`\n\nand `\\[`\n\nmean *literal* `(`\n\nand `[`\n\n. Turning on TeX-style delimiters changes that.\n\nIf you need the original behavior back:\n\n```\nmath({backslashDelimiters: false})\n```\n\nor with remark:\n\n```\nunified().use(remarkMath, {\n  backslashDelimiters: false\n})\n```\n\nDollar-delimited math keeps working either way.\n\n`remark-math-extended`\n\nreuses the existing `mdast-util-math`\n\ntree and serializer. The math value and its inline/display meaning survive the round trip, but serialization normalizes delimiters back to dollars:\n\n``` php\n\\(x\\)  ->  $x$\n\\[x\\]  ->  $$x$$\n```\n\nFine 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.\n\nIf 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]`\n\none took me embarrassingly long to find, and I'm sure it's not the last.\n\nIf these saved you from maintaining yet another delimiter-conversion parser, you can [buy me a coffee](https://buymeacoffee.com/duz52) ☕", "url": "https://wpnews.pro/news/your-llm-writes-x-your-markdown-parser-wants-x", "canonical_source": "https://dev.to/duz52/your-llm-writes-x-your-markdown-parser-wants-x-3n3l", "published_at": "2026-07-31 07:54:51+00:00", "updated_at": "2026-07-31 08:03:17.842875+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "natural-language-processing"], "entities": ["micromark-extension-math-extended", "remark-math-extended", "micromark", "remark", "unified", "OpenAI", "KaTeX"], "alternates": {"html": "https://wpnews.pro/news/your-llm-writes-x-your-markdown-parser-wants-x", "markdown": "https://wpnews.pro/news/your-llm-writes-x-your-markdown-parser-wants-x.md", "text": "https://wpnews.pro/news/your-llm-writes-x-your-markdown-parser-wants-x.txt", "jsonld": "https://wpnews.pro/news/your-llm-writes-x-your-markdown-parser-wants-x.jsonld"}}