JavaScript CSS HTML
Switch branches/tags
Nothing to show
Clone or download
Latest commit c36c461 Jun 18, 2018

README.md

MDXC

MDXC is a tool to convert Markdown into React Components. It lets you import and use other React Components within your Markdown. Try it yourself with the MDXC Playground.

npm version

MDX is a simpler way to write content for your React applications. While standard Markdown compiles to a string of HTML, MDX compiles directly to JavaScript. If you're writing a React app, MDX is both easier to use and more flexible than standard Markdown.

Writing with MDX let's you use the full power of React, even when writing content.

  • You can import and use custom components within your content
  • You can replace Markdown's default elements with your own, allowing you to add custom behavior to links, headings, etc.
  • You can nest Markdown-formatted content within JSX elements

Try it yourself

You can try your hand at MDX using the online demo, or the old fashioned way:

# Install the `mdxc` command line tool using npm
npm install -g mdxc

# Create a file with a single heading
echo '# Hello, World' > example.mdx

# Output the compiled component to your console
mdxc example.mdx

Other ways to use MDX include:

For details, jump to the usage section

Examples

Let's go over some of the basics -- we'll leave out the boilerplate on the JavaScript output. For more detailed examples or examples which can be run locally, see the examples directory.

Tags

MDX, like JSX, requires all tags to be closed. This means that unclosed tags like <br> are just treated as plain text, while properly closed tags result in React elements.

The tag <br> is not closed. The tag <br /> is closed.

Mixing <span>unclosed and <span>closed</span> treats the outermost tags as text.
// Output
p({},
  "The tag <br> is not closed. The tag ",
  createElement("br", null),
  " is closed.",
),
p({},
  "Mixing <span>unclosed and ",
  createElement("span", null,
    "closed",
  ),
  " treats the outermost tags as text.",
)

Attributes

Because an MDX file compiles to a React Component, you'll need to use React-style attributes like className (as opposed to class).

<div className='super-classy' style={{textTransform: 'uppercase'}}>classy</div>
// Output
createElement(
  'div',
  { className: 'super-classy', style: { textTransform: 'uppercase' } },
  'classy'
)

Children

For tags which are "inline" -- i.e. within a paragraph of text, the tag's content is treated as Markdown -- you can use `backticks`, *asterisks**, etc. Inline tags can also contain nested tags, but unlike JSX, {braces} are treated as text.

Within inline tags, <sup>{braces} are **text** and <sub>nested elements are ok</sub></sup>.
// Output
p({},
  "Within inline tags, ",
  createElement("sup", null,
    "{braces} are ",
    strong({},
      "text",
    ),
    " and ",
    createElement("sub", null,
      "nested elements are ok",
    ),
  ),
  ".",
)

For tag "blocks" -- where a paragraph starts and ends with the same tag -- the content is treated as it would be in a JSX document. This means that whitespace is ignored, backticks are plain text and {braces} can be used to break out to JavaScript. If you'd like to pass Markdown-formatted text as children, wrap it in the special <markdown> tag.

<div>
  # Markdown {"doesn't".toUpperCase()} work here.
  <markdown>
    *But does work here*.
  </markdown>
</div>
// Output
createElement(
  "div",
  null,
  "# Markdown ",
  "doesn't".toUpperCase(),
  " work here.",
  wrapper({},
    p({},
      em({},
        "But does work here",
      ),
      ".",
    ),
  ),
)

Imports

Markdown is limited in functionality by design, and MDX is no different. When you want to do something complex, you'll need to delegate to an old fashioned React component. And to access custom React components, you'll need to use import.

The syntax for MDX import is identical to ES2015 import. The only difference is that import statements must come before any whitespace or blank lines.

Say you have a <Playground> component that compiles and runs some example code on the fly. You might use it like this:

import Example from './components/Example'

<Example>{`
console.log('Hello, World!')
`}</Example>
import React, { createElement, createFactory } from 'react'
import Example from './components/Example'

export default function({ factories={} }) {
  const {
    wrapper = createFactory('div'),
  } = factories

  return wrapper({},

createElement(
  Example,
  null,
  `
console.log('Hello, World!')
`
)

  )
}

Notice how your content in the generated code uses a different indentation level to the wrapper code. This ensures that any strings within your MDX do not have unwanted spaces inserted at the start of each line.

