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

react-native-hyper-markdown

v0.1.1

Published

react-native-hyper-markdown is a react native package built with Nitro

Downloads

261

Readme

React Native Hyper Markdown

High-performance markdown parser and renderer for React Native. Built with Nitro Modules and the battle-tested md4c C parser for blazing-fast parsing with complete markdown support.

Version Downloads License

Features

  • Fast Parsing: Native C++ parser (md4c) via Nitro Modules for 100x faster parsing than JS implementations
  • Complete Markdown: All CommonMark elements plus GitHub Flavored Markdown extensions
  • Task Lists: Built-in support for checkboxes and task list items
  • Tables: Full GFM table support with cell alignment control
  • Code Highlighting Ready: Language-aware code blocks with built-in language detection
  • Math Support: LaTeX math expressions (inline and blocks)
  • Wiki Links: Extended markdown with wiki-style link notation
  • Fully Themeable: Complete light/dark theme system with deep customization
  • Custom Renderers: Override any markdown element renderer for maximum flexibility
  • Accessible: Full accessibility support (roles, labels, semantic elements)
  • Type-Safe: Complete TypeScript definitions and strict types
  • Live Preview: Debounced parsing hook for real-time editors
  • Cross-Platform: iOS and Android with zero code differences

Screenshots

| Basic Rendering | Theming | |:---:|:---:| | Basic Rendering | Theming |

| Code Blocks | Live Preview | |:---:|:---:| | Code Blocks | Live Preview |

Requirements

  • React Native: 0.76.0+ (0.78.0+ for Nitro Views support)
  • Node.js: 18.0.0+
  • React: 19.2.0+
  • React Native Nitro Modules: 0.32.0+

[!IMPORTANT] For Nitro Views support, upgrade to React Native 0.78.0 or higher.

Installation

Package Installation

npm install react-native-hyper-markdown react-native-nitro-modules

Optional: Syntax Highlighting

For syntax highlighted code blocks, install the peer dependency:

npm install react-syntax-highlighter
npm install --save-dev @types/react-syntax-highlighter

See Syntax Highlighting section for theme setup.

Version Compatibility

| Package Version | React Native | React | Nitro Modules | |----------------|--------------|--------|---------------| | 0.1.x | 0.76.0+ | 19.2.0+ | 0.32.0+ |

Nitro Views Support: Requires React Native 0.78.0 or higher

Quick Start

Basic Usage

import { MarkdownView } from 'react-native-hyper-markdown'

export function MyScreen() {
  const markdown = `# Hello World

This is a **markdown** document with *formatting*.

- Item 1
- Item 2
  - Nested item`

  return <MarkdownView content={markdown} />
}

With Theme

import { ThemeProvider, MarkdownView, darkTheme } from 'react-native-hyper-markdown'

export function App() {
  return (
    <ThemeProvider theme={darkTheme}>
      <MarkdownView content={markdown} />
    </ThemeProvider>
  )
}

With Event Handlers

<MarkdownView
  content={markdown}
  onLinkPress={(url) => {
    Linking.openURL(url)
  }}
  onImagePress={(src) => {
    console.log('Image pressed:', src)
  }}
  onCheckboxToggle={(checked, index) => {
    console.log('Task toggled:', checked, index)
  }}
/>

Live Preview with Debouncing

import { useDebouncedParsing, MarkdownView } from 'react-native-hyper-markdown'

export function Editor() {
  const [content, setContent] = useState('')
  const parseResult = useDebouncedParsing(content, {}, 300)

  return (
    <View style={{ flex: 1, flexDirection: 'row' }}>
      <TextInput
        value={content}
        onChangeText={setContent}
        multiline
        style={{ flex: 1 }}
      />
      {parseResult.success && (
        <MarkdownView ast={parseResult.nodes} style={{ flex: 1 }} />
      )}
    </View>
  )
}

Usage Examples

Real-World Use Cases

Blog/Article Rendering

Perfect for displaying blog posts, articles, or documentation with navigation support:

import { MarkdownView } from 'react-native-hyper-markdown'
import { Linking } from 'react-native'

export function ArticleViewer({ articleContent }) {
  return (
    <MarkdownView
      content={articleContent}
      onLinkPress={(url) => {
        if (url.startsWith('/')) {
          // Internal navigation
          navigation.navigate(url.replace('/', ''))
        } else {
          // External links
          Linking.openURL(url)
        }
      }}
      parserOptions={{ gfm: true, enableTables: true }}
    />
  )
}

Live Markdown Editor

Real-time preview with debounced parsing for markdown editors:

import { useState } from 'react'
import { View, TextInput } from 'react-native'
import { useDebouncedParsing } from 'react-native-hyper-markdown'

export function MarkdownEditor() {
  const [content, setContent] = useState('# Start writing...')
  const parseResult = useDebouncedParsing(content, {}, 300)

  return (
    <View style={{ flex: 1, flexDirection: 'row' }}>
      <TextInput
        value={content}
        onChangeText={setContent}
        multiline
        style={{ flex: 1, padding: 16, fontFamily: 'monospace' }}
      />
      {parseResult.success && (
        <MarkdownView
          ast={parseResult.nodes}
          style={{ flex: 1, padding: 16 }}
        />
      )}
    </View>
  )
}

