doc.js

import AST from './ast.js';
import Parsers from './parser.js';
import Symbols from './symbols.js';
import Utils from './utils.js';
import Version from './version.js';
import katex from '../lib/katex/katex-modified.min.js';

/**
   @class
   @classdesc A class representing a Guppy document.  To access this
   class, use `Guppy.Doc`.  To get the document for a particular guppy
   instance, say called `"guppy1"`, do `Guppy("guppy1").doc()`.
   @param {string} [doc=<m><e></e></m>] - An XML string representing the document
   @constructor
 */
var Doc = function(doc, type){
    type = type || "xml";
    if(type == "xml") this.set_content(doc || "<m><e></e></m>");
    else if(type == "latex") this.import_latex(doc);
    else if(type == "text") this.import_text(doc);
    else if(type == "ast") this.import_ast(doc);
    if(this.root().hasAttribute("v") && this.root().getAttribute("v") != Version.DOC_VERSION)
    throw Version.DOC_ERROR;
    else
    this.root().setAttribute("v",Version.DOC_VERSION);
}

Doc.prototype.is_small = function(nn){
    var n = nn.parentNode;
    while(n != null && n.nodeName != 'm'){
        if(n.getAttribute("small") == "yes") return true;
        n = n.parentNode
        while(n != null && n.nodeName != 'c') n = n.parentNode;
    }
    return false;
}

Doc.prototype.ensure_text_nodes = function(){
    var l = this.base.getElementsByTagName("e");
    for(var i = 0; i < l.length; i++){
        if(!(l[i].firstChild)) l[i].appendChild(this.base.createTextNode(""));
    }
}

Doc.prototype.is_blank = function(){
    if(this.base.getElementsByTagName("f").length > 0) return false;
    var l = this.base.getElementsByTagName("e");
    if(l.length == 1 && (!(l[0].firstChild) || l[0].firstChild.textContent == "")) return true;
    return false;
}


/**
    Get the document as a DOM object
    @memberof Doc
    @returns {Element}
*/
Doc.prototype.root = function(){
    return this.base.documentElement;
}

/**
    Get the content of the document as a string
    @memberof Doc
    @param {string} t - The rendering method to use ("latex", "text", "ast" (for syntax tree), or "xml" (for internal XML representation))
    @returns {string}
*/
Doc.prototype.get_content = function(t,r){
    if(t == "xml") return (new XMLSerializer()).serializeToString(this.base);
    else if(t == "ast") return JSON.stringify(this.syntax_tree());
    else if(t == "text") return AST.to_text(this.syntax_tree());
    else if(t == "function") return AST.to_function(this.syntax_tree(), r);
    else if(t == "eqns") return JSON.stringify(AST.to_eqlist(this.syntax_tree()));
    else return this.manual_render(t,this.root(),r);
}

/**
    Evaluate the document using user-supplied functions to interpret symbols
    @memberof Doc
    @param {Object} evaluators - A dictionary where each key is a node
    type in the AST ("var", "val", "sin", "cos", etc.) and the
    corresponding value is a function that takes a list of argument
    (the results of evaluating that AST node's arguments) as well as,
    optionally, a second argument for the parent AST node to the one
    currently being evaluated.
    @returns {Object}
*/
Doc.prototype.evaluate = function(evaluators){
    return AST.eval(this.syntax_tree(), evaluators);
}

Doc.prototype.import_text = function(text, syms, s2n){
    var ast = Parsers.TextParser.tokenise_and_parse(text);
    this.import_ast(ast, syms, s2n);
}

Doc.prototype.import_latex = function(text, syms, s2n){
    var ast = Parsers.LaTeXParser.tokenise_and_parse(text);
    this.import_ast(ast, syms, s2n);
}

Doc.prototype.import_ast = function(ast, syms, s2n){
    syms = syms || Symbols.symbols;
    s2n = s2n || Symbols.symbol_to_node;
    var doc = AST.to_xml(ast, syms, s2n);
    this.base = doc;
    this.ensure_text_nodes();
}

