本文引至: please call me hrcss
由于本人平时写做方式就是使用的markdown, 感受有些引擎解析快,有些慢. 但又迫不得已. 就像:html
我就喜欢你看不惯我,又干不掉个人样子node
因此, 这里,相对markdown语法引擎作一个简单分析。或者说,本身动手来写一个micro-markdown-parser.
markdown 引擎其实并不复杂,只要你获得了对应的regexp,而后替换一下HTML tag便可. 目前市面上流行的几种markdown 解析器 无外乎就是: marker,markdown-js.
一开始,markdown是由John Gruber用Perl写出来的语法解析器. 因为md在后面过于火爆,出现了不一样的支持引擎. 不过,后面在github上,提出了GFM (Github Flavored Markdown) 这一个标准以后. 大部分引擎的解析规范也获得了统一.
最最基本的一个md引擎,应该须要可以解析: Inline HTML, Automatic paragraphs, headers, blockquotes, lists, code blocks, horizontal rules, links, emphasis, inline code and images 这几种. 详情,能够参考: md features
接下来,咱们正式的 make a md parser.git
关于md parser 最最基本的就是正则和exec方法. 先简单说一下exec方法吧.github
exec是用来在特定str中,匹配指定正则的方法. 实际上可使用String.prototype.match代替.基本使用为:面试
regexObj.exec(str)
返回值为 array(匹配到) 和 null (没有匹配到)
若是返回array则:正则表达式
[1]...[n]: 正则分组匹配到的内容.编程
index: 正则开始匹配到string的位置markdown
input: 原始的stringapp
具体的demo:
var re = /quick\s(brown).+?(jumps)/ig; var result = re.exec('The Quick Brown Fox Jumps Over The Lazy Dog'); // 结果为 [ 'Quick Brown Fox Jumps', 'Brown', 'Jumps', index: 4, input: 'The Quick Brown Fox Jumps Over The Lazy Dog' ]
而后是基本的正则匹配:
正则表达式很容易去源码里翻一翻就找到了.
regexobject: { headline: /^(\#{1,6})([^\#\n]+)$/m, code: /\s\`\`\`\n?([^`]+)\`\`\`/g, hr: /^(?:([\*\-_] ?)+)\1\1$/gm, lists: /^((\s*((\*|\-)|\d(\.|\))) [^\n]+)\n)+/gm, bolditalic: /(?:([\*_~]{1,3}))([^\*_~\n]+[^\*_~\s])\1/g, links: /!?\[([^\]<>]+)\]\(([^ \)<>]+)( "[^\(\)\"]+")?\)/g, reflinks: /\[([^\]]+)\]\[([^\]]+)\]/g, smlinks: /\@([a-z0-9]{3,})\@(t|gh|fb|gp|adn)/gi, mail: /<(([a-z0-9_\-\.])+\@([a-z0-9_\-\.])+\.([a-z]{2,7}))>/gmi, tables: /\n(([^|\n]+ *\| *)+([^|\n]+\n))((:?\-+:?\|)+(:?\-+:?)*\n)((([^|\n]+ *\| *)+([^|\n]+)\n)+)/g, include: /[\[<]include (\S+) from (https?:\/\/[a-z0-9\.\-]+\.[a-z]{2,9}[a-z0-9\.\-\?\&\/]+)[\]>]/gi, url: /<([a-zA-Z0-9@:%_\+.~#?&\/=]{2,256}\.[a-z]{2,4}\b(\/[\-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)?)>/g }
本文参考的是一个教学用的markdown语法parser.github源码 有兴趣,能够查看一下. 读起来很是简单.没有过多的逻辑处理. 因此,这里也是基于这个来进行讲解的.
最简单的匹配应该算headline. 他的正则表达式为: /^(\#{1,6})([^\#\n]+)$/m
. 后面的m
很是重要. 由于,全部的标题应该是写在首行的,如:
# abc ## sub_abc
使用m
flag 来做为首行匹配标识符.完美~
而后,只须要进行一个循环便可.
var headling = /^(\#{1,6})([^\#\n]+)$/m while ((stra = headline.exec(str)) !== null) { count = stra[1].length; str = str.replace(stra[0], '<h' + count + '>' + stra[2].trim() + '</h' + count + '>').trim(); }
固然,这里并不涉及到彻底性的处理. 最简单的方式就是过滤字符串,不过过滤字符串也有不少方法. 最直接的就是replace直接替换.
function escape(html, encode) { return html .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }
这就算一个简单的替换. 另外,还有一种是使用textNode内置的替换方案
// 使用textNode内置的替换引擎,将 < > $等字符替换. 但不会替换' 和 " var escape = function(str) { 'use strict'; var div = document.createElement('div'); div.appendChild(document.createTextNode(str)); str = div.innerHTML; div = undefined; return str; }
则上面的内容则能够写为:
var headling = /^(\#{1,6})([^\#\n]+)$/m while ((stra = headline.exec(str)) !== null) { count = stra[1].length; str = str.replace(stra[0], '<h' + count + '>' + escape(stra[2].trim()) + '</h' + count + '>').trim(); }
实际上基于这点,咱们就能够进行简单的发散. 好比marked.js 根据正则提出了自定义化的匹配模式.
一些正则细节和匹配细节,咱们这里就不过多探讨了, 由于处理的内容主要是\r\n ' "
. 咱们这里,简单的来看一下marked.js 里面的一些精华部分. 特别是他提出来的可自定义化的正则样式. 即,Renderer方法.
// 官方提出的demo var marked = require('marked'); var renderer = new marked.Renderer(); renderer.heading = function (text, level) { var escapedText = text.toLowerCase().replace(/[^\w]+/g, '-'); return '<h' + level + '><a name="' + escapedText + '" class="anchor" href="#' + escapedText + '"><span class="header-link"></span></a>' + text + '</h' + level + '>'; }, console.log(marked('# heading+', { renderer: renderer }));
咱们能够看一下他源码里面的思路:
首先,他有一个Renderer的构造函数:
function Renderer(options) { this.options = options || {}; }
接着就是绑定在prototype上面的方法:
Renderer.prototype.blockquote = function(quote) { return '<blockquote>\n' + quote + '</blockquote>\n'; };
可能有的童鞋会想,这里他并无作什么语法解析呢?
亲, 请注意他的参数quote
. 而后再看他的渲染内容,就一目了然. quote 是已经转义事后匹配的内容.
咱们接着,来看一下调用方法:
// url (gfm) if (!this.inLink && (cap = this.rules.url.exec(src))) { src = src.substring(cap[0].length); text = escape(cap[1]); href = text; // out 这里是指所有输出的结果. out += this.renderer.link(href, null, text); continue; }
有童鞋可能又会疑问了, 你正则不是所有匹配的吗? 这样作不会遗漏信息吗?
因此说, marked.js为了实现自定义话的模式,牺牲了性能.咱们看一下他的正则表达式便可:
var block = { newline: /^\n+/, code: /^( {4}[^\n]+\n*)+/, fences: noop, hr: /^( *[-*_]){3,} *(?:\n+|$)/, heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/, list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, html: /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/, def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/, text: /^[^\n]+/ };
能够看出,他没有添加任何的pattern... 这就是marked.js精妙的地方. 因此, 上面的out 看起来,也并无什么神奇的地方了:
out += this.renderer.link(href, null, text);
所以, 经过将renderer对象中方法的override. 形成自定义的效果. 这也是灰常好的. 另外,还有一点须要讲解一下,就是marked.js构造的注释替换的方法.
function replace(regex, opt) { regex = regex.source; opt = opt || ''; return function self(name, val) { if (!name) return new RegExp(regex, opt); val = val.source || val; val = val.replace(/(^|[^\[])\^/g, '$1'); regex = regex.replace(name, val); return self; }; } // 看一下他的调用方法 // 相面的block.xxx 都是正则表达式,我这里就不赘述了 block.paragraph = replace(block.paragraph) ('hr', block.hr) ('heading', block.heading) ('lheading', block.lheading) ('blockquote', block.blockquote) ('tag', '<' + block._tag) ('def', block.def) (); // 实际上,这个方法运行的结果是生成一个新的正则表达式. 即,把上面用单词的地方替换为指定的正则 // 例如 paragraph 里面的hr, heading paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/
固然,也有其余的实现方式。 只是marked.js在这里作的比较完美.
前面提到了使用out+=的方式进行解析. 固然,可能会想到下列问题:
段落嵌套语法怎么解析的呢?
这实际上,他在嵌套的语法层里,并无作out+= 能够看下列源码:
// code if (cap = this.rules.code.exec(src)) { src = src.substring(cap[0].length); cap = cap[0].replace(/^ {4}/gm, ''); this.tokens.push({ type: 'code', text: !this.options.pedantic ? cap.replace(/\n+$/, '') : cap }); continue; }
他在这里传了一个tokens, 而后 传到外层这里再次进行的解析.
Parser.prototype.tok = function() { switch (this.token.type) { case 'space': { return ''; } case 'hr': { return this.renderer.hr(); } ... }
因此, marked.js为了完成自定义化的解析真的是挖了一个很大的坑. 但相对于全局匹配在替换的模式来讲, 这样灵活性大一点。
flexibility + speed = const
ok, 如今咱们已经简单的了解了大局方面的marked.js解析原理. 接下来,咱们来看一下比较难的code解析。
若是只是表层的code解析,很是简单. 使用下面的正则表达式便可
code: /\s?\`\`\`\n?([^`]+)\`\`\`/g
可是,这样仅仅只是替换出下列格式.
<pre> <code> .... </code> <pre>
并无像下面这样,带上颜色的匹配.
var a =1; var b =2;
简单的替换原理也很好解释.就是给指定的span添加上不一样的class便可.
// 替换:
's'
// 生成span <span class="str">'abc'</span>
它里面的解析机制,主要就是根据不一样的语法正则来添加不一样的className.
具体,咱们能够参照 highlight.js里面的源码:
function highlightBlock(block) { var node, originalStream, result, resultNode, text; var language = blockLanguage(block); text = node.textContent; ... result = language ? highlight(language, text, true) : highlightAuto(text); ... }
经过blockLanguage 找出指定的code的编程语言. 查找细节有一个方法比较重要:
function registerLanguage(name, language) { var lang = languages[name] = language(hljs); if (lang.aliases) { lang.aliases.forEach(function(alias) {aliases[alias] = name;}); } }
该方法用来手动将language的配置文件挂载到里面。 咱们看一看js的配置文件
/* Language: JavaScript Category: common, scripting */ function(hljs) { return { aliases: ['js', 'jsx'], keywords: { keyword: 'in of if for while finally var new function do return void else break catch ' + 'instanceof with throw case default try this switch continue typeof delete ' + 'let yield const export super debugger as async await static ' + // ECMAScript 6 modules import 'import from as' , literal: 'true false null undefined NaN Infinity', built_in: 'eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent ' + ... }
而后经过指定的正则来进行匹配和替换. 因此, 通常的md parser引擎解析并不会自带code解析, 由于是在太复杂了... 编程语言这么多.. 这么搞的玩. 因此, highlight 本身自定义了一套 common 机制. 一方, 没有传入指定language的状况.
hljs.COMMENT = function (begin, end, inherits) { var mode = hljs.inherit( { className: 'comment', begin: begin, end: end, contains: [] }, inherits || {} ); mode.contains.push(hljs.PHRASAL_WORDS_MODE); mode.contains.push({ className: 'doctag', begin: "(?:TODO|FIXME|NOTE|BUG|XXX):", relevance: 0 }); return mode; }; hljs.C_LINE_COMMENT_MODE = hljs.COMMENT('//', '$'); hljs.C_BLOCK_COMMENT_MODE = hljs.COMMENT('/\\*', '\\*/'); hljs.HASH_COMMENT_MODE = hljs.COMMENT('#', '$'); hljs.NUMBER_MODE = { className: 'number', begin: hljs.NUMBER_RE, relevance: 0 }; hljs.C_NUMBER_MODE = { className: 'number', begin: hljs.C_NUMBER_RE, relevance: 0 }; hljs.BINARY_NUMBER_MODE = { className: 'number', begin: hljs.BINARY_NUMBER_RE, relevance: 0 }; hljs.CSS_NUMBER_MODE = { className: 'number', begin: hljs.NUMBER_RE + '(' + '%|em|ex|ch|rem' + '|vw|vh|vmin|vmax' + '|cm|mm|in|pt|pc|px' + '|deg|grad|rad|turn' + '|s|ms' + '|Hz|kHz' + '|dpi|dpcm|dppx' + ')?', relevance: 0 };
不说了,最近被面试官调戏,心情比较差... 在博客最后放一个鸡汤.
mdzz, 说好约定的时间呢? 连时间都不遵照的面试官...请自重