做者:嵇智html
markdown-it
是一个 parser。它接收一些字符串,而且通过内部的 rule 函数处理以后,调用 render 以后输出 HTML 字符串。既然是接受字符串,那么以下所见node
// node.js var md = require('markdown-it')(); var result = md.render('# markdown-it rulezz!'); // output "<h1>markdown-it rulezz!</h1>" // browser var md = require('markdown-it')(); var result = md.render('# markdown-it rulezz!'); // output "<h1>markdown-it rulezz!</h1>" 复制代码
输入必定格式的字符串给 md,输出 HTML 字符串,只要将其 append 到 DOM Tree 中,便可完成渲染。简单易懂。git
那么内部的 parse 机制究竟是怎样的呢?先从入口文件 markdown-it/lib/index.js
入手。github
function MarkdownIt(presetName, options) { if (!(this instanceof MarkdownIt)) { return new MarkdownIt(presetName, options); } if (!options) { if (!utils.isString(presetName)) { options = presetName || {}; presetName = 'default'; } } this.inline = new ParserInline(); this.block = new ParserBlock(); this.core = new ParserCore(); this.renderer = new Renderer(); this.linkify = new LinkifyIt(); this.validateLink = validateLink; this.normalizeLink = normalizeLink; this.normalizeLinkText = normalizeLinkText; this.utils = utils; this.helpers = utils.assign({}, helpers); this.options = {}; this.configure(presetName); if (options) { this.set(options); } } 复制代码
MarkdownIt 支持传入两个参数 presetName 和 options.markdown
首先,构造函数内部先对调用方是否用了 new 操做符来调用 MarkdownIt 作了兼容。而且对 presetName 与 options 作了缺省值的一些操做。先来看下 presetName。架构
// 构造函数调用 configure this.configure(presetName) var config = { 'default': require('./presets/default'), zero: require('./presets/zero'), commonmark: require('./presets/commonmark') }; MarkdownIt.prototype.configure = function (presets) { var self = this, presetName; if (utils.isString(presets)) { presetName = presets; presets = config[presetName]; if (!presets) { throw new Error('Wrong `markdown-it` preset "' + presetName + '", check name'); } } if (!presets) { throw new Error('Wrong `markdown-it` preset, can\'t be empty'); } if (presets.options) { self.set(presets.options); } if (presets.components) { Object.keys(presets.components).forEach(function (name) { if (presets.components[name].rules) { self[name].ruler.enableOnly(presets.components[name].rules); } if (presets.components[name].rules2) { self[name].ruler2.enableOnly(presets.components[name].rules2); } }); } return this; } 复制代码
configure 方法接收 presets 做为参数,内部会对其作一些适配的工做。当 presets 为 commonmark | default | zero
的时候,就赋值为不一样对象。这三种配置对象位于 lib/presets
下面,就拿 default
来讲吧。app
module.exports = { options: { html: false, // Enable HTML tags in source xhtmlOut: false, // Use '/' to close single tags (<br />) breaks: false, // Convert '\n' in paragraphs into <br> langPrefix: 'language-', // CSS language prefix for fenced blocks linkify: false, // autoconvert URL-like texts to links // Enable some language-neutral replacements + quotes beautification typographer: false, // 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). quotes: '\u201c\u201d\u2018\u2019', /* “”‘’ */ // Highlighter function. Should return escaped HTML, // or '' if the source string is not changed and should be escaped externaly. // If result starts with <pre... internal wrapper is skipped. // // function (/*str, lang*/) { return ''; } // highlight: null, maxNesting: 100 // Internal protection, recursion limit }, components: { core: {}, block: {}, inline: {} } }; 复制代码
导出了拥有 options 与 components 属性的对象。接下来就走到 configure 内部判断 options 与 components 的逻辑来。components 下面的顶级属性 core & block & inline 分别对应了如下三个实例,其目的是为了禁用它们内部的一些 rule 函数,让 Markdown 的 parse 更快,更纯粹。这也体现出 MarkdownIt 的灵活性以及高度定制的特性。函数
this.inline = new ParserInline(); this.block = new ParserBlock(); this.core = new ParserCore(); this.renderer = new Renderer(); 复制代码
这四个类,是整个 MarkdownIt 的灵魂。它们贯穿 MarkdownIt 的 parse、tokenize、render 的全流程,这四个类,我会在接下来的系列来一一分析。oop
接着初始化跟 url 相关的三个功能函数post
this.validateLink = validateLink; this.normalizeLink = normalizeLink; this.normalizeLinkText = normalizeLinkText; 复制代码
主要是用来校验 url 的合法以及 decode 与 encode,感兴趣的同窗,能够研究内部用到的 mdurl 以及 punycode 的逻辑。
最后部分就是一些 util 与 helpers 函数的挂载以及对 options 的配置。
从上面来看,整个初始化工做的逻辑很是清晰、明了,并且源码内部有大量的用法介绍,对开发者是很是的友好。
大体了解了构造函数的逻辑以后,咱们看下 MarkdownIt 原型上的一些方法。
MarkdownIt.prototype.set = function (options) { utils.assign(this.options, options); return this; }; 复制代码
MarkdownIt.prototype.configure = function (presets) { var self = this, presetName; if (utils.isString(presets)) { presetName = presets; presets = config[presetName]; if (!presets) { throw new Error('Wrong `markdown-it` preset "' + presetName + '", check name'); } } ... // 省略部分代码 return this; }; 复制代码
MarkdownIt.prototype.enable = function (list, ignoreInvalid) { var result = []; if (!Array.isArray(list)) { list = [ list ]; } [ 'core', 'block', 'inline' ].forEach(function (chain) { result = result.concat(this[chain].ruler.enable(list, true)); }, this); ... // 省略部分代码 return this; }; 复制代码
MarkdownIt.prototype.disable = function (list, ignoreInvalid) { var result = []; if (!Array.isArray(list)) { list = [ list ]; } [ 'core', 'block', 'inline' ].forEach(function (chain) { result = result.concat(this[chain].ruler.disable(list, true)); }, this); ... // 省略部分代码 return this; }; 复制代码
// plugin 是一个函数,会将后面的参数传入 plugin 而且执行。 MarkdownIt.prototype.use = function (plugin /*, params, ... */) { var args = [ this ].concat(Array.prototype.slice.call(arguments, 1)); plugin.apply(plugin, args); return this; }; 复制代码
// 1.,输入必须是字符串 // 2. State 是 CoreParser 的状态管理类,保存了 md 单例、src 编译字符串、tokens 词法单元等重要信息。 // 3.每种 Parser(InlineParser & BlockParser) 都有对应的状态管理类,内部的实现有必定区别。 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; }; 复制代码
// md 实例上是有 renderer 属性,能够理解为渲染器,对外暴露一些特定的 API,接收 tokens 来生成 HTML 字符串。 MarkdownIt.prototype.render = function (src, env) { env = env || {}; return this.renderer.render(this.parse(src, env), this.options, env); }; 复制代码
// md 实例上的 renderer,能够理解为渲染器,对外暴露一些特定的 API,接收 tokens 来生成 HTML 字符串。 MarkdownIt.prototype.parseInline = function (src, env) { var state = new this.core.State(src, this, env); state.inlineMode = true; this.core.process(state); return state.tokens; }; 复制代码
MarkdownIt.prototype.renderInline = function (src, env) { env = env || {}; return this.renderer.render(this.parseInline(src, env), this.options, env); }; 复制代码
从总体来看,MarkdownIt 的流程以下图:
随着一步步对 MarkdownIt 的剖析以后,是否是惊叹于 Markdown 的架构设计,至少对于我来讲,不少信息经过代码的组织以及变量命名就已经传达给我了,这也体现了阅读优秀源码的好处。
下一篇文章,将讲解 Ruler 以及 Token 这两个基础类。