VuePress 博客优化之拓展 Markdown 语法

冴羽
• 阅读 1209

前言

《一篇带你用 VuePress + Github Pages 搭建博客》中,我们使用 VuePress 搭建了一个博客,最终的效果查看:TypeScript 中文文档

如果我们浏览过 TypeScript 官方文档,我们会发现一个很好用的功能,那就是很多代码块,在悬浮上去的时候都会出现一个 Try 按钮:

VuePress 博客优化之拓展 Markdown 语法

点击就会跳转到对应的 Playground,比如图示的按钮跳转的就是这个链接,我们可以在这个 Playground 修改并验证代码效果。

如果我们要实现这样的功能,该怎么实现呢?

思考

我们很容易想到,写一个 VuePress 插件来实现它,这个效果看起来有点像代码复制插件,但细细一想,又非如此。

代码复制插件的实现方式,参考 《从零实现一个 VuePress 插件》,可以在页面渲染完成后,遍历每一个代码块然后插入一个复制按钮,点击复制的时候将代码写入剪切板,但是代码块跳转就不一样了,代码跳转需要我们先写入一个链接地址,然后再渲染按钮,问题是这个链接的地址写在哪里呢?要知道,我们能写的只是一个普通的 markdown 文件呀……

于是我们就想到,是否可以拓展 markdown 的语法呢?就比如正常的代码块写法是:

```typescript
const message = "Hello World!";
```

为了实现这个效果,我们是否可以这样写:

```typescript
// try-link: https://www.baidu.com
const message = 'Hello World!';
```

但是渲染的时候,并不渲染 try-link 这行注释,而是变成这样的效果:

VuePress 博客优化之拓展 Markdown 语法

当点击 Try 的时候,跳转到对应的链接。

当然效果更好的话,可以在鼠标悬浮在代码块上方的时候,才显示这个 Try 按钮,类似于这种效果:

VuePress 博客优化之拓展 Markdown 语法

markdown-it

查阅 VuePress 的官方文档,我们可以知道:VuePress 使用 markdown-it来渲染 Markdown,那markdown-it是什么呢?查阅 markdown-it 的 Github 仓库,可以看到这样一段介绍:

Markdown parser done right. Fast and easy to extend.

简单的来说,markdown-it就是一个 markdown 渲染器,可以将 markdown 渲染成 html 等,而且 markdown-it 支持写插件拓展功能,实际上,VuePress 项目中的 markdown 文件为什么能支持写 Vue 组件,就是因为 VuePress 写了插件支持了 Vue 语法,那我们是不是也可以拓展 markdown 的语法呢?

还好在 VuePress 文档里,提供了配置,可以自定义 markdown-it 插件:

VuePress 使用 markdown-it (opens new window)来渲染 Markdown,上述大多数的拓展也都是通过自定义的插件实现的。想要进一步的话,你可以通过 .vuepress/config.js 的 markdown 选项,来对当前的 markdown-it 实例做一些自定义的配置:

module.exports = {
  markdown: {
    // markdown-it-anchor 的选项
    anchor: { permalink: false },
    // markdown-it-toc 的选项
    toc: { includeLevel: [1, 2] },
    extendMarkdown: md => {
      // 使用更多的 markdown-it 插件!
      md.use(require('markdown-it-xxx'))
    }
  }
}

引入的方法知道了,但怎么写这个 markdown-it 插件呢?

markdown-it 插件

查阅 markdown-itGithub 仓库代码文档,我们可以大致了解到 markdown-it的工作原理,其转换过程类似于 Babel,先转换成抽象语法树,然后生成对应的代码,简单的概括就是分为 Parse 和 Render 两个过程。

这点我们查看源码也可以看到:

MarkdownIt.prototype.render = function (src, env) {
  env = env || {};
  return this.renderer.render(this.parse(src, env), this.options, env);
};

所以这里我们解决问题的思路有两个,一个是在 Parse 过程中处理,一个在 Render 过程中处理,为了简单起见,我决定直接处理 Render 过程,查看 Render 的源码,我们可以看到 Render 里其实已经根据一些固定的类型写了默认 Rules(渲染规则),就比如关于代码块:

default_rules.fence = function (tokens, idx, options, env, slf) {
  var token = tokens[idx],
      info = token.info ? unescapeAll(token.info).trim() : '',
      langName = '',
      langAttrs = '',
      highlighted, i, arr, tmpAttrs, tmpToken;

  if (info) {
    // ...
  }

  if (options.highlight) {
    highlighted = options.highlight(token.content, langName, langAttrs) || escapeHtml(token.content);
  } else {
    highlighted = escapeHtml(token.content);
  }

  if (highlighted.indexOf('<pre') === 0) {
    return highlighted + '\n';
  }

  if (info) {
    //...
  }


  return  '<pre><code' + slf.renderAttrs(token) + '>'
        + highlighted
        + '</code></pre>\n';
};

