做者:嵇智javascript
编译的核心管理者,掌握着不一样类型的 token 生成的流程。它内部管理了 ParserBlock、ParserInline、linkify、replacements 等 rule 函数。也就是说,用户传入一个字符串,经历了这些 rule 函数处理以后,获得了一个由许多 token 组成的 tokens 数组,最后再交由 renderer 处理以后,吐出 HTML 字符串。html
先看下 MarkdownIt 的执行逻辑。java
var md = require('markdown-it')({ html: true, linkify: true, typographer: true }) md.render('# markdown-it rulezz!') 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; }; 复制代码
咱们重点关注一下 ParserCore 这个类。它位于 lib/parser_core.js
。node
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') ] ]; function Core() { this.ruler = new Ruler(); for (var i = 0; i < _rules.length; i++) { this.ruler.push(_rules[i][0], _rules[i][1]); } } Core.prototype.process = function (state) { var i, l, rules; rules = this.ruler.getRules(''); for (i = 0, l = rules.length; i < l; i++) { rules[i](state); } }; Core.prototype.State = require('./rules_core/state_core'); 复制代码
parserCore 实例上仅有一个 ruler 属性,这个是用来管理内部全部的 rule 函数,而且原型上。只有一个 process 方法。linux
当调用 process 的时候,首先会拿到职责链名为空字符串(''
)的 rule 组成的数组,将 state 做为入参传入至每个 rule 函数,获得 tokens 以后挂载到 state 上去。相似的伪代码以下:git
const rules = [function normalize, function block, function inline, function ...] for (const rule of rules) { rule(state) // rule 内部生成一个个 token,而且存放在 state.tokens 数组里。 } // 最后在 md.parse 函数体内部返回 state.tokens 复制代码
所以咱们的关注点就在于这些属于 parserCore 的 rule 究竟是作了什么工做?state 又是什么呢?先来看下属于 parserCore 的 state。它位于 lib/rules_core/state_core.js
github
function StateCore(src, md, env) { this.src = src; this.env = env; this.tokens = []; this.inlineMode = false; this.md = md; // link to parser instance } StateCore.prototype.Token = Token; 复制代码
src
用来放用户输入的字符串,tokens 存放编译出来的 token。inlineMode
表示 parse 的时候是否编译成 type 为 inline 的 token。md
就是当前 MarkdownIt 的实例。编程
而属于 ParserCore 的 rules 的职能是什么?咱们先粗略了解一下。它们都在 lib/rules_core
文件夹。windows
module.exports = function inline(state) { var str; // Normalize newlines str = state.src.replace(NEWLINES_RE, '\n'); // Replace NULL characters str = str.replace(NULL_RE, '\uFFFD'); state.src = str; }; 复制代码
做用很简单,就是兼容一下 linux 和 windows 换行符的问题。数组
module.exports = 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); } }; 复制代码
内部逻辑很清晰,先判断是否开启 inline 模式的 parse。不然经过 md 调用 ParserBlock 的 parse 方法。这一步是将换行分隔符(\n
) 做为 src 的划分界限,生成不少 block 为 true 的 token。咱们在接下来的一篇关于 ParserBlock 分析的文章里面详细阐述。
module.exports = function inline(state) { var tokens = state.tokens, tok, i, l; // Parse inlines for (i = 0, l = tokens.length; i < l; i++) { tok = tokens[i]; if (tok.type === 'inline') { state.md.inline.parse(tok.content, state.md, state.env, tok.children); } } }; 复制代码
这一步是在 ParserBlock 以后的,由于 ParserBlock 处理以后会生成 type 为 inline 的token。这种 token 属于未彻底解析的 token,须要 ParserInline 进一步处理,生成新的token。这些新生成的 token 会存放在 children 属性上。举个栗子来讲:
const src = '__ad__' md.render(src) // 1.通过 ParserBlock 处理以后是这样的 token: { type: "inline", tag: "", attrs: null, block: true, children: [] content: "__ad__", hidden: false, ... type:"inline" } // 从 content 能够看出 '__' 并未生成 token,这个符号表明强调的意思,应该替换成 strong 标签 // 2.再通过 ParserInline 处理以后,会发现 children 多了 5 个 token。代码以下 { ..., children: [ { type: "text", tag: "", attrs: null, ... }, { type: "strong_open", tag: "strong", attrs: null, … }, { type: "text", tag: "", attrs: null, … }, { type: "strong_close", tag: "strong", attrs: null, … }, { type: "text", tag: "", attrs: null, … } ] } // 最后传给 md.renderer.render 以后,就能生成加粗的文字了。 复制代码
ParserInline 的揭秘,会在另一片文章详细分析。
module.exports = function linkify(state) { var i, j, l, tokens, token, currentToken, nodes, ln, text, pos, lastPos, level, htmlLinkLevel, url, fullUrl, urlText, blockTokens = state.tokens, links; if (!state.md.options.linkify) { return; } for (j = 0, l = blockTokens.length; j < l; j++) { if (blockTokens[j].type !== 'inline' || !state.md.linkify.pretest(blockTokens[j].content)) { continue; } tokens = blockTokens[j].children; htmlLinkLevel = 0; for (i = tokens.length - 1; i >= 0; i--) { currentToken = tokens[i]; if (currentToken.type === 'link_close') { i--; while (tokens[i].level !== currentToken.level && tokens[i].type !== 'link_open') { i--; } continue; } if (currentToken.type === 'html_inline') { if (isLinkOpen(currentToken.content) && htmlLinkLevel > 0) { htmlLinkLevel--; } if (isLinkClose(currentToken.content)) { htmlLinkLevel++; } } if (htmlLinkLevel > 0) { continue; } if (currentToken.type === 'text' && state.md.linkify.test(currentToken.content)) { text = currentToken.content; links = state.md.linkify.match(text); // Now split string to nodes nodes = []; level = currentToken.level; lastPos = 0; for (ln = 0; ln < links.length; ln++) { url = links[ln].url; fullUrl = state.md.normalizeLink(url); if (!state.md.validateLink(fullUrl)) { continue; } urlText = links[ln].text; if (!links[ln].schema) { urlText = state.md.normalizeLinkText('http://' + urlText).replace(/^http:\/\//, ''); } else if (links[ln].schema === 'mailto:' && !/^mailto:/i.test(urlText)) { urlText = state.md.normalizeLinkText('mailto:' + urlText).replace(/^mailto:/, ''); } else { urlText = state.md.normalizeLinkText(urlText); } pos = links[ln].index; if (pos > lastPos) { token = new state.Token('text', '', 0); token.content = text.slice(lastPos, pos); token.level = level; nodes.push(token); } token = new state.Token('link_open', 'a', 1); token.attrs = [ [ 'href', fullUrl ] ]; token.level = level++; token.markup = 'linkify'; token.info = 'auto'; nodes.push(token); token = new state.Token('text', '', 0); token.content = urlText; token.level = level; nodes.push(token); token = new state.Token('link_close', 'a', -1); token.level = --level; token.markup = 'linkify'; token.info = 'auto'; nodes.push(token); lastPos = links[ln].lastIndex; } if (lastPos < text.length) { token = new state.Token('text', '', 0); token.content = text.slice(lastPos); token.level = level; nodes.push(token); } blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes); } } } }; 复制代码
这个 rule 的做用就是将 URL-like 的字符串转化成超连接。rule 是否执行,是取决于你实例化 md 传入的 options.linkify
。内部检测 URL-like 的字符串用的库是 linkify-it
。里面对不少种 url 格式作了检验,有兴趣的能够详细研究一下。
module.exports = function replace(state) { var blkIdx; if (!state.md.options.typographer) { return; } for (blkIdx = state.tokens.length - 1; blkIdx >= 0; blkIdx--) { if (state.tokens[blkIdx].type !== 'inline') { continue; } if (SCOPED_ABBR_TEST_RE.test(state.tokens[blkIdx].content)) { replace_scoped(state.tokens[blkIdx].children); } if (RARE_RE.test(state.tokens[blkIdx].content)) { replace_rare(state.tokens[blkIdx].children); } } }; 复制代码
初始化 md 的时候传入的 options.typographer
为 true 的时候,开启该 rule。这个 rule 的做用,就是替换一些印刷字体,好比相似于下面的:
// (c) (C) → © // (tm) (TM) → ™ // (r) (R) → ® // +- → ± // (p) (P) -> § 复制代码
初始化 md 的时候传入的 options.typographer
为 true 的时候,开启该 rule。rule 的做用就是为了处理一些不一样国家语言的引号问题。官网给出的解释以下
// Double + single quotes replacement pairs, when typographer enabled, // and smartquotes on. Could be either a String or an Array. // // For example, you can use '«»„“' for Russian, '„“‚‘' for German, // and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). 复制代码
如此一来,咱们从宏观的角度全面分析了 MarkdownIt 的 parse、tokenize、render 的全流程。代码的总体设计思路很是的清晰,内部的源码注释也是很是的丰富到位,用一张图来简单阐述下流程。
可是若是有细心的同窗,会发现以下的一段代码,颇有意思。
Core.prototype.process = function (state) { var i, l, rules; rules = this.ruler.getRules(''); for (i = 0, l = rules.length; i < l; i++) { rules[i](state); } }; 复制代码
在调用 process 的函数体内部,每次调用一个 rule,会将 state 传入。state 的 tokens 属性存储了全部的 token。所以咱们发现,全部 rule 函数内部必须维持对 state.tokens 和 state 的引用不变,所以不能作相似于如下的赋值操做。
function rule (state) { state = xxx // wrong state.tokens = [token1, token2] // wrong state.tokens.push(token1) // true } // 第一个语句错误的缘由,是由于你改了 state 的指向,切断了与老 state 的联系。 // 第二个语句错误的缘由,是改了 tokens 的指向。这样接下的 rule 函数拿到的 state.tokens 就丢失了以前 rule 生成的 tokens。 复制代码
这种函数在函数式编程里面叫作拥有反作用的函数,由于输入的 state 在函数内部发生了变化,致使外层 state 也被改变。这也是 javascript 里面基础类型与引用类型的区别。可是 MarkdownIt 的总体架构设计就是基于这种引用类型的机制,不然必须在 rule 里面返回每次新生成的 tokens,而且统一管理。
分析完了 ParserCore,让咱们从总体上对 MarkdownIt 的原理有了必定的了解。下两篇文章 ParserBlock&ParserInline,咱们分别详细分析 ParserBlock 和 ParserInline,这两部分篇幅会比较长,由于这属于核心的 parse 逻辑。