跳至内容
返回

在 Astro 项目中使用自定义 Remark 插件

发布于:  at  05:00 下午
阅读量: 0

本文介绍如何在全新的 Astro 项目中使用两个自定义 Remark 插件——remark-admonitions(提示框)和 remark-button(按钮),让 Markdown 内容支持丰富的自定义组件。

当然我也知道网上应该有不少开箱即用的插件,但我还是选择了这个方式。

效果预览

在正式安装之前,先来看看这两个插件能实现什么效果。

提示框(Admonitions)

五种提示框分别适用于不同场景:

这是一个普通的注意提示框,用于补充说明。

这是一个友好的提示信息,可以分享小技巧。

这是一个信息提示框,用于提供额外信息。

这是一个警告提示框,用于提醒用户注意。

这是一个危险警告,用于提醒用户谨慎操作。

按钮(Button)

以下是各种按钮变体:

小号主要按钮

中号成功按钮

大号危险按钮

警告按钮

自定义紫色圆角按钮

插件简介

这两个插件基于 remark-directive 构建,利用 Markdown 的 ::: 指令语法在文章中插入自定义 UI 组件。

前置依赖

在开始之前,确保你的项目满足以下条件:

需要安装的 npm 包:

包名用途
remark-directive解析 Markdown 中的 ::: 指令语法
unist-util-visit遍历和操作 AST 节点
hastscript以声明式方式创建 hast(HTML AST)节点
pnpm add remark-directive unist-util-visit hastscript

第一步:创建插件文件

在项目根目录下创建 plugins/ 目录,并将以下两个文件放入其中。

remark-admonitions.ts

import { visit } from "unist-util-visit";
import { h } from "hastscript";

const admonitions = ["note", "tip", "info", "warning", "danger"];

export default function remarkAdmonitions() {
  return tree => {
    visit(tree, node => {
      if (
        node.type === "containerDirective" ||
        node.type === "leafDirective" ||
        node.type === "textDirective"
      ) {
        if (admonitions.includes(node.name)) {
          const data = node.data || (node.data = {});
          const hast = h("div", node.attributes || {});

          if (node.children && node.children.length > 0) {
            const firstChild = node.children[0];
            if (firstChild.type === "paragraph") {
              const firstChildData = firstChild.data || (firstChild.data = {});
              const existingClass =
                (firstChildData.hProperties?.className as string[]) || [];
              firstChildData.hProperties = {
                ...firstChildData.hProperties,
                className: ["admonition-title", ...existingClass],
              };
            }
          }

          data.hName = hast.tagName;
          data.hProperties = {
            ...hast.properties,
            className: [
              node.name,
              ...((hast.properties?.className as string[]) || []),
            ],
          };
        }
      }
    });
  };
}

remark-button.ts

import { visit } from "unist-util-visit";
import { h } from "hastscript";

const PRESET_SIZES = ["xs", "sm", "md", "lg", "xl"];
const PRESET_COLORS = [
  "primary",
  "secondary",
  "success",
  "danger",
  "warning",
  "info",
  "light",
  "dark",
  "pink",
  "purple",
  "indigo",
  "teal",
  "cyan",
  "orange",
];
const PRESET_TEXT_COLORS = ["white", "black", "gray", "light", "dark"];

export default function remarkButton() {
  return tree => {
    visit(tree, node => {
      if (
        node.type === "containerDirective" ||
        node.type === "leafDirective" ||
        node.type === "textDirective"
      ) {
        if (node.name === "button") {
          const data = node.data || (node.data = {});
          const hast = h("a", node.attributes || {});

          const classNames = ["custom-button"];
          const inlineStyles: string[] = [];

          if (node.attributes?.size) {
            if (PRESET_SIZES.includes(node.attributes.size)) {
              classNames.push(`button-${node.attributes.size}`);
            } else {
              inlineStyles.push(`font-size: ${node.attributes.size}`);
            }
          }

          if (node.attributes?.color) {
            if (PRESET_COLORS.includes(node.attributes.color)) {
              classNames.push(`button-${node.attributes.color}`);
            } else {
              inlineStyles.push(`background-color: ${node.attributes.color}`);
            }
          }

          if (node.attributes?.textColor) {
            if (PRESET_TEXT_COLORS.includes(node.attributes.textColor)) {
              classNames.push(`button-text-${node.attributes.textColor}`);
            } else {
              inlineStyles.push(`color: ${node.attributes.textColor}`);
            }
          }

          if (node.attributes?.padding) {
            inlineStyles.push(`padding: ${node.attributes.padding}`);
          }

          if (node.attributes?.borderRadius) {
            inlineStyles.push(`border-radius: ${node.attributes.borderRadius}`);
          }

          classNames.push(...((hast.properties?.className as string[]) || []));

          const properties: Record<string, string | string[]> = {
            ...hast.properties,
            className: classNames,
            target: node.attributes?.target || "_blank",
            rel: "noopener noreferrer",
          };

          if (inlineStyles.length > 0) {
            properties.style = inlineStyles.join("; ");
          }

          data.hName = "a";
          data.hProperties = properties;
        }
      }
    });
  };
}

