markdown-to-jsx
Advanced tools
Comparing version 3.1.1 to 4.0.0-beta
@@ -0,1 +1,9 @@ | ||
### 4.0.0-beta (September 16, 2016) | ||
f269a87 [pre 4.0] Drop second argument in function signature | ||
f50219b [Experimental] Begin parsing inline arbitrary HTML | ||
c381ddd Light refactor to pull some functions out of the main closure | ||
--- | ||
### 3.1.1 (September 15, 2016) | ||
@@ -2,0 +10,0 @@ |
467
index.es5.js
@@ -15,2 +15,6 @@ 'use strict'; | ||
var _lodash = require('lodash.get'); | ||
var _lodash2 = _interopRequireDefault(_lodash); | ||
var _unified = require('unified'); | ||
@@ -24,157 +28,346 @@ | ||
var _lodash = require('lodash.get'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
var _lodash2 = _interopRequireDefault(_lodash); | ||
function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); } | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
var BLOCK_ELEMENT_TAGS = ['article', 'header', 'aside', 'hgroup', 'blockquote', 'hr', 'iframe', 'body', 'li', 'map', 'button', 'object', 'canvas', 'ol', 'caption', 'output', 'col', 'p', 'colgroup', 'pre', 'dd', 'progress', 'div', 'section', 'dl', 'table', 'td', 'dt', 'tbody', 'embed', 'textarea', 'fieldset', 'tfoot', 'figcaption', 'th', 'figure', 'thead', 'footer', 'tr', 'form', 'ul', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'video', 'script', 'style']; | ||
var BLOCK_ELEMENT_REGEX = new RegExp('^<(' + BLOCK_ELEMENT_TAGS.join('|') + ')', 'i'); | ||
// [0] === tag, [...] = attribute pairs | ||
var HTML_EXTRACTOR_REGEX = /([-A-Za-z0-9_]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; | ||
var SELF_CLOSING_ELEMENT_TAGS = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']; | ||
var SELF_CLOSING_ELEMENT_REGEX = new RegExp('^<(' + SELF_CLOSING_ELEMENT_TAGS.join('|') + ')', 'i'); | ||
var TEXT_AST_TYPES = ['text', 'textNode']; | ||
var ATTRIBUTE_TO_JSX_PROP_MAP = { | ||
'accept-charset': 'acceptCharset', | ||
'accesskey': 'accessKey', | ||
'allowfullscreen': 'allowFullScreen', | ||
'allowtransparency': 'allowTransparency', | ||
'autocomplete': 'autoComplete', | ||
'autofocus': 'autoFocus', | ||
'autoplay': 'autoPlay', | ||
'cellpadding': 'cellPadding', | ||
'cellspacing': 'cellSpacing', | ||
'charset': 'charSet', | ||
'class': 'className', | ||
'classid': 'classId', | ||
'colspan': 'colSpan', | ||
'contenteditable': 'contentEditable', | ||
'contextmenu': 'contextMenu', | ||
'crossorigin': 'crossOrigin', | ||
'enctype': 'encType', | ||
'for': 'htmlFor', | ||
'formaction': 'formAction', | ||
'formenctype': 'formEncType', | ||
'formmethod': 'formMethod', | ||
'formnovalidate': 'formNoValidate', | ||
'formtarget': 'formTarget', | ||
'frameborder': 'frameBorder', | ||
'hreflang': 'hrefLang', | ||
'http-equiv': 'httpEquiv', | ||
'inputmode': 'inputMode', | ||
'keyparams': 'keyParams', | ||
'keytype': 'keyType', | ||
'marginheight': 'marginHeight', | ||
'marginwidth': 'marginWidth', | ||
'maxlength': 'maxLength', | ||
'mediagroup': 'mediaGroup', | ||
'minlength': 'minLength', | ||
'novalidate': 'noValidate', | ||
'radiogroup': 'radioGroup', | ||
'readonly': 'readOnly', | ||
'rowspan': 'rowSpan', | ||
'spellcheck': 'spellCheck', | ||
'srcdoc': 'srcDoc', | ||
'srclang': 'srcLang', | ||
'srcset': 'srcSet', | ||
'tabindex': 'tabIndex', | ||
'usemap': 'useMap' | ||
}; | ||
var getType = Object.prototype.toString; | ||
var textTypes = ['text', 'textNode']; | ||
function markdownToJSX(markdown) { | ||
var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; | ||
var overrides = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; | ||
function extractDefinitionsFromASTTree(ast, parser) { | ||
function reducer(aggregator, node) { | ||
if (node.type === 'definition' || node.type === 'footnoteDefinition') { | ||
aggregator.definitions[node.identifier] = node; | ||
var definitions = void 0; | ||
var footnotes = void 0; | ||
if (node.type === 'footnoteDefinition') { | ||
if (node.children && node.children.length === 1 && node.children[0].type === 'paragraph') { | ||
node.children[0].children.unshift({ | ||
type: 'textNode', | ||
value: '[' + node.identifier + ']: ' | ||
}); | ||
} /* package the prefix inside the first child */ | ||
function getHTMLNodeTypeFromASTNodeType(node) { | ||
switch (node.type) { | ||
case 'break': | ||
return 'br'; | ||
aggregator.footnotes.push(_react2.default.createElement( | ||
'div', | ||
{ key: node.identifier, id: node.identifier }, | ||
node.value || node.children.map(parser) | ||
)); | ||
} | ||
} | ||
case 'delete': | ||
return 'del'; | ||
return Array.isArray(node.children) ? node.children.reduce(reducer, aggregator) : aggregator; | ||
}; | ||
case 'emphasis': | ||
return 'em'; | ||
return [ast].reduce(reducer, { | ||
definitions: {}, | ||
footnotes: [] | ||
}); | ||
} | ||
case 'footnoteReference': | ||
return 'a'; | ||
function formExtraPropsForHTMLNodeType() { | ||
var props = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; | ||
var ast = arguments[1]; | ||
var definitions = arguments[2]; | ||
case 'heading': | ||
return 'h' + node.depth; | ||
switch (ast.type) { | ||
case 'footnoteReference': | ||
return _extends({}, props, { | ||
href: '#' + ast.identifier | ||
}); | ||
case 'html': | ||
return 'div'; | ||
case 'image': | ||
return _extends({}, props, { | ||
title: ast.title, | ||
alt: ast.alt, | ||
src: ast.url | ||
}); | ||
case 'image': | ||
case 'imageReference': | ||
return 'img'; | ||
case 'imageReference': | ||
return _extends({}, props, { | ||
title: (0, _lodash2.default)(definitions, '[\'' + ast.identifier + '\'].title'), | ||
alt: ast.alt, | ||
src: (0, _lodash2.default)(definitions, '[\'' + ast.identifier + '\'].url') | ||
}); | ||
case 'inlineCode': | ||
return 'code'; | ||
case 'link': | ||
return _extends({}, props, { | ||
title: ast.title, | ||
href: ast.url | ||
}); | ||
case 'link': | ||
case 'linkReference': | ||
return 'a'; | ||
case 'linkReference': | ||
return _extends({}, props, { | ||
title: (0, _lodash2.default)(definitions, '[\'' + ast.identifier + '\'].title'), | ||
href: (0, _lodash2.default)(definitions, '[\'' + ast.identifier + '\'].url') | ||
}); | ||
case 'list': | ||
return node.ordered ? 'ol' : 'ul'; | ||
case 'list': | ||
return _extends({}, props, { | ||
start: ast.start | ||
}); | ||
case 'listItem': | ||
return 'li'; | ||
case 'tableCell': | ||
case 'th': | ||
return _extends({}, props, { | ||
style: { textAlign: ast.align } | ||
}); | ||
} | ||
case 'paragraph': | ||
return 'p'; | ||
return props; | ||
} | ||
case 'root': | ||
return 'div'; | ||
function getHTMLNodeTypeFromASTNodeType(node) { | ||
switch (node.type) { | ||
case 'break': | ||
return 'br'; | ||
case 'tableHeader': | ||
return 'thead'; | ||
case 'delete': | ||
return 'del'; | ||
case 'tableRow': | ||
return 'tr'; | ||
case 'emphasis': | ||
return 'em'; | ||
case 'tableCell': | ||
return 'td'; | ||
case 'footnoteReference': | ||
return 'a'; | ||
case 'thematicBreak': | ||
return 'hr'; | ||
case 'heading': | ||
return 'h' + node.depth; | ||
case 'definition': | ||
case 'footnoteDefinition': | ||
case 'yaml': | ||
return null; | ||
case 'image': | ||
case 'imageReference': | ||
return 'img'; | ||
default: | ||
return node.type; | ||
} | ||
} | ||
case 'inlineCode': | ||
return 'code'; | ||
function formExtraPropsForHTMLNodeType() { | ||
var props = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; | ||
var ast = arguments[1]; | ||
case 'link': | ||
case 'linkReference': | ||
return 'a'; | ||
switch (ast.type) { | ||
case 'footnoteReference': | ||
return _extends({}, props, { | ||
href: '#' + ast.identifier | ||
}); | ||
case 'list': | ||
return node.ordered ? 'ol' : 'ul'; | ||
case 'image': | ||
return _extends({}, props, { | ||
title: ast.title, | ||
alt: ast.alt, | ||
src: ast.url | ||
}); | ||
case 'listItem': | ||
return 'li'; | ||
case 'imageReference': | ||
return _extends({}, props, { | ||
title: (0, _lodash2.default)(definitions, '[\'' + ast.identifier + '\'].title'), | ||
alt: ast.alt, | ||
src: (0, _lodash2.default)(definitions, '[\'' + ast.identifier + '\'].url') | ||
}); | ||
case 'paragraph': | ||
return 'p'; | ||
case 'link': | ||
return _extends({}, props, { | ||
title: ast.title, | ||
href: ast.url | ||
}); | ||
case 'root': | ||
return 'div'; | ||
case 'linkReference': | ||
return _extends({}, props, { | ||
title: (0, _lodash2.default)(definitions, '[\'' + ast.identifier + '\'].title'), | ||
href: (0, _lodash2.default)(definitions, '[\'' + ast.identifier + '\'].url') | ||
}); | ||
case 'tableHeader': | ||
return 'thead'; | ||
case 'list': | ||
return _extends({}, props, { | ||
start: ast.start | ||
}); | ||
case 'tableRow': | ||
return 'tr'; | ||
case 'tableCell': | ||
case 'th': | ||
return _extends({}, props, { | ||
style: { textAlign: ast.align } | ||
}); | ||
case 'tableCell': | ||
return 'td'; | ||
case 'thematicBreak': | ||
return 'hr'; | ||
case 'definition': | ||
case 'footnoteDefinition': | ||
case 'yaml': | ||
return null; | ||
default: | ||
return node.type; | ||
} | ||
} | ||
function seekCellsAndAlignThemIfNecessary(root, alignmentValues) { | ||
var mapper = function mapper(child, index) { | ||
if (child.type === 'tableCell') { | ||
return _extends({}, child, { | ||
align: alignmentValues[index] | ||
}); | ||
} else if (Array.isArray(child.children) && child.children.length) { | ||
return child.children.map(mapper); | ||
} | ||
return props; | ||
return child; | ||
}; | ||
if (Array.isArray(root.children) && root.children.length) { | ||
root.children = root.children.map(mapper); | ||
} | ||
function seekCellsAndAlignThemIfNecessary(root, alignmentValues) { | ||
var mapper = function mapper(child, index) { | ||
if (child.type === 'tableCell') { | ||
return _extends({}, child, { | ||
align: alignmentValues[index] | ||
}); | ||
} else if (Array.isArray(child.children) && child.children.length) { | ||
return child.children.map(mapper); | ||
return root; | ||
} | ||
function attributeValueToJSXPropValue(key, value) { | ||
if (key === 'style') { | ||
return value.split(/;\s?/).reduce(function (styles, kvPair) { | ||
var key = kvPair.slice(0, kvPair.indexOf(':')); | ||
// snake-case to camelCase | ||
// also handles PascalCasing vendor prefixes | ||
var camelCasedKey = key.replace(/(\-[a-z])/g, function (substr) { | ||
return substr[1].toUpperCase(); | ||
}); | ||
// key.length + 1 to skip over the colon | ||
styles[camelCasedKey] = kvPair.slice(key.length + 1).trim(); | ||
return styles; | ||
}, {}); | ||
} | ||
return value; | ||
} | ||
function coalesceInlineHTML(ast) { | ||
function coalescer(node, index, siblings) { | ||
if (node.type === 'html') { | ||
// ignore block-level elements | ||
if (BLOCK_ELEMENT_REGEX.test(node.value)) { | ||
return; | ||
} | ||
return child; | ||
}; | ||
// ignore self-closing or non-content-bearing elements | ||
if (SELF_CLOSING_ELEMENT_REGEX.test(node.value)) { | ||
return; | ||
} | ||
if (Array.isArray(root.children) && root.children.length) { | ||
root.children = root.children.map(mapper); | ||
// are there more html nodes directly after? if so, fold them into the current node | ||
if (index < siblings.length - 1 && siblings[index + 1].type === 'html') { | ||
// further folding is needed | ||
} | ||
var i = index + 1; | ||
var end = void 0; | ||
// where's the end tag? | ||
while (end === undefined && i < siblings.length) { | ||
if (siblings[i].type !== 'html') { | ||
i += 1; | ||
continue; | ||
} | ||
end = siblings[i]; | ||
} | ||
/* all interim elements now become children of the current node, and we splice them (including end tag) | ||
out of the sibling array so they will not be iterated-over by forEach */ | ||
node.children = siblings.slice(index + 1, i); | ||
siblings.splice(index + 1, i - index); | ||
var _node$value$match = node.value.match(HTML_EXTRACTOR_REGEX); | ||
var _node$value$match2 = _toArray(_node$value$match); | ||
var tag = _node$value$match2[0]; | ||
var attributePairs = _node$value$match2.slice(1); | ||
// reassign the current node to whatever its tag is | ||
node.type = tag.toLowerCase(); | ||
// make a best-effort conversion to JSX props | ||
node.props = attributePairs.reduce(function (props, kvPair) { | ||
var valueIndex = kvPair.indexOf('='); | ||
var key = kvPair.slice(0, valueIndex === -1 ? undefined : valueIndex); | ||
// ignoring inline event handlers at this time - they pose enough of a security risk that they're | ||
// not worth preserving; there's a reason React calls it "dangerouslySetInnerHTML"! | ||
if (key.indexOf('on') !== 0) { | ||
var value = kvPair.slice(key.length + 1); | ||
// strip the outermost single/double quote if it exists | ||
if (value[0] === '"' || value[0] === '\'') { | ||
value = value.slice(1, value.length - 1); | ||
} | ||
props[ATTRIBUTE_TO_JSX_PROP_MAP[key] || key] = attributeValueToJSXPropValue(key, value) || true; | ||
} | ||
return props; | ||
}, {}); | ||
// null out .value or astToJSX() will set it as the child | ||
node.value = null; | ||
} | ||
return root; | ||
} | ||
if (node.children) { | ||
node.children.forEach(coalescer); | ||
} | ||
}; | ||
return ast.children.forEach(coalescer); | ||
} | ||
function markdownToJSX(markdown) { | ||
var _ref = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; | ||
var _ref$overrides = _ref.overrides; | ||
var overrides = _ref$overrides === undefined ? {} : _ref$overrides; | ||
var definitions = void 0; | ||
var footnotes = void 0; | ||
function astToJSX(ast, index) { | ||
/* `this` is the dictionary of definitions */ | ||
if (textTypes.indexOf(ast.type) !== -1) { | ||
if (TEXT_AST_TYPES.indexOf(ast.type) !== -1) { | ||
return ast.value; | ||
@@ -279,3 +472,3 @@ } | ||
var props = { key: key }; | ||
var props = _extends({ key: key }, ast.props); | ||
@@ -299,6 +492,6 @@ var override = overrides[htmlNodeType]; | ||
will be overwritten on a key collision) */ | ||
var finalProps = formExtraPropsForHTMLNodeType(props, ast); | ||
var finalProps = formExtraPropsForHTMLNodeType(props, ast, definitions); | ||
if (ast.children && ast.children.length === 1) { | ||
if (textTypes.indexOf(ast.children[0].type) !== -1) { | ||
if (TEXT_AST_TYPES.indexOf(ast.children[0].type) !== -1) { | ||
ast.children = ast.children[0].value; | ||
@@ -313,32 +506,2 @@ } | ||
function extractDefinitionsFromASTTree(ast) { | ||
var reducer = function reducer(aggregator, node) { | ||
if (node.type === 'definition' || node.type === 'footnoteDefinition') { | ||
aggregator.definitions[node.identifier] = node; | ||
if (node.type === 'footnoteDefinition') { | ||
if (node.children && node.children.length === 1 && node.children[0].type === 'paragraph') { | ||
node.children[0].children.unshift({ | ||
type: 'textNode', | ||
value: '[' + node.identifier + ']: ' | ||
}); | ||
} /* package the prefix inside the first child */ | ||
aggregator.footnotes.push(_react2.default.createElement( | ||
'div', | ||
{ key: node.identifier, id: node.identifier }, | ||
node.value || node.children.map(astToJSX) | ||
)); | ||
} | ||
} | ||
return Array.isArray(node.children) ? node.children.reduce(reducer, aggregator) : aggregator; | ||
}; | ||
return [ast].reduce(reducer, { | ||
definitions: {}, | ||
footnotes: [] | ||
}); | ||
} | ||
if (typeof markdown !== 'string') { | ||
@@ -348,15 +511,13 @@ throw new Error('markdown-to-jsx: the first argument must be\n a string'); | ||
if (getType.call(options) !== '[object Object]') { | ||
throw new Error('markdown-to-jsx: the second argument must be\n undefined or an object literal ({}) containing\n valid remark options'); | ||
} | ||
if (getType.call(overrides) !== '[object Object]') { | ||
throw new Error('markdown-to-jsx: the third argument must be\n undefined or an object literal with shape:\n {\n htmltagname: {\n component: string|ReactComponent(optional),\n props: object(optional)\n }\n }'); | ||
throw new Error('markdown-to-jsx: options.overrides (second argument property) must be\n undefined or an object literal with shape:\n {\n htmltagname: {\n component: string|ReactComponent(optional),\n props: object(optional)\n }\n }'); | ||
} | ||
options.position = options.position || false; | ||
options.footnotes = options.footnotes || true; | ||
var remarkAST = (0, _unified2.default)().use(_remarkParse2.default).parse(markdown, { | ||
footnotes: true, | ||
gfm: true, | ||
position: false | ||
}); | ||
var remarkAST = (0, _unified2.default)().use(_remarkParse2.default).parse(markdown, options); | ||
var extracted = extractDefinitionsFromASTTree(remarkAST); | ||
var extracted = extractDefinitionsFromASTTree(remarkAST, astToJSX); | ||
@@ -366,2 +527,4 @@ definitions = extracted.definitions; | ||
coalesceInlineHTML(remarkAST); | ||
var jsx = astToJSX(remarkAST); | ||
@@ -368,0 +531,0 @@ |
@@ -6,3 +6,3 @@ { | ||
"license": "MIT", | ||
"version": "3.1.1", | ||
"version": "4.0.0-beta", | ||
"engines": { | ||
@@ -9,0 +9,0 @@ "node": ">= 4" |
@@ -1,12 +0,15 @@ | ||
# markdown to jsx converter | ||
# markdown to jsx compiler | ||
![build status](https://api.travis-ci.org/yaycmyk/markdown-to-jsx.svg) [![codecov](https://codecov.io/gh/yaycmyk/markdown-to-jsx/branch/master/graph/badge.svg)](https://codecov.io/gh/yaycmyk/markdown-to-jsx) | ||
Enables the safe parsing of markdown into proper React JSX objects, so you don't need to use a pattern like `dangerouslySetInnerHTML` and potentially open your application up to security issues. | ||
The only exception is arbitrary HTML in the markdown (kind of an antipattern), which will still use the unsafe method. | ||
The only exception is arbitrary block-level HTML in the markdown (considered a markdown antipattern), which will still use the unsafe method. | ||
Uses [remark](https://github.com/wooorm/remark) under the hood to parse markdown into a consistent AST format. | ||
Uses [remark-parse](https://github.com/wooorm/remark-parse) under the hood to parse markdown into a consistent AST format. The following [remark](https://github.com/wooorm/remark) settings are set by `markdown-to-jsx`: | ||
- footnotes: true | ||
- gfm: true | ||
- position: false | ||
Requires React >= 0.14. | ||
@@ -16,37 +19,50 @@ | ||
The default export function signature: | ||
```js | ||
import converter from 'markdown-to-jsx'; | ||
compiler(markdown: string, options: object?) | ||
``` | ||
ES6-style usage: | ||
```js | ||
import compiler from 'markdown-to-jsx'; | ||
import React from 'react'; | ||
import {render} from 'react-dom'; | ||
render(converter('# Hello world!'), document.body); | ||
render(compiler('# Hello world!'), document.body); | ||
``` | ||
[remark options](https://github.com/wooorm/remark#remarkprocessvalue-options-done) can be passed as the second argument: | ||
Override a particular HTML tag's output: | ||
```js | ||
converter('* abc\n* def\n* ghi', {bullet: '*'}); | ||
``` | ||
```jsx | ||
import compiler from 'markdown-to-jsx'; | ||
import React from 'react'; | ||
import {render} from 'react-dom'; | ||
_Footnotes are enabled by default as of `markdown-to-jsx@2.0.0`._ | ||
// surprise, it's a div instead! | ||
const MyParagraph = ({children, ...props}) => (<div {...props}>{children}</div>); | ||
## Overriding tags and adding props | ||
render( | ||
compiler('# Hello world!', { | ||
overrides: { | ||
h1: { | ||
component: MyParagraph, | ||
props: { | ||
className: 'foo', | ||
}, | ||
}, | ||
}, | ||
}), document.body | ||
); | ||
As of `markdown-to-jsx@2.0.0`, it's now possible to selectively override a given HTML tag's JSX representation. This is done through a new third argument to the converter: an object made of keys, each being the lowercase html tag name (p, figure, a, etc.) to be overridden. | ||
/* | ||
renders: | ||
Each override can be given a `component` that will be substituted for the tag name and/or `props` that will be applied as you would expect. | ||
```js | ||
converter('Hello there!', {}, { | ||
p: { | ||
component: MyParagraph, | ||
props: { | ||
className: 'foo' | ||
}, | ||
} | ||
}); | ||
<div class="foo"> | ||
Hello World | ||
</div> | ||
*/ | ||
``` | ||
The code above will replace all emitted `<p>` tags with the given component `MyParagraph`, and add the `className` specified in `props`. | ||
Depending on the type of element, there are some props that must be preserved to ensure the markdown is converted as intended. They are: | ||
@@ -62,6 +78,2 @@ | ||
## Known Issues | ||
- remark's handling of arbitrary HTML causes nodes to be split, which causes garbage and malformed HTML - [Bug Ticket](https://github.com/wooorm/remark/issues/124) | ||
MIT |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
29475
423
78
2