cli-kit
Version:
Everything you need to create awesome command line interfaces
146 lines (128 loc) • 4.17 kB
JavaScript
// import debug from '../lib/debug';
import E from '../lib/errors.js';
import fs from 'fs';
import { trim, trimEnd } from '../lib/ansi.js';
// const logger = debug('cli-kit:template:in');
// const { log } = logger;
// const log2 = logger('out').log;
/**
* Matches intentional line breaks in multiline strings.
*
* @type {RegExp}
*/
const breakRegExp = /[ \t]?\\\n/g;
/**
* Finds output statements and formats them into print statements.
*
* Regex breakdown:
* `(?<=^|\n)([ \t]*)(>+)`: Find one or more contiguous `>` characters where they are at the
* beginning of template or line. We capture each `>` so that we can
* determine how many line returns to add after the line is printed.
* `(\|\?|\?\||\||\?)?`: Detect modifier flags. These control rendering such as trimming the
* output.
* `(.*?)(?:(?<!\\)\n|$)`: Capture the entire message, including multiline `\` tokens, up to the
* first line break.
* `/gs`: Set the `global` and `dot all` flags. `global` will find all matches.
* `dot all` (introduced in ES2018), allows us to capture intentional
* line breaks.
*
* @type {RegExp}
*/
let printRegExp;
/**
* Escapes tildes in a string that is to be evaluated as a template literal. It uses a simple state
* machine to keep track of whether it's in an expression or template literal.
*
* @param {String} str - The string to escape.
* @returns {String}
*/
export function escapeTildes(str) {
let state = [ 0 ];
let s = '';
for (let i = 0, l = str.length; i < l; i++) {
switch (state[0]) {
case 0: // not in an expression
if ((i === 0 || str[i - 1] !== '\\') && str[i] === '$' && str[i + 1] === '{') {
s += str[i++]; // $
s += str[i]; // {
state.unshift(1);
} else if (str[i] === '`') {
s += '\\`';
} else {
s += str[i];
}
break;
case 1: // in an expression
if (str[i] === '}') {
state.shift();
} else if (str[i] === '`') {
state.unshift(2);
}
s += str[i];
break;
case 2: // in template literal
if (str[i] === '`') {
state.shift();
}
s += str[i];
break;
}
}
return s;
}
/**
* Renders a template with the supplied data.
*
* @param {String} template - The template to render.
* @param {Object} [data] - An object to inject into the template.
* @returns {String}
*/
export function render(template, data) {
if (!printRegExp) {
try {
printRegExp = new RegExp('(?<=^|\\n)([ \\t]*)(>+)(\\|\\?|\\?\\||\\||\\?)?(.*?)(?:(?<!\\\\)\\n|$)', 'gs');
} catch (e) {
// istanbul ignore next
throw E.INVALID_NODE_JS('Node.js version is too old; must be v8.10 or newer');
}
}
if (!data || typeof data !== 'object') {
data = {};
}
// log(template);
// log(Object.keys(data));
const vars = Object.keys(data);
let body = (vars.length ? `let { ${vars.join(', ')} } = __data;\n\n` : '') +
template.replace(printRegExp, (_, ws, lines, flags, str) => {
str = str.replace(/\\(?!\n)/g, '\\\\');
str = escapeTildes(str);
str = str.replace(breakRegExp, '\\n');
return `${ws}__print(\`${str}\`, ${lines.length - 1}${flags === undefined ? '' : `, '${flags}'`});\n`;
});
// log2(body);
const fn = new Function('__data', '__print', body);
let output = '';
fn(data, (str, linebreaks, flags) => {
str = flags?.includes('|') ? trimEnd(str) : trim(str);
if (!flags?.includes('?') || str) {
output += `${str}${linebreaks ? '\n'.repeat(linebreaks) : ''}`;
}
});
return output.replace(/(\r\n|\r|\n)+$/g, '\n');
}
/**
* Reads in a template file and renders it.
*
* @param {String} file - The path to the template file.
* @param {Object} [data] - An object to inject into the template.
* @returns {String}
*/
export function renderFile(file, data) {
let template;
try {
template = fs.readFileSync(file, 'utf8');
} catch (e) {
throw E.TEMPLATE_NOT_FOUND(`Unable to find template: ${file}`, { name: 'file', scope: 'template.renderFile', value: file });
}
return render(template, data);
}