Props

You can declare that your generated component accepts a prop by adding a prop [propname] at the top of your file, after any imports. This variable can then be used within your embedded elements. Remember that {braces} are treated as strings within inline tags; if you want to interpolate a prop within your text, you'll need to do it within a JSX block!

prop value
prop onChange

<h1>Hello{value ? ', '+value : ''}!</h1>

<input
  placeholder='What is your name?'
  value={value}
  onChange={onChange}
/>
// Output
import React, { createElement, createFactory } from 'react'

export default function({ factories={}, value, onChange }) {
  const {
    wrapper = createFactory('div'),
  } = factories

  return wrapper({},

createElement(
  'h1',
  null,
  'Hello',
  value ? ', ' + value : '',
  '!'
),
createElement('input', {
  placeholder: 'What is your name?',
  value: value,
  onChange: onChange
})

  )
}

Factories

When MDX generates code for your Markdown, it doesn't directly generate a JSX <tag /> (or its JavaScript equivalent React.createElement('tag')). Instead, it calls a factory function with the tag's props and children.

Generally, you don't need to worry about factories. MDX provides default factories that just render the tag -- as expected. But variety is the spice of life, so in all likelihood you'll sometimes want to customize the behavior of certain tags. And doing so is as simple as passing an object to your generated component's factories prop!

Adding pushState support to links

If you're loading content within a React app, then in all likelihood your app is using HTML5 history through a library like react-router or react-junctions.

The great thing about HTML5 history is that it provides a way to navigate without reloading the page. The crap thing about it is that good-ol' <a> tags will still reload the page. So if you want your content to feel snappy and not out of place, you'll want it to use a <Link> component instead.

// Assume './content' is a file generated by mdxc
import Content from './content'
import { Link } from 'react-router'

export default function ContentWrapper(props) {
  return (
    <Content
      {...props}
      factories={{ a: Link }}
    />
  )
}

Adding anchor links to headings

Another use for factories is to add "anchor links" to your headings. To do so, you'd pass in custom factories for tags like h1 and h2.

// Assume './content' is a file generated by mdxc
import Content from './content'

function headingFactory(type, props, ...children) {
  // Render the same props and children that were passed in, but prepend a
  // link to this title with the text '#'. Note that MDX already adds a slug
  // to each title under its `id` attribute.
  return React.createElement(
    type,
    props,
    <a href={'#'+props.id}>#</a>,
    ...children
  )
}

export default function ContentWrapper(props) {
  return (
    <Content
      {...props}
      factories={{
        h1: (props, children) => headingFactory('h1', props, children),
        h2: (props, children) => headingFactory('h2', props, children),
        h3: (props, children) => headingFactory('h3', props, children),
      }}
    />
  )
}

Using MDX

Videos

Create and import React components with Markdown using MDXC by @andrewdelprete

Command line

A tool wouldn't be a tool without a good old CLI. It probably isn't that useful, but hey. It's the vibe, and all that. What I mean is, if you must, you may use the CLI to create js files from mdx files by hand.

# Install the `mdxc` command line tool using npm
> npm install -g mdxc

# Then call `mdxc --help` to see how to use it
> mdxc --help

Usage: mdxc [options] <file>

Compile mdx to js

Options:

  -h, --help           output usage information
  -V, --version        output the version number
  -c, --common         use commonJS modules (i.e. module.exports and require)
  -p, --pragma         set the JSX pragma (defaults to React.createElement)
  -o, --output <file>  output to file instead of console
  -u, --unwrapped      don't wrap the content in a React component

create-react-app

MDX can be used with unejected create-react-app projects! To start, you'll need to add a .babelrc file to the root level of your project:

{
    "presets": ["babel-preset-react-app"]
}

Then, you can import a component from any Markdown file by prepending the filename with !babel-loader!mdx-loader!. For example:

/* eslint-disable import/no-webpack-loader-syntax */
import DocumentComponent from '!babel-loader!mdx-loader!../pages/index.md'

You can also import documents dynamically using the proposed import() syntax.

For an example of a statically rendered site using create-react-site and MDX, see the source for the Junctions router site.

Webpack with mdx-loader

