本文引至: 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, 說好約定的時間呢? 連時間都不遵照的面試官...請自重