├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── rules.js └── styles.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | /node_modules/ 4 | /npm-debug.log 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm downloads](https://img.shields.io/npm/dm/react-native-markdown-package)](https://www.npmjs.com/package/react-native-markdown-package) [![npm_downloads](https://img.shields.io/npm/dt/react-native-markdown-package)](https://www.npmjs.com/package/react-native-markdown-package) 2 | 3 | # React Native Markdown Package 4 | React Native Markdown Package is a library for implementing markdown syntax in React Native. 5 | 6 | ## Getting started 7 | 8 | To install this library, you can easily run this command from your project folder. 9 | 10 |    `npm i react-native-markdown-package --save` 11 | 12 | 13 | Check this simple app for implementation example [Example app](https://github.com/andangrd/rn-markdown-example) 14 | 15 | 16 | 17 | ## How to use 18 | 19 | What you need to do is `import` the `react-native-markdown-package` module and then use the 20 | `` tag. 21 | 22 | How to use? 23 | 24 | Here we are, take a look at this simple implementation: 25 | 26 | ```javascript 27 | /** 28 | * Sample React Native App 29 | * https://github.com/facebook/react-native 30 | * 31 | * @format 32 | * @flow 33 | */ 34 | 35 | import React, {Component} from 'react'; 36 | import { 37 | StyleSheet, 38 | ScrollView, 39 | View, 40 | Text, 41 | Linking 42 | } from 'react-native'; 43 | import { 44 | Colors, 45 | } from 'react-native/Libraries/NewAppScreen'; 46 | import Markdown from 'react-native-markdown-package'; 47 | 48 | const text = ` 49 | # This is Heading 1 50 | ## This is Heading 2 51 | 1. List1 52 | 2. List2 53 | This is a \`description\` for List2 .\n 54 | * test 55 | * test 56 | 3. List3 57 | 4. List4. 58 | 59 | 60 | You can also put some url as a link [like This](https://www.google.com) or write it as a plain text: 61 | https://www.google.com 62 | 63 | 64 | --- 65 | 66 | This text should be printed between horizontal rules 67 | 68 | --- 69 | 70 | The following code is an example for codeblock: 71 | 72 | const a = function() { 73 | runSomeFunction() 74 | }; 75 | 76 | Below is some example to print blockquote 77 | 78 | > Test block Quote 79 | > Another block Quote 80 | 81 | this is _italic_ 82 | this is **strong** 83 | Some *really* ~~basic~~ **Markdown**. 84 | 85 | 86 | | # | Name | Age 87 | |---|--------|-----| 88 | | 1 | John | 19 | 89 | | 2 | Sally | 18 | 90 | | 3 | Stream | 20 | 91 | 92 | 93 | this is an example for adding picture: 94 | 95 | ![Screen Shot 2019-10-05 at 3 19 33 AM](https://user-images.githubusercontent.com/26213148/66237659-d11f4280-e71f-11e9-91e3-7a3f08659d89.png) 96 | 97 | 98 | `; 99 | 100 | export default class App extends Component<{}> { 101 | render() { 102 | return ( 103 | 106 | 107 | 108 | Welcome to React Native! 109 | 110 | Linking.openURL(url)} 113 | > 114 | { text } 115 | 116 | 119 | this is a test single line md 120 | 121 | 122 | 123 | ); 124 | } 125 | } 126 | const singleStyle = { 127 | text: { 128 | color: 'blue', 129 | textAlign: "right" 130 | }, 131 | view: { 132 | alignSelf: 'stretch', 133 | } 134 | }; 135 | 136 | const markdownStyle = { 137 | singleLineMd: { 138 | text: { 139 | color: 'blue', 140 | textAlign: "right" 141 | }, 142 | view: { 143 | alignSelf: 'stretch', 144 | } 145 | }, 146 | collectiveMd: { 147 | heading1: { 148 | color: 'red' 149 | }, 150 | heading2: { 151 | color: 'green', 152 | textAlign: "right" 153 | }, 154 | strong: { 155 | color: 'blue' 156 | }, 157 | em: { 158 | color: 'cyan' 159 | }, 160 | text: { 161 | color: 'black', 162 | }, 163 | blockQuoteText: { 164 | color: 'grey' 165 | }, 166 | blockQuoteSection: { 167 | flexDirection: 'row', 168 | }, 169 | blockQuoteSectionBar: { 170 | width: 3, 171 | height: null, 172 | backgroundColor: '#DDDDDD', 173 | marginRight: 15, 174 | }, 175 | codeBlock: { 176 | fontFamily: 'Courier', 177 | fontWeight: '500', 178 | backgroundColor: '#DDDDDD', 179 | }, 180 | tableHeader: { 181 | backgroundColor: 'grey', 182 | }, 183 | } 184 | }); 185 | 186 | const styles = StyleSheet.create({ 187 | container: { 188 | flex: 1, 189 | justifyContent: 'center', 190 | alignItems: 'center', 191 | backgroundColor: '#F5FCFF', 192 | margin: 10, 193 | padding:20 194 | }, 195 | scrollView: { 196 | backgroundColor: Colors.lighter, 197 | }, 198 | welcome: { 199 | fontSize: 20, 200 | textAlign: 'center', 201 | }, 202 | instructions: { 203 | textAlign: 'center', 204 | color: '#333333', 205 | marginBottom: 5, 206 | } 207 | }); 208 | 209 | ``` 210 | 211 | ## Properties 212 | 213 | #### `styles` 214 | 215 | Default style properties will be applied to the markdown. You could replace it with your preference by adding `styles` property like the example above. 216 | 217 | #### `onLink` 218 | 219 | This prop will accept a function. This is a callback function for any link inside markdown syntax, so you could costumize the handler for onClick event from the link. 220 | 221 | `onLinkCallback` should be a function that returns a promise. 222 | 223 | 224 | ``` 225 | 226 | const onLinkCallback = (url) => { 227 | console.log('test test test'); 228 | 229 | const isErrorResult = false; 230 | 231 | return new Promise((resolve, reject) => { 232 | isErrorResult ? reject() : resolve(); 233 | }); 234 | }; 235 | 236 | ... 237 | 238 | 241 | {text} 242 | 243 | 244 | ... 245 | 246 | 247 | ``` 248 | 249 | 250 | *NOTE :* 251 | _Email link (mailto) could be tested on real device only, it won't be able to test on Simulator as discuss in this [StackOverflow](https://stackoverflow.com/questions/44769710/opneurl-react-native-linking-call-mailto)_ 252 | 253 | ## Thanks To 254 | 255 | thanks to all contributors who help me to make this libary better 256 | 257 | This project was actually forked from [lwansbrough](https://github.com/lwansbrough) , with some enhancements below : 258 | 1. Styling method. 259 | 260 | Now you can easily add styling on each syntax, e.g. add different color either in `strong`, `header`, or another md syntax. All default styles in this package is also already moved to new file `styles.js`. 261 | 2. Refactoring some codes to adopt ES6 style. 262 | 263 | Refactor index.js using ES6. :) 264 | 3. Support `Sublist`. 265 | 266 | In the previous library, you couldn't add sublist. It was not supported. But now, this feature already added here. Please follow the instruction above... 267 | 4. Latest release: 268 | 269 | * add Proptypes Support, (1.0.1) 270 | 271 | * Fix deprecated View.proptypes and update Readme (1.0.3) 272 | 273 | * Upgrade dependency, lodash, avoid vulnerabilities (1.1.0) 274 | 275 | * Fix performance issue, import only necessarry function from lodash (1.1.1) 276 | 277 | * Finalize Blockquote feature (1.2.0) 278 | 279 | * Update Docs (1.2.1) 280 | 281 | * Allow user to include plain text from variable using back tick (1.3.3) 282 | 283 | * New feature, codeblock (1.4.0) 284 | 285 | * New feature, on link handler (1.4.3) 286 | 287 | * Bug fix, Strike through issue (1.4.4) 288 | 289 | * Default Style for outer View, remove deprecated ComponentWillMount (1.5.0) 290 | 291 | * Allow user to replace default rules, update default font family for `codeBlock` on android [(v1.6.0)](https://github.com/andangrd/react-native-markdown-package/releases/tag/v1.6.0) 292 | 293 | * Update to use latest simple-markdown [(v1.7.0)](https://github.com/andangrd/react-native-markdown-package/releases/tag/v1.7.0) 294 | 295 | * Update to use latest simple-markdown [(v1.8.0)](https://github.com/andangrd/react-native-markdown-package/releases/tag/v1.8.0) 296 | 297 | * Remove deprecated `prop-types` from list of dependencies [(v1.8.2)](https://github.com/andangrd/react-native-markdown-package/releases/tag/v1.8.2) 298 | 299 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {View} from 'react-native'; 3 | import {merge, isEqual, isArray} from 'lodash'; 4 | import SimpleMarkdown from 'simple-markdown'; 5 | import styles from './styles'; 6 | 7 | class Markdown extends Component { 8 | constructor(props) { 9 | super(props); 10 | if (props.enableLightBox && !props.navigator) { 11 | throw new Error('props.navigator must be specified when enabling lightbox'); 12 | } 13 | 14 | const opts = { 15 | enableLightBox: props.enableLightBox, 16 | navigator: props.navigator, 17 | imageParam: props.imageParam, 18 | onLink: props.onLink, 19 | bgImage: props.bgImage, 20 | onImageOpen: props.onImageOpen, 21 | onImageClose: props.onImageClose, 22 | rules: props.rules 23 | }; 24 | 25 | const mergedStyles = merge({}, styles, props.styles); 26 | var rules = require('./rules')(mergedStyles, opts); 27 | rules = merge({}, SimpleMarkdown.defaultRules, rules, opts.rules); 28 | 29 | const parser = SimpleMarkdown.parserFor(rules); 30 | this.parse = function (source) { 31 | const blockSource = source + '\n\n'; 32 | return parser(blockSource, {inline: false}); 33 | }; 34 | this.renderer = SimpleMarkdown.outputFor(rules, 'react'); 35 | } 36 | 37 | componentDidMount() { 38 | if (this.props.onLoad) { 39 | this.props.onLoad(); 40 | } 41 | } 42 | 43 | shouldComponentUpdate(nextProps, nextState) { 44 | return !isEqual(nextProps.children, this.props.children); 45 | } 46 | 47 | render() { 48 | const child = isArray(this.props.children) 49 | ? this.props.children.join('') 50 | : this.props.children; 51 | 52 | const tree = this.parse(child); 53 | 54 | return {this.renderer(tree)} 55 | } 56 | } 57 | 58 | export default Markdown; 59 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-markdown-package", 3 | "version": "1.8.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/prop-types": { 8 | "version": "15.7.5", 9 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", 10 | "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" 11 | }, 12 | "@types/react": { 13 | "version": "18.0.15", 14 | "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.15.tgz", 15 | "integrity": "sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==", 16 | "requires": { 17 | "@types/prop-types": "*", 18 | "@types/scheduler": "*", 19 | "csstype": "^3.0.2" 20 | } 21 | }, 22 | "@types/scheduler": { 23 | "version": "0.16.2", 24 | "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", 25 | "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" 26 | }, 27 | "csstype": { 28 | "version": "3.1.0", 29 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", 30 | "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" 31 | }, 32 | "js-tokens": { 33 | "version": "4.0.0", 34 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 35 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 36 | }, 37 | "lodash": { 38 | "version": "4.17.21", 39 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 40 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 41 | }, 42 | "loose-envify": { 43 | "version": "1.4.0", 44 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 45 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 46 | "requires": { 47 | "js-tokens": "^3.0.0 || ^4.0.0" 48 | } 49 | }, 50 | "object-assign": { 51 | "version": "4.1.1", 52 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 53 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" 54 | }, 55 | "prop-types": { 56 | "version": "15.8.1", 57 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", 58 | "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", 59 | "requires": { 60 | "loose-envify": "^1.4.0", 61 | "object-assign": "^4.1.1", 62 | "react-is": "^16.13.1" 63 | } 64 | }, 65 | "react-is": { 66 | "version": "16.13.1", 67 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", 68 | "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" 69 | }, 70 | "react-native-lightbox": { 71 | "version": "0.7.0", 72 | "resolved": "https://registry.npmjs.org/react-native-lightbox/-/react-native-lightbox-0.7.0.tgz", 73 | "integrity": "sha512-HS3T4WlCd0Gb3us2d6Jse5m6KjNhngnKm35Wapq30WtQa9s+/VMmtuktbGPGaWtswcDyOj6qByeJBw9W80iPCA==", 74 | "requires": { 75 | "prop-types": "^15.5.10" 76 | } 77 | }, 78 | "simple-markdown": { 79 | "version": "0.7.3", 80 | "resolved": "https://registry.npmjs.org/simple-markdown/-/simple-markdown-0.7.3.tgz", 81 | "integrity": "sha512-uGXIc13NGpqfPeFJIt/7SHHxd6HekEJYtsdoCM06mEBPL9fQH/pSD7LRM6PZ7CKchpSvxKL4tvwMamqAaNDAyg==", 82 | "requires": { 83 | "@types/react": ">=16.0.0" 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-markdown-package", 3 | "version": "1.8.2", 4 | "description": "Package for implementing markdown syntax in React Native", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies": { 10 | "lodash": "^4.17.15", 11 | "react-native-lightbox": "^0.7.0", 12 | "simple-markdown": "^0.7.1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/andangrd/react-native-markdown-package.git" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "native", 21 | "markdown", 22 | "md", 23 | "reactnative" 24 | ], 25 | "author": "andangrd", 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/andangrd/react-native-markdown-package/issues" 29 | }, 30 | "homepage": "https://github.com/andangrd/react-native-markdown-package#readme" 31 | } 32 | -------------------------------------------------------------------------------- /rules.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Image, 5 | Text, 6 | View, 7 | } from 'react-native'; 8 | 9 | import Lightbox from 'react-native-lightbox'; 10 | 11 | import SimpleMarkdown from 'simple-markdown'; 12 | import {map, includes, head, noop, some, size} from 'lodash'; 13 | 14 | module.exports = function (styles, opts = {}) { 15 | const enableLightBox = opts.enableLightBox || false; 16 | const navigator = opts.navigator; 17 | 18 | const LINK_INSIDE = '(?:\\[[^\\]]*\\]|[^\\]]|\\](?=[^\\[]*\\]))*'; 19 | const LINK_HREF_AND_TITLE = 20 | "\\s*?(?:\\s+['\"]([\\s\\S]*?)['\"])?\\s*"; 21 | var pressHandler = function (target) { 22 | if (opts.onLink) { 23 | opts.onLink(target).catch(function (error) { 24 | console.log('There has been a problem with this action. ' + error.message); 25 | throw error; 26 | }); 27 | } 28 | }; 29 | var parseInline = function (parse, content, state) { 30 | var isCurrentlyInline = state.inline || false; 31 | state.inline = true; 32 | var result = parse(content, state); 33 | state.inline = isCurrentlyInline; 34 | return result; 35 | }; 36 | var parseCaptureInline = function (capture, parse, state) { 37 | return { 38 | content: parseInline(parse, capture[2], state) 39 | }; 40 | }; 41 | return { 42 | autolink: { 43 | react: function (node, output, {...state}) { 44 | state.withinText = true; 45 | const _pressHandler = () => { 46 | pressHandler(node.target); 47 | }; 48 | return React.createElement(Text, { 49 | key: state.key, 50 | style: styles.autolink, 51 | onPress: _pressHandler, 52 | }, output(node.content, state)); 53 | }, 54 | }, 55 | blockQuote: { 56 | react: function (node, output, {...state}) { 57 | state.withinQuote = true; 58 | 59 | const img = React.createElement(View, { 60 | key: state.key - state.key, 61 | style: [styles.blockQuoteSectionBar, styles.blockQuoteBar], 62 | }); 63 | 64 | let blockQuote = React.createElement(Text, { 65 | key: state.key, 66 | style: styles.blockQuoteText, 67 | }, output(node.content, state)); 68 | 69 | 70 | return React.createElement(View, { 71 | key: state.key, 72 | style: [styles.blockQuoteSection, styles.blockQuoteText], 73 | }, [img, blockQuote]); 74 | 75 | }, 76 | }, 77 | br: { 78 | react: function (node, output, {...state}) { 79 | return React.createElement(Text, { 80 | key: state.key, 81 | style: styles.br, 82 | }, '\n\n'); 83 | }, 84 | }, 85 | codeBlock: { 86 | react: function (node, output, {...state}) { 87 | state.withinText = true; 88 | return React.createElement(Text, { 89 | key: state.key, 90 | style: styles.codeBlock, 91 | }, node.content); 92 | }, 93 | }, 94 | del: { 95 | react: function (node, output, {...state}) { 96 | state.withinText = true; 97 | return React.createElement(Text, { 98 | key: state.key, 99 | style: styles.del, 100 | }, output(node.content, state)); 101 | }, 102 | }, 103 | em: { 104 | react: function (node, output, {...state}) { 105 | state.withinText = true; 106 | state.style = { 107 | ...(state.style || {}), 108 | ...styles.em 109 | }; 110 | return React.createElement(Text, { 111 | key: state.key, 112 | style: styles.em, 113 | }, output(node.content, state)); 114 | }, 115 | }, 116 | heading: { 117 | match: SimpleMarkdown.blockRegex(/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n *)+/), 118 | react: function (node, output, {...state}) { 119 | // const newState = {...state}; 120 | state.withinText = true; 121 | state.withinHeading = true; 122 | 123 | state.style = { 124 | ...(state.style || {}), 125 | ...styles['heading' + node.level] 126 | }; 127 | 128 | const ret = React.createElement(Text, { 129 | key: state.key, 130 | style: state.style, 131 | }, output(node.content, state)); 132 | return ret; 133 | }, 134 | }, 135 | hr: { 136 | react: function (node, output, {...state}) { 137 | return React.createElement(View, {key: state.key, style: styles.hr}); 138 | }, 139 | }, 140 | image: { 141 | react: function (node, output, {...state}) { 142 | var imageParam = opts.imageParam ? opts.imageParam : ''; 143 | var target = node.target + imageParam; 144 | var image = React.createElement(Image, { 145 | key: state.key, 146 | // resizeMode: 'contain', 147 | source: {uri: target}, 148 | style: styles.image, 149 | }); 150 | if (enableLightBox) { 151 | return React.createElement(Lightbox, { 152 | activeProps: styles.imageBox, 153 | key: state.key, 154 | navigator, 155 | onOpen: opts.onImageOpen, 156 | onClose: opts.onImageClose, 157 | }, image); 158 | } 159 | return image; 160 | }, 161 | }, 162 | inlineCode: { 163 | parse: parseCaptureInline, 164 | react: function (node, output, {...state}) { 165 | state.withinText = true; 166 | return React.createElement(Text, { 167 | key: state.key, 168 | style: styles.inlineCode, 169 | }, output(node.content, state)); 170 | }, 171 | }, 172 | link: { 173 | match: SimpleMarkdown.inlineRegex(new RegExp( 174 | '^\\[(' + LINK_INSIDE + ')\\]\\(' + LINK_HREF_AND_TITLE + '\\)' 175 | )), 176 | react: function (node, output, {...state}) { 177 | state.withinLink = true; 178 | const _pressHandler = () => { 179 | pressHandler(node.target); 180 | }; 181 | const link = React.createElement(Text, { 182 | key: state.key, 183 | style: styles.autolink, 184 | onPress: _pressHandler, 185 | }, output(node.content, state)); 186 | state.withinLink = false; 187 | return link; 188 | }, 189 | }, 190 | list: { 191 | react: function (node, output, {...state}) { 192 | var numberIndex = 1; 193 | var items = map(node.items, function (item, i) { 194 | var bullet; 195 | state.withinList = false; 196 | 197 | if (node.ordered) { 198 | bullet = React.createElement(Text, {key: 0, style: [styles.text, styles.listItemNumber]}, (numberIndex) + '. '); 199 | } 200 | else { 201 | bullet = React.createElement(Text, {key: 0, style: [styles.text, styles.listItemBullet]}, '\u2022 '); 202 | } 203 | 204 | if (item.length > 1) { 205 | if (item[1].type == 'list') { 206 | state.withinList = true; 207 | } 208 | } 209 | 210 | 211 | 212 | var content = output(item, state); 213 | var listItem; 214 | if (includes(['text', 'paragraph', 'strong'], (head(item) || {}).type) && state.withinList == false) { 215 | state.withinList = true; 216 | listItem = React.createElement(Text, { 217 | style: [styles.listItemText, {marginBottom: 0}], 218 | key: 1, 219 | }, content); 220 | } else { 221 | listItem = React.createElement(View, { 222 | style: styles.listItemText, 223 | key: 1, 224 | }, content); 225 | } 226 | state.withinList = false; 227 | numberIndex++; 228 | 229 | return React.createElement(View, { 230 | key: i, 231 | style: styles.listRow, 232 | }, [bullet, listItem]); 233 | }); 234 | 235 | return React.createElement(View, {key: state.key, style: styles.list}, items); 236 | }, 237 | }, 238 | sublist: { 239 | react: function (node, output, {...state}) { 240 | 241 | var items = map(node.items, function (item, i) { 242 | var bullet; 243 | if (node.ordered) { 244 | bullet = React.createElement(Text, {key: 0, style: [styles.text, styles.listItemNumber]}, (i + 1) + '. '); 245 | } 246 | else { 247 | bullet = React.createElement(Text, {key: 0, style: [styles.text, styles.listItemBullet]}, '\u2022 '); 248 | } 249 | 250 | var content = output(item, state); 251 | var listItem; 252 | state.withinList = true; 253 | if (includes(['text', 'paragraph', 'strong'], (head(item) || {}).type)) { 254 | listItem = React.createElement(Text, { 255 | style: styles.listItemText, 256 | key: 1, 257 | }, content); 258 | } else { 259 | listItem = React.createElement(View, { 260 | style: styles.listItem, 261 | key: 1, 262 | }, content); 263 | } 264 | state.withinList = false; 265 | return React.createElement(View, { 266 | key: i, 267 | style: styles.listRow, 268 | }, [bullet, listItem]); 269 | }); 270 | 271 | return React.createElement(View, {key: state.key, style: styles.sublist}, items); 272 | }, 273 | }, 274 | mailto: { 275 | react: function (node, output, {...state}) { 276 | state.withinText = true; 277 | return React.createElement(Text, { 278 | key: state.key, 279 | style: styles.mailto, 280 | onPress: noop, 281 | }, output(node.content, state)); 282 | }, 283 | }, 284 | newline: { 285 | react: function (node, output, {...state}) { 286 | return React.createElement(Text, { 287 | key: state.key, 288 | style: styles.newline, 289 | }, '\n'); 290 | }, 291 | }, 292 | paragraph: { 293 | react: function (node, output, {...state}) { 294 | let paragraphStyle = styles.paragraph; 295 | // Allow image to drop in next line within the paragraph 296 | if (some(node.content, {type: 'image'})) { 297 | state.withinParagraphWithImage = true; 298 | var paragraph = React.createElement(View, { 299 | key: state.key, 300 | style: styles.paragraphWithImage, 301 | }, output(node.content, state)); 302 | state.withinParagraphWithImage = false; 303 | return paragraph; 304 | } else if (size(node.content) < 3 && some(node.content, {type: 'strong'})) { 305 | // align to center for Strong only content 306 | // require a check of content array size below 3, 307 | // as parse will include additional space as `text` 308 | paragraphStyle = styles.paragraphCenter; 309 | } 310 | if (state.withinList) { 311 | paragraphStyle = [paragraphStyle, styles.noMargin]; 312 | } 313 | return React.createElement(Text, { 314 | key: state.key, 315 | style: paragraphStyle, 316 | }, output(node.content, state)); 317 | }, 318 | }, 319 | strong: { 320 | react: function (node, output, {...state}) { 321 | state.withinText = true; 322 | state.style = { 323 | ...(state.style || {}), 324 | ...styles.strong 325 | }; 326 | return React.createElement(Text, { 327 | key: state.key, 328 | style: state.style, 329 | }, output(node.content, state)); 330 | }, 331 | }, 332 | table: { 333 | react: function (node, output, {...state}) { 334 | var headers = map(node.header, function (content, i) { 335 | return React.createElement(Text, { 336 | key: i, 337 | style: styles.tableHeaderCell, 338 | }, output(content, state)); 339 | }); 340 | 341 | var header = React.createElement(View, {key: -1, style: styles.tableHeader}, headers); 342 | 343 | var rows = map(node.cells, function (row, r) { 344 | var cells = map(row, function (content, c) { 345 | return React.createElement(View, { 346 | key: c, 347 | style: styles.tableRowCell, 348 | }, output(content, state)); 349 | }); 350 | var rowStyles = [styles.tableRow]; 351 | if (node.cells.length - 1 == r) { 352 | rowStyles.push(styles.tableRowLast); 353 | } 354 | return React.createElement(View, {key: r, style: rowStyles}, cells); 355 | }); 356 | 357 | return React.createElement(View, {key: state.key, style: styles.table}, [header, rows]); 358 | }, 359 | }, 360 | text: { 361 | react: function (node, output, {...state}) { 362 | let textStyle = { 363 | ...styles.text, 364 | ...(state.style || {}) 365 | }; 366 | 367 | if (state.withinLink) { 368 | textStyle = [styles.text, styles.autolink]; 369 | } 370 | 371 | if (state.withinQuote) { 372 | textStyle = [styles.text, styles.blockQuoteText]; 373 | } 374 | 375 | return React.createElement(Text, { 376 | key: state.key, 377 | style: textStyle, 378 | }, node.content); 379 | }, 380 | }, 381 | u: { // u will to the same as strong, to avoid the View nested inside text problem 382 | react: function (node, output, {...state}) { 383 | state.withinText = true; 384 | state.style = { 385 | ...(state.style || {}), 386 | ...styles.u 387 | }; 388 | return React.createElement(Text, { 389 | key: state.key, 390 | style: styles.strong, 391 | }, output(node.content, state)); 392 | }, 393 | }, 394 | url: { 395 | react: function (node, output, {...state}) { 396 | state.withinText = true; 397 | const _pressHandler = () => { 398 | pressHandler(node.target); 399 | }; 400 | return React.createElement(Text, { 401 | key: state.key, 402 | style: styles.autolink, 403 | onPress: _pressHandler, 404 | }, output(node.content, state)); 405 | }, 406 | }, 407 | }; 408 | }; 409 | -------------------------------------------------------------------------------- /styles.js: -------------------------------------------------------------------------------- 1 | 2 | import {Dimensions, StyleSheet, Platform} from 'react-native'; 3 | 4 | export default StyleSheet.create({ 5 | autolink: { 6 | color: 'blue', 7 | }, 8 | blockQuoteText: { 9 | color: 'grey' 10 | }, 11 | blockQuoteSection: { 12 | flexDirection: 'row', 13 | }, 14 | blockQuoteSectionBar: { 15 | width: 3, 16 | height: null, 17 | backgroundColor: '#DDDDDD', 18 | marginRight: 15, 19 | }, 20 | bgImage: { 21 | flex: 1, 22 | position: 'absolute', 23 | top: 0, 24 | left: 0, 25 | right: 0, 26 | bottom: 0, 27 | }, 28 | bgImageView: { 29 | flex: 1, 30 | overflow: 'hidden', 31 | }, 32 | view: { 33 | alignSelf: 'stretch', 34 | }, 35 | codeBlock: { 36 | fontFamily: Platform.OS === 'ios' ? 'Courier' : 'Monospace', 37 | fontWeight: '500', 38 | backgroundColor: '#DDDDDD', 39 | }, 40 | del: { 41 | textDecorationLine: 'line-through', 42 | textDecorationStyle: 'solid' 43 | }, 44 | em: { 45 | fontStyle: 'italic', 46 | }, 47 | heading: { 48 | fontWeight: '200', 49 | }, 50 | heading1: { 51 | fontSize: 32, 52 | }, 53 | heading2: { 54 | fontSize: 24, 55 | }, 56 | heading3: { 57 | fontSize: 18, 58 | }, 59 | heading4: { 60 | fontSize: 16, 61 | }, 62 | heading5: { 63 | fontSize: 13, 64 | }, 65 | heading6: { 66 | fontSize: 11, 67 | }, 68 | hr: { 69 | backgroundColor: '#cccccc', 70 | height: 1, 71 | }, 72 | image: { 73 | height: 200, // Image maximum height 74 | width: Dimensions.get('window').width - 30, // Width based on the window width 75 | alignSelf: 'center', 76 | resizeMode: 'contain', // The image will scale uniformly (maintaining aspect ratio) 77 | }, 78 | imageBox: { 79 | flex: 1, 80 | resizeMode: 'cover', 81 | }, 82 | inlineCode: { 83 | backgroundColor: '#eeeeee', 84 | borderColor: '#dddddd', 85 | borderRadius: 3, 86 | borderWidth: 1, 87 | fontFamily: Platform.OS === 'ios' ? 'Courier' : 'Monospace', 88 | fontWeight: 'bold', 89 | }, 90 | list: { 91 | }, 92 | sublist: { 93 | paddingLeft: 20, 94 | width: Dimensions.get('window').width - 60, 95 | }, 96 | listItem: { 97 | flexDirection: 'row', 98 | }, 99 | listItemText: { 100 | flex: 1, 101 | 102 | }, 103 | listItemBullet: { 104 | fontSize: 20, 105 | lineHeight: 20, 106 | }, 107 | listItemNumber: { 108 | fontWeight: 'normal', // unnecessary 'normal' - just keeping the style name documented 109 | }, 110 | listRow: { 111 | flexDirection: 'row', 112 | }, 113 | paragraph: { 114 | marginTop: 10, 115 | marginBottom: 10, 116 | flexWrap: 'wrap', 117 | flexDirection: 'row', 118 | alignItems: 'flex-start', 119 | justifyContent: 'flex-start', 120 | }, 121 | paragraphCenter: { 122 | marginTop: 10, 123 | marginBottom: 10, 124 | flexWrap: 'wrap', 125 | flexDirection: 'row', 126 | textAlign: 'center', 127 | alignItems: 'flex-start', 128 | justifyContent: 'center', 129 | }, 130 | paragraphWithImage: { 131 | flex: 1, 132 | marginTop: 10, 133 | marginBottom: 10, 134 | alignItems: 'flex-start', 135 | justifyContent: 'flex-start', 136 | }, 137 | noMargin: { 138 | marginTop: 0, 139 | marginBottom: 0, 140 | }, 141 | strong: { 142 | fontWeight: 'bold', 143 | }, 144 | table: { 145 | borderWidth: 1, 146 | borderColor: '#222222', 147 | borderRadius: 3, 148 | }, 149 | tableHeader: { 150 | backgroundColor: '#222222', 151 | flexDirection: 'row', 152 | justifyContent: 'space-around', 153 | }, 154 | tableHeaderCell: { 155 | color: '#ffffff', 156 | fontWeight: 'bold', 157 | padding: 5, 158 | }, 159 | tableRow: { 160 | //borderBottomWidth: 1, 161 | borderColor: '#222222', 162 | flexDirection: 'row', 163 | justifyContent: 'space-around', 164 | }, 165 | tableRowLast: { 166 | borderColor: 'transparent', 167 | }, 168 | tableRowCell: { 169 | padding: 5, 170 | }, 171 | text: { 172 | color: '#222222', 173 | }, 174 | textRow: { 175 | flexDirection: 'row', 176 | }, 177 | u: { 178 | borderColor: '#222222', 179 | borderBottomWidth: 1, 180 | }, 181 | }); 182 | --------------------------------------------------------------------------------