UNPKG

convo

Version:

Easily create conversations (for more natural bots)

217 lines (181 loc) 4.56 kB
/** * Module Dependencies */ var strip_indent = require('strip-indent') var assign = require('object-assign') var is_regexp = require('is-regexp') var find = require('array-find') var parse = require('./parser') var lexer = require('./lexer') var clone = require('clone') /** * Export `convo` * * @param {String} language * @return {Function} */ module.exports = function convo (language) { language = strip_indent(language) var tokens = lexer(language) var ast = parse(tokens) return Convo(ast) } /** * Initialize a `Convo` * * @param {Array} ast * @return {Function} Say */ function Convo (ast) { return function Say (context) { var response = null var choices = [] switch (typeof context) { case 'string': var outgoing = goto_id(clone(ast), context) choices = outgoing ? outgoing.choices : null response = outgoing break case 'object': choices = clone(ast); response = null break default: choices = clone(ast) response = null break } var ids = {} function say (incoming) { var is_params = typeof incoming === 'object' if (!arguments.length || is_params) { return Question(response)(incoming) } // Find the choice that matches the incoming statement var choice = find(choices, function (choice) { return is_regexp(choice.incoming) ? choice.incoming.test(incoming) : choice.incoming === incoming }) // ignore the choice if it doesn't match if (!choice) return Noop() // cache ref for later if (choice.outgoing.id) { ids[choice.outgoing.id] = choice } // Update the set of choices if (choice.outgoing.goto) { var next_choice = ids[choice.outgoing.goto] || (ids[choice.outgoing.goto] = find_id(ast, choice.outgoing.goto) || {}) choices = next_choice.outgoing.choices response = next_choice.outgoing } else if (choice.outgoing.reset) { choices = clone(ast) response = null } else { choices = choice.outgoing.choices || [] response = choice.outgoing || null } // Create a response function if (choice.outgoing.response) { return Template(choice, incoming) } else if (choice.outgoing.goto) { return Template(ids[choice.outgoing.goto], incoming) } else { return Noop() } } say.toJSON = function () { return clone(choices) } say.toString = function () { return response ? response.id : null } return say } } /** * Create a template * * @param {Object} choice * @param {String} incoming * @return {Function} */ function Template (choice, incoming) { var response = choice.outgoing.response var alias = choice.alias || choice.outgoing.response var params = {} if (is_regexp(choice.incoming)) { var m = incoming.match(choice.incoming) for (var i = 0, len = m.length; i < len; i++) { params[i] = m[i] } } function template (obj) { obj = assign(params, obj || {}) return new Function('_', 'return `' + response + '`')(obj || {}) } // return the alias template.toString = function () { return alias } // attach the params in case we need them template.params = params return template } /** * Create a Question * * @param {Object} outgoing * @return {Function} */ function Question (outgoing) { function template (obj) { return new Function('_', 'return `' + outgoing.response + '`')(obj || {}) } // return the id template.toString = function () { return outgoing.id } return template } /** * Noop */ function Noop () { function noop () {} noop.toString = function () { return null } return noop } /** * Find an ID in the AST * * @param {Array} choices * @param {String} id * @return {Object} */ function find_id (choices, id) { for (var i = 0, choice; choice = choices[i]; i++) { if (choice.outgoing.id && choice.outgoing.id === id) { return choice } else if (choice.outgoing.choices && choice.outgoing.choices.length) { var res = find_id(choice.outgoing.choices, id) if (res) return res } } return null } /** * Go to a particular ID * * @param {Array} choices * @param {String} id * @return {Array} */ function goto_id (choices, id) { var choice = find_id(choices, id) if (!choice || !choice.outgoing) return null return choice.outgoing }