markdown-it源碼分析3-ParserCore

做者:嵇智javascript

ParserCore

編譯的核心管理者,掌握着不一樣類型的 token 生成的流程。它內部管理了 ParserBlock、ParserInline、linkify、replacements 等 rule 函數。也就是說,用戶傳入一個字符串,經歷了這些 rule 函數處理以後,獲得了一個由許多 token 組成的 tokens 數組,最後再交由 renderer 處理以後,吐出 HTML 字符串。html

先看下 MarkdownIt 的執行邏輯。java

var md = require('markdown-it')({
  html: true,
  linkify: true,
  typographer: true
})
md.render('# markdown-it rulezz!')

MarkdownIt.prototype.render = function (src, env) {
  env = env || {};

  return this.renderer.render(this.parse(src, env), this.options, env);
};

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;
};
複製代碼
  1. render 接收一個字符串。內部調用 parse 方法獲得 tokens。
  2. parse 內部先實例化一個屬於 core 的 state,而且調用 parserCore.process 方法。state 是一個擁有當前 parserCore 的編譯狀態的實例。
  3. this.renderer.render 接收 tokens,最後輸出 HTML 字符串。

咱們重點關注一下 ParserCore 這個類。它位於 lib/parser_core.jsnode

var _rules = [
  [ 'normalize',      require('./rules_core/normalize')      ],
  [ 'block',          require('./rules_core/block')          ],
  [ 'inline',         require('./rules_core/inline')         ],
  [ 'linkify',        require('./rules_core/linkify')        ],
  [ 'replacements',   require('./rules_core/replacements')   ],
  [ 'smartquotes',    require('./rules_core/smartquotes')    ]
];

function Core() {
  this.ruler = new Ruler();

  for (var i = 0; i < _rules.length; i++) {
    this.ruler.push(_rules[i][0], _rules[i][1]);
  }
}

Core.prototype.process = function (state) {
  var i, l, rules;

  rules = this.ruler.getRules('');

  for (i = 0, l = rules.length; i < l; i++) {
    rules[i](state);
  }
};

Core.prototype.State = require('./rules_core/state_core');
複製代碼

parserCore 實例上僅有一個 ruler 屬性,這個是用來管理內部全部的 rule 函數,而且原型上。只有一個 process 方法。linux

當調用 process 的時候,首先會拿到職責鏈名爲空字符串('')的 rule 組成的數組,將 state 做爲入參傳入至每個 rule 函數,獲得 tokens 以後掛載到 state 上去。相似的僞代碼以下:git

const rules = [function normalize, function block, function inline, function ...] for (const rule of rules) {
  rule(state) // rule 內部生成一個個 token,而且存放在 state.tokens 數組裏。
}
// 最後在 md.parse 函數體內部返回 state.tokens
複製代碼

所以咱們的關注點就在於這些屬於 parserCore 的 rule 究竟是作了什麼工做?state 又是什麼呢?先來看下屬於 parserCore 的 state。它位於 lib/rules_core/state_core.jsgithub

function StateCore(src, md, env) {
  this.src = src;
  this.env = env;
  this.tokens = [];
  this.inlineMode = false;
  this.md = md; // link to parser instance
}

StateCore.prototype.Token = Token;
複製代碼

src 用來放用戶輸入的字符串,tokens 存放編譯出來的 token。inlineMode 表示 parse 的時候是否編譯成 type 爲 inline 的 token。md 就是當前 MarkdownIt 的實例。編程

而屬於 ParserCore 的 rules 的職能是什麼?咱們先粗略瞭解一下。它們都在 lib/rules_core 文件夾。windows

  1. normalize.js
module.exports = function inline(state) {
  var str;

  // Normalize newlines
  str = state.src.replace(NEWLINES_RE, '\n');

  // Replace NULL characters
  str = str.replace(NULL_RE, '\uFFFD');

  state.src = str;
};
複製代碼

做用很簡單,就是兼容一下 linux 和 windows 換行符的問題。數組

  1. block.js
module.exports = function block(state) {
  var token;

  if (state.inlineMode) {
    token          = new state.Token('inline', '', 0);
    token.content  = state.src;
    token.map      = [ 0, 1 ];
    token.children = [];
    state.tokens.push(token);
  } else {
    state.md.block.parse(state.src, state.md, state.env, state.tokens);
  }
};
複製代碼

