Command-handling object

    This object defines command-handling behavior.

    Public methods:

        match_command(str)                      Match commands against str
        commands()                              Get list of commands
        all_commands()                          Get commands on ancestors too
        shortcuts()                             Get list of shortcuts
        all_shortcuts()                         Get shortcuts on ancestors too
        sub_shortcut_fields(subs, fields)       Substitute for shortcut fields

    Owner methods:

        add_command(template, method)           Add a command
        del_command(method)                     Remove a command
        add_shortcut(pattern, method, subs)     Add a shortcut
        del_shortcut(method)                    Remove a shortcut

parent root
object has_commands

var root name 'has_commands
var has_commands commands 0
var has_commands shortcuts 0

method init_has_commands
    if (caller() != $root)
        throw(~perm, "Caller is not $root.");
    commands = [];
    shortcuts = [];
.

method uninit_has_commands
    if (caller() != $root)
        throw(~perm, "Caller is not $root.");
    commands = 0;
    shortcuts = 0;
.

eval
    .initialize();
.

method add_command
    arg template, method;

    if (!.is_owned_by(sender()))
        throw(~perm, "Sender is not an owner.");
    if (type(template) != 'string || type(method) != 'symbol)
        throw(~type, "Template and method are not a string and symbol.");
    commands = [@commands, [template, method]];
.

method del_command
    arg method;
    var command;

    if (!.is_owned_by(sender()))
        throw(~perm, "Sender is not an owner.");
    for command in (commands) {
        if (command[2] == method) {
            commands = setremove(commands, command);
            return;
        }
    }
    throw(~commandnf, "No command with method " + tostr(method));
.

method add_shortcut
    arg pattern, method, subs;

    if (!.is_owned_by(sender()))
        throw(~perm, "Sender is not an owner.");
    if (type(pattern) != 'string || type(method) != 'symbol || type(subs) != 'list)
        throw(~type, "Pattern, method, and subs are not a string, symbol, and list.");
    shortcuts = [@shortcuts, [pattern, method, subs]];
.

method del_shortcut
    arg method;
    var shortcut;

    if (!.is_owned_by(sender()))
        throw(~perm, "Sender is not an owner.");
    for shortcut in (shortcuts) {
        if (shortcut[2] == method) {
            shortcuts = setremove(shortcuts, shortcut);
            return;
        }
    }
    throw(~shortcutnf, "No shortcut with method " + tostr(method));
.

method match_command
    arg str;
    var shortcut, cmd, fields;

    // Try shortcuts.
    for shortcut in (.all_shortcuts()) {
        fields = match_pattern(shortcut[1], str);
        if (fields)
            return [shortcut[2], .sub_shortcut_fields(shortcut[3], fields)];
    }

    // Try commands.
    for cmd in (.all_commands()) {
        fields = match_template(cmd[1], str);
        if (fields)
            return [cmd[2], fields];
    }

    // Give up.
    return 0;
.

method commands
    return commands;
.

method all_commands
    var p, cmdlist, pc;

    // Collect complete command list from ancestors.  Ancestors may not be
    // command-handling objects, in which case (| p.commands() |) is
    // ~methodnf.
    cmdlist = [];
    for p in (ancestors()) {
        pc = (| p.commands() |);
        if (pc)
            cmdlist = cmdlist + pc;
        if (p == definer())
            break;
    }
    return cmdlist;
.

method shortcuts
    return shortcuts;
.

method all_shortcuts
    var ancestor, list, ancestor_shortcuts;

    // Collect complete command list from ancestors.  Ancestors may not be
    // command-handling objects, in which case (| p.shortcuts() |) is
    // ~methodnf.
    list = [];
    for ancestor in (ancestors()) {
        ancestor_shortcuts = (| ancestor.shortcuts() |);
        if (ancestor_shortcuts)
            list = list + ancestor_shortcuts;
        if (ancestor == definer())
            break;
    }
    return list;
.

method sub_shortcut_fields
    arg subs, fields;
    var subbed_list, elem;

    subbed_list = [];
    for elem in (subs) {
        if (type(elem) == 'string)
            subbed_list = [@subbed_list, elem];
        else if (type(elem) == 'integer)
            subbed_list = [@subbed_list, (> fields[elem] <)];
        else
            throw(~type, "Substitution element is of wrong type.");
    }
    return subbed_list;
.