我们可以覆盖这个规则,参照 markdown-it 提供的插件编写原则,我们可以这样写:

# 获取 md 实例后
md.renderer.rules.fence = function (tokens, idx, options, env, self) {
    // ...
};

为了再省事一点,我准备直接获取最后渲染的 HTML 结果,它是一个字符串,然后匹配 //try-link: xxx生成的 HTML,替换成一个 <a>链接,我们查看下 //try-link: xxx这句注释生成的 HTML:

VuePress 博客优化之拓展 Markdown 语法

修改下 config.js文件:

module.exports = {
    markdown: {
      extendMarkdown: md => {
        md.use(function(md) {
          const fence = md.renderer.rules.fence
          md.renderer.rules.fence = (...args) => {
            let rawCode = fence(...args);
            rawCode = rawCode.replace(/<span class="token comment">\/\/ try-link https:\/\/(.*)<\/span>\n/ig, '<a href="$1" class="try-button" target="_blank">Try</a>');
            return `${rawCode}`
          }
              })
      }
    }
}

这里为了简洁,我没有将 <a>链接的样式直接内联写入其中,而是加了一个类,那在哪里写这个类的样式呢?

VuePress 提供了 docs/.vuepress/styles/index.styl文件,作为将会被自动应用的全局样式文件,会生成在最终的 CSS 文件结尾,具有比默认样式更高的优先级。

所以我们在 index.styl文件下写入样式:

// 默认样式
.try-button {
    position: absolute;
    bottom: 1em;
    right: 1em;
    font-weight: 100;
    border: 1px solid #719af4;
    border-radius: 4px;
    color: #719af4;
    padding: 2px 8px;
    text-decoration: none;
    transition-timing-function: ease;
    transition: opacity .3s;
    opacity: 0;
}

// hover 样式
.content__default:not(.custom) a.try-button:hover {
    background-color: #719af4;
    color: #fff;
    text-decoration: none;
}

有的时候,自动编译可能不会生效,我们可以重新运行 yarn run docs:dev

此时已经可以正常显示按钮了(默认样式透明度为 0,这里为了截图强行设置透明度为 1):

VuePress 博客优化之拓展 Markdown 语法

接下来我们要实现,鼠标悬浮在代码块的时候,才显示这个按钮,这里我们可以借助 《从零实现一个 VuePress 插件》中的方法,在页面 mounted 的时候,获取所有的代码块元素,然后添加事件,我们再修改下 config.js文件:

module.exports = {
    plugins: [
      (options, ctx) => {
        return {
          name: 'vuepress-plugin-code-try',
          clientRootMixin: path.resolve(__dirname, 'vuepress-plugin-code-try/index.js')
        }
      }
    ],
    markdown: {
      extendMarkdown: md => {
        md.use(function(md) {
          const fence = md.renderer.rules.fence
          md.renderer.rules.fence = (...args) => {
            let rawCode = fence(...args);
            rawCode = rawCode.replace(/<span class="token comment">\/\/ try-link https:\/\/(.*)<\/span>\n/ig, '<a href="$1" class="try-button" target="_blank">Try</a>');
            return `${rawCode}`
          }
              })
      }
    }
}

然后在同级目录config.js下新建一个 vuepress-plugin-code-try目录,然后新建一个 index.js文件:

export default {
  mounted () {
    setTimeout(() => {
        document.querySelectorAll('div[class*="language-"] pre').forEach(el => {
            if (el.querySelector('.try-button')) {
                el.addEventListener('mouseover', () => {
                    el.querySelector('.try-button').style.opacity = '1';
                })
                el.addEventListener('mouseout', () => {
                    el.querySelector('.try-button').style.opacity = '0';
                })
            }
        })
    }, 100)
  }
}

此时,再运行项目,我们就实现了最初想要的效果:

VuePress 博客优化之拓展 Markdown 语法

系列文章

博客搭建系列是我至今写的唯一一个偏实战的系列教程,讲解如何使用 VuePress 搭建博客,并部署到 GitHub、Gitee、个人服务器等平台。

  1. 一篇带你用 VuePress + GitHub Pages 搭建博客
  2. 一篇教你代码同步 GitHub 和 Gitee
  3. 还不会用 GitHub Actions ?看看这篇
  4. Gitee 如何自动部署 Pages?还是用 GitHub Actions!
  5. 一份前端够用的 Linux 命令
  6. 一份简单够用的 Nginx Location 配置讲解
  7. 一篇从购买服务器到部署博客代码的详细教程
  8. 一篇域名从购买到备案到解析的详细教程
  9. VuePress 博客优化之 last updated 最后更新时间如何设置
  10. VuePress 博客优化之添加数据统计功能
  11. VuePress 博客优化之开启 HTTPS
  12. VuePress 博客优化之开启 Gzip 压缩
  13. 从零实现一个 VuePress 插件