第二步:配置 Astro

编辑 astro.config.ts,在 markdown.remarkPlugins 中注册这两个插件:

import { defineConfig } from "astro/config";
import remarkDirective from "remark-directive";
import remarkAdmonitions from "./plugins/remark-admonitions";
import remarkButton from "./plugins/remark-button";

export default defineConfig({
  markdown: {
    remarkPlugins: [
      remarkDirective, // 必须在自定义插件之前注册
      remarkAdmonitions,
      remarkButton,
    ],
  },
});

remarkDirective 必须排在 remarkAdmonitionsremarkButton 之前。因为 remark-directive 负责解析 ::: 语法生成 directive 节点,自定义插件再将这些节点转换为 HTML。如果顺序错误,自定义插件收不到 directive 节点,所有 ::: 区块都不会被渲染。

第三步:添加 CSS 样式

提示框样式

在你的全局 CSS 文件(如 src/styles/global.css)中添加:

/* 提示框基础样式 */
.prose .note,
.prose .tip,
.prose .info,
.prose .warning,
.prose .danger {
  @apply my-4 rounded-lg border-l-4 p-4;
}

/* 提示框标题 */
.prose .note .admonition-title,
.prose .tip .admonition-title,
.prose .info .admonition-title,
.prose .warning .admonition-title,
.prose .danger .admonition-title {
  @apply mb-2 text-lg font-bold;
}

/* 各类型配色 */
.prose .note {
  @apply border-blue-500 bg-blue-50 text-blue-900;
}

.prose .tip {
  @apply border-green-500 bg-green-50 text-green-900;
}

.prose .info {
  @apply border-cyan-500 bg-cyan-50 text-cyan-900;
}

.prose .warning {
  @apply border-yellow-500 bg-yellow-50 text-yellow-900;
}

.prose .danger {
  @apply border-red-500 bg-red-50 text-red-900;
}

/* 暗色模式适配 */
html[data-theme="dark"] .prose .note {
  @apply border-blue-400 bg-blue-950/50 text-blue-100;
}

html[data-theme="dark"] .prose .tip {
  @apply border-green-400 bg-green-950/50 text-green-100;
}

html[data-theme="dark"] .prose .info {
  @apply border-cyan-400 bg-cyan-950/50 text-cyan-100;
}

html[data-theme="dark"] .prose .warning {
  @apply border-yellow-400 bg-yellow-950/50 text-yellow-100;
}

html[data-theme="dark"] .prose .danger {
  @apply border-red-400 bg-red-950/50 text-red-100;
}

按钮样式

/* 按钮基础样式 */
.custom-button {
  @apply inline-flex items-center justify-center rounded-lg bg-accent px-6 py-1 text-base font-medium text-white transition-all duration-200 hover:shadow-lg;
  text-decoration: none;
}

.custom-button:hover {
  transform: translateY(-2px);
}

.custom-button:active {
  transform: translateY(0);
}

/* 尺寸变体 */
.button-xs {
  @apply px-3 py-1 text-xs;
}
.button-sm {
  @apply px-4 py-1.5 text-sm;
}
.button-md {
  @apply px-6 py-2.5 text-base;
}
.button-lg {
  @apply px-8 py-3 text-lg;
}
.button-xl {
  @apply px-10 py-4 text-xl;
}

/* 颜色变体 */
.button-primary {
  @apply bg-blue-600 hover:bg-blue-700;
}
.button-secondary {
  @apply bg-gray-600 hover:bg-gray-700;
}
.button-success {
  @apply bg-green-600 hover:bg-green-700;
}
.button-danger {
  @apply bg-red-600 hover:bg-red-700;
}
.button-warning {
  @apply bg-yellow-500 hover:bg-yellow-600;
}
.button-info {
  @apply bg-blue-500 hover:bg-blue-600;
}
.button-light {
  @apply bg-gray-200 hover:bg-gray-300;
}
.button-dark {
  @apply bg-gray-800 hover:bg-gray-900;
}
.button-pink {
  @apply bg-pink-500 hover:bg-pink-600;
}
.button-purple {
  @apply bg-purple-600 hover:bg-purple-700;
}
.button-indigo {
  @apply bg-indigo-600 hover:bg-indigo-700;
}
.button-teal {
  @apply bg-teal-500 hover:bg-teal-600;
}
.button-cyan {
  @apply bg-cyan-500 hover:bg-cyan-600;
}
.button-orange {
  @apply bg-orange-500 hover:bg-orange-600;
}

