├── .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 | [](https://www.npmjs.com/package/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 | 
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]*?)['\"])?\\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 |
--------------------------------------------------------------------------------