Documentation Viewer

In-app help system with topic navigation:

import { MarkdownView } from 'react-native-hyper-markdown'

export function HelpScreen({ navigation }) {
  const helpContent = `
# Help & Documentation

Click on topics below to learn more:

- [Getting Started](/getting-started)
- [Features Overview](/features)
- [Troubleshooting](/troubleshooting)
  `

  return (
    <MarkdownView
      content={helpContent}
      parserOptions={{ gfm: true, enableTables: true }}
      onLinkPress={(url) => {
        navigation.navigate('HelpTopic', { id: url.replace('/', '') })
      }}
    />
  )
}

Event Handlers

Link Handling

<MarkdownView
  content={markdown}
  onLinkPress={(url, title) => {
    console.log('Link pressed:', url, title)
    if (url.startsWith('http')) {
      Linking.openURL(url)
    } else {
      navigation.navigate(url)
    }
  }}
/>

Image Handling

<MarkdownView
  content={markdown}
  onImagePress={(src, alt, title) => {
    // Open image viewer modal
    navigation.navigate('ImageViewer', {
      uri: src,
      caption: alt,
      title: title,
    })
  }}
/>

Task List Interaction

const [tasks, setTasks] = useState([
  { text: 'Complete documentation', checked: false },
  { text: 'Write tests', checked: true },
])

const markdown = tasks
  .map((t) => `- [${t.checked ? 'x' : ' '}] ${t.text}`)
  .join('\n')

<MarkdownView
  content={markdown}
  onCheckboxToggle={(checked, node) => {
    const taskText = node.children?.[0]?.content
    setTasks((prev) =>
      prev.map((t) => (t.text === taskText ? { ...t, checked } : t))
    )
  }}
/>

Parser Configuration

GitHub Flavored Markdown (Default)

<MarkdownView
  content={markdown}
  parserOptions={{
    gfm: true,
    enableTables: true,
    enableTaskLists: true,
    enableStrikethrough: true,
    enableAutolink: true,
  }}
/>

LaTeX Math Support

const mathMarkdown = `
Inline math: $E=mc^2$

Block math:
$$
\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}
$$
`

<MarkdownView content={mathMarkdown} parserOptions={{ math: true }} />

Wiki Links

const wikiContent = `
See also: [[Related Topic]] or [[Custom Label|topic-slug]]
`

<MarkdownView content={wikiContent} parserOptions={{ wiki: true }} />

Performance Optimization

Pre-parsed AST (Avoid Re-parsing)

import { useMemo } from 'react'
import { parseMarkdown } from 'react-native-hyper-markdown'

const ast = useMemo(
  () => parseMarkdown(content, options),
  [content, options]
)

return <MarkdownView ast={ast.nodes} />

Large Documents with Virtualization

import { FlatList } from 'react-native'
import { parseMarkdown } from 'react-native-hyper-markdown'

const sections = useMemo(() => parseMarkdown(longDocument).nodes, [longDocument])

<FlatList
  data={sections}
  renderItem={({ item }) => <MarkdownView ast={[item]} />}
  keyExtractor={(_, i) => `section-${i}`}
  initialNumToRender={3}
/>

Debounced Parsing for Live Editors

// Parsing happens 300ms after last keystroke
const result = useDebouncedParsing(content, {}, 300)

// Reduces parsing frequency from every keystroke to ~3x per second

Accessibility

Screen Reader Support

<MarkdownView
  content={markdown}
  accessible={true}
  accessibilityLabel="Article content"
  accessibilityRole="text"
/>

Custom Accessibility Labels

const customRenderers = {
  heading: (node, key, ctx) => (
    <Text
      key={key}
      accessible={true}
      accessibilityRole="header"
      accessibilityLevel={node.level}
      style={ctx.theme.textStyles[`heading${node.level}`]}
    >
      {node.children?.map((child, i) => ctx.renderNode(child, i))}
    </Text>
  ),
}

API Reference

Components

<MarkdownView>

Main component for rendering markdown content.

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | content | string | Required | Markdown string to render | | ast | MarkdownNode[] | undefined | Pre-parsed AST (skips parsing if provided) | | parserOptions | ParserOptions | {} | Parser configuration options | | theme | MarkdownTheme | Context theme | Theme override (uses ThemeProvider theme if not specified) | | customRenderers | CustomRenderers | undefined | Override default element renderers | | style | ViewStyle | undefined | Container view style | | onLinkPress | (url: string, title?: string) => void | undefined | Link press handler | | onImagePress | (src: string, alt?: string, title?: string) => void | undefined | Image press handler | | onCheckboxToggle | (checked: boolean, node: MarkdownNode) => void | undefined | Task list checkbox toggle handler | | showErrors | boolean | false | Display parse errors in development |

Example - All Props:

<MarkdownView
  content="# Hello World"
  parserOptions={{ gfm: true, math: true }}
  theme={darkTheme}
  customRenderers={{ heading: CustomHeading }}
  style={{ padding: 16, backgroundColor: '#fff' }}
  onLinkPress={(url) => Linking.openURL(url)}
  onImagePress={(src) => console.log('Image:', src)}
  onCheckboxToggle={(checked, node) => console.log('Checked:', checked)}
  showErrors={__DEV__}
