|
|
'use strict';
const colors = require('ansi-colors'); const clean = (str = '') => { return typeof str === 'string' ? str.replace(/^['"]|['"]$/g, '') : ''; };
/** * This file contains the interpolation and rendering logic for * the Snippet prompt. */
class Item { constructor(token) { this.name = token.key; this.field = token.field || {}; this.value = clean(token.initial || this.field.initial || ''); this.message = token.message || this.name; this.cursor = 0; this.input = ''; this.lines = []; } }
const tokenize = async(options = {}, defaults = {}, fn = token => token) => { let unique = new Set(); let fields = options.fields || []; let input = options.template; let tabstops = []; let items = []; let keys = []; let line = 1;
if (typeof input === 'function') { input = await input(); }
let i = -1; let next = () => input[++i]; let peek = () => input[i + 1]; let push = token => { token.line = line; tabstops.push(token); };
push({ type: 'bos', value: '' });
while (i < input.length - 1) { let value = next();
if (/^[^\S\n ]$/.test(value)) { push({ type: 'text', value }); continue; }
if (value === '\n') { push({ type: 'newline', value }); line++; continue; }
if (value === '\\') { value += next(); push({ type: 'text', value }); continue; }
if ((value === '$' || value === '#' || value === '{') && peek() === '{') { let n = next(); value += n;
let token = { type: 'template', open: value, inner: '', close: '', value }; let ch;
while ((ch = next())) { if (ch === '}') { if (peek() === '}') ch += next(); token.value += ch; token.close = ch; break; }
if (ch === ':') { token.initial = ''; token.key = token.inner; } else if (token.initial !== void 0) { token.initial += ch; }
token.value += ch; token.inner += ch; }
token.template = token.open + (token.initial || token.inner) + token.close; token.key = token.key || token.inner;
if (defaults.hasOwnProperty(token.key)) { token.initial = defaults[token.key]; }
token = fn(token); push(token);
keys.push(token.key); unique.add(token.key);
let item = items.find(item => item.name === token.key); token.field = fields.find(ch => ch.name === token.key);
if (!item) { item = new Item(token); items.push(item); }
item.lines.push(token.line - 1); continue; }
let last = tabstops[tabstops.length - 1]; if (last.type === 'text' && last.line === line) { last.value += value; } else { push({ type: 'text', value }); } }
push({ type: 'eos', value: '' }); return { input, tabstops, unique, keys, items }; };
module.exports = async prompt => { let options = prompt.options; let required = new Set(options.required === true ? [] : (options.required || [])); let defaults = { ...options.values, ...options.initial }; let { tabstops, items, keys } = await tokenize(options, defaults);
let result = createFn('result', prompt, options); let format = createFn('format', prompt, options); let isValid = createFn('validate', prompt, options, true); let isVal = prompt.isValue.bind(prompt);
return async(state = {}, submitted = false) => { let index = 0;
state.required = required; state.items = items; state.keys = keys; state.output = '';
let validate = async(value, state, item, index) => { let error = await isValid(value, state, item, index); if (error === false) { return 'Invalid field ' + item.name; } return error; };
for (let token of tabstops) { let value = token.value; let key = token.key;
if (token.type !== 'template') { if (value) state.output += value; continue; }
if (token.type === 'template') { let item = items.find(ch => ch.name === key);
if (options.required === true) { state.required.add(item.name); }
let val = [item.input, state.values[item.value], item.value, value].find(isVal); let field = item.field || {}; let message = field.message || token.inner;
if (submitted) { let error = await validate(state.values[key], state, item, index); if ((error && typeof error === 'string') || error === false) { state.invalid.set(key, error); continue; }
state.invalid.delete(key); let res = await result(state.values[key], state, item, index); state.output += colors.unstyle(res); continue; }
item.placeholder = false;
let before = value; value = await format(value, state, item, index);
if (val !== value) { state.values[key] = val; value = prompt.styles.typing(val); state.missing.delete(message);
} else { state.values[key] = void 0; val = `<${message}>`; value = prompt.styles.primary(val); item.placeholder = true;
if (state.required.has(key)) { state.missing.add(message); } }
if (state.missing.has(message) && state.validating) { value = prompt.styles.warning(val); }
if (state.invalid.has(key) && state.validating) { value = prompt.styles.danger(val); }
if (index === state.index) { if (before !== value) { value = prompt.styles.underline(value); } else { value = prompt.styles.heading(colors.unstyle(value)); } }
index++; }
if (value) { state.output += value; } }
let lines = state.output.split('\n').map(l => ' ' + l); let len = items.length; let done = 0;
for (let item of items) { if (state.invalid.has(item.name)) { item.lines.forEach(i => { if (lines[i][0] !== ' ') return; lines[i] = state.styles.danger(state.symbols.bullet) + lines[i].slice(1); }); }
if (prompt.isValue(state.values[item.name])) { done++; } }
state.completed = ((done / len) * 100).toFixed(0); state.output = lines.join('\n'); return state.output; }; };
function createFn(prop, prompt, options, fallback) { return (value, state, item, index) => { if (typeof item.field[prop] === 'function') { return item.field[prop].call(prompt, value, state, item, index); } return [fallback, value].find(v => prompt.isValue(v)); }; }
|