@angular-devkit/core
Version:
Angular DevKit - Core Utility Library
262 lines (259 loc) • 10.9 kB
JavaScript
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.templateParser = templateParser;
exports.template = template;
const source_map_1 = require("source-map");
// Matches <%= expr %>. This does not support structural JavaScript (for/if/...).
const kInterpolateRe = /<%=([\s\S]+?)%>/g;
// Matches <%# text %>. It's a comment and will be entirely ignored.
const kCommentRe = /<%#([\s\S]+?)%>/g;
// Used to match template delimiters.
// <%- expr %>: HTML escape the value.
// <% ... %>: Structural template code.
const kEscapeRe = /<%-([\s\S]+?)%>/g;
const kEvaluateRe = /<%([\s\S]+?)%>/g;
/** Used to map characters to HTML entities. */
const kHtmlEscapes = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'`': '`',
};
// Used to match HTML entities and HTML characters.
const reUnescapedHtml = new RegExp(`[${Object.keys(kHtmlEscapes).join('')}]`, 'g');
function _positionFor(content, offset) {
let line = 1;
let column = 0;
for (let i = 0; i < offset - 1; i++) {
if (content[i] == '\n') {
line++;
column = 0;
}
else {
column++;
}
}
return {
line,
column,
};
}
/**
* Given a source text (and a fileName), returns a TemplateAst.
*/
function templateParser(sourceText, fileName) {
const children = [];
// Compile the regexp to match each delimiter.
const reExpressions = [kEscapeRe, kCommentRe, kInterpolateRe, kEvaluateRe];
const reDelimiters = RegExp(reExpressions.map((x) => x.source).join('|') + '|$', 'g');
const parsed = sourceText.split(reDelimiters);
let offset = 0;
// Optimization that uses the fact that the end of a node is always the beginning of the next
// node, so we keep the positioning of the nodes in memory.
let start = _positionFor(sourceText, offset);
let end;
const increment = reExpressions.length + 1;
for (let i = 0; i < parsed.length; i += increment) {
const [content, escape, comment, interpolate, evaluate] = parsed.slice(i, i + increment);
if (content) {
end = _positionFor(sourceText, offset + content.length);
offset += content.length;
children.push({ kind: 'content', content, start, end });
start = end;
}
if (escape) {
end = _positionFor(sourceText, offset + escape.length + 5);
offset += escape.length + 5;
children.push({ kind: 'escape', expression: escape, start, end });
start = end;
}
if (comment) {
end = _positionFor(sourceText, offset + comment.length + 5);
offset += comment.length + 5;
children.push({ kind: 'comment', text: comment, start, end });
start = end;
}
if (interpolate) {
end = _positionFor(sourceText, offset + interpolate.length + 5);
offset += interpolate.length + 5;
children.push({
kind: 'interpolate',
expression: interpolate,
start,
end,
});
start = end;
}
if (evaluate) {
end = _positionFor(sourceText, offset + evaluate.length + 5);
offset += evaluate.length + 5;
children.push({ kind: 'evaluate', expression: evaluate, start, end });
start = end;
}
}
return {
fileName,
content: sourceText,
children,
};
}
/**
* Fastest implementation of the templating algorithm. It only add strings and does not bother
* with source maps.
*/
function templateFast(ast, options) {
const module = options && options.module ? 'module.exports.default =' : '';
const reHtmlEscape = reUnescapedHtml.source.replace(/[']/g, "\\\\\\'");
return `
return ${module} function(obj) {
obj || (obj = {});
let __t;
let __p = '';
const __escapes = ${JSON.stringify(kHtmlEscapes)};
const __escapesre = new RegExp('${reHtmlEscape}', 'g');
const __e = function(s) {
return s ? s.replace(__escapesre, function(key) { return __escapes[key]; }) : '';
};
with (obj) {
${ast.children
.map((node) => {
switch (node.kind) {
case 'content':
return `__p += ${JSON.stringify(node.content)};`;
case 'interpolate':
return `__p += ((__t = (${node.expression})) == null) ? '' : __t;`;
case 'escape':
return `__p += __e(${node.expression});`;
case 'evaluate':
return node.expression;
}
})
.join('\n')}
}
return __p;
};
`;
}
/**
* Templating algorithm with source map support. The map is outputted as //# sourceMapUrl=...
*/
function templateWithSourceMap(ast, options) {
const sourceUrl = ast.fileName;
const module = options && options.module ? 'module.exports.default =' : '';
const reHtmlEscape = reUnescapedHtml.source.replace(/[']/g, "\\\\\\'");
const preamble = new source_map_1.SourceNode(1, 0, sourceUrl, '').add(new source_map_1.SourceNode(1, 0, sourceUrl, [
`return ${module} function(obj) {\n`,
' obj || (obj = {});\n',
' let __t;\n',
' let __p = "";\n',
` const __escapes = ${JSON.stringify(kHtmlEscapes)};\n`,
` const __escapesre = new RegExp('${reHtmlEscape}', 'g');\n`,
`\n`,
` const __e = function(s) { `,
` return s ? s.replace(__escapesre, function(key) { return __escapes[key]; }) : '';`,
` };\n`,
` with (obj) {\n`,
]));
const end = ast.children.length
? ast.children[ast.children.length - 1].end
: { line: 0, column: 0 };
const nodes = ast.children
.reduce((chunk, node) => {
let code = '';
switch (node.kind) {
case 'content':
code = [
new source_map_1.SourceNode(node.start.line, node.start.column, sourceUrl, '__p = __p'),
...node.content.split('\n').map((line, i, arr) => {
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')));
}),
new source_map_1.SourceNode(node.end.line, node.end.column, sourceUrl, ';\n'),
];
break;
case 'interpolate':
code = [
new source_map_1.SourceNode(node.start.line, node.start.column, sourceUrl, '__p += ((__t = '),
...node.expression.split('\n').map((line, i, arr) => {
return new source_map_1.SourceNode(node.start.line + i, i == 0 ? node.start.column : 0, sourceUrl, line + (i == arr.length - 1 ? '' : '\n'));
}),
new source_map_1.SourceNode(node.end.line, node.end.column, sourceUrl, ') == null ? "" : __t);\n'),
];
break;
case 'escape':
code = [
new source_map_1.SourceNode(node.start.line, node.start.column, sourceUrl, '__p += __e('),
...node.expression.split('\n').map((line, i, arr) => {
return new source_map_1.SourceNode(node.start.line + i, i == 0 ? node.start.column : 0, sourceUrl, line + (i == arr.length - 1 ? '' : '\n'));
}),
new source_map_1.SourceNode(node.end.line, node.end.column, sourceUrl, ');\n'),
];
break;
case 'evaluate':
code = [
...node.expression.split('\n').map((line, i, arr) => {
return new source_map_1.SourceNode(node.start.line + i, i == 0 ? node.start.column : 0, sourceUrl, line + (i == arr.length - 1 ? '' : '\n'));
}),
new source_map_1.SourceNode(node.end.line, node.end.column, sourceUrl, '\n'),
];
break;
}
return chunk.add(new source_map_1.SourceNode(node.start.line, node.start.column, sourceUrl, code));
}, preamble)
.add(new source_map_1.SourceNode(end.line, end.column, sourceUrl, [' };\n', '\n', ' return __p;\n', '}\n']));
const code = nodes.toStringWithSourceMap({
file: sourceUrl,
sourceRoot: (options && options.sourceRoot) || '.',
});
// Set the source content in the source map, otherwise the sourceUrl is not enough
// to find the content.
code.map.setSourceContent(sourceUrl, ast.content);
return (code.code +
'\n//# sourceMappingURL=data:application/json;base64,' +
Buffer.from(code.map.toString()).toString('base64'));
}
/**
* An equivalent of EJS templates, which is based on John Resig's `tmpl` implementation
* (http://ejohn.org/blog/javascript-micro-templating/) and Laura Doktorova's doT.js
* (https://github.com/olado/doT).
*
* This version differs from lodash by removing support from ES6 quasi-literals, and making the
* code slightly simpler to follow. It also does not depend on any third party, which is nice.
*
* Finally, it supports SourceMap, if you ever need to debug, which is super nice.
*
* @param content The template content.
* @param options Optional Options. See TemplateOptions for more description.
* @return {(input: T) => string} A function that accept an input object and returns the content
* of the template with the input applied.
*/
function template(content, options) {
const sourceUrl = (options && options.sourceURL) || 'ejs';
const ast = templateParser(content, sourceUrl);
let source;
// If there's no need for source map support, we revert back to the fast implementation.
if (options && options.sourceMap) {
source = templateWithSourceMap(ast, options);
}
else {
source = templateFast(ast, options);
}
// We pass a dummy module in case the module option is passed. If `module: true` is passed, we
// need to only use the source, not the function itself. Otherwise expect a module object to be
// passed, and we use that one.
const fn = Function('module', source);
const module = options && options.module ? (options.module === true ? { exports: {} } : options.module) : null;
const result = fn(module);
// Provide the compiled function's source by its `toString` method or
// the `source` property as a convenience for inlining compiled templates.
result.source = source;
return result;
}
;