likes
comments
collection
share

翻阅十几个源码仓库,只为了加一个$

作者站长头像
站长
· 阅读数 29

为了在vitepress项目上的代码块某行上添加类似终端输入提示的$#符,我翻阅了vitepress等各个文档及源码,最后新建了一个vitepress-markdown-it-repl插件,才得以实现。。。。。。

缘起

vitepress中可用markdown语法来写文章的现代静态站点生成(SSG)框架。还支持各种主题及自定义主题,更让我觉得眼前一亮的地方是代码块支持代码随语言高亮及聚焦。有天,我想在代码块上某行加上一个$前缀,以示该行是终端命令行。

但是翻遍文档及插件,没有找到能满足需求的解决方案。本着工匠(轴)精神,我就从源码开始深入,直到开发了一个插件才得以实现。

缘续

直接在行的前面添加$ 命令,这种方式显然不好,因为右上角复制代码按钮,会把这个$符号给复制出来(复制出来就不能愉快地Ctrl CCtrl V了,手动狗头)。

一开始想到的方式是在需要的行上新增一个比如叫.repl-in-line的class,通过before伪类加上$,大概的CSS就是:

.repl-in-line:before {
  content: "$";
  position: absolute;
  display: block;
  left: 10px;
  color: #7f7f7f;
}

然后通过自定义主题-自定义CSS,加载这部分CSS即可。

但是,要能够做到在代码块上加上CSS class,只要通过写markdown-it的插件来实现。因为vitepress里用markdown-it来实现markdown语法解析,一些markdown扩展的功能就需要用markdown-it的插件实现。比如,vitepress内置的lineNumberPlugin就是其中的一个插件,用来显示代码块左边的行号;预设了markdown-it-attrs插件用来给markdown内容转换后的html元素添加属性(如添加class,style属性);代码块的语法高亮用了shiki做转换。

这么看下来,就可以用markdown-it-attrs的方式给代码块对应行添加.repl-in-line class就行了。

```bash {class=repl-in-line}
echo a b
```

事情没那么简单,结果渲染后的html中的<code>元素中没有如期的class="repl-in-line"属性。怀疑是不是因为和行语法高亮冲突了(都用{}来配置)。修改markdown-it-attrs的配置前后缀:

export default defineConfig({
  markdown: {
    attrs: {
      leftDelimiter: '[',
      rightDelimiter: ']'
    },
  }
})

结果,还是没效果,看vitepress issues,查到有人提出来了。比如markdown-it-highlight lines conflictsupport adding attributes to code blocks这两条。

维护人员虽然最后添加了enhancement标签,但一时半会儿肯定是用不了的。

原因

那为啥markdown-it-attrs给代码块添加不了属性呢?

我们在vitepress仓库项目目录下,pnpm install安装依赖。查看package.json中的scripts,看到文档启动命令是docs,启动pnpm run docs,实际上执行的命令总结下来就是先将src/client目录和src/node目录打包到dist对应目录上,然后用生成的vitepress(在dist/node/cli.js中)命令执行vitepress dev docs

我们到src/node/cli.ts中,找到命令为dev时的代码:

if (!command || command === 'dev') {
  // ...
  const createDevServer = async () => {
    const server = await createServer(root, argv, async () => {
      // ...
    })
    // ...
  }
  createDevServer().catch((err) => {
    // ...
  })
}

接着createServer来自src/node/serve.ts:

export async function createServer(
  root: string = process.cwd(),
  serverOptions: ServerOptions = {},
  recreateServer?: () => Promise<void>
) {
  // ...
  return createViteServer({
    root: config.srcDir,
    base: config.site.base,
    cacheDir: config.cacheDir,
    plugins: await createVitePressPlugin(config, false, {}, {}, recreateServer),
    server: serverOptions,
    customLogger: config.logger
  })
}

createVitePressPlugin又来自src/node/plugin.ts:

export async function createVitePressPlugin(
  // ...
) {
  // ...
  let markdownToVue: Awaited<ReturnType<typeof createMarkdownToVueRenderFn>>
  // ...
  const vitePressPlugin: Plugin = {
    name: 'vitepress',

    async configResolved(resolvedConfig) {
      config = resolvedConfig
      markdownToVue = await createMarkdownToVueRenderFn(
        // ...
      )
    },
    // ...
  }

  return [
    vitePressPlugin,
    // ...
  ]
}

总算到挂钩markdown了。createMarkdownToVueRenderFn来自于src/node/markdownToVue.ts:

export async function createMarkdownToVueRenderFn(
  // ...
) {
  const md = await createMarkdownRenderer(
    srcDir,
    options,
    base,
    siteConfig?.logger
  )
  // ...
  const html = md.render(src, env)
  // ...
}

createMarkdownRenderer来自于src/node/markdown/markdown.ts:

export const createMarkdownRenderer = async (
  // ...
): Promise<MarkdownRenderer> => {
  // ...

  const md = MarkdownIt({
    html: true,
    linkify: true,
    highlight:
      options.highlight ||
      (await highlight(
        theme,
        options.languages,
        options.defaultHighlightLang,
        logger
      )),
    ...options
  }) as MarkdownRenderer
}

到这里,有两条路,要么深入到MarkdownIt里面,要么深入到highlight。接着先深入到MarkdownIt里面。 注意看上面提到的createMarkdownToVueRenderFn函数里面,const html = md.render(src, env),可见,调用了markdown-it生成对象的render函数。我们再打开markdown-it项目。在markdown-it项目中,入口比较好找,lib/index.js:

