Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,3 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm run typecheck
- run: pnpm test

peer-matrix:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# supported @tanstack/markdown versions with inlineComponent nodes (>= 0.0.15);
# 0.0.13/0.0.14 still satisfy the peer range but only support 'html' output mode
markdown: ['0.0.15', '0.0.16', '1.0.0']
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --no-frozen-lockfile
- run: pnpm add -D @tanstack/markdown@${{ matrix.markdown }}
- run: pnpm run typecheck
- run: pnpm test
42 changes: 28 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pnpm add tanstack-markdown-math @tanstack/markdown katex
# yarn add tanstack-markdown-math @tanstack/markdown katex
```

`@tanstack/markdown` and `katex` are peer dependencies. A custom `render` option lets you avoid KaTeX at runtime, but it remains a peer dependency for now. `react` (>=18) is an optional peer, only needed for the `tanstack-markdown-math/react` subpath.
`@tanstack/markdown` (`>=1.0.0`) and `katex` are peer dependencies. A custom `render` option lets you avoid KaTeX at runtime, but it remains a peer dependency for now. `react` (>=18) is an optional peer, only needed for the `tanstack-markdown-math/react` subpath.

## Quick start

Expand Down Expand Up @@ -64,12 +64,12 @@ import {

- `mathExtension(opts?)` → `[mathBlockExtension(opts), mathInlineExtension(opts)]`
- `mathBlockExtension(opts?)` → parses `$$...$$` blocks into `html` nodes (`component` nodes with `output: 'component'`).
- `mathInlineExtension(opts?)` → walks the AST and converts `$...$` into `inlineHtml` nodes (`inlineComponent` nodes with `output: 'component'`).
- `mathInlineExtension(opts?)` → registers a `$` inline parser (`@tanstack/markdown` 1.0 `inlineParser`) that converts `$...$` / `$$...$$` into `inlineHtml` nodes (`inlineComponent` nodes with `output: 'component'`).

The extensions also export helpers used when pre-processing content through other tools:

- `protectMath(content)` → replaces `\`, `_` and `*` inside math spans with private-use characters (`MATH_BACKSLASH_SUB`, `MATH_UNDERSCORE_SUB`, `MATH_ASTERISK_SUB`) so other markdown passes cannot unescape TeX commands or turn subscripts into emphasis.
- `restoreMathChars(tex)` → inverse mapping, applied automatically before rendering.
- `protectMath(content)` → **deprecated** (not needed with `@tanstack/markdown` >= 1.0; math is parsed before emphasis/escape handling). Replaces replaces `\`, `_` and `*` inside math spans with private-use characters (`MATH_BACKSLASH_SUB`, `MATH_UNDERSCORE_SUB`, `MATH_ASTERISK_SUB`) so other markdown passes cannot unescape TeX commands or turn subscripts into emphasis. Kept for pre-protected content.
- `restoreMathChars(tex)` → **deprecated**, inverse mapping, applied automatically before rendering (no-op when content was not protected).

### `MathOptions`

Expand All @@ -80,15 +80,16 @@ The extensions also export helpers used when pre-processing content through othe
| `output` | `'html' \| 'component'` | `'html'` | Block and inline math. `'component'` emits `component`/`inlineComponent` nodes carrying raw TeX, removing the `allowHtml` requirement (see below). |
| `tagName` | `string` | `'MathBlock'` | Block-math only. Tag name of the `component` node, to be mapped through the renderers' `components` option. |
| `inlineTagName`| `string` | `'MathInline'` | Inline-math only. Tag name of the `inlineComponent` node, to be mapped through the renderers' `components` option. |
| `unclosedBlock`| `boolean` | `true` | Block-math only. `true` renders an unclosed `$$` block as partial math (streaming-friendly). `false` leaves it as plain text until the closing `$$` arrives. |
| `cache` | `boolean \| number` | `true` | Render cache (LRU, 1000 entries). `false` disables it — use this with an impure custom `render` (side effects, non-deterministic output). A number sets a custom entry limit. |

`mathBlockExtension` accepts `render`, `output`, `tagName` and `cache`; `mathInlineExtension` accepts `render`, `allowSpaces`, `output`, `inlineTagName` and `cache`.
`mathBlockExtension` accepts `render`, `output`, `tagName`, `unclosedBlock` and `cache`; `mathInlineExtension` accepts `render`, `allowSpaces`, `output`, `inlineTagName` and `cache`.

## Math without `allowHtml` (component output)

With `output: 'component'`, math is emitted as `component` (block) / `inlineComponent` (inline) nodes carrying the raw TeX in `properties.tex`, instead of pre-rendered `html`/`inlineHtml` nodes. The AST stays serialization-clean and no `allowHtml` is needed.

> Inline component output requires `@tanstack/markdown` >= 0.0.15 (`inlineComponent` node type, landed via [PR #17](https://github.com/TanStack/markdown/pull/17)).
> Requires `@tanstack/markdown` >= 1.0.0.

**HTML renderer** — zero configuration: the extension ships a `renderHtml` hook that renders both block and inline component nodes.

Expand Down Expand Up @@ -132,7 +133,9 @@ mathInlineExtension()

- `$x$` → rendered math
- `$5` or `$ 5 $` → left as literal text
- `$5 and $10` → left as literal text (no false monetary positives)
- `$5 and $10`, `$5-$10` → left as literal text (no false monetary positives: whitespace before the closing `$`, or a digit right after it, rejects the span)
- `\$5` → escaped dollar, literal text; `$a \$ b$` → valid math containing an escaped dollar
- `_`, `*` and `\` inside math need no pre-protection (`protectMath` is no longer required)

### Permissive mode

Expand Down Expand Up @@ -178,7 +181,9 @@ $$
$$ \\frac{1}{2} $$
```

