| '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)); |
| }; |
| } |