UNPKG

11 kBJavaScriptView Raw
1"use strict";
2/**
3 * @license
4 * Copyright Google LLC All Rights Reserved.
5 *
6 * Use of this source code is governed by an MIT-style license that can be
7 * found in the LICENSE file at https://angular.io/license
8 */
9Object.defineProperty(exports, "__esModule", { value: true });
10exports.template = exports.templateParser = void 0;
11const source_map_1 = require("source-map");
12// Matches <%= expr %>. This does not support structural JavaScript (for/if/...).
13const kInterpolateRe = /<%=([\s\S]+?)%>/g;
14// Matches <%# text %>. It's a comment and will be entirely ignored.
15const kCommentRe = /<%#([\s\S]+?)%>/g;
16// Used to match template delimiters.
17// <%- expr %>: HTML escape the value.
18// <% ... %>: Structural template code.
19const kEscapeRe = /<%-([\s\S]+?)%>/g;
20const kEvaluateRe = /<%([\s\S]+?)%>/g;
21/** Used to map characters to HTML entities. */
22const kHtmlEscapes = {
23 '&': '&amp;',
24 '<': '&lt;',
25 '>': '&gt;',
26 '"': '&quot;',
27 "'": '&#39;',
28 '`': '&#96;',
29};
30// Used to match HTML entities and HTML characters.
31const reUnescapedHtml = new RegExp(`[${Object.keys(kHtmlEscapes).join('')}]`, 'g');
32function _positionFor(content, offset) {
33 let line = 1;
34 let column = 0;
35 for (let i = 0; i < offset - 1; i++) {
36 if (content[i] == '\n') {
37 line++;
38 column = 0;
39 }
40 else {
41 column++;
42 }
43 }
44 return {
45 line,
46 column,
47 };
48}
49/**
50 * Given a source text (and a fileName), returns a TemplateAst.
51 */
52function templateParser(sourceText, fileName) {
53 const children = [];
54 // Compile the regexp to match each delimiter.
55 const reExpressions = [kEscapeRe, kCommentRe, kInterpolateRe, kEvaluateRe];
56 const reDelimiters = RegExp(reExpressions.map((x) => x.source).join('|') + '|$', 'g');
57 const parsed = sourceText.split(reDelimiters);
58 let offset = 0;
59 // Optimization that uses the fact that the end of a node is always the beginning of the next
60 // node, so we keep the positioning of the nodes in memory.
61 let start = _positionFor(sourceText, offset);
62 let end;
63 const increment = reExpressions.length + 1;
64 for (let i = 0; i < parsed.length; i += increment) {
65 const [content, escape, comment, interpolate, evaluate] = parsed.slice(i, i + increment);
66 if (content) {
67 end = _positionFor(sourceText, offset + content.length);
68 offset += content.length;
69 children.push({ kind: 'content', content, start, end });
70 start = end;
71 }
72 if (escape) {
73 end = _positionFor(sourceText, offset + escape.length + 5);
74 offset += escape.length + 5;
75 children.push({ kind: 'escape', expression: escape, start, end });
76 start = end;
77 }
78 if (comment) {
79 end = _positionFor(sourceText, offset + comment.length + 5);
80 offset += comment.length + 5;
81 children.push({ kind: 'comment', text: comment, start, end });
82 start = end;
83 }
84 if (interpolate) {
85 end = _positionFor(sourceText, offset + interpolate.length + 5);
86 offset += interpolate.length + 5;
87 children.push({
88 kind: 'interpolate',
89 expression: interpolate,
90 start,
91 end,
92 });
93 start = end;
94 }
95 if (evaluate) {
96 end = _positionFor(sourceText, offset + evaluate.length + 5);
97 offset += evaluate.length + 5;
98 children.push({ kind: 'evaluate', expression: evaluate, start, end });
99 start = end;
100 }
101 }
102 return {
103 fileName,
104 content: sourceText,
105 children,
106 };
107}
108exports.templateParser = templateParser;
109/**
110 * Fastest implementation of the templating algorithm. It only add strings and does not bother
111 * with source maps.
112 */
113function templateFast(ast, options) {
114 const module = options && options.module ? 'module.exports.default =' : '';
115 const reHtmlEscape = reUnescapedHtml.source.replace(/[']/g, "\\\\\\'");
116 return `
117 return ${module} function(obj) {
118 obj || (obj = {});
119 let __t;
120 let __p = '';
121 const __escapes = ${JSON.stringify(kHtmlEscapes)};
122 const __escapesre = new RegExp('${reHtmlEscape}', 'g');
123
124 const __e = function(s) {
125 return s ? s.replace(__escapesre, function(key) { return __escapes[key]; }) : '';
126 };
127 with (obj) {
128 ${ast.children
129 .map((node) => {
130 switch (node.kind) {
131 case 'content':
132 return `__p += ${JSON.stringify(node.content)};`;
133 case 'interpolate':
134 return `__p += ((__t = (${node.expression})) == null) ? '' : __t;`;
135 case 'escape':
136 return `__p += __e(${node.expression});`;
137 case 'evaluate':
138 return node.expression;
139 }
140 })
141 .join('\n')}
142 }
143
144 return __p;
145 };
146 `;
147}
148/**
149 * Templating algorithm with source map support. The map is outputted as //# sourceMapUrl=...
150 */
151function templateWithSourceMap(ast, options) {
152 const sourceUrl = ast.fileName;
153 const module = options && options.module ? 'module.exports.default =' : '';
154 const reHtmlEscape = reUnescapedHtml.source.replace(/[']/g, "\\\\\\'");
155 const preamble = new source_map_1.SourceNode(1, 0, sourceUrl, '').add(new source_map_1.SourceNode(1, 0, sourceUrl, [
156 `return ${module} function(obj) {\n`,
157 ' obj || (obj = {});\n',
158 ' let __t;\n',
159 ' let __p = "";\n',
160 ` const __escapes = ${JSON.stringify(kHtmlEscapes)};\n`,
161 ` const __escapesre = new RegExp('${reHtmlEscape}', 'g');\n`,
162 `\n`,
163 ` const __e = function(s) { `,
164 ` return s ? s.replace(__escapesre, function(key) { return __escapes[key]; }) : '';`,
165 ` };\n`,
166 ` with (obj) {\n`,
167 ]));
168 const end = ast.children.length
169 ? ast.children[ast.children.length - 1].end
170 : { line: 0, column: 0 };
171 const nodes = ast.children
172 .reduce((chunk, node) => {
173 let code = '';
174 switch (node.kind) {
175 case 'content':
176 code = [
177 new source_map_1.SourceNode(node.start.line, node.start.column, sourceUrl, '__p = __p'),
178 ...node.content.split('\n').map((line, i, arr) => {
179 return new source_map_1.SourceNode(node.start.line + i, i == 0 ? node.start.column : 0, sourceUrl, '\n + ' + JSON.stringify(line + (i == arr.length - 1 ? '' : '\n')));
180 }),
181 new source_map_1.SourceNode(node.end.line, node.end.column, sourceUrl, ';\n'),
182 ];
183 break;
184 case 'interpolate':
185 code = [
186 new source_map_1.SourceNode(node.start.line, node.start.column, sourceUrl, '__p += ((__t = '),
187 ...node.expression.split('\n').map((line, i, arr) => {
188 return new source_map_1.SourceNode(node.start.line + i, i == 0 ? node.start.column : 0, sourceUrl, line + (i == arr.length - 1 ? '' : '\n'));
189 }),
190 new source_map_1.SourceNode(node.end.line, node.end.column, sourceUrl, ') == null ? "" : __t);\n'),
191 ];
192 break;
193 case 'escape':
194 code = [
195 new source_map_1.SourceNode(node.start.line, node.start.column, sourceUrl, '__p += __e('),
196 ...node.expression.split('\n').map((line, i, arr) => {
197 return new source_map_1.SourceNode(node.start.line + i, i == 0 ? node.start.column : 0, sourceUrl, line + (i == arr.length - 1 ? '' : '\n'));
198 }),
199 new source_map_1.SourceNode(node.end.line, node.end.column, sourceUrl, ');\n'),
200 ];
201 break;
202 case 'evaluate':
203 code = [
204 ...node.expression.split('\n').map((line, i, arr) => {
205 return new source_map_1.SourceNode(node.start.line + i, i == 0 ? node.start.column : 0, sourceUrl, line + (i == arr.length - 1 ? '' : '\n'));
206 }),
207 new source_map_1.SourceNode(node.end.line, node.end.column, sourceUrl, '\n'),
208 ];
209 break;
210 }
211 return chunk.add(new source_map_1.SourceNode(node.start.line, node.start.column, sourceUrl, code));
212 }, preamble)
213 .add(new source_map_1.SourceNode(end.line, end.column, sourceUrl, [' };\n', '\n', ' return __p;\n', '}\n']));
214 const code = nodes.toStringWithSourceMap({
215 file: sourceUrl,
216 sourceRoot: (options && options.sourceRoot) || '.',
217 });
218 // Set the source content in the source map, otherwise the sourceUrl is not enough
219 // to find the content.
220 code.map.setSourceContent(sourceUrl, ast.content);
221 return (code.code +
222 '\n//# sourceMappingURL=data:application/json;base64,' +
223 Buffer.from(code.map.toString()).toString('base64'));
224}
225/**
226 * An equivalent of EJS templates, which is based on John Resig's `tmpl` implementation
227 * (http://ejohn.org/blog/javascript-micro-templating/) and Laura Doktorova's doT.js
228 * (https://github.com/olado/doT).
229 *
230 * This version differs from lodash by removing support from ES6 quasi-literals, and making the
231 * code slightly simpler to follow. It also does not depend on any third party, which is nice.
232 *
233 * Finally, it supports SourceMap, if you ever need to debug, which is super nice.
234 *
235 * @param content The template content.
236 * @param options Optional Options. See TemplateOptions for more description.
237 * @return {(input: T) => string} A function that accept an input object and returns the content
238 * of the template with the input applied.
239 */
240function template(content, options) {
241 const sourceUrl = (options && options.sourceURL) || 'ejs';
242 const ast = templateParser(content, sourceUrl);
243 let source;
244 // If there's no need for source map support, we revert back to the fast implementation.
245 if (options && options.sourceMap) {
246 source = templateWithSourceMap(ast, options);
247 }
248 else {
249 source = templateFast(ast, options);
250 }
251 // We pass a dummy module in case the module option is passed. If `module: true` is passed, we
252 // need to only use the source, not the function itself. Otherwise expect a module object to be
253 // passed, and we use that one.
254 const fn = Function('module', source);
255 const module = options && options.module ? (options.module === true ? { exports: {} } : options.module) : null;
256 const result = fn(module);
257 // Provide the compiled function's source by its `toString` method or
258 // the `source` property as a convenience for inlining compiled templates.
259 result.source = source;
260 return result;
261}
262exports.template = template;