diff --git a/CHANGELOG.md b/CHANGELOG.md index b498653b..f38ec114 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # markdown-to-jsx ## Changelog +### 2.0.0 (March 20, 2016) + +- Add jsnext:main field (5597aa4f745e9e57c9b84b250f8d13df1bba72cc) +- Update to remark 4.x (cfa49465508dd16e68915657fcf4626d5b758e3d) +- Add tag & props override functionality (ca7271b0139400e768a482083f6a4392c4d6be77) +- Give a proper warning if improper arguments are passed (4adcd455a6ba3d6046ccd3da1ee16b8a92291b86) +- Enable footnotes support by default (8764bb181f1f0f8d510cc74d0b417c90698e11b5) + ### 1.2.0 (January 4, 2016) - Upgrade mdast 2.x to remark 3.x (same project, rewritten internals + changed name) diff --git a/LICENSE b/LICENSE index 7dde0f8c..6efb1dc4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Evan Jacobs +Copyright (c) 2015-present Evan Jacobs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 9ec74cb3..7270d9bb 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,40 @@ render(converter('# Hello world!'), document.body); [remark options](https://github.com/wooorm/remark#remarkprocessvalue-options-done) can be passed as the second argument: ```js -converter('# Hello world[^2]!\n\n[^2]: A beautiful place.', {footnotes: true}); +converter('* abc\n* def\n* ghi', {bullet: '*'}); ``` +_Footnotes are enabled by default as of `markdown-to-jsx@2.0.0`._ + +## Overriding tags and adding props + +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. + +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' + }, + } +}); +``` + +The code above will replace all emitted `
` 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: + +- `a`: `title`, `href` +- `img`: `title`, `alt`, `src` +- `ol`: `start` +- `td`: `style` +- `th`: `style` + +Any conflicts between passed `props` and the specific properties above will be resolved in favor of `markdown-to-jsx`'s code. + ## Known Issues - remark's handling of lists will sometimes add a child paragraph tag inside the diff --git a/__tests__/index.js b/__tests__/index.js index 5b7e1d54..5510e26b 100644 --- a/__tests__/index.js +++ b/__tests__/index.js @@ -8,6 +8,40 @@ describe('markdown-to-jsx', () => { afterEach(() => ReactDOM.unmountComponentAtNode(mountNode)); + it('should throw if not passed a string (first arg)', () => { + expect(() => converter('')).not.toThrow(); + + expect(() => converter()).toThrow(); + expect(() => converter(1)).toThrow(); + expect(() => converter(function(){})).toThrow(); + expect(() => converter({})).toThrow(); + expect(() => converter([])).toThrow(); + expect(() => converter(null)).toThrow(); + expect(() => converter(true)).toThrow(); + }); + + it('should throw if not passed an object or undefined (second arg)', () => { + expect(() => converter('')).not.toThrow(); + expect(() => converter('', {})).not.toThrow(); + + expect(() => converter('', 1)).toThrow(); + expect(() => converter('', function(){})).toThrow(); + expect(() => converter('', [])).toThrow(); + expect(() => converter('', null)).toThrow(); + expect(() => converter('', true)).toThrow(); + }); + + it('should throw if not passed an object or undefined (third arg)', () => { + expect(() => converter('', {})).not.toThrow(); + expect(() => converter('', {}, {})).not.toThrow(); + + expect(() => converter('', {}, 1)).toThrow(); + expect(() => converter('', {}, function(){})).toThrow(); + expect(() => converter('', {}, [])).toThrow(); + expect(() => converter('', {}, null)).toThrow(); + expect(() => converter('', {}, true)).toThrow(); + }); + it('should handle a basic string', () => { const element = render(converter('Hello.')); const elementNode = ReactDOM.findDOMNode(element); @@ -520,7 +554,7 @@ describe('markdown-to-jsx', () => { 'foo[^abc] bar', '', '[^abc]: Baz baz', - ].join('\n'), {footnotes: true})); + ].join('\n'))); const elementNode = ReactDOM.findDOMNode(element); @@ -546,7 +580,7 @@ describe('markdown-to-jsx', () => { 'foo[^abc] bar', '', '[^abc]: Baz baz', - ].join('\n'), {footnotes: true})); + ].join('\n'))); const elementNode = ReactDOM.findDOMNode(element); const definitions = elementNode.children[1]; @@ -563,7 +597,7 @@ describe('markdown-to-jsx', () => { 'foo[^abc] bar', '', '[^abc]: Baz', - ].join('\n'), {footnotes: true})); + ].join('\n'))); const elementNode = ReactDOM.findDOMNode(element); const definitions = elementNode.children[1]; @@ -575,4 +609,29 @@ describe('markdown-to-jsx', () => { expect(definitions.children[0].textContent).toBe('[abc]: Baz'); }); }); + + describe('overrides', () => { + it('should substitute the appropriate JSX tag if given a component', () => { + const FakeParagraph = (props) =>
{props.children}
; + const element = render( + converter('Hello.', {}, {p: {component: FakeParagraph}}) + ); + + const elementNode = ReactDOM.findDOMNode(element); + + expect(elementNode.children.length).toBe(1); + expect(elementNode.children[0].className).toBe('foo'); + }); + + it('should add props to the appropriate JSX tag if supplied', () => { + const element = render( + converter('Hello.', {}, {p: {props: {className: 'abc'}}}) + ); + + const elementNode = ReactDOM.findDOMNode(element); + + expect(elementNode.children.length).toBe(1); + expect(elementNode.children[0].className).toBe('abc'); + }); + }); }); diff --git a/index.js b/index.js index f37af85d..ce0db293 100644 --- a/index.js +++ b/index.js @@ -1,312 +1,356 @@ import React from 'react'; import {parse} from 'remark'; +const getType = Object.prototype.toString; const textTypes = ['text', 'textNode']; -let definitions; -let footnotes; +export default function markdownToJSX(markdown, options = {}, overrides = {}) { + let definitions; + let footnotes; -function getHTMLNodeTypeFromASTNodeType(node) { - switch (node.type) { - case 'break': - return 'br'; + function getHTMLNodeTypeFromASTNodeType(node) { + switch (node.type) { + case 'break': + return 'br'; - case 'delete': - return 'del'; + case 'delete': + return 'del'; - case 'emphasis': - return 'em'; + case 'emphasis': + return 'em'; - case 'footnoteReference': - return 'a'; + case 'footnoteReference': + return 'a'; - case 'heading': - return `h${node.depth}`; + case 'heading': + return `h${node.depth}`; - case 'horizontalRule': - return 'hr'; + case 'html': + return 'div'; - case 'html': - return 'div'; + case 'image': + case 'imageReference': + return 'img'; - case 'image': - case 'imageReference': - return 'img'; + case 'inlineCode': + return 'code'; - case 'inlineCode': - return 'code'; + case 'link': + case 'linkReference': + return 'a'; - case 'link': - case 'linkReference': - return 'a'; + case 'list': + return node.ordered ? 'ol' : 'ul'; - case 'list': - return node.ordered ? 'ol' : 'ul'; + case 'listItem': + return 'li'; - case 'listItem': - return 'li'; + case 'paragraph': + return 'p'; - case 'paragraph': - return 'p'; + case 'root': + return 'div'; - case 'root': - return 'div'; + case 'tableHeader': + return 'thead'; - case 'tableHeader': - return 'thead'; + case 'tableRow': + return 'tr'; - case 'tableRow': - return 'tr'; + case 'tableCell': + return 'td'; - case 'tableCell': - return 'td'; + case 'thematicBreak': + return 'hr'; - case 'definition': - case 'footnoteDefinition': - case 'yaml': - return null; + case 'definition': + case 'footnoteDefinition': + case 'yaml': + return null; - default: - return node.type; + default: + return node.type; + } } -} -function formExtraPropsForHTMLNodeType(props = {}, ast) { - switch (ast.type) { - case 'footnoteReference': - return { - ...props, - href: `#${ast.identifier}`, - }; - - case 'image': - return { - ...props, - title: ast.title, - alt: ast.alt, - src: ast.src, - }; - - case 'imageReference': - return { - ...props, - title: definitions[ast.identifier].title, - alt: ast.alt, - src: definitions[ast.identifier].link, - }; + function formExtraPropsForHTMLNodeType(props = {}, ast) { + switch (ast.type) { + case 'footnoteReference': + return { + ...props, + href: `#${ast.identifier}`, + }; - case 'link': - return { - ...props, - title: ast.title, - href: ast.href, - }; + case 'image': + return { + ...props, + title: ast.title, + alt: ast.alt, + src: ast.url, + }; - case 'linkReference': - return { - ...props, - title: definitions[ast.identifier].title, - href: definitions[ast.identifier].link, - }; + case 'imageReference': + return { + ...props, + title: definitions[ast.identifier].title, + alt: ast.alt, + src: definitions[ast.identifier].url, + }; - case 'list': - return { - ...props, - start: ast.start, - }; + case 'link': + return { + ...props, + title: ast.title, + href: ast.url, + }; - case 'tableCell': - case 'th': - return { - ...props, - style: {textAlign: ast.align}, - }; - } + case 'linkReference': + return { + ...props, + title: definitions[ast.identifier].title, + href: definitions[ast.identifier].url, + }; - return props; -} + case 'list': + return { + ...props, + start: ast.start, + }; -function seekCellsAndAlignThemIfNecessary(root, alignmentValues) { - const mapper = (child, index) => { - if (child.type === 'tableCell') { + case 'tableCell': + case 'th': return { - ...child, - align: alignmentValues[index], + ...props, + style: {textAlign: ast.align}, }; - } else if (Array.isArray(child.children) && child.children.length) { - return child.children.map(mapper); } - return child; - }; - - if (Array.isArray(root.children) && root.children.length) { - root.children = root.children.map(mapper); + return props; } - return root; -} - -function astToJSX(ast, index) { /* `this` is the dictionary of definitions */ - if (textTypes.indexOf(ast.type) !== -1) { - return ast.value; - } + function seekCellsAndAlignThemIfNecessary(root, alignmentValues) { + const mapper = (child, index) => { + if (child.type === 'tableCell') { + return { + ...child, + align: alignmentValues[index], + }; + } else if (Array.isArray(child.children) && child.children.length) { + return child.children.map(mapper); + } - const key = index || '0'; + return child; + }; - if (ast.type === 'code') { - return ( -
-
- {ast.value}
-
-
- );
- } /* Refers to fenced blocks, need to create a pre:code nested structure */
+ if (Array.isArray(root.children) && root.children.length) {
+ root.children = root.children.map(mapper);
+ }
- if (ast.type === 'listItem') {
- if (ast.checked === true || ast.checked === false) {
- return (
-
+
+ {ast.value}
+
+
+ );
+ } /* Refers to fenced blocks, need to create a pre:code nested structure */
+
+ if (ast.type === 'listItem') {
+ if (ast.checked === true || ast.checked === false) {
+ return (
+