diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..a9ce136 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["react-native"] +} diff --git a/.gitignore b/.gitignore index fd4f2b0..77058f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules .DS_Store +/lib diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..fd4f2b0 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +node_modules +.DS_Store diff --git a/README.md b/README.md index d7f51f4..bb7e937 100644 --- a/README.md +++ b/README.md @@ -63,30 +63,77 @@ render() { | `style` | Style for the `````` component | object | {} | | `renderImage` | Custom renderer for images | function | none | | `renderLink` | Custom renderer for links | function | none | -| `renderListBullet` | Custom rendered for list bullets | function | none | +| `renderListBullet` | Custom renderer for list bullets | function | none | +| `renderLine` | Custom renderer for Line | function | none | +| `renderList` | Custom renderer for list | function | none | +| `renderListItem` | Custom renderer for list item | function | none | +| `renderBlockQuote` | Custom renderer for Block Quote | function | none | +| `renderBlockText` | Custom renderer for Block Text | function | none | +| `renderBlock` | Custom renderer for Block | function | none | +| `renderText` | Custom renderer for various types of text | function | none | -If you need more control over how some of the components are rendered, you may provide the custom renderers outlined above like so: +If you need more control over how some of the components are rendered, you may provide the custom renderers outlined above like so. + +Beware, these functions are experimental in nature and may not work well for all use cases. These are barebones functions that give you the needed information from markdown and allow you to choose what to render. + +In all cases, the 'children' argument refers to markdown nodes that have already been converted to JSX.Elements through the default or custom renderers. + +If using custom renders, no style logic is applied. You may opt in to any custom renderers by passing functions that match the below signatures, and it not present, `` will refer to your custom/default styles and the default legacy implementation. ``` -renderImage(src, alt, title) { - return( - - ); -} +renderImage(src, alt, title, key) + +renderLink(href, title, children, key) + +// `ordered: true` signifies this is the bullet point of a list ordered by index +renderListBullet(ordered, index) + +// Responsible for the containing List element, children are the elements that represent the list items +renderList(ordered, children, key) + +// Line break custom renderer +renderLine(key) -renderLink(href, title, children) { - return( - console.log("Opening link: " + href)}> - {children} - - ); +// example usage +renderListItem(index, ordered, children, key) { + if (!ordered) { + return { children }; + } else { + return { children } + } } -renderListBullet(ordered, index) { - return( - - ); +renderText(textType, children: React.Element | React.ReactElement[], , key) { + // Possible textTypes: h1, h2, h3, h4, h5, h6, strong, del, em, u + switch (textType) { + case 'h1' + case 'h2' + case 'h3' + case 'h4' + case 'h5' + case 'h6': + return {children} + case 'strong': + return {children} + case 'del': + return {children} + case 'em': + return {children} + case 'u: + return {children} + default: + return {children} + } } + +// Responsible for rendering the block container as well as the children +renderBlockQuote(children, key) + +// Responsible for rendering the block container as well as the children +renderBlock(children, key) + +// Responsible for any block that will only contain text elements below +renderBlockText(children, key) ``` Notice the `children` parameter passed to `renderLink`, which contains whatever children would otherwise be rendered within the link. In the default implementation, those children will be rendered within a `` but this gives you the possibility to provide your own touchable component. @@ -114,7 +161,7 @@ You can supply the component with your own ```markdownStyles``` prop to override | imageWrapper | `` | Wrapper around images, for easier layouting | | image | `` | Image component | -See [default styles](https://github.com/lappalj4/react-native-easy-markdown/blob/master/styles.js) for reference. +See [default styles](https://github.com/TitanInvest/react-native-easy-markdown/blob/master/styles.js) for reference. # Caveats @@ -123,6 +170,22 @@ See [default styles](https://github.com/lappalj4/react-native-easy-markdown/blob # Change Log +**2.0.0** +* Add ability to pass custom renderers for various markdown components. + +**1.5.0** +* Add typescript definitions + +**1.4.1** +* Fix an unexpected mutation of default styles when specifying `markdownStyles` + +**1.4.0** +* Update simple-markdown dependency to `0.4.4` for XSS vulnerability + +**1.3.0** +* Fix text wrapping unexpectedly +* Bring back Utils.isTextOnly to prevent Views nested in Texts +* New maintainer: TitanInvest **1.2.0** * Fixed crash on RN > 0.55 (#17) @@ -178,7 +241,7 @@ Possible features to implement: # License (MIT) -Copyright 2017 Juuso Lappalainen +Copyright 2019 Juuso Lappalainen, Zach Ivester Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/Utils.js b/Utils.js index 1842e02..96065bd 100644 --- a/Utils.js +++ b/Utils.js @@ -1,24 +1,34 @@ -class Utils { - - // Was causing crashes with RN > 0.55, removed for now. - // - // static isTextOnly(nodes) { - // if (nodes.length) { - // for (let i = 0; i < nodes.length; i++) { - // if (nodes[i] && - // nodes[i].hasOwnProperty('type') && - // nodes[i].type.hasOwnProperty('displayName')) { - // if (nodes[i].type.displayName !== 'Text') { - // return false; - // } - // } +const Utils = { + isTextOnly(nodes) { + try { + if (nodes.length) { + for (let i = 0; i < nodes.length; i++) { + if (nodes[i] && + typeof nodes[i].hasOwnProperty === 'function' && + nodes[i].hasOwnProperty('type') && + typeof nodes[i].type.hasOwnProperty === 'function' && + ( + nodes[i].type.hasOwnProperty('displayName') || + nodes[i].type.hasOwnProperty('name') + ) + ) { + if (nodes[i].type.displayName !== 'Text' && nodes[i].type.name !== 'Text') { + return false; + } + } else { + return false; + } + } + } + } catch(e) { + return false; + } - // } - // } - // return true; - // } + // It's a miracle, I guess we're text only + return true; + }, - static concatStyles(extras, newStyle) { + concatStyles: function concatStyles(extras, newStyle) { let newExtras; if (extras) { newExtras = JSON.parse(JSON.stringify(extras)); @@ -34,9 +44,9 @@ class Utils { }; } return newExtras; - } + }, - static logDebug(nodeTree, level = 0) { + logDebug: function logDebug(nodeTree, level = 0) { for (let i = 0; i < nodeTree.length; i++) { const node = nodeTree[i]; diff --git a/entry.js b/entry.js new file mode 100644 index 0000000..ba6f941 --- /dev/null +++ b/entry.js @@ -0,0 +1,2 @@ +module.exports = require('./lib/index.js'); + diff --git a/index.js b/index.js index b933978..dc39dd6 100644 --- a/index.js +++ b/index.js @@ -14,23 +14,6 @@ import styles from './styles'; import Utils from './Utils'; class Markdown extends Component { - static propTypes = { - debug: PropTypes.bool, - parseInline: PropTypes.bool, - markdownStyles: PropTypes.object, - useDefaultStyles: PropTypes.bool, - renderImage: PropTypes.func, - renderLink: PropTypes.func, - renderListBullet: PropTypes.func, - } - - static defaultProps = { - debug: false, - useDefaultStyles: true, - parseInline: false, - markdownStyles: {} - } - constructor(props) { super(props); @@ -42,7 +25,7 @@ class Markdown extends Component { const outputResult = this.reactOutput(parseTree); const defaultStyles = this.props.useDefaultStyles && styles ? styles : {}; - const _styles = StyleSheet.create(Object.assign(defaultStyles, this.props.markdownStyles)); + const _styles = StyleSheet.create(Object.assign({}, defaultStyles, this.props.markdownStyles)); this.state = { syntaxTree: outputResult, @@ -80,7 +63,7 @@ class Markdown extends Component { const { styles } = this.state; if (this.props.renderImage) { - return this.props.renderImage(node.props.src, node.props.alt, node.props.title); + return this.props.renderImage(node.props.src, node.props.alt, node.props.title, key); } return ( @@ -93,6 +76,10 @@ class Markdown extends Component { renderLine(node, key) { const { styles } = this.state; + if (this.props.renderLine) { + return this.props.renderLine(key); + } + return ( ); @@ -102,6 +89,11 @@ class Markdown extends Component { const { styles } = this.state; + if (this.props.renderList) { + const children = this.renderNodes(node.props.children, key, { ordered }); + return this.props.renderList(ordered, children, key); + } + return ( {this.renderNodes(node.props.children, key, { ordered })} @@ -130,33 +122,56 @@ class Markdown extends Component { let children = this.renderNodes(node.props.children, key, extras); + if (this.props.renderListItem) { + const { ordered } = extras; + return this.props.renderListItem(index, ordered, children, key); + } + + const SafeWrapper = Utils.isTextOnly(children) ? Text : View; + return ( {this.props.renderListBullet ? this.props.renderListBullet(extras.ordered, index) : this.renderListBullet(extras.ordered, index)} - + {children} - + ); } - renderText(node, key, extras) { - + renderText(node, key, extras, textType) { const { styles } = this.state; let style = (extras && extras.style) ? [styles.text].concat(extras.style) : styles.text; - if (node.props) { - return ( - - {this.renderNodes(node.props.children, key, extras)} - - ); + let text = null; + + if (node && node.props && node.props.children) { + if (Array.isArray(node.props.children)) { + + // If we have a custom renderer, we convert the child nodes to elements and pass to the consumer + let children = this.renderNodes(node.props.children, key, extras); + if (this.props.renderText) { + // Text is an array of JSX.Elements + return this.props.renderText(textType, children, key); + } else { + return ({ children }); + } + } else { + // Text is a string value + text = node.props.children; + } } else { - return ( - {node} - ); + // Node should just be a text string + text = node; } + + if (this.props.renderText) { + return this.props.renderText(textType, text, key); + } + return ( + {text} + ); } renderLink(node, key) { @@ -166,13 +181,15 @@ class Markdown extends Component { let children = this.renderNodes(node.props.children, key, extras); if (this.props.renderLink) { - return this.props.renderLink(node.props.href, node.props.title, children); + return this.props.renderLink(node.props.href, node.props.title, children, key); } + const SafeWrapper = Utils.isTextOnly(children) ? Text : TouchableOpacity; + return ( - Linking.openURL(node.props.href).catch(() => { })}> + Linking.openURL(node.props.href).catch(() => { })}> {children} - + ); } @@ -194,20 +211,33 @@ class Markdown extends Component { */ delete extras.blockQuote; } - const nodes = this.renderNodes(node.props.children, key, extras); + const children = this.renderNodes(node.props.children, key, extras); if (isBlockQuote) { - style.push(styles.blockQuote) + if (this.props.renderBlockQuote) { + return this.props.renderBlockQuote(children, key) + } return ( - {nodes} + {children} ); } + else if (Utils.isTextOnly(children)) { + if (this.props.renderBlockText) { + return this.props.renderBlockText(children, key) + } + return ( + {children} + ); + } else { + if (this.props.renderBlock) { + return this.props.renderBlock(children, key); + } return ( - {nodes} + {children} ); } @@ -220,14 +250,15 @@ class Markdown extends Component { const { styles } = this.state; + if (this.props.debug) console.log('rendering node: ', node); switch (node.type) { - case 'h1': return this.renderText(node, key, Utils.concatStyles(extras, styles.h1)); - case 'h2': return this.renderText(node, key, Utils.concatStyles(extras, styles.h2)); - case 'h3': return this.renderText(node, key, Utils.concatStyles(extras, styles.h3)); - case 'h4': return this.renderText(node, key, Utils.concatStyles(extras, styles.h4)) - case 'h5': return this.renderText(node, key, Utils.concatStyles(extras, styles.h5)); - case 'h6': return this.renderText(node, key, Utils.concatStyles(extras, styles.h6)); + case 'h1': return this.renderText(node, key, Utils.concatStyles(extras, styles.h1), 'h1' ); + case 'h2': return this.renderText(node, key, Utils.concatStyles(extras, styles.h2), 'h2' ); + case 'h3': return this.renderText(node, key, Utils.concatStyles(extras, styles.h3), 'h3' ); + case 'h4': return this.renderText(node, key, Utils.concatStyles(extras, styles.h4), 'h4' ); + case 'h5': return this.renderText(node, key, Utils.concatStyles(extras, styles.h5), 'h5' ); + case 'h6': return this.renderText(node, key, Utils.concatStyles(extras, styles.h6), 'h6' ); case 'hr': return this.renderLine(node, key); case 'div': return this.renderBlock(node, key, extras); case 'ul': return this.renderList(node, key, false); @@ -235,10 +266,11 @@ class Markdown extends Component { case 'li': return this.renderListItem(node, key, index, extras); case 'a': return this.renderLink(node, key); case 'img': return this.renderImage(node, key); - case 'strong': return this.renderText(node, key, Utils.concatStyles(extras, styles.strong)); - case 'del': return this.renderText(node, key, Utils.concatStyles(extras, styles.del)); - case 'em': return this.renderText(node, key, Utils.concatStyles(extras, styles.em)); - case 'u': return this.renderText(node, key, Utils.concatStyles(extras, styles.u)); + case 'strong': return this.renderText(node, key, Utils.concatStyles(extras, styles.strong), 'strong'); + case 'del': return this.renderText(node, key, Utils.concatStyles(extras, styles.del), 'del'); + case 'em': return this.renderText(node, key, Utils.concatStyles(extras, styles.em), 'em'); + case 'u': return this.renderText(node, key, Utils.concatStyles(extras, styles.u), 'u'); + case 'code': return this.renderText(node, key, Utils.concatStyles(extras, styles.code), 'code'); case 'blockquote': return this.renderBlockQuote(node, key); case undefined: return this.renderText(node, key, extras); default: if (this.props.debug) console.log('Node type ' + node.type + ' is not supported'); return null; @@ -268,4 +300,28 @@ class Markdown extends Component { } } +Markdown.propTypes = { + debug: PropTypes.bool, + parseInline: PropTypes.bool, + markdownStyles: PropTypes.object, + useDefaultStyles: PropTypes.bool, + renderImage: PropTypes.func, + renderLink: PropTypes.func, + renderListBullet: PropTypes.func, + renderLine: PropTypes.func, + renderList: PropTypes.func, + renderListItem: PropTypes.func, + renderText: PropTypes.func, + renderBlockQuote: PropTypes.func, + renderBlockText: PropTypes.func, + renderBlock: PropTypes.func, +}; + +Markdown.defaultProps = { + debug: false, + useDefaultStyles: true, + parseInline: false, + markdownStyles: {} +}; + export default Markdown; diff --git a/package.json b/package.json index 6a85cfd..5db7f4e 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,12 @@ { "name": "react-native-easy-markdown", - "version": "1.2.0", + "version": "2.0.0", "description": "Simple & customizable React Native component to render Github-flavoured markdown using minimal native components.", - "main": "index.js", - "repository": "https://github.com/lappalj4/react-native-easy-markdown", + "main": "lib/index.js", + "repository": "https://github.com/TitanInvest/react-native-easy-markdown", "scripts": { + "build": "babel index.js styles.js Utils.js -d lib", + "prepare": "npm run build", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ @@ -14,12 +16,19 @@ "parse", "parser" ], - "author": "Juuso Lappalainen", + "contributors": [ + "Juuso Lappalainen", + "Zach Ivester " + ], "license": "MIT", "dependencies": { - "simple-markdown": "^0.1.1" + "simple-markdown": "^0.7.2" }, "peerDependencies": { "react-native": "*" + }, + "devDependencies": { + "babel-cli": "^6.26.0", + "babel-preset-react-native": "^4.0.1" } -} \ No newline at end of file +} diff --git a/styles.js b/styles.js index 6b841af..1a20be2 100644 --- a/styles.js +++ b/styles.js @@ -46,6 +46,10 @@ const defaultStyles = { backgroundColor: '#333333', marginVertical: 8, }, + code: { + backgroundColor: '#333333', + color: 'orange', + }, text: { alignSelf: 'flex-start' }, @@ -79,6 +83,7 @@ const defaultStyles = { }, listItemContent: { flexDirection: 'row', + flexShrink: 1, justifyContent: 'flex-start', alignItems: 'flex-start', },