text: /**
   * @fileoverview Convert text by binding expressions with context.
   * @author NHN FE Development Lab <dl_javascript@nhn.com>
   */
  
  'use strict';
  
  var inArray = require('../array/inArray');
  var forEach = require('../collection/forEach');
  var isArray = require('../type/isArray');
  var isString = require('../type/isString');
  var extend = require('../object/extend');
  
  // IE8 does not support capture groups.
  var EXPRESSION_REGEXP = /{{\s?|\s?}}/g;
  var BRACKET_NOTATION_REGEXP = /^[a-zA-Z0-9_@]+\[[a-zA-Z0-9_@"']+\]$/;
  var BRACKET_REGEXP = /\[\s?|\s?\]/;
  var DOT_NOTATION_REGEXP = /^[a-zA-Z_]+\.[a-zA-Z_]+$/;
  var DOT_REGEXP = /\./;
  var STRING_NOTATION_REGEXP = /^["']\w+["']$/;
  var STRING_REGEXP = /"|'/g;
  var NUMBER_REGEXP = /^-?\d+\.?\d*$/;
  
  var EXPRESSION_INTERVAL = 2;
  
  var BLOCK_HELPERS = {
    'if': handleIf,
    'each': handleEach,
    'with': handleWith
  };
  
  var isValidSplit = 'a'.split(/a/).length === 3;
  
  /**
   * Split by RegExp. (Polyfill for IE8)
   * @param {string} text - text to be splitted\
   * @param {RegExp} regexp - regular expression
   * @returns {Array.<string>}
   */
  var splitByRegExp = (function() {
    if (isValidSplit) {
      return function(text, regexp) {
        return text.split(regexp);
      };
    }
  
    return function(text, regexp) {
      var result = [];
      var prevIndex = 0;
      var match, index;
  
      if (!regexp.global) {
        regexp = new RegExp(regexp, 'g');
      }
  
      match = regexp.exec(text);
      while (match !== null) {
        index = match.index;
        result.push(text.slice(prevIndex, index));
  
        prevIndex = index + match[0].length;
        match = regexp.exec(text);
      }
      result.push(text.slice(prevIndex));
  
      return result;
    };
  })();
  
  /**
   * Find value in the context by an expression.
   * @param {string} exp - an expression
   * @param {object} context - context
   * @returns {*}
   * @private
   */
  // eslint-disable-next-line complexity
  function getValueFromContext(exp, context) {
    var splitedExps;
    var value = context[exp];
  
    if (exp === 'true') {
      value = true;
    } else if (exp === 'false') {
      value = false;
    } else if (STRING_NOTATION_REGEXP.test(exp)) {
      value = exp.replace(STRING_REGEXP, '');
    } else if (BRACKET_NOTATION_REGEXP.test(exp)) {
      splitedExps = exp.split(BRACKET_REGEXP);
      value = getValueFromContext(splitedExps[0], context)[getValueFromContext(splitedExps[1], context)];
    } else if (DOT_NOTATION_REGEXP.test(exp)) {
      splitedExps = exp.split(DOT_REGEXP);
      value = getValueFromContext(splitedExps[0], context)[splitedExps[1]];
    } else if (NUMBER_REGEXP.test(exp)) {
      value = parseFloat(exp);
    }
  
    return value;
  }
  
  /**
   * Extract elseif and else expressions.
   * @param {Array.<string>} ifExps - args of if expression
   * @param {Array.<string>} sourcesInsideBlock - sources inside if block
   * @returns {object} - exps: expressions of if, elseif, and else / sourcesInsideIf: sources inside if, elseif, and else block.
   * @private
   */
  function extractElseif(ifExps, sourcesInsideBlock) {
    var exps = [ifExps];
    var sourcesInsideIf = [];
    var otherIfCount = 0;
    var start = 0;
  
    // eslint-disable-next-line complexity
    forEach(sourcesInsideBlock, function(source, index) {
      if (source.indexOf('if') === 0) {
        otherIfCount += 1;
      } else if (source === '/if') {
        otherIfCount -= 1;
      } else if (!otherIfCount && (source.indexOf('elseif') === 0 || source === 'else')) {
        exps.push(source === 'else' ? ['true'] : source.split(' ').slice(1));
        sourcesInsideIf.push(sourcesInsideBlock.slice(start, index));
        start = index + 1;
      }
    });
  
    sourcesInsideIf.push(sourcesInsideBlock.slice(start));
  
    return {
      exps: exps,
      sourcesInsideIf: sourcesInsideIf
    };
  }
  
  /**
   * Helper function for "if". 
   * @param {Array.<string>} exps - array of expressions split by spaces
   * @param {Array.<string>} sourcesInsideBlock - array of sources inside the if block
   * @param {object} context - context
   * @returns {string}
   * @private
   */
  function handleIf(exps, sourcesInsideBlock, context) {
    var analyzed = extractElseif(exps, sourcesInsideBlock);
    var result = false;
    var compiledSource = '';
  
    forEach(analyzed.exps, function(exp, index) {
      result = handleExpression(exp, context);
      if (result) {
        compiledSource = compile(analyzed.sourcesInsideIf[index], context);
      }
  
      return !result;
    });
  
    return compiledSource;
  }
  
  /**
   * Helper function for "each".
   * @param {Array.<string>} exps - array of expressions split by spaces
   * @param {Array.<string>} sourcesInsideBlock - array of sources inside the each block
   * @param {object} context - context
   * @returns {string}
   * @private
   */
  function handleEach(exps, sourcesInsideBlock, context) {
    var collection = handleExpression(exps, context);
    var additionalKey = isArray(collection) ? '@index' : '@key';
    var additionalContext = {};
    var result = '';
  
    forEach(collection, function(item, key) {
      additionalContext[additionalKey] = key;
      additionalContext['@this'] = item;
      extend(context, additionalContext);
  
      result += compile(sourcesInsideBlock.slice(), context);
    });
  
    return result;
  }
  
  /**
   * Helper function for "with ... as"
   * @param {Array.<string>} exps - array of expressions split by spaces
   * @param {Array.<string>} sourcesInsideBlock - array of sources inside the with block
   * @param {object} context - context
   * @returns {string}
   * @private
   */
  function handleWith(exps, sourcesInsideBlock, context) {
    var asIndex = inArray('as', exps);
    var alias = exps[asIndex + 1];
    var result = handleExpression(exps.slice(0, asIndex), context);
  
    var additionalContext = {};
    additionalContext[alias] = result;
  
    return compile(sourcesInsideBlock, extend(context, additionalContext)) || '';
  }
  
  /**
   * Extract sources inside block in place.
   * @param {Array.<string>} sources - array of sources
   * @param {number} start - index of start block
   * @param {number} end - index of end block
   * @returns {Array.<string>}
   * @private
   */
  function extractSourcesInsideBlock(sources, start, end) {
    var sourcesInsideBlock = sources.splice(start + 1, end - start);
    sourcesInsideBlock.pop();
  
    return sourcesInsideBlock;
  }
  
  /**
   * Handle block helper function
   * @param {string} helperKeyword - helper keyword (ex. if, each, with)
   * @param {Array.<string>} sourcesToEnd - array of sources after the starting block
   * @param {object} context - context
   * @returns {Array.<string>}
   * @private
   */
  function handleBlockHelper(helperKeyword, sourcesToEnd, context) {
    var executeBlockHelper = BLOCK_HELPERS[helperKeyword];
    var helperCount = 1;
    var startBlockIndex = 0;
    var endBlockIndex;
    var index = startBlockIndex + EXPRESSION_INTERVAL;
    var expression = sourcesToEnd[index];
  
    while (helperCount && isString(expression)) {
      if (expression.indexOf(helperKeyword) === 0) {
        helperCount += 1;
      } else if (expression.indexOf('/' + helperKeyword) === 0) {
        helperCount -= 1;
        endBlockIndex = index;
      }
  
      index += EXPRESSION_INTERVAL;
      expression = sourcesToEnd[index];
    }
  
    if (helperCount) {
      throw Error(helperKeyword + ' needs {{/' + helperKeyword + '}} expression.');
    }
  
    sourcesToEnd[startBlockIndex] = executeBlockHelper(
      sourcesToEnd[startBlockIndex].split(' ').slice(1),
      extractSourcesInsideBlock(sourcesToEnd, startBlockIndex, endBlockIndex),
      context
    );
  
    return sourcesToEnd;
  }
  
  /**
   * Helper function for "custom helper".
   * If helper is not a function, return helper itself.
   * @param {Array.<string>} exps - array of expressions split by spaces (first element: helper)
   * @param {object} context - context
   * @returns {string}
   * @private
   */
  function handleExpression(exps, context) {
    var result = getValueFromContext(exps[0], context);
  
    if (result instanceof Function) {
      return executeFunction(result, exps.slice(1), context);
    }
  
    return result;
  }
  
  /**
   * Execute a helper function.
   * @param {Function} helper - helper function
   * @param {Array.<string>} argExps - expressions of arguments
   * @param {object} context - context
   * @returns {string} - result of executing the function with arguments
   * @private
   */
  function executeFunction(helper, argExps, context) {
    var args = [];
    forEach(argExps, function(exp) {
      args.push(getValueFromContext(exp, context));
    });
  
    return helper.apply(null, args);
  }
  
  /**
   * Get a result of compiling an expression with the context.
   * @param {Array.<string>} sources - array of sources split by regexp of expression.
   * @param {object} context - context
   * @returns {Array.<string>} - array of sources that bind with its context
   * @private
   */
  function compile(sources, context) {
    var index = 1;
    var expression = sources[index];
    var exps, firstExp, result;
  
    while (isString(expression)) {
      exps = expression.split(' ');
      firstExp = exps[0];
  
      if (BLOCK_HELPERS[firstExp]) {
        result = handleBlockHelper(firstExp, sources.splice(index, sources.length - index), context);
        sources = sources.concat(result);
      } else {
        sources[index] = handleExpression(exps, context);
      }
  
      index += EXPRESSION_INTERVAL;
      expression = sources[index];
    }
  
    return sources.join('');
  }
  
  /**
   * Convert text by binding expressions with context.
   * <br>
   * If expression exists in the context, it will be replaced.
   * ex) '{{title}}' with context {title: 'Hello!'} is converted to 'Hello!'.
   * An array or object can be accessed using bracket and dot notation.
   * ex) '{{odds\[2\]}}' with context {odds: \[1, 3, 5\]} is converted to '5'.
   * ex) '{{evens\[first\]}}' with context {evens: \[2, 4\], first: 0} is converted to '2'.
   * ex) '{{project\["name"\]}}' and '{{project.name}}' with context {project: {name: 'CodeSnippet'}} is converted to 'CodeSnippet'.
   * <br>
   * If replaced expression is a function, next expressions will be arguments of the function.
   * ex) '{{add 1 2}}' with context {add: function(a, b) {return a + b;}} is converted to '3'.
   * <br>
   * It has 3 predefined block helpers '{{helper ...}} ... {{/helper}}': 'if', 'each', 'with ... as ...'.
   * 1) 'if' evaluates conditional statements. It can use with 'elseif' and 'else'.
   * 2) 'each' iterates an array or object. It provides '@index'(array), '@key'(object), and '@this'(current element).
   * 3) 'with ... as ...' provides an alias.
   * @param {string} text - text with expressions
   * @param {object} context - context
   * @returns {string} - text that bind with its context
   * @memberof module:domUtil
   * @example
   * // ES6
   * import template from 'tui-code-snippet/domUtil/template'; 
   * 
   * // CommonJS
   * const template = require('tui-code-snippet/domUtil/template'); 
   * 
   * const source = 
   *     '<h1>'
   *   +   '{{if isValidNumber title}}'
   *   +     '{{title}}th'
   *   +   '{{elseif isValidDate title}}'
   *   +     'Date: {{title}}'
   *   +   '{{/if}}'
   *   + '</h1>'
   *   + '{{each list}}'
   *   +   '{{with addOne @index as idx}}'
   *   +     '<p>{{idx}}: {{@this}}</p>'
   *   +   '{{/with}}'
   *   + '{{/each}}';
   * 
   * const context = {
   *   isValidDate: function(text) {
   *     return /^\d{4}-(0|1)\d-(0|1|2|3)\d$/.test(text);
   *   },
   *   isValidNumber: function(text) {
   *     return /^\d+$/.test(text);
   *   },
   *   title: '2019-11-25',
   *   list: ['Clean the room', 'Wash the dishes'],
   *   addOne: function(num) {
   *     return num + 1;
   *   }
   * };
   * 
   * const result = template(source, context);
   * console.log(result); // <h1>Date: 2019-11-25</h1><p>1: Clean the room</p><p>2: Wash the dishes</p>
   */
  function template(text, context) {
    return compile(splitByRegExp(text, EXPRESSION_REGEXP), context);
  }
  
  module.exports = template;
  