`, 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 |
--------------------------------------------------------------------------------