You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

266 lines
6.6 KiB

4 years ago
  1. 'use strict';
  2. const colors = require('ansi-colors');
  3. const clean = (str = '') => {
  4. return typeof str === 'string' ? str.replace(/^['"]|['"]$/g, '') : '';
  5. };
  6. /**
  7. * This file contains the interpolation and rendering logic for
  8. * the Snippet prompt.
  9. */
  10. class Item {
  11. constructor(token) {
  12. this.name = token.key;
  13. this.field = token.field || {};
  14. this.value = clean(token.initial || this.field.initial || '');
  15. this.message = token.message || this.name;
  16. this.cursor = 0;
  17. this.input = '';
  18. this.lines = [];
  19. }
  20. }
  21. const tokenize = async(options = {}, defaults = {}, fn = token => token) => {
  22. let unique = new Set();
  23. let fields = options.fields || [];
  24. let input = options.template;
  25. let tabstops = [];
  26. let items = [];
  27. let keys = [];
  28. let line = 1;
  29. if (typeof input === 'function') {
  30. input = await input();
  31. }
  32. let i = -1;
  33. let next = () => input[++i];
  34. let peek = () => input[i + 1];
  35. let push = token => {
  36. token.line = line;
  37. tabstops.push(token);
  38. };
  39. push({ type: 'bos', value: '' });
  40. while (i < input.length - 1) {
  41. let value = next();
  42. if (/^[^\S\n ]$/.test(value)) {
  43. push({ type: 'text', value });
  44. continue;
  45. }
  46. if (value === '\n') {
  47. push({ type: 'newline', value });
  48. line++;
  49. continue;
  50. }
  51. if (value === '\\') {
  52. value += next();
  53. push({ type: 'text', value });
  54. continue;
  55. }
  56. if ((value === '$' || value === '#' || value === '{') && peek() === '{') {
  57. let n = next();
  58. value += n;
  59. let token = { type: 'template', open: value, inner: '', close: '', value };
  60. let ch;
  61. while ((ch = next())) {
  62. if (ch === '}') {
  63. if (peek() === '}') ch += next();
  64. token.value += ch;
  65. token.close = ch;
  66. break;
  67. }
  68. if (ch === ':') {
  69. token.initial = '';
  70. token.key = token.inner;
  71. } else if (token.initial !== void 0) {
  72. token.initial += ch;
  73. }
  74. token.value += ch;
  75. token.inner += ch;
  76. }
  77. token.template = token.open + (token.initial || token.inner) + token.close;
  78. token.key = token.key || token.inner;
  79. if (defaults.hasOwnProperty(token.key)) {
  80. token.initial = defaults[token.key];
  81. }
  82. token = fn(token);
  83. push(token);
  84. keys.push(token.key);
  85. unique.add(token.key);
  86. let item = items.find(item => item.name === token.key);
  87. token.field = fields.find(ch => ch.name === token.key);
  88. if (!item) {
  89. item = new Item(token);
  90. items.push(item);
  91. }
  92. item.lines.push(token.line - 1);
  93. continue;
  94. }
  95. let last = tabstops[tabstops.length - 1];
  96. if (last.type === 'text' && last.line === line) {
  97. last.value += value;
  98. } else {
  99. push({ type: 'text', value });
  100. }
  101. }
  102. push({ type: 'eos', value: '' });
  103. return { input, tabstops, unique, keys, items };
  104. };
  105. module.exports = async prompt => {
  106. let options = prompt.options;
  107. let required = new Set(options.required === true ? [] : (options.required || []));
  108. let defaults = { ...options.values, ...options.initial };
  109. let { tabstops, items, keys } = await tokenize(options, defaults);
  110. let result = createFn('result', prompt, options);
  111. let format = createFn('format', prompt, options);
  112. let isValid = createFn('validate', prompt, options, true);
  113. let isVal = prompt.isValue.bind(prompt);
  114. return async(state = {}, submitted = false) => {
  115. let index = 0;
  116. state.required = required;
  117. state.items = items;
  118. state.keys = keys;
  119. state.output = '';
  120. let validate = async(value, state, item, index) => {
  121. let error = await isValid(value, state, item, index);
  122. if (error === false) {
  123. return 'Invalid field ' + item.name;
  124. }
  125. return error;
  126. };
  127. for (let token of tabstops) {
  128. let value = token.value;
  129. let key = token.key;
  130. if (token.type !== 'template') {
  131. if (value) state.output += value;
  132. continue;
  133. }
  134. if (token.type === 'template') {
  135. let item = items.find(ch => ch.name === key);
  136. if (options.required === true) {
  137. state.required.add(item.name);
  138. }
  139. let val = [item.input, state.values[item.value], item.value, value].find(isVal);
  140. let field = item.field || {};
  141. let message = field.message || token.inner;
  142. if (submitted) {
  143. let error = await validate(state.values[key], state, item, index);
  144. if ((error && typeof error === 'string') || error === false) {
  145. state.invalid.set(key, error);
  146. continue;
  147. }
  148. state.invalid.delete(key);
  149. let res = await result(state.values[key], state, item, index);
  150. state.output += colors.unstyle(res);
  151. continue;
  152. }
  153. item.placeholder = false;
  154. let before = value;
  155. value = await format(value, state, item, index);
  156. if (val !== value) {
  157. state.values[key] = val;
  158. value = prompt.styles.typing(val);
  159. state.missing.delete(message);
  160. } else {
  161. state.values[key] = void 0;
  162. val = `<${message}>`;
  163. value = prompt.styles.primary(val);
  164. item.placeholder = true;
  165. if (state.required.has(key)) {
  166. state.missing.add(message);
  167. }
  168. }
  169. if (state.missing.has(message) && state.validating) {
  170. value = prompt.styles.warning(val);
  171. }
  172. if (state.invalid.has(key) && state.validating) {
  173. value = prompt.styles.danger(val);
  174. }
  175. if (index === state.index) {
  176. if (before !== value) {
  177. value = prompt.styles.underline(value);
  178. } else {
  179. value = prompt.styles.heading(colors.unstyle(value));
  180. }
  181. }
  182. index++;
  183. }
  184. if (value) {
  185. state.output += value;
  186. }
  187. }
  188. let lines = state.output.split('\n').map(l => ' ' + l);
  189. let len = items.length;
  190. let done = 0;
  191. for (let item of items) {
  192. if (state.invalid.has(item.name)) {
  193. item.lines.forEach(i => {
  194. if (lines[i][0] !== ' ') return;
  195. lines[i] = state.styles.danger(state.symbols.bullet) + lines[i].slice(1);
  196. });
  197. }
  198. if (prompt.isValue(state.values[item.name])) {
  199. done++;
  200. }
  201. }
  202. state.completed = ((done / len) * 100).toFixed(0);
  203. state.output = lines.join('\n');
  204. return state.output;
  205. };
  206. };
  207. function createFn(prop, prompt, options, fallback) {
  208. return (value, state, item, index) => {
  209. if (typeof item.field[prop] === 'function') {
  210. return item.field[prop].call(prompt, value, state, item, index);
  211. }
  212. return [fallback, value].find(v => prompt.isValue(v));
  213. };
  214. }