parent root
object help_node

var root name 'help_node

var help_node title 0
var help_node brief 0
var help_node text 0
var help_node footnotes 0
var help_node references 0
var help_node upnode 0
var help_node subnodes 0
var help_node menu 0

method init_help_node
    if (caller() != $root)
        throw(~perm, "Caller is not root.");
    title = "untitled";
    brief = "";
    text = [];
    footnotes = [];
    references = #[];
    upnode = $help_root;
    subnodes = [];
    menu = 0;
    $help_root.add_subnode();
.

method uninit_help_node
    var i;

    if (caller() != $root)
        throw(~perm, "Caller is not root.");
    for i in (subnodes)
        (| i.set_upnode(.upnode()) |);
    (| upnode.del_subnode() |);
.

eval
    .initialize();
    .set_fertile(1);
    .set_title("Generic");
.

method title
    return title;
.

method set_title
    arg new_title;

    if (!.is_owned_by(sender()))
        throw(~perm, "Sender is not an owner.");
    if (type(new_title) != 'string)
        throw(~perm, "New title is not a string.");
    title = uppercase(new_title);
    upnode.invalidate_menu();
.

method invalidate_menu
    menu = 0;
.

method upnode
    return upnode;
.

method subnodes
    return subnodes;
.

method set_upnode
    arg node;

    if (!node.is_owned_by(sender()))
        throw(~perm, "Sender isn't an owner.");
    (> node.add_subnode() <);
    (| upnode.del_subnode() |);
    upnode = node;
.

method add_subnode
    if (caller() != definer())
        throw(~perm, "Invalid access to private method.");
    subnodes = setadd(subnodes, sender());
    .invalidate_menu();
.

method del_subnode
    if (caller() != definer())
        throw(~perm, "Invalid access to private method.");
    subnodes = setremove(subnodes, sender());
    .invalidate_menu();
.

method path
    var cur, list;

    list = [];
    cur = this();
    while (cur != $help_root) {
        cur = cur.upnode();
        list = [cur, @list];
    }
    return list;
.

method brief
    return brief;
.

method set_brief
    arg string;

    if (!.is_owned_by(sender()))
        throw( ~perm, "Sender isn't an owner.");
    brief = string;
.

method text
    var ret, sub;

    ret = .path();
    // The returned text is the path from root to node, the node's name and brief description, and its text.
    ret = ["Help path: " + (ret ? $list.to_string( $list.map(ret, 'title), " ") | "<none>"), .title() + " [" + .brief() + "]", "----------------", @text];
    if (!.subnodes())
        return ret;
    // If there are subnodes, list them one per line with their brief descriptions
    ret = [@ret, ""];
    for sub in (.subnodes())
        ret = [@ret, "** " + pad(sub.name() + ":", 13) + sub.brief()];
    return ret;
.

method footnote
    arg n;

    return (> footnotes[n] <);
.

method match_menu
    arg name;
    var sub, l;

    if (menu == 0) {
        // menu is invalid.  Build it from scratch
        menu = references;
        for sub in (.subnodes())
            menu = dict_add(menu, sub.name(), sub);
    }
    catch ~keynf {
        return menu[name];
    }
    l = strlen(name);
    for sub in (menu)
        if (name == (|substr(sub[1], 1, l)|))
            return sub[2];
    throw(~keynf, "There is no subnode called " + toliteral(name) + ".");
.

method parse_line
    arg line, fns, refs;
    var sub, i, j, name, obj;

    // Shift parsed prefixes of `line' into `sub'.
    sub = "";
    while (1) {
        // Look for a reference begin-marker: `(:' or `[:'
        i = "(:" in line;
        j = "[:" in line;
        if (!i && !j)
            break;
        if (!j || (i && i < j)) {
            // `(:' came first.
            sub = sub + substr(line, 1, i + 1);
            line = substr(line, i + 2);

            // Match the trigger name.
            i = ":" in line;
            if (!i)
                throw(~parse, "Expected `:' in " + toliteral(line) + ".");
            name = substr(line, 1, i-1);
            line = substr(line,i+1);

            // Match the referenced node.
            i = ":)" in line;
            if (!i)
                throw(~parse, "Expected `:)' in " + toliteral(line) + ".");
            obj = substr(line, 1, i - 1);
            catch any {
                obj = sender().match_help_path(obj);
            } with handler {
                if (obj[1] != "$")
                    rethrow(error());
                obj = todbref(substr(obj,2));
            }
            line = substr(line, i + 2);
            sub = sub + name + ":)";

            // write down the new reference
            refs = dict_add(refs, name, obj);
        } else {
            // '[:' came first.
            sub = sub + substr(line, 1, j + 1);
            line = substr(line, j + 2);
            i = ":]" in line;
            if (!i)
                throw(~parse, "Expected `:]' in " + toliteral(line) + ".");
            fns = [@fns, substr(line, 1, i - 1)];
            sub = sub + tostr(listlen(fns)) + ":]";
            line = substr(line, i+1);
        }
    }
    return [sub + line, fns, refs];
.

method set_text
    arg input;
    var text, tmp, line;

    if (!.is_owned_by(sender()))
        throw(~perm, "Sender isn't an owner.");
    text = [];
    tmp = ["", [], #[]];
    for line in (input) {
        tmp = replace(tmp, 1, line);
        tmp = (> .parse_line(@tmp) <);

        // Add the newly parsed line.
        text = [@text, tmp[1]];
    }

    // If all went well, store the new info and invalidate the menu.
    text = text;
    footnotes = tmp[2];
    references = tmp[3];
    .invalidate_menu();
.

method create_subnode
    arg name, b, t;
    var sub;

    if (!.is_owned_by(sender()))
        throw(~perm, "Sender isn't an owner.");
    sub = definer().spawn( name);
    sub.add_owner($help_root);
    sub.add_owner(sender());
    catch any {
        sub.set_brief(b);
        sub.set_text(t);
        sub.set_upnode(this());
    } with handler {
        sub.destroy();
        rethrow(error());
    }
    return sub;
.