├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples └── usage.js ├── package.json ├── src └── commonmark-react-renderer.js └── test ├── .eslintrc └── commonmark-react-renderer.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | # Use hard or soft tabs 6 | indent_style = space 7 | 8 | # Size of a single indent 9 | indent_size = tab 10 | 11 | # Number of columns representing a tab character 12 | tab_width = 4 13 | 14 | # Use line-feed as EOL indicator 15 | end_of_line = lf 16 | 17 | # Use UTF-8 character encoding for all files 18 | charset = utf-8 19 | 20 | # Remove any whitespace characters preceding newline characters 21 | trim_trailing_whitespace = true 22 | 23 | # Ensure file ends with a newline when saving 24 | insert_final_newline = true 25 | 26 | [*.md] 27 | trim_trailing_whitespace = false 28 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "vaffel/react" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directories 26 | node_modules 27 | vendor 28 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .editorconfig 3 | .eslintignore 4 | .eslintrc 5 | .travis.yml 6 | npm-debug.log 7 | test/**/* 8 | test 9 | coverage 10 | example 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.12' 4 | - '4' 5 | - '6' 6 | script: 7 | - npm run test-travis 8 | after_script: 9 | - npm install codeclimate-test-reporter && cat coverage/lcov.info | codeclimate 10 | env: 11 | global: 12 | - secure: HX3p969dBOsaTbTwZpz/74GnYCfnBgx4Mnhi+x0aGETXFtVGulpxR2lGq5XHsk5XsKpJ+a9rwgeqi8vaerjARxElKGcrqEFxL5T6PVS6O28Hcudlsy3puloOYFmhwLjdAERfIfCX6o7/S9Ga1Ci+4pG2oQ6tlLNMUFQ4NrG/qJ67afEyL+RW6s+Il1lXQtmvEsseSaKKwIKV6YcYiTgYtVD6XA/YQXHs6Go5MQbc0rts+TaqcCYlJ9kozG7ciRH/0LUVBFtjtYGYYvGrUqgWKGga6a7pFlnL8P9HMztVFs/Yjuc3KWtWE8E6T6EIPwoOdM1kjQIHIJl7HiWrZOR0agba4e7q9WXAqzl64dhNnN9eZ6yKnoDXZSgYv1le/gad94bLTF5S19sN8wpHD+Tr5jbEZnBWygnt3xdK9x4PvZg5emhpr4wiuErmTCVG/xwgtR56VMNTEIJeeNZa/QpBt4ISE5+aWPPDNPYNguqFc23qtZrRZ9ylJpg563OTPTfYP2ppT7uUQRAIjoiWUIg49PKygZmQicF9Foxm4IjO4aY7ueiTqePbv8sHe2JbfEbGCj1xP0MFqyB4er9Y+oeEnS31dBka7LnvP/2WHf4bI4BGplQYKz3RWF/3ChHvLQVUXNQB/REvvAN9tnpIg6pgW3hzAmXgCr++DoK/U3WPBYk= 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes will be documented in this file. 4 | 5 | ## 4.3.3 - 2017-05-28 6 | 7 | - Expose all of codeinfo to CodeBlock renderer (Tauren Mills) 8 | - Include className in core props (Espen Hovlandsdal) 9 | 10 | ## 4.3.2 - 2016-12-08 11 | 12 | - Add `linkTarget` prop to optionally add a `target` attribute on links (James Simpson) 13 | - Fix typo in readme (Chase Ricketts) 14 | - Allow using Commonmark 0.27 (Lukas Geiger) 15 | 16 | ## 4.3.1 - 2016-08-23 17 | 18 | - Update dependencies to latest versions (Espen Hovlandsdal) 19 | 20 | ## 4.3.0 - 2016-08-23 21 | 22 | - Enable Commonmark.js 0.26 compatibility (Espen Hovlandsdal) 23 | 24 | ## 4.2.4 - 2016-07-09 25 | 26 | - Fix bug where nodes would not be rendered due to duplicate keys (Espen Hovlandsdal) 27 | 28 | ## 4.2.3 - 2016-07-09 29 | 30 | - Fix regression in passing props to `Code`-nodes (Espen Hovlandsdal) 31 | 32 | ## 4.2.2 - 2016-07-09 33 | 34 | ### Changes 35 | 36 | - Give `Code` renderers an `inline` property that is always true, allowing reuse of renderer for `CodeBlock` and `Code` (Espen Hovlandsdal) 37 | 38 | ## 4.2.1 - 2016-07-09 39 | 40 | ### Changes 41 | 42 | - Fix bug where lists, codeblocks and headings would not get passed `sourcepos` prop (Espen Hovlandsdal) 43 | 44 | ## 4.2.0 - 2016-07-09 45 | 46 | ### Changes 47 | 48 | - Plain DOM-node renderers are now given only their respective props. Fixes warnings when using React >= 15.2 (Espen Hovlandsdal) 49 | 50 | ### Added 51 | 52 | - New `transformImageUri` option allows you to transform URIs for images. (Petri Lehtinen) 53 | 54 | ## 4.1.4 - 2016-04-27 55 | 56 | ### Changes 57 | 58 | - Fix image alt text when it includes special characters (Ramsay Stirling II) 59 | 60 | ## 4.1.3 - 2016-04-26 61 | 62 | ### Changes 63 | 64 | - Pass `nodeKey` as prop to complex renderers. Fixes warning in React >= 15 (Espen Hovlandsdal) 65 | 66 | ## 4.1.2 - 2016-03-12 67 | 68 | ### Changes 69 | 70 | - Also join sibling nodes within paragraphs and similar (Espen Hovlandsdal) 71 | 72 | ## 4.1.1 - 2016-03-12 73 | 74 | ### Changes 75 | 76 | - Join sibling text nodes into one text node (Espen Hovlandsdal) 77 | 78 | ## 4.0.1 - 2016-02-21 79 | 80 | ### Changes 81 | 82 | - Use strings as renderers in simple cases (Glen Mailer) 83 | - Set keys on lists and code blocks (Guillaume Plique) 84 | 85 | ## 4.0.0 - 2016-02-21 86 | 87 | ### Changes 88 | 89 | - **Breaking change**: Inline HTML nodes are now wrapped in a ``, block HTML nodes in `
`. This is necessary to properly support custom renderers. 90 | 91 | ## 3.0.2 - 2016-02-21 92 | 93 | ### Changes 94 | 95 | - The default URI transformer no longer applies double URI-encoding. 96 | 97 | ## 3.0.1 - 2016-02-21 98 | 99 | ### Added 100 | 101 | - The default URI transformer is now exposed on the `uriTransformer` property of the renderer, allowing it to be reused. 102 | - Documentation for `transformLinkUri`-option. 103 | 104 | ## 3.0.0 - 2016-02-21 105 | 106 | ### Changes 107 | 108 | - **Breaking change**: The renderer now requires Node 0.14 or higher. This is because the renderer uses stateless components internally. 109 | - **Breaking change**: `allowNode` now receives different properties in the options argument. See `README.md` for more details. 110 | - **Breaking change**: CommonMark has changed some type names. `Html` is now `HtmlInline`, `Header` is now `Heading` and `HorizontalRule` is now `ThematicBreak`. This affects the `allowedTypes` and `disallowedTypes` options. 111 | - **Breaking change**: A bug in the `allowedTypes`/`disallowedTypes` and `allowNode` options made them only applicable to certain types. In this version, all types are filtered, as expected. 112 | - **Breaking change**: Link URIs are now filtered through an XSS-filter by default, prefixing "dangerous" protocols such as `javascript:` with `x-` (eg: `javascript:alert('foo')` turns into `x-javascript:alert('foo')`). This can be overridden with the `transformLinkUri`-option. Pass `null` to disable the feature or a custom function to replace the built-in behaviour. 113 | 114 | ### Added 115 | 116 | - New `renderers` option allows you to customize which React component should be used for rendering given types. See `README.md` for more details. (Espen Hovlandsdal / Guillaume Plique) 117 | - New `unwrapDisallowed` option allows you to select if the contents of a disallowed node should be "unwrapped" (placed into the disallowed node position). For instance, setting this option to true and disallowing a link would still render the text of the link, instead of the whole link node and all it's children disappearing. (Espen Hovlandsdal) 118 | - New `transformLinkUri` option allows you to transform URIs in links. By default, an XSS-filter is used, but you could also use this for use cases like transforming absolute to relative URLs, or similar. (Espen Hovlandsdal) 119 | 120 | ## 2.2.2 - 2016-01-22 121 | 122 | ### Added 123 | 124 | - Provide index-based keys to generated elements to silent warnings from React (Guillaume Plique) 125 | 126 | ## 2.2.1 - 2016-01-22 127 | 128 | ### Changed 129 | 130 | - Upgrade commonmark to latest version (Guillaume Plique) 131 | 132 | ## 2.2.0 - 2015-12-11 133 | 134 | ### Added 135 | 136 | - Allow passing `allowNode` - a function which determines if a given node should be allowed (Espen Hovlandsdal) 137 | 138 | ## 2.1.0 - 2015-11-20 139 | 140 | ### Added 141 | 142 | - Add support for specifying which types should be allowed - `allowTypes`/`disallowedTypes` (Espen Hovlandsdal) 143 | 144 | ## 2.0.2 - 2015-11-19 145 | 146 | ### Added 147 | 148 | - Add support for hard linebreaks (marlonbaeten) 149 | 150 | ## 2.0.1 - 2015-10-22 151 | 152 | ### Changed 153 | 154 | - Peer dependency for React was (incorrectly) set to >= 0.14.0, when 0.13.3 was supported. 155 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Espen Hovlandsdal 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 | # commonmark-react-renderer 2 | 3 | [![npm version](http://img.shields.io/npm/v/commonmark-react-renderer.svg?style=flat-square)](http://browsenpm.org/package/commonmark-react-renderer)[![Build Status](http://img.shields.io/travis/rexxars/commonmark-react-renderer/master.svg?style=flat-square)](https://travis-ci.org/rexxars/commonmark-react-renderer)[![Coverage Status](http://img.shields.io/codeclimate/coverage/github/rexxars/commonmark-react-renderer.svg?style=flat-square)](https://codeclimate.com/github/rexxars/commonmark-react-renderer)[![Code Climate](http://img.shields.io/codeclimate/github/rexxars/commonmark-react-renderer.svg?style=flat-square)](https://codeclimate.com/github/rexxars/commonmark-react-renderer/) 4 | 5 | Renderer for CommonMark which returns an array of React elements, ready to be used in a React component. See [react-markdown](https://github.com/rexxars/react-markdown/) for such a component. 6 | 7 | ## Installing 8 | 9 | ``` 10 | npm install --save commonmark-react-renderer 11 | ``` 12 | 13 | ## Basic usage 14 | 15 | ```js 16 | var CommonMark = require('commonmark'); 17 | var ReactRenderer = require('commonmark-react-renderer'); 18 | 19 | var parser = new CommonMark.Parser(); 20 | var renderer = new ReactRenderer(); 21 | 22 | var input = '# This is a header\n\nAnd this is a paragraph'; 23 | var ast = parser.parse(input); 24 | var result = renderer.render(ast); 25 | 26 | // `result`: 27 | [ 28 |

