Package detail

markdown-to-jsx

quantizor16.6mMIT9.5.0

A very fast and versatile markdown toolchain. AST, React, React Native, SolidJS, Vue, Markdown, and HTML output available with full customization.

markdown, markdown parser, react, preact

readme

npm version downloads

markdown-to-jsx is a gfm+commonmark compliant markdown parser and compiler toolchain for JavaScript and TypeScript-based projects. It is extremely fast, capable of processing large documents fast enough for real-time interactivity.

Some special features of the library:

  • Arbitrary HTML is supported and parsed into the appropriate JSX representation without dangerouslySetInnerHTML

  • Any HTML tags rendered by the compiler and/or <Markdown> component can be overridden to include additional props or even a different HTML representation entirely.

  • All GFM special syntaxes are supported, including tables, task lists, strikethrough, autolinks, tag filtering, and more.

  • Fenced code blocks with highlight.js support; see Syntax highlighting for instructions on setting up highlight.js.

Table of Contents

Upgrading

From v8.x to v9.x

Breaking Changes:

  • ast option removed: The ast: true option on compiler() has been removed. Use the new parser() function instead to access the AST directly.
/** v8 */ compiler('# Hello world', { ast: true })
/** v9 */ parser('# Hello world')
  • namedCodesToUnicode option removed: The namedCodesToUnicode option has been removed. All named HTML entities are now supported by default via the full entity list, so custom entity mappings are no longer needed.
/** v8 */ compiler('&le; symbol', { namedCodesToUnicode: { le: '\u2264' } })
/** v9 */ compiler('&le; symbol')
  • tagfilter enabled by default: Dangerous HTML tags (script, iframe, style, title, textarea, xmp, noembed, noframes, plaintext) are now escaped by default in both HTML string output and React JSX output. Previously these tags were rendered as JSX elements in React output.
/** v8 */ tags rendered as JSX elements
/** v9 */ tags escaped by default
compiler('<script>alert("xss")</script>') // <span>&lt;script&gt;</span>

/** Restore old behavior */
compiler('<script>alert("xss")</script>', { tagfilter: false })

New Features:

  • New parser function: Provides direct access to the parsed AST without rendering. This is the recommended way to get AST nodes.

  • New entry points: React-specific, HTML-specific, and markdown-specific entry points are now available for better tree-shaking and separation of concerns.

// React-specific usage
import Markdown, { compiler, parser } from 'markdown-to-jsx/react'

// HTML string output
import { compiler, astToHTML, parser } from 'markdown-to-jsx/html'

// Markdown string output (round-trip compilation)
import { compiler, astToMarkdown, parser } from 'markdown-to-jsx/markdown'

Migration Guide:

  1. Replace compiler(..., { ast: true }) with parser():
/** v8 */ compiler(markdown, { ast: true })
/** v9 */ parser(markdown)
  1. Migrate React imports to /react entry point (optional but recommended):
/** Legacy */ import from 'markdown-to-jsx'
/** Recommended */ import from 'markdown-to-jsx/react'
  1. Remove namedCodesToUnicode option: All named HTML entities are now supported automatically, so you can remove any custom entity mappings.
/** v8 */ compiler('&le; symbol', { namedCodesToUnicode: { le: '\u2264' } })
/** v9 */ compiler('&le; symbol')

Note: The main entry point (markdown-to-jsx) continues to work for backward compatibility, but React code there is deprecated and will be removed in a future major release. Consider migrating to markdown-to-jsx/react for React-specific usage.

<summary>### Older Migration Guides</summary> ### From v7.x to v8.x Breaking Changes: - Type ParserResult renamed to ASTNode - If you were using MarkdownToJSX.ParserResult in your code, update to MarkdownToJSX.ASTNode typescript /** v7 */ MarkdownToJSX.ParserResult[] /** v8+ */ MarkdownToJSX.ASTNode[] - Multiple RuleType enums consolidated into RuleType.textFormatted - If you were checking for RuleType.textBolded, RuleType.textEmphasized, RuleType.textMarked, or RuleType.textStrikethroughed, update to check for RuleType.textFormatted and inspect the node's boolean flags: typescript /** v7 */ RuleType.textBolded /** v8+ */ RuleType.textFormatted && node.bold

Installation

Install markdown-to-jsx with your favorite package manager.

npm i markdown-to-jsx

Usage

markdown-to-jsx exports a React component by default for easy JSX composition:

ES6-style usage*:

import Markdown from 'markdown-to-jsx'
import React from 'react'
import { render } from 'react-dom'

