Spaces:
Runtime error
Runtime error
; | |
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)); | |
}; | |
} | |