Socket
Socket
Sign inDemoInstall

markdown-to-jsx

Package Overview
Dependencies
Maintainers
1
Versions
110
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

markdown-to-jsx - npm Package Compare versions

Comparing version 3.1.1 to 4.0.0-beta

8

CHANGELOG.md

@@ -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
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc