convo
Version:
Easily create conversations (for more natural bots)
217 lines (181 loc) • 4.56 kB
JavaScript
/**
* 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
}