0

Is it possible to define custom MDX components using a data-attribute of the HTML element?

I'm working on a Next.js blog using contentlayer for MDX and Rehype Pretty Code for syntax highlighting code blocks. Below is the HTML output structure from Rehype Pretty code. I would like to create a custom MDX component for the below HTML, but the root elemen is a <div>. So is it possible to target a div element using its data attribute - in this case div[data-rehype-pretty-code-fragment] to create a custom MDX component?

<div data-rehype-pretty-code-fragment>
    <div data-rehype-pretty-code-title="" data-language="js">sample.js</div>
    <pre data-language="js">
        <code data-line-numbers data-line-numbers-max-digits="2">
            </span data-line="">
            </span style="color:#C678DD">const</span>
            <!-- more span elements ... -->
        </code>
    </pre>
    <div data-rehype-pretty-code-caption>this is a sample caption</div>
</div>

above code block is shortened for brevity

Custom MDX Components:

is it possible to do this?

export const components: MDXComponents = {
    // Add a custom component.
    MyComponent: () => <div>Hello World!</div>,
    div[data-rehype-pretty-code-fragment]: (props) => <CodeBlock {...props}  />, // is this possible?
};

My Requirement

What I'm trying to do is to add additional DOM elements & classes to the code-block output like below classes codeBlockRoot, customContent & moreCustomContent using a Custom JSX component and add styles to the entire code-block. Right now I'm having to use div[data-rehype-pretty-code-fragment] to add styles to the code block:

<div class="codeBlockRoot" data-rehype-pretty-code-fragment>
    <div class="customContent">this is custom HTML</div>
    <div data-rehype-pretty-code-title="" data-language="js">
        sample.js
        <div class="moreCustomContent">more custom content</div>
    </div>
    <pre data-language="js">
        <code data-line-numbers data-line-numbers-max-digits="2">
            </span data-line="">
            </span style="color:#C678DD">const</span>
            <!-- more span elements ... -->
        </code>
    </pre>
    <div data-rehype-pretty-code-caption>this is a sample caption</div>
</div>
whytheq
  • 34,466
  • 65
  • 172
  • 267
Gangula
  • 5,193
  • 4
  • 30
  • 59
  • Can you describe what you want your custom MDX component to do? If you are trying to modify the code produced by rehype plugin, then you should probably implement this functionality as your own rehype plugin. – Igor Danchenko Jun 26 '23 at 15:55
  • I added my requirement in the question. – Gangula Jun 27 '23 at 04:49
  • Just out of curiosity, what kind of content are you adding in the "this is custom HTML" and "more custom content"? I'm just wondering if it's purely to style the title and could be achieved by styling the `::before` and `::after` pseudo-elements. – Igor Danchenko Jun 27 '23 at 15:22
  • I would like to add a copy to clipboard button and a element that displays language. It can also have a file title mentioned for blog posts. So out of these 3, title is already being added by `Rehype pretty code` under `data-rehype-pretty-code-title` which is where I would like to add the language and copy button. But if the codeblock doesn't have a title, I would like to add a div to add these 2. So in summary, it gets a little complex in logic - and that's where I need a little more control. – Gangula Jun 27 '23 at 18:02
  • I see, thank you for the explanation. – Igor Danchenko Jun 27 '23 at 18:03

1 Answers1

0

I believe the following solution meets your criteria.

// mdx-components.tsx

import * as React from "react";
import type { MDXComponents } from "mdx/types";
import CodeBlock from "@/components/CodeBlock";

declare module "react" {
  interface HTMLAttributes<T> {
    [key: `data-${string}`]: unknown;
  }
}

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    div: ({ children, ...rest }) =>
      React.createElement(
        rest["data-rehype-pretty-code-fragment"] !== undefined
          ? CodeBlock
          : "div",
        rest,
        children
      ),
    ...components,
  };
}
// CodeBlock.tsx

import * as React from "react";

export default function CodeBlock({
  className,
  children,
  ...rest
}: React.JSX.IntrinsicElements["div"]) {
  return (
    <div
      className={[className, "codeBlockRoot"].filter(Boolean).join(" ")}
      {...rest}
    >
      {React.Children.map(children, (child) =>
        React.isValidElement(child) &&
        child.props["data-rehype-pretty-code-title"] !== undefined ? (
          <>
            <div className="customContent">this is custom HTML</div>
            {React.cloneElement(child, child.props, [
              ...React.Children.toArray(child.props.children),
              <div key="moreCustomContent" className="moreCustomContent">
                more custom content
              </div>,
            ])}
          </>
        ) : (
          child
        )
      )}
    </div>
  );
}
Igor Danchenko
  • 1,980
  • 1
  • 3
  • 13