DEV Community

Matsura Yuma
Matsura Yuma

Posted on • Originally published at rubiq.vercel.app

How to support admonition (or callout) in Markdown

This post was originally published on my blog

Sometimes you may want to emphasize that command is destructive or note additional information in your blog or docs like this:

Admonitions

But this is not included in standard Markdown or GitHub Flavored Markdown. In this post, I will show you how to support this feature and notation with remark plugins.

I assume you've already set up a static site generator that supports MDX by unified pipeline.

Remark plugins

Support notation

We'd like to add this custom notation for admonition:

:::info
Laboris dolore aliquip laboris irure.
:::
Enter fullscreen mode Exit fullscreen mode

Fortunately, we don't have to write any code as there is remark-directive that does exactly what we need.

yarn add remark-directive
Enter fullscreen mode Exit fullscreen mode

And add it to your own unified pipeline.

import remarkDirective from "remark-directive"
import remarkAdmonition from "./remarkAdmonitions" // We'll implement this next

const remarkPlugins = [remarkDirective, remarkAdmonition]
Enter fullscreen mode Exit fullscreen mode

Convert to mdxJsxFlowElement

But remark-directive doesn't do everything we need for displaying admonitions. To convert the block into an MDX component, define the following plugin:

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

export default function remarkAdmonition() {
  return (tree) => {
    visit(tree, (node) => {
      if (
        node.type === "textDirective" ||
        node.type === "leafDirective" ||
        node.type === "containerDirective"
      ) {
        if (!["info", "warn", "danger"].includes(node.name)) return
        // Store node.name before overwritten with "Alert".
        const status = node.name

        const data = node.data || (node.data = {})
        const tagName = node.type === "textDirective" ? "span" : "div"

        node.type = "mdxJsxFlowElement"
        node.name = "Admonition"
        node.attributes = [{ type: "mdxJsxAttribute", name: "status", value: status }]
      }
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Note that the node's name is Admonition. We'll have to add a custom MDX component that has the same name.

React component and styles

Then we need a React component that accepts the status property. This value comes from any of :::info, :::warn or :::danger.

import { HiInformationCircle, HiExclamationTriangle } from "react-icons/hi2"
import clsx from "clsx"
import React from "react"

import styles from "./styles.module.scss"

type SvgComponent = React.ComponentType<React.ComponentProps<"svg">>
type Status = "info" | "danger" | "warn"

type Props = {
  status?: Status
  title?: React.ReactNode
  children?: React.ReactNode
  className?: string
}

const statusIconMap: { [S in Status]: SvgComponent } = {
  info: HiInformationCircle,
  danger: HiExclamationTriangle,
  warn: HiExclamationTriangle,
}

export default function Message({ status = "info", children, className }: Props) {
  const Icon = statusIconMap[status]
  const statusClass = styles[`--${status}`]
  return (
    <aside className={clsx(styles.container, statusClass, className)}>
      <div>
        <Icon className={clsx(styles.icon, statusClass)} />
      </div>
      <div className={styles.text}>
        {children && <div className={styles.message}>{children}</div>}
      </div>
    </aside>
  )
}
Enter fullscreen mode Exit fullscreen mode
$infoColor: #3182ce;
$warnColor: #e6a700;
$dangerColor: #e53e3e;

.container {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.5rem;
  padding: 1rem;
  border-radius: 0.5rem;
  border: 1px solid #e2e8f0;
  border-left-width: 4px;
  background: white;

  &.--info {
    border-left-color: $infoColor;
  }
  &.--warn {
    border-left-color: $warnColor;
  }
  &.--danger {
    border-left-color: $dangerColor;
  }
}

.text {
  display: grid;
  row-gap: 0.25rem;
}

.title {
  display: flex;
  align-items: center;
  column-gap: 0.5rem;
  font-weight: var(--camome-font-weight-bold);
  font-size: var(--camome-font-size-lg);

  &.--info {
    color: $infoColor;
  }
  &.--warn {
    color: $warnColor;
  }
  &.--danger {
    color: $dangerColor;
  }
}

.message {
  & > *:first-child {
    margin-top: 0;
  }

  & > *:last-child {
    margin-bottom: 0;
  }
}

.icon {
  width: 1.75rem;
  height: 1.75rem;

  &.--info {
    color: $infoColor;
  }
  &.--warn {
    color: $warnColor;
  }
  &.--danger {
    color: $dangerColor;
  }
}
Enter fullscreen mode Exit fullscreen mode

Pass it as a custom component

Now, register it as a custom component otherwise MDX complains that you've forgotten to import it!

import { MDXProvider } from "@mdx-js/react"
import Admonition from "@/components/Admonition"

const components = {
  Admonition,
}

export default function Post(props) {
  return (
    <MDXProvider components={components}>
      <main {...props} />
    </MDXProvider>
  )
}
Enter fullscreen mode Exit fullscreen mode

Exact code could be different if you're using mdx-bundler, Contentlayer, or anything else. But there should be a similar API for adding components.

That's all

You can add another type like :::note or anything, and style the component as you want.

Thank you for reading!

Top comments (0)