A remark plugin to parse card layout component(s). It consists of these two elements:
card-grid
- A container for cards that helps users work on the CSS grid layout or the likes
card
- Self-explanatory
- Compatible with the proposed generic syntax for custom directives/plugins in Markdown
- Fully customizable styles
- Written in TypeScript
- ESM only
To install the plugin:
With npm
:
npm install remark-card
With yarn
:
yarn add remark-card
With pnpm
:
pnpm add remark-card
With bun
:
bun install remark-card
General usage:
import rehypeStringify from "rehype-stringify";
import remarkCard from "remark-card";
import remarkDirective from "remark-directive";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified } from "unified";
const normalizeHtml = (html: string) => {
return html.replace(/[\n\s]*(<)|>([\n\s]*)/g, (_match, p1, _p2) =>
p1 ? "<" : ">"
);
};
const parseMarkdown = async (markdown: string) => {
const remarkProcessor = unified()
.use(remarkParse)
.use(remarkDirective)
.use(remarkCard)
.use(remarkRehype)
.use(rehypeStringify);
const output = String(await remarkProcessor.process(markdown));
return output;
}
const input = `
:::card
![image alt](https://xxxxx.xxx/yyy.jpg)
Card content
`
const html = await parseMarkdown(input);
console.log(normalizeHtml(html));
Yields:
<div>
<div class="image-container">
<img src="https://xxxxx.xxx/yyy.jpg" alt="image alt" />
</div>
<div class="content-container">Card content</div>
</div>
At the moment, it takes the following option(s):
export type Config = {
// Whether or not to use custom HTML tags for both `card` and `card-grid`. By default, it's set to `false`.
customHTMLTags?: {
enabled: boolean;
};
cardGridClass?: string;
cardClass?: string;
imageContainerClass?: string;
contentContainerClass?: string;
}
Note
Why do we need those card & card grid class options?
- Since MDX 2, the compiler has come to throw an error "Could not parse expression with acorn: $error" whenever there are unescaped curly braces and the expression inside them is invalid. This breaking change leads the directive syntax (:::xxx{a=b}
) to cause the error, so the options are like an escape hatch for that situation.
For more possible patterns and in-depths explanations on the generic syntax(e.g., :::something[...]{...}
), see ./test/index.test.ts
and this page, respectively.
For example, the following Markdown content:
:::card{.card#card-id}
![image alt](https://xxxxx.xxx/yyy.jpg)
Card content
:::
Yields:
<div id="card-id" class="card">
<div class="image-container">
<img src="https://xxxxx.xxx/yyy.jpg" alt="image alt" />
</div>
<div class="content-container">Card content</div>
</div>
The card-grid
element can be used in combination with the card
element.
For example, the following Markdown content:
::::card-grid{.card-grid}
:::card{.card-1}
![card 1](https://xxxxx.xxx/yyy.jpg)
Card 1
:::
:::card{.card-2}
![card 2](https://xxxxx.xxx/yyy.jpg)
Card 2
:::
:::card{.card-3}
![card 3](https://xxxxx.xxx/yyy.jpg)
Card 3
:::
::::
Yields:
<div class="card-grid">
<div class="card-1">
<div class="image-container">
<img src="https://xxxxx.xxx/yyy.jpg" alt="card 1">
</div>
<div class="content-container">Card 1</div>
</div>
<div class="card-2">
<div class="image-container">
<img src="https://xxxxx.xxx/yyy.jpg" alt="card 2">
</div>
<div class="content-container">Card 2</div>
</div>
<div class="card-3">
<div class="image-container">
<img src="https://xxxxx.xxx/yyy.jpg" alt="card 3">
</div>
<div class="content-container">Card 3</div>
</div>
</div>
If you want to use this in your Astro project, note that you need to install remark-directive
and add it to the astro.config.{js,mjs,ts}
file simultaneously.
import { defineConfig } from 'astro/config';
import remarkCard from "remark-card";
import remarkDirective from "remark-directive";
// ...
export default defineConfig({
// ...
markdown: {
// ...
remarkPlugins: [
// ...
remarkDirective,
remarkCard,
// ...
]
// ...
}
// ...
})
Also, if you want to use your Astro component(s) for customization purposes, make sure to set the customHTMLTags.enabled
to true
and assign your custom components like this:
~/lib/mdx-components.ts
import { Card, CardGrid } from '~/components/elements/Card';
// ...
export const mdxComponents = {
// ...
card: Card,
'card-grid': CardGrid,
// ...
};
~/pages/page.astro
---
import { mdxComponents } from '~/lib/mdx-components';
// ...
const { page } = Astro.props;
const { Content } = await page.render();
---
<Layout>
<Content components={mdxComponents}>
</Layout>
Some key takeaways are:
- The former will be prioritized and used as the alt value of
img
if both the image alt and the card alt are provided - Both
card
andcard-grid
take common & custom HTML attributes- their styles are customizable by providing user-defined CSS class(es)
- e.g.,
border
,background-color
, etc.
- e.g.,
- their styles are customizable by providing user-defined CSS class(es)
- The default values of this plugin's options:
customHTMLTags.enabled
:false
imageContainerClass
: "image-container"contentContainerClass
: "content-container"cardGridClass
:undefined
cardClass
:undefined
- Nested cards
- It seems technically feasible but the use case of this might be rare
- Customizable class names for image & content containers
- It seems technically feasible but the use case of this might be rare
- add a demo screenshot of the actual
card-grid
&card
combination to this page - add customizable class name options for image & content containers
This project is licensed under the MIT License, see the LICENSE file for more details.