UNPKG

tui-code-snippet

Version:
389 lines (338 loc) 11.6 kB
/** * @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;