Currently there's an issue that can pop up with respect to MDX and compatibility with the wider Remark ecosystem. Remark AST transformations often assume HTML semantics and thus don't handle the case where valid JavaScript escape sequences can occur and need to be escaped. While this is a problem that should still be solved for wider ecosystem compatibility, we have a more powerful option available to us when using MDX: Compoennts.
I've begun work on a set of components that are going to be
drop-in replacements for gatsby-remark plugins where
appropriate. This list includes plugins such as
gatsby-remark-prismjs
and
gatsby-remark-autolink-headers
. Some plugins don't make
sense to implement like this currently, such as
gatsby-remark-images
, because these plugins use Gatsby
APIs to optimize images, etc.
Since I practice README-first API design, This brings up an interesting point of API design quite early in the process. The core idea is something like this:
MDXProvider
using the wrapRootElement
APIs.In the end, what we end up with is:
const components = {pre: MDXPrismJSComponent};export const wrapRootElement = ({ element }) => (<MDXProvider components={components}>{element}</MDXProvider>);
We have the following two options when it comes to how we require users to use gatsby plugins for these components.
modules.exports = {plugins: [`gatsby-mdx`, `gatsby-plugin-mdx-prismjs`]};
modules.exports = {plugins: [{resolve: `gatsby-mdx`,options: { plugins: [`gatsby-plugin-mdx-prismjs`] }}]};
That is, do we want to nest MDXProviders
or not. The first
plugin API option lets us include an arbitrary number of
MDXProviders
at the root of the tree, which implicitly
lets us also consume any potential components above our
position in the tree and defer to another component later if
we want to. It would look something like this for
implementation (simplified here, it would be multiple
wrapRootElement
calls from multiple plugins).
const components = {pre: MDXPrismJSComponent};const nestedComponents = {h1: MDXAutoLinkHeaders};export const wrapRootElement = ({ element }) => (<MDXProvider components={components}><MDXProvider>{element}</MDXProvider></MDXProvider>);
We could, as we said before, progressively enhance components over time. If we originally only used the PrismJS component, we could add a ReactLive component by including it in a lower position in the plugin list.
const components = {pre: withMDXComponents(({ components, live, ...props }) => {const OriginalPre = components.pre;if (live) {return <ReactLive {...props} />;} else {return <OriginalPre {...props} />;}})};
This is sort of complicated for people to understand and is
not obvious if you're going to write an MDX component plugin
for the first time. Instead what we can do is make this API
explicit and documented, then merge the result into a single
components
object and use a single MDXProvider
. The
default export for the gatsby-plugin-mdx components would be
an object matching the MDXProvider
spec. (Take the
following PrismJS example).
export default {pre: PrismJSComponent};
Then we say "Do what you want, and return undefined if you
don't want to handle it", which allows us to handle the
passthrough inside of gatsby-mdx
in a more complex way
(which could let us optimize a bit more) instead of pushing
the complexity onto component plugin authors. Our
ReactLive
implementation now becomes simpler:
const components = {pre: ({ live, ...props }) => {if(live) {return <ReactLive {...props} />} else {return undefined}})}
and also handles the "lazy" case where someone just wants to not deal with anything but conditionally using their own component.
const components = {pre: ({ live, ...props }) => {if(live) {return <ReactLive {...props} />}})}
In gasby-mdx
we could basically write some code to attempt
to render each component and if it returns undefined
, try
the next component (otherwise pass it through to usage).
This is ok, but we can also make this a bit more explicit
too by adding guards. An example guard for the ReactLive
component would be:
export const guards = {pre: ({ live }) => !!live};
Then in gatsby-mdx
, we can write code that operates more
like:
const pre = [{guard: reactLiveGuard,Component: ReactLive},{guard: undefined,Component: PrismJS},{guard: undefined,Component: "pre"}];const components = {pre: props => {pre.find(({ guard, Component }) => {if (!guard || guard(props)) {return <Component {...props} />;}});}};
This allows us explicit composition while also falling back
to always render something. Some components can completely
take over rendering, like PrismJS
, while others can only
take over if we use a specific style code block:
const some = {}
So that's some of the API design I've been mulling over over
the last day or so for gatsby-mdx. This should lead to a
nice set of packages like gatsby-mdx-plugin-defaults
and
gatsby-mdx-plugin-from-remark
that are drop-in
enhancements to the default MDX experience and make it
easier to port code from Remark.