function MarkdownIt(presetName, options) {
  // ...
  this.renderer = new Renderer();
  // ...
}
MarkdownIt.prototype.render = function (src, env) {
  // ...
  return this.renderer.render(this.parse(src, env), this.options, env);
};

Renderer来自于lib/renderer.js:

var default_rules = {};
// ...
default_rules.fence = function (tokens, idx, options, env, slf) {
  // ...
  if (highlighted.indexOf('<pre') === 0) {
    return highlighted + '\n';
  }
  // ...
};
// ...
function Renderer() {
  // ...
  this.rules = assign({}, default_rules);
}
Renderer.prototype.render = function (tokens, options, env) {
  var i, len, type,
      result = '',
      rules = this.rules;

  for (i = 0, len = tokens.length; i < len; i++) {
    type = tokens[i].type;

    if (type === 'inline') {
      result += this.renderInline(tokens[i].children, options, env);
    } else if (typeof rules[type] !== 'undefined') {
      result += rules[type](tokens, i, options, env, this);
    } else {
      result += this.renderToken(tokens, i, options, env);
    }
  }

  return result;
};

在这里,我们发现,如果highlighted.indexOf('<pre') === 0成立,就return highlighted + '\n';直接返回了。也就是说,如果有highlighted,就不走markdown-it后面的解析了,走的是highlighted的解析。我们回到vitepress项目中的highlight

highlight来自于src/node/markdown/plugins/highlight.ts:

export async function highlight(
  // ...
): Promise<(str: string, lang: string, attrs: string) => string> {
  // ...
  const highlighter = await getHighlighter({
    themes: hasSingleTheme ? [theme] : [theme.dark, theme.light],
    langs: [...BUNDLED_LANGUAGES, ...languages],
    processors
  })
  // ...
  return (str: string, lang: string, attrs: string) => {
    // ...
    const codeToHtml = (theme: IThemeRegistration) => {
      const res =
        lang === 'ansi'
          ? highlighter.ansiToHtml(str, {
              lineOptions,
              theme: getThemeName(theme)
            })
          : highlighter.codeToHtml(str, {
              lang,
              lineOptions,
              theme: getThemeName(theme)
            })

      return fillEmptyHighlightedLine(cleanup(restoreMustache(res)))
    }
    // ...
  }
}

getHighlighter来自于shiki-processor包,又切到shiki-processor项目。在文件src/highlighter.ts找到getHighlighter:

import { getHighlighter as getShikiHighlighter, Highlighter, HighlighterOptions as ShikiHighlighterOptions } from 'shiki'
// ...
export async function getHighlighter(options: HighlighterOptions = {}): Promise<Highlighter> {
  const highlighter = await getShikiHighlighter(options)
  // ...
  return {
    ...highlighter,
    codeToHtml: (str, htmlOptions) => {
      // ...
      const highlighted = highlighter.codeToHtml(code, {
        // ...
      })

      return postProcess(processors, highlighted, lang)
    },
  }
}

getHighlighter实际来源于shiki包,再切到shiki项目。在文件packages/shiki/src/highlighter.ts找到getHighlighter:

export async function getHighlighter(options: HighlighterOptions): Promise<Highlighter> {
  // ...
  function codeToHtml(
  // ...
  ) {
    // ...
    return renderToHtml(tokens, {
      // ...
    })
  }
  // ...
  return {
    // ...
    codeToHtml,
    // ...
  }
}

renderToHtml来自文件packages/shiki/src/highlighter.ts:

export function renderToHtml(lines: IThemedToken[][], options: HtmlRendererOptions = {}) {
  // ...
  return h(
    'pre',
    { className: 'shiki ' + (options.themeName || ''), style: `background-color: ${bg}` },
    [
      options.langId ? `<div class="language-id">${options.langId}</div>` : '',
      h(
        'code',
        {},
        lines.map((line, index) => {
          // ...
          const lineClasses = getLineClasses(lineOptions).join(' ')
          return h(
            'line',
            {
              className: lineClasses,
              // ...
            },
            line.map((token, index) => {
              // ...
            })
          )
        })
      )
    ]
  )
}

在这里终于看到code元素为啥没有属性了,就算markdown-it-attrs传进来了属性,这里也没设进去!!!

原理

如果要走markdown-it-attrs这条路线,那必须从shiki改起,再到shiki-processor,再到markdown-it,最后再到vitepress。这样下来,你才能在你的vitepress项目使用!

有没有其他更便捷快速的路呢?

经过翻阅markdown-it的近十个插件仓库发现,vitepress内置的markdown-it插件lineNumberPlugin(文件src/node/markdown/lineNumbers.ts)是通过绝对定位在代码块的左边来实现的。虽然$是想显示在代码块里面,但实现方式可以取巧!

实现原理就是:通过配置方式$(2-4,6),明白要在第2,3,4,6这四行开头添加$;再生成对应行内容的html,跟lineNumberPlugin一样通过绝对定位在代码块的开头。

缘灭

于是,开了这么个仓库vitepress-markdown-it-repl,也打包到npm仓库中了。可在你的vitepress项目中方便快捷食用。

具体实现可看源码,不到100行即可实现。

现在,终于可以在我的vitepress项目中实现如下图的显示了!

翻阅十几个源码仓库,只为了加一个$

转载自:https://juejin.cn/post/7237726397220683831
评论
请登录