3 const colors = require('ansi-colors');
4 const clean = (str = '') => {
5 return typeof str === 'string' ? str.replace(/^['"]|['"]$/g, '') : '';
9 * This file contains the interpolation and rendering logic for
15 this.name = token.key;
16 this.field = token.field || {};
17 this.value = clean(token.initial || this.field.initial || '');
18 this.message = token.message || this.name;
25 const tokenize = async(options = {}, defaults = {}, fn = token => token) => {
26 let unique = new Set();
27 let fields = options.fields || [];
28 let input = options.template;
34 if (typeof input === 'function') {
35 input = await input();
39 let next = () => input[++i];
40 let peek = () => input[i + 1];
46 push({ type: 'bos', value: '' });
48 while (i < input.length - 1) {
51 if (/^[^\S\n ]$/.test(value)) {
52 push({ type: 'text', value });
57 push({ type: 'newline', value });
64 push({ type: 'text', value });
68 if ((value === '$' || value === '#' || value === '{') && peek() === '{') {
72 let token = { type: 'template', open: value, inner: '', close: '', value };
75 while ((ch = next())) {
77 if (peek() === '}') ch += next();
85 token.key = token.inner;
86 } else if (token.initial !== void 0) {
94 token.template = token.open + (token.initial || token.inner) + token.close;
95 token.key = token.key || token.inner;
97 if (defaults.hasOwnProperty(token.key)) {
98 token.initial = defaults[token.key];
104 keys.push(token.key);
105 unique.add(token.key);
107 let item = items.find(item => item.name === token.key);
108 token.field = fields.find(ch => ch.name === token.key);
111 item = new Item(token);
115 item.lines.push(token.line - 1);
119 let last = tabstops[tabstops.length - 1];
120 if (last.type === 'text' && last.line === line) {
123 push({ type: 'text', value });
127 push({ type: 'eos', value: '' });
128 return { input, tabstops, unique, keys, items };
131 module.exports = async prompt => {
132 let options = prompt.options;
133 let required = new Set(options.required === true ? [] : (options.required || []));
134 let defaults = { ...options.values, ...options.initial };
135 let { tabstops, items, keys } = await tokenize(options, defaults);
137 let result = createFn('result', prompt, options);
138 let format = createFn('format', prompt, options);
139 let isValid = createFn('validate', prompt, options, true);
140 let isVal = prompt.isValue.bind(prompt);
142 return async(state = {}, submitted = false) => {
145 state.required = required;
150 let validate = async(value, state, item, index) => {
151 let error = await isValid(value, state, item, index);
152 if (error === false) {
153 return 'Invalid field ' + item.name;
158 for (let token of tabstops) {
159 let value = token.value;
162 if (token.type !== 'template') {
163 if (value) state.output += value;
167 if (token.type === 'template') {
168 let item = items.find(ch => ch.name === key);
170 if (options.required === true) {
171 state.required.add(item.name);
174 let val = [item.input, state.values[item.value], item.value, value].find(isVal);
175 let field = item.field || {};
176 let message = field.message || token.inner;
179 let error = await validate(state.values[key], state, item, index);
180 if ((error && typeof error === 'string') || error === false) {
181 state.invalid.set(key, error);
185 state.invalid.delete(key);
186 let res = await result(state.values[key], state, item, index);
187 state.output += colors.unstyle(res);
191 item.placeholder = false;
194 value = await format(value, state, item, index);
197 state.values[key] = val;
198 value = prompt.styles.typing(val);
199 state.missing.delete(message);
202 state.values[key] = void 0;
203 val = `<${message}>`;
204 value = prompt.styles.primary(val);
205 item.placeholder = true;
207 if (state.required.has(key)) {
208 state.missing.add(message);
212 if (state.missing.has(message) && state.validating) {
213 value = prompt.styles.warning(val);
216 if (state.invalid.has(key) && state.validating) {
217 value = prompt.styles.danger(val);
220 if (index === state.index) {
221 if (before !== value) {
222 value = prompt.styles.underline(value);
224 value = prompt.styles.heading(colors.unstyle(value));
232 state.output += value;
236 let lines = state.output.split('\n').map(l => ' ' + l);
237 let len = items.length;
240 for (let item of items) {
241 if (state.invalid.has(item.name)) {
242 item.lines.forEach(i => {
243 if (lines[i][0] !== ' ') return;
244 lines[i] = state.styles.danger(state.symbols.bullet) + lines[i].slice(1);
248 if (prompt.isValue(state.values[item.name])) {
253 state.completed = ((done / len) * 100).toFixed(0);
254 state.output = lines.join('\n');
259 function createFn(prop, prompt, options, fallback) {
260 return (value, state, item, index) => {
261 if (typeof item.field[prop] === 'function') {
262 return item.field[prop].call(prompt, value, state, item, index);
264 return [fallback, value].find(v => prompt.isValue(v));