markdown-it源代码分析

经常用markdown写博客,会发现markdown的功能并不够用,比如原生markdown不支持表格合并功能:

列合并 列合并 行合并
- - -
- 行列都合并
-

原生markdown不支持文字颜色。原生markown不支持数学公式:

\lim_{x \rightarrow 0} \frac{x-\sin x}{x^3} = \frac{1}{6}

原生markown还不支持显示pdf和video的语法,等等。

想要解决这些问题,必须从markdown解析着手,https://github.com/markdown-it/markdown-it是一款将markdown解析成html的库,支持自定义语法插件。有了它之后,再写加几个按钮,写点css,一个markdown编辑器就诞生了。

本文主要讲解markdown-it的工作流程,以及自定义语法插件。

MarkdownIt

function MarkdownIt(presetName, options) {
  // 这3个用于将marken解析成token
  this.inline = new ParserInline();
  this.block = new ParserBlock();
  this.core = new ParserCore();

  // 用于将token转化为html输出
  this.renderer = new Renderer();

  ...
}

重要的是use方法,因为我们就靠它添加插件:

MarkdownIt.prototype.use = function (plugin /*, params, ... */) {
  var args = [ this ].concat(Array.prototype.slice.call(arguments, 1));
  plugin.apply(plugin, args);
  return this;
};

use方法会直接apply(plugin, args),就是调用了插件中的函数。

当我们调用render函数时,先会调用parse函数将markdown解析成tokens,然后再将tokens转换成html

MarkdownIt.prototype.render = function (src, env) {
  env = env || {};

  return this.renderer.render(this.parse(src, env), this.options, env);
};

显然,核心在于如何将markdown解析成token。

token

token是什么?其实就是js对象:

{
    "type": "heading_open",
    "tag": "h1",
    "attrs": null,
    "map": [
      2,
      3
    ],
    "nesting": 1,
    "level": 0,
    "children": null,
    "content": "",
    "markup": "#",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
}

官方的例子中,右侧点击debug可以看到tokens输出。

核心规则

在parse函数中,首先会调用this.core.process(state),在这个函数中会执行如下核心规则

var _rules = [
  [ 'normalize',      require('./rules_core/normalize')      ],
  [ 'block',          require('./rules_core/block')          ],
  [ 'inline',         require('./rules_core/inline')         ],
  [ 'linkify',        require('./rules_core/linkify')        ],
  [ 'replacements',   require('./rules_core/replacements')   ],
  [ 'smartquotes',    require('./rules_core/smartquotes')    ]
];
  1. normalize用于规范输入
  2. block用于解析多行markdown标记
  3. inline用于解析行内markdown标记
  4. linkify用于解析未使用markdown语法,文本形式的链接
  5. replacements用于一些自动替换
  6. smartquotes将直接引号转换为印刷引号

显然,重点在block和inline这两个核心规则。

核心block规则最终会执行MarkdownIt.block.tokenize()函数,在这个函数中,会从MarkdownIt.block.ruler取出所有block规则,然后对每一行都执行所有block规则:

ParserBlock.prototype.tokenize = function (state, startLine, endLine) {
    ...
    while (line < endLine) {
        for (i = 0; i < len; i++) {
          ok = rules[i](state, line, endLine, false);
          if (ok) { break; }
        }
    }
    ...
};

每个合格的block规则都会做这3件事

  1. update state.line
  2. update state.tokens
  3. return true

核心block规则结束后,会生产一系列token,对于需要inline规则继续处理的内容会包裹在类型为inline的token中。

核心inline规则最终会对所有类型为inline的token执行MarkdownIt.inline.tokenize()函数,在这个函数中,会从MarkdownIt.inline.ruler取出所有inline规则,对该token的每个字符都执行所有inline规则:

ParserInline.prototype.tokenize = function (state) {
  ...
  while (state.pos < end) {
      for (i = 0; i < len; i++) {
        ok = rules[i](state, false);
        if (ok) { break; }
      }
  }
  ...
};

每个合格的inline规则都会做这3件事

  1. update state.pos
  2. update state.tokens
  3. return true

block规则、inline规则

显然MarkdownIt.block.rulerMarkdownIt.inline.ruler中含有哪些规则是markdown解析的重点。

初始化时,MarkdownIt会将如下block规则放入MarkdownIt.block.ruler

var _rules = [
  // First 2 params - rule name & source. Secondary array - list of rules,
  // which can be terminated by this one.
  [ 'table',      require('./rules_block/table'),      [ 'paragraph', 'reference' ] ],
  [ 'code',       require('./rules_block/code') ],
  [ 'fence',      require('./rules_block/fence'),      [ 'paragraph', 'reference', 'blockquote', 'list' ] ],
  [ 'blockquote', require('./rules_block/blockquote'), [ 'paragraph', 'reference', 'blockquote', 'list' ] ],
  [ 'hr',         require('./rules_block/hr'),         [ 'paragraph', 'reference', 'blockquote', 'list' ] ],
  [ 'list',       require('./rules_block/list'),       [ 'paragraph', 'reference', 'blockquote' ] ],
  [ 'reference',  require('./rules_block/reference') ],
  [ 'heading',    require('./rules_block/heading'),    [ 'paragraph', 'reference', 'blockquote' ] ],
  [ 'lheading',   require('./rules_block/lheading') ],
  [ 'html_block', require('./rules_block/html_block'), [ 'paragraph', 'reference', 'blockquote' ] ],
  [ 'paragraph',  require('./rules_block/paragraph') ]
];