Doc.prototype.syntax_tree = function(n){
    n = n || this.root()
    if(n.nodeName == "f"){
        var ans = {"args":[], "kwargs":{}};
        ans['value'] = n.getAttribute("type");
        ans['type'] = "function";
        if(n.hasAttribute("ast_value")) ans['value'] = n.getAttribute("ast_value");
        if(n.hasAttribute("ast_type")) ans['type'] = n.getAttribute("ast_type");
        else if(Utils.is_char(n)) ans['type'] = "name";

        var iterator = this.xpath_list("./*[name()='c' or name()='l']", n)
        for(var nn = iterator.iterateNext(); nn != null; nn = iterator.iterateNext()){
            //if(nn.hasAttribute("name")) ans.kwargs[nn.getAttribute("name")] = this.syntax_tree(nn)
            //else ans.args.push(this.syntax_tree(nn))
            ans.args.push(this.syntax_tree(nn))
        }
    }
    else if(n.nodeName == "l"){
        ans = [];
        for(nn = n.firstChild; nn != null; nn = nn.nextSibling){
        ans.push(this.syntax_tree(nn));
        }
        ans = ["list",ans];
    }
    else if(n.nodeName == "c" || n.nodeName == "m"){
        if(n.hasAttribute("mode") && n.getAttribute("mode") == "text"){
            ans = n.firstChild.firstChild.textContent;
        }
        else{
            var tokens = []
            for(nn = n.firstChild; nn != null; nn = nn.nextSibling){
                if(nn.nodeName == "e"){
                    tokens = tokens.concat(Parsers.EParser.tokenise(nn.firstChild.textContent));
                }
                else if(nn.nodeName == "f"){
                    tokens.push(this.syntax_tree(nn));
                }
            }
            ans = Parsers.EParser.parse(tokens);
        }
    }
    return ans;
}

