# micro-mdx-parser

> A tiny parser to convert markdown or html into JSON

Latest version **1.1.35** (published 2026-09-26) · MIT license · 0 weekly downloads

## Install

```sh
npm install micro-mdx-parser
pnpm add micro-mdx-parser
yarn add micro-mdx-parser
bun add micro-mdx-parser
```

## Health

**Score 55/100 (C)** — status: active.

Positive: no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types; no esm support.

## Facts

| | |
|---|---|
| Version | 1.1.35 |
| Published | 2026-09-26 |
| First published | 2023-01-06 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | none |
| Module format | CommonJS |
| Dependencies | 1 |
| Unpacked size | 353.2 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | David Wells |
| Maintainers | davidwells |
| Keywords | mdx |

## Links

- npm: https://www.npmjs.com/package/micro-mdx-parser
- Repository: https://github.com/DavidWells/micro-mdx
- Issues: https://github.com/DavidWells/micro-mdx/issues
- npm.io page: https://npm.io/package/micro-mdx-parser

## Dependencies (1)

- [oparser](https://npm.io/package/oparser.md) ^3.0.22

## Alternatives

- [@mdxeditor/editor](https://npm.io/package/@mdxeditor/editor.md) — 962.4K weekly downloads
- [mmdb-lib](https://npm.io/package/mmdb-lib.md) — 680.9K weekly downloads
- [playcanvas](https://npm.io/package/playcanvas.md) — 36.2K weekly downloads
- [@glw907/cairn-cms](https://npm.io/package/@glw907/cairn-cms.md) — 967 weekly downloads
- [markdown-to-confluence](https://npm.io/package/markdown-to-confluence.md) — 103 weekly downloads

## Recent versions

- 1.1.35 (latest) — 2026-09-26
- 1.1.34 — 2026-08-06
- 1.1.33 — 2026-08-06
- 1.1.32 — 2026-07-18
- 1.1.31 — 2026-07-18
- 1.1.29 — 2026-07-16
- 1.1.28 — 2026-07-15
- 1.1.27 — 2026-06-29
- 1.1.26 — 2026-06-27
- 1.1.24 — 2024-10-04
- 1.1.23 — 2024-10-04
- 1.1.22 — 2024-10-04
- 1.1.21 — 2024-10-04
- 1.1.20 — 2024-10-04
- 1.1.19 — 2024-10-04
- … 27 more at https://npm.io/package/micro-mdx-parser/versions

## README

<div align="left">

# micro-mdx-parser

**A tiny parser that turns markdown + HTML + JSX into a JSON tree you can render however you want.**

[![npm version](https://img.shields.io/npm/v/micro-mdx-parser.svg?style=flat-square)](https://www.npmjs.com/package/micro-mdx-parser)
[![npm downloads](https://img.shields.io/npm/dm/micro-mdx-parser.svg?style=flat-square)](https://www.npmjs.com/package/micro-mdx-parser)
[![license](https://img.shields.io/npm/l/micro-mdx-parser.svg?style=flat-square)](./LICENSE)
[![runs in the browser](https://img.shields.io/badge/runs%20in-browser%20%26%20node-brightgreen.svg?style=flat-square)](#why-micro-mdx-parser)

</div>

```bash
npm install micro-mdx-parser
```

```js
const { parse } = require('micro-mdx-parser')

parse('<Callout type="warning">Heads up **friend**</Callout>')
// → [{ type: 'component', tagName: 'Callout', props: { type: 'warning' }, children: [...] }]
```

That's the whole idea: hand it a string, get back a flat JSON array of nodes (`text`, `element`, `component`, `comment`) with parsed props and source positions. What you render those nodes into is up to you.

---

## TL;DR

**The problem.** You want to write content in markdown *and* drop in React/JSX components (`<Callout>`, `<YouTube id="..."/>`, `<div style={{...}}>`) — i.e. MDX. But the official MDX toolchain compiles to a JS module at build time. That's heavy, needs a bundler step, and **can't run in the browser** at edit-time. If you're building a CMS, a live preview, or a comment box, you need something that parses MDX-ish content *in the client*, cheaply, without `eval` or a compile step.

**The solution.** `micro-mdx-parser` does one job: it walks the string and pulls every HTML tag, self-closing element, and capitalized component out into structured nodes — parsing their props into real JavaScript values — while leaving the markdown prose untouched as `text` nodes. No compilation. No evaluation. No build step. Just a tree.

**You stay in control of rendering.** Text nodes still contain raw markdown (`**bold**`, `[links](…)`, `# headings`). Pair them with any markdown renderer; map element/component nodes to your own component registry. (See [Rendering the tree](#rendering-the-tree).)

### Why micro-mdx-parser?

| You want… | micro-mdx-parser |
|---|---|
| Parse MDX-ish content **in the browser** | ✅ Pure JS, no bundler/compiler step |
| **JSX props as real values** (`style={{…}}`, `count={2}`, arrow fns) | ✅ via [`oparser`](https://github.com/DavidWells/oparser) |
| **Capitalized tags → components** (`<MyThing/>`) | ✅ `type: 'component'` |
| **Source positions** (line/column/char index) for every node | ✅ great for editors & CMS |
| Round-trip back to HTML | ✅ `stringify(ast)` |
| Structural validation (e.g. unclosed void tags) | ✅ `validate(ast)`, pluggable rules |
| Tiny footprint, one dependency | ✅ |
| A markdown → HTML *renderer* | ❌ that's your job — see below |
| A full MDX compiler with imports/exports/`eval` | ❌ by design |

---

## Quick example

```js
const { parse } = require('micro-mdx-parser')

const input = `# Welcome

Some **markdown** prose, then a component:

<Callout type="warning">
  Watch out!
</Callout>

<img src="a.jpg" alt="pic" />`

console.log(parse(input))
```

```js
[
  // markdown stays raw, as text — note positions
  {
    type: 'text',
    content: '# Welcome\n\nSome **markdown** prose, then a component:\n\n',
    position: { start: { index: 0, line: 1, column: 1 }, end: {/*…*/} }
  },
  // capitalized tag → component, with parsed props + nested children
  {
    type: 'component',
    tagName: 'Callout',
    tagValue: '<Callout type="warning">\n  Watch out!\n</Callout>',
    props: { type: 'warning' },
    propsRaw: ' type="warning"',
    children: [ { type: 'text', content: '\n  Watch out!\n', position: {/*…*/} } ],
    position: {/*…*/}
  },
  // lowercase tag → element; self-closing flagged; props coerced
  {
    type: 'element',
    tagName: 'img',
    tagValue: '<img src="a.jpg" alt="pic" />',
    props: { src: 'a.jpg', alt: 'pic' },
    propsRaw: ' src="a.jpg" alt="pic" ',
    children: [],
    isSelfClosing: true,
    position: {/*…*/}
  }
]
```

**The key thing to understand:** the heading and `**markdown**` came back as a plain `text` node — *not* an `<h1>` or `<strong>`. This parser separates the **component layer** from the **prose layer**; it does not render markdown. That separation is the whole point (see next section).

---

## The mental model

```text
input string
────────────────────────────────────────────────────────────
# Welcome

Some **markdown** prose
  → stays as a `text` node

<Callout type="warning"> … </Callout>
  → becomes a `component` node

<img src="a.jpg" />
  → becomes an `element` node

                         parse()
                            │
                            ▼

flat array of nodes: [ text, component, element, … ]

      ┌─────────────────────┴─────────────────────┐
      ▼                                           ▼

text nodes                              element/component nodes
→ your markdown renderer                → your component registry

markdown-it, marked,                    React, Preact, Vue,
@davidwells/markdown                    plain DOM…
```

`micro-mdx-parser` owns the boring, fiddly part — finding tag boundaries, matching close tags, coercing JSX props, tracking positions — and hands you a clean tree. You own rendering.

---

## Design philosophy

1. **Do one thing.** Structure mixed markdown/HTML/JSX into JSON. Rendering, sanitizing, and markdown-to-HTML are explicitly *out of scope* — compose them yourself.
2. **Client-first.** Must run in the browser with no build step, so live preview / CMS editing works. No `eval`, no dynamic `import`, no compiler.
3. **Mappable.** Every node carries its exact `position` (line, column, char index) and `tagValue`, so you can map a node back to the source — essential for editors, error reporting, and partial re-rendering.
4. **JSX props are data, not strings.** `style={{…}}`, numbers, booleans, and arrays come back as real JS values via [`oparser`](https://github.com/DavidWells/oparser); functions come back as source strings you can choose to evaluate (or not).
5. **HTML semantics where it matters.** Void tags (`<img>`, `<br>`), auto-closing tags (`<li>`, `<td>`), and raw-text tags (`<script>`, `<style>`, `<template>`) follow HTML rules so real-world content doesn't blow up the tree.

---

## How it compares

| | **micro-mdx-parser** | `@mdx-js/mdx` | `unified` (remark/rehype) | `himalaya` | `marked` / `markdown-it` |
|---|---|---|---|---|---|
| Output | JSON node array | compiled JS module | mdast/hast AST | JSON (HTML only) | HTML string |
| Markdown + JSX in one pass | ✅ (JSX structural) | ✅ (full) | ✅ (with `remark-mdx`) | ❌ | ❌ |
| Parses JSX props to JS values | ✅ | ✅ | partial | ❌ | n/a |
| Runs in browser, no build step | ✅ | ⚠️ heavy | ⚠️ heavy | ✅ | ✅ |
| Compiles / evaluates code | ❌ (none) | ✅ | ❌ | ❌ | ❌ |
| Renders markdown to HTML | ❌ | ✅ | ✅ | ❌ | ✅ |
| Source positions per node | ✅ | ✅ | ✅ | ✅ | partial |
| Footprint | tiny (1 dep) | large | large | tiny | small |

**Pick this** when you need to *understand the structure* of MDX-ish content at runtime (CMS, live preview, custom renderer) without a compiler. **Pick `@mdx-js/mdx`** when you want full MDX (imports/exports, expressions) compiled at build time. **Pick `unified`** when you want the rich remark/rehype plugin ecosystem and a true markdown AST.

---

## Installation

```bash
# npm
npm install micro-mdx-parser

# pnpm
pnpm add micro-mdx-parser

# yarn
yarn add micro-mdx-parser
```

**Module format.** Ships as CommonJS (`main: src/index.js`). Use `require(...)` in Node, or import via any bundler (webpack, Vite, esbuild, Rollup) for the browser:

```js
import { parse, stringify, validate } from 'micro-mdx-parser'
```

Runtime: Node and modern browsers. One runtime dependency ([`oparser`](https://github.com/DavidWells/oparser)) for prop parsing.

---

## API

```js
const { parse, stringify, validate, parseDefaults } = require('micro-mdx-parser')
```

### `parse(input, options?) → Node[]`

Parse a string into a flat array of nodes (elements nest their children).

```js
parse('<div class="box"><span>hi</span></div>')
```

**Options**

| Option | Type | Default | Description |
|---|---|---|---|
| `includePositions` | `boolean` | `true` | Attach `position` (line/column/char index) to every node. |
| `offset` | `{ lineOffset, charOffset }` | – | Shift all positions. Handy when you stripped frontmatter before parsing and want positions relative to the original file. |
| `transforms` | `{ [tagName]: (node) => node }` | – | Rewrite a node by tag name at parse time (e.g. remap `img` → `Image`). |
| `voidTags` | `string[]` | [HTML void tags](src/tags.js) | Tags with no closing tag (`img`, `br`, …). |
| `closingTags` | `string[]` | [list](src/tags.js) | Tags that auto-close when a sibling opens (`li`, `td`, …). |
| `childlessTags` | `string[]` | `['style','script','template']` | Raw-text tags whose body is kept verbatim, not parsed. |
| `closingTagAncestorBreakers` | `object` | [map](src/tags.js) | Ancestors that prevent auto-closing (e.g. `li` inside `ul`). |

```js
// Remap a tag during parse
parse('<img src="a.jpg" />', {
  transforms: { img: (node) => ({ ...node, tagName: 'Image' }) }
})
// → [{ type: 'element', tagName: 'Image', props: { src: 'a.jpg' }, ... }]

// Re-base positions onto the original file after stripping frontmatter
parse(bodyWithoutFrontmatter, { offset: { lineOffset: 7, charOffset: 210 } })
```

### `stringify(ast, options?) → string`

Serialize a node array back to an HTML string. Round-trips the structure (note: markdown text inside `text` nodes is emitted as-is, not converted to HTML).

```js
const ast = parse('<div class="a"><span>hi</span></div>')
stringify(ast)
// → '<div class="a"><span>hi</span></div>'
```

### `validate(stringOrAst, options?) → Error[]`

Run structural rules over the tree. Accepts a string (parses it first) or an existing AST. Returns a flat array of `{ message, value, position }` — empty when clean. The default rule flags void tags missing their `/>`.

```js
validate('<img src="a.jpg">')
// → [{ message: 'Missing closing "/>" on "element". <img src="a.jpg">', value: '<img src="a.jpg">', position: {…} }]

// Bring your own rules: each is (node) => string | undefined
validate('<div>hi</div>', {
  rules: [ (node) => node.tagName === 'div' ? 'No bare divs allowed' : undefined ]
})
// → [{ message: 'No bare divs allowed', ... }]
```

### `parseDefaults`

The default options object (tag tables + `includePositions: true`). Exported so you can read or extend the defaults.

---

## Node shapes

Every node has a `type`. `position` is present unless `includePositions: false`.

```js
// Prose / markdown / whitespace — content is raw, never rendered
{ type: 'text', content: '# Heading\n\nSome **bold** text', position }

// Lowercase HTML tag
{
  type: 'element',
  tagName: 'div',
  tagValue: '<div class="box">hi</div>',  // exact source slice
  props: { class: 'box' },                // parsed JS values
  propsRaw: ' class="box"',               // raw attribute string
  children: [ /* nodes */ ],
  isSelfClosing: true,                    // only when self-closed
  position
}

// Capitalized tag → component (same shape, different type)
{ type: 'component', tagName: 'Callout', props: {…}, children: [...], position }

// HTML comment
{ type: 'comment', content: ' TODO ', position }
```

`props` coercion examples (powered by `oparser`):

```js
parse('<Widget count={2} active style={{ color: "red" }} onClick={() => run()} />')[0].props
// → {
//     count: 2,                       // number
//     active: true,                   // boolean shorthand
//     style: { color: 'red' },        // object
//     onClick: '() => run()'          // function kept as a source string
//   }
```

---

## Rendering the tree

`parse()` gives you structure; you decide what it becomes. A typical React renderer walks the array, runs `text` nodes through a markdown renderer and maps `element`/`component` nodes to a component registry:

```jsx
import { parse } from 'micro-mdx-parser'
import Markdown from '@davidwells/markdown' // or markdown-it, marked, etc.

const registry = { Callout, YouTube, img: 'img' /* … */ }

function render(nodes) {
  return nodes.map((node, i) => {
    if (node.type === 'text')    return <Markdown key={i}>{node.content}</Markdown>
    if (node.type === 'comment') return null
    const Tag = registry[node.tagName] || node.tagName
    return <Tag key={i} {...node.props}>{render(node.children)}</Tag>
  })
}

function Mdx({ source }) {
  return <>{render(parse(source))}</>
}
```

The same tree works for Preact, Vue, plain DOM, or non-DOM targets — nothing here is React-specific.

---

## Architecture

```text
 input string
      │
      ▼
 ┌──────────┐   masks code fences, JSX props, arrow fns & inline URLs so
 │  format  │   their <, {, } don't confuse the tokenizer (un-masked later)
 └──────────┘
      │
      ▼
 ┌──────────┐   scans text vs. tags, emits a flat token stream,
 │  lexer   │   tracking line/column/index as it goes
 └──────────┘
      │
      ▼
 ┌──────────┐   builds the tree: matches open/close tags, applies
 │  parser  │   void / auto-close / raw-text HTML rules
 └──────────┘
      │
      ▼
 ┌──────────┐   shapes nodes (element vs component), parses props via
 │  format  │   oparser, restores masked chars, runs transforms
 └──────────┘
      │
      ▼
 Node[]   ──►  stringify()  back to HTML
          └─►  validate()   structural checks
```

Source map: [`src/lexer.js`](src/lexer.js) · [`src/parser.js`](src/parser.js) · [`src/format.js`](src/format.js) · [`src/stringify.js`](src/stringify.js) · [`src/validate.js`](src/validate.js) · [`src/tags.js`](src/tags.js)

---

## Performance

Pure single-pass parsing, no compile step. On Node 22, a 24 KB mixed-MDX file (`examples/giant-md/giant.md`) parses in **~1.4 ms** (≈17 MB/s), single-threaded.

A golden-output perf harness lives in [`scripts/perf-parse-corpus.cjs`](scripts/perf-parse-corpus.cjs):

```bash
node scripts/perf-parse-corpus.cjs                 # benchmark + check golden outputs
node scripts/perf-parse-corpus.cjs --write-golden  # regenerate golden checksums
MICRO_MDX_PERF_ITERATIONS=1000 node scripts/perf-parse-corpus.cjs
```

---

## Testing

Tests use [`uvu`](https://github.com/lukeed/uvu) — run any test file directly, no config:

```bash
npm test               # run everything in src/*.test.js
npm run test:lexer     # lexer only
npm run test:parser    # parser only
npm run test:stringify # stringify only
node src/parser.test.js  # run a single file directly
```

Runnable input/output samples live in [`examples/`](examples) (try `node examples/md.js`, `node examples/html.js`).

---

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `# Heading` / `**bold**` came back as a `text` node, not `<h1>` | By design — this parser doesn't render markdown | Run `text` node `content` through a markdown renderer (see [Rendering the tree](#rendering-the-tree)). |
| Component didn't become `type: 'component'` | Tag name isn't capitalized | Components must start with an uppercase letter (`<Callout/>`, not `<callout/>`). |
| `onClick`/`onChange` prop is a string, not a function | Functions are intentionally **not** evaluated | `new Function('return (' + props.onClick + ')')()` *only if you trust the source* — never on untrusted input. |
| `<` or `{` inside a code fence broke the tree | A fence the masker didn't catch | Confirm fences are well-formed; file a repro from [`examples/giant-md/`](examples/giant-md) — many tricky cases are covered there. |
| Positions look wrong after stripping frontmatter | Positions are relative to the string you passed in | Pass `offset: { lineOffset, charOffset }` to re-base onto the original file. |
| `<script>`/`<style>` body got mangled | – | It shouldn't — raw-text tag bodies are preserved verbatim. If not, please open an issue with a repro. |

---

## Limitations

Honest about what this is **not**:

- **Not a markdown renderer.** Markdown stays as raw text. Bring your own renderer.
- **Not a full MDX compiler.** No `import`/`export`, no JSX expression evaluation, no `{count + 1}` interpolation. Capitalized tags and their props are parsed structurally; nothing is executed.
- **Not a sanitizer.** Output reflects input. If you render untrusted content, sanitize it yourself (e.g. DOMPurify on the rendered output).
- **Function props are strings.** Evaluating them is your call (and a security decision).
- **Lenient, not spec-strict.** It aims to produce a *useful* tree from real-world MDX-ish content, not to enforce strict HTML5 or JSX validity. Use `validate()` for the checks you care about.

---

## FAQ

**Does it convert markdown to HTML?**
No. It separates components/HTML from prose and leaves markdown as `text` nodes. Pair it with any markdown renderer. This is intentional — see [the mental model](#the-mental-model).

**Why not just use `@mdx-js/mdx`?**
MDX compiles to a JS module at build time and is heavy to run in a browser at edit-time. This was built to parse MDX-ish content *in the client* (CMS, live preview) cheaply, with no compile/`eval` step.

**Does it run in the browser?**
Yes — pure JS, no build step required. Import it through your bundler.

**How are JSX props parsed?**
Via [`oparser`](https://github.com/DavidWells/oparser): numbers, booleans, objects, and arrays become real JS values; `style={{…}}` becomes an object; functions are returned as source strings (not evaluated).

**Lowercase vs uppercase tags?**
Lowercase → `type: 'element'`. Uppercase first letter → `type: 'component'`. That's the only difference in shape.

**Can I get the original source for a node?**
Yes — every node has `tagValue` (the exact source slice) and `position` (`start`/`end` with `line`, `column`, `index`).

**Can I turn an AST back into a string?**
Yes — `stringify(ast)`.

**Is it TypeScript?**
It's JavaScript. The node shapes above are stable; types may be added later.

---

## Credits

Fork of [himalaya](https://github.com/andrejewski/himalaya) by Chris Andrejewski, extended to handle MDX-style components, JSX prop parsing, validation, and richer position tracking. Prop parsing by [oparser](https://github.com/DavidWells/oparser).

## License

[MIT](./LICENSE) © David Wells

---
_Source: https://npm.io/package/micro-mdx-parser · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
