# engine.js

    import Doc from './doc.js';
    import Settings from './settings.js';
    import Symbols from './symbols.js';
    import Utils from './utils.js';

    String.prototype.splice = function(idx, s){ return (this.slice(0,idx) + s + this.slice(idx)); };
    String.prototype.splicen = function(idx, s, n){ return (this.slice(0,idx) + s + this.slice(idx+n));};
    String.prototype.search_at = function(idx, s){ return (this.substring(idx-s.length,idx) == s); };

    /**
     * @class
     * @classdesc The engine for scripting the editor.  To access the
     * engine for scripting a particular Guppy instance, say called
     * `"guppy1"`, do `Guppy("guppy1").engine`.
     *
     * At that point, you can, for example, move that editor's cursor
     * one spot to the left with `Guppy("guppy1").engine.left()`.
    */
    var Engine = function(config){
        config = config || {};
        var events = config['events'] || {};

        var settings = config['settings'] || {};
        this.parent = config['parent'];

        this.events = {};
        this.settings = {};

        var evts = ["change", "left_end", "right_end", "done", "completion", "debug", "error", "focus"];

        for(var i = 0; i < evts.length; i++){
            var e = evts[i];
            if(e in events) this.events[e] = e in events ? events[e] : null;
        }

        var opts = ["blank_caret", "empty_content", "blacklist", "autoreplace", "cliptype"];

        for(var j = 0; j < opts.length; j++){
            var p = opts[j];
            if(p in settings) this.settings[p] = settings[p];
        }

        this.symbols = {};
        this.doc = new Doc(settings["xml_content"]);

        this.current = this.doc.root().firstChild;
        this.caret = 0;
        this.space_caret = 0;
        this.sel_start = null;
        this.sel_end = null;
        this.undo_data = [];
        this.undo_now = -1;
        this.sel_status = Engine.SEL_NONE;
        this.checkpoint();
        this.symbols = JSON.parse(JSON.stringify(Symbols.symbols));
    }

    Engine.SEL_NONE = 0;
    Engine.SEL_CURSOR_AT_START = 1;
    Engine.SEL_CURSOR_AT_END = 2;
    Engine.clipboard = null;

    Engine.prototype.setting = function(name){
        return name in this.settings ? this.settings[name] : Settings.config.settings[name];
    }

    Engine.prototype.event = function(name){
        return name in this.events ? this.events[name] : Settings.config.events[name];
    }

    /**
        Get the content of the editor
        @memberof Engine
        @param {string} t - The type of content to render ("latex", "text", or "xml").
    */
    Engine.prototype.get_content = function(t,r){
        return this.doc.get_content(t,r);
    }

    /**
        Set the XML content of the editor
        @memberof Engine
        @param {string} xml_data - An XML string of the content to place in the editor
    */
    Engine.prototype.set_content = function(xml_data){
        this.set_doc(new Doc(xml_data));
    }

    /**
        Set the document of the editor
        @memberof Engine
        @param {Doc} doc - The Doc that will be the editor's source
    */
    Engine.prototype.set_doc = function(doc){
        this.doc = doc;
        this.current = this.doc.root().firstChild;
        this.caret = 0;
        this.sel_start = null;
        this.sel_end = null;
        this.undo_data = [];
        this.undo_now = -1;
        this.sel_status = Engine.SEL_NONE;
        this.checkpoint();
    }

    Engine.prototype.import_text = function(text){
        this.doc.import_text(text, this.symbols);
        this.set_doc(this.doc);
    }

    Engine.prototype.import_latex = function(text){
        this.doc.import_latex(text, this.symbols);
        this.set_doc(this.doc);
    }

    Engine.prototype.import_ast = function(ast){
        this.doc.import_ast(ast, this.symbols);
        this.set_doc(this.doc);
    }

    Engine.prototype.fire_event = function(event, args){
        args = args || {};
        args.target = this.parent || this;
        args.type = event;
        var ev = this.event(event);
        if(ev) ev(args);
    }

    /**
        Remove a symbol from this instance of the editor.
        @memberof Engine
        @param {string} name - The name of the symbol to remove.
    */
    Engine.prototype.remove_symbol = function(name){
        if(this.symbols[name]) delete this.symbols[name];
    }

    /**
        Add a symbol to this instance of the editor.
        @memberof Engine
        @param {string} name - param
        @param {Object} symbol - If `template` is present, this is the
        template arguments.  Otherwise, it is a complete specification
        of the symbol, the format for which can be found in the
        documentation for Guppy.add_global_symbol.
        @param {string} [template] - The name of the template to use.
    */
    Engine.prototype.add_symbol = function(name, symbol){
        this.symbols[name] = symbol;
    }

    Engine.prototype.select_to = function(loc, sel_cursor, sel_caret, mouse){
        if(loc.current == sel_cursor && loc.caret == sel_caret){
            this.current = loc.current;
            this.caret = loc.caret;
            this.sel_status = Engine.SEL_NONE;
        }
        else if(loc.pos == "left"){
            this.sel_end = {"node":sel_cursor,"caret":sel_caret};
            this.current = loc.current;
            this.caret = loc.caret;
            this.set_sel_boundary(Engine.SEL_CURSOR_AT_START, mouse);
        }
        else if(loc.pos == "right"){
            this.sel_start = {"node":sel_cursor,"caret":sel_caret};
            this.current = loc.current;
            this.caret = loc.caret;
            this.set_sel_boundary(Engine.SEL_CURSOR_AT_END, mouse);
        }
    }

    Engine.prototype.set_sel_start = function(){
        this.sel_start = {"node":this.current, "caret":this.caret};
    }

    Engine.prototype.set_sel_end = function(){
        this.sel_end = {"node":this.current, "caret":this.caret};
    }

    Engine.prototype.add_paths = function(n,path){
        if(n.nodeName == "e"){
            n.setAttribute("path",path);
        }
        else{
            var es = 1, fs = 1, cs = 1, ls = 1;
            for(var c = n.firstChild; c != null; c = c.nextSibling){
                if(c.nodeName == "c"){ this.add_paths(c, path+"_c"+cs); cs++; }
                else if(c.nodeName == "f"){ this.add_paths(c, path+"_f"+fs); fs++; }
                else if(c.nodeName == "l"){ this.add_paths(c, path+"_l"+ls); ls++; }
                else if(c.nodeName == "e"){ this.add_paths(c, path+"_e"+es); es++; }
            }
        }
    }

    Engine.prototype.add_classes_cursors = function(n){
        if(n.nodeName == "e"){
            var text = n.firstChild.nodeValue;
            var ans = "";
            var sel_cursor;
            var text_node = Utils.is_text(n);
            if(this.sel_status == Engine.SEL_CURSOR_AT_START) sel_cursor = this.sel_end;
            if(this.sel_status == Engine.SEL_CURSOR_AT_END) sel_cursor = this.sel_start;
            if(this.sel_status != Engine.SEL_NONE){
                var sel_caret_text = Utils.is_small(sel_cursor.node) ? Utils.SMALL_SEL_CARET : Utils.SEL_CARET;
                if(!text_node && text.length == 0 && n.parentNode.childElementCount > 1){
                    sel_caret_text = "\\blue{\\xmlClass{guppy_elt guppy_blank guppy_loc_"+n.getAttribute("path")+"_0}{"+sel_caret_text+"}}";
                }
                else{
                    sel_caret_text = "\\blue{"+sel_caret_text+"}";
                }
                if(this.sel_status == Engine.SEL_CURSOR_AT_END) sel_caret_text = text_node ? "[" : sel_caret_text + "\\"+Utils.SEL_COLOR+"{";
                if(this.sel_status == Engine.SEL_CURSOR_AT_START) sel_caret_text = text_node ? "]" : "}" + sel_caret_text;
            }
            var caret_text = "";
            var temp_caret_text = "";
            if(text.length == 0){
                if(text_node) caret_text = "\\_";
                else if(n.parentNode.childElementCount == 1){
                    if(this.current == n){
                        var blank_caret = this.setting("blank_caret") || (Utils.is_small(this.current) ? Utils.SMALL_CARET : Utils.CARET);
                        ans = "\\red{\\xmlClass{main_cursor guppy_elt guppy_blank guppy_loc_"+n.getAttribute("path")+"_0"+"}{"+blank_caret+"}}";
                    }
                    else if(this.temp_cursor.node == n)
                        ans = "\\gray{\\xmlClass{guppy_elt guppy_blank guppy_loc_"+n.getAttribute("path")+"_0"+"}{[?]}}";
                    else
                        ans = "\\blue{\\xmlClass{guppy_elt guppy_blank guppy_loc_"+n.getAttribute("path")+"_0"+"}{[?]}}";
                }
                else if(this.temp_cursor.node != n && this.current != n && (!(sel_cursor) || sel_cursor.node != n)){
                    // These are the empty e elements at either end of
                    // a c or m node, such as the space before and
                    // after both the sin and x^2 in sin(x^2)
                    //
                    // Here, we add in a small element so that we can
                    // use the mouse to select these areas
                    ans = "\\phantom{\\xmlClass{guppy_elt guppy_blank guppy_loc_"+n.getAttribute("path")+"_0"+"}{\\hspace{0pt}}}";
                }
            }
            for(var i = 0; i < text.length+1; i++){
                if(n == this.current && i == this.caret && (text.length > 0 || n.parentNode.childElementCount > 1)){
                    if(text_node){
                        if(this.sel_status == Engine.SEL_CURSOR_AT_START)
                            caret_text = "[";
                        else if(this.sel_status == Engine.SEL_CURSOR_AT_END)
                            caret_text = "]";
                        else
                            caret_text = "\\_";
                    }
                    else{
                        caret_text = Utils.is_small(this.current) ? Utils.SMALL_CARET : Utils.CARET;
                        if(text.length == 0)
                            caret_text = "\\red{\\xmlClass{main_cursor guppy_elt guppy_blank guppy_loc_"+n.getAttribute("path")+"_0}{"+caret_text+"}}";
                        else{
                            caret_text = "\\red{\\xmlClass{main_cursor}{"+caret_text+"}}"
                        }
                        if(this.sel_status == Engine.SEL_CURSOR_AT_START)
                            caret_text = caret_text + "\\"+Utils.SEL_COLOR+"{";
                        else if(this.sel_status == Engine.SEL_CURSOR_AT_END)
                            caret_text = "}" + caret_text;
                    }
                    ans += caret_text;
                }
                else if(n == this.current && i == this.caret && text_node){
                    ans += caret_text;
                }
                else if(this.sel_status != Engine.SEL_NONE && sel_cursor.node == n && i == sel_cursor.caret){
                    ans += sel_caret_text;
                }
                else if(this.temp_cursor.node == n && i == this.temp_cursor.caret && (text.length > 0 || n.parentNode.childElementCount > 1)){
                    if(text_node)
                        temp_caret_text = ".";
                    else{
                        temp_caret_text = Utils.is_small(this.current) ? Utils.TEMP_SMALL_CARET : Utils.TEMP_CARET;
                        if(text.length == 0){
                            temp_caret_text = "\\gray{\\xmlClass{guppy_elt guppy_blank guppy_loc_"+n.getAttribute("path")+"_0}{"+temp_caret_text+"}}";
                        }
                        else
                            temp_caret_text = "\\gray{"+temp_caret_text+"}";
                    }
                    ans += temp_caret_text;
                }
                if(i < text.length) ans += "\\xmlClass{guppy_elt guppy_loc_"+n.getAttribute("path")+"_"+i+"}{"+text[i]+"}";
            }
            if(text_node && n == this.current){
                ans = "\\xmlClass{guppy_text_current}{{"+ans+"}}";
            }
            n.setAttribute("render", ans);
            n.removeAttribute("path");
        }
        else{
            for(var c = n.firstChild; c != null; c = c.nextSibling){
                if(c.nodeName == "c" || c.nodeName == "l" || c.nodeName == "f" || c.nodeName == "e"){ this.add_classes_cursors(c); }
            }
        }
    }

    Engine.prototype.remove_cursors_classes = function(n){
        if(n.nodeName == "e"){
            n.removeAttribute("path");
            n.removeAttribute("render");
            n.removeAttribute("current");
            n.removeAttribute("temp");
        }
        else{
            for(var c = n.firstChild; c != null; c = c.nextSibling){
                if(c.nodeType == 1){ this.remove_cursors_classes(c); }
            }
        }
    }

    Engine.prototype.down_from_f = function(){
        var nn = this.current.firstChild;
        while(nn != null && nn.nodeName != 'c' && nn.nodeName != 'l') nn = nn.nextSibling;
        if(nn != null){
            while(nn.nodeName == 'l') nn = nn.firstChild;
            this.current = nn.firstChild;
        }
    }

    Engine.prototype.down_from_f_to_blank = function(){
        var nn = this.current.firstChild;
        while(nn != null && !(nn.nodeName == 'c' && nn.children.length == 1 && nn.firstChild.firstChild.nodeValue == "")){
            nn = nn.nextSibling;
        }
        if(nn != null){
            //Sanity check:

            while(nn.nodeName == 'l') nn = nn.firstChild;
            if(nn.nodeName != 'c' || nn.firstChild.nodeName != 'e'){
                this.problem('dfftb');
                return;
            }
            this.current = nn.firstChild;
        }
        else this.down_from_f();
    }

    Engine.prototype.delete_from_f = function(to_insert){
        var n = this.current;
        var p = n.parentNode;
        var prev = n.previousSibling;
        var next = n.nextSibling;
        var middle = to_insert || "";
        var new_node = this.make_e(prev.firstChild.textContent + middle + next.firstChild.textContent);
        this.current = new_node;
        this.caret = prev.firstChild.textContent.length;
        p.insertBefore(new_node, prev);
        p.removeChild(prev);
        p.removeChild(n);
        p.removeChild(next);
    }

    Engine.prototype.symbol_to_node = function(sym_name, content){
        return Symbols.symbol_to_node(this.symbols[sym_name], content, this.doc.base);
    }

    /**
        Insert a symbol into the document at the current cursor position.
        @memberof Engine
        @param {string} sym_name - The name of the symbol to insert.
        Should match one of the keys in the symbols JSON object
    */
    Engine.prototype.insert_symbol = function(sym_name){
        var s = this.symbols[sym_name];
        if(s.attrs && this.is_blacklisted(s.attrs.type)){
            return false;
        }
        var content = {};
        var left_piece,right_piece;
        var cur = "input" in s ? s.input : 0;
        var to_remove = [];
        var to_replace = null;
        var replace_f = false;
        var sel;

        if(cur > 0){
            cur--;
            if(this.sel_status != Engine.SEL_NONE){
                sel = this.sel_get();
                to_remove = sel.involved;
                left_piece = this.make_e(sel.remnant.firstChild.nodeValue.slice(0,this.sel_start.caret));
                right_piece = this.make_e(sel.remnant.firstChild.nodeValue.slice(this.sel_start.caret));
                content[cur] = sel.node_list;
            }
            else if("input" in s){
                // If we're at the beginning, then the token is the previous f node
                if(this.caret == 0 && this.current.previousSibling != null){
                    content[cur] = [this.make_e(""), this.current.previousSibling, this.make_e("")];
                    to_replace = this.current.previousSibling;
                    replace_f = true;
                }
                else{
                    // look for [0-9.]+|[a-zA-Z] immediately preceeding the caret and use that as token
                    var prev = this.current.firstChild.nodeValue.substring(0,this.caret);
                    var token = prev.match(/[0-9.]+$|[a-zA-Z]$/);
                    if(token != null && token.length > 0){
                        token = token[0];
                        left_piece = this.make_e(this.current.firstChild.nodeValue.slice(0,this.caret-token.length));
                        right_piece = this.make_e(this.current.firstChild.nodeValue.slice(this.caret));
                        content[cur] = [this.make_e(token)];
                    }
                }
            }
        }
        if(!replace_f && (left_piece == null || right_piece == null)){
            if(this.sel_status != Engine.SEL_NONE){
                sel = this.sel_get();
                to_remove = sel.involved;
                left_piece = this.make_e(sel.remnant.firstChild.nodeValue.slice(0,this.sel_start.caret));
                right_piece = this.make_e(sel.remnant.firstChild.nodeValue.slice(this.sel_start.caret));
                content = [sel.node_list];
    	}
    	else{
                left_piece = this.make_e(this.current.firstChild.nodeValue.slice(0,this.caret));
                right_piece = this.make_e(this.current.firstChild.nodeValue.slice(this.caret));
                to_remove = [this.current];
    	}
        }

        // By now:
        //
        // content contains whatever we want to pre-populate the 'current' field with (if any)
        //
        // right_piece contains whatever content was in an involved node
        // to the right of the cursor but is not part of the insertion.
        // Analogously for left_piece
        //
        // Thus all we should have to do now is symbol_to_node(sym_type,
        // content) and then add the left_piece, resulting node, and
        // right_piece in that order.
        var sym = this.symbol_to_node(sym_name,content);
        var current_parent = this.current.parentNode;

        var f = sym.f;

        var next = this.current.nextSibling;

        if(replace_f){
            current_parent.replaceChild(f,to_replace);
        }
        else{
            if(to_remove.length == 0) this.current.parentNode.removeChild(this.current);

            for(var i = 0; i < to_remove.length; i++){
                if(next == to_remove[i]) next = next.nextSibling;
                current_parent.removeChild(to_remove[i]);
            }
            current_parent.insertBefore(left_piece, next);
            current_parent.insertBefore(f, next);
            current_parent.insertBefore(right_piece, next);
        }

        this.caret = 0;
        this.current = f;
        if(sym.args.length == 0 || ("input" in s && s.input >= sym.args.length)){
            this.current = this.current.nextSibling;
        }
        else{
            this.down_from_f_to_blank();
            this.caret = this.current.firstChild.textContent.length;
        }

        this.sel_clear();
        this.checkpoint();
        return true;
    }

    Engine.prototype.sel_get = function(){
        if(this.sel_status == Engine.SEL_NONE){
            return null;
        }
        var involved = [];
        var node_list = [];
        var remnant = null;

        if(this.sel_start.node == this.sel_end.node){
            return {"node_list":[this.make_e(this.sel_start.node.firstChild.nodeValue.substring(this.sel_start.caret, this.sel_end.caret))],
                    "remnant":this.make_e(this.sel_start.node.firstChild.nodeValue.substring(0, this.sel_start.caret) + this.sel_end.node.firstChild.nodeValue.substring(this.sel_end.caret)),
                    "involved":[this.sel_start.node]};
        }

        node_list.push(this.make_e(this.sel_start.node.firstChild.nodeValue.substring(this.sel_start.caret)));
        involved.push(this.sel_start.node);
        involved.push(this.sel_end.node);
        remnant = this.make_e(this.sel_start.node.firstChild.nodeValue.substring(0, this.sel_start.caret) + this.sel_end.node.firstChild.nodeValue.substring(this.sel_end.caret));
        var n = this.sel_start.node.nextSibling;
        while(n != null && n != this.sel_end.node){
            involved.push(n);
            node_list.push(n);
            n = n.nextSibling;
        }
        node_list.push(this.make_e(this.sel_end.node.firstChild.nodeValue.substring(0, this.sel_end.caret)));
        return {"node_list":node_list,
                "remnant":remnant,
                "involved":involved,
                "cursor":0};
    }

    Engine.prototype.make_e = function(text){
        var base = this.doc.base;
        var new_node = base.createElement("e");
        new_node.appendChild(base.createTextNode(text));
        return new_node;
    }

    /**
        Insert a string into the document at the current cursor position.
        @memberof Engine
        @param {string} s - The string to insert.
    */
    Engine.prototype.insert_string = function(s){
        var self = this;
        if(this.sel_status != Engine.SEL_NONE){
            this.sel_delete();
            this.sel_clear();
        }
        this.current.firstChild.nodeValue = this.current.firstChild.nodeValue.splice(this.caret,s)
        this.caret += s.length;
        this.checkpoint();
        if(this.setting("autoreplace") == "auto") this.check_for_symbol(false);
        if(this.setting("autoreplace") == "whole") this.check_for_symbol(true);
        if(this.setting("autoreplace") == "delay" && setTimeout){
            if(this.delayed_check) clearTimeout(this.delayed_check);
            this.delayed_check = setTimeout(function(){ self.check_for_symbol(false); }, 200);
        }
    }

    /**
        Insert a copy of the given document into the editor at the current cursor position.
        @memberof Engine
        @param {Doc} doc - The document to insert.
    */
    Engine.prototype.insert_doc = function(doc){
        this.insert_nodes(doc.root().childNodes, true);
    }

    /**
        Copy the current selection, leaving the document unchanged but
        placing the contents of the current selection on the clipboard.
        @memberof Engine
    */
    Engine.prototype.sel_copy = function(){
        var sel = this.sel_get();
        if(!sel) return;
        Engine.clipboard = [];
        var cliptype = this.setting("cliptype");
        if(cliptype != "none") var clip_doc = new Doc("<m></m>");
        for(var i = 0; i < sel.node_list.length; i++){
            var node = sel.node_list[i].cloneNode(true);
            Engine.clipboard.push(node);
            if(cliptype != "none") clip_doc.root().appendChild(node.cloneNode(true));//clip_text += this.doc.manual_render(cliptype, node);
        }
        if(cliptype != "none"){
            try{
                this.system_copy(clip_doc.get_content(cliptype));
            }
            catch(e){
                this.system_copy("Syntax error");
            }
        }
        this.sel_clear();
    }

    Engine.prototype.system_copy = function(text) {
        if (window.clipboardData && window.clipboardData.setData)
            return window.clipboardData.setData("Text", text);
        else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
            var textarea = document.createElement("textarea");
            textarea.textContent = text;
            textarea.style.position = "fixed";
            textarea.style.background = "transparent";
            document.body.appendChild(textarea);
            textarea.select();
            try { return document.execCommand("copy"); }
            catch (ex) { return false; }
            finally { document.body.removeChild(textarea); }
        }
    }

    /**
        Cut the current selection, removing it from the document and placing it in the clipboard.
        @memberof Engine
    */
    Engine.prototype.sel_cut = function(){
        var node_list = this.sel_delete();
        if(!node_list) return;
        Engine.clipboard = [];
        var cliptype = this.setting("cliptype");
        var clip_text = "";
        for(var i = 0; i < node_list.length; i++){
            var node = node_list[i].cloneNode(true);
            Engine.clipboard.push(node);
            if(cliptype != "none") clip_text += this.doc.manual_render(cliptype, node);
        }
        if(cliptype != "none") this.system_copy(clip_text);
        this.sel_clear();
        this.checkpoint();
    }

    Engine.prototype.insert_nodes = function(node_list, move_cursor){
        var real_clipboard = [];
        for(var i = 0; i < node_list.length; i++){
            real_clipboard.push(node_list[i].cloneNode(true));
        }

        if(real_clipboard.length == 1){
            this.current.firstChild.nodeValue = this.current.firstChild.nodeValue.substring(0,this.caret) + real_clipboard[0].firstChild.nodeValue + this.current.firstChild.nodeValue.substring(this.caret);
            if(move_cursor) this.caret += real_clipboard[0].firstChild.nodeValue.length;
        }
        else{
            var nn = this.make_e(real_clipboard[real_clipboard.length-1].firstChild.nodeValue + this.current.firstChild.nodeValue.substring(this.caret));
            this.current.firstChild.nodeValue = this.current.firstChild.nodeValue.substring(0,this.caret) + real_clipboard[0].firstChild.nodeValue;
            if(this.current.nextSibling == null)
                this.current.parentNode.appendChild(nn)
            else
                this.current.parentNode.insertBefore(nn, this.current.nextSibling)
            for(var j = 1; j < real_clipboard.length - 1; j++)
                this.current.parentNode.insertBefore(real_clipboard[j], nn);
            if(move_cursor){
                this.current = nn;
                this.caret = real_clipboard[real_clipboard.length-1].firstChild.nodeValue.length
            }
        }
    }

    /**
        Paste the current contents of the clipboard.
        @memberof Engine
    */
    Engine.prototype.sel_paste = function(){
        this.sel_delete();
        this.sel_clear();
        if(!(Engine.clipboard) || Engine.clipboard.length == 0) return;
        this.insert_nodes(Engine.clipboard, true);
        this.checkpoint();
        return;
    }

    /**
        Clear the current selection, leaving the document unchanged and
        nothing selected.
        @memberof Engine
    */
    Engine.prototype.sel_clear = function(){
        this.sel_start = null;
        this.sel_end = null;
        this.sel_status = Engine.SEL_NONE;
    }

    /**
        Delete the current selection.
        @memberof Engine
    */
    Engine.prototype.sel_delete = function(){
        var sel = this.sel_get();
        if(!sel) return null;
        var sel_parent = sel.involved[0].parentNode;
        var sel_prev = sel.involved[0].previousSibling;
        for(var i = 0; i < sel.involved.length; i++){
            var n = sel.involved[i];
            sel_parent.removeChild(n);
        }
        if(sel_prev == null){
            if(sel_parent.firstChild == null)
                sel_parent.appendChild(sel.remnant);
            else
                sel_parent.insertBefore(sel.remnant, sel_parent.firstChild);
        }
        else if(sel_prev.nodeName == 'f'){
            if(sel_prev.nextSibling == null)
                sel_parent.appendChild(sel.remnant);
            else
                sel_parent.insertBefore(sel.remnant, sel_prev.nextSibling);
        }
        this.current = sel.remnant
        this.caret = this.sel_start.caret;
        return sel.node_list;
    }

    /**
        Select the entire contents of the editor.
        @memberof Engine
    */
    Engine.prototype.sel_all = function(){
        this.home();
        this.set_sel_start();
        this.end();
        this.set_sel_end();
        if(this.sel_start.node != this.sel_end.node || this.sel_start.caret != this.sel_end.caret)
            this.sel_status = Engine.SEL_CURSOR_AT_END;
    }

    /**
        function
        @memberof Engine
        @param {string} name - param
    */
    Engine.prototype.sel_right = function(){
        if(this.sel_status == Engine.SEL_NONE){
            this.set_sel_start();
            this.sel_status = Engine.SEL_CURSOR_AT_END;
        }
        if(this.caret >= Utils.get_length(this.current)){
            var nn = this.current.nextSibling;
            if(nn != null){
                this.current = nn.nextSibling;
                this.caret = 0;
                this.set_sel_boundary(Engine.SEL_CURSOR_AT_END);
            }
            else{
                this.set_sel_boundary(Engine.SEL_CURSOR_AT_END);
            }
        }
        else{
            this.caret += 1;
            this.set_sel_boundary(Engine.SEL_CURSOR_AT_END);
        }
        if(this.sel_start.node == this.sel_end.node && this.sel_start.caret == this.sel_end.caret){
            this.sel_status = Engine.SEL_NONE;
        }
    }

    Engine.prototype.set_sel_boundary = function(sstatus, mouse){
        if(this.sel_status == Engine.SEL_NONE || mouse) this.sel_status = sstatus;
        if(this.sel_status == Engine.SEL_CURSOR_AT_START)
            this.set_sel_start();
        else if(this.sel_status == Engine.SEL_CURSOR_AT_END)
            this.set_sel_end();
    }

    /**
        Move the cursor to the left, adjusting the selection along with
        the cursor.
        @memberof Engine
    */
    Engine.prototype.sel_left = function(){
        if(this.sel_status == Engine.SEL_NONE){
            this.set_sel_end();
            this.sel_status = Engine.SEL_CURSOR_AT_START;
        }
        if(this.caret <= 0){
            var nn = this.current.previousSibling;
            if(nn != null){
                this.current = nn.previousSibling;
                this.caret = this.current.firstChild.nodeValue.length;
                this.set_sel_boundary(Engine.SEL_CURSOR_AT_START);
            }
            else{
                this.set_sel_boundary(Engine.SEL_CURSOR_AT_START);
            }
        }
        else{
            this.caret -= 1;
            this.set_sel_boundary(Engine.SEL_CURSOR_AT_START);
        }
        if(this.sel_start.node == this.sel_end.node && this.sel_start.caret == this.sel_end.caret){
            this.sel_status = Engine.SEL_NONE;
        }
    }

    Engine.prototype.list_extend_copy_right = function(){this.list_extend("right", true);}
    Engine.prototype.list_extend_copy_left = function(){this.list_extend("left", true);}
    Engine.prototype.list_extend_right = function(){this.list_extend("right", false);}
    Engine.prototype.list_extend_left = function(){this.list_extend("left", false);}
    Engine.prototype.list_extend_up = function(){this.list_extend("up", false);}
    Engine.prototype.list_extend_down = function(){this.list_extend("down", false);}
    Engine.prototype.list_extend_copy_up = function(){this.list_extend("up", true);}
    Engine.prototype.list_extend_copy_down = function(){this.list_extend("down", true);}

    /**
        Move the cursor by one row up or down in a matrix.
        @memberof Engine
        @param {boolean} down - If `true`, move down in the matrix;
        otherwise, up.
    */
    Engine.prototype.list_vertical_move = function(down){
        var n = this.current;
        while(n.parentNode && n.parentNode.parentNode && !(n.nodeName == 'c' && n.parentNode.nodeName == 'l' && n.parentNode.parentNode.nodeName == 'l')){
            n = n.parentNode;
        }
        if(!n.parentNode) return;
        var pos = 1;
        var cc = n;
        while(cc.previousSibling != null){
            pos++;
            cc = cc.previousSibling;
        }
        var new_l = down ? n.parentNode.nextSibling : n.parentNode.previousSibling
        if(!new_l) return;
        var idx = 1;
        var nn = new_l.firstChild;
        while(idx < pos){
            idx++;
            nn = nn.nextSibling;
        }
        this.current = nn.firstChild;
        this.caret = down ? 0 : this.current.firstChild.textContent.length;
    }

    /**
        Add an element to a list (or row/column to a matrix) in the
        specified direction.  Can optionally copy the current
        element/row/column to the new one.
        @memberof Engine
        @param {string} direction - One of `"up"`, `"down"`, `"left"`, or
        `"right"`.
        @param {boolean} copy - Whether or not to copy the current
        element/row/column into the new one.
    */
    Engine.prototype.list_extend = function(direction, copy){
        var base = this.doc.base;
        var vertical = direction == "up" || direction == "down";
        var before = direction == "up" || direction == "left";
        var this_name = vertical ? "l" : "c";
        var n = this.current;
        while(n.parentNode && !(n.nodeName == this_name && n.parentNode.nodeName == 'l')){
            n = n.parentNode;
        }
        if(!n.parentNode) return;
        var to_insert;

        // check if 2D and horizontal and extend all the other rows if so
        if(!vertical && n.parentNode.parentNode.nodeName == "l"){
            to_insert = base.createElement("c");
            to_insert.appendChild(this.make_e(""));
            var pos = 1;
            var cc = n;
            while(cc.previousSibling != null){
                pos++;
                cc = cc.previousSibling;
            }
            var to_modify = [];
            var iterator = this.doc.xpath_list("./l/c[position()="+pos+"]", n.parentNode.parentNode);
    	var nn = null;
            try{ for(nn = iterator.iterateNext(); nn != null; nn = iterator.iterateNext()){ to_modify.push(nn); }}
            catch(e) { this.fire_event("error",{"message":'XML modified during iteration? ' + e}); }
            for(var j = 0; j < to_modify.length; j++){
                nn = to_modify[j];
                if(copy) nn.parentNode.insertBefore(nn.cloneNode(true), before ? nn : nn.nextSibling);
                else nn.parentNode.insertBefore(to_insert.cloneNode(true), before ? nn : nn.nextSibling);
                nn.parentNode.setAttribute("s",parseInt(nn.parentNode.getAttribute("s"))+1);
            }
            this.sel_clear();
            this.current = before ? n.previousSibling.lastChild : n.nextSibling.firstChild;
            this.caret = this.current.firstChild.textContent.length;
            this.checkpoint();
            return;
        }

        if(copy){
            to_insert = n.cloneNode(true);
        }
        else{
            if(vertical){
                to_insert = base.createElement("l");
                to_insert.setAttribute("s",n.getAttribute("s"))
                for(var i = 0; i < parseInt(n.getAttribute("s")); i++){
                    var c = base.createElement("c");
                    c.appendChild(this.make_e(""));
                    to_insert.appendChild(c);
                }
            }
            else{
                to_insert = base.createElement("c");
                to_insert.appendChild(this.make_e(""));
            }
        }
        n.parentNode.setAttribute("s",parseInt(n.parentNode.getAttribute("s"))+1);
        n.parentNode.insertBefore(to_insert, before ? n : n.nextSibling);
        this.sel_clear();
        if(vertical) this.current = to_insert.firstChild.firstChild;
        else this.current = to_insert.firstChild;
        this.caret = 0;
        this.checkpoint();
    }

    /**
        Remove the current column from a matrix
        @memberof Engine
    */
    Engine.prototype.list_remove_col = function(){
        var n = this.current;
        while(n.parentNode && n.parentNode.parentNode && !(n.nodeName == 'c' && n.parentNode.nodeName == 'l' && n.parentNode.parentNode.nodeName == 'l')){
            n = n.parentNode;
        }
        if(!n.parentNode) return;

        // Don't remove if there is only a single column:
        if(n.previousSibling != null){
            this.current = n.previousSibling.lastChild;
            this.caret = n.previousSibling.lastChild.firstChild.textContent.length;
        }
        else if(n.nextSibling != null){
            this.current = n.nextSibling.firstChild;
            this.caret = 0;
        }
        else return;

        var pos = 1;
        var cc = n;

        // Find position of column
        while(cc.previousSibling != null){
            pos++;
            cc = cc.previousSibling;
        }
        var to_modify = [];
        var iterator = this.doc.xpath_list("./l/c[position()="+pos+"]", n.parentNode.parentNode)
        var nn = null;
        try{ for(nn = iterator.iterateNext(); nn != null; nn = iterator.iterateNext()){ to_modify.push(nn); }}
        catch(e) { this.fire_event("error",{"message":'XML modified during iteration? ' + e}); }
        for(var j = 0; j < to_modify.length; j++){
            nn = to_modify[j];
            nn.parentNode.setAttribute("s",parseInt(nn.parentNode.getAttribute("s"))-1);
            nn.parentNode.removeChild(nn);
        }
        this.checkpoint();
    }

    /**
        Remove the current row from a matrix
        @memberof Engine
    */
    Engine.prototype.list_remove_row = function(){
        var n = this.current;
        while(n.parentNode && !(n.nodeName == 'l' && n.parentNode.nodeName == 'l')){
            n = n.parentNode;
        }
        if(!n.parentNode) return;
        // Don't remove if there is only a single row:
        if(n.previousSibling != null){
            this.current = n.previousSibling.firstChild.lastChild;
            this.caret = n.previousSibling.lastChild.firstChild.textContent.length;
        }
        else if(n.nextSibling != null){
            this.current = n.nextSibling.firstChild.firstChild;
            this.caret = 0;
        }
        else return;

        n.parentNode.setAttribute("s",parseInt(n.parentNode.getAttribute("s"))-1);
        n.parentNode.removeChild(n);
        this.checkpoint();
    }

    /**
        Remove the current element from a list (or column from a matrix)
        @memberof Engine
    */
    Engine.prototype.list_remove = function(){
        var n = this.current;
        while(n.parentNode && !(n.nodeName == 'c' && n.parentNode.nodeName == 'l')){
            n = n.parentNode;
        }
        if(!n.parentNode) return;
        if(n.parentNode.parentNode && n.parentNode.parentNode.nodeName == "l"){
            this.list_remove_col();
            return;
        }
        if(n.previousSibling != null){
            this.current = n.previousSibling.lastChild;
            this.caret = n.previousSibling.lastChild.firstChild.textContent.length;
        }
        else if(n.nextSibling != null){
            this.current = n.nextSibling.firstChild;
            this.caret = 0;
        }
        else return;
        n.parentNode.setAttribute("s",parseInt(n.parentNode.getAttribute("s"))-1);
        n.parentNode.removeChild(n);
        this.checkpoint();
    }

    /**
        Simulate the right arrow key press
        @memberof Engine
    */
    Engine.prototype.right = function(){
        this.sel_clear();
        if(this.caret >= Utils.get_length(this.current)){
            var nn = this.doc.xpath_node("following::e[1]", this.current);
            if(nn != null){
                this.current = nn;
                this.caret = 0;
            }
            else{
                this.fire_event("right_end");
            }
        }
        else{
            this.caret += 1;
        }
    }

    /**
        Simulate the spacebar key press
        @memberof Engine
    */
    Engine.prototype.spacebar = function(){
        if(Utils.is_text(this.current)) this.insert_string(" ");
        else this.space_caret = this.caret;
    }

    /**
        Simulate the left arrow key press
        @memberof Engine
    */
    Engine.prototype.left = function(){
        this.sel_clear();
        if(this.caret <= 0){
            var pn = this.doc.xpath_node("preceding::e[1]", this.current);
            if(pn != null){
                this.current = pn;
                this.caret = this.current.firstChild.nodeValue.length;
            }
            else{
                this.fire_event("left_end");
            }
        }
        else{
            this.caret -= 1;
        }
    }

    Engine.prototype.delete_from_c = function(){
        var pos = 0;
        var c = this.current.parentNode;
        while(c && c.nodeName == "c"){
            pos++;
            c = c.previousSibling;
        }
        var idx = this.current.parentNode.getAttribute("delete");
        var survivor_node = this.doc.xpath_node("./c[position()="+idx+"]", this.current.parentNode.parentNode);
        var survivor_nodes = [];
        for(var n = survivor_node.firstChild; n != null; n = n.nextSibling){
            survivor_nodes.push(n);
        }
        this.current = this.current.parentNode.parentNode;
        this.delete_from_f();
        this.insert_nodes(survivor_nodes, pos > idx);
    }

    Engine.prototype.delete_from_e = function(){
        // return false if we deleted something, and true otherwise.
        if(this.caret > 0){
            this.current.firstChild.nodeValue = this.current.firstChild.nodeValue.splicen(this.caret-1,"",1);
            this.caret--;
        }
        else{
            // The order of these is important
            if(this.current.previousSibling != null && Utils.is_char(this.current.previousSibling)){
                // The previous node is an f node but is really just a character.  Delete it.
                this.current = this.current.previousSibling;
                this.delete_from_f();
            }
            else if(this.current.previousSibling != null && this.current.previousSibling.nodeName == 'f'){
                // We're in an e node just after an f node.  Move back into the f node (delete it?)
                this.left();
                return false;
            }
            else if(this.current.parentNode.previousSibling != null && this.current.parentNode.previousSibling.nodeName == 'c'){
                // We're in a c child of an f node, but not the first one.  Go to the previous c
                if(this.current.parentNode.hasAttribute("delete")){
                    this.delete_from_c();
                }
                else{
                    this.left();
                    return false;
                }
            }
            else if(this.current.previousSibling == null && this.current.parentNode.nodeName == 'c' && (this.current.parentNode.previousSibling == null || this.current.parentNode.previousSibling.nodeName != 'c')){
                // We're in the first c child of an f node and at the beginning--delete the f node
                var par = this.current.parentNode;
                while(par.parentNode.nodeName == 'l' || par.parentNode.nodeName == 'c'){
                    par = par.parentNode;
                }
                if(par.hasAttribute("delete")){
                    this.delete_from_c();
                }
                else{
                    this.current = par.parentNode;
                    this.delete_from_f();
                }
            }
            else{
                // We're at the beginning (hopefully!)
                return false;
            }
        }
        return true;
    }

    Engine.prototype.delete_forward_from_e = function(){
        // return false if we deleted something, and true otherwise.
        if(this.caret < this.current.firstChild.nodeValue.length){
            this.current.firstChild.nodeValue = this.current.firstChild.nodeValue.splicen(this.caret,"",1);
        }
        else{
            //We're at the end
            if(this.current.nextSibling != null){
                // The next node is an f node.  Delete it.
                this.current = this.current.nextSibling;
                this.delete_from_f();
            }
            else if(this.current.parentNode.nodeName == 'c'){
                // We're in a c child of an f node.  Do nothing
                return false;
            }
        }
        return true;
    }

    /**
        Simulate the "backspace" key press
        @memberof Engine
    */
    Engine.prototype.backspace = function(){
        if(this.sel_status != Engine.SEL_NONE){
            this.sel_delete();
            this.sel_status = Engine.SEL_NONE;
            this.checkpoint();
        }
        else if(this.delete_from_e()){
            this.checkpoint();
        }
    }

    /**
        Simulate the "delete" key press
        @memberof Engine
    */
    Engine.prototype.delete_key = function(){
        if(this.sel_status != Engine.SEL_NONE){
            this.sel_delete();
            this.sel_status = Engine.SEL_NONE;
            this.checkpoint();
        }
        else if(this.delete_forward_from_e()){
            this.checkpoint();
        }
    }

    Engine.prototype.backslash = function(){
        if(Utils.is_text(this.current)) return;
        this.insert_symbol("sym_name");
    }

    /**
        Simulate a tab key press
        @memberof Engine
    */
    Engine.prototype.tab = function(){
        if(!Utils.is_symbol(this.current)){
            if(this.check_for_symbol()) return;
        }
        var sym_name = this.current.firstChild.textContent;
        var candidates = [];
        for(var n in this.symbols){
            if(n.startsWith(sym_name)) candidates.push(n);
        }
        if(candidates.length == 1){
            this.current.firstChild.textContent = candidates[0];
            this.caret = candidates[0].length;
    	this.check_for_symbol();
        }
        else {
            this.fire_event("completion",{"candidates":candidates});
        }
    }

    Engine.prototype.right_paren = function(){
        if(this.current.nodeName == 'e' && this.caret < this.current.firstChild.nodeValue.length - 1) return;
        else this.right();
    }

    /**
        Simulate an up arrow key press
        @memberof Engine
    */
    Engine.prototype.up = function(){
        this.sel_clear();
        if(this.current.parentNode.hasAttribute("up")){
            var t = parseInt(this.current.parentNode.getAttribute("up"));
            var f = this.current.parentNode.parentNode;
            var n = f.firstChild;
            while(n != null && t > 0){
                if(n.nodeName == 'c') t--;
                if(t > 0) n = n.nextSibling;
            }
            this.current = n.lastChild;
            this.caret = this.current.firstChild.nodeValue.length;
        }
        else this.list_vertical_move(false);
    }

    /**
        Simulate a down arrow key press
        @memberof Engine
    */
    Engine.prototype.down = function(){
        this.sel_clear();
        if(this.current.parentNode.hasAttribute("down")){
            var t = parseInt(this.current.parentNode.getAttribute("down"));
            var f = this.current.parentNode.parentNode;
            var n = f.firstChild;
            while(n != null && t > 0){
                if(n.nodeName == 'c') t--;
                if(t > 0) n = n.nextSibling;
            }
            this.current = n.lastChild;
            this.caret = this.current.firstChild.nodeValue.length;
        }
        else this.list_vertical_move(true);
    }

    /**
        Move the cursor to the beginning of the document
        @memberof Engine
    */
    Engine.prototype.home = function(){
        this.current = this.doc.root().firstChild;
        this.caret = 0;
    }

    /**
        Move the cursor to the end of the document
        @memberof Engine
    */
    Engine.prototype.end = function(){
        this.current = this.doc.root().lastChild;
        this.caret = this.current.firstChild.nodeValue.length;
    }

    Engine.prototype.checkpoint = function(){
        var base = this.doc.base;
        this.current.setAttribute("current","yes");
        this.current.setAttribute("caret",this.caret.toString());
        this.undo_now++;
        this.undo_data[this.undo_now] = base.cloneNode(true);
        this.undo_data.splice(this.undo_now+1, this.undo_data.length);
        var old_data = this.undo_data[this.undo_now-1] ? (new XMLSerializer()).serializeToString(this.undo_data[this.undo_now-1]) : "[none]";
        var new_data = (new XMLSerializer()).serializeToString(this.undo_data[this.undo_now]);
        this.fire_event("change",{"old":old_data,"new":new_data});
        this.current.removeAttribute("current");
        this.current.removeAttribute("caret");
    }

    Engine.prototype.restore = function(t){
        this.doc.base = this.undo_data[t].cloneNode(true);
        this.find_current();
        this.current.removeAttribute("current");
        this.current.removeAttribute("caret");
    }

    Engine.prototype.find_current = function(){
        this.current = this.doc.xpath_node("//*[@current='yes']");
        this.caret = parseInt(this.current.getAttribute("caret"));
    }

    /**
        Undo the last action
        @memberof Engine
    */
    Engine.prototype.undo = function(){
        this.sel_clear();
        if(this.undo_now <= 0) return;
        this.undo_now--;
        this.restore(this.undo_now);
        var old_data = this.undo_data[this.undo_now+1] ? (new XMLSerializer()).serializeToString(this.undo_data[this.undo_now+1]) : "[none]";
        var new_data = (new XMLSerializer()).serializeToString(this.undo_data[this.undo_now]);
        this.fire_event("change",{"old":old_data,"new":new_data});
    }

    /**
        Redo the last undone action
        @memberof Engine
    */
    Engine.prototype.redo = function(){
        this.sel_clear();
        if(this.undo_now >= this.undo_data.length-1) return;
        this.undo_now++;
        this.restore(this.undo_now);
        var old_data = this.undo_data[this.undo_now-1] ? (new XMLSerializer()).serializeToString(this.undo_data[this.undo_now-1]) : "[none]";
        var new_data = (new XMLSerializer()).serializeToString(this.undo_data[this.undo_now]);
        this.fire_event("change",{"old":old_data,"new":new_data});
    }

    /**
        Execute the "done" callback
        @memberof Engine
    */
    Engine.prototype.done = function(){
        if(Utils.is_symbol(this.current)) this.complete_symbol();
        else this.fire_event("done");
    }

    Engine.prototype.complete_symbol = function(){
        var sym_name = this.current.firstChild.textContent;
        if(!(this.symbols[sym_name])) return;
        this.current = this.current.parentNode.parentNode;
        this.delete_from_f();
        this.insert_symbol(sym_name);
    }

    Engine.prototype.problem = function(message){
        this.fire_event("error",{"message":message});
    }

    Engine.prototype.is_blacklisted = function(symb_type){
        var blacklist = this.setting("blacklist");
        for(var i = 0; i < blacklist.length; i++)
            if(symb_type == blacklist[i]) return true;
        return false;
    }

    Engine.prototype.check_for_symbol = function(whole_node){
        var instance = this;
        if(Utils.is_text(this.current)) return false;
        var sym = "";
        var n = null;
        if(whole_node){
            n = instance.current.firstChild.nodeValue.substring(instance.space_caret, instance.caret);
            var m = /[a-zA-Z_]+$/.exec(n);
            if(m){
                var s = m[0];
                if(this.symbols[s]) sym = s;
            }
        }
        else{
            n = instance.current.firstChild.nodeValue.substring(instance.space_caret, instance.caret);
            while(n.length > 0){
                if(n in this.symbols){
                    sym = n;
                    break;
                }
                n = n.substring(1);
            }
        }

        if(sym == "") return false;

        var temp = instance.current.firstChild.nodeValue;
        var temp_caret = instance.caret;
        instance.current.firstChild.nodeValue = instance.current.firstChild.nodeValue.slice(0,instance.caret-sym.length)+instance.current.firstChild.nodeValue.slice(instance.caret);
        instance.caret -= sym.length;
        var success = instance.insert_symbol(sym);
        if(!success){
            instance.current.firstChild.nodeValue = temp;
            instance.caret = temp_caret;
        }
        return success;
    }

    export default Engine;