Doc.prototype.xpath_node = function(xpath, node){
    node = node || this.root()
    return this.base.evaluate(xpath, node, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}

Doc.prototype.xpath_list = function(xpath, node){
    node = node || this.root()
    return this.base.evaluate(xpath, node, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
}

/**
    Get the names of symbols used in this document
    @memberof Doc
    @param {string[]} [groups] - A list of groups you want strings for
    @returns {string[]}
*/
Doc.prototype.get_symbols = function(groups){
    var types = {};
    var ans = [];
    var groups_selector = "//f";
    if(groups) groups_selector += "[" + groups.map(function(){ return ""; }).join(" or ") + "]";
    var iterator = this.xpath_list(groups_selector)
    for(var nn = iterator.iterateNext(); nn != null; nn = iterator.iterateNext())
        types[nn.getAttribute("type")] = true;
    for(var t in types)
        ans.push(t);
    return ans;
}

/**
    Set the content of the document
    @memberof Doc
    @param {string} xml_data - An XML string representing the content of the document
*/
Doc.prototype.set_content = function(xml_data){
    this.base = (new window.DOMParser()).parseFromString(xml_data, "text/xml");
    this.ensure_text_nodes();
}

Doc.prototype.auto_bracket = function(n){
    var e0 = n.firstChild;
    var e1 = n.lastChild;
    if(n.childElementCount == 3 && e0.firstChild.textContent == "" && e1.firstChild.textContent == ""){ // single f child, all e children empty
        var f = e0.nextSibling;
    var cs = 0;
    var c = null;
    // Count immediate children of f that are c nodes in cs and store the last one in c
    for(var nn = f.firstChild; nn; nn = nn.nextSibling) if(nn.tagName == "c"){ c = nn; cs++; }
        if(cs == 1 && c.getAttribute("is_bracket") == "yes") return false; // if the f child is a bracket, don't bracket
        if(Utils.is_char(f) && e0.getAttribute("current") != "yes" && e0.getAttribute("temp") != "yes" && e1.getAttribute("current") != "yes" && e1.getAttribute("temp") != "yes") return false; // if the f child is a character and not current or temp cursor location, don't bracket
    }
    else if(n.childElementCount == 1){ // Single e child
        var s = e0.firstChild.textContent;
        if(s.length != 1 && Number(s)+"" != s) return true; // If content is neither a single character nor a number, bracket it
        if(e0.getAttribute("current") == "yes" || e0.getAttribute("temp") == "yes") return true; // If content has the cursor or temp cursor, bracket it
        return false;
    }
    return true;
}

Doc.prototype.manual_render = function(t,n,r){
    var ans = "";
    var nn = null;
    var i = null;
    var spacer = t == "latex" ? " " : "";
    if(n.nodeName == "e"){
        if(t == "latex" && r){
            ans = n.getAttribute("render");
        }
        else{
            ans = n.firstChild.textContent;
        }
    }
    else if(n.nodeName == "f"){
        var real_type = (t == "latex" && this.is_small(n)) ? "small_latex" : t;
        nn = this.xpath_node("./b[@p='"+real_type+"']", n) || this.xpath_node("./b[@p='"+t+"']", n);
        if(nn) ans = this.manual_render(t,nn,r);
    }
    else if(n.nodeName == "b"){
        var cs = []
        i = 1;
        var par = n.parentNode;
        for(nn = par.firstChild; nn != null; nn = nn.nextSibling)
            if(nn.nodeName == "c" || nn.nodeName == "l") cs[i++] = this.manual_render(t,nn,r);
        for(nn = n.firstChild; nn != null; nn = nn.nextSibling){
            if(nn.nodeType == 3) ans += nn.textContent + spacer;
            else if(nn.nodeType == 1){
                if(nn.hasAttribute("d")){
                    var dim = parseInt(nn.getAttribute("d"));
                    var joiner = function(d,l){
                        if(d > 1) for(var k = 0; k < l.length; k++) l[k] = joiner(d-1,l[k]);
                        return l.join(nn.getAttribute('sep'+(d-1)));
                    }
                ans += joiner(dim,cs[parseInt(nn.getAttribute("ref"))]) + spacer;
                }
                else ans += cs[parseInt(nn.getAttribute("ref"))] + spacer;
            }
        }
    }
    else if(n.nodeName == "l"){
        ans = [];
        i = 0;
        for(nn = n.firstChild; nn != null; nn = nn.nextSibling){
            ans[i++] = this.manual_render(t,nn,r);
        }
    }
    else if(n.nodeName == "c" || n.nodeName == "m"){
        for(nn = n.firstChild; nn != null; nn = nn.nextSibling)
            ans += this.manual_render(t,nn,r) + spacer;
        if(t == "latex" && n.getAttribute("bracket") == "yes" && this.auto_bracket(n)) {
            ans = "\\left("+ans+"\\right)";
        }
    }
    return ans;
}

Doc.render_all = function(t, delim, root_node){
    var l,i,n,d,s,ans = [];
    if(!t || t == "xml"){
        l = document.getElementsByTagName("script");
        for(i = 0; i < l.length; i++){
            if(l[i].getAttribute("type") == "text/guppy_xml"){
                n = l[i];
                d = new Doc(n.innerHTML);
                s = document.createElement("span");
        var len = ans.length;
        var new_id = "guppy-"+t+"-render-"+len;
        while(document.getElementById(new_id)) new_id = "guppy-xml-render-"+(++len);
                s.setAttribute("id",new_id);
                s.setAttribute("class","guppy-render");
                katex.render(d.get_content("latex"), s);
                n.parentNode.insertBefore(s, n);
                n.parentNode.removeChild(n);
                ans.push({"container":s, "doc":d})
            }
        }
    }
    else {
        var subs = function(node) {
            if(!node) return;
            var excludeElements = ['script', 'style', 'iframe', 'canvas', 'pre', 'code'];
            do {
                switch (node.nodeType) {
                case 1:
                    // Don't process KaTeX elements, Guppy instances, Javascript, or CSS
                    if (excludeElements.indexOf(node.tagName.toLowerCase()) > -1 || (" "+node.getAttribute("class")+" ").indexOf(" katex ") > -1 || (""+node.getAttribute("class")).indexOf("guppy") > -1) {
                        continue;
                    }
                    subs(node.firstChild);
                    break;
                case 3:
                    var text_node = node;
                    var offset = text_node.textContent.indexOf(delim);
                    while(offset > -1){
                        var next = text_node.textContent.substring(offset+delim.length).indexOf(delim);
                        if(next == -1) break;
                        var before = text_node.textContent.substring(0,offset);
                        var content = text_node.textContent.substring(offset+delim.length,offset+delim.length+next);
                        var after = text_node.textContent.substring(offset+delim.length+next+delim.length);

                        // Make the span to render the doc in
                        var s = document.createElement("span");
            var l = ans.length;
            var new_id = "guppy-"+t+"-render-"+l;
            while(document.getElementById(new_id)) new_id = "guppy-"+t+"-render-"+(++l);
                        s.setAttribute("id",new_id);
            s.setAttribute("class","guppy-render");

            try {
                            // Create the document
                            d = new Doc(content,t);

                            // Render the doc
                            katex.render(d.get_content("latex"), s);
            }
            catch (e) {
                s.innerHTML = "ERROR: "+e.message;
            }
                        var new_node = document.createTextNode(after)
                        text_node.parentNode.insertBefore(document.createTextNode(before), text_node);
                        text_node.parentNode.insertBefore(s, text_node);
                        text_node.parentNode.insertBefore(new_node, text_node);
                        text_node.parentNode.removeChild(text_node);
                        text_node = new_node;
            node = new_node;
                        ans.push({"id":new_id, "doc":d});

                        offset = text_node.textContent.indexOf(delim);
                    }
                    break;
        default:
                    break;
                }
            } while ((node = node.nextSibling));

        }
        delim = delim || "$$";
        subs(root_node || document.documentElement);
    }
    return ans;
}

/**
    Render a given document into a specified HTML element.
    @param {string} doc - A GuppyXML string to be rendered
    @param {string} target_id - The ID of the HTML element to render into
    @memberof Doc
*/
Doc.render = function(doc, target_id){
    var d = new Doc(doc);
    var target = document.getElementById(target_id);
    katex.render(d.get_content("latex"), target);
    return {"container":target, "doc":d};
}


export default Doc;