├── .gitignore ├── README.md ├── example └── render.js ├── index.js ├── lib ├── escape_code.js ├── jsx_inline.js └── jsx_parser.js ├── package.json └── test ├── fixtures ├── basic.txt ├── block.txt ├── escaped_code.txt └── inline.txt └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdown-it-jsx 2 | 3 | Plugin for [markdown-it](https://github.com/markdown-it/markdown-it) 4 | to embed React/JSX components (and therefore JavaScript code) instead 5 | of raw HTML. 6 | 7 | Meant as part of larger system [TODO]. 8 | 9 | When rendering, the JSX (including both tags and braced JS 10 | expressions) is passed through verbatim to the output: this plugin is 11 | basically just a minimal recognizer, intended to keep markdown-it from 12 | messing up JSX tags or interpolated JavaScript spans by treating them 13 | as Markdown. 14 | 15 | *Note: if you render Markdown with JSX in it, the output won't be valid HTML 16 | anymore (since it may have React component tags, embedded JavaScript, 17 | etc); instead, it'll have to be rendered by React first.* 18 | 19 | ## Install 20 | 21 | ```sh 22 | npm i markdown-it markdown-it-jsx 23 | ``` 24 | 25 | ## Use 26 | 27 | ```javascript 28 | const md = require('markdown-it')(); 29 | const jsx = require('markdown-it-jsx'); 30 | md.use(jsx); 31 | 32 | console.log(md.render(` 33 | # A sample document 34 | 35 | Two times three is {3}. 36 | 37 | We can intermix **Markdown** and JSX. 38 | 39 | The current date is {new Date().toString()}. 40 | 41 | Some {"[link](/link)"} that will not be rendered. 42 | 43 | `)); 44 | ``` 45 | 46 | prints this JSX output 47 | 48 | ```jsx 49 |

A sample document

50 |

Two times three is {3}.

51 |

We can intermix Markdown and JSX.

52 |

The current date is {new Date().toString()}.

53 |

Some {"[link](/link)"} that will not be rendered.

54 | ``` 55 | 56 | **[Run example](https://codesandbox.io/s/xjykoj0vmq)**. 57 | 58 | which isn't quite a valid JSX expression, but will 59 | be if you wrap it in an outer `
` and `
`. 60 | 61 | The `` is now actually treated as JSX and not HTML, even though 62 | it's also a standard HTML tag and would be treated as HTML by a normal 63 | Markdown parser. 64 | 65 | Again, the render output is not valid HTML in itself -- you'll 66 | probably want to wrap a React component declaration around that outer 67 | `
`, run that through Babel or TypeScript to make it runnable JS, 68 | then use React to actually render the component to a browser 69 | DOM. (Here, you'd need to define the `Doubler` component, too.) 70 | 71 | See [example/render.js](example/render.js) for a more complete example 72 | of writing a document and then rendering it (offline, in that case). 73 | 74 | See [TODO] for an even bigger, in-browser, dynamic example. 75 | 76 | ## Implementation 77 | 78 | I'm using 79 | [parsimmon parser combinators](https://github.com/jneen/parsimmon) in 80 | the [JSX matcher](lib/jsx_parser.js) instead of the regexes in the 81 | original 82 | [HTML matcher](https://github.com/markdown-it/markdown-it/blob/9074242bdd6b25abf0b8bfe432f152e7b409b8e1/lib/common/html_re.js), 83 | to scan over braced expressions for embedded JS. This is probably 84 | excessive, and a for loop with a state variable would suffice. The 85 | behavior is also not really correct (what about braces inside string 86 | literals in JS?), but good enough for now. 87 | 88 | The render rule in [index.js](index.js) just passes the JSX code string 89 | straight through into the rendered output; the parser doesn't construct 90 | a coherent AST of the JSX or anything. 91 | 92 | There's a hack for JSX blocks (like 93 | [HTML blocks](http://spec.commonmark.org/0.25/#html-blocks) in 94 | standard Markdown); if we see a paragraph with JSX tags at its open 95 | and end, we remove the paragraph open and end, effectively promoting 96 | the JSX inside. Again, not really correct, and will treat some things 97 | differently from CommonMark spec, but good enough. 98 | 99 | ## Changelog 100 | 101 | - 1.1.0: Merged PRs from Xiphe: update dependencies, treat contents of 102 | backtick code inlines/fenced blocks as literal JS strings (so braces 103 | aren't annoying in code samples, for example). 104 | - 1.0.0: Initial release. 105 | 106 | ## See also 107 | 108 | Similar things and classes of things: 109 | 110 | - Andrey Popp's [Reactdown](https://andreypopp.github.io/reactdown/) 111 | was the first I saw, in its 112 | [iteration from 2014](https://github.com/andreypopp/reactdown/tree/prev). The 113 | old version was pretty similar but had a less reliable Markdown 114 | parser, I think. The new version seems to abandon the JSX 115 | syntax. Very much worth looking at, though. 116 | - [mdreact](https://github.com/funkjunky/mdreact) also appears similar 117 | but also abandons JSX syntax, and it looks a little unreliable. 118 | - [react-showdown](https://github.com/jerolimov/react-showdown) can 119 | only generate the React component as a runtime object, not as JSX 120 | source; it's not a static compilation pipeline. 121 | - [rexxars/react-markdown](https://github.com/rexxars/react-markdown) and some 122 | others render ordinary Markdown to React components, but don't let 123 | you embed React components in the Markdown (since that would make 124 | the output a component instead of a static element tree) 125 | - [sunflowerdeath/react-markdown](https://github.com/sunflowerdeath/react-markdown) 126 | and some others provide a React component that renders children or 127 | props as React components. Again, can't actually embed custom tags 128 | in the Markdown. 129 | - [markdown-component-loader](https://www.npmjs.com/package/markdown-component-loader) 130 | (recent) looks pretty good at a glance. Slightly heavier 131 | double-brace syntax. Also, a Webpack loader, so very build-system-y 132 | instead of just being a markdown-it plugin? 133 | 134 | Things I wanted from this syntax extension: 135 | 136 | - Use arbitrary React components inside Markdown document. (This 137 | plugin alone doesn't deal with scoping and defining names used in 138 | your JSX, though. See [TODO] for one solution to that issue.) 139 | - Use standard JSX syntax inside Markdown document. 140 | - Statically compile Markdown+JSX source to JavaScript React component 141 | source. 142 | -------------------------------------------------------------------------------- /example/render.js: -------------------------------------------------------------------------------- 1 | // A simple 'compiler' that uses the plugin. 2 | // Run at command-line with `node render.js`. 3 | 4 | // Has a fixed sample Markdown/JSX document, which gets fed into 5 | // markdown-it + markdown-it-jsx to convert it to JSX. 6 | 7 | // Then the JSX is fed into Babel to convert it to ordinary JS which 8 | // Node can actually run. (Node doesn't support JSX syntax.) 9 | 10 | // Then we eval that ordinary JS to construct the React component `Document`. 11 | 12 | // Finally, we render as static HTML to show you how it'd 13 | // look (you might put that in an html file and look at it in a browser). 14 | 15 | // Other DOM states might exist for other models, but this is a simple 16 | // example that shows you the whole Markdown -> rendering pipeline 17 | // without us needing to use a browser. 18 | 19 | var md = require('markdown-it')(); 20 | var jsx = require('..'); 21 | md.use(jsx); 22 | 23 | // We'll define the Doubler component later, when we're about to execute 24 | // this document. 25 | var documentSource = ` 26 | # A sample document 27 | 28 | Two times three is {3}. 29 | 30 | We can intermix **Markdown** and JSX. 31 | 32 | The current date is {new Date().toString()}. 33 | `; 34 | 35 | var markdownCompileResult = md.render(documentSource); 36 | // Now markdownCompileResult is a series of JSX tags concatenated together. 37 | // Should be something like: 38 | // 39 | //

A sample document

40 | //

Two times three is {3}.

41 | //

We can intermix Markdown and JSX.

42 | //

The current date is {new Date().toString()}.

43 | 44 | // Next, the JSX -> raw JS compilation with Babel. 45 | var babel = require('babel-core'); 46 | 47 | var babelCompileResult = babel.transform( 48 | // We need to wrap the JSX in a div so it's a valid JSX expression. 49 | '() => (
' + markdownCompileResult + '
)', 50 | { presets: ['react'] } 51 | ).code; 52 | 53 | // Finally, import React and friends and actually execute that JS code. 54 | var React = require('react'); 55 | var ReactDOMServer = require('react-dom/server'); 56 | 57 | var Doubler = function(props) { 58 | return React.createElement('span', null, 2 * props.children); 59 | }; 60 | 61 | var Document = eval(babelCompileResult); 62 | // Now Document is a React component, which might be instantiated with 63 | // in JSX syntax. 64 | 65 | // Print out a concrete rendering of the document as static HTML. 66 | console.log(ReactDOMServer.renderToStaticMarkup(React.createElement(Document))); 67 | // Should be something like: 68 | // 69 | //

