[!important] Website: https://r4ai.github.io/remark-callout
A remark plugin to add obsidian style callouts to markdown.
> [!note] title here
> body here
# npm
npm install @r4ai/remark-callout
# pnpm
pnpm install @r4ai/remark-callout
# bun
bun add @r4ai/remark-callout
See Usage.
import remarkParse from "remark-parse";
import { unified } from "unified";
import remarkCallout from "@r4ai/remark-callout";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
const md = `
> [!note] title here
> body here
`;
const html = unified()
.use(remarkParse)
.use(remarkCallout)
.use(remarkRehype)
.use(rehypeStringify)
.processSync(md)
.toString();
console.log(html);
yields:
<div data-callout data-callout-type="note">
<div data-callout-title>title here</div>
<div data-callout-body>
<p>body here</p>
</div>
</div>
-
Install the plugin:
npm install @r4ai/remark-callout
-
Add
@r4ai/remark-callout
to remark plugins in your astro config file (e.g.astro.config.ts
):// astro.config.ts import remarkCallout from "@r4ai/remark-callout"; export default defineConfig({ // ... markdown: { // ... remarkPlugins: [ // ... remarkCallout, ], }, });
Note: This plugin works fine in MDX files as well. For instructions on how to use MDX with Astro, see @astrojs/mdx.
-
Start using callouts in your markdown or mdx files:
> [!note] title here > body here
yields:
<div data-callout data-callout-type="note"> <div data-callout-title>title here</div> <div data-callout-body> <p>body here</p> </div> </div>
Now you can style the callouts using CSS. Following is an example of how you can style the callouts using Tailwind CSS:
Or if you are using MDX, you can use custom components to style the callouts:
// astro.config.ts import { remarkCallout } from "@r4ai/remark-callout"; export default defineConfig({ // ... markdown: { // ... remarkPlugins: [ // ... [ remarkCallout, { root: (callout) => ({ tagName: "callout", properties: { calloutType: callout.type, isFoldable: String(callout.isFoldable), }, }), title: (callout) => ({ tagName: "callout-title", properties: { calloutType: callout.type, isFoldable: String(callout.isFoldable), }, }), }, ], ], }, });
--- // src/components/Callout.astro type Props = { calloutType: string isFoldable: boolean } const { calloutType, isFoldable } = Astro.props --- <div class={/* Your TailwindCSS style here */} > <slot /> </div>
--- // src/components/CalloutTitle.astro type Props = { callouType: string isFoldable: boolean } const { calloutType, isFoldable } = Astro.props --- <div class={/* Your TailwindCSS style here */} > <SomeIconComponent /> <slot /> </div>
--- // src/pages/callout-example.astro import { Content, components } from "../content.mdx"; import Callout from "../components/Callout.astro"; import CalloutTitle from "../components/CalloutTitle.astro"; --- <Content components={{ ...components, callout: Callout, "callout-title": CalloutTitle }} />
Options type:
export type Options = {
/**
* The root node of the callout.
*
* @default
* (callout) => ({
* tagName: callout.isFoldable ? "details" : "div",
* properties: {
* dataCallout: true,
* dataCalloutType: callout.type,
* open: callout.defaultFolded === undefined ? false : !callout.defaultFolded,
* },
* })
*/
root?: NodeOptions | NodeOptionsFunction;
/**
* The title node of the callout.
*
* @default
* (callout) => ({
* tagName: callout.isFoldable ? "summary" : "div",
* properties: {
* dataCalloutTitle: true,
* },
* })
*/
title?: NodeOptions | NodeOptionsFunction;
/**
* The body node of the callout.
*
* @default
* () => ({
* tagName: "div",
* properties: {
* dataCalloutBody: true,
* },
* })
*/
body?: NodeOptions | NodeOptionsFunction;
/**
* A list of callout types that are supported.
* - If `undefined`, all callout types are supported. This means that this plugin will not check if the given callout type is in `callouts` and never call `onUnknownCallout`.
* - If a list, only the callout types in the list are supported. This means that if the given callout type is not in `callouts`, this plugin will call `onUnknownCallout`.
* @example ["info", "warning", "danger"]
* @default undefined
*/
callouts?: string[] | null;
/**
* A function that is called when the given callout type is not in `callouts`.
*
* - If the function returns `undefined`, the callout is ignored. This means that the callout is rendered as a normal blockquote.
* - If the function returns a `Callout`, the callout is replaced with the returned `Callout`.
*/
onUnknownCallout?: (callout: Callout, file: VFile) => Callout | undefined;
};
export type NodeOptions = {
/**
* The HTML tag name of the node.
*
* @see https://github.com/syntax-tree/hast?tab=readme-ov-file#element
*/
tagName: string;
/**
* The html properties of the node.
*
* @see https://github.com/syntax-tree/hast?tab=readme-ov-file#properties
* @see https://github.com/syntax-tree/hast?tab=readme-ov-file#element
* @example { "className": "callout callout-info" }
*/
properties: Properties;
};
export type NodeOptionsFunction = (callout: Callout) => NodeOptions;
export type Callout = {
/**
* The type of the callout.
*/
type: string;
/**
* Whether the callout is foldable.
*/
isFoldable: boolean;
/**
* Whether the callout is folded by default.
*/
defaultFolded?: boolean;
/**
* The title of the callout.
*/
title?: string;
};
Default options:
export const defaultOptions: Required<Options> = {
root: (callout) => ({
tagName: callout.isFoldable ? "details" : "div",
properties: {
dataCallout: true,
dataCalloutType: callout.type,
open:
callout.defaultFolded === undefined ? false : !callout.defaultFolded,
},
}),
title: (callout) => ({
tagName: callout.isFoldable ? "summary" : "div",
properties: {
dataCalloutTitle: true,
},
}),
body: () => ({
tagName: "div",
properties: {
dataCalloutBody: true,
},
}),
callouts: null,
onUnknownCallout: () => undefined,
};
To install dependencies:
bun install
To run tests with web ui:
bun run test --ui
To build the project:
bun run build