This is a header

, 29 |

And this is a paragraph

30 | ] 31 | ``` 32 | 33 | ## Options 34 | 35 | Pass an object of options to the renderer constructor to configure it. Available options: 36 | 37 | * `sourcePos` - *boolean* Setting to `true` will add `data-sourcepos` attributes to all elements, indicating where in the markdown source they were rendered from (default: `false`). 38 | * `escapeHtml` - *boolean* Setting to `true` will escape HTML blocks, rendering plain text instead of inserting the blocks as raw HTML (default: `false`). 39 | * `skipHtml` - *boolean* Setting to `true` will skip inlined and blocks of HTML (default: `false`). 40 | * `softBreak` - *string* Setting to `br` will create `
` tags instead of newlines (default: `\n`). 41 | * `allowedTypes` - *array* Defines which types of nodes should be allowed (rendered). (default: all types). 42 | * `disallowedTypes` - *array* Defines which types of nodes should be disallowed (not rendered). (default: none). 43 | * `unwrapDisallowed` - *boolean* Setting to `true` will try to extract/unwrap the children of disallowed nodes. For instance, if disallowing `Strong`, the default behaviour is to simply skip the text within the strong altogether, while the behaviour some might want is to simply have the text returned without the strong wrapping it. (default: `false`) 44 | * `allowNode` - *function* Function execute if in order to determine if the node should be allowed. Ran prior to checking `allowedTypes`/`disallowedTypes`. Returning a truthy value will allow the node to be included. Note that if this function returns `true` and the type is not in `allowedTypes` (or specified as a `disallowedType`), it won't be included. The function will get a single object argument (`node`), which includes the following properties: 45 | * `type` - *string* The type of node - same ones accepted in `allowedTypes` and `disallowedTypes` 46 | * `renderer` - *string* The resolved renderer for this node 47 | * `props` - *object* Properties for this node 48 | * `children` - *array* Array of children 49 | * `renderers` - *object* An object where the keys represent the node type and the value is a React component. The object is merged with the default renderers. The props passed to the component varies based on the type of node. See the `Type renderer options` section below for more details. 50 | * `transformLinkUri` - *function|null* Function that gets called for each encountered link with a single argument - `uri`. The returned value is used in place of the original. The default link URI transformer acts as an XSS-filter, neutralizing things like `javascript:`, `vbscript:` and `file:` protocols. If you specify a custom function, this default filter won't be called, but you can access it as `require('commonmark-react-renderer').uriTransformer`. If you want to disable the default transformer, pass `null` to this option. 51 | * `transformImageUri` - *function|null* Function that gets called for each encountered image with a single argument - `uri`. The returned value is used in place of the original. 52 | * `linkTarget` - *string* A string to be used in the anchor tags `target` attribute e.g., `"_blank"` 53 | 54 | ## Type renderer options 55 | 56 | ### HtmlInline / HtmlBlock 57 | 58 | **Note**: Inline HTML is [currently broken](https://github.com/rexxars/commonmark-react-renderer/issues/9) 59 | 60 | * `isBlock` - *boolean* `true` if type is `HtmlBlock`, `false` otherwise 61 | * `escapeHtml` - *boolean* Same as renderer option, see above 62 | * `skipHtml` - *boolean* Same as renderer option, see above 63 | * `literal` - *string* The HTML fragment 64 | 65 | ### CodeBlock 66 | 67 | * `language` - *string* Language info tag, for instance \```js would set this to `js`. Undefined if the tag is not present in the source. 68 | * `literal` - *string* The string value of the code block 69 | 70 | ### Code 71 | 72 | * `literal` - *string* The string value of the inline code 73 | * `inline` - *boolean* Always true. Present to allow reuse of the same renderer for both `CodeBlock` and `Code`. 74 | 75 | ### Heading 76 | 77 | * `level` - *number* Heading level, from 1 to 6. 78 | * `children` - *node* One or more child nodes for the heading 79 | 80 | ### Softbreak 81 | 82 | * `softBreak` - *mixed* Depending on the `softBreak` setting of the actual renderer, either a given string or a React linebreak element 83 | 84 | ### Link 85 | 86 | * `href` - *string* URL for the link 87 | * `title` - *string* Title for the link, if any 88 | * `children` - *node* One or more child nodes for the link 89 | 90 | ### Image 91 | 92 | * `src` - *string* URL for the image 93 | * `title` - *string* Title for the image, if any 94 | * `alt` - *string* Alternative text for the image, if any 95 | 96 | ### List 97 | 98 | * `start` - *number* Start index of the list 99 | * `type` - *string* Type of list (`Bullet`/`Ordered`) 100 | * `tight` - *boolean* Whether the list is tight or not (see [http://spec.commonmark.org/0.23/#lists](CommonMark spec) for more details) 101 | 102 | ### Common 103 | 104 | * `nodeKey` - *string* A key that can be used by React for the `key` hint 105 | * `children` - *node* Child nodes of the current node 106 | * `literal` - *string* A literal representation of the node, where applicable 107 | * `data-sourcepos` - *string* If `sourcePos` option is set, passed to all types and should be present in all the DOM-representations to signify the source position of this node 108 | 109 | ## Testing 110 | 111 | ```bash 112 | git clone git@github.com:rexxars/commonmark-react-renderer.git 113 | cd commonmark-react-renderer 114 | npm install 115 | npm test 116 | ``` 117 | 118 | ## License 119 | 120 | MIT-licensed. See LICENSE. 121 | -------------------------------------------------------------------------------- /examples/usage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var CommonMark = require('commonmark'); 4 | var ReactRenderer = require('../'); 5 | 6 | var parser = new CommonMark.Parser(); 7 | var renderer = new ReactRenderer(); 8 | 9 | var input = '# This is a header\n\nAnd this is a paragraph'; 10 | var ast = parser.parse(input); 11 | 12 | // Result of this operation will be an array of React elements 13 | renderer.render(ast); 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commonmark-react-renderer", 3 | "description": "React renderer for CommonMark (rationalized Markdown)", 4 | "version": "4.3.5", 5 | "keywords": [ 6 | "commonmark", 7 | "markdown", 8 | "react", 9 | "renderer" 10 | ], 11 | "main": "src/commonmark-react-renderer.js", 12 | "scripts": { 13 | "coverage": "istanbul cover node_modules/.bin/_mocha -- --reporter spec", 14 | "lint": "eslint .", 15 | "prepublishOnly": "npm test", 16 | "posttest": "npm run lint", 17 | "test": "mocha --reporter spec", 18 | "test-travis": "istanbul cover node_modules/.bin/_mocha --report lcovonly -- --reporter spec" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git@github.com:rexxars/commonmark-react-renderer.git" 23 | }, 24 | "author": "Espen Hovlandsdal ", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "chai": "^3.4.0", 28 | "commonmark": "^0.27.0", 29 | "eslint": "^1.7.3", 30 | "eslint-config-vaffel": "^3.0.0", 31 | "eslint-plugin-react": "^3.6.3", 32 | "istanbul": "^0.4.5", 33 | "mocha": "^3.0.2", 34 | "react": "^15.3.1", 35 | "react-dom": "^15.3.1" 36 | }, 37 | "peerDependencies": { 38 | "react": ">=0.14.0", 39 | "commonmark": "^0.27.0 || ^0.26.0 || ^0.24.0" 40 | }, 41 | "dependencies": { 42 | "lodash.assign": "^4.2.0", 43 | "lodash.isplainobject": "^4.0.6", 44 | "pascalcase": "^0.1.1", 45 | "xss-filters": "^1.2.6" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/commonmark-react-renderer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var assign = require('lodash.assign'); 5 | var isPlainObject = require('lodash.isplainobject'); 6 | var xssFilters = require('xss-filters'); 7 | var pascalCase = require('pascalcase'); 8 | 9 | var typeAliases = { 10 | blockquote: 'block_quote', 11 | thematicbreak: 'thematic_break', 12 | htmlblock: 'html_block', 13 | htmlinline: 'html_inline', 14 | codeblock: 'code_block', 15 | hardbreak: 'linebreak' 16 | }; 17 | 18 | var defaultRenderers = { 19 | block_quote: 'blockquote', // eslint-disable-line camelcase 20 | emph: 'em', 21 | linebreak: 'br', 22 | image: 'img', 23 | item: 'li', 24 | link: 'a', 25 | paragraph: 'p', 26 | strong: 'strong', 27 | thematic_break: 'hr', // eslint-disable-line camelcase 28 | 29 | html_block: HtmlRenderer, // eslint-disable-line camelcase 30 | html_inline: HtmlRenderer, // eslint-disable-line camelcase 31 | 32 | list: function List(props) { 33 | var tag = props.type.toLowerCase() === 'bullet' ? 'ul' : 'ol'; 34 | var attrs = getCoreProps(props); 35 | 36 | if (props.start !== null && props.start !== 1) { 37 | attrs.start = props.start.toString(); 38 | } 39 | 40 | return createElement(tag, attrs, props.children); 41 | }, 42 | code_block: function CodeBlock(props) { // eslint-disable-line camelcase 43 | var className = props.language && 'language-' + props.language; 44 | var code = createElement('code', { className: className }, props.literal); 45 | return createElement('pre', getCoreProps(props), code); 46 | }, 47 | code: function Code(props) { 48 | return createElement('code', getCoreProps(props), props.children); 49 | }, 50 | heading: function Heading(props) { 51 | return createElement('h' + props.level, getCoreProps(props), props.children); 52 | }, 53 | 54 | text: null, 55 | softbreak: null 56 | }; 57 | 58 | var coreTypes = Object.keys(defaultRenderers); 59 | 60 | function getCoreProps(props) { 61 | var propKeys = Object.keys(props); 62 | 63 | var dataPropKeys = propKeys.filter(function(propKey) { 64 | return propKey.match(/data-.*/g); 65 | }); 66 | 67 | var base = { 68 | key: props.nodeKey, 69 | className: props.className 70 | }; 71 | 72 | var dataAttributes = dataPropKeys.reduce(function(prev, dataPropKey) { 73 | var attributes = {}; 74 | attributes[dataPropKey] = props[dataPropKey]; 75 | 76 | return assign(attributes, prev); 77 | }, {}); 78 | 79 | return assign(dataAttributes, base); 80 | } 81 | 82 | function normalizeTypeName(typeName) { 83 | var norm = typeName.toLowerCase(); 84 | var type = typeAliases[norm] || norm; 85 | return typeof defaultRenderers[type] !== 'undefined' ? type : typeName; 86 | } 87 | 88 | function normalizeRenderers(renderers) { 89 | return Object.keys(renderers || {}).reduce(function(normalized, type) { 90 | var norm = normalizeTypeName(type); 91 | normalized[norm] = renderers[type]; 92 | return normalized; 93 | }, {}); 94 | } 95 | 96 | function HtmlRenderer(props) { 97 | var coreProps = getCoreProps(props); 98 | var nodeProps = props.escapeHtml ? {} : { dangerouslySetInnerHTML: { __html: props.literal } }; 99 | var children = props.escapeHtml ? [props.literal] : null; 100 | 101 | if (props.escapeHtml || !props.skipHtml) { 102 | var actualProps = assign(coreProps, nodeProps); 103 | return createElement(props.isBlock ? 'div' : 'span', actualProps, children); 104 | } 105 | } 106 | 107 | function isGrandChildOfList(node) { 108 | var grandparent = node.parent.parent; 109 | return ( 110 | grandparent && 111 | grandparent.type.toLowerCase() === 'list' && 112 | grandparent.listTight 113 | ); 114 | } 115 | 116 | function addChild(node, child) { 117 | var parent = node; 118 | do { 119 | parent = parent.parent; 120 | } while (!parent.react); 121 | 122 | parent.react.children.push(child); 123 | } 124 | 125 | function createElement(tagName, props, children) { 126 | var nodeChildren = Array.isArray(children) && children.reduce(reduceChildren, []); 127 | var args = [tagName, props].concat(nodeChildren || children); 128 | return React.createElement.apply(React, args); 129 | } 130 | 131 | function reduceChildren(children, child) { 132 | var lastIndex = children.length - 1; 133 | if (typeof child === 'string' && typeof children[lastIndex] === 'string') { 134 | children[lastIndex] += child; 135 | } else { 136 | children.push(child); 137 | } 138 | 139 | return children; 140 | } 141 | 142 | function flattenPosition(pos) { 143 | return [ 144 | pos[0][0], ':', pos[0][1], '-', 145 | pos[1][0], ':', pos[1][1] 146 | ].map(String).join(''); 147 | } 148 | 149 | // For some nodes, we want to include more props than for others 150 | function getNodeProps(node, key, opts, renderer) { 151 | var props = { key: key }, undef; 152 | 153 | // `sourcePos` is true if the user wants source information (line/column info from markdown source) 154 | if (opts.sourcePos && node.sourcepos) { 155 | props['data-sourcepos'] = flattenPosition(node.sourcepos); 156 | } 157 | 158 | var type = normalizeTypeName(node.type); 159 | 160 | switch (type) { 161 | case 'html_inline': 162 | case 'html_block': 163 | props.isBlock = type === 'html_block'; 164 | props.escapeHtml = opts.escapeHtml; 165 | props.skipHtml = opts.skipHtml; 166 | break; 167 | case 'code_block': 168 | var codeInfo = node.info ? node.info.split(/ +/) : []; 169 | if (codeInfo.length > 0 && codeInfo[0].length > 0) { 170 | props.language = codeInfo[0]; 171 | props.codeinfo = codeInfo; 172 | } 173 | break; 174 | case 'code': 175 | props.children = node.literal; 176 | props.inline = true; 177 | break; 178 | case 'heading': 179 | props.level = node.level; 180 | break; 181 | case 'softbreak': 182 | props.softBreak = opts.softBreak; 183 | break; 184 | case 'link': 185 | props.href = opts.transformLinkUri ? opts.transformLinkUri(node.destination) : node.destination; 186 | props.title = node.title || undef; 187 | if (opts.linkTarget) { 188 | props.target = opts.linkTarget; 189 | } 190 | break; 191 | case 'image': 192 | props.src = opts.transformImageUri ? opts.transformImageUri(node.destination) : node.destination; 193 | props.title = node.title || undef; 194 | 195 | // Commonmark treats image description as children. We just want the text 196 | props.alt = node.react.children.join(''); 197 | node.react.children = undef; 198 | break; 199 | case 'list': 200 | props.start = node.listStart; 201 | props.type = node.listType; 202 | props.tight = node.listTight; 203 | break; 204 | default: 205 | } 206 | 207 | if (typeof renderer !== 'string') { 208 | props.literal = node.literal; 209 | } 210 | 211 | var children = props.children || (node.react && node.react.children); 212 | if (Array.isArray(children)) { 213 | props.children = children.reduce(reduceChildren, []) || null; 214 | } 215 | 216 | return props; 217 | } 218 | 219 | function getPosition(node) { 220 | if (!node) { 221 | return null; 222 | } 223 | 224 | if (node.sourcepos) { 225 | return flattenPosition(node.sourcepos); 226 | } 227 | 228 | return getPosition(node.parent); 229 | } 230 | 231 | function renderNodes(block) { 232 | var walker = block.walker(); 233 | 234 | var propOptions = { 235 | sourcePos: this.sourcePos, 236 | escapeHtml: this.escapeHtml, 237 | skipHtml: this.skipHtml, 238 | transformLinkUri: this.transformLinkUri, 239 | transformImageUri: this.transformImageUri, 240 | softBreak: this.softBreak, 241 | linkTarget: this.linkTarget 242 | }; 243 | 244 | var e, node, entering, leaving, type, doc, key, nodeProps, prevPos, prevIndex = 0; 245 | while ((e = walker.next())) { 246 | var pos = getPosition(e.node.sourcepos ? e.node : e.node.parent); 247 | if (prevPos === pos) { 248 | key = pos + prevIndex; 249 | prevIndex++; 250 | } else { 251 | key = pos; 252 | prevIndex = 0; 253 | } 254 | 255 | prevPos = pos; 256 | entering = e.entering; 257 | leaving = !entering; 258 | node = e.node; 259 | type = normalizeTypeName(node.type); 260 | nodeProps = null; 261 | 262 | // If we have not assigned a document yet, assume the current node is just that 263 | if (!doc) { 264 | doc = node; 265 | node.react = { children: [] }; 266 | continue; 267 | } else if (node === doc) { 268 | // When we're leaving... 269 | continue; 270 | } 271 | 272 | // In HTML, we don't want paragraphs inside of list items 273 | if (type === 'paragraph' && isGrandChildOfList(node)) { 274 | continue; 275 | } 276 | 277 | // If we're skipping HTML nodes, don't keep processing 278 | if (this.skipHtml && (type === 'html_block' || type === 'html_inline')) { 279 | continue; 280 | } 281 | 282 | var isDocument = node === doc; 283 | var disallowedByConfig = this.allowedTypes.indexOf(type) === -1; 284 | var disallowedByUser = false; 285 | 286 | // Do we have a user-defined function? 287 | var isCompleteParent = node.isContainer && leaving; 288 | var renderer = this.renderers[type]; 289 | if (this.allowNode && (isCompleteParent || !node.isContainer)) { 290 | var nodeChildren = isCompleteParent ? node.react.children : []; 291 | 292 | nodeProps = getNodeProps(node, key, propOptions, renderer); 293 | disallowedByUser = !this.allowNode({ 294 | type: pascalCase(type), 295 | renderer: this.renderers[type], 296 | props: nodeProps, 297 | children: nodeChildren 298 | }); 299 | } 300 | 301 | if (!isDocument && (disallowedByUser || disallowedByConfig)) { 302 | if (!this.unwrapDisallowed && entering && node.isContainer) { 303 | walker.resumeAt(node, false); 304 | } 305 | 306 | continue; 307 | } 308 | 309 | var isSimpleNode = type === 'text' || type === 'softbreak'; 310 | if (typeof renderer !== 'function' && !isSimpleNode && typeof renderer !== 'string') { 311 | throw new Error( 312 | 'Renderer for type `' + pascalCase(node.type) + '` not defined or is not renderable' 313 | ); 314 | } 315 | 316 | if (node.isContainer && entering) { 317 | node.react = { 318 | component: renderer, 319 | props: {}, 320 | children: [] 321 | }; 322 | } else { 323 | var childProps = nodeProps || getNodeProps(node, key, propOptions, renderer); 324 | if (renderer) { 325 | childProps = typeof renderer === 'string' 326 | ? childProps 327 | : assign(childProps, {nodeKey: childProps.key}); 328 | 329 | addChild(node, React.createElement(renderer, childProps)); 330 | } else if (type === 'text') { 331 | addChild(node, node.literal); 332 | } else if (type === 'softbreak') { 333 | // Softbreaks are usually treated as newlines, but in HTML we might want explicit linebreaks 334 | var softBreak = ( 335 | this.softBreak === 'br' ? 336 | React.createElement('br', {key: key}) : 337 | this.softBreak 338 | ); 339 | addChild(node, softBreak); 340 | } 341 | } 342 | } 343 | 344 | return doc.react.children; 345 | } 346 | 347 | function defaultLinkUriFilter(uri) { 348 | var url = uri.replace(/file:\/\//g, 'x-file://'); 349 | 350 | // React does a pretty swell job of escaping attributes, 351 | // so to prevent double-escaping, we need to decode 352 | return decodeURI(xssFilters.uriInDoubleQuotedAttr(url)); 353 | } 354 | 355 | function ReactRenderer(options) { 356 | var opts = options || {}; 357 | 358 | if (opts.allowedTypes && opts.disallowedTypes) { 359 | throw new Error('Only one of `allowedTypes` and `disallowedTypes` should be defined'); 360 | } 361 | 362 | if (opts.allowedTypes && !Array.isArray(opts.allowedTypes)) { 363 | throw new Error('`allowedTypes` must be an array'); 364 | } 365 | 366 | if (opts.disallowedTypes && !Array.isArray(opts.disallowedTypes)) { 367 | throw new Error('`disallowedTypes` must be an array'); 368 | } 369 | 370 | if (opts.allowNode && typeof opts.allowNode !== 'function') { 371 | throw new Error('`allowNode` must be a function'); 372 | } 373 | 374 | var linkFilter = opts.transformLinkUri; 375 | if (typeof linkFilter === 'undefined') { 376 | linkFilter = defaultLinkUriFilter; 377 | } else if (linkFilter && typeof linkFilter !== 'function') { 378 | throw new Error('`transformLinkUri` must either be a function, or `null` to disable'); 379 | } 380 | 381 | var imageFilter = opts.transformImageUri; 382 | if (typeof imageFilter !== 'undefined' && typeof imageFilter !== 'function') { 383 | throw new Error('`transformImageUri` must be a function'); 384 | } 385 | 386 | if (opts.renderers && !isPlainObject(opts.renderers)) { 387 | throw new Error('`renderers` must be a plain object of `Type`: `Renderer` pairs'); 388 | } 389 | 390 | var allowedTypes = (opts.allowedTypes && opts.allowedTypes.map(normalizeTypeName)) || coreTypes; 391 | if (opts.disallowedTypes) { 392 | var disallowed = opts.disallowedTypes.map(normalizeTypeName); 393 | allowedTypes = allowedTypes.filter(function filterDisallowed(type) { 394 | return disallowed.indexOf(type) === -1; 395 | }); 396 | } 397 | 398 | return { 399 | sourcePos: Boolean(opts.sourcePos), 400 | softBreak: opts.softBreak || '\n', 401 | renderers: assign({}, defaultRenderers, normalizeRenderers(opts.renderers)), 402 | escapeHtml: Boolean(opts.escapeHtml), 403 | skipHtml: Boolean(opts.skipHtml), 404 | transformLinkUri: linkFilter, 405 | transformImageUri: imageFilter, 406 | allowNode: opts.allowNode, 407 | allowedTypes: allowedTypes, 408 | unwrapDisallowed: Boolean(opts.unwrapDisallowed), 409 | render: renderNodes, 410 | linkTarget: opts.linkTarget || false 411 | }; 412 | } 413 | 414 | ReactRenderer.uriTransformer = defaultLinkUriFilter; 415 | ReactRenderer.types = coreTypes.map(pascalCase); 416 | ReactRenderer.renderers = coreTypes.reduce(function(renderers, type) { 417 | renderers[pascalCase(type)] = defaultRenderers[type]; 418 | return renderers; 419 | }, {}); 420 | 421 | module.exports = ReactRenderer; 422 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-unused-expressions": 0, 4 | "react/prop-types": 0, 5 | "react/no-multi-comp": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/commonmark-react-renderer.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'), 4 | renderHtml = require('react-dom/server'), 5 | commonmark = require('commonmark'), 6 | expect = require('chai').expect, 7 | ReactRenderer = require('../'); 8 | 9 | var parser = new commonmark.Parser(), 10 | reactRenderer = new ReactRenderer(); 11 | 12 | var xssInput = [ 13 | '# [Much fun](javascript:alert("foo"))', 14 | 'Can be had with [XSS links](vbscript:foobar)', 15 | '> And [other](VBSCRIPT:bap) nonsense... [files](file:///etc/passwd) for instance', 16 | '## [Entities](javascript:alert("bazinga")) can be tricky, too' 17 | ].join('\n\n'); 18 | 19 | var CodeBlockComponent = React.createClass({ 20 | displayName: 'CodeBlock', 21 | render: function() { 22 | return React.createElement('pre', null, JSON.stringify(this.props)); 23 | } 24 | }); 25 | 26 | describe('react-markdown', function() { 27 | it('should wrap single-line plain text in a paragraph', function() { 28 | var input = 'React is awesome'; 29 | expect(parse(input)).to.equal('

React is awesome

'); 30 | }); 31 | 32 | it('should handle multiline paragraphs properly (softbreak/hardbreak)', function() { 33 | var input = 'React is awesome\nAnd so is markdown\n\nCombining = epic'; 34 | var expected = '

React is awesome\nAnd so is markdown

Combining = epic

'; 35 | expect(parse(input)).to.equal(expected); 36 | }); 37 | 38 | it('should handle
as softbreak', function() { 39 | var input = 'React is awesome\nAnd so is markdown\n\nCombining = epic'; 40 | var expected = '

React is awesome
And so is markdown

Combining = epic

'; 41 | expect(parse(input, { softBreak: 'br' })).to.equal(expected); 42 | }); 43 | 44 | it('should add a key to generated
children', function() { 45 | var input = 'React is awesome\nAnd so is markdown\n\nCombining = epic'; 46 | var ast = parser.parse(input); 47 | var result = getRenderer({ softBreak: 'br' }).render(ast); 48 | var allHaveKeys = extractChildren(result).every(function(el) { 49 | return el.key !== null; 50 | }); 51 | expect(allHaveKeys).to.equal(true); 52 | }); 53 | 54 | it('should handle multi-space+break as hardbreak', function() { 55 | var input = 'React is awesome \nAnd so is markdown'; 56 | var expected = '

React is awesome
And so is markdown

'; 57 | expect(parse(input)).to.equal(expected); 58 | }); 59 | 60 | it('should handle emphasis', function() { 61 | var input = 'React is _totally_ *awesome*'; 62 | var expected = '

React is totally awesome

'; 63 | expect(parse(input)).to.equal(expected); 64 | }); 65 | 66 | it('should handle bold/strong text', function() { 67 | var input = 'React is **totally** __awesome__'; 68 | var expected = '

React is totally awesome

'; 69 | expect(parse(input)).to.equal(expected); 70 | }); 71 | 72 | it('should handle links without title tags', function() { 73 | var input = 'This is [a link](http://vaffel.ninja/) to VaffelNinja.'; 74 | var expected = '

This is a link to VaffelNinja.

'; 75 | expect(parse(input)).to.equal(expected); 76 | }); 77 | 78 | it('should handle links with title tags', function() { 79 | var input = 'A [link](http://vaffel.ninja "Foo") to the Ninja.'; 80 | var expected = '

A link to the Ninja.

'; 81 | expect(parse(input)).to.equal(expected); 82 | }); 83 | 84 | it('should handle images without title tags', function() { 85 | var input = 'This is ![an image](/ninja.png).'; 86 | var expected = '

This is an image.

'; 87 | expect(parse(input)).to.equal(expected); 88 | }); 89 | 90 | it('should handle images with title tags', function() { 91 | var input = 'This is ![an image](/ninja.png "foo bar").'; 92 | var expected = '

This is an image.

'; 93 | expect(parse(input)).to.equal(expected); 94 | }); 95 | 96 | it('should handle images without special characters in alternative text', function() { 97 | var input = 'This is ![a ninja\'s image](/ninja.png).'; 98 | var expected = '

This is a ninja's image.

'; 99 | expect(parse(input)).to.equal(expected); 100 | }); 101 | 102 | it('should be able to render headers', function() { 103 | expect(parse('# Awesome')).to.equal('

Awesome

'); 104 | expect(parse('## Awesome')).to.equal('

Awesome

'); 105 | expect(parse('### Awesome')).to.equal('

Awesome

'); 106 | expect(parse('#### Awesome')).to.equal('

Awesome

'); 107 | expect(parse('##### Awesome')).to.equal('
Awesome
'); 108 | }); 109 | 110 | it('should handle "inline" code', function() { 111 | var input = '`renderToStaticMarkup()`'; 112 | var expected = '

renderToStaticMarkup()

'; 113 | expect(parse(input)).to.equal(expected); 114 | }); 115 | 116 | it('should handle code tags without any specifications', function() { 117 | var input = '```\nvar foo = require(\'bar\');\nfoo();\n```'; 118 | var expected = '
var foo = require('bar');\nfoo();\n
'; 119 | expect(parse(input)).to.equal(expected); 120 | }); 121 | 122 | it('should handle code tags with language specification', function() { 123 | var input = '```js\nvar foo = require(\'bar\');\nfoo();\n```'; 124 | var expected = [ 125 | '
',
126 |             'var foo = require('bar');\n',
127 |             'foo();\n
' 128 | ].join(''); 129 | 130 | expect(parse(input)).to.equal(expected); 131 | }); 132 | 133 | it('should handle code blocks by indentation', function() { 134 | var input = [ 135 | '', '
\n', '', 136 | '© 2014 Foo Bar\n', '
' 137 | ].join(' '); 138 | 139 | var expected = [ 140 | '
<footer class="footer">\n    ',
141 |             '&copy; 2014 Foo Bar\n</footer>\n
' 142 | ].join(''); 143 | 144 | expect(parse(input)).to.equal(expected); 145 | }); 146 | 147 | it('should handle blockquotes', function() { 148 | var input = '> Moo\n> Tools\n> FTW\n'; 149 | var expected = '

Moo\nTools\nFTW

'; 150 | expect(parse(input)).to.equal(expected); 151 | }); 152 | 153 | it('should handle nested blockquotes', function() { 154 | var input = [ 155 | '> > Lots of ex-Mootoolers on the React team\n>\n', 156 | '> Totally didn\'t know that.\n>\n', 157 | '> > There\'s a reason why it turned out so awesome\n>\n', 158 | '> Haha I guess you\'re right!' 159 | ].join(''); 160 | 161 | var expected = [ 162 | '

Lots of ex-Mootoolers on the React team

', 163 | '

Totally didn't know that.

There's a reason why it ', 164 | 'turned out so awesome

Haha I guess you're right!', 165 | '

' 166 | ].join(''); 167 | 168 | expect(parse(input)).to.equal(expected); 169 | }); 170 | 171 | it('should handle unordered lists', function() { 172 | var input = '* Unordered\n* Lists\n* Are cool\n'; 173 | var expected = '
  • Unordered
  • Lists
  • Are cool
'; 174 | expect(parse(input)).to.equal(expected); 175 | }); 176 | 177 | it('should handle ordered lists', function() { 178 | var input = '1. Ordered\n2. Lists\n3. Are cool\n'; 179 | var expected = '
  1. Ordered
  2. Lists
  3. Are cool
'; 180 | expect(parse(input)).to.equal(expected); 181 | }); 182 | 183 | it('should handle ordered lists with a start index', function() { 184 | var input = '7. Ordered\n8. Lists\n9. Are cool\n'; 185 | var expected = '
  1. Ordered
  2. Lists
  3. Are cool
'; 186 | expect(parse(input)).to.equal(expected); 187 | }); 188 | 189 | it('should handle inline html', function() { 190 | var input = 'I am having so much fun'; 191 | var expected = '

I am having so much fun

'; 192 | expect(parse(input)).to.equal(expected); 193 | }); 194 | 195 | it('should handle inline html with escapeHtml option enabled', function() { 196 | var input = 'I am having so much fun'; 197 | var expected = '

I am having <strong>so</strong> much fun

'; 198 | expect(parse(input, { escapeHtml: true })).to.equal(expected); 199 | }); 200 | 201 | it('should skip inline html when skipHtml option enabled', function() { 202 | var input = 'I am having so much fun'; 203 | var expected = '

I am having so much fun

'; 204 | expect(parse(input, { skipHtml: true })).to.equal(expected); 205 | }); 206 | 207 | it('should handle html blocks', function() { 208 | var input = [ 209 | 'This is a regular paragraph.\n\n\n \n ', 210 | '\n \n
Foo
\n\nThis is another', 211 | ' regular paragraph.' 212 | ].join(''); 213 | 214 | var expected = [ 215 | '

This is a regular paragraph.

\n \n', 216 | ' \n \n
Foo

This is ', 217 | 'another regular paragraph.

' 218 | ].join(''); 219 | 220 | expect(parse(input)).to.equal(expected); 221 | }); 222 | 223 | it('should handle html with escapeHtml option enabled', function() { 224 | var input = [ 225 | 'This is a regular paragraph.\n\n\n \n ', 226 | '\n \n
Foo
\n\nThis is another ', 227 | 'regular paragraph.' 228 | ].join(''); 229 | 230 | var expected = [ 231 | '

This is a regular paragraph.

<table>\n ', 232 | '<tr>\n <td>Foo</td>\n </tr>\n', 233 | '</table>

This is another regular paragraph.

' 234 | ].join(''); 235 | 236 | expect(parse(input, { escapeHtml: true })).to.equal(expected); 237 | }); 238 | 239 | it('should skip html blocks when skipHtml option enabled', function() { 240 | var input = [ 241 | 'This is a regular paragraph.\n\n\n \n ', 242 | '\n \n
Foo
\n\nThis is another ', 243 | 'regular paragraph.' 244 | ].join(''); 245 | 246 | var expected = [ 247 | '

This is a regular paragraph.

', 248 | '

This is another regular paragraph.

' 249 | ].join(''); 250 | 251 | expect(parse(input, { skipHtml: true })).to.equal(expected); 252 | }); 253 | 254 | it('should handle horizontal rules', function() { 255 | var input = 'Foo\n\n------------\n\nBar'; 256 | var expected = '

Foo


Bar

'; 257 | expect(parse(input)).to.equal(expected); 258 | }); 259 | 260 | it('should throw on unknown node type', function() { 261 | var renderer = new ReactRenderer({ allowedTypes: ['FakeType'] }); 262 | expect(function() { 263 | renderer.render({ walker: getFakeWalker }); 264 | }).to.throw(Error, /FakeType/); 265 | }); 266 | 267 | it('should set source position attrs if sourcePos option is set', function() { 268 | var input = 'Foo\n\n------------\n\nBar'; 269 | var expected = [ 270 | '

Foo

', 271 | '
', 272 | '

Bar

' 273 | ].join(''); 274 | 275 | expect(parse(input, { sourcePos: true })).to.equal(expected); 276 | }); 277 | 278 | it('should skip nodes that are not defined as allowed', function() { 279 | var input = '# Header\n\nParagraph\n## New header\n1. List item\n2. List item 2'; 280 | var expected = [ 281 | '

Paragraph

', 282 | '
    ', 283 | '
  1. List item
  2. ', 284 | '
  3. List item 2
  4. ', 285 | '
' 286 | ].join(''); 287 | 288 | expect(parse(input, { allowedTypes: ['Text', 'Paragraph', 'List', 'Item'] })).to.equal(expected); 289 | }); 290 | 291 | it('should skip nodes that are defined as disallowed', function() { 292 | var input = '# Header\n\nParagraph\n## New header\n1. List item\n2. List item 2\n\nFoo'; 293 | var expected = [ 294 | '

Header

', 295 | '

Paragraph

', 296 | '

New header

', 297 | '

Foo

' 298 | ].join(''); 299 | 300 | expect(parse(input, { disallowedTypes: ['List'] })).to.equal(expected); 301 | }); 302 | 303 | it('should unwrap child nodes from disallowed nodes, if unwrapDisallowed option is enabled', function() { 304 | var input = 'Espen *initiated* this, but has had several **contributors**'; 305 | var expected = '

Espen initiated this, but has had several contributors

'; 306 | var expectedRaw = '

Espen initiated this, but has had several contributors

'; 307 | var expectedSkip = '

Espen this, but has had several

'; 308 | 309 | expect(parse(input, { disallowedTypes: ['Emph', 'Strong'], unwrapDisallowed: true })).to.equal(expected); 310 | expect(parse(input, { disallowedTypes: ['Emph', 'Strong'] })).to.equal(expectedSkip); 311 | expect(parse(input, {})).to.equal(expectedRaw); 312 | }); 313 | 314 | describe('should skip nodes that are defined as disallowed', function() { 315 | var samples = { 316 | HtmlInline: { input: 'Foobar', shouldNotContain: 'Foo' }, 317 | HtmlBlock: { input: '
\nvar foo = "bar";\n\n
\nYup', shouldNotContain: 'var foo' }, 318 | Text: { input: 'Zing', shouldNotContain: 'Zing' }, 319 | Paragraph: { input: 'Paragraphs are cool', shouldNotContain: 'Paragraphs are cool' }, 320 | Heading: { input: '# Headers are neat', shouldNotContain: 'Headers are neat' }, 321 | Softbreak: { input: 'Text\nSoftbreak', shouldNotContain: 'Text\nSoftbreak' }, 322 | Hardbreak: { input: 'Text \nHardbreak', shouldNotContain: '
' }, 323 | Link: { input: '[Espen\'s blog](http://espen.codes/) yeh?', shouldNotContain: '' }, 328 | BlockQuote: { input: '> Moo\n> Tools\n> FTW\n', shouldNotContain: 'Header

react-markdown', 397 | ' is a nice helper

Also check out

' 398 | ].join('')); 399 | }); 400 | 401 | it('should be possible to override renderers used for given types', function() { 402 | var input = '# Header\n---\nParagraph a day...\n```js\nvar keepTheDoctor = "away";\n```\n> Foo'; 403 | expect(parse(input, { 404 | renderers: { 405 | Heading: function(props) { 406 | return React.createElement('div', {className: 'level-' + props.level}, props.children); 407 | }, 408 | CodeBlock: CodeBlockComponent 409 | } 410 | }).replace(/"/g, '"')).to.equal([ 411 | '
Header

Paragraph a day...

', 412 | '
{"language":"js","codeinfo":["js"],"literal":',
413 |             '"var keepTheDoctor = \\"away\\";\\n","nodeKey":"4:1-6:27"}
', 414 | '

Foo

' 415 | ].join('')); 416 | }); 417 | 418 | it('should be possible to "unset" renderers by passing null-values for given types', function() { 419 | var input = '# Header\n---\nParagraph a day...\n```js\nvar keepTheDoctor = "away";\n```'; 420 | 421 | expect(function() { 422 | parse(input, { renderers: { Heading: null } }); 423 | }).to.throw(Error, /Heading/); 424 | }); 425 | 426 | it('does not allow javascript, vbscript or file protocols by default', function() { 427 | expect(parse(xssInput)).to.equal([ 428 | '

Much fun

Can be had with ', 429 | 'XSS links

', 430 | 'And other nonsense... ', 431 | 'files for instance

', 432 | 'Entities can be tricky, too

' 433 | ].join('')); 434 | }); 435 | 436 | it('allows disabling built-in link uri filter', function() { 437 | var output = parse(xssInput, { transformLinkUri: null }); 438 | 439 | expect(output).to.equal([ 440 | '

Much fun

Can be had with ', 441 | 'XSS links

', 442 | 'And other nonsense... ', 443 | 'files for instance

', 444 | 'Entities can be tricky, too

' 445 | ].join('')); 446 | }); 447 | 448 | it('allows specifying a custom link uri filter', function() { 449 | var output = parse('[foo](http://snails.r.us/pfft), also [bar](http://foo.bar/)', { 450 | transformLinkUri: function(uri) { 451 | return uri.replace(/snails/g, 'cheetahs'); 452 | } 453 | }); 454 | 455 | expect(output).to.equal( 456 | '

foo, also bar

' 457 | ); 458 | }); 459 | 460 | it('allows specifying a custom image uri filter', function() { 461 | var output = parse('![foo](/pfft.png), also ![bar](/baz.jpg)', { 462 | transformImageUri: function(uri) { 463 | return uri.replace(/pfft.png/g, 'blurp.jpg'); 464 | } 465 | }); 466 | 467 | expect(output).to.equal( 468 | '

foo, also bar

' 469 | ); 470 | }); 471 | 472 | it('exposes a list of available types on the `types`-property', function() { 473 | expect(ReactRenderer.types).to.be.an('array'); 474 | expect(ReactRenderer.types).to.include('ThematicBreak'); 475 | }); 476 | 477 | it('exposes the default renders on the `renderers`-property', function() { 478 | expect(ReactRenderer.renderers.Image).to.be.a('string'); 479 | expect(ReactRenderer.renderers.HtmlBlock).to.be.a('function'); 480 | }); 481 | 482 | it('exposes the default URI-transformer on the `uriTransformer`-property', function() { 483 | expect(ReactRenderer.uriTransformer).to.be.a('function'); 484 | expect(ReactRenderer.uriTransformer('javascript:alert("foo")')) // eslint-disable-line no-script-url 485 | .to.equal('x-javascript:alert("foo")'); 486 | }); 487 | 488 | it('should reduce sibling text nodes into one text node', function() { 489 | var input = 'What does "this" thing turn into?'; 490 | expect(parse(input).replace(/"/g, '"')).to.equal('

What does "this" thing turn into?

'); 491 | }); 492 | 493 | it('should render sub-lists when the parent list item has inline formatting', function() { 494 | var input = [ 495 | '* If you escape or skip the HTML, no `dangerouslySetInnerHTML` is used!', 496 | ' * Yay', 497 | ' * More things', 498 | '* Root level thing' 499 | ].join('\n') + '\n'; 500 | 501 | expect(parse(input)).to.equal([ 502 | '
  • If you escape or skip the HTML, no dangerouslySetInnerHTML is used!', 503 | '
    • Yay
    • More things
  • Root level thing
' 504 | ].join('')); 505 | }); 506 | 507 | describe('should pass datapos and key onto children', function() { 508 | it('lists', function() { 509 | expect(parse('3. Foo\n4. Bar', {sourcePos: true})) 510 | .to.contain('
    '); 511 | }); 512 | 513 | it('codeblocks', function() { 514 | expect(parse('```js\nvar foo = bar;\n```', {sourcePos: true})) 515 | .to.contain('
    ');
    516 |         });
    517 | 
    518 |         it('headings', function() {
    519 |             expect(parse('# Foo', {sourcePos: true})).to.contain('

    '); 520 | expect(parse('## Foo', {sourcePos: true})).to.contain('

    '); 521 | expect(parse('### Foo', {sourcePos: true})).to.contain('

    '); 522 | }); 523 | }); 524 | 525 | describe('should allow data attributes to be passed', function() { 526 | function addDataId(result) { 527 | return React.cloneElement(result[0], {'data-id': 'test'}); 528 | } 529 | 530 | it('lists', function() { 531 | expect(parse('3. Foo\n4. Bar', {}, addDataId)) 532 | .to.contain('
      '); 533 | }); 534 | 535 | it('codeblocks', function() { 536 | expect(parse('```js\nvar foo = bar;\n```', {}, addDataId)) 537 | .to.contain('
      ');
      538 |         });
      539 | 
      540 |         it('headings', function() {
      541 |             expect(parse('# Foo', {}, addDataId)).to.contain('

      '); 542 | expect(parse('## Foo', {}, addDataId)).to.contain('

      '); 543 | expect(parse('### Foo', {}, addDataId)).to.contain('

      '); 544 | }); 545 | }); 546 | 547 | describe('should only pass necessary props onto plain dom element renderers', function() { 548 | it('should pass only children onto blockquote', function() { 549 | expect(parse('> Foo\n> Bar\n> Baz\n')).to.contain('

      Foo'); 550 | }); 551 | 552 | it('should pass only children onto inline code', function() { 553 | expect(parse('`var foo = bar`')).to.contain('var foo = bar'); 554 | }); 555 | 556 | it('should pass children and className onto block code', function() { 557 | expect(parse('```js\nvar foo = "bar"\n```')).to.contain(''); 558 | }); 559 | 560 | it('should pass only children onto em', function() { 561 | expect(parse('react is _clever_, amirite?')).to.contain('clever'); 562 | }); 563 | 564 | it('should pass nothing onto a hardbreak', function() { 565 | expect(parse('React is cool \nAnd so is markdown. Kinda.')).to.contain('
      '); 566 | }); 567 | 568 | it('should pass alt, title and src onto img', function() { 569 | var input = 'This is ![an image](/ninja.png "foo bar").'; 570 | var expected = 'an image'; 571 | expect(parse(input)).to.contain(expected); 572 | }); 573 | 574 | it('should pass no props onto list items', function() { 575 | expect(parse('* Foo\n* Bar\n')).to.contain('

      • '); 576 | }); 577 | 578 | it('should pass no props onto list items', function() { 579 | expect(parse('* Foo\n* Bar\n')).to.contain('
        • '); 580 | }); 581 | 582 | it('should pass children, href and title onto links', function() { 583 | var input = 'A [link](http://vaffel.ninja "Foo") to the Ninja.'; 584 | expect(parse(input)).to.contain('link'); 585 | }); 586 | 587 | it('should insert the target tag if present in configuration', function() { 588 | var input = 'Please go to my [link](http://vaffel.ninja)'; 589 | var target = '_blank'; 590 | var expected = '

          Please go to my link

          '; 591 | expect(parse(input, { linkTarget: target })).to.equal(expected); 592 | }); 593 | 594 | it('should pass only children onto paragraphs', function() { 595 | expect(parse('Foo bar')).to.contain('

          '); 596 | }); 597 | 598 | it('should pass only children onto strong', function() { 599 | expect(parse('**React** strongly')).to.contain('React'); 600 | }); 601 | 602 | it('should pass no props onto horizontal rules', function() { 603 | expect(parse('# Foo\n---\nBar')).to.contain('


          '); 604 | }); 605 | 606 | it('should pass data sourcepos prop if configured', function() { 607 | expect(parse('# Foo\n---\nBar', { sourcePos: true })) 608 | .to.contain('
          '); 609 | }); 610 | }); 611 | }); 612 | 613 | function extractChildren(elements, allChildren) { 614 | var validElements = elements.filter(React.isValidElement); 615 | return validElements.reduce(function(acc, current) { 616 | var children = current.props.children; 617 | return typeof children !== 'undefined' 618 | ? extractChildren(children, acc).concat(current) 619 | : acc.concat(current); 620 | }, allChildren || []); 621 | } 622 | 623 | function getRenderer(opts) { 624 | if (opts) { 625 | return new ReactRenderer(opts); 626 | } 627 | 628 | return reactRenderer; 629 | } 630 | 631 | function getFakeWalker() { 632 | var numRuns = -1; 633 | return { 634 | next: function() { 635 | numRuns++; 636 | 637 | if (numRuns === 0) { 638 | return { 639 | entering: true, 640 | node: { type: 'document' } 641 | }; 642 | } else if (numRuns === 1) { 643 | return { 644 | entering: true, 645 | node: { type: 'FakeType' } 646 | }; 647 | } 648 | 649 | return null; 650 | } 651 | }; 652 | } 653 | 654 | function parse(markdown, opts, modifyResult) { 655 | var ast = parser.parse(markdown); 656 | var result = getRenderer(opts).render(ast); 657 | var maybeModifiedResult = modifyResult ? modifyResult(result) : result; 658 | 659 | var html = renderHtml.renderToStaticMarkup( 660 | React.createElement.apply(React, ['div', null].concat(maybeModifiedResult)) 661 | ); 662 | 663 | return html.substring('
          '.length, html.length - '
          '.length); 664 | } 665 | --------------------------------------------------------------------------------