UNPKG

6.47 kBJavaScriptView Raw
1var T_STR = 1; // Stands for a normal string.
2var T_EXP = 2; // Stands for an expression.
3
4/**
5 * A simple template function
6 *
7 * @example
8 * // Rreturns '/post/1'
9 * template('/post/{ post.id }', { post: { id: 1 } })
10 *
11 * @param {string} template The template text.
12 * @param {Object.<string, *>} data The data object.
13 * @param {TemplateOptions} options The template options.
14 * @returns {string} Returns the compiled text.
15 */
16function template(template, data, options) {
17 var templ = (template === null || template === undefined) ? '' : (template + '');
18 var model = data || {};
19 var opts = options || {};
20 var openingTag = opts.openingTag || '{';
21 var closingTag = opts.closingTag || '}';
22 var encode = opts.encode || encodeURIComponent;
23 var result = parse(templ, openingTag, closingTag, function (exp) {
24 var first = exp.charAt(0);
25 var second = exp.charAt(1);
26 var raw = false;
27
28 if (first === '-' && second === ' ') {
29 raw = true;
30 exp = exp.substr(2);
31 }
32
33 exp = exp.replace(/^\s+|\s+$/g, '');
34
35 return {
36 type: T_EXP,
37 text: exp,
38 raw: raw
39 };
40 });
41
42 var render = compile(result, encode);
43
44 try {
45 return render(model);
46 } catch (e) {
47 throw new Error('Compile Error:\n\n' + template + '\n\n' + e.message);
48 }
49}
50
51/**
52 * Compile the result of `parse` to a function.
53 *
54 * @param {Object.<string, *>[]} result The result of `parse`.
55 * @param {(str: string) => string} encode The function to encode the string.
56 * @returns {(model: Object.<string, *>) => string} Returns a function that compile data to string.
57 */
58function compile(result, encode) {
59 var fn;
60 var line;
61 var lines = [];
62 var i = 0;
63 var l = result.length;
64
65 lines.push('var __o=[]');
66 lines.push('with(__s){');
67
68 for ( ; i < l; ++i) {
69 line = result[i];
70
71 if (line.type === T_STR) {
72 lines.push('__o.push(' + JSON.stringify(line.text) + ')');
73 } else if (line.type === T_EXP && line.text) {
74 if (line.raw) {
75 lines.push('__o.push(' + line.text + ')');
76 } else {
77 lines.push('__o.push(__e(' + line.text + '))');
78 }
79 }
80 }
81
82 lines.push('}');
83 lines.push('return __o.join("")');
84
85 fn = new Function('__s', '__e', lines.join('\n'));
86
87 return function (model) {
88 return fn(model, function (val) {
89 return (val === null || val === undefined) ? '' : encode(val + '');
90 });
91 };
92}
93
94/**
95 * The function to parse the template string.
96 *
97 * @param {string} template The template string to parse.
98 * @param {string} openingTag The opening tag, for example `{`.
99 * @param {string} closingTag The closing tag, for example `}`.
100 * @param {(exp: string) => Object.<string, *>} handleExp The function to handle each expression.
101 * @returns {Object.<string, *>[]} Returns the parsed result.
102 */
103function parse(template, openingTag, closingTag, handleExp) {
104 var res;
105 var templ = template;
106 var regOpeningTag = createRegExp(openingTag);
107 var regClosingTag = createRegExp(closingTag);
108 var ERR_UNEXPECTED_END = 'Unexpected end';
109 var type = T_STR;
110 var strCache = [];
111 var expCache = [];
112 var output = [];
113
114 /**
115 * Create a `RegExp` for the given tag.
116 *
117 * @param {string} tag The tag to create a `RegExp`.
118 * @returns {RegExp} Returns an instance of `RegExp`.
119 */
120 function createRegExp(tag) {
121 var regChars = /[\\|{}()[\].*+?^$]/g;
122 var escapedTag = tag.replace(regChars, function (char) {
123 return '\\' + char;
124 });
125 return new RegExp('(\\\\*)' + escapedTag);
126 }
127
128 /**
129 * Flush the text in `strCache` into `output` and reset `strCache`.
130 */
131 function flushStr() {
132 output.push({
133 type: T_STR,
134 text: strCache.join('')
135 });
136 strCache = [];
137 }
138
139 /**
140 * Flush the text in `expCache` into `output` and reset `expCache`.
141 */
142 function flushExp() {
143 output.push(handleExp(expCache.join('')));
144 expCache = [];
145 }
146
147 /**
148 * Check whether the tag is escaped. If it is, put is to the cache.
149 *
150 * @param {Object.<string, *>} res The result of `RegExp#exec`.
151 * @param {string} tag The tag to escape.
152 * @param {string[]} cache The array to save escaped text.
153 * @returns {boolean} Returns `true` on it is NOT escaped.
154 */
155 function esc(res, tag, cache) {
156 var slashes = res[1] || '';
157 var count = slashes.length;
158
159 if (count % 2 === 0) {
160 if (count) {
161 cache.push(slashes.substr(count / 2));
162 }
163 return true;
164 } else {
165 if (count > 1) {
166 cache.push(slashes.substr((count + 1) / 2));
167 }
168 cache.push(tag);
169 return false;
170 }
171 }
172
173 while (templ.length) {
174 if (type === T_STR) {
175 res = regOpeningTag.exec(templ);
176 if (res) {
177 strCache.push(templ.substr(0, res.index));
178 templ = templ.substr(res.index + res[0].length);
179 if (esc(res, openingTag, strCache)) {
180 flushStr();
181 type = T_EXP;
182 if (!templ) {
183 throw new Error(ERR_UNEXPECTED_END);
184 }
185 }
186 } else {
187 strCache.push(templ);
188 flushStr();
189 templ = '';
190 }
191 } else { // if (type === T_EXP)
192 res = regClosingTag.exec(templ);
193 if (res) {
194 expCache.push(templ.substr(0, res.index));
195 templ = templ.substr(res.index + res[0].length);
196 if (esc(res, closingTag, expCache)) {
197 flushExp();
198 type = T_STR;
199 }
200 } else {
201 throw new Error(ERR_UNEXPECTED_END);
202 }
203 }
204 }
205
206 return output;
207}
208
209/**
210 * @typedef {Object.<string, *>} TemplateOptions
211 * @property {string} [openingTag] The opening tag of the template, default is `{`.
212 * @property {string} [closingTag] The closing tag of the template, default is `}`.
213 * @property {(value: string) => string} [encode] The function to encode the string, default is `encodeURIComponent`.
214 */
215
216module.exports = template;