@novastera-oss/react-native-markdown-display
v8.1.0
Published
Markdown renderer for react-native, with CommonMark spec support. Maintained by Novastera.
Maintainers
Readme
React Native Markdown Display
It is a CommonMark-compatible React Native markdown renderer that uses native components rather than a web view. Maintained by Novastera, an open-source CRM/ERP platform, it is designed to be easy to theme and extend.
Install
Yarn
yarn add @novastera-oss/react-native-markdown-displayNPM
npm install -S @novastera-oss/react-native-markdown-displayBreaking Changes
See BREAKING_CHANGE.md for the full migration notes from the legacy version.
Get Started
import React from 'react';
import { SafeAreaView, ScrollView, StatusBar } from 'react-native';
import Markdown from '@novastera-oss/react-native-markdown-display';
const copy = `# h1 Heading 8-)
**This is some bold text!**
This is normal text
`;
const App: () => React.ReactNode = () => {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{height: '100%'}}
>
<Markdown>
{copy}
</Markdown>
</ScrollView>
</SafeAreaView>
</>
);
};
export default App;Props and Functions
The <Markdown> object takes the following common props:
| Property | Default | Required | Description
| --- | --- | --- | ---
| children | N/A | true | The markdown string to render, or the pre-processed tree
| style | source | false | An object to override the styling for the various rules, see style section below for more info
| mergeStyle | true | false | If true, when a style is supplied, the individual items are merged with the default styles instead of overwriting them
| rules | source | false | An object of rules that specify how to render each markdown item, see rules section below for more info
| onLinkPress | import { Linking } from 'react-native'; and Linking.openURL(url); | false | A handler function to change click behaviour, see handling links section below for more info
| debugPrintTree | false | false | Will print the AST tree to the console to help you see what the markdown is being translated to
And some additional, less used options:
| Property | Default | Required | Description
| --- | --- | --- | ---
| renderer | instanceOf(AstRenderer) | false | Used to specify a custom renderer, you can not use the rules or styles props with a custom renderer.
| markdownit | instanceOf(MarkdownIt) | false | A custom markdownit instance with your configuration, default is MarkdownIt({typographer: true})
| maxTopLevelChildren | null | false | If defined as a number will only render out first n many top level children, then will try to render out topLevelMaxExceededItem
| topLevelMaxExceededItem | <Text key="dotdotdot">...</Text> | false | Will render when maxTopLevelChildren is hit. Make sure to give it a key!
| allowedImageHandlers | ['data:image/png;base64', 'data:image/gif;base64', 'data:image/jpeg;base64', 'https://', 'http://'] | false | Any image that does not start with one of these will have the defaultImageHandler value prepended to it (unless defaultImageHandler is null in which case it won't try to render anything)
| defaultImageHandler | http:// | false | Will be prepended to an image url if it does not start with something in the allowedImageHandlers array, if this is set to null, it won't try to recover but will just not render anything instead.
Syntax Support
# h1 Heading 8-)
## h2 Heading
### h3 Heading
#### h4 Heading
##### h5 Heading
###### h6 Heading| iOS | Android | --- | --- | |
Some text above
___
Some text in the middle
---
Some text below| iOS | Android | --- | --- | |
**This is bold text**
__This is bold text__
*This is italic text*
_This is italic text_
~~Strikethrough~~| iOS | Android | --- | --- | |
> Blockquotes can also be nested...
>> ...by using additional greater-than signs right next to each other...
> > > ...or with spaces between arrows.| iOS | Android | --- | --- | |
Unordered
+ Create a list by starting a line with `+`, `-`, or `*`
+ Sub-lists are made by indenting 2 spaces:
- Marker character change forces new list start:
* Ac tristique libero volutpat at
+ Facilisis in pretium nisl aliquet. This is a very long list item that will surely wrap onto the next line.
- Nulla volutpat aliquam velit
+ Very easy!
Ordered
1. Lorem ipsum dolor sit amet
2. Consectetur adipiscing elit. This is a very long list item that will surely wrap onto the next line.
3. Integer molestie lorem at massa
Start numbering with offset:
57. foo
58. bar| iOS | Android | --- | --- | |
Inline \`code\`
Indented code
// Some comments
line 1 of code
line 2 of code
line 3 of code
Block code "fences"
\`\`\`
Sample text here...
\`\`\`
Syntax highlighting
\`\`\` js
var foo = function (bar) {
return bar++;
};
console.log(foo(5));
\`\`\`| iOS | Android | --- | --- | |
| Option | Description |
| ------ | ----------- |
| data | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext | extension to be used for dest files. |
Right aligned columns
| Option | Description |
| ------:| -----------:|
| data | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext | extension to be used for dest files. || iOS | Android | --- | --- | |
[link text](https://www.google.com)
[link with title](https://www.google.com "title text!")
Autoconverted link https://www.google.com (enable linkify to see)| iOS | Android | --- | --- | |
You can pass image attributes (e.g. width, height, contentFit, contentPosition) via markdown-it attributes or HTML to map onto Expo Image props.


Like links, Images also have a footnote style syntax
![Alt text][id]
With a reference later in the document defining the URL location:
[id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat"| iOS | Android | --- | --- | |
Enable typographer option to see result.
(c) (C) (r) (R) (tm) (TM) (p) (P) +-
test.. test... test..... test?..... test!....
!!!!!! ???? ,, -- ---
"Smartypants, double quotes" and 'single quotes'| iOS | Android | --- | --- | |
Plugins for extra syntax support can be added using any markdown-it compatible plugins - see plugins for documentation from markdown-it. An example for integration follows:
Step 1
Identify the new components and integrate the plugin with a rendered component. We can use the debugPrintTree property to see what rules we are rendering:
import React from 'react';
import { SafeAreaView, ScrollView, StatusBar } from 'react-native';
import Markdown, { MarkdownIt } from '@novastera-oss/react-native-markdown-display';
import blockEmbedPlugin from 'markdown-it-block-embed';
const markdownItInstance =
MarkdownIt({typographer: true})
.use(blockEmbedPlugin, {
containerClassName: "video-embed"
});
const copy = `
# Some header
@[youtube](lJIrF4YjHfQ)
`;
const App: () => React.ReactNode = () => {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{height: '100%'}}
>
<Markdown
debugPrintTree
markdownit={markdownItInstance}
>
{copy}
</Markdown>
</ScrollView>
</SafeAreaView>
</>
);
};
export default App;
In the console, we will see the following rendered tree:
body
-heading1
--textgroup
---text
-videoWith the following error message:
Warning, unknown render rule encountered: video. 'unknown' render rule used (by default, returns null - nothing rendered) Step 2
We need to create the render rules and styles to handle this new 'video' component
import React from 'react';
import { SafeAreaView, ScrollView, StatusBar } from 'react-native';
import Markdown, { MarkdownIt } from '@novastera-oss/react-native-markdown-display';
import blockEmbedPlugin from 'markdown-it-block-embed';
const markdownItInstance =
MarkdownIt({typographer: true})
.use(blockEmbedPlugin, {
containerClassName: "video-embed"
});
const copy = `
# Some header
@[youtube](lJIrF4YjHfQ)
`;
const App: () => React.ReactNode = () => {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{height: '100%'}}
>
<Markdown
debugPrintTree
markdownit={markdownItInstance}
style={{
video: {
color: 'red',
}
}}
rules={{
video: (node, children, parent, styles) =>{
// examine the node properties to see what video we need to render
console.log(node); // expected output of this is in readme.md below this code snip
return (<Text key={node.key} style={styles.video}>
Return a video component instead of this text component!
</Text>);
}
}}
>
{copy}
</Markdown>
</ScrollView>
</SafeAreaView>
</>
);
};
export default App;And all of the video properties needed to render something meaningful are on the node, like this:
{type: "video", sourceType: "video", sourceInfo: {…}, sourceMeta: null, block: true, …}
attributes: {}
block: true
children: []
content: ""
index: 1
key: "rnmr_1720a98f540_video"
markup: "@[youtube](lJIrF4YjHfQ)"
sourceInfo:
service: YouTubeService
env: PluginEnvironment {md: MarkdownIt, options: {…}, services: {…}}
name: "youtube"
options:
height: 390
width: 640
serviceName: "youtube"
videoID: "lJIrF4YjHfQ"
videoReference: "lJIrF4YjHfQ"
sourceMeta: null
sourceType: "video"
tokenIndex: 5
type: "video"Other Debugging
You can do some additional debugging of what the markdown instance is spitting out like this:
import Markdown, { MarkdownIt } from '@novastera-oss/react-native-markdown-display';
import blockEmbedPlugin from 'markdown-it-block-embed';
const markdownItInstance =
MarkdownIt({typographer: true})
.use(blockEmbedPlugin, {
containerClassName: "video-embed"
});
const copy = `
# Some header
@[youtube](lJIrF4YjHfQ)
`;
// this shows you the tree that is used by the react-native-markdown-display
const astTree = markdownItInstance.parse(copy, {});
console.log(astTree);
//this contains the html that would be generated - not used by react-native-markdown-display but useful for reference
const html = markdownItInstance.render(copy);
console.log(html);
The above code will output something like this:
astTree:
(4) [Token, Token, Token, Token]
0: Token {type: "heading_open", tag: "h1", attrs: null, map: Array(2), nesting: 1, …}
1: Token {type: "inline", tag: "", attrs: null, map: Array(2), nesting: 0, …}
2: Token {type: "heading_close", tag: "h1", attrs: null, map: null, nesting: -1, …}
3: Token {type: "video", tag: "div", attrs: null, map: Array(2), nesting: 0, …}
length: 4html:
<h1>Some header</h1>
<div class="video-embed block-embed-service-youtube"><iframe type="text/html" src="//www.youtube.com/embed/lJIrF4YjHfQ" frameborder="0" width="640" height="390" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div>This is all of the markdown in one place for testing that your applied styles work in all cases
Headings
# h1 Heading 8-)
## h2 Heading
### h3 Heading
#### h4 Heading
##### h5 Heading
###### h6 Heading
Horizontal Rules
Some text above
___
Some text in the middle
---
Some text below
Emphasis
**This is bold text**
__This is bold text__
*This is italic text*
_This is italic text_
~~Strikethrough~~
Blockquotes
> Blockquotes can also be nested...
>> ...by using additional greater-than signs right next to each other...
> > > ...or with spaces between arrows.
Lists
Unordered
+ Create a list by starting a line with `+`, `-`, or `*`
+ Sub-lists are made by indenting 2 spaces:
- Marker character change forces new list start:
* Ac tristique libero volutpat at
+ Facilisis in pretium nisl aliquet. This is a very long list item that will surely wrap onto the next line.
- Nulla volutpat aliquam velit
+ Very easy!
Ordered
1. Lorem ipsum dolor sit amet
2. Consectetur adipiscing elit. This is a very long list item that will surely wrap onto the next line.
3. Integer molestie lorem at massa
Start numbering with offset:
57. foo
58. bar
Code
Inline \`code\`
Indented code
// Some comments
line 1 of code
line 2 of code
line 3 of code
Block code "fences"
\`\`\`
Sample text here...
\`\`\`
Syntax highlighting
\`\`\` js
var foo = function (bar) {
return bar++;
};
console.log(foo(5));
\`\`\`
Tables
| Option | Description |
| ------ | ----------- |
| data | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext | extension to be used for dest files. |
Right aligned columns
| Option | Description |
| ------:| -----------:|
| data | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext | extension to be used for dest files. |
Links
[link text](https://www.google.com)
[link with title](https://www.google.com "title text!")
Autoconverted link https://www.google.com (enable linkify to see)
Images


Like links, Images also have a footnote style syntax
![Alt text][id]
With a reference later in the document defining the URL location:
[id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat"
Typographic Replacements
Enable typographer option to see result.
(c) (C) (r) (R) (tm) (TM) (p) (P) +-
test.. test... test..... test?..... test!....
!!!!!! ???? ,, -- ---
"Smartypants, double quotes" and 'single quotes'
Rules and Styles
How to style stuff
Text styles are applied in a way that makes it much more convenient to manage changes to global styles while also allowing fine tuning of individual elements.
Think of the implementation like applying styles in CSS. changes to the body effect everything, but can be overwritten further down the style / component tree.
Be careful when styling 'text': the text rule is not applied to all rendered text, most notably list bullet points. If you want to, for instance, color all text, change the body style.
import React from 'react';
import { SafeAreaView, ScrollView, StatusBar } from 'react-native';
import Markdown from '@novastera-oss/react-native-markdown-display';
const copy = `
This is some text which is red because of the body style, which is also really small!
\`\`\`
//This is a code block woooo
const cool = () => {
console.log('????');
};
\`\`\`
and some more small text
# This is a h1
## this is a h2
### this is a h3
`;
const App: () => React.ReactNode = () => {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{height: '100%'}}
>
<Markdown
style={{
body: {color: 'red', fontSize: 10},
heading1: {color: 'purple'},
code_block: {color: 'black', fontSize: 14}
}}
>
{copy}
</Markdown>
</ScrollView>
</SafeAreaView>
</>
);
};
export default App;Styles
Styles are used to override how certain rules are styled. The existing
implementation is in src/lib/styles.ts.
NOTE: By default styles are merged with the existing implementation, to
change this, see the mergeStyle prop.
Inline CSS (HTML style attributes)
Inline style="..." attributes inside markdown are supported only with a
minimal parser. It accepts simple key:value; pairs and basic numeric/px
values, and is not a full CSS engine. For anything complex, use the style
prop or custom rules.
import React from 'react';
import { SafeAreaView, ScrollView, StatusBar, StyleSheet } from 'react-native';
import Markdown from '@novastera-oss/react-native-markdown-display';
const styles = StyleSheet.create({
heading1: {
fontSize: 32,
backgroundColor: '#000000',
color: '#FFFFFF',
},
heading2: {
fontSize: 24,
},
heading3: {
fontSize: 18,
},
heading4: {
fontSize: 16,
},
heading5: {
fontSize: 13,
},
heading6: {
fontSize: 11,
}
});
const copy = `
# h1 Heading 8-)
## h2 Heading 8-)
### h3 Heading 8-)
| Option | Description |
| ------ | ----------- |
| data | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext | extension to be used for dest files. |
`;
const App: () => React.ReactNode = () => {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{height: '100%'}}
>
<Markdown
style={styles}
>
{copy}
</Markdown>
</ScrollView>
</SafeAreaView>
</>
);
};
export default App;Rules
Rules are used to specify how you want certain elements to be displayed. The existing implementation is here: https://github.com/novastera/react-native-markdown-display/blob/master/src/lib/renderRules.tsx
import React from 'react';
import { SafeAreaView, ScrollView, StatusBar, Text } from 'react-native';
import Markdown from '@novastera-oss/react-native-markdown-display';
const rules = {
heading1: (node, children, parent, styles) =>
<Text key={node.key} style={[styles.heading, styles.heading1]}>
>> H1 TEXT HERE >> "{children}"
</Text>,
heading2: (node, children, parent, styles) =>
<Text key={node.key} style={[styles.heading, styles.heading2]}>
>> H2 TEXT HERE >> "{children}"
</Text>,
heading3: (node, children, parent, styles) =>
<Text key={node.key} style={[styles.heading, styles.heading3]}>
>> H3 TEXT HERE >> "{children}"
</Text>,
};
const copy = `
# h1 Heading 8-)
## h2 Heading 8-)
### h3 Heading 8-)
| Option | Description |
| ------ | ----------- |
| data | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext | extension to be used for dest files. |
`;
const App: () => React.ReactNode = () => {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{height: '100%'}}
>
<Markdown
rules={rules}
>
{copy}
</Markdown>
</ScrollView>
</SafeAreaView>
</>
);
};
export default App;All rules and their associated styles:
| Render Rule | Style(s) |
| ------ | ----------- |
| body | body |
| heading1 | heading1 |
| heading2 | heading2 |
| heading3 | heading3 |
| heading4 | heading4 |
| heading5 | heading5 |
| heading6 | heading6 |
| hr | hr |
| strong | strong |
| em | em |
| s | s |
| blockquote | blockquote |
| bullet_list | bullet_list |
| ordered_list | ordered_list |
| list_item | list_item - This is a special case that contains a set of pseudo classes that don't align to the render rule: ordered_list_icon, ordered_list_content, bullet_list_icon, bullet_list_content |
| code_inline | code_inline |
| code_block | code_block |
| fence | fence |
| table | table |
| thead | thead |
| tbody | tbody |
| th | th |
| tr | tr |
| td | td |
| link | link |
| blocklink | blocklink |
| image | image |
| text | text |
| textgroup | textgroup |
| paragraph | paragraph |
| hardbreak | hardbreak |
| softbreak | softbreak |
| pre | pre |
| inline | inline |
| span | span |
Handling Links
Links, by default, will be handled with the import { Linking } from 'react-native'; import and Linking.openURL(url); call.
It is possible to overwrite this behaviour in one of two ways:
import React from 'react';
import { SafeAreaView, ScrollView, StatusBar } from 'react-native';
import Markdown from '@novastera-oss/react-native-markdown-display';
const copy = `[This is a link!](https://novastera.com)`;
const onLinkPress = (url) => {
if (url) {
// some custom logic
return false;
}
// return true to open with `Linking.openURL
// return false to handle it yourself
return true
}
const App: () => React.ReactNode = () => {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{height: '100%'}}
>
<Markdown
onLinkPress={onLinkPress}
>
{copy}
</Markdown>
</ScrollView>
</SafeAreaView>
</>
);
};
export default App;You will need to overwrite one or both of link and blocklink, the original definitions can be found here: https://github.com/novastera/react-native-markdown-display/blob/master/src/lib/renderRules.tsx
Something like this with yourCustomHandlerFunctionOrLogicHere:
import React from 'react';
import { SafeAreaView, ScrollView, StatusBar, Text } from 'react-native';
import Markdown from '@novastera-oss/react-native-markdown-display';
const copy = `[This is a link!](https://novastera.com)`;
const rules = {
link: (node, children, parent, styles) => {
return (
<Text key={node.key} style={styles.link} onPress={() => yourCustomHandlerFunctionOrLogicHere(node.attributes.href) }>
{children}
</Text>
);
},
};
const App: () => React.ReactNode = () => {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{height: '100%'}}
>
<Markdown
rules={rules}
>
{copy}
</Markdown>
</ScrollView>
</SafeAreaView>
</>
);
};
export default App;Disabling Specific Types of Markdown
You can disable any type of markdown you want, which is very useful in a mobile environment, by passing the markdownit property like below. Note that for convenience we also export the MarkdownIt instance so you don't have to include it as a project dependency directly just to remove some types of markdown.
This example will stop images and links.
import React from 'react';
import { SafeAreaView, ScrollView, StatusBar, Text } from 'react-native';
import Markdown, { MarkdownIt } from '@novastera-oss/react-native-markdown-display';
const copy = `
# This heading will show with formatting
[but this link will just](be displayed as this text)
`;
const App: () => React.ReactNode = () => {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{height: '100%'}}
>
<Markdown
markdownit={
MarkdownIt({typographer: true}).disable([ 'link', 'image' ])
}
>
{copy}
</Markdown>
</ScrollView>
</SafeAreaView>
</>
);
};
export default App;A full list of things you can turn off lives in the markdown-it CommonMark preset (the preset list may move across releases). You can also configure this renderer by supplying your own markdown-it instance; see the markdown-it docs for preset and rule options: https://github.com/markdown-it/markdown-it
Pre Processing
It is possible to need to pre-process the data outside of this library. As a result, you can pass an AST tree directly as the children like this:
import React from 'react';
import { SafeAreaView, ScrollView, StatusBar, Text } from 'react-native';
import Markdown, { MarkdownIt, tokensToAST, stringToTokens } from '@novastera-oss/react-native-markdown-display';
const markdownItInstance = MarkdownIt({typographer: true});
const copy = `
# Hello this is a title
This is some text with **BOLD!**
`;
const ast = tokensToAST(stringToTokens(copy, markdownItInstance))
const App: () => React.ReactNode = () => {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{height: '100%'}}
>
<Markdown
>
{ast}
</Markdown>
</ScrollView>
</SafeAreaView>
</>
);
};
export default App;Other Notes
See iamacup/react-native-markdown-display for the legacy fork lineage. This project removes unmaintained libraries and uses TypeScript.
About Novastera
This library is part of the Novastera CRM/ERP open-source ecosystem, a modern platform designed for the next generation of business applications. Novastera combines AI capabilities with comprehensive business management tools, enabling organizations to leverage on-device AI for enhanced productivity and data privacy.
Key Features of Novastera Platform
- Modern CRM/ERP system with AI-powered insights
- On-device AI with a privacy-first approach
- Mobile-first native iOS and Android apps built with React Native
- Open-source commitment to developer-friendly tooling
This library is currently used in Novastera’s mobile application, demonstrating its capabilities in production environments. We are committed to enabling privacy-preserving AI and building tools that respect user data.
Learn more about Novastera: https://novastera.com/resources
Acknowledgments
Complete rework by Hassan Allybocus.
Foundation and original authors:
- Mient-jan Stelling and Tom Pickard
- The react-native-markdown-renderer community