Unclosed `$$` blocks are consumed to the end of the document and rendered as partial math without crashing. This is important for streaming UIs.
Unclosed `$$` blocks are consumed to the end of the document and rendered as partial math without crashing. This is important for streaming UIs. Set `unclosedBlock: false` to leave unclosed blocks as plain text instead.

The opening fence may be indented by at most 3 spaces; 4-space-indented `$$` stays an indented code block.

## Streaming

Expand All @@ -189,21 +194,30 @@ Because `@tanstack/markdown` extensions run during document parsing, both block
- Several inline math expressions on the same line are each rendered separately.
- Math inside inline HTML (`<u>$x$</u>`) is rendered; structural block HTML containing `$` is left untouched.
- Code fences / inline code containing `$` are preserved verbatim.
- Invalid TeX renders as a styled error badge/box instead of raw KaTeX output (default renderer only).
- Invalid TeX renders as a neutral error element instead of raw KaTeX output (default renderer only): `<span class="katex-error-badge">` inline and `<div class="katex-error-box">` in display mode, with English text ("KaTeX error"). Style these classes yourself; the React components use the same output.
- Heading anchors: math is now parsed as part of the heading's inline content, so generated heading slugs are derived from the heading's plain text and no longer include math fragments.
- Math is trimmed in both output modes (`$$ x $$` → `x`).
- `mathBlockExtension` runs before built-in block parsers because it is registered earlier in the extension array.

## Design notes

These notes follow the upstream discussion in [TanStack/markdown#13](https://github.com/TanStack/markdown/issues/13) (consolidated into [#9](https://github.com/TanStack/markdown/issues/9)):

1. **First-class math nodes vs. html injection**
The default mode emits `html`/`inlineHtml` nodes. The `component` output mode demonstrates the richer design enabled by `ComponentNode` and `InlineComponentNode` (added in `@tanstack/markdown` 0.0.15, [PR #17](https://github.com/TanStack/markdown/pull/17)): raw TeX in the AST, rendering deferred to the renderer (`renderHtml` hook / React `components` map), no `allowHtml`.
The default mode emits `html`/`inlineHtml` nodes. The `component` output mode uses `ComponentNode` and `InlineComponentNode`: raw TeX in the AST, rendering deferred to the renderer (`renderHtml` hook / React `components` map), no `allowHtml`.

2. **`inlineParser` instead of an AST walker**
Since `@tanstack/markdown` 1.0, extensions can register an `inlineParser` with `markers: '$'`. The parser sees the raw source before emphasis, escapes and links are resolved, so math bodies containing `_`, `*` or `\` are never corrupted and `protectMath` is unnecessary. Spans are recognised in every inline context (headings, links, lists, tables, ...) and never inside code.

3. **Pandoc-style delimiter rules**
Single `$` spans reject whitespace after the opener and before the closer, reject a closer followed by a digit (`$5-$10`), honour `\$` escapes, and stop at blank lines. Scanning is linear. A small `transformDocument` remains only for block-level raw HTML nodes (`allowHtml`), and it builds new nodes instead of mutating the input so cached ASTs can be re-parsed safely.

2. **`transformDocument` vs `transformInline`**
`transformInline` is called once per inline container (paragraph, heading, list item, table cell) with the container's top-level nodes, during parsing. This implementation instead uses `transformDocument`, which traverses the fully parsed AST. Both work in 0.0.13; `transformInline` would avoid the full-document walk and could simplify future versions.
## Upgrading to v1

3. **Inline parsing order**
Inline math must be processed after bold, italic, links, and code have already been parsed into typed nodes, so the walker skips typed nodes that should not contain math. A true inline transformer in the parser would achieve the same result more cleanly.
1. **Peer dependency raised to `@tanstack/markdown` >= 1.0.0.** Migrate with `pnpm add @tanstack/markdown@^1 tanstack-markdown-math@^1` (or the npm/yarn equivalent).
2. **`protectMath` / `restoreMathChars` are deprecated.** Remove your `protectMath(content)` calls before parsing: math containing `_`, `*` and `\` now works as-is. Both functions remain exported for already-protected content.
3. **Heading slugs change when headings contain inline math.** Math is now parsed inside the heading's inline content, so regenerate any stored heading anchors / table-of-contents links / deep links for headings that contain `$...$`.
4. **Default error output changed.** Invalid TeX now renders `<span class="katex-error-badge">` (inline) / `<div class="katex-error-box">` (display) with English text ("KaTeX error") instead of Tailwind utility classes and French strings. Add your own CSS for `katex-error-badge` / `katex-error-box`, or pass a custom `render` to keep your previous markup.

## License

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"prepublishOnly": "pnpm run build"
},
"peerDependencies": {
"@tanstack/markdown": ">=0.0.13 <2.0.0",
"@tanstack/markdown": ">=1.0.0 <2.0.0",
"katex": ">=0.16",
"react": ">=18"
},
Expand Down
Loading
Loading