A sample document

Two times three is 6.

70 | //

We can intermix Markdown and JSX.

71 | //

The current date is Thu Jun 23 2016 22:25:54 GMT-0700 (PDT).

72 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var jsx_inline = require('./lib/jsx_inline'); 4 | var escape_code = require('./lib/escape_code'); 5 | 6 | module.exports = function jsx_plugin(md) { 7 | md.set({ xhtmlOut: true }); 8 | 9 | // JSX should entirely replace embedded HTML. 10 | md.inline.ruler.before('html_inline', 'jsx_inline', jsx_inline); 11 | md.disable('html_inline'); 12 | // We'll parse blocks as inline and then strip the surrounding paragraph at the end; it's easier. 13 | md.disable('html_block'); 14 | 15 | md.core.ruler.push('jsx_blockify', function(state) { 16 | // Look for things like

...

and strip the

,

there. 17 | // FIXME Quadratic time in worst case, I think? 18 | var paragraphTokensToRemove = []; 19 | 20 | var lastInlineTokenSeen; 21 | for (var blkIdx = 0; blkIdx < state.tokens.length; blkIdx++) { 22 | if (state.tokens[blkIdx].type !== 'paragraph_open') { 23 | continue; 24 | } 25 | 26 | var nextBlkToken = state.tokens[blkIdx + 1]; 27 | if (nextBlkToken.type !== 'inline' || nextBlkToken.children[0].type !== 'jsx_inline') { 28 | continue; 29 | } 30 | 31 | // FIXME Incorrect and a hack: 32 | //

...

will also get stripped. 33 | var paragraphOpens = 0; 34 | for (var i = blkIdx + 1; i < state.tokens.length; i++) { 35 | if (state.tokens[i].type === 'paragraph_open') { 36 | paragraphOpens++; 37 | continue; 38 | 39 | } else if (state.tokens[i].type !== 'paragraph_close') { 40 | continue; 41 | } 42 | 43 | if (paragraphOpens > 0) { 44 | paragraphOpens--; 45 | continue; 46 | } 47 | 48 | // OK, this is the paragraph_close matching the open we started on. 49 | // What came right before here? 50 | var prevBlkToken = state.tokens[i - 1]; 51 | if (prevBlkToken.type !== 'inline') { 52 | break; 53 | } 54 | var prevInlineToken = prevBlkToken.children[prevBlkToken.children.length - 1]; 55 | if (prevInlineToken.type !== 'jsx_inline') { 56 | break; 57 | } 58 | 59 | // If we got this far, we're stripping the surrounding paragraph. 60 | 61 | // FIXME Also a hack. The 'inline' JSX that's inside the paragraph should 62 | // now get a linebreak after it in the generated HTML. Easier to test 63 | // and looks better in the HTML. 64 | prevInlineToken.content += '\n'; 65 | paragraphTokensToRemove.push( 66 | blkIdx, 67 | i 68 | ); 69 | break; 70 | } 71 | } 72 | 73 | state.tokens = state.tokens.filter(function(blkToken, idx) { 74 | return paragraphTokensToRemove.indexOf(idx) === -1; 75 | }); 76 | }); 77 | 78 | md.renderer.rules.fence = escape_code(md.renderer.rules.fence); 79 | md.renderer.rules.code_inline = escape_code(md.renderer.rules.code_inline); 80 | md.renderer.rules.code_block = escape_code(md.renderer.rules.code_block); 81 | 82 | md.renderer.rules['jsx_inline'] = function(tokens, idx) { 83 | // If the span is JSX, just pass the original source for the span 84 | // through to output. 85 | return tokens[idx].content; 86 | }; 87 | }; 88 | -------------------------------------------------------------------------------- /lib/escape_code.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function escape_code(default_renderer) { 4 | return function escape_renderer(tokens, idx, options, env, slf) { 5 | tokens[idx].content = '{`' + tokens[idx].content.replace(/`/g, '\\\`') + '`}' 6 | 7 | return default_renderer(tokens, idx, options, env, slf) 8 | .replace(/class="/g, 'className="'); 9 | }; 10 | } -------------------------------------------------------------------------------- /lib/jsx_inline.js: -------------------------------------------------------------------------------- 1 | // Process JSX tags. 2 | // Based on https://github.com/markdown-it/markdown-it/blob/9074242bdd6b25abf0b8bfe432f152e7b409b8e1/lib/rules_inline/html_inline.js 3 | 4 | 'use strict'; 5 | 6 | 7 | var JSX_INLINE_PARSER = require('./jsx_parser').JSX_INLINE_PARSER; 8 | 9 | 10 | function isLetter(ch) { 11 | /*eslint no-bitwise:0*/ 12 | var lc = ch | 0x20; // to lower case 13 | return (lc >= 0x61/* a */) && (lc <= 0x7a/* z */); 14 | } 15 | 16 | 17 | module.exports = function jsx_inline(state, silent) { 18 | var result, max, token, 19 | pos = state.pos; 20 | 21 | // Check start 22 | max = state.posMax; 23 | var firstCh = state.src.charCodeAt(pos); 24 | if ((firstCh !== 0x3C/* < */ && 25 | firstCh !== 0x7B/* { */) || 26 | pos + 2 >= max) { 27 | return false; 28 | } 29 | 30 | // Quick fail on second char if < was first char 31 | var secondCh = state.src.charCodeAt(pos + 1); 32 | if (secondCh === 0x3C/* < */ && 33 | (secondCh !== 0x21/* ! */ && 34 | secondCh !== 0x3F/* ? */ && 35 | secondCh !== 0x2F/* / */ && 36 | !isLetter(secondCh))) { 37 | return false; 38 | } 39 | 40 | result = JSX_INLINE_PARSER.parse(state.src.slice(pos)); 41 | 42 | if (!result.status) { return false; } 43 | 44 | if (!silent) { 45 | token = state.push('jsx_inline', '', 0); 46 | token.content = state.src.slice(pos, pos + result.value.end.offset); 47 | } 48 | 49 | state.pos += result.value.end.offset; 50 | return true; 51 | }; 52 | -------------------------------------------------------------------------------- /lib/jsx_parser.js: -------------------------------------------------------------------------------- 1 | // Parser to match JSX tags. 2 | // Based on https://github.com/markdown-it/markdown-it/blob/9074242bdd6b25abf0b8bfe432f152e7b409b8e1/lib/common/html_re.js 3 | // Extended to use parsimmon parser generator instead of regexes so we can do 4 | // balanced-brace matching to consume JSX attribute values (which are arbitrary JS expressions). 5 | 6 | 'use strict'; 7 | 8 | var parsimmon = require('parsimmon'); 9 | var regex = parsimmon.regex; 10 | var string = parsimmon.string; 11 | var whitespace = parsimmon.whitespace; 12 | var optWhitespace = parsimmon.optWhitespace; 13 | var lazy = parsimmon.lazy; 14 | var alt = parsimmon.alt; 15 | var all = parsimmon.all; 16 | 17 | var attr_name = regex(/[a-zA-Z_:][a-zA-Z0-9:._-]*/); 18 | 19 | var unquoted = regex(/[^"'=<>`\x00-\x20]+/); 20 | var single_quoted = regex(/'[^']*'/); 21 | var double_quoted = regex(/"[^"]*"/); 22 | 23 | // FIXME Hack: won't deal with braces inside a string or something 24 | // inside the JS expression, if they're mismatched. 25 | // (But you really shouldn't have complex JS in the JSX attribute value, anyway.) 26 | var braced_expression = lazy(function() { 27 | return string('{').then( 28 | alt(braced_expression, regex(/[^{}]+/)).many() 29 | ).skip(string('}')); 30 | }); 31 | 32 | var attr_value = alt(braced_expression, unquoted, single_quoted, double_quoted); 33 | 34 | var attribute = whitespace.then(attr_name).then(regex(/\s*=\s*/).then(attr_value).atMost(1)); 35 | 36 | var open_tag = regex(/<[_A-Za-z][_A-Za-z0-9.\-]*/).then(attribute.many()).skip(regex(/\s*\/?>/)); 37 | var close_tag = regex(/<\/[_A-Za-z][_A-Za-z0-9.\-]*\s*>/); 38 | 39 | var comment = regex(/|/); 40 | var processing = regex(/<[?].*?[?]>/); 41 | var declaration = regex(/]*>/); 42 | var cdata = regex(//); 43 | 44 | exports.JSX_INLINE_PARSER = alt( 45 | open_tag, 46 | close_tag, 47 | comment, 48 | processing, 49 | declaration, 50 | cdata, 51 | braced_expression // Arbitrary JS {"expressions"} in the Markdown. 52 | ).mark().skip(all); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-it-jsx", 3 | "version": "1.1.0", 4 | "description": "markdown-it plugin for embedding JSX syntax instead of raw HTML.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha test/test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/osnr/markdown-it-jsx.git" 12 | }, 13 | "keywords": [ 14 | "markdown-it", 15 | "markdown", 16 | "markdown-it-plugin" 17 | ], 18 | "author": "Omar Rizwan ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/osnr/markdown-it-jsx/issues" 22 | }, 23 | "homepage": "https://github.com/osnr/markdown-it-jsx#readme", 24 | "devDependencies": { 25 | "babel-core": "^6.21.0", 26 | "babel-preset-react": "^6.16.0", 27 | "markdown-it": "^8.2.2", 28 | "markdown-it-testgen": "^0.1.4", 29 | "mocha": "^3.2.0", 30 | "react": "^15.4.2", 31 | "react-dom": "^15.4.2" 32 | }, 33 | "dependencies": { 34 | "parsimmon": "^1.2.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/fixtures/basic.txt: -------------------------------------------------------------------------------- 1 | --- 2 | desc: Non-interpolating JSX and plain Markdown 3 | --- 4 | 5 | plain Markdown with just header 6 | . 7 | # markdown-it rulezz! 8 | . 9 |

markdown-it rulezz!

10 | . 11 | 12 | non-interpolating JSX, div tag 13 | . 14 |
15 | blah 16 |
17 | . 18 |
19 | blah 20 |
21 | . 22 | -------------------------------------------------------------------------------- /test/fixtures/block.txt: -------------------------------------------------------------------------------- 1 | --- 2 | desc: Block JSX 3 | --- 4 | 5 | interpolated property 6 | . 7 | # heading 8 | test text 9 | 10 | 11 | 12 | end text 13 | . 14 |

heading

15 |

test text

16 | 17 |

end text

18 | . 19 | 20 | event handler containing angle bracket 21 | . 22 | hello 23 | 24 | 0) { 26 | console.log('some message'); 27 | } 28 | }}/> 29 | 30 | there 31 | . 32 |

