VuePress 部落格優化之擴充 Markdown 語法

冴羽發表於2022-01-22

前言

《一篇帶你用 VuePress + Github Pages 搭建部落格》中,我們使用 VuePress 搭建了一個部落格,最終的效果檢視:TypeScript 中文文件

如果我們瀏覽過 TypeScript 官方文件,我們會發現一個很好用的功能,那就是很多程式碼塊,在懸浮上去的時候都會出現一個 Try 按鈕:

image.png

點選就會跳轉到對應的 Playground,比如圖示的按鈕跳轉的就是這個連結,我們可以在這個 Playground 修改並驗證程式碼效果。

如果我們要實現這樣的功能,該怎麼實現呢?

思考

我們很容易想到,寫一個 VuePress 外掛來實現它,這個效果看起來有點像程式碼複製外掛,但細細一想,又非如此。

程式碼複製外掛的實現方式,參考 《從零實現一個 VuePress 外掛》,可以在頁面渲染完成後,遍歷每一個程式碼塊然後插入一個複製按鈕,點選複製的時候將程式碼寫入剪下板,但是程式碼塊跳轉就不一樣了,程式碼跳轉需要我們先寫入一個連結地址,然後再渲染按鈕,問題是這個連結的地址寫在哪裡呢?要知道,我們能寫的只是一個普通的 markdown 檔案呀……

於是我們就想到,是否可以擴充 markdown 的語法呢?就比如正常的程式碼塊寫法是:

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

為了實現這個效果,我們是否可以這樣寫:

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

但是渲染的時候,並不渲染 try-link 這行註釋,而是變成這樣的效果:

image.png

當點選 Try 的時候,跳轉到對應的連結。

當然效果更好的話,可以在滑鼠懸浮在程式碼塊上方的時候,才顯示這個 Try 按鈕,類似於這種效果:

1234.gif

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:

image.png

修改下 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):

image.png

接下來我們要實現,滑鼠懸浮在程式碼塊的時候,才顯示這個按鈕,這裡我們可以藉助 《從零實現一個 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)
  }
}

此時,再執行專案,我們就實現了最初想要的效果:

1234.gif

系列文章

部落格搭建系列是我至今寫的唯一一個偏實戰的系列教程,講解如何使用 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,對作者也是一種鼓勵。

相關文章