同样的,初始化时会将如下inline规则放入MarkdownIt.inline.ruler


var _rules = [
  [ 'text',            require('./rules_inline/text') ],
  [ 'newline',         require('./rules_inline/newline') ],
  [ 'escape',          require('./rules_inline/escape') ],
  [ 'backticks',       require('./rules_inline/backticks') ],
  [ 'strikethrough',   require('./rules_inline/strikethrough').tokenize ],
  [ 'emphasis',        require('./rules_inline/emphasis').tokenize ],
  [ 'link',            require('./rules_inline/link') ],
  [ 'image',           require('./rules_inline/image') ],
  [ 'autolink',        require('./rules_inline/autolink') ],
  [ 'html_inline',     require('./rules_inline/html_inline') ],
  [ 'entity',          require('./rules_inline/entity') ]
];

var _rules2 = [
  [ 'balance_pairs',   require('./rules_inline/balance_pairs') ],
  [ 'strikethrough',   require('./rules_inline/strikethrough').postProcess ],
  [ 'emphasis',        require('./rules_inline/emphasis').postProcess ],
  [ 'text_collapse',   require('./rules_inline/text_collapse') ]
];

在这些规则中,会解析markdown文本,将对应的内容转换为token,至此markdown解析部分结束。

render

渲染的时候更简单,只需根据token的type去MarkdownIt.render.rules中找到对应的渲染函数,执行即可拿到html:

Renderer.prototype.render = function (tokens, options, env) {
  ...
  for (i = 0, len = tokens.length; i < len; i++) {
      result += rules[tokens[i].type](tokens, i, options, env, this);
  }
  ...
  return result;
};

在插件中自定义规则

前面提到过,当我们use一个插件时,MarkdownIt会自动调用插件中的函数,例如:

var md = require('markdown-it')()
            .use(require('markdown-it-multimd-table'));

所以我们只需在插件中新增或修改MarkdownIt.block.rulerMarkdownIt.inline.rulerMarkdownIt.render.rules中的规则,即可达到自定义的目的。

插件中能获得什么参数呢?

block规则传入的参数是StateBlock类:

function StateBlock(src, md, env, tokens) {
  this.src = src;
  this.tokens = tokens;
  this.bMarks = [];  // 行的起始位置
  this.eMarks = [];  // 行的结束位置
  this.line       = 0; // 行号
  ...
}

inline规则传入的参数是StateInline类:

function StateInline(src, md, env, outTokens) {
  this.src = src;
  this.tokens = outTokens;
  this.pos = 0;
  ...
}

两者主要的区别是block规则中this.line指当处理的行在src中的索引;inline规则中this.pos指当前处理字符串在src中的索引。

插件实例

需求:平时会翻译一些英文文章,自己写的翻译和官方翻译放在一起容易弄混,所以想给自己写的翻译加个颜色,以便与官方翻译区分开来。我的想法是以“&”开头的段落就自动变成蓝色。

首先设计token,翻译段落不需要继续解析inline规则,所以单个token足以:

{
    "type": "colorblock",
    "tag": "",
    "attrs": null,
    "map": null,
    "nesting": 0,
    "level": 0,
    "children": null,
    "content": "",
    "markup": "&",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
}

如果某行第一个字符是"&",第二个字符是空格,则加入token,并将line+1.

function tokenize(state, startLine, endLine, silent) {
        if (silent) {
            return true;
        }
        var pos = state.bMarks[startLine],
            max = state.eMarks[startLine];

        if (state.src.charCodeAt(pos) !== 0x26/* & */) {
            return false;
        }

        if (state.src.charCodeAt(pos + 1) !== 0x20/*  空格 */) {
            return false;
        }
        let token = state.push('colorblock', '', 0);
        token.markup = "&";
        token.content = state.src.slice(pos + 2, max);

        state.line = startLine + 1;
        return true
    }

返回html时将内容加个style:

function colorblockRenderer(tokens, idx) {
        return '<p><span style="color:blue">' + tokens[idx].content + '</span></p>';
    }

最后,将block规则加入到md.block.ruler中,将渲染规则加入到md.renderer.rules中:

module.exports = function colorblock(md, options) {
    md.block.ruler.after('blockquote', 'colorblock', tokenize);
    md.renderer.rules.colorblock = colorblockRenderer;
}
posted @ 2021-02-08 17:31:31
评论加载中...

发表评论