十分钟定制自己的Markdown语法

作为一门易读易写的语音,markdown的应用越来越广,对markdown语法解析规则进行特殊扩展的场景诉求也越来越多。
本文为大家简单介绍以marked.js为基础的markdown语法扩展。  

marked.js基本用法

marked.js是一款性能不错的前端markdown解析库,它的用法非常简单marked.js readme

import  marked  from  'marked';

marked.setOptions({
    breaks: true, // 是否回车换行
    tables: 
    highlight(code, lang) { //  语法高亮
        let  val  =  code;
        if (lang) {
            val  =  hljs.highlight(lang, code).value;
        } else {
            val  =  hljs.highlightAuto(code).value;
        }
        return  val;
    },
    ....
});
const  html  =  marked('# 这是一个一级标题');
// output: <h1 id="这是一个一级标题">这是一个一级标题</h1>

renderer对象

marked.js的所有输出基本都依赖renderer对象,并且这个renderer我们是可以自定义规则的。
比如我们希望图片后面带上图片的说明

const renderer = new marked.Renderer();
renderer.image = function(href, title, text) {
    return '<img src="' + imageLink + '" alt="'  +  title  +  '"/><div class="desc">'+ title +'</div>';
};
const  html  =  marked('[测试图片](http://test.png)');
// output: <img src="http://test.png" alt="测试图片"/><div class="desc">测试图片</div>

自定义Lexer Token

通过自定义renderer对象的方式我们已经可以满足大部分自定义场景的诉求了。
什么?你说renderer只能自定义规则的输出,你还要加自己的解析规则?没关系,接着往下看。

block对象

打开marked.js源码,映入眼帘的就是一个包裹着一大堆正则的block对象

var  block  = {
    newline:  /^\n+/,
    code:  /^( {4}[^\n]+\n*)+/,
    fences: /^  {0,3}(`{3,}|~{3,})([^`~\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]*  *(?:\n+|$)|$)/,
    hr: /^  {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,
    heading:  /^  {0,3}(#{1,6}) +([^\n]*?)(?: +#+)?  *(?:\n+|$)/
    ...
}

可以猜测这应该是块级markdown语法的解析规则

Lexer.prototype.token

接着往下看你会发现这么一段代码,block里的正则用于校验lexer(层),并返回相应的token

Lexer.rules  =  block;

Lexer.prototype.lex  =  function(src) {
    src  =  src
    .replace(/\r\n|\r/g, '\n')
    .replace(/\t/g, ' ')
    .replace(/\u00a0/g, ' ')
    .replace(/\u2424/g, '\n');

    return  this.token(src, true);
};

Lexer.prototype.token  =  function(src, top) {
    src  =  src.replace(/^  +$/gm, '');
    while (src) {
    // newline

        if ((cap  =  this.rules.newline.exec(src))) {
            src  =  src.substring(cap[0].length);
            if (cap[0].length  >  1) {
            this.tokens.push({
                type: 'space',
            });
            }
        }

        // code
        if ((cap  =  this.rules.code.exec(src))) {
            var  lastToken  =  this.tokens[this.tokens.length  -  1];
            src  =  src.substring(cap[0].length);
            // An indented code block cannot interrupt a paragraph.
            if (lastToken  &&  lastToken.type  ===  'paragraph') {
                lastToken.text  +=  '\n'  +  cap[0].trimRight();
            } else {
                cap  =  cap[0].replace(/^  {4}/gm, '');
                this.tokens.push({
                    type: 'code',
                    codeBlockStyle: 'indented',
                    text: !this.options.pedantic  ?  rtrim(cap, '\n') :  cap,
                });
            }
        continue;
        }
        ...
    }
}

我们再来看一下marked入口函数以及return的Parser对象

  • 入口函数
function  marked(src, opt, callback) {
// throw error in case of non string input
if (typeof  src  ===  'undefined'  ||  src  ===  null) {...}

if (typeof  src  !==  'string') {...}

if (callback  ||  typeof  opt  ===  'function') {...}

try {
    if (opt) opt  =  merge({}, marked.defaults, opt);
    checkSanitizeDeprecation(opt);
    return  Parser.parse(Lexer.lex(src, opt), opt); 
} catch (e) {
    e.message  +=  '\nPlease report this to https://github.com/markedjs/marked.';
    if ((opt  ||  marked.defaults).silent) {
        return  '<p>An error occurred:</p><pre>'  +  escape(e.message  +  '', true) +  '</pre>';
    }
    throw  e;
    }
}
  • Parser
Parser.prototype.parse  =  function(src) {
    this.inline  =  new  InlineLexer(src.links, this.options);
    // use an InlineLexer with a TextRenderer to extract pure text
    this.inlineText  =  new  InlineLexer(
        src.links,
        merge({}, this.options, { renderer: new  TextRenderer() }),
    );
    this.tokens  =  src.reverse();

    var  out  =  '';
    while (this.next()) {
        out  +=  this.tok(); // 使用tok方法循环输出解析后的字符串
    }
    return  out;
};

Parser.prototype.next  =  function() {
    this.token  =  this.tokens.pop(); // lexer解析的tokens队列
    return  this.token;
};

Parser.prototype.tok  =  function() {
    switch (this.token.type) {
        case  'space': {
            return  '';
        }

        case  'hr': {
            return  this.renderer.hr();
        }
        ...
    }
}

markdown字符串首先按blocks中的正则解析成tokens队列,然后按照token输出不同的字符串。

添加自定义规则

看到这里我们也发现了,要想自定义规则就需要添加token也就需要重写Lexer.token方法。那只能copy过来改源码了。


举个栗子,我们希望把markdown语法里(((text)))字符串解析成<button>text</button>
首先我们在blocks里加上正则。注:Lexer.token为块级解析,行内字符串规则需在inlineLexer中添加

var blocks = {
    ...
    button: /^\(\(\((.+)?\)\)\)/
}

然后在Lexer.prototype.token方法中加上我们的token

Lexer.prototype.token = function (src, top){
    ...
    if ((cap  =  this.rules.button.exec(src))) {
        src  =  src.substring(cap[0].length);
        if (cap[0].length  >  1) {
        this.tokens.push({
            type: 'button',
            text: cap[1]
        });
        }
    }
}

最后在Parser.prototype.tok方法中加上我们要输出的字符串

Parser.prototype.tok = function () {
    ...
    case  'button': {
        return  '<button>' + this.token.text + '</button>';
  }
}

写在最后

最后咱们来体验一下投票功能吧