內部邏輯很清晰,先判斷是否開啓 inline 模式的 parse。不然經過 md 調用 ParserBlock 的 parse 方法。這一步是將換行分隔符(\n) 做爲 src 的劃分界限,生成不少 block 爲 true 的 token。咱們在接下來的一篇關於 ParserBlock 分析的文章裏面詳細闡述。

  1. inline.js
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);
    }
  }
};
複製代碼

這一步是在 ParserBlock 以後的,由於 ParserBlock 處理以後會生成 type 爲 inline 的token。這種 token 屬於未徹底解析的 token,須要 ParserInline 進一步處理,生成新的token。這些新生成的 token 會存放在 children 屬性上。舉個栗子來講:

const src = '__ad__'
md.render(src)

// 1.通過 ParserBlock 處理以後是這樣的 token:

{
  type: "inline",
  tag: "",
  attrs: null,
  block: true,
  children: []
  content: "__ad__",
  hidden: false,
  ...
  type:"inline"
}
// 從 content 能夠看出 '__' 並未生成 token,這個符號表明強調的意思,應該替換成 strong 標籤

// 2.再通過 ParserInline 處理以後,會發現 children 多了 5 個 token。代碼以下
{
  ...,
  children: [
    {
      type: "text", tag: "", attrs: null, ...
    },
    {
      type: "strong_open", tag: "strong", attrs: null, …
    },
    {
      type: "text", tag: "", attrs: null, …
    },
    {
      type: "strong_close", tag: "strong", attrs: null, …
    },
    {
      type: "text", tag: "", attrs: null, …
    }
  ]
}

// 最後傳給 md.renderer.render 以後,就能生成加粗的文字了。
複製代碼

ParserInline 的揭祕,會在另一片文章詳細分析。

  1. linkify.js
module.exports = function linkify(state) {
  var i, j, l, tokens, token, currentToken, nodes, ln, text, pos, lastPos,
      level, htmlLinkLevel, url, fullUrl, urlText,
      blockTokens = state.tokens,
      links;

  if (!state.md.options.linkify) { return; }

  for (j = 0, l = blockTokens.length; j < l; j++) {
    if (blockTokens[j].type !== 'inline' ||
        !state.md.linkify.pretest(blockTokens[j].content)) {
      continue;
    }

    tokens = blockTokens[j].children;

    htmlLinkLevel = 0;

    for (i = tokens.length - 1; i >= 0; i--) {
      currentToken = tokens[i];

      if (currentToken.type === 'link_close') {
        i--;
        while (tokens[i].level !== currentToken.level && tokens[i].type !== 'link_open') {
          i--;
        }
        continue;
      }

      if (currentToken.type === 'html_inline') {
        if (isLinkOpen(currentToken.content) && htmlLinkLevel > 0) {
          htmlLinkLevel--;
        }
        if (isLinkClose(currentToken.content)) {
          htmlLinkLevel++;
        }
      }
      if (htmlLinkLevel > 0) { continue; }

      if (currentToken.type === 'text' && state.md.linkify.test(currentToken.content)) {

        text = currentToken.content;
        links = state.md.linkify.match(text);

        // Now split string to nodes
        nodes = [];
        level = currentToken.level;
        lastPos = 0;

        for (ln = 0; ln < links.length; ln++) {

          url = links[ln].url;
          fullUrl = state.md.normalizeLink(url);
          if (!state.md.validateLink(fullUrl)) { continue; }

          urlText = links[ln].text;

          if (!links[ln].schema) {
            urlText = state.md.normalizeLinkText('http://' + urlText).replace(/^http:\/\//, '');
          } else if (links[ln].schema === 'mailto:' && !/^mailto:/i.test(urlText)) {
            urlText = state.md.normalizeLinkText('mailto:' + urlText).replace(/^mailto:/, '');
          } else {
            urlText = state.md.normalizeLinkText(urlText);
          }

          pos = links[ln].index;

          if (pos > lastPos) {
            token         = new state.Token('text', '', 0);
            token.content = text.slice(lastPos, pos);
            token.level   = level;
            nodes.push(token);
          }

          token         = new state.Token('link_open', 'a', 1);
          token.attrs   = [ [ 'href', fullUrl ] ];
          token.level   = level++;
          token.markup  = 'linkify';
          token.info    = 'auto';
          nodes.push(token);

          token         = new state.Token('text', '', 0);
          token.content = urlText;
          token.level   = level;
          nodes.push(token);

          token         = new state.Token('link_close', 'a', -1);
          token.level   = --level;
          token.markup  = 'linkify';
          token.info    = 'auto';
          nodes.push(token);

          lastPos = links[ln].lastIndex;
        }
        if (lastPos < text.length) {
          token         = new state.Token('text', '', 0);
          token.content = text.slice(lastPos);
          token.level   = level;
          nodes.push(token);
        }

        blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes);
      }
    }
  }
};
複製代碼

