/**
|
* @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;
|