react-markdown
Advanced tools
Comparing version
@@ -5,2 +5,20 @@ # Change Log | ||
## 4.0.0 - 2018-10-03 | ||
### BREAKING | ||
* `text` is now a first-class node + renderer - if you are using `allowedNodes`, it needs to be included in this list. Since it is now a React component, it will be passed an object of props instead of the old approach where a string was passed. `children` will contain the actual text string. | ||
* On React >= 16.2, if no `className` prop is provided, a fragment will be used instead of a div. To always render a div, pass `'div'` as the `root` renderer. | ||
* On React >= 16.2, escaped HTML will no longer be rendered with div/span containers | ||
* The UMD bundle now exports the component as `window.ReactMarkdown` instead of `window.reactMarkdown` | ||
### Added | ||
* HTML parser plugin for full HTML compatibility (Espen Hovlandsdal) | ||
### Fixes | ||
* URI transformer allows uppercase http/https URLs (Liam Kennedy) | ||
* [TypeScript] Strongly type the keys of `renderers` (Linus Unnebäck) | ||
## 3.6.0 - 2018-09-05 | ||
@@ -7,0 +25,0 @@ |
@@ -82,3 +82,3 @@ // Type definitions for react-markdown > v3.3.0 | ||
readonly unwrapDisallowed?: boolean | ||
readonly renderers?: {[nodeType: string]: ReactType} | ||
readonly renderers?: {[nodeType: NodeType]: ReactType} | ||
readonly astPlugins?: MdastPlugin[] | ||
@@ -85,0 +85,0 @@ readonly plugins?: any[] | (() => void) |
'use strict'; | ||
var React = require('react'); | ||
var xtend = require('xtend'); | ||
@@ -9,18 +10,11 @@ | ||
var index = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; | ||
var renderer = options.renderers[node.type]; | ||
var pos = node.position.start; | ||
var key = [node.type, pos.line, pos.column].join('-'); | ||
if (node.type === 'text') { | ||
return renderer ? renderer(node.value, key) : node.value; | ||
} | ||
if (typeof renderer !== 'function' && typeof renderer !== 'string' && !isReactFragment(renderer)) { | ||
throw new Error('Renderer for type `' + node.type + '` not defined or is not renderable'); | ||
throw new Error("Renderer for type `".concat(node.type, "` not defined or is not renderable")); | ||
} | ||
var nodeProps = getNodeProps(node, key, options, renderer, parent, index); | ||
return React.createElement(renderer, nodeProps, nodeProps.children || resolveChildren() || undefined); | ||
@@ -30,3 +24,6 @@ | ||
return node.children && node.children.map(function (childNode, i) { | ||
return astToReact(childNode, options, { node: node, props: nodeProps }, i); | ||
return astToReact(childNode, options, { | ||
node: node, | ||
props: nodeProps | ||
}, i); | ||
}); | ||
@@ -38,11 +35,11 @@ } | ||
return React.Fragment && React.Fragment === renderer; | ||
} | ||
} // eslint-disable-next-line max-params, complexity | ||
// eslint-disable-next-line max-params, complexity | ||
function getNodeProps(node, key, opts, renderer, parent, index) { | ||
var props = { key: key }; | ||
var props = { | ||
key: key | ||
}; | ||
var isTagRenderer = typeof renderer === 'string'; // `sourcePos` is true if the user wants source information (line/column info from markdown source) | ||
var isTagRenderer = typeof renderer === 'string'; | ||
// `sourcePos` is true if the user wants source information (line/column info from markdown source) | ||
if (opts.sourcePos && node.position) { | ||
@@ -54,5 +51,5 @@ props['data-sourcepos'] = flattenPosition(node.position); | ||
props.sourcePosition = node.position; | ||
} | ||
} // If `includeNodeIndex` is true, pass node index info to all non-tag renderers | ||
// If `includeNodeIndex` is true, pass node index info to all non-tag renderers | ||
if (opts.includeNodeIndex && parent.node && parent.node.children && !isTagRenderer) { | ||
@@ -67,7 +64,16 @@ props.index = parent.node.children.indexOf(node); | ||
case 'root': | ||
assignDefined(props, { className: opts.className }); | ||
assignDefined(props, { | ||
className: opts.className | ||
}); | ||
break; | ||
case 'text': | ||
props.nodeKey = key; | ||
props.children = node.value; | ||
break; | ||
case 'heading': | ||
props.level = node.depth; | ||
break; | ||
case 'list': | ||
@@ -79,2 +85,3 @@ props.start = node.start; | ||
break; | ||
case 'listItem': | ||
@@ -86,11 +93,23 @@ props.checked = node.checked; | ||
props.children = (props.tight ? unwrapParagraphs(node) : node.children).map(function (childNode, i) { | ||
return astToReact(childNode, opts, { node: node, props: props }, i); | ||
return astToReact(childNode, opts, { | ||
node: node, | ||
props: props | ||
}, i); | ||
}); | ||
break; | ||
case 'definition': | ||
assignDefined(props, { identifier: node.identifier, title: node.title, url: node.url }); | ||
assignDefined(props, { | ||
identifier: node.identifier, | ||
title: node.title, | ||
url: node.url | ||
}); | ||
break; | ||
case 'code': | ||
assignDefined(props, { language: node.lang && node.lang.split(/\s/, 1)[0] }); | ||
assignDefined(props, { | ||
language: node.lang && node.lang.split(/\s/, 1)[0] | ||
}); | ||
break; | ||
case 'inlineCode': | ||
@@ -100,2 +119,3 @@ props.children = node.value; | ||
break; | ||
case 'link': | ||
@@ -108,2 +128,3 @@ assignDefined(props, { | ||
break; | ||
case 'image': | ||
@@ -116,2 +137,3 @@ assignDefined(props, { | ||
break; | ||
case 'linkReference': | ||
@@ -122,2 +144,3 @@ assignDefined(props, xtend(ref, { | ||
break; | ||
case 'imageReference': | ||
@@ -130,2 +153,3 @@ assignDefined(props, { | ||
break; | ||
case 'table': | ||
@@ -136,2 +160,3 @@ case 'tableHead': | ||
break; | ||
case 'tableRow': | ||
@@ -141,2 +166,3 @@ props.isHeader = parent.node.type === 'tableHead'; | ||
break; | ||
case 'tableCell': | ||
@@ -148,5 +174,7 @@ assignDefined(props, { | ||
break; | ||
case 'virtualHtml': | ||
props.tag = node.tag; | ||
break; | ||
case 'html': | ||
@@ -158,2 +186,14 @@ // @todo find a better way than this | ||
break; | ||
case 'parsedHtml': | ||
props.escapeHtml = opts.escapeHtml; | ||
props.skipHtml = opts.skipHtml; | ||
props.element = mergeNodeChildren(node, (node.children || []).map(function (child, i) { | ||
return astToReact(child, opts, { | ||
node: node, | ||
props: props | ||
}, i); | ||
})); | ||
break; | ||
default: | ||
@@ -182,2 +222,14 @@ assignDefined(props, xtend(node, { | ||
function mergeNodeChildren(node, parsedChildren) { | ||
var el = node.element; | ||
if (Array.isArray(el)) { | ||
var Fragment = React.Fragment || 'div'; | ||
return React.createElement(Fragment, null, el); | ||
} | ||
var children = (el.props.children || []).concat(parsedChildren); | ||
return React.cloneElement(el, null, children); | ||
} | ||
function flattenPosition(pos) { | ||
@@ -184,0 +236,0 @@ return [pos.start.line, ':', pos.start.column, '-', pos.end.line, ':', pos.end.column].map(String).join(''); |
@@ -5,3 +5,2 @@ 'use strict'; | ||
var defs = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
return (node.children || []).reduce(function (definitions, child) { | ||
@@ -8,0 +7,0 @@ if (child.type === 'definition') { |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -3,0 +3,0 @@ var visit = require('unist-util-visit'); |
@@ -1,2 +0,2 @@ | ||
'use strict'; | ||
"use strict"; | ||
@@ -15,4 +15,4 @@ /** | ||
module.exports = function (tree) { | ||
var open = void 0; | ||
var currentParent = void 0; | ||
var open; | ||
var currentParent; | ||
visit(tree, 'html', function (node, index, parent) { | ||
@@ -25,2 +25,3 @@ if (currentParent !== parent) { | ||
var selfClosing = getSelfClosing(node); | ||
if (selfClosing) { | ||
@@ -36,2 +37,3 @@ parent.children.splice(index, 1, { | ||
var current = getSimpleTag(node, parent); | ||
if (!current) { | ||
@@ -52,3 +54,2 @@ return true; | ||
); | ||
return tree; | ||
@@ -59,2 +60,3 @@ }; | ||
var i = open.length; | ||
while (i--) { | ||
@@ -71,3 +73,7 @@ if (open[i].tag === matchingTag) { | ||
var match = node.value.match(simpleTagRe); | ||
return match ? { tag: match[2], opening: !match[1], node: node } : false; | ||
return match ? { | ||
tag: match[2], | ||
opening: !match[1], | ||
node: node | ||
} : false; | ||
} | ||
@@ -83,3 +89,2 @@ | ||
var toIndex = parent.children.indexOf(toNode.node); | ||
var extracted = parent.children.splice(fromIndex, toIndex - fromIndex + 1); | ||
@@ -86,0 +91,0 @@ var children = extracted.slice(1, -1); |
'use strict'; | ||
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } | ||
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } | ||
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } | ||
function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } | ||
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } | ||
var xtend = require('xtend'); | ||
var unified = require('unified'); | ||
var parse = require('remark-parse'); | ||
var PropTypes = require('prop-types'); | ||
var addListMetadata = require('mdast-add-list-metadata'); | ||
var naiveHtml = require('./plugins/naive-html'); | ||
var disallowNode = require('./plugins/disallow-node'); | ||
var astToReact = require('./ast-to-react'); | ||
var wrapTableRows = require('./wrap-table-rows'); | ||
var getDefinitions = require('./get-definitions'); | ||
var uriTransformer = require('./uriTransformer'); | ||
var uriTransformer = require('./uri-transformer'); | ||
var defaultRenderers = require('./renderers'); | ||
var symbols = require('./symbols'); | ||
var allTypes = Object.keys(defaultRenderers); | ||
@@ -28,6 +47,4 @@ | ||
var renderers = xtend(defaultRenderers, props.renderers); | ||
var plugins = [parse].concat(props.plugins || []); | ||
var parser = plugins.reduce(applyParserPlugin, unified()); | ||
var rawAst = parser.parse(src); | ||
@@ -38,3 +55,2 @@ var renderProps = xtend(props, { | ||
}); | ||
var astPlugins = determineAstPlugins(props); | ||
@@ -44,3 +60,2 @@ var ast = astPlugins.reduce(function (node, plugin) { | ||
}, rawAst); | ||
return astToReact(ast, renderProps); | ||
@@ -55,4 +70,4 @@ }; | ||
var plugins = [wrapTableRows, addListMetadata()]; | ||
var disallowedTypes = props.disallowedTypes; | ||
var disallowedTypes = props.disallowedTypes; | ||
if (props.allowedTypes) { | ||
@@ -65,2 +80,3 @@ disallowedTypes = allTypes.filter(function (type) { | ||
var removalMethod = props.unwrapDisallowed ? 'unwrap' : 'remove'; | ||
if (disallowedTypes && disallowedTypes.length > 0) { | ||
@@ -75,3 +91,8 @@ plugins.push(disallowNode.ofType(disallowedTypes, removalMethod)); | ||
var renderHtml = !props.escapeHtml && !props.skipHtml; | ||
if (renderHtml) { | ||
var hasHtmlParser = (props.astPlugins || []).some(function (item) { | ||
var plugin = Array.isArray(item) ? item[0] : item; | ||
return plugin.identity === symbols.HtmlParser; | ||
}); | ||
if (renderHtml && !hasHtmlParser) { | ||
plugins.push(naiveHtml); | ||
@@ -93,3 +114,2 @@ } | ||
}; | ||
ReactMarkdown.propTypes = { | ||
@@ -114,7 +134,5 @@ className: PropTypes.string, | ||
}; | ||
ReactMarkdown.types = allTypes; | ||
ReactMarkdown.renderers = defaultRenderers; | ||
ReactMarkdown.uriTransformer = uriTransformer; | ||
module.exports = ReactMarkdown; |
@@ -5,7 +5,7 @@ /* eslint-disable react/prop-types, react/no-multi-comp */ | ||
var xtend = require('xtend'); | ||
var React = require('react'); | ||
var createElement = React.createElement; | ||
module.exports = { | ||
root: 'div', | ||
break: 'br', | ||
@@ -27,3 +27,4 @@ paragraph: 'p', | ||
tableCell: TableCell, | ||
root: Root, | ||
text: TextRenderer, | ||
list: List, | ||
@@ -36,5 +37,16 @@ listItem: ListItem, | ||
html: Html, | ||
virtualHtml: VirtualHtml | ||
virtualHtml: VirtualHtml, | ||
parsedHtml: ParsedHtml | ||
}; | ||
function TextRenderer(props) { | ||
return props.children; | ||
} | ||
function Root(props) { | ||
var useFragment = !props.className; | ||
var root = useFragment ? React.Fragment || 'div' : 'div'; | ||
return createElement(root, useFragment ? null : props, props.children); | ||
} | ||
function SimpleRenderer(tag, props) { | ||
@@ -45,9 +57,13 @@ return createElement(tag, getCoreProps(props), props.children); | ||
function TableCell(props) { | ||
var style = props.align ? { textAlign: props.align } : undefined; | ||
var style = props.align ? { | ||
textAlign: props.align | ||
} : undefined; | ||
var coreProps = getCoreProps(props); | ||
return createElement(props.isHeader ? 'th' : 'td', style ? xtend({ style: style }, coreProps) : coreProps, props.children); | ||
return createElement(props.isHeader ? 'th' : 'td', style ? xtend({ | ||
style: style | ||
}, coreProps) : coreProps, props.children); | ||
} | ||
function Heading(props) { | ||
return createElement('h' + props.level, getCoreProps(props), props.children); | ||
return createElement("h".concat(props.level), getCoreProps(props), props.children); | ||
} | ||
@@ -57,2 +73,3 @@ | ||
var attrs = getCoreProps(props); | ||
if (props.start !== null && props.start !== 1) { | ||
@@ -67,5 +84,10 @@ attrs.start = props.start.toString(); | ||
var checkbox = null; | ||
if (props.checked !== null) { | ||
var checked = props.checked; | ||
checkbox = createElement('input', { type: 'checkbox', checked: checked, readOnly: true }); | ||
checkbox = createElement('input', { | ||
type: 'checkbox', | ||
checked: checked, | ||
readOnly: true | ||
}); | ||
} | ||
@@ -77,4 +99,6 @@ | ||
function CodeBlock(props) { | ||
var className = props.language && 'language-' + props.language; | ||
var code = createElement('code', className ? { className: className } : null, props.value); | ||
var className = props.language && "language-".concat(props.language); | ||
var code = createElement('code', className ? { | ||
className: className | ||
} : null, props.value); | ||
return createElement('pre', getCoreProps(props), code); | ||
@@ -93,11 +117,22 @@ } | ||
var tag = props.isBlock ? 'div' : 'span'; | ||
if (props.escapeHtml) { | ||
// @todo when fiber lands, we can simply render props.value | ||
return createElement(tag, null, props.value); | ||
var comp = React.Fragment || tag; | ||
return createElement(comp, null, props.value); | ||
} | ||
var nodeProps = { dangerouslySetInnerHTML: { __html: props.value } }; | ||
var nodeProps = { | ||
dangerouslySetInnerHTML: { | ||
__html: props.value | ||
} | ||
}; | ||
return createElement(tag, nodeProps); | ||
} | ||
function ParsedHtml(props) { | ||
return props['data-sourcepos'] ? React.cloneElement(props.element, { | ||
'data-sourcepos': props['data-sourcepos'] | ||
}) : props.element; | ||
} | ||
function VirtualHtml(props) { | ||
@@ -112,3 +147,5 @@ return createElement(props.tag, getCoreProps(props), props.children); | ||
function getCoreProps(props) { | ||
return props['data-sourcepos'] ? { 'data-sourcepos': props['data-sourcepos'] } : {}; | ||
return props['data-sourcepos'] ? { | ||
'data-sourcepos': props['data-sourcepos'] | ||
} : {}; | ||
} |
@@ -18,2 +18,3 @@ 'use strict'; | ||
}]; | ||
if (children.length > 1) { | ||
@@ -20,0 +21,0 @@ table.children.push({ |
{ | ||
"name": "react-markdown", | ||
"description": "Renders Markdown as React components", | ||
"version": "3.6.0", | ||
"version": "4.0.0", | ||
"keywords": [ | ||
@@ -13,2 +13,3 @@ "markdown", | ||
"scripts": { | ||
"analyze": "npm run clean && npm run compile && NODE_ENV=production ANALYZE_BUNDLE=1 webpack -p", | ||
"build": "npm run clean && npm run compile && NODE_ENV=production webpack -p && npm run build:demo", | ||
@@ -24,3 +25,4 @@ "build:demo": "NODE_ENV=production webpack -p --config webpack.config.demo.js", | ||
"test": "jest --coverage", | ||
"watch": "webpack --watch" | ||
"watch": "webpack --watch", | ||
"watch:demo": "webpack --watch --config webpack.config.demo.js" | ||
}, | ||
@@ -41,2 +43,3 @@ "repository": { | ||
"dependencies": { | ||
"html-to-react": "^1.3.3", | ||
"mdast-add-list-metadata": "1.0.1", | ||
@@ -50,24 +53,28 @@ "prop-types": "^15.6.1", | ||
"devDependencies": { | ||
"babel-cli": "^6.26.0", | ||
"babel-loader": "^7.1.3", | ||
"babel-plugin-transform-react-remove-prop-types": "^0.4.13", | ||
"babel-preset-env": "^1.6.1", | ||
"babel-preset-react": "^6.24.1", | ||
"eslint": "^4.18.2", | ||
"eslint-config-prettier": "^2.9.0", | ||
"@babel/cli": "^7.1.0", | ||
"@babel/core": "^7.1.0", | ||
"@babel/preset-env": "^7.1.0", | ||
"@babel/preset-react": "^7.0.0", | ||
"babel-core": "^7.0.0-bridge.0", | ||
"babel-jest": "^23.6.0", | ||
"babel-loader": "^8.0.2", | ||
"babel-plugin-transform-react-remove-prop-types": "^0.4.18", | ||
"eslint": "^5.6.0", | ||
"eslint-config-prettier": "^3.1.0", | ||
"eslint-config-sanity": "^4.0.2", | ||
"eslint-plugin-react": "^7.7.0", | ||
"gh-pages-deploy": "^0.4.2", | ||
"jest": "^22.4.2", | ||
"prettier": "^1.11.1", | ||
"react": "^16.2.0", | ||
"gh-pages-deploy": "^0.5.0", | ||
"jest": "^23.6.0", | ||
"prettier": "^1.14.3", | ||
"react": "^16.5.2", | ||
"react-addons-test-utils": "^15.6.2", | ||
"react-dom": "^16.2.0", | ||
"react-test-renderer": "^16.2.0", | ||
"react-dom": "^16.5.2", | ||
"react-test-renderer": "^16.5.2", | ||
"remark-breaks": "^1.0.0", | ||
"remark-shortcodes": "^0.1.5", | ||
"remark-shortcodes": "^0.2.1", | ||
"rimraf": "^2.6.2", | ||
"uglify-js": "^3.4.5", | ||
"webpack": "^4.1.0", | ||
"webpack-cli": "^2.0.10" | ||
"webpack": "^4.20.2", | ||
"webpack-bundle-analyzer": "^3.0.2", | ||
"webpack-cli": "^3.1.1" | ||
}, | ||
@@ -74,0 +81,0 @@ "peerDependencies": { |
@@ -41,2 +41,10 @@ # react-markdown | ||
## Upgrading to 4.0 | ||
Should be straightforward. You might need to alter you code slightly if you: | ||
- Are using `allowedTypes` (add `text` to the list) | ||
- Rely on there being a container `<div>` without a class name around your rendered markdown | ||
See [CHANGELOG](CHANGELOG.md) for more details. | ||
## Notes | ||
@@ -47,13 +55,7 @@ | ||
## Inline HTML is broken | ||
Inline HTML is currently broken for any tags that include attributes. A vague idea of how to fix | ||
this has been planned, but if you're feeling up to the task, create an issue and let us know! | ||
## Options | ||
* `source` or `children` - _string_ The Markdown source to parse (**required**) | ||
* `className` - _string_ Class name of the container element (default: `''`). | ||
* `escapeHtml` - _boolean_ Setting to `false` will cause HTML to be rendered (see note above about | ||
broken HTML, though). Be aware that setting this to `false` might cause security issues if the | ||
* `className` - _string_ Class name of the container element. If none is passed, a container will not be rendered. | ||
* `escapeHtml` - _boolean_ Setting to `false` will cause HTML to be rendered (see notes below about proper HTML support). Be aware that setting this to `false` might cause security issues if the | ||
input is user-generated. Use at your own risk. (default: `true`). | ||
@@ -93,6 +95,41 @@ * `skipHtml` - _boolean_ Setting to `true` will skip inlined and blocks of HTML (default: `false`). | ||
varies based on the type of node. | ||
* With one exception: if the key is `text`, the value should be a function that takes the literal text and returns a new string or React element. | ||
* `plugins` - _array_ An array of unified/remark parser plugins. If you need to pass options to the plugin, pass an array with two elements, the first being the plugin and the second being the options - for instance: `{plugins: [[require('remark-shortcodes'), {your: 'options'}]]`. (default: `[]`) | ||
## Parsing HTML | ||
If you are in a trusted environment and want to parse and render HTML, you will want to use the `html-parser` plugin. For a default configuration, import `react-markdown/with-html` instead of the default: | ||
```js | ||
const ReactMarkdown = require('react-markdown/with-html') | ||
const markdown = ` | ||
This block of Markdown contains <a href="https://en.wikipedia.org/wiki/HTML">HTML</a>, and will require the <code>html-parser</code> AST plugin to be loaded, in addition to setting the <code class="prop">escapeHtml</code> property to false. | ||
` | ||
<ReactMarkdown | ||
source={markdown} | ||
escapeHtml={false} | ||
> | ||
``` | ||
If you want to specify options for the HTML parsing step, you can instead import the HTML parser plugin directly: | ||
```js | ||
const ReactMarkdown = require('react-markdown') | ||
const htmlParser = require('react-markdown/plugins/html-parser') | ||
// See https://github.com/aknuds1/html-to-react#with-custom-processing-instructions | ||
// for more info on the processing instructions | ||
const parseHtml = htmlParser({ | ||
isValidNode: node => node.type !== 'script', | ||
processingInstructions: [/* ... */] | ||
}) | ||
<ReactMarkdown | ||
source={markdown} | ||
escapeHtml={false} | ||
astPlugins={[parseHtml]} | ||
> | ||
``` | ||
## Node types | ||
@@ -128,2 +165,4 @@ | ||
* `html` - HTML node (Best-effort rendering) | ||
* `virtualHtml` - When not using the HTML parser plugin, a cheap and dirty approach to supporting simple HTML elements without a complete parser. | ||
* `parsedHtml` - When using the HTML parser plugin, HTML parsed to a React element. | ||
@@ -130,0 +169,0 @@ Note: Disallowing a node will also prevent the rendering of any children of that node, unless the |
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
21
31.25%955
35.27%181
27.46%114442
-14.17%8
14.29%26
18.18%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added