/>

<ThemeProvider>

Provides theme context to all child MarkdownView components.

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | theme | MarkdownTheme | Required | Theme object (lightTheme or darkTheme or custom) | | children | ReactNode | Required | Child components |

Example:

import { ThemeProvider, darkTheme } from 'react-native-hyper-markdown'

<ThemeProvider theme={darkTheme}>
  <MarkdownView content={markdown1} />
  <MarkdownView content={markdown2} />
</ThemeProvider>

Hooks

useMarkdown(content, options)

Parse markdown and return memoized result.

Parameters:

  • content: string - Markdown string to parse
  • options?: ParserOptions - Parser configuration

Returns: ParseResult

  • success: boolean - Parse success status
  • nodes: MarkdownNode[] - Parsed AST nodes
  • error?: ParseError - Error details (if parsing failed)

Example:

import { useMarkdown } from 'react-native-hyper-markdown'

const result = useMarkdown(content, { gfm: true })
if (result.success) {
  return <MarkdownView ast={result.nodes} />
} else {
  return <ErrorView error={result.error} />
}

useDebouncedParsing(content, options, delay)

Parse markdown with debounce for live editors.

Parameters:

  • content: string - Markdown string to parse
  • options?: ParserOptions - Parser configuration
  • delay?: number - Debounce delay in milliseconds (default: 300)

Returns: ParseResult

Example:

import { useDebouncedParsing } from 'react-native-hyper-markdown'

const [input, setInput] = useState('')
const result = useDebouncedParsing(input, {}, 200)

return (
  <>
    <TextInput value={input} onChangeText={setInput} />
    {result.success && <MarkdownView ast={result.nodes} />}
  </>
)

useMarkdownAST(content, options)

Get parsed AST nodes only (returns empty array on error).

Parameters:

  • content: string - Markdown string to parse
  • options?: ParserOptions - Parser configuration

Returns: MarkdownNode[] - AST nodes (empty on error)

Example:

import { useMarkdownAST } from 'react-native-hyper-markdown'

const nodes = useMarkdownAST(content)
const headings = nodes.filter(n => n.type === 'heading')
console.log('Found', headings.length, 'headings')

useMarkdownTheme()

Get current theme from ThemeProvider context.

Returns: MarkdownTheme - Current theme object

Example:

import { useMarkdownTheme } from 'react-native-hyper-markdown'

function MyComponent() {
  const theme = useMarkdownTheme()
  return <Text style={{ color: theme.colors.text }}>Custom text</Text>
}

Parser Functions

parseMarkdown(content, options)

Directly parse markdown to AST (synchronous, non-hook version).

Parameters:

  • content: string - Markdown string
  • options?: ParserOptions - Parser configuration

Returns: ParseResult

Example:

import { parseMarkdown } from 'react-native-hyper-markdown'

const result = parseMarkdown('# Hello World', { gfm: true })
console.log(result.nodes)

getNativeModule()

Access the native Nitro module directly for advanced use cases.

Returns: HyperMarkdownSpec - Native module instance

Example:

import { getNativeModule } from 'react-native-hyper-markdown'

const module = getNativeModule()
const result = module.parse('# Test', {})

Parser Options

Complete ParserOptions configuration:

| Option | Type | Default | Description | |--------|------|---------|-------------| | gfm | boolean | true | Enable GitHub Flavored Markdown | | enableTables | boolean | true | Enable GFM tables support | | enableTaskLists | boolean | true | Enable task list checkboxes | | enableStrikethrough | boolean | true | Enable ~~strikethrough~~ text | | enableAutolink | boolean | true | Auto-link URLs without markup | | math | boolean | false | Enable LaTeX math expressions ($inline$ and $$block$$) | | wiki | boolean | false | Enable Wiki-style [[links]] | | maxInputSize | number | 10485760 | Maximum input size in bytes (10MB default) | | timeout | number | 5000 | Parse timeout in milliseconds |

Examples:

// Strict CommonMark only (disable all GFM features)
parserOptions={{ gfm: false }}

// Enable math and wiki links
parserOptions={{ math: true, wiki: true }}

// Large document handling
parserOptions={{
  maxInputSize: 50 * 1024 * 1024, // 50MB
  timeout: 10000, // 10 seconds
}}

// Minimal features for performance
parserOptions={{
  gfm: true,
  enableTables: false,
  enableTaskLists: false,
  enableStrikethrough: false,
}}

Markdown Support

Basic Elements

  • Headings (h1-h6)
  • Paragraphs
  • Text formatting: bold, italic, ~~strikethrough~~, underline
  • Line breaks (soft and hard)

Lists

  • Unordered lists
  • Ordered lists
  • Task lists with checkboxes
  • Nested lists

Code

  • Inline code: `code`
  • Code blocks with language specification
  • Syntax highlight ready (theme provides hooks)

Tables

GitHub Flavored Markdown tables with alignment:

| Left | Center | Right |
|------|:------:|------:|
| A    |   B    |     C |

Advanced

  • Block quotes, links, images, HTML, LaTeX math, wiki links

Theme System

Built-in Themes

