跳转到内容

黑幕文本markdown-it插件编写教程,在Markdown中实现类似萌百的效果

发布于: 2026/01/31 06:42:01

内容概要

此文介绍了利用markdown-it插件实现像萌娘百科那样的黑幕文本的效果

想要实现像萌百那样的黑幕文本,或者说剧透文本(Spoiler)的效果,但是显然Markdown的标准语法是不包含这个的。

首先想到的是自制插件实现。

动手制作插件

从VitePress的官方文档了解到其使用了markdown-it来渲染Markdown文件,所以我们实际上就是要编写一个markdown-it插件。

了解mdit的原理

根据markdown-it官方给出的开发指导,开发插件之前至少需要清楚markdown-it的工作原理,比如TokenRules这些概念。然而我把文档读了几遍都还没有什么印象(就算曾经有写此文时也忘得差不多了),就决定“借鉴”一下别人现成的插件。

修改现有插件

搜索NPM包之后,找了一个逻辑比较简单的插件markdown-it-sub,简单改动后得到以下代码。

mjs
// same as UNESCAPE_MD_RE plus a space
const UNESCAPE_RE = /\\([ \\!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/g;

function subscript(state, silent) {
  const max = state.posMax;
  const start = state.pos;

  const mark = '@@'; // 自定义标记

  if (!state.src.startsWith(mark, start)) { return false; }
  if (silent) { return false; } // don't run any pairs in validation mode
  if (start + 2 * mark.length >= max) { return false; }

  state.pos = start + mark.length;
  let found = false;

  while (state.pos < max) {
    if (state.src.startsWith(mark, state.pos)) {
      found = true;
      break;
    }

    state.md.inline.skipToken(state);
  }

  if (!found || start + mark.length === state.pos) {
    state.pos = start;
    return false;
  }

  const content = state.src.slice(start + mark.length, state.pos);

  // don't allow unescaped spaces/newlines inside
  if (content.match(/(^|[^\\])(\\\\)*\s/)) {
    state.pos = start;
    return false;
  }

  // found!
  state.posMax = state.pos;
  state.pos = start + mark.length;

  // Earlier we checked !silent, but this implementation does not need it
  const token_so = state.push('heimu_open', 'span', 1);
  token_so.markup = mark;

  const token_t = state.push('text', '', 0);
  token_t.content = content.replace(UNESCAPE_RE, '$1');

  const token_sc = state.push('heimu_close', 'span', -1);
  token_sc.markup = mark;

  state.pos = state.posMax + mark.length;
  state.posMax = max;
  return true;
}

export default function heimu_plugin(md) {
  md.inline.ruler.after('emphasis', 'heimu', subscript);
  
  var defaultRender = md.renderer.rules.heimu_open || function (tokens, idx, options, env, self) {
    return self.renderToken(tokens, idx, options);
  };

  md.renderer.rules.heimu_open = function (tokens, idx, options, env, self) {
    // Add a new `target` attribute, or replace the value of the existing one.
    tokens[idx].attrSet('class', 'heimu');

    // 添加title属性以提供悬停提示
    tokens[idx].attrSet('title', '你知道的太多了');

    // Pass the token to the default renderer.
    return defaultRender(tokens, idx, options, env, self);
  };
};

其主要逻辑就是在默认导出函数heimu_plugin中挂载subscript函数作为一个Rule,实现对Markdown源文件的解析,找到标签的起始点,设置一些Token,最后设置HTML元素的classtitle属性。

此外还需要利用css给拥有heimu类的元素添加样式。

css
:root {
    --wzb-heimu-bg-color: #252525;
    --wzb-heimu-text-color: #252525;
}

.dark{
    --wzb-heimu-bg-color: #eeeeee;
    --wzb-heimu-text-color: #eeeeee;
}

.heimu {
    color: var(--wzb-heimu-text-color);
    background-color: var(--wzb-heimu-bg-color);
    text-shadow: none;
    transition: color 0.13s linear, background-color 0.13s linear, text-shadow 0.13s linear;
}

.heimu:active, .heimu:hover {
    color: unset;
    background-color: unset;
    text-shadow: unset;
}

通过设置变量的方式使得黑幕文本支持暗色主题。

加载mdit插件

修改config.mts文件和index.mts文件如下。

mts
import heimu from './markdown-it-plugins/heimu.mjs'
export default defineConfig({
  markdown: {
    config: (md) => {
      md.use(heimu); // 加载插件
    }
  }
})
mts
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'

import './css/heimu.css' // 加载css

export default {
  extends: DefaultTheme
} satisfies Theme

换用现有插件

上述方法的问题

使用上述方法制作的插件,虽然实现了黑幕效果,但其内部却不能存在其他效果,比如在黑幕文本内部使用强调语法,会直接显示出星号。

Markdown示例

md
黑幕在强调内:**外部@@内部@@外部**

强调在黑幕内:@@外部**内部**外部@@

渲染效果

黑幕在强调内:外部内部外部

强调在黑幕内:外部**内部**外部

换用spoiler插件

出现上述问题的原因还是在于sub插件的需求比较简单,没有处理Token内部的其他Token(甚至还专门写了逻辑避免内部出现Markdown语法)。

那么为了实现更为复杂的功能,就需要参考其他插件。不过这里就要涉及到一些较为复杂的对于Token等等的处理,我最终还是选择了使用现有插件。

我最终安装的插件是@mdit/plugin-spoiler - npm

安装插件之后,还需要设置选项,使该插件渲染的元素套用heimu类的样式。

mts
import { spoiler } from "@mdit/plugin-spoiler"
export default defineConfig({
  markdown: {
    config: (md) => {
      md.use(spoiler, {
        tag: 'span',
        attrs: [["class", "heimu"], ['title', '你知道的太多了'], ["tabindex", "-1"]]
      });
    }
  }
})

然后就可以在黑幕内部使用强调语法了。

甚至黑幕内还能有黑幕(套娃)

处理链接文本

然而直接在黑幕内部使用一些其他效果,还是有可能产生问题的。比如在黑幕内部的链接文本,虽然背景为黑色,但文本仍为原色。

大致效果这样

解决方法就是改良css逻辑,利用CSS优先级单独定义黑幕内部<a>标签的样式。

最终确定的css如下。

css
:root {
    --wzb-heimu-bg-color: #252525;
    --wzb-heimu-text-color: #252525;
}

.dark{
    --wzb-heimu-bg-color: #eeeeee;
    --wzb-heimu-text-color: #eeeeee;
}

.heimu {
    color: var(--wzb-heimu-text-color);
    background-color: var(--wzb-heimu-bg-color);
    text-shadow: none;
    transition: color 0.13s linear, background-color 0.13s linear, text-shadow 0.13s linear;
}

.heimu:active, .heimu:hover {
    color: unset;
    background-color: unset;
    text-shadow: unset;
}

.vp-doc .heimu a {
    color: var(--wzb-heimu-text-color);
    text-shadow: none;
    transition: color 0.13s linear, background-color 0.13s linear, text-shadow 0.13s linear;
}

.vp-doc .heimu:active a, .vp-doc .heimu:hover a {
    color: var(--vp-c-brand-1);
    text-shadow: unset;
}

之后黑幕内部的链接就不会暴露了。