做者:嵇智html
咱們在 ParserCore 講到了,通過 ParserCore 處理以後,生成了 type 爲 inline 的 token。下一步就是交給 ParserInline 處理。而這個 rule 函數的代碼以下:node
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); } } }; 複製代碼
也就是拿到 type 爲 inline 的 token,調用 ParserInline 的 parse 方法。ParserInline 位於 lib/parser_inline.js
。git
var _rules = [ [ 'text', require('./rules_inline/text') ], [ 'newline', require('./rules_inline/newline') ], [ 'escape', require('./rules_inline/escape') ], [ 'backticks', require('./rules_inline/backticks') ], [ 'strikethrough', require('./rules_inline/strikethrough').tokenize ], [ 'emphasis', require('./rules_inline/emphasis').tokenize ], [ 'link', require('./rules_inline/link') ], [ 'image', require('./rules_inline/image') ], [ 'autolink', require('./rules_inline/autolink') ], [ 'html_inline', require('./rules_inline/html_inline') ], [ 'entity', require('./rules_inline/entity') ] ]; var _rules2 = [ [ 'balance_pairs', require('./rules_inline/balance_pairs') ], [ 'strikethrough', require('./rules_inline/strikethrough').postProcess ], [ 'emphasis', require('./rules_inline/emphasis').postProcess ], [ 'text_collapse', require('./rules_inline/text_collapse') ] ]; function ParserInline() { var i; this.ruler = new Ruler(); for (i = 0; i < _rules.length; i++) { this.ruler.push(_rules[i][0], _rules[i][1]); } this.ruler2 = new Ruler(); for (i = 0; i < _rules2.length; i++) { this.ruler2.push(_rules2[i][0], _rules2[i][1]); } } 複製代碼
從構造函數看出,ParserInline 不一樣於 ParserBlock,它是有兩個 Ruler 實例的。 ruler 是在 tokenize 調用的,ruler2 是在 tokenize 以後再使用的。github
ParserInline.prototype.tokenize = function (state) { var ok, i, rules = this.ruler.getRules(''), len = rules.length, end = state.posMax, maxNesting = state.md.options.maxNesting; while (state.pos < end) { if (state.level < maxNesting) { for (i = 0; i < len; i++) { ok = rules[i](state, false); if (ok) { break; } } } if (ok) { if (state.pos >= end) { break; } continue; } state.pending += state.src[state.pos++]; } if (state.pending) { state.pushPending(); } }; ParserInline.prototype.parse = function (str, md, env, outTokens) { var i, rules, len; var state = new this.State(str, md, env, outTokens); this.tokenize(state); rules = this.ruler2.getRules(''); len = rules.length; for (i = 0; i < len; i++) { rules[i](state); } }; 複製代碼
文章的開頭說到將 type 爲 inline 的 token 傳給 md.inline.parse 方法,這樣就走進了 parse 的函數內部,首先生成屬於 ParserInline 的 state,還記得 ParserCore 與 ParserBlock 的 state 麼?它們的做用都是存放不一樣 parser 在 parse 過程當中的狀態信息。數組
咱們先來看下 State 類,它位於 lib/rules_inline/state_inline.js
。markdown
function StateInline(src, md, env, outTokens) { this.src = src; this.env = env; this.md = md; this.tokens = outTokens; this.pos = 0; this.posMax = this.src.length; this.level = 0; this.pending = ''; this.pendingLevel = 0; this.cache = {}; this.delimiters = []; } 複製代碼
列舉一些比較有用的字段信息:函數
當前 token 的 content 的第幾個字符串索引oop
當前 token 的 content 的最大索引post
存放一段完整的字符串,好比ui
let src = "**emphasis**" let state = new StateInline(src) // state.pending 就是 'emphasis' 複製代碼
存放一些特殊標記的分隔符,好比 *
、~
等。元素格式以下:
{ close:false end:-1 jump:0 length:2 level:0 marker:42 open:true token:0 } // marker 表示字符串對應的 ascii 碼 複製代碼
生成 state 以後,而後調用 tokenize 方法。
ParserInline.prototype.tokenize = function (state) { var ok, i, rules = this.ruler.getRules(''), len = rules.length, end = state.posMax, maxNesting = state.md.options.maxNesting; while (state.pos < end) { if (state.level < maxNesting) { for (i = 0; i < len; i++) { ok = rules[i](state, false); if (ok) { break; } } } if (ok) { if (state.pos >= end) { break; } continue; } state.pending += state.src[state.pos++]; } if (state.pending) { state.pushPending(); } }; 複製代碼
首先獲取默認的 rule chain,而後掃描 token 的 content 字段,從第一個字符掃描至尾部,每個字符依次調用 ruler 的 rule 函數。它們位於 lib/rules_inline/
文件夾下面。調用順序依次以下:
text.js
module.exports = function text(state, silent) { var pos = state.pos; while (pos < state.posMax && !isTerminatorChar(state.src.charCodeAt(pos))) { pos++; } if (pos === state.pos) { return false; } if (!silent) { state.pending += state.src.slice(state.pos, pos); } state.pos = pos; return true; }; 複製代碼
做用是提取連續的非 isTerminatorChar 字符。isTerminatorChar 字符的規定以下:
function isTerminatorChar(ch) { switch (ch) { case 0x0A/* \n */: case 0x21/* ! */: case 0x23/* # */: case 0x24/* $ */: case 0x25/* % */: case 0x26/* & */: case 0x2A/* * */: case 0x2B/* + */: case 0x2D/* - */: case 0x3A/* : */: case 0x3C/* < */: case 0x3D/* = */: case 0x3E/* > */: case 0x40/* @ */: case 0x5B/* [ */: case 0x5C/* \ */: case 0x5D/* ] */: case 0x5E/* ^ */: case 0x5F/* _ */: case 0x60/* ` */: case 0x7B/* { */: case 0x7D/* } */: case 0x7E/* ~ */: return true; default: return false; } } 複製代碼
假如輸入是 "__ad__",那麼這個 rule 就能提取 "ad" 字符串出來。
newline.js
module.exports = function newline(state, silent) { var pmax, max, pos = state.pos; if (state.src.charCodeAt(pos) !== 0x0A/* \n */) { return false; } pmax = state.pending.length - 1; max = state.posMax; if (!silent) { if (pmax >= 0 && state.pending.charCodeAt(pmax) === 0x20) { if (pmax >= 1 && state.pending.charCodeAt(pmax - 1) === 0x20) { state.pending = state.pending.replace(/ +$/, ''); state.push('hardbreak', 'br', 0); } else { state.pending = state.pending.slice(0, -1); state.push('softbreak', 'br', 0); } } else { state.push('softbreak', 'br', 0); } } pos++; while (pos < max && isSpace(state.src.charCodeAt(pos))) { pos++; } state.pos = pos; return true; }; 複製代碼
處理換行符(\n
)。
escape.js
module.exports = function escape(state, silent) { var ch, pos = state.pos, max = state.posMax; if (state.src.charCodeAt(pos) !== 0x5C/* \ */) { return false; } pos++; if (pos < max) { ch = state.src.charCodeAt(pos); if (ch < 256 && ESCAPED[ch] !== 0) { if (!silent) { state.pending += state.src[pos]; } state.pos += 2; return true; } if (ch === 0x0A) { if (!silent) { state.push('hardbreak', 'br', 0); } pos++; // skip leading whitespaces from next line while (pos < max) { ch = state.src.charCodeAt(pos); if (!isSpace(ch)) { break; } pos++; } state.pos = pos; return true; } } if (!silent) { state.pending += '\\'; } state.pos++; return true; }; 複製代碼
處理轉義字符(\
)。
backtick.js
module.exports = function backtick(state, silent) { var start, max, marker, matchStart, matchEnd, token, pos = state.pos, ch = state.src.charCodeAt(pos); if (ch !== 0x60/* ` */) { return false; } start = pos; pos++; max = state.posMax; while (pos < max && state.src.charCodeAt(pos) === 0x60/* ` */) { pos++; } marker = state.src.slice(start, pos); matchStart = matchEnd = pos; while ((matchStart = state.src.indexOf('`', matchEnd)) !== -1) { matchEnd = matchStart + 1; while (matchEnd < max && state.src.charCodeAt(matchEnd) === 0x60/* ` */) { matchEnd++; } if (matchEnd - matchStart === marker.length) { if (!silent) { token = state.push('code_inline', 'code', 0); token.markup = marker; token.content = state.src.slice(pos, matchStart) .replace(/[ \n]+/g, ' ') .trim(); } state.pos = matchEnd; return true; } } if (!silent) { state.pending += marker; } state.pos += marker.length; return true; }; 複製代碼
處理反引號字符(`)。
markdown 語法: `這是反引號`。
strikethrough.js
代碼太長,就不粘貼了,做用是處理刪除字符(~
)。
markdown 語法: ~~strike~~
。
emphasis.js
做用是處理加粗文字的字符(*
或者 _
)。
markdown 語法: **strong**
。
link.js
做用是解析超連接。
markdown 語法: [text](href)
。
image.js
做用是解析圖片。
markdown 語法: 
。
autolink.js
module.exports = function autolink(state, silent) { var tail, linkMatch, emailMatch, url, fullUrl, token, pos = state.pos; if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false; } tail = state.src.slice(pos); if (tail.indexOf('>') < 0) { return false; } if (AUTOLINK_RE.test(tail)) { linkMatch = tail.match(AUTOLINK_RE); url = linkMatch[0].slice(1, -1); fullUrl = state.md.normalizeLink(url); if (!state.md.validateLink(fullUrl)) { return false; } if (!silent) { token = state.push('link_open', 'a', 1); token.attrs = [ [ 'href', fullUrl ] ]; token.markup = 'autolink'; token.info = 'auto'; token = state.push('text', '', 0); token.content = state.md.normalizeLinkText(url); token = state.push('link_close', 'a', -1); token.markup = 'autolink'; token.info = 'auto'; } state.pos += linkMatch[0].length; return true; } if (EMAIL_RE.test(tail)) { emailMatch = tail.match(EMAIL_RE); url = emailMatch[0].slice(1, -1); fullUrl = state.md.normalizeLink('mailto:' + url); if (!state.md.validateLink(fullUrl)) { return false; } if (!silent) { token = state.push('link_open', 'a', 1); token.attrs = [ [ 'href', fullUrl ] ]; token.markup = 'autolink'; token.info = 'auto'; token = state.push('text', '', 0); token.content = state.md.normalizeLinkText(url); token = state.push('link_close', 'a', -1); token.markup = 'autolink'; token.info = 'auto'; } state.pos += emailMatch[0].length; return true; } return false; }; 複製代碼
能夠看到 autolink 就是解析 <
與 >
之間的 url。
markdown 語法: <http://somewhere.com>
。
html_inline.js
module.exports = function html_inline(state, silent) { var ch, match, max, token, pos = state.pos; if (!state.md.options.html) { return false; } // Check start max = state.posMax; if (state.src.charCodeAt(pos) !== 0x3C/* < */ || pos + 2 >= max) { return false; } // Quick fail on second char ch = state.src.charCodeAt(pos + 1); if (ch !== 0x21/* ! */ && ch !== 0x3F/* ? */ && ch !== 0x2F/* / */ && !isLetter(ch)) { return false; } match = state.src.slice(pos).match(HTML_TAG_RE); if (!match) { return false; } if (!silent) { token = state.push('html_inline', '', 0); token.content = state.src.slice(pos, pos + match[0].length); } state.pos += match[0].length; return true; }; 複製代碼
解析 HTML 行內標籤。
markdown 語法: <span>inline html</span>
。
entity.js
module.exports = function entity(state, silent) { var ch, code, match, pos = state.pos, max = state.posMax; if (state.src.charCodeAt(pos) !== 0x26/* & */) { return false; } if (pos + 1 < max) { ch = state.src.charCodeAt(pos + 1); if (ch === 0x23 /* # */) { match = state.src.slice(pos).match(DIGITAL_RE); if (match) { if (!silent) { code = match[1][0].toLowerCase() === 'x' ? parseInt(match[1].slice(1), 16) : parseInt(match[1], 10); state.pending += isValidEntityCode(code) ? fromCodePoint(code) : fromCodePoint(0xFFFD); } state.pos += match[0].length; return true; } } else { match = state.src.slice(pos).match(NAMED_RE); if (match) { if (has(entities, match[1])) { if (!silent) { state.pending += entities[match[1]]; } state.pos += match[0].length; return true; } } } } if (!silent) { state.pending += '&'; } state.pos++; return true; }; 複製代碼
解析 HTML 實體標籤,好比
、"
、'
等等。
這就是 ParserInline.prototype.tokenize
的全流程,也就是 type 爲 inline 的 token 通過 ruler 的全部 rule 處理以後,生成了不一樣的 token 存儲到 token 的 children 屬性上了。可是 ParserInline.prototype.parse
並無完成,它還要通過 ruler2 的全部 rule 處理。它們分別是 balance_pairs.js
、strikethrough.postProcess
、emphasis.postProcess
、text_collapse.js
。
balance_pairs.js
module.exports = function link_pairs(state) { var i, j, lastDelim, currDelim, delimiters = state.delimiters, max = state.delimiters.length; for (i = 0; i < max; i++) { lastDelim = delimiters[i]; if (!lastDelim.close) { continue; } j = i - lastDelim.jump - 1; while (j >= 0) { currDelim = delimiters[j]; if (currDelim.open && currDelim.marker === lastDelim.marker && currDelim.end < 0 && currDelim.level === lastDelim.level) { // typeofs are for backward compatibility with plugins var odd_match = (currDelim.close || lastDelim.open) && typeof currDelim.length !== 'undefined' && typeof lastDelim.length !== 'undefined' && (currDelim.length + lastDelim.length) % 3 === 0; if (!odd_match) { lastDelim.jump = i - j; lastDelim.open = false; currDelim.end = i; currDelim.jump = 0; break; } } j -= currDelim.jump + 1; } } }; 複製代碼
處理 state.delimiters 數組,主要是給諸如 *
、~
等找到配對的開閉標籤。
strikethrough.postProcess
位於 lib/rules_inline/strikethrough
,函數是處理 ~
字符,生成 <s>
標籤的 token。
emphasis.postProcess
位於 lib/rules_inline/emphasis
,函數是處理 *
或者 _
字符,生成 <strong>
或者 <em>
標籤的 token。
text_collapse.js
module.exports = function text_collapse(state) { var curr, last, level = 0, tokens = state.tokens, max = state.tokens.length; for (curr = last = 0; curr < max; curr++) { // re-calculate levels level += tokens[curr].nesting; tokens[curr].level = level; if (tokens[curr].type === 'text' && curr + 1 < max && tokens[curr + 1].type === 'text') { // collapse two adjacent text nodes tokens[curr + 1].content = tokens[curr].content + tokens[curr + 1].content; } else { if (curr !== last) { tokens[last] = tokens[curr]; } last++; } } if (curr !== last) { tokens.length = last; } }; 複製代碼
函數是用來合併相鄰的文本節點。舉個栗子
const src = '12_' md.parse(src) // state.tokens 以下 [ { content:"12", tag:"", type:"text" }, { content:"_", tag:"", type:"text", ... } ] // 通過 text_collapse 函數以後, [ { content:"12_", tag:"", type:"text" } ] 複製代碼
至此,ParserInline 就已經走完了。若是你打 debugger 調試會發現,在 ParserInline.prototype.parse
以後,type 爲 inline 的 token 上的 children 屬性已經存在了一些子 token。這些子 token 的產生就是 ParserInline 的功勞。而 ParserInline 以後,就是 linkify
、replacements
、smartquotes
這些 rule 函數。細節能夠在 ParserCore 裏面找到。最後咱們再回到 markdownIt
的 parse
部分
MarkdownIt.prototype.render = function (src, env) { env = env || {}; return this.renderer.render(this.parse(src, env), this.options, env); }; 複製代碼
那麼 this.parse
函數執行完成表示全部的 token 都 ready 了,是時候啓動渲染器了!
咱們先來張流程圖,大體看下 parse 的過程。
在調用 this.parse
以後 生成所有的 tokens。這個時候將 tokens 傳入了 this.renderer.render
裏面,最後渲染出 HTML 字符串。下一篇咱們看一下 render
的邏輯。