markdown-it 外掛如何寫(一)

冴羽發表於2022-01-27

前言

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

在搭建部落格的過程中,我們出於實際的需求,在《VuePress 部落格優化之擴充 Markdown 語法》中講解了如何寫一個 markdown-it外掛,又在 《markdown-it 原理解析》中講解了 markdown-it的執行原理,本篇我們將講解具體的實戰程式碼,幫助大家更好的寫外掛。

renderer

markdown-it的渲染過程分為兩部分,ParseRender,如果我們要更改渲染的效果,就比如在外層包裹一層 div,或者修改 HTML 元素的屬性、新增 class 等,就可以從 Render 過程入手。

markdown-it 的官方文件裡就可以找到自定義 Render 渲染規則的方式:

Instance of Renderer. Use it to modify output look. Or to add rendering rules for new token types, generated by plugins.
var md = require('markdown-it')();

function myToken(tokens, idx, options, env, self) {
  //...
  return result;
};

md.renderer.rules['my_token'] = myToken

markdown-it 內建了一些預設的 rules,你可以直接修改它們,具體有哪些以及渲染方式可以檢視 renderer.js 的原始碼,這裡直接列出來:

  • code_inline
  • code_block
  • fence
  • image
  • hardbreak
  • softbreak
  • text
  • html_block
  • html_inline

例項一

如果我們檢視 VuePress 中程式碼塊的渲染結果,我們會發現每一個程式碼塊外層都包了一層帶 extra-class類名的 div:

image.png

其實,這就是 VuePress 修改了渲染規則產生的,檢視 VuePress 原始碼

module.exports = md => {
  const fence = md.renderer.rules.fence
  md.renderer.rules.fence = (...args) => {
    const [tokens, idx] = args
    const token = tokens[idx]
    const rawCode = fence(...args)
    return `<!--beforebegin--><div class="language-${token.info.trim()} extra-class">` +
    `<!--afterbegin-->${rawCode}<!--beforeend--></div><!--afterend-->`
  }
}

我們可以看到這裡非常巧妙的避開了處理 token,直接使用渲染後的結果,在外層包了一層 div。

例項二

類似於 VuePress 的這種方式,我們也可以在獲取預設渲染內容後,再使用 replace 替換掉一些內容,比如在《VuePress 部落格優化之擴充 Markdown 語法》這篇中,我們自定義了一個程式碼塊語法,就是在 rules.fence中修改了渲染的內容:

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}`
  }
})

例項三

但不可能總是可以這麼取巧,有的時候就是需要處理 token,這裡我們參考 markdown-it 官方提供的設計準則中的例子,當渲染一個圖片的時候,如果連結匹配 /^https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/,我們就將其渲染為一個 iframe ,其他的則保持預設渲染方式:

var md = require('markdown-it')();

var defaultRender = md.renderer.rules.image,
    vimeoRE       = /^https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/;

md.renderer.rules.image = function (tokens, idx, options, env, self) {
  var token = tokens[idx],
      aIndex = token.attrIndex('src');

  if (vimeoRE.test(token.attrs[aIndex][1])) {

    var id = token.attrs[aIndex][1].match(vimeoRE)[2];

    return '<div class="embed-responsive embed-responsive-16by9">\n' +
           '  <iframe class="embed-responsive-item" src="//player.vimeo.com/video/' + id + '"></iframe>\n' +
           '</div>\n';
  }

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

rules.image 傳入的函式引數可以檢視 renderer.js 的原始碼:

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[tokens[i].type](tokens, i, options, env, this);
    } else {
      result += this.renderToken(tokens, i, options, env);
    }
  }

  return result;
};

我們可以看到 rules 傳入的引數,其中 tokens 就是指 tokens 列表,idx 則是指要渲染的 token 索引,所以在程式碼裡才可以通過 tokens[index]獲取目標 token。

然後我們又使用了 tokens.attrIndex,tokens 提供了哪些方法可以檢視官方 API,或者直接檢視 Token 原始碼

我們解釋一下這個示例裡用到的一些方法,先從 token 開始說起,我們舉個例子,看下![video link]([https://www.vimeo.com/123)](https://www.vimeo.com/123))這句 markdown 語法產生的 token(這裡進行了簡化):

{
    "type": "image",
    "tag": "img",
    "attrs": [
        [
            "src",
            "https://www.vimeo.com/123"
        ],
        [
            "alt",
            ""
        ]
    ],
    "children": [
        {
            "type": "text",
            "tag": "",
            "attrs": null,
            "children": null,
            "content": "video link",

        }
    ],
    "content": "video link"
}

可以看到 token 是有一個 attr 屬性的,表明了要渲染的 img 標籤的屬性有哪些,token.attrIndex 就是通過名字獲取屬性索引,然後再通過 token.attrs[aIndex][1]獲取具體的屬性值。

例項四

同樣是來自 markdown-it 官方提供的設計準則中的例子,給所有的連結新增 target="_blank"

// Remember old renderer, if overridden, or proxy to default renderer
var defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) {
  return self.renderToken(tokens, idx, options);
};

md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
  // If you are sure other plugins can't add `target` - drop check below
  var aIndex = tokens[idx].attrIndex('target');

  if (aIndex < 0) {
    tokens[idx].attrPush(['target', '_blank']); // add new attribute
  } else {
    tokens[idx].attrs[aIndex][1] = '_blank';    // replace value of existing attr
  }

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

也許你會奇怪,為什麼會有 rules.link_open?這個並不在預設規則裡呀,可以直接使用嗎?

還真是可以的,其實這裡的 link_open 和之前的 imagefence 等都是 token 的 type,所以只要是 token 的 type 就可以,那 token 有哪些 type 呢?有具體的說明文件嗎?

關於這個問題, markdown-it 也有 issues 裡提出:
image.png

作者的意思就是,沒有,如果你想寫外掛,你就去看原始碼……

那成吧,其實在我們的實際開發中,如果你想要知道某一個 token type,其實完全可以列印出 token 看一下,官方的 Live Demo 提供了 debug 模式用於檢視 token:

image.png

當然就這個例子裡的需求,作者還提供了 markdown-it-for-inline 外掛用於簡化程式碼書寫:

var iterator = require('markdown-it-for-inline');

var md = require('markdown-it')()
            .use(iterator, 'url_new_win', 'link_open', function (tokens, idx) {
              var aIndex = tokens[idx].attrIndex('target');

              if (aIndex < 0) {
                tokens[idx].attrPush(['target', '_blank']);
              } else {
                tokens[idx].attrs[aIndex][1] = '_blank';
              }
            });

關於 markdown-it-for-inline就在以後的文章裡介紹了。

系列文章

部落格搭建系列是我至今寫的唯一一個偏實戰的系列教程,預計 20 篇左右,講解如何使用 VuePress 搭建、優化部落格,並部署到 GitHub、Gitee、私有伺服器等平臺。全系列文章地址:https://github.com/mqyqingfeng/Blog

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

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。

相關文章