Skip to content

Commit a11b7b3

Browse files
committed
feat: add template function in domUtil
1 parent c0cb74f commit a11b7b3

File tree

3 files changed

+595
-0
lines changed

3 files changed

+595
-0
lines changed

domUtil/template.js

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
/**
2+
* @fileoverview Convert text by binding expressions with context.
3+
* @author NHN FE Development Lab <dl_javascript@nhn.com>
4+
*/
5+
6+
'use strict';
7+
8+
var inArray = require('../array/inArray');
9+
var forEach = require('../collection/forEach');
10+
var isArray = require('../type/isArray');
11+
var isString = require('../type/isString');
12+
var extend = require('../object/extend');
13+
14+
var EXPRESSION_REGEXP = /{{\s?(\/?[a-zA-Z0-9_.@[\] ]+)\s?}}/g;
15+
var BRACKET_REGEXP = /^([a-zA-Z0-9_@]+)\[([a-zA-Z0-9_@]+)\]$/;
16+
var NUMBER_REGEXP = /^-?\d+\.?\d*$/;
17+
18+
var BLOCK_HELPERS = {
19+
'if': handleIf,
20+
'each': handleEach,
21+
'with': handleWith
22+
};
23+
24+
/**
25+
* Find value in the context by an expression.
26+
* @param {string} exp - an expression
27+
* @param {object} context - context
28+
* @return {*}
29+
* @private
30+
*/
31+
function getValueFromContext(exp, context) {
32+
var bracketExps;
33+
var value = context[exp];
34+
35+
if (exp === 'true') {
36+
value = true;
37+
} else if (exp === 'false') {
38+
value = false;
39+
} else if (BRACKET_REGEXP.test(exp)) {
40+
bracketExps = exp.split(BRACKET_REGEXP);
41+
value = getValueFromContext(bracketExps[1], context)[getValueFromContext(bracketExps[2], context)];
42+
} else if (NUMBER_REGEXP.test(exp)) {
43+
value = parseFloat(exp);
44+
}
45+
46+
return value;
47+
}
48+
49+
/**
50+
* Extract elseif and else expressions.
51+
* @param {Array.<string>} ifExps - args of if expression
52+
* @param {Array.<string>} sourcesInsideBlock - sources inside if block
53+
* @return {object} - exps: expressions of if, elseif, and else / sourcesInsideIf: sources inside if, elseif, and else block.
54+
* @private
55+
*/
56+
function extractElseif(ifExps, sourcesInsideBlock) {
57+
var exps = [ifExps];
58+
var sourcesInsideIf = [];
59+
60+
var start = 0;
61+
var i, len, source;
62+
63+
for (i = 0, len = sourcesInsideBlock.length; i < len; i += 1) {
64+
source = sourcesInsideBlock[i];
65+
66+
if (source.indexOf('elseif') > -1 || source === 'else') {
67+
exps.push(source === 'else' ? ['true'] : source.split(' ').slice(1));
68+
sourcesInsideIf.push(sourcesInsideBlock.slice(start, i));
69+
start = i + 1;
70+
}
71+
}
72+
sourcesInsideIf.push(sourcesInsideBlock.slice(start));
73+
74+
return {
75+
exps: exps,
76+
sourcesInsideIf: sourcesInsideIf
77+
};
78+
}
79+
80+
/**
81+
* Helper function for "if".
82+
* @param {Array.<string>} exps - array of expressions split by spaces
83+
* @param {Array.<string>} sourcesInsideBlock - array of sources inside the if block
84+
* @param {object} context - context
85+
* @return {string}
86+
* @private
87+
*/
88+
function handleIf(exps, sourcesInsideBlock, context) {
89+
var analyzed = extractElseif(exps, sourcesInsideBlock);
90+
var result = false;
91+
var compiledSource = '';
92+
93+
forEach(analyzed.exps, function(exp, index) {
94+
result = handleExpression(exp, context);
95+
if (result) {
96+
compiledSource = compile(analyzed.sourcesInsideIf[index], context);
97+
}
98+
99+
return !result;
100+
});
101+
102+
return compiledSource;
103+
}
104+
105+
/**
106+
* Helper function for "each".
107+
* @param {Array.<string>} exps - array of expressions split by spaces
108+
* @param {Array.<string>} sourcesInsideBlock - array of sources inside the each block
109+
* @param {object} context - context
110+
* @return {string}
111+
* @private
112+
*/
113+
function handleEach(exps, sourcesInsideBlock, context) {
114+
var collection = handleExpression(exps, context);
115+
var additionalKey = isArray(collection) ? '@index' : '@key';
116+
var additionalContext = {};
117+
var result = '';
118+
119+
forEach(collection, function(item, key) {
120+
additionalContext[additionalKey] = key;
121+
additionalContext['@this'] = item;
122+
extend(additionalContext, context);
123+
124+
result += compile(sourcesInsideBlock.slice(), additionalContext);
125+
});
126+
127+
return result;
128+
}
129+
130+
/**
131+
* Helper function for "with ... as"
132+
* @param {Array.<string>} exps - array of expressions split by spaces
133+
* @param {Array.<string>} sourcesInsideBlock - array of sources inside the with block
134+
* @param {object} context - context
135+
* @return {string}
136+
* @private
137+
*/
138+
function handleWith(exps, sourcesInsideBlock, context) {
139+
var asIndex = inArray('as', exps);
140+
var alias = exps[asIndex + 1];
141+
var result = handleExpression(exps.slice(0, asIndex), context);
142+
143+
var additionalContext = {};
144+
additionalContext[alias] = result;
145+
146+
return compile(sourcesInsideBlock, extend(additionalContext, context)) || '';
147+
}
148+
149+
/**
150+
* Extract sources inside block in place.
151+
* @param {Array.<string>} sources - array of sources
152+
* @param {number} start - index of start block
153+
* @param {number} end - index of end block
154+
* @return {Array.<string>}
155+
* @private
156+
*/
157+
function extractSourcesInsideBlock(sources, start, end) {
158+
var sourcesInsideBlock = sources.splice(start + 1, end - start);
159+
sourcesInsideBlock.pop();
160+
161+
return sourcesInsideBlock;
162+
}
163+
164+
/**
165+
* Concatenate the elements between previous and next of the base element in place.
166+
* @param {Array.<string>} sources - array of sources
167+
* @param {number} index - index of base element
168+
* @private
169+
*/
170+
function joinBetween(source, index) {
171+
var start = Math.max(index - 1, 0);
172+
var end = Math.min(index + 1, source.length - 1);
173+
var deleteCount = end - start + 1;
174+
var result = source.splice(start, deleteCount).join('');
175+
176+
if (deleteCount < 3) {
177+
source.splice(start, 0, '', result);
178+
} else {
179+
source.splice(start, 0, result);
180+
}
181+
}
182+
183+
/**
184+
* Handle block helper function
185+
* @param {string} helperKeyword - helper keyword (ex. if, each, with)
186+
* @param {Array.<string>} sourcesToEnd - array of sources after the starting block
187+
* @param {object} context - context
188+
* @return {Array.<string>}
189+
* @private
190+
*/
191+
function handleBlockHelper(helperKeyword, sourcesToEnd, context) {
192+
var executeBlockHelper = BLOCK_HELPERS[helperKeyword];
193+
var startBlockIndices = [];
194+
var helperCount = 0;
195+
var index = 0;
196+
var expression = sourcesToEnd[index];
197+
var startBlockIndex;
198+
199+
do {
200+
if (expression.indexOf(helperKeyword) === 0) {
201+
helperCount += 1;
202+
startBlockIndices.push(index);
203+
} else if (expression.indexOf('/' + helperKeyword) === 0) {
204+
helperCount -= 1;
205+
startBlockIndex = startBlockIndices.pop();
206+
207+
sourcesToEnd[startBlockIndex] = executeBlockHelper(
208+
sourcesToEnd[startBlockIndex].split(' ').slice(1),
209+
extractSourcesInsideBlock(sourcesToEnd, startBlockIndex, index),
210+
context
211+
);
212+
joinBetween(sourcesToEnd, startBlockIndex);
213+
index = startBlockIndex - 2;
214+
}
215+
216+
index += 2;
217+
expression = sourcesToEnd[index];
218+
} while (helperCount !== 0 && isString(expression));
219+
220+
if (helperCount !== 0) {
221+
throw Error(helperKeyword + ' needs {{/' + helperKeyword + '}} expression.');
222+
}
223+
224+
return sourcesToEnd;
225+
}
226+
227+
/**
228+
* Helper function for "custom helper".
229+
* If helper is not a function, return helper itself.
230+
* @param {Array.<string>} exps - array of expressions split by spaces (first element: helper)
231+
* @param {object} context - context
232+
* @return {string}
233+
* @private
234+
*/
235+
function handleExpression(exps, context) {
236+
var result = getValueFromContext(exps[0], context);
237+
238+
if (result instanceof Function) {
239+
result = executeFunction(result, exps.slice(1), context);
240+
}
241+
242+
return result;
243+
}
244+
245+
/**
246+
* Execute a helper function.
247+
* @param {Function} helper - helper function
248+
* @param {Array.<string>} argExps - expressions of arguments
249+
* @param {object} context - context
250+
* @return {string} - result of executing the function with arguments
251+
* @private
252+
*/
253+
function executeFunction(helper, argExps, context) {
254+
var args = [];
255+
forEach(argExps, function(exp) {
256+
args.push(getValueFromContext(exp, context));
257+
});
258+
259+
return helper.apply(null, args);
260+
}
261+
262+
/**
263+
* Get a result of compiling an expression with the context.
264+
* @param {Array.<string>} sources - array of sources split by regexp of expression.
265+
* @param {object} context - context
266+
* @return {Array.<string>} - array of sources that bind with its context
267+
* @private
268+
*/
269+
function compile(sources, context) {
270+
var index = 1;
271+
var expression = sources[index];
272+
var exps, firstExp, result;
273+
274+
while (isString(expression)) {
275+
exps = expression.split(' ');
276+
firstExp = exps[0];
277+
278+
if (BLOCK_HELPERS[firstExp]) {
279+
result = handleBlockHelper(firstExp, sources.splice(index, sources.length - index), context);
280+
sources = sources.concat(result);
281+
} else {
282+
sources[index] = handleExpression(exps, context);
283+
}
284+
285+
index += 2;
286+
expression = sources[index];
287+
}
288+
289+
return sources.join('');
290+
}
291+
292+
/**
293+
* Convert text by binding expressions with context.
294+
* <br>
295+
* If expression exists in the context, it will be replaced.
296+
* ex) '{{title}}' with context {title: 'Hello!'} is converted to 'Hello!'.
297+
* <br>
298+
* If replaced expression is a function, next expressions will be arguments of the function.
299+
* ex) '{{add 1 2}}' with context {add: function(a, b) {return a + b;}} is converted to '3'.
300+
* <br>
301+
* It has 3 predefined block helpers '{{helper ...}} ... {{/helper}}': 'if', 'each', 'with ... as ...'.
302+
* 1) 'if' evaluates conditional statements. It can use with 'elseif' and 'else'.
303+
* 2) 'each' iterates an array or object. It provides '@index'(array), '@key'(object), and '@this'(current element).
304+
* 3) 'with ... as ...' provides an alias.
305+
* @param {string} text - text with expressions
306+
* @param {object} context - context
307+
* @return {string} - text that bind with its context
308+
* @memberof module:domUtil
309+
* @example
310+
* var template = require('tui-code-snippet/domUtil/template');
311+
*
312+
* var source =
313+
* '<h1>'
314+
* + '{{if isValidNumber title}}'
315+
* + '{{title}}th'
316+
* + '{{elseif isValidDate title}}'
317+
* + 'Date: {{title}}'
318+
* + '{{/if}}'
319+
* + '</h1>'
320+
* + '{{each list}}'
321+
* + '{{with addOne @index as idx}}'
322+
* + '<p>{{idx}}: {{@this}}</p>'
323+
* + '{{/with}}'
324+
* + '{{/each}}';
325+
*
326+
* var context = {
327+
* isValidDate: function(text) {
328+
* return /^\d{4}-(0|1)\d-(0|1|2|3)\d$/.test(text);
329+
* },
330+
* isValidNumber: function(text) {
331+
* return /^\d+$/.test(text);
332+
* }
333+
* title: '2019-11-25',
334+
* list: ['Clean the room', 'Wash the dishes'],
335+
* addOne: function(num) {
336+
* return num + 1;
337+
* }
338+
* };
339+
*
340+
* var result = template(source, context);
341+
* console.log(result); // <h1>Date: 2019-11-25</h1><p>1: Clean the room</p><p>2: Wash the dishes</p>
342+
*/
343+
function template(text, context) {
344+
text = text.replace(/\n\s*/g, '');
345+
346+
return compile(text.split(EXPRESSION_REGEXP), context);
347+
}
348+
349+
module.exports = template;

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ require('./domUtil/removeClass');
3737
require('./domUtil/removeData');
3838
require('./domUtil/removeElement');
3939
require('./domUtil/setData');
40+
require('./domUtil/template');
4041
require('./domUtil/toggleClass');
4142
require('./enum/enum');
4243
require('./formatDate/formatDate');

0 commit comments

Comments
 (0)