/* 文字颜色变体 */
.button-text-white {
  @apply text-white;
}
.button-text-black {
  @apply text-black;
}
.button-text-gray {
  @apply text-gray-800;
}
.button-text-light {
  @apply text-gray-100;
}
.button-text-dark {
  @apply text-gray-900;
}

以上样式使用 Tailwind CSS 的 @apply 指令。如果你不使用 Tailwind,将 @apply 替换为等价的 CSS 属性即可。

第四步:在 Markdown 中使用

提示框语法

支持五种类型:notetipinfowarningdanger。提示框内的第一个段落会自动渲染为加粗标题。

:::note
这是一个普通的注意提示框。
:::

:::tip
这是一个友好的提示信息。
:::

:::info
这是一个信息提示框,用于提供额外信息。
:::

:::warning
这是一个警告提示框,用于提醒用户注意。
:::

:::danger
这是一个危险警告,用于提醒用户谨慎操作。
:::

按钮语法

按钮使用 :::button{属性...} 语法,支持以下属性:

属性说明预设值自定义示例
href按钮链接地址任意 URLhref="https://example.com"
size按钮尺寸xs, sm, md, lg, xlsize="1.5rem"
color背景颜色primary, secondary, success, danger, warning, info, light, dark, pink, purple, indigo, teal, cyan, orangecolor="#ff6b6b"
textColor文字颜色white, black, gray, light, darktextColor="#2d3436"
padding内边距任意 CSS 值padding="1rem 2rem"
borderRadius圆角任意 CSS 值borderRadius="50px"
target链接打开方式默认 _blanktarget="_self"

sizecolortextColor 使用预设值时生成对应的 CSS 类名;使用非预设值时,插件会将值直接作为内联 CSS 属性写入元素的 style 属性中。

预设尺寸

:::button{href="https://example.com" size="xs"}
超小号按钮
:::

:::button{href="https://example.com" size="sm"}
小号按钮
:::

:::button{href="https://example.com" size="md"}
中号按钮
:::

:::button{href="https://example.com" size="lg"}
大号按钮
:::

:::button{href="https://example.com" size="xl"}
超大号按钮
:::

预设颜色

:::button{href="https://example.com" color="primary"}
主要按钮
:::

:::button{href="https://example.com" color="danger"}
危险按钮
:::

:::button{href="https://example.com" color="warning" textColor="black"}
警告按钮(深色文字)
:::

完全自定义

:::button{href="https://example.com" size="0.75rem"}
超小字体按钮
:::

:::button{href="https://example.com" color="#ff6b6b"}
自定义红色按钮
:::

:::button{href="https://example.com" color="#ffeaa7" textColor="#2d3436"}
自定义黄色按钮深色文字
:::

:::button{href="https://example.com" padding="1rem 2rem"}
自定义内边距按钮
:::

:::button{href="https://example.com" borderRadius="2rem"}
圆角按钮
:::

组合使用

:::button{href="https://example.com" size="lg" color="success"}
大号成功按钮
:::

:::button{href="https://example.com" size="sm" color="danger"}
小号危险按钮
:::

:::button{href="https://example.com" color="#fd79a8" textColor="white" padding="1.5rem 3rem" borderRadius="50px"}
完全自定义按钮
:::

更多注意事项

插件注册顺序

处理流程如下:

  1. remark-directive:::name{...} 语法解析为 directive AST 节点
  2. remark-admonitions / remark-button 将匹配到的 directive 节点转换为 HTML

关于 TypeScript

如果你使用 astro check 做类型检查,可能需要为插件文件添加类型声明。推荐在 plugins/ 目录下创建对应的声明文件:

// plugins/remark-admonitions.d.ts
declare module "./plugins/remark-admonitions" {
  const value: () => (tree: any) => void;
  export default value;
}

// plugins/remark-button.d.ts
declare module "./plugins/remark-button" {
  const value: () => (tree: any) => void;
  export default value;
}

CSS 框架兼容性

已知限制

  1. 按钮默认在新标签页打开(target="_blank"),如需当前页跳转请指定 target="_self"
  2. 按钮的 rel 属性固定为 "noopener noreferrer",无法自定义
  3. 提示框的第一个段落始终被渲染为标题,无法禁用
  4. 提示框目前不支持自定义属性传入,仅支持五种预设类型

在以下平台分享此文章:

上一篇
尝试着接入了一个在线游戏平台
下一篇
全链条白嫖建站资源