react-markdown
Version:
Renders Markdown as React components
270 lines (219 loc) • 7.37 kB
JavaScript
;
var React = require('react');
var xtend = require('xtend');
var ReactIs = require('react-is');
var defaultNodePosition = {
start: {
line: 1,
column: 1,
offset: 0
},
end: {
line: 1,
column: 1,
offset: 0
}
};
function astToReact(node, options) {
var parent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
var index = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
var renderer = options.renderers[node.type]; // nodes generated by plugins may not have position data
// much of the code after this point will attempt to access properties of the node.position
// this will set the node position to the parent node's position to prevent errors
if (node.position === undefined) {
node.position = parent.node && parent.node.position || defaultNodePosition;
}
var pos = node.position.start;
var key = [node.type, pos.line, pos.column, index].join('-');
if (!ReactIs.isValidElementType(renderer)) {
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);
function resolveChildren() {
return node.children && node.children.map(function (childNode, i) {
return astToReact(childNode, options, {
node: node,
props: nodeProps
}, i);
});
}
} // eslint-disable-next-line max-params, complexity
function getNodeProps(node, key, opts, renderer, parent, index) {
var props = {
key: key
};
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) {
props['data-sourcepos'] = flattenPosition(node.position);
}
if (opts.rawSourcePos && !isTagRenderer) {
props.sourcePosition = node.position;
} // If `includeNodeIndex` is true, pass node index info to all non-tag renderers
if (opts.includeNodeIndex && parent.node && parent.node.children && !isTagRenderer) {
props.index = parent.node.children.indexOf(node);
props.parentChildCount = parent.node.children.length;
}
var ref = node.identifier !== null && node.identifier !== undefined ? opts.definitions[node.identifier] || {} : null;
switch (node.type) {
case 'root':
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':
props.start = node.start;
props.ordered = node.ordered;
props.tight = !node.loose;
props.depth = node.depth;
break;
case 'listItem':
props.checked = node.checked;
props.tight = !node.loose;
props.ordered = node.ordered;
props.index = node.index;
props.children = getListItemChildren(node, parent).map(function (childNode, i) {
return astToReact(childNode, opts, {
node: node,
props: props
}, i);
});
break;
case 'definition':
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]
});
break;
case 'inlineCode':
props.children = node.value;
props.inline = true;
break;
case 'link':
assignDefined(props, {
title: node.title || undefined,
target: typeof opts.linkTarget === 'function' ? opts.linkTarget(node.url, node.children, node.title) : opts.linkTarget,
href: opts.transformLinkUri ? opts.transformLinkUri(node.url, node.children, node.title) : node.url
});
break;
case 'image':
assignDefined(props, {
alt: node.alt || undefined,
title: node.title || undefined,
src: opts.transformImageUri ? opts.transformImageUri(node.url, node.children, node.title, node.alt) : node.url
});
break;
case 'linkReference':
assignDefined(props, xtend(ref, {
href: opts.transformLinkUri ? opts.transformLinkUri(ref.href) : ref.href
}));
break;
case 'imageReference':
assignDefined(props, {
src: opts.transformImageUri && ref.href ? opts.transformImageUri(ref.href, node.children, ref.title, node.alt) : ref.href,
title: ref.title || undefined,
alt: node.alt || undefined
});
break;
case 'table':
case 'tableHead':
case 'tableBody':
props.columnAlignment = node.align;
break;
case 'tableRow':
props.isHeader = parent.node.type === 'tableHead';
props.columnAlignment = parent.props.columnAlignment;
break;
case 'tableCell':
assignDefined(props, {
isHeader: parent.props.isHeader,
align: parent.props.columnAlignment[index]
});
break;
case 'virtualHtml':
props.tag = node.tag;
break;
case 'html':
// @todo find a better way than this
props.isBlock = node.position.start.line !== node.position.end.line;
props.escapeHtml = opts.escapeHtml;
props.skipHtml = opts.skipHtml;
break;
case 'parsedHtml':
{
var parsedChildren;
if (node.children) {
parsedChildren = node.children.map(function (child, i) {
return astToReact(child, opts, {
node: node,
props: props
}, i);
});
}
props.escapeHtml = opts.escapeHtml;
props.skipHtml = opts.skipHtml;
props.element = mergeNodeChildren(node, parsedChildren);
break;
}
default:
assignDefined(props, xtend(node, {
type: undefined,
position: undefined,
children: undefined
}));
}
if (!isTagRenderer && node.value) {
props.value = node.value;
}
return props;
}
function assignDefined(target, attrs) {
for (var key in attrs) {
if (typeof attrs[key] !== 'undefined') {
target[key] = attrs[key];
}
}
}
function mergeNodeChildren(node, parsedChildren) {
var el = node.element;
if (Array.isArray(el)) {
var Fragment = React.Fragment || 'div';
return React.createElement(Fragment, null, el);
}
if (el.props.children || parsedChildren) {
var children = React.Children.toArray(el.props.children).concat(parsedChildren);
return React.cloneElement(el, null, children);
}
return React.cloneElement(el, null);
}
function flattenPosition(pos) {
return [pos.start.line, ':', pos.start.column, '-', pos.end.line, ':', pos.end.column].map(String).join('');
}
function getListItemChildren(node, parent) {
if (node.loose) {
return node.children;
}
if (parent.node && node.index > 0 && parent.node.children[node.index - 1].loose) {
return node.children;
}
return unwrapParagraphs(node);
}
function unwrapParagraphs(node) {
return node.children.reduce(function (array, child) {
return array.concat(child.type === 'paragraph' ? child.children || [] : [child]);
}, []);
}
module.exports = astToReact;