Markdown-it源码分析

基础用法

const MarkdownIt = require('markdown-it');

const md = new MarkdownIt();
var htmlString = md.render('## hello!');

解析流程 - render方法做了什么

要理解MarkdownIt的解析流程,就必须知道render方法做了哪些工作. 首先让我们来看一下render方法的源码

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

MarkdownIt.prototype.parse = function(src, env) {
  if (typeof src !== "string") {
    throw new Error("Input data should be a String");
  }
  var state = new this.core.State(src, this, env);
  this.core.process(state);
  return state.tokens;
};

稍作整理, 我们可以将以上代码简化为

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

根据源码我们可以将整个解析流程归纳成两个步骤:

  1. 将传入的markdown字符串解析成tokens
  2. tokens转化成HTML字符串, 并返回给用户

Tokens - 转换的秘诀

现在我们接触到了MarkdownIt的第一个核心概念 - tokens. 那么什么是tokens呢? 我们通过控制台打印可以获得以下结果

var tokens = [
  [
    type: "heading_open",
    tag: "h2",
    attrs: ["id", "tokens2"],
    block: true,
    content: "",
    children: null,
  ],
  [
    type: "inline",
    tag: "h2",
    attrs: null,
    block: false,
    content: "hello!"
    children: [
      [
        type: "text"        
        tag: ""
        attrs: null
        block: false
        children: null
        content: "Tokens2"
      ]
    ]
  ],
  [
    type: "heading_close",
    tag: "h2",
    attrs: null,
    block: true,
    content: "",
    children: null,
    hidden: false,
  ]
]

可以看到, 一个token包含了type, tag, content, children等属性. 其中tag代表了token将被渲染成的HTML标签. content则表示内容.children则是该标签的子元素.

用户输入的markdown字符串, 在经过md.parse方法处理之后,都会转换成一个个的token. 并放到state.tokens数组中.

所以我们可以将tokens理解为从markdown转换到html的一种必经的, 具有固定格式的中间态

Ruler - token是怎么来的

知道了tokens是做什么的, 那么tokens是怎么来的呢? 现在让我们一起来学习MarkdownIt的第二个核心概念 - Ruler.

让我们一起看看md.parse方法做了什么

Ruler的构造方法非常简单. __rules__数组, 用于缓存各种解析规则, 也就是rule. 根据作者的注释, 我们也能大致了解rule大概长什么样, 包含了name,enabled, fn, alt4个属性. __cache__则 缓存了这些规则.

function Ruler() {
  // List of added rules. Each element is:
  //
  // {
  //   name: XXX,
  //   enabled: Boolean,
  //   fn: Function(),
  //   alt: [ name2, name3 ]
  // }
  //
  this.__rules__ = [];
  // Cached rule chains.
  this.__cache__ = null;
}

看完了Ruler, 我们再来看看 具体的rule, 也就是解析规则. rule的作用可以归纳为以下两点

  1. 标准化或格式化markdown字符串, 使其具备特定的格式, 比如normalize函数
  2. 生成token, 比如block函数. 所有的rules将按照特定的顺序对markdown字符串进行处理, 并最终生成tokens数组
function normalize(state) {
  var str;
  // Normalize newlines
  str = state.src.replace(NEWLINES_RE, "\n");
  // Replace NULL characters
  str = str.replace(NULL_RE, "\ufffd");
  state.src = str;
};


function block(state) {
  var token;

  if (state.inlineMode) {
    token          = new state.Token('inline', '', 0);
    token.content  = state.src;
    token.map      = [ 0, 1 ];
    token.children = [];
    state.tokens.push(token);
  } else {
    state.md.block.parse(state.src, state.md, state.env, state.tokens);
  }
};

Renderer - token的解析

知道了tokens是怎么来的, 以及它的作用, 我们再来看看tokens是如何被解析成html的.

回到md.render方法, tokens生成之后, 作为参数, 传递给了md.renderer.render方法. 先来看看Renderer对象, Renderer对象缓存了一系列默认的rules.

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


// Renderer构造方法
function Renderer() {
  this.rules = assign({}, default_rules);
}

再来看看Renderer.render方法. 可以看到传入的token根据不同的type被传递到不同的rule进行了处理.

这里的rule不同于Ruler对象保存的rule. 它的作用只有一个 - 将token解析成html

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;
};

下面是一个内置的rule, 用来解析typecode_inline的token, 并最终返回一串html.

var default_rules = {};

default_rules.code_inline = function (tokens, idx, options, env, slf) {
  var token = tokens[idx];

  return  '<code' + slf.renderAttrs(token) + '>' +
          escapeHtml(tokens[idx].content) +
          '</code>';
};

总结

通过简单的分析, 我们可以将MarkdownIt的执行过程归纳为下图, 希望能让你对MarkdownIt有一个更好的理解.