Custom pragma
MDXTag
, for those that aren’t aware, is a critical piece in the way
MDX replaces HTML primitives like <pre>
and <h1>
with custom React
Components. I’ve previously
written
about the way MDXTag
works when trying to replace the <pre>
tag
with a custom code component.
mdx-utils
contains the methodology for pulling the props around appropriately
through the MDXTag
elements that are inbetween pre
and code
.
exports.preToCodeBlock = preProps => {
if (
// children is MDXTag
preProps.children &&
// MDXTag props
preProps.children.props &&
// if MDXTag is going to render a <code>
preProps.children.props.name === 'code'
) {
// we have a <pre><code> situation
const {
children: codeString,
props: {className, ...props}
} = preProps.children.props
return {
codeString: codeString.trim(),
language: className && className.split('-')[1],
...props
}
}
return undefined
}
So MDXTag
is a real Component in the middle of all of the other MDX
rendered elements. All of the code is included here for reference.
import React, {Component} from 'react'
import {withMDXComponents} from './mdx-provider'
const defaults = {
inlineCode: 'code',
wrapper: 'div'
}
class MDXTag extends Component {
render() {
const {
name,
parentName,
props: childProps = {},
children,
components = {},
Layout,
layoutProps
} = this.props
const Component =
components[`${parentName}.${name}`] ||
components[name] ||
defaults[name] ||
name
if (Layout) {
return (
<Layout components={components} {...layoutProps}>
<Component {...childProps}>{children}</Component>
</Layout>
)
}
return <Component {...childProps}>{children}</Component>
}
}
export default withMDXComponents(MDXTag)
MDXTag
is used in the mdx-hast-to-jsx
conversion,
which is the final step in the MDX AST pipeline. Every renderable
element is wrapped in an MDXTag
, and MDXTag
handles rendering the
element later.
return `<MDXTag name="${node.tagName}" components={components}${
parentNode.tagName ? ` parentName="${parentNode.tagName}"` : ''
}${props ? ` props={${props}}` : ''}>${children}</MDXTag>`
A concrete example
The following MDX
# a title
and such
testing
turns into the following React code
export default ({components, ...props}) => (
<MDXTag name="wrapper" components={components}>
<MDXTag name="h1" components={components}>{`a title`}</MDXTag>{' '}
<MDXTag name="pre" components={components}>
<MDXTag
name="code"
components={components}
parentName="pre"
props={{metaString: null}}
>{`and such `}</MDXTag>
</MDXTag>{' '}
<MDXTag name="p" components={components}>{`testing`}</MDXTag>
</MDXTag>
)
resulting in the following HTML
<div>
<h1>a title</h1>
<pre>
<code>and such</code>
</pre>
<p>testing</p>
</div>
createElement
With the new approach, the above MDX transforms into this new React code
const layoutProps = {}
export default function MDXContent({components, ...props}) {
return (
<MDXLayout
{...layoutProps}
{...props}
components={components}
mdxType="MDXLayout"
>
<h1>{`a title`}</h1>
<pre>
<code parentName="pre" {...{}}>{`and such
`}</code>
</pre>
<p>{`testing`}</p>
</MDXLayout>
)
}
MDXContent.isMDXComponent = true
Notice how now the React elements are plainly readable without
wrapping MDXTag
.
Now that we’ve cleaned up the intermediary representation, we need to
make sure that we have the same functionality as the old
MDXTag
. This is done through a custom createElement
implementation. Typically when using React, we use
React.createElement
to render the elements on screen. This is even
true if you’re using JSX because JSX tags such as <div>
compile to
createElement
calls. So this time instead of using
React.createElement
we’ll be using our own. Check it out in the MDX
repo
Reproduced here is our createElement
function and the logic for how
we decide whether or not MDX should take over the rendering of the
createElement
call.
export default function (type, props) {
const args = arguments
const mdxType = props && props.mdxType
if (typeof type === 'string' || mdxType) {
const argsLength = args.length
const createElementArgArray = new Array(argsLength)
createElementArgArray[0] = MDXCreateElement
const newProps = {}
for (let key in props) {
if (hasOwnProperty.call(props, key)) {
newProps[key] = props[key]
}
}
newProps.originalType = type
newProps[TYPE_PROP_NAME] = typeof type === 'string' ? type : mdxType
createElementArgArray[1] = newProps
for (let i = 2; i < argsLength; i++) {
createElementArgArray[i] = args[i]
}
return React.createElement.apply(null, createElementArgArray)
}
return React.createElement.apply(null, args)
}
Vue
One really cool application of the new output format using a custom
createElement
is that we can now write versions of it for Vue and
other frameworks. Since the pragma insertion is the responsibility of
the webpack (or other bundlers) loader, swapping the pragma can be an
option in mdx-loader as long as we have a Vue createElement
to point
to.
Written by Chris Biscardi
Edit this page on GitHub