做者:嵇智html
插件擴展了在 md 文件裏面識別 emoji 的能力。通常 emoji 的語法是 :名稱:
。名稱通常是指定的英文或者數字,同時還支持一些 shortcuts。例如node
:100: => 💯
:stuck_out_tongue: => 😛
// shortcuts
:D => 😄
複製代碼
註冊插件的邏輯以下:git
var md = require('markdown-it')();
var emoji = require('markdown-it-emoji');
md.use(emoji [, options]);
複製代碼
而 MarkdownIt 的 use
的邏輯很簡單,就是調用 use
傳入的第一個參數,它是一個函數,這函數會被調用,而且入參是從第二個參數開始的全部參數。github
MarkdownIt.prototype.use = function (plugin /*, params, ... */) {
var args = [ this ].concat(Array.prototype.slice.call(arguments, 1));
plugin.apply(plugin, args);
return this;
};
複製代碼
而咱們更加關注的是 markdown-it-emoji 的這個函數,它位於 markdown-it-emoji/index.js
。json
var emojies_defs = require('./lib/data/full.json');
var emojies_shortcuts = require('./lib/data/shortcuts');
var emoji_html = require('./lib/render');
var emoji_replace = require('./lib/replace');
var normalize_opts = require('./lib/normalize_opts');
module.exports = function emoji_plugin(md, options) {
// 步驟一
var defaults = {
defs: emojies_defs,
shortcuts: emojies_shortcuts,
enabled: []
};
// 步驟二
var opts = normalize_opts(md.utils.assign({}, defaults, options || {}));
// 步驟三
md.renderer.rules.emoji = emoji_html;
// 步驟四
md.core.ruler.push('emoji', emoji_replace(md, opts.defs, opts.shortcuts, opts.scanRE, opts.replaceRE));
};
複製代碼
emoji_plugin 這個函數看起來也是很是的簡單,首先它有 md
和 options
兩個參數。 options
會與內置的 defaults
作一次 assign 操做。咱們根據函數的執行,大體分爲 4 個步驟。markdown
defaultsapp
// defs 屬性值是 emoji 的映射。
defs = {
"100": "💯",
"1234": "🔢",
"grinning": "😀",
"smiley": "😃",
"smile": "😄",
"grin": "😁",
"laughing": "😆",
......
// 全部的配置在 `lib/data/full.json`
}
// shortcuts 屬性值是一些短名稱的映射配置。
// 好比你能夠用 ":smile:",也能夠用 ":D"
shortcuts = [
angry: [ '>:(', '>:-(' ],
blush: [ ':")', ':-")' ],
broken_heart: [ '</3', '<\\3' ],
......
// 全部的配置在 `lib/data/shortcuts.js`
]
// 開啓的 emoji 規則。僅僅只開啓 eabled 配置的 emoji,會把其餘的默認 emoji 規則關閉
enabled = []
複製代碼
normalize_opts函數
module.exports = function normalize_opts(options) {
var emojies = options.defs,
shortcuts;
// Filter emojies by whitelist, if needed
if (options.enabled.length) {
emojies = Object.keys(emojies).reduce(function (acc, key) {
if (options.enabled.indexOf(key) >= 0) {
acc[key] = emojies[key];
}
return acc;
}, {});
}
// Flatten shortcuts to simple object: { alias: emoji_name }
shortcuts = Object.keys(options.shortcuts).reduce(function (acc, key) {
// Skip aliases for filtered emojies, to reduce regexp
if (!emojies[key]) { return acc; }
if (Array.isArray(options.shortcuts[key])) {
options.shortcuts[key].forEach(function (alias) {
acc[alias] = key;
});
return acc;
}
acc[options.shortcuts[key]] = key;
return acc;
}, {});
// Compile regexp
var names = Object.keys(emojies)
.map(function (name) { return ':' + name + ':'; })
.concat(Object.keys(shortcuts))
.sort()
.reverse()
.map(function (name) { return quoteRE(name); })
.join('|');
var scanRE = RegExp(names);
var replaceRE = RegExp(names, 'g');
return {
defs: emojies,
shortcuts: shortcuts,
scanRE: scanRE,
replaceRE: replaceRE
};
};
複製代碼
函數邏輯很簡單,就是處理用戶輸入的 options。首先處理 enabled
白名單校驗,而後再支持 shortcuts
的語法,最後生成 scanRE
正則,這個是用來識別 emoji 語法。它是以 |
爲分割,而且擁有校驗 full.json
和 shortcuts.js
全部的 emoji 語法的能力。ui
添加渲染 emoji 的 rulethis
md.renderer.rules.emoji = emoji_html;
module.exports = function emoji_html(tokens, idx /*, options, env */) {
return tokens[idx].content;
};
複製代碼
渲染 rule,是在 MarkdownIt.renderer.render 以後調用的。也就是全部的 Parser 生成不一樣 type 的 token 以後,開始渲染輸出的。正如上面 emoji_html
函數同樣簡單,就是返回 token 的 content 就行。content 這個時候已是 emoji 了。
ParserCore 添加 emoji rule
module.exports = function create_rule(md, emojies, shortcuts, scanRE, replaceRE) {
var arrayReplaceAt = md.utils.arrayReplaceAt,
ucm = md.utils.lib.ucmicro,
ZPCc = new RegExp([ ucm.Z.source, ucm.P.source, ucm.Cc.source ].join('|'));
function splitTextToken(text, level, Token) {
......
}
return function emoji_replace(state) {
var i, j, l, tokens, token,
blockTokens = state.tokens,
autolinkLevel = 0;
for (j = 0, l = blockTokens.length; j < l; j++) {
if (blockTokens[j].type !== 'inline') { continue; }
tokens = blockTokens[j].children;
// We scan from the end, to keep position when new tags added.
// Use reversed logic in links start/end match
for (i = tokens.length - 1; i >= 0; i--) {
token = tokens[i];
if (token.type === 'link_open' || token.type === 'link_close') {
if (token.info === 'auto') { autolinkLevel -= token.nesting; }
}
if (token.type === 'text' && autolinkLevel === 0 && scanRE.test(token.content)) {
// replace current node
blockTokens[j].children = tokens = arrayReplaceAt(
tokens, i, splitTextToken(token.content, token.level, state.Token)
);
}
}
}
};
};
複製代碼
步驟 4 的執行時間是發生在步驟 3 以前的,由於步驟 4 的 rule 是在 ParserCore.parse 的時候調用,而步驟 3 是在 render 的過程當中調用。
emoji_replace 的邏輯很清晰,給 ParserCore 的 ParserBlock 處理完成以後,這個時候,會生成 type 爲 inline 的 token。而 emoji_replace 函數先過濾出 type 爲 inline 的 token。再拿到 token.children
,從後往前掃描存在 children 裏的 token。若是命中瞭如下邏輯,就開始準備生成 type 爲 emoji 的 token而且調用 arrayReplaceAt
插入到 token.children 當中,最後再通過步驟 3 的 md.renderer.rules.emoji
處理,生成對應的 emoji。
if (token.type === 'text' && autolinkLevel === 0 && scanRE.test(token.content)) {
// replace current node
blockTokens[j].children = tokens = arrayReplaceAt(
tokens, i, splitTextToken(token.content, token.level, state.Token)
);
}
複製代碼
咱們再來看下 splitTextToken
是怎麼處理 token.content
,最終生成 type 爲 emoji 的 token的。
function splitTextToken(text, level, Token) {
var token, last_pos = 0, nodes = [];
text.replace(replaceRE, function (match, offset, src) {
var emoji_name;
if (shortcuts.hasOwnProperty(match)) {
emoji_name = shortcuts[match];
if (offset > 0 && !ZPCc.test(src[offset - 1])) {
return;
}
if (offset + match.length < src.length && !ZPCc.test(src[offset + match.length])) {
return;
}
} else {
emoji_name = match.slice(1, -1);
}
if (offset > last_pos) {
token = new Token('text', '', 0);
token.content = text.slice(last_pos, offset);
nodes.push(token);
}
token = new Token('emoji', '', 0);
token.markup = emoji_name;
token.content = emojies[emoji_name];
nodes.push(token);
last_pos = offset + match.length;
});
if (last_pos < text.length) {
token = new Token('text', '', 0);
token.content = text.slice(last_pos);
nodes.push(token);
}
return nodes;
}
複製代碼
第一個參數 text
,就是 token.content
。它是一個含有 emoji 語法,但還未生成 emoji 的字符串,好比 ":smile:"
,接着調用 text.replace
函數,而且傳入 replaceRE
這個正則,replaceRE
是擁有解析 lib/data/full.json
以及 lib/data/shortcuts.js
emoji 語法的正則,它是一個全局匹配模式,會逐步的將 text
內符合對應 emoji 語法的字符串轉化爲 emoji。舉個例子:
const text = ":D,:100:,:-1:"
// 通過 splitTextToken 處理,最後輸出 😄,💯,👎
複製代碼
通過 markdown-it-emoji 的插件的處理以後,最後 md 文件裏面的 emoji
語法,都將被識別而且渲染成 emoji
。
從這個插件來看,MarkdownIt 的擴展性是很是優秀的。你老是能在不一樣的階段去觸及到 tokens,甚至還能夠更改 render rule 來定製化本身的需求。