做者:嵇智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 語法的能力。oop
添加渲染 emoji 的 ruleui
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 來定製化本身的需求。