If you'd like to use MDX within an existing react app, chances are you'll want to use mdx-loader. To do so, just add it to your project and then update your webpack.config.js. Bear in mind that MDXC outputs ES2015, so you'll need to run the output through Babel if you want to support older browsers.

# Add mdx-loader to your project
npm install --save-dev mdx-loader

Assuming you're using Webpack 2, you'll need to add an entry to your module.rules array:

module: {
  rules: [
    /**
     * MDX is a tool that converts Markdown files to React components. This 
     * loader uses MDX to create Page objects for Markdown files. As it
     * produces ES2015, the result is then passed through babel.
     */
    { test: /\.mdx?$/,
      use: [
        'babel-loader',
        'mdx-loader',
      ]
    },

    // ...
  ]
},

Then import and use your components as you'd do with standard JavaScript!

You may also specify options in Webpack config:

module: {
  rules: [
    { test: /\.mdx?$/,
      use: [
        'babel-loader',
        {
          loader: 'mdx-loader',
          options: {
            pragma: 'h',
            imports: [
              'import h from \'h\''
            ]
          }
      ]
    }
  ]
}

API

At its core, MDXC is just a wrapper around the excellent and highly configurable markdown-it project. This means that its API is mostly the same.

The major difference is that the default renderer from markdown-it has been replaced with a non-configurable renderer. Existing markdown-it plugins will work, but any hooks on the renderer will silently fail. This is the behavior you want, as these hooks transform HTML strings, and MDXC doesn't use any HTML strings. If you need to transform the output, use factories instead.

To transform MDX into JavaScript, create an instance of the MDXC class and then call its render method. If you'd like to load any markdown-it plugins, call the use method on your MDXC instance before calling render.

import MDXC from 'mdxc'

var mdxc = new MDXC({
  linkify: true,
  typographer: true,
})

var js = mdxc.render('# This is a heading')

The options to mdxc are mostly the same as the options for markdown-it. For details, see the markdown-it API docs.

MDXC also provides a few extra options.

  • commonJS bool If true, your imports/exports will be transformed to use require() and module.exports
  • unwrapped bool If true, the component definition boilerplate will be omitted
  • pragma string Allows you to set the JSX pragma to something other than the default of React.createElement. Setting the pragma will stop mdxc from importing React by default.

To see this all in use, here is a slimmed-down version of the mdx-loader package:

const url = require('url')
const path = require('path')
const loaderUtils = require('loader-utils')
const Prism = require('prismjs')
const MDXC = require('mdxc')

// Set up syntax highlighting for code blocks with PrismJS
const aliases = {
  'js': 'jsx',
  'html': 'markup'
}
function highlight(str, lang) {
  if (!lang) {
    return str
  }
  else {
    lang = aliases[lang] || lang
    require(`prismjs/components/prism-${lang}.js`)
    if (Prism.languages[lang]) {
      return Prism.highlight(str, Prism.languages[lang])
    } else {
      return str
    }
  }
}

// A markdown-it plugin that requires your images with Webpack
function mdImageReplacer(md) {
  md.core.ruler.push('imageReplacer', function(state) {
    function applyFilterToTokenHierarchy(token) {
      if (token.children) {
        token.children.map(applyFilterToTokenHierarchy);
      }
      if (token.type === 'image') {
        const src = token.attrGet('src')
        if(!loaderUtils.isUrlRequest(src)) return;
        const uri = url.parse(src);
        uri.hash = null;
        token.attrSet('src', { __jsx: 'require("'+uri.format()+'")' });
      }
    }
    state.tokens.map(applyFilterToTokenHierarchy);
  })
}

module.exports = function mdxLoader(content) {
  const mdxc =
    new MDXC({
      commonJS: true,
      linkify: true,
      typographer: true,
      highlight: highlight,
    })
      .use(mdImageReplacer)

  return mdxc.render(content);
}

MDX in the wild

Acknowledgments

Mad props to markdown-it for doing the hard yards which make MDXC possible. Also, markdown-it-jsx provided the base for parsing JSX tags out of Markdown files. Finally, there would be no use making MDXC unless React, Webpack and Babel already existed.

Contributing

Help is muchly appreciated! In particular, PRs for the following would be super duper great:

  • Improved tests
  • Refactoring existing code to be prettier and faster
  • Generating more concise JavaScript