markdown-it源碼分析5-ParserInline

做者:嵇智html

ParserInline

咱們在 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.jsgit

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.jsmarkdown

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 = [];
}
複製代碼

列舉一些比較有用的字段信息:函數

  1. pos

當前 token 的 content 的第幾個字符串索引post

  1. posMax

當前 token 的 content 的最大索引ui

  1. pending

存放一段完整的字符串,好比this

let src = "**emphasis**"
let state = new StateInline(src)

// state.pending 就是 'emphasis'
複製代碼
  1. delimiters

存放一些特殊標記的分隔符,好比 *~ 等。元素格式以下:

{
  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 實體標籤,好比 &nbsp;&quot;&apos; 等等。

這就是 ParserInline.prototype.tokenize 的全流程,也就是 type 爲 inline 的 token 通過 ruler 的全部 rule 處理以後,生成了不一樣的 token 存儲到 token 的 children 屬性上了。可是 ParserInline.prototype.parse 並無完成,它還要通過 ruler2 的全部 rule 處理。它們分別是 balance_pairs.jsstrikethrough.postProcessemphasis.postProcesstext_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 以後,就是 linkifyreplacementssmartquotes 這些 rule 函數。細節能夠在 ParserCore 裏面找到。最後咱們再回到 markdownItparse 部分

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

  return this.renderer.render(this.parse(src, env), this.options, env);
};
複製代碼

那麼 this.parse 函數執行完成表示全部的 token 都 ready 了,是時候啓動渲染器了!

總結

咱們先來張流程圖,大體看下 parse 的過程。

parser-inline

在調用 this.parse 以後 生成所有的 tokens。這個時候將 tokens 傳入了 this.renderer.render 裏面,最後渲染出 HTML 字符串。下一篇咱們看一下 render 的邏輯。

相關文章
相關標籤/搜索