做者:嵇智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 的第幾個字符串索引post
當前 token 的 content 的最大索引ui
存放一段完整的字符串,好比this
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 語法: ![image](<src> "title")
。
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
的邏輯。