kentcdodds / mdx-bundler

🦤 Give me MDX/TSX strings and I'll give you back a component you can render. Supports imports!
MIT License
1.75k stars 75 forks source link

Custom props of code blocks #132

Open kettanaito opened 2 years ago

kettanaito commented 2 years ago

Relevant code or config

Here's how I handle an MDX file with mdx-bundler:

import { bundleMdx } from 'mdx-bundler'

const content = file.readFileSync(filePath, 'utf8')
const { code } = await bundleMdx(content)

Here's how I define the React component:

import { getMDXComponent } from 'mdx-bundler/client'

export function getMdxComponent(code: string) {
  const Component = getMDXComponent(code)

  return function MdxComponent({ components, ...rest }: MDXContentProps) {
    return (
      <Component
        components={{
          code(props) {
            console.log(props) // <--
            return <code {...props} />
          }
        }}
        {...rest}
      />
    )
  }
}

What you did:

Here's my MDX file that's not parsing correctly:

```js a=1 b=2
function foo() {}
\```

I wish for the a and b props given to the code block to be exposed under props when writing custom components object for the getMDXComponent.

What happened:

The props of the code component do not have the a and b keys. I've noticed that only className and children props are passed to the code component (may be that only the HTML props are supported).

Suggested solution:

I believe the issue lies somewhere in MDX parsing, which is likely to be done by a different library (xdm). Still, this is where it's surfaced for me, I think it's nice to keep track of it in this repository for posterity (we may create an issue in xdm if necessary).

sarimabbas commented 2 years ago

You can try a plugin for this: https://github.com/remcohaszing/remark-mdx-code-meta

PsyGik commented 2 years ago

From https://github.com/wooorm/xdm#syntax-highlighting-with-the-meta-field

import { visit } from 'unist-util-visit'

const re = /\b([-\w]+(?![^{]*}))(?:=(?:"([^"]*)"|'([^']*)'|([^"'\s]+)))?/g

export const rehypeMetaAttribute = (options = {}) => {
  return (tree) => {
    visit(tree, 'element', visitor)
  }

  function visitor(node, index, parentNode) {
    let match

    if (node.tagName === 'code' && node.data && node.data.meta) {
      re.lastIndex = 0 // Reset regex.

      while ((match = re.exec(node.data.meta))) {
        parentNode.properties[match[1]] = match[2] || match[3] || match[4] || ''
      }
    }
  }
}

Then in bundleMDX pass the above as a plugin,

 options.rehypePlugins = [
        ...(options.rehypePlugins ?? []),
        rehypeMetaAttribute,
        ...
      ]

For the given markdown,

```js a=1 b=2
function foo() {}


you will get `a` and `b` as props 
allangrds commented 2 years ago

I'm also having this problem using v9.0.1. @PsyGik the problem is that the "meta" field doesn't exist inside node

I only have this:

Captura de Tela 2022-08-11 às 12 22 57

My 'index.mdx'

Captura de Tela 2022-08-11 às 12 23 16

UI:

Captura de Tela 2022-08-11 às 12 23 39

HTML output:

Captura de Tela 2022-08-11 às 12 24 05

This is a problem with @mdx-js/esbuild?

PsyGik commented 2 years ago

@allangrds I am on "mdx-bundler": "^9.0.1" and it works for my projects. Mind sharing a min reproducible code of the issue that you are facing?

allangrds commented 2 years ago

Hello @PsyGik, thanks for helping me. I'm creating a boilerplate using Next.js: https://github.com/allangrds/laxus-nextjs-blog/pull/5 - you just need to run npm install and npm run dev to run the project. You can access my repo and the actual code using this url, and cloning the repo.

I have the folling code there:

index.mdx

'''js filename="awesome.js"
var num1, num2, sum
num1 = prompt('Enter first number')
num2 = prompt('Enter second number')
sum = parseInt(num1) + parseInt(num2) // "+" means "add"
alert('Sum = ' + sum) // "+" means combine into a string "+" means combine into a string "+" means combine into a string
'''

api.ts which generate the post detail content:

export const getPostDetail = async (slug: string) => {
  const post = getPostBySlug(slug)
  const categories = getCategoriesFromPosts()
  const tags = getTagsFromPosts()
  const toc = []

  const { code, frontmatter } = await bundleMDX({
    source: post?.content,
    cwd: path.join(root),
    mdxOptions (options) {
      options.remarkPlugins = [
        ...(options.remarkPlugins ?? []),
        remarkPrism,
        remarkCodeTitles,
        [remarkTocHeadings, { exportRef: toc }],
      ]
      options.rehypePlugins = [
        ...(options.rehypePlugins ?? []),
        rehypeSlug,
        rehypeAutolinkHeadings,
      ]

      return options
    },
  })

  return {
    categories,
    post: {
      content: code,
      frontmatter: post?.frontmatter,
      toc,
    },
    tags,
  }
}

The content from the function remarkCodeTitles

export default function remarkCodeTitles (options) {
  return (tree) => visit(tree, 'element', (node, index, parent) => {
    if (node.tagName === 'code') {
      console.log(node)
      console.log('----')
    }

    if (node.tagName === 'code' && node.data && node.data.meta) {
      node.properties.meta = node.data.meta
      console.log({ data: node.data })
    }
  })
}

I know I'm doing nothing on this function, I'm trying to access the custom props from remarkCodeTitles.

{
  type: 'element',
  tagName: 'code',
  data: {
    hName: 'code',
    hProperties: { className: 'language-js' },
    hChildren: [
      [Object], [Object], [Object], [Object],
...rest of unused props
PsyGik commented 2 years ago

Thank you for the code samples @allangrds. Couple of things to note here:

I've created a bare-bones sample repo here. https://github.com/PsyGik/mdx-bundler-nextjs/tree/main.

Follow the usual steps, npm install && npm run dev and navigate to localhost:3000/hello. In your terminal, you should see the logs which prints the meta values.

❯ npm run dev

> dev
> next dev

ready - started server on 0.0.0.0:3000, url: http://localhost:3000
event - compiled client and server successfully in 382 ms (178 modules)
wait  - compiling...
event - compiled client and server successfully in 91 ms (178 modules)
wait  - compiling /hello (client and server)...
event - compiled client and server successfully in 159 ms (193 modules)
node.data.meta title='code.js'
node.data.meta filename="awesome.js"
allangrds commented 2 years ago

Thanks a lot @PsyGik. I'm using ''' only here, to create this comments, but as you can see here(https://user-images.githubusercontent.com/4103305/184169786-1283655e-7b70-434d-9bc3-df93a9d84c4b.png), I'm using backticks correctly. Thanks a lot for you pacience.

Checking you code and mine, I've noticed that 'remark-prismjs' was removing meta, so I've changed to rehypePrism and it's all good now :)