File:  [LON-CAPA] / loncom / html / adm / LC_math_editor / src / parser.js
Revision 1.2: download - view: text, annotated - select for diffs
Tue Feb 24 15:20:44 2015 UTC (9 years, 6 months ago) by damieng
Branches: MAIN
CVS tags: version_2_12_X, HEAD
updated to git version (see git for log history), and changed param separator to ,

/*

Copyright (C) 2014  Michigan State University Board of Trustees

The JavaScript code in this page is free software: you can
redistribute it and/or modify it under the terms of the GNU
General Public License (GNU GPL) as published by the Free Software
Foundation, either version 3 of the License, or (at your option)
any later version.  The code is distributed WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.

As additional permission under GNU GPL version 3 section 7, you
may distribute non-source (e.g., minimized or compacted) forms of
that code without the copy of the GNU GPL normally required by
section 4, provided you include this license notice and a URL
through which recipients can access the Corresponding Source.

*/

"use strict";

/**
 * Equation parser
 * @constructor
 * @param {boolean} [implicit_operators] - assume hidden multiplication and unit operators in some cases (unlike maxima)
 * @param {boolean} [unit_mode] - handle only numerical expressions with units (no variable)
 * @param {Array.<string>} [constants] - array of constant names for unit mode
 */
function Parser(implicit_operators, unit_mode, constants) {
    if (typeof implicit_operators == "undefined")
        this.implicit_operators = false;
    else
        this.implicit_operators = implicit_operators;
    if (typeof unit_mode == "undefined")
        this.unit_mode = false;
    else
        this.unit_mode = unit_mode;
    if (typeof constants == "undefined" || constants == null)
        this.constants = [];
    else
        this.constants = constants;
    this.defs = new Definitions();
    this.defs.define();
    this.operators = this.defs.operators;
    this.oph = {}; // operator hash table
    for (var i=0; i<this.operators.length; i++)
        this.oph[this.operators[i].id] = this.operators[i];
}

/**
 * Returns the right node at the current token, based on top-down operator precedence.
 * @param {number} rbp - Right binding power
 * @returns {ENode}
 */
Parser.prototype.expression = function(rbp) {
    var left; // ENode
    var t = this.current_token;
    if (t == null)
        throw new ParseException("Expected something at the end",
            this.tokens[this.tokens.length - 1].to + 1);
    this.advance();
    if (t.op == null)
        left = new ENode(t.type, null, t.value, null);
    else if (t.op.nud == null)
        throw new ParseException("Unexpected operator '" + t.op.id + "'", t.from);
    else
        left = t.op.nud(this);
    while (this.current_token != null && this.current_token.op != null &&
            rbp < this.current_token.op.lbp) {
        t = this.current_token;
        this.advance();
        left = t.op.led(this, left);
    }
    return left;
};

/**
 * Advance to the next token,
 * expecting the given operator id if it is provided.
 * Throws a ParseException if a given operator id is not found.
 * @param {string} [id] - Operator id
 */
Parser.prototype.advance = function(id) {
    if (id && (this.current_token == null || this.current_token.op == null ||
            this.current_token.op.id !== id)) {
        if (this.current_token == null)
            throw new ParseException("Expected '" + id + "' at the end",
                this.tokens[this.tokens.length - 1].to + 1);
        else
            throw new ParseException("Expected '" + id + "'", this.current_token.from);
    }
    if (this.token_nr >= this.tokens.length) {
        this.current_token = null;
        return;
    }
    this.current_token = this.tokens[this.token_nr];
    this.token_nr += 1;
};

/**
 * Adds hidden multiplication and unit operators to the token stream
 */