這個 rule 的做用就是將 URL-like 的字符串轉化成超連接。rule 是否執行,是取決於你實例化 md 傳入的 options.linkify。內部檢測 URL-like 的字符串用的庫是 linkify-it。裏面對不少種 url 格式作了檢驗,有興趣的能夠詳細研究一下。

  1. replacements.js
module.exports = function replace(state) {
  var blkIdx;

  if (!state.md.options.typographer) { return; }

  for (blkIdx = state.tokens.length - 1; blkIdx >= 0; blkIdx--) {

    if (state.tokens[blkIdx].type !== 'inline') { continue; }

    if (SCOPED_ABBR_TEST_RE.test(state.tokens[blkIdx].content)) {
      replace_scoped(state.tokens[blkIdx].children);
    }

    if (RARE_RE.test(state.tokens[blkIdx].content)) {
      replace_rare(state.tokens[blkIdx].children);
    }

  }
};
複製代碼

初始化 md 的時候傳入的 options.typographer 爲 true 的時候,開啓該 rule。這個 rule 的做用,就是替換一些印刷字體,好比相似於下面的:

// (c) (C) → ©
// (tm) (TM) → ™
// (r) (R) → ®
// +- → ±
// (p) (P) -> §
複製代碼
  1. smartquotes.js

初始化 md 的時候傳入的 options.typographer 爲 true 的時候,開啓該 rule。rule 的做用就是爲了處理一些不一樣國家語言的引號問題。官網給出的解釋以下

// 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).
複製代碼

小結

如此一來,咱們從宏觀的角度全面分析了 MarkdownIt 的 parse、tokenize、render 的全流程。代碼的總體設計思路很是的清晰,內部的源碼註釋也是很是的豐富到位,用一張圖來簡單闡述下流程。

parser-core

可是若是有細心的同窗,會發現以下的一段代碼,頗有意思。

Core.prototype.process = function (state) {
  var i, l, rules;

  rules = this.ruler.getRules('');

  for (i = 0, l = rules.length; i < l; i++) {
    rules[i](state);
  }
};
複製代碼

在調用 process 的函數體內部,每次調用一個 rule,會將 state 傳入。state 的 tokens 屬性存儲了全部的 token。所以咱們發現,全部 rule 函數內部必須維持對 state.tokens 和 state 的引用不變,所以不能作相似於如下的賦值操做。

function rule (state) {
  state = xxx // wrong
  state.tokens = [token1, token2] // wrong
  state.tokens.push(token1) // true
}
// 第一個語句錯誤的緣由,是由於你改了 state 的指向,切斷了與老 state 的聯繫。
// 第二個語句錯誤的緣由,是改了 tokens 的指向。這樣接下的 rule 函數拿到的 state.tokens 就丟失了以前 rule 生成的 tokens。
複製代碼

這種函數在函數式編程裏面叫作擁有反作用的函數,由於輸入的 state 在函數內部發生了變化,致使外層 state 也被改變。這也是 javascript 裏面基礎類型與引用類型的區別。可是 MarkdownIt 的總體架構設計就是基於這種引用類型的機制,不然必須在 rule 裏面返回每次新生成的 tokens,而且統一管理。

總結

分析完了 ParserCore,讓咱們從總體上對 MarkdownIt 的原理有了必定的瞭解。下兩篇文章 ParserBlock&ParserInline,咱們分別詳細分析 ParserBlock 和 ParserInline,這兩部分篇幅會比較長,由於這屬於核心的 parse 邏輯。

相關文章
相關標籤/搜索