render(<Markdown># Hello world!</Markdown>, document.body)

/*
    renders:

    <h1>Hello world!</h1>
 */

* NOTE: JSX does not natively preserve newlines in multiline text. In general, writing markdown directly in JSX is discouraged and it's a better idea to keep your content in separate .md files and require them, perhaps using webpack's raw-loader.

Entry Points

markdown-to-jsx provides multiple entry points for different use cases:

Main

The legacy*default entry point exports everything, including the React compiler and component:

import Markdown, { compiler, parser } from 'markdown-to-jsx'

The React code in this entry point is deprecated and will be removed in a future major release, migrate to markdown-to-jsx/react.

React

For React-specific usage, import from the /react entry point:

import Markdown, { compiler, parser, astToJSX } from 'markdown-to-jsx/react'

const jsxElement = compiler('# Hello world')

function App() {
  return <Markdown children="# Hello world" />
}

/** Or use parser + astToJSX */
const ast = parser('# Hello world')
const jsxElement2 = astToJSX(ast)
React Server Components (RSC)

The Markdown component automatically detects whether it's running in a React Server Component (RSC) or client environment and adapts accordingly. No 'use client' directive is required.

Server Component (RSC) usage:

// Server Component - works automatically
import Markdown from 'markdown-to-jsx/react'

export default async function Page() {
  const content = await fetchMarkdownContent()
  return <Markdown>{content}</Markdown>
}

Client Component usage:

// Client Component - also works automatically
'use client'
import Markdown from 'markdown-to-jsx/react'

export function ClientMarkdown({ content }: { content: string }) {
  return <Markdown>{content}</Markdown>
}

Notes:

  • MarkdownProvider and MarkdownContext are client-only and become no-ops in RSC environments
  • RSC rendering provides better performance by avoiding client-side hydration
  • The component maintains identical output in both environments
  • No migration needed for existing code

React Native

For React Native usage, import from the /native entry point:

import Markdown, { compiler, parser, astToNative } from 'markdown-to-jsx/native'
import { View, Text, StyleSheet, Linking } from 'react-native'

const nativeElement = compiler('# Hello world', {
  styles: {
    heading1: { fontSize: 32, fontWeight: 'bold' },
    paragraph: { marginVertical: 8 },
    link: { color: 'blue', textDecorationLine: 'underline' },
  },
  onLinkPress: url => {
    Linking.openURL(url)
  },
})

const markdown = `# Hello world

This is a [link](https://example.com) with **bold** and *italic* text.
`

function App() {
  return (
    <View>
      <Markdown
        children={markdown}
        options={{
          styles: StyleSheet.create({
            heading1: { fontSize: 32, fontWeight: 'bold' },
            paragraph: { marginVertical: 8 },
            link: { color: 'blue', textDecorationLine: 'underline' },
          }),
          onLinkPress: url => {
            Linking.openURL(url)
          },
        }}
      />
    </View>
  )
}

React Native-specific options:

  • onLinkPress?: (url: string, title?: string) => void - Custom handler for link presses (defaults to Linking.openURL)
  • onLinkLongPress?: (url: string, title?: string) => void - Handler for link long presses
  • styles?: Partial<Record<NativeStyleKey, StyleProp<ViewStyle | TextStyle | ImageStyle>>> - Style overrides for each element type
  • wrapperProps?: ViewProps | TextProps - Props for the wrapper component (defaults to View for block, Text for inline)

HTML Tag Mapping: HTML tags are automatically mapped to React Native components:

  • <img>Image component
  • Block elements (<div>, <section>, <article>, <blockquote>, <ul>, <ol>, <li>, <table>, etc.) → View component
  • Inline elements (<span>, <strong>, <em>, <a>, etc.) → Text component
  • Type 1 blocks (<pre>, <script>, <style>, <textarea>) → View component

Note: Links are underlined by default for better accessibility and discoverability. You can override this via the styles.link option.

SolidJS

For SolidJS usage, import from the /solid entry point:

import Markdown, {
  compiler,
  parser,
  astToJSX,
  MarkdownProvider,
} from 'markdown-to-jsx/solid'
import { createSignal } from 'solid-js'

// Static content
const solidElement = compiler('# Hello world')

function App() {
  return <Markdown children="# Hello world" />
}

// Reactive content (automatically updates when content changes)
function ReactiveApp() {
  const [content, setContent] = createSignal('# Hello world')
  return <Markdown>{content}</Markdown>
}

// Or use parser + astToJSX
const ast = parser('# Hello world')
const solidElement2 = astToJSX(ast)

// Use context for default options
function AppWithContext() {
  return (
    <MarkdownProvider options={{ sanitizer: customSanitizer }}>
      <Markdown># Content</Markdown>
    </MarkdownProvider>
  )
}

SolidJS-specific features:

  • Reactive content: The Markdown component accepts signals/accessors for automatic updates when markdown content changes
  • Memoization: AST parsing is automatically memoized for optimal performance
  • Context API: Use MarkdownProvider to provide default options and avoid prop drilling

Vue.js

For Vue.js 3 usage, import from the /vue entry point:

import Markdown, { compiler, parser, astToJSX } from 'markdown-to-jsx/vue'
import { h } from 'vue'

// Using compiler
const vnode = compiler('# Hello world')

// Using component
<Markdown children="# Hello world" />

// Or use parser + astToJSX
const ast = parser('# Hello world')
const vnode2 = astToJSX(ast)

Vue.js-specific features:

  • Vue 3 support: Uses Vue 3's h() render function API
  • JSX support: Works with Vue 3 JSX via @vue/babel-plugin-jsx or @vitejs/plugin-vue-jsx
  • HTML attributes: Uses standard HTML attributes (class instead of className)
  • Component overrides: Support for both Options API and Composition API componen

HTML

For HTML string output (server-side rendering), import from the /html entry point:

import { compiler, html, parser } from 'markdown-to-jsx/html'

const htmlString = compiler('# Hello world')

/** Or use parser + html */
const ast = parser('# Hello world')
const htmlString2 = html(ast)

Markdown

For markdown-to-markdown compilation (normalization and formatting), import from the /markdown entry point:

import { compiler, astToMarkdown, parser } from 'markdown-to-jsx/markdown'

const normalizedMarkdown = compiler('# Hello  world\n\nExtra spaces!')

/** Or work with AST */
const ast = parser('# Hello  world')
const normalizedMarkdown2 = astToMarkdown(ast)

Library Options

All Options

Option Type Default Description
createElement function - Custom createElement behavior (React/React Native/SolidJS/Vue only). See createElement for details.
disableAutoLink boolean false Disable automatic conversion of bare URLs to anchor tags.
disableParsingRawHTML boolean false Disable parsing of raw HTML into JSX.
enforceAtxHeadings boolean false Require space between # and header text (GFM spec compliance).
evalUnserializableExpressions boolean false ⚠️ Eval unserializable props (DANGEROUS). See evalUnserializableExpressions for details.
forceBlock boolean false Force all content to be treated as block-level.
forceInline boolean false Force all content to be treated as inline.
forceWrapper boolean false Force wrapper even with single child (React/React Native/Vue only). See forceWrapper for details.
overrides object - Override HTML tag rendering. See overrides for details.
preserveFrontmatter boolean false Include frontmatter in rendered output (as <pre> for HTML/JSX, included in markdown). Behavior varies by compiler type.
renderRule function - Custom rendering for AST rules. See renderRule for details.
sanitizer function built-in Custom URL sanitizer function. See sanitizer for details.
slugify function built-in Custom slug generation for heading IDs. See slugify for details.
tagfilter boolean true Escape dangerous HTML tags (script, iframe, style, etc.) to prevent XSS.
wrapper `string \ component \ null` 'div' Wrapper element for multiple children (React/React Native/Vue only). See wrapper for details.
wrapperProps object - Props for wrapper element (React/React Native/Vue only). See wrapperProps for details.

options.createElement

Sometimes, you might want to override the React.createElement default behavior to hook into the rendering process before the JSX gets rendered. This might be useful to add extra children or modify some props based on runtime conditions. The function mirrors the React.createElement function, so the params are type, [props], [...children]:

import Markdown from 'markdown-to-jsx'
import React from 'react'
import { render } from 'react-dom'

const md = `
# Hello world
`

render(
  <Markdown
    children={md}
    options={{
      createElement(type, props, children) {
        return (
          <div className="parent">
            {React.createElement(type, props, children)}
          </div>
        )
      },
    }}
  />,
  document.body
)

options.forceWrapper

By default, the compiler does not wrap the rendered contents if there is only a single child. You can change this by setting forceWrapper to true. If the child is inline, it will not necessarily be wrapped in a span.

// Using `forceWrapper` with a single, inline child…
<Markdown options={{ wrapper: 'aside', forceWrapper: true }}>
  Mumble, mumble…
</Markdown>

// renders

<aside>Mumble, mumble…</aside>

options.overrides

Override HTML tag rendering or render custom React components. Three use cases:

1. Remove tags: Return null to completely remove tags (beyond tagfilter escaping):

<Markdown options={{ overrides: { iframe: () => null } }}>
  <iframe src="..."></iframe>
</Markdown>

2. Override HTML tags: Change component, props, or both:

const MyParagraph = ({ children, ...props }) => <div {...props}>{children}</div>

<Markdown options={{ overrides: { h1: { component: MyParagraph, props: { className: 'foo' } } } }}>
  # Hello
</Markdown>

/** Simplified */ { overrides: { h1: MyParagraph } }

3. Render React components: Use custom components in markdown:

import DatePicker from './date-picker'

const md = `<DatePicker timezone="UTC+5" startTime={1514579720511} />`

<Markdown options={{ overrides: { DatePicker } }}>{md}</Markdown>

Important notes:

  • JSX props are intelligently parsed (v9.1+):
    • Arrays and objects: data={[1, 2, 3]} → parsed as [1, 2, 3]
    • Booleans: enabled={true} → parsed as true
    • Functions: onClick={() => ...} → kept as string for security (use renderRule for case-by-case handling, or see evalUnserializableExpressions)
    • Complex expressions: value={someVar} → kept as string
  • The original raw attribute string is available in node.rawAttrs when using parser()
  • Some props are preserved: a (href, title), img (src, alt, title), input[type="checkbox"] (checked, readonly), ol (start), td/th (style)
  • Element mappings: span for inline text, code for inline code, pre > code for code blocks

options.evalUnserializableExpressions

⚠️ SECURITY WARNING: STRONGLY DISCOURAGED FOR USER INPUTS

When enabled, attempts to eval expressions in JSX props that cannot be serialized as JSON (functions, variables, complex expressions). This uses eval() which can execute arbitrary code.

By default (recommended), unserializable expressions are kept as strings for security:

import { parser } from 'markdown-to-jsx'

const ast = parser('<Button onClick={() => alert("hi")} />')
// ast[0].attrs.onClick === "() => alert(\"hi\")" (string, safe)

// Arrays and objects are automatically parsed (no eval needed):
const ast2 = parser('<Table data={[1, 2, 3]} />')
// ast2[0].attrs.data === [1, 2, 3] (parsed via JSON.parse)

ONLY enable this option when:

  • The markdown source is completely trusted (e.g., your own documentation)
  • You control all JSX components and their props
  • The content is NOT user-generated or user-editable

DO NOT enable this option when:

  • Processing user-submitted markdown
  • Rendering untrusted content
  • Building public-facing applications with user content

Example of the danger:

// User-submitted markdown with malicious code
const userMarkdown = '<Component onClick={() => fetch("/admin/delete-all")} />'

// ❌ DANGEROUS - function will be executable
parser(userMarkdown, { evalUnserializableExpressions: true })

// ✅ SAFE - function kept as string
parser(userMarkdown) // default behavior

Safe alternative: Use renderRule for case-by-case handling:

// Instead of eval'ing arbitrary expressions, handle them selectively in renderRule:
const handlers = {
  handleClick: () => console.log('clicked'),
  handleSubmit: () => console.log('submitted'),
}

compiler(markdown, {
  renderRule(next, node) {
    if (
      node.type === RuleType.htmlBlock &&
      typeof node.attrs?.onClick === 'string'
    ) {
      // Option 1: Named handler lookup (safest)
      const handler = handlers[node.attrs.onClick]
      if (handler) {
        return <button onClick={handler}>{/* ... */}</button>
      }

      // Option 2: Selective eval with allowlist (still risky)
      if (
        node.tag === 'TrustedComponent' &&
        node.attrs.onClick.startsWith('() =>')
      ) {
        try {
          const fn = eval(`(${node.attrs.onClick})`)
          return <button onClick={fn}>{/* ... */}</button>
        } catch (e) {
          // Handle error
        }
      }
    }
    return next()
  },
})

This approach gives you full control over which expressions are evaluated and under what conditions.

options.renderRule

Supply your own rendering function that can selectively override how rules are rendered (note, this is different than options.overrides which operates at the HTML tag level and is more general). The renderRule function always executes before any other rendering code, giving you full control over how nodes are rendered, including normally-skipped nodes like ref, footnote, and frontmatter.

You can use this functionality to do pretty much anything with an established AST node; here's an example of selectively overriding the "codeBlock" rule to process LaTeX syntax using the @matejmazur/react-katex library:

import Markdown, { RuleType } from 'markdown-to-jsx'
import TeX from '@matejmazur/react-katex'

const exampleContent =
  'Some important formula:\n\n```latex\nmathbb{N} = { a in mathbb{Z} : a > 0 }\n```\n'

function App() {
  return (
    <Markdown
      children={exampleContent}
      options={{
        renderRule(next, node, renderChildren, state) {
          if (node.type === RuleType.codeBlock && node.lang === 'latex') {
            return (
              <TeX as="div" key={state.key}>{String.raw`${node.text}`}</TeX>
            )
          }

          return next()
        },
      }}
    />
  )
}

Accessing parsed HTML content: For HTML blocks marked as verbatim (like <script>, <style>, <pre>), default renderers use rawText for CommonMark compliance, but renderRule can access the fully parsed AST in children:

<Markdown
  options={{
    renderRule(next, node, renderChildren) {
      if (node.type === RuleType.htmlBlock && node.tag === 'script') {
        // Access parsed children even for verbatim blocks
        const parsedContent = node.children || []
        // Or use rawText for original content
        const rawContent = node.rawText || ''

        // Custom rendering logic here
        return <CustomScript content={parsedContent} raw={rawContent} />
      }
      return next()
    },
  }}
>
  <script>Hello **world**</script>
</Markdown>

options.sanitizer

By default a lightweight URL sanitizer function is provided to avoid common attack vectors that might be placed into the href of an anchor tag, for example. The sanitizer receives the input, the HTML tag being targeted, and the attribute name. The original function is available as a library export called sanitizer.

This can be overridden and replaced with a custom sanitizer if desired via options.sanitizer:

// sanitizer in this situation would receive:
// ('javascript:alert("foo")', 'a', 'href')

<Markdown options={{ sanitizer: (value, tag, attribute) => value }}>
  {`[foo](javascript:alert("foo"))`}
</Markdown>

// or

compiler('[foo](javascript:alert("foo"))', {
  sanitizer: value => value,
})

options.slugify

By default, a lightweight deburring function is used to generate an HTML id from headings. You can override this by passing a function to options.slugify. This is helpful when you are using non-alphanumeric characters (e.g. Chinese or Japanese characters) in headings. For example:

<Markdown options={{ slugify: str => str }}># 中文</Markdown>
compiler('# 中文', { slugify: str => str })

The original function is available as a library export called slugify.

options.wrapper

When there are multiple children to be rendered, the compiler will wrap the output in a div by default. You can override this default by setting the wrapper option to either a string (React Element) or a component.

const str = '# Heck Yes\n\nThis is great!'

<Markdown options={{ wrapper: 'article' }}>{str}</Markdown>

compiler(str, { wrapper: 'article' })
Other useful recipes

To get an array of children back without a wrapper, set wrapper to null. This is particularly useful when using compiler(…) directly.

compiler('One\n\nTwo\n\nThree', { wrapper: null })[
  /** Returns */ ((<p>One</p>), (<p>Two</p>), (<p>Three</p>))
]

To render children at the same DOM level as <Markdown> with no HTML wrapper, set wrapper to React.Fragment. This will still wrap your children in a React node for the purposes of rendering, but the wrapper element won't show up in the DOM.

options.wrapperProps

Props to apply to the wrapper element when wrapper is used.

<Markdown
  options={{
    wrapper: 'article',
    wrapperProps: { className: 'post', 'data-testid': 'markdown-content' },
  }}
>
  # Hello World
</Markdown>

Syntax highlighting

When using fenced code blocks with language annotation, that language will be added to the <code> element as class="lang-${language}". For best results, you can use options.overrides to provide an appropriate syntax highlighting integration like this one using highlight.js:

<!-- Add the following tags to your page <head> to automatically load hljs and styles: -->
<link
  rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/obsidian.min.css"
/>

<script
  crossorigin
  src="https://unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js"
></script>
import { Markdown, RuleType } from 'markdown-to-jsx'

const mdContainingFencedCodeBlock = '```js\nconsole.log("Hello world!");\n```\n'

function App() {
  return (
    <Markdown
      children={mdContainingFencedCodeBlock}
      options={{
        overrides: {
          code: SyntaxHighlightedCode,
        },
      }}
    />
  )
}

function SyntaxHighlightedCode(props) {
  const ref = React.useRef<HTMLElement | null>(null)

  React.useEffect(() => {
    if (ref.current && props.className?.includes('lang-') && window.hljs) {
      window.hljs.highlightElement(ref.current)

      // hljs won't reprocess the element unless this attribute is removed
      ref.current.removeAttribute('data-highlighted')
    }
  }, [props.className, props.children])

  return <code {...props} ref={ref} />
}

Handling shortcodes

For Slack-style messaging with arbitrary shortcodes like :smile:, you can use options.renderRule to hook into the plain text rendering and adjust things to your liking, for example:

import Markdown, { RuleType } from 'markdown-to-jsx'

const shortcodeMap = {
  smile: '🙂',
}

const detector = /(:[^:]+:)/g

const replaceEmoji = (text: string): React.ReactNode => {
  return text.split(detector).map((part, index) => {
    if (part.startsWith(':') && part.endsWith(':')) {
      const shortcode = part.slice(1, -1)

      return <span key={index}>{shortcodeMap[shortcode] || part}</span>
    }

    return part
  })
}

function Example() {
  return (
    <Markdown
      options={{
        renderRule(next, node) {
          if (node.type === RuleType.text && detector.test(node.text)) {
            return replaceEmoji(node.text)
          }

          return next()
        },
      }}
    >
      {`On a beautiful summer day, all I want to do is :smile:.`}
    </Markdown>
  )
}

When you use options.renderRule, any React-renderable JSX may be returned including images and GIFs. Ensure you benchmark your solution as the text rule is one of the hottest paths in the system!

Usage with Preact

Everything will work just fine! Simply Alias react to preact/compat like you probably already are doing.

AST Anatomy

The Abstract Syntax Tree (AST) is a structured representation of parsed markdown. Each node in the AST has a type property that identifies its kind, and type-specific properties.

Important: The first node in the AST is typically a RuleType.refCollection node that contains all reference definitions found in the document, including footnotes (stored with keys prefixed with ^). This node is skipped during rendering but is useful for accessing reference data. Footnotes are automatically extracted from the refCollection and rendered in a <footer> element by both compiler() and astToJSX().

Node Types

The AST consists of the following node types (use RuleType to check node types):

Block-level nodes:

  • RuleType.heading - Headings (# Heading)
    { type: RuleType.heading, level: 1, id: "heading", children: [...] }
  • RuleType.paragraph - Paragraphs
    { type: RuleType.paragraph, children: [...] }
  • RuleType.codeBlock - Fenced code blocks (```)
    { type: RuleType.codeBlock, lang: "javascript", text: "code content" }
  • RuleType.blockQuote - Blockquotes (>)
    { type: RuleType.blockQuote, children: [...], alert?: "note" }
  • RuleType.orderedList / RuleType.unorderedList - Lists
    { type: RuleType.orderedList, items: [[...]], start?: 1 }
    { type: RuleType.unorderedList, items: [[...]] }
  • RuleType.table - Tables
    { type: RuleType.table, header: [...], cells: [[...]], align: [...] }
  • RuleType.htmlBlock - HTML blocks and JSX components

    {
      type: RuleType.htmlBlock,
      tag: "div",
      attrs: {},
      rawAttrs?: string,
      children?: ASTNode[],
      verbatim?: boolean,
      rawText?: string,
      text?: string // @deprecated - use rawText instead
    }

    Note (v9.1+): JSX components with blank lines between opening/closing tags now properly nest children instead of creating sibling nodes.

    HTML Block Parsing (v9.2+): HTML blocks are always fully parsed into the children property, even when marked as verbatim. The verbatim flag acts as a rendering hint (default renderers use rawText for verbatim blocks to maintain CommonMark compliance), but renderRule implementations can access the fully parsed AST in children for all HTML blocks. The rawText field contains the original raw HTML content for verbatim blocks, while rawAttrs contains the original attribute string.

Inline nodes:

  • RuleType.text - Plain text
    { type: RuleType.text, text: "Hello world" }
  • RuleType.textFormatted - Bold, italic, etc.
    { type: RuleType.textFormatted, tag: "strong", children: [...] }
  • RuleType.codeInline - Inline code (` )
    { type: RuleType.codeInline, text: "code" }
  • RuleType.link - Links
    { type: RuleType.link, target: "https://example.com", children: [...] }
  • RuleType.image - Images
    { type: RuleType.image, target: "image.png", alt: "description" }

Other nodes:

  • RuleType.breakLine - Hard line breaks ( )
  • RuleType.breakThematic - Horizontal rules (---)
  • RuleType.gfmTask - GFM task list items (- [ ])
  • RuleType.ref - Reference definition node (not rendered, stored in refCollection)
  • RuleType.refCollection - Reference definitions collection (appears at AST root, includes footnotes with ^ prefix)
  • RuleType.footnote - Footnote definition node (not rendered, stored in refCollection)
  • RuleType.footnoteReference - Footnote reference ([^identifier])
  • RuleType.frontmatter - YAML frontmatter blocks
    { type: RuleType.frontmatter, text: "---\ntitle: My Title\n---" }
  • RuleType.htmlComment - HTML comment nodes
    { type: RuleType.htmlComment, text: "<!-- comment -->" }
  • RuleType.htmlSelfClosing - Self-closing HTML tags
    { type: RuleType.htmlSelfClosing, tag: "img", attrs: { src: "image.png" } }

JSX Prop Parsing (v9.1+):

The parser intelligently parses JSX prop values:

  • Arrays/objects are parsed via JSON.parse(): rows={[["a", "b"]]}attrs.rows = [["a", "b"]]
  • Functions are kept as strings for security: onClick={() => ...}attrs.onClick = "() => ..."
  • Booleans are parsed: enabled={true}attrs.enabled = true
  • The original raw attribute string is preserved in rawAttrs field

Example AST Structure

import { parser, RuleType } from 'markdown-to-jsx'

const ast = parser(`# Hello World

This is a **paragraph** with [a link](https://example.com).

[linkref]: https://example.com

```javascript
console.log('code')
```

`)

// AST structure:
[
  // Reference collection (first node, if references exist)
  {
    type: RuleType.refCollection,
    refs: {
      linkref: { target: 'https://example.com', title: undefined },
    },
  },
  {
    type: RuleType.heading,
    level: 1,
    id: 'hello-world',
    children: [{ type: RuleType.text, text: 'Hello World' }],
  },
  {
    type: RuleType.paragraph,
    children: [
      { type: RuleType.text, text: 'This is a ' },
      {
        type: RuleType.textFormatted,
        tag: 'strong',
        children: [{ type: RuleType.text, text: 'paragraph' }],
      },
      { type: RuleType.text, text: ' with ' },
      {
        type: RuleType.link,
        target: 'https://example.com',
        children: [{ type: RuleType.text, text: 'a link' }],
      },
      { type: RuleType.text, text: '.' },
    ],
  },
  {
    type: RuleType.codeBlock,
    lang: 'javascript',
    text: "console.log('code')",
  },
]

Type Checking

Use the RuleType enum to identify AST nodes:

import { RuleType } from 'markdown-to-jsx'

if (node.type === RuleType.heading) {
  const heading = node as MarkdownToJSX.HeadingNode
  console.log(`Heading level ${heading.level}: ${heading.id}`)
}

When to use compiler vs parser vs <Markdown>:

  • Use <Markdown> when you need a simple React component that renders markdown to JSX.
  • Use compiler when you need React JSX output from markdown (the component uses this internally).
  • Use parser + astToJSX when you need the AST for custom processing before rendering to JSX, or just the AST itself.

Gotchas

JSX prop parsing (v9.1+): Arrays and objects in JSX props are automatically parsed:

// In markdown:
<Table
  columns={['Name', 'Age']}
  data={[
    ['Alice', 30],
    ['Bob', 25],
  ]}
/>

// In your component (v9.1+):
const Table = ({ columns, data, ...props }) => {
  // columns is already an array: ["Name", "Age"]
  // data is already an array: [["Alice", 30], ["Bob", 25]]
  // No JSON.parse needed!
}

// For backwards compatibility, check types:
const Table = ({ columns, data, ...props }) => {
  const parsedColumns =
    typeof columns === 'string' ? JSON.parse(columns) : columns
  const parsedData = typeof data === 'string' ? JSON.parse(data) : data
}

Function props are kept as strings for security. Use renderRule for case-by-case handling, or see evalUnserializableExpressions for opt-in eval.

HTML indentation: Leading whitespace in HTML blocks is auto-trimmed based on the first line's indentation to avoid markdown syntax conflicts.

Code in HTML: Don't put code directly in HTML divs. Use fenced code blocks instead:

<div>
```js
var code = here();
```
</div>

Changelog

See Github Releases.

Donate

Like this library? It's developed entirely on a volunteer basis; chip in a few bucks if you can via the Sponsor link!

changelog

markdown-to-jsx

9.5.0

Minor Changes

  • 7605d88: Add React Server Components (RSC) support with automatic environment detection.

    The Markdown component now seamlessly works in both RSC and client-side React environments without requiring 'use client' directives. The component automatically detects hook availability and adapts its behavior accordingly:

    • In RSC environments: Uses direct compilation without hooks for optimal server performance
    • In client environments: Uses hooks and memoization for optimal client performance
    • MarkdownProvider and MarkdownContext gracefully become no-ops in RSC environments
    • Maintains identical output and API in both contexts
    • Zero breaking changes for existing users

    This enables better bundle splitting and SSR performance by allowing markdown rendering to happen on the server when possible.

Patch Changes

  • d2075d2: Fix hard line breaks (two trailing spaces) inside list items not being converted to <br/>.

    In v9, hard line breaks inside list items were being lost because the first line content and continuation lines were being parsed separately, causing the trailing spaces before the newline to be stripped before the hard break could be detected.

    The fix ensures that for tight list items (without blank lines), simple text continuation lines are collected and concatenated with the first line content before parsing. This preserves the trailing spaces + newline sequence that triggers hard break detection.

    This fix also handles hard line breaks inside blockquotes that are nested within list items, ensuring the blockquote continuation lines are properly collected together.

    Fixes #766.

9.4.2

Patch Changes

  • 775b4bf: Expose parser and RuleType from the markdown entry point as documented.

9.4.1

Patch Changes

  • 7ee8a22: Ensure renderRule always executes before any other rendering code across all renderers. The renderRule function now has full control over node rendering, including normally-skipped nodes like ref, footnote, and frontmatter. Additionally, renderChildren in the markdown renderer now invokes renderRule for recursively rendered child nodes, ensuring consistent behavior when customizing rendering logic.
  • 7ee8a22: HTML blocks are now always fully parsed into the AST children property, even when marked as verbatim. The verbatim flag now acts as a rendering hint rather than a parsing control. Default renderers still use rawText for verbatim blocks (maintaining CommonMark compliance), but renderRule implementations can now access the fully parsed AST in children for all HTML blocks. The noInnerParse property has been replaced with verbatim for clarity.
  • 7ee8a22: Add HTMLNode.rawText field for consistency with rawAttrs. The rawText field contains the raw text content for verbatim HTML blocks, while children contains the parsed AST. The text property is now deprecated and will be removed in a future major version. Both fields are set to the same value for backward compatibility.

9.4.0

Minor Changes

  • c1be885: Added context providers and memoization to all major renderers for better developer experience and performance.

    React:

    • MarkdownContext - React context for default options
    • MarkdownProvider - Provider component to avoid prop-drilling
    • useMemo - 3-stage memoization (options, content, JSX)

    React Native:

    • MarkdownContext - React context for default options
    • MarkdownProvider - Provider component to avoid prop-drilling
    • useMemo - 3-stage memoization (options, content, JSX)

    Vue:

    • MarkdownOptionsKey - InjectionKey for provide/inject pattern
    • MarkdownProvider - Provider component using Vue's provide
    • computed - Reactive memoization for options, content, and JSX

    Benefits:

    1. Avoid prop-drilling - Set options once at the top level:
    <MarkdownProvider options={commonOptions}>
      <App>
        <Markdown>...</Markdown>
        <Markdown>...</Markdown>
      </App>
    </MarkdownProvider>
    1. Performance optimization - Content is only parsed when it actually changes, not on every render
    2. Fully backwards compatible - Existing usage works unchanged, providers are optional

    Example:

    import { MarkdownProvider } from 'markdown-to-jsx/react'
    
    function App() {
      return (
        <MarkdownProvider options={{ wrapper: 'article', tagfilter: true }}>
          <Markdown># Page 1</Markdown>
          <Markdown># Page 2</Markdown>
          {/* Both inherit options from provider */}
        </MarkdownProvider>
      )
    }
  • ef8a002: Added opt-in options.evalUnserializableExpressions to eval function expressions and other unserializable JSX props from trusted markdown sources.

    ⚠️ SECURITY WARNING: STRONGLY DISCOURAGED FOR USER INPUTS

    This option uses eval() and should ONLY be used with completely trusted markdown sources (e.g., your own documentation). Never enable this for user-submitted content.

    Usage:

    // For trusted sources only
    const markdown = `
    <Button onPress={() => alert('clicked!')} />
    <ApiEndpoint url={process.env.API_URL} />
    `
    
    parser(markdown, { evalUnserializableExpressions: true })
    
    // Components receive:
    // - onPress: actual function () => alert('clicked!')
    // - url: the value of process.env.API_URL from your environment
    // Without this option, these would be strings "() => alert('clicked!')" and "process.env.API_URL"

    Safer alternative: Use renderRule to handle stringified expressions on a case-by-case basis with your own validation and allowlists.

    See the README for detailed security considerations and safe alternatives.

  • ef8a002: JSX prop values are now intelligently parsed instead of always being strings:

    • Arrays and objects are parsed via JSON.parse(): data={[1, 2, 3]}attrs.data = [1, 2, 3]
    • Booleans are parsed: enabled={true}attrs.enabled = true
    • Functions are kept as strings for security: onClick={() => ...}attrs.onClick = "() => ..."
    • Complex expressions are kept as strings: value={someVar}attrs.value = "someVar"

    The original raw attribute string is preserved in the rawAttrs field.

    Benefits:

    • Type-safe access to structured data without manual parsing
    • Backwards compatible - check types before using
    • Secure by default - functions remain as strings

    Example:

    // In markdown:
    <ApiTable
      rows={[
        ['Name', 'Value'],
        ['foo', 'bar'],
      ]}
    />
    
    // In your component:
    const ApiTable = ({ rows }) => {
      // rows is already an array, no JSON.parse needed!
      return <table>...</table>
    }
    
    // For backwards compatibility:
    const rows =
      typeof props.rows === 'string' ? JSON.parse(props.rows) : props.rows

    Security: Functions remain as strings by default. Use renderRule for case-by-case handling, or see the new options.evalUnserializableExpressions feature for opt-in eval (not recommended for user inputs).

Patch Changes

  • ef8a002: JSX components with double-newlines (blank lines) between opening and closing tags now properly nest children instead of creating sibling nodes. This fixes incorrect AST structure for JSX/MDX content.

    Before:

    <Figure>
    
      <div>content</div>
    
    </Figure>

    Parsed as 3 siblings: <Figure>, <div>, </Figure>

    After:

    Parsed as parent-child: <Figure> contains <div> as a child

    This was a bug where the parser incorrectly treated JSX components as siblings when double-newlines were present between the tags. The fix ensures proper parent-child relationships match expected JSX/MDX semantics.

9.3.5

Patch Changes

  • 08dfe8a: Fix regression: Tables within list items are now properly parsed.

9.3.4

Patch Changes

  • c5b6259: Fixed URIError when parsing HTML attributes containing the % character (e.g., width="100%"). The parser now gracefully handles invalid URI encodings in attribute values instead of throwing an error.

9.3.3

Patch Changes

  • 7ac3408: Restore angle-bracket autolinks when raw HTML parsing is disabled so <https://...> still renders as links
  • 7ac3408: Improve autolink parsing: stricter angle controls, domain underscore validation, and added coverage for mailto labels and raw-HTML-disabled cases.

9.3.2

Patch Changes

  • a84c300: Ensure Solid renderer uses Solid's hyperscript runtime so JSX returns real elements instead of [object Object] placeholders

9.3.1

Patch Changes

  • c1b0ea2: Fix unintended node-specific code from entering browser bundles by changing build target from 'node' to 'browser'

9.3.0

Minor Changes

  • a482de6: Add SolidJS integration with full JSX output support. Includes compiler, parser, astToJSX, and Markdown component with reactive support via signals/accessors.
  • f9a8fca: Add Vue.js 3+ integration. Includes compiler, parser, astToJSX, and Markdown component. Vue uses standard HTML attributes (class, not className) with minimal attribute mapping (only 'for' -> 'htmlFor').

Patch Changes

  • 2bb3f2b: Fix AST and options mutation bugs that could cause unexpected side effects when using memoization or reusing objects across multiple compiler calls.

9.2.0

Minor Changes

  • 88d4b1f: Add comprehensive React Native support with new /native export. Includes:

    • React Native Component Mapping: Enhanced HTML tag to React Native component mapping with semantic support for imgImage, block elements (div, section, article, blockquote, ul, ol, li, table, etc.) → View, and inline elements → Text
    • Link Handling: Native link support with onLinkPress and onLinkLongPress callbacks, defaulting to Linking.openURL
    • Styling System: Complete NativeStyleKey type system with styles for all markdown elements and HTML semantic tags
    • Component Overrides: Full support for overriding default components with custom React Native components and props
    • Accessibility: Built-in accessibility support with accessibilityLabel for images and proper link handling
    • Type Safety: Comprehensive TypeScript definitions with NativeOptions and NativeStyleKey types
    • Performance: Optimized rendering with proper React Native best practices and component lifecycle

    React Native is an optional peer dependency, making this a zero-dependency addition for existing users.

9.1.2

Patch Changes

  • f93214a: Fix infinite recursion when using forceBlock: true with empty unclosed HTML tags

    When React.createElement(Markdown, {options: {forceBlock: true}}, '<var>') was called with an empty unclosed tag, it would cause infinite recursion. The parser would set the text field to the opening tag itself (e.g., <var>), which would then be parsed again in the rendering phase, causing recursion.

    This fix adds detection in createVerbatimHTMLBlock to detect when forceBlock is used and the text contains just the opening tag (empty unclosed tag), rendering it as an empty element to prevent recursion.

9.1.1

Patch Changes

  • 733f10e: Fix lazy continuation lines for list items when continuation text appears at base indentation without a blank line. Previously, continuation text was incorrectly appended inline to the list item. Now both the existing inline content and the continuation text are properly wrapped in separate paragraphs.

9.1.0

Minor Changes

  • 0ba757d: Add preserveFrontmatter option to control whether YAML frontmatter is rendered in the output. When set to true, frontmatter is rendered as a <pre> element in HTML/JSX output. For markdown-to-markdown compilation, frontmatter is preserved by default but can be excluded with preserveFrontmatter: false.

    | Compiler Type | Default Behavior | When preserveFrontmatter: true | When preserveFrontmatter: false | | ------------------------ | --------------------------- | -------------------------------- | --------------------------------- | | React/HTML | ❌ Don't render frontmatter | ✅ Render as <pre> element | ❌ Don't render frontmatter | | Markdown-to-Markdown | ✅ Preserve frontmatter | ✅ Preserve frontmatter | ❌ Exclude frontmatter |

Patch Changes

  • f945132: Fix lazy continuation lines for list items when continuation text appears at base indentation without a blank line before it. Previously, such lines were incorrectly parsed as separate paragraphs instead of being appended to the list item content.
  • 36ef089: yWork around a bundling bug with exporting TypeScript namespaces directly. Bonus: MarkdownToJSX is now declared ambiently so you may not need to import it.

9.0.0

Major Changes

  • 1ce83eb: Complete GFM+CommonMark specification compliance

    • Full CommonMark compliance: All 652 official test cases now pass
    • Verified GFM extensions: Tables, task lists, strikethrough, autolinks with spec compliance
    • Tag filtering: Default filtering of dangerous HTML tags (<script>, <iframe>, etc.) in both HTML string output and React JSX output
    • URL sanitization: Protection against javascript:, vbscript:, and malicious data: URLs

    Default filtering of dangerous HTML tags:

    • <script>, <iframe>, <object>, <embed>
    • <title>, <textarea>, <style>, <xmp>
    • <plaintext>, <noembed>, <noframes>

    ⚠️ Breaking Changes

    • Tagfilter enabled by default: Dangerous HTML tags are now escaped by default in both HTML and React output
    • Inline formatting restrictions: Inline formatting delimiters (emphasis, bold, strikethrough, mark) can no longer span across newlines, per CommonMark specification

    📋 Migration

    Tagfilter Migration

    No changes necessary in most cases, but if you need to render potentially dangerous HTML tags, you can disable tag filtering:

    compiler(markdown, { tagfilter: false })

    Inline Formatting Migration

    Previous Behavior (Non-Compliant): The library previously allowed inline formatting to span multiple lines:

    _Hello
    World._

    This was parsed as a single <em> element containing the newline.

    New Behavior (CommonMark Compliant): Per CommonMark specification, inline formatting cannot span newlines. The above example is now parsed as literal underscores:

    _Hello
    World._

    Renders as: <p>_Hello World._</p>

    Impact:

    • Single-line formatting still works: *Hello World*<em>Hello World</em>
    • Multi-line formatting is now rejected: *Hello\nWorld* → literal asterisks
    • Affects all inline formatting: *emphasis*, **bold**, ~~strikethrough~~, ==mark==

    Migration Options: If you have markdown with multi-line inline formatting:

    1. Keep formatting on a single line: *Hello World*
    2. Use HTML tags: <em>Hello\nWorld</em>
    3. Accept that multi-line formatting renders as literal delimiters

    Examples:

    # Works (single line)
    
    _This is emphasized_
    **This is bold**
    
    # No longer works (multi-line)
    
    _This is
    emphasized_
    **This is
    bold**
    
    # Renders as literal delimiters:
    
    <p>_This is
    emphasized_</p>
    <p>**This is
    bold**</p>
    
    # Workaround: Use HTML tags
    
    <em>This is
    emphasized</em>
    <strong>This is
    bold</strong>
  • 1ce83eb: Remove internal type definitions and rename MarkdownToJSX.RuleOutput to MarkdownToJSX.ASTRender

    This change removes internal type definitions from the MarkdownToJSX namespace:

    • Removed NestedParser type
    • Removed Parser type
    • Removed Rule type
    • Removed Rules type
    • Renamed RuleOutput to ASTRender for clarity

    Breaking changes:

    If you are using the internal types directly:

    • Code referencing MarkdownToJSX.NestedParser, MarkdownToJSX.Parser, MarkdownToJSX.Rule, or MarkdownToJSX.Rules will need to be updated
    • The renderRule option in MarkdownToJSX.Options now uses ASTRender instead of RuleOutput for the renderChildren parameter type
    • HTMLNode.children type changed from ReturnType<MarkdownToJSX.NestedParser> to ASTNode[] (semantically equivalent, but requires updates if using the old type)
  • 1ce83eb: Remove options.namedCodesToUnicode. The library now encodes the full HTML entity list by default per CommonMark specification requirements.

    Migration:

    If you were using options.namedCodesToUnicode to add custom entity mappings, you can remove the option entirely as all specified HTML entities are now supported automatically.

  • 1ce83eb: Drop support for React versions less than 16

    • Update peer dependency requirement from >= 0.14.0 to >= 16.0.0
    • Remove legacy code that wrapped string children in <span> elements for React < 16 compatibility
    • Directly return single children and null without wrapper elements
  • 1ce83eb: Upgrade to React 19 types

    • Update to @types/react@^19.2.2 and @types/react-dom@^19.2.2
    • Use React.JSX.* namespace instead of JSX.* for React 19 compatibility

Minor Changes

  • 1ce83eb: Adopt CommonMark-compliant class naming for code blocks

    Code blocks now use both the language- and lang- class name prefixes to match the CommonMark specification for compatibility.

    Before

    ```js
    console.log('hello')
    ```

    Generated:

    <pre><code class="lang-js">console.log('hello');</code></pre>

    After

    ```js
    console.log('hello')
    ```

    Generated:

    <pre><code class="language-js lang-js">console.log('hello');</code></pre>
  • 1ce83eb: Separate JSX renderer from compiler and add new entry points

    New Features

    • New parser function: Low-level API that returns AST nodes. Exported from main entry point and all sub-entry points.

      import { parser } from 'markdown-to-jsx'
      const source = '# Hello world'
      const ast = parser(source)
    • New /react entry point: React-specific entry point that exports compiler, Markdown component, parser, types, and utils.

      import Markdown, { astToJSX, compiler, parser } from 'markdown-to-jsx/react'
      
      const source = '# Hello world'
      const oneStepJSX = compiler(source)
      const twoStepJSX = astToJSX(parser(source))
      
      function App() {
        return <Markdown children={source} />
        // or
        // return <Markdown>{source}</Markdown>
      }
    • New /html entry point: HTML string output entry point that exports html function, parser, types, and utils.

      import { astToHTML, compiler, parser } from 'markdown-to-jsx/html'
      const source = '# Hello world'
      const oneStepHTML = compiler(source)
      const twoStepHTML = astToHTML(parser(source))
    • New /markdown entry point: Useful for situations where editing of the markdown is desired without resorting to gnarly regex-based parsing.

      import { astToMarkdown, compiler, parser } from 'markdown-to-jsx/markdown'
      const source = '# Hello world'
      const oneStepMarkdown = compiler(source)
      const twoStepMarkdown = astToMarkdown(parser(source))

    Deprecations

    React code in the main entry point markdown-to-jsx is deprecated and will be removed in a future major release. In v10, the main entry point will only export the parser function, the types, and any exposed utility functions.

    Migration

    • For React-specific usage, switch imports to markdown-to-jsx/react
    • For HTML output, use markdown-to-jsx/html entry point
    • Use parser() for direct acces to AST

8.0.0

Major Changes

  • 450d2bb: Added ast option to compiler to expose the parsed AST directly. When ast: true, the compiler returns the AST structure (ASTNode[]) instead of rendered JSX.

    Breaking Changes:

    • The internal type ParserResult has been renamed to ASTNode for clarity. If you were accessing this type directly (e.g., via module augmentation or type manipulation), you'll need to update references from MarkdownToJSX.ParserResult to MarkdownToJSX.ASTNode.

    First time the AST is accessible to users! This enables:

    • AST manipulation and transformation before rendering
    • Custom rendering logic without parsing
    • Caching parsed AST for performance
    • Linting or validation of markdown structure

    Usage:

    import { compiler } from 'markdown-to-jsx'
    import type { MarkdownToJSX } from 'markdown-to-jsx'
    
    // Get the AST structure
    const ast: MarkdownToJSX.ASTNode[] = compiler('# Hello world', {
      ast: true,
    })
    
    // Inspect/modify AST
    console.log(ast) // Array of parsed nodes
    
    // Render AST to JSX using createRenderer (not implemented yet)

    The AST format is MarkdownToJSX.ASTNode[]. When footnotes are present, the returned value will be an object with ast and footnotes properties instead of just the AST array.

  • 3fa0c22: Refactored inline formatting parsing to eliminate ReDoS vulnerabilities and improve performance. The previous regex-based approach was susceptible to exponential backtracking on certain inputs and had several edge case bugs with nested formatting, escaped characters, and formatting inside links. The new implementation uses a custom iterative scanner that runs in O(n) time and is immune to ReDoS attacks.

    This also consolidates multiple formatting rule types into a single unified rule with boolean flags, reducing code duplication and bundle size. Performance has improved measurably on simple markdown strings:

    Breaking Changes:

    The following RuleType enum values have been removed and consolidated into a single RuleType.textFormatted:

    • RuleType.textBolded
    • RuleType.textEmphasized
    • RuleType.textMarked
    • RuleType.textStrikethroughed

    If you're using these rule types directly (e.g., for custom AST processing or overrides), you'll need to update your code to check for RuleType.textFormatted instead and inspect the node's boolean flags (bold, italic, marked, strikethrough) to determine which formatting is applied.

Minor Changes

  • a421067: fix: overhaul HTML block parsing to eliminate exponential backtracking

    Replaced the complex nested regex HTML_BLOCK_ELEMENT_R with an efficient iterative depth-counting algorithm that maintains O(n) complexity. The new implementation uses stateful regex matching with lastIndex to avoid exponential backtracking on nested HTML elements while preserving all existing functionality.

    Performance improvements:

    • Eliminates O(2^n) worst-case exponential backtracking
    • Linear O(n) time complexity regardless of nesting depth

Patch Changes

  • e6b1e14: Fix renderer crash on extremely deeply nested markdown content

    Previously, rendering markdown with extremely deeply nested content (e.g., thousands of nested bold markers like ****************...text...****************) would cause a stack overflow crash. The renderer now gracefully handles such edge cases by falling back to plain text rendering instead of crashing.

    Technical details:

    • Added render depth tracking to prevent stack overflow
    • Graceful fallback at 2500 levels of nesting (way beyond normal usage)
    • Try/catch safety net as additional protection for unexpected errors
    • Zero performance impact during normal operation
    • Prevents crashes while maintaining O(n) parsing complexity

    This fix ensures stability even with adversarial or malformed inputs while having no impact on normal markdown documents.

  • fe95c02: Remove unnecessary wrapper when footnotes are present.

7.7.17

Patch Changes

  • acc11ad: Fix null children crashing app in production

    When null is passed as children to the <Markdown> component, it would previously crash the app in production. This fix handles this case by converting it to empty string.

    Usage Example

    Before this fix, the following code would crash in production:

    <Markdown>{null}</Markdown>

    After this fix, this case is handled gracefully and renders nothing.

7.7.16

Patch Changes

  • 7e487bd: Fix the issue where YAML frontmatter in code blocks doesn't render properly.

    This is done by lowering the parsing priority of Setext headings to match ATX headings; both are now prioritized lower than code blocks.

7.7.15

Patch Changes

  • 8e4c270: Mark react as an optional peer dependency as when passing createElement, you don't need React

7.7.14

Patch Changes

  • 73d4398: Cut down on unnecessary matching operations by improving qualifiers. Also improved the matching speed of paragraphs, which led to a roughly 2x boost in throughput for larger input strings.

7.7.13

Patch Changes

  • da003e4: Fix exponential backtracking issue for unpaired inline delimiter sequences.

7.7.12

Patch Changes

  • 4351ef5: Adjust text parsing to not split on double spaces unless followed by a newline.
  • 4351ef5: Special case detection of :shortcode: so the text processor doesn't break it into chunks, enables shortcode replacement via renderRule.

7.7.11

Patch Changes

  • 4a692dc: Fixes the issue where link text containing multiple nested brackets is not parsed correctly.

    Before: [title[bracket1][bracket2]](url) fails to parse as a link After: [title[bracket1][bracket2]](url) correctly parses as a link

7.7.10

Patch Changes

  • bf9dd3d: Unescape content intended for JSX attributes.

7.7.9

Patch Changes

  • 95dda3e: Avoid creating unnecessary paragraphs inside of HTML.
  • 95dda3e: Fix HTML parser to avoid processing the inside of <pre> blocks.

7.7.8

Patch Changes

  • db378c7: Implement early short-circuit for rules to avoid expensive throwaway work.
  • db378c7: Simpler fix that preserves existing performance.
  • db378c7: Various low-hanging minor performance enhancements by doing less work.
  • db378c7: Improve compression by inlining static RuleType entries when used in the codebase.

7.7.7

Patch Changes

  • 89c87e5: Handle spaces in text as a stop token to improve processing, also adapt paragraph detection to exclude non-atx compliant headings if that option is enabled.

    Fixes #680

7.7.6

Patch Changes

  • 654855b: Sanitize more attributes by default to help address XSS vectors.
  • 7639c08: Improve splitting of style attributes.

7.7.5

Patch Changes

  • 0ddaabb: Remove unescaping of content inside fenced code blocks.
  • 07b4280: Better handle exotic backtick scenarios for inline code blocks.
  • 0dad192: Fix consecutive marked text.

7.7.4

Patch Changes

  • adc08c7: Further optimize the plain text splitting regex.
  • c8bc5f3: Remove redundant detectors when processing paragraphs.
  • d96a8d8: Replace some regexes with optimized functions to avoid polynomial time scenarios. Also fixes compatibility issues in some older browsers with the trimEnd API.
  • 7be3d77: Optimize regexes and parsing to do less work.
  • cf7693c: Rework inline code syntax handling, handle escaped characters in code blocks correctly so they render without the backslash.

7.7.3

Patch Changes

  • 8026103: Handle paragraph splitting better, fixes #641.
  • 1ea00bb: Adjust table row parsing to better handle inline syntaxes and improve performance.

7.7.2

Patch Changes

  • 52a727c: Use ReactNode instead of ReactChild for React 19 compatibility
  • 4fa87d8: Bump ws from 8.11.0 to 8.18.0

7.7.1

Patch Changes

  • 9d42449: Factor out unnecessary element cloning.
  • 8920038: Remove use of explicit React.createElement.

7.7.0

Minor Changes

  • 20777bf: Add support for GFM alert-style blockquotes.

    > [!Note]
    > This is a note-flavored alert blockquote. The "Note" text is injected as a `<header>` by
    > default and the blockquote can be styled via the injected class `markdown-alert-note`
    > for example.

Patch Changes

  • 5d7900b: Adjust type signature for <Markdown> component to allow for easier composition.
  • 918b44b: Use newer React.JSX.* namespace instead of JSX.* for React 19 compatibility.
  • 91a5948: Arbitrary HTML no longer punches out pipes when parsing rows. If you absolutely need a pipe character that isn't a table separator, either escape it or enclose it in backticks to trigger inline code handling.
  • 23caecb: Drop encountered ref attributes when processing inline HTML, React doesn't handle it well.

7.6.2

Patch Changes

  • 0274445: Fix false detection of tables in some scenarios.
  • 69f815e: Handle class attribute from arbitrary HTML properly to avoid React warnings.
  • 857809a: Fenced code blocks are now tolerant to a missing closing sequence; this improves use in LLM scenarios where the code block markdown is being streamed into the editor in chunks.

7.6.1

Patch Changes

  • 87d8bd3: Handle class attribute from arbitrary HTML properly to avoid React warnings.

7.6.0

Minor Changes

  • 2281a4d: Add options.disableAutoLink to customize bare URL handling behavior.

    By default, bare URLs in the markdown document will be converted into an anchor tag. This behavior can be disabled if desired.

    <Markdown options={{ disableAutoLink: true }}>
      The URL https://quantizor.dev will not be rendered as an anchor tag.
    </Markdown>
    
    // or
    
    compiler(
      'The URL https://quantizor.dev will not be rendered as an anchor tag.',
      { disableAutoLink: true }
    )
    
    // renders:
    
    <span>
      The URL https://quantizor.dev will not be rendered as an anchor tag.
    </span>

Patch Changes

  • fb3d716: Simplify handling of fallback scenario if a link reference is missing its corresponding footnote.

7.5.1

Patch Changes

  • b16f668: Fix issue with lookback cache resulting in false detection of lists inside lists in some scenarios
  • 58b96d3: fix: handle empty HTML tags more consistently #597

7.5.0

Minor Changes

  • 62a16f3: Allow modifying HTML attribute sanitization when options.sanitizer is passed by the composer.

    By default a lightweight URL sanitizer function is provided to avoid common attack vectors that might be placed into the href of an anchor tag, for example. The sanitizer receives the input, the HTML tag being targeted, and the attribute name. The original function is available as a library export called sanitizer.

    This can be overridden and replaced with a custom sanitizer if desired via options.sanitizer:

    // sanitizer in this situation would receive:
    // ('javascript:alert("foo")', 'a', 'href')
    
    ;<Markdown options={{ sanitizer: (value, tag, attribute) => value }}>
      {`[foo](javascript:alert("foo"))`}
    </Markdown>
    
    // or
    
    compiler('[foo](javascript:alert("foo"))', {
      sanitizer: (value, tag, attribute) => value,
    })

Patch Changes

  • 553a175: Replace RuleType enum with an object

7.4.7

Patch Changes

  • 7603248: Fix parsing isolation of individual table cells.
  • f9328cc: Improved block html detection regex to handle certain edge cases that cause extreme slowness. Thank you @devbrains-com for the basis for this fix 🤝

7.4.6

Patch Changes

  • a9e5276: Browsers assign element with id to the global scope using the value as the variable name. E.g.: <h1 id="analytics"> can be referenced via window.analytics. This can be a problem when a name conflict happens. For instance, pages that expect analytics.push() to be a function will stop working if the an element with an id of analytics exists in the page.

    In this change, we export the slugify function so that users can easily augment it. This can be used to avoid variable name conflicts by giving the element a different id.

    import { slugify } from 'markdown-to-jsx';
    
    options={{
      slugify: str => {
        let result = slugify(str)
    
        return result ? '-' + str : result;
      }
    }}

7.4.5

Patch Changes

  • f5a0079: fix: double newline between consecutive blockquote syntax creates separate blockquotes

    Previously, for consecutive blockquotes they were rendered as one:

    Input

    > Block A.1
    > Block A.2
    
    > Block B.1

    Output

    <blockquote>
      <p>Block A.1</p>
      <p>Block A.2</p>
      <p>Block.B.1</p>
    </blockquote>

    This is not compliant with the GFM spec which states that consecutive blocks should be created if there is a blank line between them.

7.4.4

Patch Changes

  • 8eb8a13: Handle newlines inside of HTML tags themselves (not just nested children.)
  • c72dd31: Default children to an empty string if no content is passed.
  • 4f752c8: Fix handling of deeply-nested HTML in some scenarios.
  • 1486aa4: Handle extra brackets in links, thanks @zegl!
  • 1486aa4: Allow a newline to appear within inline formatting like bold, emphasis, etc, thanks @austingreco!
  • 1486aa4: Starting using changesets
  • fd35402: Fix HTML block regex for custom component scenarios where a nested component shares the same prefix as the parent, e.g. Accordion vs AccordionItem.
  • 1486aa4: Fix support for multi-line footnotes, thanks @zegl!