hello

33 | 0) { 35 | console.log('some message'); 36 | } 37 | }}/> 38 |

there

39 | . 40 | 41 | child being function 42 | . 43 | 44 | interesting: 45 | {i =>
{i *2* 3}
} 46 |
47 | . 48 | 49 | interesting: 50 | {i =>
{i *2* 3}
} 51 |
52 | . 53 | -------------------------------------------------------------------------------- /test/fixtures/escaped_code.txt: -------------------------------------------------------------------------------- 1 | --- 2 | desc: code_inline code_block and fence 3 | --- 4 | 5 | escaped content of code_inline 6 | . 7 | `alert('yay');` 8 | . 9 |

{`alert('yay');`}

10 | . 11 | 12 | escaped content of code_block 13 | . 14 | window.location = 'example.org'; 15 | . 16 |
{`window.location = 'example.org';
17 | `}
18 | . 19 | 20 | escaped content of fence 21 | . 22 | ```jsx 23 | {isEscaped(`?!`)} 24 | ``` 25 | . 26 |
{`<MyComponent>{isEscaped(\`?!\`)}</MyComponent>
27 | `}
28 | . 29 | -------------------------------------------------------------------------------- /test/fixtures/inline.txt: -------------------------------------------------------------------------------- 1 | --- 2 | desc: Inline JSX 3 | --- 4 | 5 | interpolated property 6 | . 7 | hello there 8 | . 9 |

hello there

10 | . 11 | 12 | Markdown inside inline JSX 13 | . 14 | hello there _friend_! 15 | . 16 |

hello there friend!

17 | . 18 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var MarkdownIt = require('markdown-it'); 2 | var md = MarkdownIt().use(require('..')); 3 | 4 | var path = require('path'); 5 | var testgen = require('markdown-it-testgen'); 6 | 7 | describe('markdown-it-jsx', function() { 8 | var test = function(filename) { 9 | testgen(path.join(__dirname, 'fixtures', filename), { header: true }, md); 10 | }; 11 | 12 | test('basic.txt'); 13 | test('inline.txt'); 14 | test('block.txt'); 15 | test('escaped_code.txt'); 16 | }); 17 | --------------------------------------------------------------------------------