├── package.json ├── LICENSE ├── README.md ├── Markdown.js └── rules.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-markdown", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/lwansbrough/react-native-markdown.git" 6 | }, 7 | "version": "0.1.1", 8 | "description": "A component for rendering Markdown in React Native", 9 | "main": "Markdown.js", 10 | "author": "Lochlan Wansbrough (http://lwansbrough.com)", 11 | "dependencies": { 12 | "lodash": "^3.6.0", 13 | "simple-markdown": "git://github.com/lwansbrough/simple-markdown.git" 14 | }, 15 | "peerDependencies": { 16 | "react-native": "*" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "native", 21 | "markdown", 22 | "md", 23 | "parse", 24 | "parser" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Loch Wansbrough 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-markdown 2 | 3 | A component for rendering Markdown in React Native. Pull requests welcome. 4 | 5 | ## Known issues 6 | 7 | - Due to [a bug](https://github.com/facebook/react-native/issues/824) in underlying layout engine for React Native ([facebook/css-layout](https://github.com/facebook/css-layout)), this module will put your application in an infinite loop unless you patch the upstream changes from `css-layout`'s' `Layout.c` and `Layout.h` files. 8 | 9 | ## Getting started 10 | 11 | 1. `npm install react-native-markdown --save` 12 | 13 | ## Usage 14 | 15 | All you need is to `require` the `react-native-markdown` module and then use the 16 | `` tag. 17 | 18 | ```javascript 19 | var React = require('react-native'); 20 | var { 21 | AppRegistry, 22 | StyleSheet, 23 | View 24 | } = React; 25 | var Markdown = require('react-native-markdown'); 26 | 27 | var mdApp = React.createClass({ 28 | render: function() { 29 | return ( 30 | 31 | 32 | Some *really* basic **Markdown**. 33 | {'\n\n'} 34 | | # | Name | Age |{'\n'} 35 | |---|--------|-----|{'\n'} 36 | | 1 | John | 19 |{'\n'} 37 | | 2 | Sally | 18 |{'\n'} 38 | | 3 | Stream | 20 |{'\n'} 39 | 40 | 41 | ); 42 | } 43 | }); 44 | 45 | AppRegistry.registerComponent('mdApp', () => mdApp); 46 | ``` 47 | 48 | ## Properties 49 | 50 | #### `style` 51 | 52 | Default style properties will be applied to the markdown. You will likely want to customize these styles, the following properties can be used to modify the rendered elements: 53 | 54 | *Note: The text inside the parentheses denotes the element type.* 55 | 56 | - `autolink` (``) - WIP 57 | - `blockQuote` (``) - WIP 58 | - `br` (``) 59 | - `codeBlock` (``) - WIP 60 | - `del` (``) 61 | - `em` (``) 62 | - `heading` (``) - Also `heading1` through `heading6` 63 | - `hr` (``) 64 | - `image` (``) - Implemented but size is fixed to `50x50` until auto width is supported by React Native. 65 | - `inlineCode` (``) 66 | - `link` (``) - WIP 67 | - `list` (``) - Also `listItem` (``), `listItemBullet` (``) and `listItemNumber` (``) 68 | - `mailto` (``) - WIP 69 | - `newline` (``) - WIP 70 | - `paragraph` (``) 71 | - `plainText` (``) - Use for styling text without any associated styles 72 | - `strong` (``) 73 | - `table` (``) 74 | - `tableHeader` (``) 75 | - `tableHeaderCell` (``) 76 | - `tableRow` (``) 77 | - `tableRowCell` (``) 78 | - `tableRowLast` (``, inherits from `tableRow`) 79 | - `text` (``) - Inherited by all text based elements 80 | - `u` (``) 81 | - `url` (``) 82 | - `view` (``) - This is the container `View` that the Markdown is rendered in. 83 | -------------------------------------------------------------------------------- /Markdown.js: -------------------------------------------------------------------------------- 1 | var React = require('react-native'); 2 | var { 3 | View 4 | } = React; 5 | var _ = require('lodash'); 6 | var SimpleMarkdown = require('simple-markdown'); 7 | 8 | var styles = { 9 | view: { 10 | }, 11 | codeBlock: { 12 | fontFamily: 'Courier', 13 | fontWeight: '500' 14 | }, 15 | del: { 16 | containerBackgroundColor: '#222222' 17 | }, 18 | em: { 19 | fontStyle: 'italic' 20 | }, 21 | heading: { 22 | fontWeight: '200' 23 | }, 24 | heading1: { 25 | fontSize: 32 26 | }, 27 | heading2: { 28 | fontSize: 24 29 | }, 30 | heading3: { 31 | fontSize: 18 32 | }, 33 | heading4: { 34 | fontSize: 16 35 | }, 36 | heading5: { 37 | fontSize: 13 38 | }, 39 | heading6: { 40 | fontSize: 11 41 | }, 42 | hr: { 43 | backgroundColor: '#cccccc', 44 | height: 1 45 | }, 46 | image: { 47 | height: 50, // TODO: React Native needs to support auto image size 48 | width: 50 // TODO: React Native needs to support auto image size 49 | }, 50 | inlineCode: { 51 | backgroundColor: '#eeeeee', 52 | borderColor: '#dddddd', 53 | borderRadius: 3, 54 | borderWidth: 1, 55 | fontFamily: 'Courier', 56 | fontWeight: 'bold' 57 | }, 58 | list: { 59 | 60 | }, 61 | listItem: { 62 | flexDirection: 'row' 63 | }, 64 | listItemBullet: { 65 | fontSize: 20, 66 | lineHeight: 20 67 | }, 68 | listItemNumber: { 69 | fontWeight: 'bold' 70 | }, 71 | paragraph: { 72 | marginTop: 10, 73 | marginBottom: 10, 74 | flexWrap: 'wrap', 75 | flexDirection: 'row', 76 | alignItems: 'flex-start', 77 | justifyContent: 'flex-start' 78 | }, 79 | strong: { 80 | fontWeight: 'bold' 81 | }, 82 | table: { 83 | borderWidth: 1, 84 | borderColor: '#222222', 85 | borderRadius: 3 86 | }, 87 | tableHeader: { 88 | backgroundColor: '#222222', 89 | flexDirection: 'row', 90 | justifyContent: 'space-around' 91 | }, 92 | tableHeaderCell: { 93 | color: '#ffffff', 94 | fontWeight: 'bold', 95 | padding: 5 96 | }, 97 | tableRow: { 98 | borderBottomWidth: 1, 99 | borderColor: '#222222', 100 | flexDirection: 'row', 101 | justifyContent: 'space-around' 102 | }, 103 | tableRowLast: { 104 | borderColor: 'transparent' 105 | }, 106 | tableRowCell: { 107 | padding: 5 108 | }, 109 | text: { 110 | color: '#222222' 111 | }, 112 | u: { 113 | borderColor: '#222222', 114 | borderBottomWidth: 1 115 | } 116 | }; 117 | 118 | 119 | var Markdown = React.createClass({ 120 | 121 | getDefaultProps: function() { 122 | return { 123 | style: styles 124 | }; 125 | }, 126 | 127 | componentWillMount: function() { 128 | var mergedStyles = _.merge({}, styles, this.props.style); 129 | var rules = require('./rules')(mergedStyles); 130 | rules = _.merge({}, SimpleMarkdown.defaultRules, rules); 131 | 132 | var parser = SimpleMarkdown.parserFor(rules); 133 | this.parse = function(source) { 134 | var blockSource = source + '\n\n'; 135 | return parser(blockSource, {inline: false}); 136 | }; 137 | this.renderer = SimpleMarkdown.reactFor(SimpleMarkdown.ruleOutput(rules, 'react')); 138 | }, 139 | 140 | render: function() { 141 | 142 | var child = _.isArray(this.props.children) 143 | ? this.props.children.join('') : this.props.children; 144 | var tree = this.parse(child); 145 | return {this.renderer(tree)}; 146 | } 147 | }); 148 | 149 | module.exports = Markdown; 150 | -------------------------------------------------------------------------------- /rules.js: -------------------------------------------------------------------------------- 1 | var React = require('react-native'); 2 | var { 3 | Image, 4 | Text, 5 | View, 6 | } = React; 7 | var SimpleMarkdown = require('simple-markdown'); 8 | var _ = require('lodash'); 9 | 10 | module.exports = function(styles) { 11 | return { 12 | autolink: { 13 | react: function(node, output, state) { 14 | state.withinText = true; 15 | return React.createElement(Text, { 16 | key: state.key, 17 | style: styles.autolink, 18 | onPress: _.noop 19 | }, output(node.content, state)); 20 | } 21 | }, 22 | blockQuote: { 23 | react: function(node, output, state) { 24 | state.withinText = true; 25 | return React.createElement(Text, { 26 | key: state.key, 27 | style: styles.blockQuote 28 | }, output(node.content, state)); 29 | } 30 | }, 31 | br: { 32 | react: function(node, output, state) { 33 | return React.createElement(Text, { 34 | key: state.key, 35 | style: styles.br 36 | }, '\n\n'); 37 | } 38 | }, 39 | codeBlock: { 40 | react: function(node, output, state) { 41 | state.withinText = true; 42 | return React.createElement(Text, { 43 | key: state.key, 44 | style: styles.codeBlock 45 | }, null); 46 | } 47 | }, 48 | del: { 49 | react: function(node, output, state) { 50 | state.withinText = true; 51 | return React.createElement(Text, { 52 | key: state.key, 53 | style: styles.del 54 | }, output(node.content, state)); 55 | } 56 | }, 57 | em: { 58 | react: function(node, output, state) { 59 | state.withinText = true; 60 | return React.createElement(Text, { 61 | key: state.key, 62 | style: styles.em 63 | }, output(node.content, state)); 64 | } 65 | }, 66 | heading: { 67 | react: function(node, output, state) { 68 | state.withinText = true; 69 | return React.createElement(Text, { 70 | key: state.key, 71 | style: [styles.heading, styles['heading' + node.level]] 72 | }, output(node.content, state)); 73 | } 74 | }, 75 | hr: { 76 | react: function(node, output, state) { 77 | return React.createElement(View, { key: state.key, style: styles.hr }); 78 | } 79 | }, 80 | image: { 81 | react: function(node, output, state) { 82 | return React.createElement(Image, { 83 | key: state.key, 84 | source: { uri: node.target }, 85 | style: styles.image 86 | }); 87 | } 88 | }, 89 | inlineCode: { 90 | react: function(node, output, state) { 91 | state.withinText = true; 92 | return React.createElement(Text, { 93 | key: state.key, 94 | style: styles.inlineCode 95 | }, output(node.content, state)); 96 | } 97 | }, 98 | link: { 99 | react: function(node, output, state) { 100 | state.withinText = true; 101 | return React.createElement(Text, { 102 | key: state.key, 103 | style: styles.autolink 104 | }, output(node.content, state)); 105 | } 106 | }, 107 | list: { 108 | react: function(node, output, state) { 109 | 110 | var items = _.map(node.items, function(item, i) { 111 | var bullet; 112 | if (node.ordered) { 113 | bullet = React.createElement(Text, { style: styles.listItemNumber }, (i + 1) + '. '); 114 | } 115 | else { 116 | bullet = React.createElement(Text, { style: styles.listItemBullet }, '\u2022 '); 117 | } 118 | return React.createElement(View, { 119 | key: i, 120 | style: styles.listItem 121 | }, [bullet, output(item, state)]); 122 | }); 123 | 124 | return React.createElement(View, { key: state.key, style: styles.list }, items); 125 | } 126 | }, 127 | mailto: { 128 | react: function(node, output, state) { 129 | state.withinText = true; 130 | return React.createElement(Text, { 131 | key: state.key, 132 | style: styles.mailto, 133 | onPress: _.noop 134 | }, output(node.content, state)); 135 | } 136 | }, 137 | newline: { 138 | react: function(node, output, state) { 139 | return React.createElement(Text, { 140 | key: state.key, 141 | style: styles.newline 142 | }, '\n'); 143 | } 144 | }, 145 | paragraph: { 146 | react: function(node, output, state) { 147 | return React.createElement(View, { 148 | key: state.key, 149 | style: styles.paragraph 150 | }, output(node.content, state)); 151 | } 152 | }, 153 | strong: { 154 | react: function(node, output, state) { 155 | state.withinText = true; 156 | return React.createElement(Text, { 157 | key: state.key, 158 | style: styles.strong 159 | }, output(node.content, state)); 160 | } 161 | }, 162 | table: { 163 | react: function(node, output, state) { 164 | var headers = _.map(node.header, function(content, i) { 165 | return React.createElement(Text, { 166 | style: styles.tableHeaderCell 167 | }, output(content, state)); 168 | }); 169 | 170 | var header = React.createElement(View, { style: styles.tableHeader }, headers); 171 | 172 | var rows = _.map(node.cells, function(row, r) { 173 | var cells = _.map(row, function(content, c) { 174 | return React.createElement(View, { 175 | key: c, 176 | style: styles.tableRowCell 177 | }, output(content, state)); 178 | }); 179 | var rowStyles = [styles.tableRow]; 180 | if (node.cells.length - 1 == r) { 181 | rowStyles.push(styles.tableRowLast); 182 | } 183 | return React.createElement(View, { key: r, style: rowStyles }, cells); 184 | }); 185 | 186 | return React.createElement(View, { key: state.key, style: styles.table }, [ header, rows ]); 187 | } 188 | }, 189 | text: { 190 | react: function(node, output, state) { 191 | // Breaking words up in order to allow for text reflowing in flexbox 192 | var words = node.content.split(' '); 193 | words = _.map(words, function(word, i) { 194 | var elements = []; 195 | if (i != words.length - 1) { 196 | word = word + ' '; 197 | } 198 | var textStyles = [styles.text]; 199 | if (!state.withinText) { 200 | textStyles.push(styles.plainText); 201 | } 202 | return React.createElement(Text, { 203 | style: textStyles 204 | }, word); 205 | }); 206 | return words; 207 | } 208 | }, 209 | u: { 210 | react: function(node, output, state) { 211 | state.withinText = true; 212 | return React.createElement(View, { 213 | key: state.key, 214 | style: styles.u 215 | }, output(node.content, state)); 216 | } 217 | }, 218 | url: { 219 | react: function(node, output, state) { 220 | state.withinText = true; 221 | return React.createElement(Text, { 222 | key: state.key, 223 | style: styles.url, 224 | onPress: _.noop 225 | }, output(node.content, state)); 226 | } 227 | } 228 | } 229 | }; 230 | --------------------------------------------------------------------------------