微信:「mqyqingfeng」,加我进冴羽唯一的读者群。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

点赞
收藏
评论区
推荐文章
冴羽 冴羽
2年前
有的时候我觉得我不会 Markdown
前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。在优化博客的过程中,因为需要写markdownit插件,翻了下markdown的,突然发现对Markdown还远不够了解:软换行(Softlinebreaks)换行符不在行内代码或HTML标签内,前面没有两个或以上的空格,将解析为软换行(Softlinebr
冴羽 冴羽
2年前
从零实现一个 VuePress 插件
前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。但在搭建VuePress博客的过程中,也并不是所有的插件都能满足需求,所以本篇我们以实现一个代码复制插件为例,教大家如何从零实现一个VuePress插件。本地开发开发插件第一个要解决的问题就是如何本地开发,我们查看VuePress1.0官方文档的「」章节,并没有找到解决
冴羽 冴羽
2年前
搭建 VuePress 站点必做的 10 个优化
前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。在搭建这样一个博客后,其实还有很多的优化工作需要做,本篇我们来盘点一下那些完成基础搭建后必做的10个优化。1.开启HTTPS开启HTTPS有很多好处,比如可以实现数据加密传输等,SEO也会更容易收录:Google会优先选择HTTPS网页(而非等效的HTTP网
冴羽 冴羽
2年前
markdown-it 插件如何写(二)
前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。在搭建博客的过程中,我们出于实际的需求,在中讲解了如何写一个markdownit插件,又在中讲解了markdownit的执行原理,本篇我们将讲解具体的实战代码,帮助大家更好的写插件。Parsemarkdownit的渲染过程分为两部分,Parse和Render,如果我们要实现
冴羽 冴羽
2年前
VuePress 博客之 SEO 优化(一)之 sitemap 与搜索引擎收录
前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。本篇讲讲如何进行SEO优化。1.生成sitemap借助生成站点地图:1.1安装bashyarnaddvuepresspluginsitemapD1.2修改config.jsjavascript//.vuepress/config.jsmodule.expo
冴羽 冴羽
2年前
VuePress 博客优化之添加数据统计功能
前言在中,我们使用VuePress搭建了一个博客,最终的效果点击查看:。今天我们给博客添加数据统计功能。1.百度统计1.1创建站点1.1.1登陆站点登陆百度统计后台:1.1.2新增网站在「管理」「网站列表」中,点击「新增网站」:1.1.3填写信息添加网站域名、网站首页等信息:1.1.4获取代码添加完后,会自动跳转到代码获
冴羽 冴羽
2年前
VuePress 博客之 SEO 优化(五)添加 JSON-LD 数据
前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。本篇讲SEO中的JSONLD。JSONLD如果我们打开掘金任意一篇文章,比如这篇,查看DOM元素,我们可以在head中找到这样一段script标签:在思否等其他平台也是可以看到的:那这个type为application/ldjson的script,到底
冴羽 冴羽
2年前
VuePress 博客之 SEO 优化(四) Open Graph protocol
前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。本篇讲讲SEO优化中的OpenGraphprotocol。meta标签如果我们打开思否任意一篇文章,比如这篇,查看DOM元素,我们可以在head中找到这样一段meta标签:我们可以发现name都是以og:开头,这是什么意思呢,又是什么作用呢?其实这是
冴羽 冴羽
2年前
markdown-it 插件如何写(一)
前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。在搭建博客的过程中,我们出于实际的需求,在中讲解了如何写一个markdownit插件,又在中讲解了markdownit的执行原理,本篇我们将讲解具体的实战代码,帮助大家更好的写插件。renderermarkdownit的渲染过程分为两部分,Parse和Render,如果我们
冴羽 冴羽
2年前
VuePress 博客如何开启本地 HTTPS 访问
前言在中,我们使用VuePress搭建了一个博客,最终的效果查看:。如果我们在本地运行项目,运行地址类似于http://localhost:8080/learntypescript/,以http开头,这在大部分时候都满足了需要,但有的时候,比如兼容PWA,就需要以https开头,那我们如何在本地使用https地址呢?开启HTTPS在
冴羽
冴羽
Lv1
男 · 淘宝 · 前端工程师
GitHub 26K Star 的博客: https://github.com/mqyqingfeng/Blog
文章
32
粉丝
15
获赞
67