Parser.prototype.addHiddenOperators = function() {
    var multiplication = this.defs.findOperator("*");
    var unit_operator = this.defs.findOperator("`");
    var in_units = false; // we check if we are already in the units to avoid adding two ` operators inside
    var in_exp = false;
    for (var i=0; i<this.tokens.length - 1; i++) {
        var token = this.tokens[i];
        var next_token = this.tokens[i + 1];
        if (this.unit_mode) {
            if (token.value == "`")
                in_units = true;
            else if (in_units) {
                if (token.value == "^")
                    in_exp = true;
                else if (in_exp && token.type == Token.NUMBER)
                    in_exp = false;
                else if (!in_exp && token.type == Token.NUMBER)
                    in_units = false;
                else if (!in_exp && token.type == Token.OPERATOR && "*/^()".indexOf(token.value) == -1)
                    in_units = false;
                else if (token.type == Token.NAME && next_token.value == "(")
                    in_units = false;
            }
        }
        if (
                (token.type == Token.NAME && next_token.type == Token.NAME) ||
                (token.type == Token.NUMBER && next_token.type == Token.NAME) ||
                (token.type == Token.NUMBER && next_token.type == Token.NUMBER) ||
                (token.type == Token.NUMBER && (next_token.value == "(" || next_token.value == "[" || next_token.value == "{")) ||
                /*(token.type == Token.NAME && next_token.value == "(") ||*/
                /* name ( could be a function call */
                ((token.value == ")" || token.value == "]" || token.value == "}") && next_token.type == Token.NAME) ||
                ((token.value == ")" || token.value == "]" || token.value == "}") && next_token.type == Token.NUMBER) ||
                ((token.value == ")" || token.value == "]" || token.value == "}") && next_token.value == "(")
           ) {
            // support for things like "(1/2) (m/s)" is complex...
            var units = (this.unit_mode && !in_units && (token.type == Token.NUMBER ||
                (token.value == ")" || token.value == "]" || token.value == "}")) &&
                (next_token.type == Token.NAME ||
                    ((next_token.value == "(" || next_token.value == "[" || next_token.value == "{") && this.tokens.length > i + 2 &&
                    this.tokens[i + 2].type == Token.NAME)));
            if (units) {
                var test_token, index_test;
                if (next_token.type == Token.NAME) {
                    test_token = next_token;
                    index_test = i + 1;
                } else {
                    // for instance for "2 (m/s)"
                    index_test = i + 2;
                    test_token = this.tokens[index_test];
                }
                for (var j=0; j<this.constants.length; j++) {
                    if (test_token.value == this.constants[j]) {
                        units = false;
                        break;
                    }
                }
                if (this.tokens.length > index_test + 1 && this.tokens[index_test + 1].value == "(") {
                    var known_functions = ["pow", "sqrt", "abs", "exp", "factorial", "diff",
                        "integrate", "sum", "product", "limit", "binomial", "matrix",
                        "ln", "log", "log10", "mod", "signum", "ceiling", "floor",
                        "sin", "cos", "tan", "asin", "acos", "atan", "atan2",
                        "sinh", "cosh", "tanh", "asinh", "acosh", "atanh"];
                    for (var j=0; j<known_functions.length; j++) {
                        if (test_token.value == known_functions[j]) {
                            units = false;
                            break;
                        }
                    }
                }
            }
            var new_token;
            if (units) {
                new_token = new Token(Token.OPERATOR, next_token.from,
                    next_token.from, unit_operator.id, unit_operator);
                in_units = true;
            } else {
                new_token = new Token(Token.OPERATOR, next_token.from,
                    next_token.from, multiplication.id, multiplication);
            }
            this.tokens.splice(i+1, 0, new_token);
        }
    }
}

/**
 * Parse the string, returning an ENode tree.
 * @param {string} text - The text to parse.
 * @returns {ENode}
 */
Parser.prototype.parse = function(text) {
    var tokenizer = new Tokenizer(this.defs, text);
    this.tokens = tokenizer.tokenize();
    if (this.tokens.length == 0) {
        return null;
    }
    if (this.implicit_operators) {
        this.addHiddenOperators();
    }
    this.token_nr = 0;
    this.current_token = this.tokens[this.token_nr];
    this.advance();
    var root = this.expression(0);
    if (this.current_token != null) {
        throw new ParseException("Expected the end", this.current_token.from);
    }
    return root;
};

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>