npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

micro-mdx-parser

v1.1.28

Published

A tiny parser to convert markdown or html into JSON

Readme

micro-mdx-parser

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

npm version npm downloads license runs in the browser

npm install micro-mdx-parser
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.)

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 | | 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

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))
[
  // 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

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; 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

# 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:

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

Runtime: Node and modern browsers. One runtime dependency (oparser) for prop parsing.


API

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).

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 imgImage). | | voidTags | string[] | HTML void tags | Tags with no closing tag (img, br, …). | | closingTags | string[] | list | 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 | Ancestors that prevent auto-closing (e.g. li inside ul). |

// 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).

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 />.

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.

// 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):

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:

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

 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/parser.js · src/format.js · src/stringify.js · src/validate.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:

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 — run any test file directly, no config:

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/ (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). | | 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/ — 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.

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: 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 by Chris Andrejewski, extended to handle MDX-style components, JSX prop parsing, validation, and richer position tracking. Prop parsing by oparser.

License

MIT © David Wells