lightTheme:

  • Light background (#ffffff)
  • Dark text (#1f2937)
  • Blue links (#3b82f6)
  • Gray code blocks (#f3f4f6)

darkTheme:

  • Dark background (#1f2937)
  • Light text (#f9fafb)
  • Sky blue links (#60a5fa)
  • Darker code blocks (#111827)

Usage:

import { ThemeProvider, lightTheme, darkTheme } from 'react-native-hyper-markdown'

<ThemeProvider theme={darkTheme}>
  <MarkdownView content={markdown} />
</ThemeProvider>

Theme Structure

Complete theme interface with all available properties:

interface MarkdownTheme {
  // Colors
  colors: {
    text: string                    // Primary text color
    background: string              // Background color
    link: string                    // Link color
    codeBackground: string          // Code background
    blockquoteBorder: string        // Blockquote border
    tableBorder: string             // Table borders
    hr: string                      // Horizontal rule
  }

  // Text Styles
  textStyles: {
    text?: TextStyle                // Default text
    heading1?: TextStyle            // H1 heading
    heading2?: TextStyle            // H2 heading
    heading3?: TextStyle            // H3 heading
    heading4?: TextStyle            // H4 heading
    heading5?: TextStyle            // H5 heading
    heading6?: TextStyle            // H6 heading
    strong?: TextStyle              // Bold text
    emphasis?: TextStyle            // Italic text
    strikethrough?: TextStyle       // Strikethrough
    link?: TextStyle                // Link text
    codeInline?: TextStyle          // Inline code
    codeBlock?: TextStyle           // Code block text
    blockquote?: TextStyle          // Blockquote text
    listItem?: TextStyle            // List item text
    tableCell?: TextStyle           // Table cell
    tableHeader?: TextStyle         // Table header
  }

  // Container Styles
  containerStyles: {
    document?: ViewStyle            // Root container
    paragraph?: ViewStyle           // Paragraph container
    blockquoteContainer?: ViewStyle // Blockquote container
    codeBlockContainer?: ViewStyle  // Code block container
    list?: ViewStyle                // List container
    listItemContainer?: ViewStyle   // List item container
    table?: ViewStyle               // Table container
    tableRow?: ViewStyle            // Table row
    tableCellContainer?: ViewStyle  // Table cell container
    imageContainer?: ViewStyle      // Image wrapper
    thematicBreak?: ViewStyle       // Horizontal rule
    checkbox?: ViewStyle            // Unchecked checkbox
    checkboxChecked?: ViewStyle     // Checked checkbox
  }

  // Image Styles
  imageStyles: {
    image?: ImageStyle              // Image style
  }

  // Spacing
  spacing: {
    paragraph: number               // Paragraph margin
    listIndent: number              // List indentation
    codeBlockPadding: number        // Code block padding
    blockquotePadding: number       // Blockquote padding
    tableCellPadding: number        // Table cell padding
  }

  // Syntax Highlighting (optional)
  syntaxHighlighting?: {
    enabled?: boolean               // Enable highlighting
    theme?: object                  // react-syntax-highlighter theme
    highlighter?: 'hljs' | 'prism' // Highlighter type
    fontSize?: number               // Code font size
    fontFamily?: string             // Code font family
  }
}

Custom Theme Example

Create a custom theme by extending built-in themes:

import { lightTheme, type MarkdownTheme } from 'react-native-hyper-markdown'

const customTheme: MarkdownTheme = {
  ...lightTheme,
  colors: {
    ...lightTheme.colors,
    link: '#ff6b6b',
    text: '#2d3748',
  },
  textStyles: {
    ...lightTheme.textStyles,
    heading1: {
      fontSize: 36,
      fontWeight: 'bold',
      color: '#1a202c',
      marginBottom: 16,
    },
    heading2: {
      fontSize: 30,
      fontWeight: '600',
      color: '#2d3748',
      marginBottom: 12,
    },
    codeInline: {
      fontFamily: 'Menlo',
      fontSize: 14,
      backgroundColor: '#f7fafc',
      paddingHorizontal: 4,
      borderRadius: 3,
    },
  },
  spacing: {
    ...lightTheme.spacing,
    paragraph: 16,
    listIndent: 20,
  },
}

<ThemeProvider theme={customTheme}>
  <MarkdownView content={markdown} />
</ThemeProvider>

Partial Theme Overrides

Override specific properties without specifying the entire theme:

const themeOverrides = {
  textStyles: {
    heading1: { fontSize: 32, color: '#ff0000' },
  },
  colors: {
    link: '#0000ff',
  },
}

// Merge with lightTheme
const mergedTheme = { ...lightTheme, ...themeOverrides }

Custom Renderers

Override rendering for any of the 25+ markdown element types with custom React components.

Renderer Signature

type CustomRenderer<T extends MarkdownNode = MarkdownNode> = (
  node: T,           // The AST node to render
  key: string | number,  // React key for the element
  ctx: RenderContext     // Context with theme, handlers, and renderNode function
) => ReactNode

interface RenderContext {
  theme: MarkdownTheme                        // Current theme
  onLinkPress?: (url: string, title?: string) => void
  onImagePress?: (src: string, alt?: string, title?: string) => void
  onCheckboxToggle?: (checked: boolean, node: MarkdownNode) => void
  renderNode: (node: MarkdownNode, key: string | number) => ReactNode
}

Available Node Types

You can customize any of these 25 node types:

  • Document & Structure: document, paragraph, heading
  • Text Formatting: text, strong, emphasis, strikethrough, underline
  • Links & Media: link, image
  • Code: code_block, code_inline
  • Quotes: blockquote
  • Lists: list, list_item, task_list_item
  • Tables: table, table_row, table_cell
  • Math: math_inline, math_block
  • Breaks: thematic_break, softbreak, hardbreak
  • Extended: wiki_link, html_block

Example: Custom Heading

const customRenderers = {
  heading: (node, key, ctx) => {
    const level = node.level || 1
    const fontSize = 32 - level * 4

    return (
      <View
        key={key}
        style={{
          marginVertical: 16,
          paddingBottom: 8,
          borderBottomWidth: level <= 2 ? 2 : 0,
          borderBottomColor: ctx.theme.colors.hr,
        }}
      >
        <Text
          style={{
            fontSize,
            fontWeight: 'bold',
            color: ctx.theme.colors.text,
          }}
        >
          {node.children?.map((child, i) =>
            ctx.renderNode(child, `${key}-${i}`)
          )}
        </Text>
      </View>
    )
  },
}

Example: Custom Link with Icon

import Icon from 'react-native-vector-icons/Feather'

const customRenderers = {
  link: (node, key, ctx) => (
    <Pressable
      key={key}
      onPress={() => ctx.onLinkPress?.(node.href, node.title)}
      style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}
    >
      <Icon name="link" size={14} color={ctx.theme.colors.link} />
      <Text style={{ color: ctx.theme.colors.link, textDecorationLine: 'underline' }}>
        {node.children?.map((child, i) =>
          ctx.renderNode(child, `${key}-${i}`)
        )}
      </Text>
    </Pressable>
  ),
}

Example: Custom Code Block with Syntax Highlighting

import { SyntaxHighlighter } from 'react-native-hyper-markdown'
import { atomOneDark } from 'react-syntax-highlighter/styles/hljs'

const customRenderers = {
  code_block: (node, key, ctx) => (
    <View
      key={key}
      style={{
        marginVertical: 8,
        borderRadius: 8,
        overflow: 'hidden',
      }}
    >
      {node.language && (
        <View style={{ backgroundColor: '#2d3748', padding: 8 }}>
          <Text style={{ color: '#a0aec0', fontSize: 12 }}>
            {node.language}
          </Text>
        </View>
      )}
      <SyntaxHighlighter
        language={node.language || 'text'}
        style={atomOneDark}
        fontSize={14}
      >
        {node.content || ''}
      </SyntaxHighlighter>
    </View>
  ),
}

Example: Custom Task List Item

const customRenderers = {
  task_list_item: (node, key, ctx) => (
    <Pressable
      key={key}
      onPress={() => ctx.onCheckboxToggle?.(!node.checked, node)}
      style={{
        flexDirection: 'row',
        alignItems: 'center',
        paddingVertical: 4,
        minHeight: 44, // Accessibility touch target
      }}
    >
      <View
        style={{
          width: 20,
          height: 20,
          borderWidth: 2,
          borderColor: ctx.theme.colors.link,
          borderRadius: 4,
          marginRight: 12,
          justifyContent: 'center',
          alignItems: 'center',
          backgroundColor: node.checked ? ctx.theme.colors.link : 'transparent',
        }}
      >
        {node.checked && (
          <Icon name="check" size={14} color="#fff" />
        )}
      </View>
      <Text style={{ flex: 1, color: ctx.theme.colors.text }}>
        {node.children?.map((child, i) =>
          ctx.renderNode(child, `${key}-${i}`)
        )}
      </Text>
    </Pressable>
  ),
}

Key Patterns

  1. Access Theme: Use ctx.theme for consistent styling
  2. Render Children: Use ctx.renderNode(child, key) for recursive rendering
  3. Handle Events: Use ctx.onLinkPress?.(...), ctx.onImagePress?.(...), etc.
  4. Preserve Keys: Always pass key prop to root element
  5. Node Properties: Access node-specific data like node.level, node.href, node.checked, etc.

Usage

<MarkdownView
  content={markdown}
  customRenderers={customRenderers}
/>

See example/screens/CustomRenderersScreen.tsx for more complete examples.

Advanced Usage

Syntax Highlighting

Code syntax highlighting requires the react-syntax-highlighter peer dependency.

Installation

npm install react-syntax-highlighter
npm install --save-dev @types/react-syntax-highlighter

Basic Setup

import { MarkdownView, darkTheme } from 'react-native-hyper-markdown'
import { atomOneDark } from 'react-syntax-highlighter/styles/hljs'

<MarkdownView
  content={markdown}
  theme={{
    ...darkTheme,
    syntaxHighlighting: {
      enabled: true,
      theme: atomOneDark,
      highlighter: 'hljs',
      fontSize: 14,
      fontFamily: 'Menlo',
    },
  }}
/>

Available Themes

HLJS Themes:

  • atomOneDark, atomOneLight - Modern Atom themes
  • github, githubGist - GitHub-style themes
  • monokai, monokaiSublime - Monokai variants
  • vs, vs2015 - Visual Studio themes
  • xcode - Xcode theme

Prism Themes:

  • prism, dark, funky, okaidia, twilight, coy

Import from:

import { atomOneDark } from 'react-syntax-highlighter/styles/hljs'
import { prism } from 'react-syntax-highlighter/styles/prism'

Using Prism Highlighter

import { prism } from 'react-syntax-highlighter/styles/prism'

theme={{
  syntaxHighlighting: {
    enabled: true,
    theme: prism,
    highlighter: 'prism',  // Use Prism instead of hljs
  },
}}

Custom Code Renderer with Highlighting

import { SyntaxHighlighter } from 'react-native-hyper-markdown'
import { atomOneDark } from 'react-syntax-highlighter/styles/hljs'

const customRenderers = {
  code_block: (node, key, ctx) => (
    <View key={key} style={{ marginVertical: 12 }}>
      {/* Language label */}
      {node.language && (
        <View style={{ backgroundColor: '#2d3748', padding: 8 }}>
          <Text style={{ color: '#cbd5e0', fontSize: 12, fontWeight: '600' }}>
            {node.language}
          </Text>
        </View>
      )}

      {/* Syntax highlighted code */}
      <SyntaxHighlighter
        language={node.language || 'text'}
        style={atomOneDark}
        fontSize={14}
        fontFamily="Menlo"
      >
        {node.content || ''}
      </SyntaxHighlighter>
    </View>
  ),
}

Performance Optimization

Memoization Best Practices

import { useMemo } from 'react'
import { parseMarkdown } from 'react-native-hyper-markdown'

// Parse once, reuse AST
const ast = useMemo(
  () => parseMarkdown(content, options),
  [content, options]
)

// Avoid re-parsing on every render
return <MarkdownView ast={ast.nodes} />
// Memoize custom renderers to prevent re-creation
const customRenderers = useMemo(() => ({
  heading: (node, key, ctx) => { /* ... */ },
  link: (node, key, ctx) => { /* ... */ },
}), []) // Empty deps - renderers don't change

Debounced Parsing for Live Editors

import { useState } from 'react'
import { useDebouncedParsing } from 'react-native-hyper-markdown'

const [input, setInput] = useState('')
const result = useDebouncedParsing(input, {}, 300)

// Parsing happens 300ms after last keystroke
// Reduces parsing from every keystroke to ~3x per second

Large Document Strategies

1. Pagination:

const pages = useMemo(() => {
  const nodes = parseMarkdown(content).nodes
  const pageSize = 10
  return Array.from(
    { length: Math.ceil(nodes.length / pageSize) },
    (_, i) => nodes.slice(i * pageSize, (i + 1) * pageSize)
  )
}, [content])

return <MarkdownView ast={pages[currentPage]} />

2. Virtualization with FlatList:

import { FlatList } from 'react-native'

const sections = useMemo(() => parseMarkdown(longDocument).nodes, [longDocument])

<FlatList
  data={sections}
  renderItem={({ item }) => <MarkdownView ast={[item]} />}
  keyExtractor={(_, i) => `section-${i}`}
  initialNumToRender={5}
  maxToRenderPerBatch={5}
  windowSize={10}
/>

3. Lazy Loading:

import { Suspense, lazy } from 'react'

const LazyMarkdown = lazy(() => import('./MarkdownSection'))

<Suspense fallback={<ActivityIndicator />}>
  <LazyMarkdown content={section} />
</Suspense>

Error Handling

Parse Error Display

import { parseMarkdown } from 'react-native-hyper-markdown'

const result = parseMarkdown(content)

if (!result.success) {
  console.error('Parse failed:', result.error)
  return (
    <View style={{ padding: 16, backgroundColor: '#fee' }}>
      <Text style={{ color: '#c00' }}>
        Parse Error: {result.error?.message}
      </Text>
      {result.error?.line && (
        <Text style={{ color: '#666', fontSize: 12 }}>
          Line {result.error.line}, Column {result.error.column}
        </Text>
      )}
    </View>
  )
}

return <MarkdownView ast={result.nodes} />

Error Boundaries for Safe Rendering

class MarkdownErrorBoundary extends React.Component {
  state = { hasError: false, error: null }

  static getDerivedStateFromError(error) {
    return { hasError: true, error }
  }

  componentDidCatch(error, errorInfo) {
    console.error('Markdown render error:', error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      return (
        <View style={{ padding: 16 }}>
          <Text style={{ color: '#c00' }}>
            Failed to render markdown
          </Text>
        </View>
      )
    }
    return this.props.children
  }
}

// Usage
<MarkdownErrorBoundary>
  <MarkdownView content={untrustedContent} />
</MarkdownErrorBoundary>

Timeout Handling

<MarkdownView
  content={potentiallyLargeContent}
  parserOptions={{
    timeout: 3000,        // 3 seconds max
    maxInputSize: 1024 * 1024,  // 1MB max
  }}
  showErrors={__DEV__}  // Show errors in development
/>

Accessibility Implementation

WCAG 2.1 Compliant Renderers

const accessibleRenderers = {
  heading: (node, key, ctx) => (
    <Text
      key={key}
      accessible={true}
      accessibilityRole="header"
      accessibilityLevel={node.level}
      style={ctx.theme.textStyles[`heading${node.level}`]}
    >
      {node.children?.map((child, i) =>
        ctx.renderNode(child, `${key}-${i}`)
      )}
    </Text>
  ),

  link: (node, key, ctx) => (
    <Text
      key={key}
      accessible={true}
      accessibilityRole="link"
      accessibilityHint="Double tap to open link"
      accessibilityLabel={node.title || node.href}
      onPress={() => ctx.onLinkPress?.(node.href, node.title)}
      style={ctx.theme.textStyles.link}
    >
      {node.children?.map((child, i) =>
        ctx.renderNode(child, `${key}-${i}`)
      )}
    </Text>
  ),

  image: (node, key, ctx) => (
    <Image
      key={key}
      source={{ uri: node.src }}
      accessible={true}
      accessibilityLabel={node.alt || 'Image'}
      accessibilityRole="image"
      style={ctx.theme.imageStyles.image}
    />
  ),

  task_list_item: (node, key, ctx) => (
    <Pressable
      key={key}
      accessible={true}
      accessibilityRole="checkbox"
      accessibilityState={{ checked: node.checked }}
      accessibilityLabel={node.children?.[0]?.content}
      onPress={() => ctx.onCheckboxToggle?.(!node.checked, node)}
      style={{ minHeight: 44, minWidth: 44 }}  // Touch target
    >
      {/* Checkbox UI */}
    </Pressable>
  ),
}

Touch Target Sizing

iOS Human Interface Guidelines and Material Design recommend minimum 44x44pt touch targets:

const MIN_TOUCH_SIZE = 44

const customRenderers = {
  task_list_item: (node, key, ctx) => (
    <Pressable
      key={key}
      style={{
        minHeight: MIN_TOUCH_SIZE,
        minWidth: MIN_TOUCH_SIZE,
        flexDirection: 'row',
        alignItems: 'center',
      }}
      onPress={() => ctx.onCheckboxToggle?.(!node.checked, node)}
    >
      {/* Content */}
    </Pressable>
  ),

  link: (node, key, ctx) => (
    <Pressable
      key={key}
      style={{ minHeight: MIN_TOUCH_SIZE }}
      onPress={() => ctx.onLinkPress?.(node.href)}
    >
      <Text style={ctx.theme.textStyles.link}>
        {node.children?.map((child, i) =>
          ctx.renderNode(child, `${key}-${i}`)
        )}
      </Text>
    </Pressable>
  ),
}

Screen Reader Announcements

<MarkdownView
  content={markdown}
  accessible={true}
  accessibilityLabel="Article content"
  accessibilityRole="text"
  accessibilityHint="Swipe to read article"
/>

Platform-Specific Considerations

iOS

Font Rendering:

  • Uses system fonts by default (San Francisco)
  • Code blocks use Menlo (monospace) font
  • Supports custom fonts via fontFamily in theme

Safe Areas:

import { SafeAreaView } from 'react-native'

<SafeAreaView style={{ flex: 1 }}>
  <MarkdownView content={markdown} />
</SafeAreaView>

Dark Mode:

import { useColorScheme } from 'react-native'
import { lightTheme, darkTheme } from 'react-native-hyper-markdown'

const colorScheme = useColorScheme()
const theme = colorScheme === 'dark' ? darkTheme : lightTheme

<MarkdownView content={markdown} theme={theme} />

Performance Characteristics

| Document Size | Parse Time | Rendering Time | |---------------|------------|----------------| | 10 KB | 1-5ms | 50-100ms | | 100 KB | 10-50ms | 200-500ms | | 1 MB | 100-500ms | 1-3s |

  • All parsing happens on native thread (non-blocking UI) via Nitro's threadpool
  • Rendering happens on main thread
  • Use virtualization for documents >100KB

Migration Guide

From react-native-markdown-display

Before:

import Markdown from 'react-native-markdown-display'

<Markdown style={styles}>
  {content}
</Markdown>

After:

import { MarkdownView } from 'react-native-hyper-markdown'

<MarkdownView
  content={content}
  theme={{ containerStyles: styles }}
/>

Key Differences:

  1. Theme Structure:

    • react-native-markdown-display uses flat style object
    • react-native-hyper-markdown uses structured theme with textStyles, containerStyles, colors, spacing
  2. Custom Renderers:

    // Before (markdown-display)
    const rules = {
      heading1: (node, children) => <Text>{children}</Text>
    }
    
    // After (hyper-markdown)
    const customRenderers = {
      heading: (node, key, ctx) => (
        <Text key={key}>{ctx.renderNode(node.children)}</Text>
      )
    }
  3. Performance:

    • 100x faster parsing (native C++ vs JavaScript)
    • AST can be pre-parsed and reused
    • Better performance on large documents
  4. API Differences:

    • style prop → theme.containerStyles
    • rules prop → customRenderers
    • Children content → content prop

Troubleshooting & FAQ

Parse Errors

Q: "Unknown parse error" or parsing fails silently

  • Check markdown syntax, especially unclosed code blocks or malformed tables
  • Enable error display: showErrors={__DEV__}
  • Check console for detailed error messages

Q: Timeout errors on large documents

parserOptions={{
  timeout: 10000,  // Increase to 10 seconds
  maxInputSize: 50 * 1024 * 1024,  // Increase to 50MB
}}

Q: Math expressions not rendering

  • Enable math parser: parserOptions={{ math: true }}
  • Check LaTeX syntax is valid
  • Math rendering is basic - consider mathjs library for advanced features

Rendering Issues

Q: Styles not applying

  • Ensure ThemeProvider wraps MarkdownView, or pass theme prop directly
  • Check theme structure matches MarkdownTheme interface
  • Verify style properties are valid React Native styles

Q: Images not showing

  • Verify image URLs are accessible
  • Check onImagePress handler if custom image rendering
  • Ensure network permissions in AndroidManifest.xml

Q: Code blocks render as plain text (no syntax highlighting)

  • Install peer dependency: npm install react-syntax-highlighter
  • Enable in theme: syntaxHighlighting: { enabled: true }
  • Check isSyntaxHighlightingAvailable() returns true

Q: Custom renderers not working

  • Ensure renderer returns React element with key prop
  • Use ctx.renderNode() for children, not direct mapping
  • Check node type matches (e.g., heading not heading1)

Performance Issues

Q: Slow rendering on large documents

  • Use virtualization with FlatList (see Performance section)
  • Implement pagination
  • Pre-parse AST and memoize result
  • Consider splitting document into sections

Q: Re-parsing on every render

// Bad - re-parses every render
<MarkdownView content={content} />

// Good - memoize AST
const ast = useMemo(() => parseMarkdown(content), [content])
return <MarkdownView ast={ast.nodes} />

Q: Memory issues with many MarkdownView components

  • Share parsed AST across components
  • Use ast prop instead of content to avoid duplicate parsing
  • Implement component pooling or recycling

Platform Issues

Q: iOS build fails with pod install errors

cd ios
rm -rf Pods Podfile.lock
pod install
cd ..

Q: Android NDK errors

  • Check NDK version in android/gradle.properties
  • Update to compatible NDK version (r21-r26)
  • Clear Gradle cache: cd android && ./gradlew clean

Q: Metro bundler crashes or "Nitro Module Not Found"

npm start -- --reset-cache
# or
watchman watch-del-all
rm -rf node_modules
npm install

Q: TypeScript errors with custom renderers

  • Import types: import type { CustomRenderers } from 'react-native-hyper-markdown'
  • Use as const for renderer objects
  • Ensure node types match MarkdownNode interface

Known Limitations

  • HTML Sanitization: Raw HTML rendered as-is (no XSS protection built-in)

    • Sanitize untrusted content before rendering
    • Consider using DOMPurify or similar for HTML sanitization
  • Complex Tables: Very wide tables may require horizontal scrolling

    • Implement custom table renderer with ScrollView
    • Consider responsive table layouts
  • Math Rendering: LaTeX display is basic

    • For advanced math, integrate mathjs or react-native-mathjax
    • Custom math renderers recommended for production
  • Image Sizing: No automatic image resizing

    • Handle in custom image renderer
    • Use resizeMode in imageStyles.image
  • RTL Languages: Limited right-to-left language support

    • May require custom renderers for proper RTL rendering
    • Contributions welcome
  • Inline HTML: Limited inline HTML support

    • Most HTML passes through but may not render correctly
    • Use custom renderers for specific HTML elements

Comparison with Alternatives

| Feature | hyper-markdown | markdown-display | markdown-renderer | |---------|----------------|------------------|-------------------| | Parse Speed | 100x faster (C++) | JavaScript | JavaScript | | Bundle Size | ~500KB (with native) | ~200KB | ~150KB | | TypeScript | Full support | Partial | Partial | | Custom Renderers | 25+ node types | Limited | Custom rules | | Theme System | Deep customization | Style props | Limited | | GFM Support | Full (tables, tasks, strikethrough) | Partial | Partial | | Math Support | Built-in ($LaTeX$) | Plugin | No | | Wiki Links | Built-in | No | No | | Performance | Excellent (native) | Good | Good | | Maintenance | Active | Active | Archived | | React Native | 0.76+ | 0.60+ | 0.60+ |

Choose hyper-markdown if:

  • You need maximum performance (large documents, frequent updates)
  • You want complete GFM support out of the box
  • You need advanced features (math, wiki links, syntax highlighting)
  • You want full TypeScript support
  • You're building a production app with React Native 0.76+

Choose alternatives if:

  • You need React Native <0.76 support
  • Bundle size is critical concern
  • You don't need native performance

Example App

Clone the repository and run the example app:

cd example
npm install
npm run ios    # Run on iOS
npm run android # Run on Android

Performance

  • Parse 10KB document: ~1-5ms
  • Parse 100KB document: ~10-50ms
  • Parse 1MB document: ~100-500ms
  • Live preview with debounce: 300ms default

All parsing happens on a native thread (non-blocking) via Nitro's threadpool.

License

MIT - See LICENSE

Credits

Built with Nitro Modules and the md4c parser.